@cdxgen/cdxgen 13.0.0 → 13.1.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 (170) hide show
  1. package/README.md +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +140 -8
  4. package/bin/convert.js +7 -0
  5. package/bin/evinse.js +1 -1
  6. package/bin/hbom.js +13 -4
  7. package/bin/tracebom.js +9 -1
  8. package/data/component-tags.json +8 -1
  9. package/data/frameworks-list.json +86 -1
  10. package/data/remediations.json +1144 -0
  11. package/data/rules/asar-archives.yaml +1 -1
  12. package/data/rules/build-fidelity.yaml +782 -0
  13. package/data/rules/package-integrity.yaml +13 -13
  14. package/lib/audit/index.js +48 -4
  15. package/lib/audit/scoring.js +2 -8
  16. package/lib/cli/bomAssembly.js +83 -15
  17. package/lib/cli/cliOptions.js +124 -7
  18. package/lib/cli/index.js +76 -48
  19. package/lib/cli/jsBom.js +32 -3
  20. package/lib/cli/jvmBom.js +1494 -1315
  21. package/lib/cli/managedBom.js +97 -28
  22. package/lib/cli/nativeBom.js +189 -10
  23. package/lib/core/buildLedger.js +799 -0
  24. package/lib/core/deferredExit.js +141 -0
  25. package/lib/core/env.js +69 -1
  26. package/lib/core/fs.js +60 -0
  27. package/lib/core/logger.js +76 -25
  28. package/lib/core/severity.js +16 -0
  29. package/lib/core/syncFileWriter.js +106 -0
  30. package/lib/core/ui.js +46 -2
  31. package/lib/ecosystems/ecosystems.js +9 -0
  32. package/lib/ecosystems/gradleutils.js +355 -15
  33. package/lib/ecosystems/parsers-js.js +26 -0
  34. package/lib/ecosystems/parsers-jvm.js +9 -0
  35. package/lib/ecosystems/parsers-misc.js +16 -2
  36. package/lib/ecosystems/parsers-python.js +171 -35
  37. package/lib/ecosystems/parsers-rust.js +12 -2
  38. package/lib/ecosystems/pypiClassifiers.js +326 -0
  39. package/lib/ecosystems/pythonutils.js +49 -1
  40. package/lib/ecosystems/rubyutils.js +13 -0
  41. package/lib/ecosystems/utils.js +9 -1
  42. package/lib/evinser/evinser.js +30 -12
  43. package/lib/helpers/remote/dependency-track.js +81 -14
  44. package/lib/inventory/auditCategories.js +9 -0
  45. package/lib/inventory/depsUtils.js +72 -0
  46. package/lib/inventory/dosai.js +431 -22
  47. package/lib/inventory/envcontext.js +211 -19
  48. package/lib/inventory/fetchBatch.js +57 -0
  49. package/lib/inventory/jvmToolEnv.js +593 -0
  50. package/lib/inventory/osinfo.js +123 -16
  51. package/lib/inventory/table.js +9 -5
  52. package/lib/inventory/toolRequirements.js +903 -0
  53. package/lib/managers/binary.js +216 -40
  54. package/lib/managers/docker.js +26 -5
  55. package/lib/server/server.js +1 -1
  56. package/lib/stages/postgen/annotationBuilder.js +97 -0
  57. package/lib/stages/postgen/auditBom.js +76 -79
  58. package/lib/stages/postgen/introspection/annotate.js +320 -0
  59. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  60. package/lib/stages/postgen/introspection/emit.js +192 -0
  61. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  62. package/lib/stages/postgen/introspection/markers.js +227 -0
  63. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  64. package/lib/stages/postgen/introspection/report.js +1208 -0
  65. package/lib/stages/postgen/introspection/score.js +1067 -0
  66. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  67. package/lib/stages/postgen/parentEdge.js +156 -0
  68. package/lib/stages/postgen/postgen.js +115 -2
  69. package/lib/stages/postgen/ruleEngine.js +17 -0
  70. package/lib/stages/pregen/envAudit.js +42 -2
  71. package/lib/stages/pregen/pregen.js +1171 -6
  72. package/lib/validator/bomValidator.js +11 -2
  73. package/package.json +14 -14
  74. package/types/lib/audit/index.d.ts.map +1 -1
  75. package/types/lib/audit/scoring.d.ts +2 -8
  76. package/types/lib/audit/scoring.d.ts.map +1 -1
  77. package/types/lib/cli/bomAssembly.d.ts +14 -0
  78. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  79. package/types/lib/cli/cliOptions.d.ts +2 -1
  80. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  81. package/types/lib/cli/index.d.ts +8 -0
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/cli/jsBom.d.ts.map +1 -1
  84. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  85. package/types/lib/cli/managedBom.d.ts.map +1 -1
  86. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  87. package/types/lib/core/buildLedger.d.ts +272 -0
  88. package/types/lib/core/buildLedger.d.ts.map +1 -0
  89. package/types/lib/core/deferredExit.d.ts +84 -0
  90. package/types/lib/core/deferredExit.d.ts.map +1 -0
  91. package/types/lib/core/env.d.ts +22 -1
  92. package/types/lib/core/env.d.ts.map +1 -1
  93. package/types/lib/core/fs.d.ts +21 -0
  94. package/types/lib/core/fs.d.ts.map +1 -1
  95. package/types/lib/core/logger.d.ts +14 -1
  96. package/types/lib/core/logger.d.ts.map +1 -1
  97. package/types/lib/core/severity.d.ts +11 -0
  98. package/types/lib/core/severity.d.ts.map +1 -0
  99. package/types/lib/core/syncFileWriter.d.ts +33 -0
  100. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  101. package/types/lib/core/ui.d.ts +1 -1
  102. package/types/lib/core/ui.d.ts.map +1 -1
  103. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  104. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  105. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  109. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  110. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  111. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  112. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  113. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  114. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  115. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  116. package/types/lib/ecosystems/utils.d.ts +4 -4
  117. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  118. package/types/lib/evinser/evinser.d.ts +5 -1
  119. package/types/lib/evinser/evinser.d.ts.map +1 -1
  120. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  121. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  122. package/types/lib/inventory/auditCategories.d.ts +8 -0
  123. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  125. package/types/lib/inventory/dosai.d.ts +20 -0
  126. package/types/lib/inventory/dosai.d.ts.map +1 -1
  127. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  128. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  129. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  130. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  131. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  132. package/types/lib/inventory/osinfo.d.ts +83 -2
  133. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  134. package/types/lib/inventory/table.d.ts +4 -1
  135. package/types/lib/inventory/table.d.ts.map +1 -1
  136. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  137. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  138. package/types/lib/managers/binary.d.ts +71 -0
  139. package/types/lib/managers/binary.d.ts.map +1 -1
  140. package/types/lib/managers/docker.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  142. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  144. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  145. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  146. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  147. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  148. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  149. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  150. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  151. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  152. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  153. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  154. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  155. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  156. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  157. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  158. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  159. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  160. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  161. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  162. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  163. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  164. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  165. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  166. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  167. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  168. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  169. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  170. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -10,6 +10,14 @@ import {
10
10
  isSecureMode,
11
11
  readEnvironmentVariable,
12
12
  } from "../core/activity.js";
13
+ import {
14
+ commandOutputText,
15
+ isLedgerEnabled,
16
+ LEDGER_EVENT_IMPACTS,
17
+ LEDGER_EVENT_KINDS,
18
+ recordDegradation,
19
+ recordLedgerEvent,
20
+ } from "../core/buildLedger.js";
13
21
  import {
14
22
  CARGO_CMD,
15
23
  DOTNET_CMD,
@@ -31,8 +39,14 @@ import {
31
39
  safeExistsSync,
32
40
  safeSpawnSync,
33
41
  } from "../core/fs.js";
42
+ import { diagnosticStream } from "../core/logger.js";
34
43
  import { dirNameStr, isMac, isWin } from "../core/paths.js";
44
+ import { isValidSdkmanVersion } from "./jvmToolEnv.js";
35
45
  import { applyPurl, tryBuildPurl } from "./purl.js";
46
+ import {
47
+ classifySpawnRestriction,
48
+ describeSpawnRestriction,
49
+ } from "./toolRequirements.js";
36
50
 
37
51
  /**
38
52
  * Git executable name used for all git invocations, overridable via GIT_CMD.
@@ -204,12 +218,17 @@ export function execGitCommand(dir, args) {
204
218
  // Prepend hardening `-c` overrides before the subcommand and run git with a
205
219
  // hardened environment so a scanned/untrusted repo cannot execute hooks,
206
220
  // fsmonitor commands, or config-driven scripts.
221
+ // git is a real executable, so Windows can spawn it without a shell. Doing
222
+ // so keeps the pretty-format arguments intact: `%H`, `%n` and `%x1e` are
223
+ // cmd.exe variable-expansion syntax, which the shell-execution guard rightly
224
+ // refuses to pass through a shell.
207
225
  return getCommandOutput(
208
226
  GIT_COMMAND,
209
227
  dir,
210
228
  [...GIT_HARDENING_CONFIG_ARGS, ...args],
211
229
  {
212
230
  env: hardenedGitEnv(),
231
+ shell: false,
213
232
  },
214
233
  );
215
234
  }
@@ -291,6 +310,60 @@ export function gitLogTrailers(dir, maxCount = 20) {
291
310
  return commits;
292
311
  }
293
312
 
313
+ /**
314
+ * Record a successful version probe with the version the tool reported.
315
+ *
316
+ * @param {string} tool Tool name
317
+ * @param {string} ecosystem Ecosystem the tool belongs to
318
+ * @param {string} found Version string reported by the tool
319
+ */
320
+ function recordToolProbeResolved(tool, ecosystem, found) {
321
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
322
+ ecosystem,
323
+ tool,
324
+ found,
325
+ source: "PATH",
326
+ });
327
+ }
328
+
329
+ /**
330
+ * Record a failed version probe. A probe the environment did not let run is
331
+ * degraded evidence — the tool may well be present — while only a probe that
332
+ * ran and found nothing counts as a missing tool.
333
+ *
334
+ * @param {string} tool Tool name
335
+ * @param {string} ecosystem Ecosystem the tool belongs to
336
+ * @param {string} command Probe command that failed
337
+ */
338
+ function recordToolProbeFailure(tool, ecosystem, command) {
339
+ if (!isLedgerEnabled()) {
340
+ return;
341
+ }
342
+ const restriction = describeSpawnRestriction(command);
343
+ if (restriction) {
344
+ // A dry-run block is the user's own request, so it costs nothing; any
345
+ // other policy restriction hides a version the tool may well have.
346
+ const isDryRunBlock = classifySpawnRestriction(command) === "dry-run";
347
+ recordLedgerEvent(LEDGER_EVENT_KINDS.EVIDENCE_DEGRADED, {
348
+ ecosystem,
349
+ tool,
350
+ source: "PATH",
351
+ remediationId: isDryRunBlock ? "policy.dry-run" : "policy.secure-mode",
352
+ impact: isDryRunBlock
353
+ ? LEDGER_EVENT_IMPACTS.NONE
354
+ : LEDGER_EVENT_IMPACTS.VERSIONS,
355
+ detail: `The ${tool} version probe could not run: ${restriction}.`,
356
+ });
357
+ } else {
358
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
359
+ ecosystem,
360
+ tool,
361
+ source: "PATH",
362
+ detail: `No ${tool} command answered the version probe.`,
363
+ });
364
+ }
365
+ }
366
+
294
367
  /**
295
368
  * Collect Java version and installed modules
296
369
  *
@@ -302,7 +375,7 @@ export function collectJavaInfo(dir) {
302
375
  const moduleDesc =
303
376
  getCommandOutput(getJavaCommand(), dir, ["--list-modules"]) || "";
304
377
  if (versionDesc) {
305
- return {
378
+ const info = {
306
379
  type: "platform",
307
380
  name: "java",
308
381
  version: versionDesc.split("\n")[0].replace("java ", ""),
@@ -314,7 +387,10 @@ export function collectJavaInfo(dir) {
314
387
  },
315
388
  ],
316
389
  };
390
+ recordToolProbeResolved("java", "java", info.version);
391
+ return info;
317
392
  }
393
+ recordToolProbeFailure("java", "java", getJavaCommand());
318
394
  return undefined;
319
395
  }
320
396
 
@@ -329,13 +405,16 @@ export function collectDotnetInfo(dir) {
329
405
  const moduleDesc =
330
406
  getCommandOutput(DOTNET_CMD, dir, ["--list-runtimes"]) || "";
331
407
  if (versionDesc) {
332
- return {
408
+ const info = {
333
409
  type: "platform",
334
410
  name: "dotnet",
335
411
  version: versionDesc.trim(),
336
412
  description: moduleDesc.replaceAll("\n", "\\n"),
337
413
  };
414
+ recordToolProbeResolved("dotnet", "csharp", info.version);
415
+ return info;
338
416
  }
417
+ recordToolProbeFailure("dotnet", "csharp", DOTNET_CMD);
339
418
  return undefined;
340
419
  }
341
420
 
@@ -346,25 +425,21 @@ export function collectDotnetInfo(dir) {
346
425
  * @returns Object containing python details
347
426
  */
348
427
  export function collectPythonInfo(dir) {
349
- const versionDesc = getCommandOutput(getPythonCommand(), dir, [
350
- "-S",
351
- "--version",
352
- ]);
428
+ const pythonCmd = getPythonCommand();
429
+ const versionDesc = getCommandOutput(pythonCmd, dir, ["-S", "--version"]);
353
430
  const moduleDesc =
354
- getCommandOutput(getPythonCommand(), dir, [
355
- "-I",
356
- "-m",
357
- "pip",
358
- "--version",
359
- ]) || "";
431
+ getCommandOutput(pythonCmd, dir, ["-I", "-m", "pip", "--version"]) || "";
360
432
  if (versionDesc) {
361
- return {
433
+ const info = {
362
434
  type: "platform",
363
435
  name: "python",
364
436
  version: versionDesc.replace("Python ", ""),
365
437
  description: moduleDesc.replaceAll("\n", "\\n"),
366
438
  };
439
+ recordToolProbeResolved("python", "python", info.version);
440
+ return info;
367
441
  }
442
+ recordToolProbeFailure("python", "python", pythonCmd);
368
443
  return undefined;
369
444
  }
370
445
 
@@ -385,6 +460,9 @@ export function collectNodeInfo(dir) {
385
460
  const moduleDesc = getCommandOutput(NPM_CMD, dir, ["--version"]);
386
461
  if (moduleDesc) {
387
462
  nodeInfo.description = `npm: ${moduleDesc}`;
463
+ recordToolProbeResolved("npm", "npm", moduleDesc);
464
+ } else {
465
+ recordToolProbeFailure("npm", "npm", NPM_CMD);
388
466
  }
389
467
  return nodeInfo;
390
468
  }
@@ -399,13 +477,16 @@ export function collectGccInfo(dir) {
399
477
  const versionDesc = getCommandOutput(GCC_CMD, dir, ["--version"]);
400
478
  const moduleDesc = getCommandOutput(GCC_CMD, dir, ["-print-search-dirs"]);
401
479
  if (versionDesc) {
402
- return {
480
+ const info = {
403
481
  type: "platform",
404
482
  name: "gcc",
405
483
  version: versionDesc.split("\n")[0],
406
484
  description: (moduleDesc || "").replaceAll("\n", "\\n"),
407
485
  };
486
+ recordToolProbeResolved("gcc", "c", info.version);
487
+ return info;
408
488
  }
489
+ recordToolProbeFailure("gcc", "c", GCC_CMD);
409
490
  return undefined;
410
491
  }
411
492
 
@@ -419,13 +500,16 @@ export function collectRustInfo(dir) {
419
500
  const versionDesc = getCommandOutput(RUSTC_CMD, dir, ["--version"]);
420
501
  const moduleDesc = getCommandOutput(CARGO_CMD, dir, ["--version"]);
421
502
  if (versionDesc) {
422
- return {
503
+ const info = {
423
504
  type: "platform",
424
505
  name: "rustc",
425
506
  version: versionDesc.trim(),
426
507
  description: (moduleDesc || "").trim(),
427
508
  };
509
+ recordToolProbeResolved("rustc", "rust", info.version);
510
+ return info;
428
511
  }
512
+ recordToolProbeFailure("rustc", "rust", RUSTC_CMD);
429
513
  return undefined;
430
514
  }
431
515
 
@@ -438,12 +522,15 @@ export function collectRustInfo(dir) {
438
522
  export function collectGoInfo(dir) {
439
523
  const versionDesc = getCommandOutput(GO_CMD, dir, ["version"]);
440
524
  if (versionDesc) {
441
- return {
525
+ const info = {
442
526
  type: "platform",
443
527
  name: "go",
444
528
  version: versionDesc.trim(),
445
529
  };
530
+ recordToolProbeResolved("go", "go", info.version);
531
+ return info;
446
532
  }
533
+ recordToolProbeFailure("go", "go", GO_CMD);
447
534
  return undefined;
448
535
  }
449
536
 
@@ -456,12 +543,15 @@ export function collectGoInfo(dir) {
456
543
  export function collectSwiftInfo(dir) {
457
544
  const versionDesc = getCommandOutput(SWIFT_CMD, dir, ["--version"]);
458
545
  if (versionDesc) {
459
- return {
546
+ const info = {
460
547
  type: "platform",
461
548
  name: "swift",
462
549
  version: versionDesc.trim(),
463
550
  };
551
+ recordToolProbeResolved("swift", "swift", info.version);
552
+ return info;
464
553
  }
554
+ recordToolProbeFailure("swift", "swift", SWIFT_CMD);
465
555
  return undefined;
466
556
  }
467
557
 
@@ -474,12 +564,15 @@ export function collectSwiftInfo(dir) {
474
564
  export function collectRubyInfo(dir) {
475
565
  const versionDesc = getCommandOutput(RUBY_CMD, dir, ["--version"]);
476
566
  if (versionDesc) {
477
- return {
567
+ const info = {
478
568
  type: "platform",
479
569
  name: "ruby",
480
570
  version: versionDesc.trim(),
481
571
  };
572
+ recordToolProbeResolved("ruby", "ruby", info.version);
573
+ return info;
482
574
  }
575
+ recordToolProbeFailure("ruby", "ruby", RUBY_CMD);
483
576
  return undefined;
484
577
  }
485
578
 
@@ -666,6 +759,17 @@ export function installSdkmanTool(toolType, toolName) {
666
759
  return false;
667
760
  }
668
761
  toolName = getSdkmanToolFullname(toolName);
762
+ // The version is interpolated into an interactive shell command, so reject
763
+ // any identifier that is not a single safe token before spawning.
764
+ if (
765
+ !/^[a-z][a-z0-9-]*$/.test(`${toolType}`) ||
766
+ !isValidSdkmanVersion(toolName)
767
+ ) {
768
+ console.log(
769
+ `Unable to install ${toolType} with an invalid version identifier '${toolName}'. Use exact sdkman version identifiers such as '3.9.9' or '21.0.7-tem'.`,
770
+ );
771
+ return false;
772
+ }
669
773
  let result;
670
774
  if (!isSdkmanToolAvailable(toolType, toolName)) {
671
775
  let installDir = "";
@@ -919,9 +1023,12 @@ export function bundleInstallWithDocker(rubyVersion, cdxgenGemHome, filePath) {
919
1023
  "install",
920
1024
  ];
921
1025
  console.log(`Performing bundle install with: ${ociCmd}`);
1026
+ // The container's own output is a diagnostic. Inheriting stdout would let it
1027
+ // interleave with the BOM document when the caller asked for `-o -`, so the
1028
+ // child's stdout is pointed at the diagnostic stream instead.
922
1029
  const result = safeSpawnSync(ociCmd, ociArgs, {
923
1030
  shell: isWin,
924
- stdio: "inherit",
1031
+ stdio: ["inherit", diagnosticStream, "inherit"],
925
1032
  });
926
1033
  if (result.error || result.status !== 0) {
927
1034
  return false;
@@ -964,6 +1071,14 @@ export function installRubyVersion(rubyVersion, filePath) {
964
1071
  if (result.stderr) {
965
1072
  console.log(result.stderr);
966
1073
  }
1074
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1075
+ ecosystem: "ruby",
1076
+ tool: "rbenv",
1077
+ command: `rbenv local ${rubyVersion}`,
1078
+ exitCode: typeof result.status === "number" ? result.status : undefined,
1079
+ detail: `Selecting Ruby ${rubyVersion} with rbenv local did not succeed.`,
1080
+ outputExcerpt: commandOutputText(result),
1081
+ });
967
1082
  }
968
1083
  if (result.status === 0) {
969
1084
  return { fullToolBinDir, status: true };
@@ -971,6 +1086,14 @@ export function installRubyVersion(rubyVersion, filePath) {
971
1086
  }
972
1087
  // Check if we're trying to install Ruby 1.x or 2.x
973
1088
  if (rubyVersion.startsWith("1.")) {
1089
+ recordDegradation("ruby.version.unsupported-host", {
1090
+ ecosystem: "ruby",
1091
+ tool: "ruby",
1092
+ impact: "transitive-deps",
1093
+ wanted: rubyVersion,
1094
+ detail:
1095
+ "Building Ruby 1.x on this host is unsupported; a matching legacy container image is required to build this project.",
1096
+ });
974
1097
  console.log(
975
1098
  `Ruby version ${rubyVersion} requires very old versions of Linux such as debian:8. Consider using the container image "ghcr.io/cdxgen/debian-ruby18:master" to build the application first and then invoke cdxgen with the arguments "--lifecycle pre-build".`,
976
1099
  );
@@ -979,6 +1102,14 @@ export function installRubyVersion(rubyVersion, filePath) {
979
1102
  rubyVersion.startsWith("2.") &&
980
1103
  readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true"
981
1104
  ) {
1105
+ recordDegradation("ruby.build-deps.missing", {
1106
+ ecosystem: "ruby",
1107
+ tool: "ruby",
1108
+ impact: "transitive-deps",
1109
+ wanted: rubyVersion,
1110
+ detail:
1111
+ "Building Ruby 2.x on this host requires development libraries that are not installed.",
1112
+ });
982
1113
  console.log(
983
1114
  `Installing Ruby version ${rubyVersion} requires specific development libraries. Consider using the custom container image "ghcr.io/cdxgen/cdxgen-debian-ruby26:v13" instead.`,
984
1115
  );
@@ -992,6 +1123,12 @@ export function installRubyVersion(rubyVersion, filePath) {
992
1123
  `To speed up this step, use bind mounts. Example: "--mount type=bind,src=/tmp/rbenv,dst=/root/.rbenv/versions/${rubyVersion}"`,
993
1124
  );
994
1125
  }
1126
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_ATTEMPTED, {
1127
+ ecosystem: "ruby",
1128
+ tool: "rbenv",
1129
+ command: `rbenv install ${rubyVersion}`,
1130
+ detail: `Installing Ruby ${rubyVersion} with rbenv.`,
1131
+ });
995
1132
  const result = safeSpawnSync(
996
1133
  readEnvironmentVariable("RBENV_CMD") || "rbenv",
997
1134
  ["install", rubyVersion],
@@ -1000,6 +1137,14 @@ export function installRubyVersion(rubyVersion, filePath) {
1000
1137
  },
1001
1138
  );
1002
1139
  if (result.error || result.status !== 0) {
1140
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1141
+ ecosystem: "ruby",
1142
+ tool: "rbenv",
1143
+ command: `rbenv install ${rubyVersion}`,
1144
+ exitCode: typeof result.status === "number" ? result.status : undefined,
1145
+ detail: `The rbenv install of Ruby ${rubyVersion} did not succeed.`,
1146
+ outputExcerpt: commandOutputText(result),
1147
+ });
1003
1148
  if (result.stdout) {
1004
1149
  console.log(result.stdout);
1005
1150
  }
@@ -1125,6 +1270,13 @@ export function performBundleInstall(
1125
1270
  console.log(
1126
1271
  `Invoking ${bundleCommand} ${installArgs.join(" ")} from ${basePath} with GEM_HOME ${cdxgenGemHome}. Please wait ...`,
1127
1272
  );
1273
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_ATTEMPTED, {
1274
+ ecosystem: "ruby",
1275
+ tool: "bundler",
1276
+ command: `${bundleCommand} ${installArgs.join(" ")}`,
1277
+ path: basePath,
1278
+ detail: "Installing the project dependencies with bundler.",
1279
+ });
1128
1280
  let result = safeSpawnSync(bundleCommand, installArgs, {
1129
1281
  shell: isWin,
1130
1282
  cwd: basePath,
@@ -1134,6 +1286,15 @@ export function performBundleInstall(
1134
1286
  },
1135
1287
  });
1136
1288
  if (result.error || result.status !== 0) {
1289
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1290
+ ecosystem: "ruby",
1291
+ tool: "bundler",
1292
+ command: `${bundleCommand} ${installArgs.join(" ")}`,
1293
+ path: basePath,
1294
+ exitCode: typeof result.status === "number" ? result.status : undefined,
1295
+ detail: "The bundle install did not succeed.",
1296
+ outputExcerpt: commandOutputText(result),
1297
+ });
1137
1298
  let pythonWarningShown = false;
1138
1299
  let rubyVersionWarningShown = false;
1139
1300
  if (
@@ -1180,6 +1341,13 @@ export function performBundleInstall(
1180
1341
  );
1181
1342
  }
1182
1343
  console.log(`${bundleCommand}`);
1344
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_ATTEMPTED, {
1345
+ ecosystem: "ruby",
1346
+ tool: "bundler",
1347
+ command: `${bundleCommand} ${updateArgs.join(" ")}`,
1348
+ path: basePath,
1349
+ detail: "Updating the outdated lockfile with bundler.",
1350
+ });
1183
1351
  result = safeSpawnSync(bundleCommand, updateArgs, {
1184
1352
  shell: isWin,
1185
1353
  cwd: basePath,
@@ -1189,6 +1357,16 @@ export function performBundleInstall(
1189
1357
  },
1190
1358
  });
1191
1359
  if (result.error || result.status !== 0) {
1360
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1361
+ ecosystem: "ruby",
1362
+ tool: "bundler",
1363
+ command: `${bundleCommand} ${updateArgs.join(" ")}`,
1364
+ path: basePath,
1365
+ exitCode:
1366
+ typeof result.status === "number" ? result.status : undefined,
1367
+ detail: "The bundle update did not succeed.",
1368
+ outputExcerpt: commandOutputText(result),
1369
+ });
1192
1370
  console.log("------------");
1193
1371
  if (result.stdout) {
1194
1372
  console.log(result.stdout);
@@ -1736,6 +1914,20 @@ export function getRuntimeInformation() {
1736
1914
  runtimeInfo.version = "N/A";
1737
1915
  }
1738
1916
 
1917
+ // The runtime hosting the cdxgen run is part of the report's
1918
+ // reproducibility context, so it is recorded alongside the scanned tools.
1919
+ const runtimeTools = { Deno: "deno", Bun: "bun", "Node.js": "node" };
1920
+ const runtimeTool = runtimeTools[runtimeInfo.runtime];
1921
+ if (runtimeTool && runtimeInfo.version && runtimeInfo.version !== "N/A") {
1922
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
1923
+ ecosystem: "generic",
1924
+ tool: runtimeTool,
1925
+ found: runtimeInfo.version,
1926
+ source: "runtime",
1927
+ detail:
1928
+ "The runtime hosting this cdxgen run, recorded for report reproducibility.",
1929
+ });
1930
+ }
1739
1931
  return runtimeInfo;
1740
1932
  }
1741
1933
 
@@ -34,10 +34,13 @@
34
34
  */
35
35
 
36
36
  import {
37
+ BLOCKED_HOST_ERROR_CODE,
37
38
  cdxgenAgent,
38
39
  DEBUG_MODE,
40
+ DRY_RUN_ERROR_CODE,
39
41
  readEnvironmentVariable,
40
42
  } from "../core/activity.js";
43
+ import { isLedgerEnabled, recordDegradation } from "../core/buildLedger.js";
41
44
  import { resolveCacheDir } from "./cacheDir.js";
42
45
  import { cdxrsAvailable, runCdxrs } from "./cdxrs.js";
43
46
  import {
@@ -545,6 +548,59 @@ async function issueWithRetries(request, limiter) {
545
548
  }
546
549
  }
547
550
 
551
+ /** Policy and connectivity degradations already recorded this run; these describe the run, not one URL. */
552
+ const recordedPolicyDegradations = new Set();
553
+
554
+ /**
555
+ * Record a run-level policy or connectivity degradation once per process. A
556
+ * batch touches many URLs, but "dry-run blocked the network", "a host was not
557
+ * permitted" and "the network is unreachable" are properties of the run, so
558
+ * only the first occurrence is recorded and the report reads one line, not
559
+ * one per URL.
560
+ *
561
+ * @param {string} remediationId Remediation id from data/remediations.json
562
+ * @param {Object} fields Ledger event fields
563
+ * @returns {void}
564
+ */
565
+ function recordPolicyDegradationOnce(remediationId, fields) {
566
+ if (!isLedgerEnabled() || recordedPolicyDegradations.has(remediationId)) {
567
+ return;
568
+ }
569
+ recordedPolicyDegradations.add(remediationId);
570
+ recordDegradation(remediationId, fields);
571
+ }
572
+
573
+ /**
574
+ * Record the run-level policy or connectivity condition carried by a caught
575
+ * fetch error, if it names one. Shared by the batch retry loop and by the
576
+ * serial enrichment paths that catch and swallow the same typed errors.
577
+ *
578
+ * @param {Error} err Error caught from a `cdxgenAgent` request.
579
+ * @returns {void}
580
+ */
581
+ export function recordPolicyDegradationFromError(err) {
582
+ if (err?.code === DRY_RUN_ERROR_CODE) {
583
+ recordPolicyDegradationOnce("policy.dry-run", {
584
+ ecosystem: "generic",
585
+ impact: "none",
586
+ detail: `Dry-run mode blocked a registry request: ${err.message}`,
587
+ });
588
+ } else if (err?.code === BLOCKED_HOST_ERROR_CODE) {
589
+ recordPolicyDegradationOnce("policy.host-blocked", {
590
+ ecosystem: "generic",
591
+ impact: "licenses",
592
+ detail: `A registry request was blocked by policy: ${err.message}`,
593
+ });
594
+ } else if (err?.name === "RequestError") {
595
+ recordPolicyDegradationOnce("policy.offline", {
596
+ ecosystem: "generic",
597
+ impact: "licenses",
598
+ detail:
599
+ "A registry request failed with a transport error, so registry metadata and license enrichment may be missing.",
600
+ });
601
+ }
602
+ }
603
+
548
604
  /**
549
605
  * Classify an error thrown by `cdxgenAgent.get` into a retry decision and the
550
606
  * batch entry to record when retries are exhausted.
@@ -553,6 +609,7 @@ async function issueWithRetries(request, limiter) {
553
609
  * @returns {{retryable: boolean, delay: number|null, entry: BatchEntry}}
554
610
  */
555
611
  function classifyAgentError(err) {
612
+ recordPolicyDegradationFromError(err);
556
613
  // Secure-mode and dry-run blocks must not be retried, and the caller's own
557
614
  // fallback `cdxgenAgent.get` will surface the same error. Recording this as
558
615
  // non-definite lets the caller's catch run exactly as it would on the serial