import { spawnSync } from "node:child_process"; import { createHash, generateKeyPairSync, sign } from "node:crypto"; import { link, mkdir, mkdtemp, open, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { afterEach, describe, expect, it } from "vitest"; import type { CiGateArtifact, CiGateArtifactSchema, } from "../../scripts/contracts/ci-gates.ts"; import { readBoundedRegularFile, validateCiArtifact } from "../../scripts/lib/ci-artifact-validator.ts"; import { writeCiGateLogAtomic } from "../../scripts/lib/ci-gate-log.ts"; import { verifyCiCandidateArchive } from "../../scripts/lib/ci-candidate-archive.ts"; import { CANDIDATE_ARCHIVE_USAGE, parseCandidateArchiveArguments, } from "../../scripts/lib/ci-candidate-archive-cli.ts"; import { validateProviderUpload } from "../../scripts/lib/provider-upload-validator.ts"; import { PROMOTED_STAGING_PATHS, stageVerifiedPromotion, } from "../../scripts/lib/promotion-stager.ts"; import { providerEvidenceSignaturePayload, providerVerificationArtifactSchema, } from "../../scripts/lib/provider-evidence.ts"; import { createReleaseCandidateManifest, RELEASE_CANDIDATE_EVIDENCE_PATHS, RELEASE_CANDIDATE_MANIFEST_PATH, } from "../../scripts/lib/release-candidate.ts"; const temporaryRoots: string[] = []; const sha256 = (value: Buffer | string) => createHash("sha256").update(value).digest("hex"); afterEach(async () => { await Promise.all( temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })), ); }); async function temporaryRoot(prefix: string): Promise { const root = await mkdtemp(path.join(tmpdir(), prefix)); temporaryRoots.push(root); return root; } async function writeArtifact(root: string, relative: string, value: string | Buffer) { await mkdir(path.dirname(path.join(root, relative)), { recursive: true }); await writeFile(path.join(root, relative), value); } function artifact(pathname: string, schemaId: string): CiGateArtifact { return { id: `artifact-${schemaId}`, path: pathname, schemaId, production: "source-controlled", }; } describe("CI artifact validator", () => { it("does not create directories through a pre-existing log ancestor symlink", async () => { const root = await temporaryRoot("ci-log-root-"); const outside = await temporaryRoot("ci-log-outside-"); await symlink(outside, path.join(root, "linked")); await expect( writeCiGateLogAtomic({ root, relativePath: "linked/new/report.txt", content: "blocked\n", }), ).rejects.toThrow(/ancestor is unsafe/i); await expect( import("node:fs/promises").then(({ lstat }) => lstat(path.join(outside, "new"))), ).rejects.toMatchObject({ code: "ENOENT" }); }); it.each([ ["report.txt", { id: "text", kind: "text", maxBytes: 1_024 }, "gate output\n"], ["report.xml", { id: "junit", kind: "junit", maxBytes: 1_024 }, '\n'], ["report.html", { id: "html", kind: "html", maxBytes: 1_024 }, "\n"], ["report.md", { id: "markdown", kind: "markdown", maxBytes: 1_024 }, "# Review\n"], ["schema.json", { id: "json-schema", kind: "json-schema", maxBytes: 1_024 }, '{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object"}\n'], ["scan.sarif", { id: "sarif", kind: "sarif", maxBytes: 4_096 }, JSON.stringify({ version: "2.1.0", $schema: "https://json.schemastore.org/sarif-2.1.0.json", runs: [{ tool: { driver: { name: "ca-frontend-secret-scan", rules: [] } }, results: [] }] })], ] as const)("accepts a valid %s artifact", async (relative, schema, content) => { const root = await temporaryRoot("ci-artifact-kind-"); await writeArtifact(root, relative, content); await expect( validateCiArtifact({ root, artifact: artifact(relative, schema.id), schema: schema as CiGateArtifactSchema, }), ).resolves.toBeUndefined(); }); it("accepts schema-valid negative evidence without treating status as command authority", async () => { const root = await temporaryRoot("ci-artifact-negative-"); const relative = "negative.json"; await writeArtifact( root, relative, `${JSON.stringify({ schemaVersion: 2, sourceRoot: "src", status: "FAIL", facts: { scannedFiles: 0, visualBaselines: 0, sharedScenarios: 0, declaredScenarioExecutions: 0, executedScenarioExecutions: 0 }, failures: ["fixture"] })}\n`, ); await expect( validateCiArtifact({ root, artifact: artifact(relative, "test-evidence"), schema: { id: "test-evidence", kind: "json", maxBytes: 4_096, executableSchemaId: "test-evidence-report", }, }), ).resolves.toBeUndefined(); }); const invalidFixtures: ReadonlyArray< readonly [string, (root: string) => Promise, RegExp] > = [ ["missing", async (_root: string): Promise => undefined, /not a regular file|ENOENT/i], ["empty", async (root: string): Promise => { await writeArtifact(root, "report.json", ""); }, /size is outside/i], ["directory", async (root: string): Promise => { await mkdir(path.join(root, "report.json")); }, /not a regular file/i], ["oversized", async (root: string): Promise => { await writeArtifact(root, "report.json", "12345"); }, /size is outside/i], ["invalid UTF-8", async (root: string): Promise => { await writeArtifact(root, "report.json", Buffer.from([0xc3, 0x28])); }, /encoded data was not valid|UTF-8/i], ["primitive JSON", async (root: string): Promise => { await writeArtifact(root, "report.json", "1\n"); }, /record|object/i], ["array JSON", async (root: string): Promise => { await writeArtifact(root, "report.json", "[]\n"); }, /record|object/i], ]; it.each(invalidFixtures)("rejects %s artifacts", async (_name, setup, diagnostic) => { const root = await temporaryRoot("ci-artifact-invalid-"); await setup(root); await expect( validateCiArtifact({ root, artifact: artifact("report.json", "generic"), schema: { id: "generic", kind: "json", maxBytes: _name === "oversized" ? 4 : 4_096, executableSchemaId: "generic-json-object", }, }), ).rejects.toThrow(diagnostic); }); it("rejects leaf and ancestor symlinks before opening evidence", async () => { const root = await temporaryRoot("ci-artifact-symlink-"); await writeArtifact(root, "real.json", "{\"ok\":true}\n"); await symlink("real.json", path.join(root, "leaf.json")); await mkdir(path.join(root, "real-directory")); await writeArtifact(root, "real-directory/report.json", "{\"ok\":true}\n"); await symlink("real-directory", path.join(root, "linked-directory")); const schema = { id: "generic", kind: "json", maxBytes: 4_096, executableSchemaId: "generic-json-object", } as const; await expect( validateCiArtifact({ root, artifact: artifact("leaf.json", "generic"), schema }), ).rejects.toThrow(/not a regular file/i); await expect( validateCiArtifact({ root, artifact: artifact("linked-directory/report.json", "generic"), schema, }), ).rejects.toThrow(/ancestor is unsafe/i); }); it("rejects strict JSON evidence with unknown fields", async () => { const root = await temporaryRoot("ci-artifact-strict-"); await writeArtifact( root, "report.json", `${JSON.stringify({ schemaVersion: 2, nodeVersion: "24.14.0", gateCount: 26, commandDefinitionCount: 81, commandReferenceCount: 93, artifactCount: 105, jobCount: 9, workflowSha256: "a".repeat(64), durationStatus: "UNSUPPORTED", negativeFixtures: [], failures: [], passed: true, unknown: true })}\n`, ); await expect( validateCiArtifact({ root, artifact: artifact("report.json", "ci-contract"), schema: { id: "ci-contract", kind: "json", maxBytes: 8_192, executableSchemaId: "ci-contract-report", }, }), ).rejects.toThrow(/unrecognized|unknown/i); }); it.each([ ["broken.xml", { id: "junit", kind: "junit", maxBytes: 1_024 }, ""], ["mismatched.xml", { id: "junit", kind: "junit", maxBytes: 1_024 }, ""], ["trailing.xml", { id: "junit", kind: "junit", maxBytes: 1_024 }, "garbage"], ["doctype.xml", { id: "junit", kind: "junit", maxBytes: 1_024 }, "]>"], ["broken.html", { id: "html", kind: "html", maxBytes: 1_024 }, ""], ] as const)("rejects structurally incomplete %s", async (relative, schema, content) => { const root = await temporaryRoot("ci-artifact-structure-"); await writeArtifact(root, relative, content); await expect( validateCiArtifact({ root, artifact: artifact(relative, schema.id), schema: schema as CiGateArtifactSchema, }), ).rejects.toThrow(/invalid (?:JUnit|HTML) artifact/u); }); it("fails closed when a regular artifact grows after its bounded lstat", async () => { const root = await temporaryRoot("ci-artifact-growth-"); await writeArtifact(root, "report.txt", "1234"); const realHandle = await open(path.join(root, "report.txt"), "r"); await expect( readBoundedRegularFile( { root, relativePath: "report.txt", maxBytes: 4 }, { openFile: async () => ({ stat: async () => realHandle.stat(), read: async (buffer, offset) => { Buffer.from("12345").copy(buffer, offset); return { bytesRead: 5 }; }, close: async () => realHandle.close(), }), }, ), ).rejects.toThrow(/changed size or exceeds bound/i); }); it("rejects cross-field tampering in risk coverage evidence", async () => { const root = await temporaryRoot("ci-artifact-risk-"); const risk = { schemaVersion: 3, policy: "config/testing/risk-coverage.json", summary: "artifacts/tests/coverage/coverage-summary.json", status: "PASS", selectedTotal: 2, repositoryTotal: 2, counterBearingTotal: 1, instrumentedCounterBearingTotal: 1, counterlessTotal: 1, counterlessModules: ["src/types.ts"], preExclusionTotal: 2, generatedExclusionCount: 0, generatedExclusions: [], ownershipScope: "ALL_POLICY_HIGH_RISK", ownedHighRiskPaths: ["src/runtime.ts"], waivedHighRiskPaths: [], uncoveredModules: [], results: ["lines", "statements", "functions", "branches"].map((metric) => ({ scope: "summary", metric, threshold: 80, received: 90, passed: true, })), failures: [], }; const schema = { id: "risk", kind: "json", maxBytes: 16_384, executableSchemaId: "risk-coverage-v3", } as const; await writeArtifact(root, "risk.json", `${JSON.stringify(risk)}\n`); await expect( validateCiArtifact({ root, artifact: artifact("risk.json", "risk"), schema }), ).resolves.toBeUndefined(); await writeArtifact( root, "risk.json", `${JSON.stringify({ ...risk, counterlessTotal: 0 })}\n`, ); await expect( validateCiArtifact({ root, artifact: artifact("risk.json", "risk"), schema }), ).rejects.toThrow(/counter partition|counterless list length/u); for (const [mutation, diagnostic] of [ [{ preExclusionTotal: 3 }, /pre-exclusion inventory total drift/u], [{ status: "PASS", failures: [], results: risk.results.map((entry, index) => index === 0 ? { ...entry, received: 70, passed: false } : entry) }, /status must agree with failures and threshold results/u], [{ waivedHighRiskPaths: ["src/runtime.ts"] }, /owned and waived high-risk paths overlap/u], [{ results: risk.results.slice(0, 3) }, /all four metrics/u], [{ results: [...risk.results, risk.results[0]] }, /duplicated within scope/u], [{ results: [] }, /too small|at least 4/iu], ] as const) { await writeArtifact(root, "risk.json", `${JSON.stringify({ ...risk, ...mutation })}\n`); await expect( validateCiArtifact({ root, artifact: artifact("risk.json", "risk"), schema }), ).rejects.toThrow(diagnostic); } }); it("rejects coverage counters whose covered and skipped partitions exceed total", async () => { const root = await temporaryRoot("ci-artifact-coverage-"); const counter = { total: 10, covered: 8, skipped: 3, pct: 80 }; await writeArtifact( root, "coverage.json", `${JSON.stringify({ total: { lines: counter, statements: counter, functions: counter, branches: counter } })}\n`, ); await expect( validateCiArtifact({ root, artifact: artifact("coverage.json", "coverage"), schema: { id: "coverage", kind: "json", maxBytes: 4_096, executableSchemaId: "coverage-summary-v8", }, }), ).rejects.toThrow(/coverage counter exceeds total/u); }); }); describe("candidate archive and provider upload boundaries", () => { it("accepts only the manifest-bound candidate member set and bytes", async () => { const fixture = await createCandidateArchiveFixture(); await expect( verifyCiCandidateArchive({ archivePath: fixture.archivePath }), ).resolves.toEqual( expect.objectContaining({ archiveSha256: sha256(await readFile(fixture.archivePath)) }), ); }); it("rejects an extra candidate member before extraction", async () => { const fixture = await createCandidateArchiveFixture({ extraMember: true }); await expect( verifyCiCandidateArchive({ archivePath: fixture.archivePath }), ).rejects.toThrow(/exact member set drift before extraction/i); }); it("rejects duplicate archive members before extraction", async () => { const fixture = await createCandidateArchiveFixture({ duplicateMember: true }); await expect(verifyCiCandidateArchive({ archivePath: fixture.archivePath })) .rejects.toThrow(/duplicate member/i); }); it.each(["symlink", "hardlink"] as const)("rejects a %s archive member without touching an outside canary", async (kind) => { const root = await temporaryRoot(`ci-candidate-${kind}-`); const outside = await temporaryRoot(`ci-candidate-${kind}-outside-`); const canary = path.join(outside, "canary"); await writeFile(canary, "unchanged\n"); await writeArtifact(root, "target", "target\n"); if (kind === "symlink") await symlink("target", path.join(root, "unsafe")); else await link(path.join(root, "target"), path.join(root, "unsafe")); const archivePath = path.join(root, "unsafe.tar.gz"); const tar = spawnSync("/usr/bin/tar", ["-czf", archivePath, ...(kind === "hardlink" ? ["target"] : []), "unsafe"], { cwd: root, encoding: "utf8" }); if (tar.status !== 0) throw new Error(tar.stderr); await expect(verifyCiCandidateArchive({ archivePath })).rejects.toThrow(/non-regular member/i); await expect(readFile(canary, "utf8")).resolves.toBe("unchanged\n"); }); it("rejects traversal members and preserves the outside canary", async () => { const root = await temporaryRoot("ci-candidate-traversal-"); const canary = path.join(root, "outside-canary"); await writeArtifact(root, "safe", "safe\n"); await writeFile(canary, "unchanged\n"); const archivePath = path.join(root, "traversal.tar.gz"); const tar = spawnSync("/usr/bin/tar", ["-czf", archivePath, "--transform=s|safe|../outside-canary|", "safe"], { cwd: root, encoding: "utf8" }); if (tar.status !== 0) throw new Error(tar.stderr); await expect(verifyCiCandidateArchive({ archivePath })).rejects.toThrow(/unsafe member path/i); await expect(readFile(canary, "utf8")).resolves.toBe("unchanged\n"); }); it("rejects an oversized manifest from tar headers before full extraction", async () => { const fixture = await createCandidateArchiveFixture({ oversizedManifest: true }); await expect(verifyCiCandidateArchive({ archivePath: fixture.archivePath })) .rejects.toThrow(/manifest exceeds 8388608 bytes/i); }); it("rejects an expanded-byte bomb before extraction and preserves its canary", async () => { const root = await temporaryRoot("ci-candidate-expanded-bomb-"); const huge = path.join(root, "huge.bin"); const handle = await open(huge, "w"); await handle.truncate(268_435_457); await handle.close(); const canary = path.join(root, "canary"); await writeFile(canary, "unchanged\n"); const archivePath = path.join(root, "bomb.tar.gz"); const tar = spawnSync("/usr/bin/tar", ["-czf", archivePath, "huge.bin"], { cwd: root, encoding: "utf8", timeout: 30_000, }); if (tar.status !== 0) throw new Error(tar.stderr || String(tar.error)); await expect(verifyCiCandidateArchive({ archivePath })) .rejects.toThrow(/expanded bytes exceed the bound/i); await expect(readFile(canary, "utf8")).resolves.toBe("unchanged\n"); }, 40_000); it.each([ ["missing value", ["--archive"]], [ "option-like value", ["--archive", "missing.tar.gz", "--extract-to", "--github-output", "out"], ], ])("maps a %s to the deterministic CLI Usage result", (_label, arguments_) => { expect(parseCandidateArchiveArguments(arguments_)).toBeNull(); expect(CANDIDATE_ARCHIVE_USAGE).toBe( "Usage: verify-ci-candidate-archive --archive [--extract-to ] [--github-output ]\n", ); }); it("rejects archive digest mismatch and symlink substitution", async () => { const fixture = await createCandidateArchiveFixture(); await expect( verifyCiCandidateArchive({ archivePath: fixture.archivePath, expectedSha256: "0".repeat(64), }), ).rejects.toThrow(/SHA-256 mismatch/u); const linked = `${fixture.archivePath}.link`; await symlink(path.basename(fixture.archivePath), linked); await expect( verifyCiCandidateArchive({ archivePath: linked }), ).rejects.toThrow(/regular non-symlink/u); }); it("rejects an excessive archive member universe before per-member reads", async () => { const fixture = await createCandidateArchiveFixture({ repeatedExtraMembers: 8_200 }); await expect( verifyCiCandidateArchive({ archivePath: fixture.archivePath }), ).rejects.toThrow(/member count is outside 1\.\.8192|exceeds 8192 members/u); }); it("uses the captured archive inode when the pathname is replaced mid-verification", async () => { const original = await createCandidateArchiveFixture(); const replacement = await createCandidateArchiveFixture({ extraMember: true }); const originalArchiveSha256 = sha256(await readFile(original.archivePath)); const displaced = `${original.archivePath}.displaced`; const extractTo = path.join(path.dirname(original.archivePath), "verified-candidate"); await expect( verifyCiCandidateArchive( { archivePath: original.archivePath, extractTo, repositoryRoot: path.dirname(original.archivePath), }, { afterArchiveRead: async () => { await rename(original.archivePath, displaced); await rename(replacement.archivePath, original.archivePath); }, }, ), ).resolves.toEqual(expect.objectContaining({ archiveSha256: originalArchiveSha256 })); await expect(readFile(displaced)).resolves.toBeDefined(); await expect(readFile(path.join(extractTo, "dist/app.js"), "utf8")).resolves.toBe("app\n"); }); it("validates provider JSON against candidate dist and lockfile digests", async () => { const fixture = await createProviderFixture(); await expect( validateProviderUpload({ kind: "vulnerability", candidateRoot: fixture.candidateRoot, archivePath: fixture.archivePath, expectedArchiveSha256: fixture.archiveSha256, workspaceRoot: fixture.root, reportPath: fixture.reportPath, expectedDistSha256: fixture.distSha256, }), ).resolves.toEqual(expect.objectContaining({ provider: "fixture" })); const report = JSON.parse(await readFile(fixture.reportPath, "utf8")) as Record; report.scannedDistSha256 = "f".repeat(64); await writeFile(fixture.reportPath, `${JSON.stringify(report)}\n`); await expect( validateProviderUpload({ kind: "vulnerability", candidateRoot: fixture.candidateRoot, archivePath: fixture.archivePath, expectedArchiveSha256: fixture.archiveSha256, workspaceRoot: fixture.root, reportPath: fixture.reportPath, expectedDistSha256: fixture.distSha256, }), ).rejects.toThrow(/candidate digest mismatch/i); }); it("uses the reverified archive manifest when extracted candidate files are mutated", async () => { const fixture = await createProviderFixture(); await writeArtifact(fixture.candidateRoot, "dist/app.js", "mutated\n"); const mutableManifest = JSON.parse( await import("node:fs/promises").then(({ readFile }) => readFile(path.join(fixture.candidateRoot, RELEASE_CANDIDATE_MANIFEST_PATH), "utf8"), ), ) as Record; mutableManifest.distSha256 = "e".repeat(64); await writeFile( path.join(fixture.candidateRoot, RELEASE_CANDIDATE_MANIFEST_PATH), `${JSON.stringify(mutableManifest)}\n`, ); await expect( validateProviderUpload({ kind: "vulnerability", candidateRoot: fixture.candidateRoot, archivePath: fixture.archivePath, expectedArchiveSha256: fixture.archiveSha256, workspaceRoot: fixture.root, reportPath: fixture.reportPath, expectedDistSha256: fixture.distSha256, }), ).rejects.toThrow(/candidate root changed/i); }); it("rejects symlinked provider reports at the bounded file boundary", async () => { const fixture = await createProviderFixture(); const real = path.join(fixture.root, "real-report.json"); await writeFile(real, await readFile(fixture.reportPath)); await rm(fixture.reportPath); await symlink(path.relative(path.dirname(fixture.reportPath), real), fixture.reportPath); await expect( validateProviderUpload({ kind: "vulnerability", candidateRoot: fixture.candidateRoot, archivePath: fixture.archivePath, expectedArchiveSha256: fixture.archiveSha256, workspaceRoot: fixture.root, reportPath: fixture.reportPath, expectedDistSha256: fixture.distSha256, }), ).rejects.toThrow(/not a regular file/i); }); it("rejects oversized provider reports before JSON parsing", async () => { const fixture = await createProviderFixture(); await writeFile(fixture.reportPath, Buffer.alloc(8_388_609, 0x20)); await expect( validateProviderUpload({ kind: "vulnerability", candidateRoot: fixture.candidateRoot, archivePath: fixture.archivePath, expectedArchiveSha256: fixture.archiveSha256, workspaceRoot: fixture.root, reportPath: fixture.reportPath, expectedDistSha256: fixture.distSha256, }), ).rejects.toThrow(/size is outside/u); }); it("rejects a stale raw provider report before starting the provider", async () => { const fixture = await createProviderFixture(); const markerPath = path.join(fixture.root, "provider-started"); const result = runProviderSupervisor(fixture, { command: `node -e 'require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started")'`, sealedPath: path.join(fixture.root, "provider-evidence/vulnerability-report.json"), }); expect(result.status).not.toBe(0); expect(result.stderr).toMatch(/raw provider report already exists/i); await expect(readFile(markerPath)).rejects.toMatchObject({ code: "ENOENT" }); }); it("drains and kills provider background processes before sealing evidence", async () => { const fixture = await createProviderFixture(); const rawReport = await readFile(fixture.reportPath, "utf8"); await rm(fixture.reportPath); const sealedPath = path.join( fixture.root, "provider-evidence/vulnerability-report.json", ); const mutatorMarker = path.join(fixture.root, "background-mutator-ran"); const providerScript = path.join(fixture.root, "provider.mjs"); const mutator = [ "process.on('SIGTERM', () => {});", "setTimeout(() => {", ` require('node:fs').writeFileSync(${JSON.stringify(path.join(fixture.candidateRoot, "dist/app.js"))}, 'mutated\\n');`, ` require('node:fs').writeFileSync(${JSON.stringify(sealedPath)}, '{"mutated":true}\\n');`, ` require('node:fs').writeFileSync(${JSON.stringify(mutatorMarker)}, 'ran\\n');`, "}, 1200);", ].join("\n"); await writeFile( providerScript, [ "import { spawn } from 'node:child_process';", "import { writeFileSync } from 'node:fs';", `writeFileSync(${JSON.stringify(fixture.reportPath)}, ${JSON.stringify(rawReport)});`, `const child = spawn(process.execPath, ['-e', ${JSON.stringify(mutator)}], { stdio: 'ignore' });`, "child.unref();", ].join("\n"), ); const result = runProviderSupervisor(fixture, { command: `node ${JSON.stringify(providerScript)}`, sealedPath, }); expect(result.status, result.stderr).toBe(0); await delay(1_500); await expect(readFile(path.join(fixture.candidateRoot, "dist/app.js"), "utf8")).resolves.toBe("app\n"); await expect(readFile(mutatorMarker)).rejects.toMatchObject({ code: "ENOENT" }); expect(JSON.parse(await readFile(sealedPath, "utf8"))).toEqual( expect.objectContaining({ provider: "fixture" }), ); }, 10_000); it("does not expose or mutate a host path outside the sandboxed workspace", async () => { const fixture = await createProviderFixture(); const rawReport = await readFile(fixture.reportPath, "utf8"); await rm(fixture.reportPath); const outside = await temporaryRoot("provider-host-canary-"); const canary = path.join(outside, "secret-canary"); await writeFile(canary, "host-secret\n"); const providerScript = path.join(fixture.root, "provider-host-boundary.mjs"); await writeFile(providerScript, [ "import { readFileSync, writeFileSync } from 'node:fs';", `try { readFileSync(${JSON.stringify(canary)}); process.exit(9); } catch {}`, `try { writeFileSync(${JSON.stringify(canary)}, 'mutated\\n'); } catch {}`, `writeFileSync(${JSON.stringify(fixture.reportPath)}, ${JSON.stringify(rawReport)});`, ].join("\n")); const result = runProviderSupervisor(fixture, { command: `node ${JSON.stringify(providerScript)}`, sealedPath: path.join(fixture.root, "provider-evidence/vulnerability-report.json"), }); expect(result.status, result.stderr).toBe(0); await expect(readFile(canary, "utf8")).resolves.toBe("host-secret\n"); }, 10_000); }); describe("verified promotion staging", () => { it("publishes the exact five captured promotion inputs", async () => { const fixture = await createPromotionStagingFixture(); const staged = await stageVerifiedPromotion(fixture.input, fixture.dependencies); expect(staged.map(({ path: stagedPath }) => stagedPath)).toEqual(PROMOTED_STAGING_PATHS); for (const [stagedPath, expected] of fixture.expectedStagedBytes) { await expect(readFile(path.join(fixture.root, stagedPath))).resolves.toEqual(expected); expect(staged.find(({ path: candidate }) => candidate === stagedPath)?.sha256).toBe( sha256(expected), ); } expect(staged).toHaveLength(5); await writeFile(fixture.input.vulnerabilityReportPath, "mutated after capture\n"); await expect( readFile(path.join(fixture.root, ".release/promoted-staging/vulnerability-report.json")), ).resolves.toEqual( fixture.expectedStagedBytes.get( ".release/promoted-staging/vulnerability-report.json", ), ); }); it("rejects archive digest drift before publishing staging", async () => { const fixture = await createPromotionStagingFixture(); await expect( stageVerifiedPromotion({ ...fixture.input, expectedArchiveSha256: "0".repeat(64), }, fixture.dependencies), ).rejects.toThrow(/archive SHA-256/u); await expect( readFile(path.join(fixture.root, ".release/promoted-staging/release-candidate.tar.gz")), ).rejects.toMatchObject({ code: "ENOENT" }); }); it("rejects a byte-different signed report whose producer digest is stale", async () => { const fixture = await createPromotionStagingFixture(); const report = JSON.parse( await readFile(fixture.input.vulnerabilityReportPath, "utf8"), ) as unknown; const replacement = Buffer.from(`${JSON.stringify(report, null, 2)}\n`); await writeFile(fixture.input.vulnerabilityReportPath, replacement); await expect(stageVerifiedPromotion(fixture.input, fixture.dependencies)).rejects.toThrow( /vulnerabilityReportSha256 digest mismatch/i, ); }); it.each([ ["provider-verification.json", "promotion-verification.json"], ["promotion-verification.json", "provider-verification.json"], ] as const)("rejects %s copied into the %s role", async (sourceName, targetName) => { const fixture = await createPromotionStagingFixture(); const sourcePath = path.join(fixture.root, "artifacts/security", sourceName); const targetPath = path.join(fixture.root, "artifacts/security", targetName); await writeFile(targetPath, await readFile(sourcePath)); await expect(stageVerifiedPromotion(fixture.input, fixture.dependencies)).rejects.toThrow( /artifactType role mismatch/i, ); }); it("stages captured bytes and uses captured trust keys after source mutation", async () => { const fixture = await createPromotionStagingFixture(); const sourceMappings = [ [fixture.input.archivePath, ".release/promoted-staging/release-candidate.tar.gz"], [fixture.input.vulnerabilityReportPath, ".release/promoted-staging/vulnerability-report.json"], [fixture.input.provenanceAttestationPath, ".release/promoted-staging/provenance-attestation.json"], [path.join(fixture.root, "artifacts/security/provider-verification.json"), ".release/promoted-staging/provider-verification.json"], [path.join(fixture.root, "artifacts/security/promotion-verification.json"), ".release/promoted-staging/promotion-verification.json"], ] as const; const originalSources = new Map(await Promise.all( sourceMappings.map(async ([sourcePath]) => [sourcePath, await readFile(sourcePath)] as const), )); const staged = await stageVerifiedPromotion(fixture.input, { ...fixture.dependencies, afterCapture: async () => { await Promise.all([ writeFile(fixture.input.archivePath, "replaced archive\n"), writeFile(fixture.input.vulnerabilityReportPath, "replaced vulnerability\n"), writeFile(fixture.input.provenanceAttestationPath, "replaced provenance\n"), writeFile(path.join(fixture.root, "artifacts/security/provider-verification.json"), "replaced provider verification\n"), writeFile(path.join(fixture.root, "artifacts/security/promotion-verification.json"), "replaced promotion verification\n"), writeFile(fixture.input.vulnerabilityPublicKeyPath, "replaced key\n"), writeFile(fixture.input.provenancePublicKeyPath, "replaced key\n"), ]); }, }); for (const [sourcePath, stagedPath] of sourceMappings) { const original = originalSources.get(sourcePath)!; await expect(readFile(path.join(fixture.root, stagedPath))).resolves.toEqual(original); expect(staged.find(({ path: candidate }) => candidate === stagedPath)?.sha256) .toBe(sha256(original)); } }); it("reruns archived local evidence instead of trusting a pre-existing PASS JSON", async () => { const fixture = await createPromotionStagingFixture(); await expect(stageVerifiedPromotion(fixture.input)).rejects.toThrow( /captured local evidence failed final verification/i, ); }); it("rejects symlinked and oversized promotion sources", async () => { const linked = await createPromotionStagingFixture(); const realReport = path.join(linked.root, "real-vulnerability-report.json"); await writeFile(realReport, await readFile(linked.input.vulnerabilityReportPath)); await rm(linked.input.vulnerabilityReportPath); await symlink(realReport, linked.input.vulnerabilityReportPath); await expect(stageVerifiedPromotion(linked.input, linked.dependencies)).rejects.toThrow(/regular file/i); const oversized = await createPromotionStagingFixture(); await writeFile( oversized.input.vulnerabilityReportPath, Buffer.alloc(16_777_217, 0x20), ); await expect(stageVerifiedPromotion(oversized.input, oversized.dependencies)).rejects.toThrow(/size is outside/i); }); it.each(["directory", "symlink"] as const)( "rejects a pre-existing %s staging target", async (targetKind) => { const fixture = await createPromotionStagingFixture(); const target = path.join(fixture.root, ".release/promoted-staging"); if (targetKind === "directory") { await mkdir(target, { recursive: true }); } else { const outside = await temporaryRoot("promotion-staging-outside-"); await symlink(outside, target); } await expect(stageVerifiedPromotion(fixture.input, fixture.dependencies)).rejects.toThrow( /publish leaf is unsafe|target already exists/i, ); }, ); it("rejects a .release ancestor symlink introduced after capture", async () => { const fixture = await createPromotionStagingFixture(); const original = path.join(fixture.root, ".release-original"); const outside = await temporaryRoot("promotion-release-symlink-"); await expect( stageVerifiedPromotion(fixture.input, { ...fixture.dependencies, afterCapture: async () => { await rename(path.join(fixture.root, ".release"), original); await symlink(outside, path.join(fixture.root, ".release")); }, }), ).rejects.toThrow(/publish ancestor is unsafe|publish directory/u); await expect(readdir(outside)).resolves.toEqual([]); }); it("detects a staging parent identity swap and cleans its owned temporary", async () => { const fixture = await createPromotionStagingFixture(); const displaced = path.join(fixture.root, ".release-displaced"); const dependencies = { ...fixture.dependencies, beforePublishRename: async () => { await rename(path.join(fixture.root, ".release"), displaced); await mkdir(path.join(fixture.root, ".release")); }, } as Parameters[1]; await expect(stageVerifiedPromotion(fixture.input, dependencies)).rejects.toThrow( /parent identity changed/u, ); expect((await readdir(displaced)).filter((entry) => entry.startsWith(".promoted-staging."))).toEqual([]); }); }); function runProviderSupervisor( fixture: Awaited>, input: Readonly<{ command: string; sealedPath: string }>, ) { return spawnSync( process.execPath, [path.resolve("scripts/run-and-validate-provider.ts"), "--kind", "vulnerability"], { cwd: fixture.root, encoding: "utf8", timeout: 8_000, env: { ...process.env, VULNERABILITY_PROVIDER_COMMAND: input.command, VULNERABILITY_REPORT_PATH: fixture.reportPath, VALIDATED_PROVIDER_REPORT_PATH: input.sealedPath, CANDIDATE_LOCKFILE_PATH: path.join(fixture.candidateRoot, "pnpm-lock.yaml"), CANDIDATE_ARCHIVE_PATH: fixture.archivePath, CANDIDATE_ARCHIVE_SHA256: fixture.archiveSha256, CANDIDATE_DIST_SHA256: fixture.distSha256, }, }, ); } async function createCandidateArchiveFixture( options: Readonly<{ extraMember?: boolean; repeatedExtraMembers?: number; duplicateMember?: boolean; oversizedManifest?: boolean }> = {}, ): Promise> { const root = await temporaryRoot("ci-candidate-archive-"); const files = new Map(); files.set("dist/app.js", Buffer.from("app\n")); for (const evidencePath of RELEASE_CANDIDATE_EVIDENCE_PATHS) { files.set(evidencePath, Buffer.from(`${evidencePath}\n`)); } for (const [relative, content] of files) await writeArtifact(root, relative, content); await writeArtifact( root, "artifacts/release/dependency-inventory.json", `${JSON.stringify({ lockfileSha256: sha256(files.get("pnpm-lock.yaml")!) })}\n`, ); const manifest = await createReleaseCandidateManifest(root); await writeArtifact( root, RELEASE_CANDIDATE_MANIFEST_PATH, `${JSON.stringify(manifest)}${options.oversizedManifest ? " ".repeat(8_388_609) : "\n"}`, ); if (options.extraMember || options.repeatedExtraMembers) { await writeArtifact(root, "extra.txt", "extra\n"); } const archivePath = path.join(root, "candidate.tar.gz"); const members = [ "dist", ...RELEASE_CANDIDATE_EVIDENCE_PATHS, RELEASE_CANDIDATE_MANIFEST_PATH, ...(options.duplicateMember ? ["pnpm-lock.yaml"] : []), ...(options.extraMember ? ["extra.txt"] : []), ...Array.from({ length: options.repeatedExtraMembers ?? 0 }, () => "extra.txt"), ]; const tar = spawnSync("tar", [...(options.duplicateMember ? ["--hard-dereference"] : []), "-czf", archivePath, ...members], { cwd: root, encoding: "utf8", }); if (tar.status !== 0) throw new Error(tar.stderr); return { root, archivePath }; } async function createProviderFixture() { const root = await temporaryRoot("ci-provider-upload-"); const candidateRoot = path.join(root, "candidate"); const lockfile = Buffer.from("lockfileVersion: '9.0'\n"); const lockfileSha256 = sha256(lockfile); await writeArtifact(candidateRoot, "pnpm-lock.yaml", lockfile); await writeArtifact(candidateRoot, "dist/app.js", "app\n"); for (const evidencePath of RELEASE_CANDIDATE_EVIDENCE_PATHS) { if (evidencePath === "pnpm-lock.yaml") continue; const content = evidencePath === "artifacts/release/dependency-inventory.json" ? `${JSON.stringify({ lockfileSha256 })}\n` : `${evidencePath}\n`; await writeArtifact(candidateRoot, evidencePath, content); } const candidate = await createReleaseCandidateManifest(candidateRoot); await writeArtifact(candidateRoot, RELEASE_CANDIDATE_MANIFEST_PATH, `${JSON.stringify(candidate)}\n`); const distSha256 = candidate.distSha256; const archivePath = path.join(root, "candidate.tar.gz"); const tar = spawnSync( "tar", ["-czf", archivePath, "dist", ...RELEASE_CANDIDATE_EVIDENCE_PATHS, RELEASE_CANDIDATE_MANIFEST_PATH], { cwd: candidateRoot, encoding: "utf8" }, ); if (tar.status !== 0) throw new Error(tar.stderr); const archiveSha256 = sha256( await import("node:fs/promises").then(({ readFile }) => readFile(archivePath)), ); const reportPath = path.join( root, "provider-evidence/untrusted/vulnerability-report.json", ); await writeArtifact( root, "provider-evidence/untrusted/vulnerability-report.json", `${JSON.stringify({ schemaVersion: 1, provider: "fixture", generatedAt: "2026-08-02T00:00:00.000Z", scannedLockfileSha256: lockfileSha256, scannedDistSha256: distSha256, findings: [], signature: { algorithm: "Ed25519", keyId: "fixture", value: "AA==" } })}\n`, ); return { root, candidateRoot, archivePath, archiveSha256, reportPath, distSha256 }; } async function createPromotionStagingFixture() { const candidateFixture = await createCandidateArchiveFixture(); const root = path.dirname(candidateFixture.archivePath); const candidate = await verifyCiCandidateArchive({ archivePath: candidateFixture.archivePath, }); const vulnerabilityKeys = generateKeyPairSync("ed25519"); const provenanceKeys = generateKeyPairSync("ed25519"); const vulnerabilityKeyId = "fixture-vulnerability"; const provenanceKeyId = "fixture-provenance"; const vulnerabilityUnsigned = { schemaVersion: 1 as const, provider: "fixture-vulnerability", generatedAt: "2026-08-02T00:00:00.000Z", scannedLockfileSha256: candidate.manifest.lockfileSha256, scannedDistSha256: candidate.manifest.distSha256, findings: [], }; const vulnerabilityReport = { ...vulnerabilityUnsigned, signature: { algorithm: "Ed25519" as const, keyId: vulnerabilityKeyId, value: sign( null, providerEvidenceSignaturePayload(vulnerabilityUnsigned), vulnerabilityKeys.privateKey, ).toString("base64"), }, }; const provenanceUnsigned = { schemaVersion: 1 as const, provider: "fixture-provenance", signer: "fixture-signer", generatedAt: "2026-08-02T00:00:00.000Z", subject: { name: "dist" as const, digest: { sha256: candidate.manifest.distSha256 }, }, }; const provenanceAttestation = { ...provenanceUnsigned, signature: { algorithm: "Ed25519" as const, keyId: provenanceKeyId, value: sign( null, providerEvidenceSignaturePayload(provenanceUnsigned), provenanceKeys.privateKey, ).toString("base64"), }, }; const candidateArchiveBytes = await readFile(candidateFixture.archivePath); const vulnerabilityReportBytes = Buffer.from(`${JSON.stringify(vulnerabilityReport)}\n`); const provenanceAttestationBytes = Buffer.from(`${JSON.stringify(provenanceAttestation)}\n`); const verificationBindings = { candidateArchiveSha256: sha256(candidateArchiveBytes), vulnerabilityReportSha256: sha256(vulnerabilityReportBytes), provenanceAttestationSha256: sha256(provenanceAttestationBytes), }; const verificationBase = { schemaVersion: 2 as const, status: "PASS" as const, vulnerabilityStatus: "PASS" as const, provenanceAttestationStatus: "PASS" as const, lockfileSha256: candidate.manifest.lockfileSha256, distSha256: candidate.manifest.distSha256, ...verificationBindings, failures: [], }; const providerVerificationBytes = Buffer.from(`${JSON.stringify({ ...verificationBase, artifactType: "provider-verification", })}\n`); const promotionVerificationBytes = Buffer.from(`${JSON.stringify({ ...verificationBase, artifactType: "promotion-verification", })}\n`); const vulnerabilityReportPath = path.join( root, ".release/vulnerability/vulnerability-report.json", ); const provenanceAttestationPath = path.join( root, ".release/provenance/provenance-attestation.json", ); const vulnerabilityPublicKeyPath = path.join(root, "keys/vulnerability.pem"); const provenancePublicKeyPath = path.join(root, "keys/provenance.pem"); await writeArtifact( root, ".release/vulnerability/vulnerability-report.json", vulnerabilityReportBytes, ); await writeArtifact( root, ".release/provenance/provenance-attestation.json", provenanceAttestationBytes, ); await writeArtifact( root, "keys/vulnerability.pem", vulnerabilityKeys.publicKey.export({ type: "spki", format: "pem" }), ); await writeArtifact( root, "keys/provenance.pem", provenanceKeys.publicKey.export({ type: "spki", format: "pem" }), ); await writeArtifact( root, "artifacts/security/provider-verification.json", providerVerificationBytes, ); await writeArtifact( root, "artifacts/security/promotion-verification.json", promotionVerificationBytes, ); const expectedStagedBytes = new Map([ [ ".release/promoted-staging/release-candidate.tar.gz", candidateArchiveBytes, ], [ ".release/promoted-staging/vulnerability-report.json", await readFile(vulnerabilityReportPath), ], [ ".release/promoted-staging/provenance-attestation.json", await readFile(provenanceAttestationPath), ], [ ".release/promoted-staging/provider-verification.json", await readFile(path.join(root, "artifacts/security/provider-verification.json")), ], [ ".release/promoted-staging/promotion-verification.json", await readFile(path.join(root, "artifacts/security/promotion-verification.json")), ], ]); return { root, dependencies: { verifyLocalEvidence: async () => ({ status: "PASS" as const, failures: [] }), }, expectedStagedBytes, input: { repositoryRoot: root, archivePath: candidateFixture.archivePath, expectedArchiveSha256: sha256(await readFile(candidateFixture.archivePath)), vulnerabilityReportPath, provenanceAttestationPath, vulnerabilityPublicKeyPath, vulnerabilityKeyId, provenancePublicKeyPath, provenanceKeyId, }, }; }