Files
tech-log-frontend/scripts/lib/provider-supervisor.ts

164 lines
6.7 KiB
TypeScript

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<Record<string, string>>;
}>;
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<void>;
captureReport: () => Promise<Buffer>;
}>, dependencies: Readonly<{
captureArchive?: typeof captureCiCandidateArchive;
withVerifiedCandidate?: typeof withVerifiedCapturedCandidate;
verifyLocalEvidence?: typeof verifyArchivedLocalEvidence;
validateUpload?: typeof validateProviderUpload;
randomBytes?: (bytes: number) => Buffer;
nowEpochMs?: () => number;
}> = {}): Promise<Readonly<{
evidence: unknown;
invocationNonce: string;
expectedContext: ExpectedPromotionContext;
}>> {
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<Record<string, string>> {
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<CapturedCandidateArchive>;