115 lines
4.2 KiB
JavaScript
115 lines
4.2 KiB
JavaScript
import console from "node:console";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const rootManifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
const components = [];
|
|
|
|
function npmPurl(name, version) {
|
|
if (name.startsWith("@")) {
|
|
const slash = name.indexOf("/");
|
|
return `pkg:npm/%40${encodeURIComponent(name.slice(1, slash))}/${encodeURIComponent(name.slice(slash + 1))}@${version}`;
|
|
}
|
|
return `pkg:npm/${encodeURIComponent(name)}@${version}`;
|
|
}
|
|
|
|
for (const entry of readdirSync(join(root, "packages"), { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue;
|
|
const file = join(root, "packages", entry.name, "package.json");
|
|
try {
|
|
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
|
if (!manifest.name?.startsWith("@wrnexus/")) continue;
|
|
components.push({
|
|
type: "library",
|
|
name: manifest.name,
|
|
version: manifest.version,
|
|
"bom-ref": npmPurl(manifest.name, manifest.version),
|
|
purl: npmPurl(manifest.name, manifest.version),
|
|
properties: [
|
|
{ name: "wrnexus:workspace", value: "true" },
|
|
{ name: "wrnexus:private", value: String(manifest.publishConfig?.access !== "public") },
|
|
],
|
|
});
|
|
} catch {
|
|
// Not a workspace package.
|
|
}
|
|
}
|
|
|
|
// Bun's text lockfile is JSON with optional trailing commas. Package tuples use
|
|
// ["name@version", resolved, metadata, integrity], which is enough to include
|
|
// every resolved transitive dependency without requiring node_modules.
|
|
try {
|
|
const lockSource = readFileSync(join(root, "bun.lock"), "utf8").replace(/,\s*([}\]])/g, "$1");
|
|
const lock = JSON.parse(lockSource);
|
|
const external = new Map();
|
|
for (const value of Object.values(lock.packages ?? {})) {
|
|
if (!Array.isArray(value) || typeof value[0] !== "string") continue;
|
|
const specifier = value[0];
|
|
const split = specifier.lastIndexOf("@");
|
|
if (split <= 0) continue;
|
|
const name = specifier.slice(0, split);
|
|
const version = specifier.slice(split + 1);
|
|
if (!name || !version || name.startsWith("@wrnexus/")) continue;
|
|
const ref = npmPurl(name, version);
|
|
const component = {
|
|
type: "library",
|
|
name,
|
|
version,
|
|
"bom-ref": ref,
|
|
purl: ref,
|
|
properties: [{ name: "wrnexus:dependency-source", value: "bun.lock" }],
|
|
};
|
|
const integrity = typeof value[3] === "string" ? value[3] : "";
|
|
if (integrity.startsWith("sha512-")) {
|
|
component.hashes = [{ alg: "SHA-512", content: integrity.slice("sha512-".length) }];
|
|
}
|
|
external.set(ref, component);
|
|
}
|
|
components.push(...external.values());
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Unable to parse bun.lock for the SBOM: ${error instanceof Error ? error.message : String(error)}`,
|
|
{ cause: error },
|
|
);
|
|
}
|
|
|
|
components.sort((a, b) => {
|
|
const left = `${a.name}@${a.version}`;
|
|
const right = `${b.name}@${b.version}`;
|
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
});
|
|
|
|
function deterministicUuid(value) {
|
|
const hash = createHash("sha256").update(value).digest("hex").slice(0, 32).split("");
|
|
hash[12] = "5";
|
|
hash[16] = ((Number.parseInt(hash[16], 16) & 0x3) | 0x8).toString(16);
|
|
const hex = hash.join("");
|
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
}
|
|
|
|
const version = String(rootManifest.version);
|
|
const identity = JSON.stringify({ framework: "WRNexusJS", version, components });
|
|
const document = {
|
|
bomFormat: "CycloneDX",
|
|
specVersion: "1.5",
|
|
serialNumber: `urn:uuid:${deterministicUuid(identity)}`,
|
|
version: 1,
|
|
metadata: {
|
|
component: {
|
|
type: "framework",
|
|
name: "WRNexusJS",
|
|
version,
|
|
"bom-ref": `pkg:generic/WRNexusJS@${version}`,
|
|
},
|
|
},
|
|
components,
|
|
};
|
|
const reportDir = join(root, ".wrnexus", "reports");
|
|
mkdirSync(reportDir, { recursive: true });
|
|
const file = join(reportDir, `SBOM-${version}.cdx.json`);
|
|
writeFileSync(file, `${JSON.stringify(document, null, 2)}\n`);
|
|
console.log(`Generated ${file} with ${components.length} workspace and transitive components.`);
|