chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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>;
|
||||
Reference in New Issue
Block a user