tools/sbom.mjs
All files · Open raw file
7983 bytes · SHA-256: 5fa4248d7f678dfe7537ad70d55b32f8dcaa468ca42e76ea89c84db9210f99ff
// Copyright 2026 Richard Andresik. SPDX-License-Identifier: Apache-2.0
// Build-time inventory: installed Debian packages, Node, npm/Yarn package
// metadata and every published review file. Unknown licenses stay NOASSERTION.
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("../", import.meta.url));
const lock = JSON.parse(fs.readFileSync(path.join(root, "container-lock.json")));
const output = process.argv[2];
assert.equal(output, "/opt/icyzip-inventory", "Inventory output is the fixed image directory");
const hash = bytes => createHash("sha256").update(bytes).digest("hex");
const json = filename => JSON.parse(fs.readFileSync(filename));
const sources = lock.files.map(name => {
const filename = path.join(root, name);
assert.equal(fs.realpathSync(filename), filename);
const body = fs.readFileSync(filename);
return { path: name, bytes: body.length, sha256: hash(body) };
});
const sourceId = hash(sources.map(file => file.sha256 + " " + file.path + "\n").join(""));
const packages = [];
const licenses = {};
const relationships = [];
function add(name, version, location, license = "NOASSERTION", extra = {}) {
const id = "SPDXRef-Package-" + hash(name + "\n" + version + "\n" + location).slice(0, 24);
packages.push({ name, SPDXID: id, versionInfo: version, downloadLocation: "NOASSERTION",
filesAnalyzed: false, licenseConcluded: "NOASSERTION", licenseDeclared: license,
copyrightText: "NOASSERTION", sourceInfo: location, ...extra });
relationships.push({ spdxElementId: "SPDXRef-DOCUMENT", relationshipType: "DESCRIBES", relatedSpdxElement: id });
return id;
}
const rows = execFileSync("dpkg-query", ["-W", "-f=${binary:Package}\t${Version}\t${Architecture}\t${db:Status-Status}\t${source:Package}\t${source:Version}\n"],
{ encoding: "utf8" }).trim().split("\n").sort();
for (const row of rows) {
const [name, version, architecture, status, sourceName, sourceVersion] = row.split("\t");
if (status !== "installed") continue;
const basename = name.split(":")[0];
const copyright = "/usr/share/doc/" + basename + "/copyright";
licenses["deb:" + name] = fs.existsSync(copyright) ? [copyright] : [];
add(basename, version, "Debian " + lock.debianRelease + " snapshot " + lock.debianSnapshot + "; " + copyright
+ "; corresponding source: https://snapshot.debian.org/package/" + encodeURIComponent(sourceName)
+ "/" + encodeURIComponent(sourceVersion) + "/",
"NOASSERTION", { externalRefs: [{ referenceCategory: "PACKAGE-MANAGER", referenceType: "purl",
referenceLocator: "pkg:deb/debian/" + encodeURIComponent(basename) + "@" + encodeURIComponent(version)
+ "?arch=" + architecture + "&distro=debian-" + lock.debianVersion }] });
}
const nodeLicense = ["/usr/local/LICENSE", "/usr/local/share/doc/node/LICENSE"].find(name => fs.existsSync(name));
assert.ok(nodeLicense, "Node license must be retained from the official base image");
licenses.node = [nodeLicense];
add("node", process.versions.node, "Official Node image " + lock.base + "; " + nodeLicense, "MIT",
{ checksums: [{ algorithm: "SHA256", checksumValue: hash(fs.readFileSync(process.execPath)) }],
externalRefs: [{ referenceCategory: "PACKAGE-MANAGER", referenceType: "purl",
referenceLocator: "pkg:generic/node@" + process.versions.node }] });
for (const [name, version] of Object.entries(process.versions).sort()) {
if (name === "node") continue;
add("node-component:" + name, version, "Bundled in Node; component notices: " + nodeLicense);
}
function nodePackages(directory) {
const manifest = path.join(directory, "package.json");
if (!fs.existsSync(manifest)) return;
const item = json(manifest);
if (typeof item.name !== "string" || typeof item.version !== "string") return;
const candidates = fs.readdirSync(directory).filter(name => {
const lower = name.toLowerCase();
return ["license", "licence", "copying"].some(prefix => lower === prefix
|| lower.startsWith(prefix + ".") || lower.startsWith(prefix + "-"));
}).map(name => path.join(directory, name));
const found = candidates.filter(name => fs.existsSync(name) && fs.statSync(name).isFile());
licenses["npm:" + item.name + "@" + item.version + ":" + directory] = found;
add(item.name, item.version, directory + "; license files: " + found.join(", "),
typeof item.license === "string" && !item.license.startsWith("SEE ") ? item.license : "NOASSERTION",
{ externalRefs: [{ referenceCategory: "PACKAGE-MANAGER", referenceType: "purl",
referenceLocator: "pkg:npm/" + item.name.split("/").map(encodeURIComponent).join("/") + "@" + encodeURIComponent(item.version) }] });
const modules = path.join(directory, "node_modules");
if (!fs.existsSync(modules)) return;
for (const entry of fs.readdirSync(modules, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith("@")) {
for (const scoped of fs.readdirSync(path.join(modules, entry.name), { withFileTypes: true })) {
if (scoped.isDirectory()) nodePackages(path.join(modules, entry.name, scoped.name));
}
} else nodePackages(path.join(modules, entry.name));
}
}
assert.equal(json("/usr/local/lib/node_modules/npm/package.json").version, lock.npm.version);
nodePackages("/usr/local/lib/node_modules/npm");
for (const entry of fs.readdirSync("/opt", { withFileTypes: true })) {
if (entry.isDirectory() && entry.name.startsWith("yarn-v")) nodePackages("/opt/" + entry.name);
}
licenses["icyzip-e2ee-review"] = [path.join(root, "LICENSE")];
const reviewId = add("icyzip-e2ee-review", lock.version, "https://icyzip.com/open-source; sourceId sha256:" + sourceId,
"Apache-2.0", { downloadLocation: "git+https://icyzip.com/open-source/icyzip-e2ee.git",
copyrightText: "Copyright 2026 Richard Andresik" });
const files = sources.map((file, index) => {
const id = "SPDXRef-File-" + index;
relationships.push({ spdxElementId: reviewId, relationshipType: "CONTAINS", relatedSpdxElement: id });
return { fileName: "./" + file.path, SPDXID: id,
checksums: [{ algorithm: "SHA256", checksumValue: file.sha256 }], licenseConcluded: "NOASSERTION",
licenseInfoInFiles: ["NOASSERTION"], copyrightText: "NOASSERTION" };
});
const sbom = { spdxVersion: "SPDX-2.3", dataLicense: "CC0-1.0", SPDXID: "SPDXRef-DOCUMENT",
name: "icyzip-e2ee-review-" + lock.version + "-" + process.arch,
documentNamespace: "https://icyzip.com/open-source/sbom/" + sourceId + "/" + process.arch,
creationInfo: { created: new Date(lock.sourceDateEpoch * 1000).toISOString().split(".000").join(""),
creators: ["Tool: icyzip-e2ee-inventory-1", "Organization: IcyZip (contact@icyzip.com)"],
comment: "Package metadata inventory, not a vulnerability or license-compliance verdict. Debian copyright files and the complete Node third-party notices are retained. Bundled components lacking separate package metadata may not be individually enumerated." },
packages, files, relationships };
fs.mkdirSync(output, { recursive: true });
for (const [name, value] of Object.entries({ "SBOM.spdx.json": sbom, "LICENSES.json": licenses, "sources.json": sources,
"build.json": { version: lock.version, base: lock.base, debianSnapshot: lock.debianSnapshot,
architecture: process.arch, sourceId, node: process.version } })) {
fs.writeFileSync(path.join(output, name), JSON.stringify(value, null, 2) + "\n", { mode: 0o644 });
}
console.log(JSON.stringify({ inventory: output, packages: packages.length, sourceFiles: sources.length, sourceId }));