@cdxgen/cdxgen 13.3.0 → 13.5.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 (372) hide show
  1. package/README.md +12 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +115 -31
  4. package/bin/evinse.js +23 -1
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +115 -0
  7. package/bin/sign.js +45 -15
  8. package/bin/tracebom.js +5 -1
  9. package/bin/validate.js +84 -18
  10. package/bin/verify.js +155 -86
  11. package/data/cbomosdb-queries.json +13 -1
  12. package/data/component-tags.json +8 -0
  13. package/data/helpers/init.gradle +46 -21
  14. package/data/queries-darwin.json +25 -1
  15. package/data/queries-win.json +19 -1
  16. package/data/queries.json +41 -0
  17. package/data/remediations.json +344 -1
  18. package/data/rules/cbom-security.yaml +235 -4
  19. package/data/rules/dependency-sources.yaml +67 -0
  20. package/data/rules/go-binaries.yaml +46 -0
  21. package/data/rules/golem-go.yaml +80 -0
  22. package/data/rules/hbom-compliance.yaml +40 -22
  23. package/data/rules/hbom-performance.yaml +7 -1
  24. package/data/rules/host-topology.yaml +1 -1
  25. package/data/rules/obom-runtime.yaml +31 -2
  26. package/data/rules/php-runtime.yaml +143 -0
  27. package/data/rules/scala-evidence.yaml +115 -0
  28. package/lib/audit/index.js +52 -6
  29. package/lib/cli/bomAssembly.js +28 -3
  30. package/lib/cli/cliOptions.js +110 -3
  31. package/lib/cli/index.js +186 -58
  32. package/lib/cli/jsBom.js +50 -29
  33. package/lib/cli/jvmBom.js +779 -146
  34. package/lib/cli/managedBom.js +1021 -261
  35. package/lib/cli/nativeBom.js +364 -58
  36. package/lib/core/activity.js +12 -0
  37. package/lib/core/env.js +47 -17
  38. package/lib/core/fs.js +31 -21
  39. package/lib/core/httpClient.js +233 -69
  40. package/lib/core/pluginRun.js +184 -0
  41. package/lib/core/processTree.js +100 -0
  42. package/lib/core/projectConfig.js +1 -0
  43. package/lib/core/projectTooling.js +97 -0
  44. package/lib/core/propertySanitizer.js +72 -0
  45. package/lib/core/runState.js +39 -0
  46. package/lib/core/toml.js +72 -0
  47. package/lib/ecosystems/asarutils.js +4 -2
  48. package/lib/ecosystems/bunutils.js +5 -7
  49. package/lib/ecosystems/cargoCache.js +231 -8
  50. package/lib/ecosystems/cmakeResolver.js +10 -21
  51. package/lib/ecosystems/cppBuildContext.js +631 -0
  52. package/lib/ecosystems/cppEvidence.js +337 -22
  53. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  54. package/lib/ecosystems/denoutils.js +93 -66
  55. package/lib/ecosystems/ecosystems.js +1967 -788
  56. package/lib/ecosystems/golem.js +578 -18
  57. package/lib/ecosystems/gradleutils.js +340 -46
  58. package/lib/ecosystems/jsEvidence.js +85 -24
  59. package/lib/ecosystems/kosi.js +21 -4
  60. package/lib/ecosystems/npmutils.js +64 -0
  61. package/lib/ecosystems/parsers-dotnet.js +668 -153
  62. package/lib/ecosystems/parsers-elm.js +46 -28
  63. package/lib/ecosystems/parsers-gleam.js +3 -4
  64. package/lib/ecosystems/parsers-go.js +1283 -297
  65. package/lib/ecosystems/parsers-js.js +92 -26
  66. package/lib/ecosystems/parsers-julia.js +2 -3
  67. package/lib/ecosystems/parsers-jvm.js +463 -29
  68. package/lib/ecosystems/parsers-misc.js +405 -93
  69. package/lib/ecosystems/parsers-python.js +152 -20
  70. package/lib/ecosystems/parsers-rust.js +4 -4
  71. package/lib/ecosystems/parsers-terraform.js +1573 -19
  72. package/lib/ecosystems/pythonutils.js +102 -6
  73. package/lib/ecosystems/remote/huggingface.js +3 -0
  74. package/lib/ecosystems/rubyutils.js +73 -20
  75. package/lib/ecosystems/rusi.js +32 -9
  76. package/lib/ecosystems/sbtutils.js +243 -187
  77. package/lib/ecosystems/scalacliutils.js +343 -0
  78. package/lib/ecosystems/scalasem.js +2620 -0
  79. package/lib/ecosystems/terraformRegistry.js +288 -0
  80. package/lib/ecosystems/utils.js +2 -0
  81. package/lib/ecosystems/vendoredCode.js +156 -0
  82. package/lib/evinser/evinser.js +695 -78
  83. package/lib/evinser/swiftsem.js +68 -5
  84. package/lib/helpers/agenticToolutils.js +2 -2
  85. package/lib/helpers/bomSigner.js +576 -128
  86. package/lib/inventory/agentSkillSpec.js +17 -0
  87. package/lib/inventory/aiCollector.js +4 -1
  88. package/lib/inventory/analyzer.js +110 -127
  89. package/lib/inventory/apkInstalledDb.js +243 -0
  90. package/lib/inventory/atomUtils.js +204 -48
  91. package/lib/inventory/auditCategories.js +10 -3
  92. package/lib/inventory/buildToolRateLimit.js +144 -0
  93. package/lib/inventory/cacheCommands.js +2 -1
  94. package/lib/inventory/cacheDir.js +52 -9
  95. package/lib/inventory/certificates.js +2371 -0
  96. package/lib/inventory/cmakeBuildDirs.js +254 -0
  97. package/lib/inventory/communityAiConfigParser.js +4 -9
  98. package/lib/inventory/deps.js +507 -128
  99. package/lib/inventory/display.js +200 -3
  100. package/lib/inventory/dosai.js +747 -101
  101. package/lib/inventory/dosaiParsers.js +310 -15
  102. package/lib/inventory/dynamic.js +96 -8
  103. package/lib/inventory/envcontext.js +168 -5
  104. package/lib/inventory/evidenceUtils.js +590 -132
  105. package/lib/inventory/fetchBatch.js +574 -38
  106. package/lib/inventory/fetchDiskCache.js +597 -0
  107. package/lib/inventory/forgeEnricher.js +14 -5
  108. package/lib/inventory/formulationParsers.js +3 -2
  109. package/lib/inventory/goBuildInfo.js +140 -0
  110. package/lib/inventory/hbom.js +67 -7
  111. package/lib/inventory/hbomAnalysis.js +16 -0
  112. package/lib/inventory/hostTopology.js +21 -0
  113. package/lib/inventory/jvmLocalRepos.js +917 -0
  114. package/lib/inventory/mcpConfigParser.js +2 -3
  115. package/lib/inventory/osPackagePurl.js +244 -0
  116. package/lib/inventory/osPackageResolver.js +89 -87
  117. package/lib/inventory/osinfo.js +79 -31
  118. package/lib/inventory/osqueryTransform.js +114 -5
  119. package/lib/inventory/parentSelection.js +146 -0
  120. package/lib/inventory/phpBuildInfo.js +487 -0
  121. package/lib/inventory/phpHostProbe.js +641 -0
  122. package/lib/inventory/rootfsCerts.js +426 -0
  123. package/lib/inventory/rustFormulationParser.js +3 -4
  124. package/lib/inventory/scalaCoords.js +153 -0
  125. package/lib/inventory/source.js +215 -11
  126. package/lib/inventory/spdx.js +245 -0
  127. package/lib/inventory/traceRunner.js +93 -8
  128. package/lib/inventory/tracedCrypto.js +423 -0
  129. package/lib/managers/binary.js +529 -183
  130. package/lib/managers/docker.js +199 -44
  131. package/lib/managers/oci.js +24 -2
  132. package/lib/parsers/cmakeCache.js +69 -0
  133. package/lib/parsers/cmakeLists.js +16 -2
  134. package/lib/parsers/cmakePresets.js +400 -0
  135. package/lib/parsers/compileCommands.js +557 -0
  136. package/lib/parsers/email.js +277 -0
  137. package/lib/parsers/largeJson.js +34 -2
  138. package/lib/parsers/strictJson.js +322 -0
  139. package/lib/parsers/terraformConfig.js +1073 -0
  140. package/lib/parsers/terraformSource.js +652 -0
  141. package/lib/server/server.js +209 -1
  142. package/lib/stages/postgen/annotator.js +17 -1
  143. package/lib/stages/postgen/introspection/markers.js +1 -1
  144. package/lib/stages/postgen/introspection/reflect.js +4 -1
  145. package/lib/stages/postgen/postgen.js +20 -6
  146. package/lib/stages/postgen/ruleEngine.js +58 -1
  147. package/lib/stages/postgen/specVersionCompat.js +62 -0
  148. package/lib/stages/pregen/pregen.js +117 -5
  149. package/lib/validator/bomValidator.js +26 -5
  150. package/lib/validator/generated/runtime.mjs +13 -0
  151. package/lib/validator/generated/validate-1.6.mjs +26 -7
  152. package/lib/validator/generated/validate-1.7.mjs +31 -12
  153. package/lib/validator/generated/validate-2.0.mjs +30 -11
  154. package/lib/validator/index.js +25 -10
  155. package/lib/validator/reporters/json.js +1 -0
  156. package/package.json +23 -23
  157. package/types/bin/cdxgen-secure.d.ts +3 -0
  158. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  159. package/types/bin/repl.d.ts.map +1 -1
  160. package/types/lib/audit/index.d.ts.map +1 -1
  161. package/types/lib/cli/bomAssembly.d.ts +9 -2
  162. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  163. package/types/lib/cli/cliOptions.d.ts +43 -0
  164. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  165. package/types/lib/cli/index.d.ts.map +1 -1
  166. package/types/lib/cli/jsBom.d.ts.map +1 -1
  167. package/types/lib/cli/jvmBom.d.ts +44 -1
  168. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  169. package/types/lib/cli/managedBom.d.ts +7 -4
  170. package/types/lib/cli/managedBom.d.ts.map +1 -1
  171. package/types/lib/cli/nativeBom.d.ts +3 -1
  172. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  173. package/types/lib/core/activity.d.ts.map +1 -1
  174. package/types/lib/core/env.d.ts +27 -2
  175. package/types/lib/core/env.d.ts.map +1 -1
  176. package/types/lib/core/fs.d.ts.map +1 -1
  177. package/types/lib/core/httpClient.d.ts +13 -8
  178. package/types/lib/core/httpClient.d.ts.map +1 -1
  179. package/types/lib/core/pluginRun.d.ts +97 -0
  180. package/types/lib/core/pluginRun.d.ts.map +1 -0
  181. package/types/lib/core/processTree.d.ts +36 -0
  182. package/types/lib/core/processTree.d.ts.map +1 -0
  183. package/types/lib/core/projectConfig.d.ts.map +1 -1
  184. package/types/lib/core/projectTooling.d.ts +41 -0
  185. package/types/lib/core/projectTooling.d.ts.map +1 -0
  186. package/types/lib/core/propertySanitizer.d.ts +26 -0
  187. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  188. package/types/lib/core/runState.d.ts +29 -0
  189. package/types/lib/core/runState.d.ts.map +1 -0
  190. package/types/lib/core/toml.d.ts +20 -0
  191. package/types/lib/core/toml.d.ts.map +1 -0
  192. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  193. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  194. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  195. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  196. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  197. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  198. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  199. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  200. package/types/lib/ecosystems/cppEvidence.d.ts +19 -3
  201. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  202. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  203. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  204. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  205. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  207. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  208. package/types/lib/ecosystems/golem.d.ts +81 -1
  209. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  210. package/types/lib/ecosystems/gradleutils.d.ts +59 -7
  211. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  212. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  213. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  214. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  215. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  216. package/types/lib/ecosystems/parsers-dotnet.d.ts +155 -5
  217. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  218. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  219. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  220. package/types/lib/ecosystems/parsers-go.d.ts +119 -5
  221. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  222. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  223. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  224. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  225. package/types/lib/ecosystems/parsers-jvm.d.ts +74 -3
  226. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  227. package/types/lib/ecosystems/parsers-misc.d.ts +21 -59
  228. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  229. package/types/lib/ecosystems/parsers-python.d.ts +57 -6
  230. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  231. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  232. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  233. package/types/lib/ecosystems/pythonutils.d.ts +26 -1
  234. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  235. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  236. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  237. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  238. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  239. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  240. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  241. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  242. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  243. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  244. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  245. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  246. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  247. package/types/lib/ecosystems/utils.d.ts +2 -2
  248. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  249. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  250. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  251. package/types/lib/evinser/evinser.d.ts +100 -8
  252. package/types/lib/evinser/evinser.d.ts.map +1 -1
  253. package/types/lib/evinser/swiftsem.d.ts +10 -0
  254. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  255. package/types/lib/helpers/bomSigner.d.ts +80 -4
  256. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  257. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  258. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  259. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  260. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  261. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  262. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  263. package/types/lib/inventory/atomUtils.d.ts +96 -10
  264. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  265. package/types/lib/inventory/auditCategories.d.ts +3 -0
  266. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  267. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  268. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  269. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  270. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  271. package/types/lib/inventory/cacheDir.d.ts +12 -4
  272. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  273. package/types/lib/inventory/certificates.d.ts +205 -0
  274. package/types/lib/inventory/certificates.d.ts.map +1 -0
  275. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  276. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  277. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  278. package/types/lib/inventory/deps.d.ts +61 -2
  279. package/types/lib/inventory/deps.d.ts.map +1 -1
  280. package/types/lib/inventory/display.d.ts +18 -0
  281. package/types/lib/inventory/display.d.ts.map +1 -1
  282. package/types/lib/inventory/dosai.d.ts +202 -14
  283. package/types/lib/inventory/dosai.d.ts.map +1 -1
  284. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  285. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  286. package/types/lib/inventory/dynamic.d.ts +16 -1
  287. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  288. package/types/lib/inventory/envcontext.d.ts +40 -2
  289. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  290. package/types/lib/inventory/evidenceUtils.d.ts +48 -14
  291. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  292. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  293. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  294. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  295. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  296. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  297. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  298. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  299. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  300. package/types/lib/inventory/hbom.d.ts.map +1 -1
  301. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  302. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  303. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  304. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  305. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  306. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  307. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  308. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  309. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  310. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  311. package/types/lib/inventory/osinfo.d.ts +31 -6
  312. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  313. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  314. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  315. package/types/lib/inventory/parentSelection.d.ts +34 -0
  316. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  317. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  318. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  319. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  320. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  321. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  322. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  323. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  324. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  325. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  326. package/types/lib/inventory/source.d.ts +28 -0
  327. package/types/lib/inventory/source.d.ts.map +1 -1
  328. package/types/lib/inventory/spdx.d.ts +24 -0
  329. package/types/lib/inventory/spdx.d.ts.map +1 -1
  330. package/types/lib/inventory/traceRunner.d.ts +16 -1
  331. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  332. package/types/lib/inventory/tracedCrypto.d.ts +64 -0
  333. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  334. package/types/lib/managers/binary.d.ts +10 -0
  335. package/types/lib/managers/binary.d.ts.map +1 -1
  336. package/types/lib/managers/docker.d.ts +12 -0
  337. package/types/lib/managers/docker.d.ts.map +1 -1
  338. package/types/lib/managers/oci.d.ts +3 -1
  339. package/types/lib/managers/oci.d.ts.map +1 -1
  340. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  341. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  342. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  343. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  344. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  345. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  346. package/types/lib/parsers/compileCommands.d.ts +115 -0
  347. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  348. package/types/lib/parsers/email.d.ts +19 -0
  349. package/types/lib/parsers/email.d.ts.map +1 -0
  350. package/types/lib/parsers/largeJson.d.ts +13 -0
  351. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  352. package/types/lib/parsers/strictJson.d.ts +46 -0
  353. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  354. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  355. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  356. package/types/lib/parsers/terraformSource.d.ts +66 -0
  357. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  358. package/types/lib/server/server.d.ts +8 -0
  359. package/types/lib/server/server.d.ts.map +1 -1
  360. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  361. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  362. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  363. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  364. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  365. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  366. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  367. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  368. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  369. package/types/lib/validator/index.d.ts +8 -0
  370. package/types/lib/validator/index.d.ts.map +1 -1
  371. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  372. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -1,11 +1,24 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFileSync, statSync } from "node:fs";
2
2
  import { platform as _platform } from "node:os";
3
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
+ import {
4
+ basename,
5
+ dirname,
6
+ extname,
7
+ isAbsolute,
8
+ join,
9
+ resolve,
10
+ } from "node:path";
4
11
  import process from "node:process";
5
12
 
6
13
  import { build } from "@cdxgen/cdx-purl";
7
14
 
8
- import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
15
+ import {
16
+ DEBUG_MODE,
17
+ hasDangerousUnicode,
18
+ isDryRun,
19
+ readEnvironmentVariable,
20
+ recordActivity,
21
+ } from "../core/activity.js";
9
22
  import { recordDegradation } from "../core/buildLedger.js";
10
23
  import { deferFailOnError } from "../core/deferredExit.js";
11
24
  import {
@@ -13,6 +26,7 @@ import {
13
26
  hasAnyProjectType,
14
27
  isFeatureEnabled,
15
28
  shouldFetchLicense,
29
+ shouldFetchPackageMetadata,
16
30
  } from "../core/env.js";
17
31
  import {
18
32
  checksumFile,
@@ -25,7 +39,10 @@ import {
25
39
  safeWriteSync,
26
40
  } from "../core/fs.js";
27
41
  import { thoughtLog } from "../core/logger.js";
28
- import { mapWithConcurrency } from "../core/parallel.js";
42
+ import {
43
+ isProjectToolingAllowed,
44
+ reportProjectToolingSkipped,
45
+ } from "../core/projectTooling.js";
29
46
  import {
30
47
  extractJarArchive,
31
48
  getNugetMetadata,
@@ -33,14 +50,20 @@ import {
33
50
  parseBdistMetadata,
34
51
  } from "../ecosystems/ecosystems.js";
35
52
  import {
53
+ agreedNugetVersions,
54
+ applyNugetVersion,
55
+ dotnetManifestProjectDir,
56
+ dotnetRestoredProjectFile,
36
57
  getCentralPackageVersions,
37
- getPropertyGroupTextNodes,
58
+ indexPropsProperties,
38
59
  parseCsPkgData,
39
60
  parseCsPkgLockData,
40
61
  parseCsProjAssetsData,
41
62
  parseCsProjData,
42
63
  parseNupkg,
43
64
  parsePaketLockData,
65
+ projectPropertyCandidates,
66
+ resolveInstalledNugetVersion,
44
67
  } from "../ecosystems/parsers-dotnet.js";
45
68
  import { parseMinJs } from "../ecosystems/parsers-js.js";
46
69
  import {
@@ -52,7 +75,9 @@ import {
52
75
  } from "../ecosystems/parsers-misc.js";
53
76
  import { parseCpanSnapshot } from "../ecosystems/parsers-perl.js";
54
77
  import {
78
+ enrichFromInstalledMetadata,
55
79
  generatePixiLockFile,
80
+ installedMetadataKey,
56
81
  parseMojoProject,
57
82
  parsePiplockData,
58
83
  parsePixiLockFile,
@@ -63,7 +88,7 @@ import {
63
88
  parseSetupPyFile,
64
89
  } from "../ecosystems/parsers-python.js";
65
90
  import { parseRenvLock } from "../ecosystems/parsers-r.js";
66
- import { parseTerraformLockFile } from "../ecosystems/parsers-terraform.js";
91
+ import { parseTerraformWorkspace } from "../ecosystems/parsers-terraform.js";
67
92
  import { collectEmbeddedSboms } from "../ecosystems/pep770.js";
68
93
  import { isPyLockFile } from "../ecosystems/pylockutils.js";
69
94
  import {
@@ -82,6 +107,7 @@ import {
82
107
  parseGemfileLockData,
83
108
  parseGemspecData,
84
109
  } from "../ecosystems/rubyutils.js";
110
+ import { getTerraformRegistryMetadata } from "../ecosystems/terraformRegistry.js";
85
111
  import {
86
112
  collectAiInventory,
87
113
  summarizeAiInventory,
@@ -92,6 +118,15 @@ import {
92
118
  collectDosaiCryptoComponents,
93
119
  collectSourceCryptoComponents,
94
120
  } from "../inventory/cbomutils.js";
121
+ import {
122
+ androidNetworkSecurityConfigPins,
123
+ CertificateCollector,
124
+ certRecordFromDer,
125
+ classifyCryptoFile,
126
+ containerRelativePath,
127
+ kubernetesTlsSecretRecords,
128
+ linkFallbackIssuers,
129
+ } from "../inventory/certificates.js";
95
130
  import { readZipEntry } from "../inventory/deps.js";
96
131
  import {
97
132
  filterInvalidCryptoComponents,
@@ -105,11 +140,17 @@ import {
105
140
  collectDosaiServiceComponents,
106
141
  collectDosaiServicesFromMethods,
107
142
  createDosaiMethodsSlice,
143
+ dosaiFailureReported,
144
+ dosaiMethodsSectionsArgument,
145
+ dosaiReportFileOmittedSections,
146
+ dosaiToolComponents,
108
147
  isDosaiDotnetLanguage,
109
148
  normalizeDosaiServiceMap,
110
149
  readDosaiMethodsReport,
150
+ runsEvidenceAnalysis,
111
151
  } from "../inventory/dosai.js";
112
152
  import { addEvidenceForDotnet } from "../inventory/evidenceUtils.js";
153
+ import { resolvePluginBinary } from "../inventory/plugins.js";
113
154
  import { nugetPurl, pypiBomRef } from "../inventory/purl.js";
114
155
  import { classifyProbeResult } from "../inventory/toolRequirements.js";
115
156
  import { getPluginToolComponents } from "../managers/binary.js";
@@ -216,11 +257,14 @@ export function createPixiBom(path, options) {
216
257
  generatePixiLockFile(path);
217
258
 
218
259
  const pixiLockFile = join(path, "pixi.lock");
219
- if (!safeExistsSync(pixiLockFile) && DEBUG_MODE) {
220
- console.log(
221
- "Unexpected Error tried to generate pixi.lock file but failed.",
222
- );
223
- console.log("This will result in creations of empty BOM.");
260
+ if (!safeExistsSync(pixiLockFile)) {
261
+ if (DEBUG_MODE) {
262
+ console.log(
263
+ "Unexpected Error tried to generate pixi.lock file but failed.",
264
+ );
265
+ }
266
+ // Let createPythonBOM() generate the BOM from the other manifests.
267
+ return null;
224
268
  }
225
269
  PixiLockData = parsePixiLockFile(pixiLockFile);
226
270
  metadataFilename = "pixi.lock";
@@ -404,6 +448,17 @@ export async function createPythonBom(path, options) {
404
448
  !pyLockFiles?.length;
405
449
  const poetryMode = poetryFiles?.length;
406
450
 
451
+ // The installed dist-info METADATA files describe the distributions this
452
+ // machine holds. They are read once, before any lockfile is parsed, so the
453
+ // lockfile entries they match are enriched before PyPI is asked and the
454
+ // registry is left only what the installations do not answer.
455
+ const installedLists = (metadataFiles || []).map((mf) =>
456
+ parseBdistMetadata(mf),
457
+ );
458
+ const installedMetadata = installedLists
459
+ .map((dlist) => dlist?.[0])
460
+ .filter((md) => md?.name);
461
+
407
462
  // TODO: Support for nested directories
408
463
  const setupPy = join(path, "setup.py");
409
464
  const setupPyMode = safeExistsSync(setupPy);
@@ -416,7 +471,9 @@ export async function createPythonBom(path, options) {
416
471
  if (DEBUG_MODE) {
417
472
  console.log(`Parsing ${f}`);
418
473
  }
419
- let retMap = await parsePyLockData(lockData, f);
474
+ let retMap = await parsePyLockData(lockData, f, undefined, {
475
+ installedMetadata,
476
+ });
420
477
  if (retMap?.workspaceWarningShown) {
421
478
  deferFailOnError(options, {
422
479
  ecosystem: "python",
@@ -472,11 +529,14 @@ export async function createPythonBom(path, options) {
472
529
  retMap.dependenciesList,
473
530
  parentComponent,
474
531
  );
475
- } else {
532
+ } else if (isProjectToolingAllowed(options)) {
533
+ // poetry export and pdm export load plugins from the project tree
534
+ // (.poetry/plugins, .pdm-plugins), so they run only when the
535
+ // project's own tooling may.
476
536
  let exportedReqs = "";
477
537
  if (f.endsWith("poetry.lock")) {
478
538
  thoughtLog(
479
- "Using poetry export as a safe, static alternative to pip install.",
539
+ "Using poetry export to collect the resolved closure without installing anything.",
480
540
  );
481
541
  const expCmd = safeSpawnSync(
482
542
  "poetry",
@@ -487,7 +547,7 @@ export async function createPythonBom(path, options) {
487
547
  exportedReqs = expCmd.stdout.toString();
488
548
  } else if (f.endsWith("pdm.lock")) {
489
549
  thoughtLog(
490
- "Using pdm export as a safe, static alternative to pip install.",
550
+ "Using pdm export to collect the resolved closure without installing anything.",
491
551
  );
492
552
  const expCmd = safeSpawnSync(
493
553
  "pdm",
@@ -508,25 +568,33 @@ export async function createPythonBom(path, options) {
508
568
  `exported-${basename(basePath)}-reqs.txt`,
509
569
  );
510
570
  safeWriteSync(tmpReqFile, exportedReqs);
511
- const dlist = await parseReqFile(tmpReqFile, false);
571
+ const dlist = await parseReqFile(
572
+ tmpReqFile,
573
+ false,
574
+ installedMetadata,
575
+ );
512
576
  if (dlist?.length) {
513
577
  pkgList = pkgList.concat(dlist);
514
578
  }
515
579
  }
580
+ } else {
581
+ reportProjectToolingSkipped(
582
+ options,
583
+ "python.poetry-export.tooling-disabled",
584
+ "Skipping 'poetry export' and 'pdm export' since dependency installation is disabled. Only the packages parsed from the lock file will be used.",
585
+ {
586
+ ecosystem: "python",
587
+ tool: f.endsWith("pdm.lock") ? "pdm" : "poetry",
588
+ impact: "transitive-deps",
589
+ detail:
590
+ "Dependency installation is disabled, so poetry or pdm export was not run and only the packages parsed from the lock file are available.",
591
+ },
592
+ );
516
593
  }
517
594
  }
518
595
  }
519
596
  options.parentComponent = parentComponent;
520
597
  } // poetryMode
521
- if (metadataFiles?.length) {
522
- // dist-info directories
523
- for (const mf of metadataFiles) {
524
- const dlist = parseBdistMetadata(mf);
525
- if (dlist?.length) {
526
- pkgList = pkgList.concat(dlist);
527
- }
528
- }
529
- }
530
598
  // .whl files. Zip file containing dist-info directory
531
599
  if (whlFiles?.length) {
532
600
  for (const wf of whlFiles) {
@@ -592,7 +660,7 @@ export async function createPythonBom(path, options) {
592
660
  let dlist;
593
661
  try {
594
662
  const lockData = JSON.parse(readFileSync(piplockFile));
595
- dlist = await parsePiplockData(lockData);
663
+ dlist = await parsePiplockData(lockData, installedMetadata);
596
664
  } catch (err) {
597
665
  recordDegradation("python.lockfile-unparseable.pipenv", {
598
666
  ecosystem: "python",
@@ -617,7 +685,7 @@ export async function createPythonBom(path, options) {
617
685
  } else if (reqDirFiles?.length) {
618
686
  for (const j in reqDirFiles) {
619
687
  const f = reqDirFiles[j];
620
- const dlist = await parseReqFile(f, false);
688
+ const dlist = await parseReqFile(f, false, installedMetadata);
621
689
  if (dlist?.length) {
622
690
  pkgList = pkgList.concat(dlist);
623
691
  }
@@ -625,13 +693,30 @@ export async function createPythonBom(path, options) {
625
693
  metadataFilename = reqDirFiles.join(", ");
626
694
  } else if (reqFiles?.length) {
627
695
  for (const f of reqFiles) {
628
- const dlist = await parseReqFile(f, false);
696
+ const dlist = await parseReqFile(f, false, installedMetadata);
629
697
  if (dlist?.length) {
630
698
  pkgList = pkgList.concat(dlist);
631
699
  }
632
700
  }
633
701
  }
634
702
  }
703
+ if (metadataFiles?.length) {
704
+ // dist-info directories, after every manifest block has added its
705
+ // entries. An installation whose package a lockfile, Pipfile.lock or
706
+ // requirements entry already lists enriched that entry, so it is not
707
+ // also emitted as a component of its own; an installation nothing else
708
+ // covers still is one. This pass is the emission decision only: each
709
+ // parser enriched its own entries before it asked PyPI.
710
+ const enriched = enrichFromInstalledMetadata(pkgList, installedMetadata);
711
+ for (const dlist of installedLists) {
712
+ if (dlist?.length) {
713
+ if (enriched.has(installedMetadataKey(dlist[0]))) {
714
+ continue;
715
+ }
716
+ pkgList = pkgList.concat(dlist);
717
+ }
718
+ }
719
+ }
635
720
 
636
721
  const parentDependsOn = new Set();
637
722
 
@@ -742,79 +827,81 @@ export async function createPythonBom(path, options) {
742
827
  );
743
828
  }
744
829
  }
745
- // ATOM parsedeps block
746
- // Atom parsedeps slices can be used to identify packages that are not declared in manifests
747
- // Since it is a slow operation, we only use it as a fallback or in deep mode
748
- // This change was made in 10.9.2 release onwards
749
- if (options.deep || !pkgList.length) {
750
- if (!pkgList.length) {
751
- thoughtLog(
752
- "I couldn't find any components yet. Let's try static analysis with atom parsedeps command.",
753
- );
754
- }
755
- const retMap = await getPyModules(path, pkgList, options);
756
- // We need to patch the existing package list to add ImportedModules for evinse to work
757
- if (retMap.modList?.length) {
758
- const iSymbolsMap = {};
759
- retMap.modList.forEach((v) => {
760
- iSymbolsMap[v.name] = v.importedSymbols;
761
- iSymbolsMap[v.name.replace(/_/g, "-")] = v.importedSymbols;
762
- });
763
- for (const apkg of pkgList) {
764
- if (iSymbolsMap[apkg.name]) {
765
- apkg.scope = "required";
766
- apkg.properties = apkg.properties || [];
767
- apkg.properties.push({
768
- name: "internal:ImportedModules",
769
- value: iSymbolsMap[apkg.name],
770
- });
771
- }
830
+ }
831
+ // ATOM parsedeps block
832
+ // Atom parsedeps slices can be used to identify packages that are not declared in manifests
833
+ // Since it is a slow operation, we only use it as a fallback or in deep mode
834
+ // This change was made in 10.9.2 release onwards. It reads the sources and
835
+ // installs nothing, so it runs whether or not dependencies are installed.
836
+ if (options.deep || !pkgList.length) {
837
+ if (!pkgList.length) {
838
+ thoughtLog(
839
+ "I couldn't find any components yet. Let's try static analysis with atom parsedeps command.",
840
+ );
841
+ }
842
+ const retMap = await getPyModules(path, pkgList, options);
843
+ // We need to patch the existing package list to add ImportedModules for evinse to work
844
+ if (retMap.modList?.length) {
845
+ const iSymbolsMap = {};
846
+ retMap.modList.forEach((v) => {
847
+ iSymbolsMap[v.name] = v.importedSymbols;
848
+ iSymbolsMap[v.name.replace(/_/g, "-")] = v.importedSymbols;
849
+ });
850
+ for (const apkg of pkgList) {
851
+ if (iSymbolsMap[apkg.name]) {
852
+ apkg.scope = "required";
853
+ apkg.properties = apkg.properties || [];
854
+ apkg.properties.push({
855
+ name: "internal:ImportedModules",
856
+ value: iSymbolsMap[apkg.name],
857
+ });
772
858
  }
773
859
  }
774
- if (retMap.pkgList?.length) {
775
- pkgList = pkgList.concat(retMap.pkgList);
776
- for (const p of retMap.pkgList) {
777
- if (
778
- !p.version ||
779
- (parentComponent &&
780
- p.name === parentComponent.name &&
781
- (p.version === parentComponent.version ||
782
- p.version === "latest"))
783
- ) {
784
- continue;
785
- }
786
- parentDependsOn.add(pypiBomRef(p.name, p.version));
860
+ }
861
+ if (retMap.pkgList?.length) {
862
+ pkgList = pkgList.concat(retMap.pkgList);
863
+ for (const p of retMap.pkgList) {
864
+ if (
865
+ !p.version ||
866
+ (parentComponent &&
867
+ p.name === parentComponent.name &&
868
+ (p.version === parentComponent.version || p.version === "latest"))
869
+ ) {
870
+ continue;
787
871
  }
788
- }
789
- if (retMap.dependenciesList) {
790
- dependencies = mergeDependencies(
791
- dependencies,
792
- retMap.dependenciesList,
793
- parentComponent,
794
- );
795
- }
796
- if (retMap.allImports) {
797
- allImports = { ...allImports, ...retMap.allImports };
872
+ parentDependsOn.add(pypiBomRef(p.name, p.version));
798
873
  }
799
874
  }
800
- // ATOM parsedeps block
801
- let parentPresent = false;
802
- for (const d of dependencies) {
803
- if (d.ref === parentComponent["bom-ref"]) {
804
- parentPresent = true;
805
- break;
806
- }
875
+ if (retMap.dependenciesList) {
876
+ dependencies = mergeDependencies(
877
+ dependencies,
878
+ retMap.dependenciesList,
879
+ parentComponent,
880
+ );
807
881
  }
808
- if (!parentPresent) {
809
- const pdependencies = {
810
- ref: parentComponent["bom-ref"],
811
- dependsOn: Array.from(parentDependsOn).filter(
812
- (r) => parentComponent && r !== parentComponent["bom-ref"],
813
- ),
814
- };
815
- dependencies.splice(0, 0, pdependencies);
882
+ if (retMap.allImports) {
883
+ allImports = { ...allImports, ...retMap.allImports };
816
884
  }
817
885
  }
886
+ // ATOM parsedeps block
887
+ let parentPresent = false;
888
+ for (const d of dependencies) {
889
+ if (d.ref === parentComponent["bom-ref"]) {
890
+ parentPresent = true;
891
+ break;
892
+ }
893
+ }
894
+ // Without an install, the parent's dependencies are known only when the
895
+ // import scan found some: an empty list would claim it has none.
896
+ if (!parentPresent && (options.installDeps || parentDependsOn.size)) {
897
+ const pdependencies = {
898
+ ref: parentComponent["bom-ref"],
899
+ dependsOn: Array.from(parentDependsOn).filter(
900
+ (r) => parentComponent && r !== parentComponent["bom-ref"],
901
+ ),
902
+ };
903
+ dependencies.splice(0, 0, pdependencies);
904
+ }
818
905
  }
819
906
  // Final fallback is to manually parse setup.py if we still
820
907
  // have an empty list
@@ -843,9 +930,11 @@ export async function createPythonBom(path, options) {
843
930
  );
844
931
  }
845
932
  emitAiInventorySummary(aiInventory, path);
846
- // Check and complete the dependency tree
933
+ // Check and complete the dependency tree. This installs every package into a
934
+ // virtual environment, so --no-install-deps turns it off too.
847
935
  if (
848
936
  isFeatureEnabled(options, "safe-pip-install") &&
937
+ options.installDeps !== false &&
849
938
  pkgList.length &&
850
939
  isPartialTree(dependencies, pkgList.length)
851
940
  ) {
@@ -1072,6 +1161,28 @@ export function createHelmBom(path, options) {
1072
1161
  return {};
1073
1162
  }
1074
1163
 
1164
+ /**
1165
+ * The properties of a list followed by those of another it does not hold yet.
1166
+ * It returns a new array, so a shallow copy of a component keeps its own
1167
+ * properties.
1168
+ *
1169
+ * @param {{name: string, value: string}[]|undefined} existing Properties held so far
1170
+ * @param {{name: string, value: string}[]} additions Properties to add
1171
+ * @returns {{name: string, value: string}[]} The merged properties
1172
+ */
1173
+ function withMissingProperties(existing, additions) {
1174
+ const held = existing || [];
1175
+ return [
1176
+ ...held,
1177
+ ...additions.filter(
1178
+ (added) =>
1179
+ !held.some(
1180
+ (known) => known.name === added.name && known.value === added.value,
1181
+ ),
1182
+ ),
1183
+ ];
1184
+ }
1185
+
1075
1186
  /**
1076
1187
  * Function to create bom string for php projects
1077
1188
  *
@@ -1104,6 +1215,10 @@ export function createPHPBom(path, options) {
1104
1215
  composerLockSearchOptions,
1105
1216
  );
1106
1217
  let pkgList = [];
1218
+ // Platform facts of the scanned directory's lock when its composer.json names
1219
+ // no project: there is no module component to carry them, so they go on the
1220
+ // document. A nested lock like that has nowhere to put them.
1221
+ let metadataProperties = [];
1107
1222
  const composerJsonMode = composerJsonFiles.length;
1108
1223
  const composerLockMode = composerLockFiles.length;
1109
1224
  // Create a composer.lock file for each composer.json file if needed.
@@ -1217,6 +1332,37 @@ export function createPHPBom(path, options) {
1217
1332
  }
1218
1333
  }
1219
1334
  const retMap = parseComposerLock(f, rootRequires);
1335
+ if (retMap.platformProperties?.length) {
1336
+ if (moduleParent?.["bom-ref"]) {
1337
+ // The root composer.json is parsed twice, once for the parent
1338
+ // component and once per lock, so the object that ends up as
1339
+ // metadata.component is not always the module parsed here.
1340
+ for (const module of [moduleParent, parentComponent]) {
1341
+ if (module?.["bom-ref"] === moduleParent["bom-ref"]) {
1342
+ module.properties = withMissingProperties(
1343
+ module.properties,
1344
+ retMap.platformProperties,
1345
+ );
1346
+ }
1347
+ }
1348
+ } else if (resolve(basePath) === resolve(path)) {
1349
+ // The document describes the scanned directory, so only the lock
1350
+ // in it may put its facts on the document.
1351
+ metadataProperties = withMissingProperties(
1352
+ metadataProperties,
1353
+ retMap.platformProperties,
1354
+ );
1355
+ } else {
1356
+ recordDegradation("php.composer.platform-unattributed", {
1357
+ ecosystem: "php",
1358
+ tool: "composer",
1359
+ impact: "versions",
1360
+ path: f,
1361
+ detail:
1362
+ "The composer.json beside this lock names no project, so no component can carry the PHP and extension requirements of the lock.",
1363
+ });
1364
+ }
1365
+ }
1220
1366
  if (retMap.pkgList?.length) {
1221
1367
  pkgList = pkgList.concat(retMap.pkgList);
1222
1368
  pkgList = trimComponents(pkgList);
@@ -1256,6 +1402,7 @@ export function createPHPBom(path, options) {
1256
1402
  filename: composerLockFiles.join(", "),
1257
1403
  dependencies,
1258
1404
  parentComponent,
1405
+ metadataProperties,
1259
1406
  });
1260
1407
  }
1261
1408
  if (composerJsonMode && !composerLockMode) {
@@ -1403,7 +1550,7 @@ export async function createRubyBom(path, options) {
1403
1550
  console.log(`Parsing ${f}`);
1404
1551
  }
1405
1552
  const gemLockData = readFileSync(f, { encoding: "utf-8" });
1406
- const retMap = await parseGemfileLockData(gemLockData, f);
1553
+ const retMap = await parseGemfileLockData(gemLockData, f, { gemHome });
1407
1554
  if (retMap.pkgList?.length) {
1408
1555
  pkgList = pkgList.concat(retMap.pkgList);
1409
1556
  pkgList = trimComponents(pkgList);
@@ -1457,7 +1604,7 @@ export async function createRubyBom(path, options) {
1457
1604
  const gpkgList = await parseGemspecData(
1458
1605
  gemspecData,
1459
1606
  f,
1460
- isRootGemspec ? { lockedVersions } : {},
1607
+ isRootGemspec ? { lockedVersions, projectGemspec: true } : {},
1461
1608
  );
1462
1609
  if (!gpkgList.length) {
1463
1610
  continue;
@@ -1503,7 +1650,14 @@ export async function createRubyBom(path, options) {
1503
1650
  options.resolveClass ||
1504
1651
  (options.deep && !hasAnyProjectType(["oci"], options, false))
1505
1652
  ) {
1506
- if (gemHome && !isGemHomeEmpty) {
1653
+ // `bundle exec` evaluates the project's Gemfile, a Ruby program
1654
+ if (gemHome && !isGemHomeEmpty && !isProjectToolingAllowed(options)) {
1655
+ reportProjectToolingSkipped(
1656
+ options,
1657
+ null,
1658
+ "Skipping the module names of gems since dependency installation is disabled. Collecting them runs 'bundle exec', which evaluates the Gemfile.",
1659
+ );
1660
+ } else if (gemHome && !isGemHomeEmpty) {
1507
1661
  const rubyCommand =
1508
1662
  readEnvironmentVariable("CDXGEN_RUBY_CMD") ||
1509
1663
  readEnvironmentVariable("RUBY_CMD") ||
@@ -1619,6 +1773,12 @@ export async function createCsharpBom(path, options) {
1619
1773
  return createBinaryBom(path, options);
1620
1774
  }
1621
1775
  let parentComponent = createDefaultParentComponent(path, "nuget", options);
1776
+ // The directory a scan starts from is not a package, so it has a version
1777
+ // only when --project-version gives it one; "latest" is not a version.
1778
+ applyNugetVersion(
1779
+ parentComponent,
1780
+ options.projectVersion ? `${options.projectVersion}` : undefined,
1781
+ );
1622
1782
  const slnFiles = getAllFiles(
1623
1783
  path,
1624
1784
  `${options.multiProject ? "**/" : ""}*.sln*`,
@@ -1821,49 +1981,129 @@ export async function createCsharpBom(path, options) {
1821
1981
  const parentDependsOn = new Set();
1822
1982
  if (nupkgFiles.length && projAssetsFiles.length === 0) {
1823
1983
  manifestFiles = manifestFiles.concat(nupkgFiles);
1824
- // When parsing nupkg files, only version ranges will be specified under dependencies
1825
- // To resolve the version, we need to track the mapping between name and resolved versions here
1826
- let dependenciesMap = {};
1827
- const pkgNameVersions = {};
1984
+ // A nuspec states the range each dependency accepts, not the version
1985
+ // installed. The installed packages are the components, and a dependency
1986
+ // edge points at the installed version its range resolves to; a minimum
1987
+ // version nobody installed is not a component.
1988
+ const installedVersions = {};
1989
+ const installedNames = {};
1990
+ let dependencyRanges = {};
1828
1991
  for (const nf of nupkgFiles) {
1829
1992
  if (DEBUG_MODE) {
1830
1993
  console.log(`Parsing ${nf}`);
1831
1994
  }
1832
1995
  const retMap = await parseNupkg(nf);
1833
- if (retMap?.pkgList?.length) {
1834
- pkgList = pkgList.concat(retMap.pkgList);
1835
- for (const d of retMap.pkgList) {
1836
- parentDependsOn.add(d["bom-ref"]);
1837
- pkgNameVersions[d.name] = d.version;
1996
+ const installed = retMap?.pkgList?.[0];
1997
+ if (installed) {
1998
+ pkgList.push(installed);
1999
+ parentDependsOn.add(installed["bom-ref"]);
2000
+ const key = installed.name.toLowerCase();
2001
+ installedNames[key] ??= installed.name;
2002
+ installedVersions[key] ??= [];
2003
+ if (installed.version) {
2004
+ installedVersions[key].push(installed.version);
1838
2005
  }
1839
2006
  }
1840
- if (retMap?.dependenciesMap) {
1841
- dependenciesMap = { ...dependenciesMap, ...retMap.dependenciesMap };
2007
+ if (retMap?.dependencyRanges) {
2008
+ dependencyRanges = { ...dependencyRanges, ...retMap.dependencyRanges };
1842
2009
  }
1843
2010
  } // end for
1844
- for (const k of Object.keys(dependenciesMap)) {
1845
- const dependsOn = dependenciesMap[k].map((p) => {
1846
- const ver = pkgNameVersions[p] || "latest";
1847
- const purl = nugetPurl(p, ver);
1848
- return purl ? decodeURIComponent(purl) : `library:${p}:${ver}`;
1849
- });
1850
- dependencies.push({ ref: k, dependsOn: [...new Set(dependsOn)].sort() });
2011
+ for (const [ref, ranges] of Object.entries(dependencyRanges)) {
2012
+ const dependsOn = new Set();
2013
+ for (const [name, range] of Object.entries(ranges)) {
2014
+ const key = name.toLowerCase();
2015
+ const version = resolveInstalledNugetVersion(
2016
+ range,
2017
+ installedVersions[key],
2018
+ );
2019
+ const purl = version ? nugetPurl(installedNames[key], version) : null;
2020
+ if (purl) {
2021
+ dependsOn.add(decodeURIComponent(purl));
2022
+ }
2023
+ }
2024
+ dependencies.push({ ref, dependsOn: [...dependsOn].sort() });
1851
2025
  }
1852
2026
  }
2027
+ // Versions each project restored, by project directory and lowercased
2028
+ // package id. Two projects of one tree routinely restore different versions
2029
+ // of a package, so a version missing from one project's manifest is taken
2030
+ // from that project's restore output, never from another project's.
2031
+ // The restore output also spells each package id the way its nuspec does.
2032
+ const projectVersions = {};
2033
+ const projectNames = {};
2034
+ const recordProjectVersions = (projectDir, pkgs) => {
2035
+ projectVersions[projectDir] ??= {};
2036
+ projectNames[projectDir] ??= {};
2037
+ for (const p of pkgs || []) {
2038
+ if (
2039
+ p?.name &&
2040
+ p.version &&
2041
+ !["application", "project"].includes(p.type)
2042
+ ) {
2043
+ projectVersions[projectDir][p.name.toLowerCase()] ??= p.version;
2044
+ projectNames[projectDir][p.name.toLowerCase()] ??= p.name;
2045
+ }
2046
+ }
2047
+ };
2048
+ // Direct dependencies of each project directory from its lock file or
2049
+ // packages.config, attached to that project once the project files are read.
2050
+ const rootsByProjectDir = {};
2051
+ const recordRoots = (projectDir, pkgs) => {
2052
+ rootsByProjectDir[projectDir] ??= new Set();
2053
+ for (const p of pkgs || []) {
2054
+ if (p?.["bom-ref"]) {
2055
+ rootsByProjectDir[projectDir].add(p["bom-ref"]);
2056
+ }
2057
+ }
2058
+ };
1853
2059
  // In a multi-project scan, different projects may use different manifest
1854
2060
  // formats: modern SDK-style (project.assets.json / packages.lock.json) or
1855
2061
  // legacy (packages.config). Apply the assets > lock > config precedence per
1856
2062
  // project directory instead of across the whole scan, so dependencies from
1857
2063
  // one project's manifest are not dropped just because another format exists
1858
2064
  // elsewhere in the scan.
1859
- const assetsProjectDirs = new Set(
1860
- projAssetsFiles.map((af) => {
1861
- // project.assets.json is generated under <project>/obj/
1862
- const d = dirname(af);
1863
- return basename(d) === "obj" ? dirname(d) : d;
1864
- }),
1865
- );
1866
- const lockProjectDirs = new Set(pkgLockFiles.map((af) => dirname(af)));
2065
+ const assetsProjectDirs = new Set();
2066
+ // Restore records a project file the way the build was pointed at it, which
2067
+ // on a case-insensitive file system can differ in case from the file found
2068
+ // here. Such a path takes the found file's spelling, unless that is ambiguous.
2069
+ const foundProjectFiles = new Set();
2070
+ const foundProjectFilesByKey = new Map();
2071
+ for (const f of csProjFiles) {
2072
+ const file = resolve(f);
2073
+ const key = file.toLowerCase();
2074
+ foundProjectFiles.add(file);
2075
+ foundProjectFilesByKey.set(
2076
+ key,
2077
+ foundProjectFilesByKey.has(key) ? null : file,
2078
+ );
2079
+ }
2080
+ const foundProjectFile = (file) =>
2081
+ !file || foundProjectFiles.has(file)
2082
+ ? file
2083
+ : foundProjectFilesByKey.get(file.toLowerCase()) || file;
2084
+ // The package id and version each project's restore output records, by
2085
+ // project file. A project whose restore outputs disagree, such as a stale
2086
+ // obj/ beside an artifacts directory, takes neither.
2087
+ const restoredIdentities = new Map();
2088
+ const recordRestoredIdentity = (projectFile, identity) => {
2089
+ if (!projectFile || !identity) {
2090
+ return;
2091
+ }
2092
+ const known = restoredIdentities.get(projectFile);
2093
+ if (known === undefined) {
2094
+ restoredIdentities.set(projectFile, identity);
2095
+ } else if (
2096
+ known &&
2097
+ (known.name !== identity.name || known.version !== identity.version)
2098
+ ) {
2099
+ if (DEBUG_MODE) {
2100
+ console.log(
2101
+ `Restore outputs disagree on the package id or version of ${projectFile}, so the identity its project file states is used.`,
2102
+ );
2103
+ }
2104
+ restoredIdentities.set(projectFile, null);
2105
+ }
2106
+ };
1867
2107
  // project.assets.json parsing
1868
2108
  if (projAssetsFiles.length) {
1869
2109
  manifestFiles = manifestFiles.concat(projAssetsFiles);
@@ -1875,6 +2115,15 @@ export async function createCsharpBom(path, options) {
1875
2115
  const results = parseCsProjAssetsData(pkgData, af);
1876
2116
  const deps = results["dependenciesList"];
1877
2117
  const dlist = results["pkgList"];
2118
+ // Restore output names its project; an artifacts layout keeps it outside
2119
+ // the project directory.
2120
+ const projectFile = foundProjectFile(
2121
+ dotnetRestoredProjectFile(af, results.projectPath, results.outputPath),
2122
+ );
2123
+ const projectDir = dotnetManifestProjectDir(af, projectFile);
2124
+ assetsProjectDirs.add(projectDir);
2125
+ recordProjectVersions(projectDir, dlist);
2126
+ recordRestoredIdentity(projectFile, results.projectIdentity);
1878
2127
  if (dlist?.length) {
1879
2128
  pkgList = pkgList.concat(dlist);
1880
2129
  }
@@ -1907,9 +2156,12 @@ export async function createCsharpBom(path, options) {
1907
2156
  });
1908
2157
  }
1909
2158
  }
2159
+ const lockProjectDirs = new Set(
2160
+ pkgLockFiles.map((af) => dotnetManifestProjectDir(af)),
2161
+ );
1910
2162
  // packages.lock.json parsing for any project without a project.assets.json
1911
2163
  const pkgLockToParse = pkgLockFiles.filter(
1912
- (af) => !assetsProjectDirs.has(dirname(af)),
2164
+ (af) => !assetsProjectDirs.has(dotnetManifestProjectDir(af)),
1913
2165
  );
1914
2166
  if (pkgLockToParse.length) {
1915
2167
  manifestFiles = manifestFiles.concat(pkgLockToParse);
@@ -1923,37 +2175,29 @@ export async function createCsharpBom(path, options) {
1923
2175
  const deps = results["dependenciesList"];
1924
2176
  const dlist = results["pkgList"];
1925
2177
  const rootList = results["rootList"];
2178
+ const projectDir = dotnetManifestProjectDir(af);
2179
+ recordProjectVersions(projectDir, dlist);
1926
2180
  if (dlist?.length) {
1927
2181
  pkgList = pkgList.concat(dlist);
1928
2182
  }
1929
2183
  if (deps?.length) {
1930
2184
  dependencies = mergeDependencies(dependencies, deps, parentComponent);
1931
2185
  }
1932
- // Keep track of the direct dependencies so that we can construct one complete
1933
- // list after processing all lock files
1934
- if (rootList?.length) {
1935
- for (const p of rootList) {
1936
- parentDependsOn.add(p["bom-ref"]);
1937
- }
1938
- }
2186
+ recordRoots(projectDir, rootList);
1939
2187
  }
1940
2188
  }
1941
2189
  // packages.config parsing for any project without a modern manifest
1942
2190
  // (project.assets.json / packages.lock.json) covering the same directory
1943
2191
  const pkgConfigToParse = pkgConfigFiles.filter((f) => {
1944
- const projDir = dirname(f);
2192
+ const projDir = dotnetManifestProjectDir(f);
1945
2193
  return !assetsProjectDirs.has(projDir) && !lockProjectDirs.has(projDir);
1946
2194
  });
1947
2195
  if (pkgConfigToParse.length) {
1948
2196
  manifestFiles = manifestFiles.concat(pkgConfigToParse);
1949
- // Versions already resolved from project.assets.json / packages.lock.json
1950
- // can backfill templated or missing versions in packages.config
1951
- const resolvedVersions = {};
1952
- for (const p of pkgList) {
1953
- if (p.name && p.version) {
1954
- resolvedVersions[p.name] = p.version;
1955
- }
1956
- }
2197
+ // A templated or missing version is backfilled only from a version every
2198
+ // other manifest of the scan agrees on: these projects restored nothing of
2199
+ // their own, and another project's version may not be theirs.
2200
+ const resolvedVersions = agreedNugetVersions(pkgList);
1957
2201
  // packages.config parsing
1958
2202
  for (const f of pkgConfigToParse) {
1959
2203
  if (DEBUG_MODE) {
@@ -1961,11 +2205,11 @@ export async function createCsharpBom(path, options) {
1961
2205
  }
1962
2206
  pkgData = readFileSync(f, { encoding: "utf-8" });
1963
2207
  const dlist = parseCsPkgData(pkgData, f, resolvedVersions);
2208
+ const projectDir = dotnetManifestProjectDir(f);
2209
+ recordProjectVersions(projectDir, dlist);
1964
2210
  if (dlist?.length) {
1965
2211
  pkgList = pkgList.concat(dlist);
1966
- for (const d of dlist) {
1967
- parentDependsOn.add(d["bom-ref"]);
1968
- }
2212
+ recordRoots(projectDir, dlist);
1969
2213
  }
1970
2214
  }
1971
2215
  }
@@ -1988,27 +2232,34 @@ export async function createCsharpBom(path, options) {
1988
2232
  }
1989
2233
  }
1990
2234
  }
1991
- const pkgNameVersions = {};
2235
+ const projectRefByDir = {};
1992
2236
  let services = [];
2237
+ let dosaiTools = [];
1993
2238
  if (csProjFiles.length) {
1994
2239
  manifestFiles = manifestFiles.concat(csProjFiles);
1995
- // Parsing csproj is quite error-prone. Some project files may not have versions specified
1996
- // To work around this, we make use of the version from the existing list
1997
- for (const p of pkgList) {
1998
- if (p.version) {
1999
- pkgNameVersions[p.name] = p.version;
2000
- }
2001
- }
2240
+ // Parsing csproj is quite error-prone. Some project files may not have
2241
+ // versions specified. Such a version comes from what this project restored,
2242
+ // then from central package management, and only then from a version the
2243
+ // whole scan agrees on.
2244
+ const agreedVersions = agreedNugetVersions(pkgList);
2002
2245
  let msbuildInstalled = false;
2003
- let pkgVersionLabelCandidates = {};
2004
- const result = safeSpawnSync(DOTNET_CMD, ["msbuild --version"], {
2005
- shell: isWin,
2006
- });
2007
- if (result.status === 0 && !result.error && result.stdout.trim()) {
2008
- msbuildInstalled = true;
2009
- } else {
2010
- pkgVersionLabelCandidates = getPropertyGroupTextNodes(propsFiles);
2246
+ // `dotnet msbuild` evaluates the project (loading Directory.Build.rsp,
2247
+ // Directory.Build.props/targets and build extensions), so it belongs to
2248
+ // the dependency-tooling surface disabled by --no-install-deps and the
2249
+ // pre-build lifecycle. Version labels it would resolve then fall back to
2250
+ // the static props/central-version resolution.
2251
+ if (isProjectToolingAllowed(options)) {
2252
+ const result = safeSpawnSync(DOTNET_CMD, ["msbuild", "--version"], {
2253
+ shell: isWin,
2254
+ });
2255
+ if (result.status === 0 && !result.error && result.stdout?.trim()) {
2256
+ msbuildInstalled = true;
2257
+ }
2011
2258
  }
2259
+ // `$(Name)` version labels resolve from the props files that can apply to
2260
+ // each project (beside and above it), read once for the whole scan.
2261
+ // msbuild evaluates only what they leave unresolved.
2262
+ const propsIndex = indexPropsProperties(propsFiles);
2012
2263
  // Directory.Packages.props can sit above the directory being scanned, so it is
2013
2264
  // resolved per project file by walking up rather than from the props glob below.
2014
2265
  // The cache keeps a repository with many projects from re-parsing the same file.
@@ -2019,15 +2270,20 @@ export async function createCsharpBom(path, options) {
2019
2270
  console.log(`Parsing ${f}`);
2020
2271
  }
2021
2272
  const csProjData = readFileSync(f, { encoding: "utf-8" });
2273
+ const projectDir = dotnetManifestProjectDir(f);
2022
2274
  const retMap = parseCsProjData(
2023
2275
  csProjData,
2024
2276
  f,
2025
- pkgNameVersions,
2277
+ projectVersions[projectDir] || {},
2026
2278
  msbuildInstalled,
2027
- pkgVersionLabelCandidates,
2279
+ projectPropertyCandidates(f, propsIndex),
2028
2280
  getCentralPackageVersions(f, centralVersionsCache),
2281
+ agreedVersions,
2282
+ projectNames[projectDir] || {},
2283
+ restoredIdentities.get(resolve(f)) || undefined,
2029
2284
  );
2030
2285
  if (retMap?.parentComponent?.purl) {
2286
+ projectRefByDir[projectDir] ??= retMap.parentComponent["bom-ref"];
2031
2287
  // If there are multiple project files, track the parent components using nested components
2032
2288
  if (csProjFiles.length > 1) {
2033
2289
  if (!parentComponent.components) {
@@ -2049,8 +2305,56 @@ export async function createCsharpBom(path, options) {
2049
2305
  parentComponent,
2050
2306
  );
2051
2307
  }
2308
+ // Only a version that needed msbuild makes its skip a loss
2309
+ if (
2310
+ retMap?.unresolvedVersionLabels?.length &&
2311
+ !isProjectToolingAllowed(options)
2312
+ ) {
2313
+ reportProjectToolingSkipped(
2314
+ options,
2315
+ "dotnet.msbuild.tooling-disabled",
2316
+ "Skipping 'dotnet msbuild' property evaluation since dependency installation is disabled. Package versions that come from MSBuild properties no project or props file sets stay empty.",
2317
+ {
2318
+ ecosystem: "csharp",
2319
+ tool: "msbuild",
2320
+ impact: "versions",
2321
+ detail:
2322
+ "Dependency installation is disabled, so $(Property) version references that the project and props files do not set were not evaluated with msbuild.",
2323
+ },
2324
+ );
2325
+ }
2326
+ }
2327
+ }
2328
+ // A lock file's or packages.config's direct dependencies belong to the
2329
+ // project in that directory; without a project file there, to the parent.
2330
+ for (const [projectDir, roots] of Object.entries(rootsByProjectDir)) {
2331
+ if (!roots.size) {
2332
+ continue;
2333
+ }
2334
+ const projectRef = projectRefByDir[projectDir];
2335
+ if (projectRef && projectRef !== parentComponent?.["bom-ref"]) {
2336
+ dependencies = mergeDependencies(
2337
+ dependencies,
2338
+ [{ ref: projectRef, dependsOn: [...roots].sort() }],
2339
+ parentComponent,
2340
+ );
2341
+ } else {
2342
+ for (const ref of roots) {
2343
+ parentDependsOn.add(ref);
2344
+ }
2052
2345
  }
2053
2346
  }
2347
+ // A scan of several projects is made of them, so the directory depends on
2348
+ // each, as a Gradle build does on its subprojects. Restore output also lists
2349
+ // every project it restored as a package; the parent and its projects are
2350
+ // already in the BOM, and a second component with the same bom-ref would
2351
+ // merge the two in every consumer.
2352
+ const projectRefs = new Set([parentComponent?.["bom-ref"]]);
2353
+ for (const project of parentComponent?.components || []) {
2354
+ parentDependsOn.add(project["bom-ref"]);
2355
+ projectRefs.add(project["bom-ref"]);
2356
+ }
2357
+ pkgList = pkgList.filter((p) => !projectRefs.has(p["bom-ref"]));
2054
2358
  if (pkgList.length) {
2055
2359
  pkgList = trimComponents(pkgList);
2056
2360
  // Perform deep analysis using dosai
@@ -2059,44 +2363,92 @@ export async function createCsharpBom(path, options) {
2059
2363
  // as-is: join would append it to the scan path, which is never a valid
2060
2364
  // path on Windows and silently loses the .NET evidence elsewhere
2061
2365
  // (issue 4394). A relative value still resolves against the scan path.
2062
- const slicesFile = resolve(
2063
- options.depsSlicesFile
2064
- ? resolve(path, options.depsSlicesFile)
2065
- : join(getTmpDir(), "dosai.json"),
2066
- );
2067
- // Create the slices file if it doesn't exist
2068
- if (!safeExistsSync(slicesFile)) {
2069
- thoughtLog(
2070
- "Alright, the next step is to invoke the dosai command to identify evidence of occurrences for various components.",
2071
- );
2072
- const sliceResult = createDosaiMethodsSlice(
2073
- resolve(path),
2074
- resolve(slicesFile),
2075
- options,
2076
- );
2077
- if (!sliceResult) {
2078
- // Visible without --debug: otherwise the components silently lose
2079
- // the dependency-slice evidence while the scan still exits 0.
2080
- // --evidence (which implies --deep) runs dosai again during evinse
2081
- // and can still add occurrences, so this names only what is lost.
2082
- console.warn(
2083
- "Slicing with dosai was unsuccessful, so the .NET components get no occurrence, reachability, or services evidence from the dependency slice. Re-run with --debug for the dosai errors.",
2366
+ // Without --deps-slices-file the slice goes to a directory of this run:
2367
+ // a fixed name in the shared temp directory was reused by every later
2368
+ // scan, of any project, as if it were that project's evidence.
2369
+ const slicesDir = options.depsSlicesFile
2370
+ ? undefined
2371
+ : safeMkdtempSync(join(getTmpDir(), "cdxgen-dosai-"));
2372
+ const slicesFile = options.depsSlicesFile
2373
+ ? resolve(path, options.depsSlicesFile)
2374
+ : join(slicesDir, "dosai.json");
2375
+ try {
2376
+ // A run that will run evinse (--evidence or --include-crypto, after
2377
+ // profiles are applied) keeps the report full: evinse reuses this
2378
+ // file as its usages slice and persists it as the native report.
2379
+ // Anything else needs only the sections cdxgen reads, so dosai
2380
+ // leaves the rest out (Metadata.OmittedSections names it).
2381
+ const willRunEvinse = runsEvidenceAnalysis(options);
2382
+ // Reuse an existing slice as-is, with one exception: a trimmed file
2383
+ // (an earlier --deep run left it behind) is regenerated full before
2384
+ // an evidence run uses it, so dosai still runs once per scan.
2385
+ let reuseExisting = safeExistsSync(slicesFile);
2386
+ if (reuseExisting && willRunEvinse) {
2387
+ const omitted = dosaiReportFileOmittedSections(slicesFile);
2388
+ if (omitted.length) {
2389
+ if (DEBUG_MODE) {
2390
+ console.log(
2391
+ `The existing dosai methods slice "${slicesFile}" omits sections (${omitted.join(", ")}); regenerating the full report because this run performs evidence analysis.`,
2392
+ );
2393
+ }
2394
+ reuseExisting = false;
2395
+ }
2396
+ }
2397
+ // Create the slices file if it doesn't exist
2398
+ if (!reuseExisting) {
2399
+ thoughtLog(
2400
+ "Alright, the next step is to invoke the dosai command to identify evidence of occurrences for various components.",
2401
+ );
2402
+ const sliceResult = createDosaiMethodsSlice(
2403
+ resolve(path),
2404
+ resolve(slicesFile),
2405
+ {
2406
+ ...options,
2407
+ dosaiMethodsSections: willRunEvinse
2408
+ ? undefined
2409
+ : dosaiMethodsSectionsArgument(),
2410
+ },
2411
+ );
2412
+ // A stopped run, or one the .NET host refused to start, has
2413
+ // already said why and what to change; the debug output would show
2414
+ // no other dosai error for it (issue 4438).
2415
+ if (!sliceResult && !dosaiFailureReported(resolve(slicesFile))) {
2416
+ // Visible without debug output: otherwise the components silently
2417
+ // lose the dependency-slice evidence while the scan still exits 0.
2418
+ // --evidence (which implies --deep) runs dosai again during evinse
2419
+ // and can still add occurrences, so this names only what is lost.
2420
+ // Debug output comes from the environment, not a CLI flag (issue
2421
+ // 4438).
2422
+ console.warn(
2423
+ "Slicing with dosai was unsuccessful, so the .NET components get no occurrence, reachability, or services evidence from the dependency slice. Re-run with CDXGEN_DEBUG_MODE=debug for the dosai errors.",
2424
+ );
2425
+ }
2426
+ } else if (DEBUG_MODE) {
2427
+ console.log(
2428
+ `Reusing the existing dosai methods slice "${slicesFile}" without checking whether it honours the current --exclude patterns. Delete the file to force a fresh run.`,
2084
2429
  );
2085
2430
  }
2086
- } else if (DEBUG_MODE) {
2087
- console.log(
2088
- `Reusing the existing dosai methods slice "${slicesFile}" without checking whether it honours the current --exclude patterns. Delete the file to force a fresh run.`,
2431
+ pkgList = addEvidenceForDotnet(pkgList, slicesFile, {
2432
+ srcPath: resolve(path),
2433
+ });
2434
+ const methodsSlice = readDosaiMethodsReport(slicesFile, {
2435
+ sections: ["Metadata", "Services", "ApiEndpoints"],
2436
+ });
2437
+ // Provider-backed Services[] first so bom-refs, trust zones, and data
2438
+ // classifications survive; ApiEndpoints derivation fills in the rest.
2439
+ const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
2440
+ collectDosaiServicesFromMethods(methodsSlice, servicesMap);
2441
+ services = normalizeDosaiServiceMap(servicesMap);
2442
+ dosaiTools = dosaiToolComponents(
2443
+ slicesFile,
2444
+ methodsSlice,
2445
+ getPluginToolComponents(["dosai"])[0],
2089
2446
  );
2447
+ } finally {
2448
+ if (slicesDir) {
2449
+ safeRmSync(slicesDir, { recursive: true, force: true });
2450
+ }
2090
2451
  }
2091
- pkgList = addEvidenceForDotnet(pkgList, slicesFile);
2092
- const methodsSlice = readDosaiMethodsReport(slicesFile, {
2093
- sections: ["Metadata", "Services", "ApiEndpoints"],
2094
- });
2095
- // Provider-backed Services[] first so bom-refs, trust zones, and data
2096
- // classifications survive; ApiEndpoints derivation fills in the rest.
2097
- const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
2098
- collectDosaiServicesFromMethods(methodsSlice, servicesMap);
2099
- services = normalizeDosaiServiceMap(servicesMap);
2100
2452
  }
2101
2453
  }
2102
2454
  // Parent dependency tree. A single project file already contributed an
@@ -2131,7 +2483,7 @@ export async function createCsharpBom(path, options) {
2131
2483
  dependencies,
2132
2484
  parentComponent,
2133
2485
  services,
2134
- tools: options.deep ? getPluginToolComponents(["dosai"]) : [],
2486
+ tools: dosaiTools,
2135
2487
  });
2136
2488
  }
2137
2489
 
@@ -2150,58 +2502,239 @@ export async function createCryptoCertsBom(path, options) {
2150
2502
  },
2151
2503
  };
2152
2504
  }
2505
+ const collector = new CertificateCollector();
2153
2506
  const pkgList = [];
2154
- const certFiles = getAllFiles(
2155
- path,
2156
- `${
2157
- options.multiProject ? "**/" : ""
2158
- }*.{p12,jks,jceks,bks,keystore,key,pem,cer,gpg,pub}`,
2159
- options,
2507
+ // In rootfs/container mode the scan path sits inside the extracted tree;
2508
+ // locations are recorded relative to that tree so they merge with the
2509
+ // trustinspector rootfs producer (G18).
2510
+ const locationRoot = options.allLayersExplodedDir;
2511
+ const relativeLocation = (file) =>
2512
+ containerRelativePath(file, locationRoot) || file;
2513
+ // Bom-refs never carry host paths: outside rootfs mode, key and keystore
2514
+ // refs are relative to the scanned path.
2515
+ const refLocation = (file) =>
2516
+ containerRelativePath(file, locationRoot || path) || file;
2517
+ const certFiles = [
2518
+ ...new Set([
2519
+ ...getAllFiles(
2520
+ path,
2521
+ `${
2522
+ options.multiProject ? "**/" : ""
2523
+ }*.{p12,jks,jceks,bks,keystore,key,pem,cer,crt,der,p7b,p7c,pfx,truststore,gpg,pub}`,
2524
+ options,
2525
+ ),
2526
+ // Java truststores carry no extension; match them by exact name.
2527
+ ...getAllFiles(
2528
+ path,
2529
+ `${options.multiProject ? "**/" : ""}{cacerts,jssecacerts}`,
2530
+ options,
2531
+ ),
2532
+ ]),
2533
+ ];
2534
+ // Newlines would corrupt the plugin's stdin file list, and oversized
2535
+ // files are capped in the fallback; the plugin list gets the same caps.
2536
+ const pluginCandidateFiles = certFiles.filter(
2537
+ (file) =>
2538
+ !/[\r\n]/.test(file) &&
2539
+ statSync(file, { throwIfNoEntry: false })?.size <= MAX_CRYPTO_FILE_BYTES,
2160
2540
  );
2161
- const certHashes = await mapWithConcurrency(certFiles, (f) =>
2162
- checksumFile("sha256", f),
2541
+ const pluginMaterials = runTrustInspectorCertsMode(
2542
+ pluginCandidateFiles,
2543
+ locationRoot,
2163
2544
  );
2164
- for (let ci = 0; ci < certFiles.length; ci++) {
2165
- const f = certFiles[ci];
2166
- const fileHash = certHashes[ci];
2167
- const name = basename(f);
2168
- let evidence;
2169
- if (options.evidence) {
2170
- const identityEvidence = {
2171
- field: "name",
2172
- confidence: 1,
2173
- concludedValue: name,
2174
- methods: [
2545
+ const fallbackRecords = [];
2546
+ if (pluginMaterials) {
2547
+ collector.addTrustInspectorMaterials(pluginMaterials);
2548
+ for (const material of pluginMaterials) {
2549
+ if (typeof material?.path !== "string" || !material.path) {
2550
+ continue;
2551
+ }
2552
+ if (material.kind === "keystore") {
2553
+ pkgList.push(
2554
+ sourceKeystoreComponent(material, refLocation(material.path)),
2555
+ );
2556
+ } else if (material.kind === "private-key") {
2557
+ pkgList.push(
2558
+ sourcePrivateKeyComponent(material, refLocation(material.path)),
2559
+ );
2560
+ }
2561
+ }
2562
+ }
2563
+ // Files the plugin was not given (names with line breaks, oversized
2564
+ // files) are still read here, so no name can hide a file from the
2565
+ // inventory; without a plugin every file is. The files it was given are
2566
+ // its own, except for the keys it builds nothing from: it reports nothing
2567
+ // for a bare PEM public key, and an OpenPGP key only as public-key
2568
+ // material, so both are built here whether it saw them or not.
2569
+ const pluginFiles = new Set(pluginMaterials ? pluginCandidateFiles : []);
2570
+ for (const f of certFiles) {
2571
+ let bytes;
2572
+ try {
2573
+ const stats = statSync(f);
2574
+ if (stats.size > MAX_CRYPTO_FILE_BYTES) {
2575
+ continue;
2576
+ }
2577
+ bytes = readFileSync(f);
2578
+ } catch {
2579
+ continue;
2580
+ }
2581
+ const location = relativeLocation(f);
2582
+ const classification = classifyCryptoFile(bytes);
2583
+ if (
2584
+ pluginFiles.has(f) &&
2585
+ classification.kind !== "gpg" &&
2586
+ classification.kind !== "public-key"
2587
+ ) {
2588
+ continue;
2589
+ }
2590
+ if (classification.kind === "certificates") {
2591
+ for (const der of classification.ders) {
2592
+ const record = certRecordFromDer(der, {
2593
+ location,
2594
+ sourceType: deriveCryptoSourceType(f),
2595
+ });
2596
+ if (record) {
2597
+ record.fileExtension = extname(f).replace(/^\./, "") || undefined;
2598
+ fallbackRecords.push(record);
2599
+ }
2600
+ }
2601
+ } else if (classification.kind === "private-key") {
2602
+ pkgList.push(
2603
+ sourcePrivateKeyComponent(
2175
2604
  {
2176
- technique: "filename",
2177
- confidence: 1,
2178
- value: f,
2605
+ format: classification.format,
2606
+ sourceType: deriveCryptoSourceType(f),
2179
2607
  },
2180
- ],
2181
- };
2182
- evidence = {
2183
- identity:
2184
- options.specVersion >= 1.6 ? [identityEvidence] : identityEvidence,
2185
- occurrences: [{ location: f }],
2186
- };
2608
+ refLocation(f),
2609
+ ),
2610
+ );
2611
+ } else if (classification.kind === "gpg") {
2612
+ pkgList.push(await sourceGpgComponent(f, location, options));
2613
+ } else if (classification.kind === "public-key") {
2614
+ pkgList.push(await sourcePemPublicKeyComponent(f, location, options));
2615
+ } else if (classification.kind === "keystore") {
2616
+ pkgList.push(
2617
+ sourceKeystoreComponent(
2618
+ {
2619
+ format: classification.format,
2620
+ sourceType: deriveCryptoSourceType(f),
2621
+ },
2622
+ refLocation(f),
2623
+ ),
2624
+ );
2187
2625
  }
2188
- const apkg = {
2189
- name,
2190
- type: "cryptographic-asset",
2191
- version: fileHash,
2192
- "bom-ref": `crypto/certificate/${name}@sha256:${fileHash}`,
2193
- cryptoProperties: {
2194
- assetType: "certificate",
2195
- algorithmProperties: {
2196
- executionEnvironment: "unknown",
2197
- implementationPlatform: "unknown",
2626
+ }
2627
+ // Kubernetes TLS secrets and Android SPKI pins are cdxgen-side producers:
2628
+ // the plugin has no YAML or Android-config parser.
2629
+ const yamlFiles = getAllFiles(
2630
+ path,
2631
+ `${options.multiProject ? "**/" : ""}*.{yaml,yml}`,
2632
+ options,
2633
+ );
2634
+ for (const yamlFile of yamlFiles) {
2635
+ let yamlText;
2636
+ try {
2637
+ const stats = statSync(yamlFile);
2638
+ if (stats.size > MAX_CRYPTO_FILE_BYTES) {
2639
+ continue;
2640
+ }
2641
+ yamlText = readFileSync(yamlFile, "utf-8");
2642
+ } catch {
2643
+ continue;
2644
+ }
2645
+ // Cheap pre-filter: the Secret type string must be present before the
2646
+ // YAML parser runs.
2647
+ if (!yamlText.includes("kubernetes.io/tls")) {
2648
+ continue;
2649
+ }
2650
+ for (const record of kubernetesTlsSecretRecords(yamlText, {
2651
+ file: relativeLocation(yamlFile),
2652
+ })) {
2653
+ fallbackRecords.push(record);
2654
+ }
2655
+ }
2656
+ const androidXmlFiles = getAllFiles(path, "**/res/xml/*.xml", options);
2657
+ for (const xmlFile of androidXmlFiles) {
2658
+ let xmlText;
2659
+ try {
2660
+ const stats = statSync(xmlFile, { throwIfNoEntry: false });
2661
+ if (!stats || stats.size > MAX_CRYPTO_FILE_BYTES) {
2662
+ continue;
2663
+ }
2664
+ xmlText = readFileSync(xmlFile, "utf-8");
2665
+ } catch {
2666
+ continue;
2667
+ }
2668
+ if (!xmlText.includes("<network-security-config")) {
2669
+ continue;
2670
+ }
2671
+ for (const pin of androidNetworkSecurityConfigPins(xmlText, {
2672
+ file: relativeLocation(xmlFile),
2673
+ })) {
2674
+ collector.addPin(pin);
2675
+ }
2676
+ }
2677
+ if (fallbackRecords.length) {
2678
+ linkFallbackIssuers(fallbackRecords);
2679
+ collector.addRecords(fallbackRecords);
2680
+ }
2681
+ const {
2682
+ components: certificateComponents,
2683
+ dependencies: certificateDependencies,
2684
+ } = collector.build();
2685
+ if (pluginMaterials) {
2686
+ // The plugin reports the certificates a readable keystore holds, not the
2687
+ // keystore itself; give it the same component the fallback emits and
2688
+ // link it to what it holds.
2689
+ const keystoreMaterials = new Map();
2690
+ for (const material of pluginMaterials) {
2691
+ if (material?.kind === "certificate" && material.path) {
2692
+ keystoreMaterials.set(material.path, material);
2693
+ }
2694
+ }
2695
+ for (const file of pluginCandidateFiles) {
2696
+ const location = relativeLocation(file);
2697
+ const material = keystoreMaterials.get(location);
2698
+ if (!material) {
2699
+ continue;
2700
+ }
2701
+ let classification;
2702
+ try {
2703
+ classification = classifyCryptoFile(readFileSync(file));
2704
+ } catch {
2705
+ continue;
2706
+ }
2707
+ if (classification.kind !== "keystore") {
2708
+ continue;
2709
+ }
2710
+ const keystore = sourceKeystoreComponent(
2711
+ {
2712
+ format: classification.format,
2713
+ sourceType: material.sourceType || deriveCryptoSourceType(file),
2714
+ readable: true,
2198
2715
  },
2199
- },
2200
- ...(evidence ? { evidence } : {}),
2201
- properties: [{ name: "internal:SrcFile", value: f }],
2202
- };
2203
- pkgList.push(apkg);
2716
+ refLocation(file),
2717
+ );
2718
+ pkgList.push(keystore);
2719
+ const held = certificateComponents
2720
+ .filter(
2721
+ (component) =>
2722
+ component.cryptoProperties?.assetType === "certificate" &&
2723
+ component.evidence?.occurrences?.some(
2724
+ (occurrence) => occurrence.location === location,
2725
+ ),
2726
+ )
2727
+ .map((component) => component["bom-ref"])
2728
+ .sort();
2729
+ if (held.length) {
2730
+ certificateDependencies.push({
2731
+ ref: keystore["bom-ref"],
2732
+ dependsOn: held,
2733
+ });
2734
+ }
2735
+ }
2204
2736
  }
2737
+ pkgList.push(...certificateComponents);
2205
2738
  const sourceCryptoComponents = await collectSourceCryptoComponents(
2206
2739
  path,
2207
2740
  options,
@@ -2222,9 +2755,229 @@ export async function createCryptoCertsBom(path, options) {
2222
2755
  bomJson: {
2223
2756
  components: filterInvalidCryptoComponents(pkgList),
2224
2757
  },
2758
+ dependencies: certificateDependencies,
2759
+ };
2760
+ }
2761
+
2762
+ // Files larger than this are not classified: no real certificate bundle or
2763
+ // keystore comes close, and the cap bounds hostile inputs.
2764
+ const MAX_CRYPTO_FILE_BYTES = 16 * 1024 * 1024;
2765
+
2766
+ function deriveCryptoSourceType(filePath) {
2767
+ const normalized = String(filePath).replace(/\\/g, "/");
2768
+ if (
2769
+ normalized.includes("/etc/ssl/certs") ||
2770
+ normalized.includes("ca-certificates")
2771
+ ) {
2772
+ return "ca-store";
2773
+ }
2774
+ if (normalized.includes("certifi") || normalized.includes("botocore")) {
2775
+ return "language-bundle";
2776
+ }
2777
+ if (
2778
+ /\/etc\/(nginx|apache2|httpd|haproxy|letsencrypt|kubernetes|docker|containerd)\//.test(
2779
+ normalized,
2780
+ )
2781
+ ) {
2782
+ return "server-config";
2783
+ }
2784
+ if (
2785
+ normalized.includes("cacerts") ||
2786
+ /\.(jks|jceks|p12|pfx|truststore)$/i.test(normalized)
2787
+ ) {
2788
+ return "java-truststore";
2789
+ }
2790
+ return "generic";
2791
+ }
2792
+
2793
+ function sourceCryptoFileEvidence(location, options) {
2794
+ if (!options.evidence) {
2795
+ return undefined;
2796
+ }
2797
+ const identityEvidence = {
2798
+ field: "name",
2799
+ confidence: 1,
2800
+ concludedValue: basename(location),
2801
+ methods: [
2802
+ {
2803
+ technique: "filename",
2804
+ confidence: 1,
2805
+ value: location,
2806
+ },
2807
+ ],
2808
+ };
2809
+ return {
2810
+ identity:
2811
+ options.specVersion >= 1.6 ? [identityEvidence] : identityEvidence,
2812
+ occurrences: [{ location }],
2813
+ };
2814
+ }
2815
+
2816
+ async function sourceGpgComponent(srcFile, location, options) {
2817
+ const fileHash = await checksumFile("sha256", srcFile);
2818
+ return {
2819
+ name: basename(location),
2820
+ type: "cryptographic-asset",
2821
+ version: fileHash,
2822
+ "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(basename(location))}@sha256:${fileHash}`,
2823
+ cryptoProperties: {
2824
+ assetType: "related-crypto-material",
2825
+ relatedCryptoMaterialProperties: {
2826
+ type: "public-key",
2827
+ format: "OpenPGP",
2828
+ },
2829
+ },
2830
+ ...(sourceCryptoFileEvidence(location, options)
2831
+ ? { evidence: sourceCryptoFileEvidence(location, options) }
2832
+ : {}),
2833
+ properties: [
2834
+ { name: "internal:SrcFile", value: location },
2835
+ { name: "cdx:crypto:sourceType", value: "generic" },
2836
+ ],
2837
+ };
2838
+ }
2839
+
2840
+ async function sourcePemPublicKeyComponent(srcFile, location, options) {
2841
+ const fileHash = await checksumFile("sha256", srcFile);
2842
+ return {
2843
+ name: basename(location),
2844
+ type: "cryptographic-asset",
2845
+ version: fileHash,
2846
+ "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(basename(location))}@sha256:${fileHash}`,
2847
+ cryptoProperties: {
2848
+ assetType: "related-crypto-material",
2849
+ relatedCryptoMaterialProperties: {
2850
+ type: "public-key",
2851
+ format: "SPKI",
2852
+ },
2853
+ },
2854
+ ...(sourceCryptoFileEvidence(location, options)
2855
+ ? { evidence: sourceCryptoFileEvidence(location, options) }
2856
+ : {}),
2857
+ properties: [
2858
+ { name: "internal:SrcFile", value: location },
2859
+ { name: "cdx:crypto:sourceType", value: "generic" },
2860
+ ],
2861
+ };
2862
+ }
2863
+
2864
+ function sourcePrivateKeyComponent(material, location) {
2865
+ return {
2866
+ name: basename(location),
2867
+ type: "cryptographic-asset",
2868
+ "bom-ref": `crypto/related-crypto-material/private-key/${encodeURIComponent(location)}`,
2869
+ cryptoProperties: {
2870
+ assetType: "related-crypto-material",
2871
+ relatedCryptoMaterialProperties: {
2872
+ type: "private-key",
2873
+ ...(material?.format ? { format: material.format } : {}),
2874
+ },
2875
+ },
2876
+ evidence: { occurrences: [{ location }] },
2877
+ properties: [
2878
+ { name: "internal:SrcFile", value: location },
2879
+ ...(material?.sourceType
2880
+ ? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
2881
+ : []),
2882
+ ],
2883
+ };
2884
+ }
2885
+
2886
+ function sourceKeystoreComponent(material, location) {
2887
+ const readable = (material?.properties || []).find(
2888
+ (property) => property.name === "cdx:crypto:keystoreReadable",
2889
+ )?.value;
2890
+ return {
2891
+ name: basename(location),
2892
+ type: "cryptographic-asset",
2893
+ "bom-ref": `crypto/related-crypto-material/keystore/${encodeURIComponent(location)}`,
2894
+ cryptoProperties: {
2895
+ assetType: "related-crypto-material",
2896
+ relatedCryptoMaterialProperties: {
2897
+ type: "other",
2898
+ ...(material?.format ? { format: material.format } : {}),
2899
+ },
2900
+ },
2901
+ evidence: { occurrences: [{ location }] },
2902
+ properties: [
2903
+ { name: "internal:SrcFile", value: location },
2904
+ ...(material?.sourceType
2905
+ ? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
2906
+ : []),
2907
+ // A keystore the plugin read has no keystoreReadable property.
2908
+ ...(material?.readable
2909
+ ? []
2910
+ : [
2911
+ { name: "cdx:crypto:keystoreReadable", value: readable || "false" },
2912
+ ]),
2913
+ ],
2225
2914
  };
2226
2915
  }
2227
2916
 
2917
+ /**
2918
+ * Parse the candidate certificate files with the trustinspector `certs`
2919
+ * mode. Only a report-2 envelope is accepted: report-1 output has no
2920
+ * certificate identity to build the shared model from, and the Node
2921
+ * fallback covers that case.
2922
+ *
2923
+ * @param {string[]} files candidate files
2924
+ * @param {string} root paths are reported relative to this directory
2925
+ * @returns {object[]|undefined} materials, or undefined when the plugin is
2926
+ * absent, blocked or not report-2
2927
+ */
2928
+ function runTrustInspectorCertsMode(files, root) {
2929
+ if (!files?.length) {
2930
+ return undefined;
2931
+ }
2932
+ if (isDryRun) {
2933
+ recordActivity({
2934
+ kind: "trustinspector",
2935
+ reason:
2936
+ "Dry run mode blocks trustinspector execution and reports the requested inspection instead.",
2937
+ status: "blocked",
2938
+ target: `certs --stdin (${files.length} files)`,
2939
+ });
2940
+ return undefined;
2941
+ }
2942
+ const trustInspectorBin = resolvePluginBinary("trustinspector");
2943
+ if (!trustInspectorBin) {
2944
+ return undefined;
2945
+ }
2946
+ // `--root` is passed only in rootfs/container mode, where locations must
2947
+ // come out relative to the extracted tree; a plain source scan keeps the
2948
+ // host paths every other producer records. The value reaches a spawned
2949
+ // command, so it gets the same rejection the rootfs path applies:
2950
+ // dangerous unicode or line breaks disable the plugin call.
2951
+ const args = ["certs", "--stdin"];
2952
+ const safeRoot =
2953
+ typeof root === "string" &&
2954
+ !hasDangerousUnicode(root) &&
2955
+ !/[\r\n]/.test(root)
2956
+ ? root
2957
+ : undefined;
2958
+ if (safeRoot) {
2959
+ args.push("--root", safeRoot);
2960
+ }
2961
+ const result = safeSpawnSync(trustInspectorBin, args, {
2962
+ input: `${files.join("\n")}\n`,
2963
+ maxBuffer: 256 * 1024 * 1024,
2964
+ // A hung plugin must not hang cdxgen; the Node fallback takes over.
2965
+ timeout: 10 * 60 * 1000,
2966
+ });
2967
+ if (result?.status !== 0 || result?.error || !result?.stdout) {
2968
+ return undefined;
2969
+ }
2970
+ try {
2971
+ const report = JSON.parse(result.stdout);
2972
+ if (report?.schema !== "trustinspector/report-2") {
2973
+ return undefined;
2974
+ }
2975
+ return report.materials || [];
2976
+ } catch {
2977
+ return undefined;
2978
+ }
2979
+ }
2980
+
2228
2981
  /**
2229
2982
  * Function to create bom string for R projects using renv.
2230
2983
  *
@@ -2342,34 +3095,41 @@ export function createPerlBom(path, options) {
2342
3095
  /**
2343
3096
  * Function to create bom string for Terraform and OpenTofu projects.
2344
3097
  *
2345
- * The `.terraform.lock.hcl` file pins the providers the configuration
2346
- * resolves to, with content hashes suitable for the CycloneDX hashes array.
3098
+ * The workspace assembly turns the provider lock file, the module manifest
3099
+ * `init` writes and the configuration files into module and provider
3100
+ * components with a dependency graph and offline licenses. When metadata
3101
+ * fetching is enabled, the public registries add licenses, repository URLs
3102
+ * and registry flags to components the offline pass could not resolve.
2347
3103
  *
2348
3104
  * @param {string} path to the project
2349
3105
  * @param {Object} options Parse options from the cli
2350
- * @returns {Object} BOM object
3106
+ * @returns {Promise<Object>} BOM object
2351
3107
  */
2352
- export function createTerraformBom(path, options) {
2353
- let pkgList = [];
2354
- const lockFiles = getAllFiles(
3108
+ export async function createTerraformBom(path, options) {
3109
+ const parentComponent = createDefaultParentComponent(
2355
3110
  path,
2356
- `${options.multiProject ? "**/" : ""}.terraform.lock.hcl`,
3111
+ "generic",
2357
3112
  options,
2358
3113
  );
2359
- for (const lockFile of lockFiles) {
2360
- if (DEBUG_MODE) {
2361
- console.log(`Parsing ${lockFile}`);
2362
- }
2363
- const result = parseTerraformLockFile(lockFile);
2364
- if (result.pkgList?.length) {
2365
- pkgList = pkgList.concat(result.pkgList);
2366
- }
3114
+ const { pkgList, dependencies, parentProperties, srcFiles } =
3115
+ parseTerraformWorkspace(path, options, parentComponent["bom-ref"]);
3116
+ // No module or provider found (for example, resources only, or a stray
3117
+ // .tf in a JS repo): emit nothing, including no root applications, so
3118
+ // multi-type scans do not gain an empty Terraform parent.
3119
+ if (!pkgList.some((p) => p.type === "library")) {
3120
+ return {};
2367
3121
  }
2368
- if (pkgList.length) {
2369
- return buildBomNSData(options, pkgList, "terraform", {
2370
- src: path,
2371
- filename: lockFiles.join(", "),
2372
- });
3122
+ parentComponent.properties = [
3123
+ ...(parentComponent.properties || []),
3124
+ ...parentProperties,
3125
+ ];
3126
+ if (pkgList.length && shouldFetchPackageMetadata()) {
3127
+ await getTerraformRegistryMetadata(pkgList);
2373
3128
  }
2374
- return {};
3129
+ return buildBomNSData(options, pkgList, "terraform", {
3130
+ src: path,
3131
+ filename: srcFiles.join(", "),
3132
+ dependencies,
3133
+ parentComponent,
3134
+ });
2375
3135
  }