refactor: generate CI workflow from gate contracts
This commit is contained in:
@@ -23,6 +23,7 @@ import { checkSecurityFixtures } from "../../scripts/lib/security-fixture-check.
|
||||
import {
|
||||
evaluatePromotionEvidence,
|
||||
providerEvidenceSignaturePayload,
|
||||
providerVerificationArtifactSchema,
|
||||
} from "../../scripts/lib/provider-evidence.ts";
|
||||
import {
|
||||
createReleaseCandidateManifest,
|
||||
@@ -31,6 +32,10 @@ import {
|
||||
} from "../../scripts/lib/release-candidate.ts";
|
||||
import { deterministicSupplyChainGeneratedAt } from "../../scripts/lib/supply-chain-time.ts";
|
||||
import { verifyPromotionInputs } from "../../scripts/lib/promotion-verifier.ts";
|
||||
import {
|
||||
indexCiGateContract,
|
||||
loadCiGateContract,
|
||||
} from "../../scripts/contracts/ci-gates.ts";
|
||||
|
||||
const integrity = `sha512-${Buffer.alloc(64, 7).toString("base64")}`;
|
||||
const dependency = {
|
||||
@@ -125,6 +130,7 @@ async function writeProviderEnvironment(
|
||||
);
|
||||
await mkdir(path.join(root, "provider"), { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(path.join(root, "provider/candidate.tar.gz"), "fixture archive\n"),
|
||||
writeFile(
|
||||
path.join(root, "provider/vulnerability.json"),
|
||||
`${JSON.stringify(vulnerabilityReport)}\n`,
|
||||
@@ -147,6 +153,10 @@ async function writeProviderEnvironment(
|
||||
),
|
||||
]);
|
||||
return {
|
||||
CANDIDATE_ARCHIVE_PATH: "provider/candidate.tar.gz",
|
||||
CANDIDATE_ARCHIVE_SHA256: createHash("sha256")
|
||||
.update("fixture archive\n")
|
||||
.digest("hex"),
|
||||
VULNERABILITY_REPORT_PATH: "provider/vulnerability.json",
|
||||
PROVENANCE_ATTESTATION_PATH: "provider/provenance.json",
|
||||
VULNERABILITY_PUBLIC_KEY_PATH: "provider/vulnerability.pem",
|
||||
@@ -157,6 +167,39 @@ async function writeProviderEnvironment(
|
||||
}
|
||||
|
||||
describe("supply-chain policy", () => {
|
||||
it("emits a strict role-bound v2 verification record from exact input bytes", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "promotion-verification-v2-"));
|
||||
try {
|
||||
const manifest = await createMinimalCandidateTree(root);
|
||||
const environment = await writeProviderEnvironment(
|
||||
root,
|
||||
manifest.distSha256,
|
||||
manifest.lockfileSha256,
|
||||
);
|
||||
const report = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment,
|
||||
verifyLocalEvidence: async () => ({ status: "PASS" as const, failures: [] }),
|
||||
} as Parameters<typeof verifyPromotionInputs>[0]);
|
||||
expect(providerVerificationArtifactSchema.parse(report)).toEqual(
|
||||
expect.objectContaining({
|
||||
schemaVersion: 2,
|
||||
artifactType: "provider-verification",
|
||||
candidateArchiveSha256: environment.CANDIDATE_ARCHIVE_SHA256,
|
||||
vulnerabilityReportSha256: createHash("sha256")
|
||||
.update(await readFile(path.join(root, environment.VULNERABILITY_REPORT_PATH!)))
|
||||
.digest("hex"),
|
||||
provenanceAttestationSha256: createHash("sha256")
|
||||
.update(await readFile(path.join(root, environment.PROVENANCE_ATTESTATION_PATH!)))
|
||||
.digest("hex"),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("wires candidate files, PEM trust, env report paths, and mutation checks", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "promotion-wiring-"));
|
||||
try {
|
||||
@@ -171,11 +214,13 @@ describe("supply-chain policy", () => {
|
||||
failures: [] as const,
|
||||
});
|
||||
const valid = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment: validEnvironment,
|
||||
verifyLocalEvidence: acceptLocalEvidence,
|
||||
});
|
||||
const absent = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment: {},
|
||||
verifyLocalEvidence: acceptLocalEvidence,
|
||||
@@ -186,12 +231,14 @@ describe("supply-chain policy", () => {
|
||||
manifest.lockfileSha256,
|
||||
);
|
||||
const wrongDigest = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment: wrongEnvironment,
|
||||
verifyLocalEvidence: acceptLocalEvidence,
|
||||
});
|
||||
await writeFile(path.join(root, "dist/app.js"), "mutated\n");
|
||||
const postAttestationMutation = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment: validEnvironment,
|
||||
verifyLocalEvidence: acceptLocalEvidence,
|
||||
@@ -228,6 +275,7 @@ describe("supply-chain policy", () => {
|
||||
);
|
||||
const before = await readFile(localVerificationPath, "utf8");
|
||||
const result = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment,
|
||||
});
|
||||
@@ -656,20 +704,28 @@ describe("supply-chain policy", () => {
|
||||
});
|
||||
|
||||
it("wires the exact security fixture checker as a passing CI gate", async () => {
|
||||
const gates = JSON.parse(await readFile("config/ci/gates.json", "utf8")) as {
|
||||
gates: Record<string, { steps: unknown[]; evidence: string[] }>;
|
||||
};
|
||||
const securityGate = gates.gates["FE-GATE-013"]!;
|
||||
expect(securityGate.steps).toContainEqual({
|
||||
script: "check:security:fixtures",
|
||||
expect: "pass",
|
||||
});
|
||||
expect(securityGate.steps).not.toEqual(
|
||||
const contract = await loadCiGateContract(process.cwd());
|
||||
const index = indexCiGateContract(contract);
|
||||
const securityGate = index.gates.get("FE-GATE-013");
|
||||
expect(securityGate).toBeDefined();
|
||||
const commands = securityGate!.commandIds.map((commandId) =>
|
||||
index.commands.get(commandId),
|
||||
);
|
||||
expect(commands).toContainEqual(
|
||||
expect.objectContaining({
|
||||
script: "check:security:fixtures",
|
||||
expect: "pass",
|
||||
}),
|
||||
);
|
||||
expect(commands).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ script: "scan:security:fixture" }),
|
||||
]),
|
||||
);
|
||||
expect(securityGate.evidence).not.toContain(
|
||||
const evidence = securityGate!.evidenceArtifactIds.map(
|
||||
(artifactId) => index.artifacts.get(artifactId)?.path,
|
||||
);
|
||||
expect(evidence).not.toContain(
|
||||
"artifacts/security/scan-fixture.sarif",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user