import { randomBytes as cryptoRandomBytes } from "node:crypto"; import { captureCiCandidateArchive, withVerifiedCapturedCandidate, type CapturedCandidateArchive, } from "./ci-candidate-archive.ts"; import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts"; import type { ExpectedPromotionContext, ProviderTrust } from "./provider-evidence.ts"; import { validateProviderUpload } from "./provider-upload-validator.ts"; export type ProviderInvocation = Readonly<{ candidateRoot: string; environment: Readonly>; }>; export async function superviseProviderEvidence(input: Readonly<{ kind: "vulnerability" | "provenance"; archivePath: string; expectedArchiveSha256: string; expectedRun: Readonly<{ id: string; attempt: number; sourceRevision: string }>; trust: ProviderTrust; executeProvider: (invocation: ProviderInvocation) => Promise; captureReport: () => Promise; }>, dependencies: Readonly<{ captureArchive?: typeof captureCiCandidateArchive; withVerifiedCandidate?: typeof withVerifiedCapturedCandidate; verifyLocalEvidence?: typeof verifyArchivedLocalEvidence; validateUpload?: typeof validateProviderUpload; randomBytes?: (bytes: number) => Buffer; nowEpochMs?: () => number; }> = {}): Promise> { const captured = await (dependencies.captureArchive ?? captureCiCandidateArchive)({ archivePath: input.archivePath, expectedSha256: input.expectedArchiveSha256, }); const nonceBytes = (dependencies.randomBytes ?? cryptoRandomBytes)(32); if (nonceBytes.byteLength !== 32) { throw new TypeError("provider invocation nonce must contain exactly 32 bytes"); } const invocationNonce = nonceBytes.toString("hex"); const nowEpochMs = dependencies.nowEpochMs ?? Date.now; const result = await (dependencies.withVerifiedCandidate ?? withVerifiedCapturedCandidate)({ captured, verify: async ({ extractionRoot, manifest }) => { const local = await (dependencies.verifyLocalEvidence ?? verifyArchivedLocalEvidence)({ extractionRoot, expectedManifest: manifest, }); if (local.status !== "PASS" || !local.identity) { throw new Error( `provider candidate local assessment failed: ${local.failures.join("; ")}`, ); } if (local.identity.sourceRevision !== input.expectedRun.sourceRevision) { throw new Error("provider candidate source revision mismatch"); } const expectedContext: ExpectedPromotionContext = Object.freeze({ run: Object.freeze({ id: input.expectedRun.id, attempt: input.expectedRun.attempt }), source: Object.freeze({ revision: local.identity.sourceRevision, sourceSetSha256: local.identity.sourceSetSha256, }), candidate: Object.freeze({ archiveSha256: captured.archiveSha256, bundleSha256: manifest.bundleSha256, distSha256: manifest.distSha256, lockfileSha256: manifest.lockfileSha256, }), secretScanAttestation: Object.freeze({ status: "PASS" as const, localEvidenceAssessmentSha256: local.identity.assessmentSha256, sourceSetSha256: local.identity.sourceSetSha256, policySha256: local.identity.secretScan.policySha256, sarifSha256: local.identity.secretScan.sarifSha256, scanInputSha256: local.identity.secretScan.scanInputSha256, }), vulnerabilityInvocationNonce: input.kind === "vulnerability" ? invocationNonce : "0".repeat(64), provenanceInvocationNonce: input.kind === "provenance" ? invocationNonce : "0".repeat(64), }); const issuedNow = nowEpochMs(); const issuedAt = new Date(issuedNow).toISOString(); const expiresAt = new Date(issuedNow + 60 * 60 * 1_000).toISOString(); await input.executeProvider({ candidateRoot: extractionRoot, environment: providerInvocationEnvironment({ kind: input.kind, expectedContext, invocationNonce, issuedAt, expiresAt, trust: input.trust, }), }); const capturedReport = await input.captureReport(); const evidence = await (dependencies.validateUpload ?? validateProviderUpload)({ kind: input.kind, verifiedManifest: manifest, archiveSha256: captured.archiveSha256, candidateRoot: extractionRoot, capturedReport, expectedContext, trust: input.trust, nowEpochMs, }); return Object.freeze({ evidence, invocationNonce, expectedContext }); }, }); return result; } export function providerInvocationEnvironment(input: Readonly<{ kind: "vulnerability" | "provenance"; expectedContext: ExpectedPromotionContext; invocationNonce: string; issuedAt: string; expiresAt: string; trust: ProviderTrust; }>): Readonly> { return Object.freeze({ PROVIDER_EVIDENCE_SCHEMA_VERSION: "2", PROVIDER_EVIDENCE_TYPE: input.kind === "vulnerability" ? "vulnerability-report" : "provenance-attestation", PROVIDER_ISSUED_AT: input.issuedAt, PROVIDER_EXPIRES_AT: input.expiresAt, PROVIDER_INVOCATION_NONCE: input.invocationNonce, PROVIDER_KEY_ID: input.trust.keyId, PROVIDER_PUBLIC_KEY_FINGERPRINT: input.trust.publicKeyFingerprint, CI_RUN_ID: input.expectedContext.run.id, CI_RUN_ATTEMPT: String(input.expectedContext.run.attempt), SOURCE_REVISION: input.expectedContext.source.revision, SOURCE_SET_SHA256: input.expectedContext.source.sourceSetSha256, CANDIDATE_ROOT: "/candidate", CANDIDATE_LOCKFILE_PATH: "/candidate/pnpm-lock.yaml", CANDIDATE_ARCHIVE_SHA256: input.expectedContext.candidate.archiveSha256, CANDIDATE_BUNDLE_SHA256: input.expectedContext.candidate.bundleSha256, CANDIDATE_DIST_SHA256: input.expectedContext.candidate.distSha256, CANDIDATE_LOCKFILE_SHA256: input.expectedContext.candidate.lockfileSha256, SECRET_SCAN_STATUS: input.expectedContext.secretScanAttestation.status, SECRET_SCAN_LOCAL_EVIDENCE_ASSESSMENT_SHA256: input.expectedContext.secretScanAttestation.localEvidenceAssessmentSha256, SECRET_SCAN_SOURCE_SET_SHA256: input.expectedContext.secretScanAttestation.sourceSetSha256, SECRET_SCAN_POLICY_SHA256: input.expectedContext.secretScanAttestation.policySha256, SECRET_SCAN_SARIF_SHA256: input.expectedContext.secretScanAttestation.sarifSha256, SECRET_SCAN_INPUT_SHA256: input.expectedContext.secretScanAttestation.scanInputSha256, }); } export type CaptureArchiveDependency = ( input: Readonly<{ archivePath: string; expectedSha256: string }>, ) => Promise;