@cyclonedx/cdxgen 12.3.3 → 12.4.1

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 (175) hide show
  1. package/README.md +69 -25
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +270 -127
  4. package/bin/convert.js +34 -15
  5. package/bin/hbom.js +495 -0
  6. package/bin/repl.js +592 -37
  7. package/bin/validate.js +31 -4
  8. package/bin/verify.js +18 -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/cyclonedx-2.0-bundled.schema.json +7182 -0
  13. package/data/predictive-audit-allowlist.json +11 -0
  14. package/data/queries-darwin.json +12 -1
  15. package/data/queries-win.json +7 -1
  16. package/data/queries.json +39 -2
  17. package/data/rules/ai-agent-governance.yaml +16 -0
  18. package/data/rules/asar-archives.yaml +150 -0
  19. package/data/rules/chrome-extensions.yaml +8 -0
  20. package/data/rules/ci-permissions.yaml +42 -18
  21. package/data/rules/container-risk.yaml +14 -7
  22. package/data/rules/dependency-sources.yaml +11 -0
  23. package/data/rules/hbom-compliance.yaml +325 -0
  24. package/data/rules/hbom-performance.yaml +307 -0
  25. package/data/rules/hbom-security.yaml +248 -0
  26. package/data/rules/host-topology.yaml +165 -0
  27. package/data/rules/mcp-servers.yaml +18 -3
  28. package/data/rules/obom-runtime.yaml +907 -22
  29. package/data/rules/package-integrity.yaml +14 -0
  30. package/data/rules/rootfs-hardening.yaml +179 -0
  31. package/data/rules/vscode-extensions.yaml +9 -0
  32. package/lib/audit/index.js +210 -8
  33. package/lib/audit/index.poku.js +332 -0
  34. package/lib/audit/reporters.js +222 -0
  35. package/lib/audit/targets.js +146 -1
  36. package/lib/audit/targets.poku.js +186 -0
  37. package/lib/cli/asar.poku.js +328 -0
  38. package/lib/cli/index.js +527 -99
  39. package/lib/cli/index.poku.js +1469 -212
  40. package/lib/evinser/evinser.js +14 -9
  41. package/lib/helpers/analyzer.js +1406 -29
  42. package/lib/helpers/analyzer.poku.js +342 -0
  43. package/lib/helpers/analyzerScope.js +712 -0
  44. package/lib/helpers/asarutils.js +1556 -0
  45. package/lib/helpers/asarutils.poku.js +443 -0
  46. package/lib/helpers/auditCategories.js +12 -0
  47. package/lib/helpers/auditCategories.poku.js +32 -0
  48. package/lib/helpers/bomUtils.js +155 -1
  49. package/lib/helpers/bomUtils.poku.js +79 -1
  50. package/lib/helpers/cbomutils.js +271 -1
  51. package/lib/helpers/cbomutils.poku.js +248 -5
  52. package/lib/helpers/display.js +291 -1
  53. package/lib/helpers/display.poku.js +149 -0
  54. package/lib/helpers/evidenceUtils.js +58 -0
  55. package/lib/helpers/evidenceUtils.poku.js +54 -0
  56. package/lib/helpers/exportUtils.js +9 -0
  57. package/lib/helpers/gtfobins.js +142 -8
  58. package/lib/helpers/gtfobins.poku.js +24 -1
  59. package/lib/helpers/hbom.js +710 -0
  60. package/lib/helpers/hbom.poku.js +496 -0
  61. package/lib/helpers/hbomAnalysis.js +268 -0
  62. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  63. package/lib/helpers/hbomLoader.js +35 -0
  64. package/lib/helpers/hostTopology.js +803 -0
  65. package/lib/helpers/hostTopology.poku.js +363 -0
  66. package/lib/helpers/inventoryStats.js +69 -0
  67. package/lib/helpers/inventoryStats.poku.js +86 -0
  68. package/lib/helpers/lolbas.js +19 -1
  69. package/lib/helpers/lolbas.poku.js +23 -0
  70. package/lib/helpers/osqueryTransform.js +47 -0
  71. package/lib/helpers/osqueryTransform.poku.js +47 -0
  72. package/lib/helpers/plugins.js +350 -0
  73. package/lib/helpers/plugins.poku.js +57 -0
  74. package/lib/helpers/protobom.js +209 -45
  75. package/lib/helpers/protobom.poku.js +183 -5
  76. package/lib/helpers/protobomLoader.js +43 -0
  77. package/lib/helpers/protobomLoader.poku.js +31 -0
  78. package/lib/helpers/remote/dependency-track.js +36 -3
  79. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  80. package/lib/helpers/source.js +24 -0
  81. package/lib/helpers/source.poku.js +32 -0
  82. package/lib/helpers/utils.js +1438 -93
  83. package/lib/helpers/utils.poku.js +846 -4
  84. package/lib/managers/binary.e2e.poku.js +367 -0
  85. package/lib/managers/binary.js +2293 -353
  86. package/lib/managers/binary.poku.js +1699 -1
  87. package/lib/managers/docker.js +201 -79
  88. package/lib/managers/docker.poku.js +337 -12
  89. package/lib/server/server.js +4 -28
  90. package/lib/stages/postgen/annotator.js +38 -0
  91. package/lib/stages/postgen/annotator.poku.js +107 -1
  92. package/lib/stages/postgen/auditBom.js +121 -18
  93. package/lib/stages/postgen/auditBom.poku.js +1366 -31
  94. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  95. package/lib/stages/postgen/postgen.js +406 -8
  96. package/lib/stages/postgen/postgen.poku.js +484 -0
  97. package/lib/stages/postgen/ruleEngine.js +116 -0
  98. package/lib/stages/pregen/envAudit.js +14 -3
  99. package/lib/validator/bomValidator.js +90 -38
  100. package/lib/validator/bomValidator.poku.js +90 -0
  101. package/lib/validator/complianceRules.js +4 -2
  102. package/lib/validator/index.poku.js +14 -0
  103. package/package.json +23 -21
  104. package/types/bin/hbom.d.ts +3 -0
  105. package/types/bin/hbom.d.ts.map +1 -0
  106. package/types/bin/repl.d.ts +1 -1
  107. package/types/bin/repl.d.ts.map +1 -1
  108. package/types/lib/audit/index.d.ts +44 -0
  109. package/types/lib/audit/index.d.ts.map +1 -1
  110. package/types/lib/audit/reporters.d.ts +16 -0
  111. package/types/lib/audit/reporters.d.ts.map +1 -1
  112. package/types/lib/audit/targets.d.ts.map +1 -1
  113. package/types/lib/cli/index.d.ts +16 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/evinser/evinser.d.ts +4 -0
  116. package/types/lib/evinser/evinser.d.ts.map +1 -1
  117. package/types/lib/helpers/analyzer.d.ts +33 -0
  118. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  119. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  120. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  121. package/types/lib/helpers/asarutils.d.ts +34 -0
  122. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  123. package/types/lib/helpers/auditCategories.d.ts +5 -0
  124. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  125. package/types/lib/helpers/bomUtils.d.ts +10 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  127. package/types/lib/helpers/cbomutils.d.ts +3 -2
  128. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  129. package/types/lib/helpers/display.d.ts.map +1 -1
  130. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  131. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  132. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  133. package/types/lib/helpers/gtfobins.d.ts +8 -0
  134. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  135. package/types/lib/helpers/hbom.d.ts +49 -0
  136. package/types/lib/helpers/hbom.d.ts.map +1 -0
  137. package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
  138. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  139. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  140. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  141. package/types/lib/helpers/hostTopology.d.ts +12 -0
  142. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  143. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  144. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  145. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  146. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  147. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  148. package/types/lib/helpers/plugins.d.ts +58 -0
  149. package/types/lib/helpers/plugins.d.ts.map +1 -0
  150. package/types/lib/helpers/protobom.d.ts +5 -4
  151. package/types/lib/helpers/protobom.d.ts.map +1 -1
  152. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  153. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  154. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  155. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  156. package/types/lib/helpers/source.d.ts.map +1 -1
  157. package/types/lib/helpers/utils.d.ts +45 -8
  158. package/types/lib/helpers/utils.d.ts.map +1 -1
  159. package/types/lib/managers/binary.d.ts +5 -0
  160. package/types/lib/managers/binary.d.ts.map +1 -1
  161. package/types/lib/managers/docker.d.ts.map +1 -1
  162. package/types/lib/server/server.d.ts +2 -1
  163. package/types/lib/server/server.d.ts.map +1 -1
  164. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  165. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  166. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  167. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  168. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  170. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  171. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  172. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  173. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  174. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  175. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -241,6 +241,443 @@ it("postProcess passes formulationList from bomNSData into the formulation secti
241
241
  );
242
242
  });
243
243
 
244
+ it("postProcess finalizes CycloneDX 2.0-dev root fields and strips legacy fields", () => {
245
+ const bomNSData = {
246
+ bomJson: {
247
+ bomFormat: "CycloneDX",
248
+ specVersion: "1.7",
249
+ metadata: {
250
+ manufacture: { name: "Legacy Factory" },
251
+ component: "not-a-component-object",
252
+ tools: [
253
+ {
254
+ author: "OWASP Foundation",
255
+ name: "cdxgen",
256
+ vendor: "OWASP Foundation",
257
+ version: "12.4.0",
258
+ components: [
259
+ {
260
+ author: "Nested Tool Author",
261
+ modified: true,
262
+ name: "cdxgen-plugin",
263
+ type: "library",
264
+ },
265
+ ],
266
+ },
267
+ ],
268
+ },
269
+ components: [
270
+ {
271
+ author: "Jane Doe",
272
+ modified: false,
273
+ name: "demo-lib",
274
+ type: "library",
275
+ version: "1.0.0",
276
+ components: [
277
+ {
278
+ author: "Nested Author",
279
+ modified: true,
280
+ name: "nested-lib",
281
+ type: "library",
282
+ },
283
+ ],
284
+ },
285
+ ],
286
+ },
287
+ };
288
+
289
+ const result = postProcess(bomNSData, { specVersion: 2.0 });
290
+ const [toolComponent] = result.bomJson.metadata.tools.components;
291
+ const [component] = result.bomJson.components;
292
+
293
+ assert.strictEqual(result.bomJson.specFormat, "CycloneDX");
294
+ assert.strictEqual(result.bomJson.bomFormat, undefined);
295
+ assert.strictEqual(result.bomJson.specVersion, "2.0");
296
+ assert.strictEqual(result.bomJson.metadata.manufacture, undefined);
297
+ assert.deepStrictEqual(result.bomJson.metadata.manufacturer, {
298
+ name: "Legacy Factory",
299
+ });
300
+ assert.strictEqual(
301
+ result.bomJson.metadata.component,
302
+ "not-a-component-object",
303
+ );
304
+ assert.strictEqual(toolComponent.publisher, "OWASP Foundation");
305
+ assert.deepStrictEqual(toolComponent.authors, [{ name: "OWASP Foundation" }]);
306
+ assert.strictEqual(toolComponent.author, undefined);
307
+ assert.deepStrictEqual(toolComponent.components[0].authors, [
308
+ { name: "Nested Tool Author" },
309
+ ]);
310
+ assert.strictEqual(toolComponent.components[0].author, undefined);
311
+ assert.strictEqual(toolComponent.components[0].modified, undefined);
312
+ assert.deepStrictEqual(component.authors, [{ name: "Jane Doe" }]);
313
+ assert.strictEqual(component.author, undefined);
314
+ assert.strictEqual(component.modified, undefined);
315
+ assert.deepStrictEqual(component.components[0].authors, [
316
+ { name: "Nested Author" },
317
+ ]);
318
+ assert.strictEqual(component.components[0].author, undefined);
319
+ assert.strictEqual(component.components[0].modified, undefined);
320
+ });
321
+
322
+ it("postProcess preserves malformed explicit specVersion values instead of coercing them to 1.7", () => {
323
+ const malformedBomResult = postProcess(
324
+ {
325
+ bomJson: {
326
+ bomFormat: "CycloneDX",
327
+ specVersion: "2.0.1",
328
+ components: [],
329
+ dependencies: [],
330
+ metadata: { properties: [] },
331
+ },
332
+ },
333
+ {},
334
+ );
335
+ assert.strictEqual(malformedBomResult.bomJson.specVersion, "2.0.1");
336
+ assert.strictEqual(malformedBomResult.bomJson.bomFormat, "CycloneDX");
337
+ assert.strictEqual(malformedBomResult.bomJson.specFormat, undefined);
338
+
339
+ const malformedOptionResult = postProcess(
340
+ {
341
+ bomJson: {
342
+ bomFormat: "CycloneDX",
343
+ specVersion: "1.7",
344
+ components: [],
345
+ dependencies: [],
346
+ metadata: { properties: [] },
347
+ },
348
+ },
349
+ { specVersion: "2.0.1" },
350
+ );
351
+ assert.strictEqual(malformedOptionResult.bomJson.specVersion, "1.7");
352
+ assert.strictEqual(malformedOptionResult.bomJson.bomFormat, "CycloneDX");
353
+ assert.strictEqual(malformedOptionResult.bomJson.specFormat, undefined);
354
+ });
355
+
356
+ it("postProcess migrates CycloneDX 2.0 metadata manufacture and tool services without broad recursion", () => {
357
+ const bomNSData = {
358
+ bomJson: {
359
+ bomFormat: "CycloneDX",
360
+ specVersion: "1.7",
361
+ metadata: {
362
+ manufacture: { name: "Component Factory" },
363
+ component: {
364
+ name: "demo-app",
365
+ type: "application",
366
+ },
367
+ tools: {
368
+ components: [],
369
+ services: [
370
+ {
371
+ author: "Legacy Author",
372
+ name: "scanner-service",
373
+ vendor: "Scanner Vendor",
374
+ },
375
+ ],
376
+ },
377
+ },
378
+ components: [],
379
+ dependencies: [{ ref: "pkg:generic/demo@1.0.0", dependsOn: [] }],
380
+ unrelatedInventory: {
381
+ components: [
382
+ {
383
+ author: "Should Not Be Traversed",
384
+ name: "not-a-component-list",
385
+ },
386
+ ],
387
+ },
388
+ },
389
+ };
390
+
391
+ const result = postProcess(bomNSData, { specVersion: 2.0 });
392
+ const [toolService] = result.bomJson.metadata.tools.services;
393
+
394
+ assert.strictEqual(result.bomJson.metadata.manufacture, undefined);
395
+ assert.deepStrictEqual(result.bomJson.metadata.component.manufacturer, {
396
+ name: "Component Factory",
397
+ });
398
+ assert.deepStrictEqual(toolService.provider, { name: "Scanner Vendor" });
399
+ assert.strictEqual(toolService.vendor, undefined);
400
+ assert.strictEqual(toolService.author, undefined);
401
+ assert.strictEqual(
402
+ result.bomJson.unrelatedInventory.components[0].author,
403
+ "Should Not Be Traversed",
404
+ );
405
+ });
406
+
407
+ it("postProcess downgrades certificate crypto properties for spec version 1.6", () => {
408
+ const bomNSData = {
409
+ bomJson: {
410
+ bomFormat: "CycloneDX",
411
+ specVersion: "1.6",
412
+ components: [
413
+ {
414
+ type: "cryptographic-asset",
415
+ name: "demo-cert",
416
+ cryptoProperties: {
417
+ assetType: "certificate",
418
+ certificateProperties: {
419
+ serialNumber: "1234",
420
+ subjectName: "CN=demo",
421
+ issuerName: "CN=demo",
422
+ notValidBefore: "2024-01-01T00:00:00.000Z",
423
+ notValidAfter: "2034-01-01T00:00:00.000Z",
424
+ certificateFormat: "X.509",
425
+ certificateFileExtension: "crt",
426
+ fingerprint: { alg: "SHA-1", content: "a".repeat(40) },
427
+ },
428
+ },
429
+ },
430
+ ],
431
+ dependencies: [],
432
+ formulation: [
433
+ {
434
+ components: [
435
+ {
436
+ type: "cryptographic-asset",
437
+ name: "formulation-cert",
438
+ cryptoProperties: {
439
+ assetType: "certificate",
440
+ certificateProperties: {
441
+ serialNumber: "5678",
442
+ subjectName: "CN=formulation",
443
+ certificateFileExtension: "pem",
444
+ fingerprint: { alg: "SHA-1", content: "b".repeat(40) },
445
+ },
446
+ },
447
+ },
448
+ ],
449
+ },
450
+ ],
451
+ metadata: {
452
+ properties: [],
453
+ tools: {
454
+ components: [{ name: "cdxgen" }],
455
+ },
456
+ },
457
+ },
458
+ };
459
+ const result = postProcess(bomNSData, { specVersion: 1.6 });
460
+ const componentCert =
461
+ result.bomJson.components[0].cryptoProperties.certificateProperties;
462
+ const formulationCert =
463
+ result.bomJson.formulation[0].components[0].cryptoProperties
464
+ .certificateProperties;
465
+
466
+ assert.deepStrictEqual(componentCert, {
467
+ subjectName: "CN=demo",
468
+ issuerName: "CN=demo",
469
+ notValidBefore: "2024-01-01T00:00:00.000Z",
470
+ notValidAfter: "2034-01-01T00:00:00.000Z",
471
+ certificateFormat: "X.509",
472
+ certificateExtension: "crt",
473
+ });
474
+ assert.deepStrictEqual(formulationCert, {
475
+ subjectName: "CN=formulation",
476
+ certificateExtension: "pem",
477
+ });
478
+ });
479
+
480
+ it("postProcess removes remaining 1.7-only fields from metadata, components, and formulation inventories for spec version 1.6", () => {
481
+ const bomNSData = {
482
+ bomJson: {
483
+ bomFormat: "CycloneDX",
484
+ specVersion: "1.6",
485
+ components: [
486
+ {
487
+ type: "library",
488
+ name: "demo-lib",
489
+ version: "1.0.0",
490
+ isExternal: true,
491
+ patentAssertions: [{ patentNumber: "US-123" }],
492
+ versionRange: "vers:npm/>=1.0.0|<2.0.0",
493
+ },
494
+ ],
495
+ dependencies: [],
496
+ formulation: [
497
+ {
498
+ components: [
499
+ {
500
+ type: "library",
501
+ name: "formulation-lib",
502
+ version: "2.0.0",
503
+ isExternal: true,
504
+ versionRange: "vers:npm/>=2.0.0|<3.0.0",
505
+ },
506
+ ],
507
+ services: [
508
+ {
509
+ name: "formulation-service",
510
+ patentAssertions: [{ patentNumber: "US-456" }],
511
+ },
512
+ ],
513
+ },
514
+ ],
515
+ metadata: {
516
+ distributionConstraints: { tlp: "GREEN" },
517
+ component: {
518
+ type: "application",
519
+ name: "demo-app",
520
+ version: "1.0.0",
521
+ isExternal: true,
522
+ versionRange: "vers:npm/>=1.0.0|<2.0.0",
523
+ },
524
+ properties: [],
525
+ tools: {
526
+ components: [{ name: "cdxgen" }],
527
+ },
528
+ },
529
+ services: [
530
+ {
531
+ name: "demo-service",
532
+ patentAssertions: [{ patentNumber: "US-789" }],
533
+ },
534
+ ],
535
+ },
536
+ };
537
+
538
+ const result = postProcess(bomNSData, { specVersion: 1.6 });
539
+ const rootComponent = result.bomJson.components[0];
540
+ const formulationComponent = result.bomJson.formulation[0].components[0];
541
+ const rootService = result.bomJson.services[0];
542
+ const formulationService = result.bomJson.formulation[0].services[0];
543
+ const metadataComponent = result.bomJson.metadata.component;
544
+
545
+ assert.strictEqual(
546
+ result.bomJson.metadata.distributionConstraints,
547
+ undefined,
548
+ );
549
+ assert.strictEqual(rootComponent.isExternal, undefined);
550
+ assert.strictEqual(rootComponent.patentAssertions, undefined);
551
+ assert.strictEqual(rootComponent.versionRange, undefined);
552
+ assert.strictEqual(formulationComponent.isExternal, undefined);
553
+ assert.strictEqual(formulationComponent.versionRange, undefined);
554
+ assert.strictEqual(rootService.patentAssertions, undefined);
555
+ assert.strictEqual(formulationService.patentAssertions, undefined);
556
+ assert.strictEqual(metadataComponent.isExternal, undefined);
557
+ assert.strictEqual(metadataComponent.versionRange, undefined);
558
+ });
559
+
560
+ it("postProcess removes remaining 1.6-only fields from metadata, components, and formulation inventories for spec version 1.5", () => {
561
+ const bomNSData = {
562
+ bomJson: {
563
+ bomFormat: "CycloneDX",
564
+ specVersion: "1.5",
565
+ components: [
566
+ {
567
+ type: "library",
568
+ name: "demo-lib",
569
+ version: "1.0.0",
570
+ authors: [{ name: "Alice" }],
571
+ manufacturer: { name: "Acme" },
572
+ omniborId: ["gitoid:blob:sha1:abc"],
573
+ swhid: ["swh:1:rev:def"],
574
+ tags: ["demo"],
575
+ },
576
+ ],
577
+ dependencies: [],
578
+ formulation: [
579
+ {
580
+ components: [
581
+ {
582
+ type: "library",
583
+ name: "formulation-lib",
584
+ version: "2.0.0",
585
+ authors: [{ name: "Bob" }],
586
+ manufacturer: { name: "Builder" },
587
+ omniborId: ["gitoid:blob:sha1:ghi"],
588
+ swhid: ["swh:1:dir:jkl"],
589
+ tags: ["workflow"],
590
+ },
591
+ ],
592
+ services: [
593
+ {
594
+ name: "formulation-service",
595
+ tags: ["ci"],
596
+ },
597
+ ],
598
+ },
599
+ ],
600
+ metadata: {
601
+ manufacturer: { name: "BOM Factory" },
602
+ component: {
603
+ type: "application",
604
+ name: "demo-app",
605
+ version: "1.0.0",
606
+ authors: [{ name: "Carol" }],
607
+ manufacturer: { name: "Acme" },
608
+ tags: ["root"],
609
+ },
610
+ properties: [],
611
+ },
612
+ services: [
613
+ {
614
+ name: "demo-service",
615
+ tags: ["runtime"],
616
+ },
617
+ ],
618
+ },
619
+ };
620
+
621
+ const result = postProcess(bomNSData, { specVersion: 1.5 });
622
+ const rootComponent = result.bomJson.components[0];
623
+ const formulationComponent = result.bomJson.formulation[0].components[0];
624
+ const rootService = result.bomJson.services[0];
625
+ const formulationService = result.bomJson.formulation[0].services[0];
626
+ const metadataComponent = result.bomJson.metadata.component;
627
+
628
+ assert.strictEqual(result.bomJson.metadata.manufacturer, undefined);
629
+ assert.strictEqual(rootComponent.authors, undefined);
630
+ assert.strictEqual(rootComponent.manufacturer, undefined);
631
+ assert.strictEqual(rootComponent.omniborId, undefined);
632
+ assert.strictEqual(rootComponent.swhid, undefined);
633
+ assert.strictEqual(rootComponent.tags, undefined);
634
+ assert.strictEqual(formulationComponent.authors, undefined);
635
+ assert.strictEqual(formulationComponent.manufacturer, undefined);
636
+ assert.strictEqual(formulationComponent.omniborId, undefined);
637
+ assert.strictEqual(formulationComponent.swhid, undefined);
638
+ assert.strictEqual(formulationComponent.tags, undefined);
639
+ assert.strictEqual(rootService.tags, undefined);
640
+ assert.strictEqual(formulationService.tags, undefined);
641
+ assert.strictEqual(metadataComponent.authors, undefined);
642
+ assert.strictEqual(metadataComponent.manufacturer, undefined);
643
+ assert.strictEqual(metadataComponent.tags, undefined);
644
+ });
645
+
646
+ it("postProcess removes unsupported evidence occurrence details for spec version 1.5", () => {
647
+ const bomNSData = {
648
+ bomJson: {
649
+ bomFormat: "CycloneDX",
650
+ specVersion: "1.5",
651
+ components: [
652
+ {
653
+ type: "file",
654
+ name: "deviceTypeManager.js",
655
+ evidence: {
656
+ occurrences: [
657
+ {
658
+ location: "source/microservices/lib/deviceTypeManager.js",
659
+ line: 11,
660
+ offset: 2,
661
+ symbol: "deviceTypeManager",
662
+ additionalContext: "source-import",
663
+ },
664
+ ],
665
+ },
666
+ },
667
+ ],
668
+ dependencies: [],
669
+ metadata: { properties: [] },
670
+ },
671
+ };
672
+ const result = postProcess(bomNSData, { specVersion: 1.5 });
673
+
674
+ assert.deepStrictEqual(result.bomJson.components[0].evidence.occurrences, [
675
+ {
676
+ location: "source/microservices/lib/deviceTypeManager.js",
677
+ },
678
+ ]);
679
+ });
680
+
244
681
  it("postProcess merges formulation-discovered MCP config services into bomJson.services", () => {
245
682
  const tmpDir = join(tmpdir(), `cdxgen-postgen-${Date.now()}`);
246
683
  mkdirSync(join(tmpDir, ".vscode"), { recursive: true });
@@ -372,6 +809,53 @@ it("postProcess attaches releaseNotes to cdxgen metadata tool component", () =>
372
809
  }
373
810
  });
374
811
 
812
+ it("postProcess refreshes unpackaged native file inventory counts from the final BOM", () => {
813
+ const bomNSData = {
814
+ bomJson: {
815
+ bomFormat: "CycloneDX",
816
+ specVersion: "1.7",
817
+ components: [
818
+ {
819
+ name: "demo",
820
+ type: "file",
821
+ properties: [{ name: "internal:is_executable", value: "true" }],
822
+ },
823
+ {
824
+ name: "libdemo.so",
825
+ type: "file",
826
+ properties: [{ name: "internal:is_shared_library", value: "true" }],
827
+ },
828
+ ],
829
+ dependencies: [],
830
+ metadata: {
831
+ properties: [
832
+ { name: "cdx:container:unpackagedExecutableCount", value: "0" },
833
+ {
834
+ name: "cdx:container:unpackagedSharedLibraryCount",
835
+ value: "0",
836
+ },
837
+ ],
838
+ tools: {
839
+ components: [
840
+ { group: "@cyclonedx", name: "cdxgen", version: "test" },
841
+ ],
842
+ },
843
+ },
844
+ },
845
+ };
846
+
847
+ const result = postProcess(bomNSData, { specVersion: 1.7 });
848
+ assert.deepStrictEqual(
849
+ result.bomJson.metadata.properties.filter((property) =>
850
+ property.name.startsWith("cdx:container:unpackaged"),
851
+ ),
852
+ [
853
+ { name: "cdx:container:unpackagedExecutableCount", value: "1" },
854
+ { name: "cdx:container:unpackagedSharedLibraryCount", value: "1" },
855
+ ],
856
+ );
857
+ });
858
+
375
859
  it("postProcess fails for weak TLP when sensitive property values are present", () => {
376
860
  const bomNSData = {
377
861
  bomJson: {
@@ -136,6 +136,24 @@ function normalizeStandardsMetadata(rule) {
136
136
  return Object.keys(normalized).length ? normalized : undefined;
137
137
  }
138
138
 
139
+ function normalizeDryRunSupport(rule) {
140
+ const rawValue =
141
+ typeof rule?.["dry-run-support"] === "string"
142
+ ? rule["dry-run-support"]
143
+ : typeof rule?.dryRunSupport === "string"
144
+ ? rule.dryRunSupport
145
+ : undefined;
146
+ if (rawValue !== undefined && !["no", "partial", "full"].includes(rawValue)) {
147
+ if (DEBUG_MODE) {
148
+ console.warn(
149
+ `Rule ${rule?.id || "unknown"} has invalid dry-run-support '${rawValue}'; defaulting to 'partial'`,
150
+ );
151
+ }
152
+ return "partial";
153
+ }
154
+ return ["no", "partial", "full"].includes(rawValue) ? rawValue : "partial";
155
+ }
156
+
139
157
  /**
140
158
  * Helper: Check if property exists and equals expected value
141
159
  * Usage: $hasProp(component, 'cdx:foo', 'bar')
@@ -247,6 +265,103 @@ function registerCdxHelpers(expression) {
247
265
  expression.registerFunction("safeStr", (val) => {
248
266
  return val === null || val === undefined ? "" : String(val).trim();
249
267
  });
268
+ expression.registerFunction("parseSizeBytes", (val) => {
269
+ if (val === null || val === undefined || val === "") {
270
+ return null;
271
+ }
272
+ if (typeof val === "number") {
273
+ return Number.isFinite(val) ? val : null;
274
+ }
275
+ const normalizedValue = String(val).trim();
276
+ if (!normalizedValue) {
277
+ return null;
278
+ }
279
+ const matchedValue = normalizedValue.match(
280
+ /^(\d+(?:\.\d+)?)\s*([kmgtpe]?i?b)?$/iu,
281
+ );
282
+ if (!matchedValue) {
283
+ return null;
284
+ }
285
+ const numericValue = Number.parseFloat(matchedValue[1]);
286
+ if (!Number.isFinite(numericValue)) {
287
+ return null;
288
+ }
289
+ const unit = (matchedValue[2] || "").toLowerCase();
290
+ const unitMap = {
291
+ b: 1,
292
+ kb: 1000,
293
+ kib: 1024,
294
+ mb: 1000 ** 2,
295
+ mib: 1024 ** 2,
296
+ gb: 1000 ** 3,
297
+ gib: 1024 ** 3,
298
+ tb: 1000 ** 4,
299
+ tib: 1024 ** 4,
300
+ pb: 1000 ** 5,
301
+ pib: 1024 ** 5,
302
+ eb: 1000 ** 6,
303
+ eib: 1024 ** 6,
304
+ };
305
+ const multiplier = unitMap[unit || "b"];
306
+ if (!multiplier) {
307
+ return null;
308
+ }
309
+ return numericValue * multiplier;
310
+ });
311
+ expression.registerFunction("firstNonEmpty", (...values) => {
312
+ for (const value of values) {
313
+ if (value === null || value === undefined) {
314
+ continue;
315
+ }
316
+ if (Array.isArray(value)) {
317
+ const candidate = value
318
+ .map((entry) =>
319
+ entry === null || entry === undefined ? "" : String(entry).trim(),
320
+ )
321
+ .filter(Boolean)
322
+ .join(", ");
323
+ if (candidate) {
324
+ return candidate;
325
+ }
326
+ continue;
327
+ }
328
+ const candidate = String(value).trim();
329
+ if (candidate) {
330
+ return candidate;
331
+ }
332
+ }
333
+ return "";
334
+ });
335
+ expression.registerFunction("isDarwinSystemPath", (value) => {
336
+ if (typeof value !== "string") {
337
+ return false;
338
+ }
339
+ const normalized = value.trim();
340
+ return (
341
+ normalized.startsWith("/bin/") ||
342
+ normalized.startsWith("/sbin/") ||
343
+ normalized.startsWith("/System/") ||
344
+ normalized.startsWith("/usr/bin/") ||
345
+ normalized.startsWith("/usr/libexec/") ||
346
+ normalized.startsWith("/usr/sbin/") ||
347
+ normalized.startsWith("/Library/Apple/System/") ||
348
+ normalized.startsWith("/System/Volumes/Preboot/Cryptexes/")
349
+ );
350
+ });
351
+ expression.registerFunction("isWindowsUserControlledPath", (value) => {
352
+ if (typeof value !== "string") {
353
+ return false;
354
+ }
355
+ const normalized = value.trim().replaceAll("/", "\\").toLowerCase();
356
+ return (
357
+ normalized.includes("\\users\\") ||
358
+ normalized.includes("\\programdata\\") ||
359
+ normalized.includes("\\appdata\\") ||
360
+ normalized.includes("\\downloads\\") ||
361
+ normalized.includes("\\desktop\\") ||
362
+ normalized.includes("\\temp\\")
363
+ );
364
+ });
250
365
  expression.registerFunction("auditComponents", (bomJson) =>
251
366
  getAuditComponents(bomJson),
252
367
  );
@@ -331,6 +446,7 @@ export async function loadRules(rulesDir) {
331
446
  }
332
447
  rule.severity = rule.severity || "medium";
333
448
  rule.category = rule.category || "unknown";
449
+ rule.dryRunSupport = normalizeDryRunSupport(rule);
334
450
  const attack = normalizeAttackMetadata(rule);
335
451
  if (attack.tactics.length || attack.techniques.length) {
336
452
  rule.attack = attack;
@@ -1,5 +1,7 @@
1
1
  import process from "node:process";
2
2
 
3
+ import { recordEnvironmentRead } from "../../helpers/utils.js";
4
+
3
5
  const PERMISSION_FLAGS = [
4
6
  "--permission",
5
7
  "--allow-fs-read",
@@ -57,8 +59,13 @@ const PERMISSION_FLAG_PATTERNS = PERMISSION_FLAGS.map(
57
59
 
58
60
  export function auditEnvironment(env = process.env) {
59
61
  const findings = [];
60
- const nodeOptions = env.NODE_OPTIONS || "";
61
- const cdxgenNodeOptions = env.CDXGEN_NODE_OPTIONS || "";
62
+ const envSource = env === process.env ? "process.env" : "env";
63
+ const readEnv = (varName) => {
64
+ recordEnvironmentRead(varName, { source: envSource });
65
+ return env[varName];
66
+ };
67
+ const nodeOptions = readEnv("NODE_OPTIONS") || "";
68
+ const cdxgenNodeOptions = readEnv("CDXGEN_NODE_OPTIONS") || "";
62
69
  const hasPermission = PERMISSION_FLAG_PATTERNS.some((re) =>
63
70
  re.test(nodeOptions),
64
71
  );
@@ -93,7 +100,7 @@ export function auditEnvironment(env = process.env) {
93
100
  });
94
101
  }
95
102
  // NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS verification; any other value is benign.
96
- if (env.NODE_TLS_REJECT_UNAUTHORIZED === "0") {
103
+ if (readEnv("NODE_TLS_REJECT_UNAUTHORIZED") === "0") {
97
104
  findings.push({
98
105
  type: "environment-variable",
99
106
  variable: "NODE_TLS_REJECT_UNAUTHORIZED",
@@ -107,6 +114,7 @@ export function auditEnvironment(env = process.env) {
107
114
 
108
115
  for (const varName of RISKY_PRESENCE_VARS) {
109
116
  if (env[varName] != null && env[varName] !== "") {
117
+ recordEnvironmentRead(varName, { source: envSource });
110
118
  const messages = {
111
119
  NODE_PATH:
112
120
  "NODE_PATH is set and may cause unexpected modules to be loaded, enabling module-resolution poisoning.",
@@ -166,6 +174,7 @@ export function auditEnvironment(env = process.env) {
166
174
  "JDK_JAVA_OPTIONS",
167
175
  ]) {
168
176
  const jvmOptions = env[jvmVar] || "";
177
+ recordEnvironmentRead(jvmVar, { source: envSource });
169
178
  if (jvmOptions) {
170
179
  for (const pattern of JVM_CODE_EXECUTION_PATTERNS) {
171
180
  if (pattern.test(jvmOptions)) {
@@ -184,6 +193,7 @@ export function auditEnvironment(env = process.env) {
184
193
  // Proxy interception — informational
185
194
  const activeProxy = PROXY_VARS.find((v) => env[v] != null && env[v] !== "");
186
195
  if (activeProxy) {
196
+ recordEnvironmentRead(activeProxy, { source: envSource });
187
197
  findings.push({
188
198
  type: "network-interception",
189
199
  variable: activeProxy,
@@ -197,6 +207,7 @@ export function auditEnvironment(env = process.env) {
197
207
  // Credential exposure — detect any env var whose name follows a credential-naming convention.
198
208
  for (const [varName, varValue] of Object.entries(env)) {
199
209
  if (varValue && CREDENTIAL_VAR_PATTERN.test(varName)) {
210
+ recordEnvironmentRead(varName, { source: envSource });
200
211
  findings.push({
201
212
  type: "credential-exposure",
202
213
  variable: varName,