49 lines
3.4 KiB
JavaScript
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`);
|