import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { gzipSync } from "node:zlib"; import { mkdir, readFile, readdir, stat, writeFile, } from "node:fs/promises"; import path from "node:path"; import { diffDependencyInventories, flattenPnpmDependencyTree, isValidSha512Integrity, parsePnpmLockfilePackages, supplyChainDigest, validateDependencyReview, validateLicensePolicy, validateVulnerabilityReport, verifySupplyChainCoherence, type DependencyInventoryDiff, } from "./lib/supply-chain.ts"; type Document = Record; function isRecord(value: unknown): value is Document { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function documentValue(value: unknown, label: string): Document { if (!isRecord(value)) throw new Error(`${label} must be a JSON object`); return value; } function stringMap(value: unknown): Record { if (!isRecord(value)) return {}; return Object.fromEntries( Object.entries(value).filter( (entry): entry is [string, string] => typeof entry[1] === "string", ), ); } async function jsonDocument(file: string): Promise { const parsed: unknown = JSON.parse(await readFile(file, "utf8")); return documentValue(parsed, file); } async function filesWithin(directory: string): Promise { try { const entries = await readdir(directory, { withFileTypes: true }); const nested: 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 []; } } async function sha256File(file: string): Promise { return createHash("sha256").update(await readFile(file)).digest("hex"); } async function digestFileSet(files: string[]): Promise { const rows = await Promise.all( files.sort().map(async (file) => ({ path: file.replaceAll("\\", "/"), sha256: await sha256File(file), })), ); return supplyChainDigest(rows); } async function optionalJson(file: string): Promise { try { const parsed: unknown = JSON.parse(await readFile(file, "utf8")); return isRecord(parsed) ? parsed : null; } catch { return null; } } export async function buildDependencyInventory() { const packageJson = await jsonDocument("package.json"); 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: unknown = JSON.parse(listed.stdout); const root = Array.isArray(roots) && isRecord(roots[0]) ? roots[0] : null; if (!root) throw new Error("pnpm dependency graph root is invalid"); const flattened = await flattenPnpmDependencyTree( root, stringMap(packageJson.dependencies), stringMap(packageJson.devDependencies), ); const lockRows = parsePnpmLockfilePackages(lockfileText); const lockByIdentity = new Map( lockRows.map((row) => [`${row.name}@${row.version}`, row]), ); const failures: string[] = []; 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: String(packageJson.packageManager ?? ""), lockfileSha256, dependencyCount: dependencies.length, directDependencyCount: dependencies.filter((entry) => entry.direct).length, dependencies, }; } const packageJson = await jsonDocument("package.json"); 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.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 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: string[] = []; let dependencyDiff: DependencyInventoryDiff = Object.freeze({ added: Object.freeze([]), removed: Object.freeze([]), changed: Object.freeze([]), upgrades: Object.freeze([]), }); let reviewResult: ReturnType = Object.freeze({ passed: skipsBaseline, highRisk: Object.freeze([]), failures: Object.freeze( 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.ts", ].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: String(packageJson.name ?? ""), version: String(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: String(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 attestation = isRecord(attestationInput?.subject) ? attestationInput.subject : {}; const attestationSubject = isRecord(attestation.digest) ? attestation.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, }; 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( { schemaVersion: 1, generatedAt: new Date().toISOString(), context: { nodeVersion: process.version, packageManager: String(packageJson.packageManager ?? ""), runnerImage: process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`, }, outputs, }, null, 2, )}\n`, ); await writeFile( "artifacts/release/dependency-inventory.json", `${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: 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`, );