import { createHash, generateKeyPairSync, sign } from "node:crypto"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { diffDependencyInventories, isValidSha512Integrity, parsePnpmLockfilePackages, supplyChainDigest, validateDependencyReview, validateLicensePolicy, } from "../../scripts/lib/supply-chain.ts"; import { digestReleaseInputFiles } from "../../scripts/lib/release-input-evidence.ts"; import { isReducedCiContractRun } from "../../scripts/contracts/ci-gates.ts"; import { findSecretMatches } from "../../scripts/lib/secret-scan.ts"; import { parseSecretScanIncludedPaths, selectIncludedInventoryFiles, } from "../../scripts/lib/secret-scan-policy.ts"; import { checkSecurityFixtures } from "../../scripts/lib/security-fixture-check.ts"; import { evaluatePromotionEvidence, providerEvidenceSignaturePayload, providerPublicKeyFingerprint, } from "../../scripts/lib/provider-evidence.ts"; import { createReleaseCandidateManifest, RELEASE_CANDIDATE_EVIDENCE_PATHS, verifyReleaseCandidate, } from "../../scripts/lib/release-candidate.ts"; import { deterministicSupplyChainGeneratedAt } from "../../scripts/lib/supply-chain-time.ts"; import { indexCiGateContract, loadCiGateContract, } from "../../scripts/contracts/ci-gates.ts"; const integrity = `sha512-${Buffer.alloc(64, 7).toString("base64")}`; const dependency = { name: "fixture", version: "1.0.0", direct: true, scope: "production", optional: false, license: "MIT", integrity, dependencies: [], }; const candidateDistSha256 = "1".repeat(64); const lockfileSha256 = "2".repeat(64); const NOW = Date.parse("2026-08-02T01:00:00.000Z"); const sourceIdentity = Object.freeze({ revision: "a".repeat(40), sourceSetSha256: "b".repeat(64), }); const expectedProviderContext = Object.freeze({ run: Object.freeze({ id: "fixture-run", attempt: 1 }), source: sourceIdentity, candidate: Object.freeze({ archiveSha256: "3".repeat(64), bundleSha256: "4".repeat(64), distSha256: candidateDistSha256, lockfileSha256, }), vulnerabilityInvocationNonce: "5".repeat(64), provenanceInvocationNonce: "6".repeat(64), secretScanAttestation: Object.freeze({ status: "PASS" as const, localEvidenceAssessmentSha256: "7".repeat(64), sourceSetSha256: sourceIdentity.sourceSetSha256, policySha256: "8".repeat(64), sarifSha256: "9".repeat(64), scanInputSha256: "a".repeat(64), }), }); function signedProviderEvidence( value: Record, keyId: string, privateKey: ReturnType["privateKey"], publicKeyFingerprint: string, ) { return { ...value, signature: { algorithm: "Ed25519", keyId, publicKeyFingerprint, value: sign( null, providerEvidenceSignaturePayload(value), privateKey, ).toString("base64"), }, }; } function providerPair(input: Readonly<{ vulnerabilityKeys: ReturnType; provenanceKeys: ReturnType; candidate?: typeof expectedProviderContext.candidate; vulnerabilityFingerprint?: string; provenanceFingerprint?: string; }>) { const candidate = input.candidate ?? expectedProviderContext.candidate; const vulnerabilityFingerprint = input.vulnerabilityFingerprint ?? providerPublicKeyFingerprint(input.vulnerabilityKeys.publicKey); const provenanceFingerprint = input.provenanceFingerprint ?? providerPublicKeyFingerprint(input.provenanceKeys.publicKey); return { vulnerabilityReport: signedProviderEvidence({ schemaVersion: 2, evidenceType: "vulnerability-report", provider: "fixture-vulnerability-provider", issuedAt: "2026-08-02T01:00:00.000Z", expiresAt: "2026-08-02T02:00:00.000Z", run: { ...expectedProviderContext.run, invocationNonce: expectedProviderContext.vulnerabilityInvocationNonce }, source: expectedProviderContext.source, candidate, secretScanAttestation: expectedProviderContext.secretScanAttestation, findings: [], }, "fixture-vulnerability-key", input.vulnerabilityKeys.privateKey, vulnerabilityFingerprint), provenanceAttestation: signedProviderEvidence({ schemaVersion: 2, evidenceType: "provenance-attestation", provider: "fixture-provenance-provider", issuedAt: "2026-08-02T01:00:00.000Z", expiresAt: "2026-08-02T02:00:00.000Z", run: { ...expectedProviderContext.run, invocationNonce: expectedProviderContext.provenanceInvocationNonce }, source: expectedProviderContext.source, candidate, signer: "fixture-workload-identity", subject: { name: "dist", digest: { sha256: candidate.distSha256 } }, }, "fixture-provenance-key", input.provenanceKeys.privateKey, provenanceFingerprint), }; } function providerTrust( keyId: string, publicKey: ReturnType["publicKey"], publicKeyFingerprint = providerPublicKeyFingerprint(publicKey), ) { return { keyId, publicKey, publicKeyFingerprint }; } describe("supply-chain policy", () => { it("derives a stable supply-chain timestamp from the immutable build epoch", () => { const input = { generatedAt: "2026-08-01T00:00:00.000Z", sourceDateEpoch: "1785542400", }; expect(deterministicSupplyChainGeneratedAt(input)).toBe( "2026-08-01T00:00:00.000Z", ); expect(deterministicSupplyChainGeneratedAt(input)).toBe( deterministicSupplyChainGeneratedAt({ ...input }), ); expect(() => deterministicSupplyChainGeneratedAt({ generatedAt: "not-a-time", sourceDateEpoch: "1785542400", }), ).toThrow(/generatedAt/u); expect(() => deterministicSupplyChainGeneratedAt({ generatedAt: "2026-08-01T00:00:00.000Z", sourceDateEpoch: "1785542401", }), ).toThrow(/SOURCE_DATE_EPOCH/u); }); it("rejects release candidate dist bytes changed after manifest creation", async () => { const root = await mkdtemp(path.join(tmpdir(), "release-candidate-")); try { const rawLockfile = "lockfileVersion: '9.0'\n"; const rawLockfileSha256 = createHash("sha256") .update(rawLockfile) .digest("hex"); await mkdir(path.join(root, "dist/.vite"), { recursive: true }); await writeFile(path.join(root, "dist/app.js"), "immutable\n"); await writeFile(path.join(root, "dist/.vite/metadata.json"), "{}\n"); await writeFile(path.join(root, "pnpm-lock.yaml"), rawLockfile); for (const file of RELEASE_CANDIDATE_EVIDENCE_PATHS) { if (file === "pnpm-lock.yaml") continue; await mkdir(path.dirname(path.join(root, file)), { recursive: true }); await writeFile( path.join(root, file), file === "artifacts/release/dependency-inventory.json" ? `${JSON.stringify({ lockfileSha256: rawLockfileSha256 })}\n` : `${file}\n`, ); } const manifest = await createReleaseCandidateManifest(root); expect(manifest.lockfileSha256).toBe(rawLockfileSha256); expect(manifest.files).toContainEqual( expect.objectContaining({ path: "pnpm-lock.yaml", sha256: rawLockfileSha256, }), ); expect(await createReleaseCandidateManifest(root)).toEqual(manifest); expect((await verifyReleaseCandidate(manifest, root)).failures).toEqual( [], ); await writeFile(path.join(root, "dist/app.js"), "mutated\n"); expect( (await verifyReleaseCandidate(manifest, root)).failures, ).toEqual( expect.arrayContaining([ "release candidate dist digest mismatch", "release candidate bundle digest mismatch", "release candidate file set or file digest mismatch", ]), ); } finally { await rm(root, { recursive: true, force: true }); } }); it("rejects a dependency inventory digest that differs from raw pnpm-lock bytes", async () => { const root = await mkdtemp(path.join(tmpdir(), "release-lockfile-")); try { await mkdir(path.join(root, "dist"), { recursive: true }); await writeFile(path.join(root, "dist/app.js"), "immutable\n"); await writeFile(path.join(root, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n"); for (const file of RELEASE_CANDIDATE_EVIDENCE_PATHS) { if (file === "pnpm-lock.yaml") continue; await mkdir(path.dirname(path.join(root, file)), { recursive: true }); await writeFile( path.join(root, file), file === "artifacts/release/dependency-inventory.json" ? `${JSON.stringify({ lockfileSha256 })}\n` : `${file}\n`, ); } await expect(createReleaseCandidateManifest(root)).rejects.toThrow( /raw pnpm-lock digest mismatch/u, ); } finally { await rm(root, { recursive: true, force: true }); } }); it("fails promotion when external provider evidence is absent", () => { const result = evaluatePromotionEvidence({ expected: expectedProviderContext, localStatus: "PASS", vulnerabilityReport: null, provenanceAttestation: null, vulnerabilityTrust: null, provenanceTrust: null, nowEpochMs: () => NOW, }); expect(result.status).toBe("FAIL_UNVERIFIED"); }); it("passes only signed provider evidence for the exact immutable candidate", () => { const vulnerabilityKeys = generateKeyPairSync("ed25519"); const provenanceKeys = generateKeyPairSync("ed25519"); const { vulnerabilityReport, provenanceAttestation } = providerPair({ vulnerabilityKeys, provenanceKeys, }); const result = evaluatePromotionEvidence({ expected: expectedProviderContext, localStatus: "PASS", vulnerabilityReport, provenanceAttestation, vulnerabilityTrust: providerTrust( "fixture-vulnerability-key", vulnerabilityKeys.publicKey, ), provenanceTrust: providerTrust( "fixture-provenance-key", provenanceKeys.publicKey, ), nowEpochMs: () => NOW, }); expect(result).toMatchObject({ status: "PASS", vulnerabilityStatus: "PASS", provenanceAttestationStatus: "PASS", failures: [], }); }); it("rejects correctly signed provider evidence for a different digest", () => { const vulnerabilityKeys = generateKeyPairSync("ed25519"); const provenanceKeys = generateKeyPairSync("ed25519"); const wrongDistSha256 = "3".repeat(64); const { vulnerabilityReport, provenanceAttestation } = providerPair({ vulnerabilityKeys, provenanceKeys, candidate: { ...expectedProviderContext.candidate, distSha256: wrongDistSha256 }, }); const result = evaluatePromotionEvidence({ expected: expectedProviderContext, localStatus: "PASS", vulnerabilityReport, provenanceAttestation, vulnerabilityTrust: providerTrust("fixture-vulnerability-key", vulnerabilityKeys.publicKey), provenanceTrust: providerTrust("fixture-provenance-key", provenanceKeys.publicKey), nowEpochMs: () => NOW, }); expect(result.status).toBe("FAIL_UNVERIFIED"); expect(result.failures).toEqual( expect.arrayContaining([ "vulnerability report candidate identity mismatch", "provenance attestation candidate identity mismatch", ]), ); }); it("rejects candidate bytes changed after provider attestation", () => { const vulnerabilityKeys = generateKeyPairSync("ed25519"); const provenanceKeys = generateKeyPairSync("ed25519"); const { vulnerabilityReport, provenanceAttestation } = providerPair({ vulnerabilityKeys, provenanceKeys, }); const result = evaluatePromotionEvidence({ expected: { ...expectedProviderContext, candidate: { ...expectedProviderContext.candidate, distSha256: "4".repeat(64) }, }, localStatus: "PASS", vulnerabilityReport, provenanceAttestation, vulnerabilityTrust: providerTrust("fixture-vulnerability-key", vulnerabilityKeys.publicKey), provenanceTrust: providerTrust("fixture-provenance-key", provenanceKeys.publicKey), nowEpochMs: () => NOW, }); expect(result.status).toBe("FAIL_UNVERIFIED"); expect(result.failures).toContain("vulnerability report candidate identity mismatch"); }); it("rejects Ed448 keys mislabeled as Ed25519 evidence", () => { const vulnerabilityKeys = generateKeyPairSync("ed448"); const provenanceKeys = generateKeyPairSync("ed448"); const fakeFingerprint = `sha256:${"7".repeat(64)}`; const { vulnerabilityReport, provenanceAttestation } = providerPair({ vulnerabilityKeys, provenanceKeys, vulnerabilityFingerprint: fakeFingerprint, provenanceFingerprint: fakeFingerprint, }); expect( evaluatePromotionEvidence({ expected: expectedProviderContext, localStatus: "PASS", vulnerabilityReport, provenanceAttestation, vulnerabilityTrust: { keyId: "fixture-vulnerability-key", publicKey: vulnerabilityKeys.publicKey, publicKeyFingerprint: fakeFingerprint, }, provenanceTrust: { keyId: "fixture-provenance-key", publicKey: provenanceKeys.publicKey, publicKeyFingerprint: fakeFingerprint, }, nowEpochMs: () => NOW, }).status, ).toBe("FAIL_UNVERIFIED"); }); it.each([ ["empty", []], ["empty entry", [""]], ["blank entry", [" "]], ["absolute", ["/src"]], ["backslash", ["src\\file.ts"]], ["dot", ["."]], ["dotdot", [".."]], ["traversal", ["src/../docs"]], ["trailing slash", ["src/"]], ["mixed", ["src", 42]], ["duplicate", ["src", "src"]], ])("rejects %s secret-scan include paths", (_name, includedPaths) => { expect(() => parseSecretScanIncludedPaths(includedPaths)).toThrow(); }); it("requires every configured include path to match the inventory", () => { expect( selectIncludedInventoryFiles( ["README.md", "src/app.ts"], ["src"], ), ).toEqual(["src/app.ts"]); expect(() => selectIncludedInventoryFiles( ["README.md", "src/app.ts"], ["misspelled"], ), ).toThrow(/misspelled/u); expect( selectIncludedInventoryFiles( ["README.md", "src/app.ts"], null, ), ).toEqual(["README.md", "src/app.ts"]); }); it("rejects a crashed fixture scan and cannot reuse a stale repository artifact", async () => { const cleaned: string[] = []; await expect( checkSecurityFixtures({ createTempDirectory: async () => "/tmp/fresh-security-fixture", runScan: () => ({ status: 1, signal: null, stdout: "", stderr: "Security scan found 3 blocking result(s).\n", }), readArtifact: async (artifactPath) => { expect(artifactPath).toBe( "/tmp/fresh-security-fixture/scan-fixture.sarif", ); throw Object.assign(new Error("fresh artifact missing"), { code: "ENOENT", }); }, cleanup: async (directory) => { cleaned.push(directory); }, }), ).rejects.toThrow(/fresh artifact missing/u); expect(cleaned).toEqual(["/tmp/fresh-security-fixture"]); await expect( checkSecurityFixtures({ createTempDirectory: async () => "/tmp/fresh-security-fixture", runScan: () => ({ status: null, signal: "SIGTERM", stdout: "", stderr: "Security scan found 3 blocking result(s).\n", }), readArtifact: async () => "{}", cleanup: async () => undefined, }), ).rejects.toThrow(/did not fail exactly/u); }); it("wires the exact security fixture checker as a passing CI gate", async () => { const contract = await loadCiGateContract(process.cwd(), { mode: process.env.CI_CONTRACT_MODE === "removal-fixture" ? "removal-fixture" : "canonical", }); 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" }), ]), ); const evidence = securityGate!.evidenceArtifactIds.map( (artifactId) => index.artifacts.get(artifactId)?.path, ); expect(evidence).not.toContain( "artifacts/security/scan-fixture.sarif", ); }); it("uses one fail-closed repository inventory for provenance and secret scanning", async () => { const [provenanceSource, securityCliSource, securityEvaluatorSource] = await Promise.all([ readFile("scripts/generate-supply-chain.ts", "utf8"), readFile("scripts/security-scan.ts", "utf8"), readFile("scripts/lib/secret-scan-evaluator.ts", "utf8"), ]); expect(securityCliSource).toContain("evaluateRepositorySecretScan"); for (const source of [provenanceSource, securityEvaluatorSource]) { expect(source).toContain("buildRepositoryFileInventory"); expect(source).not.toContain("async function filesWithin"); } }); it("binds provenance digest behavior to tracked files outside policy roots", async () => { const contents = new Map([ ["src/app.ts", Buffer.from("app\n")], ["README.md", Buffer.from("one\n")], ]); const first = await digestReleaseInputFiles( ["README.md", "src/app.ts"], async (file) => contents.get(file)!, ); contents.set("README.md", Buffer.from("two\n")); const second = await digestReleaseInputFiles( ["README.md", "src/app.ts"], async (file) => contents.get(file)!, ); expect(second).not.toBe(first); }); it("detects every forbidden secret fixture, including quoted JSON keys", async () => { const fixtureRoot = "tests/fixtures/security/secret-detection/forbidden"; const findings = ( await Promise.all( ["source.ts", "dist.ts", "config.json"].map(async (file) => findSecretMatches( `${fixtureRoot}/${file}`, await readFile(`${fixtureRoot}/${file}`, "utf8"), ), ), ) ).flat(); expect(findings.map((finding) => [finding.file, finding.ruleId])).toEqual([ [`${fixtureRoot}/source.ts`, "aws-access-key"], [`${fixtureRoot}/dist.ts`, "assigned-secret"], [`${fixtureRoot}/config.json`, "assigned-secret"], ]); }); // A removal fixture deletes some of these inputs on purpose — removing the // browser file/storage capability takes the whole browser-capability harness // with it — and prunes them from its own policy. This is a claim about the // full repository, so it does not describe a deliberately reduced one. it.skipIf(isReducedCiContractRun())("covers every mandatory release input in the secret scan policy", async () => { const policy = JSON.parse( await readFile("config/security/secret-scan-policy.json", "utf8"), ) as { trackedRoots: string[] }; expect(policy.trackedRoots).toEqual( expect.arrayContaining([ "index.html", ".dependency-cruiser.json", ".nvmrc", ".npmrc", "eslint.config.ts", "package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "scripts", "schemas", "config", ".gitea/workflows/quality-gates.yml", "vite.config.ts", "vite.service-worker.config.ts", "vitest.config.ts", "playwright.config.ts", "playwright.capabilities.config.ts", "playwright.dev.config.ts", "playwright.storybook.config.ts", "playwright.visual.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.base.json", "tsconfig.node.json", "tsconfig.recipes.json", "tsconfig.service-worker.json", "tsconfig.test.json", "tsconfig.web-worker.json", ]), ); }); it("parses every top-level lockfile package and validates SRI", () => { const parsed = parsePnpmLockfilePackages(` packages: '@scope/one@1.0.0': resolution: {integrity: ${integrity}} two@2.0.0: resolution: {integrity: ${integrity}} snapshots: `); expect(parsed).toEqual([ { name: "@scope/one", version: "1.0.0", integrity }, { name: "two", version: "2.0.0", integrity }, ]); expect(parsed.every((entry) => isValidSha512Integrity(entry.integrity))).toBe( true, ); }); it("keeps inventory digests stable when dependency ordering changes", () => { const other = { ...dependency, name: "other" }; expect(supplyChainDigest([dependency, other])).toBe( supplyChainDigest([other, dependency]), ); }); it("calculates actual additions and requires independent high-risk review", () => { const before = { dependencies: [] }; const after = { dependencies: [dependency] }; const diff = diffDependencyInventories(before, after); expect(diff.added).toEqual(["fixture@1.0.0"]); expect( validateDependencyReview(diff, after, { changes: [ { changeId: "add:fixture@1.0.0", owner: "one", reviewer: "one", reason: "fixture", rollback: "remove", }, ], }).passed, ).toBe(false); }); it("allows explicit policy licenses and rejects denied licenses", () => { expect( validateLicensePolicy( { dependencies: [dependency] }, { allowedLicenses: ["MIT"], deniedLicensePatterns: ["AGPL"] }, ).passed, ).toBe(true); expect( validateLicensePolicy( { dependencies: [{ ...dependency, license: "AGPL-3.0" }], }, { allowedLicenses: ["MIT"], deniedLicensePatterns: ["AGPL"] }, ).passed, ).toBe(false); }); });