fix: close immutable promotion trust gaps
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { createPublicKey } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
evaluatePromotionEvidence,
|
||||
@@ -10,38 +11,63 @@ import {
|
||||
releaseCandidateManifestSchema,
|
||||
verifyReleaseCandidate,
|
||||
} from "./release-candidate.ts";
|
||||
import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts";
|
||||
|
||||
type LocalEvidenceVerifier = typeof verifyArchivedLocalEvidence;
|
||||
|
||||
export type VerifyPromotionInputsOptions = Readonly<{
|
||||
environment?: NodeJS.ProcessEnv;
|
||||
repositoryRoot?: string;
|
||||
verifyLocalEvidence?: LocalEvidenceVerifier;
|
||||
}>;
|
||||
|
||||
export async function verifyPromotionInputs(
|
||||
environment: NodeJS.ProcessEnv = process.env,
|
||||
options: VerifyPromotionInputsOptions = {},
|
||||
) {
|
||||
const manifestDocument = await requiredJson(RELEASE_CANDIDATE_MANIFEST_PATH);
|
||||
const manifest = releaseCandidateManifestSchema.parse(manifestDocument);
|
||||
const candidate = await verifyReleaseCandidate(manifestDocument);
|
||||
const localVerification = await requiredJson(
|
||||
"artifacts/security/supply-chain-verification.json",
|
||||
const environment = options.environment ?? process.env;
|
||||
const repositoryRoot = path.resolve(options.repositoryRoot ?? process.cwd());
|
||||
const manifestDocument = await requiredJson(
|
||||
repositoryRoot,
|
||||
RELEASE_CANDIDATE_MANIFEST_PATH,
|
||||
);
|
||||
const manifest = releaseCandidateManifestSchema.parse(manifestDocument);
|
||||
const candidate = await verifyReleaseCandidate(
|
||||
manifestDocument,
|
||||
repositoryRoot,
|
||||
);
|
||||
const localEvidence = await (
|
||||
options.verifyLocalEvidence ?? verifyArchivedLocalEvidence
|
||||
)({ repositoryRoot, candidate: manifest });
|
||||
const vulnerabilityReport = await optionalJson(
|
||||
repositoryRoot,
|
||||
environment.VULNERABILITY_REPORT_PATH,
|
||||
);
|
||||
const provenanceAttestation = await optionalJson(
|
||||
repositoryRoot,
|
||||
environment.PROVENANCE_ATTESTATION_PATH,
|
||||
);
|
||||
const result = evaluatePromotionEvidence({
|
||||
candidate: manifest,
|
||||
currentDistSha256: candidate.currentDistSha256 ?? "",
|
||||
localStatus: localVerification.localStatus,
|
||||
localStatus: localEvidence.status,
|
||||
vulnerabilityReport,
|
||||
provenanceAttestation,
|
||||
vulnerabilityTrust: await readTrust(
|
||||
repositoryRoot,
|
||||
environment.VULNERABILITY_PUBLIC_KEY_PATH,
|
||||
environment.VULNERABILITY_KEY_ID,
|
||||
),
|
||||
provenanceTrust: await readTrust(
|
||||
repositoryRoot,
|
||||
environment.PROVENANCE_PUBLIC_KEY_PATH,
|
||||
environment.PROVENANCE_KEY_ID,
|
||||
),
|
||||
});
|
||||
const failures = [...candidate.failures, ...result.failures];
|
||||
const failures = [
|
||||
...candidate.failures,
|
||||
...localEvidence.failures,
|
||||
...result.failures,
|
||||
];
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
status:
|
||||
@@ -57,6 +83,7 @@ export async function verifyPromotionInputs(
|
||||
}
|
||||
|
||||
async function readTrust(
|
||||
repositoryRoot: string,
|
||||
publicKeyPath: string | undefined,
|
||||
keyId: string | undefined,
|
||||
): Promise<ProviderTrust | null> {
|
||||
@@ -64,24 +91,36 @@ async function readTrust(
|
||||
try {
|
||||
return Object.freeze({
|
||||
keyId,
|
||||
publicKey: createPublicKey(await readFile(publicKeyPath, "utf8")),
|
||||
publicKey: createPublicKey(
|
||||
await readFile(path.resolve(repositoryRoot, publicKeyPath), "utf8"),
|
||||
),
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function optionalJson(file: string | undefined): Promise<unknown> {
|
||||
async function optionalJson(
|
||||
repositoryRoot: string,
|
||||
file: string | undefined,
|
||||
): Promise<unknown> {
|
||||
if (!file) return null;
|
||||
try {
|
||||
return JSON.parse(await readFile(file, "utf8")) as unknown;
|
||||
return JSON.parse(
|
||||
await readFile(path.resolve(repositoryRoot, file), "utf8"),
|
||||
) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function requiredJson(file: string): Promise<Record<string, unknown>> {
|
||||
const value: unknown = JSON.parse(await readFile(file, "utf8"));
|
||||
async function requiredJson(
|
||||
repositoryRoot: string,
|
||||
file: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const value: unknown = JSON.parse(
|
||||
await readFile(path.join(repositoryRoot, file), "utf8"),
|
||||
);
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new TypeError(`${file} must be a JSON object`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user