@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
1,497 lines (1,468 loc) • 58.6 kB
JavaScript
import { spawnSync } from "node:child_process";
import {
existsSync,
mkdirSync,
mkdtempSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import esmock from "esmock";
import { assert, it } from "poku";
import sinon from "sinon";
function createStubbedPluginRuntime() {
let platform = process.platform;
let extn = "";
if (platform === "win32") {
platform = "windows";
extn = ".exe";
}
let arch = process.arch;
if (arch === "x64") {
arch = "amd64";
} else if (arch === "ppc64") {
arch = "ppc64le";
} else if (arch === "x32") {
arch = "386";
}
const pluginsDir = process.env.CDXGEN_PLUGINS_DIR || "";
const pluginManifestFile =
pluginsDir && existsSync(path.join(pluginsDir, "plugins-manifest.json"))
? path.join(pluginsDir, "plugins-manifest.json")
: undefined;
return {
arch,
extn,
extraNMBinPath: undefined,
platform,
pluginManifestFile,
pluginVersion: "1.0.0",
pluginsBinSuffix: "",
pluginsDir,
};
}
function resolveStubbedPluginBinary(toolName, pluginRuntime) {
const envCommandNames = {
"cargo-auditable": "CARGO_AUDITABLE_CMD",
dosai: "DOSAI_CMD",
osquery: "OSQUERY_CMD",
sourcekitten: "SOURCEKITTEN_CMD",
trivy: "TRIVY_CMD",
trustinspector: "TRUSTINSPECTOR_CMD",
};
const envCommandName = envCommandNames[toolName];
if (envCommandName && process.env[envCommandName]) {
return process.env[envCommandName];
}
if (!pluginRuntime?.pluginsDir) {
return undefined;
}
if (!existsSync(path.join(pluginRuntime.pluginsDir, toolName))) {
return undefined;
}
switch (toolName) {
case "trivy":
return path.join(
pluginRuntime.pluginsDir,
"trivy",
`trivy-cdxgen-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
);
case "osquery": {
const expectedPath = path.join(
pluginRuntime.pluginsDir,
"osquery",
`osqueryi-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
);
return pluginRuntime.platform === "darwin"
? `${expectedPath}.app/Contents/MacOS/osqueryd`
: expectedPath;
}
case "trustinspector":
return path.join(
pluginRuntime.pluginsDir,
"trustinspector",
`trustinspector-cdxgen-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
);
default:
return undefined;
}
}
async function loadBinaryModule({ pluginsOverrides, utilsOverrides } = {}) {
return esmock("./binary.js", {
"../helpers/plugins.js": {
resolveCdxgenPlugins: sinon
.stub()
.callsFake(() => createStubbedPluginRuntime()),
resolvePluginBinary: sinon
.stub()
.callsFake((toolName, pluginRuntime = createStubbedPluginRuntime()) =>
resolveStubbedPluginBinary(toolName, pluginRuntime),
),
setPluginsPathEnv: sinon
.stub()
.callsFake((pluginRuntime) => pluginRuntime),
...pluginsOverrides,
},
"../helpers/utils.js": {
adjustLicenseInformation: sinon.stub(),
attachIdentityTools: sinon.stub(),
collectExecutables: sinon.stub().returns([]),
collectSharedLibs: sinon.stub().returns([]),
DEBUG_MODE: false,
dirNameStr: "/tmp",
extractPathEnv: sinon.stub().returns([]),
extractToolRefs: sinon.stub().returns([]),
findLicenseId: sinon.stub(),
getTmpDir: sinon.stub().returns("/tmp"),
hasDangerousUnicode: sinon.stub().returns(false),
isDryRun: false,
isValidDriveRoot: sinon
.stub()
.callsFake((root) => /^[A-Za-z]:\\$/.test(root)),
isSpdxLicenseExpression: sinon.stub().returns(false),
multiChecksumFile: sinon.stub(),
recordActivity: sinon.stub(),
recordSymlinkResolution: sinon.stub(),
retrieveCdxgenPluginVersion: sinon.stub().returns("1.0.0"),
safeExistsSync: sinon.stub().returns(false),
safeMkdirSync: sinon.stub(),
safeMkdtempSync: sinon.stub().returns("/tmp/trivy-cdxgen-test"),
safeRmSync: sinon.stub(),
safeSpawnSync: sinon
.stub()
.returns({ status: 1, stdout: "", stderr: "" }),
...utilsOverrides,
},
"./containerutils.js": {
getDirs: sinon.stub().returns([]),
},
});
}
function loadPluginToolsInSubprocess(
pluginsDir,
toolNames = ["trustinspector"],
) {
const binaryModuleUrl = new URL("./binary.js", import.meta.url);
const result = spawnSync(
process.execPath,
[
"--input-type=module",
"-e",
`import { getPluginToolComponents } from ${JSON.stringify(binaryModuleUrl.href)}; console.log(JSON.stringify(getPluginToolComponents(${JSON.stringify(toolNames)})));`,
],
{
cwd: path.dirname(fileURLToPath(binaryModuleUrl)),
encoding: "utf-8",
env: {
...process.env,
CDXGEN_PLUGINS_DIR: pluginsDir,
},
},
);
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
return JSON.parse(result.stdout.trim() || "[]");
}
it("executeOsQuery() reports a blocked dry-run activity", async () => {
const recordActivity = sinon.stub();
const { executeOsQuery } = await loadBinaryModule({
utilsOverrides: {
isDryRun: true,
recordActivity,
},
});
const result = executeOsQuery("select * from processes");
assert.strictEqual(result, undefined);
sinon.assert.calledWithMatch(recordActivity, {
kind: "osquery",
status: "blocked",
target: "select * from processes",
});
});
it("executeOsQuery() uses osquery shell mode with the persistent database disabled", async () => {
const safeSpawnSync = sinon
.stub()
.returns({ status: 0, stdout: '[{"ok":"1"}]', stderr: "" });
const previousOsqueryCmd = process.env.OSQUERY_CMD;
process.env.OSQUERY_CMD = "/tmp/osqueryd";
try {
const { executeOsQuery } = await loadBinaryModule({
utilsOverrides: {
safeSpawnSync,
},
});
const result = executeOsQuery("select 1 as ok");
assert.deepStrictEqual(result, [{ ok: "1" }]);
assert.ok(safeSpawnSync.callCount >= 1);
assert.strictEqual(safeSpawnSync.lastCall.args[0], "/tmp/osqueryd");
const args = safeSpawnSync.lastCall.args[1];
assert.ok(args.includes("--S"));
assert.ok(args.includes("--disable_database"));
assert.ok(args.includes("--json"));
assert.ok(args.includes("select 1 as ok;"));
if (process.platform === "darwin") {
assert.ok(args.includes("--allow_unsafe"));
assert.ok(args.includes("--disable_logging"));
assert.ok(args.includes("--disable_events"));
}
} finally {
if (previousOsqueryCmd === undefined) {
delete process.env.OSQUERY_CMD;
} else {
process.env.OSQUERY_CMD = previousOsqueryCmd;
}
}
});
it("getOSPackages() does not misclassify non-launchpad URLs as PPAs", async () => {
const rootfs = mkdtempSync(path.join(tmpdir(), "cdxgen-rootfs-ppa-check-"));
try {
mkdirSync(path.join(rootfs, "etc", "apt", "sources.list.d"), {
recursive: true,
});
writeFileSync(
path.join(rootfs, "etc", "apt", "sources.list.d", "example.list"),
[
"deb https://example.com/redirect/ppa.launchpad.net/ondrej/php ubuntu main",
].join("\n"),
);
const { getOSPackages } = await loadBinaryModule({
utilsOverrides: {
collectExecutables: sinon.stub().returns([]),
collectSharedLibs: sinon.stub().returns([]),
extractPathEnv: sinon.stub().returns([]),
safeExistsSync: sinon
.stub()
.callsFake((filePath) => existsSync(filePath)),
safeSpawnSync: sinon
.stub()
.returns({ status: 0, stdout: "", stderr: "" }),
},
});
const result = await getOSPackages(rootfs, { Env: [] });
const repoComponent = result.osPackages.find((component) =>
component.properties?.some(
(property) => property.name === "cdx:os:repo:url",
),
);
assert.ok(repoComponent);
assert.ok(
repoComponent.properties?.some(
(property) =>
property.name === "cdx:os:repo:type" &&
property.value === "apt-source",
),
);
} finally {
rmSync(rootfs, { recursive: true, force: true });
}
});
it("getOSPackages() skips trustinspector rootfs execution for dangerous paths", async () => {
const pluginsDir = mkdtempSync(path.join(tmpdir(), "cdxgen-plugins-trust-"));
const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
const safeSpawnSync = sinon.stub().callsFake((command) => {
if (command === "ldd") {
return { status: 1, stdout: "", stderr: "" };
}
return { status: 0, stdout: "", stderr: "" };
});
try {
writeFileSync(
path.join(pluginsDir, "plugins-manifest.json"),
JSON.stringify({
plugins: [
{
name: "trustinspector",
component: {
type: "application",
name: "trustinspector",
version: "2.1.0",
},
},
],
}),
);
process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
const { getOSPackages } = await loadBinaryModule({
utilsOverrides: {
collectExecutables: sinon.stub().returns([]),
collectSharedLibs: sinon.stub().returns([]),
extractPathEnv: sinon.stub().returns([]),
hasDangerousUnicode: sinon
.stub()
.callsFake((value) => `${value || ""}`.includes("\u202e")),
safeExistsSync: sinon.stub().returns(false),
safeSpawnSync,
},
});
await getOSPackages("/tmp/rootfs\u202e", { Env: [] });
assert.ok(
safeSpawnSync
.getCalls()
.every(
(call) =>
call.args[0] !== "/tmp/trustinspector" ||
call.args[1]?.[0] !== "rootfs",
),
);
} finally {
if (previousPluginsDir === undefined) {
delete process.env.CDXGEN_PLUGINS_DIR;
} else {
process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
}
if (previousTrustInspectorCmd === undefined) {
delete process.env.TRUSTINSPECTOR_CMD;
} else {
process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
}
rmSync(pluginsDir, { recursive: true, force: true });
}
});
it("getOSPackages() skips trustinspector rootfs execution for non-directory targets", async () => {
const pluginsDir = mkdtempSync(path.join(tmpdir(), "cdxgen-plugins-trust-"));
const rootfsFile = path.join(pluginsDir, "not-a-rootfs.txt");
const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
const safeSpawnSync = sinon.stub().callsFake((command) => {
if (command === "ldd") {
return { status: 1, stdout: "", stderr: "" };
}
return { status: 0, stdout: "", stderr: "" };
});
try {
writeFileSync(rootfsFile, "not a directory\n");
writeFileSync(
path.join(pluginsDir, "plugins-manifest.json"),
JSON.stringify({
plugins: [
{
name: "trustinspector",
component: {
type: "application",
name: "trustinspector",
version: "2.1.0",
},
},
],
}),
);
process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
const safeExistsSync = sinon
.stub()
.callsFake((targetPath) => targetPath === rootfsFile);
const { getOSPackages } = await loadBinaryModule({
utilsOverrides: {
collectExecutables: sinon.stub().returns([]),
collectSharedLibs: sinon.stub().returns([]),
extractPathEnv: sinon.stub().returns([]),
safeExistsSync,
safeSpawnSync,
},
});
await getOSPackages(rootfsFile, { Env: [] });
assert.ok(
safeSpawnSync
.getCalls()
.every(
(call) =>
call.args[0] !== "/tmp/trustinspector" ||
call.args[1]?.[0] !== "rootfs",
),
);
} finally {
if (previousPluginsDir === undefined) {
delete process.env.CDXGEN_PLUGINS_DIR;
} else {
process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
}
if (previousTrustInspectorCmd === undefined) {
delete process.env.TRUSTINSPECTOR_CMD;
} else {
process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
}
rmSync(pluginsDir, { recursive: true, force: true });
}
});
it("getOSPackages() preserves a valid symlinked rootfs path for trustinspector", async () => {
if (process.platform === "win32") {
return;
}
const pluginsDir = mkdtempSync(path.join(tmpdir(), "cdxgen-plugins-trust-"));
const realRootfsDir = mkdtempSync(path.join(tmpdir(), "cdxgen-rootfs-real-"));
const rootfsLink = path.join(pluginsDir, "rootfs-link");
const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
const safeSpawnSync = sinon.stub().callsFake((command, args) => {
if (command === "ldd") {
return { status: 1, stdout: "", stderr: "" };
}
if (command === "/tmp/trustinspector" && args?.[0] === "rootfs") {
return {
status: 0,
stdout: JSON.stringify({ materials: [] }),
stderr: "",
};
}
return { status: 0, stdout: "", stderr: "" };
});
try {
mkdirSync(path.join(realRootfsDir, "etc"), { recursive: true });
writeFileSync(
path.join(realRootfsDir, "etc", "os-release"),
'ID="debian"\nVERSION_ID="12"\n',
);
symlinkSync(realRootfsDir, rootfsLink);
writeFileSync(
path.join(pluginsDir, "plugins-manifest.json"),
JSON.stringify({
plugins: [
{
name: "trustinspector",
component: {
type: "application",
name: "trustinspector",
version: "2.1.0",
},
},
],
}),
);
process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
const { getOSPackages } = await loadBinaryModule({
utilsOverrides: {
collectExecutables: sinon.stub().returns([]),
collectSharedLibs: sinon.stub().returns([]),
extractPathEnv: sinon.stub().returns([]),
safeExistsSync: sinon
.stub()
.callsFake((targetPath) => existsSync(targetPath)),
safeSpawnSync,
},
});
await getOSPackages(rootfsLink, { Env: [] });
assert.ok(
safeSpawnSync.calledWith(
"/tmp/trustinspector",
sinon.match(
(args) => args?.[0] === "rootfs" && args?.[1] === rootfsLink,
),
),
);
} finally {
if (previousPluginsDir === undefined) {
delete process.env.CDXGEN_PLUGINS_DIR;
} else {
process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
}
if (previousTrustInspectorCmd === undefined) {
delete process.env.TRUSTINSPECTOR_CMD;
} else {
process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
}
rmSync(pluginsDir, { recursive: true, force: true });
rmSync(realRootfsDir, { recursive: true, force: true });
}
});
it("getPluginToolComponents() reads precise tool metadata from the plugins manifest", async () => {
const pluginsDir = mkdtempSync(
path.join(tmpdir(), "cdxgen-plugins-manifest-"),
);
const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
try {
writeFileSync(
path.join(pluginsDir, "plugins-manifest.json"),
JSON.stringify({
plugins: [
{
name: "trustinspector",
component: {
type: "application",
name: "trustinspector",
version: "2.1.0",
purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
"bom-ref":
"pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
hashes: [{ alg: "SHA-256", content: "a".repeat(64) }],
},
},
],
}),
);
const tools = loadPluginToolsInSubprocess(pluginsDir);
assert.strictEqual(tools.length, 1);
assert.strictEqual(tools[0].name, "trustinspector");
assert.strictEqual(tools[0].version, "2.1.0");
assert.match(tools[0].purl, /trustinspector-cdxgen/);
} finally {
if (previousPluginsDir === undefined) {
delete process.env.CDXGEN_PLUGINS_DIR;
} else {
process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
}
rmSync(pluginsDir, { recursive: true, force: true });
}
});
it("getPluginToolComponents() sanitizes manifest tool metadata before use", async () => {
const pluginsDir = mkdtempSync(
path.join(tmpdir(), "cdxgen-plugins-manifest-sanitize-"),
);
const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
try {
writeFileSync(
path.join(pluginsDir, "plugins-manifest.json"),
JSON.stringify({
plugins: [
{
name: "trustinspector",
component: {
name: "trustinspector",
version: "2.1.0",
purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
"bom-ref":
"pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
properties: [
{ name: "cdx:tool:origin", value: "plugins-manifest" },
{ name: "", value: "ignored" },
{ name: 1, value: "ignored" },
],
externalReferences: [
{ type: "vcs", url: "https://example.com/trustinspector" },
{ type: "distribution", url: "" },
],
hashes: [
{ alg: "SHA-256", content: "a".repeat(64) },
{ alg: "", content: "ignored" },
],
nested: { should: "not-survive" },
},
},
],
}),
);
const tools = loadPluginToolsInSubprocess(pluginsDir);
assert.strictEqual(tools.length, 1);
assert.strictEqual(tools[0].name, "trustinspector");
assert.strictEqual(tools[0].nested, undefined);
assert.strictEqual({}.polluted, undefined);
assert.deepStrictEqual(tools[0].properties, [
{ name: "cdx:tool:origin", value: "plugins-manifest" },
]);
assert.deepStrictEqual(tools[0].externalReferences, [
{ type: "vcs", url: "https://example.com/trustinspector" },
]);
assert.deepStrictEqual(tools[0].hashes, [
{ alg: "SHA-256", content: "a".repeat(64) },
]);
} finally {
if (previousPluginsDir === undefined) {
delete process.env.CDXGEN_PLUGINS_DIR;
} else {
process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
}
rmSync(pluginsDir, { recursive: true, force: true });
}
});
it("getPluginToolComponents() ignores oversized plugins manifests", async () => {
const pluginsDir = mkdtempSync(
path.join(tmpdir(), "cdxgen-plugins-manifest-large-"),
);
const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
try {
writeFileSync(
path.join(pluginsDir, "plugins-manifest.json"),
`${JSON.stringify({
plugins: [
{
name: "trustinspector",
component: {
name: "trustinspector",
"bom-ref": "pkg:generic/trustinspector@2.1.0",
},
},
],
})}${" ".repeat(1024 * 1024)}`,
);
assert.deepStrictEqual(loadPluginToolsInSubprocess(pluginsDir), []);
} finally {
if (previousPluginsDir === undefined) {
delete process.env.CDXGEN_PLUGINS_DIR;
} else {
process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
}
rmSync(pluginsDir, { recursive: true, force: true });
}
});
it("getOSPackages() returns empty collections and reports a blocked dry-run activity", async () => {
const recordActivity = sinon.stub();
const { getOSPackages } = await loadBinaryModule({
utilsOverrides: {
isDryRun: true,
recordActivity,
},
});
const result = await getOSPackages("/tmp/rootfs", {});
assert.deepStrictEqual(result.osPackages, []);
assert.deepStrictEqual(result.dependenciesList, []);
assert.deepStrictEqual(result.binPaths, []);
assert.deepStrictEqual(Array.from(result.allTypes), []);
assert.deepStrictEqual(result.tools, []);
sinon.assert.calledWithMatch(recordActivity, {
kind: "container",
status: "blocked",
target: "/tmp/rootfs",
});
});
it("getOSPackages() creates package-owned file components and services from Trivy properties", async () => {
const rootfs = mkdtempSync(path.join(tmpdir(), "cdxgen-rootfs-"));
const trivyTempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-trivy-"));
const bomJsonFile = path.join(trivyTempDir, "trivy-bom.json");
const packagePurl = "pkg:apk/alpine/demo@1.0-r0?distro=alpine-3.20";
const packageRef = decodeURIComponent(packagePurl);
const collectExecutables = sinon.stub().returns([]);
const collectSharedLibs = sinon.stub().returns([]);
try {
mkdirSync(path.join(rootfs, "usr", "bin"), { recursive: true });
mkdirSync(path.join(rootfs, "usr", "lib"), { recursive: true });
mkdirSync(path.join(rootfs, "etc", "init.d"), { recursive: true });
mkdirSync(path.join(rootfs, "etc"), { recursive: true });
writeFileSync(path.join(rootfs, "usr", "bin", "demo"), "#!/bin/sh\n", {
mode: 0o644,
});
writeFileSync(path.join(rootfs, "usr", "lib", "libdemo.so.1"), "binary", {
mode: 0o644,
});
writeFileSync(
path.join(rootfs, "etc", "init.d", "demosvc"),
[
"#!/bin/sh",
"### BEGIN INIT INFO",
"# Provides: demosvc",
"# Short-Description: Demo service",
"### END INIT INFO",
"/usr/bin/demo start",
"",
].join("\n"),
{ mode: 0o755 },
);
writeFileSync(
path.join(rootfs, "etc", "os-release"),
"ID=alpine\nVERSION_ID=3.20.0\n",
);
writeFileSync(
bomJsonFile,
JSON.stringify({
metadata: { tools: [] },
components: [
{
"bom-ref": packageRef,
name: "demo",
purl: packagePurl,
properties: [
{ name: "aquasecurity:trivy:PkgID", value: "demo@1.0-r0" },
{ name: "aquasecurity:trivy:PkgType", value: "apk" },
{ name: "aquasecurity:trivy:Capability", value: "cmd:demo" },
{
name: "aquasecurity:trivy:CapabilityCount",
value: "1",
},
{
name: "aquasecurity:trivy:InstalledCommand",
value: "demo",
},
{
name: "aquasecurity:trivy:InstalledCommandCount",
value: "1",
},
{
name: "aquasecurity:trivy:InstalledCommandPath",
value: "/usr/bin/demo",
},
{
name: "aquasecurity:trivy:InstalledFileCount",
value: "3",
},
{
name: "aquasecurity:trivy:InstalledFile",
value: "/usr/bin/demo",
},
{
name: "aquasecurity:trivy:InstalledFile",
value: "/usr/lib/libdemo.so.1",
},
{
name: "aquasecurity:trivy:InstalledFile",
value: "/etc/init.d/demosvc",
},
{
name: "aquasecurity:trivy:PackageVendor",
value: "Demo Vendor",
},
],
supplier: { name: "Demo Maintainers <demo@example.test>" },
},
],
dependencies: [],
}),
);
const originalTrivyCmd = process.env.TRIVY_CMD;
process.env.TRIVY_CMD = "/usr/bin/true";
const { getOSPackages } = await loadBinaryModule({
utilsOverrides: {
collectExecutables,
collectSharedLibs,
extractPathEnv: sinon.stub().returns(["/usr/bin"]),
getTmpDir: sinon.stub().returns(path.dirname(trivyTempDir)),
multiChecksumFile: sinon.stub().resolves({
md5: "a".repeat(32),
sha1: "b".repeat(40),
}),
safeExistsSync: sinon
.stub()
.callsFake((filePath) => existsSync(filePath)),
safeMkdtempSync: sinon.stub().returns(trivyTempDir),
safeSpawnSync: sinon.stub().callsFake((command) => {
if (command === "ldd") {
return { status: 1, stdout: "", stderr: "" };
}
return { status: 0, stdout: "", stderr: "" };
}),
},
});
const result = await getOSPackages(rootfs, { Env: ["PATH=/usr/bin"] });
process.env.TRIVY_CMD = originalTrivyCmd;
assert.strictEqual(result.osPackages.length, 1);
assert.strictEqual(result.osPackageFiles.length, 3);
assert.strictEqual(result.services.length, 1);
assert.strictEqual(result.services[0].name, "demosvc");
assert.ok(
result.osPackages[0].properties.some(
(prop) => prop.name.endsWith("Capability") && prop.value === "cmd:demo",
),
);
assert.deepStrictEqual(result.osPackages[0].supplier, {
name: "Demo Maintainers <demo@example.test>",
});
assert.deepStrictEqual(result.osPackages[0].manufacturer, {
name: "Demo Vendor",
});
assert.deepStrictEqual(result.osPackages[0].authors, [
{ name: "Demo Maintainers", email: "demo@example.test" },
]);
assert.ok(
!(result.osPackages[0].properties || []).some((prop) =>
prop.name.endsWith("PackageVendor"),
),
);
assert.ok(
result.osPackageFiles.some(
(component) =>
component.properties.some(
(prop) => prop.name === "SrcFile" && prop.value === "/usr/bin/demo",
) &&
component.properties.some(
(prop) =>
prop.name === "internal:is_executable" && prop.value === "true",
),
),
);
assert.ok(
result.dependenciesList.some(
(dependency) =>
Array.isArray(dependency.provides) && dependency.provides.length >= 3,
),
);
assert.ok(
result.dependenciesList.some(
(dependency) =>
dependency.ref === result.services[0]["bom-ref"] &&
Array.isArray(dependency.dependsOn) &&
dependency.dependsOn.length > 0,
),
);
sinon.assert.calledWithMatch(
collectExecutables,
rootfs,
["/usr/bin"],
["/etc/init.d/demosvc", "/usr/bin/demo", "/usr/lib/libdemo.so.1"],
);
sinon.assert.calledWithMatch(
collectSharedLibs,
rootfs,
sinon.match.array,
"/etc/ld.so.conf",
"/etc/ld.so.conf.d/*.conf",
["/etc/init.d/demosvc", "/usr/bin/demo", "/usr/lib/libdemo.so.1"],
);
} finally {
rmSync(rootfs, { recursive: true, force: true });
rmSync(trivyTempDir, { recursive: true, force: true });
delete process.env.TRIVY_CMD;
}
});
it("getOSPackages() omits setuid metadata from package-owned file components", async () => {
const rootfs = mkdtempSync(path.join(tmpdir(), "cdxgen-rootfs-setuid-"));
const trivyTempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-trivy-setuid-"));
const bomJsonFile = path.join(trivyTempDir, "trivy-bom.json");
const packagePurl = "pkg:apk/alpine/demo@1.0-r0?distro=alpine-3.20";
const packageRef = decodeURIComponent(packagePurl);
const originalTrivyCmd = process.env.TRIVY_CMD;
try {
mkdirSync(path.join(rootfs, "usr", "bin"), { recursive: true });
mkdirSync(path.join(rootfs, "etc"), { recursive: true });
writeFileSync(path.join(rootfs, "usr", "bin", "demo"), "#!/bin/sh\n", {
mode: 0o4755,
});
writeFileSync(
path.join(rootfs, "etc", "os-release"),
"ID=alpine\nVERSION_ID=3.20.0\n",
);
writeFileSync(
bomJsonFile,
JSON.stringify({
metadata: { tools: [] },
components: [
{
"bom-ref": packageRef,
name: "demo",
purl: packagePurl,
properties: [
{ name: "aquasecurity:trivy:PkgID", value: "demo@1.0-r0" },
{ name: "aquasecurity:trivy:PkgType", value: "apk" },
{
name: "aquasecurity:trivy:InstalledFile",
value: "/usr/bin/demo",
},
{
name: "aquasecurity:trivy:InstalledCommandPath",
value: "/usr/bin/demo",
},
],
},
],
dependencies: [],
}),
);
process.env.TRIVY_CMD = "/usr/bin/true";
const { getOSPackages } = await loadBinaryModule({
utilsOverrides: {
collectExecutables: sinon.stub().returns([]),
collectSharedLibs: sinon.stub().returns([]),
extractPathEnv: sinon.stub().returns(["/usr/bin"]),
getTmpDir: sinon.stub().returns(path.dirname(trivyTempDir)),
multiChecksumFile: sinon.stub().resolves({
md5: "a".repeat(32),
sha1: "b".repeat(40),
}),
safeExistsSync: sinon
.stub()
.callsFake((filePath) => existsSync(filePath)),
safeMkdtempSync: sinon.stub().returns(trivyTempDir),
safeSpawnSync: sinon.stub().callsFake((command) => {
if (command === "ldd") {
return { status: 1, stdout: "", stderr: "" };
}
return { status: 0, stdout: "", stderr: "" };
}),
},
});
const result = await getOSPackages(rootfs, { Env: ["PATH=/usr/bin"] });
process.env.TRIVY_CMD = originalTrivyCmd;
const fileComponent = result.osPackageFiles.find((component) =>
component.properties.some(
(prop) => prop.name === "SrcFile" && prop.value === "/usr/bin/demo",
),
);
assert.ok(fileComponent);
assert.ok(
!fileComponent.properties.some(
(prop) => prop.name === "internal:has_setuid",
),
);
} finally {
if (originalTrivyCmd === undefined) {
delete process.env.TRIVY_CMD;
} else {
process.env.TRIVY_CMD = originalTrivyCmd;
}
rmSync(rootfs, { recursive: true, force: true });
rmSync(trivyTempDir, { recursive: true, force: true });
}
});
it("getOSPackages() preserves conflicting native origin fields and retains fallback trust properties", async () => {
const rootfs = mkdtempSync(
path.join(tmpdir(), "cdxgen-rootfs-native-conflict-"),
);
const trivyTempDir = mkdtempSync(
path.join(tmpdir(), "cdxgen-trivy-native-conflict-"),
);
const bomJsonFile = path.join(trivyTempDir, "trivy-bom.json");
const packagePurl = "pkg:apk/alpine/demo@1.0-r0?distro=alpine-3.20";
const packageRef = decodeURIComponent(packagePurl);
try {
mkdirSync(path.join(rootfs, "etc"), { recursive: true });
writeFileSync(
path.join(rootfs, "etc", "os-release"),
"ID=alpine\nVERSION_ID=3.20.0\n",
{ encoding: "utf-8" },
);
writeFileSync(
bomJsonFile,
JSON.stringify({
metadata: { tools: [] },
components: [
{
"bom-ref": packageRef,
name: "demo",
purl: packagePurl,
supplier: { name: "Existing Supplier" },
manufacturer: { name: "Existing Manufacturer" },
authors: [
{ name: "Existing Author", email: "author@example.test" },
],
properties: [
{ name: "aquasecurity:trivy:PkgID", value: "demo@1.0-r0" },
{ name: "aquasecurity:trivy:PkgType", value: "apk" },
{
name: "aquasecurity:trivy:PackageMaintainer",
value: "Demo Maintainers <demo@example.test>",
},
{
name: "aquasecurity:trivy:PackageVendor",
value: "Demo Vendor",
},
],
},
],
dependencies: [],
}),
);
process.env.TRIVY_CMD = "/usr/bin/true";
const { getOSPackages } = await loadBinaryModule({
utilsOverrides: {
extractPathEnv: sinon.stub().returns([]),
getTmpDir: sinon.stub().returns(path.dirname(trivyTempDir)),
safeExistsSync: sinon
.stub()
.callsFake((filePath) => existsSync(filePath)),
safeMkdtempSync: sinon.stub().returns(trivyTempDir),
safeSpawnSync: sinon.stub().callsFake((command) => {
if (command === "ldd") {
return { status: 1, stdout: "", stderr: "" };
}
return { status: 0, stdout: "", stderr: "" };
}),
},
});
const result = await getOSPackages(rootfs, {});
assert.strictEqual(result.osPackages.length, 1);
assert.deepStrictEqual(result.osPackages[0].supplier, {
name: "Existing Supplier",
});
assert.deepStrictEqual(result.osPackages[0].manufacturer, {
name: "Existing Manufacturer",
});
assert.deepStrictEqual(result.osPackages[0].authors, [
{ name: "Existing Author", email: "author@example.test" },
]);
assert.ok(
(result.osPackages[0].properties || []).some(
(prop) =>
prop.name.endsWith("PackageMaintainer") &&
prop.value === "Demo Maintainers <demo@example.test>",
),
);
assert.ok(
(result.osPackages[0].properties || []).some(
(prop) =>
prop.name.endsWith("PackageVendor") && prop.value === "Demo Vendor",
),
);
} finally {
rmSync(rootfs, { recursive: true, force: true });
rmSync(trivyTempDir, { recursive: true, force: true });
delete process.env.TRIVY_CMD;
}
});
it("getOSPackages() inventories rootfs repository sources and trusted keys without Trivy package data", async () => {
const rootfs = mkdtempSync(path.join(tmpdir(), "cdxgen-rootfs-repos-"));
const pluginsDir = mkdtempSync(path.join(tmpdir(), "cdxgen-plugins-rootfs-"));
const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
try {
mkdirSync(path.join(rootfs, "etc", "apt", "sources.list.d"), {
recursive: true,
});
mkdirSync(path.join(rootfs, "usr", "share", "keyrings"), {
recursive: true,
});
mkdirSync(path.join(rootfs, "etc", "yum.repos.d"), { recursive: true });
mkdirSync(path.join(rootfs, "etc", "pki", "rpm-gpg"), {
recursive: true,
});
writeFileSync(
path.join(rootfs, "etc", "apt", "sources.list.d", "ondrej-php.list"),
"deb [signed-by=/usr/share/keyrings/ondrej-php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main\n",
);
writeFileSync(
path.join(rootfs, "usr", "share", "keyrings", "ondrej-php.gpg"),
"fake-apt-key",
);
writeFileSync(
path.join(rootfs, "etc", "yum.repos.d", "custom.repo"),
[
"[custom]",
"name=Custom Repo",
"baseurl=https://packages.example.test/rpm/$basearch",
"enabled=1",
"gpgcheck=1",
"gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-test",
"",
].join("\n"),
);
writeFileSync(
path.join(rootfs, "etc", "pki", "rpm-gpg", "RPM-GPG-KEY-test"),
"fake-rpm-key",
);
writeFileSync(
path.join(pluginsDir, "plugins-manifest.json"),
JSON.stringify({
plugins: [
{
name: "trustinspector",
component: {
type: "application",
name: "trustinspector",
version: "2.1.0",
purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
"bom-ref":
"pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
},
},
],
}),
);
process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
const { getOSPackages } = await loadBinaryModule({
utilsOverrides: {
collectExecutables: sinon.stub().returns([]),
collectSharedLibs: sinon.stub().returns([]),
extractPathEnv: sinon.stub().returns([]),
multiChecksumFile: sinon
.stub()
.callsFake(async (_algorithms, filePath) => ({
sha1: filePath.includes("ondrej") ? "1".repeat(40) : "2".repeat(40),
sha256: filePath.includes("ondrej")
? "a".repeat(64)
: "b".repeat(64),
})),
safeExistsSync: sinon
.stub()
.callsFake((filePath) => existsSync(filePath)),
safeSpawnSync: sinon.stub().callsFake((command, args) => {
if (command === "ldd") {
return { status: 1, stdout: "", stderr: "" };
}
if (command === "/tmp/trustinspector" && args[0] === "rootfs") {
return {
status: 0,
stdout: JSON.stringify({
materials: [
{
kind: "public-key",
path: "/usr/share/keyrings/ondrej-php.gpg",
name: "ondrej-php.gpg",
trustDomain: "apt",
sourceType: "repository-keyring",
fileExtension: "gpg",
sha1: "1".repeat(40),
sha256: "a".repeat(64),
keyId: "ABCDEF1234567890",
algorithm: "RSA",
keyStrength: 4096,
fingerprint: "F".repeat(40),
userIds: ["Ondrej Surý <ondrej@example.test>"],
properties: [
{
name: "cdx:crypto:sourceType",
value: "repository-keyring",
},
],
},
{
kind: "certificate",
path: "/etc/ssl/certs/demo-root.crt",
name: "demo-root",
trustDomain: "ca-store",
sourceType: "ca-store",
fileExtension: "crt",
sha1: "3".repeat(40),
sha256: "c".repeat(64),
algorithm: "RSA",
keyStrength: 2048,
createdAt: "2024-01-01T00:00:00Z",
expiresAt: "2034-01-01T00:00:00Z",
fingerprint: "D".repeat(64),
subject: "CN=demo-root,O=Example Org",
issuer: "CN=demo-root,O=Example Org",
serial: "42",
format: "X.509",
properties: [{ name: "cdx:crypto:isCA", value: "true" }],
},
],
}),
stderr: "",
};
}
return { status: 0, stdout: "", stderr: "" };
}),
},
});
const result = await getOSPackages(rootfs, { Env: [] });
const cryptoComponents = result.osPackages.filter(
(component) => component.type === "cryptographic-asset",
);
const ppaComponent = result.osPackages.find(
(component) =>
component.type === "data" &&
component.properties?.some(
(property) =>
property.name === "cdx:os:repo:type" &&
property.value === "ppa-source",
),
);
const yumComponent = result.osPackages.find(
(component) =>
component.type === "data" &&
component.properties?.some(
(property) =>
property.name === "cdx:os:repo:type" &&
property.value === "yum-source",
),
);
const ppaKeyRef = cryptoComponents.find((component) =>
component.properties?.some(
(property) =>
property.name === "SrcFile" &&
property.value === "/usr/share/keyrings/ondrej-php.gpg",
),
)?.["bom-ref"];
const yumKeyRef = cryptoComponents.find((component) =>
component.properties?.some(
(property) =>
property.name === "SrcFile" &&
property.value === "/etc/pki/rpm-gpg/RPM-GPG-KEY-test",
),
)?.["bom-ref"];
assert.strictEqual(cryptoComponents.length, 3);
assert.ok(
cryptoComponents.some(
(component) =>
component.cryptoProperties?.assetType === "related-crypto-material" &&
component.cryptoProperties?.relatedCryptoMaterialProperties?.type ===
"public-key",
),
);
assert.ok(
cryptoComponents.some(
(component) =>
component.cryptoProperties?.assetType === "certificate" &&
component.properties?.some(
(property) =>
property.name === "cdx:crypto:trustDomain" &&
property.value === "ca-store",
),
),
);
assert.ok(
cryptoComponents.some((component) =>
component.properties?.some(
(property) =>
property.name === "cdx:crypto:keyId" &&
property.value === "ABCDEF1234567890",
),
),
);
assert.ok(ppaComponent);
assert.ok(yumComponent);
assert.ok(ppaKeyRef);
assert.ok(yumKeyRef);
assert.ok(
result.dependenciesList.some(
(dependency) =>
dependency.ref === ppaComponent["bom-ref"] &&
Array.isArray(dependency.dependsOn) &&
dependency.dependsOn.includes(ppaKeyRef),
),
);
assert.ok(
result.dependenciesList.some(
(dependency) =>
dependency.ref === yumComponent["bom-ref"] &&
Array.isArray(dependency.dependsOn) &&
dependency.dependsOn.includes(yumKeyRef),
),
);
} finally {
if (previousPluginsDir === undefined) {
delete process.env.CDXGEN_PLUGINS_DIR;
} else {
process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
}
if (previousTrustInspectorCmd === undefined) {
delete process.env.TRUSTINSPECTOR_CMD;
} else {
process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
}
rmSync(pluginsDir, { recursive: true, force: true });
rmSync(rootfs, { recursive: true, force: true });
}
});
it("enrichOSComponentsWithTrustData() merges path inspections and host findings", async () => {
if (!["darwin", "win32"].includes(process.platform)) {
return;
}
const pluginsDir = mkdtempSync(
path.join(tmpdir(), "cdxgen-plugins-hosttrust-"),
);
const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
const inspectedPath =
process.platform === "win32"
? "C:\\Demo\\demo.exe"
: "/Applications/Demo.app";
const expectedProperty =
process.platform === "win32"
? { name: "cdx:windows:authenticode:status", value: "Valid" }
: { name: "cdx:darwin:codesign:teamIdentifier", value: "ABCDE12345" };
const hostFinding =
process.platform === "win32"
? {
kind: "windows-wdac-status",
name: "wdac-active-policies",
version: "1",
description: "active policies",
properties: [
{
name: "cdx:windows:wdac:activePolicyCount",
value: "1",
},
],
}
: {
kind: "darwin-gatekeeper-status",
name: "gatekeeper-system-policy",
version: "enabled",
description: "assessments enabled",
properties: [
{
name: "cdx:darwin:gatekeeper:status",
value: "enabled",
},
],
};
try {
writeFileSync(
path.join(pluginsDir, "plugins-manifest.json"),
JSON.stringify({
plugins: [
{
name: "trustinspector",
component: {
type: "application",
name: "trustinspector",
version: "2.1.0",
purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
"bom-ref":
"pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
},
},
],
}),
);
process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
const { enrichOSComponentsWithTrustData } = await loadBinaryModule({
utilsOverrides: {
safeExistsSync: sinon
.stub()
.callsFake((filePath) => existsSync(filePath)),
safeSpawnSync: sinon.stub().callsFake((command, args) => {
if (command === "ldd") {
return { status: 1, stdout: "", stderr: "" };
}
if (command === "/tmp/trustinspector" && args[0] === "paths") {
return {
status: 0,
stdout: JSON.stringify({
inspections: [
{
path: inspectedPath,
properties: [expectedProperty],
},
],
}),
stderr: "",
};
}
if (command === "/tmp/trustinspector" && args[0] === "host") {
return {
status: 0,
stdout: JSON.stringify({ hostFindings: [hostFinding] }),
stderr: "",
};
}
return { status: 0, stdout: "", stderr: "" };
}),
},
});
const result = enrichOSComponentsWithTrustData([
{
type: "application",
name: "Demo",
"bom-ref": "app-demo",
properties: [{ name: "path", value: inspectedPath }],
},
]);
assert.ok(
result.components[0].properties.some(
(property) =>
property.name === expectedProperty.name &&
property.value === expectedProperty.value,
),
);
assert.ok(
result.components.some(
(component) =>
component.type === "data" && component.name === hostFinding.name,
),
);
assert.strictEqual(result.tools.length, 1);
assert.strictEqual(result.tools[0].name, "trustinspector");
} finally {
if (previousPluginsDir === undefined) {
delete process.env.CDXGEN_PLUGINS_DIR;
} else {
process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
}
if (previousTrustInspectorCmd === undefined) {
delete process.env.TRUSTINSPECTOR_CMD;
} else {
process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
}
rmSync(pluginsDir, { recursive: true, force: true });
}
});
it("enrichOSComponentsWithTrustData() skips macOS plist paths for notarization checks", async () => {
if (process.platform !== "darwin") {
return;
}
const pluginsDir = mkdtempSync(
path.join(tmpdir(), "cdxgen-plugins-hosttrust-plist-"),
);
const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
const safeSpawnSync = sinon.stub().callsFake((command, args) => {
if (command === "ldd") {
return { status: 1, stdout: "", stderr: "" };
}
if (command === "/tmp/trustinspector" && args[0] === "paths") {
assert.deepStrictEqual(args, [
"paths",
"/Library/PrivilegedHelperTools/demo-helper",
]);
return {
status: 0,
stdout: JSON.stringify({
inspections: [
{
path: "/Library/PrivilegedHelperTools/demo-helper",
properties: [
{
name: "cdx:darwin:notarization:assessment",
value: "accepted",
},
],
},
],
}),
stderr: "",
};
}
if (command === "/tmp/trustinspector" && args[0] === "host") {
return {
status: 0,
stdout: JSON.stringify({ hostFindings: [] }),
stderr: "",
};
}
return { status: 0, stdout: "", stderr: "" };
});
try {
writeFileSync(
path.join(pluginsDir, "plugins-manifest.json"),
JSON.stringify({
plugins: [
{
name: "trustinspector",
component: {
type: "application",
name: "trustinspector",
version: "2.1.1",
purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.1",
"bom-ref":
"pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.1",
},
},
],
}),
);
process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
const { enrichOSComponentsWithTrustData } = await loadBinaryModule({
utilsOverrides: {
safeExistsSync: sinon
.stub()
.callsFake((filePath) => existsSync(filePath)),
safeSpawnSync,
},
});
const result = enrichOSComponentsWithTrustData([
{
type: "application",
name: "demo-helper",
"bom-ref":
"pkg:swid/demo-helper#/Library/LaunchDaemons/org.nixos.nix-daemon.plist",
properties: [
{
name: "path",
value: "/Library/LaunchDaemons/org.nixos.nix-daemon.plist",
},
{
name: "program",
value: "/Library/PrivilegedHelperTools/demo-helper",
},
],
},
]);
assert.ok(
result.components[0].properties.some(
(property) =>
property.name === "cdx:darwin:notarization:assessment" &&
property.value === "accepted",
),
);
const trustInspectorPathCalls = safeSpawnSync
.getCalls()
.filter(
(call) =>
call.args[0] === "/tmp/trustinspector" &&
call.args[1]?.[0] === "paths",
);
assert.strictEqual(trustInspector