// 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 }));