Files
workinf_Blender_Wasm/tools/web/generate-sbom.mjs
2026-08-12 15:23:35 -04:00

49 lines
3.4 KiB
JavaScript

import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const lockPath = path.join(root, "web/package-lock.json");
const outputPath = path.join(root, "docs/web/sbom.spdx.json");
const lockBytes = fs.readFileSync(lockPath);
const lock = JSON.parse(lockBytes);
const noticesBytes = fs.readFileSync(path.join(root, "docs/web/third-party-notices.json"));
const notices = JSON.parse(noticesBytes);
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const spdxId = (value) => `SPDXRef-${value.replace(/[^A-Za-z0-9.-]/g, "-")}-${sha256(value).slice(0, 12)}`;
const packages = [];
const rootId = "SPDXRef-Package-blender-web-editor";
packages.push({ SPDXID: rootId, name: lock.name, versionInfo: lock.version, downloadLocation: "NOASSERTION", filesAnalyzed: false, licenseConcluded: "NOASSERTION", licenseDeclared: "NOASSERTION", copyrightText: "NOASSERTION", checksums: [{ algorithm: "SHA256", checksumValue: sha256(lockBytes) }] });
for (const [packagePath, entry] of Object.entries(lock.packages).sort(([left], [right]) => left.localeCompare(right))) {
if (!packagePath || !entry.version) continue;
const marker = packagePath.lastIndexOf("node_modules/");
const name = entry.name ?? packagePath.slice(marker + "node_modules/".length);
const item = { SPDXID: spdxId(`npm-${packagePath}`), name, versionInfo: entry.version, downloadLocation: entry.resolved ?? "NOASSERTION", filesAnalyzed: false, licenseConcluded: "NOASSERTION", licenseDeclared: "NOASSERTION", copyrightText: "NOASSERTION", externalRefs: [{ referenceCategory: "PACKAGE-MANAGER", referenceType: "purl", referenceLocator: `pkg:npm/${encodeURIComponent(name)}@${entry.version}` }] };
if (typeof entry.integrity === "string" && entry.integrity.startsWith("sha512-")) item.checksums = [{ algorithm: "SHA512", checksumValue: Buffer.from(entry.integrity.slice(7), "base64").toString("hex") }];
packages.push(item);
}
for (const notice of notices.packages.filter((item) => !item.source.includes("node_modules"))) {
packages.push({ SPDXID: spdxId(`vendored-${notice.name}-${notice.version}`), name: notice.name, versionInfo: notice.version, downloadLocation: "NOASSERTION", filesAnalyzed: false, licenseConcluded: "NOASSERTION", licenseDeclared: notice.license, copyrightText: "NOASSERTION", sourceInfo: notice.source });
}
packages.sort((left, right) => left.SPDXID.localeCompare(right.SPDXID));
const document = {
spdxVersion: "SPDX-2.3",
dataLicense: "CC0-1.0",
SPDXID: "SPDXRef-DOCUMENT",
name: "blender-web-editor-sbom",
documentNamespace: `https://blender-web.local/spdx/${sha256(Buffer.concat([lockBytes, noticesBytes]))}`,
creationInfo: { created: "1970-01-01T00:00:00Z", creators: ["Tool: tools/web/generate-sbom.mjs"] },
documentDescribes: [rootId],
packages,
relationships: packages.filter((item) => item.SPDXID !== rootId).map((item) => ({ spdxElementId: rootId, relationshipType: "DEPENDS_ON", relatedSpdxElement: item.SPDXID })),
};
fs.writeFileSync(outputPath, `${JSON.stringify(document, null, 2)}\n`);
assert.equal(new Set(packages.map((item) => item.SPDXID)).size, packages.length);
assert.ok(packages.length >= Object.keys(lock.packages).length);
process.stdout.write(`sbom-ok packages=${packages.length} sha256=${sha256(fs.readFileSync(outputPath))}\n`);