Files
clean-architecture-frontend…/tests/unit/supply-chain.test.ts
T

641 lines
22 KiB
TypeScript

import { createHash, generateKeyPairSync, sign } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
diffDependencyInventories,
isValidSha512Integrity,
parsePnpmLockfilePackages,
supplyChainDigest,
validateDependencyReview,
validateLicensePolicy,
} from "../../scripts/lib/supply-chain.ts";
import { digestReleaseInputFiles } from "../../scripts/lib/release-input-evidence.ts";
import { findSecretMatches } from "../../scripts/lib/secret-scan.ts";
import {
parseSecretScanIncludedPaths,
selectIncludedInventoryFiles,
} from "../../scripts/lib/secret-scan-policy.ts";
import { checkSecurityFixtures } from "../../scripts/lib/security-fixture-check.ts";
import {
evaluatePromotionEvidence,
providerEvidenceSignaturePayload,
providerPublicKeyFingerprint,
} from "../../scripts/lib/provider-evidence.ts";
import {
createReleaseCandidateManifest,
RELEASE_CANDIDATE_EVIDENCE_PATHS,
verifyReleaseCandidate,
} from "../../scripts/lib/release-candidate.ts";
import { deterministicSupplyChainGeneratedAt } from "../../scripts/lib/supply-chain-time.ts";
import {
indexCiGateContract,
loadCiGateContract,
} from "../../scripts/contracts/ci-gates.ts";
const integrity = `sha512-${Buffer.alloc(64, 7).toString("base64")}`;
const dependency = {
name: "fixture",
version: "1.0.0",
direct: true,
scope: "production",
optional: false,
license: "MIT",
integrity,
dependencies: [],
};
const candidateDistSha256 = "1".repeat(64);
const lockfileSha256 = "2".repeat(64);
const NOW = Date.parse("2026-08-02T01:00:00.000Z");
const sourceIdentity = Object.freeze({
revision: "a".repeat(40),
sourceSetSha256: "b".repeat(64),
});
const expectedProviderContext = Object.freeze({
run: Object.freeze({ id: "fixture-run", attempt: 1 }),
source: sourceIdentity,
candidate: Object.freeze({
archiveSha256: "3".repeat(64),
bundleSha256: "4".repeat(64),
distSha256: candidateDistSha256,
lockfileSha256,
}),
vulnerabilityInvocationNonce: "5".repeat(64),
provenanceInvocationNonce: "6".repeat(64),
secretScanAttestation: Object.freeze({
status: "PASS" as const,
localEvidenceAssessmentSha256: "7".repeat(64),
sourceSetSha256: sourceIdentity.sourceSetSha256,
policySha256: "8".repeat(64),
sarifSha256: "9".repeat(64),
scanInputSha256: "a".repeat(64),
}),
});
function signedProviderEvidence(
value: Record<string, unknown>,
keyId: string,
privateKey: ReturnType<typeof generateKeyPairSync>["privateKey"],
publicKeyFingerprint: string,
) {
return {
...value,
signature: {
algorithm: "Ed25519",
keyId,
publicKeyFingerprint,
value: sign(
null,
providerEvidenceSignaturePayload(value),
privateKey,
).toString("base64"),
},
};
}
function providerPair(input: Readonly<{
vulnerabilityKeys: ReturnType<typeof generateKeyPairSync>;
provenanceKeys: ReturnType<typeof generateKeyPairSync>;
candidate?: typeof expectedProviderContext.candidate;
vulnerabilityFingerprint?: string;
provenanceFingerprint?: string;
}>) {
const candidate = input.candidate ?? expectedProviderContext.candidate;
const vulnerabilityFingerprint = input.vulnerabilityFingerprint ??
providerPublicKeyFingerprint(input.vulnerabilityKeys.publicKey);
const provenanceFingerprint = input.provenanceFingerprint ??
providerPublicKeyFingerprint(input.provenanceKeys.publicKey);
return {
vulnerabilityReport: signedProviderEvidence({
schemaVersion: 2,
evidenceType: "vulnerability-report",
provider: "fixture-vulnerability-provider",
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { ...expectedProviderContext.run, invocationNonce: expectedProviderContext.vulnerabilityInvocationNonce },
source: expectedProviderContext.source,
candidate,
secretScanAttestation: expectedProviderContext.secretScanAttestation,
findings: [],
}, "fixture-vulnerability-key", input.vulnerabilityKeys.privateKey, vulnerabilityFingerprint),
provenanceAttestation: signedProviderEvidence({
schemaVersion: 2,
evidenceType: "provenance-attestation",
provider: "fixture-provenance-provider",
issuedAt: "2026-08-02T01:00:00.000Z",
expiresAt: "2026-08-02T02:00:00.000Z",
run: { ...expectedProviderContext.run, invocationNonce: expectedProviderContext.provenanceInvocationNonce },
source: expectedProviderContext.source,
candidate,
signer: "fixture-workload-identity",
subject: { name: "dist", digest: { sha256: candidate.distSha256 } },
}, "fixture-provenance-key", input.provenanceKeys.privateKey, provenanceFingerprint),
};
}
function providerTrust(
keyId: string,
publicKey: ReturnType<typeof generateKeyPairSync>["publicKey"],
publicKeyFingerprint = providerPublicKeyFingerprint(publicKey),
) {
return { keyId, publicKey, publicKeyFingerprint };
}
describe("supply-chain policy", () => {
it("derives a stable supply-chain timestamp from the immutable build epoch", () => {
const input = {
generatedAt: "2026-08-01T00:00:00.000Z",
sourceDateEpoch: "1785542400",
};
expect(deterministicSupplyChainGeneratedAt(input)).toBe(
"2026-08-01T00:00:00.000Z",
);
expect(deterministicSupplyChainGeneratedAt(input)).toBe(
deterministicSupplyChainGeneratedAt({ ...input }),
);
expect(() =>
deterministicSupplyChainGeneratedAt({
generatedAt: "not-a-time",
sourceDateEpoch: "1785542400",
}),
).toThrow(/generatedAt/u);
expect(() =>
deterministicSupplyChainGeneratedAt({
generatedAt: "2026-08-01T00:00:00.000Z",
sourceDateEpoch: "1785542401",
}),
).toThrow(/SOURCE_DATE_EPOCH/u);
});
it("rejects release candidate dist bytes changed after manifest creation", async () => {
const root = await mkdtemp(path.join(tmpdir(), "release-candidate-"));
try {
const rawLockfile = "lockfileVersion: '9.0'\n";
const rawLockfileSha256 = createHash("sha256")
.update(rawLockfile)
.digest("hex");
await mkdir(path.join(root, "dist/.vite"), { recursive: true });
await writeFile(path.join(root, "dist/app.js"), "immutable\n");
await writeFile(path.join(root, "dist/.vite/metadata.json"), "{}\n");
await writeFile(path.join(root, "pnpm-lock.yaml"), rawLockfile);
for (const file of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
if (file === "pnpm-lock.yaml") continue;
await mkdir(path.dirname(path.join(root, file)), { recursive: true });
await writeFile(
path.join(root, file),
file === "artifacts/release/dependency-inventory.json"
? `${JSON.stringify({ lockfileSha256: rawLockfileSha256 })}\n`
: `${file}\n`,
);
}
const manifest = await createReleaseCandidateManifest(root);
expect(manifest.lockfileSha256).toBe(rawLockfileSha256);
expect(manifest.files).toContainEqual(
expect.objectContaining({
path: "pnpm-lock.yaml",
sha256: rawLockfileSha256,
}),
);
expect(await createReleaseCandidateManifest(root)).toEqual(manifest);
expect((await verifyReleaseCandidate(manifest, root)).failures).toEqual(
[],
);
await writeFile(path.join(root, "dist/app.js"), "mutated\n");
expect(
(await verifyReleaseCandidate(manifest, root)).failures,
).toEqual(
expect.arrayContaining([
"release candidate dist digest mismatch",
"release candidate bundle digest mismatch",
"release candidate file set or file digest mismatch",
]),
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("rejects a dependency inventory digest that differs from raw pnpm-lock bytes", async () => {
const root = await mkdtemp(path.join(tmpdir(), "release-lockfile-"));
try {
await mkdir(path.join(root, "dist"), { recursive: true });
await writeFile(path.join(root, "dist/app.js"), "immutable\n");
await writeFile(path.join(root, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
for (const file of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
if (file === "pnpm-lock.yaml") continue;
await mkdir(path.dirname(path.join(root, file)), { recursive: true });
await writeFile(
path.join(root, file),
file === "artifacts/release/dependency-inventory.json"
? `${JSON.stringify({ lockfileSha256 })}\n`
: `${file}\n`,
);
}
await expect(createReleaseCandidateManifest(root)).rejects.toThrow(
/raw pnpm-lock digest mismatch/u,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("fails promotion when external provider evidence is absent", () => {
const result = evaluatePromotionEvidence({
expected: expectedProviderContext,
localStatus: "PASS",
vulnerabilityReport: null,
provenanceAttestation: null,
vulnerabilityTrust: null,
provenanceTrust: null,
nowEpochMs: () => NOW,
});
expect(result.status).toBe("FAIL_UNVERIFIED");
});
it("passes only signed provider evidence for the exact immutable candidate", () => {
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const { vulnerabilityReport, provenanceAttestation } = providerPair({
vulnerabilityKeys,
provenanceKeys,
});
const result = evaluatePromotionEvidence({
expected: expectedProviderContext,
localStatus: "PASS",
vulnerabilityReport,
provenanceAttestation,
vulnerabilityTrust: providerTrust(
"fixture-vulnerability-key",
vulnerabilityKeys.publicKey,
),
provenanceTrust: providerTrust(
"fixture-provenance-key",
provenanceKeys.publicKey,
),
nowEpochMs: () => NOW,
});
expect(result).toMatchObject({
status: "PASS",
vulnerabilityStatus: "PASS",
provenanceAttestationStatus: "PASS",
failures: [],
});
});
it("rejects correctly signed provider evidence for a different digest", () => {
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const wrongDistSha256 = "3".repeat(64);
const { vulnerabilityReport, provenanceAttestation } = providerPair({
vulnerabilityKeys,
provenanceKeys,
candidate: { ...expectedProviderContext.candidate, distSha256: wrongDistSha256 },
});
const result = evaluatePromotionEvidence({
expected: expectedProviderContext,
localStatus: "PASS",
vulnerabilityReport,
provenanceAttestation,
vulnerabilityTrust: providerTrust("fixture-vulnerability-key", vulnerabilityKeys.publicKey),
provenanceTrust: providerTrust("fixture-provenance-key", provenanceKeys.publicKey),
nowEpochMs: () => NOW,
});
expect(result.status).toBe("FAIL_UNVERIFIED");
expect(result.failures).toEqual(
expect.arrayContaining([
"vulnerability report candidate identity mismatch",
"provenance attestation candidate identity mismatch",
]),
);
});
it("rejects candidate bytes changed after provider attestation", () => {
const vulnerabilityKeys = generateKeyPairSync("ed25519");
const provenanceKeys = generateKeyPairSync("ed25519");
const { vulnerabilityReport, provenanceAttestation } = providerPair({
vulnerabilityKeys,
provenanceKeys,
});
const result = evaluatePromotionEvidence({
expected: {
...expectedProviderContext,
candidate: { ...expectedProviderContext.candidate, distSha256: "4".repeat(64) },
},
localStatus: "PASS",
vulnerabilityReport,
provenanceAttestation,
vulnerabilityTrust: providerTrust("fixture-vulnerability-key", vulnerabilityKeys.publicKey),
provenanceTrust: providerTrust("fixture-provenance-key", provenanceKeys.publicKey),
nowEpochMs: () => NOW,
});
expect(result.status).toBe("FAIL_UNVERIFIED");
expect(result.failures).toContain("vulnerability report candidate identity mismatch");
});
it("rejects Ed448 keys mislabeled as Ed25519 evidence", () => {
const vulnerabilityKeys = generateKeyPairSync("ed448");
const provenanceKeys = generateKeyPairSync("ed448");
const fakeFingerprint = `sha256:${"7".repeat(64)}`;
const { vulnerabilityReport, provenanceAttestation } = providerPair({
vulnerabilityKeys,
provenanceKeys,
vulnerabilityFingerprint: fakeFingerprint,
provenanceFingerprint: fakeFingerprint,
});
expect(
evaluatePromotionEvidence({
expected: expectedProviderContext,
localStatus: "PASS",
vulnerabilityReport,
provenanceAttestation,
vulnerabilityTrust: {
keyId: "fixture-vulnerability-key",
publicKey: vulnerabilityKeys.publicKey,
publicKeyFingerprint: fakeFingerprint,
},
provenanceTrust: {
keyId: "fixture-provenance-key",
publicKey: provenanceKeys.publicKey,
publicKeyFingerprint: fakeFingerprint,
},
nowEpochMs: () => NOW,
}).status,
).toBe("FAIL_UNVERIFIED");
});
it.each([
["empty", []],
["empty entry", [""]],
["blank entry", [" "]],
["absolute", ["/src"]],
["backslash", ["src\\file.ts"]],
["dot", ["."]],
["dotdot", [".."]],
["traversal", ["src/../docs"]],
["trailing slash", ["src/"]],
["mixed", ["src", 42]],
["duplicate", ["src", "src"]],
])("rejects %s secret-scan include paths", (_name, includedPaths) => {
expect(() => parseSecretScanIncludedPaths(includedPaths)).toThrow();
});
it("requires every configured include path to match the inventory", () => {
expect(
selectIncludedInventoryFiles(
["README.md", "src/app.ts"],
["src"],
),
).toEqual(["src/app.ts"]);
expect(() =>
selectIncludedInventoryFiles(
["README.md", "src/app.ts"],
["misspelled"],
),
).toThrow(/misspelled/u);
expect(
selectIncludedInventoryFiles(
["README.md", "src/app.ts"],
null,
),
).toEqual(["README.md", "src/app.ts"]);
});
it("rejects a crashed fixture scan and cannot reuse a stale repository artifact", async () => {
const cleaned: string[] = [];
await expect(
checkSecurityFixtures({
createTempDirectory: async () => "/tmp/fresh-security-fixture",
runScan: () => ({
status: 1,
signal: null,
stdout: "",
stderr: "Security scan found 3 blocking result(s).\n",
}),
readArtifact: async (artifactPath) => {
expect(artifactPath).toBe(
"/tmp/fresh-security-fixture/scan-fixture.sarif",
);
throw Object.assign(new Error("fresh artifact missing"), {
code: "ENOENT",
});
},
cleanup: async (directory) => {
cleaned.push(directory);
},
}),
).rejects.toThrow(/fresh artifact missing/u);
expect(cleaned).toEqual(["/tmp/fresh-security-fixture"]);
await expect(
checkSecurityFixtures({
createTempDirectory: async () => "/tmp/fresh-security-fixture",
runScan: () => ({
status: null,
signal: "SIGTERM",
stdout: "",
stderr: "Security scan found 3 blocking result(s).\n",
}),
readArtifact: async () => "{}",
cleanup: async () => undefined,
}),
).rejects.toThrow(/did not fail exactly/u);
});
it("wires the exact security fixture checker as a passing CI gate", async () => {
const contract = await loadCiGateContract(process.cwd(), {
mode: process.env.CI_CONTRACT_MODE === "removal-fixture"
? "removal-fixture"
: "canonical",
});
const index = indexCiGateContract(contract);
const securityGate = index.gates.get("FE-GATE-013");
expect(securityGate).toBeDefined();
const commands = securityGate!.commandIds.map((commandId) =>
index.commands.get(commandId),
);
expect(commands).toContainEqual(
expect.objectContaining({
script: "check:security:fixtures",
expect: "pass",
}),
);
expect(commands).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ script: "scan:security:fixture" }),
]),
);
const evidence = securityGate!.evidenceArtifactIds.map(
(artifactId) => index.artifacts.get(artifactId)?.path,
);
expect(evidence).not.toContain(
"artifacts/security/scan-fixture.sarif",
);
});
it("uses one fail-closed repository inventory for provenance and secret scanning", async () => {
const [provenanceSource, securityCliSource, securityEvaluatorSource] =
await Promise.all([
readFile("scripts/generate-supply-chain.ts", "utf8"),
readFile("scripts/security-scan.ts", "utf8"),
readFile("scripts/lib/secret-scan-evaluator.ts", "utf8"),
]);
expect(securityCliSource).toContain("evaluateRepositorySecretScan");
for (const source of [provenanceSource, securityEvaluatorSource]) {
expect(source).toContain("buildRepositoryFileInventory");
expect(source).not.toContain("async function filesWithin");
}
});
it("binds provenance digest behavior to tracked files outside policy roots", async () => {
const contents = new Map([
["src/app.ts", Buffer.from("app\n")],
["README.md", Buffer.from("one\n")],
]);
const first = await digestReleaseInputFiles(
["README.md", "src/app.ts"],
async (file) => contents.get(file)!,
);
contents.set("README.md", Buffer.from("two\n"));
const second = await digestReleaseInputFiles(
["README.md", "src/app.ts"],
async (file) => contents.get(file)!,
);
expect(second).not.toBe(first);
});
it("detects every forbidden secret fixture, including quoted JSON keys", async () => {
const fixtureRoot = "tests/fixtures/security/secret-detection/forbidden";
const findings = (
await Promise.all(
["source.ts", "dist.ts", "config.json"].map(async (file) =>
findSecretMatches(
`${fixtureRoot}/${file}`,
await readFile(`${fixtureRoot}/${file}`, "utf8"),
),
),
)
).flat();
expect(findings.map((finding) => [finding.file, finding.ruleId])).toEqual([
[`${fixtureRoot}/source.ts`, "aws-access-key"],
[`${fixtureRoot}/dist.ts`, "assigned-secret"],
[`${fixtureRoot}/config.json`, "assigned-secret"],
]);
});
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);
});
});