@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
@@ -20,6 +20,7 @@ import {
20
20
  finalizeAuditReport,
21
21
  groupAuditResults,
22
22
  loadInputBoms,
23
+ runDirectBomAuditFromBoms,
23
24
  } from "./index.js";
24
25
  import {
25
26
  formatPredictiveAnnotations,
@@ -179,6 +180,7 @@ describe("runAuditFromBoms()", () => {
179
180
 
180
181
  assert.strictEqual(report.summary.totalTargets, 1);
181
182
  assert.deepStrictEqual(collectAuditTargetsStub.firstCall.args[1], {
183
+ allowlistFile: undefined,
182
184
  maxTargets: 50,
183
185
  prioritizeDirectRuntime: true,
184
186
  scope: "required",
@@ -192,6 +194,266 @@ describe("runAuditFromBoms()", () => {
192
194
  );
193
195
  assert.strictEqual(progressEvents[1].type, "run:start");
194
196
  });
197
+
198
+ it("supports dry-run predictive audit planning without cloning targets", async () => {
199
+ const collectAuditTargetsStub = sinon.stub().returns({
200
+ skipped: [],
201
+ stats: {
202
+ availableTargets: 1,
203
+ nonRequiredTargets: 0,
204
+ requiredTargets: 1,
205
+ trustedTargets: 0,
206
+ trustedTargetsExcluded: 0,
207
+ truncatedTargets: 0,
208
+ },
209
+ targets: [
210
+ {
211
+ name: "core",
212
+ namespace: "acme",
213
+ purl: "pkg:npm/acme/core@1.0.0",
214
+ required: true,
215
+ type: "npm",
216
+ version: "1.0.0",
217
+ },
218
+ ],
219
+ });
220
+ const enrichInputBomsWithRegistryMetadataStub = sinon.stub().resolves();
221
+ const recordActivityStub = sinon.stub();
222
+ const { runAuditFromBoms: mockedRunAuditFromBoms } = await esmock(
223
+ "./index.js",
224
+ {
225
+ "../cli/index.js": {
226
+ createBom: sinon.stub(),
227
+ },
228
+ "../helpers/bomUtils.js": {
229
+ getNonCycloneDxErrorMessage: sinon.stub(),
230
+ isCycloneDxBom: () => true,
231
+ },
232
+ "../helpers/logger.js": { thoughtLog: sinon.stub() },
233
+ "../helpers/provenanceUtils.js": {
234
+ hasRegistryProvenanceEvidenceProperties: () => false,
235
+ hasTrustedPublishingProperties: () => false,
236
+ },
237
+ "../helpers/source.js": {
238
+ cleanupSourceDir: sinon.stub(),
239
+ findGitRefForPurlVersion: sinon.stub().returns(undefined),
240
+ hardenedGitCommand: sinon.stub(),
241
+ resolveGitUrlFromPurl: sinon.stub(),
242
+ resolvePurlSourceDirectory: sinon.stub(),
243
+ sanitizeRemoteUrlForLogs: (value) => value,
244
+ },
245
+ "../helpers/utils.js": {
246
+ dirNameStr: path.resolve("."),
247
+ getTmpDir: () => os.tmpdir(),
248
+ isDryRun: true,
249
+ recordActivity: recordActivityStub,
250
+ safeExistsSync: (filePath) => existsSync(filePath),
251
+ safeMkdirSync: (filePath, options) => mkdirSync(filePath, options),
252
+ safeMkdtempSync: sinon.stub(),
253
+ safeRmSync: sinon.stub(),
254
+ safeWriteSync: sinon.stub(),
255
+ },
256
+ "../stages/postgen/auditBom.js": {
257
+ auditBom: sinon.stub().resolves([]),
258
+ },
259
+ "../stages/postgen/postgen.js": {
260
+ postProcess: sinon.stub().callsFake((bomNSData) => bomNSData),
261
+ },
262
+ "./targets.js": {
263
+ collectAuditTargets: collectAuditTargetsStub,
264
+ enrichInputBomsWithRegistryMetadata:
265
+ enrichInputBomsWithRegistryMetadataStub,
266
+ normalizePackageName: (value) =>
267
+ (value || "").toLowerCase().replace(/[-_.]+/g, "-"),
268
+ },
269
+ },
270
+ );
271
+
272
+ const report = await mockedRunAuditFromBoms(
273
+ [
274
+ {
275
+ bomJson: {
276
+ bomFormat: "CycloneDX",
277
+ components: [],
278
+ specVersion: "1.7",
279
+ version: 1,
280
+ },
281
+ source: "bom.json",
282
+ },
283
+ ],
284
+ {},
285
+ );
286
+
287
+ assert.strictEqual(report.dryRun, true);
288
+ assert.strictEqual(report.summary.predictiveDryRun, true);
289
+ assert.strictEqual(report.summary.totalTargets, 1);
290
+ assert.strictEqual(report.summary.scannedTargets, 0);
291
+ assert.strictEqual(report.summary.skippedTargets, 1);
292
+ assert.strictEqual(report.results[0].status, "skipped");
293
+ assert.match(
294
+ report.results[0].assessment.reasons[0],
295
+ /skipped registry metadata fetches/i,
296
+ );
297
+ sinon.assert.notCalled(enrichInputBomsWithRegistryMetadataStub);
298
+ sinon.assert.calledWithMatch(recordActivityStub, {
299
+ kind: "audit",
300
+ reason: sinon.match(/skipped registry metadata fetches/i),
301
+ target: "predictive-dependency-audit",
302
+ });
303
+ });
304
+ });
305
+
306
+ describe("runDirectBomAuditFromBoms()", () => {
307
+ it("throws when no BOM inputs are provided", async () => {
308
+ await assert.rejects(
309
+ runDirectBomAuditFromBoms([], {}),
310
+ /No CycloneDX BOM inputs were found/,
311
+ );
312
+ });
313
+
314
+ it("defaults OBOM-like saved BOMs to the obom-runtime rule category", async () => {
315
+ const auditBomStub = sinon.stub().resolves([
316
+ {
317
+ category: "obom-runtime",
318
+ message: "Mount '/dev/shm' is missing noexec.",
319
+ ruleId: "OBOM-LNX-018",
320
+ severity: "high",
321
+ },
322
+ ]);
323
+ const { runDirectBomAuditFromBoms: mockedRunDirectBomAuditFromBoms } =
324
+ await esmock("./index.js", {
325
+ "../stages/postgen/auditBom.js": {
326
+ auditBom: auditBomStub,
327
+ isObomLikeBom: () => true,
328
+ },
329
+ });
330
+
331
+ const report = await mockedRunDirectBomAuditFromBoms(
332
+ [
333
+ {
334
+ bomJson: {
335
+ bomFormat: "CycloneDX",
336
+ metadata: {
337
+ lifecycles: [{ phase: "operations" }],
338
+ },
339
+ specVersion: "1.7",
340
+ version: 1,
341
+ },
342
+ source: "saved-obom.json",
343
+ },
344
+ ],
345
+ {
346
+ minSeverity: "low",
347
+ },
348
+ );
349
+
350
+ assert.strictEqual(auditBomStub.callCount, 1);
351
+ assert.deepStrictEqual(auditBomStub.firstCall.args[1], {
352
+ bomAuditCategories: "obom-runtime",
353
+ bomAuditMinSeverity: "low",
354
+ bomAuditRulesDir: undefined,
355
+ });
356
+ assert.strictEqual(report.auditMode, "direct");
357
+ assert.strictEqual(report.summary.totalFindings, 1);
358
+ assert.strictEqual(report.summary.findingsBySeverity.high, 1);
359
+ });
360
+
361
+ it("defaults HBOM-like saved BOMs to the HBOM rule categories", async () => {
362
+ const auditBomStub = sinon.stub().resolves([
363
+ {
364
+ category: "hbom-security",
365
+ message: "Storage component 'Main SSD' is reported as unencrypted",
366
+ ruleId: "HBS-001",
367
+ severity: "high",
368
+ },
369
+ ]);
370
+ const { runDirectBomAuditFromBoms: mockedRunDirectBomAuditFromBoms } =
371
+ await esmock("./index.js", {
372
+ "../stages/postgen/auditBom.js": {
373
+ auditBom: auditBomStub,
374
+ isHbomLikeBom: () => true,
375
+ isObomLikeBom: () => false,
376
+ },
377
+ });
378
+
379
+ const report = await mockedRunDirectBomAuditFromBoms(
380
+ [
381
+ {
382
+ bomJson: {
383
+ bomFormat: "CycloneDX",
384
+ metadata: {
385
+ component: {
386
+ properties: [
387
+ {
388
+ name: "cdx:hbom:platform",
389
+ value: "darwin",
390
+ },
391
+ ],
392
+ type: "device",
393
+ },
394
+ },
395
+ properties: [
396
+ {
397
+ name: "cdx:hbom:collectorProfile",
398
+ value: "darwin-arm64",
399
+ },
400
+ ],
401
+ specVersion: "1.7",
402
+ version: 1,
403
+ },
404
+ source: "saved-hbom.json",
405
+ },
406
+ ],
407
+ {
408
+ minSeverity: "low",
409
+ },
410
+ );
411
+
412
+ assert.strictEqual(auditBomStub.callCount, 1);
413
+ assert.deepStrictEqual(auditBomStub.firstCall.args[1], {
414
+ bomAuditCategories: "hbom-security,hbom-performance,hbom-compliance",
415
+ bomAuditMinSeverity: "low",
416
+ bomAuditRulesDir: undefined,
417
+ });
418
+ assert.strictEqual(report.auditMode, "direct");
419
+ assert.strictEqual(report.summary.totalFindings, 1);
420
+ assert.strictEqual(report.summary.findingsBySeverity.high, 1);
421
+ });
422
+
423
+ it("honors explicit categories and custom rules for direct BOM audit", async () => {
424
+ const auditBomStub = sinon.stub().resolves([]);
425
+ const { runDirectBomAuditFromBoms: mockedRunDirectBomAuditFromBoms } =
426
+ await esmock("./index.js", {
427
+ "../stages/postgen/auditBom.js": {
428
+ auditBom: auditBomStub,
429
+ isObomLikeBom: () => false,
430
+ },
431
+ });
432
+
433
+ await mockedRunDirectBomAuditFromBoms(
434
+ [
435
+ {
436
+ bomJson: {
437
+ bomFormat: "CycloneDX",
438
+ specVersion: "1.7",
439
+ version: 1,
440
+ },
441
+ source: "saved-bom.json",
442
+ },
443
+ ],
444
+ {
445
+ categories: ["obom-runtime", "rootfs-hardening"],
446
+ minSeverity: "medium",
447
+ rulesDir: "/tmp/custom-rules",
448
+ },
449
+ );
450
+
451
+ assert.deepStrictEqual(auditBomStub.firstCall.args[1], {
452
+ bomAuditCategories: "obom-runtime,rootfs-hardening",
453
+ bomAuditMinSeverity: "medium",
454
+ bomAuditRulesDir: "/tmp/custom-rules",
455
+ });
456
+ });
195
457
  });
196
458
 
197
459
  describe("finalizeAuditReport()", () => {
@@ -426,6 +688,49 @@ describe("finalizeAuditReport()", () => {
426
688
  );
427
689
  });
428
690
 
691
+ it("returns exit code 3 for direct BOM audit findings at the fail threshold", () => {
692
+ const finalized = finalizeAuditReport(
693
+ {
694
+ auditMode: "direct",
695
+ inputs: ["saved-obom.json"],
696
+ results: [
697
+ {
698
+ findings: [
699
+ {
700
+ description: "Temporary mounts should carry noexec.",
701
+ message: "Mount '/dev/shm' is missing noexec.",
702
+ ruleId: "OBOM-LNX-018",
703
+ severity: "high",
704
+ },
705
+ ],
706
+ source: "saved-obom.json",
707
+ },
708
+ ],
709
+ summary: {
710
+ bomsWithFindings: 1,
711
+ findingsBySeverity: {
712
+ critical: 0,
713
+ high: 1,
714
+ low: 0,
715
+ medium: 0,
716
+ },
717
+ inputBomCount: 1,
718
+ maxSeverity: "high",
719
+ totalFindings: 1,
720
+ },
721
+ },
722
+ {
723
+ failSeverity: "high",
724
+ minSeverity: "low",
725
+ report: "console",
726
+ },
727
+ );
728
+
729
+ assert.strictEqual(finalized.exitCode, 3);
730
+ assert.match(finalized.output, /\/dev\/shm/);
731
+ assert.match(finalized.output, /Mount '\/dev\/shm' is missing noexec/);
732
+ });
733
+
429
734
  it("includes synthetic SARIF results when a target fails before findings are produced", () => {
430
735
  const rendered = renderAuditReport(
431
736
  "sarif",
@@ -689,6 +994,33 @@ describe("finalizeAuditReport()", () => {
689
994
  assert.match(rendered, /No dependencies require your attention right now/);
690
995
  assert.match(rendered, /configured severity threshold \('low'\)/);
691
996
  });
997
+
998
+ it("explains predictive audit planning limits in dry-run mode", () => {
999
+ const rendered = renderConsoleReport(
1000
+ {
1001
+ dryRun: true,
1002
+ results: [],
1003
+ summary: {
1004
+ erroredTargets: 0,
1005
+ groupedResultCount: 0,
1006
+ inputBomCount: 1,
1007
+ predictiveDryRun: true,
1008
+ scannedTargets: 0,
1009
+ skippedTargets: 2,
1010
+ totalTargets: 2,
1011
+ },
1012
+ },
1013
+ {
1014
+ minSeverity: "low",
1015
+ },
1016
+ );
1017
+
1018
+ assert.match(
1019
+ rendered,
1020
+ /Dry-run mode only planned predictive audit targets/i,
1021
+ );
1022
+ assert.match(rendered, /Re-run without --dry-run/i);
1023
+ });
692
1024
  });
693
1025
 
694
1026
  describe("groupAuditResults()", () => {
@@ -1,6 +1,7 @@
1
1
  import { buildAnnotationText } from "../helpers/annotationFormatter.js";
2
2
  import { table } from "../helpers/table.js";
3
3
  import { getTimestamp } from "../helpers/utils.js";
4
+ import { renderBomAuditConsoleReport } from "../stages/postgen/auditBom.js";
4
5
  import { severityMeetsThreshold } from "./scoring.js";
5
6
 
6
7
  const SARIF_VERSION = "2.1.0";
@@ -21,6 +22,18 @@ function filterResults(results, minSeverity) {
21
22
  );
22
23
  }
23
24
 
25
+ function filterDirectFindingEntries(report, minSeverity) {
26
+ const entries = [];
27
+ for (const result of report?.results || []) {
28
+ for (const finding of result?.findings || []) {
29
+ if (severityMeetsThreshold(finding?.severity || "none", minSeverity)) {
30
+ entries.push({ finding, result });
31
+ }
32
+ }
33
+ }
34
+ return entries;
35
+ }
36
+
24
37
  function effectiveResults(report) {
25
38
  return report.groupedResults?.length
26
39
  ? report.groupedResults
@@ -50,6 +63,97 @@ function severityToSarifLevel(severity) {
50
63
  }
51
64
  }
52
65
 
66
+ function directBomFindingLocations(finding, result) {
67
+ const bomRef =
68
+ finding?.location?.bomRef ||
69
+ finding?.location?.purl ||
70
+ result?.serialNumber ||
71
+ result?.source;
72
+ if (finding?.location?.file) {
73
+ return [
74
+ {
75
+ physicalLocation: {
76
+ artifactLocation: {
77
+ uri: finding.location.file,
78
+ },
79
+ },
80
+ logicalLocations: bomRef
81
+ ? [{ fullyQualifiedName: bomRef, kind: "package" }]
82
+ : undefined,
83
+ },
84
+ ];
85
+ }
86
+ if (bomRef) {
87
+ return [
88
+ {
89
+ logicalLocations: [{ fullyQualifiedName: bomRef, kind: "package" }],
90
+ },
91
+ ];
92
+ }
93
+ return [
94
+ {
95
+ logicalLocations: [{ fullyQualifiedName: "cdx-audit", kind: "tool" }],
96
+ },
97
+ ];
98
+ }
99
+
100
+ function deriveDirectBomSarifRules(entries) {
101
+ const rulesById = new Map();
102
+ for (const { finding } of entries) {
103
+ if (rulesById.has(finding.ruleId)) {
104
+ continue;
105
+ }
106
+ rulesById.set(finding.ruleId, {
107
+ id: finding.ruleId,
108
+ name: finding.name || finding.ruleId,
109
+ shortDescription: {
110
+ text: finding.name || finding.ruleId,
111
+ },
112
+ fullDescription: {
113
+ text: finding.description || finding.name || finding.ruleId,
114
+ },
115
+ defaultConfiguration: {
116
+ level: severityToSarifLevel(finding.severity),
117
+ },
118
+ help: finding.mitigation
119
+ ? {
120
+ markdown: `**Remediation:** ${finding.mitigation}`,
121
+ text: finding.mitigation,
122
+ }
123
+ : undefined,
124
+ properties: {
125
+ attackTactics: finding.attackTactics,
126
+ attackTechniques: finding.attackTechniques,
127
+ category: finding.category,
128
+ engine: "cdx-audit-direct-bom",
129
+ tags: attackTags(finding),
130
+ },
131
+ });
132
+ }
133
+ return [...rulesById.values()];
134
+ }
135
+
136
+ function directBomFindingToSarifResult(finding, result) {
137
+ return {
138
+ level: severityToSarifLevel(finding?.severity),
139
+ locations: directBomFindingLocations(finding, result),
140
+ message: {
141
+ text: finding?.message || finding?.description || finding?.ruleId,
142
+ },
143
+ properties: {
144
+ attackTactics: finding?.attackTactics,
145
+ attackTechniques: finding?.attackTechniques,
146
+ category: finding?.category,
147
+ description: finding?.description,
148
+ evidence: finding?.evidence,
149
+ inputBom: result?.source,
150
+ mitigation: finding?.mitigation,
151
+ severity: finding?.severity,
152
+ },
153
+ ruleId: finding?.ruleId || AUDIT_ERROR_RULE_ID,
154
+ };
155
+ }
156
+
53
157
  function splitCsv(value) {
54
158
  return String(value || "")
55
159
  .split(",")
@@ -481,6 +585,105 @@ export function renderJsonReport(report) {
481
585
  return `${JSON.stringify(report, null, 2)}\n`;
482
586
  }
483
587
 
588
+ /**
589
+ * Render a direct BOM audit report for terminal output.
590
+ *
591
+ * @param {object} report aggregate direct audit report
592
+ * @param {object} options render options
593
+ * @returns {string} console report text
594
+ */
595
+ export function renderDirectBomConsoleReport(report, options = {}) {
596
+ const minSeverity = options.minSeverity || "low";
597
+ const visibleResults = (report?.results || [])
598
+ .map((result) => ({
599
+ ...result,
600
+ findings: (result.findings || []).filter((finding) =>
601
+ severityMeetsThreshold(finding?.severity || "none", minSeverity),
602
+ ),
603
+ }))
604
+ .filter((result) => result.findings.length > 0);
605
+ if (report?.results?.length === 1) {
606
+ if (visibleResults.length > 0) {
607
+ return `${renderBomAuditConsoleReport(visibleResults[0].findings)}\n`;
608
+ }
609
+ return [
610
+ "cdx-audit — direct BOM policy audit",
611
+ "",
612
+ `Input BOMs: ${report?.summary?.inputBomCount || 0}`,
613
+ `Findings: ${report?.summary?.totalFindings || 0}`,
614
+ "",
615
+ `No direct BOM findings met or exceeded the configured severity threshold ('${minSeverity}').`,
616
+ ]
617
+ .join("\n")
618
+ .concat("\n");
619
+ }
620
+ const lines = [];
621
+ lines.push("cdx-audit — direct BOM policy audit");
622
+ lines.push("");
623
+ lines.push(`Input BOMs: ${report?.summary?.inputBomCount || 0}`);
624
+ lines.push(`BOMs with findings: ${report?.summary?.bomsWithFindings || 0}`);
625
+ lines.push(`Findings: ${report?.summary?.totalFindings || 0}`);
626
+ lines.push("");
627
+ if (!visibleResults.length) {
628
+ lines.push(
629
+ `No direct BOM findings met or exceeded the configured severity threshold ('${minSeverity}').`,
630
+ );
631
+ return `${lines.join("\n")}\n`;
632
+ }
633
+ for (const result of visibleResults) {
634
+ lines.push(`Input BOM: ${result.source}`);
635
+ lines.push(renderBomAuditConsoleReport(result.findings));
636
+ lines.push("");
637
+ }
638
+ return `${lines.join("\n")}\n`;
639
+ }
640
+
641
+ /**
642
+ * Render a direct BOM audit report as SARIF 2.1.0 output.
643
+ *
644
+ * @param {object} report aggregate direct audit report
645
+ * @param {object} [options] render options
646
+ * @returns {string} SARIF output
647
+ */
648
+ export function renderDirectBomSarifReport(report, options = {}) {
649
+ const minSeverity = options.minSeverity || "low";
650
+ const entries = filterDirectFindingEntries(report, minSeverity);
651
+ const sarifResults = entries.map(({ finding, result }) =>
652
+ directBomFindingToSarifResult(finding, result),
653
+ );
654
+ const toolName = report?.tool?.name || "cdx-audit";
655
+ const toolVersion = report?.tool?.version || "v12";
656
+ const log = {
657
+ $schema: SARIF_SCHEMA,
658
+ version: SARIF_VERSION,
659
+ runs: [
660
+ {
661
+ tool: {
662
+ driver: {
663
+ informationUri: "https://cdxgen.github.io/cdxgen/",
664
+ name: toolName,
665
+ rules: deriveDirectBomSarifRules(entries),
666
+ version: toolVersion,
667
+ },
668
+ },
669
+ invocations: [
670
+ {
671
+ executionSuccessful: true,
672
+ },
673
+ ],
674
+ properties: {
675
+ auditMode: report?.auditMode,
676
+ generatedAt: report?.generatedAt,
677
+ inputs: report?.inputs || [],
678
+ summary: report?.summary,
679
+ },
680
+ results: sarifResults,
681
+ },
682
+ ],
683
+ };
684
+ return `${JSON.stringify(log, null, 2)}\n`;
685
+ }
686
+
484
687
  /**
485
688
  * Render an audit report for terminal output.
486
689
  *
@@ -514,6 +717,16 @@ export function renderConsoleReport(report, options = {}) {
514
717
  lines.push(
515
718
  `No predictive findings met or exceeded the configured severity threshold ('${minSeverity}').`,
516
719
  );
720
+ if (report.summary?.predictiveDryRun) {
721
+ lines.push(
722
+ "Dry-run mode only planned predictive audit targets. Registry metadata fetches, upstream repository cloning, and child SBOM generation were intentionally skipped.",
723
+ );
724
+ if (report.summary.totalTargets > 0) {
725
+ lines.push(
726
+ "Re-run without --dry-run to analyze the planned targets with the predictive dependency audit.",
727
+ );
728
+ }
729
+ }
517
730
  if (report.summary.erroredTargets > 0) {
518
731
  lines.push(
519
732
  "Some targets could not be fully analyzed, so review the recorded analysis errors before treating this rollup as complete.",
@@ -546,6 +759,15 @@ export function renderConsoleReport(report, options = {}) {
546
759
  * @returns {string} rendered report
547
760
  */
548
761
  export function renderAuditReport(reportType, report, options = {}) {
762
+ if (report?.auditMode === "direct") {
763
+ if ((reportType || "console") === "json") {
764
+ return renderJsonReport(report);
765
+ }
766
+ if ((reportType || "console") === "sarif") {
767
+ return renderDirectBomSarifReport(report, options);
768
+ }
769
+ return renderDirectBomConsoleReport(report, options);
770
+ }
549
771
  if ((reportType || "console") === "json") {
550
772
  return renderJsonReport(report);
551
773
  }