import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { mkdir, readFile, writeFile, } from "node:fs/promises"; import { bundleOutputInventoryArtifactSchema, buildManifestArtifactSchema, dependencyDiffArtifactSchema, dependencyInventoryArtifactSchema, licenseReportArtifactSchema, provenanceArtifactSchema, sbomArtifactSchema, supplyChainVerificationArtifactSchema, vulnerabilityReportArtifactSchema, } from "./contracts/release-artifacts.ts"; import { flattenPnpmDependencyTree, isValidSha512Integrity, parsePnpmLockfilePackages, supplyChainDigest, verifySupplyChainCoherence, } from "./lib/supply-chain.ts"; import { createLocalVulnerabilityReport, distChecksumsText, LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS, recomputeDependencyEvidence, recomputeLicenseEvidence, } from "./lib/local-policy-evidence.ts"; import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts"; import { digestReleaseInputFiles } from "./lib/release-input-evidence.ts"; import { buildRepositoryFileInventory, parseRepositoryFileInventoryPolicy, } from "./lib/repository-file-inventory.ts"; import { collectDistOutputs, distSha256 } from "./lib/release-candidate.ts"; import { deterministicSupplyChainGeneratedAt } from "./lib/supply-chain-time.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 sha256File(file: string): Promise { return createHash("sha256").update(await readFile(file)).digest("hex"); } 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 buildManifest = buildManifestArtifactSchema.parse( await jsonDocument("artifacts/release/build-manifest.json"), ); const secretScanPolicy = documentValue( JSON.parse(await readFile("config/security/secret-scan-policy.json", "utf8")), "secret scan policy", ); const inventoryPolicy = parseRepositoryFileInventoryPolicy(secretScanPolicy); const repositoryInventory = await buildRepositoryFileInventory({ trackedRoots: inventoryPolicy.trackedRoots, generatedRoots: inventoryPolicy.generatedRoots, optionalRoots: inventoryPolicy.optionalRoots, }); const outputs = await collectDistOutputs(); const distDigest = distSha256(outputs); const inventory = await buildDependencyInventory(); const licensePolicy = JSON.parse( await readFile("config/security/dependency-policy.json", "utf8"), ); 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 dependencyPolicy = recomputeDependencyEvidence({ inventory, baseline, baselineApproval, dependencyChangeEvidence: dependencyEvidence, skipBaseline: skipsBaseline, }); const licenseEvidence = recomputeLicenseEvidence({ inventory, policy: licensePolicy, }); const vulnerabilityReport = createLocalVulnerabilityReport( inventory.lockfileSha256, ); const sourceFiles = [...repositoryInventory.trackedFiles]; const sourceSetSha256 = await digestReleaseInputFiles(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:tech-log-frontend" }, metadata: { invocationId: "LOCAL_UNSIGNED" }, }, materials: { lockfileSha256: inventory.lockfileSha256, sourceSetSha256, sbomSha256: supplyChainDigest(sbom), }, }, }; const coherence = verifySupplyChainCoherence( sbom, inventory, provenance, distDigest, ); const localFailures = [ ...licenseEvidence.failures, ...dependencyPolicy.failures, ...coherence.failures, ]; const localPassed = localFailures.length === 0; const verification = { schemaVersion: 1, localStatus: localPassed ? "PASS" : "FAIL", ...LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS, lockfileSha256: inventory.lockfileSha256, sourceSetSha256, distSha256: distDigest, sbomSha256: supplyChainDigest(sbom), dependencyDiff: dependencyPolicy.dependencyDiff, highRiskReview: dependencyPolicy.highRisk, failures: localFailures, }; const bundleReport = { schemaVersion: 1, generatedAt: deterministicSupplyChainGeneratedAt({ generatedAt: buildManifest.generatedAt, sourceDateEpoch: buildManifest.buildContext.sourceDateEpoch, }), context: { nodeVersion: process.version, packageManager: String(packageJson.packageManager ?? ""), runnerImage: process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`, }, outputs, }; const dependencyDiffReport = dependencyPolicy.report; const licenseReport = licenseEvidence.report; await mkdir("artifacts/performance", { recursive: true }); await mkdir("artifacts/release", { recursive: true }); await mkdir("artifacts/security", { recursive: true }); await writeValidatedJsonArtifact({ path: "artifacts/performance/bundle.json", schema: bundleOutputInventoryArtifactSchema, value: bundleReport, }); await writeValidatedJsonArtifact({ path: "artifacts/release/dependency-inventory.json", schema: dependencyInventoryArtifactSchema, value: inventory, }); await writeValidatedJsonArtifact({ path: "artifacts/release/sbom.cdx.json", schema: sbomArtifactSchema, value: sbom, }); await writeValidatedJsonArtifact({ path: "artifacts/release/provenance.json", schema: provenanceArtifactSchema, value: provenance, }); await writeFile( "artifacts/release/checksums.txt", distChecksumsText(outputs), ); await writeValidatedJsonArtifact({ path: "artifacts/security/dependency-diff.json", schema: dependencyDiffArtifactSchema, value: dependencyDiffReport, }); await writeValidatedJsonArtifact({ path: "artifacts/security/license-report.json", schema: licenseReportArtifactSchema, value: licenseReport, }); await writeValidatedJsonArtifact({ path: "artifacts/security/vulnerability-report.json", schema: vulnerabilityReportArtifactSchema, value: vulnerabilityReport, }); await writeValidatedJsonArtifact({ path: "artifacts/security/supply-chain-verification.json", schema: supplyChainVerificationArtifactSchema, value: verification, }); 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`, );