@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
@@ -0,0 +1,496 @@
1
+ import esmock from "esmock";
2
+ import { assert, describe, it } from "poku";
3
+ import sinon from "sinon";
4
+
5
+ import {
6
+ addHbomAnalysisProperties,
7
+ ensureNoMixedHbomProjectTypes,
8
+ ensureSupportedHbomSpecVersion,
9
+ hasHbomProjectType,
10
+ isHbomOnlyProjectTypes,
11
+ normalizeHbomOptions,
12
+ } from "./hbom.js";
13
+
14
+ describe("hbom helpers", () => {
15
+ it("detects hbom project types and rejects mixed project selections", () => {
16
+ assert.strictEqual(hasHbomProjectType(undefined), false);
17
+ assert.strictEqual(hasHbomProjectType(["js"]), false);
18
+ assert.strictEqual(hasHbomProjectType(["hbom"]), true);
19
+ assert.strictEqual(hasHbomProjectType(["hardware"]), true);
20
+ assert.strictEqual(isHbomOnlyProjectTypes(undefined), false);
21
+ assert.strictEqual(isHbomOnlyProjectTypes(["hbom"]), true);
22
+ assert.strictEqual(isHbomOnlyProjectTypes(["hardware"]), true);
23
+ assert.strictEqual(isHbomOnlyProjectTypes(["hbom", "hardware"]), true);
24
+ assert.strictEqual(isHbomOnlyProjectTypes(["hbom", "js"]), false);
25
+ ensureNoMixedHbomProjectTypes(["hbom"]);
26
+ ensureNoMixedHbomProjectTypes(["hardware"]);
27
+ assert.throws(
28
+ () => ensureNoMixedHbomProjectTypes(["hbom", "js"]),
29
+ /cannot be mixed/u,
30
+ );
31
+ });
32
+
33
+ it("enforces CycloneDX 1.7 for hbom generation", () => {
34
+ ensureSupportedHbomSpecVersion(undefined);
35
+ ensureSupportedHbomSpecVersion(1.7);
36
+ assert.throws(
37
+ () => ensureSupportedHbomSpecVersion(1.6),
38
+ /only CycloneDX 1\.7/u,
39
+ );
40
+ });
41
+
42
+ it("normalizes hbom collector options", () => {
43
+ assert.deepStrictEqual(
44
+ normalizeHbomOptions({
45
+ arch: "arm64",
46
+ noCommandEnrichment: true,
47
+ platform: "darwin",
48
+ plistEnrichment: true,
49
+ privileged: true,
50
+ sensitive: true,
51
+ strict: true,
52
+ timeout: "2500",
53
+ }),
54
+ {
55
+ allowPartial: false,
56
+ architecture: "arm64",
57
+ dryRun: false,
58
+ includeCommandEnrichment: false,
59
+ includePlistEnrichment: true,
60
+ includePrivilegedEnrichment: true,
61
+ includeSensitiveIdentifiers: true,
62
+ platform: "darwin",
63
+ timeoutMs: 2500,
64
+ },
65
+ );
66
+ });
67
+
68
+ it("adds derived analysis properties for hbom command diagnostics", () => {
69
+ const bomJson = addHbomAnalysisProperties({
70
+ components: [],
71
+ metadata: {
72
+ component: {
73
+ name: "demo-host",
74
+ properties: [
75
+ { name: "cdx:hbom:platform", value: "linux" },
76
+ { name: "cdx:hbom:architecture", value: "amd64" },
77
+ ],
78
+ type: "device",
79
+ },
80
+ },
81
+ properties: [
82
+ { name: "cdx:hbom:evidence:commandDiagnosticCount", value: "2" },
83
+ {
84
+ name: "cdx:hbom:evidence:commandDiagnostic",
85
+ value: JSON.stringify({
86
+ command: "lsusb",
87
+ installHint:
88
+ "Command not found: install the Linux package providing lsusb (commonly `usbutils`).",
89
+ issue: "missing-command",
90
+ }),
91
+ },
92
+ {
93
+ name: "cdx:hbom:evidence:commandDiagnostic",
94
+ value: JSON.stringify({
95
+ command: "drm_info",
96
+ issue: "permission-denied",
97
+ privilegeHint:
98
+ "Retry with --privileged to allow a non-interactive sudo attempt for permission-sensitive Linux commands.",
99
+ }),
100
+ },
101
+ ],
102
+ });
103
+
104
+ assert.ok(
105
+ bomJson.properties.some(
106
+ (property) =>
107
+ property.name === "cdx:hbom:analysis:commandDiagnosticCount" &&
108
+ property.value === "2",
109
+ ),
110
+ );
111
+ assert.ok(
112
+ bomJson.properties.some(
113
+ (property) =>
114
+ property.name === "cdx:hbom:analysis:missingCommands" &&
115
+ property.value === "lsusb",
116
+ ),
117
+ );
118
+ assert.ok(
119
+ bomJson.properties.some(
120
+ (property) =>
121
+ property.name === "cdx:hbom:analysis:missingCommandIds" &&
122
+ property.value === "lsusb",
123
+ ),
124
+ );
125
+ assert.ok(
126
+ bomJson.properties.some(
127
+ (property) =>
128
+ property.name === "cdx:hbom:analysis:installHintCount" &&
129
+ property.value === "1",
130
+ ),
131
+ );
132
+ assert.ok(
133
+ bomJson.properties.some(
134
+ (property) =>
135
+ property.name === "cdx:hbom:analysis:permissionDeniedCommands" &&
136
+ property.value === "drm_info",
137
+ ),
138
+ );
139
+ assert.ok(
140
+ bomJson.properties.some(
141
+ (property) =>
142
+ property.name === "cdx:hbom:analysis:permissionDeniedIds" &&
143
+ property.value === "drm_info",
144
+ ),
145
+ );
146
+ assert.ok(
147
+ bomJson.properties.some(
148
+ (property) =>
149
+ property.name === "cdx:hbom:analysis:privilegeHintCount" &&
150
+ property.value === "1",
151
+ ),
152
+ );
153
+ assert.ok(
154
+ bomJson.properties.some(
155
+ (property) =>
156
+ property.name === "cdx:hbom:analysis:requiresPrivileged" &&
157
+ property.value === "true",
158
+ ),
159
+ );
160
+ });
161
+
162
+ it("propagates native cdx-hbom dry-run trace activities into cdxgen", async () => {
163
+ const recordActivity = sinon.stub();
164
+ let collectorTrace;
165
+ const { createHbomDocument } = await esmock("./hbom.js", {
166
+ "./utils.js": {
167
+ isDryRun: true,
168
+ recordActivity,
169
+ },
170
+ "./hbomLoader.js": {
171
+ importHbomModule: sinon.stub().resolves({
172
+ collectHardware: sinon.stub().callsFake(async (options) => {
173
+ collectorTrace = options.trace;
174
+ collectorTrace.activities.push({
175
+ category: "cpu-memory",
176
+ command: "/usr/sbin/sysctl",
177
+ id: "sysctl-baseline",
178
+ kind: "command",
179
+ reason: "Dry run mode blocks HBOM command 'sysctl-baseline'.",
180
+ status: "blocked",
181
+ target: "/usr/sbin/sysctl -n hw.model",
182
+ });
183
+ collectorTrace.activities.push({
184
+ kind: "file-read",
185
+ status: "completed",
186
+ target: "/etc/os-release",
187
+ });
188
+ return {
189
+ bomFormat: "CycloneDX",
190
+ components: [],
191
+ dependencies: [],
192
+ metadata: {
193
+ timestamp: "2026-05-13T00:00:00.000Z",
194
+ tools: {
195
+ components: [],
196
+ },
197
+ },
198
+ specVersion: "1.7",
199
+ version: 1,
200
+ };
201
+ }),
202
+ createCollectorTrace: sinon
203
+ .stub()
204
+ .callsFake(() => ({ activities: [] })),
205
+ getCollectorTrace: sinon.stub().callsFake(() => collectorTrace),
206
+ }),
207
+ },
208
+ });
209
+
210
+ const bomJson = await createHbomDocument({
211
+ arch: "arm64",
212
+ platform: "linux",
213
+ specVersion: 1.7,
214
+ });
215
+
216
+ assert.strictEqual(recordActivity.callCount, 2);
217
+ sinon.assert.calledWithMatch(recordActivity.firstCall, {
218
+ commandId: "sysctl-baseline",
219
+ kind: "execute",
220
+ reason: sinon.match(/Dry run mode blocks HBOM command/u),
221
+ status: "blocked",
222
+ target: "/usr/sbin/sysctl -n hw.model",
223
+ });
224
+ sinon.assert.calledWithMatch(recordActivity.secondCall, {
225
+ kind: "read",
226
+ reason: undefined,
227
+ status: "completed",
228
+ target: "/etc/os-release",
229
+ });
230
+ assert.strictEqual(bomJson.bomFormat, "CycloneDX");
231
+ assert.strictEqual(bomJson.specVersion, "1.7");
232
+ assert.strictEqual(bomJson.version, 1);
233
+ assert.deepStrictEqual(bomJson.components, []);
234
+ assert.deepStrictEqual(bomJson.dependencies, []);
235
+ assert.ok(typeof bomJson.metadata?.timestamp === "string");
236
+ assert.deepStrictEqual(bomJson.metadata?.tools?.components, []);
237
+ });
238
+
239
+ it("returns a synthetic hbom document during dry-run mode when cdx-hbom lacks trace support", async () => {
240
+ const recordActivity = sinon.stub();
241
+ const { createHbomDocument } = await esmock("./hbom.js", {
242
+ "./utils.js": {
243
+ isDryRun: true,
244
+ recordActivity,
245
+ },
246
+ "./hbomLoader.js": {
247
+ importHbomModule: sinon.stub().resolves({
248
+ collectHardware: sinon.stub(),
249
+ }),
250
+ },
251
+ });
252
+
253
+ const bomJson = await createHbomDocument({
254
+ arch: "arm64",
255
+ platform: "linux",
256
+ specVersion: 1.7,
257
+ });
258
+
259
+ sinon.assert.calledOnce(recordActivity);
260
+ sinon.assert.calledWithMatch(recordActivity, {
261
+ kind: "hbom",
262
+ reason: sinon.match(/Dry run mode blocks HBOM collection/u),
263
+ status: "blocked",
264
+ target: "linux/arm64",
265
+ });
266
+ assert.strictEqual(bomJson.bomFormat, "CycloneDX");
267
+ assert.strictEqual(bomJson.specVersion, "1.7");
268
+ });
269
+
270
+ it("blocks secure-mode live collection when the HBOM plan includes disallowed commands or paths", async () => {
271
+ const collectHardware = sinon.stub().resolves({
272
+ bomFormat: "CycloneDX",
273
+ components: [],
274
+ dependencies: [],
275
+ metadata: {
276
+ timestamp: "2026-05-13T00:00:00.000Z",
277
+ tools: {
278
+ components: [],
279
+ },
280
+ },
281
+ specVersion: "1.7",
282
+ version: 1,
283
+ });
284
+ let preflightTrace;
285
+ collectHardware.onFirstCall().callsFake(async (options) => {
286
+ preflightTrace = options.trace;
287
+ preflightTrace.activities.push(
288
+ {
289
+ command: "/usr/sbin/system_profiler",
290
+ id: "system-profiler-json",
291
+ kind: "command",
292
+ target: "/usr/sbin/system_profiler SPHardwareDataType -json",
293
+ },
294
+ {
295
+ id: "plist-read",
296
+ kind: "file-read",
297
+ path: "/Library/Preferences/SystemConfiguration/preferences.plist",
298
+ target: "/Library/Preferences/SystemConfiguration/preferences.plist",
299
+ },
300
+ );
301
+ return {
302
+ bomFormat: "CycloneDX",
303
+ components: [],
304
+ dependencies: [],
305
+ metadata: {
306
+ timestamp: "2026-05-13T00:00:00.000Z",
307
+ tools: {
308
+ components: [],
309
+ },
310
+ },
311
+ specVersion: "1.7",
312
+ version: 1,
313
+ };
314
+ });
315
+
316
+ const recordActivity = sinon.stub();
317
+ const { createHbomDocument: createSecureHbomDocument } = await esmock(
318
+ "./hbom.js",
319
+ {
320
+ "./hbomLoader.js": {
321
+ importHbomModule: sinon.stub().resolves({
322
+ collectHardware,
323
+ createCollectorTrace: sinon
324
+ .stub()
325
+ .callsFake(() => ({ activities: [] })),
326
+ getCollectorTrace: sinon.stub().callsFake(() => preflightTrace),
327
+ }),
328
+ },
329
+ "./source.js": {
330
+ isAllowedPath: sinon.stub().returns(false),
331
+ },
332
+ "./utils.js": {
333
+ isDryRun: false,
334
+ isSecureMode: true,
335
+ readEnvironmentVariable: sinon.stub().callsFake((name) => {
336
+ if (name === "CDXGEN_ALLOWED_COMMANDS") {
337
+ return "sysctl";
338
+ }
339
+ if (name === "CDXGEN_ALLOWED_PATHS") {
340
+ return "/Users/example/allowed";
341
+ }
342
+ return undefined;
343
+ }),
344
+ recordActivity,
345
+ },
346
+ },
347
+ );
348
+
349
+ await assert.rejects(
350
+ () =>
351
+ createSecureHbomDocument({
352
+ platform: "darwin",
353
+ arch: "arm64",
354
+ specVersion: 1.7,
355
+ }),
356
+ /Commands not allowed by CDXGEN_ALLOWED_COMMANDS:[\s\S]*\/usr\/sbin\/system_profiler[\s\S]*Paths not allowed by CDXGEN_ALLOWED_PATHS:[\s\S]*preferences\.plist/u,
357
+ );
358
+ sinon.assert.calledOnce(collectHardware);
359
+ sinon.assert.calledTwice(recordActivity);
360
+ sinon.assert.calledWithMatch(recordActivity.firstCall, {
361
+ kind: "policy",
362
+ policyType: "hbom-command-allowlist",
363
+ status: "blocked",
364
+ target: "/usr/sbin/system_profiler",
365
+ });
366
+ sinon.assert.calledWithMatch(recordActivity.secondCall, {
367
+ kind: "policy",
368
+ policyType: "hbom-path-allowlist",
369
+ status: "blocked",
370
+ target: "/Library/Preferences/SystemConfiguration/preferences.plist",
371
+ });
372
+ });
373
+
374
+ it("skips secure-mode allowlist preflight when the caller explicitly requested dry-run mode", async () => {
375
+ const collectHardware = sinon.stub().resolves({
376
+ bomFormat: "CycloneDX",
377
+ components: [],
378
+ dependencies: [],
379
+ metadata: {
380
+ timestamp: "2026-05-13T00:00:00.000Z",
381
+ tools: {
382
+ components: [],
383
+ },
384
+ },
385
+ specVersion: "1.7",
386
+ version: 1,
387
+ });
388
+ const { createHbomDocument: createDryRunHbomDocument } = await esmock(
389
+ "./hbom.js",
390
+ {
391
+ "./hbomLoader.js": {
392
+ importHbomModule: sinon.stub().resolves({
393
+ collectHardware,
394
+ createCollectorTrace: sinon
395
+ .stub()
396
+ .callsFake(() => ({ activities: [] })),
397
+ }),
398
+ },
399
+ "./source.js": {
400
+ isAllowedPath: sinon.stub().returns(false),
401
+ },
402
+ "./utils.js": {
403
+ isDryRun: true,
404
+ isSecureMode: true,
405
+ readEnvironmentVariable: sinon.stub().callsFake((name) => {
406
+ if (name === "CDXGEN_ALLOWED_COMMANDS") {
407
+ return "sysctl";
408
+ }
409
+ if (name === "CDXGEN_ALLOWED_PATHS") {
410
+ return "/Users/example/allowed";
411
+ }
412
+ return undefined;
413
+ }),
414
+ recordActivity: sinon.stub(),
415
+ },
416
+ },
417
+ );
418
+
419
+ const bomJson = await createDryRunHbomDocument({
420
+ platform: "darwin",
421
+ arch: "arm64",
422
+ specVersion: 1.7,
423
+ });
424
+
425
+ sinon.assert.calledOnce(collectHardware);
426
+ assert.strictEqual(bomJson.bomFormat, "CycloneDX");
427
+ });
428
+
429
+ it("blocks secure-mode privileged Linux collection when the plan can retry with sudo but sudo is not allowlisted", async () => {
430
+ const collectHardware = sinon.stub().resolves({
431
+ bomFormat: "CycloneDX",
432
+ components: [],
433
+ dependencies: [],
434
+ metadata: {
435
+ timestamp: "2026-05-13T00:00:00.000Z",
436
+ tools: {
437
+ components: [],
438
+ },
439
+ },
440
+ specVersion: "1.7",
441
+ version: 1,
442
+ });
443
+ const recordActivity = sinon.stub();
444
+ const { createHbomDocument: createSecureHbomDocument } = await esmock(
445
+ "./hbom.js",
446
+ {
447
+ "./hbomLoader.js": {
448
+ importHbomModule: sinon.stub().resolves({
449
+ collectHardware,
450
+ getCommandPlan: sinon.stub().returns([
451
+ {
452
+ args: ["-j"],
453
+ command: "drm_info",
454
+ id: "drm-info-json",
455
+ sudoRetryOnPermissionDenied: true,
456
+ },
457
+ ]),
458
+ }),
459
+ },
460
+ "./source.js": {
461
+ isAllowedPath: sinon.stub().returns(true),
462
+ },
463
+ "./utils.js": {
464
+ isDryRun: false,
465
+ isSecureMode: true,
466
+ readEnvironmentVariable: sinon.stub().callsFake((name) => {
467
+ if (name === "CDXGEN_ALLOWED_COMMANDS") {
468
+ return "drm_info";
469
+ }
470
+ return undefined;
471
+ }),
472
+ recordActivity,
473
+ },
474
+ },
475
+ );
476
+
477
+ await assert.rejects(
478
+ () =>
479
+ createSecureHbomDocument({
480
+ arch: "amd64",
481
+ platform: "linux",
482
+ privileged: true,
483
+ specVersion: 1.7,
484
+ }),
485
+ /Commands not allowed by CDXGEN_ALLOWED_COMMANDS:[\s\S]*- sudo — ids=drm-info-json:sudo-retry; targets=sudo -n drm_info -j/u,
486
+ );
487
+ sinon.assert.notCalled(collectHardware);
488
+ sinon.assert.calledOnce(recordActivity);
489
+ sinon.assert.calledWithMatch(recordActivity, {
490
+ kind: "policy",
491
+ policyType: "hbom-command-allowlist",
492
+ status: "blocked",
493
+ target: "sudo",
494
+ });
495
+ });
496
+ });