#!/usr/bin/env node import { execFileSync } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; import { resolve, relative } from "node:path"; const root = resolve(import.meta.dirname, ".."); const outputFlag = process.argv.indexOf("--output"); const output = resolve(root, outputFlag >= 0 ? process.argv[outputFlag + 1] : "release-artifacts"); const sha256 = (value) => createHash("sha256").update(value).digest("hex"); const command = (program, args) => execFileSync(program, args, { cwd: root, encoding: "utf8", windowsHide: true }).trim(); const pnpmOutput = process.env.npm_execpath ? command(process.execPath, [process.env.npm_execpath, "list", "--prod", "--json", "--depth", "Infinity"]) : command("pnpm", ["list", "--prod", "--json", "--depth", "Infinity"]); const listed = JSON.parse(pnpmOutput); const components = new Map(); const collect = (dependencies = {}) => { for (const [name, dependency] of Object.entries(dependencies)) { if (dependency.version) components.set(`${name}@${dependency.version}`, { type: "library", name, version: dependency.version, purl: `pkg:npm/${encodeURIComponent(name)}@${dependency.version}` }); collect(dependency.dependencies); } }; for (const project of listed) collect(project.dependencies); const packageJson = JSON.parse(await readFile(resolve(root, "package.json"), "utf8")); const serialNumber = `urn:uuid:${randomUUID()}`; const sbom = { bomFormat: "CycloneDX", specVersion: "1.6", serialNumber, version: 1, metadata: { component: { type: "application", name: packageJson.name, version: packageJson.version }, tools: { components: [{ type: "application", name: "ProjectBrain release metadata generator", version: "1.0.0" }] } }, components: [...components.values()].sort((left, right) => left.purl.localeCompare(right.purl)), }; const artifactFiles = []; const collectFiles = async (directory) => { for (const entry of await readdir(directory, { withFileTypes: true })) { const path = resolve(directory, entry.name); if (entry.isDirectory()) await collectFiles(path); else artifactFiles.push({ name: relative(root, path).replaceAll("\\", "/"), digest: { sha256: sha256(await readFile(path)) }, bytes: (await stat(path)).size }); } }; await collectFiles(resolve(root, "dist")); for (const name of ["Dockerfile.frontend", "Dockerfile.backend", "Dockerfile.unraid", "pnpm-lock.yaml"]) { const path = resolve(root, name); try { artifactFiles.push({ name, digest: { sha256: sha256(await readFile(path)) }, bytes: (await stat(path)).size }); } catch (error) { if (error.code !== "ENOENT") throw error; } } artifactFiles.sort((left, right) => left.name.localeCompare(right.name)); const revision = command("git", ["rev-parse", "HEAD"]); const provenance = { predicateType: "https://slsa.dev/provenance/v1", subject: artifactFiles, predicate: { buildDefinition: { buildType: "https://projectbrain.local/build/v1", externalParameters: { node: process.version, packageManager: packageJson.packageManager, revision }, resolvedDependencies: [{ uri: "git+local:projectbrain", digest: { sha1: revision } }] }, runDetails: { builder: { id: "https://projectbrain.local/ci" }, metadata: { invocationId: process.env.GITHUB_RUN_ID ?? `local-${revision.slice(0, 12)}`, startedOn: new Date().toISOString(), sourceDirty: command("git", ["status", "--porcelain"]) !== "" } }, }, sbom: { file: "projectbrain.cdx.json", sha256: sha256(JSON.stringify(sbom)) }, }; await mkdir(output, { recursive: true }); await writeFile(resolve(output, "projectbrain.cdx.json"), `${JSON.stringify(sbom, null, 2)}\n`); await writeFile(resolve(output, "projectbrain.provenance.json"), `${JSON.stringify(provenance, null, 2)}\n`); console.log(`Generated ${components.size} component SBOM and provenance for ${artifactFiles.length} artifacts in ${output}.`);