90 lines
2.8 KiB
TypeScript
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>;
|
|
}
|