@cdxgen/cdxgen 13.0.1 → 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 (155) hide show
  1. package/README.md +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +125 -4
  4. package/bin/evinse.js +1 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/frameworks-list.json +86 -1
  7. package/data/remediations.json +1144 -0
  8. package/data/rules/asar-archives.yaml +1 -1
  9. package/data/rules/build-fidelity.yaml +782 -0
  10. package/data/rules/package-integrity.yaml +13 -13
  11. package/lib/audit/index.js +48 -4
  12. package/lib/audit/scoring.js +2 -8
  13. package/lib/cli/bomAssembly.js +20 -1
  14. package/lib/cli/cliOptions.js +94 -7
  15. package/lib/cli/index.js +76 -48
  16. package/lib/cli/jsBom.js +32 -3
  17. package/lib/cli/jvmBom.js +1494 -1315
  18. package/lib/cli/managedBom.js +97 -28
  19. package/lib/cli/nativeBom.js +189 -10
  20. package/lib/core/buildLedger.js +799 -0
  21. package/lib/core/deferredExit.js +141 -0
  22. package/lib/core/env.js +69 -1
  23. package/lib/core/fs.js +60 -0
  24. package/lib/core/logger.js +12 -99
  25. package/lib/core/severity.js +16 -0
  26. package/lib/core/syncFileWriter.js +106 -0
  27. package/lib/ecosystems/ecosystems.js +9 -0
  28. package/lib/ecosystems/gradleutils.js +355 -15
  29. package/lib/ecosystems/parsers-js.js +26 -0
  30. package/lib/ecosystems/parsers-jvm.js +9 -0
  31. package/lib/ecosystems/parsers-misc.js +16 -2
  32. package/lib/ecosystems/parsers-python.js +171 -35
  33. package/lib/ecosystems/parsers-rust.js +12 -2
  34. package/lib/ecosystems/pypiClassifiers.js +326 -0
  35. package/lib/ecosystems/pythonutils.js +49 -1
  36. package/lib/ecosystems/rubyutils.js +13 -0
  37. package/lib/evinser/evinser.js +30 -12
  38. package/lib/helpers/remote/dependency-track.js +81 -14
  39. package/lib/inventory/auditCategories.js +9 -0
  40. package/lib/inventory/depsUtils.js +72 -0
  41. package/lib/inventory/dosai.js +431 -22
  42. package/lib/inventory/envcontext.js +206 -18
  43. package/lib/inventory/fetchBatch.js +57 -0
  44. package/lib/inventory/jvmToolEnv.js +593 -0
  45. package/lib/inventory/osinfo.js +1 -0
  46. package/lib/inventory/toolRequirements.js +903 -0
  47. package/lib/managers/binary.js +24 -0
  48. package/lib/managers/docker.js +26 -5
  49. package/lib/server/server.js +1 -1
  50. package/lib/stages/postgen/annotationBuilder.js +97 -0
  51. package/lib/stages/postgen/auditBom.js +76 -79
  52. package/lib/stages/postgen/introspection/annotate.js +320 -0
  53. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  54. package/lib/stages/postgen/introspection/emit.js +192 -0
  55. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  56. package/lib/stages/postgen/introspection/markers.js +227 -0
  57. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  58. package/lib/stages/postgen/introspection/report.js +1208 -0
  59. package/lib/stages/postgen/introspection/score.js +1067 -0
  60. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  61. package/lib/stages/postgen/parentEdge.js +156 -0
  62. package/lib/stages/postgen/postgen.js +115 -2
  63. package/lib/stages/postgen/ruleEngine.js +17 -0
  64. package/lib/stages/pregen/envAudit.js +42 -2
  65. package/lib/stages/pregen/pregen.js +1171 -6
  66. package/lib/validator/bomValidator.js +11 -2
  67. package/package.json +14 -14
  68. package/types/lib/audit/index.d.ts.map +1 -1
  69. package/types/lib/audit/scoring.d.ts +2 -8
  70. package/types/lib/audit/scoring.d.ts.map +1 -1
  71. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  72. package/types/lib/cli/cliOptions.d.ts +2 -1
  73. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  74. package/types/lib/cli/index.d.ts +8 -0
  75. package/types/lib/cli/index.d.ts.map +1 -1
  76. package/types/lib/cli/jsBom.d.ts.map +1 -1
  77. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  78. package/types/lib/cli/managedBom.d.ts.map +1 -1
  79. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  80. package/types/lib/core/buildLedger.d.ts +272 -0
  81. package/types/lib/core/buildLedger.d.ts.map +1 -0
  82. package/types/lib/core/deferredExit.d.ts +84 -0
  83. package/types/lib/core/deferredExit.d.ts.map +1 -0
  84. package/types/lib/core/env.d.ts +22 -1
  85. package/types/lib/core/env.d.ts.map +1 -1
  86. package/types/lib/core/fs.d.ts +21 -0
  87. package/types/lib/core/fs.d.ts.map +1 -1
  88. package/types/lib/core/logger.d.ts +2 -11
  89. package/types/lib/core/logger.d.ts.map +1 -1
  90. package/types/lib/core/severity.d.ts +11 -0
  91. package/types/lib/core/severity.d.ts.map +1 -0
  92. package/types/lib/core/syncFileWriter.d.ts +33 -0
  93. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  94. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  95. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  96. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  97. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  98. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  99. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  100. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  101. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  102. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  103. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  104. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  105. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  107. package/types/lib/evinser/evinser.d.ts +5 -1
  108. package/types/lib/evinser/evinser.d.ts.map +1 -1
  109. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  110. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  111. package/types/lib/inventory/auditCategories.d.ts +8 -0
  112. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  113. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  114. package/types/lib/inventory/dosai.d.ts +20 -0
  115. package/types/lib/inventory/dosai.d.ts.map +1 -1
  116. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  117. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  118. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  119. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  120. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  121. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  122. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  123. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  124. package/types/lib/managers/binary.d.ts.map +1 -1
  125. package/types/lib/managers/docker.d.ts.map +1 -1
  126. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  127. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  128. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  129. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  130. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  131. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  132. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  133. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  134. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  135. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  136. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  137. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  138. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  139. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  140. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  141. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  142. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  144. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  145. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  146. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  147. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  148. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  149. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  150. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  151. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  152. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  153. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  154. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  155. 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,
@@ -33,7 +41,12 @@ import {
33
41
  } from "../core/fs.js";
34
42
  import { diagnosticStream } from "../core/logger.js";
35
43
  import { dirNameStr, isMac, isWin } from "../core/paths.js";
44
+ import { isValidSdkmanVersion } from "./jvmToolEnv.js";
36
45
  import { applyPurl, tryBuildPurl } from "./purl.js";
46
+ import {
47
+ classifySpawnRestriction,
48
+ describeSpawnRestriction,
49
+ } from "./toolRequirements.js";
37
50
 
38
51
  /**
39
52
  * Git executable name used for all git invocations, overridable via GIT_CMD.
@@ -205,12 +218,17 @@ export function execGitCommand(dir, args) {
205
218
  // Prepend hardening `-c` overrides before the subcommand and run git with a
206
219
  // hardened environment so a scanned/untrusted repo cannot execute hooks,
207
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.
208
225
  return getCommandOutput(
209
226
  GIT_COMMAND,
210
227
  dir,
211
228
  [...GIT_HARDENING_CONFIG_ARGS, ...args],
212
229
  {
213
230
  env: hardenedGitEnv(),
231
+ shell: false,
214
232
  },
215
233
  );
216
234
  }
@@ -292,6 +310,60 @@ export function gitLogTrailers(dir, maxCount = 20) {
292
310
  return commits;
293
311
  }
294
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
+
295
367
  /**
296
368
  * Collect Java version and installed modules
297
369
  *
@@ -303,7 +375,7 @@ export function collectJavaInfo(dir) {
303
375
  const moduleDesc =
304
376
  getCommandOutput(getJavaCommand(), dir, ["--list-modules"]) || "";
305
377
  if (versionDesc) {
306
- return {
378
+ const info = {
307
379
  type: "platform",
308
380
  name: "java",
309
381
  version: versionDesc.split("\n")[0].replace("java ", ""),
@@ -315,7 +387,10 @@ export function collectJavaInfo(dir) {
315
387
  },
316
388
  ],
317
389
  };
390
+ recordToolProbeResolved("java", "java", info.version);
391
+ return info;
318
392
  }
393
+ recordToolProbeFailure("java", "java", getJavaCommand());
319
394
  return undefined;
320
395
  }
321
396
 
@@ -330,13 +405,16 @@ export function collectDotnetInfo(dir) {
330
405
  const moduleDesc =
331
406
  getCommandOutput(DOTNET_CMD, dir, ["--list-runtimes"]) || "";
332
407
  if (versionDesc) {
333
- return {
408
+ const info = {
334
409
  type: "platform",
335
410
  name: "dotnet",
336
411
  version: versionDesc.trim(),
337
412
  description: moduleDesc.replaceAll("\n", "\\n"),
338
413
  };
414
+ recordToolProbeResolved("dotnet", "csharp", info.version);
415
+ return info;
339
416
  }
417
+ recordToolProbeFailure("dotnet", "csharp", DOTNET_CMD);
340
418
  return undefined;
341
419
  }
342
420
 
@@ -347,25 +425,21 @@ export function collectDotnetInfo(dir) {
347
425
  * @returns Object containing python details
348
426
  */
349
427
  export function collectPythonInfo(dir) {
350
- const versionDesc = getCommandOutput(getPythonCommand(), dir, [
351
- "-S",
352
- "--version",
353
- ]);
428
+ const pythonCmd = getPythonCommand();
429
+ const versionDesc = getCommandOutput(pythonCmd, dir, ["-S", "--version"]);
354
430
  const moduleDesc =
355
- getCommandOutput(getPythonCommand(), dir, [
356
- "-I",
357
- "-m",
358
- "pip",
359
- "--version",
360
- ]) || "";
431
+ getCommandOutput(pythonCmd, dir, ["-I", "-m", "pip", "--version"]) || "";
361
432
  if (versionDesc) {
362
- return {
433
+ const info = {
363
434
  type: "platform",
364
435
  name: "python",
365
436
  version: versionDesc.replace("Python ", ""),
366
437
  description: moduleDesc.replaceAll("\n", "\\n"),
367
438
  };
439
+ recordToolProbeResolved("python", "python", info.version);
440
+ return info;
368
441
  }
442
+ recordToolProbeFailure("python", "python", pythonCmd);
369
443
  return undefined;
370
444
  }
371
445
 
@@ -386,6 +460,9 @@ export function collectNodeInfo(dir) {
386
460
  const moduleDesc = getCommandOutput(NPM_CMD, dir, ["--version"]);
387
461
  if (moduleDesc) {
388
462
  nodeInfo.description = `npm: ${moduleDesc}`;
463
+ recordToolProbeResolved("npm", "npm", moduleDesc);
464
+ } else {
465
+ recordToolProbeFailure("npm", "npm", NPM_CMD);
389
466
  }
390
467
  return nodeInfo;
391
468
  }
@@ -400,13 +477,16 @@ export function collectGccInfo(dir) {
400
477
  const versionDesc = getCommandOutput(GCC_CMD, dir, ["--version"]);
401
478
  const moduleDesc = getCommandOutput(GCC_CMD, dir, ["-print-search-dirs"]);
402
479
  if (versionDesc) {
403
- return {
480
+ const info = {
404
481
  type: "platform",
405
482
  name: "gcc",
406
483
  version: versionDesc.split("\n")[0],
407
484
  description: (moduleDesc || "").replaceAll("\n", "\\n"),
408
485
  };
486
+ recordToolProbeResolved("gcc", "c", info.version);
487
+ return info;
409
488
  }
489
+ recordToolProbeFailure("gcc", "c", GCC_CMD);
410
490
  return undefined;
411
491
  }
412
492
 
@@ -420,13 +500,16 @@ export function collectRustInfo(dir) {
420
500
  const versionDesc = getCommandOutput(RUSTC_CMD, dir, ["--version"]);
421
501
  const moduleDesc = getCommandOutput(CARGO_CMD, dir, ["--version"]);
422
502
  if (versionDesc) {
423
- return {
503
+ const info = {
424
504
  type: "platform",
425
505
  name: "rustc",
426
506
  version: versionDesc.trim(),
427
507
  description: (moduleDesc || "").trim(),
428
508
  };
509
+ recordToolProbeResolved("rustc", "rust", info.version);
510
+ return info;
429
511
  }
512
+ recordToolProbeFailure("rustc", "rust", RUSTC_CMD);
430
513
  return undefined;
431
514
  }
432
515
 
@@ -439,12 +522,15 @@ export function collectRustInfo(dir) {
439
522
  export function collectGoInfo(dir) {
440
523
  const versionDesc = getCommandOutput(GO_CMD, dir, ["version"]);
441
524
  if (versionDesc) {
442
- return {
525
+ const info = {
443
526
  type: "platform",
444
527
  name: "go",
445
528
  version: versionDesc.trim(),
446
529
  };
530
+ recordToolProbeResolved("go", "go", info.version);
531
+ return info;
447
532
  }
533
+ recordToolProbeFailure("go", "go", GO_CMD);
448
534
  return undefined;
449
535
  }
450
536
 
@@ -457,12 +543,15 @@ export function collectGoInfo(dir) {
457
543
  export function collectSwiftInfo(dir) {
458
544
  const versionDesc = getCommandOutput(SWIFT_CMD, dir, ["--version"]);
459
545
  if (versionDesc) {
460
- return {
546
+ const info = {
461
547
  type: "platform",
462
548
  name: "swift",
463
549
  version: versionDesc.trim(),
464
550
  };
551
+ recordToolProbeResolved("swift", "swift", info.version);
552
+ return info;
465
553
  }
554
+ recordToolProbeFailure("swift", "swift", SWIFT_CMD);
466
555
  return undefined;
467
556
  }
468
557
 
@@ -475,12 +564,15 @@ export function collectSwiftInfo(dir) {
475
564
  export function collectRubyInfo(dir) {
476
565
  const versionDesc = getCommandOutput(RUBY_CMD, dir, ["--version"]);
477
566
  if (versionDesc) {
478
- return {
567
+ const info = {
479
568
  type: "platform",
480
569
  name: "ruby",
481
570
  version: versionDesc.trim(),
482
571
  };
572
+ recordToolProbeResolved("ruby", "ruby", info.version);
573
+ return info;
483
574
  }
575
+ recordToolProbeFailure("ruby", "ruby", RUBY_CMD);
484
576
  return undefined;
485
577
  }
486
578
 
@@ -667,6 +759,17 @@ export function installSdkmanTool(toolType, toolName) {
667
759
  return false;
668
760
  }
669
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
+ }
670
773
  let result;
671
774
  if (!isSdkmanToolAvailable(toolType, toolName)) {
672
775
  let installDir = "";
@@ -968,6 +1071,14 @@ export function installRubyVersion(rubyVersion, filePath) {
968
1071
  if (result.stderr) {
969
1072
  console.log(result.stderr);
970
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
+ });
971
1082
  }
972
1083
  if (result.status === 0) {
973
1084
  return { fullToolBinDir, status: true };
@@ -975,6 +1086,14 @@ export function installRubyVersion(rubyVersion, filePath) {
975
1086
  }
976
1087
  // Check if we're trying to install Ruby 1.x or 2.x
977
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
+ });
978
1097
  console.log(
979
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".`,
980
1099
  );
@@ -983,6 +1102,14 @@ export function installRubyVersion(rubyVersion, filePath) {
983
1102
  rubyVersion.startsWith("2.") &&
984
1103
  readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true"
985
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
+ });
986
1113
  console.log(
987
1114
  `Installing Ruby version ${rubyVersion} requires specific development libraries. Consider using the custom container image "ghcr.io/cdxgen/cdxgen-debian-ruby26:v13" instead.`,
988
1115
  );
@@ -996,6 +1123,12 @@ export function installRubyVersion(rubyVersion, filePath) {
996
1123
  `To speed up this step, use bind mounts. Example: "--mount type=bind,src=/tmp/rbenv,dst=/root/.rbenv/versions/${rubyVersion}"`,
997
1124
  );
998
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
+ });
999
1132
  const result = safeSpawnSync(
1000
1133
  readEnvironmentVariable("RBENV_CMD") || "rbenv",
1001
1134
  ["install", rubyVersion],
@@ -1004,6 +1137,14 @@ export function installRubyVersion(rubyVersion, filePath) {
1004
1137
  },
1005
1138
  );
1006
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
+ });
1007
1148
  if (result.stdout) {
1008
1149
  console.log(result.stdout);
1009
1150
  }
@@ -1129,6 +1270,13 @@ export function performBundleInstall(
1129
1270
  console.log(
1130
1271
  `Invoking ${bundleCommand} ${installArgs.join(" ")} from ${basePath} with GEM_HOME ${cdxgenGemHome}. Please wait ...`,
1131
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
+ });
1132
1280
  let result = safeSpawnSync(bundleCommand, installArgs, {
1133
1281
  shell: isWin,
1134
1282
  cwd: basePath,
@@ -1138,6 +1286,15 @@ export function performBundleInstall(
1138
1286
  },
1139
1287
  });
1140
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
+ });
1141
1298
  let pythonWarningShown = false;
1142
1299
  let rubyVersionWarningShown = false;
1143
1300
  if (
@@ -1184,6 +1341,13 @@ export function performBundleInstall(
1184
1341
  );
1185
1342
  }
1186
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
+ });
1187
1351
  result = safeSpawnSync(bundleCommand, updateArgs, {
1188
1352
  shell: isWin,
1189
1353
  cwd: basePath,
@@ -1193,6 +1357,16 @@ export function performBundleInstall(
1193
1357
  },
1194
1358
  });
1195
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
+ });
1196
1370
  console.log("------------");
1197
1371
  if (result.stdout) {
1198
1372
  console.log(result.stdout);
@@ -1740,6 +1914,20 @@ export function getRuntimeInformation() {
1740
1914
  runtimeInfo.version = "N/A";
1741
1915
  }
1742
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
+ }
1743
1931
  return runtimeInfo;
1744
1932
  }
1745
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