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 { 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, } 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 { verifyPromotionInputs } from "../../scripts/lib/promotion-verifier.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); function signedProviderEvidence( value: Record, keyId: string, privateKey: ReturnType["privateKey"], ) { return { ...value, signature: { algorithm: "Ed25519", keyId, value: sign( null, providerEvidenceSignaturePayload(value), privateKey, ).toString("base64"), }, }; } async function createMinimalCandidateTree(root: string) { const rawLockfile = "lockfileVersion: '9.0'\n"; const rawLockfileSha256 = createHash("sha256") .update(rawLockfile) .digest("hex"); 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"), 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 }); const value = file === "artifacts/release/dependency-inventory.json" ? { lockfileSha256: rawLockfileSha256 } : file === "artifacts/security/supply-chain-verification.json" ? { localStatus: "PASS" } : { fixture: file }; await writeFile(path.join(root, file), `${JSON.stringify(value)}\n`); } const manifest = await createReleaseCandidateManifest(root); await writeFile( path.join(root, "artifacts/release/release-candidate.json"), `${JSON.stringify(manifest)}\n`, ); return manifest; } async function writeProviderEnvironment( root: string, distDigest: string, candidateLockfileSha256: string, ) { const vulnerabilityKeys = generateKeyPairSync("ed25519"); const provenanceKeys = generateKeyPairSync("ed25519"); const vulnerabilityReport = signedProviderEvidence( { schemaVersion: 1, provider: "fixture-vulnerability-provider", generatedAt: "2026-08-01T00:00:00.000Z", scannedLockfileSha256: candidateLockfileSha256, scannedDistSha256: distDigest, findings: [], }, "fixture-vulnerability-key", vulnerabilityKeys.privateKey, ); const provenanceAttestation = signedProviderEvidence( { schemaVersion: 1, provider: "fixture-provenance-provider", signer: "fixture-workload-identity", generatedAt: "2026-08-01T00:00:00.000Z", subject: { name: "dist", digest: { sha256: distDigest } }, }, "fixture-provenance-key", provenanceKeys.privateKey, ); await mkdir(path.join(root, "provider"), { recursive: true }); await Promise.all([ writeFile( path.join(root, "provider/vulnerability.json"), `${JSON.stringify(vulnerabilityReport)}\n`, ), writeFile( path.join(root, "provider/provenance.json"), `${JSON.stringify(provenanceAttestation)}\n`, ), writeFile( path.join(root, "provider/vulnerability.pem"), vulnerabilityKeys.publicKey .export({ type: "spki", format: "pem" }) .toString(), ), writeFile( path.join(root, "provider/provenance.pem"), provenanceKeys.publicKey .export({ type: "spki", format: "pem" }) .toString(), ), ]); return { VULNERABILITY_REPORT_PATH: "provider/vulnerability.json", PROVENANCE_ATTESTATION_PATH: "provider/provenance.json", VULNERABILITY_PUBLIC_KEY_PATH: "provider/vulnerability.pem", VULNERABILITY_KEY_ID: "fixture-vulnerability-key", PROVENANCE_PUBLIC_KEY_PATH: "provider/provenance.pem", PROVENANCE_KEY_ID: "fixture-provenance-key", } satisfies NodeJS.ProcessEnv; } describe("supply-chain policy", () => { it("wires candidate files, PEM trust, env report paths, and mutation checks", async () => { const root = await mkdtemp(path.join(tmpdir(), "promotion-wiring-")); try { const manifest = await createMinimalCandidateTree(root); const validEnvironment = await writeProviderEnvironment( root, manifest.distSha256, manifest.lockfileSha256, ); const acceptLocalEvidence = async () => ({ status: "PASS" as const, failures: [] as const, }); const valid = await verifyPromotionInputs({ repositoryRoot: root, environment: validEnvironment, verifyLocalEvidence: acceptLocalEvidence, }); const absent = await verifyPromotionInputs({ repositoryRoot: root, environment: {}, verifyLocalEvidence: acceptLocalEvidence, }); const wrongEnvironment = await writeProviderEnvironment( root, "3".repeat(64), manifest.lockfileSha256, ); const wrongDigest = await verifyPromotionInputs({ repositoryRoot: root, environment: wrongEnvironment, verifyLocalEvidence: acceptLocalEvidence, }); await writeFile(path.join(root, "dist/app.js"), "mutated\n"); const postAttestationMutation = await verifyPromotionInputs({ repositoryRoot: root, environment: validEnvironment, verifyLocalEvidence: acceptLocalEvidence, }); expect({ valid: valid.status, absent: absent.status, wrongDigest: wrongDigest.status, postAttestationMutation: postAttestationMutation.status, }).toEqual({ valid: "PASS", absent: "FAIL_UNVERIFIED", wrongDigest: "FAIL_UNVERIFIED", postAttestationMutation: "FAIL_UNVERIFIED", }); } finally { await rm(root, { recursive: true, force: true }); } }); it("rejects a self-consistent candidate that merely claims localStatus PASS", async () => { const root = await mkdtemp(path.join(tmpdir(), "promotion-local-status-")); try { const manifest = await createMinimalCandidateTree(root); const environment = await writeProviderEnvironment( root, manifest.distSha256, manifest.lockfileSha256, ); const localVerificationPath = path.join( root, "artifacts/security/supply-chain-verification.json", ); const before = await readFile(localVerificationPath, "utf8"); const result = await verifyPromotionInputs({ repositoryRoot: root, environment, }); expect(result.status).toBe("FAIL_UNVERIFIED"); expect(result.failures).toEqual( expect.arrayContaining([ expect.stringMatching(/executable schema mismatch/u), "local supply-chain evidence is not PASS", ]), ); expect(await readFile(localVerificationPath, "utf8")).toBe(before); } finally { await rm(root, { recursive: true, force: true }); } }); 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({ candidate: { distSha256: candidateDistSha256, lockfileSha256, }, currentDistSha256: candidateDistSha256, localStatus: "PASS", vulnerabilityReport: null, provenanceAttestation: null, vulnerabilityTrust: null, provenanceTrust: null, }); 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 = signedProviderEvidence( { schemaVersion: 1, provider: "fixture-vulnerability-provider", generatedAt: "2026-08-01T00:00:00.000Z", scannedLockfileSha256: lockfileSha256, scannedDistSha256: candidateDistSha256, findings: [], }, "fixture-vulnerability-key", vulnerabilityKeys.privateKey, ); const provenanceAttestation = signedProviderEvidence( { schemaVersion: 1, provider: "fixture-provenance-provider", signer: "fixture-workload-identity", generatedAt: "2026-08-01T00:00:00.000Z", subject: { name: "dist", digest: { sha256: candidateDistSha256 }, }, }, "fixture-provenance-key", provenanceKeys.privateKey, ); const result = evaluatePromotionEvidence({ candidate: { distSha256: candidateDistSha256, lockfileSha256, }, currentDistSha256: candidateDistSha256, localStatus: "PASS", vulnerabilityReport, provenanceAttestation, vulnerabilityTrust: { keyId: "fixture-vulnerability-key", publicKey: vulnerabilityKeys.publicKey, }, provenanceTrust: { keyId: "fixture-provenance-key", publicKey: provenanceKeys.publicKey, }, }); 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 = signedProviderEvidence( { schemaVersion: 1, provider: "fixture-vulnerability-provider", generatedAt: "2026-08-01T00:00:00.000Z", scannedLockfileSha256: lockfileSha256, scannedDistSha256: wrongDistSha256, findings: [], }, "fixture-vulnerability-key", vulnerabilityKeys.privateKey, ); const provenanceAttestation = signedProviderEvidence( { schemaVersion: 1, provider: "fixture-provenance-provider", signer: "fixture-workload-identity", generatedAt: "2026-08-01T00:00:00.000Z", subject: { name: "dist", digest: { sha256: wrongDistSha256 } }, }, "fixture-provenance-key", provenanceKeys.privateKey, ); const result = evaluatePromotionEvidence({ candidate: { distSha256: candidateDistSha256, lockfileSha256, }, currentDistSha256: candidateDistSha256, localStatus: "PASS", vulnerabilityReport, provenanceAttestation, vulnerabilityTrust: { keyId: "fixture-vulnerability-key", publicKey: vulnerabilityKeys.publicKey, }, provenanceTrust: { keyId: "fixture-provenance-key", publicKey: provenanceKeys.publicKey, }, }); expect(result.status).toBe("FAIL_UNVERIFIED"); expect(result.failures).toEqual( expect.arrayContaining([ "vulnerability report dist digest mismatch", "provenance attestation dist digest mismatch", ]), ); }); it("rejects candidate bytes changed after provider attestation", () => { const vulnerabilityKeys = generateKeyPairSync("ed25519"); const provenanceKeys = generateKeyPairSync("ed25519"); const vulnerabilityReport = signedProviderEvidence( { schemaVersion: 1, provider: "fixture-vulnerability-provider", generatedAt: "2026-08-01T00:00:00.000Z", scannedLockfileSha256: lockfileSha256, scannedDistSha256: candidateDistSha256, findings: [], }, "fixture-vulnerability-key", vulnerabilityKeys.privateKey, ); const provenanceAttestation = signedProviderEvidence( { schemaVersion: 1, provider: "fixture-provenance-provider", signer: "fixture-workload-identity", generatedAt: "2026-08-01T00:00:00.000Z", subject: { name: "dist", digest: { sha256: candidateDistSha256 }, }, }, "fixture-provenance-key", provenanceKeys.privateKey, ); const result = evaluatePromotionEvidence({ candidate: { distSha256: candidateDistSha256, lockfileSha256, }, currentDistSha256: "4".repeat(64), localStatus: "PASS", vulnerabilityReport, provenanceAttestation, vulnerabilityTrust: { keyId: "fixture-vulnerability-key", publicKey: vulnerabilityKeys.publicKey, }, provenanceTrust: { keyId: "fixture-provenance-key", publicKey: provenanceKeys.publicKey, }, }); expect(result.status).toBe("FAIL_UNVERIFIED"); expect(result.failures).toContain( "candidate dist bytes changed after immutable build", ); }); it("rejects Ed448 keys mislabeled as Ed25519 evidence", () => { const vulnerabilityKeys = generateKeyPairSync("ed448"); const provenanceKeys = generateKeyPairSync("ed448"); const vulnerabilityReport = signedProviderEvidence( { schemaVersion: 1, provider: "fixture-vulnerability-provider", generatedAt: "2026-08-01T00:00:00.000Z", scannedLockfileSha256: lockfileSha256, scannedDistSha256: candidateDistSha256, findings: [], }, "fixture-vulnerability-key", vulnerabilityKeys.privateKey, ); const provenanceAttestation = signedProviderEvidence( { schemaVersion: 1, provider: "fixture-provenance-provider", signer: "fixture-workload-identity", generatedAt: "2026-08-01T00:00:00.000Z", subject: { name: "dist", digest: { sha256: candidateDistSha256 }, }, }, "fixture-provenance-key", provenanceKeys.privateKey, ); expect( evaluatePromotionEvidence({ candidate: { distSha256: candidateDistSha256, lockfileSha256 }, currentDistSha256: candidateDistSha256, localStatus: "PASS", vulnerabilityReport, provenanceAttestation, vulnerabilityTrust: { keyId: "fixture-vulnerability-key", publicKey: vulnerabilityKeys.publicKey, }, provenanceTrust: { keyId: "fixture-provenance-key", publicKey: provenanceKeys.publicKey, }, }).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 gates = JSON.parse(await readFile("config/ci/gates.json", "utf8")) as { gates: Record; }; const securityGate = gates.gates["FE-GATE-013"]!; expect(securityGate.steps).toContainEqual({ script: "check:security:fixtures", expect: "pass", }); expect(securityGate.steps).not.toEqual( expect.arrayContaining([ expect.objectContaining({ script: "scan:security:fixture" }), ]), ); expect(securityGate.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"], ]); }); it("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); }); });