122 lines
3.5 KiB
JavaScript
122 lines
3.5 KiB
JavaScript
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`,
|
|
);
|