75 lines
2.7 KiB
TypeScript
75 lines
2.7 KiB
TypeScript
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<unknown> {
|
|
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;
|
|
}
|