feat: verify frontend supply chain

This commit is contained in:
donghyeon-ka
2026-07-26 17:37:51 +09:00
parent a64708f3de
commit 8b4f875c1c
35 changed files with 8910 additions and 141 deletions
+39
View File
@@ -0,0 +1,39 @@
import { spawnSync } from "node:child_process";
import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
const fixtureRoot = await mkdtemp(
path.join(tmpdir(), "ca-frontend-frozen-lockfile-"),
);
try {
await cp("pnpm-lock.yaml", path.join(fixtureRoot, "pnpm-lock.yaml"));
const manifest = JSON.parse(await readFile("package.json", "utf8"));
manifest.dependencies.react = "0.0.0-invalid-fixture";
await writeFile(
path.join(fixtureRoot, "package.json"),
`${JSON.stringify(manifest, null, 2)}\n`,
);
const result = spawnSync(
"corepack",
[
"pnpm",
"install",
"--frozen-lockfile",
"--lockfile-only",
"--ignore-scripts",
],
{
cwd: fixtureRoot,
encoding: "utf8",
},
);
if (result.status === 0) {
process.stderr.write("Tampered manifest unexpectedly passed frozen install.\n");
process.exitCode = 1;
} else {
process.stdout.write("Frozen lockfile mismatch fixture: rejected PASS\n");
}
} finally {
await rm(fixtureRoot, { recursive: true, force: true });
}
+148
View File
@@ -0,0 +1,148 @@
import { mkdir, writeFile } from "node:fs/promises";
import {
diffDependencyInventories,
isValidSha512Integrity,
supplyChainDigest,
validateDependencyReview,
validateLicensePolicy,
validateVulnerabilityReport,
verifySupplyChainCoherence,
} from "./lib/supply-chain.mjs";
const integrity = `sha512-${Buffer.alloc(64, 1).toString("base64")}`;
const baseDependency = {
name: "base",
version: "1.0.0",
direct: false,
scope: "production",
optional: false,
license: "MIT",
integrity,
dependencies: [],
};
const directDependency = {
...baseDependency,
name: "new-direct",
direct: true,
};
const before = { dependencies: [baseDependency] };
const after = { dependencies: [baseDependency, directDependency] };
const diff = diffDependencyInventories(before, after);
const selfReview = validateDependencyReview(diff, after, {
changes: [
{
changeId: "add:new-direct@1.0.0",
owner: "same-person",
reviewer: "same-person",
reason: "fixture",
rollback: "remove",
},
],
});
const deniedLicense = validateLicensePolicy(
{
dependencies: [{ ...baseDependency, license: "AGPL-3.0" }],
},
{
allowedLicenses: ["MIT"],
deniedLicensePatterns: ["AGPL"],
},
);
const vulnerable = validateVulnerabilityReport(
{
provider: "fixture",
scannedLockfileSha256: "lock",
findings: [
{
id: "CVE-FIXTURE",
packageName: "base",
version: "1.0.0",
severity: "critical",
},
],
},
{ blockAtSeverity: "high" },
{
exceptions: [
{
vulnerabilityId: "CVE-FIXTURE",
packageName: "base",
owner: "owner",
reviewer: "reviewer",
reason: "expired fixture",
expiresAt: "2000-01-01T00:00:00.000Z",
},
],
},
"lock",
new Date("2026-07-26T00:00:00.000Z"),
);
const mismatchedCoherence = verifySupplyChainCoherence(
{
components: [],
metadata: {
properties: [{ name: "ca:lockfileSha256", value: "wrong" }],
},
},
{ dependencies: [baseDependency], lockfileSha256: "lock" },
{
subject: [{ digest: { sha256: "wrong" } }],
predicate: { materials: { lockfileSha256: "wrong" } },
},
"dist",
);
const orderingStable =
supplyChainDigest({ dependencies: [baseDependency, directDependency] }) ===
supplyChainDigest({ dependencies: [directDependency, baseDependency] });
const approvedDigest = supplyChainDigest(before);
const tamperedBaselineRejected =
approvedDigest !==
supplyChainDigest({
dependencies: [{ ...baseDependency, version: "9.9.9-tampered" }],
});
const providerFailure = validateVulnerabilityReport(
{
provider: "",
scannedLockfileSha256: "wrong",
findings: [],
},
{ blockAtSeverity: "high" },
{ exceptions: [] },
"lock",
);
const results = [
{
id: "transitive-removal-is-real-diff",
passed:
diffDependencyInventories(after, before).removed[0] ===
"new-direct@1.0.0",
},
{
id: "tampered-integrity-rejected",
passed: !isValidSha512Integrity("sha512-dGFtcGVyZWQ="),
},
{ id: "high-risk-self-approval-rejected", passed: !selfReview.passed },
{ id: "denied-license-rejected", passed: !deniedLicense.passed },
{
id: "critical-vulnerability-expired-exception-rejected",
passed: !vulnerable.passed,
},
{ id: "sbom-provenance-mismatch-rejected", passed: !mismatchedCoherence.passed },
{ id: "dependency-ordering-deterministic", passed: orderingStable },
{ id: "baseline-digest-tamper-rejected", passed: tamperedBaselineRejected },
{
id: "vulnerability-provider-evidence-invalid",
passed: !providerFailure.passed,
},
];
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/supply-chain-fixtures.json",
`${JSON.stringify({ schemaVersion: 1, results }, null, 2)}\n`,
);
if (results.some((result) => !result.passed)) {
process.stderr.write("Supply-chain negative fixture failed.\n");
process.exit(1);
}
process.stdout.write(`Supply-chain fixtures: ${results.length} PASS\n`);
@@ -0,0 +1,105 @@
import { spawnSync } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
const fixtureDirectory = path.resolve(".tmp/supply-chain-provider-fixture");
await rm(fixtureDirectory, { recursive: true, force: true });
await mkdir(fixtureDirectory, { recursive: true });
const inventory = JSON.parse(
await readFile("artifacts/release/dependency-inventory.json", "utf8"),
);
const verification = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
);
const vulnerabilityPath = path.join(
fixtureDirectory,
"vulnerability-report.json",
);
const attestationPath = path.join(fixtureDirectory, "attestation.json");
await writeFile(
vulnerabilityPath,
`${JSON.stringify(
{
schemaVersion: 1,
provider: "fixture-scanner",
scannedLockfileSha256: inventory.lockfileSha256,
generatedAt: "2026-07-26T00:00:00.000Z",
findings: [],
},
null,
2,
)}\n`,
);
await writeFile(
attestationPath,
`${JSON.stringify(
{
schemaVersion: 1,
provider: "fixture-attestor",
signer: "fixture-workload-identity",
subject: {
name: "dist",
digest: { sha256: verification.distSha256 },
},
},
null,
2,
)}\n`,
);
const providerRun = spawnSync(
"node",
["scripts/generate-supply-chain.mjs"],
{
env: {
...process.env,
VULNERABILITY_REPORT_PATH: vulnerabilityPath,
PROVENANCE_ATTESTATION_PATH: attestationPath,
},
encoding: "utf8",
},
);
let promotionStatus = "MISSING";
if (providerRun.status === 0) {
promotionStatus = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
).promotionStatus;
}
const restore = spawnSync(
"node",
["scripts/generate-supply-chain.mjs"],
{ encoding: "utf8" },
);
await rm(fixtureDirectory, { recursive: true, force: true });
const passed =
providerRun.status === 0 &&
promotionStatus === "PASS" &&
restore.status === 0;
await writeFile(
"artifacts/security/supply-chain-provider-fixtures.json",
`${JSON.stringify(
{
schemaVersion: 1,
providerAccepted: providerRun.status === 0,
promotionStatus,
unverifiedDefaultRestored: restore.status === 0,
status: passed ? "PASS" : "FAIL",
},
null,
2,
)}\n`,
);
if (!passed) {
process.stderr.write(
`Supply-chain provider fixture failed: ${providerRun.stderr || restore.stderr}\n`,
);
process.exit(1);
}
process.stdout.write(
"Supply-chain provider fixture: verified PASS and unconfigured default restored\n",
);
+7 -1
View File
@@ -15,7 +15,13 @@ const buildId = process.env.VITE_BUILD_ID ?? "local-build";
const commitSha = process.env.VITE_COMMIT_SHA ?? "local";
const releaseId = process.env.RELEASE_ID ?? "local-release";
const runnerImage = process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`;
const builtAt = new Date().toISOString();
const buildTime = process.env.SOURCE_DATE_EPOCH
? new Date(Number(process.env.SOURCE_DATE_EPOCH) * 1_000)
: new Date();
if (!Number.isFinite(buildTime.getTime())) {
throw new Error("SOURCE_DATE_EPOCH must be epoch seconds");
}
const builtAt = buildTime.toISOString();
const viteManifest = await readFile("dist/.vite/manifest.json", "utf8");
const viteManifestObject =
/** @type {Record<string, {file: string, name?: string, isDynamicEntry?: boolean}>} */ (
+425 -36
View File
@@ -1,3 +1,4 @@
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { gzipSync } from "node:zlib";
import {
@@ -9,47 +10,404 @@ import {
} from "node:fs/promises";
import path from "node:path";
import {
diffDependencyInventories,
flattenPnpmDependencyTree,
isValidSha512Integrity,
parsePnpmLockfilePackages,
supplyChainDigest,
validateDependencyReview,
validateLicensePolicy,
validateVulnerabilityReport,
verifySupplyChainCoherence,
} from "./lib/supply-chain.mjs";
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat().sort();
try {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat().sort();
} catch {
return [];
}
}
/** @param {string} file */
async function sha256File(file) {
return createHash("sha256").update(await readFile(file)).digest("hex");
}
/** @param {string[]} files */
async function digestFileSet(files) {
const rows = await Promise.all(
files.sort().map(async (file) => ({
path: file.replaceAll("\\", "/"),
sha256: await sha256File(file),
})),
);
return supplyChainDigest(rows);
}
/** @param {string} file @returns {Promise<Record<string, unknown> | null>} */
async function optionalJson(file) {
try {
return JSON.parse(await readFile(file, "utf8"));
} catch {
return null;
}
}
export async function buildDependencyInventory() {
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
const lockfileText = await readFile("pnpm-lock.yaml", "utf8");
const lockfileSha256 = createHash("sha256")
.update(lockfileText)
.digest("hex");
const listed = spawnSync(
"corepack",
["pnpm", "list", "--json", "--depth", "Infinity"],
{
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
},
);
if (listed.status !== 0) {
throw new Error(`pnpm dependency graph failed: ${listed.stderr}`);
}
const roots = JSON.parse(listed.stdout);
const root = roots[0];
const flattened = await flattenPnpmDependencyTree(
root,
packageJson.dependencies ?? {},
packageJson.devDependencies ?? {},
);
const lockRows = parsePnpmLockfilePackages(lockfileText);
const lockByIdentity = new Map(
lockRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const failures = [];
const dependencies = flattened.map((dependency) => {
const identity = `${dependency.name}@${dependency.version}`;
const lockRow = lockByIdentity.get(identity);
if (!lockRow) failures.push(`dependency missing from lockfile: ${identity}`);
if (lockRow && !isValidSha512Integrity(lockRow.integrity)) {
failures.push(`dependency has invalid sha512 integrity: ${identity}`);
}
return {
...dependency,
integrity: lockRow?.integrity ?? "missing",
};
});
const inventoryIds = new Set(
dependencies.map((dependency) => `${dependency.name}@${dependency.version}`),
);
for (const lockRow of lockRows) {
const identity = `${lockRow.name}@${lockRow.version}`;
if (!inventoryIds.has(identity)) {
failures.push(`transitive lockfile dependency omitted: ${identity}`);
}
}
if (failures.length > 0) {
throw new Error(failures.join("\n"));
}
return {
schemaVersion: 2,
packageManager: packageJson.packageManager,
lockfileSha256,
dependencyCount: dependencies.length,
directDependencyCount: dependencies.filter((entry) => entry.direct).length,
dependencies,
};
}
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
const lockfile = await readFile("pnpm-lock.yaml");
const outputFiles = await filesWithin("dist");
if (outputFiles.length === 0) {
throw new Error("dist is missing; run the production build first");
}
const outputs = await Promise.all(
outputFiles.map(async (outputFile) => {
const content = await readFile(outputFile);
const metadata = await stat(outputFile);
return {
path: outputFile,
path: outputFile.replaceAll("\\", "/"),
bytes: metadata.size,
gzipBytes: gzipSync(content).byteLength,
sha256: createHash("sha256").update(content).digest("hex"),
};
}),
);
const distDigest = supplyChainDigest(
outputs.map(({ path: outputPath, bytes, sha256 }) => ({
path: outputPath,
bytes,
sha256,
})),
);
const inventory = await buildDependencyInventory();
const licensePolicy = JSON.parse(
await readFile("config/security/dependency-policy.json", "utf8"),
);
const licenseResult = validateLicensePolicy(inventory, licensePolicy);
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
const baseline = await optionalJson(
"config/security/dependency-baseline.json",
);
const baselineApproval = await optionalJson(
"config/security/dependency-baseline.approval.json",
);
const dependencyEvidence = JSON.parse(
await readFile(
"config/security/dependency-change-evidence.json",
"utf8",
),
);
const skipsBaseline = process.argv.includes("--no-baseline");
const baselineFailures = [];
let dependencyDiff =
/** @type {ReturnType<typeof diffDependencyInventories>} */ ({
added: [],
removed: [],
changed: [],
upgrades: [],
});
let reviewResult =
/** @type {ReturnType<typeof validateDependencyReview>} */ ({
passed: skipsBaseline,
highRisk: [],
failures: skipsBaseline ? [] : ["dependency baseline unavailable"],
});
if (baseline && baselineApproval) {
const actualBaselineDigest = supplyChainDigest(baseline);
if (
baselineApproval.schemaVersion !== 1 ||
baselineApproval.snapshotDigest !== actualBaselineDigest ||
typeof baselineApproval.owner !== "string" ||
!baselineApproval.owner
) {
baselineFailures.push("dependency baseline approval digest mismatch");
}
dependencyDiff = diffDependencyInventories(baseline, inventory);
reviewResult = validateDependencyReview(
dependencyDiff,
inventory,
dependencyEvidence,
);
} else if (!skipsBaseline) {
baselineFailures.push("dependency baseline and approval are required");
}
const vulnerabilityPolicy = JSON.parse(
await readFile("config/security/vulnerability-policy.json", "utf8"),
);
const vulnerabilityExceptions = JSON.parse(
await readFile("config/security/vulnerability-exceptions.json", "utf8"),
);
const vulnerabilityInput = process.env.VULNERABILITY_REPORT_PATH
? await optionalJson(process.env.VULNERABILITY_REPORT_PATH)
: null;
const vulnerabilityResult = vulnerabilityInput
? validateVulnerabilityReport(
vulnerabilityInput,
vulnerabilityPolicy,
vulnerabilityExceptions,
inventory.lockfileSha256,
)
: {
passed: false,
failures: ["external vulnerability provider report is missing"],
blocking: [],
};
const vulnerabilityReport = {
schemaVersion: 1,
provider: vulnerabilityInput?.provider ?? "UNCONFIGURED",
scannedLockfileSha256:
vulnerabilityInput?.scannedLockfileSha256 ?? inventory.lockfileSha256,
status: vulnerabilityInput
? vulnerabilityResult.passed
? "PASS"
: "FAIL"
: "FAIL_UNVERIFIED",
findings: vulnerabilityInput?.findings ?? [],
exceptionsApplied:
vulnerabilityInput && vulnerabilityResult.passed
? vulnerabilityExceptions.exceptions
: [],
failures: vulnerabilityResult.failures,
blocking: vulnerabilityResult.blocking,
};
const sourceFiles = (
await Promise.all(
[
"src",
"scripts",
"config",
"public",
"schemas",
"package.json",
"pnpm-lock.yaml",
"vite.config.js",
].map(async (target) => {
try {
const metadata = await stat(target);
return metadata.isDirectory() ? filesWithin(target) : [target];
} catch {
return [];
}
}),
)
).flat();
const sourceSetSha256 = await digestFileSet(sourceFiles);
const components = inventory.dependencies.map((dependency) => ({
type: "library",
"bom-ref": `pkg:npm/${encodeURIComponent(dependency.name)}@${dependency.version}`,
name: dependency.name,
version: dependency.version,
scope: dependency.optional ? "optional" : "required",
hashes: [
{
alg: "SHA-512",
content: dependency.integrity.slice("sha512-".length),
},
],
licenses:
dependency.license === "NOASSERTION"
? [{ expression: "NOASSERTION" }]
: [{ expression: dependency.license }],
properties: [
{ name: "ca:direct", value: String(dependency.direct) },
{ name: "ca:scope", value: dependency.scope },
],
}));
const serialSeed = supplyChainDigest({
lockfileSha256: inventory.lockfileSha256,
components: components.map((component) => component["bom-ref"]),
});
const sbom = {
bomFormat: "CycloneDX",
specVersion: "1.6",
serialNumber: `urn:uuid:${serialSeed.slice(0, 8)}-${serialSeed.slice(8, 12)}-${serialSeed.slice(12, 16)}-${serialSeed.slice(16, 20)}-${serialSeed.slice(20, 32)}`,
version: 1,
metadata: {
component: {
type: "application",
name: packageJson.name,
version: packageJson.version,
},
properties: [
{
name: "ca:lockfileSha256",
value: inventory.lockfileSha256,
},
],
},
components,
dependencies: inventory.dependencies.map((dependency) => ({
ref: `pkg:npm/${encodeURIComponent(dependency.name)}@${dependency.version}`,
dependsOn: dependency.dependencies.map((identity) => {
const separator = identity.lastIndexOf("@");
return `pkg:npm/${encodeURIComponent(identity.slice(0, separator))}@${identity.slice(separator + 1)}`;
}),
})),
};
const provenance = {
_type: "https://in-toto.io/Statement/v1",
subject: [{ name: "dist", digest: { sha256: distDigest } }],
predicateType: "https://slsa.dev/provenance/v1",
predicate: {
buildDefinition: {
buildType: "https://vite.dev/build/v1",
externalParameters: {
nodeVersion: process.version,
packageManager: packageJson.packageManager,
},
internalParameters: {
sourceSetSha256,
},
resolvedDependencies: [
{
uri: "pnpm-lock.yaml",
digest: { sha256: inventory.lockfileSha256 },
},
],
},
runDetails: {
builder: { id: "local:clean-architecture-frontend-template" },
metadata: { invocationId: "LOCAL_UNSIGNED" },
},
materials: {
lockfileSha256: inventory.lockfileSha256,
sourceSetSha256,
sbomSha256: supplyChainDigest(sbom),
},
},
};
const coherence = verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
);
const attestationInput = process.env.PROVENANCE_ATTESTATION_PATH
? await optionalJson(process.env.PROVENANCE_ATTESTATION_PATH)
: null;
const attestationSubject =
/** @type {Record<string, unknown>} */ (
/** @type {Record<string, unknown>} */ (
attestationInput?.subject ?? {}
).digest ?? {}
);
const attestationPassed =
attestationSubject.sha256 === distDigest &&
typeof attestationInput?.provider === "string" &&
Boolean(attestationInput.provider) &&
typeof attestationInput?.signer === "string" &&
Boolean(attestationInput.signer);
const localFailures = [
...licenseResult.failures,
...baselineFailures,
...reviewResult.failures,
...coherence.failures,
];
if (vulnerabilityInput && !vulnerabilityResult.passed) {
localFailures.push(
...vulnerabilityResult.failures,
...vulnerabilityResult.blocking,
);
}
const localPassed = localFailures.length === 0;
const promotionPassed =
localPassed && vulnerabilityResult.passed && attestationPassed;
const verification = {
schemaVersion: 1,
localStatus: localPassed ? "PASS" : "FAIL",
promotionStatus: promotionPassed ? "PASS" : "FAIL_UNVERIFIED",
lockfileSha256: inventory.lockfileSha256,
sourceSetSha256,
distSha256: distDigest,
sbomSha256: supplyChainDigest(sbom),
dependencyDiff,
highRiskReview: reviewResult.highRisk,
vulnerabilityStatus: vulnerabilityReport.status,
provenanceAttestationStatus: attestationPassed
? "PASS"
: "FAIL_UNVERIFIED",
failures: localFailures,
};
const inventory = Object.entries(dependencies)
.sort(([left], [right]) => left.localeCompare(right))
.map(([name, version]) => ({ name, version, direct: true }));
await mkdir("artifacts/performance", { recursive: true });
await mkdir("artifacts/release", { recursive: true });
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/performance/bundle.json",
`${JSON.stringify(
@@ -59,7 +417,8 @@ await writeFile(
context: {
nodeVersion: process.version,
packageManager: packageJson.packageManager,
runnerImage: process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
runnerImage:
process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
},
outputs,
},
@@ -67,36 +426,66 @@ await writeFile(
2,
)}\n`,
);
await writeFile(
"artifacts/release/dependency-inventory.json",
`${JSON.stringify(
{
schemaVersion: 1,
lockfileSha256: createHash("sha256").update(lockfile).digest("hex"),
dependencies: inventory,
},
null,
2,
)}\n`,
`${JSON.stringify(inventory, null, 2)}\n`,
);
await writeFile(
"artifacts/release/sbom.cdx.json",
`${JSON.stringify(sbom, null, 2)}\n`,
);
await writeFile(
"artifacts/release/provenance.json",
`${JSON.stringify(provenance, null, 2)}\n`,
);
await writeFile(
"artifacts/release/checksums.txt",
`${outputs.map((output) => `${output.sha256} ${output.path}`).join("\n")}\n`,
);
await writeFile(
"artifacts/security/dependency-diff.json",
`${JSON.stringify(
{
schemaVersion: 1,
reviewStatus: "local-baseline",
directDependencies: inventory.length,
highRiskUnreviewed: [],
lockfileSha256: createHash("sha256").update(lockfile).digest("hex"),
schemaVersion: 2,
baselineDigest: baseline ? supplyChainDigest(baseline) : null,
currentDigest: supplyChainDigest(inventory),
...dependencyDiff,
highRisk: reviewResult.highRisk,
reviewFailures: reviewResult.failures,
},
null,
2,
)}\n`,
);
await writeFile(
"artifacts/security/license-report.json",
`${JSON.stringify(
{
schemaVersion: 1,
status: licenseResult.passed ? "PASS" : "FAIL",
dependencyCount: inventory.dependencyCount,
results: licenseResult.results,
failures: licenseResult.failures,
},
null,
2,
)}\n`,
);
await writeFile(
"artifacts/security/vulnerability-report.json",
`${JSON.stringify(vulnerabilityReport, null, 2)}\n`,
);
await writeFile(
"artifacts/security/supply-chain-verification.json",
`${JSON.stringify(verification, null, 2)}\n`,
);
if (!localPassed) {
process.stderr.write(
`Local supply-chain verification failed:\n- ${localFailures.join("\n- ")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Supply chain: LOCAL PASS (${inventory.dependencyCount} dependencies); promotion=${verification.promotionStatus}\n`,
);
+547
View File
@@ -0,0 +1,547 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
/** @param {unknown} value @returns {unknown} */
export function canonicalizeSupplyChainValue(value) {
if (Array.isArray(value)) {
return value
.map(canonicalizeSupplyChainValue)
.sort((left, right) =>
JSON.stringify(left).localeCompare(JSON.stringify(right)),
);
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, canonicalizeSupplyChainValue(item)]),
);
}
return value;
}
/** @param {unknown} value */
export function supplyChainDigest(value) {
return createHash("sha256")
.update(JSON.stringify(canonicalizeSupplyChainValue(value)))
.digest("hex");
}
/** @param {string} lockfile */
export function parsePnpmLockfilePackages(lockfile) {
const entries =
/** @type {Array<{name: string, version: string, integrity: string}>} */ (
[]
);
let inPackages = false;
/** @type {{name: string, version: string, integrity: string} | null} */
let current = null;
for (const line of lockfile.split(/\r?\n/)) {
if (line === "packages:") {
inPackages = true;
continue;
}
if (line === "snapshots:") {
if (current) entries.push(current);
break;
}
if (!inPackages) continue;
const packageMatch = line.match(/^ {2}(\S.*):$/);
if (packageMatch) {
if (current) entries.push(current);
const key = packageMatch[1].replace(/^['"]|['"]$/g, "");
const separator = key.lastIndexOf("@");
current = {
name: key.slice(0, separator),
version: key.slice(separator + 1),
integrity: "",
};
continue;
}
const integrityMatch = line.match(/\bintegrity:\s*([^,}\s]+)/);
if (current && integrityMatch) {
current.integrity = integrityMatch[1];
}
}
return entries.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
/** @param {string} integrity */
export function isValidSha512Integrity(integrity) {
if (!integrity.startsWith("sha512-")) return false;
try {
return Buffer.from(integrity.slice("sha512-".length), "base64").length === 64;
} catch {
return false;
}
}
/**
* @param {unknown} raw
* @returns {string}
*/
export function normalizeLicense(raw) {
if (typeof raw === "string" && raw.trim()) return raw.trim();
if (
raw &&
typeof raw === "object" &&
"type" in raw &&
typeof raw.type === "string"
) {
return raw.type;
}
if (Array.isArray(raw)) {
const licenses = raw.map(normalizeLicense).filter(
(license) => license !== "NOASSERTION",
);
return licenses.length > 0 ? licenses.join(" OR ") : "NOASSERTION";
}
return "NOASSERTION";
}
/**
* @param {Record<string, unknown>} root
* @param {Readonly<Record<string, string>>} directProduction
* @param {Readonly<Record<string, string>>} directDevelopment
*/
export async function flattenPnpmDependencyTree(
root,
directProduction,
directDevelopment,
) {
const records =
/** @type {Map<string, {
* name: string,
* version: string,
* direct: boolean,
* scope: "production" | "development",
* optional: boolean,
* packagePath: string,
* dependencies: Set<string>
* }>} */ (new Map());
const directIds = new Set();
for (const [name, rawDependency] of Object.entries(
/** @type {Record<string, unknown>} */ (root.dependencies ?? {}),
)) {
if (
Object.hasOwn(directProduction, name) &&
rawDependency &&
typeof rawDependency === "object" &&
!Array.isArray(rawDependency)
) {
directIds.add(
`${name}@${String(
/** @type {Record<string, unknown>} */ (rawDependency).version ?? "",
)}`,
);
}
}
for (const [name, rawDependency] of Object.entries(
/** @type {Record<string, unknown>} */ (root.devDependencies ?? {}),
)) {
if (
Object.hasOwn(directDevelopment, name) &&
rawDependency &&
typeof rawDependency === "object" &&
!Array.isArray(rawDependency)
) {
directIds.add(
`${name}@${String(
/** @type {Record<string, unknown>} */ (rawDependency).version ?? "",
)}`,
);
}
}
/**
* @param {Record<string, unknown>} node
* @param {"production" | "development"} scope
* @param {boolean} optionalPath
*/
function visit(node, scope, optionalPath) {
for (const [groupName, group] of Object.entries({
dependencies: node.dependencies,
devDependencies: node.devDependencies,
optionalDependencies: node.optionalDependencies,
})) {
if (!group || typeof group !== "object" || Array.isArray(group)) continue;
for (const [name, rawDependency] of Object.entries(group)) {
if (
!rawDependency ||
typeof rawDependency !== "object" ||
Array.isArray(rawDependency)
) {
continue;
}
const dependency =
/** @type {Record<string, unknown>} */ (rawDependency);
const version = String(dependency.version ?? "");
const packagePath = String(dependency.path ?? "");
const identity = `${name}@${version}`;
const childScope =
scope === "production" && groupName !== "devDependencies"
? "production"
: "development";
const childOptional =
optionalPath || groupName === "optionalDependencies";
const previous = records.get(identity);
const dependencies = previous?.dependencies ?? new Set();
for (const childGroup of [
dependency.dependencies,
dependency.optionalDependencies,
]) {
if (
!childGroup ||
typeof childGroup !== "object" ||
Array.isArray(childGroup)
) {
continue;
}
for (const [childName, rawChild] of Object.entries(childGroup)) {
if (
rawChild &&
typeof rawChild === "object" &&
!Array.isArray(rawChild)
) {
dependencies.add(
`${childName}@${String(rawChild.version ?? "")}`,
);
}
}
}
records.set(identity, {
name,
version,
direct: directIds.has(identity),
scope:
previous?.scope === "production" || childScope === "production"
? "production"
: "development",
optional: previous ? previous.optional && childOptional : childOptional,
packagePath: previous?.packagePath || packagePath,
dependencies,
});
visit(dependency, childScope, childOptional);
}
}
}
const productionRoot = {
dependencies: Object.fromEntries(
Object.entries(
/** @type {Record<string, unknown>} */ (root.dependencies ?? {}),
).filter(([name]) => Object.hasOwn(directProduction, name)),
),
};
const developmentRoot = {
devDependencies: Object.fromEntries(
Object.entries(
/** @type {Record<string, unknown>} */ (root.devDependencies ?? {}),
).filter(([name]) => Object.hasOwn(directDevelopment, name)),
),
};
visit(productionRoot, "production", false);
visit(developmentRoot, "development", false);
const result = [];
for (const record of records.values()) {
let license = "NOASSERTION";
let optional = record.optional;
if (record.packagePath) {
try {
const manifest = JSON.parse(
await readFile(`${record.packagePath}/package.json`, "utf8"),
);
license = normalizeLicense(manifest.license ?? manifest.licenses);
} catch {
// Platform-specific optional packages may not be materialized locally.
optional = true;
}
}
result.push({
name: record.name,
version: record.version,
direct: record.direct,
scope: record.scope,
optional,
license,
dependencies: [...record.dependencies].sort(),
});
}
return result.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
/**
* @param {Readonly<Record<string, unknown>>} before
* @param {Readonly<Record<string, unknown>>} after
*/
export function diffDependencyInventories(before, after) {
const beforeRows =
/** @type {Array<Record<string, unknown>>} */ (before.dependencies ?? []);
const afterRows =
/** @type {Array<Record<string, unknown>>} */ (after.dependencies ?? []);
const beforeMap = new Map(
beforeRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const afterMap = new Map(
afterRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const added = [...afterMap.keys()].filter((key) => !beforeMap.has(key));
const removed = [...beforeMap.keys()].filter((key) => !afterMap.has(key));
const changed = [];
for (const key of [...beforeMap.keys()].filter((item) => afterMap.has(item))) {
if (
supplyChainDigest(beforeMap.get(key)) !==
supplyChainDigest(afterMap.get(key))
) {
changed.push(key);
}
}
const upgrades = [];
for (const removedKey of removed) {
const previous = beforeMap.get(removedKey);
const replacement = added.find(
(addedKey) => afterMap.get(addedKey)?.name === previous?.name,
);
if (replacement) {
upgrades.push({
name: previous?.name,
from: previous?.version,
to: afterMap.get(replacement)?.version,
});
}
}
return Object.freeze({
added: Object.freeze(added.sort()),
removed: Object.freeze(removed.sort()),
changed: Object.freeze(changed.sort()),
upgrades: Object.freeze(
upgrades.sort((left, right) =>
String(left.name).localeCompare(String(right.name)),
),
),
});
}
/**
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} policy
*/
export function validateLicensePolicy(inventory, policy) {
const allowed = new Set(
/** @type {string[]} */ (policy.allowedLicenses ?? []),
);
const denied = /** @type {string[]} */ (policy.deniedLicensePatterns ?? []);
const failures = [];
const results = [];
for (const dependency of /** @type {Array<Record<string, unknown>>} */ (
inventory.dependencies ?? []
)) {
const license = String(dependency.license ?? "NOASSERTION");
const explicitlyDenied = denied.some((pattern) =>
new RegExp(pattern, "i").test(license),
);
const unknownAccepted =
license === "NOASSERTION" && dependency.optional === true;
const passed =
!explicitlyDenied && (allowed.has(license) || unknownAccepted);
results.push({
package: `${dependency.name}@${dependency.version}`,
license,
passed,
reason: unknownAccepted ? "platform-optional-not-materialized" : null,
});
if (!passed) {
failures.push(
`${dependency.name}@${dependency.version} has disallowed license ${license}`,
);
}
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
results: Object.freeze(results),
});
}
/**
* @param {ReturnType<typeof diffDependencyInventories>} diff
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} evidenceFile
*/
export function validateDependencyReview(diff, inventory, evidenceFile) {
const rows =
/** @type {Array<Record<string, unknown>>} */ (inventory.dependencies ?? []);
const byIdentity = new Map(
rows.map((row) => [`${row.name}@${row.version}`, row]),
);
const evidence = new Map(
/** @type {Array<Record<string, unknown>>} */ (
evidenceFile.changes ?? []
).map((entry) => [entry.changeId, entry]),
);
const highRisk = diff.added.filter((identity) => {
const row = byIdentity.get(identity);
return row?.direct === true && row.scope === "production";
});
const failures = [];
for (const identity of highRisk) {
const changeId = `add:${identity}`;
const entry = evidence.get(changeId);
if (!entry) {
failures.push(`high-risk dependency missing review: ${changeId}`);
continue;
}
for (const field of ["owner", "reviewer", "reason", "rollback"]) {
if (typeof entry[field] !== "string" || !entry[field].trim()) {
failures.push(`${changeId} missing ${field}`);
}
}
if (entry.owner === entry.reviewer) {
failures.push(`${changeId} may not be self-approved`);
}
}
return Object.freeze({
passed: failures.length === 0,
highRisk: Object.freeze(highRisk),
failures: Object.freeze(failures),
});
}
const severityRank = new Map([
["unknown", 0],
["low", 1],
["moderate", 2],
["high", 3],
["critical", 4],
]);
/**
* @param {Readonly<Record<string, unknown>>} report
* @param {Readonly<Record<string, unknown>>} policy
* @param {Readonly<Record<string, unknown>>} exceptionFile
* @param {string} lockfileSha256
* @param {Date} [now]
*/
export function validateVulnerabilityReport(
report,
policy,
exceptionFile,
lockfileSha256,
now = new Date(),
) {
const failures = [];
if (report.scannedLockfileSha256 !== lockfileSha256) {
failures.push("vulnerability report lockfile digest mismatch");
}
if (typeof report.provider !== "string" || !report.provider.trim()) {
failures.push("vulnerability report provider missing");
}
const threshold = severityRank.get(String(policy.blockAtSeverity)) ?? 3;
const exceptions =
/** @type {Array<Record<string, unknown>>} */ (
exceptionFile.exceptions ?? []
);
const blocking = [];
for (const finding of /** @type {Array<Record<string, unknown>>} */ (
report.findings ?? []
)) {
const severity = String(finding.severity ?? "unknown").toLowerCase();
if ((severityRank.get(severity) ?? 0) < threshold) continue;
const exception = exceptions.find(
(entry) =>
entry.vulnerabilityId === finding.id &&
entry.packageName === finding.packageName,
);
const expiry =
typeof exception?.expiresAt === "string"
? Date.parse(exception.expiresAt)
: Number.NaN;
const validException =
exception &&
typeof exception.owner === "string" &&
exception.owner.trim() &&
typeof exception.reviewer === "string" &&
exception.reviewer.trim() &&
exception.owner !== exception.reviewer &&
typeof exception.reason === "string" &&
exception.reason.trim() &&
Number.isFinite(expiry) &&
expiry > now.getTime();
if (!validException) {
blocking.push(
`${finding.id}:${finding.packageName}@${finding.version}:${severity}`,
);
}
}
return Object.freeze({
passed: failures.length === 0 && blocking.length === 0,
failures: Object.freeze(failures),
blocking: Object.freeze(blocking),
});
}
/**
* @param {Readonly<Record<string, unknown>>} sbom
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} provenance
* @param {string} distDigest
*/
export function verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
) {
const failures = [];
const componentCount = Array.isArray(sbom.components)
? sbom.components.length
: -1;
const dependencyCount = Array.isArray(inventory.dependencies)
? inventory.dependencies.length
: -2;
if (componentCount !== dependencyCount) {
failures.push("SBOM component count does not match inventory");
}
const metadata =
/** @type {Record<string, unknown>} */ (sbom.metadata ?? {});
const properties =
/** @type {Array<{name?: string, value?: string}>} */ (
metadata.properties ?? []
);
if (properties.find(
/** @param {{name?: string, value?: string}} property */
(property) =>
property.name === "ca:lockfileSha256" &&
property.value === inventory.lockfileSha256,
) === undefined) {
failures.push("SBOM lockfile digest does not match inventory");
}
const subject =
/** @type {Array<Record<string, unknown>>} */ (provenance.subject ?? [])[0];
const subjectDigest =
/** @type {Record<string, unknown>} */ (subject?.digest ?? {});
if (subjectDigest.sha256 !== distDigest) {
failures.push("provenance subject does not match built dist digest");
}
const predicate =
/** @type {Record<string, unknown>} */ (provenance.predicate ?? {});
const materials =
/** @type {Record<string, unknown>} */ (predicate.materials ?? {});
if (materials.lockfileSha256 !== inventory.lockfileSha256) {
failures.push("provenance lockfile material does not match inventory");
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
});
}
+149 -44
View File
@@ -1,10 +1,37 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
const scanRoots = ["src", "dist"];
const findings = /** @type {Array<{ruleId: string, file: string}>} */ ([]);
/** @param {string} name @param {string} fallback */
function argumentValue(name, fallback) {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1]
? process.argv[index + 1]
: fallback;
}
const policyPath = argumentValue(
"--policy",
"config/security/secret-scan-policy.json",
);
const artifactPath = argumentValue(
"--artifact",
"artifacts/security/scan.sarif",
);
const policy = JSON.parse(await readFile(policyPath, "utf8"));
const findings =
/** @type {Array<{
* ruleId: string,
* file: string,
* line: number,
* fingerprint: string
* }>} */ ([]);
const policyFailures = [];
const patterns = [
{ id: "private-key", expression: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g },
{
id: "private-key",
expression: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g,
},
{ id: "aws-access-key", expression: /\bAKIA[0-9A-Z]{16}\b/g },
{ id: "github-token", expression: /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g },
{
@@ -14,35 +41,101 @@ const patterns = [
},
];
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat();
/** @param {string} target @returns {Promise<string[]>} */
async function filesWithin(target) {
try {
const metadata = await stat(target);
if (metadata.isFile()) return [target];
const entries = await readdir(target, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const child = path.join(target, entry.name);
return entry.isDirectory() ? filesWithin(child) : [child];
}),
));
return nested.flat();
} catch {
return [];
}
}
for (const root of scanRoots) {
for (const scanFile of await filesWithin(root)) {
if (/\.(png|jpg|jpeg|gif|woff2?|zip)$/i.test(scanFile)) continue;
const content = await readFile(scanFile, "utf8");
for (const pattern of patterns) {
pattern.expression.lastIndex = 0;
if (pattern.expression.test(content)) {
findings.push({ ruleId: pattern.id, file: scanFile });
}
const excluded = new Set(
/** @type {string[]} */ (policy.excludedPaths ?? []).map((entry) =>
entry.replaceAll("\\", "/"),
),
);
const allowlist =
/** @type {Array<{
* path: string,
* ruleId: string,
* owner: string,
* reason: string,
* expiresAt: string
* }>} */ (policy.allowlist ?? []);
for (const entry of allowlist) {
const expiry = Date.parse(entry.expiresAt);
if (
!entry.path.startsWith("tests/") ||
!entry.owner?.trim() ||
!entry.reason?.trim() ||
!Number.isFinite(expiry) ||
expiry <= Date.now()
) {
policyFailures.push(
`invalid or expired secret allowlist entry: ${entry.path}:${entry.ruleId}`,
);
}
}
const roots = [
...(/** @type {string[]} */ (policy.trackedRoots ?? [])),
...(/** @type {string[]} */ (policy.generatedRoots ?? [])),
];
const scanFiles = (
await Promise.all(roots.map((root) => filesWithin(root)))
).flat();
for (const scanFile of [...new Set(scanFiles)].sort()) {
const normalized = scanFile.replaceAll("\\", "/");
if (
[...excluded].some(
(entry) => normalized === entry || normalized.startsWith(`${entry}/`),
) ||
/\.(?:png|jpe?g|gif|webp|woff2?|zip|gz|sarif)$/i.test(normalized)
) {
continue;
}
let content;
try {
content = await readFile(scanFile, "utf8");
} catch {
continue;
}
for (const pattern of patterns) {
pattern.expression.lastIndex = 0;
for (const match of content.matchAll(pattern.expression)) {
const isAllowed = allowlist.some(
(entry) =>
entry.path === normalized &&
entry.ruleId === pattern.id &&
Date.parse(entry.expiresAt) > Date.now(),
);
if (isAllowed) continue;
const prefix = content.slice(0, match.index);
findings.push({
ruleId: pattern.id,
file: normalized,
line: prefix.split(/\r?\n/).length,
fingerprint: createHash("sha256")
.update(`${pattern.id}:${normalized}:${String(match.index)}`)
.digest("hex"),
});
}
}
}
const sarif = {
version: "2.1.0",
$schema:
"https://json.schemastore.org/sarif-2.1.0.json",
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
runs: [
{
tool: {
@@ -54,29 +147,41 @@ const sarif = {
})),
},
},
results: findings.map((finding) => ({
ruleId: finding.ruleId,
message: { text: "Potential secret material must be removed." },
locations: [
{
physicalLocation: {
artifactLocation: { uri: finding.file },
},
results: [
...findings.map((finding) => ({
ruleId: finding.ruleId,
message: {
text: "Potential secret material must be removed.",
},
],
})),
partialFingerprints: {
primaryLocationLineHash: finding.fingerprint,
},
locations: [
{
physicalLocation: {
artifactLocation: { uri: finding.file },
region: { startLine: finding.line },
},
},
],
})),
...policyFailures.map((failure) => ({
ruleId: "invalid-allowlist",
message: { text: failure },
})),
],
},
],
};
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/scan.sarif",
`${JSON.stringify(sarif, null, 2)}\n`,
);
if (findings.length > 0) {
process.stderr.write(`Security scan found ${findings.length} blocking result(s).\n`);
await mkdir(path.dirname(artifactPath), { recursive: true });
await writeFile(artifactPath, `${JSON.stringify(sarif, null, 2)}\n`);
if (findings.length > 0 || policyFailures.length > 0) {
process.stderr.write(
`Security scan found ${findings.length + policyFailures.length} blocking result(s).\n`,
);
process.exit(1);
}
process.stdout.write("Source and built-asset secret scan: PASS\n");
process.stdout.write(
`Tracked source, config, built asset and artifact secret scan: PASS (${scanFiles.length} files)\n`,
);
+1
View File
@@ -18,6 +18,7 @@ const featureOwnedPaths = [
featureSource,
featureTests,
"tests/e2e/reference-form.spec.js",
"tests/e2e/reference-route.spec.js",
"tests/mocks",
];
const copyTargets = [
+47
View File
@@ -0,0 +1,47 @@
import { spawnSync } from "node:child_process";
import { readFile, writeFile } from "node:fs/promises";
import { supplyChainDigest } from "./lib/supply-chain.mjs";
const owner = process.env.DEPENDENCY_BASELINE_OWNER;
const reason = process.env.DEPENDENCY_BASELINE_REASON;
if (!owner?.trim() || !reason?.trim()) {
process.stderr.write(
"DEPENDENCY_BASELINE_OWNER and DEPENDENCY_BASELINE_REASON are required.\n",
);
process.exit(2);
}
const commands = /** @type {Array<[string, string[]]>} */ ([
["corepack", ["pnpm", "build"]],
["node", ["scripts/generate-supply-chain.mjs", "--no-baseline"]],
]);
for (const [command, args] of commands) {
const result = spawnSync(command, args, { stdio: "inherit" });
if (result.status !== 0) process.exit(result.status ?? 1);
}
const inventory = JSON.parse(
await readFile("artifacts/release/dependency-inventory.json", "utf8"),
);
await writeFile(
"config/security/dependency-baseline.json",
`${JSON.stringify(inventory, null, 2)}\n`,
);
await writeFile(
"config/security/dependency-baseline.approval.json",
`${JSON.stringify(
{
schemaVersion: 1,
snapshotDigest: supplyChainDigest(inventory),
owner,
reason,
approvedAt: new Date().toISOString(),
},
null,
2,
)}\n`,
);
process.stdout.write(
`Dependency baseline approved: ${inventory.dependencyCount} packages\n`,
);
+76
View File
@@ -0,0 +1,76 @@
import { spawnSync } from "node:child_process";
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { supplyChainDigest } from "./lib/supply-chain.mjs";
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat().sort();
}
async function distDigest() {
const rows = await Promise.all(
(await filesWithin("dist")).map(async (file) => ({
path: path.relative("dist", file).replaceAll("\\", "/"),
bytes: (await readFile(file)).byteLength,
content: supplyChainDigest(await readFile(file)),
})),
);
return supplyChainDigest(rows);
}
function build(environment = process.env) {
return spawnSync("corepack", ["pnpm", "build"], {
env: environment,
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
});
}
const deterministicEnvironment = {
...process.env,
SOURCE_DATE_EPOCH: "946684800",
};
const firstBuild = build(deterministicEnvironment);
const firstDigest = firstBuild.status === 0 ? await distDigest() : "BUILD_FAILED";
const secondBuild = build(deterministicEnvironment);
const secondDigest =
secondBuild.status === 0 ? await distDigest() : "BUILD_FAILED";
const restoreBuild = build();
const passed =
firstBuild.status === 0 &&
secondBuild.status === 0 &&
restoreBuild.status === 0 &&
firstDigest === secondDigest;
await mkdir("artifacts/release", { recursive: true });
await writeFile(
"artifacts/release/reproducible-build.json",
`${JSON.stringify(
{
schemaVersion: 1,
sourceDateEpoch: deterministicEnvironment.SOURCE_DATE_EPOCH,
firstDigest,
secondDigest,
restored: restoreBuild.status === 0,
status: passed ? "PASS" : "FAIL",
},
null,
2,
)}\n`,
);
if (!passed) {
process.stderr.write(
`Reproducible build failed: first=${firstDigest} second=${secondDigest}\n`,
);
process.exit(1);
}
process.stdout.write(`Reproducible build: PASS (${firstDigest})\n`);
+121
View File
@@ -0,0 +1,121 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import {
isValidSha512Integrity,
parsePnpmLockfilePackages,
supplyChainDigest,
verifySupplyChainCoherence,
} from "./lib/supply-chain.mjs";
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat().sort();
}
const inventory = JSON.parse(
await readFile("artifacts/release/dependency-inventory.json", "utf8"),
);
const sbom = JSON.parse(
await readFile("artifacts/release/sbom.cdx.json", "utf8"),
);
const provenance = JSON.parse(
await readFile("artifacts/release/provenance.json", "utf8"),
);
const verification = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
);
const lockfileText = await readFile("pnpm-lock.yaml", "utf8");
const lockfileSha256 = createHash("sha256")
.update(lockfileText)
.digest("hex");
const outputs = await Promise.all(
(await filesWithin("dist")).map(async (file) => {
const content = await readFile(file);
return {
path: file.replaceAll("\\", "/"),
bytes: (await stat(file)).size,
sha256: createHash("sha256").update(content).digest("hex"),
};
}),
);
const distDigest = supplyChainDigest(outputs);
const coherence = verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
);
const failures = [...coherence.failures];
if (
inventory.lockfileSha256 !== lockfileSha256 ||
verification.lockfileSha256 !== lockfileSha256
) {
failures.push("inventory/verification lockfile digest mismatch");
}
if (
verification.distSha256 !== distDigest ||
verification.sbomSha256 !== supplyChainDigest(sbom)
) {
failures.push("verification digest set is incoherent");
}
const lockRows = parsePnpmLockfilePackages(lockfileText);
const inventoryRows =
/** @type {Array<Record<string, unknown>>} */ (
inventory.dependencies ?? []
);
const inventoryByIdentity = new Map(
inventoryRows.map((entry) => [
`${entry.name}@${entry.version}`,
entry,
]),
);
if (lockRows.length !== inventoryRows.length) {
failures.push("transitive dependency count differs from lockfile");
}
for (const lockRow of lockRows) {
const identity = `${lockRow.name}@${lockRow.version}`;
const dependency = inventoryByIdentity.get(identity);
if (
!dependency ||
dependency.integrity !== lockRow.integrity ||
!isValidSha512Integrity(lockRow.integrity)
) {
failures.push(`lockfile inventory integrity mismatch: ${identity}`);
}
}
const report = {
schemaVersion: 1,
status: failures.length === 0 ? "PASS" : "FAIL",
dependencyCount: inventoryRows.length,
lockfileSha256,
distSha256: distDigest,
sbomSha256: supplyChainDigest(sbom),
failures,
};
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/supply-chain-coherence.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (failures.length > 0) {
process.stderr.write(
`Supply-chain artifact coherence failed:\n- ${failures.join("\n- ")}\n`,
);
process.exit(1);
}
process.stdout.write(
`Supply-chain artifact coherence: PASS (${inventoryRows.length} dependencies)\n`,
);
+30
View File
@@ -0,0 +1,30 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
const verification = JSON.parse(
await readFile(
"artifacts/security/supply-chain-verification.json",
"utf8",
),
);
const passed = verification.promotionStatus === "PASS";
const report = {
schemaVersion: 1,
status: passed ? "PASS" : "FAIL_UNVERIFIED",
vulnerabilityStatus: verification.vulnerabilityStatus,
provenanceAttestationStatus:
verification.provenanceAttestationStatus,
lockfileSha256: verification.lockfileSha256,
distSha256: verification.distSha256,
};
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/promotion-verification.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (!passed) {
process.stderr.write(
"Supply-chain promotion is FAIL_UNVERIFIED: external vulnerability and signed provenance evidence are required.\n",
);
process.exit(1);
}
process.stdout.write("Supply-chain promotion evidence: PASS\n");