Files
clean-architecture-frontend…/scripts/lib/promotion-verifier.ts
T

90 lines
2.8 KiB
TypeScript

import { createPublicKey } from "node:crypto";
import { readFile } from "node:fs/promises";
import {
evaluatePromotionEvidence,
type ProviderTrust,
} from "./provider-evidence.ts";
import {
RELEASE_CANDIDATE_MANIFEST_PATH,
releaseCandidateManifestSchema,
verifyReleaseCandidate,
} from "./release-candidate.ts";
export async function verifyPromotionInputs(
environment: NodeJS.ProcessEnv = process.env,
) {
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 vulnerabilityReport = await optionalJson(
environment.VULNERABILITY_REPORT_PATH,
);
const provenanceAttestation = await optionalJson(
environment.PROVENANCE_ATTESTATION_PATH,
);
const result = evaluatePromotionEvidence({
candidate: manifest,
currentDistSha256: candidate.currentDistSha256 ?? "",
localStatus: localVerification.localStatus,
vulnerabilityReport,
provenanceAttestation,
vulnerabilityTrust: await readTrust(
environment.VULNERABILITY_PUBLIC_KEY_PATH,
environment.VULNERABILITY_KEY_ID,
),
provenanceTrust: await readTrust(
environment.PROVENANCE_PUBLIC_KEY_PATH,
environment.PROVENANCE_KEY_ID,
),
});
const failures = [...candidate.failures, ...result.failures];
return Object.freeze({
schemaVersion: 1 as const,
status:
failures.length === 0 && result.status === "PASS"
? ("PASS" as const)
: ("FAIL_UNVERIFIED" as const),
vulnerabilityStatus: result.vulnerabilityStatus,
provenanceAttestationStatus: result.provenanceAttestationStatus,
lockfileSha256: manifest.lockfileSha256,
distSha256: manifest.distSha256,
failures: Object.freeze(failures),
});
}
async function readTrust(
publicKeyPath: string | undefined,
keyId: string | undefined,
): Promise<ProviderTrust | null> {
if (!publicKeyPath || !keyId?.trim()) return null;
try {
return Object.freeze({
keyId,
publicKey: createPublicKey(await readFile(publicKeyPath, "utf8")),
});
} catch {
return null;
}
}
async function optionalJson(file: string | undefined): Promise<unknown> {
if (!file) return null;
try {
return JSON.parse(await readFile(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"));
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(`${file} must be a JSON object`);
}
return value as Record<string, unknown>;
}