@cdxgen/cdxgen 13.1.0 → 13.3.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 (283) hide show
  1. package/README.md +32 -15
  2. package/bin/cdxgen.js +243 -122
  3. package/bin/evinse.js +7 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/crypto-oid.json +88 -12
  9. package/data/remediations.json +47 -0
  10. package/data/rules/ai-bom.yaml +78 -0
  11. package/data/rules/cargo-rust.yaml +132 -0
  12. package/data/rules/mcp-servers.yaml +122 -0
  13. package/lib/audit/targets.js +6 -1
  14. package/lib/cli/bomAssembly.js +117 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +235 -5
  17. package/lib/cli/jsBom.js +108 -26
  18. package/lib/cli/jvmBom.js +68 -12
  19. package/lib/cli/managedBom.js +264 -41
  20. package/lib/cli/nativeBom.js +923 -15
  21. package/lib/core/activity.js +120 -2
  22. package/lib/core/env.js +44 -7
  23. package/lib/core/fs.js +174 -6
  24. package/lib/core/metadataProperties.js +97 -0
  25. package/lib/core/projectConfig.js +31 -4
  26. package/lib/core/userAgent.js +151 -0
  27. package/lib/ecosystems/bunutils.js +269 -101
  28. package/lib/ecosystems/cargoCache.js +364 -0
  29. package/lib/ecosystems/caxa.js +447 -7
  30. package/lib/ecosystems/cmakeResolver.js +66 -0
  31. package/lib/ecosystems/cppEvidence.js +104 -33
  32. package/lib/ecosystems/ecosystems.js +260 -99
  33. package/lib/ecosystems/golem.js +11 -1
  34. package/lib/ecosystems/gradleutils.js +185 -9
  35. package/lib/ecosystems/jsEvidence.js +19 -20
  36. package/lib/ecosystems/kosi.js +1263 -0
  37. package/lib/ecosystems/npmutils.js +110 -9
  38. package/lib/ecosystems/parsers-crystal.js +179 -0
  39. package/lib/ecosystems/parsers-dotnet.js +120 -49
  40. package/lib/ecosystems/parsers-elm.js +816 -0
  41. package/lib/ecosystems/parsers-gleam.js +1 -1
  42. package/lib/ecosystems/parsers-go.js +47 -9
  43. package/lib/ecosystems/parsers-js.js +171 -50
  44. package/lib/ecosystems/parsers-julia.js +268 -0
  45. package/lib/ecosystems/parsers-lua.js +218 -0
  46. package/lib/ecosystems/parsers-misc.js +919 -161
  47. package/lib/ecosystems/parsers-nim.js +221 -0
  48. package/lib/ecosystems/parsers-opam.js +180 -0
  49. package/lib/ecosystems/parsers-perl.js +277 -0
  50. package/lib/ecosystems/parsers-python.js +27 -15
  51. package/lib/ecosystems/parsers-r.js +148 -0
  52. package/lib/ecosystems/parsers-rebar.js +411 -0
  53. package/lib/ecosystems/parsers-rust.js +954 -33
  54. package/lib/ecosystems/parsers-spack.js +204 -0
  55. package/lib/ecosystems/parsers-stack.js +264 -0
  56. package/lib/ecosystems/parsers-terraform.js +194 -0
  57. package/lib/ecosystems/rubyutils.js +1426 -166
  58. package/lib/ecosystems/rusi.js +158 -7
  59. package/lib/evinser/evinser.js +547 -129
  60. package/lib/evinser/swiftsem.js +1914 -302
  61. package/lib/helpers/agenticToolutils.js +925 -0
  62. package/lib/helpers/bomSigner.js +652 -221
  63. package/lib/inventory/agentFormulationParser.js +57 -4
  64. package/lib/inventory/aiCollector.js +504 -10
  65. package/lib/inventory/aiInventory.js +12 -0
  66. package/lib/inventory/analyzer.js +488 -30
  67. package/lib/inventory/astWorker.js +6 -0
  68. package/lib/inventory/atomUtils.js +568 -31
  69. package/lib/inventory/auditCategories.js +8 -0
  70. package/lib/inventory/cbomutils.js +30 -0
  71. package/lib/inventory/cdxrs.js +110 -14
  72. package/lib/inventory/communityAiConfigParser.js +215 -2
  73. package/lib/inventory/depsUtils.js +183 -11
  74. package/lib/inventory/display.js +4 -5
  75. package/lib/inventory/dosai.js +627 -13
  76. package/lib/inventory/dynamic.js +39 -1
  77. package/lib/inventory/envcontext.js +9 -6
  78. package/lib/inventory/evidenceUtils.js +26 -5
  79. package/lib/inventory/fetchBatch.js +190 -19
  80. package/lib/inventory/fetchRate.js +74 -11
  81. package/lib/inventory/forgeEnricher.js +3 -2
  82. package/lib/inventory/jvmToolEnv.js +6 -5
  83. package/lib/inventory/mcpConfigParser.js +166 -2
  84. package/lib/inventory/mcpDiscovery.js +11 -0
  85. package/lib/inventory/mcpServerManifest.js +389 -0
  86. package/lib/inventory/mcpbBundle.js +419 -0
  87. package/lib/inventory/plugins.js +3 -0
  88. package/lib/inventory/protobom.js +62 -89
  89. package/lib/inventory/protobomLoader.js +4 -0
  90. package/lib/inventory/purl.js +152 -3
  91. package/lib/inventory/rustFormulationParser.js +1 -1
  92. package/lib/inventory/sbomDocument.js +2 -70
  93. package/lib/inventory/source.js +47 -23
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/traceRunner.js +210 -21
  97. package/lib/managers/binary.js +115 -24
  98. package/lib/managers/docker.js +440 -20
  99. package/lib/managers/oci.js +39 -1
  100. package/lib/parsers/cmakeLists.js +452 -0
  101. package/lib/parsers/huggingfaceUtils.js +15 -7
  102. package/lib/parsers/largeJson.js +454 -0
  103. package/lib/parsers/npmrc.js +19 -1
  104. package/lib/parsers/onnx.js +294 -0
  105. package/lib/parsers/safetensors.js +114 -0
  106. package/lib/stages/postgen/introspection/markers.js +29 -6
  107. package/lib/stages/postgen/postgen.js +237 -16
  108. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  109. package/lib/validator/bomValidator.js +6 -16
  110. package/lib/validator/index.js +48 -10
  111. package/lib/validator/reporters/console.js +3 -0
  112. package/lib/validator/reporters/json.js +1 -0
  113. package/lib/validator/reporters/sarif.js +7 -0
  114. package/package.json +27 -27
  115. package/types/lib/audit/targets.d.ts.map +1 -1
  116. package/types/lib/cli/bomAssembly.d.ts +2 -1
  117. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  118. package/types/lib/cli/cliOptions.d.ts +33 -1
  119. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  120. package/types/lib/cli/index.d.ts +2 -2
  121. package/types/lib/cli/index.d.ts.map +1 -1
  122. package/types/lib/cli/jsBom.d.ts +23 -0
  123. package/types/lib/cli/jsBom.d.ts.map +1 -1
  124. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  125. package/types/lib/cli/managedBom.d.ts +33 -0
  126. package/types/lib/cli/managedBom.d.ts.map +1 -1
  127. package/types/lib/cli/nativeBom.d.ts +88 -8
  128. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  129. package/types/lib/core/activity.d.ts.map +1 -1
  130. package/types/lib/core/env.d.ts +21 -0
  131. package/types/lib/core/env.d.ts.map +1 -1
  132. package/types/lib/core/fs.d.ts +55 -0
  133. package/types/lib/core/fs.d.ts.map +1 -1
  134. package/types/lib/core/metadataProperties.d.ts +39 -0
  135. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  136. package/types/lib/core/projectConfig.d.ts +6 -2
  137. package/types/lib/core/projectConfig.d.ts.map +1 -1
  138. package/types/lib/core/userAgent.d.ts +56 -0
  139. package/types/lib/core/userAgent.d.ts.map +1 -0
  140. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  141. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  143. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  144. package/types/lib/ecosystems/caxa.d.ts +14 -0
  145. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  146. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  147. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  148. package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
  149. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  150. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  151. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  152. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  153. package/types/lib/ecosystems/gradleutils.d.ts +40 -4
  154. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  155. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  156. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  157. package/types/lib/ecosystems/kosi.d.ts +86 -0
  158. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  159. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  160. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  161. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  162. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  163. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  164. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  165. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  166. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  167. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  168. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  169. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  170. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  171. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  172. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  173. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  174. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
  176. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  177. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  178. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  179. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  180. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  181. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  182. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  183. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  184. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  185. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  187. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  188. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  189. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  191. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  192. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  193. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  194. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  195. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  196. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  197. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  198. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  199. package/types/lib/ecosystems/rusi.d.ts +13 -0
  200. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  201. package/types/lib/evinser/evinser.d.ts +81 -2
  202. package/types/lib/evinser/evinser.d.ts.map +1 -1
  203. package/types/lib/evinser/swiftsem.d.ts +280 -15
  204. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  205. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  206. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  207. package/types/lib/helpers/bomSigner.d.ts +106 -13
  208. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  209. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  210. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  211. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  212. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  213. package/types/lib/inventory/atomUtils.d.ts +145 -3
  214. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +4 -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/cdxrs.d.ts +8 -3
  219. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  220. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  221. package/types/lib/inventory/depsUtils.d.ts +15 -0
  222. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/display.d.ts.map +1 -1
  224. package/types/lib/inventory/dosai.d.ts +87 -1
  225. package/types/lib/inventory/dosai.d.ts.map +1 -1
  226. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  227. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  228. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  229. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  230. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  231. package/types/lib/inventory/fetchRate.d.ts +9 -6
  232. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  233. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  234. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  235. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  236. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  237. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  238. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  239. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  240. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  241. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  242. package/types/lib/inventory/plugins.d.ts.map +1 -1
  243. package/types/lib/inventory/protobom.d.ts +18 -13
  244. package/types/lib/inventory/protobom.d.ts.map +1 -1
  245. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  246. package/types/lib/inventory/purl.d.ts +71 -2
  247. package/types/lib/inventory/purl.d.ts.map +1 -1
  248. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  249. package/types/lib/inventory/source.d.ts.map +1 -1
  250. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  251. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  252. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  253. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  254. package/types/lib/inventory/traceRunner.d.ts +47 -1
  255. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  256. package/types/lib/managers/binary.d.ts +30 -1
  257. package/types/lib/managers/binary.d.ts.map +1 -1
  258. package/types/lib/managers/docker.d.ts +29 -0
  259. package/types/lib/managers/docker.d.ts.map +1 -1
  260. package/types/lib/managers/oci.d.ts +14 -0
  261. package/types/lib/managers/oci.d.ts.map +1 -1
  262. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  263. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  264. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  265. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  266. package/types/lib/parsers/largeJson.d.ts +52 -0
  267. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  268. package/types/lib/parsers/npmrc.d.ts +5 -0
  269. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  270. package/types/lib/parsers/onnx.d.ts +28 -0
  271. package/types/lib/parsers/onnx.d.ts.map +1 -0
  272. package/types/lib/parsers/safetensors.d.ts +22 -0
  273. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  275. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  276. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  277. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  278. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  279. package/types/lib/validator/index.d.ts +10 -1
  280. package/types/lib/validator/index.d.ts.map +1 -1
  281. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  282. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  283. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -12,11 +12,13 @@ import { recordDegradation } from "../core/buildLedger.js";
12
12
  import { deferFailOnError } from "../core/deferredExit.js";
13
13
  import {
14
14
  CARGO_CMD,
15
+ CARGO_TARGET,
15
16
  CLJ_CMD,
16
17
  hasAnyProjectType,
17
18
  LEIN_CMD,
18
19
  SWIFT_CMD,
19
20
  shouldFetchLicense,
21
+ shouldFetchPackageMetadata,
20
22
  } from "../core/env.js";
21
23
  import {
22
24
  checksumFile,
@@ -29,15 +31,26 @@ import { dirNameStr, isMac } from "../core/paths.js";
29
31
  import {
30
32
  buildDependentPurl,
31
33
  collapseCmakeVersions,
34
+ preferFetchedCmakeDependencies,
32
35
  resolveCmakeContext,
36
+ withoutScrapedFetchDependency,
33
37
  } from "../ecosystems/cmakeResolver.js";
34
38
  import { getCppModules } from "../ecosystems/cppEvidence.js";
35
39
  import {
36
40
  getCratesMetadata,
37
41
  getSwiftPackageMetadata,
38
42
  } from "../ecosystems/ecosystems.js";
43
+ import { parseShardsProject } from "../ecosystems/parsers-crystal.js";
44
+ import {
45
+ createElmCacheReader,
46
+ getElmMetadata,
47
+ isElmProjectFile,
48
+ parseElmProject,
49
+ parseLegacyElmProject,
50
+ } from "../ecosystems/parsers-elm.js";
39
51
  import { parseGleamProject } from "../ecosystems/parsers-gleam.js";
40
52
  import {
53
+ isGoModWhyNotNeeded,
41
54
  parseGoListDep,
42
55
  parseGoModData,
43
56
  parseGoModGraph,
@@ -46,14 +59,20 @@ import {
46
59
  parseGopkgData,
47
60
  parseGosumData,
48
61
  } from "../ecosystems/parsers-go.js";
62
+ import { parseJuliaProject } from "../ecosystems/parsers-julia.js";
49
63
  import {
50
64
  parseCljDep,
51
65
  parseEdnData,
52
66
  parseLeinDep,
53
67
  parseLeiningenData,
54
68
  } from "../ecosystems/parsers-jvm.js";
69
+ import {
70
+ parseLuaRocksLock,
71
+ parseRockspecFile,
72
+ } from "../ecosystems/parsers-lua.js";
55
73
  import {
56
74
  buildObjectForCocoaPod,
75
+ classifySwiftDependencyScopes,
57
76
  executeAlpmList,
58
77
  executeApkList,
59
78
  executeDpkgList,
@@ -78,12 +97,24 @@ import {
78
97
  parseSwiftResolved,
79
98
  prefetchCocoaPodspecs,
80
99
  } from "../ecosystems/parsers-misc.js";
100
+ import { parseNimProject } from "../ecosystems/parsers-nim.js";
101
+ import { parseOpamLockedFile } from "../ecosystems/parsers-opam.js";
102
+ import { parseRebarLock } from "../ecosystems/parsers-rebar.js";
81
103
  import {
104
+ applyCargoDependencyKindMetadata,
105
+ buildCargoLockDependencyGraph,
106
+ buildCargoNativeLibraryComponents,
107
+ cargoComponentKey,
82
108
  parseCargoData,
83
109
  parseCargoDependencyData,
84
110
  parseCargoManifestDependencyData,
111
+ parseCargoManifestDependencyKinds,
112
+ parseCargoMetadataResolve,
85
113
  parseCargoTomlData,
114
+ rollupCargoDependencyKinds,
86
115
  } from "../ecosystems/parsers-rust.js";
116
+ import { parseSpackLock } from "../ecosystems/parsers-spack.js";
117
+ import { parseStackProject } from "../ecosystems/parsers-stack.js";
87
118
  import { resolveZigGraph } from "../ecosystems/zigResolver.js";
88
119
  import { mergeDependencies, trimComponents } from "../inventory/depsUtils.js";
89
120
  import { convertOSQueryResults } from "../inventory/evidenceUtils.js";
@@ -169,7 +200,6 @@ export function getCargoCacheDir() {
169
200
  export async function createGoBom(path, options) {
170
201
  let pkgList = [];
171
202
  let dependencies = [];
172
- const allImports = {};
173
203
  let parentComponent = createDefaultParentComponent(path, "golang", options);
174
204
  // Is this a binary file
175
205
  let maybeBinary;
@@ -209,6 +239,8 @@ export async function createGoBom(path, options) {
209
239
  }
210
240
  }
211
241
  const doneList = {};
242
+ const requiredModules = new Set();
243
+ const unneededModules = new Set();
212
244
  let circuitBreak = false;
213
245
  if (DEBUG_MODE) {
214
246
  console.log(
@@ -222,12 +254,12 @@ export async function createGoBom(path, options) {
222
254
  }
223
255
  const pkgFullName = `${apkg.name}`;
224
256
  if (apkg.scope === "required") {
225
- allImports[pkgFullName] = true;
257
+ requiredModules.add(pkgFullName);
226
258
  continue;
227
259
  }
228
260
  if (
229
261
  apkg.scope === "optional" ||
230
- allImports[pkgFullName] ||
262
+ requiredModules.has(pkgFullName) ||
231
263
  doneList[pkgFullName]
232
264
  ) {
233
265
  continue;
@@ -256,19 +288,40 @@ export async function createGoBom(path, options) {
256
288
  const mstdout = mresult.stdout;
257
289
  if (mstdout) {
258
290
  const cmdOutput = Buffer.from(mstdout).toString();
259
- const whyPkg = parseGoModWhy(cmdOutput);
260
- // whyPkg would include this package string
261
- // github.com/golang/protobuf/proto github.com/golang/protobuf
262
- // golang.org/x/tools/cmd/goimports golang.org/x/tools
263
- if (whyPkg?.includes(pkgFullName)) {
264
- allImports[pkgFullName] = true;
291
+ if (isGoModWhyNotNeeded(cmdOutput)) {
292
+ unneededModules.add(pkgFullName);
293
+ } else {
294
+ // The last line of the import chain is a package of the module:
295
+ // github.com/golang/protobuf/proto for github.com/golang/protobuf
296
+ const whyPkg = parseGoModWhy(cmdOutput);
297
+ if (
298
+ whyPkg === pkgFullName ||
299
+ whyPkg?.startsWith(`${pkgFullName}/`)
300
+ ) {
301
+ requiredModules.add(pkgFullName);
302
+ }
265
303
  }
266
304
  doneList[pkgFullName] = true;
267
305
  }
268
306
  }
269
307
  }
308
+ // A module go reports as unneeded is optional, but only when every module
309
+ // was checked: once go mod why fails, the answers are incomplete, and a
310
+ // tool failure is not evidence that a module is unused.
311
+ for (const apkg of pkgList) {
312
+ if (apkg.scope) {
313
+ continue;
314
+ }
315
+ if (requiredModules.has(apkg.name)) {
316
+ apkg.scope = "required";
317
+ } else if (!circuitBreak && unneededModules.has(apkg.name)) {
318
+ apkg.scope = "optional";
319
+ }
320
+ }
270
321
  if (DEBUG_MODE) {
271
- console.log(`Required packages: ${Object.keys(allImports).length}`);
322
+ console.log(
323
+ `Required packages: ${requiredModules.size}, not needed: ${circuitBreak ? "unknown" : unneededModules.size}`,
324
+ );
272
325
  }
273
326
  return buildBomNSData(options, pkgList, "golang", {
274
327
  src: path,
@@ -533,7 +586,6 @@ export async function createGoBom(path, options) {
533
586
  }
534
587
  if (pkgList.length && !shouldManuallyParse) {
535
588
  return buildBomNSData(options, pkgList, "golang", {
536
- allImports,
537
589
  dependencies,
538
590
  parentComponent,
539
591
  src: path,
@@ -664,6 +716,223 @@ function cargoAnswersVersion(cwd) {
664
716
  * @param {Object} options Parse options from the cli
665
717
  * @returns {Promise<Object|undefined>} Promise resolving to a BOM object or undefined
666
718
  */
719
+ /**
720
+ * Run `cargo metadata` for a manifest and return its parsed resolve graph.
721
+ *
722
+ * The lock file is the union of every dependency kind and every target triple,
723
+ * so it cannot answer which crates are dev-only or platform-gated. Only
724
+ * `cargo metadata` carries that, per dependency edge, in `resolve.nodes[].deps[].dep_kinds`.
725
+ *
726
+ * @param {string} cargoTomlFile Manifest path
727
+ * @param {string} [filterPlatform] Target triple to resolve the graph for
728
+ * @returns {object|undefined} Parsed `cargo metadata` output
729
+ */
730
+ function runCargoMetadata(cargoTomlFile, filterPlatform) {
731
+ const basePath = dirname(cargoTomlFile);
732
+ const platformArgs = filterPlatform
733
+ ? ["--filter-platform", filterPlatform]
734
+ : [];
735
+ // `--locked` keeps the user's lock file untouched; a workspace that is out of
736
+ // date with its lock file needs the retry without it.
737
+ for (const lockArgs of [["--locked"], []]) {
738
+ const cargoArgs = [
739
+ "metadata",
740
+ "--format-version",
741
+ "1",
742
+ "--manifest-path",
743
+ cargoTomlFile,
744
+ ...platformArgs,
745
+ ...lockArgs,
746
+ ];
747
+ if (DEBUG_MODE) {
748
+ console.log("Executing", CARGO_CMD, cargoArgs.join(" "), "in", basePath);
749
+ }
750
+ const result = safeSpawnSync(CARGO_CMD, cargoArgs, {
751
+ cwd: basePath,
752
+ shell: isWin,
753
+ });
754
+ if (result.status === 0 && !result.error && result.stdout) {
755
+ try {
756
+ return JSON.parse(result.stdout);
757
+ } catch (err) {
758
+ if (DEBUG_MODE) {
759
+ console.log(`Unable to parse the cargo metadata output: ${err}`);
760
+ }
761
+ return undefined;
762
+ }
763
+ }
764
+ if (DEBUG_MODE) {
765
+ console.log(result.error, result.stderr);
766
+ }
767
+ }
768
+ return undefined;
769
+ }
770
+
771
+ /**
772
+ * Annotate cargo components with their dependency kind, platform gate and
773
+ * scope.
774
+ *
775
+ * With `--deep`, the edge kinds come from `cargo metadata` and are exact at
776
+ * every depth. Otherwise they are recovered from the manifests, which is exact
777
+ * for the dependencies a workspace member declares and inherited below them.
778
+ *
779
+ * @param {object[]} pkgList Components to annotate
780
+ * @param {object[]} dependencyTree Lockfile dependency relationships
781
+ * @param {string[]} cargoFiles Manifest paths
782
+ * @param {Map<string, string>} manifestRootKeys Manifest path to root component key
783
+ * @param {object} options CLI options
784
+ */
785
+ function annotateCargoDependencyKinds(
786
+ pkgList,
787
+ dependencyTree,
788
+ cargoFiles,
789
+ manifestRootKeys,
790
+ options,
791
+ ) {
792
+ if (!pkgList?.length) {
793
+ return;
794
+ }
795
+ const allEdges = new Map();
796
+ if (options?.deep) {
797
+ const rollups = new Map();
798
+ const packageInfo = new Map();
799
+ const visitedWorkspaceRoots = new Set();
800
+ // The keys that survive resolution for the requested target triple. The
801
+ // unfiltered graph still supplies the kinds, because a crate's kind does
802
+ // not change with the triple.
803
+ const targetIncludedKeys = CARGO_TARGET ? new Set() : undefined;
804
+ let targetFilterApplied = !CARGO_TARGET;
805
+ let anyMetadata = false;
806
+ let everyMetadata = true;
807
+ for (const cargoTomlFile of cargoFiles) {
808
+ const metadata = runCargoMetadata(cargoTomlFile);
809
+ if (!metadata) {
810
+ everyMetadata = false;
811
+ continue;
812
+ }
813
+ // Every member of a workspace answers with the same resolve graph.
814
+ const workspaceRoot = metadata.workspace_root || cargoTomlFile;
815
+ if (visitedWorkspaceRoots.has(workspaceRoot)) {
816
+ anyMetadata = true;
817
+ continue;
818
+ }
819
+ visitedWorkspaceRoots.add(workspaceRoot);
820
+ anyMetadata = true;
821
+ if (CARGO_TARGET) {
822
+ const targetMetadata = runCargoMetadata(cargoTomlFile, CARGO_TARGET);
823
+ if (targetMetadata) {
824
+ for (const key of parseCargoMetadataResolve(targetMetadata)
825
+ .resolvedKeys) {
826
+ targetIncludedKeys.add(key);
827
+ }
828
+ targetFilterApplied = true;
829
+ } else {
830
+ console.warn(
831
+ `cargo metadata could not resolve the dependency graph for the target ${CARGO_TARGET}. The components are not filtered by target.`,
832
+ );
833
+ }
834
+ }
835
+ const graph = parseCargoMetadataResolve(metadata);
836
+ for (const [key, value] of graph.packageInfo) {
837
+ packageInfo.set(key, value);
838
+ }
839
+ for (const [key, value] of graph.edges) {
840
+ allEdges.set(key, [...(allEdges.get(key) || []), ...value]);
841
+ }
842
+ for (const [key, value] of rollupCargoDependencyKinds(graph)) {
843
+ const existing = rollups.get(key);
844
+ if (!existing) {
845
+ rollups.set(key, value);
846
+ continue;
847
+ }
848
+ // A crate that is a runtime dependency of any workspace in the scan is
849
+ // a runtime dependency of the BOM.
850
+ for (const kind of value.kinds) {
851
+ existing.kinds.add(kind);
852
+ }
853
+ for (const target of value.targets) {
854
+ existing.targets.add(target);
855
+ }
856
+ const precedence = ["root", "runtime", "build", "dev"];
857
+ if (
858
+ precedence.indexOf(value.kind) < precedence.indexOf(existing.kind)
859
+ ) {
860
+ existing.kind = value.kind;
861
+ existing.targetGated = value.targetGated;
862
+ } else if (value.kind === existing.kind && !value.targetGated) {
863
+ existing.targetGated = false;
864
+ }
865
+ }
866
+ }
867
+ if (anyMetadata && rollups.size) {
868
+ applyCargoDependencyKindMetadata(pkgList, rollups, packageInfo, {
869
+ resolveIsComplete: everyMetadata,
870
+ filterPlatform: targetFilterApplied ? CARGO_TARGET : undefined,
871
+ targetIncludedKeys: targetFilterApplied
872
+ ? targetIncludedKeys
873
+ : undefined,
874
+ });
875
+ // A `-sys` crate puts a C library into the artifact that the cargo
876
+ // dependency graph itself cannot describe.
877
+ const nativeLibraries = buildCargoNativeLibraryComponents(
878
+ packageInfo,
879
+ allEdges,
880
+ rollups,
881
+ );
882
+ if (nativeLibraries.components.length) {
883
+ pkgList.push(...nativeLibraries.components);
884
+ if (dependencyTree) {
885
+ dependencyTree.push(...nativeLibraries.dependencies);
886
+ }
887
+ }
888
+ return;
889
+ }
890
+ recordDegradation("rust.cargo-metadata-unavailable", {
891
+ ecosystem: "rust",
892
+ tool: "cargo",
893
+ impact: "components",
894
+ command: `${basename(CARGO_CMD)} metadata`,
895
+ detail:
896
+ "cargo metadata could not be executed, so dependency kinds and platform gates were recovered from the manifests alone.",
897
+ });
898
+ }
899
+ if (!dependencyTree?.length || !manifestRootKeys.size) {
900
+ return;
901
+ }
902
+ const declaredEdgesByRoot = new Map();
903
+ for (const [cargoTomlFile, rootKey] of manifestRootKeys) {
904
+ if (!rootKey) {
905
+ continue;
906
+ }
907
+ const declaredEdges = parseCargoManifestDependencyKinds(cargoTomlFile, {
908
+ includeWorkspaceMembers: !options.multiProject,
909
+ });
910
+ const existing = declaredEdgesByRoot.get(rootKey);
911
+ if (!existing) {
912
+ declaredEdgesByRoot.set(rootKey, declaredEdges);
913
+ continue;
914
+ }
915
+ for (const [name, value] of declaredEdges) {
916
+ const knownEdge = existing.get(name);
917
+ if (!knownEdge) {
918
+ existing.set(name, value);
919
+ continue;
920
+ }
921
+ for (const kind of value.kinds) {
922
+ knownEdge.kinds.add(kind);
923
+ }
924
+ for (const target of value.targets) {
925
+ knownEdge.targets.add(target);
926
+ }
927
+ }
928
+ }
929
+ const graph = buildCargoLockDependencyGraph(
930
+ dependencyTree,
931
+ declaredEdgesByRoot,
932
+ );
933
+ applyCargoDependencyKindMetadata(pkgList, rollupCargoDependencyKinds(graph));
934
+ }
935
+
667
936
  export async function createRustBom(path, options) {
668
937
  let pkgList = [];
669
938
  let parentComponent = {};
@@ -769,6 +1038,7 @@ export async function createRustBom(path, options) {
769
1038
  "No Cargo.lock was found beside the manifest, so components carry the declared dependencies without locked versions or the resolved crate graph.",
770
1039
  });
771
1040
  }
1041
+ const manifestRootKeys = new Map();
772
1042
  for (const f of cargoFiles) {
773
1043
  if (DEBUG_MODE) {
774
1044
  console.log(`Parsing ${f}`);
@@ -777,6 +1047,7 @@ export async function createRustBom(path, options) {
777
1047
  includeWorkspaceMembers: !options.multiProject,
778
1048
  });
779
1049
  if (dlist?.length) {
1050
+ manifestRootKeys.set(f, cargoComponentKey(dlist[0]));
780
1051
  if (!cargoLockMode) {
781
1052
  pkgList = pkgList.concat(dlist);
782
1053
  } else {
@@ -832,6 +1103,15 @@ export async function createRustBom(path, options) {
832
1103
  );
833
1104
  }
834
1105
  }
1106
+ if (cargoLockMode) {
1107
+ annotateCargoDependencyKinds(
1108
+ pkgList,
1109
+ dependencyTree,
1110
+ cargoFiles,
1111
+ manifestRootKeys,
1112
+ options,
1113
+ );
1114
+ }
835
1115
  return buildBomNSData(options, pkgList, "cargo", {
836
1116
  src: path,
837
1117
  filename: cargoLockFiles.join(", "),
@@ -1160,22 +1440,31 @@ export function createCppBom(path, options) {
1160
1440
  }
1161
1441
  if (cmakeLikeFiles.length) {
1162
1442
  const beforeCmake = pkgList.length;
1443
+ // The files at the scan root name the project, so they are read before
1444
+ // the files of any sub-project (the sort is stable otherwise). The scan
1445
+ // root may be given as a relative path while the files come back absolute.
1446
+ const scanRoot = resolve(path);
1447
+ const atScanRoot = (f) => resolve(dirname(f)) === scanRoot;
1448
+ cmakeLikeFiles.sort(
1449
+ (a, b) => (atScanRoot(a) ? 0 : 1) - (atScanRoot(b) ? 0 : 1),
1450
+ );
1163
1451
  for (const f of cmakeLikeFiles) {
1164
1452
  if (DEBUG_MODE) {
1165
1453
  console.log(`Parsing ${f}`);
1166
1454
  }
1167
- const basePath = dirname(f);
1168
1455
  const retMap = parseCmakeLikeFile(f, "generic");
1169
1456
  if (retMap.pkgList?.length) {
1170
1457
  pkgList = pkgList.concat(retMap.pkgList);
1171
1458
  }
1172
1459
  if (
1173
- basePath === path &&
1460
+ atScanRoot(f) &&
1174
1461
  retMap.parentComponent &&
1175
1462
  Object.keys(retMap.parentComponent).length
1176
1463
  ) {
1177
1464
  if (!parentComponent) {
1178
1465
  parentComponent = retMap.parentComponent;
1466
+ // a sub-project reusing the root project's name is the root itself
1467
+ addedParentComponentsMap[retMap.parentComponent.name] = true;
1179
1468
  } else {
1180
1469
  parentComponent.components = parentComponent.components || [];
1181
1470
  if (!addedParentComponentsMap[retMap.parentComponent.name]) {
@@ -1190,6 +1479,7 @@ export function createCppBom(path, options) {
1190
1479
  ) {
1191
1480
  retMap.parentComponent.type = "library";
1192
1481
  pkgList.push(retMap.parentComponent);
1482
+ addedParentComponentsMap[retMap.parentComponent.name] = true;
1193
1483
  }
1194
1484
  // Retain the dependency tree from cmake
1195
1485
  if (retMap.dependenciesList) {
@@ -1209,7 +1499,11 @@ export function createCppBom(path, options) {
1209
1499
  // resolved versions and are left as they are.
1210
1500
  pkgList = pkgList
1211
1501
  .slice(0, beforeCmake)
1212
- .concat(collapseCmakeVersions(pkgList.slice(beforeCmake)));
1502
+ .concat(
1503
+ collapseCmakeVersions(
1504
+ preferFetchedCmakeDependencies(pkgList.slice(beforeCmake)),
1505
+ ),
1506
+ );
1213
1507
  }
1214
1508
  // CMake describes how a project is built from source. An image or an OS
1215
1509
  // installation carries build output, not a build tree, so resolving CMake
@@ -1299,6 +1593,7 @@ export function createCppBom(path, options) {
1299
1593
  { name: "cdx:cmake:resolvedVia", value: "gitclone-script" },
1300
1594
  ],
1301
1595
  };
1596
+ pkgList = withoutScrapedFetchDependency(pkgList, dep.name);
1302
1597
  if (dep.sourceDir) {
1303
1598
  depComponent.properties.push({
1304
1599
  name: "cdx:cmake:sourceDir",
@@ -1596,6 +1891,44 @@ export function createClojureBom(path, options) {
1596
1891
  return {};
1597
1892
  }
1598
1893
 
1894
+ /**
1895
+ * Locate the Stack lock files under a path.
1896
+ *
1897
+ * Stack names the lock after the configuration file it completes, so the
1898
+ * default `stack.yaml` produces `stack.yaml.lock` and a project with several
1899
+ * configurations produces one lock per configuration. `stack.lock` is also
1900
+ * accepted because tooling and documentation examples have used that name.
1901
+ *
1902
+ * @param {string} path Project path
1903
+ * @param {Object} options Parse options from the cli
1904
+ * @returns {string[]} Lock file paths, deduplicated
1905
+ */
1906
+ export function findStackLockFiles(path, options) {
1907
+ const prefix = options.multiProject ? "**/" : "";
1908
+ const candidates = new Set([
1909
+ ...getAllFiles(path, `${prefix}*.yaml.lock`, options),
1910
+ ...getAllFiles(path, `${prefix}stack.lock`, options),
1911
+ ]);
1912
+ return [...candidates].filter((lockFile) => {
1913
+ if (/(^|[\\/])stack\.(yaml\.)?lock$/u.test(lockFile)) {
1914
+ return true;
1915
+ }
1916
+ // `*.yaml.lock` is not a name Stack owns, so a custom configuration is
1917
+ // accepted only when the file it completes names a snapshot.
1918
+ const configFile = lockFile.replace(/\.lock$/u, "");
1919
+ if (!safeExistsSync(configFile)) {
1920
+ return false;
1921
+ }
1922
+ try {
1923
+ return /^\s*(snapshot|resolver)\s*:/mu.test(
1924
+ readFileSync(configFile, "utf-8"),
1925
+ );
1926
+ } catch {
1927
+ return false;
1928
+ }
1929
+ });
1930
+ }
1931
+
1599
1932
  /**
1600
1933
  * Function to create bom string for Haskell projects
1601
1934
  *
@@ -1626,12 +1959,44 @@ export function createHaskellBom(path, options) {
1626
1959
  filename: cabalFiles.join(", "),
1627
1960
  });
1628
1961
  }
1962
+ // Stack projects keep the resolved extra-deps in a lock file named after
1963
+ // the configuration file, so `stack.yaml` locks to `stack.yaml.lock`.
1964
+ const stackLockFiles = findStackLockFiles(path, options);
1965
+ if (stackLockFiles.length) {
1966
+ const dependencies = [];
1967
+ let parentComponent = {};
1968
+ for (const stackLockFile of stackLockFiles) {
1969
+ if (DEBUG_MODE) {
1970
+ console.log(`Parsing ${stackLockFile}`);
1971
+ }
1972
+ const stackYamlFile = stackLockFile.replace(/\.lock$/u, "");
1973
+ const result = parseStackProject(
1974
+ stackLockFile,
1975
+ safeExistsSync(stackYamlFile) ? stackYamlFile : undefined,
1976
+ );
1977
+ if (result.pkgList?.length) {
1978
+ pkgList = pkgList.concat(result.pkgList);
1979
+ }
1980
+ if (!Object.keys(parentComponent).length) {
1981
+ parentComponent = result.parentComponent;
1982
+ attachRootEdge(result, parentComponent, dependencies);
1983
+ }
1984
+ }
1985
+ if (pkgList.length) {
1986
+ return buildBomNSData(options, pkgList, "hackage", {
1987
+ src: path,
1988
+ filename: stackLockFiles.join(", "),
1989
+ dependencies,
1990
+ parentComponent,
1991
+ });
1992
+ }
1993
+ }
1629
1994
  recordDegradation("haskell.freeze-missing", {
1630
1995
  ecosystem: "haskell",
1631
1996
  tool: "cabal",
1632
1997
  impact: "components",
1633
1998
  detail:
1634
- "No cabal.project.freeze was found, and the freeze file is the source the parser reads for the package set.",
1999
+ "No cabal.project.freeze or stack.yaml.lock was found; those lock files are the sources the parsers read for the package set.",
1635
2000
  });
1636
2001
  return {};
1637
2002
  }
@@ -1699,6 +2064,50 @@ export async function createSwiftBom(path, options) {
1699
2064
  let parentComponent = {};
1700
2065
  const completedPath = [];
1701
2066
  let packageArgsMessageShown = false;
2067
+ // Packages that only the root package's tests use are scoped optional so
2068
+ // that `--required-only` can narrow the BOM. A package some project (or
2069
+ // some production path) needs stays required.
2070
+ const swiftScopes = {
2071
+ optionalRefs: new Set(),
2072
+ requiredRefs: new Set(),
2073
+ optionalNames: new Set(),
2074
+ requiredNames: new Set(),
2075
+ };
2076
+ const classifiedPaths = new Set();
2077
+ const classifyScopes = (basePath, swiftCommand, treeData) => {
2078
+ if (classifiedPaths.has(basePath)) {
2079
+ return;
2080
+ }
2081
+ classifiedPaths.add(basePath);
2082
+ const dumpArgs =
2083
+ swiftCommand === "xcrun"
2084
+ ? ["swift", "package", "dump-package"]
2085
+ : ["package", "dump-package"];
2086
+ const dumpResult = safeSpawnSync(swiftCommand, dumpArgs, {
2087
+ cwd: basePath,
2088
+ });
2089
+ if (!dumpResult?.stdout) {
2090
+ return;
2091
+ }
2092
+ let dumpJson;
2093
+ let treeJson;
2094
+ try {
2095
+ dumpJson = JSON.parse(Buffer.from(dumpResult.stdout).toString());
2096
+ } catch (_e) {
2097
+ return;
2098
+ }
2099
+ try {
2100
+ treeJson = treeData ? JSON.parse(treeData) : undefined;
2101
+ } catch (_e) {
2102
+ treeJson = undefined;
2103
+ }
2104
+ const scopes = classifySwiftDependencyScopes(dumpJson, treeJson);
2105
+ for (const key of Object.keys(swiftScopes)) {
2106
+ for (const value of scopes[key]) {
2107
+ swiftScopes[key].add(value);
2108
+ }
2109
+ }
2110
+ };
1702
2111
  if (pkgResolvedFiles.length) {
1703
2112
  for (const f of pkgResolvedFiles) {
1704
2113
  if (!parentComponent || !Object.keys(parentComponent).length) {
@@ -1822,6 +2231,33 @@ export async function createSwiftBom(path, options) {
1822
2231
  detail: "the swift package command failed",
1823
2232
  });
1824
2233
  }
2234
+ // Scope classification also works without the dependency graph
2235
+ classifyScopes(basePath, swiftCommand, treeData);
2236
+ }
2237
+ }
2238
+ // Scope packages that only tests use as optional. A package some project or
2239
+ // production path needs keeps its default (required) scope; evidence-based
2240
+ // filtering after generation can still promote an optional package when
2241
+ // the sources are found to use it.
2242
+ for (const apkg of pkgList) {
2243
+ if (apkg?.scope || !apkg?.name) {
2244
+ continue;
2245
+ }
2246
+ const refs = [apkg["bom-ref"], apkg.purl].filter(Boolean);
2247
+ const names = [String(apkg.name).toLowerCase()];
2248
+ for (const aproperty of apkg.properties || []) {
2249
+ if (aproperty?.name === "cdx:swift:packageName" && aproperty.value) {
2250
+ names.push(String(aproperty.value).toLowerCase());
2251
+ }
2252
+ }
2253
+ const isOptional =
2254
+ refs.some((r) => swiftScopes.optionalRefs.has(r)) ||
2255
+ names.some((n) => swiftScopes.optionalNames.has(n));
2256
+ const isRequired =
2257
+ refs.some((r) => swiftScopes.requiredRefs.has(r)) ||
2258
+ names.some((n) => swiftScopes.requiredNames.has(n));
2259
+ if (isOptional && !isRequired) {
2260
+ apkg.scope = "optional";
1825
2261
  }
1826
2262
  }
1827
2263
  if (shouldFetchLicense()) {
@@ -2302,3 +2738,475 @@ export async function createGleamBom(path, options) {
2302
2738
 
2303
2739
  return {};
2304
2740
  }
2741
+
2742
+ /**
2743
+ * Function to create bom string for Julia projects.
2744
+ *
2745
+ * The resolved `Manifest.toml` is preferred over the declared `Project.toml`
2746
+ * so the BOM carries pinned versions and the package graph.
2747
+ *
2748
+ * @param {string} path to the project
2749
+ * @param {Object} options Parse options from the cli
2750
+ * @returns {Promise<Object>} BOM object
2751
+ */
2752
+ export async function createJuliaBom(path, options) {
2753
+ let pkgList = [];
2754
+ let dependencies = [];
2755
+ let parentComponent = {};
2756
+
2757
+ const projectTomlFiles = getAllFiles(
2758
+ path,
2759
+ `${options.multiProject ? "**/" : ""}Project.toml`,
2760
+ options,
2761
+ );
2762
+ const manifestOnly = !projectTomlFiles.length;
2763
+
2764
+ for (const projectTomlFile of projectTomlFiles) {
2765
+ if (DEBUG_MODE) {
2766
+ console.log(`Parsing ${projectTomlFile}`);
2767
+ }
2768
+ const baseDir = dirname(projectTomlFile);
2769
+ const manifestTomlFile = join(baseDir, "Manifest.toml");
2770
+ const result = parseJuliaProject(
2771
+ projectTomlFile,
2772
+ safeExistsSync(manifestTomlFile) ? manifestTomlFile : undefined,
2773
+ );
2774
+ collectProjectResult(result);
2775
+ }
2776
+ if (manifestOnly) {
2777
+ for (const manifestTomlFile of getAllFiles(
2778
+ path,
2779
+ `${options.multiProject ? "**/" : ""}Manifest.toml`,
2780
+ options,
2781
+ )) {
2782
+ if (DEBUG_MODE) {
2783
+ console.log(`Parsing ${manifestTomlFile}`);
2784
+ }
2785
+ // Gleam also writes a `manifest.toml`; on case-insensitive filesystems
2786
+ // the lookup above can surface it, so only accept files that parse as
2787
+ // a Julia manifest.
2788
+ const parsed = parseJuliaProject(undefined, manifestTomlFile);
2789
+ if (parsed.pkgList.length) {
2790
+ collectProjectResult(parsed);
2791
+ }
2792
+ }
2793
+ }
2794
+
2795
+ function collectProjectResult(result) {
2796
+ if (result.pkgList?.length) {
2797
+ pkgList = pkgList.concat(result.pkgList);
2798
+ }
2799
+ if (result.dependencies?.length) {
2800
+ dependencies = dependencies.concat(result.dependencies);
2801
+ }
2802
+ if (
2803
+ !Object.keys(parentComponent).length &&
2804
+ Object.keys(result.parentComponent).length
2805
+ ) {
2806
+ parentComponent = result.parentComponent;
2807
+ attachRootEdge(result, parentComponent, dependencies);
2808
+ }
2809
+ }
2810
+
2811
+ if (pkgList.length || Object.keys(parentComponent).length) {
2812
+ return buildBomNSData(options, pkgList, "julia", {
2813
+ src: path,
2814
+ filename: projectTomlFiles.join(", "),
2815
+ dependencies,
2816
+ parentComponent,
2817
+ });
2818
+ }
2819
+ return {};
2820
+ }
2821
+
2822
+ /**
2823
+ * Attach a root dependency edge from the parent component to its direct
2824
+ * dependencies, following the shape used by the Gleam and Nix flows.
2825
+ *
2826
+ * @param {{rootInputs?: string[]}} result Parser result with root inputs
2827
+ * @param {Object} parentComponent Parent component to give a bom-ref
2828
+ * @param {Object[]} dependencies Dependency edge list to append to
2829
+ */
2830
+ function attachRootEdge(result, parentComponent, dependencies) {
2831
+ if (result.rootInputs?.length && parentComponent.name) {
2832
+ const rootRef = `application:${parentComponent.name}:${parentComponent.version || "latest"}`;
2833
+ parentComponent["bom-ref"] = rootRef;
2834
+ dependencies.push({
2835
+ ref: rootRef,
2836
+ dependsOn: [...new Set(result.rootInputs)],
2837
+ });
2838
+ }
2839
+ }
2840
+
2841
+ /**
2842
+ * Function to create bom string for Erlang projects using rebar3.
2843
+ *
2844
+ * @param {string} path to the project
2845
+ * @param {Object} options Parse options from the cli
2846
+ * @returns {Promise<Object>} BOM object
2847
+ */
2848
+ export async function createErlangBom(path, options) {
2849
+ let pkgList = [];
2850
+ const rebarLockFiles = getAllFiles(
2851
+ path,
2852
+ `${options.multiProject ? "**/" : ""}rebar.lock`,
2853
+ options,
2854
+ );
2855
+ for (const rebarLockFile of rebarLockFiles) {
2856
+ if (DEBUG_MODE) {
2857
+ console.log(`Parsing ${rebarLockFile}`);
2858
+ }
2859
+ const result = parseRebarLock(rebarLockFile);
2860
+ if (result.pkgList?.length) {
2861
+ pkgList = pkgList.concat(result.pkgList);
2862
+ }
2863
+ }
2864
+ if (pkgList.length) {
2865
+ return buildBomNSData(options, pkgList, "erlang", {
2866
+ src: path,
2867
+ filename: rebarLockFiles.join(", "),
2868
+ });
2869
+ }
2870
+ return {};
2871
+ }
2872
+
2873
+ /**
2874
+ * Function to create bom string for OCaml projects using opam.
2875
+ *
2876
+ * Resolved `.opam.locked` files are preferred; a plain `.opam` file is used
2877
+ * as a declared fallback with unpinned versions.
2878
+ *
2879
+ * @param {string} path to the project
2880
+ * @param {Object} options Parse options from the cli
2881
+ * @returns {Promise<Object>} BOM object
2882
+ */
2883
+ export async function createOCamlBom(path, options) {
2884
+ let pkgList = [];
2885
+ const opamLockedFiles = getAllFiles(
2886
+ path,
2887
+ `${options.multiProject ? "**/" : ""}*.opam.locked`,
2888
+ options,
2889
+ );
2890
+ const opamFiles = opamLockedFiles.length
2891
+ ? []
2892
+ : getAllFiles(path, `${options.multiProject ? "**/" : ""}*.opam`, options);
2893
+ for (const opamFile of [...opamLockedFiles, ...opamFiles]) {
2894
+ if (DEBUG_MODE) {
2895
+ console.log(`Parsing ${opamFile}`);
2896
+ }
2897
+ const result = parseOpamLockedFile(opamFile);
2898
+ if (result.pkgList?.length) {
2899
+ pkgList = pkgList.concat(result.pkgList);
2900
+ }
2901
+ }
2902
+ if (pkgList.length) {
2903
+ return buildBomNSData(options, pkgList, "ocaml", {
2904
+ src: path,
2905
+ filename: [...opamLockedFiles, ...opamFiles].join(", "),
2906
+ });
2907
+ }
2908
+ return {};
2909
+ }
2910
+
2911
+ /**
2912
+ * Function to create bom string for Elm projects.
2913
+ *
2914
+ * @param {string} path to the project
2915
+ * @param {Object} options Parse options from the cli
2916
+ * @returns {Promise<Object>} BOM object
2917
+ */
2918
+ export async function createElmBom(path, options) {
2919
+ let pkgList = [];
2920
+ let dependencies = [];
2921
+ let parentComponent = {};
2922
+
2923
+ const elmJsonFiles = getAllFiles(
2924
+ path,
2925
+ `${options.multiProject ? "**/" : ""}elm.json`,
2926
+ options,
2927
+ ).filter(isElmProjectFile);
2928
+ // A single reader memoises the cache lookups shared by every manifest.
2929
+ const cacheReader = createElmCacheReader();
2930
+ for (const elmJsonFile of elmJsonFiles) {
2931
+ if (DEBUG_MODE) {
2932
+ console.log(`Parsing ${elmJsonFile}`);
2933
+ }
2934
+ const result = parseElmProject(elmJsonFile, cacheReader);
2935
+ if (result.pkgList?.length) {
2936
+ pkgList = pkgList.concat(result.pkgList);
2937
+ }
2938
+ if (result.dependencies?.length) {
2939
+ dependencies = dependencies.concat(result.dependencies);
2940
+ }
2941
+ if (!Object.keys(parentComponent).length) {
2942
+ parentComponent = result.parentComponent;
2943
+ attachRootEdge(result, parentComponent, dependencies);
2944
+ }
2945
+ }
2946
+ const legacyFiles = getAllFiles(
2947
+ path,
2948
+ `${options.multiProject ? "**/" : ""}elm-package.json`,
2949
+ options,
2950
+ );
2951
+ for (const legacyFile of legacyFiles) {
2952
+ if (DEBUG_MODE) {
2953
+ console.log(`Parsing ${legacyFile}`);
2954
+ }
2955
+ const baseDir = dirname(legacyFile);
2956
+ const exactDepsFile = join(baseDir, "elm-stuff", "exact-dependencies.json");
2957
+ const result = parseLegacyElmProject(
2958
+ legacyFile,
2959
+ safeExistsSync(exactDepsFile) ? exactDepsFile : undefined,
2960
+ );
2961
+ if (result.pkgList?.length) {
2962
+ pkgList = pkgList.concat(result.pkgList);
2963
+ }
2964
+ if (result.dependencies?.length) {
2965
+ dependencies = dependencies.concat(result.dependencies);
2966
+ }
2967
+ if (!Object.keys(parentComponent).length) {
2968
+ parentComponent = result.parentComponent;
2969
+ attachRootEdge(result, parentComponent, dependencies);
2970
+ }
2971
+ }
2972
+ if (pkgList.length && shouldFetchPackageMetadata()) {
2973
+ pkgList = await getElmMetadata(pkgList, dependencies);
2974
+ }
2975
+ if (pkgList.length || Object.keys(parentComponent).length) {
2976
+ return buildBomNSData(options, pkgList, "elm", {
2977
+ src: path,
2978
+ filename: [...elmJsonFiles, ...legacyFiles].join(", "),
2979
+ dependencies,
2980
+ parentComponent,
2981
+ });
2982
+ }
2983
+ return {};
2984
+ }
2985
+
2986
+ /**
2987
+ * Function to create bom string for Crystal projects using shards.
2988
+ *
2989
+ * @param {string} path to the project
2990
+ * @param {Object} options Parse options from the cli
2991
+ * @returns {Promise<Object>} BOM object
2992
+ */
2993
+ export async function createCrystalBom(path, options) {
2994
+ let pkgList = [];
2995
+ const dependencies = [];
2996
+ let parentComponent = {};
2997
+
2998
+ const shardYmlFiles = getAllFiles(
2999
+ path,
3000
+ `${options.multiProject ? "**/" : ""}shard.yml`,
3001
+ options,
3002
+ );
3003
+ for (const shardYmlFile of shardYmlFiles) {
3004
+ if (DEBUG_MODE) {
3005
+ console.log(`Parsing ${shardYmlFile}`);
3006
+ }
3007
+ const baseDir = dirname(shardYmlFile);
3008
+ const shardLockFile = join(baseDir, "shard.lock");
3009
+ const result = parseShardsProject(
3010
+ shardYmlFile,
3011
+ safeExistsSync(shardLockFile) ? shardLockFile : undefined,
3012
+ );
3013
+ if (result.pkgList?.length) {
3014
+ pkgList = pkgList.concat(result.pkgList);
3015
+ }
3016
+ if (!Object.keys(parentComponent).length) {
3017
+ parentComponent = result.parentComponent;
3018
+ attachRootEdge(result, parentComponent, dependencies);
3019
+ }
3020
+ }
3021
+ if (pkgList.length || Object.keys(parentComponent).length) {
3022
+ return buildBomNSData(options, pkgList, "crystal", {
3023
+ src: path,
3024
+ filename: shardYmlFiles.join(", "),
3025
+ dependencies,
3026
+ parentComponent,
3027
+ });
3028
+ }
3029
+ return {};
3030
+ }
3031
+
3032
+ /**
3033
+ * Function to create bom string for Nim projects using nimble.
3034
+ *
3035
+ * @param {string} path to the project
3036
+ * @param {Object} options Parse options from the cli
3037
+ * @returns {Promise<Object>} BOM object
3038
+ */
3039
+ export async function createNimBom(path, options) {
3040
+ let pkgList = [];
3041
+ let dependencies = [];
3042
+ let parentComponent = {};
3043
+
3044
+ const nimbleFiles = getAllFiles(
3045
+ path,
3046
+ `${options.multiProject ? "**/" : ""}*.nimble`,
3047
+ options,
3048
+ );
3049
+ for (const nimbleFile of nimbleFiles) {
3050
+ if (DEBUG_MODE) {
3051
+ console.log(`Parsing ${nimbleFile}`);
3052
+ }
3053
+ const baseDir = dirname(nimbleFile);
3054
+ const nimbleLockFile = join(baseDir, "nimble.lock");
3055
+ const result = parseNimProject(
3056
+ nimbleFile,
3057
+ safeExistsSync(nimbleLockFile) ? nimbleLockFile : undefined,
3058
+ );
3059
+ if (result.pkgList?.length) {
3060
+ pkgList = pkgList.concat(result.pkgList);
3061
+ }
3062
+ if (result.dependencies?.length) {
3063
+ dependencies = dependencies.concat(result.dependencies);
3064
+ }
3065
+ if (!Object.keys(parentComponent).length) {
3066
+ parentComponent = result.parentComponent;
3067
+ attachRootEdge(result, parentComponent, dependencies);
3068
+ }
3069
+ }
3070
+ if (pkgList.length || Object.keys(parentComponent).length) {
3071
+ return buildBomNSData(options, pkgList, "nim", {
3072
+ src: path,
3073
+ filename: nimbleFiles.join(", "),
3074
+ dependencies,
3075
+ parentComponent,
3076
+ });
3077
+ }
3078
+ return {};
3079
+ }
3080
+
3081
+ /**
3082
+ * Function to create bom string for Lua projects using LuaRocks.
3083
+ *
3084
+ * When a `luarocks.lock` is present next to a rockspec it supplies the
3085
+ * resolved versions; otherwise the rockspec's declared dependencies are
3086
+ * inventoried without versions.
3087
+ *
3088
+ * @param {string} path to the project
3089
+ * @param {Object} options Parse options from the cli
3090
+ * @returns {Promise<Object>} BOM object
3091
+ */
3092
+ export async function createLuaBom(path, options) {
3093
+ let pkgList = [];
3094
+ let parentComponent = {};
3095
+ const seenLockFiles = new Set();
3096
+ const rockspecFiles = getAllFiles(
3097
+ path,
3098
+ `${options.multiProject ? "**/" : ""}*.rockspec`,
3099
+ options,
3100
+ );
3101
+ for (const rockspecFile of rockspecFiles) {
3102
+ if (DEBUG_MODE) {
3103
+ console.log(`Parsing ${rockspecFile}`);
3104
+ }
3105
+ const baseDir = dirname(rockspecFile);
3106
+ const lockFile = join(baseDir, "luarocks.lock");
3107
+ // The rockspec names the project and its declared set either way; the
3108
+ // lock, when present, supplies the resolved versions.
3109
+ const declared = parseRockspecFile(rockspecFile);
3110
+ if (!Object.keys(parentComponent).length) {
3111
+ parentComponent = declared.parentComponent;
3112
+ }
3113
+ let resolved;
3114
+ if (safeExistsSync(lockFile)) {
3115
+ seenLockFiles.add(lockFile);
3116
+ resolved = parseLuaRocksLock(lockFile, rockspecFile);
3117
+ }
3118
+ const chosen = resolved?.pkgList?.length ? resolved : declared;
3119
+ if (chosen.pkgList?.length) {
3120
+ pkgList = pkgList.concat(chosen.pkgList);
3121
+ }
3122
+ }
3123
+ // A lock can also stand alone, in a project whose rockspec is generated at
3124
+ // release time rather than committed.
3125
+ for (const lockFile of getAllFiles(
3126
+ path,
3127
+ `${options.multiProject ? "**/" : ""}luarocks.lock`,
3128
+ options,
3129
+ )) {
3130
+ if (seenLockFiles.has(lockFile)) {
3131
+ continue;
3132
+ }
3133
+ if (DEBUG_MODE) {
3134
+ console.log(`Parsing ${lockFile}`);
3135
+ }
3136
+ const result = parseLuaRocksLock(lockFile);
3137
+ if (result.pkgList?.length) {
3138
+ pkgList = pkgList.concat(result.pkgList);
3139
+ }
3140
+ }
3141
+ if (pkgList.length) {
3142
+ return buildBomNSData(options, pkgList, "lua", {
3143
+ src: path,
3144
+ filename: rockspecFiles.join(", "),
3145
+ parentComponent,
3146
+ });
3147
+ }
3148
+ return {};
3149
+ }
3150
+
3151
+ /**
3152
+ * Function to create bom string for Spack environments.
3153
+ *
3154
+ * @param {string} path to the project
3155
+ * @param {Object} options Parse options from the cli
3156
+ * @returns {Promise<Object>} BOM object
3157
+ */
3158
+ export async function createSpackBom(path, options) {
3159
+ let pkgList = [];
3160
+ let dependencies = [];
3161
+ let rootInputs = [];
3162
+ const spackLockFiles = getAllFiles(
3163
+ path,
3164
+ `${options.multiProject ? "**/" : ""}spack.lock`,
3165
+ options,
3166
+ );
3167
+ for (const spackLockFile of spackLockFiles) {
3168
+ if (DEBUG_MODE) {
3169
+ console.log(`Parsing ${spackLockFile}`);
3170
+ }
3171
+ const result = parseSpackLock(spackLockFile);
3172
+ if (result.pkgList?.length) {
3173
+ pkgList = pkgList.concat(result.pkgList);
3174
+ }
3175
+ if (result.dependencies?.length) {
3176
+ dependencies = dependencies.concat(result.dependencies);
3177
+ }
3178
+ if (result.rootInputs?.length) {
3179
+ rootInputs = rootInputs.concat(result.rootInputs);
3180
+ }
3181
+ }
3182
+ if (!pkgList.length) {
3183
+ return {};
3184
+ }
3185
+ // The lock names no project component, so the environment directory is the
3186
+ // name the environment is already known by. Its roots are the specs the
3187
+ // lock records as requested, which is why they are linked explicitly rather
3188
+ // than inferred from which nodes have no incoming edge: a build-only tool
3189
+ // such as gmake also has none, and it is not a root.
3190
+ const envName = basename(resolve(path));
3191
+ const parentComponent = {
3192
+ type: "application",
3193
+ name: envName,
3194
+ "bom-ref": `application:${envName}:latest`,
3195
+ description: `Spack environment: ${envName}`,
3196
+ properties: [
3197
+ { name: "internal:SrcFile", value: spackLockFiles.join(", ") },
3198
+ ],
3199
+ };
3200
+ if (rootInputs.length) {
3201
+ dependencies.push({
3202
+ ref: parentComponent["bom-ref"],
3203
+ dependsOn: [...new Set(rootInputs)].sort(),
3204
+ });
3205
+ }
3206
+ return buildBomNSData(options, pkgList, "spack", {
3207
+ src: path,
3208
+ filename: spackLockFiles.join(", "),
3209
+ dependencies,
3210
+ parentComponent,
3211
+ });
3212
+ }