import { 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"; 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"), }, }; } describe("supply-chain policy", () => { it("rejects release candidate dist bytes changed after manifest creation", async () => { const root = await mkdtemp(path.join(tmpdir(), "release-candidate-")); try { 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"); for (const file of RELEASE_CANDIDATE_EVIDENCE_PATHS) { 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`, ); } const manifest = await createReleaseCandidateManifest(root); 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("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.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, securitySource] = await Promise.all([ readFile("scripts/generate-supply-chain.ts", "utf8"), readFile("scripts/security-scan.ts", "utf8"), ]); for (const source of [provenanceSource, securitySource]) { 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); }); });