Files
clean-architecture-frontend…/tests/unit/ci-artifact-contract.test.ts
T

1246 lines
53 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { createHash, generateKeyPairSync, sign } from "node:crypto";
import { cp, link, lstat, 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 { afterAll, 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 { captureCiCandidateArchive, 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 {
cleanupFinalizedPromotion,
stageVerifiedPromotion,
} from "../../scripts/lib/promotion-stager.ts";
import { PROMOTED_FILE_NAMES } from "../../scripts/contracts/promotion-artifacts.ts";
import {
providerEvidenceSignaturePayload,
providerPublicKeyFingerprint,
providerVerificationArtifactSchema,
} from "../../scripts/lib/provider-evidence.ts";
import { localEvidenceAssessmentArtifactSchema } from "../../scripts/contracts/release-artifacts.ts";
import { readProviderTrust } from "../../scripts/lib/promotion-verifier.ts";
import {
createReleaseCandidateManifest,
LOCAL_EVIDENCE_ASSESSMENT_PATH,
RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
} from "../../scripts/lib/release-candidate.ts";
const temporaryRoots: string[] = [];
let providerBaseRoot: string | undefined;
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 })),
);
});
afterAll(async () => {
if (providerBaseRoot) {
await rm(providerBaseRoot, { recursive: true, force: true });
}
});
async function temporaryRoot(prefix: string): Promise<string> {
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 }, '<testsuite name="one"/>\n'],
["report.html", { id: "html", kind: "html", maxBytes: 1_024 }, "<!doctype html><html></html>\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<void>, RegExp]
> = [
["missing", async (_root: string): Promise<void> => undefined, /not a regular file|ENOENT/i],
["empty", async (root: string): Promise<void> => { await writeArtifact(root, "report.json", ""); }, /size is outside/i],
["directory", async (root: string): Promise<void> => { await mkdir(path.join(root, "report.json")); }, /not a regular file/i],
["oversized", async (root: string): Promise<void> => { await writeArtifact(root, "report.json", "12345"); }, /size is outside/i],
["invalid UTF-8", async (root: string): Promise<void> => { await writeArtifact(root, "report.json", Buffer.from([0xc3, 0x28])); }, /encoded data was not valid|UTF-8/i],
["primitive JSON", async (root: string): Promise<void> => { await writeArtifact(root, "report.json", "1\n"); }, /record|object/i],
["array JSON", async (root: string): Promise<void> => { 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 }, "<testsuite>"],
["mismatched.xml", { id: "junit", kind: "junit", maxBytes: 1_024 }, "<testsuites><testsuite></testsuites></testsuite>"],
["trailing.xml", { id: "junit", kind: "junit", maxBytes: 1_024 }, "<testsuite/>garbage"],
["doctype.xml", { id: "junit", kind: "junit", maxBytes: 1_024 }, "<!DOCTYPE testsuite [<!ENTITY x SYSTEM 'file:///etc/passwd'>]><testsuite/>"],
["broken.html", { id: "html", kind: "html", maxBytes: 1_024 }, "<!doctype html><html>"],
] 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 <path> [--extract-to <path>] [--github-output <path>]\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(providerValidationInput(fixture)),
).resolves.toEqual(expect.objectContaining({ provider: "fixture" }));
const report = JSON.parse(await readFile(fixture.reportPath, "utf8")) as Record<string, any>;
report.candidate.distSha256 = "f".repeat(64);
report.signature.value = sign(
null,
providerEvidenceSignaturePayload(report),
fixture.privateKey,
).toString("base64");
await expect(
validateProviderUpload(
providerValidationInput(fixture, Buffer.from(`${JSON.stringify(report)}\n`)),
),
).rejects.toThrow(/candidate identity mismatch/i);
}, 30_000);
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<string, unknown>;
mutableManifest.distSha256 = "e".repeat(64);
await writeFile(
path.join(fixture.candidateRoot, RELEASE_CANDIDATE_MANIFEST_PATH),
`${JSON.stringify(mutableManifest)}\n`,
);
await expect(
validateProviderUpload(providerValidationInput(fixture)),
).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(
readBoundedRegularFile({
root: fixture.root,
relativePath: path.relative(fixture.root, fixture.reportPath),
maxBytes: 8_388_608,
}),
).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(
readBoundedRegularFile({
root: fixture.root,
relativePath: path.relative(fixture.root, fixture.reportPath),
maxBytes: 8_388_608,
}),
).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();
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(fixture.protectedCandidatePath)}, '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';",
providerV2WriterSource(),
`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(fixture.protectedCandidatePath)).resolves.toEqual(
fixture.protectedCandidateBytes,
);
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();
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 {}`,
providerV2WriterSource({ importFs: false }),
].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 finalizer", () => {
it("creates no input records and publishes deterministic exact-five strict v3 bindings", async () => {
const fixture = await createPromotionStagingFixture();
let archiveCaptureCount = 0;
await expect(readFile(path.join(fixture.root, "artifacts/security/provider-verification.json")))
.rejects.toMatchObject({ code: "ENOENT" });
await expect(readFile(path.join(fixture.root, "artifacts/security/promotion-verification.json")))
.rejects.toMatchObject({ code: "ENOENT" });
const dependencies = {
...fixture.dependencies,
captureArchive: async (input: Parameters<typeof captureCiCandidateArchive>[0]) => {
archiveCaptureCount += 1;
return captureCiCandidateArchive(input);
},
};
const finalized = await stageVerifiedPromotion(fixture.input, dependencies);
expect(archiveCaptureCount).toBe(1);
expect(finalized.files.map(({ name }) => name)).toEqual(PROMOTED_FILE_NAMES);
expect((await readdir(finalized.stagingRoot)).sort()).toEqual([...PROMOTED_FILE_NAMES].sort());
expect(finalized.stagingRoot).toBe(path.join(fixture.runnerTempRoot, fixture.cleanupToken));
expect(finalized.stagingRoot).not.toContain(".release/promoted-staging");
expect((await lstat(finalized.stagingRoot)).mode & 0o777).toBe(0o700);
for (const file of finalized.files) {
expect((await lstat(path.join(finalized.stagingRoot, file.name))).mode & 0o777).toBe(0o400);
expect(file.sha256).toBe(sha256(await readFile(path.join(finalized.stagingRoot, file.name))));
}
const providerBytes = await readFile(path.join(finalized.stagingRoot, "provider-verification.json"));
const promotionBytes = await readFile(path.join(finalized.stagingRoot, "promotion-verification.json"));
const provider = providerVerificationArtifactSchema.parse(JSON.parse(providerBytes.toString("utf8")));
const promotion = providerVerificationArtifactSchema.parse(JSON.parse(promotionBytes.toString("utf8")));
expect(provider.artifactType).toBe("provider-verification");
expect(promotion.artifactType).toBe("promotion-verification");
if (provider.artifactType !== "provider-verification" || promotion.artifactType !== "promotion-verification") {
throw new Error("verification record role narrowing failed");
}
expect(provider.status).toBe("PASS");
expect(promotion.status).toBe("PASS");
expect(promotion.providerVerificationSha256).toBe(sha256(providerBytes));
expect(promotion.localEvidenceAssessmentSha256).toBe(fixture.assessmentSha256);
expect(promotion.run).toEqual(fixture.expectedContext.run);
expect(promotion.source).toEqual(fixture.expectedContext.source);
expect(promotion.candidate).toEqual(fixture.expectedContext.candidate);
expect(promotion.providerEvidence).toEqual(provider.providerEvidence);
expect(promotion.providerEvidence).toEqual(fixture.expectedProviderEvidence);
expect(promotion.trustPolicySha256).toBe(provider.trustPolicySha256);
expect(provider.vulnerabilityStatus).toBe("PASS");
expect(provider.provenanceAttestationStatus).toBe("PASS");
await cleanupFinalizedPromotion(finalizedCleanup(fixture, finalized));
const repeated = await stageVerifiedPromotion(fixture.input, dependencies);
expect(archiveCaptureCount).toBe(2);
await expect(readFile(path.join(repeated.stagingRoot, "provider-verification.json")))
.resolves.toEqual(providerBytes);
await expect(readFile(path.join(repeated.stagingRoot, "promotion-verification.json")))
.resolves.toEqual(promotionBytes);
await cleanupFinalizedPromotion(finalizedCleanup(fixture, repeated));
}, 30_000);
it("stages captured archive and reports and validates captured keys after source mutation", async () => {
const fixture = await createPromotionStagingFixture();
const finalized = 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(fixture.input.vulnerabilityPublicKeyPath, "replaced vulnerability key\n"),
writeFile(fixture.input.provenancePublicKeyPath, "replaced provenance key\n"),
]);
},
});
for (const [name, bytes] of fixture.capturedSources) {
await expect(readFile(path.join(finalized.stagingRoot, name))).resolves.toEqual(bytes);
}
await cleanupFinalizedPromotion(finalizedCleanup(fixture, finalized));
}, 30_000);
it.each([
["archive digest", async (fixture: Awaited<ReturnType<typeof createPromotionStagingFixture>>) => {
fixture.input = { ...fixture.input, expectedArchiveSha256: "0".repeat(64) };
}, /archive SHA-256/u],
["report mutation", async (fixture: Awaited<ReturnType<typeof createPromotionStagingFixture>>) => {
await writeFile(fixture.input.vulnerabilityReportPath, "mutated report\n");
}, /invalid|provider/u],
["key rotation", async (fixture: Awaited<ReturnType<typeof createPromotionStagingFixture>>) => {
const rotated = generateKeyPairSync("ed25519");
await writeFile(fixture.input.vulnerabilityPublicKeyPath, rotated.publicKey.export({ type: "spki", format: "pem" }));
}, /provider evidence failed|trust identity/u],
["expected nonce replay", async (fixture: Awaited<ReturnType<typeof createPromotionStagingFixture>>) => {
fixture.input = { ...fixture.input, vulnerabilityInvocationNonce: "9".repeat(64) };
}, /invocation nonce/u],
] as const)("rejects %s without staging or PASS records", async (_label, mutate, diagnostic) => {
const fixture = await createPromotionStagingFixture();
await mutate(fixture);
await expect(stageVerifiedPromotion(fixture.input, fixture.dependencies)).rejects.toThrow(diagnostic);
await expect(readdir(fixture.runnerTempRoot)).resolves.toEqual([]);
}, 30_000);
it("rejects symlinked and oversized captured sources without staging", 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);
await expect(readdir(linked.runnerTempRoot)).resolves.toEqual([]);
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);
await expect(readdir(oversized.runnerTempRoot)).resolves.toEqual([]);
}, 30_000);
it("detects a runner-temp parent identity swap and removes its owned partial staging", async () => {
const fixture = await createPromotionStagingFixture();
const displaced = `${fixture.runnerTempRoot}-displaced`;
await expect(stageVerifiedPromotion(fixture.input, {
...fixture.dependencies,
afterStagingWrite: async () => {
await rename(fixture.runnerTempRoot, displaced);
await mkdir(fixture.runnerTempRoot, { mode: 0o700 });
},
})).rejects.toThrow(/parent identity changed/u);
await expect(readdir(displaced)).resolves.toEqual([]);
await expect(readdir(fixture.runnerTempRoot)).resolves.toEqual([]);
}, 30_000);
it("cleanup is token-bound and removes only the finalized private directory", async () => {
const fixture = await createPromotionStagingFixture();
const finalized = await stageVerifiedPromotion(fixture.input, fixture.dependencies);
const canary = path.join(fixture.runnerTempRoot, "canary");
await writeFile(canary, "unchanged\n");
await expect(cleanupFinalizedPromotion({
runnerTempRoot: fixture.runnerTempRoot,
stagingRoot: finalized.stagingRoot,
cleanupToken: `${finalized.cleanupToken}-wrong`,
runnerTempIdentity: finalized.runnerTempIdentity,
})).rejects.toThrow(/root\/token mismatch/u);
await cleanupFinalizedPromotion(finalizedCleanup(fixture, finalized));
await expect(readFile(canary, "utf8")).resolves.toBe("unchanged\n");
await expect(lstat(finalized.stagingRoot)).rejects.toMatchObject({ code: "ENOENT" });
}, 30_000);
it("cleanup rejects a swapped runner-temp ancestor and a symlinked staging leaf", async () => {
const swapped = await createPromotionStagingFixture();
const finalized = await stageVerifiedPromotion(swapped.input, swapped.dependencies);
const displaced = `${swapped.runnerTempRoot}-cleanup-displaced`;
await expect(cleanupFinalizedPromotion(finalizedCleanup(swapped, finalized), {
beforeRemove: async () => {
await rename(swapped.runnerTempRoot, displaced);
await mkdir(swapped.runnerTempRoot, { mode: 0o700 });
},
})).rejects.toThrow(/parent identity changed/u);
await expect(readdir(swapped.runnerTempRoot)).resolves.toEqual([]);
await expect(readdir(displaced)).resolves.toEqual([finalized.cleanupToken]);
const linked = await createPromotionStagingFixture();
const linkedFinalized = await stageVerifiedPromotion(linked.input, linked.dependencies);
const saved = `${linkedFinalized.stagingRoot}-saved`;
const outside = await temporaryRoot("promotion-cleanup-outside-");
const canary = path.join(outside, "canary");
await writeFile(canary, "unchanged\n");
await rename(linkedFinalized.stagingRoot, saved);
await symlink(outside, linkedFinalized.stagingRoot);
await expect(cleanupFinalizedPromotion(finalizedCleanup(linked, linkedFinalized)))
.rejects.toThrow(/leaf is unsafe/u);
await expect(readFile(canary, "utf8")).resolves.toBe("unchanged\n");
}, 30_000);
});
function runProviderSupervisor(
fixture: Awaited<ReturnType<typeof createProviderFixture>>,
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_PROVIDER_PRIVATE_KEY_PATH: fixture.privateKeyPath,
VULNERABILITY_PUBLIC_KEY_PATH: fixture.publicKeyPath,
VULNERABILITY_KEY_ID: fixture.keyId,
VULNERABILITY_REPORT_PATH: fixture.reportPath,
VALIDATED_PROVIDER_REPORT_PATH: input.sealedPath,
CANDIDATE_ARCHIVE_PATH: fixture.archivePath,
CANDIDATE_ARCHIVE_SHA256: fixture.archiveSha256,
CI_RUN_ID: fixture.expectedContext.run.id,
CI_RUN_ATTEMPT: String(fixture.expectedContext.run.attempt),
EXPECTED_SOURCE_REVISION: fixture.expectedContext.source.revision,
},
},
);
}
function providerValidationInput(
fixture: Awaited<ReturnType<typeof createProviderFixture>>,
capturedReport: Buffer = fixture.reportBytes,
) {
return {
kind: "vulnerability" as const,
verifiedManifest: fixture.candidate,
archiveSha256: fixture.archiveSha256,
candidateRoot: fixture.candidateRoot,
capturedReport,
expectedContext: fixture.expectedContext,
trust: fixture.trust,
nowEpochMs: () => fixture.now,
};
}
function providerV2WriterSource(
options: Readonly<{ importFs?: boolean }> = {},
): string {
return [
"import { createPrivateKey, sign } from 'node:crypto';",
...(options.importFs === false
? []
: ["import { readFileSync, writeFileSync } from 'node:fs';"]),
"const canonical = (value) => {",
" if (Array.isArray(value)) return value.map(canonical).sort((left, right) => String(JSON.stringify(left)).localeCompare(String(JSON.stringify(right))));",
" if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonical(item)]));",
" return value;",
"};",
"const unsigned = {",
" schemaVersion: Number(process.env.PROVIDER_EVIDENCE_SCHEMA_VERSION),",
" evidenceType: process.env.PROVIDER_EVIDENCE_TYPE,",
" provider: 'fixture',",
" issuedAt: process.env.PROVIDER_ISSUED_AT,",
" expiresAt: process.env.PROVIDER_EXPIRES_AT,",
" run: { id: process.env.CI_RUN_ID, attempt: Number(process.env.CI_RUN_ATTEMPT), invocationNonce: process.env.PROVIDER_INVOCATION_NONCE },",
" source: { revision: process.env.SOURCE_REVISION, sourceSetSha256: process.env.SOURCE_SET_SHA256 },",
" candidate: { archiveSha256: process.env.CANDIDATE_ARCHIVE_SHA256, bundleSha256: process.env.CANDIDATE_BUNDLE_SHA256, distSha256: process.env.CANDIDATE_DIST_SHA256, lockfileSha256: process.env.CANDIDATE_LOCKFILE_SHA256 },",
" findings: [],",
"};",
"const privateKey = createPrivateKey(readFileSync(process.env.VULNERABILITY_PROVIDER_PRIVATE_KEY_PATH));",
"const value = sign(null, Buffer.from(JSON.stringify(canonical(unsigned))), privateKey).toString('base64');",
"writeFileSync(process.env.VULNERABILITY_REPORT_PATH, `${JSON.stringify({ ...unsigned, signature: { algorithm: 'Ed25519', keyId: process.env.PROVIDER_KEY_ID, publicKeyFingerprint: process.env.PROVIDER_PUBLIC_KEY_FINGERPRINT, value } })}\\n`);",
].join("\n");
}
async function createCandidateArchiveFixture(
options: Readonly<{ extraMember?: boolean; repeatedExtraMembers?: number; duplicateMember?: boolean; oversizedManifest?: boolean }> = {},
): Promise<Readonly<{ root: string; archivePath: string }>> {
const root = await temporaryRoot("ci-candidate-archive-");
const files = new Map<string, Buffer>();
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 base = await ensureProviderBaseFixture();
const root = await temporaryRoot("ci-provider-upload-");
await cp(base, root, { recursive: true });
const candidateRoot = root;
const candidate = JSON.parse(
await readFile(path.join(root, RELEASE_CANDIDATE_MANIFEST_PATH), "utf8"),
) as Awaited<ReturnType<typeof createReleaseCandidateManifest>>;
const protectedCandidateRelative = candidate.files.find((file) =>
file.path.startsWith("dist/"),
)?.path;
if (!protectedCandidateRelative) {
throw new Error("provider fixture candidate has no dist file");
}
const protectedCandidatePath = path.join(root, protectedCandidateRelative);
const protectedCandidateBytes = await readFile(protectedCandidatePath);
const assessment = localEvidenceAssessmentArtifactSchema.parse(
JSON.parse(
await readFile(path.join(root, LOCAL_EVIDENCE_ASSESSMENT_PATH), "utf8"),
) as unknown,
);
const archivePath = path.join(root, "candidate.tar.gz");
const archiveSha256 = sha256(await readFile(archivePath));
const keys = generateKeyPairSync("ed25519");
const keyId = "fixture-vulnerability-key";
const publicKeyPath = path.join(root, "keys/vulnerability.pem");
const privateKeyPath = path.join(root, "keys/vulnerability-private.pem");
await writeArtifact(
root,
"keys/vulnerability.pem",
keys.publicKey.export({ type: "spki", format: "pem" }),
);
await writeArtifact(
root,
"keys/vulnerability-private.pem",
keys.privateKey.export({ type: "pkcs8", format: "pem" }),
);
const trust = (await readProviderTrust(root, publicKeyPath, keyId))!;
const now = Date.parse("2026-08-02T01:00:00.000Z");
const expectedContext = {
run: { id: "fixture-run", attempt: 1 },
source: {
revision: assessment.source.revision,
sourceSetSha256: assessment.source.sourceSetSha256,
},
candidate: {
archiveSha256,
bundleSha256: candidate.bundleSha256,
distSha256: candidate.distSha256,
lockfileSha256: candidate.lockfileSha256,
},
vulnerabilityInvocationNonce: "1".repeat(64),
provenanceInvocationNonce: "0".repeat(64),
} as const;
const unsigned = {
schemaVersion: 2 as const,
evidenceType: "vulnerability-report" as const,
provider: "fixture",
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { ...expectedContext.run, invocationNonce: expectedContext.vulnerabilityInvocationNonce },
source: expectedContext.source,
candidate: expectedContext.candidate,
findings: [],
};
const report = {
...unsigned,
signature: {
algorithm: "Ed25519" as const,
keyId,
publicKeyFingerprint: providerPublicKeyFingerprint(keys.publicKey),
value: sign(
null,
providerEvidenceSignaturePayload(unsigned),
keys.privateKey,
).toString("base64"),
},
};
const reportPath = path.join(
root,
"provider-evidence/untrusted/vulnerability-report.json",
);
await writeArtifact(
root,
"provider-evidence/untrusted/vulnerability-report.json",
`${JSON.stringify(report)}\n`,
);
const reportBytes = Buffer.from(`${JSON.stringify(report)}\n`);
return {
root,
candidateRoot,
candidate,
protectedCandidatePath,
protectedCandidateBytes,
archivePath,
archiveSha256,
reportPath,
reportBytes,
expectedContext,
trust,
now,
publicKeyPath,
privateKeyPath,
privateKey: keys.privateKey,
keyId,
providerWriter: path.join(root, "provider-v2-writer.mjs"),
};
}
async function ensureProviderBaseFixture(): Promise<string> {
if (providerBaseRoot) return providerBaseRoot;
const sourceRoot = process.cwd();
const root = await mkdtemp(path.join(tmpdir(), "ci-provider-v2-base-"));
await cp(sourceRoot, root, {
recursive: true,
filter: (source) => {
const relative = path.relative(sourceRoot, source);
if (!relative) return true;
const first = relative.split(path.sep)[0];
return ![".release", "artifacts", "dist", "node_modules"].includes(first ?? "");
},
});
await cp(path.join(sourceRoot, "artifacts"), path.join(root, "artifacts"), {
recursive: true,
});
await rm(path.join(root, "artifacts/release"), { recursive: true, force: true });
await symlink(path.join(sourceRoot, "node_modules"), path.join(root, "node_modules"), "dir");
const git = spawnSync("git", ["show", "-s", "--format=%H%n%ct", "HEAD"], {
cwd: sourceRoot,
encoding: "utf8",
});
if (git.status !== 0) throw new Error(git.stderr);
const [revision, sourceDateEpoch] = git.stdout.trim().split(/\r?\n/u);
const build = spawnSync("corepack", ["pnpm", "build:release-candidate"], {
cwd: root,
encoding: "utf8",
timeout: 120_000,
maxBuffer: 32 * 1024 * 1024,
env: {
...process.env,
CI: "true",
VITE_BUILD_ID: "provider-v2-fixture",
VITE_COMMIT_SHA: revision,
RELEASE_ID: "provider-v2-fixture",
SOURCE_DATE_EPOCH: sourceDateEpoch,
CI_RUNNER_IMAGE: `fixture@sha256:${"a".repeat(64)}`,
},
});
if (build.status !== 0) throw new Error(`${build.stdout}\n${build.stderr}`);
const archivePath = path.join(root, "candidate.tar.gz");
const tar = spawnSync(
"/usr/bin/tar",
[
"--sort=name",
"--mtime=@0",
"--owner=0",
"--group=0",
"--numeric-owner",
"-czf",
archivePath,
"dist",
...RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
],
{ cwd: root, encoding: "utf8" },
);
if (tar.status !== 0) throw new Error(tar.stderr);
providerBaseRoot = root;
return root;
}
async function createPromotionStagingFixture() {
const base = await ensureProviderBaseFixture();
const root = await temporaryRoot("promotion-finalizer-");
const archivePath = path.join(root, "inputs/release-candidate.tar.gz");
await mkdir(path.dirname(archivePath), { recursive: true });
await cp(path.join(base, "candidate.tar.gz"), archivePath);
const candidate = await verifyCiCandidateArchive({ archivePath });
const candidateArchiveBytes = await readFile(archivePath);
const assessmentBytes = await readFile(path.join(base, LOCAL_EVIDENCE_ASSESSMENT_PATH));
const assessment = localEvidenceAssessmentArtifactSchema.parse(
JSON.parse(assessmentBytes.toString("utf8")) as unknown,
);
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const vulnerabilityKeyId = "fixture-vulnerability";
const provenanceKeyId = "fixture-provenance";
const vulnerabilityInvocationNonce = "5".repeat(64);
const provenanceInvocationNonce = "6".repeat(64);
const expectedContext = {
run: { id: "fixture-run", attempt: 1 },
source: {
revision: assessment.source.revision,
sourceSetSha256: assessment.source.sourceSetSha256,
},
candidate: {
archiveSha256: sha256(candidateArchiveBytes),
bundleSha256: candidate.manifest.bundleSha256,
distSha256: candidate.manifest.distSha256,
lockfileSha256: candidate.manifest.lockfileSha256,
},
} as const;
const vulnerabilityUnsigned = {
schemaVersion: 2 as const,
evidenceType: "vulnerability-report" as const,
provider: "fixture-vulnerability",
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { ...expectedContext.run, invocationNonce: vulnerabilityInvocationNonce },
source: expectedContext.source,
candidate: expectedContext.candidate,
findings: [],
};
const vulnerabilityReport = {
...vulnerabilityUnsigned,
signature: {
algorithm: "Ed25519" as const,
keyId: vulnerabilityKeyId,
publicKeyFingerprint: providerPublicKeyFingerprint(vulnerabilityKeys.publicKey),
value: sign(
null,
providerEvidenceSignaturePayload(vulnerabilityUnsigned),
vulnerabilityKeys.privateKey,
).toString("base64"),
},
};
const provenanceUnsigned = {
schemaVersion: 2 as const,
evidenceType: "provenance-attestation" as const,
provider: "fixture-provenance",
signer: "fixture-signer",
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { ...expectedContext.run, invocationNonce: provenanceInvocationNonce },
source: expectedContext.source,
candidate: expectedContext.candidate,
subject: {
name: "dist" as const,
digest: { sha256: candidate.manifest.distSha256 },
},
};
const provenanceAttestation = {
...provenanceUnsigned,
signature: {
algorithm: "Ed25519" as const,
keyId: provenanceKeyId,
publicKeyFingerprint: providerPublicKeyFingerprint(provenanceKeys.publicKey),
value: sign(
null,
providerEvidenceSignaturePayload(provenanceUnsigned),
provenanceKeys.privateKey,
).toString("base64"),
},
};
const vulnerabilityReportBytes = Buffer.from(`${JSON.stringify(vulnerabilityReport)}\n`);
const provenanceAttestationBytes = Buffer.from(`${JSON.stringify(provenanceAttestation)}\n`);
const vulnerabilityReportPath = path.join(
root,
"inputs/vulnerability-report.json",
);
const provenanceAttestationPath = path.join(
root,
"inputs/provenance-attestation.json",
);
const vulnerabilityPublicKeyPath = path.join(root, "keys/vulnerability.pem");
const provenancePublicKeyPath = path.join(root, "keys/provenance.pem");
await writeArtifact(
root,
"inputs/vulnerability-report.json",
vulnerabilityReportBytes,
);
await writeArtifact(
root,
"inputs/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" }),
);
const runnerTempRoot = path.join(root, "runner-temp");
await mkdir(runnerTempRoot, { mode: 0o700 });
const cleanupToken = `promotion-fixture-run-1-${"2a".repeat(16)}`;
const expectedProviderEvidence = {
vulnerabilityReportSha256: sha256(vulnerabilityReportBytes),
provenanceAttestationSha256: sha256(provenanceAttestationBytes),
vulnerabilityInvocationNonce,
provenanceInvocationNonce,
vulnerabilityKeyId,
vulnerabilityKeyFingerprint: providerPublicKeyFingerprint(vulnerabilityKeys.publicKey),
provenanceKeyId,
provenanceKeyFingerprint: providerPublicKeyFingerprint(provenanceKeys.publicKey),
};
return {
root,
runnerTempRoot,
cleanupToken,
assessmentSha256: sha256(assessmentBytes),
expectedContext,
expectedProviderEvidence,
capturedSources: new Map([
["release-candidate.tar.gz", candidateArchiveBytes],
["vulnerability-report.json", vulnerabilityReportBytes],
["provenance-attestation.json", provenanceAttestationBytes],
]),
dependencies: {
nowEpochMs: () => Date.parse("2026-08-02T01:00:00.000Z"),
randomBytes: (bytes: number) => Buffer.alloc(bytes, 0x2a),
},
input: {
repositoryRoot: root,
archivePath,
expectedArchiveSha256: sha256(candidateArchiveBytes),
vulnerabilityReportPath,
provenanceAttestationPath,
vulnerabilityPublicKeyPath,
vulnerabilityKeyId,
provenancePublicKeyPath,
provenanceKeyId,
expectedRun: {
id: expectedContext.run.id,
attempt: expectedContext.run.attempt,
sourceRevision: expectedContext.source.revision,
},
vulnerabilityInvocationNonce,
provenanceInvocationNonce,
runnerTempRoot,
},
};
}
function finalizedCleanup(
fixture: Awaited<ReturnType<typeof createPromotionStagingFixture>>,
finalized: Awaited<ReturnType<typeof stageVerifiedPromotion>>,
) {
return {
runnerTempRoot: fixture.runnerTempRoot,
stagingRoot: finalized.stagingRoot,
cleanupToken: finalized.cleanupToken,
runnerTempIdentity: finalized.runnerTempIdentity,
};
}