feat: verify frontend supply chain
This commit is contained in:
@@ -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`,
|
||||
);
|
||||
Reference in New Issue
Block a user