import { createHash } from "node:crypto"; import path from "node:path"; import { provenanceProviderAttestationSchema, vulnerabilityProviderReportSchema, } from "./provider-evidence.ts"; import { verifyReleaseCandidate, } from "./release-candidate.ts"; import { readBoundedRegularFile } from "./ci-artifact-validator.ts"; import { verifyCiCandidateArchive } from "./ci-candidate-archive.ts"; export async function validateProviderUpload(input: Readonly<{ kind: "vulnerability" | "provenance"; candidateRoot: string; archivePath: string; expectedArchiveSha256: string; reportPath: string; workspaceRoot?: string; expectedDistSha256: string; }>): Promise { if (!/^[a-f0-9]{64}$/u.test(input.expectedDistSha256)) { throw new TypeError("expected candidate dist SHA-256 is invalid"); } const archive = await verifyCiCandidateArchive({ archivePath: input.archivePath, expectedSha256: input.expectedArchiveSha256, }); const manifest = archive.manifest; if (manifest.distSha256 !== input.expectedDistSha256) { throw new Error("provider input candidate dist digest mismatch"); } const verifiedCandidate = await verifyReleaseCandidate(manifest, input.candidateRoot); if (verifiedCandidate.failures.length > 0) { throw new Error( `provider input candidate root changed: ${verifiedCandidate.failures.join("; ")}`, ); } const reportAbsolute = path.resolve(input.reportPath); const reportRoot = path.resolve(input.workspaceRoot ?? process.cwd()); const reportRelative = path.relative(reportRoot, reportAbsolute).replaceAll(path.sep, "/"); const report = JSON.parse( new TextDecoder("utf-8", { fatal: true }).decode( await readBoundedRegularFile({ root: reportRoot, relativePath: reportRelative, maxBytes: 8_388_608, }), ), ) as unknown; if (input.kind === "vulnerability") { const parsed = vulnerabilityProviderReportSchema.parse(report); const lockfile = await readBoundedRegularFile({ root: input.candidateRoot, relativePath: "pnpm-lock.yaml", maxBytes: 67_108_864, }); const lockfileSha256 = createHash("sha256").update(lockfile).digest("hex"); if ( parsed.scannedDistSha256 !== manifest.distSha256 || parsed.scannedLockfileSha256 !== manifest.lockfileSha256 || lockfileSha256 !== manifest.lockfileSha256 ) { throw new Error("vulnerability provider evidence candidate digest mismatch"); } return parsed; } const parsed = provenanceProviderAttestationSchema.parse(report); if (parsed.subject.digest.sha256 !== manifest.distSha256) { throw new Error("provenance provider evidence candidate digest mismatch"); } return parsed; }