84 lines
2.7 KiB
JavaScript
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();
|