@cdxgen/cdxgen 13.0.1 → 13.2.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 (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. 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.
@@ -88,15 +101,18 @@ function hardenedGitEnv() {
88
101
  * @type {Object<string, string>}
89
102
  */
90
103
  export const SDKMAN_JAVA_TOOL_ALIASES = {
91
- java8: readEnvironmentVariable("JAVA8_TOOL") || "8.0.452-amzn", // Temurin no longer offers java8 :(
92
- java11: readEnvironmentVariable("JAVA11_TOOL") || "11.0.31-tem",
93
- java17: readEnvironmentVariable("JAVA17_TOOL") || "17.0.19-tem",
94
- java21: readEnvironmentVariable("JAVA21_TOOL") || "21.0.11-tem",
104
+ // Identifiers must be downloadable from the sdkman broker: the `sdk list`
105
+ // output advertises Temurin respin ids such as 21.0.12+1.1-tem, but the
106
+ // broker cannot serve ids containing '+', so the plain ids are pinned.
107
+ java8: readEnvironmentVariable("JAVA8_TOOL") || "8.0.504-zulu", // Temurin no longer offers java8 :(
108
+ java11: readEnvironmentVariable("JAVA11_TOOL") || "11.0.32-tem",
109
+ java17: readEnvironmentVariable("JAVA17_TOOL") || "17.0.20-tem",
110
+ java21: readEnvironmentVariable("JAVA21_TOOL") || "21.0.12-tem",
95
111
  java22: readEnvironmentVariable("JAVA22_TOOL") || "22.0.2-tem",
96
112
  java23: readEnvironmentVariable("JAVA23_TOOL") || "23.0.2-tem",
97
113
  java24: readEnvironmentVariable("JAVA24_TOOL") || "24.0.2-tem",
98
- java25: readEnvironmentVariable("JAVA25_TOOL") || "25.0.3-tem",
99
- java26: readEnvironmentVariable("JAVA26_TOOL") || "26.0.1-tem",
114
+ java25: readEnvironmentVariable("JAVA25_TOOL") || "25.0.4-tem",
115
+ java26: readEnvironmentVariable("JAVA26_TOOL") || "26.0.2-tem",
100
116
  };
101
117
 
102
118
  /**
@@ -205,12 +221,17 @@ export function execGitCommand(dir, args) {
205
221
  // Prepend hardening `-c` overrides before the subcommand and run git with a
206
222
  // hardened environment so a scanned/untrusted repo cannot execute hooks,
207
223
  // fsmonitor commands, or config-driven scripts.
224
+ // git is a real executable, so Windows can spawn it without a shell. Doing
225
+ // so keeps the pretty-format arguments intact: `%H`, `%n` and `%x1e` are
226
+ // cmd.exe variable-expansion syntax, which the shell-execution guard rightly
227
+ // refuses to pass through a shell.
208
228
  return getCommandOutput(
209
229
  GIT_COMMAND,
210
230
  dir,
211
231
  [...GIT_HARDENING_CONFIG_ARGS, ...args],
212
232
  {
213
233
  env: hardenedGitEnv(),
234
+ shell: false,
214
235
  },
215
236
  );
216
237
  }
@@ -292,6 +313,60 @@ export function gitLogTrailers(dir, maxCount = 20) {
292
313
  return commits;
293
314
  }
294
315
 
316
+ /**
317
+ * Record a successful version probe with the version the tool reported.
318
+ *
319
+ * @param {string} tool Tool name
320
+ * @param {string} ecosystem Ecosystem the tool belongs to
321
+ * @param {string} found Version string reported by the tool
322
+ */
323
+ function recordToolProbeResolved(tool, ecosystem, found) {
324
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
325
+ ecosystem,
326
+ tool,
327
+ found,
328
+ source: "PATH",
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Record a failed version probe. A probe the environment did not let run is
334
+ * degraded evidence — the tool may well be present — while only a probe that
335
+ * ran and found nothing counts as a missing tool.
336
+ *
337
+ * @param {string} tool Tool name
338
+ * @param {string} ecosystem Ecosystem the tool belongs to
339
+ * @param {string} command Probe command that failed
340
+ */
341
+ function recordToolProbeFailure(tool, ecosystem, command) {
342
+ if (!isLedgerEnabled()) {
343
+ return;
344
+ }
345
+ const restriction = describeSpawnRestriction(command);
346
+ if (restriction) {
347
+ // A dry-run block is the user's own request, so it costs nothing; any
348
+ // other policy restriction hides a version the tool may well have.
349
+ const isDryRunBlock = classifySpawnRestriction(command) === "dry-run";
350
+ recordLedgerEvent(LEDGER_EVENT_KINDS.EVIDENCE_DEGRADED, {
351
+ ecosystem,
352
+ tool,
353
+ source: "PATH",
354
+ remediationId: isDryRunBlock ? "policy.dry-run" : "policy.secure-mode",
355
+ impact: isDryRunBlock
356
+ ? LEDGER_EVENT_IMPACTS.NONE
357
+ : LEDGER_EVENT_IMPACTS.VERSIONS,
358
+ detail: `The ${tool} version probe could not run: ${restriction}.`,
359
+ });
360
+ } else {
361
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
362
+ ecosystem,
363
+ tool,
364
+ source: "PATH",
365
+ detail: `No ${tool} command answered the version probe.`,
366
+ });
367
+ }
368
+ }
369
+
295
370
  /**
296
371
  * Collect Java version and installed modules
297
372
  *
@@ -303,7 +378,7 @@ export function collectJavaInfo(dir) {
303
378
  const moduleDesc =
304
379
  getCommandOutput(getJavaCommand(), dir, ["--list-modules"]) || "";
305
380
  if (versionDesc) {
306
- return {
381
+ const info = {
307
382
  type: "platform",
308
383
  name: "java",
309
384
  version: versionDesc.split("\n")[0].replace("java ", ""),
@@ -315,7 +390,10 @@ export function collectJavaInfo(dir) {
315
390
  },
316
391
  ],
317
392
  };
393
+ recordToolProbeResolved("java", "java", info.version);
394
+ return info;
318
395
  }
396
+ recordToolProbeFailure("java", "java", getJavaCommand());
319
397
  return undefined;
320
398
  }
321
399
 
@@ -330,13 +408,16 @@ export function collectDotnetInfo(dir) {
330
408
  const moduleDesc =
331
409
  getCommandOutput(DOTNET_CMD, dir, ["--list-runtimes"]) || "";
332
410
  if (versionDesc) {
333
- return {
411
+ const info = {
334
412
  type: "platform",
335
413
  name: "dotnet",
336
414
  version: versionDesc.trim(),
337
415
  description: moduleDesc.replaceAll("\n", "\\n"),
338
416
  };
417
+ recordToolProbeResolved("dotnet", "csharp", info.version);
418
+ return info;
339
419
  }
420
+ recordToolProbeFailure("dotnet", "csharp", DOTNET_CMD);
340
421
  return undefined;
341
422
  }
342
423
 
@@ -347,25 +428,21 @@ export function collectDotnetInfo(dir) {
347
428
  * @returns Object containing python details
348
429
  */
349
430
  export function collectPythonInfo(dir) {
350
- const versionDesc = getCommandOutput(getPythonCommand(), dir, [
351
- "-S",
352
- "--version",
353
- ]);
431
+ const pythonCmd = getPythonCommand();
432
+ const versionDesc = getCommandOutput(pythonCmd, dir, ["-S", "--version"]);
354
433
  const moduleDesc =
355
- getCommandOutput(getPythonCommand(), dir, [
356
- "-I",
357
- "-m",
358
- "pip",
359
- "--version",
360
- ]) || "";
434
+ getCommandOutput(pythonCmd, dir, ["-I", "-m", "pip", "--version"]) || "";
361
435
  if (versionDesc) {
362
- return {
436
+ const info = {
363
437
  type: "platform",
364
438
  name: "python",
365
439
  version: versionDesc.replace("Python ", ""),
366
440
  description: moduleDesc.replaceAll("\n", "\\n"),
367
441
  };
442
+ recordToolProbeResolved("python", "python", info.version);
443
+ return info;
368
444
  }
445
+ recordToolProbeFailure("python", "python", pythonCmd);
369
446
  return undefined;
370
447
  }
371
448
 
@@ -386,6 +463,9 @@ export function collectNodeInfo(dir) {
386
463
  const moduleDesc = getCommandOutput(NPM_CMD, dir, ["--version"]);
387
464
  if (moduleDesc) {
388
465
  nodeInfo.description = `npm: ${moduleDesc}`;
466
+ recordToolProbeResolved("npm", "npm", moduleDesc);
467
+ } else {
468
+ recordToolProbeFailure("npm", "npm", NPM_CMD);
389
469
  }
390
470
  return nodeInfo;
391
471
  }
@@ -400,13 +480,16 @@ export function collectGccInfo(dir) {
400
480
  const versionDesc = getCommandOutput(GCC_CMD, dir, ["--version"]);
401
481
  const moduleDesc = getCommandOutput(GCC_CMD, dir, ["-print-search-dirs"]);
402
482
  if (versionDesc) {
403
- return {
483
+ const info = {
404
484
  type: "platform",
405
485
  name: "gcc",
406
486
  version: versionDesc.split("\n")[0],
407
487
  description: (moduleDesc || "").replaceAll("\n", "\\n"),
408
488
  };
489
+ recordToolProbeResolved("gcc", "c", info.version);
490
+ return info;
409
491
  }
492
+ recordToolProbeFailure("gcc", "c", GCC_CMD);
410
493
  return undefined;
411
494
  }
412
495
 
@@ -420,13 +503,16 @@ export function collectRustInfo(dir) {
420
503
  const versionDesc = getCommandOutput(RUSTC_CMD, dir, ["--version"]);
421
504
  const moduleDesc = getCommandOutput(CARGO_CMD, dir, ["--version"]);
422
505
  if (versionDesc) {
423
- return {
506
+ const info = {
424
507
  type: "platform",
425
508
  name: "rustc",
426
509
  version: versionDesc.trim(),
427
510
  description: (moduleDesc || "").trim(),
428
511
  };
512
+ recordToolProbeResolved("rustc", "rust", info.version);
513
+ return info;
429
514
  }
515
+ recordToolProbeFailure("rustc", "rust", RUSTC_CMD);
430
516
  return undefined;
431
517
  }
432
518
 
@@ -439,12 +525,15 @@ export function collectRustInfo(dir) {
439
525
  export function collectGoInfo(dir) {
440
526
  const versionDesc = getCommandOutput(GO_CMD, dir, ["version"]);
441
527
  if (versionDesc) {
442
- return {
528
+ const info = {
443
529
  type: "platform",
444
530
  name: "go",
445
531
  version: versionDesc.trim(),
446
532
  };
533
+ recordToolProbeResolved("go", "go", info.version);
534
+ return info;
447
535
  }
536
+ recordToolProbeFailure("go", "go", GO_CMD);
448
537
  return undefined;
449
538
  }
450
539
 
@@ -457,12 +546,15 @@ export function collectGoInfo(dir) {
457
546
  export function collectSwiftInfo(dir) {
458
547
  const versionDesc = getCommandOutput(SWIFT_CMD, dir, ["--version"]);
459
548
  if (versionDesc) {
460
- return {
549
+ const info = {
461
550
  type: "platform",
462
551
  name: "swift",
463
552
  version: versionDesc.trim(),
464
553
  };
554
+ recordToolProbeResolved("swift", "swift", info.version);
555
+ return info;
465
556
  }
557
+ recordToolProbeFailure("swift", "swift", SWIFT_CMD);
466
558
  return undefined;
467
559
  }
468
560
 
@@ -475,12 +567,15 @@ export function collectSwiftInfo(dir) {
475
567
  export function collectRubyInfo(dir) {
476
568
  const versionDesc = getCommandOutput(RUBY_CMD, dir, ["--version"]);
477
569
  if (versionDesc) {
478
- return {
570
+ const info = {
479
571
  type: "platform",
480
572
  name: "ruby",
481
573
  version: versionDesc.trim(),
482
574
  };
575
+ recordToolProbeResolved("ruby", "ruby", info.version);
576
+ return info;
483
577
  }
578
+ recordToolProbeFailure("ruby", "ruby", RUBY_CMD);
484
579
  return undefined;
485
580
  }
486
581
 
@@ -667,6 +762,17 @@ export function installSdkmanTool(toolType, toolName) {
667
762
  return false;
668
763
  }
669
764
  toolName = getSdkmanToolFullname(toolName);
765
+ // The version is interpolated into an interactive shell command, so reject
766
+ // any identifier that is not a single safe token before spawning.
767
+ if (
768
+ !/^[a-z][a-z0-9-]*$/.test(`${toolType}`) ||
769
+ !isValidSdkmanVersion(toolName)
770
+ ) {
771
+ console.log(
772
+ `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'.`,
773
+ );
774
+ return false;
775
+ }
670
776
  let result;
671
777
  if (!isSdkmanToolAvailable(toolType, toolName)) {
672
778
  let installDir = "";
@@ -968,6 +1074,14 @@ export function installRubyVersion(rubyVersion, filePath) {
968
1074
  if (result.stderr) {
969
1075
  console.log(result.stderr);
970
1076
  }
1077
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1078
+ ecosystem: "ruby",
1079
+ tool: "rbenv",
1080
+ command: `rbenv local ${rubyVersion}`,
1081
+ exitCode: typeof result.status === "number" ? result.status : undefined,
1082
+ detail: `Selecting Ruby ${rubyVersion} with rbenv local did not succeed.`,
1083
+ outputExcerpt: commandOutputText(result),
1084
+ });
971
1085
  }
972
1086
  if (result.status === 0) {
973
1087
  return { fullToolBinDir, status: true };
@@ -975,6 +1089,14 @@ export function installRubyVersion(rubyVersion, filePath) {
975
1089
  }
976
1090
  // Check if we're trying to install Ruby 1.x or 2.x
977
1091
  if (rubyVersion.startsWith("1.")) {
1092
+ recordDegradation("ruby.version.unsupported-host", {
1093
+ ecosystem: "ruby",
1094
+ tool: "ruby",
1095
+ impact: "transitive-deps",
1096
+ wanted: rubyVersion,
1097
+ detail:
1098
+ "Building Ruby 1.x on this host is unsupported; a matching legacy container image is required to build this project.",
1099
+ });
978
1100
  console.log(
979
1101
  `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
1102
  );
@@ -983,6 +1105,14 @@ export function installRubyVersion(rubyVersion, filePath) {
983
1105
  rubyVersion.startsWith("2.") &&
984
1106
  readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true"
985
1107
  ) {
1108
+ recordDegradation("ruby.build-deps.missing", {
1109
+ ecosystem: "ruby",
1110
+ tool: "ruby",
1111
+ impact: "transitive-deps",
1112
+ wanted: rubyVersion,
1113
+ detail:
1114
+ "Building Ruby 2.x on this host requires development libraries that are not installed.",
1115
+ });
986
1116
  console.log(
987
1117
  `Installing Ruby version ${rubyVersion} requires specific development libraries. Consider using the custom container image "ghcr.io/cdxgen/cdxgen-debian-ruby26:v13" instead.`,
988
1118
  );
@@ -996,6 +1126,12 @@ export function installRubyVersion(rubyVersion, filePath) {
996
1126
  `To speed up this step, use bind mounts. Example: "--mount type=bind,src=/tmp/rbenv,dst=/root/.rbenv/versions/${rubyVersion}"`,
997
1127
  );
998
1128
  }
1129
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_ATTEMPTED, {
1130
+ ecosystem: "ruby",
1131
+ tool: "rbenv",
1132
+ command: `rbenv install ${rubyVersion}`,
1133
+ detail: `Installing Ruby ${rubyVersion} with rbenv.`,
1134
+ });
999
1135
  const result = safeSpawnSync(
1000
1136
  readEnvironmentVariable("RBENV_CMD") || "rbenv",
1001
1137
  ["install", rubyVersion],
@@ -1004,6 +1140,14 @@ export function installRubyVersion(rubyVersion, filePath) {
1004
1140
  },
1005
1141
  );
1006
1142
  if (result.error || result.status !== 0) {
1143
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1144
+ ecosystem: "ruby",
1145
+ tool: "rbenv",
1146
+ command: `rbenv install ${rubyVersion}`,
1147
+ exitCode: typeof result.status === "number" ? result.status : undefined,
1148
+ detail: `The rbenv install of Ruby ${rubyVersion} did not succeed.`,
1149
+ outputExcerpt: commandOutputText(result),
1150
+ });
1007
1151
  if (result.stdout) {
1008
1152
  console.log(result.stdout);
1009
1153
  }
@@ -1129,6 +1273,13 @@ export function performBundleInstall(
1129
1273
  console.log(
1130
1274
  `Invoking ${bundleCommand} ${installArgs.join(" ")} from ${basePath} with GEM_HOME ${cdxgenGemHome}. Please wait ...`,
1131
1275
  );
1276
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_ATTEMPTED, {
1277
+ ecosystem: "ruby",
1278
+ tool: "bundler",
1279
+ command: `${bundleCommand} ${installArgs.join(" ")}`,
1280
+ path: basePath,
1281
+ detail: "Installing the project dependencies with bundler.",
1282
+ });
1132
1283
  let result = safeSpawnSync(bundleCommand, installArgs, {
1133
1284
  shell: isWin,
1134
1285
  cwd: basePath,
@@ -1138,6 +1289,15 @@ export function performBundleInstall(
1138
1289
  },
1139
1290
  });
1140
1291
  if (result.error || result.status !== 0) {
1292
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1293
+ ecosystem: "ruby",
1294
+ tool: "bundler",
1295
+ command: `${bundleCommand} ${installArgs.join(" ")}`,
1296
+ path: basePath,
1297
+ exitCode: typeof result.status === "number" ? result.status : undefined,
1298
+ detail: "The bundle install did not succeed.",
1299
+ outputExcerpt: commandOutputText(result),
1300
+ });
1141
1301
  let pythonWarningShown = false;
1142
1302
  let rubyVersionWarningShown = false;
1143
1303
  if (
@@ -1184,6 +1344,13 @@ export function performBundleInstall(
1184
1344
  );
1185
1345
  }
1186
1346
  console.log(`${bundleCommand}`);
1347
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_ATTEMPTED, {
1348
+ ecosystem: "ruby",
1349
+ tool: "bundler",
1350
+ command: `${bundleCommand} ${updateArgs.join(" ")}`,
1351
+ path: basePath,
1352
+ detail: "Updating the outdated lockfile with bundler.",
1353
+ });
1187
1354
  result = safeSpawnSync(bundleCommand, updateArgs, {
1188
1355
  shell: isWin,
1189
1356
  cwd: basePath,
@@ -1193,6 +1360,16 @@ export function performBundleInstall(
1193
1360
  },
1194
1361
  });
1195
1362
  if (result.error || result.status !== 0) {
1363
+ recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
1364
+ ecosystem: "ruby",
1365
+ tool: "bundler",
1366
+ command: `${bundleCommand} ${updateArgs.join(" ")}`,
1367
+ path: basePath,
1368
+ exitCode:
1369
+ typeof result.status === "number" ? result.status : undefined,
1370
+ detail: "The bundle update did not succeed.",
1371
+ outputExcerpt: commandOutputText(result),
1372
+ });
1196
1373
  console.log("------------");
1197
1374
  if (result.stdout) {
1198
1375
  console.log(result.stdout);
@@ -1740,6 +1917,20 @@ export function getRuntimeInformation() {
1740
1917
  runtimeInfo.version = "N/A";
1741
1918
  }
1742
1919
 
1920
+ // The runtime hosting the cdxgen run is part of the report's
1921
+ // reproducibility context, so it is recorded alongside the scanned tools.
1922
+ const runtimeTools = { Deno: "deno", Bun: "bun", "Node.js": "node" };
1923
+ const runtimeTool = runtimeTools[runtimeInfo.runtime];
1924
+ if (runtimeTool && runtimeInfo.version && runtimeInfo.version !== "N/A") {
1925
+ recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_RESOLVED, {
1926
+ ecosystem: "generic",
1927
+ tool: runtimeTool,
1928
+ found: runtimeInfo.version,
1929
+ source: "runtime",
1930
+ detail:
1931
+ "The runtime hosting this cdxgen run, recorded for report reproducibility.",
1932
+ });
1933
+ }
1743
1934
  return runtimeInfo;
1744
1935
  }
1745
1936
 
@@ -6,6 +6,7 @@ import { build } from "@cdxgen/cdx-purl";
6
6
 
7
7
  import { safeExistsSync } from "../core/fs.js";
8
8
  import { thoughtLog } from "../core/logger.js";
9
+ import { addDosaiReachabilityProperties } from "./dosai.js";
9
10
  import {
10
11
  addDosaiSetValue,
11
12
  buildDosaiPurlAliasMap,
@@ -256,6 +257,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
256
257
  const purlLocationMap = {};
257
258
  const purlModulesMap = {};
258
259
  const purlMethodsMap = {};
260
+ const purlReachabilityMap = {};
259
261
  const purlAliasMap = buildDosaiPurlAliasMap(pkgList);
260
262
  for (const apkg of pkgList) {
261
263
  if (apkg.properties && Array.isArray(apkg.properties)) {
@@ -340,6 +342,9 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
340
342
  if (!modPurl) {
341
343
  continue;
342
344
  }
345
+ // Confidence/evidence facts for this purl, applied to the component below so an
346
+ // unbuilt tree stays distinguishable from an only-imported package.
347
+ purlReachabilityMap[modPurl] ??= reachability;
343
348
  let hasExplicitSourceLocations = false;
344
349
  for (const sourceLocation of reachability.SourceLocations || []) {
345
350
  const location = dosaiSourceLocation(sourceLocation);
@@ -456,6 +461,14 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
456
461
  "I didn't find any occurrence evidence or detailed imported modules, even though there is good dependency slice data from dosai. This is surprising.",
457
462
  );
458
463
  }
464
+ // Reachability confidence facts (kind, confidence, evidence, reasons) as properties.
465
+ // Applied outside the location guard: a Low-confidence unbuilt tree must stay
466
+ // distinguishable from an only-imported package even when no locations were found.
467
+ for (const apkg of pkgList) {
468
+ if (purlReachabilityMap[apkg.purl]) {
469
+ addDosaiReachabilityProperties(apkg, purlReachabilityMap[apkg.purl]);
470
+ }
471
+ }
459
472
  return pkgList;
460
473
  }
461
474
 
@@ -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 {
@@ -245,21 +248,58 @@ export async function prefetchJson(requests, options = {}) {
245
248
  * - a non-JSON `responseType`, since the envelope has nowhere to put HTML;
246
249
  * - request-specific `headers`, since cdxrs derives its own Authorization from
247
250
  * `GITHUB_TOKEN` and would silently drop any other credential, turning an
248
- * authenticated lookup into an anonymous one.
251
+ * authenticated lookup into an anonymous one;
252
+ * - anything addressed to a host whose rate policy only one transport knows.
253
+ * crates.io's crawler policy allows one request per second with no
254
+ * parallelism, and package.elm-lang.org has a JS-only allowance because
255
+ * rate.rs carries no row for it. Both transports keep separate gates, so a
256
+ * batch split across the two would issue twice the agreed rate; the JS pool
257
+ * owns these hosts so there is one gate to reason about.
249
258
  *
250
259
  * The pool applies the same global and per-host concurrency and rate policy, so
251
260
  * the batching win is unaffected; only the on-disk cache is skipped.
252
261
  *
253
- * @param {{responseType?: string, headers?: Object}} request
262
+ * @param {{url?: string, responseType?: string, headers?: Object}} request
254
263
  * @returns {boolean}
255
264
  */
256
265
  function cdxrsCanServe(request) {
257
266
  if (request.responseType && request.responseType !== "json") {
258
267
  return false;
259
268
  }
269
+ if (isSingleGateHost(extractHost(request.url))) {
270
+ return false;
271
+ }
260
272
  return !request.headers || !Object.keys(request.headers).length;
261
273
  }
262
274
 
275
+ /**
276
+ * Whether a hostname belongs to crates.io, including its index and CDN hosts.
277
+ *
278
+ * @param {string|null} host Hostname, or null when the URL did not parse.
279
+ * @returns {boolean}
280
+ */
281
+ export function isCratesHost(host) {
282
+ if (!host) {
283
+ return false;
284
+ }
285
+ const lower = host.toLowerCase();
286
+ return lower === "crates.io" || lower.endsWith(".crates.io");
287
+ }
288
+
289
+ /**
290
+ * Whether a host's rate policy lives in the JS table alone and therefore needs
291
+ * every request to pass through the one gate the JS pool holds.
292
+ *
293
+ * @param {string|null} host Hostname, or null when the URL did not parse.
294
+ * @returns {boolean}
295
+ */
296
+ export function isSingleGateHost(host) {
297
+ if (!host) {
298
+ return false;
299
+ }
300
+ return isCratesHost(host) || host.toLowerCase() === "package.elm-lang.org";
301
+ }
302
+
263
303
  /**
264
304
  * Run the batch through the `cdxrs fetch` subprocess and translate its
265
305
  * envelope into the shared result shape.
@@ -545,6 +585,59 @@ async function issueWithRetries(request, limiter) {
545
585
  }
546
586
  }
547
587
 
588
+ /** Policy and connectivity degradations already recorded this run; these describe the run, not one URL. */
589
+ const recordedPolicyDegradations = new Set();
590
+
591
+ /**
592
+ * Record a run-level policy or connectivity degradation once per process. A
593
+ * batch touches many URLs, but "dry-run blocked the network", "a host was not
594
+ * permitted" and "the network is unreachable" are properties of the run, so
595
+ * only the first occurrence is recorded and the report reads one line, not
596
+ * one per URL.
597
+ *
598
+ * @param {string} remediationId Remediation id from data/remediations.json
599
+ * @param {Object} fields Ledger event fields
600
+ * @returns {void}
601
+ */
602
+ function recordPolicyDegradationOnce(remediationId, fields) {
603
+ if (!isLedgerEnabled() || recordedPolicyDegradations.has(remediationId)) {
604
+ return;
605
+ }
606
+ recordedPolicyDegradations.add(remediationId);
607
+ recordDegradation(remediationId, fields);
608
+ }
609
+
610
+ /**
611
+ * Record the run-level policy or connectivity condition carried by a caught
612
+ * fetch error, if it names one. Shared by the batch retry loop and by the
613
+ * serial enrichment paths that catch and swallow the same typed errors.
614
+ *
615
+ * @param {Error} err Error caught from a `cdxgenAgent` request.
616
+ * @returns {void}
617
+ */
618
+ export function recordPolicyDegradationFromError(err) {
619
+ if (err?.code === DRY_RUN_ERROR_CODE) {
620
+ recordPolicyDegradationOnce("policy.dry-run", {
621
+ ecosystem: "generic",
622
+ impact: "none",
623
+ detail: `Dry-run mode blocked a registry request: ${err.message}`,
624
+ });
625
+ } else if (err?.code === BLOCKED_HOST_ERROR_CODE) {
626
+ recordPolicyDegradationOnce("policy.host-blocked", {
627
+ ecosystem: "generic",
628
+ impact: "licenses",
629
+ detail: `A registry request was blocked by policy: ${err.message}`,
630
+ });
631
+ } else if (err?.name === "RequestError") {
632
+ recordPolicyDegradationOnce("policy.offline", {
633
+ ecosystem: "generic",
634
+ impact: "licenses",
635
+ detail:
636
+ "A registry request failed with a transport error, so registry metadata and license enrichment may be missing.",
637
+ });
638
+ }
639
+ }
640
+
548
641
  /**
549
642
  * Classify an error thrown by `cdxgenAgent.get` into a retry decision and the
550
643
  * batch entry to record when retries are exhausted.
@@ -553,6 +646,7 @@ async function issueWithRetries(request, limiter) {
553
646
  * @returns {{retryable: boolean, delay: number|null, entry: BatchEntry}}
554
647
  */
555
648
  function classifyAgentError(err) {
649
+ recordPolicyDegradationFromError(err);
556
650
  // Secure-mode and dry-run blocks must not be retried, and the caller's own
557
651
  // fallback `cdxgenAgent.get` will surface the same error. Recording this as
558
652
  // non-definite lets the caller's catch run exactly as it would on the serial
@@ -643,6 +737,41 @@ async function fetchOne(request, gauge) {
643
737
  }
644
738
  }
645
739
 
740
+ /**
741
+ * Run a caller's own request under the same per-host gate the batch pool uses.
742
+ *
743
+ * Not every registry lookup goes through the pool: a caller that misses the
744
+ * prefetch, or runs with prefetching disabled, issues `cdxgenAgent.get`
745
+ * directly. Those requests reach the same host and count against the same
746
+ * published budget, so they have to queue behind the same limiter rather than
747
+ * a second one — crates.io's one-request-per-second policy is not honoured by
748
+ * two independent gates that each allow one per second.
749
+ *
750
+ * The per-host semaphore and limiter are module state, so this shares one gate
751
+ * per host for the life of the process.
752
+ *
753
+ * @template T
754
+ * @param {string} url The URL about to be requested.
755
+ * @param {() => Promise<T>} issue Issues the request and resolves its result.
756
+ * @returns {Promise<T>} Whatever `issue` resolves to.
757
+ */
758
+ export async function withHostRateLimit(url, issue) {
759
+ const host = extractHost(url);
760
+ if (!host) {
761
+ return await issue();
762
+ }
763
+ const controls = getHostControls(host);
764
+ const releaseGlobal = await _globalSemaphore.acquire();
765
+ const releaseHost = await controls.sem.acquire();
766
+ try {
767
+ await controls.limiter.wait();
768
+ return await issue();
769
+ } finally {
770
+ releaseHost();
771
+ releaseGlobal();
772
+ }
773
+ }
774
+
646
775
  /**
647
776
  * Dedupe key for an in-flight request: the URL, plus everything that changes
648
777
  * which response the caller gets back. That is the headers which change what