@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
@@ -8,13 +8,17 @@ import { build } from "@cdxgen/cdx-purl";
8
8
  import { parse as loadYaml } from "yaml";
9
9
 
10
10
  import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
11
+ import { recordDegradation } from "../core/buildLedger.js";
12
+ import { deferFailOnError } from "../core/deferredExit.js";
11
13
  import {
12
14
  CARGO_CMD,
15
+ CARGO_TARGET,
13
16
  CLJ_CMD,
14
17
  hasAnyProjectType,
15
18
  LEIN_CMD,
16
19
  SWIFT_CMD,
17
20
  shouldFetchLicense,
21
+ shouldFetchPackageMetadata,
18
22
  } from "../core/env.js";
19
23
  import {
20
24
  checksumFile,
@@ -34,6 +38,14 @@ import {
34
38
  getCratesMetadata,
35
39
  getSwiftPackageMetadata,
36
40
  } from "../ecosystems/ecosystems.js";
41
+ import { parseShardsProject } from "../ecosystems/parsers-crystal.js";
42
+ import {
43
+ createElmCacheReader,
44
+ getElmMetadata,
45
+ isElmProjectFile,
46
+ parseElmProject,
47
+ parseLegacyElmProject,
48
+ } from "../ecosystems/parsers-elm.js";
37
49
  import { parseGleamProject } from "../ecosystems/parsers-gleam.js";
38
50
  import {
39
51
  parseGoListDep,
@@ -44,12 +56,17 @@ import {
44
56
  parseGopkgData,
45
57
  parseGosumData,
46
58
  } from "../ecosystems/parsers-go.js";
59
+ import { parseJuliaProject } from "../ecosystems/parsers-julia.js";
47
60
  import {
48
61
  parseCljDep,
49
62
  parseEdnData,
50
63
  parseLeinDep,
51
64
  parseLeiningenData,
52
65
  } from "../ecosystems/parsers-jvm.js";
66
+ import {
67
+ parseLuaRocksLock,
68
+ parseRockspecFile,
69
+ } from "../ecosystems/parsers-lua.js";
53
70
  import {
54
71
  buildObjectForCocoaPod,
55
72
  executeAlpmList,
@@ -76,16 +93,32 @@ import {
76
93
  parseSwiftResolved,
77
94
  prefetchCocoaPodspecs,
78
95
  } from "../ecosystems/parsers-misc.js";
96
+ import { parseNimProject } from "../ecosystems/parsers-nim.js";
97
+ import { parseOpamLockedFile } from "../ecosystems/parsers-opam.js";
98
+ import { parseRebarLock } from "../ecosystems/parsers-rebar.js";
79
99
  import {
100
+ applyCargoDependencyKindMetadata,
101
+ buildCargoLockDependencyGraph,
102
+ buildCargoNativeLibraryComponents,
103
+ cargoComponentKey,
80
104
  parseCargoData,
81
105
  parseCargoDependencyData,
82
106
  parseCargoManifestDependencyData,
107
+ parseCargoManifestDependencyKinds,
108
+ parseCargoMetadataResolve,
83
109
  parseCargoTomlData,
110
+ rollupCargoDependencyKinds,
84
111
  } from "../ecosystems/parsers-rust.js";
112
+ import { parseSpackLock } from "../ecosystems/parsers-spack.js";
113
+ import { parseStackProject } from "../ecosystems/parsers-stack.js";
85
114
  import { resolveZigGraph } from "../ecosystems/zigResolver.js";
86
115
  import { mergeDependencies, trimComponents } from "../inventory/depsUtils.js";
87
116
  import { convertOSQueryResults } from "../inventory/evidenceUtils.js";
88
117
  import { applyPurl, nixBomRef, tryBuildPurl } from "../inventory/purl.js";
118
+ import {
119
+ classifyProbeResult,
120
+ classifySpawnRestriction,
121
+ } from "../inventory/toolRequirements.js";
89
122
  import { executeOsQuery } from "../managers/binary.js";
90
123
  import {
91
124
  buildBomNSData,
@@ -357,13 +390,34 @@ export async function createGoBom(path, options) {
357
390
  // So we support a fallback to only operate with go mod graph command output in such instances
358
391
  console.log("go list -deps command has failed for", basePath);
359
392
  shouldManuallyParse = true;
393
+ // A spawn the runtime or a policy denied is neither a missing
394
+ // toolchain nor an engaged fallback; the version probes report
395
+ // that as degraded evidence and the policy entries name the fix.
396
+ if (
397
+ !classifySpawnRestriction("go") &&
398
+ result.error &&
399
+ classifyProbeResult(result) === "missing"
400
+ ) {
401
+ recordDegradation("go.toolchain.missing", {
402
+ ecosystem: "go",
403
+ tool: "go",
404
+ impact: "transitive-deps",
405
+ command: "go list -deps ./...",
406
+ detail:
407
+ "The go executable could not be run, so the module graph was not resolved.",
408
+ });
409
+ }
360
410
  if (DEBUG_MODE && result.stdout) {
361
411
  console.log(result.stdout);
362
412
  }
363
413
  if (DEBUG_MODE && result.stderr) {
364
414
  console.log(result.stderr);
365
415
  }
366
- options.failOnError && process.exit(1);
416
+ deferFailOnError(options, {
417
+ ecosystem: "go",
418
+ tool: "go",
419
+ detail: "the go module graph command failed",
420
+ });
367
421
  }
368
422
  const stdout = result.stdout;
369
423
  if (stdout) {
@@ -411,7 +465,11 @@ export async function createGoBom(path, options) {
411
465
  if (DEBUG_MODE && result.stderr) {
412
466
  console.log(result.stderr);
413
467
  }
414
- options.failOnError && process.exit(1);
468
+ deferFailOnError(options, {
469
+ ecosystem: "go",
470
+ tool: "go",
471
+ detail: "the go list command failed",
472
+ });
415
473
  }
416
474
  if (result.stdout) {
417
475
  cmdOutput = Buffer.from(result.stdout).toString();
@@ -492,7 +550,11 @@ export async function createGoBom(path, options) {
492
550
  console.log(
493
551
  "3. Alternatively, consider generating a post-build SBOM from the built binary using blint. Use the official container image and invoke cdxgen with the arguments `-t binary --lifecycle post-build`.",
494
552
  );
495
- options.failOnError && process.exit(1);
553
+ deferFailOnError(options, {
554
+ ecosystem: "go",
555
+ tool: "go",
556
+ detail: "no go dependency evidence could be collected",
557
+ });
496
558
  }
497
559
  }
498
560
  }
@@ -510,6 +572,19 @@ export async function createGoBom(path, options) {
510
572
  if (
511
573
  !hasAnyProjectType(["docker", "oci", "container", "os"], options, false)
512
574
  ) {
575
+ // When the environment itself denies the go command, the version
576
+ // probes record the restriction as degraded evidence and the policy
577
+ // entries name the fix; the fallback is the finding only when the
578
+ // toolchain was free to run and could not resolve the graph.
579
+ if (!classifySpawnRestriction("go")) {
580
+ recordDegradation("go.mod-only-fallback", {
581
+ ecosystem: "go",
582
+ tool: "go",
583
+ impact: "transitive-deps",
584
+ detail:
585
+ "The go toolchain could not resolve the module graph, so go.mod was parsed manually and components carry only the declared requires.",
586
+ });
587
+ }
513
588
  console.log(
514
589
  "Manually parsing go.mod files. The resultant BOM would be incomplete.",
515
590
  );
@@ -584,6 +659,31 @@ export async function createGoBom(path, options) {
584
659
  return {};
585
660
  }
586
661
 
662
+ /**
663
+ * Whether the cargo executable answers a version probe.
664
+ *
665
+ * A cargo command run through a shell — the Windows path — reports an absent
666
+ * executable as a non-zero exit with the shell's own complaint on stderr, not
667
+ * as a spawn error, so a failed build alone cannot tell a missing toolchain
668
+ * from a broken project. The probe answers that question directly.
669
+ *
670
+ * @param {string} cwd Directory to probe from.
671
+ * @returns {boolean} True when cargo answered.
672
+ */
673
+ function cargoAnswersVersion(cwd) {
674
+ const probe = safeSpawnSync(CARGO_CMD, ["--version"], {
675
+ cdxgenActivity: {
676
+ kind: "probe",
677
+ metadata: { tool: "cargo" },
678
+ probeType: "build-tool-availability",
679
+ },
680
+ cwd,
681
+ encoding: "utf-8",
682
+ shell: isWin,
683
+ });
684
+ return classifyProbeResult(probe) === "found";
685
+ }
686
+
587
687
  /**
588
688
  * Function to create bom string for Rust projects
589
689
  *
@@ -591,6 +691,223 @@ export async function createGoBom(path, options) {
591
691
  * @param {Object} options Parse options from the cli
592
692
  * @returns {Promise<Object|undefined>} Promise resolving to a BOM object or undefined
593
693
  */
694
+ /**
695
+ * Run `cargo metadata` for a manifest and return its parsed resolve graph.
696
+ *
697
+ * The lock file is the union of every dependency kind and every target triple,
698
+ * so it cannot answer which crates are dev-only or platform-gated. Only
699
+ * `cargo metadata` carries that, per dependency edge, in `resolve.nodes[].deps[].dep_kinds`.
700
+ *
701
+ * @param {string} cargoTomlFile Manifest path
702
+ * @param {string} [filterPlatform] Target triple to resolve the graph for
703
+ * @returns {object|undefined} Parsed `cargo metadata` output
704
+ */
705
+ function runCargoMetadata(cargoTomlFile, filterPlatform) {
706
+ const basePath = dirname(cargoTomlFile);
707
+ const platformArgs = filterPlatform
708
+ ? ["--filter-platform", filterPlatform]
709
+ : [];
710
+ // `--locked` keeps the user's lock file untouched; a workspace that is out of
711
+ // date with its lock file needs the retry without it.
712
+ for (const lockArgs of [["--locked"], []]) {
713
+ const cargoArgs = [
714
+ "metadata",
715
+ "--format-version",
716
+ "1",
717
+ "--manifest-path",
718
+ cargoTomlFile,
719
+ ...platformArgs,
720
+ ...lockArgs,
721
+ ];
722
+ if (DEBUG_MODE) {
723
+ console.log("Executing", CARGO_CMD, cargoArgs.join(" "), "in", basePath);
724
+ }
725
+ const result = safeSpawnSync(CARGO_CMD, cargoArgs, {
726
+ cwd: basePath,
727
+ shell: isWin,
728
+ });
729
+ if (result.status === 0 && !result.error && result.stdout) {
730
+ try {
731
+ return JSON.parse(result.stdout);
732
+ } catch (err) {
733
+ if (DEBUG_MODE) {
734
+ console.log(`Unable to parse the cargo metadata output: ${err}`);
735
+ }
736
+ return undefined;
737
+ }
738
+ }
739
+ if (DEBUG_MODE) {
740
+ console.log(result.error, result.stderr);
741
+ }
742
+ }
743
+ return undefined;
744
+ }
745
+
746
+ /**
747
+ * Annotate cargo components with their dependency kind, platform gate and
748
+ * scope.
749
+ *
750
+ * With `--deep`, the edge kinds come from `cargo metadata` and are exact at
751
+ * every depth. Otherwise they are recovered from the manifests, which is exact
752
+ * for the dependencies a workspace member declares and inherited below them.
753
+ *
754
+ * @param {object[]} pkgList Components to annotate
755
+ * @param {object[]} dependencyTree Lockfile dependency relationships
756
+ * @param {string[]} cargoFiles Manifest paths
757
+ * @param {Map<string, string>} manifestRootKeys Manifest path to root component key
758
+ * @param {object} options CLI options
759
+ */
760
+ function annotateCargoDependencyKinds(
761
+ pkgList,
762
+ dependencyTree,
763
+ cargoFiles,
764
+ manifestRootKeys,
765
+ options,
766
+ ) {
767
+ if (!pkgList?.length) {
768
+ return;
769
+ }
770
+ const allEdges = new Map();
771
+ if (options?.deep) {
772
+ const rollups = new Map();
773
+ const packageInfo = new Map();
774
+ const visitedWorkspaceRoots = new Set();
775
+ // The keys that survive resolution for the requested target triple. The
776
+ // unfiltered graph still supplies the kinds, because a crate's kind does
777
+ // not change with the triple.
778
+ const targetIncludedKeys = CARGO_TARGET ? new Set() : undefined;
779
+ let targetFilterApplied = !CARGO_TARGET;
780
+ let anyMetadata = false;
781
+ let everyMetadata = true;
782
+ for (const cargoTomlFile of cargoFiles) {
783
+ const metadata = runCargoMetadata(cargoTomlFile);
784
+ if (!metadata) {
785
+ everyMetadata = false;
786
+ continue;
787
+ }
788
+ // Every member of a workspace answers with the same resolve graph.
789
+ const workspaceRoot = metadata.workspace_root || cargoTomlFile;
790
+ if (visitedWorkspaceRoots.has(workspaceRoot)) {
791
+ anyMetadata = true;
792
+ continue;
793
+ }
794
+ visitedWorkspaceRoots.add(workspaceRoot);
795
+ anyMetadata = true;
796
+ if (CARGO_TARGET) {
797
+ const targetMetadata = runCargoMetadata(cargoTomlFile, CARGO_TARGET);
798
+ if (targetMetadata) {
799
+ for (const key of parseCargoMetadataResolve(targetMetadata)
800
+ .resolvedKeys) {
801
+ targetIncludedKeys.add(key);
802
+ }
803
+ targetFilterApplied = true;
804
+ } else {
805
+ console.warn(
806
+ `cargo metadata could not resolve the dependency graph for the target ${CARGO_TARGET}. The components are not filtered by target.`,
807
+ );
808
+ }
809
+ }
810
+ const graph = parseCargoMetadataResolve(metadata);
811
+ for (const [key, value] of graph.packageInfo) {
812
+ packageInfo.set(key, value);
813
+ }
814
+ for (const [key, value] of graph.edges) {
815
+ allEdges.set(key, [...(allEdges.get(key) || []), ...value]);
816
+ }
817
+ for (const [key, value] of rollupCargoDependencyKinds(graph)) {
818
+ const existing = rollups.get(key);
819
+ if (!existing) {
820
+ rollups.set(key, value);
821
+ continue;
822
+ }
823
+ // A crate that is a runtime dependency of any workspace in the scan is
824
+ // a runtime dependency of the BOM.
825
+ for (const kind of value.kinds) {
826
+ existing.kinds.add(kind);
827
+ }
828
+ for (const target of value.targets) {
829
+ existing.targets.add(target);
830
+ }
831
+ const precedence = ["root", "runtime", "build", "dev"];
832
+ if (
833
+ precedence.indexOf(value.kind) < precedence.indexOf(existing.kind)
834
+ ) {
835
+ existing.kind = value.kind;
836
+ existing.targetGated = value.targetGated;
837
+ } else if (value.kind === existing.kind && !value.targetGated) {
838
+ existing.targetGated = false;
839
+ }
840
+ }
841
+ }
842
+ if (anyMetadata && rollups.size) {
843
+ applyCargoDependencyKindMetadata(pkgList, rollups, packageInfo, {
844
+ resolveIsComplete: everyMetadata,
845
+ filterPlatform: targetFilterApplied ? CARGO_TARGET : undefined,
846
+ targetIncludedKeys: targetFilterApplied
847
+ ? targetIncludedKeys
848
+ : undefined,
849
+ });
850
+ // A `-sys` crate puts a C library into the artifact that the cargo
851
+ // dependency graph itself cannot describe.
852
+ const nativeLibraries = buildCargoNativeLibraryComponents(
853
+ packageInfo,
854
+ allEdges,
855
+ rollups,
856
+ );
857
+ if (nativeLibraries.components.length) {
858
+ pkgList.push(...nativeLibraries.components);
859
+ if (dependencyTree) {
860
+ dependencyTree.push(...nativeLibraries.dependencies);
861
+ }
862
+ }
863
+ return;
864
+ }
865
+ recordDegradation("rust.cargo-metadata-unavailable", {
866
+ ecosystem: "rust",
867
+ tool: "cargo",
868
+ impact: "components",
869
+ command: `${basename(CARGO_CMD)} metadata`,
870
+ detail:
871
+ "cargo metadata could not be executed, so dependency kinds and platform gates were recovered from the manifests alone.",
872
+ });
873
+ }
874
+ if (!dependencyTree?.length || !manifestRootKeys.size) {
875
+ return;
876
+ }
877
+ const declaredEdgesByRoot = new Map();
878
+ for (const [cargoTomlFile, rootKey] of manifestRootKeys) {
879
+ if (!rootKey) {
880
+ continue;
881
+ }
882
+ const declaredEdges = parseCargoManifestDependencyKinds(cargoTomlFile, {
883
+ includeWorkspaceMembers: !options.multiProject,
884
+ });
885
+ const existing = declaredEdgesByRoot.get(rootKey);
886
+ if (!existing) {
887
+ declaredEdgesByRoot.set(rootKey, declaredEdges);
888
+ continue;
889
+ }
890
+ for (const [name, value] of declaredEdges) {
891
+ const knownEdge = existing.get(name);
892
+ if (!knownEdge) {
893
+ existing.set(name, value);
894
+ continue;
895
+ }
896
+ for (const kind of value.kinds) {
897
+ knownEdge.kinds.add(kind);
898
+ }
899
+ for (const target of value.targets) {
900
+ knownEdge.targets.add(target);
901
+ }
902
+ }
903
+ }
904
+ const graph = buildCargoLockDependencyGraph(
905
+ dependencyTree,
906
+ declaredEdgesByRoot,
907
+ );
908
+ applyCargoDependencyKindMetadata(pkgList, rollupCargoDependencyKinds(graph));
909
+ }
910
+
594
911
  export async function createRustBom(path, options) {
595
912
  let pkgList = [];
596
913
  let parentComponent = {};
@@ -649,7 +966,27 @@ export async function createRustBom(path, options) {
649
966
  if (cargoInstallResult.status !== 0 || cargoInstallResult.error) {
650
967
  console.error("Error running the cargo command");
651
968
  console.log(cargoInstallResult.error, cargoInstallResult.stderr);
652
- options.failOnError && process.exit(1);
969
+ // A spawn the runtime or a policy denied is neither a missing
970
+ // toolchain nor an engaged fallback; the version probes record
971
+ // that as degraded evidence.
972
+ const toolchainMissing = cargoInstallResult.error
973
+ ? classifyProbeResult(cargoInstallResult) === "missing"
974
+ : !cargoAnswersVersion(basePath);
975
+ if (!classifySpawnRestriction(CARGO_CMD) && toolchainMissing) {
976
+ recordDegradation("rust.toolchain.missing", {
977
+ ecosystem: "rust",
978
+ tool: "cargo",
979
+ impact: "transitive-deps",
980
+ command: `${basename(CARGO_CMD)} ${cargoArgs.join(" ")}`,
981
+ detail:
982
+ "The cargo executable could not be run, so the lock file could not be generated.",
983
+ });
984
+ }
985
+ deferFailOnError(options, {
986
+ ecosystem: "rust",
987
+ tool: "cargo",
988
+ detail: "the cargo command failed",
989
+ });
653
990
  }
654
991
  }
655
992
  }
@@ -667,6 +1004,16 @@ export async function createRustBom(path, options) {
667
1004
  options,
668
1005
  );
669
1006
  const cargoLockMode = cargoLockFiles.length;
1007
+ if (cargoFiles.length && !cargoLockMode) {
1008
+ recordDegradation("rust.cargo-lock-missing", {
1009
+ ecosystem: "rust",
1010
+ tool: "cargo",
1011
+ impact: "transitive-deps",
1012
+ detail:
1013
+ "No Cargo.lock was found beside the manifest, so components carry the declared dependencies without locked versions or the resolved crate graph.",
1014
+ });
1015
+ }
1016
+ const manifestRootKeys = new Map();
670
1017
  for (const f of cargoFiles) {
671
1018
  if (DEBUG_MODE) {
672
1019
  console.log(`Parsing ${f}`);
@@ -675,6 +1022,7 @@ export async function createRustBom(path, options) {
675
1022
  includeWorkspaceMembers: !options.multiProject,
676
1023
  });
677
1024
  if (dlist?.length) {
1025
+ manifestRootKeys.set(f, cargoComponentKey(dlist[0]));
678
1026
  if (!cargoLockMode) {
679
1027
  pkgList = pkgList.concat(dlist);
680
1028
  } else {
@@ -730,6 +1078,15 @@ export async function createRustBom(path, options) {
730
1078
  );
731
1079
  }
732
1080
  }
1081
+ if (cargoLockMode) {
1082
+ annotateCargoDependencyKinds(
1083
+ pkgList,
1084
+ dependencyTree,
1085
+ cargoFiles,
1086
+ manifestRootKeys,
1087
+ options,
1088
+ );
1089
+ }
733
1090
  return buildBomNSData(options, pkgList, "cargo", {
734
1091
  src: path,
735
1092
  filename: cargoLockFiles.join(", "),
@@ -868,6 +1225,15 @@ export async function createDartBom(path, options) {
868
1225
  `${options.multiProject ? "**/" : ""}pubspec.yaml`,
869
1226
  options,
870
1227
  );
1228
+ if (pubSpecYamlFiles.length && !pubFiles.length) {
1229
+ recordDegradation("dart.pub-get-needed", {
1230
+ ecosystem: "dart",
1231
+ tool: "dart",
1232
+ impact: "versions",
1233
+ detail:
1234
+ "pubspec.yaml was parsed without pubspec.lock, so components carry version constraints instead of the locked versions.",
1235
+ });
1236
+ }
871
1237
  let dependencies = [];
872
1238
  let pkgList = [];
873
1239
  let parentComponent;
@@ -1367,8 +1733,22 @@ export function createClojureBom(path, options) {
1367
1733
  if (result.status !== 0 || result.error) {
1368
1734
  if (result.stderr) {
1369
1735
  console.error(result.stdout, result.stderr);
1370
- options.failOnError && process.exit(1);
1736
+ deferFailOnError(options, {
1737
+ ecosystem: "clojure",
1738
+ tool: "lein",
1739
+ detail: "the leiningen command failed",
1740
+ });
1371
1741
  }
1742
+ recordDegradation("clojure.lein.missing", {
1743
+ ecosystem: "clojure",
1744
+ tool: "lein",
1745
+ impact: "transitive-deps",
1746
+ command: `${LEIN_CMD} ${LEIN_ARGS.join(" ")}`,
1747
+ exitCode:
1748
+ typeof result.status === "number" ? result.status : undefined,
1749
+ detail:
1750
+ "lein did not produce a dependency list, so the project.clj manifest is parsed manually.",
1751
+ });
1372
1752
  console.log(
1373
1753
  "Check if the correct version of lein is installed and available in PATH. Falling back to manual parsing.",
1374
1754
  );
@@ -1390,7 +1770,11 @@ export function createClojureBom(path, options) {
1390
1770
  }
1391
1771
  } else {
1392
1772
  console.error("lein unexpectedly didn't return any output");
1393
- options.failOnError && process.exit(1);
1773
+ deferFailOnError(options, {
1774
+ ecosystem: "clojure",
1775
+ tool: "lein",
1776
+ detail: "the leiningen command produced no output",
1777
+ });
1394
1778
  }
1395
1779
  }
1396
1780
  }
@@ -1413,8 +1797,22 @@ export function createClojureBom(path, options) {
1413
1797
  if (result.status !== 0 || result.error) {
1414
1798
  if (result.stderr) {
1415
1799
  console.error(result.stdout, result.stderr);
1416
- options.failOnError && process.exit(1);
1800
+ deferFailOnError(options, {
1801
+ ecosystem: "clojure",
1802
+ tool: "clj",
1803
+ detail: "the clojure cli command failed",
1804
+ });
1417
1805
  }
1806
+ recordDegradation("clojure.cli.missing", {
1807
+ ecosystem: "clojure",
1808
+ tool: "clojure",
1809
+ impact: "transitive-deps",
1810
+ command: `${CLJ_CMD} ${CLJ_ARGS.join(" ")}`,
1811
+ exitCode:
1812
+ typeof result.status === "number" ? result.status : undefined,
1813
+ detail:
1814
+ "The clojure CLI did not produce a dependency tree, so the deps.edn manifest is parsed manually.",
1815
+ });
1418
1816
  console.log(
1419
1817
  "Check if the correct version of clojure cli is installed and available in PATH. Falling back to manual parsing.",
1420
1818
  );
@@ -1436,7 +1834,11 @@ export function createClojureBom(path, options) {
1436
1834
  }
1437
1835
  } else {
1438
1836
  console.error("clj unexpectedly didn't return any output");
1439
- options.failOnError && process.exit(1);
1837
+ deferFailOnError(options, {
1838
+ ecosystem: "clojure",
1839
+ tool: "clj",
1840
+ detail: "the clojure cli command produced no output",
1841
+ });
1440
1842
  }
1441
1843
  }
1442
1844
  }
@@ -1449,6 +1851,44 @@ export function createClojureBom(path, options) {
1449
1851
  return {};
1450
1852
  }
1451
1853
 
1854
+ /**
1855
+ * Locate the Stack lock files under a path.
1856
+ *
1857
+ * Stack names the lock after the configuration file it completes, so the
1858
+ * default `stack.yaml` produces `stack.yaml.lock` and a project with several
1859
+ * configurations produces one lock per configuration. `stack.lock` is also
1860
+ * accepted because tooling and documentation examples have used that name.
1861
+ *
1862
+ * @param {string} path Project path
1863
+ * @param {Object} options Parse options from the cli
1864
+ * @returns {string[]} Lock file paths, deduplicated
1865
+ */
1866
+ export function findStackLockFiles(path, options) {
1867
+ const prefix = options.multiProject ? "**/" : "";
1868
+ const candidates = new Set([
1869
+ ...getAllFiles(path, `${prefix}*.yaml.lock`, options),
1870
+ ...getAllFiles(path, `${prefix}stack.lock`, options),
1871
+ ]);
1872
+ return [...candidates].filter((lockFile) => {
1873
+ if (/(^|[\\/])stack\.(yaml\.)?lock$/u.test(lockFile)) {
1874
+ return true;
1875
+ }
1876
+ // `*.yaml.lock` is not a name Stack owns, so a custom configuration is
1877
+ // accepted only when the file it completes names a snapshot.
1878
+ const configFile = lockFile.replace(/\.lock$/u, "");
1879
+ if (!safeExistsSync(configFile)) {
1880
+ return false;
1881
+ }
1882
+ try {
1883
+ return /^\s*(snapshot|resolver)\s*:/mu.test(
1884
+ readFileSync(configFile, "utf-8"),
1885
+ );
1886
+ } catch {
1887
+ return false;
1888
+ }
1889
+ });
1890
+ }
1891
+
1452
1892
  /**
1453
1893
  * Function to create bom string for Haskell projects
1454
1894
  *
@@ -1479,6 +1919,45 @@ export function createHaskellBom(path, options) {
1479
1919
  filename: cabalFiles.join(", "),
1480
1920
  });
1481
1921
  }
1922
+ // Stack projects keep the resolved extra-deps in a lock file named after
1923
+ // the configuration file, so `stack.yaml` locks to `stack.yaml.lock`.
1924
+ const stackLockFiles = findStackLockFiles(path, options);
1925
+ if (stackLockFiles.length) {
1926
+ const dependencies = [];
1927
+ let parentComponent = {};
1928
+ for (const stackLockFile of stackLockFiles) {
1929
+ if (DEBUG_MODE) {
1930
+ console.log(`Parsing ${stackLockFile}`);
1931
+ }
1932
+ const stackYamlFile = stackLockFile.replace(/\.lock$/u, "");
1933
+ const result = parseStackProject(
1934
+ stackLockFile,
1935
+ safeExistsSync(stackYamlFile) ? stackYamlFile : undefined,
1936
+ );
1937
+ if (result.pkgList?.length) {
1938
+ pkgList = pkgList.concat(result.pkgList);
1939
+ }
1940
+ if (!Object.keys(parentComponent).length) {
1941
+ parentComponent = result.parentComponent;
1942
+ attachRootEdge(result, parentComponent, dependencies);
1943
+ }
1944
+ }
1945
+ if (pkgList.length) {
1946
+ return buildBomNSData(options, pkgList, "hackage", {
1947
+ src: path,
1948
+ filename: stackLockFiles.join(", "),
1949
+ dependencies,
1950
+ parentComponent,
1951
+ });
1952
+ }
1953
+ }
1954
+ recordDegradation("haskell.freeze-missing", {
1955
+ ecosystem: "haskell",
1956
+ tool: "cabal",
1957
+ impact: "components",
1958
+ detail:
1959
+ "No cabal.project.freeze or stack.yaml.lock was found; those lock files are the sources the parsers read for the package set.",
1960
+ });
1482
1961
  return {};
1483
1962
  }
1484
1963
 
@@ -1512,6 +1991,13 @@ export function createElixirBom(path, options) {
1512
1991
  filename: mixFiles.join(", "),
1513
1992
  });
1514
1993
  }
1994
+ recordDegradation("elixir.deps-not-fetched", {
1995
+ ecosystem: "elixir",
1996
+ tool: "mix",
1997
+ impact: "components",
1998
+ detail:
1999
+ "No mix.lock was found, and the lock file written by fetching the dependencies is the source the parser reads.",
2000
+ });
1515
2001
  return {};
1516
2002
  }
1517
2003
 
@@ -1634,6 +2120,16 @@ export async function createSwiftBom(path, options) {
1634
2120
  `It looks like the 'Sources' directory is missing, so we are missing the components and dependencies for '${basename(basePath)}'.`,
1635
2121
  );
1636
2122
  } else if (readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true") {
2123
+ recordDegradation("swift.toolchain.missing", {
2124
+ ecosystem: "swift",
2125
+ tool: "swift",
2126
+ impact: "transitive-deps",
2127
+ command: `${swiftCommand} ${packageArgs.join(" ")}`,
2128
+ exitCode:
2129
+ typeof result.status === "number" ? result.status : undefined,
2130
+ detail:
2131
+ "The swift package command did not succeed, so the swift package graph was not resolved.",
2132
+ });
1637
2133
  console.log(
1638
2134
  "Consider using the cdxgen container image (`ghcr.io/cdxgen/cdxgen`), which includes Swift and additional build tools.",
1639
2135
  );
@@ -1645,7 +2141,11 @@ export async function createSwiftBom(path, options) {
1645
2141
  }
1646
2142
  }
1647
2143
  console.error(result.stderr);
1648
- options.failOnError && process.exit(1);
2144
+ deferFailOnError(options, {
2145
+ ecosystem: "swift",
2146
+ tool: "swift",
2147
+ detail: "the swift package command failed",
2148
+ });
1649
2149
  }
1650
2150
  }
1651
2151
  }
@@ -1691,7 +2191,11 @@ export async function createCocoaBom(path, options) {
1691
2191
  "No 'Podfile.lock' found and '--no-install-deps' is set -- A Podfile.lock is needed to parse dependencies!",
1692
2192
  );
1693
2193
  missingLockWarningShown = true;
1694
- options.failOnError && process.exit(1);
2194
+ deferFailOnError(options, {
2195
+ ecosystem: "cocoa",
2196
+ tool: "pod",
2197
+ detail: "no Podfile.lock was found",
2198
+ });
1695
2199
  }
1696
2200
  } else if (options.deep && options.installDeps) {
1697
2201
  executePodCommand(["install"], projectPath, options);
@@ -2123,3 +2627,475 @@ export async function createGleamBom(path, options) {
2123
2627
 
2124
2628
  return {};
2125
2629
  }
2630
+
2631
+ /**
2632
+ * Function to create bom string for Julia projects.
2633
+ *
2634
+ * The resolved `Manifest.toml` is preferred over the declared `Project.toml`
2635
+ * so the BOM carries pinned versions and the package graph.
2636
+ *
2637
+ * @param {string} path to the project
2638
+ * @param {Object} options Parse options from the cli
2639
+ * @returns {Promise<Object>} BOM object
2640
+ */
2641
+ export async function createJuliaBom(path, options) {
2642
+ let pkgList = [];
2643
+ let dependencies = [];
2644
+ let parentComponent = {};
2645
+
2646
+ const projectTomlFiles = getAllFiles(
2647
+ path,
2648
+ `${options.multiProject ? "**/" : ""}Project.toml`,
2649
+ options,
2650
+ );
2651
+ const manifestOnly = !projectTomlFiles.length;
2652
+
2653
+ for (const projectTomlFile of projectTomlFiles) {
2654
+ if (DEBUG_MODE) {
2655
+ console.log(`Parsing ${projectTomlFile}`);
2656
+ }
2657
+ const baseDir = dirname(projectTomlFile);
2658
+ const manifestTomlFile = join(baseDir, "Manifest.toml");
2659
+ const result = parseJuliaProject(
2660
+ projectTomlFile,
2661
+ safeExistsSync(manifestTomlFile) ? manifestTomlFile : undefined,
2662
+ );
2663
+ collectProjectResult(result);
2664
+ }
2665
+ if (manifestOnly) {
2666
+ for (const manifestTomlFile of getAllFiles(
2667
+ path,
2668
+ `${options.multiProject ? "**/" : ""}Manifest.toml`,
2669
+ options,
2670
+ )) {
2671
+ if (DEBUG_MODE) {
2672
+ console.log(`Parsing ${manifestTomlFile}`);
2673
+ }
2674
+ // Gleam also writes a `manifest.toml`; on case-insensitive filesystems
2675
+ // the lookup above can surface it, so only accept files that parse as
2676
+ // a Julia manifest.
2677
+ const parsed = parseJuliaProject(undefined, manifestTomlFile);
2678
+ if (parsed.pkgList.length) {
2679
+ collectProjectResult(parsed);
2680
+ }
2681
+ }
2682
+ }
2683
+
2684
+ function collectProjectResult(result) {
2685
+ if (result.pkgList?.length) {
2686
+ pkgList = pkgList.concat(result.pkgList);
2687
+ }
2688
+ if (result.dependencies?.length) {
2689
+ dependencies = dependencies.concat(result.dependencies);
2690
+ }
2691
+ if (
2692
+ !Object.keys(parentComponent).length &&
2693
+ Object.keys(result.parentComponent).length
2694
+ ) {
2695
+ parentComponent = result.parentComponent;
2696
+ attachRootEdge(result, parentComponent, dependencies);
2697
+ }
2698
+ }
2699
+
2700
+ if (pkgList.length || Object.keys(parentComponent).length) {
2701
+ return buildBomNSData(options, pkgList, "julia", {
2702
+ src: path,
2703
+ filename: projectTomlFiles.join(", "),
2704
+ dependencies,
2705
+ parentComponent,
2706
+ });
2707
+ }
2708
+ return {};
2709
+ }
2710
+
2711
+ /**
2712
+ * Attach a root dependency edge from the parent component to its direct
2713
+ * dependencies, following the shape used by the Gleam and Nix flows.
2714
+ *
2715
+ * @param {{rootInputs?: string[]}} result Parser result with root inputs
2716
+ * @param {Object} parentComponent Parent component to give a bom-ref
2717
+ * @param {Object[]} dependencies Dependency edge list to append to
2718
+ */
2719
+ function attachRootEdge(result, parentComponent, dependencies) {
2720
+ if (result.rootInputs?.length && parentComponent.name) {
2721
+ const rootRef = `application:${parentComponent.name}:${parentComponent.version || "latest"}`;
2722
+ parentComponent["bom-ref"] = rootRef;
2723
+ dependencies.push({
2724
+ ref: rootRef,
2725
+ dependsOn: [...new Set(result.rootInputs)],
2726
+ });
2727
+ }
2728
+ }
2729
+
2730
+ /**
2731
+ * Function to create bom string for Erlang projects using rebar3.
2732
+ *
2733
+ * @param {string} path to the project
2734
+ * @param {Object} options Parse options from the cli
2735
+ * @returns {Promise<Object>} BOM object
2736
+ */
2737
+ export async function createErlangBom(path, options) {
2738
+ let pkgList = [];
2739
+ const rebarLockFiles = getAllFiles(
2740
+ path,
2741
+ `${options.multiProject ? "**/" : ""}rebar.lock`,
2742
+ options,
2743
+ );
2744
+ for (const rebarLockFile of rebarLockFiles) {
2745
+ if (DEBUG_MODE) {
2746
+ console.log(`Parsing ${rebarLockFile}`);
2747
+ }
2748
+ const result = parseRebarLock(rebarLockFile);
2749
+ if (result.pkgList?.length) {
2750
+ pkgList = pkgList.concat(result.pkgList);
2751
+ }
2752
+ }
2753
+ if (pkgList.length) {
2754
+ return buildBomNSData(options, pkgList, "erlang", {
2755
+ src: path,
2756
+ filename: rebarLockFiles.join(", "),
2757
+ });
2758
+ }
2759
+ return {};
2760
+ }
2761
+
2762
+ /**
2763
+ * Function to create bom string for OCaml projects using opam.
2764
+ *
2765
+ * Resolved `.opam.locked` files are preferred; a plain `.opam` file is used
2766
+ * as a declared fallback with unpinned versions.
2767
+ *
2768
+ * @param {string} path to the project
2769
+ * @param {Object} options Parse options from the cli
2770
+ * @returns {Promise<Object>} BOM object
2771
+ */
2772
+ export async function createOCamlBom(path, options) {
2773
+ let pkgList = [];
2774
+ const opamLockedFiles = getAllFiles(
2775
+ path,
2776
+ `${options.multiProject ? "**/" : ""}*.opam.locked`,
2777
+ options,
2778
+ );
2779
+ const opamFiles = opamLockedFiles.length
2780
+ ? []
2781
+ : getAllFiles(path, `${options.multiProject ? "**/" : ""}*.opam`, options);
2782
+ for (const opamFile of [...opamLockedFiles, ...opamFiles]) {
2783
+ if (DEBUG_MODE) {
2784
+ console.log(`Parsing ${opamFile}`);
2785
+ }
2786
+ const result = parseOpamLockedFile(opamFile);
2787
+ if (result.pkgList?.length) {
2788
+ pkgList = pkgList.concat(result.pkgList);
2789
+ }
2790
+ }
2791
+ if (pkgList.length) {
2792
+ return buildBomNSData(options, pkgList, "ocaml", {
2793
+ src: path,
2794
+ filename: [...opamLockedFiles, ...opamFiles].join(", "),
2795
+ });
2796
+ }
2797
+ return {};
2798
+ }
2799
+
2800
+ /**
2801
+ * Function to create bom string for Elm projects.
2802
+ *
2803
+ * @param {string} path to the project
2804
+ * @param {Object} options Parse options from the cli
2805
+ * @returns {Promise<Object>} BOM object
2806
+ */
2807
+ export async function createElmBom(path, options) {
2808
+ let pkgList = [];
2809
+ let dependencies = [];
2810
+ let parentComponent = {};
2811
+
2812
+ const elmJsonFiles = getAllFiles(
2813
+ path,
2814
+ `${options.multiProject ? "**/" : ""}elm.json`,
2815
+ options,
2816
+ ).filter(isElmProjectFile);
2817
+ // A single reader memoises the cache lookups shared by every manifest.
2818
+ const cacheReader = createElmCacheReader();
2819
+ for (const elmJsonFile of elmJsonFiles) {
2820
+ if (DEBUG_MODE) {
2821
+ console.log(`Parsing ${elmJsonFile}`);
2822
+ }
2823
+ const result = parseElmProject(elmJsonFile, cacheReader);
2824
+ if (result.pkgList?.length) {
2825
+ pkgList = pkgList.concat(result.pkgList);
2826
+ }
2827
+ if (result.dependencies?.length) {
2828
+ dependencies = dependencies.concat(result.dependencies);
2829
+ }
2830
+ if (!Object.keys(parentComponent).length) {
2831
+ parentComponent = result.parentComponent;
2832
+ attachRootEdge(result, parentComponent, dependencies);
2833
+ }
2834
+ }
2835
+ const legacyFiles = getAllFiles(
2836
+ path,
2837
+ `${options.multiProject ? "**/" : ""}elm-package.json`,
2838
+ options,
2839
+ );
2840
+ for (const legacyFile of legacyFiles) {
2841
+ if (DEBUG_MODE) {
2842
+ console.log(`Parsing ${legacyFile}`);
2843
+ }
2844
+ const baseDir = dirname(legacyFile);
2845
+ const exactDepsFile = join(baseDir, "elm-stuff", "exact-dependencies.json");
2846
+ const result = parseLegacyElmProject(
2847
+ legacyFile,
2848
+ safeExistsSync(exactDepsFile) ? exactDepsFile : undefined,
2849
+ );
2850
+ if (result.pkgList?.length) {
2851
+ pkgList = pkgList.concat(result.pkgList);
2852
+ }
2853
+ if (result.dependencies?.length) {
2854
+ dependencies = dependencies.concat(result.dependencies);
2855
+ }
2856
+ if (!Object.keys(parentComponent).length) {
2857
+ parentComponent = result.parentComponent;
2858
+ attachRootEdge(result, parentComponent, dependencies);
2859
+ }
2860
+ }
2861
+ if (pkgList.length && shouldFetchPackageMetadata()) {
2862
+ pkgList = await getElmMetadata(pkgList, dependencies);
2863
+ }
2864
+ if (pkgList.length || Object.keys(parentComponent).length) {
2865
+ return buildBomNSData(options, pkgList, "elm", {
2866
+ src: path,
2867
+ filename: [...elmJsonFiles, ...legacyFiles].join(", "),
2868
+ dependencies,
2869
+ parentComponent,
2870
+ });
2871
+ }
2872
+ return {};
2873
+ }
2874
+
2875
+ /**
2876
+ * Function to create bom string for Crystal projects using shards.
2877
+ *
2878
+ * @param {string} path to the project
2879
+ * @param {Object} options Parse options from the cli
2880
+ * @returns {Promise<Object>} BOM object
2881
+ */
2882
+ export async function createCrystalBom(path, options) {
2883
+ let pkgList = [];
2884
+ const dependencies = [];
2885
+ let parentComponent = {};
2886
+
2887
+ const shardYmlFiles = getAllFiles(
2888
+ path,
2889
+ `${options.multiProject ? "**/" : ""}shard.yml`,
2890
+ options,
2891
+ );
2892
+ for (const shardYmlFile of shardYmlFiles) {
2893
+ if (DEBUG_MODE) {
2894
+ console.log(`Parsing ${shardYmlFile}`);
2895
+ }
2896
+ const baseDir = dirname(shardYmlFile);
2897
+ const shardLockFile = join(baseDir, "shard.lock");
2898
+ const result = parseShardsProject(
2899
+ shardYmlFile,
2900
+ safeExistsSync(shardLockFile) ? shardLockFile : undefined,
2901
+ );
2902
+ if (result.pkgList?.length) {
2903
+ pkgList = pkgList.concat(result.pkgList);
2904
+ }
2905
+ if (!Object.keys(parentComponent).length) {
2906
+ parentComponent = result.parentComponent;
2907
+ attachRootEdge(result, parentComponent, dependencies);
2908
+ }
2909
+ }
2910
+ if (pkgList.length || Object.keys(parentComponent).length) {
2911
+ return buildBomNSData(options, pkgList, "crystal", {
2912
+ src: path,
2913
+ filename: shardYmlFiles.join(", "),
2914
+ dependencies,
2915
+ parentComponent,
2916
+ });
2917
+ }
2918
+ return {};
2919
+ }
2920
+
2921
+ /**
2922
+ * Function to create bom string for Nim projects using nimble.
2923
+ *
2924
+ * @param {string} path to the project
2925
+ * @param {Object} options Parse options from the cli
2926
+ * @returns {Promise<Object>} BOM object
2927
+ */
2928
+ export async function createNimBom(path, options) {
2929
+ let pkgList = [];
2930
+ let dependencies = [];
2931
+ let parentComponent = {};
2932
+
2933
+ const nimbleFiles = getAllFiles(
2934
+ path,
2935
+ `${options.multiProject ? "**/" : ""}*.nimble`,
2936
+ options,
2937
+ );
2938
+ for (const nimbleFile of nimbleFiles) {
2939
+ if (DEBUG_MODE) {
2940
+ console.log(`Parsing ${nimbleFile}`);
2941
+ }
2942
+ const baseDir = dirname(nimbleFile);
2943
+ const nimbleLockFile = join(baseDir, "nimble.lock");
2944
+ const result = parseNimProject(
2945
+ nimbleFile,
2946
+ safeExistsSync(nimbleLockFile) ? nimbleLockFile : undefined,
2947
+ );
2948
+ if (result.pkgList?.length) {
2949
+ pkgList = pkgList.concat(result.pkgList);
2950
+ }
2951
+ if (result.dependencies?.length) {
2952
+ dependencies = dependencies.concat(result.dependencies);
2953
+ }
2954
+ if (!Object.keys(parentComponent).length) {
2955
+ parentComponent = result.parentComponent;
2956
+ attachRootEdge(result, parentComponent, dependencies);
2957
+ }
2958
+ }
2959
+ if (pkgList.length || Object.keys(parentComponent).length) {
2960
+ return buildBomNSData(options, pkgList, "nim", {
2961
+ src: path,
2962
+ filename: nimbleFiles.join(", "),
2963
+ dependencies,
2964
+ parentComponent,
2965
+ });
2966
+ }
2967
+ return {};
2968
+ }
2969
+
2970
+ /**
2971
+ * Function to create bom string for Lua projects using LuaRocks.
2972
+ *
2973
+ * When a `luarocks.lock` is present next to a rockspec it supplies the
2974
+ * resolved versions; otherwise the rockspec's declared dependencies are
2975
+ * inventoried without versions.
2976
+ *
2977
+ * @param {string} path to the project
2978
+ * @param {Object} options Parse options from the cli
2979
+ * @returns {Promise<Object>} BOM object
2980
+ */
2981
+ export async function createLuaBom(path, options) {
2982
+ let pkgList = [];
2983
+ let parentComponent = {};
2984
+ const seenLockFiles = new Set();
2985
+ const rockspecFiles = getAllFiles(
2986
+ path,
2987
+ `${options.multiProject ? "**/" : ""}*.rockspec`,
2988
+ options,
2989
+ );
2990
+ for (const rockspecFile of rockspecFiles) {
2991
+ if (DEBUG_MODE) {
2992
+ console.log(`Parsing ${rockspecFile}`);
2993
+ }
2994
+ const baseDir = dirname(rockspecFile);
2995
+ const lockFile = join(baseDir, "luarocks.lock");
2996
+ // The rockspec names the project and its declared set either way; the
2997
+ // lock, when present, supplies the resolved versions.
2998
+ const declared = parseRockspecFile(rockspecFile);
2999
+ if (!Object.keys(parentComponent).length) {
3000
+ parentComponent = declared.parentComponent;
3001
+ }
3002
+ let resolved;
3003
+ if (safeExistsSync(lockFile)) {
3004
+ seenLockFiles.add(lockFile);
3005
+ resolved = parseLuaRocksLock(lockFile, rockspecFile);
3006
+ }
3007
+ const chosen = resolved?.pkgList?.length ? resolved : declared;
3008
+ if (chosen.pkgList?.length) {
3009
+ pkgList = pkgList.concat(chosen.pkgList);
3010
+ }
3011
+ }
3012
+ // A lock can also stand alone, in a project whose rockspec is generated at
3013
+ // release time rather than committed.
3014
+ for (const lockFile of getAllFiles(
3015
+ path,
3016
+ `${options.multiProject ? "**/" : ""}luarocks.lock`,
3017
+ options,
3018
+ )) {
3019
+ if (seenLockFiles.has(lockFile)) {
3020
+ continue;
3021
+ }
3022
+ if (DEBUG_MODE) {
3023
+ console.log(`Parsing ${lockFile}`);
3024
+ }
3025
+ const result = parseLuaRocksLock(lockFile);
3026
+ if (result.pkgList?.length) {
3027
+ pkgList = pkgList.concat(result.pkgList);
3028
+ }
3029
+ }
3030
+ if (pkgList.length) {
3031
+ return buildBomNSData(options, pkgList, "lua", {
3032
+ src: path,
3033
+ filename: rockspecFiles.join(", "),
3034
+ parentComponent,
3035
+ });
3036
+ }
3037
+ return {};
3038
+ }
3039
+
3040
+ /**
3041
+ * Function to create bom string for Spack environments.
3042
+ *
3043
+ * @param {string} path to the project
3044
+ * @param {Object} options Parse options from the cli
3045
+ * @returns {Promise<Object>} BOM object
3046
+ */
3047
+ export async function createSpackBom(path, options) {
3048
+ let pkgList = [];
3049
+ let dependencies = [];
3050
+ let rootInputs = [];
3051
+ const spackLockFiles = getAllFiles(
3052
+ path,
3053
+ `${options.multiProject ? "**/" : ""}spack.lock`,
3054
+ options,
3055
+ );
3056
+ for (const spackLockFile of spackLockFiles) {
3057
+ if (DEBUG_MODE) {
3058
+ console.log(`Parsing ${spackLockFile}`);
3059
+ }
3060
+ const result = parseSpackLock(spackLockFile);
3061
+ if (result.pkgList?.length) {
3062
+ pkgList = pkgList.concat(result.pkgList);
3063
+ }
3064
+ if (result.dependencies?.length) {
3065
+ dependencies = dependencies.concat(result.dependencies);
3066
+ }
3067
+ if (result.rootInputs?.length) {
3068
+ rootInputs = rootInputs.concat(result.rootInputs);
3069
+ }
3070
+ }
3071
+ if (!pkgList.length) {
3072
+ return {};
3073
+ }
3074
+ // The lock names no project component, so the environment directory is the
3075
+ // name the environment is already known by. Its roots are the specs the
3076
+ // lock records as requested, which is why they are linked explicitly rather
3077
+ // than inferred from which nodes have no incoming edge: a build-only tool
3078
+ // such as gmake also has none, and it is not a root.
3079
+ const envName = basename(resolve(path));
3080
+ const parentComponent = {
3081
+ type: "application",
3082
+ name: envName,
3083
+ "bom-ref": `application:${envName}:latest`,
3084
+ description: `Spack environment: ${envName}`,
3085
+ properties: [
3086
+ { name: "internal:SrcFile", value: spackLockFiles.join(", ") },
3087
+ ],
3088
+ };
3089
+ if (rootInputs.length) {
3090
+ dependencies.push({
3091
+ ref: parentComponent["bom-ref"],
3092
+ dependsOn: [...new Set(rootInputs)].sort(),
3093
+ });
3094
+ }
3095
+ return buildBomNSData(options, pkgList, "spack", {
3096
+ src: path,
3097
+ filename: spackLockFiles.join(", "),
3098
+ dependencies,
3099
+ parentComponent,
3100
+ });
3101
+ }