@cyclonedx/cdxgen 12.3.2 → 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 (182) hide show
  1. package/README.md +70 -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 +171 -15
  20. package/data/rules/container-risk.yaml +14 -7
  21. package/data/rules/dependency-sources.yaml +76 -5
  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 +36 -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 +647 -127
  38. package/lib/cli/index.poku.js +1905 -187
  39. package/lib/evinser/evinser.js +14 -9
  40. package/lib/helpers/agentFormulationParser.js +6 -2
  41. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  42. package/lib/helpers/analyzer.js +1444 -38
  43. package/lib/helpers/analyzer.poku.js +409 -0
  44. package/lib/helpers/analyzerScope.js +712 -0
  45. package/lib/helpers/asarutils.js +1556 -0
  46. package/lib/helpers/asarutils.poku.js +443 -0
  47. package/lib/helpers/auditCategories.js +12 -0
  48. package/lib/helpers/auditCategories.poku.js +32 -0
  49. package/lib/helpers/cbomutils.js +271 -1
  50. package/lib/helpers/cbomutils.poku.js +248 -5
  51. package/lib/helpers/chromextutils.js +25 -3
  52. package/lib/helpers/chromextutils.poku.js +68 -0
  53. package/lib/helpers/ciParsers/githubActions.js +79 -0
  54. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  55. package/lib/helpers/communityAiConfigParser.js +15 -5
  56. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  57. package/lib/helpers/depsUtils.js +5 -0
  58. package/lib/helpers/depsUtils.poku.js +55 -0
  59. package/lib/helpers/display.js +336 -23
  60. package/lib/helpers/display.poku.js +179 -43
  61. package/lib/helpers/evidenceUtils.js +58 -0
  62. package/lib/helpers/evidenceUtils.poku.js +54 -0
  63. package/lib/helpers/exportUtils.js +9 -0
  64. package/lib/helpers/gtfobins.js +142 -8
  65. package/lib/helpers/gtfobins.poku.js +24 -1
  66. package/lib/helpers/hbom.js +710 -0
  67. package/lib/helpers/hbom.poku.js +496 -0
  68. package/lib/helpers/hbomAnalysis.js +268 -0
  69. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  70. package/lib/helpers/hbomLoader.js +35 -0
  71. package/lib/helpers/hostTopology.js +803 -0
  72. package/lib/helpers/hostTopology.poku.js +363 -0
  73. package/lib/helpers/inventoryStats.js +69 -0
  74. package/lib/helpers/inventoryStats.poku.js +86 -0
  75. package/lib/helpers/lolbas.js +19 -1
  76. package/lib/helpers/lolbas.poku.js +23 -0
  77. package/lib/helpers/mcpConfigParser.js +21 -5
  78. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  79. package/lib/helpers/osqueryTransform.js +47 -0
  80. package/lib/helpers/osqueryTransform.poku.js +47 -0
  81. package/lib/helpers/plugins.js +349 -0
  82. package/lib/helpers/plugins.poku.js +57 -0
  83. package/lib/helpers/propertySanitizer.js +121 -0
  84. package/lib/helpers/protobom.js +156 -45
  85. package/lib/helpers/protobom.poku.js +140 -5
  86. package/lib/helpers/remote/dependency-track.js +36 -3
  87. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  88. package/lib/helpers/source.js +24 -0
  89. package/lib/helpers/source.poku.js +32 -0
  90. package/lib/helpers/utils.js +2454 -198
  91. package/lib/helpers/utils.poku.js +1798 -74
  92. package/lib/managers/binary.e2e.poku.js +367 -0
  93. package/lib/managers/binary.js +2306 -350
  94. package/lib/managers/binary.poku.js +1700 -1
  95. package/lib/managers/docker.js +441 -95
  96. package/lib/managers/docker.poku.js +1479 -14
  97. package/lib/server/server.js +2 -24
  98. package/lib/server/server.poku.js +36 -1
  99. package/lib/stages/postgen/annotator.js +38 -0
  100. package/lib/stages/postgen/annotator.poku.js +107 -1
  101. package/lib/stages/postgen/auditBom.js +121 -18
  102. package/lib/stages/postgen/auditBom.poku.js +2967 -990
  103. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  104. package/lib/stages/postgen/postgen.js +192 -1
  105. package/lib/stages/postgen/postgen.poku.js +321 -0
  106. package/lib/stages/postgen/ruleEngine.js +116 -0
  107. package/lib/stages/pregen/envAudit.js +14 -3
  108. package/package.json +24 -21
  109. package/types/bin/hbom.d.ts +3 -0
  110. package/types/bin/hbom.d.ts.map +1 -0
  111. package/types/bin/repl.d.ts.map +1 -1
  112. package/types/lib/audit/index.d.ts +44 -0
  113. package/types/lib/audit/index.d.ts.map +1 -1
  114. package/types/lib/audit/reporters.d.ts +16 -0
  115. package/types/lib/audit/reporters.d.ts.map +1 -1
  116. package/types/lib/audit/targets.d.ts.map +1 -1
  117. package/types/lib/cli/index.d.ts +16 -0
  118. package/types/lib/cli/index.d.ts.map +1 -1
  119. package/types/lib/evinser/evinser.d.ts +4 -0
  120. package/types/lib/evinser/evinser.d.ts.map +1 -1
  121. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  122. package/types/lib/helpers/analyzer.d.ts +33 -0
  123. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  124. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  125. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  126. package/types/lib/helpers/asarutils.d.ts +34 -0
  127. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  128. package/types/lib/helpers/auditCategories.d.ts +5 -0
  129. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  130. package/types/lib/helpers/cbomutils.d.ts +3 -2
  131. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  132. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  133. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  134. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  135. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  136. package/types/lib/helpers/display.d.ts +1 -0
  137. package/types/lib/helpers/display.d.ts.map +1 -1
  138. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  139. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  141. package/types/lib/helpers/gtfobins.d.ts +8 -0
  142. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  143. package/types/lib/helpers/hbom.d.ts +49 -0
  144. package/types/lib/helpers/hbom.d.ts.map +1 -0
  145. package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
  146. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  147. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  148. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  149. package/types/lib/helpers/hostTopology.d.ts +12 -0
  150. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  151. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  152. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  153. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  154. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  155. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  156. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  157. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  158. package/types/lib/helpers/plugins.d.ts +58 -0
  159. package/types/lib/helpers/plugins.d.ts.map +1 -0
  160. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  161. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  162. package/types/lib/helpers/protobom.d.ts +3 -4
  163. package/types/lib/helpers/protobom.d.ts.map +1 -1
  164. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  165. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  166. package/types/lib/helpers/source.d.ts.map +1 -1
  167. package/types/lib/helpers/utils.d.ts +74 -8
  168. package/types/lib/helpers/utils.d.ts.map +1 -1
  169. package/types/lib/managers/binary.d.ts +5 -0
  170. package/types/lib/managers/binary.d.ts.map +1 -1
  171. package/types/lib/managers/docker.d.ts +3 -0
  172. package/types/lib/managers/docker.d.ts.map +1 -1
  173. package/types/lib/server/server.d.ts +2 -0
  174. package/types/lib/server/server.d.ts.map +1 -1
  175. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  176. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  177. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  178. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  179. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  180. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  182. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -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,29 +76,7 @@ const ALLOWED_PARAMS = [
76
76
 
77
77
  const app = connect();
78
78
 
79
- 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 allowHosts = process.env.CDXGEN_ALLOWED_HOSTS.split(",")
87
- .map((host) => host.trim())
88
- .filter(Boolean);
89
- for (const allowedHost of allowHosts) {
90
- if (hostname === allowedHost) {
91
- return true;
92
- }
93
- if (
94
- allowedHost.startsWith("*.") &&
95
- hostname.endsWith(allowedHost.slice(1))
96
- ) {
97
- return true;
98
- }
99
- }
100
- return false;
101
- }
79
+ export { isAllowedHttpHost };
102
80
 
103
81
  app.use(
104
82
  bodyParser.json({
@@ -12,7 +12,12 @@ import {
12
12
  validateAndRejectGitSource,
13
13
  } from "../helpers/source.js";
14
14
  import { isWin } from "../helpers/utils.js";
15
- import { getQueryParams, parseQueryString, parseValue } from "./server.js";
15
+ import {
16
+ getQueryParams,
17
+ isAllowedHttpHost,
18
+ parseQueryString,
19
+ parseValue,
20
+ } from "./server.js";
16
21
 
17
22
  function nullProtoObj(obj) {
18
23
  if (obj === null || typeof obj !== "object") {
@@ -171,6 +176,36 @@ describe("isAllowedHost()", () => {
171
176
  });
172
177
  });
173
178
 
179
+ describe("isAllowedHttpHost()", () => {
180
+ let originalAllowedHosts;
181
+
182
+ beforeEach(() => {
183
+ originalAllowedHosts = process.env.CDXGEN_ALLOWED_HOSTS;
184
+ });
185
+
186
+ afterEach(() => {
187
+ if (originalAllowedHosts === undefined) {
188
+ delete process.env.CDXGEN_ALLOWED_HOSTS;
189
+ } else {
190
+ process.env.CDXGEN_ALLOWED_HOSTS = originalAllowedHosts;
191
+ }
192
+ });
193
+
194
+ it("allows exact host matches", () => {
195
+ process.env.CDXGEN_ALLOWED_HOSTS = "dependencytrack.example.com";
196
+ assert.strictEqual(isAllowedHttpHost("dependencytrack.example.com"), true);
197
+ assert.strictEqual(isAllowedHttpHost("other.example.com"), false);
198
+ });
199
+
200
+ it("allows only real subdomains for wildcard entries", () => {
201
+ process.env.CDXGEN_ALLOWED_HOSTS = "*.example.com";
202
+ assert.strictEqual(isAllowedHttpHost("api.example.com"), true);
203
+ assert.strictEqual(isAllowedHttpHost("deep.api.example.com"), true);
204
+ assert.strictEqual(isAllowedHttpHost("example.com"), false);
205
+ assert.strictEqual(isAllowedHttpHost("evil-example.com"), false);
206
+ });
207
+ });
208
+
174
209
  describe("isAllowedPath()", () => {
175
210
  let originalPaths;
176
211
 
@@ -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
+ });
@@ -10,6 +10,7 @@ import {
10
10
  expandBomAuditCategories,
11
11
  validateBomAuditCategories,
12
12
  } from "../../helpers/auditCategories.js";
13
+ import { isHbomLikeBom as isHbomLikeBomDocument } from "../../helpers/hbomAnalysis.js";
13
14
  import { table } from "../../helpers/table.js";
14
15
  import {
15
16
  DEBUG_MODE,
@@ -21,17 +22,7 @@ import { evaluateRules, loadRules } from "./ruleEngine.js";
21
22
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
22
23
  const BUILTIN_RULES_DIR = join(__dirname, "..", "..", "..", "data", "rules");
23
24
 
24
- /**
25
- * Audit BOM formulation section using JSONata-powered rule engine
26
- * @param {Object} bomJson - Generated CycloneDX BOM
27
- * @param {Object} options - CLI options
28
- * @returns {Promise<Array>} Array of audit findings
29
- */
30
- export async function auditBom(bomJson, options) {
31
- if (!bomJson) {
32
- return [];
33
- }
34
- const findings = [];
25
+ async function loadConfiguredBomAuditRules(options = {}) {
35
26
  const rules = await loadRules(BUILTIN_RULES_DIR);
36
27
  if (options.bomAuditRulesDir && safeExistsSync(options.bomAuditRulesDir)) {
37
28
  const userRulesDir = resolve(options.bomAuditRulesDir);
@@ -41,11 +32,11 @@ export async function auditBom(bomJson, options) {
41
32
  }
42
33
  rules.push(...userRules);
43
34
  }
44
- if (rules.length === 0) {
45
- if (DEBUG_MODE) {
46
- console.log("No audit rules loaded; formulation audit skipped");
47
- }
48
- return findings;
35
+ if (!rules.length) {
36
+ return {
37
+ activeRules: [],
38
+ rules,
39
+ };
49
40
  }
50
41
  let activeRules = rules;
51
42
  if (options.bomAuditCategories) {
@@ -64,6 +55,107 @@ export async function auditBom(bomJson, options) {
64
55
  }
65
56
  }
66
57
  }
58
+ return {
59
+ activeRules,
60
+ rules,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Detect whether a BOM looks like an HBOM inventory.
66
+ *
67
+ * @param {object} bomJson CycloneDX BOM
68
+ * @returns {boolean} True when the BOM appears to represent hardware inventory
69
+ */
70
+ export function isHbomLikeBom(bomJson) {
71
+ return isHbomLikeBomDocument(bomJson);
72
+ }
73
+
74
+ /**
75
+ * Detect whether a BOM looks like an OBOM/runtime inventory.
76
+ *
77
+ * @param {object} bomJson CycloneDX BOM
78
+ * @returns {boolean} True when the BOM appears to represent operations/runtime data
79
+ */
80
+ export function isObomLikeBom(bomJson) {
81
+ if (!bomJson) {
82
+ return false;
83
+ }
84
+ if (isHbomLikeBom(bomJson)) {
85
+ return false;
86
+ }
87
+ if (
88
+ bomJson?.metadata?.component?.type === "operating-system" ||
89
+ bomJson?.metadata?.component?.type === "device"
90
+ ) {
91
+ return true;
92
+ }
93
+ if (
94
+ Array.isArray(bomJson?.metadata?.lifecycles) &&
95
+ bomJson.metadata.lifecycles.some(
96
+ (lifecycle) => lifecycle?.phase === "operations",
97
+ )
98
+ ) {
99
+ return true;
100
+ }
101
+ return (bomJson?.components || []).some((component) =>
102
+ (component?.properties || []).some(
103
+ (property) => property?.name === "cdx:osquery:category",
104
+ ),
105
+ );
106
+ }
107
+
108
+ function summarizeDryRunSupport(activeRules = []) {
109
+ const summary = {
110
+ fullCount: 0,
111
+ noCount: 0,
112
+ partialCount: 0,
113
+ totalRules: activeRules.length,
114
+ };
115
+ for (const rule of activeRules) {
116
+ if (rule?.dryRunSupport === "no") {
117
+ summary.noCount += 1;
118
+ continue;
119
+ }
120
+ if (rule?.dryRunSupport === "full") {
121
+ summary.fullCount += 1;
122
+ continue;
123
+ }
124
+ summary.partialCount += 1;
125
+ }
126
+ return summary;
127
+ }
128
+
129
+ export async function getBomAuditDryRunSupportSummary(options = {}) {
130
+ const { activeRules } = await loadConfiguredBomAuditRules(options);
131
+ return summarizeDryRunSupport(activeRules);
132
+ }
133
+
134
+ export function formatDryRunSupportSummary(summary) {
135
+ if (!summary) {
136
+ return "";
137
+ }
138
+ return `BOM audit dry-run summary: ${summary.noCount} rule(s) do not support dry-run, ${summary.partialCount} rule(s) have partial dry-run support, ${summary.totalRules} active rule(s) total.`;
139
+ }
140
+
141
+ /**
142
+ * Audit BOM formulation section using JSONata-powered rule engine
143
+ * @param {Object} bomJson - Generated CycloneDX BOM
144
+ * @param {Object} options - CLI options
145
+ * @returns {Promise<Array>} Array of audit findings
146
+ */
147
+ export async function auditBom(bomJson, options) {
148
+ if (!bomJson) {
149
+ return [];
150
+ }
151
+ const findings = [];
152
+ const { activeRules, rules } = await loadConfiguredBomAuditRules(options);
153
+ if (rules.length === 0) {
154
+ if (DEBUG_MODE) {
155
+ console.log("No audit rules loaded; formulation audit skipped");
156
+ }
157
+ return findings;
158
+ }
67
159
  const allFindings = await evaluateRules(activeRules, bomJson);
68
160
  if (options.bomAuditMinSeverity) {
69
161
  const minSeverity = options.bomAuditMinSeverity.toLowerCase();
@@ -87,7 +179,7 @@ export async function auditBom(bomJson, options) {
87
179
  /**
88
180
  * Format findings for console output with color-coded severity
89
181
  */
90
- export function formatConsoleOutput(findings) {
182
+ export function renderBomAuditConsoleReport(findings) {
91
183
  if (!findings?.length) {
92
184
  return "";
93
185
  }
@@ -119,7 +211,18 @@ export function formatConsoleOutput(findings) {
119
211
  line.push(f.location?.file || "");
120
212
  data.push(line);
121
213
  }
122
- console.log(table(data, config));
214
+ return table(data, config);
215
+ }
216
+
217
+ /**
218
+ * Format findings for console output with color-coded severity
219
+ */
220
+ export function formatConsoleOutput(findings) {
221
+ const output = renderBomAuditConsoleReport(findings);
222
+ if (output) {
223
+ console.log(output);
224
+ }
225
+ return output;
123
226
  }
124
227
 
125
228
  /**