@cdxgen/cdxgen 13.3.0 → 13.4.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 (255) hide show
  1. package/README.md +9 -6
  2. package/bin/cdxgen.js +90 -26
  3. package/bin/evinse.js +18 -1
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +45 -15
  6. package/bin/tracebom.js +5 -1
  7. package/bin/validate.js +84 -18
  8. package/bin/verify.js +155 -86
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +61 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/index.js +113 -54
  15. package/lib/cli/jsBom.js +50 -29
  16. package/lib/cli/jvmBom.js +665 -138
  17. package/lib/cli/managedBom.js +320 -200
  18. package/lib/cli/nativeBom.js +139 -30
  19. package/lib/core/activity.js +12 -0
  20. package/lib/core/env.js +17 -9
  21. package/lib/core/fs.js +3 -2
  22. package/lib/core/httpClient.js +159 -24
  23. package/lib/core/pluginRun.js +184 -0
  24. package/lib/core/processTree.js +100 -0
  25. package/lib/core/runState.js +39 -0
  26. package/lib/core/toml.js +72 -0
  27. package/lib/ecosystems/asarutils.js +4 -2
  28. package/lib/ecosystems/bunutils.js +5 -7
  29. package/lib/ecosystems/cargoCache.js +231 -8
  30. package/lib/ecosystems/cmakeResolver.js +10 -21
  31. package/lib/ecosystems/cppBuildContext.js +631 -0
  32. package/lib/ecosystems/cppEvidence.js +337 -22
  33. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  34. package/lib/ecosystems/denoutils.js +93 -66
  35. package/lib/ecosystems/ecosystems.js +1967 -788
  36. package/lib/ecosystems/golem.js +28 -12
  37. package/lib/ecosystems/gradleutils.js +314 -30
  38. package/lib/ecosystems/jsEvidence.js +85 -24
  39. package/lib/ecosystems/kosi.js +21 -4
  40. package/lib/ecosystems/npmutils.js +64 -0
  41. package/lib/ecosystems/parsers-dotnet.js +567 -151
  42. package/lib/ecosystems/parsers-elm.js +46 -28
  43. package/lib/ecosystems/parsers-gleam.js +3 -4
  44. package/lib/ecosystems/parsers-go.js +324 -61
  45. package/lib/ecosystems/parsers-js.js +92 -26
  46. package/lib/ecosystems/parsers-julia.js +2 -3
  47. package/lib/ecosystems/parsers-jvm.js +204 -12
  48. package/lib/ecosystems/parsers-misc.js +182 -55
  49. package/lib/ecosystems/parsers-python.js +141 -10
  50. package/lib/ecosystems/parsers-rust.js +4 -4
  51. package/lib/ecosystems/parsers-terraform.js +1573 -19
  52. package/lib/ecosystems/pythonutils.js +102 -6
  53. package/lib/ecosystems/remote/huggingface.js +3 -0
  54. package/lib/ecosystems/rubyutils.js +67 -17
  55. package/lib/ecosystems/rusi.js +22 -9
  56. package/lib/ecosystems/sbtutils.js +243 -187
  57. package/lib/ecosystems/scalacliutils.js +342 -0
  58. package/lib/ecosystems/scalasem.js +2618 -0
  59. package/lib/ecosystems/terraformRegistry.js +288 -0
  60. package/lib/ecosystems/utils.js +2 -0
  61. package/lib/ecosystems/vendoredCode.js +156 -0
  62. package/lib/evinser/evinser.js +578 -69
  63. package/lib/helpers/agenticToolutils.js +2 -2
  64. package/lib/helpers/bomSigner.js +576 -128
  65. package/lib/inventory/aiCollector.js +4 -1
  66. package/lib/inventory/analyzer.js +110 -127
  67. package/lib/inventory/atomUtils.js +171 -48
  68. package/lib/inventory/auditCategories.js +7 -0
  69. package/lib/inventory/buildToolRateLimit.js +144 -0
  70. package/lib/inventory/cacheCommands.js +2 -1
  71. package/lib/inventory/cmakeBuildDirs.js +254 -0
  72. package/lib/inventory/deps.js +501 -124
  73. package/lib/inventory/dosai.js +248 -44
  74. package/lib/inventory/dosaiParsers.js +310 -15
  75. package/lib/inventory/dynamic.js +52 -6
  76. package/lib/inventory/evidenceUtils.js +154 -109
  77. package/lib/inventory/fetchBatch.js +574 -38
  78. package/lib/inventory/fetchDiskCache.js +597 -0
  79. package/lib/inventory/forgeEnricher.js +14 -5
  80. package/lib/inventory/jvmLocalRepos.js +917 -0
  81. package/lib/inventory/mcpConfigParser.js +2 -3
  82. package/lib/inventory/parentSelection.js +146 -0
  83. package/lib/inventory/rustFormulationParser.js +3 -4
  84. package/lib/inventory/scalaCoords.js +153 -0
  85. package/lib/inventory/source.js +8 -8
  86. package/lib/inventory/spdx.js +101 -0
  87. package/lib/inventory/traceRunner.js +88 -7
  88. package/lib/inventory/tracedCrypto.js +294 -0
  89. package/lib/managers/docker.js +33 -32
  90. package/lib/managers/oci.js +24 -2
  91. package/lib/parsers/cmakeCache.js +69 -0
  92. package/lib/parsers/cmakeLists.js +16 -2
  93. package/lib/parsers/cmakePresets.js +400 -0
  94. package/lib/parsers/compileCommands.js +557 -0
  95. package/lib/parsers/strictJson.js +322 -0
  96. package/lib/parsers/terraformConfig.js +1073 -0
  97. package/lib/parsers/terraformSource.js +652 -0
  98. package/lib/server/server.js +23 -0
  99. package/lib/stages/postgen/introspection/markers.js +1 -1
  100. package/lib/stages/postgen/postgen.js +15 -1
  101. package/lib/stages/postgen/specVersionCompat.js +32 -0
  102. package/lib/validator/index.js +25 -10
  103. package/lib/validator/reporters/json.js +1 -0
  104. package/package.json +21 -21
  105. package/types/lib/audit/index.d.ts.map +1 -1
  106. package/types/lib/cli/index.d.ts.map +1 -1
  107. package/types/lib/cli/jsBom.d.ts.map +1 -1
  108. package/types/lib/cli/jvmBom.d.ts +24 -1
  109. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  110. package/types/lib/cli/managedBom.d.ts +7 -4
  111. package/types/lib/cli/managedBom.d.ts.map +1 -1
  112. package/types/lib/cli/nativeBom.d.ts +3 -1
  113. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  114. package/types/lib/core/activity.d.ts.map +1 -1
  115. package/types/lib/core/env.d.ts +9 -0
  116. package/types/lib/core/env.d.ts.map +1 -1
  117. package/types/lib/core/fs.d.ts.map +1 -1
  118. package/types/lib/core/httpClient.d.ts +13 -8
  119. package/types/lib/core/httpClient.d.ts.map +1 -1
  120. package/types/lib/core/pluginRun.d.ts +97 -0
  121. package/types/lib/core/pluginRun.d.ts.map +1 -0
  122. package/types/lib/core/processTree.d.ts +36 -0
  123. package/types/lib/core/processTree.d.ts.map +1 -0
  124. package/types/lib/core/runState.d.ts +29 -0
  125. package/types/lib/core/runState.d.ts.map +1 -0
  126. package/types/lib/core/toml.d.ts +20 -0
  127. package/types/lib/core/toml.d.ts.map +1 -0
  128. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  129. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  131. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  132. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  133. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  134. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  135. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  136. package/types/lib/ecosystems/cppEvidence.d.ts +18 -2
  137. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  139. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  140. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  141. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  143. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  144. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  145. package/types/lib/ecosystems/gradleutils.d.ts +50 -0
  146. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  147. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  148. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  149. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  150. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  151. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  152. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  153. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  154. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  155. package/types/lib/ecosystems/parsers-go.d.ts +44 -2
  156. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  157. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  158. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  159. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  160. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  161. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  162. package/types/lib/ecosystems/parsers-misc.d.ts +3 -1
  163. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  164. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  165. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  166. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  167. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  168. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  169. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  170. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  171. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  172. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  173. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  174. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  175. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  176. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  177. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  178. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  179. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  180. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  181. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  182. package/types/lib/ecosystems/utils.d.ts +2 -2
  183. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  184. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  185. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  186. package/types/lib/evinser/evinser.d.ts +75 -8
  187. package/types/lib/evinser/evinser.d.ts.map +1 -1
  188. package/types/lib/helpers/bomSigner.d.ts +80 -4
  189. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  190. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  191. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  192. package/types/lib/inventory/atomUtils.d.ts +82 -10
  193. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  194. package/types/lib/inventory/auditCategories.d.ts +3 -0
  195. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  196. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  197. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  198. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  199. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  200. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  201. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  202. package/types/lib/inventory/deps.d.ts +59 -1
  203. package/types/lib/inventory/deps.d.ts.map +1 -1
  204. package/types/lib/inventory/dosai.d.ts +57 -8
  205. package/types/lib/inventory/dosai.d.ts.map +1 -1
  206. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  207. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  208. package/types/lib/inventory/dynamic.d.ts +10 -0
  209. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  210. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  211. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  212. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  213. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  214. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  215. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  216. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  217. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  218. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  219. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  220. package/types/lib/inventory/parentSelection.d.ts +34 -0
  221. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  222. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  223. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  224. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  225. package/types/lib/inventory/source.d.ts.map +1 -1
  226. package/types/lib/inventory/spdx.d.ts +9 -0
  227. package/types/lib/inventory/spdx.d.ts.map +1 -1
  228. package/types/lib/inventory/traceRunner.d.ts +12 -0
  229. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  230. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  231. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  232. package/types/lib/managers/docker.d.ts.map +1 -1
  233. package/types/lib/managers/oci.d.ts +3 -1
  234. package/types/lib/managers/oci.d.ts.map +1 -1
  235. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  236. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  237. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  238. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  239. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  240. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  241. package/types/lib/parsers/compileCommands.d.ts +115 -0
  242. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  243. package/types/lib/parsers/strictJson.d.ts +46 -0
  244. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  245. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  246. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  247. package/types/lib/parsers/terraformSource.d.ts +66 -0
  248. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  249. package/types/lib/server/server.d.ts.map +1 -1
  250. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  252. package/types/lib/validator/index.d.ts +8 -0
  253. package/types/lib/validator/index.d.ts.map +1 -1
  254. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  255. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -234,3 +234,70 @@
234
234
  "registry": $prop($, 'cdx:pypi:registry'),
235
235
  "resolvedFrom": $prop($, 'cdx:pypi:resolved_from')
236
236
  }
237
+ - id: PKG-012
238
+ name: "Terraform module from mutable VCS reference"
239
+ description: "Terraform modules sourced from git or mercurial without a commit or tag pin are not locked, so the next init can fetch different code"
240
+ severity: high
241
+ category: dependency-source
242
+ dry-run-support: full
243
+ condition: |
244
+ components[
245
+ $hasProp($, 'cdx:tf:kind') and $prop($, 'cdx:tf:kind') = 'module'
246
+ and $hasProp($, 'cdx:tf:module:sourceType')
247
+ and ($prop($, 'cdx:tf:module:sourceType') = 'git' or $prop($, 'cdx:tf:module:sourceType') = 'hg')
248
+ and ($prop($, 'cdx:tf:module:pinning') = 'branch' or $prop($, 'cdx:tf:module:pinning') = 'none')
249
+ ]
250
+ location: |
251
+ { "bomRef": $."bom-ref", "purl": purl }
252
+ message: "Terraform module '{{ name }}@{{ version }}' tracks VCS source '{{ $prop($, 'cdx:tf:module:source') }}' at a mutable reference"
253
+ mitigation: "Pin the module source with `?ref=<commit sha>` or a reviewed, signed tag; the dependency lock file does not cover modules"
254
+ evidence: |
255
+ {
256
+ "source": $prop($, 'cdx:tf:module:source'),
257
+ "sourceType": $prop($, 'cdx:tf:module:sourceType'),
258
+ "ref": $prop($, 'cdx:tf:module:ref'),
259
+ "pinning": $prop($, 'cdx:tf:module:pinning')
260
+ }
261
+ - id: PKG-013
262
+ name: "Terraform registry module not pinned to an exact version"
263
+ description: "Registry modules whose version argument is a range or absent resolve to whatever the registry serves at the next init"
264
+ severity: low
265
+ category: dependency-source
266
+ dry-run-support: full
267
+ condition: |
268
+ components[
269
+ $hasProp($, 'cdx:tf:kind') and $prop($, 'cdx:tf:kind') = 'module'
270
+ and $hasProp($, 'cdx:tf:module:sourceType') and $prop($, 'cdx:tf:module:sourceType') = 'registry'
271
+ and ($prop($, 'cdx:tf:module:pinning') = 'range' or $prop($, 'cdx:tf:module:pinning') = 'none')
272
+ ]
273
+ location: |
274
+ { "bomRef": $."bom-ref", "purl": purl }
275
+ message: "Terraform registry module '{{ $prop($, 'cdx:tf:address') }}' is not pinned to an exact version (pinning: {{ $prop($, 'cdx:tf:module:pinning') }})"
276
+ mitigation: "Set the module's `version` argument to a single exact version and review upgrades deliberately"
277
+ evidence: |
278
+ {
279
+ "constraints": $prop($, 'cdx:tf:constraints'),
280
+ "pinning": $prop($, 'cdx:tf:module:pinning'),
281
+ "address": $prop($, 'cdx:tf:address')
282
+ }
283
+ - id: PKG-014
284
+ name: "Terraform module source embeds credentials"
285
+ description: "A module source string carries a credential such as URL userinfo, a git sshkey parameter or S3 keys, exposing the secret to every reader of the configuration"
286
+ severity: high
287
+ category: dependency-source
288
+ dry-run-support: full
289
+ condition: |
290
+ components[
291
+ $hasProp($, 'cdx:tf:module:credentialInSource')
292
+ and $prop($, 'cdx:tf:module:credentialInSource') = 'true'
293
+ ]
294
+ location: |
295
+ { "bomRef": $."bom-ref", "purl": purl }
296
+ message: "Terraform module '{{ name }}@{{ version }}' resolves from a source that embeds credentials"
297
+ mitigation: "Rotate the exposed credential, move authentication to git credential helpers or an SSH agent, and keep the source URL credential-free"
298
+ evidence: |
299
+ {
300
+ "source": $prop($, 'cdx:tf:module:source'),
301
+ "sourceType": $prop($, 'cdx:tf:module:sourceType'),
302
+ "credentialInSource": $prop($, 'cdx:tf:module:credentialInSource')
303
+ }
@@ -0,0 +1,115 @@
1
+ # Scala Evinse and scalasem Rules
2
+ # Categories: scala-security, scala-compliance
3
+ # Evaluates cdx:scalasem:* properties and the crypto assets emitted by
4
+ # evinse -l scala and cdxgen --evidence for Scala projects.
5
+
6
+ - id: SCALA-SEC-001
7
+ name: "Scala source uses a weak cryptographic algorithm in runtime code"
8
+ description: "scalasem flagged a weak algorithm, mode or key size at a call site in main or generated sources (MD5, SHA-1, DES, ECB, or a key below the modern floor)"
9
+ severity: medium
10
+ category: scala-security
11
+ dry-run-support: full
12
+ condition: |
13
+ components[
14
+ type = 'cryptographic-asset'
15
+ and $prop($, 'cdx:scalasem:crypto:weak') = 'true'
16
+ and (
17
+ 'main' in properties[name = 'cdx:scalasem:usageScopes'].value
18
+ or 'generated' in properties[name = 'cdx:scalasem:usageScopes'].value
19
+ )
20
+ ]
21
+ location: |
22
+ { "bomRef": $."bom-ref", "name": name }
23
+ message: "Weak cryptographic algorithm '{{ name }}' used in Scala source"
24
+ mitigation: "Move to the modern equivalent of this algorithm, mode and key size, and confirm no compatibility constraint forces the weak one. The locations name the exact call sites"
25
+ evidence: |
26
+ {
27
+ "algorithm": name,
28
+ "oid": cryptoProperties.oid,
29
+ "api": $prop($, 'cdx:scalasem:crypto:api'),
30
+ "locations": evidence.occurrences.(location & '#' & line)
31
+ }
32
+
33
+ - id: SCALA-SEC-002
34
+ name: "Scala source calls an outbound service over plaintext transport"
35
+ description: "scalasem found an outbound HTTP or websocket client whose target URL starts with http:// or ws:// and is not a loopback address, so traffic and credentials cross the network unencrypted"
36
+ severity: medium
37
+ category: scala-security
38
+ dry-run-support: full
39
+ condition: |
40
+ services[
41
+ $exists($filter(endpoints, function($e) {
42
+ ($contains($e, 'http://') or $contains($e, 'ws://'))
43
+ and $not($contains($e, /:\/\/(localhost|127\.[0-9.]+|\[::1\])(:|\/|$)/))
44
+ }))
45
+ and $prop($, 'cdx:scalasem:service:kind') != null
46
+ ]
47
+ location: |
48
+ { "name": name, "endpoints": endpoints }
49
+ message: "Outbound service '{{ name }}' is called over plaintext"
50
+ mitigation: "Switch the target to https:// or wss://, or a local socket if the peer is on the same host. The call sites are named by the service's cdx:scalasem:service:location properties"
51
+ evidence: |
52
+ {
53
+ "kind": $prop($, 'cdx:scalasem:service:kind'),
54
+ "client": $prop($, 'cdx:scalasem:service:client'),
55
+ "locations": $filter(properties, function($p) { $p.name = 'cdx:scalasem:service:location' }).value
56
+ }
57
+
58
+ - id: SCALA-SEC-003
59
+ name: "Admin-like Scala endpoint is not marked as authenticated"
60
+ description: "scalasem found an inbound route with an admin, internal, manage or debug path segment that the report does not mark as authenticated. The source analysis does not see middleware or gateways, so this is a path-based prompt for review"
61
+ severity: low
62
+ category: scala-security
63
+ dry-run-support: full
64
+ condition: |
65
+ services[
66
+ $exists($filter(endpoints, function($e) { $contains($e, /(^|\/)(admin|internal|manage|debug)(\/|$)/) }))
67
+ and $prop($, 'cdx:scalasem:endpoint:framework') != null
68
+ and $not(authenticated = true)
69
+ ]
70
+ location: |
71
+ { "name": name, "endpoints": endpoints }
72
+ message: "Endpoint '{{ name }}' looks administrative and is not marked as authenticated"
73
+ mitigation: "Confirm the route is protected by middleware or a gateway the source does not show, and otherwise require authentication on it. The declaration site is named by the service's cdx:scalasem:service:location property"
74
+ evidence: |
75
+ {
76
+ "framework": $prop($, 'cdx:scalasem:endpoint:framework'),
77
+ "handler": $prop($, 'cdx:scalasem:endpoint:handler'),
78
+ "locations": $filter(properties, function($p) { $p.name = 'cdx:scalasem:service:location' }).value
79
+ }
80
+
81
+ - id: SCALA-SEC-004
82
+ name: "Scala source uses a weak algorithm that has no registry OID"
83
+ description: "scalasem flagged a weak algorithm that the OID registry cannot name, such as an MD5 or SHA-1 based signature or an RC4 password scheme, so it is recorded as a property instead of a crypto asset"
84
+ severity: medium
85
+ category: scala-security
86
+ dry-run-support: full
87
+ condition: |
88
+ $append(components, metadata.component)[
89
+ $exists(properties[name = 'cdx:scalasem:crypto:weakFinding'])
90
+ ]
91
+ location: |
92
+ { "bomRef": $."bom-ref", "name": name }
93
+ message: "Weak cryptographic algorithm used in Scala source, recorded on '{{ name }}'"
94
+ mitigation: "Move to a modern algorithm. Each finding names the algorithm and its call site as name@file#line"
95
+ evidence: |
96
+ {
97
+ "findings": properties[name = 'cdx:scalasem:crypto:weakFinding'].value
98
+ }
99
+
100
+ - id: SCALA-COMP-001
101
+ name: "Scala semantic evidence is incomplete"
102
+ description: "The scalasem run behind this BOM was degraded, for example by class files no compiler could read, a missing classpath, a timeout or a failed analyzer, so the BOM's Scala evidence may be missing usages, crypto findings or services"
103
+ severity: low
104
+ category: scala-compliance
105
+ dry-run-support: full
106
+ condition: |
107
+ metadata.component[$prop($, 'cdx:scalasem:degraded') = 'true']
108
+ location: |
109
+ { "bomRef": $."bom-ref", "name": name }
110
+ message: "The Scala semantic evidence for '{{ name }}' is incomplete"
111
+ mitigation: "Read the cdx:scalasem:diagnostic:* metadata properties for the cause, and build the project before scanning when the diagnostics name missing class files or a missing classpath"
112
+ evidence: |
113
+ {
114
+ "diagnostics": $filter(properties, function($p) { $contains($p.name, 'cdx:scalasem:diagnostic:') }).(name & '=' & value)
115
+ }
@@ -390,6 +390,27 @@ function targetSlug(target) {
390
390
  return `${target.type}-${normalized || "package"}-${version}-${digest}`;
391
391
  }
392
392
 
393
+ /**
394
+ * Build a deterministic directory name for a repository checkout, keyed by
395
+ * the repository URL and the ref it is cloned at.
396
+ *
397
+ * Keying by the coordinates rather than the package means the packages of
398
+ * one monorepo share a single checkout instead of each cloning the whole
399
+ * repository, while a different ref, which is a different snapshot of the
400
+ * tree, never shares one.
401
+ *
402
+ * @param {string} repoUrl Repository URL
403
+ * @param {string | undefined} gitRef Ref the checkout is cloned at
404
+ * @returns {string} slug string
405
+ */
406
+ function repositoryCheckoutSlug(repoUrl, gitRef) {
407
+ const digest = createHash("sha256")
408
+ .update(`${repoUrl}\n${gitRef || ""}`)
409
+ .digest("hex")
410
+ .slice(0, 16);
411
+ return `repo-${digest}`;
412
+ }
413
+
393
414
  /**
394
415
  * Ensure a parent directory exists before writing a file.
395
416
  *
@@ -1378,7 +1399,12 @@ async function ensureCheckout(target, resolution, workspaceDir, gitRef) {
1378
1399
  if (!safeExistsSync(resolvedWorkspaceDir)) {
1379
1400
  safeMkdirSync(resolvedWorkspaceDir, { recursive: true });
1380
1401
  }
1381
- const cloneDir = join(resolvedWorkspaceDir, targetSlug(target));
1402
+ // One checkout per repository and ref, so packages of the same monorepo
1403
+ // share it; each package still keeps its own cache and report directory.
1404
+ const cloneDir = join(
1405
+ resolvedWorkspaceDir,
1406
+ repositoryCheckoutSlug(resolution.repoUrl, gitRef),
1407
+ );
1382
1408
  if (safeExistsSync(join(cloneDir, ".git"))) {
1383
1409
  return {
1384
1410
  cleanup: false,
@@ -2635,11 +2661,17 @@ export async function runAuditFromBoms(inputBoms, options) {
2635
2661
  total: extractedTargets.targets.length,
2636
2662
  type: "run:start",
2637
2663
  });
2638
- // Every target's repository lookup is known here, before the first clone,
2639
- // so they are resolved in one round rather than one per iteration below.
2640
- await prefetchGitUrlSources(
2641
- extractedTargets.targets.map((target) => target.purl),
2642
- );
2664
+ // Every target's repository lookup is known here, before the first
2665
+ // clone, so they are resolved in one round rather than one per iteration
2666
+ // below. A target whose cached child BOM already answers it is resolved
2667
+ // from the workspace alone, so only the targets that will be cloned take
2668
+ // part in the round.
2669
+ const purlsToResolve = extractedTargets.targets
2670
+ .filter((target) => !loadCachedChildBom(options.workspaceDir, target))
2671
+ .map((target) => target.purl);
2672
+ if (purlsToResolve.length) {
2673
+ await prefetchGitUrlSources(purlsToResolve);
2674
+ }
2643
2675
  }
2644
2676
  for (const [index, target] of extractedTargets.targets.entries()) {
2645
2677
  const targetIndex = index + 1;
package/lib/cli/index.js CHANGED
@@ -44,6 +44,7 @@ import {
44
44
  parseOpenapiSpecData,
45
45
  parsePrivadoFile,
46
46
  } from "../ecosystems/parsers-misc.js";
47
+ import { hasTerraformConfiguration } from "../ecosystems/parsers-terraform.js";
47
48
  import { isPyLockFile } from "../ecosystems/pylockutils.js";
48
49
  import {
49
50
  buildDependencyTrackBomRequest,
@@ -65,6 +66,7 @@ import {
65
66
  hasHbomProjectType,
66
67
  } from "../inventory/hbom.js";
67
68
  import { mergeHostInventoryBoms } from "../inventory/hostTopology.js";
69
+ import { orderParentCandidates } from "../inventory/parentSelection.js";
68
70
  // Imported directly rather than through the utils.js barrel: purl validity is
69
71
  // checked in a hot path and the barrel is already the root of several import
70
72
  // cycles.
@@ -877,6 +879,19 @@ export async function createMultiXBom(pathList, options) {
877
879
  let components = [];
878
880
  let dependencies = [];
879
881
  let parentSubComponents = [];
882
+ // Each builder detects the project of its own ecosystem. A builder that sets
883
+ // options.parentComponent to force its project into its own BOM must not
884
+ // make that project the next builder's too, or the next ecosystem's project
885
+ // is lost: every builder starts from the parent the caller gave.
886
+ const callerParent = options.parentComponent;
887
+ const withOwnParent = async (build) => {
888
+ options.parentComponent = callerParent;
889
+ try {
890
+ return await build();
891
+ } finally {
892
+ options.parentComponent = callerParent;
893
+ }
894
+ };
880
895
  setActivityContext({
881
896
  projectType: Array.isArray(options.projectType)
882
897
  ? options.projectType.join(",")
@@ -923,7 +938,7 @@ export async function createMultiXBom(pathList, options) {
923
938
  }
924
939
  }
925
940
  setProjectTypeActivityContext(exactAiInventoryType || "js", path);
926
- bomData = await createNodejsBom(path, options);
941
+ bomData = await withOwnParent(() => createNodejsBom(path, options));
927
942
  if (bomData?.bomJson?.components?.length) {
928
943
  if (exactAiInventoryType) {
929
944
  thoughtLog(
@@ -973,7 +988,7 @@ export async function createMultiXBom(pathList, options) {
973
988
  );
974
989
  }
975
990
  setProjectTypeActivityContext("java", path);
976
- bomData = await createJavaBom(path, options);
991
+ bomData = await withOwnParent(() => createJavaBom(path, options));
977
992
  if (bomData?.bomJson?.components?.length) {
978
993
  thoughtLog(
979
994
  `I found ${bomData.bomJson.components.length} java packages.`,
@@ -1038,7 +1053,7 @@ export async function createMultiXBom(pathList, options) {
1038
1053
  );
1039
1054
  }
1040
1055
  setProjectTypeActivityContext("py", path);
1041
- bomData = await createPythonBom(path, options);
1056
+ bomData = await withOwnParent(() => createPythonBom(path, options));
1042
1057
  if (bomData?.bomJson?.components?.length) {
1043
1058
  thoughtLog(
1044
1059
  `I found ${bomData.bomJson.components.length} python packages.`,
@@ -1059,6 +1074,17 @@ export async function createMultiXBom(pathList, options) {
1059
1074
  ) {
1060
1075
  parentSubComponents.push(bomData.parentComponent);
1061
1076
  }
1077
+ // Retain metadata.component.components. uv workspace members are
1078
+ // nested under the python project, so hoisting them keeps them
1079
+ // siblings of the other sub-components instead of riding one level
1080
+ // deeper (or being dropped by the same-name collapse) when another
1081
+ // ecosystem's project becomes the parent.
1082
+ if (bomData.parentComponent?.components?.length) {
1083
+ parentSubComponents = parentSubComponents.concat(
1084
+ bomData.parentComponent.components,
1085
+ );
1086
+ delete bomData.parentComponent.components;
1087
+ }
1062
1088
  }
1063
1089
  if (bomData?.formulationList?.length) {
1064
1090
  formulationList = formulationList.concat(bomData.formulationList);
@@ -1071,7 +1097,7 @@ export async function createMultiXBom(pathList, options) {
1071
1097
  );
1072
1098
  }
1073
1099
  setProjectTypeActivityContext("go", path);
1074
- bomData = await createGoBom(path, options);
1100
+ bomData = await withOwnParent(() => createGoBom(path, options));
1075
1101
  if (bomData?.bomJson?.components?.length) {
1076
1102
  thoughtLog(`I found ${bomData.bomJson.components.length} go packages.`);
1077
1103
  if (DEBUG_MODE) {
@@ -1099,7 +1125,7 @@ export async function createMultiXBom(pathList, options) {
1099
1125
  );
1100
1126
  }
1101
1127
  setProjectTypeActivityContext("rust", path);
1102
- bomData = await createRustBom(path, options);
1128
+ bomData = await withOwnParent(() => createRustBom(path, options));
1103
1129
  if (bomData?.bomJson?.components?.length) {
1104
1130
  thoughtLog(
1105
1131
  `I found ${bomData.bomJson.components.length} rust packages.`,
@@ -1136,7 +1162,7 @@ export async function createMultiXBom(pathList, options) {
1136
1162
  );
1137
1163
  }
1138
1164
  setProjectTypeActivityContext("php", path);
1139
- bomData = createPHPBom(path, options);
1165
+ bomData = await withOwnParent(() => createPHPBom(path, options));
1140
1166
  if (bomData?.bomJson?.components?.length) {
1141
1167
  thoughtLog(
1142
1168
  `I found ${bomData.bomJson.components.length} php packages.`,
@@ -1173,7 +1199,7 @@ export async function createMultiXBom(pathList, options) {
1173
1199
  );
1174
1200
  }
1175
1201
  setProjectTypeActivityContext("ruby", path);
1176
- bomData = await createRubyBom(path, options);
1202
+ bomData = await withOwnParent(() => createRubyBom(path, options));
1177
1203
  if (bomData?.bomJson?.components?.length) {
1178
1204
  thoughtLog(
1179
1205
  `We got ${bomData.bomJson.components.length} ruby packages.`,
@@ -1209,7 +1235,7 @@ export async function createMultiXBom(pathList, options) {
1209
1235
  thoughtLog("**CSHARP**: What about csharp and fsharp projects?");
1210
1236
  }
1211
1237
  setProjectTypeActivityContext("csharp", path);
1212
- bomData = await createCsharpBom(path, options);
1238
+ bomData = await withOwnParent(() => createCsharpBom(path, options));
1213
1239
  if (bomData?.bomJson?.components?.length) {
1214
1240
  thoughtLog(
1215
1241
  `There are ${bomData.bomJson.components.length} csharp packages.`,
@@ -1246,7 +1272,7 @@ export async function createMultiXBom(pathList, options) {
1246
1272
  );
1247
1273
  }
1248
1274
  setProjectTypeActivityContext("dart", path);
1249
- bomData = await createDartBom(path, options);
1275
+ bomData = await withOwnParent(() => createDartBom(path, options));
1250
1276
  if (bomData?.bomJson?.components?.length) {
1251
1277
  thoughtLog(
1252
1278
  `I found ${bomData.bomJson.components.length} pub packages.`,
@@ -1276,7 +1302,7 @@ export async function createMultiXBom(pathList, options) {
1276
1302
  );
1277
1303
  }
1278
1304
  setProjectTypeActivityContext("haskell", path);
1279
- bomData = createHaskellBom(path, options);
1305
+ bomData = await withOwnParent(() => createHaskellBom(path, options));
1280
1306
  if (bomData?.bomJson?.components?.length) {
1281
1307
  thoughtLog(
1282
1308
  `I found ${bomData.bomJson.components.length} hackage packages.`,
@@ -1306,7 +1332,7 @@ export async function createMultiXBom(pathList, options) {
1306
1332
  );
1307
1333
  }
1308
1334
  setProjectTypeActivityContext("elixir", path);
1309
- bomData = createElixirBom(path, options);
1335
+ bomData = await withOwnParent(() => createElixirBom(path, options));
1310
1336
  if (bomData?.bomJson?.components?.length) {
1311
1337
  thoughtLog(
1312
1338
  `I found ${bomData.bomJson.components.length} mix packages.`,
@@ -1336,7 +1362,7 @@ export async function createMultiXBom(pathList, options) {
1336
1362
  );
1337
1363
  }
1338
1364
  setProjectTypeActivityContext("c", path);
1339
- bomData = createCppBom(path, options);
1365
+ bomData = await withOwnParent(() => createCppBom(path, options));
1340
1366
  if (bomData?.bomJson?.components?.length) {
1341
1367
  thoughtLog(
1342
1368
  `I found ${bomData.bomJson.components.length} cpp packages.`,
@@ -1358,6 +1384,10 @@ export async function createMultiXBom(pathList, options) {
1358
1384
  parentSubComponents.push(bomData.parentComponent);
1359
1385
  }
1360
1386
  }
1387
+ // presets and compilers describe the build, whatever it depends on
1388
+ if (bomData?.formulationList?.length) {
1389
+ formulationList = formulationList.concat(bomData.formulationList);
1390
+ }
1361
1391
  }
1362
1392
  if (hasAnyProjectType(["oci", "clojure"], options)) {
1363
1393
  if (!hasAnyProjectType(["oci"], options, false)) {
@@ -1366,7 +1396,7 @@ export async function createMultiXBom(pathList, options) {
1366
1396
  );
1367
1397
  }
1368
1398
  setProjectTypeActivityContext("clojure", path);
1369
- bomData = createClojureBom(path, options);
1399
+ bomData = await withOwnParent(() => createClojureBom(path, options));
1370
1400
  if (bomData?.bomJson?.components?.length) {
1371
1401
  thoughtLog(
1372
1402
  `I found ${bomData.bomJson.components.length} clojure packages.`,
@@ -1396,7 +1426,7 @@ export async function createMultiXBom(pathList, options) {
1396
1426
  );
1397
1427
  }
1398
1428
  setProjectTypeActivityContext("github", path);
1399
- bomData = createGitHubBom(path, options);
1429
+ bomData = await withOwnParent(() => createGitHubBom(path, options));
1400
1430
  if (bomData?.bomJson?.components?.length) {
1401
1431
  thoughtLog(
1402
1432
  `I found ${bomData.bomJson.components.length} github action packages as well. Should I convert these to formulation instead 🤔`,
@@ -1426,7 +1456,7 @@ export async function createMultiXBom(pathList, options) {
1426
1456
  );
1427
1457
  }
1428
1458
  setProjectTypeActivityContext("cloudbuild", path);
1429
- bomData = createCloudBuildBom(path, options);
1459
+ bomData = await withOwnParent(() => createCloudBuildBom(path, options));
1430
1460
  if (bomData?.bomJson?.components?.length) {
1431
1461
  thoughtLog(
1432
1462
  `I found ${bomData.bomJson.components.length} cloudbuild packages.`,
@@ -1456,7 +1486,7 @@ export async function createMultiXBom(pathList, options) {
1456
1486
  );
1457
1487
  }
1458
1488
  setProjectTypeActivityContext("swift", path);
1459
- bomData = await createSwiftBom(path, options);
1489
+ bomData = await withOwnParent(() => createSwiftBom(path, options));
1460
1490
  if (bomData?.bomJson?.components?.length) {
1461
1491
  thoughtLog(
1462
1492
  `I found ${bomData.bomJson.components.length} swift packages here.`,
@@ -1494,7 +1524,7 @@ export async function createMultiXBom(pathList, options) {
1494
1524
  detail:
1495
1525
  "Components are being inferred by scanning jar/war/ear files from disk.",
1496
1526
  });
1497
- bomData = await createJarBom(path, options);
1527
+ bomData = await withOwnParent(() => createJarBom(path, options));
1498
1528
  if (bomData?.bomJson?.components?.length) {
1499
1529
  thoughtLog(
1500
1530
  `I found ${bomData.bomJson.components.length} jar packages as well.`,
@@ -1519,7 +1549,7 @@ export async function createMultiXBom(pathList, options) {
1519
1549
  }
1520
1550
  if (hasAnyProjectType(["oci", "cocoa"], options)) {
1521
1551
  setProjectTypeActivityContext("cocoa", path);
1522
- bomData = await createCocoaBom(path, options);
1552
+ bomData = await withOwnParent(() => createCocoaBom(path, options));
1523
1553
  if (bomData?.bomJson?.components?.length) {
1524
1554
  if (DEBUG_MODE) {
1525
1555
  console.log(
@@ -1538,7 +1568,7 @@ export async function createMultiXBom(pathList, options) {
1538
1568
  }
1539
1569
  if (hasAnyProjectType(["oci", "nix"], options)) {
1540
1570
  setProjectTypeActivityContext("nix", path);
1541
- bomData = await createNixBom(path, options);
1571
+ bomData = await withOwnParent(() => createNixBom(path, options));
1542
1572
  if (bomData?.bomJson?.components?.length) {
1543
1573
  if (DEBUG_MODE) {
1544
1574
  console.log(
@@ -1560,7 +1590,7 @@ export async function createMultiXBom(pathList, options) {
1560
1590
  }
1561
1591
  if (hasAnyProjectType(["caxa"], options, false)) {
1562
1592
  setProjectTypeActivityContext("caxa", path);
1563
- bomData = await createCaxaBom(path, options);
1593
+ bomData = await withOwnParent(() => createCaxaBom(path, options));
1564
1594
  if (bomData?.bomJson?.components?.length) {
1565
1595
  if (DEBUG_MODE) {
1566
1596
  console.log(
@@ -1582,7 +1612,7 @@ export async function createMultiXBom(pathList, options) {
1582
1612
  }
1583
1613
  if (hasAnyProjectType(["asar"], options, false)) {
1584
1614
  setProjectTypeActivityContext("asar", path);
1585
- bomData = await createAsarBom(path, options);
1615
+ bomData = await withOwnParent(() => createAsarBom(path, options));
1586
1616
  if (bomData?.bomJson?.components?.length) {
1587
1617
  if (DEBUG_MODE) {
1588
1618
  console.log(
@@ -1604,7 +1634,9 @@ export async function createMultiXBom(pathList, options) {
1604
1634
  }
1605
1635
  if (hasAnyProjectType(["vscode-extension"], options, false)) {
1606
1636
  setProjectTypeActivityContext("vscode-extension", path);
1607
- bomData = await createVscodeExtensionBom(path, options);
1637
+ bomData = await withOwnParent(() =>
1638
+ createVscodeExtensionBom(path, options),
1639
+ );
1608
1640
  if (bomData?.bomJson?.components?.length) {
1609
1641
  if (DEBUG_MODE) {
1610
1642
  console.log(
@@ -1647,7 +1679,9 @@ export async function createMultiXBom(pathList, options) {
1647
1679
  sharedOptions.__didScanChromeExtensions = true;
1648
1680
  }
1649
1681
  setProjectTypeActivityContext("chrome-extension", path);
1650
- bomData = await createChromeExtensionBom(path, options);
1682
+ bomData = await withOwnParent(() =>
1683
+ createChromeExtensionBom(path, options),
1684
+ );
1651
1685
  if (bomData?.bomJson?.components?.length) {
1652
1686
  if (DEBUG_MODE) {
1653
1687
  console.log(
@@ -1668,6 +1702,34 @@ export async function createMultiXBom(pathList, options) {
1668
1702
  }
1669
1703
  }
1670
1704
  }
1705
+ if (hasAnyProjectType(["terraform"], options)) {
1706
+ thoughtLog(
1707
+ "**TERRAFORM**: Looking for Terraform and OpenTofu modules and providers. The registry metadata fetch is opt-in, so I will rely on the lock file, the module manifest and the configuration files.",
1708
+ );
1709
+ setProjectTypeActivityContext("terraform", path);
1710
+ bomData = await withOwnParent(() => createTerraformBom(path, options));
1711
+ if (bomData?.bomJson?.components?.length) {
1712
+ thoughtLog(
1713
+ `I found ${bomData.bomJson.components.length} terraform components. Should I also resolve their licenses from the registry when FETCH_LICENSE is enabled?`,
1714
+ );
1715
+ if (DEBUG_MODE) {
1716
+ console.log(
1717
+ `Found ${bomData.bomJson.components.length} terraform components at ${path}`,
1718
+ );
1719
+ }
1720
+ components = components.concat(bomData.bomJson.components);
1721
+ dependencies = mergeDependencies(
1722
+ dependencies,
1723
+ bomData.bomJson.dependencies,
1724
+ );
1725
+ if (
1726
+ bomData.parentComponent &&
1727
+ Object.keys(bomData.parentComponent).length
1728
+ ) {
1729
+ parentSubComponents.push(bomData.parentComponent);
1730
+ }
1731
+ }
1732
+ }
1671
1733
  // Collect any crypto keys
1672
1734
  if (
1673
1735
  options.specVersion >= 1.6 &&
@@ -1680,7 +1742,7 @@ export async function createMultiXBom(pathList, options) {
1680
1742
  );
1681
1743
  }
1682
1744
  setProjectTypeActivityContext("cbom", path);
1683
- bomData = await createCryptoCertsBom(path, options);
1745
+ bomData = await withOwnParent(() => createCryptoCertsBom(path, options));
1684
1746
  if (bomData?.bomJson?.components?.length) {
1685
1747
  thoughtLog(
1686
1748
  `I found ${bomData.bomJson.components.length} crypto assets.`,
@@ -1771,7 +1833,7 @@ export async function createMultiXBom(pathList, options) {
1771
1833
  !options.lastWorkingDir.includes("/home/")
1772
1834
  ) {
1773
1835
  setProjectTypeActivityContext("jar", options.lastWorkingDir);
1774
- bomData = createJarBom(options.lastWorkingDir, options);
1836
+ bomData = await createJarBom(options.lastWorkingDir, options);
1775
1837
  if (bomData?.bomJson?.components?.length) {
1776
1838
  if (DEBUG_MODE) {
1777
1839
  console.log(
@@ -1797,14 +1859,24 @@ export async function createMultiXBom(pathList, options) {
1797
1859
  thoughtLog("**METADATA**: Tweaking the parent component hierarchy.");
1798
1860
  }
1799
1861
  if (!parentComponent || !Object.keys(parentComponent).length) {
1800
- parentComponent = parentSubComponents[0];
1862
+ // the project whose manifest sits at the scan root describes the
1863
+ // repository; ties keep the order the ecosystems were scanned in
1864
+ parentComponent = orderParentCandidates(
1865
+ parentSubComponents,
1866
+ pathList,
1867
+ options,
1868
+ )[0];
1801
1869
  }
1802
1870
  // Our naive approach to appending to sub-components could result in same parent being included as a child
1803
1871
  // This is filtered out here
1804
1872
  parentSubComponents = parentSubComponents.filter(
1805
1873
  (c) => c["bom-ref"] !== parentComponent["bom-ref"],
1806
1874
  );
1807
- parentComponent.components = trimComponents(parentSubComponents);
1875
+ // keep the parent's own sub-components (a CMake project's sub-projects)
1876
+ parentComponent.components = trimComponents([
1877
+ ...(parentComponent.components || []),
1878
+ ...parentSubComponents,
1879
+ ]);
1808
1880
  if (
1809
1881
  parentComponent.components.length === 1 &&
1810
1882
  parentComponent.components[0].name === parentComponent.name &&
@@ -1814,24 +1886,15 @@ export async function createMultiXBom(pathList, options) {
1814
1886
  delete parentComponent.components;
1815
1887
  }
1816
1888
  // Add references between the multiple sub-boms
1817
- let parentDependencies = dependencies.find(
1818
- (d) => d["ref"] === parentComponent["bom-ref"],
1819
- );
1820
- if (!parentDependencies) {
1821
- parentDependencies = {
1822
- ref: parentComponent["bom-ref"],
1823
- };
1824
- dependencies = mergeDependencies(dependencies, parentDependencies);
1825
- }
1826
- if (!parentDependencies["dependsOn"]) {
1827
- parentDependencies["dependsOn"] = [];
1828
- }
1829
- for (const parentSub of parentSubComponents) {
1830
- // Issue: 1622. We might have already captured this parent component dependency
1831
- if (!parentDependencies["dependsOn"].includes(parentSub["bom-ref"])) {
1832
- parentDependencies["dependsOn"].push(parentSub["bom-ref"]);
1833
- }
1834
- }
1889
+ // mergeDependencies returns fresh objects and unions dependsOn per ref,
1890
+ // so the parent edges are folded in through a single merge
1891
+ const parentRef = parentComponent["bom-ref"];
1892
+ const parentSubRefs = parentSubComponents
1893
+ .map((c) => c["bom-ref"])
1894
+ .filter((ref) => ref && ref !== parentRef);
1895
+ dependencies = mergeDependencies(dependencies, [
1896
+ { ref: parentRef, dependsOn: parentSubRefs },
1897
+ ]);
1835
1898
  }
1836
1899
  // some cleanup, but not complete
1837
1900
  for (const path of pathList) {
@@ -2464,14 +2527,10 @@ export async function createXBom(path, options) {
2464
2527
  return createPerlBom(path, options);
2465
2528
  }
2466
2529
 
2467
- // Terraform and OpenTofu providers
2468
- const tfLockFiles = getAllFiles(
2469
- path,
2470
- `${options.multiProject ? "**/" : ""}.terraform.lock.hcl`,
2471
- options,
2472
- );
2473
- if (tfLockFiles.length) {
2474
- return createTerraformBom(path, options);
2530
+ // Terraform and OpenTofu providers and modules, discovered with the same
2531
+ // patterns and exclusions the workspace assembly uses.
2532
+ if (hasTerraformConfiguration(path, options)) {
2533
+ return await createTerraformBom(path, options);
2475
2534
  }
2476
2535
  }
2477
2536
 
@@ -2861,7 +2920,7 @@ async function createBomForPath(path, options) {
2861
2920
  return createPerlBom(path, options);
2862
2921
  }
2863
2922
  if (PROJECT_TYPE_ALIASES["terraform"].includes(projectType[0])) {
2864
- return createTerraformBom(path, options);
2923
+ return await createTerraformBom(path, options);
2865
2924
  }
2866
2925
  if (PROJECT_TYPE_ALIASES["caxa"].includes(projectType[0])) {
2867
2926
  return await createCaxaBom(path, options);