@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
@@ -124,6 +124,8 @@ it("parseImageName tests", () => {
124
124
  async function loadDockerModule({
125
125
  clientResponse,
126
126
  fsOverrides,
127
+ streamOverrides,
128
+ tarOverrides,
127
129
  utilsOverrides,
128
130
  } = {}) {
129
131
  const dockerClient = sinon.stub().resolves(
@@ -151,7 +153,13 @@ async function loadDockerModule({
151
153
  getAllFiles: sinon.stub().returns([]),
152
154
  getTmpDir: sinon.stub().returns("/tmp"),
153
155
  isDryRun: false,
156
+ readEnvironmentVariable: sinon
157
+ .stub()
158
+ .callsFake((varName) => process.env[varName]),
154
159
  recordActivity: sinon.stub(),
160
+ recordDecisionActivity: sinon.stub(),
161
+ recordSensitiveFileRead: sinon.stub(),
162
+ safeExtractArchive: sinon.stub().resolves(true),
155
163
  safeExistsSync: sinon.stub().returns(false),
156
164
  safeMkdirSync: sinon.stub(),
157
165
  safeMkdtempSync: sinon.stub().returns("/tmp/docker-images-test"),
@@ -162,7 +170,15 @@ async function loadDockerModule({
162
170
  };
163
171
  const dockerModule = await esmock("./docker.js", {
164
172
  "node:fs": fsStub,
173
+ "node:stream/promises": {
174
+ pipeline: sinon.stub().resolves(),
175
+ ...streamOverrides,
176
+ },
165
177
  got: { default: gotStub },
178
+ tar: {
179
+ x: sinon.stub().returns("extractor"),
180
+ ...tarOverrides,
181
+ },
166
182
  "../helpers/utils.js": utilsStub,
167
183
  });
168
184
  return { dockerClient, dockerModule, fsStub, gotStub, utilsStub };
@@ -386,12 +402,265 @@ await it("docker getConnection reports blocked network activity in dry-run mode"
386
402
  });
387
403
  });
388
404
 
389
- await it("docker extractTar reports a blocked untar activity in dry-run mode", async () => {
405
+ await it("docker getConnection skips dry-run tracing on containerd runtimes", async () => {
406
+ const recordActivity = sinon.stub();
407
+ const recordSensitiveFileRead = sinon.stub();
408
+ await withEnv(
409
+ {
410
+ CONTAINERD_ADDRESS: "/run/containerd/containerd.sock",
411
+ },
412
+ async () => {
413
+ const { dockerModule } = await loadDockerModule({
414
+ fsOverrides: {
415
+ readFileSync: sinon.stub().returns(authConfigData("docker.io")),
416
+ },
417
+ utilsOverrides: {
418
+ isDryRun: true,
419
+ recordActivity,
420
+ recordSensitiveFileRead,
421
+ safeExistsSync: dockerConfigExistsStub(),
422
+ },
423
+ });
424
+ const conn = await dockerModule.getConnection({}, "docker.io");
425
+ assert.strictEqual(conn, undefined);
426
+ },
427
+ );
428
+ sinon.assert.notCalled(recordActivity);
429
+ sinon.assert.notCalled(recordSensitiveFileRead);
430
+ });
431
+
432
+ await it("docker getConnection traces docker credential file reads in dry-run mode", async () => {
390
433
  const recordActivity = sinon.stub();
434
+ const recordSensitiveFileRead = sinon.stub();
435
+ await withDockerConfig(async () => {
436
+ const { dockerModule } = await loadDockerModule({
437
+ fsOverrides: {
438
+ readFileSync: sinon.stub().returns(authConfigData("docker.io")),
439
+ },
440
+ utilsOverrides: {
441
+ isDryRun: true,
442
+ recordActivity,
443
+ recordSensitiveFileRead,
444
+ safeExistsSync: dockerConfigExistsStub(),
445
+ },
446
+ });
447
+ await dockerModule.getConnection({}, "docker.io");
448
+ });
449
+ sinon.assert.calledWithMatch(recordSensitiveFileRead, sinon.match.string, {
450
+ label: "Docker credential file",
451
+ });
452
+ sinon.assert.calledWithMatch(recordActivity, {
453
+ kind: "network",
454
+ status: "blocked",
455
+ target: "docker.io",
456
+ });
457
+ });
458
+
459
+ await it("docker makeRequest does not trace docker credential file reads when the read fails", async () => {
460
+ const recordSensitiveFileRead = sinon.stub();
461
+ await withEnv(
462
+ {
463
+ DOCKER_AUTH_CONFIG: undefined,
464
+ DOCKER_EMAIL: undefined,
465
+ DOCKER_PASSWORD: undefined,
466
+ DOCKER_USER: undefined,
467
+ },
468
+ async () => {
469
+ await withDockerConfig(async () => {
470
+ const { dockerModule } = await loadDockerModule({
471
+ fsOverrides: {
472
+ readFileSync: sinon.stub().throws(new Error("read failed")),
473
+ },
474
+ utilsOverrides: {
475
+ recordSensitiveFileRead,
476
+ safeExistsSync: dockerConfigExistsStub(),
477
+ },
478
+ });
479
+ await assert.rejects(() =>
480
+ dockerModule.makeRequest(
481
+ "images/create?fromImage=docker.io/library/alpine:latest",
482
+ "POST",
483
+ "docker.io/library/alpine:latest",
484
+ ),
485
+ );
486
+ });
487
+ },
488
+ );
489
+ sinon.assert.notCalled(recordSensitiveFileRead);
490
+ });
491
+
492
+ await it("docker getConnection does not trace TLS client files when reading them fails", async () => {
493
+ const recordSensitiveFileRead = sinon.stub();
494
+ await withEnv(
495
+ {
496
+ DOCKER_AUTH_CONFIG: undefined,
497
+ DOCKER_CERT_PATH: "/tmp/docker-certs",
498
+ DOCKER_EMAIL: undefined,
499
+ DOCKER_HOST: "tcp://docker.example.test:2376",
500
+ DOCKER_PASSWORD: undefined,
501
+ DOCKER_USER: undefined,
502
+ },
503
+ async () => {
504
+ const { dockerModule } = await loadDockerModule({
505
+ fsOverrides: {
506
+ readFileSync: sinon
507
+ .stub()
508
+ .onFirstCall()
509
+ .throws(new Error("cert read failed")),
510
+ },
511
+ utilsOverrides: {
512
+ recordSensitiveFileRead,
513
+ },
514
+ });
515
+ await assert.rejects(() => dockerModule.getConnection({}, "docker.io"));
516
+ },
517
+ );
518
+ sinon.assert.notCalled(recordSensitiveFileRead);
519
+ });
520
+
521
+ await it("docker makeRequest does not trace TLS client files when reading them fails", async () => {
522
+ const recordSensitiveFileRead = sinon.stub();
523
+ await withEnv(
524
+ {
525
+ DOCKER_AUTH_CONFIG: undefined,
526
+ DOCKER_CERT_PATH: "/tmp/docker-certs",
527
+ DOCKER_EMAIL: undefined,
528
+ DOCKER_HOST: "tcp://docker.example.test:2376",
529
+ DOCKER_PASSWORD: undefined,
530
+ DOCKER_USER: undefined,
531
+ },
532
+ async () => {
533
+ const { dockerModule } = await loadDockerModule({
534
+ fsOverrides: {
535
+ readFileSync: sinon
536
+ .stub()
537
+ .onFirstCall()
538
+ .throws(new Error("cert read failed")),
539
+ },
540
+ utilsOverrides: {
541
+ recordSensitiveFileRead,
542
+ },
543
+ });
544
+ await assert.rejects(() =>
545
+ dockerModule.makeRequest(
546
+ "images/create?fromImage=docker.io/library/alpine:latest",
547
+ "POST",
548
+ "docker.io/library/alpine:latest",
549
+ ),
550
+ );
551
+ },
552
+ );
553
+ sinon.assert.notCalled(recordSensitiveFileRead);
554
+ });
555
+
556
+ await it("docker getConnection records which credential source was selected", async () => {
557
+ const recordDecisionActivity = sinon.stub();
558
+ await withDockerConfig(async () => {
559
+ const { dockerModule } = await loadDockerModule({
560
+ fsOverrides: {
561
+ readFileSync: sinon.stub().returns(authConfigData("docker.io")),
562
+ },
563
+ utilsOverrides: {
564
+ isDryRun: true,
565
+ recordDecisionActivity,
566
+ safeExistsSync: dockerConfigExistsStub(),
567
+ },
568
+ });
569
+ await dockerModule.getConnection({}, "docker.io");
570
+ });
571
+ sinon.assert.calledWithMatch(
572
+ recordDecisionActivity,
573
+ "docker-auth:docker.io",
574
+ {
575
+ metadata: sinon.match({
576
+ decisionType: "credential-source-selection",
577
+ selectedSource: "docker-config-auth",
578
+ }),
579
+ },
580
+ );
581
+ });
582
+
583
+ await it("docker getConnection traces credential helper resolution in dry-run mode", async () => {
584
+ const safeSpawnSync = sinon.stub().returns({
585
+ status: 1,
586
+ stdout: "",
587
+ stderr: "",
588
+ });
589
+ await withDockerConfig(async () => {
590
+ const { dockerModule } = await loadDockerModule({
591
+ fsOverrides: {
592
+ readFileSync: sinon.stub().returns(credHelperConfigData("docker.io")),
593
+ },
594
+ utilsOverrides: {
595
+ isDryRun: true,
596
+ safeExistsSync: dockerConfigExistsStub(),
597
+ safeSpawnSync,
598
+ },
599
+ });
600
+ await dockerModule.getConnection({}, "docker.io");
601
+ });
602
+ sinon.assert.calledWithExactly(
603
+ safeSpawnSync,
604
+ credHelperExe("osxkeychain"),
605
+ ["get"],
606
+ {
607
+ input: "docker.io",
608
+ },
609
+ );
610
+ });
611
+
612
+ await it("docker extractTar reports a blocked untar activity in dry-run mode", async () => {
613
+ const safeExtractArchive = sinon.stub().resolves(false);
391
614
  const { dockerModule } = await loadDockerModule({
392
615
  utilsOverrides: {
393
- isDryRun: true,
394
- recordActivity,
616
+ safeExtractArchive,
617
+ },
618
+ });
619
+ const result = await dockerModule.extractTar(
620
+ "/tmp/image.tar",
621
+ "/tmp/out",
622
+ {},
623
+ );
624
+ assert.strictEqual(result, false);
625
+ sinon.assert.calledWithMatch(
626
+ safeExtractArchive,
627
+ "/tmp/image.tar",
628
+ "/tmp/out",
629
+ sinon.match.func,
630
+ "untar",
631
+ {
632
+ blockedReason:
633
+ "Dry run mode blocks untar and layer extraction operations because they create files on disk.",
634
+ metadata: {
635
+ archiveFormat: "tar",
636
+ },
637
+ },
638
+ );
639
+ });
640
+
641
+ await it("docker extractTar delegates successful untar tracing to safeExtractArchive", async () => {
642
+ const safeExtractArchive = sinon.stub().resolves(true);
643
+ const { dockerModule } = await loadDockerModule({
644
+ utilsOverrides: {
645
+ safeExtractArchive,
646
+ },
647
+ });
648
+ const result = await dockerModule.extractTar(
649
+ "/tmp/image.tar",
650
+ "/tmp/out",
651
+ {},
652
+ );
653
+ assert.strictEqual(result, true);
654
+ sinon.assert.calledOnce(safeExtractArchive);
655
+ });
656
+
657
+ await it("docker extractTar preserves failure handling after safeExtractArchive rejects", async () => {
658
+ const extractionError = new Error("permission denied");
659
+ extractionError.code = "EACCES";
660
+ const safeExtractArchive = sinon.stub().rejects(extractionError);
661
+ const { dockerModule } = await loadDockerModule({
662
+ utilsOverrides: {
663
+ safeExtractArchive,
395
664
  },
396
665
  });
397
666
  const result = await dockerModule.extractTar(
@@ -400,28 +669,84 @@ await it("docker extractTar reports a blocked untar activity in dry-run mode", a
400
669
  {},
401
670
  );
402
671
  assert.strictEqual(result, false);
672
+ sinon.assert.calledOnce(safeExtractArchive);
673
+ });
674
+
675
+ await it("docker exportImage reports a blocked container activity in dry-run mode", async () => {
676
+ const recordActivity = sinon.stub();
677
+ const recordSensitiveFileRead = sinon.stub();
678
+ await withDockerConfig(async () => {
679
+ const { dockerModule } = await loadDockerModule({
680
+ fsOverrides: {
681
+ readFileSync: sinon.stub().returns(authConfigData("docker.io")),
682
+ },
683
+ utilsOverrides: {
684
+ isDryRun: true,
685
+ recordActivity,
686
+ recordSensitiveFileRead,
687
+ safeExistsSync: dockerConfigExistsStub(),
688
+ },
689
+ });
690
+ const result = await dockerModule.exportImage("alpine:3.20", {});
691
+ assert.strictEqual(result, undefined);
692
+ });
693
+ sinon.assert.calledWithMatch(recordSensitiveFileRead, sinon.match.string, {
694
+ label: "Docker credential file",
695
+ });
403
696
  sinon.assert.calledWithMatch(recordActivity, {
404
- kind: "untar",
697
+ kind: "container",
405
698
  status: "blocked",
406
- target: "/tmp/image.tar -> /tmp/out",
699
+ target: "alpine:3.20",
407
700
  });
408
701
  });
409
702
 
410
- await it("docker exportImage reports a blocked container activity in dry-run mode", async () => {
703
+ await it("docker exportImage preserves scoped registry refs for dry-run auth tracing", async () => {
704
+ const recordDecisionActivity = sinon.stub();
705
+ await withDockerConfig(async () => {
706
+ const { dockerModule } = await loadDockerModule({
707
+ fsOverrides: {
708
+ readFileSync: sinon
709
+ .stub()
710
+ .returns(authConfigData("registry.example.com/team")),
711
+ },
712
+ utilsOverrides: {
713
+ isDryRun: true,
714
+ recordDecisionActivity,
715
+ safeExistsSync: dockerConfigExistsStub(),
716
+ },
717
+ });
718
+ const result = await dockerModule.exportImage(
719
+ "registry.example.com/team/app:latest",
720
+ {},
721
+ );
722
+ assert.strictEqual(result, undefined);
723
+ });
724
+ sinon.assert.calledWithMatch(
725
+ recordDecisionActivity,
726
+ "docker-auth:registry.example.com/team/app",
727
+ {
728
+ metadata: sinon.match({
729
+ selectedSource: "docker-config-auth",
730
+ }),
731
+ },
732
+ );
733
+ });
734
+
735
+ await it("docker exportImage skips dry-run tracing for local paths", async () => {
411
736
  const recordActivity = sinon.stub();
737
+ const recordSensitiveFileRead = sinon.stub();
412
738
  const { dockerModule } = await loadDockerModule({
413
739
  utilsOverrides: {
414
740
  isDryRun: true,
415
741
  recordActivity,
742
+ recordSensitiveFileRead,
743
+ safeExistsSync: sinon.stub().returns(true),
416
744
  },
417
745
  });
418
- const result = await dockerModule.exportImage("alpine:3.20", {});
746
+ const result = await dockerModule.exportImage("/tmp/image.tar", {});
419
747
  assert.strictEqual(result, undefined);
420
- sinon.assert.calledWithMatch(recordActivity, {
421
- kind: "container",
422
- status: "blocked",
423
- target: "alpine:3.20",
424
- });
748
+ sinon.assert.notCalled(recordActivity);
749
+ sinon.assert.notCalled(recordSensitiveFileRead);
425
750
  });
426
751
 
427
752
  await it("docker exportImage ignores local directories", async () => {
@@ -27,7 +27,7 @@ import {
27
27
  } from "../helpers/source.js";
28
28
  import {
29
29
  CDXGEN_VERSION,
30
- hasDangerousUnicode,
30
+ isAllowedHttpHost,
31
31
  isSecureMode,
32
32
  isWin,
33
33
  } from "../helpers/utils.js";
@@ -76,32 +76,7 @@ const ALLOWED_PARAMS = [
76
76
 
77
77
  const app = connect();
78
78
 
79
- export function isAllowedHttpHost(hostname) {
80
- if (!process.env.CDXGEN_ALLOWED_HOSTS) {
81
- return true;
82
- }
83
- if (!hostname || hasDangerousUnicode(hostname)) {
84
- return false;
85
- }
86
- const normalizedHostname = hostname.toLowerCase();
87
- const allowHosts = process.env.CDXGEN_ALLOWED_HOSTS.split(",")
88
- .map((host) => host.trim())
89
- .filter(Boolean);
90
- for (const allowedHost of allowHosts) {
91
- const normalizedAllowedHost = allowedHost.toLowerCase();
92
- if (normalizedHostname === normalizedAllowedHost) {
93
- return true;
94
- }
95
- if (
96
- normalizedAllowedHost.startsWith("*.") &&
97
- normalizedHostname.length > normalizedAllowedHost.length - 1 &&
98
- normalizedHostname.endsWith(`.${normalizedAllowedHost.slice(2)}`)
99
- ) {
100
- return true;
101
- }
102
- }
103
- return false;
104
- }
79
+ export { isAllowedHttpHost };
105
80
 
106
81
  app.use(
107
82
  bodyParser.json({
@@ -1,6 +1,12 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
+ import {
5
+ formatHbomHardwareClassSummary,
6
+ getHbomSummary,
7
+ isHbomLikeBom,
8
+ } from "../../helpers/hbomAnalysis.js";
9
+ import { getContainerFileInventoryStats } from "../../helpers/inventoryStats.js";
4
10
  import { thoughtLog } from "../../helpers/logger.js";
5
11
  import { getTrustedPublishingComponentCounts } from "../../helpers/provenanceUtils.js";
6
12
  import { dirNameStr } from "../../helpers/utils.js";
@@ -247,6 +253,11 @@ export function findBomType(bomJson) {
247
253
  bomType = "SBOM";
248
254
  description = "Software Bill-of-Materials (SBOM) including GitHub Actions";
249
255
  }
256
+ // Is this an HBOM?
257
+ else if (isHbomLikeBom(bomJson)) {
258
+ bomType = "HBOM";
259
+ description = "Hardware Bill-of-Materials (HBOM)";
260
+ }
250
261
  // Is this an OBOM?
251
262
  else if (lifecycles.filter((l) => l.phase === "operations").length > 0) {
252
263
  bomType = "OBOM";
@@ -296,7 +307,10 @@ export function textualMetadata(bomJson) {
296
307
  const swidCount = bomJson?.components?.filter((c) =>
297
308
  c?.purl?.startsWith("pkg:swid"),
298
309
  ).length;
310
+ const { unpackagedExecutableCount, unpackagedSharedLibraryCount } =
311
+ getContainerFileInventoryStats(bomJson?.components);
299
312
  const githubStats = getGitHubWorkflowStats(bomJson?.components);
313
+ const hbomSummary = bomType === "HBOM" ? getHbomSummary(bomJson) : undefined;
300
314
  const trustedPublishingCounts = getTrustedPublishingComponentCounts(
301
315
  bomJson?.components,
302
316
  );
@@ -511,6 +525,9 @@ export function textualMetadata(bomJson) {
511
525
  if (bundledSdks.length) {
512
526
  text = `${text} Furthermore, the container image bundles the following SDKs: ${bundledSdks.join(", ")}.`;
513
527
  }
528
+ if (unpackagedExecutableCount || unpackagedSharedLibraryCount) {
529
+ text = `${text} The container or rootfs inventory includes ${unpackagedExecutableCount} executable file component(s) and ${unpackagedSharedLibraryCount} shared library component(s) that were not traced to OS package ownership.`;
530
+ }
514
531
  if (bomPkgTypes.length && bomPkgNamespaces.length) {
515
532
  if (bomPkgTypes.length === 1) {
516
533
  if (bomPkgNamespaces.length === 1) {
@@ -537,6 +554,27 @@ export function textualMetadata(bomJson) {
537
554
  text = `${text} In addition, there are ${swidCount} applications installed on the system.`;
538
555
  }
539
556
  }
557
+ if (bomType === "HBOM" && hbomSummary) {
558
+ if (hbomSummary.hardwareClassCount > 0) {
559
+ text = `${text} The hardware inventory spans ${hbomSummary.hardwareClassCount} hardware classes.`;
560
+ const hardwareClassSummary = formatHbomHardwareClassSummary(
561
+ hbomSummary.hardwareClassCounts,
562
+ );
563
+ if (hardwareClassSummary) {
564
+ text = `${text} The most represented hardware classes are ${hardwareClassSummary}.`;
565
+ }
566
+ }
567
+ if (hbomSummary.collectorProfile) {
568
+ text = `${text} Collector profile '${hbomSummary.collectorProfile}' recorded ${hbomSummary.evidenceCommandCount} command evidence entr${hbomSummary.evidenceCommandCount === 1 ? "y" : "ies"}`;
569
+ if (hbomSummary.evidenceFileCount > 0) {
570
+ text = `${text} and ${hbomSummary.evidenceFileCount} observed file entr${hbomSummary.evidenceFileCount === 1 ? "y" : "ies"}`;
571
+ }
572
+ text = `${text}.`;
573
+ }
574
+ if (hbomSummary.identifierPolicy) {
575
+ text = `${text} Identifier policy is '${hbomSummary.identifierPolicy}'.`;
576
+ }
577
+ }
540
578
  if (bomType === "SaaSBOM") {
541
579
  text = `${text} ${bomJson.services.length} are described in this ${bomType} under services.`;
542
580
  }
@@ -1,6 +1,6 @@
1
1
  import { assert, it } from "poku";
2
2
 
3
- import { extractTags, textualMetadata } from "./annotator.js";
3
+ import { extractTags, findBomType, textualMetadata } from "./annotator.js";
4
4
 
5
5
  it("textualMetadata tests", () => {
6
6
  assert.deepStrictEqual(textualMetadata({}), undefined);
@@ -303,6 +303,36 @@ it("textualMetadata tests", () => {
303
303
  "Trusted publishing metadata is present for 1 npm component(s) and 1 PyPI component(s).",
304
304
  ),
305
305
  );
306
+
307
+ assert.ok(
308
+ textualMetadata({
309
+ metadata: {
310
+ component: {
311
+ name: "demo-image",
312
+ type: "container",
313
+ version: "1.0.0",
314
+ },
315
+ timestamp: "2026-01-01T00:00:00Z",
316
+ tools: {
317
+ components: [{ name: "cdxgen" }],
318
+ },
319
+ },
320
+ components: [
321
+ {
322
+ type: "file",
323
+ name: "demo",
324
+ properties: [{ name: "internal:is_executable", value: "true" }],
325
+ },
326
+ {
327
+ type: "file",
328
+ name: "libdemo.so",
329
+ properties: [{ name: "internal:is_shared_library", value: "true" }],
330
+ },
331
+ ],
332
+ }).includes(
333
+ "The container or rootfs inventory includes 1 executable file component(s) and 1 shared library component(s) that were not traced to OS package ownership.",
334
+ ),
335
+ );
306
336
  });
307
337
 
308
338
  it("extractTags tests", () => {
@@ -326,3 +356,79 @@ it("textualMetadata includes the CycloneDX 1.7 TLP classification from distribut
326
356
  /TLP\) classification for this document is 'AMBER_AND_STRICT'/,
327
357
  );
328
358
  });
359
+
360
+ it("recognizes HBOMs and summarizes hardware-specific metadata", () => {
361
+ const hbom = {
362
+ bomFormat: "CycloneDX",
363
+ specVersion: "1.7",
364
+ metadata: {
365
+ timestamp: "2026-01-01T00:00:00Z",
366
+ tools: {
367
+ components: [{ name: "cdxgen" }],
368
+ },
369
+ component: {
370
+ name: "demo-host",
371
+ type: "device",
372
+ manufacturer: { name: "Example Corp" },
373
+ properties: [
374
+ { name: "cdx:hbom:platform", value: "linux" },
375
+ { name: "cdx:hbom:architecture", value: "amd64" },
376
+ {
377
+ name: "cdx:hbom:identifierPolicy",
378
+ value: "redacted-by-default",
379
+ },
380
+ ],
381
+ },
382
+ },
383
+ components: [
384
+ {
385
+ name: "eth0",
386
+ type: "device",
387
+ properties: [
388
+ { name: "cdx:hbom:hardwareClass", value: "network-interface" },
389
+ ],
390
+ },
391
+ {
392
+ name: "wlan0",
393
+ type: "device",
394
+ properties: [
395
+ { name: "cdx:hbom:hardwareClass", value: "network-interface" },
396
+ ],
397
+ },
398
+ {
399
+ name: "nvme0",
400
+ type: "device",
401
+ properties: [{ name: "cdx:hbom:hardwareClass", value: "storage" }],
402
+ },
403
+ ],
404
+ properties: [
405
+ { name: "cdx:hbom:collectorProfile", value: "linux-amd64-v1" },
406
+ { name: "cdx:hbom:evidence:commandCount", value: "2" },
407
+ {
408
+ name: "cdx:hbom:evidence:command",
409
+ value: "lscpu-json|cpu-memory|/usr/bin/lscpu -J",
410
+ },
411
+ {
412
+ name: "cdx:hbom:evidence:command",
413
+ value: "ip-link-json|network|/usr/sbin/ip -j link",
414
+ },
415
+ ],
416
+ };
417
+
418
+ assert.deepStrictEqual(findBomType(hbom), {
419
+ bomType: "HBOM",
420
+ bomTypeDescription: "Hardware Bill-of-Materials (HBOM)",
421
+ });
422
+ const summary = textualMetadata(hbom);
423
+ assert.match(summary, /Hardware Bill-of-Materials \(HBOM\)/u);
424
+ assert.match(summary, /The hardware inventory spans 2 hardware classes\./u);
425
+ assert.match(
426
+ summary,
427
+ /The most represented hardware classes are network-interface \(2\), storage \(1\)\./u,
428
+ );
429
+ assert.match(
430
+ summary,
431
+ /Collector profile 'linux-amd64-v1' recorded 2 command evidence entries\./u,
432
+ );
433
+ assert.match(summary, /Identifier policy is 'redacted-by-default'\./u);
434
+ });