@cyclonedx/cdxgen 12.3.3 → 12.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +64 -22
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +238 -116
  4. package/bin/convert.js +28 -13
  5. package/bin/hbom.js +490 -0
  6. package/bin/repl.js +580 -29
  7. package/bin/validate.js +34 -4
  8. package/bin/verify.js +40 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/predictive-audit-allowlist.json +11 -0
  13. package/data/queries-darwin.json +12 -1
  14. package/data/queries-win.json +7 -1
  15. package/data/queries.json +39 -2
  16. package/data/rules/ai-agent-governance.yaml +16 -0
  17. package/data/rules/asar-archives.yaml +150 -0
  18. package/data/rules/chrome-extensions.yaml +8 -0
  19. package/data/rules/ci-permissions.yaml +42 -18
  20. package/data/rules/container-risk.yaml +14 -7
  21. package/data/rules/dependency-sources.yaml +11 -0
  22. package/data/rules/hbom-compliance.yaml +325 -0
  23. package/data/rules/hbom-performance.yaml +307 -0
  24. package/data/rules/hbom-security.yaml +248 -0
  25. package/data/rules/host-topology.yaml +165 -0
  26. package/data/rules/mcp-servers.yaml +18 -3
  27. package/data/rules/obom-runtime.yaml +907 -22
  28. package/data/rules/package-integrity.yaml +14 -0
  29. package/data/rules/rootfs-hardening.yaml +179 -0
  30. package/data/rules/vscode-extensions.yaml +9 -0
  31. package/lib/audit/index.js +209 -8
  32. package/lib/audit/index.poku.js +332 -0
  33. package/lib/audit/reporters.js +222 -0
  34. package/lib/audit/targets.js +146 -1
  35. package/lib/audit/targets.poku.js +186 -0
  36. package/lib/cli/asar.poku.js +328 -0
  37. package/lib/cli/index.js +506 -88
  38. package/lib/cli/index.poku.js +1352 -212
  39. package/lib/evinser/evinser.js +14 -9
  40. package/lib/helpers/analyzer.js +1406 -29
  41. package/lib/helpers/analyzer.poku.js +342 -0
  42. package/lib/helpers/analyzerScope.js +712 -0
  43. package/lib/helpers/asarutils.js +1556 -0
  44. package/lib/helpers/asarutils.poku.js +443 -0
  45. package/lib/helpers/auditCategories.js +12 -0
  46. package/lib/helpers/auditCategories.poku.js +32 -0
  47. package/lib/helpers/cbomutils.js +271 -1
  48. package/lib/helpers/cbomutils.poku.js +248 -5
  49. package/lib/helpers/display.js +291 -1
  50. package/lib/helpers/display.poku.js +149 -0
  51. package/lib/helpers/evidenceUtils.js +58 -0
  52. package/lib/helpers/evidenceUtils.poku.js +54 -0
  53. package/lib/helpers/exportUtils.js +9 -0
  54. package/lib/helpers/gtfobins.js +142 -8
  55. package/lib/helpers/gtfobins.poku.js +24 -1
  56. package/lib/helpers/hbom.js +710 -0
  57. package/lib/helpers/hbom.poku.js +496 -0
  58. package/lib/helpers/hbomAnalysis.js +268 -0
  59. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  60. package/lib/helpers/hbomLoader.js +35 -0
  61. package/lib/helpers/hostTopology.js +803 -0
  62. package/lib/helpers/hostTopology.poku.js +363 -0
  63. package/lib/helpers/inventoryStats.js +69 -0
  64. package/lib/helpers/inventoryStats.poku.js +86 -0
  65. package/lib/helpers/lolbas.js +19 -1
  66. package/lib/helpers/lolbas.poku.js +23 -0
  67. package/lib/helpers/osqueryTransform.js +47 -0
  68. package/lib/helpers/osqueryTransform.poku.js +47 -0
  69. package/lib/helpers/plugins.js +349 -0
  70. package/lib/helpers/plugins.poku.js +57 -0
  71. package/lib/helpers/protobom.js +156 -45
  72. package/lib/helpers/protobom.poku.js +140 -5
  73. package/lib/helpers/remote/dependency-track.js +36 -3
  74. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  75. package/lib/helpers/source.js +24 -0
  76. package/lib/helpers/source.poku.js +32 -0
  77. package/lib/helpers/utils.js +1438 -93
  78. package/lib/helpers/utils.poku.js +846 -4
  79. package/lib/managers/binary.e2e.poku.js +367 -0
  80. package/lib/managers/binary.js +2293 -353
  81. package/lib/managers/binary.poku.js +1699 -1
  82. package/lib/managers/docker.js +201 -79
  83. package/lib/managers/docker.poku.js +337 -12
  84. package/lib/server/server.js +2 -27
  85. package/lib/stages/postgen/annotator.js +38 -0
  86. package/lib/stages/postgen/annotator.poku.js +107 -1
  87. package/lib/stages/postgen/auditBom.js +121 -18
  88. package/lib/stages/postgen/auditBom.poku.js +1366 -31
  89. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  90. package/lib/stages/postgen/postgen.js +192 -1
  91. package/lib/stages/postgen/postgen.poku.js +321 -0
  92. package/lib/stages/postgen/ruleEngine.js +116 -0
  93. package/lib/stages/pregen/envAudit.js +14 -3
  94. package/package.json +23 -21
  95. package/types/bin/hbom.d.ts +3 -0
  96. package/types/bin/hbom.d.ts.map +1 -0
  97. package/types/bin/repl.d.ts.map +1 -1
  98. package/types/lib/audit/index.d.ts +44 -0
  99. package/types/lib/audit/index.d.ts.map +1 -1
  100. package/types/lib/audit/reporters.d.ts +16 -0
  101. package/types/lib/audit/reporters.d.ts.map +1 -1
  102. package/types/lib/audit/targets.d.ts.map +1 -1
  103. package/types/lib/cli/index.d.ts +16 -0
  104. package/types/lib/cli/index.d.ts.map +1 -1
  105. package/types/lib/evinser/evinser.d.ts +4 -0
  106. package/types/lib/evinser/evinser.d.ts.map +1 -1
  107. package/types/lib/helpers/analyzer.d.ts +33 -0
  108. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  109. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  110. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  111. package/types/lib/helpers/asarutils.d.ts +34 -0
  112. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  113. package/types/lib/helpers/auditCategories.d.ts +5 -0
  114. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  115. package/types/lib/helpers/cbomutils.d.ts +3 -2
  116. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  117. package/types/lib/helpers/display.d.ts.map +1 -1
  118. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  119. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  120. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  121. package/types/lib/helpers/gtfobins.d.ts +8 -0
  122. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  123. package/types/lib/helpers/hbom.d.ts +49 -0
  124. package/types/lib/helpers/hbom.d.ts.map +1 -0
  125. package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
  126. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  127. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  128. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  129. package/types/lib/helpers/hostTopology.d.ts +12 -0
  130. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  131. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  132. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  133. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  134. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  135. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  136. package/types/lib/helpers/plugins.d.ts +58 -0
  137. package/types/lib/helpers/plugins.d.ts.map +1 -0
  138. package/types/lib/helpers/protobom.d.ts +3 -4
  139. package/types/lib/helpers/protobom.d.ts.map +1 -1
  140. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  141. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  142. package/types/lib/helpers/source.d.ts.map +1 -1
  143. package/types/lib/helpers/utils.d.ts +45 -8
  144. package/types/lib/helpers/utils.d.ts.map +1 -1
  145. package/types/lib/managers/binary.d.ts +5 -0
  146. package/types/lib/managers/binary.d.ts.map +1 -1
  147. package/types/lib/managers/docker.d.ts.map +1 -1
  148. package/types/lib/server/server.d.ts +2 -1
  149. package/types/lib/server/server.d.ts.map +1 -1
  150. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  151. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  152. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  153. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  154. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  155. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  156. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  157. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -1,22 +1,138 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ rmSync,
7
+ symlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
1
14
  import esmock from "esmock";
2
15
  import { assert, it } from "poku";
3
16
  import sinon from "sinon";
4
17
 
5
- async function loadBinaryModule({ utilsOverrides } = {}) {
18
+ function createStubbedPluginRuntime() {
19
+ let platform = process.platform;
20
+ let extn = "";
21
+ if (platform === "win32") {
22
+ platform = "windows";
23
+ extn = ".exe";
24
+ }
25
+
26
+ let arch = process.arch;
27
+ if (arch === "x64") {
28
+ arch = "amd64";
29
+ } else if (arch === "ppc64") {
30
+ arch = "ppc64le";
31
+ } else if (arch === "x32") {
32
+ arch = "386";
33
+ }
34
+
35
+ const pluginsDir = process.env.CDXGEN_PLUGINS_DIR || "";
36
+ const pluginManifestFile =
37
+ pluginsDir && existsSync(path.join(pluginsDir, "plugins-manifest.json"))
38
+ ? path.join(pluginsDir, "plugins-manifest.json")
39
+ : undefined;
40
+
41
+ return {
42
+ arch,
43
+ extn,
44
+ extraNMBinPath: undefined,
45
+ platform,
46
+ pluginManifestFile,
47
+ pluginVersion: "1.0.0",
48
+ pluginsBinSuffix: "",
49
+ pluginsDir,
50
+ };
51
+ }
52
+
53
+ function resolveStubbedPluginBinary(toolName, pluginRuntime) {
54
+ const envCommandNames = {
55
+ "cargo-auditable": "CARGO_AUDITABLE_CMD",
56
+ dosai: "DOSAI_CMD",
57
+ osquery: "OSQUERY_CMD",
58
+ sourcekitten: "SOURCEKITTEN_CMD",
59
+ trivy: "TRIVY_CMD",
60
+ trustinspector: "TRUSTINSPECTOR_CMD",
61
+ };
62
+ const envCommandName = envCommandNames[toolName];
63
+ if (envCommandName && process.env[envCommandName]) {
64
+ return process.env[envCommandName];
65
+ }
66
+ if (!pluginRuntime?.pluginsDir) {
67
+ return undefined;
68
+ }
69
+ if (!existsSync(path.join(pluginRuntime.pluginsDir, toolName))) {
70
+ return undefined;
71
+ }
72
+ switch (toolName) {
73
+ case "trivy":
74
+ return path.join(
75
+ pluginRuntime.pluginsDir,
76
+ "trivy",
77
+ `trivy-cdxgen-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
78
+ );
79
+ case "osquery": {
80
+ const expectedPath = path.join(
81
+ pluginRuntime.pluginsDir,
82
+ "osquery",
83
+ `osqueryi-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
84
+ );
85
+ return pluginRuntime.platform === "darwin"
86
+ ? `${expectedPath}.app/Contents/MacOS/osqueryd`
87
+ : expectedPath;
88
+ }
89
+ case "trustinspector":
90
+ return path.join(
91
+ pluginRuntime.pluginsDir,
92
+ "trustinspector",
93
+ `trustinspector-cdxgen-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
94
+ );
95
+ default:
96
+ return undefined;
97
+ }
98
+ }
99
+
100
+ async function loadBinaryModule({ pluginsOverrides, utilsOverrides } = {}) {
6
101
  return esmock("./binary.js", {
102
+ "../helpers/plugins.js": {
103
+ resolveCdxgenPlugins: sinon
104
+ .stub()
105
+ .callsFake(() => createStubbedPluginRuntime()),
106
+ resolvePluginBinary: sinon
107
+ .stub()
108
+ .callsFake((toolName, pluginRuntime = createStubbedPluginRuntime()) =>
109
+ resolveStubbedPluginBinary(toolName, pluginRuntime),
110
+ ),
111
+ setPluginsPathEnv: sinon
112
+ .stub()
113
+ .callsFake((pluginRuntime) => pluginRuntime),
114
+ ...pluginsOverrides,
115
+ },
7
116
  "../helpers/utils.js": {
8
117
  adjustLicenseInformation: sinon.stub(),
118
+ attachIdentityTools: sinon.stub(),
9
119
  collectExecutables: sinon.stub().returns([]),
10
120
  collectSharedLibs: sinon.stub().returns([]),
11
121
  DEBUG_MODE: false,
12
122
  dirNameStr: "/tmp",
13
123
  extractPathEnv: sinon.stub().returns([]),
124
+ extractToolRefs: sinon.stub().returns([]),
14
125
  findLicenseId: sinon.stub(),
15
126
  getTmpDir: sinon.stub().returns("/tmp"),
127
+ hasDangerousUnicode: sinon.stub().returns(false),
16
128
  isDryRun: false,
129
+ isValidDriveRoot: sinon
130
+ .stub()
131
+ .callsFake((root) => /^[A-Za-z]:\\$/.test(root)),
17
132
  isSpdxLicenseExpression: sinon.stub().returns(false),
18
133
  multiChecksumFile: sinon.stub(),
19
134
  recordActivity: sinon.stub(),
135
+ recordSymlinkResolution: sinon.stub(),
20
136
  retrieveCdxgenPluginVersion: sinon.stub().returns("1.0.0"),
21
137
  safeExistsSync: sinon.stub().returns(false),
22
138
  safeMkdirSync: sinon.stub(),
@@ -27,9 +143,37 @@ async function loadBinaryModule({ utilsOverrides } = {}) {
27
143
  .returns({ status: 1, stdout: "", stderr: "" }),
28
144
  ...utilsOverrides,
29
145
  },
146
+ "./containerutils.js": {
147
+ getDirs: sinon.stub().returns([]),
148
+ },
30
149
  });
31
150
  }
32
151
 
152
+ function loadPluginToolsInSubprocess(
153
+ pluginsDir,
154
+ toolNames = ["trustinspector"],
155
+ ) {
156
+ const binaryModuleUrl = new URL("./binary.js", import.meta.url);
157
+ const result = spawnSync(
158
+ process.execPath,
159
+ [
160
+ "--input-type=module",
161
+ "-e",
162
+ `import { getPluginToolComponents } from ${JSON.stringify(binaryModuleUrl.href)}; console.log(JSON.stringify(getPluginToolComponents(${JSON.stringify(toolNames)})));`,
163
+ ],
164
+ {
165
+ cwd: path.dirname(fileURLToPath(binaryModuleUrl)),
166
+ encoding: "utf-8",
167
+ env: {
168
+ ...process.env,
169
+ CDXGEN_PLUGINS_DIR: pluginsDir,
170
+ },
171
+ },
172
+ );
173
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout);
174
+ return JSON.parse(result.stdout.trim() || "[]");
175
+ }
176
+
33
177
  it("executeOsQuery() reports a blocked dry-run activity", async () => {
34
178
  const recordActivity = sinon.stub();
35
179
  const { executeOsQuery } = await loadBinaryModule({
@@ -47,6 +191,431 @@ it("executeOsQuery() reports a blocked dry-run activity", async () => {
47
191
  });
48
192
  });
49
193
 
194
+ it("executeOsQuery() uses osquery shell mode with the persistent database disabled", async () => {
195
+ const safeSpawnSync = sinon
196
+ .stub()
197
+ .returns({ status: 0, stdout: '[{"ok":"1"}]', stderr: "" });
198
+ const previousOsqueryCmd = process.env.OSQUERY_CMD;
199
+ process.env.OSQUERY_CMD = "/tmp/osqueryd";
200
+ try {
201
+ const { executeOsQuery } = await loadBinaryModule({
202
+ utilsOverrides: {
203
+ safeSpawnSync,
204
+ },
205
+ });
206
+ const result = executeOsQuery("select 1 as ok");
207
+ assert.deepStrictEqual(result, [{ ok: "1" }]);
208
+ assert.ok(safeSpawnSync.callCount >= 1);
209
+ assert.strictEqual(safeSpawnSync.lastCall.args[0], "/tmp/osqueryd");
210
+ const args = safeSpawnSync.lastCall.args[1];
211
+ assert.ok(args.includes("--S"));
212
+ assert.ok(args.includes("--disable_database"));
213
+ assert.ok(args.includes("--json"));
214
+ assert.ok(args.includes("select 1 as ok;"));
215
+ if (process.platform === "darwin") {
216
+ assert.ok(args.includes("--allow_unsafe"));
217
+ assert.ok(args.includes("--disable_logging"));
218
+ assert.ok(args.includes("--disable_events"));
219
+ }
220
+ } finally {
221
+ if (previousOsqueryCmd === undefined) {
222
+ delete process.env.OSQUERY_CMD;
223
+ } else {
224
+ process.env.OSQUERY_CMD = previousOsqueryCmd;
225
+ }
226
+ }
227
+ });
228
+
229
+ it("getOSPackages() does not misclassify non-launchpad URLs as PPAs", async () => {
230
+ const rootfs = mkdtempSync(path.join(tmpdir(), "cdxgen-rootfs-ppa-check-"));
231
+ try {
232
+ mkdirSync(path.join(rootfs, "etc", "apt", "sources.list.d"), {
233
+ recursive: true,
234
+ });
235
+ writeFileSync(
236
+ path.join(rootfs, "etc", "apt", "sources.list.d", "example.list"),
237
+ [
238
+ "deb https://example.com/redirect/ppa.launchpad.net/ondrej/php ubuntu main",
239
+ ].join("\n"),
240
+ );
241
+ const { getOSPackages } = await loadBinaryModule({
242
+ utilsOverrides: {
243
+ collectExecutables: sinon.stub().returns([]),
244
+ collectSharedLibs: sinon.stub().returns([]),
245
+ extractPathEnv: sinon.stub().returns([]),
246
+ safeExistsSync: sinon
247
+ .stub()
248
+ .callsFake((filePath) => existsSync(filePath)),
249
+ safeSpawnSync: sinon
250
+ .stub()
251
+ .returns({ status: 0, stdout: "", stderr: "" }),
252
+ },
253
+ });
254
+ const result = await getOSPackages(rootfs, { Env: [] });
255
+ const repoComponent = result.osPackages.find((component) =>
256
+ component.properties?.some(
257
+ (property) => property.name === "cdx:os:repo:url",
258
+ ),
259
+ );
260
+ assert.ok(repoComponent);
261
+ assert.ok(
262
+ repoComponent.properties?.some(
263
+ (property) =>
264
+ property.name === "cdx:os:repo:type" &&
265
+ property.value === "apt-source",
266
+ ),
267
+ );
268
+ } finally {
269
+ rmSync(rootfs, { recursive: true, force: true });
270
+ }
271
+ });
272
+
273
+ it("getOSPackages() skips trustinspector rootfs execution for dangerous paths", async () => {
274
+ const pluginsDir = mkdtempSync(path.join(tmpdir(), "cdxgen-plugins-trust-"));
275
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
276
+ const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
277
+ const safeSpawnSync = sinon.stub().callsFake((command) => {
278
+ if (command === "ldd") {
279
+ return { status: 1, stdout: "", stderr: "" };
280
+ }
281
+ return { status: 0, stdout: "", stderr: "" };
282
+ });
283
+ try {
284
+ writeFileSync(
285
+ path.join(pluginsDir, "plugins-manifest.json"),
286
+ JSON.stringify({
287
+ plugins: [
288
+ {
289
+ name: "trustinspector",
290
+ component: {
291
+ type: "application",
292
+ name: "trustinspector",
293
+ version: "2.1.0",
294
+ },
295
+ },
296
+ ],
297
+ }),
298
+ );
299
+ process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
300
+ process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
301
+ const { getOSPackages } = await loadBinaryModule({
302
+ utilsOverrides: {
303
+ collectExecutables: sinon.stub().returns([]),
304
+ collectSharedLibs: sinon.stub().returns([]),
305
+ extractPathEnv: sinon.stub().returns([]),
306
+ hasDangerousUnicode: sinon
307
+ .stub()
308
+ .callsFake((value) => `${value || ""}`.includes("\u202e")),
309
+ safeExistsSync: sinon.stub().returns(false),
310
+ safeSpawnSync,
311
+ },
312
+ });
313
+ await getOSPackages("/tmp/rootfs\u202e", { Env: [] });
314
+ assert.ok(
315
+ safeSpawnSync
316
+ .getCalls()
317
+ .every(
318
+ (call) =>
319
+ call.args[0] !== "/tmp/trustinspector" ||
320
+ call.args[1]?.[0] !== "rootfs",
321
+ ),
322
+ );
323
+ } finally {
324
+ if (previousPluginsDir === undefined) {
325
+ delete process.env.CDXGEN_PLUGINS_DIR;
326
+ } else {
327
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
328
+ }
329
+ if (previousTrustInspectorCmd === undefined) {
330
+ delete process.env.TRUSTINSPECTOR_CMD;
331
+ } else {
332
+ process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
333
+ }
334
+ rmSync(pluginsDir, { recursive: true, force: true });
335
+ }
336
+ });
337
+
338
+ it("getOSPackages() skips trustinspector rootfs execution for non-directory targets", async () => {
339
+ const pluginsDir = mkdtempSync(path.join(tmpdir(), "cdxgen-plugins-trust-"));
340
+ const rootfsFile = path.join(pluginsDir, "not-a-rootfs.txt");
341
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
342
+ const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
343
+ const safeSpawnSync = sinon.stub().callsFake((command) => {
344
+ if (command === "ldd") {
345
+ return { status: 1, stdout: "", stderr: "" };
346
+ }
347
+ return { status: 0, stdout: "", stderr: "" };
348
+ });
349
+ try {
350
+ writeFileSync(rootfsFile, "not a directory\n");
351
+ writeFileSync(
352
+ path.join(pluginsDir, "plugins-manifest.json"),
353
+ JSON.stringify({
354
+ plugins: [
355
+ {
356
+ name: "trustinspector",
357
+ component: {
358
+ type: "application",
359
+ name: "trustinspector",
360
+ version: "2.1.0",
361
+ },
362
+ },
363
+ ],
364
+ }),
365
+ );
366
+ process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
367
+ process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
368
+ const safeExistsSync = sinon
369
+ .stub()
370
+ .callsFake((targetPath) => targetPath === rootfsFile);
371
+ const { getOSPackages } = await loadBinaryModule({
372
+ utilsOverrides: {
373
+ collectExecutables: sinon.stub().returns([]),
374
+ collectSharedLibs: sinon.stub().returns([]),
375
+ extractPathEnv: sinon.stub().returns([]),
376
+ safeExistsSync,
377
+ safeSpawnSync,
378
+ },
379
+ });
380
+ await getOSPackages(rootfsFile, { Env: [] });
381
+ assert.ok(
382
+ safeSpawnSync
383
+ .getCalls()
384
+ .every(
385
+ (call) =>
386
+ call.args[0] !== "/tmp/trustinspector" ||
387
+ call.args[1]?.[0] !== "rootfs",
388
+ ),
389
+ );
390
+ } finally {
391
+ if (previousPluginsDir === undefined) {
392
+ delete process.env.CDXGEN_PLUGINS_DIR;
393
+ } else {
394
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
395
+ }
396
+ if (previousTrustInspectorCmd === undefined) {
397
+ delete process.env.TRUSTINSPECTOR_CMD;
398
+ } else {
399
+ process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
400
+ }
401
+ rmSync(pluginsDir, { recursive: true, force: true });
402
+ }
403
+ });
404
+
405
+ it("getOSPackages() preserves a valid symlinked rootfs path for trustinspector", async () => {
406
+ if (process.platform === "win32") {
407
+ return;
408
+ }
409
+ const pluginsDir = mkdtempSync(path.join(tmpdir(), "cdxgen-plugins-trust-"));
410
+ const realRootfsDir = mkdtempSync(path.join(tmpdir(), "cdxgen-rootfs-real-"));
411
+ const rootfsLink = path.join(pluginsDir, "rootfs-link");
412
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
413
+ const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
414
+ const safeSpawnSync = sinon.stub().callsFake((command, args) => {
415
+ if (command === "ldd") {
416
+ return { status: 1, stdout: "", stderr: "" };
417
+ }
418
+ if (command === "/tmp/trustinspector" && args?.[0] === "rootfs") {
419
+ return {
420
+ status: 0,
421
+ stdout: JSON.stringify({ materials: [] }),
422
+ stderr: "",
423
+ };
424
+ }
425
+ return { status: 0, stdout: "", stderr: "" };
426
+ });
427
+ try {
428
+ mkdirSync(path.join(realRootfsDir, "etc"), { recursive: true });
429
+ writeFileSync(
430
+ path.join(realRootfsDir, "etc", "os-release"),
431
+ 'ID="debian"\nVERSION_ID="12"\n',
432
+ );
433
+ symlinkSync(realRootfsDir, rootfsLink);
434
+ writeFileSync(
435
+ path.join(pluginsDir, "plugins-manifest.json"),
436
+ JSON.stringify({
437
+ plugins: [
438
+ {
439
+ name: "trustinspector",
440
+ component: {
441
+ type: "application",
442
+ name: "trustinspector",
443
+ version: "2.1.0",
444
+ },
445
+ },
446
+ ],
447
+ }),
448
+ );
449
+ process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
450
+ process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
451
+ const { getOSPackages } = await loadBinaryModule({
452
+ utilsOverrides: {
453
+ collectExecutables: sinon.stub().returns([]),
454
+ collectSharedLibs: sinon.stub().returns([]),
455
+ extractPathEnv: sinon.stub().returns([]),
456
+ safeExistsSync: sinon
457
+ .stub()
458
+ .callsFake((targetPath) => existsSync(targetPath)),
459
+ safeSpawnSync,
460
+ },
461
+ });
462
+ await getOSPackages(rootfsLink, { Env: [] });
463
+ assert.ok(
464
+ safeSpawnSync.calledWith(
465
+ "/tmp/trustinspector",
466
+ sinon.match(
467
+ (args) => args?.[0] === "rootfs" && args?.[1] === rootfsLink,
468
+ ),
469
+ ),
470
+ );
471
+ } finally {
472
+ if (previousPluginsDir === undefined) {
473
+ delete process.env.CDXGEN_PLUGINS_DIR;
474
+ } else {
475
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
476
+ }
477
+ if (previousTrustInspectorCmd === undefined) {
478
+ delete process.env.TRUSTINSPECTOR_CMD;
479
+ } else {
480
+ process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
481
+ }
482
+ rmSync(pluginsDir, { recursive: true, force: true });
483
+ rmSync(realRootfsDir, { recursive: true, force: true });
484
+ }
485
+ });
486
+
487
+ it("getPluginToolComponents() reads precise tool metadata from the plugins manifest", async () => {
488
+ const pluginsDir = mkdtempSync(
489
+ path.join(tmpdir(), "cdxgen-plugins-manifest-"),
490
+ );
491
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
492
+ try {
493
+ writeFileSync(
494
+ path.join(pluginsDir, "plugins-manifest.json"),
495
+ JSON.stringify({
496
+ plugins: [
497
+ {
498
+ name: "trustinspector",
499
+ component: {
500
+ type: "application",
501
+ name: "trustinspector",
502
+ version: "2.1.0",
503
+ purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
504
+ "bom-ref":
505
+ "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
506
+ hashes: [{ alg: "SHA-256", content: "a".repeat(64) }],
507
+ },
508
+ },
509
+ ],
510
+ }),
511
+ );
512
+ const tools = loadPluginToolsInSubprocess(pluginsDir);
513
+ assert.strictEqual(tools.length, 1);
514
+ assert.strictEqual(tools[0].name, "trustinspector");
515
+ assert.strictEqual(tools[0].version, "2.1.0");
516
+ assert.match(tools[0].purl, /trustinspector-cdxgen/);
517
+ } finally {
518
+ if (previousPluginsDir === undefined) {
519
+ delete process.env.CDXGEN_PLUGINS_DIR;
520
+ } else {
521
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
522
+ }
523
+ rmSync(pluginsDir, { recursive: true, force: true });
524
+ }
525
+ });
526
+
527
+ it("getPluginToolComponents() sanitizes manifest tool metadata before use", async () => {
528
+ const pluginsDir = mkdtempSync(
529
+ path.join(tmpdir(), "cdxgen-plugins-manifest-sanitize-"),
530
+ );
531
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
532
+ try {
533
+ writeFileSync(
534
+ path.join(pluginsDir, "plugins-manifest.json"),
535
+ JSON.stringify({
536
+ plugins: [
537
+ {
538
+ name: "trustinspector",
539
+ component: {
540
+ name: "trustinspector",
541
+ version: "2.1.0",
542
+ purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
543
+ "bom-ref":
544
+ "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
545
+ properties: [
546
+ { name: "cdx:tool:origin", value: "plugins-manifest" },
547
+ { name: "", value: "ignored" },
548
+ { name: 1, value: "ignored" },
549
+ ],
550
+ externalReferences: [
551
+ { type: "vcs", url: "https://example.com/trustinspector" },
552
+ { type: "distribution", url: "" },
553
+ ],
554
+ hashes: [
555
+ { alg: "SHA-256", content: "a".repeat(64) },
556
+ { alg: "", content: "ignored" },
557
+ ],
558
+ nested: { should: "not-survive" },
559
+ },
560
+ },
561
+ ],
562
+ }),
563
+ );
564
+ const tools = loadPluginToolsInSubprocess(pluginsDir);
565
+ assert.strictEqual(tools.length, 1);
566
+ assert.strictEqual(tools[0].name, "trustinspector");
567
+ assert.strictEqual(tools[0].nested, undefined);
568
+ assert.strictEqual({}.polluted, undefined);
569
+ assert.deepStrictEqual(tools[0].properties, [
570
+ { name: "cdx:tool:origin", value: "plugins-manifest" },
571
+ ]);
572
+ assert.deepStrictEqual(tools[0].externalReferences, [
573
+ { type: "vcs", url: "https://example.com/trustinspector" },
574
+ ]);
575
+ assert.deepStrictEqual(tools[0].hashes, [
576
+ { alg: "SHA-256", content: "a".repeat(64) },
577
+ ]);
578
+ } finally {
579
+ if (previousPluginsDir === undefined) {
580
+ delete process.env.CDXGEN_PLUGINS_DIR;
581
+ } else {
582
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
583
+ }
584
+ rmSync(pluginsDir, { recursive: true, force: true });
585
+ }
586
+ });
587
+
588
+ it("getPluginToolComponents() ignores oversized plugins manifests", async () => {
589
+ const pluginsDir = mkdtempSync(
590
+ path.join(tmpdir(), "cdxgen-plugins-manifest-large-"),
591
+ );
592
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
593
+ try {
594
+ writeFileSync(
595
+ path.join(pluginsDir, "plugins-manifest.json"),
596
+ `${JSON.stringify({
597
+ plugins: [
598
+ {
599
+ name: "trustinspector",
600
+ component: {
601
+ name: "trustinspector",
602
+ "bom-ref": "pkg:generic/trustinspector@2.1.0",
603
+ },
604
+ },
605
+ ],
606
+ })}${" ".repeat(1024 * 1024)}`,
607
+ );
608
+ assert.deepStrictEqual(loadPluginToolsInSubprocess(pluginsDir), []);
609
+ } finally {
610
+ if (previousPluginsDir === undefined) {
611
+ delete process.env.CDXGEN_PLUGINS_DIR;
612
+ } else {
613
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
614
+ }
615
+ rmSync(pluginsDir, { recursive: true, force: true });
616
+ }
617
+ });
618
+
50
619
  it("getOSPackages() returns empty collections and reports a blocked dry-run activity", async () => {
51
620
  const recordActivity = sinon.stub();
52
621
  const { getOSPackages } = await loadBinaryModule({
@@ -67,3 +636,1132 @@ it("getOSPackages() returns empty collections and reports a blocked dry-run acti
67
636
  target: "/tmp/rootfs",
68
637
  });
69
638
  });
639
+
640
+ it("getOSPackages() creates package-owned file components and services from Trivy properties", async () => {
641
+ const rootfs = mkdtempSync(path.join(tmpdir(), "cdxgen-rootfs-"));
642
+ const trivyTempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-trivy-"));
643
+ const bomJsonFile = path.join(trivyTempDir, "trivy-bom.json");
644
+ const packagePurl = "pkg:apk/alpine/demo@1.0-r0?distro=alpine-3.20";
645
+ const packageRef = decodeURIComponent(packagePurl);
646
+ const collectExecutables = sinon.stub().returns([]);
647
+ const collectSharedLibs = sinon.stub().returns([]);
648
+ try {
649
+ mkdirSync(path.join(rootfs, "usr", "bin"), { recursive: true });
650
+ mkdirSync(path.join(rootfs, "usr", "lib"), { recursive: true });
651
+ mkdirSync(path.join(rootfs, "etc", "init.d"), { recursive: true });
652
+ mkdirSync(path.join(rootfs, "etc"), { recursive: true });
653
+ writeFileSync(path.join(rootfs, "usr", "bin", "demo"), "#!/bin/sh\n", {
654
+ mode: 0o644,
655
+ });
656
+ writeFileSync(path.join(rootfs, "usr", "lib", "libdemo.so.1"), "binary", {
657
+ mode: 0o644,
658
+ });
659
+ writeFileSync(
660
+ path.join(rootfs, "etc", "init.d", "demosvc"),
661
+ [
662
+ "#!/bin/sh",
663
+ "### BEGIN INIT INFO",
664
+ "# Provides: demosvc",
665
+ "# Short-Description: Demo service",
666
+ "### END INIT INFO",
667
+ "/usr/bin/demo start",
668
+ "",
669
+ ].join("\n"),
670
+ { mode: 0o755 },
671
+ );
672
+ writeFileSync(
673
+ path.join(rootfs, "etc", "os-release"),
674
+ "ID=alpine\nVERSION_ID=3.20.0\n",
675
+ );
676
+ writeFileSync(
677
+ bomJsonFile,
678
+ JSON.stringify({
679
+ metadata: { tools: [] },
680
+ components: [
681
+ {
682
+ "bom-ref": packageRef,
683
+ name: "demo",
684
+ purl: packagePurl,
685
+ properties: [
686
+ { name: "aquasecurity:trivy:PkgID", value: "demo@1.0-r0" },
687
+ { name: "aquasecurity:trivy:PkgType", value: "apk" },
688
+ { name: "aquasecurity:trivy:Capability", value: "cmd:demo" },
689
+ {
690
+ name: "aquasecurity:trivy:CapabilityCount",
691
+ value: "1",
692
+ },
693
+ {
694
+ name: "aquasecurity:trivy:InstalledCommand",
695
+ value: "demo",
696
+ },
697
+ {
698
+ name: "aquasecurity:trivy:InstalledCommandCount",
699
+ value: "1",
700
+ },
701
+ {
702
+ name: "aquasecurity:trivy:InstalledCommandPath",
703
+ value: "/usr/bin/demo",
704
+ },
705
+ {
706
+ name: "aquasecurity:trivy:InstalledFileCount",
707
+ value: "3",
708
+ },
709
+ {
710
+ name: "aquasecurity:trivy:InstalledFile",
711
+ value: "/usr/bin/demo",
712
+ },
713
+ {
714
+ name: "aquasecurity:trivy:InstalledFile",
715
+ value: "/usr/lib/libdemo.so.1",
716
+ },
717
+ {
718
+ name: "aquasecurity:trivy:InstalledFile",
719
+ value: "/etc/init.d/demosvc",
720
+ },
721
+ {
722
+ name: "aquasecurity:trivy:PackageVendor",
723
+ value: "Demo Vendor",
724
+ },
725
+ ],
726
+ supplier: { name: "Demo Maintainers <demo@example.test>" },
727
+ },
728
+ ],
729
+ dependencies: [],
730
+ }),
731
+ );
732
+ const originalTrivyCmd = process.env.TRIVY_CMD;
733
+ process.env.TRIVY_CMD = "/usr/bin/true";
734
+ const { getOSPackages } = await loadBinaryModule({
735
+ utilsOverrides: {
736
+ collectExecutables,
737
+ collectSharedLibs,
738
+ extractPathEnv: sinon.stub().returns(["/usr/bin"]),
739
+ getTmpDir: sinon.stub().returns(path.dirname(trivyTempDir)),
740
+ multiChecksumFile: sinon.stub().resolves({
741
+ md5: "a".repeat(32),
742
+ sha1: "b".repeat(40),
743
+ }),
744
+ safeExistsSync: sinon
745
+ .stub()
746
+ .callsFake((filePath) => existsSync(filePath)),
747
+ safeMkdtempSync: sinon.stub().returns(trivyTempDir),
748
+ safeSpawnSync: sinon.stub().callsFake((command) => {
749
+ if (command === "ldd") {
750
+ return { status: 1, stdout: "", stderr: "" };
751
+ }
752
+ return { status: 0, stdout: "", stderr: "" };
753
+ }),
754
+ },
755
+ });
756
+ const result = await getOSPackages(rootfs, { Env: ["PATH=/usr/bin"] });
757
+ process.env.TRIVY_CMD = originalTrivyCmd;
758
+
759
+ assert.strictEqual(result.osPackages.length, 1);
760
+ assert.strictEqual(result.osPackageFiles.length, 3);
761
+ assert.strictEqual(result.services.length, 1);
762
+ assert.strictEqual(result.services[0].name, "demosvc");
763
+ assert.ok(
764
+ result.osPackages[0].properties.some(
765
+ (prop) => prop.name.endsWith("Capability") && prop.value === "cmd:demo",
766
+ ),
767
+ );
768
+ assert.deepStrictEqual(result.osPackages[0].supplier, {
769
+ name: "Demo Maintainers <demo@example.test>",
770
+ });
771
+ assert.deepStrictEqual(result.osPackages[0].manufacturer, {
772
+ name: "Demo Vendor",
773
+ });
774
+ assert.deepStrictEqual(result.osPackages[0].authors, [
775
+ { name: "Demo Maintainers", email: "demo@example.test" },
776
+ ]);
777
+ assert.ok(
778
+ !(result.osPackages[0].properties || []).some((prop) =>
779
+ prop.name.endsWith("PackageVendor"),
780
+ ),
781
+ );
782
+ assert.ok(
783
+ result.osPackageFiles.some(
784
+ (component) =>
785
+ component.properties.some(
786
+ (prop) => prop.name === "SrcFile" && prop.value === "/usr/bin/demo",
787
+ ) &&
788
+ component.properties.some(
789
+ (prop) =>
790
+ prop.name === "internal:is_executable" && prop.value === "true",
791
+ ),
792
+ ),
793
+ );
794
+ assert.ok(
795
+ result.dependenciesList.some(
796
+ (dependency) =>
797
+ Array.isArray(dependency.provides) && dependency.provides.length >= 3,
798
+ ),
799
+ );
800
+ assert.ok(
801
+ result.dependenciesList.some(
802
+ (dependency) =>
803
+ dependency.ref === result.services[0]["bom-ref"] &&
804
+ Array.isArray(dependency.dependsOn) &&
805
+ dependency.dependsOn.length > 0,
806
+ ),
807
+ );
808
+ sinon.assert.calledWithMatch(
809
+ collectExecutables,
810
+ rootfs,
811
+ ["/usr/bin"],
812
+ ["/etc/init.d/demosvc", "/usr/bin/demo", "/usr/lib/libdemo.so.1"],
813
+ );
814
+ sinon.assert.calledWithMatch(
815
+ collectSharedLibs,
816
+ rootfs,
817
+ sinon.match.array,
818
+ "/etc/ld.so.conf",
819
+ "/etc/ld.so.conf.d/*.conf",
820
+ ["/etc/init.d/demosvc", "/usr/bin/demo", "/usr/lib/libdemo.so.1"],
821
+ );
822
+ } finally {
823
+ rmSync(rootfs, { recursive: true, force: true });
824
+ rmSync(trivyTempDir, { recursive: true, force: true });
825
+ delete process.env.TRIVY_CMD;
826
+ }
827
+ });
828
+
829
+ it("getOSPackages() omits setuid metadata from package-owned file components", async () => {
830
+ const rootfs = mkdtempSync(path.join(tmpdir(), "cdxgen-rootfs-setuid-"));
831
+ const trivyTempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-trivy-setuid-"));
832
+ const bomJsonFile = path.join(trivyTempDir, "trivy-bom.json");
833
+ const packagePurl = "pkg:apk/alpine/demo@1.0-r0?distro=alpine-3.20";
834
+ const packageRef = decodeURIComponent(packagePurl);
835
+ const originalTrivyCmd = process.env.TRIVY_CMD;
836
+ try {
837
+ mkdirSync(path.join(rootfs, "usr", "bin"), { recursive: true });
838
+ mkdirSync(path.join(rootfs, "etc"), { recursive: true });
839
+ writeFileSync(path.join(rootfs, "usr", "bin", "demo"), "#!/bin/sh\n", {
840
+ mode: 0o4755,
841
+ });
842
+ writeFileSync(
843
+ path.join(rootfs, "etc", "os-release"),
844
+ "ID=alpine\nVERSION_ID=3.20.0\n",
845
+ );
846
+ writeFileSync(
847
+ bomJsonFile,
848
+ JSON.stringify({
849
+ metadata: { tools: [] },
850
+ components: [
851
+ {
852
+ "bom-ref": packageRef,
853
+ name: "demo",
854
+ purl: packagePurl,
855
+ properties: [
856
+ { name: "aquasecurity:trivy:PkgID", value: "demo@1.0-r0" },
857
+ { name: "aquasecurity:trivy:PkgType", value: "apk" },
858
+ {
859
+ name: "aquasecurity:trivy:InstalledFile",
860
+ value: "/usr/bin/demo",
861
+ },
862
+ {
863
+ name: "aquasecurity:trivy:InstalledCommandPath",
864
+ value: "/usr/bin/demo",
865
+ },
866
+ ],
867
+ },
868
+ ],
869
+ dependencies: [],
870
+ }),
871
+ );
872
+ process.env.TRIVY_CMD = "/usr/bin/true";
873
+ const { getOSPackages } = await loadBinaryModule({
874
+ utilsOverrides: {
875
+ collectExecutables: sinon.stub().returns([]),
876
+ collectSharedLibs: sinon.stub().returns([]),
877
+ extractPathEnv: sinon.stub().returns(["/usr/bin"]),
878
+ getTmpDir: sinon.stub().returns(path.dirname(trivyTempDir)),
879
+ multiChecksumFile: sinon.stub().resolves({
880
+ md5: "a".repeat(32),
881
+ sha1: "b".repeat(40),
882
+ }),
883
+ safeExistsSync: sinon
884
+ .stub()
885
+ .callsFake((filePath) => existsSync(filePath)),
886
+ safeMkdtempSync: sinon.stub().returns(trivyTempDir),
887
+ safeSpawnSync: sinon.stub().callsFake((command) => {
888
+ if (command === "ldd") {
889
+ return { status: 1, stdout: "", stderr: "" };
890
+ }
891
+ return { status: 0, stdout: "", stderr: "" };
892
+ }),
893
+ },
894
+ });
895
+ const result = await getOSPackages(rootfs, { Env: ["PATH=/usr/bin"] });
896
+ process.env.TRIVY_CMD = originalTrivyCmd;
897
+
898
+ const fileComponent = result.osPackageFiles.find((component) =>
899
+ component.properties.some(
900
+ (prop) => prop.name === "SrcFile" && prop.value === "/usr/bin/demo",
901
+ ),
902
+ );
903
+ assert.ok(fileComponent);
904
+ assert.ok(
905
+ !fileComponent.properties.some(
906
+ (prop) => prop.name === "internal:has_setuid",
907
+ ),
908
+ );
909
+ } finally {
910
+ if (originalTrivyCmd === undefined) {
911
+ delete process.env.TRIVY_CMD;
912
+ } else {
913
+ process.env.TRIVY_CMD = originalTrivyCmd;
914
+ }
915
+ rmSync(rootfs, { recursive: true, force: true });
916
+ rmSync(trivyTempDir, { recursive: true, force: true });
917
+ }
918
+ });
919
+
920
+ it("getOSPackages() preserves conflicting native origin fields and retains fallback trust properties", async () => {
921
+ const rootfs = mkdtempSync(
922
+ path.join(tmpdir(), "cdxgen-rootfs-native-conflict-"),
923
+ );
924
+ const trivyTempDir = mkdtempSync(
925
+ path.join(tmpdir(), "cdxgen-trivy-native-conflict-"),
926
+ );
927
+ const bomJsonFile = path.join(trivyTempDir, "trivy-bom.json");
928
+ const packagePurl = "pkg:apk/alpine/demo@1.0-r0?distro=alpine-3.20";
929
+ const packageRef = decodeURIComponent(packagePurl);
930
+ try {
931
+ mkdirSync(path.join(rootfs, "etc"), { recursive: true });
932
+ writeFileSync(
933
+ path.join(rootfs, "etc", "os-release"),
934
+ "ID=alpine\nVERSION_ID=3.20.0\n",
935
+ { encoding: "utf-8" },
936
+ );
937
+ writeFileSync(
938
+ bomJsonFile,
939
+ JSON.stringify({
940
+ metadata: { tools: [] },
941
+ components: [
942
+ {
943
+ "bom-ref": packageRef,
944
+ name: "demo",
945
+ purl: packagePurl,
946
+ supplier: { name: "Existing Supplier" },
947
+ manufacturer: { name: "Existing Manufacturer" },
948
+ authors: [
949
+ { name: "Existing Author", email: "author@example.test" },
950
+ ],
951
+ properties: [
952
+ { name: "aquasecurity:trivy:PkgID", value: "demo@1.0-r0" },
953
+ { name: "aquasecurity:trivy:PkgType", value: "apk" },
954
+ {
955
+ name: "aquasecurity:trivy:PackageMaintainer",
956
+ value: "Demo Maintainers <demo@example.test>",
957
+ },
958
+ {
959
+ name: "aquasecurity:trivy:PackageVendor",
960
+ value: "Demo Vendor",
961
+ },
962
+ ],
963
+ },
964
+ ],
965
+ dependencies: [],
966
+ }),
967
+ );
968
+ process.env.TRIVY_CMD = "/usr/bin/true";
969
+ const { getOSPackages } = await loadBinaryModule({
970
+ utilsOverrides: {
971
+ extractPathEnv: sinon.stub().returns([]),
972
+ getTmpDir: sinon.stub().returns(path.dirname(trivyTempDir)),
973
+ safeExistsSync: sinon
974
+ .stub()
975
+ .callsFake((filePath) => existsSync(filePath)),
976
+ safeMkdtempSync: sinon.stub().returns(trivyTempDir),
977
+ safeSpawnSync: sinon.stub().callsFake((command) => {
978
+ if (command === "ldd") {
979
+ return { status: 1, stdout: "", stderr: "" };
980
+ }
981
+ return { status: 0, stdout: "", stderr: "" };
982
+ }),
983
+ },
984
+ });
985
+
986
+ const result = await getOSPackages(rootfs, {});
987
+ assert.strictEqual(result.osPackages.length, 1);
988
+ assert.deepStrictEqual(result.osPackages[0].supplier, {
989
+ name: "Existing Supplier",
990
+ });
991
+ assert.deepStrictEqual(result.osPackages[0].manufacturer, {
992
+ name: "Existing Manufacturer",
993
+ });
994
+ assert.deepStrictEqual(result.osPackages[0].authors, [
995
+ { name: "Existing Author", email: "author@example.test" },
996
+ ]);
997
+ assert.ok(
998
+ (result.osPackages[0].properties || []).some(
999
+ (prop) =>
1000
+ prop.name.endsWith("PackageMaintainer") &&
1001
+ prop.value === "Demo Maintainers <demo@example.test>",
1002
+ ),
1003
+ );
1004
+ assert.ok(
1005
+ (result.osPackages[0].properties || []).some(
1006
+ (prop) =>
1007
+ prop.name.endsWith("PackageVendor") && prop.value === "Demo Vendor",
1008
+ ),
1009
+ );
1010
+ } finally {
1011
+ rmSync(rootfs, { recursive: true, force: true });
1012
+ rmSync(trivyTempDir, { recursive: true, force: true });
1013
+ delete process.env.TRIVY_CMD;
1014
+ }
1015
+ });
1016
+
1017
+ it("getOSPackages() inventories rootfs repository sources and trusted keys without Trivy package data", async () => {
1018
+ const rootfs = mkdtempSync(path.join(tmpdir(), "cdxgen-rootfs-repos-"));
1019
+ const pluginsDir = mkdtempSync(path.join(tmpdir(), "cdxgen-plugins-rootfs-"));
1020
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
1021
+ const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
1022
+ try {
1023
+ mkdirSync(path.join(rootfs, "etc", "apt", "sources.list.d"), {
1024
+ recursive: true,
1025
+ });
1026
+ mkdirSync(path.join(rootfs, "usr", "share", "keyrings"), {
1027
+ recursive: true,
1028
+ });
1029
+ mkdirSync(path.join(rootfs, "etc", "yum.repos.d"), { recursive: true });
1030
+ mkdirSync(path.join(rootfs, "etc", "pki", "rpm-gpg"), {
1031
+ recursive: true,
1032
+ });
1033
+ writeFileSync(
1034
+ path.join(rootfs, "etc", "apt", "sources.list.d", "ondrej-php.list"),
1035
+ "deb [signed-by=/usr/share/keyrings/ondrej-php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main\n",
1036
+ );
1037
+ writeFileSync(
1038
+ path.join(rootfs, "usr", "share", "keyrings", "ondrej-php.gpg"),
1039
+ "fake-apt-key",
1040
+ );
1041
+ writeFileSync(
1042
+ path.join(rootfs, "etc", "yum.repos.d", "custom.repo"),
1043
+ [
1044
+ "[custom]",
1045
+ "name=Custom Repo",
1046
+ "baseurl=https://packages.example.test/rpm/$basearch",
1047
+ "enabled=1",
1048
+ "gpgcheck=1",
1049
+ "gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-test",
1050
+ "",
1051
+ ].join("\n"),
1052
+ );
1053
+ writeFileSync(
1054
+ path.join(rootfs, "etc", "pki", "rpm-gpg", "RPM-GPG-KEY-test"),
1055
+ "fake-rpm-key",
1056
+ );
1057
+ writeFileSync(
1058
+ path.join(pluginsDir, "plugins-manifest.json"),
1059
+ JSON.stringify({
1060
+ plugins: [
1061
+ {
1062
+ name: "trustinspector",
1063
+ component: {
1064
+ type: "application",
1065
+ name: "trustinspector",
1066
+ version: "2.1.0",
1067
+ purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
1068
+ "bom-ref":
1069
+ "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
1070
+ },
1071
+ },
1072
+ ],
1073
+ }),
1074
+ );
1075
+ process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
1076
+ process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
1077
+ const { getOSPackages } = await loadBinaryModule({
1078
+ utilsOverrides: {
1079
+ collectExecutables: sinon.stub().returns([]),
1080
+ collectSharedLibs: sinon.stub().returns([]),
1081
+ extractPathEnv: sinon.stub().returns([]),
1082
+ multiChecksumFile: sinon
1083
+ .stub()
1084
+ .callsFake(async (_algorithms, filePath) => ({
1085
+ sha1: filePath.includes("ondrej") ? "1".repeat(40) : "2".repeat(40),
1086
+ sha256: filePath.includes("ondrej")
1087
+ ? "a".repeat(64)
1088
+ : "b".repeat(64),
1089
+ })),
1090
+ safeExistsSync: sinon
1091
+ .stub()
1092
+ .callsFake((filePath) => existsSync(filePath)),
1093
+ safeSpawnSync: sinon.stub().callsFake((command, args) => {
1094
+ if (command === "ldd") {
1095
+ return { status: 1, stdout: "", stderr: "" };
1096
+ }
1097
+ if (command === "/tmp/trustinspector" && args[0] === "rootfs") {
1098
+ return {
1099
+ status: 0,
1100
+ stdout: JSON.stringify({
1101
+ materials: [
1102
+ {
1103
+ kind: "public-key",
1104
+ path: "/usr/share/keyrings/ondrej-php.gpg",
1105
+ name: "ondrej-php.gpg",
1106
+ trustDomain: "apt",
1107
+ sourceType: "repository-keyring",
1108
+ fileExtension: "gpg",
1109
+ sha1: "1".repeat(40),
1110
+ sha256: "a".repeat(64),
1111
+ keyId: "ABCDEF1234567890",
1112
+ algorithm: "RSA",
1113
+ keyStrength: 4096,
1114
+ fingerprint: "F".repeat(40),
1115
+ userIds: ["Ondrej Surý <ondrej@example.test>"],
1116
+ properties: [
1117
+ {
1118
+ name: "cdx:crypto:sourceType",
1119
+ value: "repository-keyring",
1120
+ },
1121
+ ],
1122
+ },
1123
+ {
1124
+ kind: "certificate",
1125
+ path: "/etc/ssl/certs/demo-root.crt",
1126
+ name: "demo-root",
1127
+ trustDomain: "ca-store",
1128
+ sourceType: "ca-store",
1129
+ fileExtension: "crt",
1130
+ sha1: "3".repeat(40),
1131
+ sha256: "c".repeat(64),
1132
+ algorithm: "RSA",
1133
+ keyStrength: 2048,
1134
+ createdAt: "2024-01-01T00:00:00Z",
1135
+ expiresAt: "2034-01-01T00:00:00Z",
1136
+ fingerprint: "D".repeat(64),
1137
+ subject: "CN=demo-root,O=Example Org",
1138
+ issuer: "CN=demo-root,O=Example Org",
1139
+ serial: "42",
1140
+ format: "X.509",
1141
+ properties: [{ name: "cdx:crypto:isCA", value: "true" }],
1142
+ },
1143
+ ],
1144
+ }),
1145
+ stderr: "",
1146
+ };
1147
+ }
1148
+ return { status: 0, stdout: "", stderr: "" };
1149
+ }),
1150
+ },
1151
+ });
1152
+ const result = await getOSPackages(rootfs, { Env: [] });
1153
+
1154
+ const cryptoComponents = result.osPackages.filter(
1155
+ (component) => component.type === "cryptographic-asset",
1156
+ );
1157
+ const ppaComponent = result.osPackages.find(
1158
+ (component) =>
1159
+ component.type === "data" &&
1160
+ component.properties?.some(
1161
+ (property) =>
1162
+ property.name === "cdx:os:repo:type" &&
1163
+ property.value === "ppa-source",
1164
+ ),
1165
+ );
1166
+ const yumComponent = result.osPackages.find(
1167
+ (component) =>
1168
+ component.type === "data" &&
1169
+ component.properties?.some(
1170
+ (property) =>
1171
+ property.name === "cdx:os:repo:type" &&
1172
+ property.value === "yum-source",
1173
+ ),
1174
+ );
1175
+ const ppaKeyRef = cryptoComponents.find((component) =>
1176
+ component.properties?.some(
1177
+ (property) =>
1178
+ property.name === "SrcFile" &&
1179
+ property.value === "/usr/share/keyrings/ondrej-php.gpg",
1180
+ ),
1181
+ )?.["bom-ref"];
1182
+ const yumKeyRef = cryptoComponents.find((component) =>
1183
+ component.properties?.some(
1184
+ (property) =>
1185
+ property.name === "SrcFile" &&
1186
+ property.value === "/etc/pki/rpm-gpg/RPM-GPG-KEY-test",
1187
+ ),
1188
+ )?.["bom-ref"];
1189
+
1190
+ assert.strictEqual(cryptoComponents.length, 3);
1191
+ assert.ok(
1192
+ cryptoComponents.some(
1193
+ (component) =>
1194
+ component.cryptoProperties?.assetType === "related-crypto-material" &&
1195
+ component.cryptoProperties?.relatedCryptoMaterialProperties?.type ===
1196
+ "public-key",
1197
+ ),
1198
+ );
1199
+ assert.ok(
1200
+ cryptoComponents.some(
1201
+ (component) =>
1202
+ component.cryptoProperties?.assetType === "certificate" &&
1203
+ component.properties?.some(
1204
+ (property) =>
1205
+ property.name === "cdx:crypto:trustDomain" &&
1206
+ property.value === "ca-store",
1207
+ ),
1208
+ ),
1209
+ );
1210
+ assert.ok(
1211
+ cryptoComponents.some((component) =>
1212
+ component.properties?.some(
1213
+ (property) =>
1214
+ property.name === "cdx:crypto:keyId" &&
1215
+ property.value === "ABCDEF1234567890",
1216
+ ),
1217
+ ),
1218
+ );
1219
+ assert.ok(ppaComponent);
1220
+ assert.ok(yumComponent);
1221
+ assert.ok(ppaKeyRef);
1222
+ assert.ok(yumKeyRef);
1223
+ assert.ok(
1224
+ result.dependenciesList.some(
1225
+ (dependency) =>
1226
+ dependency.ref === ppaComponent["bom-ref"] &&
1227
+ Array.isArray(dependency.dependsOn) &&
1228
+ dependency.dependsOn.includes(ppaKeyRef),
1229
+ ),
1230
+ );
1231
+ assert.ok(
1232
+ result.dependenciesList.some(
1233
+ (dependency) =>
1234
+ dependency.ref === yumComponent["bom-ref"] &&
1235
+ Array.isArray(dependency.dependsOn) &&
1236
+ dependency.dependsOn.includes(yumKeyRef),
1237
+ ),
1238
+ );
1239
+ } finally {
1240
+ if (previousPluginsDir === undefined) {
1241
+ delete process.env.CDXGEN_PLUGINS_DIR;
1242
+ } else {
1243
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
1244
+ }
1245
+ if (previousTrustInspectorCmd === undefined) {
1246
+ delete process.env.TRUSTINSPECTOR_CMD;
1247
+ } else {
1248
+ process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
1249
+ }
1250
+ rmSync(pluginsDir, { recursive: true, force: true });
1251
+ rmSync(rootfs, { recursive: true, force: true });
1252
+ }
1253
+ });
1254
+
1255
+ it("enrichOSComponentsWithTrustData() merges path inspections and host findings", async () => {
1256
+ if (!["darwin", "win32"].includes(process.platform)) {
1257
+ return;
1258
+ }
1259
+ const pluginsDir = mkdtempSync(
1260
+ path.join(tmpdir(), "cdxgen-plugins-hosttrust-"),
1261
+ );
1262
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
1263
+ const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
1264
+ const inspectedPath =
1265
+ process.platform === "win32"
1266
+ ? "C:\\Demo\\demo.exe"
1267
+ : "/Applications/Demo.app";
1268
+ const expectedProperty =
1269
+ process.platform === "win32"
1270
+ ? { name: "cdx:windows:authenticode:status", value: "Valid" }
1271
+ : { name: "cdx:darwin:codesign:teamIdentifier", value: "ABCDE12345" };
1272
+ const hostFinding =
1273
+ process.platform === "win32"
1274
+ ? {
1275
+ kind: "windows-wdac-status",
1276
+ name: "wdac-active-policies",
1277
+ version: "1",
1278
+ description: "active policies",
1279
+ properties: [
1280
+ {
1281
+ name: "cdx:windows:wdac:activePolicyCount",
1282
+ value: "1",
1283
+ },
1284
+ ],
1285
+ }
1286
+ : {
1287
+ kind: "darwin-gatekeeper-status",
1288
+ name: "gatekeeper-system-policy",
1289
+ version: "enabled",
1290
+ description: "assessments enabled",
1291
+ properties: [
1292
+ {
1293
+ name: "cdx:darwin:gatekeeper:status",
1294
+ value: "enabled",
1295
+ },
1296
+ ],
1297
+ };
1298
+ try {
1299
+ writeFileSync(
1300
+ path.join(pluginsDir, "plugins-manifest.json"),
1301
+ JSON.stringify({
1302
+ plugins: [
1303
+ {
1304
+ name: "trustinspector",
1305
+ component: {
1306
+ type: "application",
1307
+ name: "trustinspector",
1308
+ version: "2.1.0",
1309
+ purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
1310
+ "bom-ref":
1311
+ "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.0",
1312
+ },
1313
+ },
1314
+ ],
1315
+ }),
1316
+ );
1317
+ process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
1318
+ process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
1319
+ const { enrichOSComponentsWithTrustData } = await loadBinaryModule({
1320
+ utilsOverrides: {
1321
+ safeExistsSync: sinon
1322
+ .stub()
1323
+ .callsFake((filePath) => existsSync(filePath)),
1324
+ safeSpawnSync: sinon.stub().callsFake((command, args) => {
1325
+ if (command === "ldd") {
1326
+ return { status: 1, stdout: "", stderr: "" };
1327
+ }
1328
+ if (command === "/tmp/trustinspector" && args[0] === "paths") {
1329
+ return {
1330
+ status: 0,
1331
+ stdout: JSON.stringify({
1332
+ inspections: [
1333
+ {
1334
+ path: inspectedPath,
1335
+ properties: [expectedProperty],
1336
+ },
1337
+ ],
1338
+ }),
1339
+ stderr: "",
1340
+ };
1341
+ }
1342
+ if (command === "/tmp/trustinspector" && args[0] === "host") {
1343
+ return {
1344
+ status: 0,
1345
+ stdout: JSON.stringify({ hostFindings: [hostFinding] }),
1346
+ stderr: "",
1347
+ };
1348
+ }
1349
+ return { status: 0, stdout: "", stderr: "" };
1350
+ }),
1351
+ },
1352
+ });
1353
+ const result = enrichOSComponentsWithTrustData([
1354
+ {
1355
+ type: "application",
1356
+ name: "Demo",
1357
+ "bom-ref": "app-demo",
1358
+ properties: [{ name: "path", value: inspectedPath }],
1359
+ },
1360
+ ]);
1361
+ assert.ok(
1362
+ result.components[0].properties.some(
1363
+ (property) =>
1364
+ property.name === expectedProperty.name &&
1365
+ property.value === expectedProperty.value,
1366
+ ),
1367
+ );
1368
+ assert.ok(
1369
+ result.components.some(
1370
+ (component) =>
1371
+ component.type === "data" && component.name === hostFinding.name,
1372
+ ),
1373
+ );
1374
+ assert.strictEqual(result.tools.length, 1);
1375
+ assert.strictEqual(result.tools[0].name, "trustinspector");
1376
+ } finally {
1377
+ if (previousPluginsDir === undefined) {
1378
+ delete process.env.CDXGEN_PLUGINS_DIR;
1379
+ } else {
1380
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
1381
+ }
1382
+ if (previousTrustInspectorCmd === undefined) {
1383
+ delete process.env.TRUSTINSPECTOR_CMD;
1384
+ } else {
1385
+ process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
1386
+ }
1387
+ rmSync(pluginsDir, { recursive: true, force: true });
1388
+ }
1389
+ });
1390
+
1391
+ it("enrichOSComponentsWithTrustData() skips macOS plist paths for notarization checks", async () => {
1392
+ if (process.platform !== "darwin") {
1393
+ return;
1394
+ }
1395
+ const pluginsDir = mkdtempSync(
1396
+ path.join(tmpdir(), "cdxgen-plugins-hosttrust-plist-"),
1397
+ );
1398
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
1399
+ const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
1400
+ const safeSpawnSync = sinon.stub().callsFake((command, args) => {
1401
+ if (command === "ldd") {
1402
+ return { status: 1, stdout: "", stderr: "" };
1403
+ }
1404
+ if (command === "/tmp/trustinspector" && args[0] === "paths") {
1405
+ assert.deepStrictEqual(args, [
1406
+ "paths",
1407
+ "/Library/PrivilegedHelperTools/demo-helper",
1408
+ ]);
1409
+ return {
1410
+ status: 0,
1411
+ stdout: JSON.stringify({
1412
+ inspections: [
1413
+ {
1414
+ path: "/Library/PrivilegedHelperTools/demo-helper",
1415
+ properties: [
1416
+ {
1417
+ name: "cdx:darwin:notarization:assessment",
1418
+ value: "accepted",
1419
+ },
1420
+ ],
1421
+ },
1422
+ ],
1423
+ }),
1424
+ stderr: "",
1425
+ };
1426
+ }
1427
+ if (command === "/tmp/trustinspector" && args[0] === "host") {
1428
+ return {
1429
+ status: 0,
1430
+ stdout: JSON.stringify({ hostFindings: [] }),
1431
+ stderr: "",
1432
+ };
1433
+ }
1434
+ return { status: 0, stdout: "", stderr: "" };
1435
+ });
1436
+ try {
1437
+ writeFileSync(
1438
+ path.join(pluginsDir, "plugins-manifest.json"),
1439
+ JSON.stringify({
1440
+ plugins: [
1441
+ {
1442
+ name: "trustinspector",
1443
+ component: {
1444
+ type: "application",
1445
+ name: "trustinspector",
1446
+ version: "2.1.1",
1447
+ purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.1",
1448
+ "bom-ref":
1449
+ "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.1",
1450
+ },
1451
+ },
1452
+ ],
1453
+ }),
1454
+ );
1455
+ process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
1456
+ process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
1457
+ const { enrichOSComponentsWithTrustData } = await loadBinaryModule({
1458
+ utilsOverrides: {
1459
+ safeExistsSync: sinon
1460
+ .stub()
1461
+ .callsFake((filePath) => existsSync(filePath)),
1462
+ safeSpawnSync,
1463
+ },
1464
+ });
1465
+ const result = enrichOSComponentsWithTrustData([
1466
+ {
1467
+ type: "application",
1468
+ name: "demo-helper",
1469
+ "bom-ref":
1470
+ "pkg:swid/demo-helper#/Library/LaunchDaemons/org.nixos.nix-daemon.plist",
1471
+ properties: [
1472
+ {
1473
+ name: "path",
1474
+ value: "/Library/LaunchDaemons/org.nixos.nix-daemon.plist",
1475
+ },
1476
+ {
1477
+ name: "program",
1478
+ value: "/Library/PrivilegedHelperTools/demo-helper",
1479
+ },
1480
+ ],
1481
+ },
1482
+ ]);
1483
+ assert.ok(
1484
+ result.components[0].properties.some(
1485
+ (property) =>
1486
+ property.name === "cdx:darwin:notarization:assessment" &&
1487
+ property.value === "accepted",
1488
+ ),
1489
+ );
1490
+ const trustInspectorPathCalls = safeSpawnSync
1491
+ .getCalls()
1492
+ .filter(
1493
+ (call) =>
1494
+ call.args[0] === "/tmp/trustinspector" &&
1495
+ call.args[1]?.[0] === "paths",
1496
+ );
1497
+ assert.strictEqual(trustInspectorPathCalls.length, 1);
1498
+ } finally {
1499
+ if (previousPluginsDir === undefined) {
1500
+ delete process.env.CDXGEN_PLUGINS_DIR;
1501
+ } else {
1502
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
1503
+ }
1504
+ if (previousTrustInspectorCmd === undefined) {
1505
+ delete process.env.TRUSTINSPECTOR_CMD;
1506
+ } else {
1507
+ process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
1508
+ }
1509
+ rmSync(pluginsDir, { recursive: true, force: true });
1510
+ }
1511
+ });
1512
+
1513
+ it("enrichOSComponentsWithTrustData() skips generic macOS app inventory paths", async () => {
1514
+ if (process.platform !== "darwin") {
1515
+ return;
1516
+ }
1517
+ const pluginsDir = mkdtempSync(
1518
+ path.join(tmpdir(), "cdxgen-plugins-hosttrust-apps-"),
1519
+ );
1520
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
1521
+ const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
1522
+ const safeSpawnSync = sinon.stub().callsFake((command, args) => {
1523
+ if (command === "ldd") {
1524
+ return { status: 1, stdout: "", stderr: "" };
1525
+ }
1526
+ if (command === "/tmp/trustinspector" && args[0] === "paths") {
1527
+ assert.deepStrictEqual(args, ["paths", "/Applications/Live.app"]);
1528
+ return {
1529
+ status: 0,
1530
+ stdout: JSON.stringify({
1531
+ inspections: [
1532
+ {
1533
+ path: "/Applications/Live.app",
1534
+ properties: [
1535
+ {
1536
+ name: "cdx:darwin:notarization:assessment",
1537
+ value: "accepted",
1538
+ },
1539
+ ],
1540
+ },
1541
+ ],
1542
+ }),
1543
+ stderr: "",
1544
+ };
1545
+ }
1546
+ if (command === "/tmp/trustinspector" && args[0] === "host") {
1547
+ return {
1548
+ status: 0,
1549
+ stdout: JSON.stringify({ hostFindings: [] }),
1550
+ stderr: "",
1551
+ };
1552
+ }
1553
+ return { status: 0, stdout: "", stderr: "" };
1554
+ });
1555
+ try {
1556
+ writeFileSync(
1557
+ path.join(pluginsDir, "plugins-manifest.json"),
1558
+ JSON.stringify({
1559
+ plugins: [
1560
+ {
1561
+ name: "trustinspector",
1562
+ component: {
1563
+ type: "application",
1564
+ name: "trustinspector",
1565
+ version: "2.1.1",
1566
+ purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.1",
1567
+ "bom-ref":
1568
+ "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.1",
1569
+ },
1570
+ },
1571
+ ],
1572
+ }),
1573
+ );
1574
+ process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
1575
+ process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
1576
+ const { enrichOSComponentsWithTrustData } = await loadBinaryModule({
1577
+ utilsOverrides: {
1578
+ safeExistsSync: sinon
1579
+ .stub()
1580
+ .callsFake((filePath) => existsSync(filePath)),
1581
+ safeSpawnSync,
1582
+ },
1583
+ });
1584
+ const result = enrichOSComponentsWithTrustData([
1585
+ {
1586
+ type: "application",
1587
+ name: "Installed",
1588
+ "bom-ref": "app-installed",
1589
+ properties: [
1590
+ { name: "cdx:osquery:category", value: "apps" },
1591
+ { name: "bundle_path", value: "/Applications/Installed.app" },
1592
+ ],
1593
+ },
1594
+ {
1595
+ type: "application",
1596
+ name: "Live",
1597
+ "bom-ref": "app-live",
1598
+ properties: [
1599
+ { name: "cdx:osquery:category", value: "running_apps" },
1600
+ { name: "bundle_path", value: "/Applications/Live.app" },
1601
+ ],
1602
+ },
1603
+ {
1604
+ type: "application",
1605
+ name: "Finder",
1606
+ "bom-ref": "app-finder",
1607
+ properties: [
1608
+ { name: "cdx:osquery:category", value: "running_apps" },
1609
+ { name: "bundle_path", value: "/System/Applications/Finder.app" },
1610
+ ],
1611
+ },
1612
+ ]);
1613
+ assert.ok(
1614
+ !result.components[0].properties.some(
1615
+ (property) => property.name === "cdx:darwin:notarization:assessment",
1616
+ ),
1617
+ );
1618
+ assert.ok(
1619
+ result.components[1].properties.some(
1620
+ (property) =>
1621
+ property.name === "cdx:darwin:notarization:assessment" &&
1622
+ property.value === "accepted",
1623
+ ),
1624
+ );
1625
+ assert.ok(
1626
+ !result.components[2].properties.some(
1627
+ (property) => property.name === "cdx:darwin:notarization:assessment",
1628
+ ),
1629
+ );
1630
+ } finally {
1631
+ if (previousPluginsDir === undefined) {
1632
+ delete process.env.CDXGEN_PLUGINS_DIR;
1633
+ } else {
1634
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
1635
+ }
1636
+ if (previousTrustInspectorCmd === undefined) {
1637
+ delete process.env.TRUSTINSPECTOR_CMD;
1638
+ } else {
1639
+ process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
1640
+ }
1641
+ rmSync(pluginsDir, { recursive: true, force: true });
1642
+ }
1643
+ });
1644
+
1645
+ it("enrichOSComponentsWithTrustData() batches trustinspector path requests", async () => {
1646
+ if (!["darwin", "win32"].includes(process.platform)) {
1647
+ return;
1648
+ }
1649
+ const pluginsDir = mkdtempSync(
1650
+ path.join(tmpdir(), "cdxgen-plugins-hosttrust-batch-"),
1651
+ );
1652
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
1653
+ const previousTrustInspectorCmd = process.env.TRUSTINSPECTOR_CMD;
1654
+ const pathPrefix =
1655
+ process.platform === "win32" ? "C:\\Demo\\app" : "/Applications/App";
1656
+ const safeSpawnSync = sinon.stub().callsFake((command, args) => {
1657
+ if (command === "ldd") {
1658
+ return { status: 1, stdout: "", stderr: "" };
1659
+ }
1660
+ if (command === "/tmp/trustinspector" && args[0] === "paths") {
1661
+ return {
1662
+ status: 0,
1663
+ stdout: JSON.stringify({
1664
+ inspections: args.slice(1).map((inspectedPath) => ({
1665
+ path: inspectedPath,
1666
+ properties: [
1667
+ {
1668
+ name:
1669
+ process.platform === "win32"
1670
+ ? "cdx:windows:authenticode:status"
1671
+ : "cdx:darwin:notarization:assessment",
1672
+ value: process.platform === "win32" ? "Valid" : "accepted",
1673
+ },
1674
+ ],
1675
+ })),
1676
+ }),
1677
+ stderr: "",
1678
+ };
1679
+ }
1680
+ if (command === "/tmp/trustinspector" && args[0] === "host") {
1681
+ return {
1682
+ status: 0,
1683
+ stdout: JSON.stringify({ hostFindings: [] }),
1684
+ stderr: "",
1685
+ };
1686
+ }
1687
+ return { status: 0, stdout: "", stderr: "" };
1688
+ });
1689
+ try {
1690
+ writeFileSync(
1691
+ path.join(pluginsDir, "plugins-manifest.json"),
1692
+ JSON.stringify({
1693
+ plugins: [
1694
+ {
1695
+ name: "trustinspector",
1696
+ component: {
1697
+ type: "application",
1698
+ name: "trustinspector",
1699
+ version: "2.1.1",
1700
+ purl: "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.1",
1701
+ "bom-ref":
1702
+ "pkg:generic/github.com/cdxgen/cdxgen-plugins-bin/trustinspector-cdxgen@2.1.1",
1703
+ },
1704
+ },
1705
+ ],
1706
+ }),
1707
+ );
1708
+ process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
1709
+ process.env.TRUSTINSPECTOR_CMD = "/tmp/trustinspector";
1710
+ const { enrichOSComponentsWithTrustData } = await loadBinaryModule({
1711
+ utilsOverrides: {
1712
+ safeExistsSync: sinon
1713
+ .stub()
1714
+ .callsFake((filePath) => existsSync(filePath)),
1715
+ safeSpawnSync,
1716
+ },
1717
+ });
1718
+ const components = Array.from({ length: 205 }, (_, index) => ({
1719
+ type: "application",
1720
+ name: `App ${index}`,
1721
+ "bom-ref": `app-${index}`,
1722
+ properties: [
1723
+ {
1724
+ name: "path",
1725
+ value:
1726
+ process.platform === "win32"
1727
+ ? `${pathPrefix}${index}.exe`
1728
+ : `${pathPrefix}${index}.app`,
1729
+ },
1730
+ ],
1731
+ }));
1732
+ const result = enrichOSComponentsWithTrustData(components);
1733
+ const pathInvocations = safeSpawnSync
1734
+ .getCalls()
1735
+ .filter(
1736
+ (call) =>
1737
+ call.args[0] === "/tmp/trustinspector" &&
1738
+ call.args[1]?.[0] === "paths",
1739
+ );
1740
+ assert.strictEqual(pathInvocations.length, 2);
1741
+ assert.strictEqual(pathInvocations[0].args[1].length, 201);
1742
+ assert.strictEqual(pathInvocations[1].args[1].length, 6);
1743
+ assert.ok(
1744
+ result.components.every((component) =>
1745
+ component.properties.some(
1746
+ (property) =>
1747
+ property.name ===
1748
+ (process.platform === "win32"
1749
+ ? "cdx:windows:authenticode:status"
1750
+ : "cdx:darwin:notarization:assessment"),
1751
+ ),
1752
+ ),
1753
+ );
1754
+ } finally {
1755
+ if (previousPluginsDir === undefined) {
1756
+ delete process.env.CDXGEN_PLUGINS_DIR;
1757
+ } else {
1758
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
1759
+ }
1760
+ if (previousTrustInspectorCmd === undefined) {
1761
+ delete process.env.TRUSTINSPECTOR_CMD;
1762
+ } else {
1763
+ process.env.TRUSTINSPECTOR_CMD = previousTrustInspectorCmd;
1764
+ }
1765
+ rmSync(pluginsDir, { recursive: true, force: true });
1766
+ }
1767
+ });