Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/generate-sbom.mjs
T

84 lines
2.7 KiB
JavaScript

#!/usr/bin/env node
// Generates a lightweight SBOM-style dependency manifest by shelling out to
// `pnpm list -r --json --depth=Infinity` and writing the result to
// `sbom-pnpm.json` in the repo root.
//
// Why pnpm-list instead of @cyclonedx/cyclonedx-npm:
// - cyclonedx-npm is opinionated about lockfile shape and tends to mis-handle
// pnpm workspaces (apps/*, packages/*). pnpm's own resolver is the source of
// truth, so we capture its output verbatim.
// - The artifact is consumed for audit traceability (which version of which
// package shipped in this image), not for CycloneDX validator gating. JSON
// tree is sufficient.
//
// The script is invoked from the Docker `build` stage and the resulting JSON
// is COPYed into each runtime stage so that the SBOM ships alongside the app.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, "..");
const OUTPUT_PATH = process.env.SBOM_OUTPUT_PATH
? path.resolve(process.env.SBOM_OUTPUT_PATH)
: path.join(ROOT, "sbom-pnpm.json");
function runPnpmList() {
const result = spawnSync(
"pnpm",
["list", "-r", "--json", "--depth=Infinity"],
{ cwd: ROOT, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 },
);
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
const stderr = result.stderr?.toString() ?? "";
throw new Error(`pnpm list exited with status ${result.status}: ${stderr}`);
}
return result.stdout?.toString() ?? "";
}
function parseTree(raw) {
if (!raw.trim()) {
return [];
}
try {
return JSON.parse(raw);
} catch (err) {
throw new Error(`pnpm list output was not valid JSON: ${(err && err.message) || err}`);
}
}
function repoVersion() {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8"));
return { name: pkg.name ?? null, version: pkg.version ?? null };
} catch {
return { name: null, version: null };
}
}
function main() {
const raw = runPnpmList();
const tree = parseTree(raw);
const repo = repoVersion();
const document = {
schema: "toolctrl.sbom-pnpm/v1",
generatedAt: new Date().toISOString(),
generator: "scripts/generate-sbom.mjs",
source: "pnpm list -r --json --depth=Infinity",
repo,
workspaces: tree,
};
fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(document, null, 2));
// eslint-disable-next-line no-console
console.log(`SBOM written: ${OUTPUT_PATH} (${tree.length} workspace entries)`);
}
main();