@cdxgen/cdxgen 13.2.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 (311) hide show
  1. package/README.md +33 -14
  2. package/bin/cdxgen.js +310 -138
  3. package/bin/evinse.js +22 -0
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +119 -15
  6. package/bin/tracebom.js +69 -5
  7. package/bin/validate.js +107 -19
  8. package/bin/verify.js +172 -63
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +85 -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/bomAssembly.js +105 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +156 -58
  17. package/lib/cli/jsBom.js +99 -53
  18. package/lib/cli/jvmBom.js +708 -147
  19. package/lib/cli/managedBom.js +415 -224
  20. package/lib/cli/nativeBom.js +264 -44
  21. package/lib/core/activity.js +12 -0
  22. package/lib/core/env.js +22 -15
  23. package/lib/core/fs.js +176 -7
  24. package/lib/core/httpClient.js +159 -24
  25. package/lib/core/metadataProperties.js +97 -0
  26. package/lib/core/pluginRun.js +184 -0
  27. package/lib/core/processTree.js +100 -0
  28. package/lib/core/projectConfig.js +31 -4
  29. package/lib/core/runState.js +39 -0
  30. package/lib/core/toml.js +72 -0
  31. package/lib/ecosystems/asarutils.js +4 -2
  32. package/lib/ecosystems/bunutils.js +274 -108
  33. package/lib/ecosystems/cargoCache.js +231 -8
  34. package/lib/ecosystems/caxa.js +434 -2
  35. package/lib/ecosystems/cmakeResolver.js +76 -21
  36. package/lib/ecosystems/cppBuildContext.js +631 -0
  37. package/lib/ecosystems/cppEvidence.js +399 -51
  38. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  39. package/lib/ecosystems/denoutils.js +93 -66
  40. package/lib/ecosystems/ecosystems.js +1983 -793
  41. package/lib/ecosystems/golem.js +28 -12
  42. package/lib/ecosystems/gradleutils.js +356 -37
  43. package/lib/ecosystems/jsEvidence.js +104 -44
  44. package/lib/ecosystems/kosi.js +31 -7
  45. package/lib/ecosystems/npmutils.js +174 -9
  46. package/lib/ecosystems/parsers-dotnet.js +616 -159
  47. package/lib/ecosystems/parsers-elm.js +46 -28
  48. package/lib/ecosystems/parsers-gleam.js +3 -4
  49. package/lib/ecosystems/parsers-go.js +371 -70
  50. package/lib/ecosystems/parsers-js.js +263 -76
  51. package/lib/ecosystems/parsers-julia.js +2 -3
  52. package/lib/ecosystems/parsers-jvm.js +204 -12
  53. package/lib/ecosystems/parsers-misc.js +929 -216
  54. package/lib/ecosystems/parsers-python.js +167 -24
  55. package/lib/ecosystems/parsers-rust.js +28 -22
  56. package/lib/ecosystems/parsers-terraform.js +1573 -19
  57. package/lib/ecosystems/pythonutils.js +102 -6
  58. package/lib/ecosystems/remote/huggingface.js +3 -0
  59. package/lib/ecosystems/rubyutils.js +1492 -182
  60. package/lib/ecosystems/rusi.js +54 -10
  61. package/lib/ecosystems/sbtutils.js +243 -187
  62. package/lib/ecosystems/scalacliutils.js +342 -0
  63. package/lib/ecosystems/scalasem.js +2618 -0
  64. package/lib/ecosystems/terraformRegistry.js +288 -0
  65. package/lib/ecosystems/utils.js +2 -0
  66. package/lib/ecosystems/vendoredCode.js +156 -0
  67. package/lib/evinser/evinser.js +1008 -191
  68. package/lib/evinser/swiftsem.js +1914 -302
  69. package/lib/helpers/agenticToolutils.js +2 -2
  70. package/lib/helpers/bomSigner.js +1101 -222
  71. package/lib/inventory/aiCollector.js +4 -1
  72. package/lib/inventory/analyzer.js +110 -127
  73. package/lib/inventory/atomUtils.js +703 -43
  74. package/lib/inventory/auditCategories.js +7 -0
  75. package/lib/inventory/buildToolRateLimit.js +144 -0
  76. package/lib/inventory/cacheCommands.js +2 -1
  77. package/lib/inventory/cdxrs.js +103 -12
  78. package/lib/inventory/cmakeBuildDirs.js +254 -0
  79. package/lib/inventory/deps.js +501 -124
  80. package/lib/inventory/depsUtils.js +183 -11
  81. package/lib/inventory/display.js +4 -5
  82. package/lib/inventory/dosai.js +798 -57
  83. package/lib/inventory/dosaiParsers.js +310 -15
  84. package/lib/inventory/dynamic.js +90 -6
  85. package/lib/inventory/evidenceUtils.js +166 -113
  86. package/lib/inventory/fetchBatch.js +690 -55
  87. package/lib/inventory/fetchDiskCache.js +597 -0
  88. package/lib/inventory/forgeEnricher.js +14 -5
  89. package/lib/inventory/jvmLocalRepos.js +917 -0
  90. package/lib/inventory/mcpConfigParser.js +2 -3
  91. package/lib/inventory/parentSelection.js +146 -0
  92. package/lib/inventory/protobom.js +8 -1
  93. package/lib/inventory/purl.js +152 -3
  94. package/lib/inventory/rustFormulationParser.js +3 -4
  95. package/lib/inventory/sbomDocument.js +2 -70
  96. package/lib/inventory/scalaCoords.js +153 -0
  97. package/lib/inventory/source.js +45 -27
  98. package/lib/inventory/spdx.js +101 -0
  99. package/lib/inventory/traceRunner.js +295 -25
  100. package/lib/inventory/tracedCrypto.js +294 -0
  101. package/lib/managers/binary.js +115 -24
  102. package/lib/managers/docker.js +467 -46
  103. package/lib/managers/oci.js +63 -3
  104. package/lib/parsers/cmakeCache.js +69 -0
  105. package/lib/parsers/cmakeLists.js +466 -0
  106. package/lib/parsers/cmakePresets.js +400 -0
  107. package/lib/parsers/compileCommands.js +557 -0
  108. package/lib/parsers/largeJson.js +454 -0
  109. package/lib/parsers/npmrc.js +19 -1
  110. package/lib/parsers/strictJson.js +322 -0
  111. package/lib/parsers/terraformConfig.js +1073 -0
  112. package/lib/parsers/terraformSource.js +652 -0
  113. package/lib/server/server.js +23 -0
  114. package/lib/stages/postgen/introspection/markers.js +1 -1
  115. package/lib/stages/postgen/postgen.js +83 -16
  116. package/lib/stages/postgen/specVersionCompat.js +32 -0
  117. package/lib/validator/bomValidator.js +6 -16
  118. package/lib/validator/index.js +64 -11
  119. package/lib/validator/reporters/console.js +3 -0
  120. package/lib/validator/reporters/json.js +2 -0
  121. package/lib/validator/reporters/sarif.js +7 -0
  122. package/package.json +23 -23
  123. package/types/lib/audit/index.d.ts.map +1 -1
  124. package/types/lib/cli/bomAssembly.d.ts +2 -1
  125. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  126. package/types/lib/cli/cliOptions.d.ts +33 -1
  127. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  128. package/types/lib/cli/index.d.ts.map +1 -1
  129. package/types/lib/cli/jsBom.d.ts.map +1 -1
  130. package/types/lib/cli/jvmBom.d.ts +24 -1
  131. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  132. package/types/lib/cli/managedBom.d.ts +7 -4
  133. package/types/lib/cli/managedBom.d.ts.map +1 -1
  134. package/types/lib/cli/nativeBom.d.ts +3 -1
  135. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  136. package/types/lib/core/activity.d.ts.map +1 -1
  137. package/types/lib/core/env.d.ts +9 -0
  138. package/types/lib/core/env.d.ts.map +1 -1
  139. package/types/lib/core/fs.d.ts +55 -0
  140. package/types/lib/core/fs.d.ts.map +1 -1
  141. package/types/lib/core/httpClient.d.ts +13 -8
  142. package/types/lib/core/httpClient.d.ts.map +1 -1
  143. package/types/lib/core/metadataProperties.d.ts +39 -0
  144. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  145. package/types/lib/core/pluginRun.d.ts +97 -0
  146. package/types/lib/core/pluginRun.d.ts.map +1 -0
  147. package/types/lib/core/processTree.d.ts +36 -0
  148. package/types/lib/core/processTree.d.ts.map +1 -0
  149. package/types/lib/core/projectConfig.d.ts +6 -2
  150. package/types/lib/core/projectConfig.d.ts.map +1 -1
  151. package/types/lib/core/runState.d.ts +29 -0
  152. package/types/lib/core/runState.d.ts.map +1 -0
  153. package/types/lib/core/toml.d.ts +20 -0
  154. package/types/lib/core/toml.d.ts.map +1 -0
  155. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  156. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  157. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  159. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  160. package/types/lib/ecosystems/caxa.d.ts +14 -0
  161. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  162. package/types/lib/ecosystems/cmakeResolver.d.ts +28 -3
  163. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  164. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  165. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  166. package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
  167. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  168. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  169. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  170. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  171. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  172. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  173. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  174. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  175. package/types/lib/ecosystems/gradleutils.d.ts +70 -4
  176. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  177. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  178. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  179. package/types/lib/ecosystems/kosi.d.ts +13 -0
  180. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  181. package/types/lib/ecosystems/npmutils.d.ts +57 -0
  182. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  183. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  184. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-go.d.ts +54 -2
  188. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  189. package/types/lib/ecosystems/parsers-js.d.ts +16 -5
  190. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  191. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  199. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  200. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  201. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rubyutils.d.ts +164 -3
  205. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/rusi.d.ts +13 -0
  207. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  208. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  209. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  210. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  211. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  212. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  213. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  214. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  215. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  216. package/types/lib/ecosystems/utils.d.ts +2 -2
  217. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  218. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  219. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  220. package/types/lib/evinser/evinser.d.ts +132 -15
  221. package/types/lib/evinser/evinser.d.ts.map +1 -1
  222. package/types/lib/evinser/swiftsem.d.ts +280 -15
  223. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  224. package/types/lib/helpers/bomSigner.d.ts +182 -13
  225. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  226. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  227. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  228. package/types/lib/inventory/atomUtils.d.ts +219 -5
  229. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  230. package/types/lib/inventory/auditCategories.d.ts +3 -0
  231. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  232. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  233. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  234. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  235. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  236. package/types/lib/inventory/cdxrs.d.ts +8 -3
  237. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  238. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  239. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  240. package/types/lib/inventory/deps.d.ts +59 -1
  241. package/types/lib/inventory/deps.d.ts.map +1 -1
  242. package/types/lib/inventory/depsUtils.d.ts +15 -0
  243. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  244. package/types/lib/inventory/display.d.ts.map +1 -1
  245. package/types/lib/inventory/dosai.d.ts +121 -9
  246. package/types/lib/inventory/dosai.d.ts.map +1 -1
  247. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  248. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  249. package/types/lib/inventory/dynamic.d.ts +10 -0
  250. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  251. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  252. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  253. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  254. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  255. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  256. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  257. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  258. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  259. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  260. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  261. package/types/lib/inventory/parentSelection.d.ts +34 -0
  262. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  263. package/types/lib/inventory/plugins.d.ts.map +1 -1
  264. package/types/lib/inventory/protobom.d.ts.map +1 -1
  265. package/types/lib/inventory/purl.d.ts +71 -2
  266. package/types/lib/inventory/purl.d.ts.map +1 -1
  267. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  268. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  269. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  270. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  271. package/types/lib/inventory/source.d.ts.map +1 -1
  272. package/types/lib/inventory/spdx.d.ts +9 -0
  273. package/types/lib/inventory/spdx.d.ts.map +1 -1
  274. package/types/lib/inventory/traceRunner.d.ts +59 -1
  275. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  276. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  277. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  278. package/types/lib/managers/binary.d.ts +30 -1
  279. package/types/lib/managers/binary.d.ts.map +1 -1
  280. package/types/lib/managers/docker.d.ts +29 -0
  281. package/types/lib/managers/docker.d.ts.map +1 -1
  282. package/types/lib/managers/oci.d.ts +17 -1
  283. package/types/lib/managers/oci.d.ts.map +1 -1
  284. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  285. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  286. package/types/lib/parsers/cmakeLists.d.ts +77 -0
  287. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  288. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  289. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  290. package/types/lib/parsers/compileCommands.d.ts +115 -0
  291. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  292. package/types/lib/parsers/largeJson.d.ts +52 -0
  293. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  294. package/types/lib/parsers/npmrc.d.ts +5 -0
  295. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  296. package/types/lib/parsers/strictJson.d.ts +46 -0
  297. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  298. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  299. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  300. package/types/lib/parsers/terraformSource.d.ts +66 -0
  301. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  302. package/types/lib/server/server.d.ts.map +1 -1
  303. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  304. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  306. package/types/lib/validator/index.d.ts +18 -1
  307. package/types/lib/validator/index.d.ts.map +1 -1
  308. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  309. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  310. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
  311. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -13,6 +13,7 @@ import {
13
13
  hasAnyProjectType,
14
14
  isFeatureEnabled,
15
15
  shouldFetchLicense,
16
+ shouldFetchPackageMetadata,
16
17
  } from "../core/env.js";
17
18
  import {
18
19
  checksumFile,
@@ -33,14 +34,18 @@ import {
33
34
  parseBdistMetadata,
34
35
  } from "../ecosystems/ecosystems.js";
35
36
  import {
37
+ agreedNugetVersions,
38
+ dotnetManifestProjectDir,
36
39
  getCentralPackageVersions,
37
- getPropertyGroupTextNodes,
40
+ indexPropsProperties,
38
41
  parseCsPkgData,
39
42
  parseCsPkgLockData,
40
43
  parseCsProjAssetsData,
41
44
  parseCsProjData,
42
45
  parseNupkg,
43
46
  parsePaketLockData,
47
+ projectPropertyCandidates,
48
+ resolveInstalledNugetVersion,
44
49
  } from "../ecosystems/parsers-dotnet.js";
45
50
  import { parseMinJs } from "../ecosystems/parsers-js.js";
46
51
  import {
@@ -52,7 +57,9 @@ import {
52
57
  } from "../ecosystems/parsers-misc.js";
53
58
  import { parseCpanSnapshot } from "../ecosystems/parsers-perl.js";
54
59
  import {
60
+ enrichFromInstalledMetadata,
55
61
  generatePixiLockFile,
62
+ installedMetadataKey,
56
63
  parseMojoProject,
57
64
  parsePiplockData,
58
65
  parsePixiLockFile,
@@ -63,7 +70,7 @@ import {
63
70
  parseSetupPyFile,
64
71
  } from "../ecosystems/parsers-python.js";
65
72
  import { parseRenvLock } from "../ecosystems/parsers-r.js";
66
- import { parseTerraformLockFile } from "../ecosystems/parsers-terraform.js";
73
+ import { parseTerraformWorkspace } from "../ecosystems/parsers-terraform.js";
67
74
  import { collectEmbeddedSboms } from "../ecosystems/pep770.js";
68
75
  import { isPyLockFile } from "../ecosystems/pylockutils.js";
69
76
  import {
@@ -73,12 +80,16 @@ import {
73
80
  getPyModules,
74
81
  } from "../ecosystems/pythonutils.js";
75
82
  import {
83
+ addRubyGemsDistributionUrls,
84
+ addRubyRuntimeComponent,
76
85
  collectGemModuleNames,
86
+ describeProjectGem,
77
87
  enrichGemsFromLocalCache,
78
88
  parseBundleConfig,
79
89
  parseGemfileLockData,
80
90
  parseGemspecData,
81
91
  } from "../ecosystems/rubyutils.js";
92
+ import { getTerraformRegistryMetadata } from "../ecosystems/terraformRegistry.js";
82
93
  import {
83
94
  collectAiInventory,
84
95
  summarizeAiInventory,
@@ -102,9 +113,10 @@ import {
102
113
  collectDosaiServiceComponents,
103
114
  collectDosaiServicesFromMethods,
104
115
  createDosaiMethodsSlice,
116
+ dosaiRunWasStopped,
105
117
  isDosaiDotnetLanguage,
106
118
  normalizeDosaiServiceMap,
107
- readDosaiJsonFile,
119
+ readDosaiMethodsReport,
108
120
  } from "../inventory/dosai.js";
109
121
  import { addEvidenceForDotnet } from "../inventory/evidenceUtils.js";
110
122
  import { nugetPurl, pypiBomRef } from "../inventory/purl.js";
@@ -401,6 +413,17 @@ export async function createPythonBom(path, options) {
401
413
  !pyLockFiles?.length;
402
414
  const poetryMode = poetryFiles?.length;
403
415
 
416
+ // The installed dist-info METADATA files describe the distributions this
417
+ // machine holds. They are read once, before any lockfile is parsed, so the
418
+ // lockfile entries they match are enriched before PyPI is asked and the
419
+ // registry is left only what the installations do not answer.
420
+ const installedLists = (metadataFiles || []).map((mf) =>
421
+ parseBdistMetadata(mf),
422
+ );
423
+ const installedMetadata = installedLists
424
+ .map((dlist) => dlist?.[0])
425
+ .filter((md) => md?.name);
426
+
404
427
  // TODO: Support for nested directories
405
428
  const setupPy = join(path, "setup.py");
406
429
  const setupPyMode = safeExistsSync(setupPy);
@@ -413,7 +436,9 @@ export async function createPythonBom(path, options) {
413
436
  if (DEBUG_MODE) {
414
437
  console.log(`Parsing ${f}`);
415
438
  }
416
- let retMap = await parsePyLockData(lockData, f);
439
+ let retMap = await parsePyLockData(lockData, f, undefined, {
440
+ installedMetadata,
441
+ });
417
442
  if (retMap?.workspaceWarningShown) {
418
443
  deferFailOnError(options, {
419
444
  ecosystem: "python",
@@ -505,7 +530,11 @@ export async function createPythonBom(path, options) {
505
530
  `exported-${basename(basePath)}-reqs.txt`,
506
531
  );
507
532
  safeWriteSync(tmpReqFile, exportedReqs);
508
- const dlist = await parseReqFile(tmpReqFile, false);
533
+ const dlist = await parseReqFile(
534
+ tmpReqFile,
535
+ false,
536
+ installedMetadata,
537
+ );
509
538
  if (dlist?.length) {
510
539
  pkgList = pkgList.concat(dlist);
511
540
  }
@@ -515,15 +544,6 @@ export async function createPythonBom(path, options) {
515
544
  }
516
545
  options.parentComponent = parentComponent;
517
546
  } // poetryMode
518
- if (metadataFiles?.length) {
519
- // dist-info directories
520
- for (const mf of metadataFiles) {
521
- const dlist = parseBdistMetadata(mf);
522
- if (dlist?.length) {
523
- pkgList = pkgList.concat(dlist);
524
- }
525
- }
526
- }
527
547
  // .whl files. Zip file containing dist-info directory
528
548
  if (whlFiles?.length) {
529
549
  for (const wf of whlFiles) {
@@ -573,14 +593,23 @@ export async function createPythonBom(path, options) {
573
593
  }
574
594
  if (requirementsMode || pipenvMode) {
575
595
  if (pipenvMode) {
576
- // TODO: Support for nested directories
577
- safeSpawnSync("pipenv", ["install"], { cwd: path, shell: isWin });
596
+ // `pipenv install` builds and installs the project's own dependencies
597
+ // (including `-e .` self-references that execute setup.py), so unlike
598
+ // the lockfile parsing below it must honor --no-install-deps and the
599
+ // pre-build lifecycle.
600
+ if (options.installDeps !== false) {
601
+ safeSpawnSync("pipenv", ["install"], { cwd: path, shell: isWin });
602
+ } else {
603
+ console.log(
604
+ "Skipping 'pipenv install' since dependency installation is disabled.",
605
+ );
606
+ }
578
607
  const piplockFile = join(path, "Pipfile.lock");
579
608
  if (safeExistsSync(piplockFile)) {
580
609
  let dlist;
581
610
  try {
582
611
  const lockData = JSON.parse(readFileSync(piplockFile));
583
- dlist = await parsePiplockData(lockData);
612
+ dlist = await parsePiplockData(lockData, installedMetadata);
584
613
  } catch (err) {
585
614
  recordDegradation("python.lockfile-unparseable.pipenv", {
586
615
  ecosystem: "python",
@@ -605,7 +634,7 @@ export async function createPythonBom(path, options) {
605
634
  } else if (reqDirFiles?.length) {
606
635
  for (const j in reqDirFiles) {
607
636
  const f = reqDirFiles[j];
608
- const dlist = await parseReqFile(f, false);
637
+ const dlist = await parseReqFile(f, false, installedMetadata);
609
638
  if (dlist?.length) {
610
639
  pkgList = pkgList.concat(dlist);
611
640
  }
@@ -613,13 +642,30 @@ export async function createPythonBom(path, options) {
613
642
  metadataFilename = reqDirFiles.join(", ");
614
643
  } else if (reqFiles?.length) {
615
644
  for (const f of reqFiles) {
616
- const dlist = await parseReqFile(f, false);
645
+ const dlist = await parseReqFile(f, false, installedMetadata);
617
646
  if (dlist?.length) {
618
647
  pkgList = pkgList.concat(dlist);
619
648
  }
620
649
  }
621
650
  }
622
651
  }
652
+ if (metadataFiles?.length) {
653
+ // dist-info directories, after every manifest block has added its
654
+ // entries. An installation whose package a lockfile, Pipfile.lock or
655
+ // requirements entry already lists enriched that entry, so it is not
656
+ // also emitted as a component of its own; an installation nothing else
657
+ // covers still is one. This pass is the emission decision only: each
658
+ // parser enriched its own entries before it asked PyPI.
659
+ const enriched = enrichFromInstalledMetadata(pkgList, installedMetadata);
660
+ for (const dlist of installedLists) {
661
+ if (dlist?.length) {
662
+ if (enriched.has(installedMetadataKey(dlist[0]))) {
663
+ continue;
664
+ }
665
+ pkgList = pkgList.concat(dlist);
666
+ }
667
+ }
668
+ }
623
669
 
624
670
  const parentDependsOn = new Set();
625
671
 
@@ -730,79 +776,81 @@ export async function createPythonBom(path, options) {
730
776
  );
731
777
  }
732
778
  }
733
- // ATOM parsedeps block
734
- // Atom parsedeps slices can be used to identify packages that are not declared in manifests
735
- // Since it is a slow operation, we only use it as a fallback or in deep mode
736
- // This change was made in 10.9.2 release onwards
737
- if (options.deep || !pkgList.length) {
738
- if (!pkgList.length) {
739
- thoughtLog(
740
- "I couldn't find any components yet. Let's try static analysis with atom parsedeps command.",
741
- );
742
- }
743
- const retMap = await getPyModules(path, pkgList, options);
744
- // We need to patch the existing package list to add ImportedModules for evinse to work
745
- if (retMap.modList?.length) {
746
- const iSymbolsMap = {};
747
- retMap.modList.forEach((v) => {
748
- iSymbolsMap[v.name] = v.importedSymbols;
749
- iSymbolsMap[v.name.replace(/_/g, "-")] = v.importedSymbols;
750
- });
751
- for (const apkg of pkgList) {
752
- if (iSymbolsMap[apkg.name]) {
753
- apkg.scope = "required";
754
- apkg.properties = apkg.properties || [];
755
- apkg.properties.push({
756
- name: "internal:ImportedModules",
757
- value: iSymbolsMap[apkg.name],
758
- });
759
- }
779
+ }
780
+ // ATOM parsedeps block
781
+ // Atom parsedeps slices can be used to identify packages that are not declared in manifests
782
+ // Since it is a slow operation, we only use it as a fallback or in deep mode
783
+ // This change was made in 10.9.2 release onwards. It reads the sources and
784
+ // installs nothing, so it runs whether or not dependencies are installed.
785
+ if (options.deep || !pkgList.length) {
786
+ if (!pkgList.length) {
787
+ thoughtLog(
788
+ "I couldn't find any components yet. Let's try static analysis with atom parsedeps command.",
789
+ );
790
+ }
791
+ const retMap = await getPyModules(path, pkgList, options);
792
+ // We need to patch the existing package list to add ImportedModules for evinse to work
793
+ if (retMap.modList?.length) {
794
+ const iSymbolsMap = {};
795
+ retMap.modList.forEach((v) => {
796
+ iSymbolsMap[v.name] = v.importedSymbols;
797
+ iSymbolsMap[v.name.replace(/_/g, "-")] = v.importedSymbols;
798
+ });
799
+ for (const apkg of pkgList) {
800
+ if (iSymbolsMap[apkg.name]) {
801
+ apkg.scope = "required";
802
+ apkg.properties = apkg.properties || [];
803
+ apkg.properties.push({
804
+ name: "internal:ImportedModules",
805
+ value: iSymbolsMap[apkg.name],
806
+ });
760
807
  }
761
808
  }
762
- if (retMap.pkgList?.length) {
763
- pkgList = pkgList.concat(retMap.pkgList);
764
- for (const p of retMap.pkgList) {
765
- if (
766
- !p.version ||
767
- (parentComponent &&
768
- p.name === parentComponent.name &&
769
- (p.version === parentComponent.version ||
770
- p.version === "latest"))
771
- ) {
772
- continue;
773
- }
774
- parentDependsOn.add(pypiBomRef(p.name, p.version));
809
+ }
810
+ if (retMap.pkgList?.length) {
811
+ pkgList = pkgList.concat(retMap.pkgList);
812
+ for (const p of retMap.pkgList) {
813
+ if (
814
+ !p.version ||
815
+ (parentComponent &&
816
+ p.name === parentComponent.name &&
817
+ (p.version === parentComponent.version || p.version === "latest"))
818
+ ) {
819
+ continue;
775
820
  }
776
- }
777
- if (retMap.dependenciesList) {
778
- dependencies = mergeDependencies(
779
- dependencies,
780
- retMap.dependenciesList,
781
- parentComponent,
782
- );
783
- }
784
- if (retMap.allImports) {
785
- allImports = { ...allImports, ...retMap.allImports };
821
+ parentDependsOn.add(pypiBomRef(p.name, p.version));
786
822
  }
787
823
  }
788
- // ATOM parsedeps block
789
- let parentPresent = false;
790
- for (const d of dependencies) {
791
- if (d.ref === parentComponent["bom-ref"]) {
792
- parentPresent = true;
793
- break;
794
- }
824
+ if (retMap.dependenciesList) {
825
+ dependencies = mergeDependencies(
826
+ dependencies,
827
+ retMap.dependenciesList,
828
+ parentComponent,
829
+ );
795
830
  }
796
- if (!parentPresent) {
797
- const pdependencies = {
798
- ref: parentComponent["bom-ref"],
799
- dependsOn: Array.from(parentDependsOn).filter(
800
- (r) => parentComponent && r !== parentComponent["bom-ref"],
801
- ),
802
- };
803
- dependencies.splice(0, 0, pdependencies);
831
+ if (retMap.allImports) {
832
+ allImports = { ...allImports, ...retMap.allImports };
833
+ }
834
+ }
835
+ // ATOM parsedeps block
836
+ let parentPresent = false;
837
+ for (const d of dependencies) {
838
+ if (d.ref === parentComponent["bom-ref"]) {
839
+ parentPresent = true;
840
+ break;
804
841
  }
805
842
  }
843
+ // Without an install, the parent's dependencies are known only when the
844
+ // import scan found some: an empty list would claim it has none.
845
+ if (!parentPresent && (options.installDeps || parentDependsOn.size)) {
846
+ const pdependencies = {
847
+ ref: parentComponent["bom-ref"],
848
+ dependsOn: Array.from(parentDependsOn).filter(
849
+ (r) => parentComponent && r !== parentComponent["bom-ref"],
850
+ ),
851
+ };
852
+ dependencies.splice(0, 0, pdependencies);
853
+ }
806
854
  }
807
855
  // Final fallback is to manually parse setup.py if we still
808
856
  // have an empty list
@@ -1352,6 +1400,7 @@ export async function createRubyBom(path, options) {
1352
1400
  let pkgList = [];
1353
1401
  let dependencies = [];
1354
1402
  let rootList = [];
1403
+ const projectGemRefs = [];
1355
1404
  const parentComponent = createDefaultParentComponent(path, "gem", options);
1356
1405
  const gemFileMode = gemFiles.length;
1357
1406
  const gemLockMode = gemLockFiles.length;
@@ -1390,7 +1439,7 @@ export async function createRubyBom(path, options) {
1390
1439
  console.log(`Parsing ${f}`);
1391
1440
  }
1392
1441
  const gemLockData = readFileSync(f, { encoding: "utf-8" });
1393
- const retMap = await parseGemfileLockData(gemLockData, f);
1442
+ const retMap = await parseGemfileLockData(gemLockData, f, { gemHome });
1394
1443
  if (retMap.pkgList?.length) {
1395
1444
  pkgList = pkgList.concat(retMap.pkgList);
1396
1445
  pkgList = trimComponents(pkgList);
@@ -1405,10 +1454,18 @@ export async function createRubyBom(path, options) {
1405
1454
  if (retMap.rootList?.length) {
1406
1455
  rootList = rootList.concat(retMap.rootList);
1407
1456
  }
1457
+ // `PATH remote: .` is relative to the lockfile, so only the lockfiles
1458
+ // of the scanned directory can serve the gem the root gemspec builds
1459
+ if (
1460
+ retMap.projectGemRefs?.length &&
1461
+ dirname(resolve(f)) === resolve(path)
1462
+ ) {
1463
+ projectGemRefs.push(...retMap.projectGemRefs);
1464
+ }
1408
1465
  }
1409
1466
  }
1410
1467
  // Parsing .gemspec files would help us get more metadata such as description, authors, licenses etc
1411
- let rootGemspecComponent;
1468
+ let projectGem;
1412
1469
  if (gemspecFiles.length) {
1413
1470
  if (!gemLockFiles.length && !hasAnyProjectType(["oci"], options, false)) {
1414
1471
  console.log(
@@ -1420,37 +1477,50 @@ export async function createRubyBom(path, options) {
1420
1477
  detail: "only gemspec files were available to parse",
1421
1478
  });
1422
1479
  }
1480
+ // Bundler evaluated the root gemspec to lock it under `PATH remote: .`,
1481
+ // so the locked release stands in for a version computed at runtime. It
1482
+ // is resolved before the merge, so that the gemspec and lockfile
1483
+ // sightings of the gem reconcile into one component.
1484
+ const lockedVersions = Object.create(null);
1485
+ for (const p of pkgList) {
1486
+ if (p.version && projectGemRefs.includes(p["bom-ref"])) {
1487
+ lockedVersions[p.name] = p.version;
1488
+ }
1489
+ }
1423
1490
  for (const f of gemspecFiles) {
1424
1491
  const gemspecData = readFileSync(f, { encoding: "utf-8" });
1425
- const gpkgList = await parseGemspecData(gemspecData, f);
1426
- if (gpkgList.length) {
1427
- pkgList = pkgList.concat(gpkgList);
1428
- pkgList = trimComponents(pkgList);
1429
- if (
1430
- !rootGemspecComponent &&
1431
- dirname(resolve(f)) === resolve(path) &&
1432
- gpkgList[0]?.name
1433
- ) {
1434
- rootGemspecComponent = gpkgList[0];
1435
- }
1492
+ const isRootGemspec = dirname(resolve(f)) === resolve(path);
1493
+ const gpkgList = await parseGemspecData(
1494
+ gemspecData,
1495
+ f,
1496
+ isRootGemspec ? { lockedVersions, projectGemspec: true } : {},
1497
+ );
1498
+ if (!gpkgList.length) {
1499
+ continue;
1436
1500
  }
1501
+ // The gemspec in the scanned directory is the gem the project builds
1502
+ if (!projectGem && isRootGemspec && gpkgList[0]?.name) {
1503
+ projectGem = gpkgList[0];
1504
+ }
1505
+ pkgList = trimComponents(pkgList.concat(gpkgList));
1437
1506
  }
1438
- if (
1439
- rootGemspecComponent &&
1440
- !("project-name" in options) &&
1441
- options.projectName === undefined
1442
- ) {
1443
- parentComponent.name = rootGemspecComponent.name;
1444
- parentComponent.version = rootGemspecComponent.version || "latest";
1445
- const parentPurl = build({
1446
- type: "gem",
1447
- namespace: parentComponent.group || null,
1448
- name: parentComponent.name,
1449
- version: parentComponent.version || null,
1450
- });
1451
- parentComponent["bom-ref"] = decodeURIComponent(parentPurl);
1452
- parentComponent["purl"] = parentPurl;
1453
- }
1507
+ }
1508
+ // Describe that gem in metadata.component, unless `--project-name` names a
1509
+ // different project or an image is being scanned
1510
+ if (
1511
+ projectGem &&
1512
+ !hasAnyProjectType(["oci"], options, false) &&
1513
+ (!("project-name" in options) || options.projectName === projectGem.name)
1514
+ ) {
1515
+ ({ pkgList, dependencies, rootList } = describeProjectGem(
1516
+ parentComponent,
1517
+ projectGem,
1518
+ pkgList,
1519
+ dependencies,
1520
+ rootList,
1521
+ projectGemRefs,
1522
+ options,
1523
+ ));
1454
1524
  }
1455
1525
  if (rootList.length) {
1456
1526
  dependencies = mergeDependencies(
@@ -1547,6 +1617,21 @@ export async function createRubyBom(path, options) {
1547
1617
  // lookup, gives us the sha256 of the exact native build, and unlike the
1548
1618
  // registry lookups it also works in dry-run mode.
1549
1619
  pkgList = await enrichGemsFromLocalCache(pkgList, { gemHome });
1620
+ // The download URL of a gem from the public registry needs no network call
1621
+ pkgList = addRubyGemsDistributionUrls(pkgList);
1622
+ // Gems declare the Ruby they run on; surface it as the runtime they depend
1623
+ // on. The component says the runtime is external, which only CycloneDX 1.7
1624
+ // can express, and a container image ships its own Ruby.
1625
+ if (
1626
+ options.specVersion >= 1.7 &&
1627
+ !hasAnyProjectType(["oci"], options, false)
1628
+ ) {
1629
+ ({ pkgList, dependencies } = addRubyRuntimeComponent(
1630
+ pkgList,
1631
+ parentComponent,
1632
+ dependencies,
1633
+ ));
1634
+ }
1550
1635
  return buildBomNSData(options, pkgList, "gem", {
1551
1636
  src: path,
1552
1637
  dependencies,
@@ -1772,49 +1857,88 @@ export async function createCsharpBom(path, options) {
1772
1857
  const parentDependsOn = new Set();
1773
1858
  if (nupkgFiles.length && projAssetsFiles.length === 0) {
1774
1859
  manifestFiles = manifestFiles.concat(nupkgFiles);
1775
- // When parsing nupkg files, only version ranges will be specified under dependencies
1776
- // To resolve the version, we need to track the mapping between name and resolved versions here
1777
- let dependenciesMap = {};
1778
- const pkgNameVersions = {};
1860
+ // A nuspec states the range each dependency accepts, not the version
1861
+ // installed. The installed packages are the components, and a dependency
1862
+ // edge points at the installed version its range resolves to; a minimum
1863
+ // version nobody installed is not a component.
1864
+ const installedVersions = {};
1865
+ const installedNames = {};
1866
+ let dependencyRanges = {};
1779
1867
  for (const nf of nupkgFiles) {
1780
1868
  if (DEBUG_MODE) {
1781
1869
  console.log(`Parsing ${nf}`);
1782
1870
  }
1783
1871
  const retMap = await parseNupkg(nf);
1784
- if (retMap?.pkgList?.length) {
1785
- pkgList = pkgList.concat(retMap.pkgList);
1786
- for (const d of retMap.pkgList) {
1787
- parentDependsOn.add(d["bom-ref"]);
1788
- pkgNameVersions[d.name] = d.version;
1872
+ const installed = retMap?.pkgList?.[0];
1873
+ if (installed) {
1874
+ pkgList.push(installed);
1875
+ parentDependsOn.add(installed["bom-ref"]);
1876
+ const key = installed.name.toLowerCase();
1877
+ installedNames[key] ??= installed.name;
1878
+ installedVersions[key] ??= [];
1879
+ if (installed.version) {
1880
+ installedVersions[key].push(installed.version);
1789
1881
  }
1790
1882
  }
1791
- if (retMap?.dependenciesMap) {
1792
- dependenciesMap = { ...dependenciesMap, ...retMap.dependenciesMap };
1883
+ if (retMap?.dependencyRanges) {
1884
+ dependencyRanges = { ...dependencyRanges, ...retMap.dependencyRanges };
1793
1885
  }
1794
1886
  } // end for
1795
- for (const k of Object.keys(dependenciesMap)) {
1796
- const dependsOn = dependenciesMap[k].map((p) => {
1797
- const ver = pkgNameVersions[p] || "latest";
1798
- const purl = nugetPurl(p, ver);
1799
- return purl ? decodeURIComponent(purl) : `library:${p}:${ver}`;
1800
- });
1801
- dependencies.push({ ref: k, dependsOn: [...new Set(dependsOn)].sort() });
1887
+ for (const [ref, ranges] of Object.entries(dependencyRanges)) {
1888
+ const dependsOn = new Set();
1889
+ for (const [name, range] of Object.entries(ranges)) {
1890
+ const key = name.toLowerCase();
1891
+ const version = resolveInstalledNugetVersion(
1892
+ range,
1893
+ installedVersions[key],
1894
+ );
1895
+ const purl = version ? nugetPurl(installedNames[key], version) : null;
1896
+ if (purl) {
1897
+ dependsOn.add(decodeURIComponent(purl));
1898
+ }
1899
+ }
1900
+ dependencies.push({ ref, dependsOn: [...dependsOn].sort() });
1802
1901
  }
1803
1902
  }
1903
+ // Versions each project restored, by project directory and lowercased
1904
+ // package id. Two projects of one tree routinely restore different versions
1905
+ // of a package, so a version missing from one project's manifest is taken
1906
+ // from that project's restore output, never from another project's.
1907
+ // The restore output also spells each package id the way its nuspec does.
1908
+ const projectVersions = {};
1909
+ const projectNames = {};
1910
+ const recordProjectVersions = (projectDir, pkgs) => {
1911
+ projectVersions[projectDir] ??= {};
1912
+ projectNames[projectDir] ??= {};
1913
+ for (const p of pkgs || []) {
1914
+ if (
1915
+ p?.name &&
1916
+ p.version &&
1917
+ !["application", "project"].includes(p.type)
1918
+ ) {
1919
+ projectVersions[projectDir][p.name.toLowerCase()] ??= p.version;
1920
+ projectNames[projectDir][p.name.toLowerCase()] ??= p.name;
1921
+ }
1922
+ }
1923
+ };
1924
+ // Direct dependencies of each project directory from its lock file or
1925
+ // packages.config, attached to that project once the project files are read.
1926
+ const rootsByProjectDir = {};
1927
+ const recordRoots = (projectDir, pkgs) => {
1928
+ rootsByProjectDir[projectDir] ??= new Set();
1929
+ for (const p of pkgs || []) {
1930
+ if (p?.["bom-ref"]) {
1931
+ rootsByProjectDir[projectDir].add(p["bom-ref"]);
1932
+ }
1933
+ }
1934
+ };
1804
1935
  // In a multi-project scan, different projects may use different manifest
1805
1936
  // formats: modern SDK-style (project.assets.json / packages.lock.json) or
1806
1937
  // legacy (packages.config). Apply the assets > lock > config precedence per
1807
1938
  // project directory instead of across the whole scan, so dependencies from
1808
1939
  // one project's manifest are not dropped just because another format exists
1809
1940
  // elsewhere in the scan.
1810
- const assetsProjectDirs = new Set(
1811
- projAssetsFiles.map((af) => {
1812
- // project.assets.json is generated under <project>/obj/
1813
- const d = dirname(af);
1814
- return basename(d) === "obj" ? dirname(d) : d;
1815
- }),
1816
- );
1817
- const lockProjectDirs = new Set(pkgLockFiles.map((af) => dirname(af)));
1941
+ const assetsProjectDirs = new Set();
1818
1942
  // project.assets.json parsing
1819
1943
  if (projAssetsFiles.length) {
1820
1944
  manifestFiles = manifestFiles.concat(projAssetsFiles);
@@ -1826,6 +1950,11 @@ export async function createCsharpBom(path, options) {
1826
1950
  const results = parseCsProjAssetsData(pkgData, af);
1827
1951
  const deps = results["dependenciesList"];
1828
1952
  const dlist = results["pkgList"];
1953
+ // Restore output names its project; an artifacts layout keeps it outside
1954
+ // the project directory.
1955
+ const projectDir = dotnetManifestProjectDir(af, results.projectPath);
1956
+ assetsProjectDirs.add(projectDir);
1957
+ recordProjectVersions(projectDir, dlist);
1829
1958
  if (dlist?.length) {
1830
1959
  pkgList = pkgList.concat(dlist);
1831
1960
  }
@@ -1858,9 +1987,12 @@ export async function createCsharpBom(path, options) {
1858
1987
  });
1859
1988
  }
1860
1989
  }
1990
+ const lockProjectDirs = new Set(
1991
+ pkgLockFiles.map((af) => dotnetManifestProjectDir(af)),
1992
+ );
1861
1993
  // packages.lock.json parsing for any project without a project.assets.json
1862
1994
  const pkgLockToParse = pkgLockFiles.filter(
1863
- (af) => !assetsProjectDirs.has(dirname(af)),
1995
+ (af) => !assetsProjectDirs.has(dotnetManifestProjectDir(af)),
1864
1996
  );
1865
1997
  if (pkgLockToParse.length) {
1866
1998
  manifestFiles = manifestFiles.concat(pkgLockToParse);
@@ -1874,37 +2006,29 @@ export async function createCsharpBom(path, options) {
1874
2006
  const deps = results["dependenciesList"];
1875
2007
  const dlist = results["pkgList"];
1876
2008
  const rootList = results["rootList"];
2009
+ const projectDir = dotnetManifestProjectDir(af);
2010
+ recordProjectVersions(projectDir, dlist);
1877
2011
  if (dlist?.length) {
1878
2012
  pkgList = pkgList.concat(dlist);
1879
2013
  }
1880
2014
  if (deps?.length) {
1881
2015
  dependencies = mergeDependencies(dependencies, deps, parentComponent);
1882
2016
  }
1883
- // Keep track of the direct dependencies so that we can construct one complete
1884
- // list after processing all lock files
1885
- if (rootList?.length) {
1886
- for (const p of rootList) {
1887
- parentDependsOn.add(p["bom-ref"]);
1888
- }
1889
- }
2017
+ recordRoots(projectDir, rootList);
1890
2018
  }
1891
2019
  }
1892
2020
  // packages.config parsing for any project without a modern manifest
1893
2021
  // (project.assets.json / packages.lock.json) covering the same directory
1894
2022
  const pkgConfigToParse = pkgConfigFiles.filter((f) => {
1895
- const projDir = dirname(f);
2023
+ const projDir = dotnetManifestProjectDir(f);
1896
2024
  return !assetsProjectDirs.has(projDir) && !lockProjectDirs.has(projDir);
1897
2025
  });
1898
2026
  if (pkgConfigToParse.length) {
1899
2027
  manifestFiles = manifestFiles.concat(pkgConfigToParse);
1900
- // Versions already resolved from project.assets.json / packages.lock.json
1901
- // can backfill templated or missing versions in packages.config
1902
- const resolvedVersions = {};
1903
- for (const p of pkgList) {
1904
- if (p.name && p.version) {
1905
- resolvedVersions[p.name] = p.version;
1906
- }
1907
- }
2028
+ // A templated or missing version is backfilled only from a version every
2029
+ // other manifest of the scan agrees on: these projects restored nothing of
2030
+ // their own, and another project's version may not be theirs.
2031
+ const resolvedVersions = agreedNugetVersions(pkgList);
1908
2032
  // packages.config parsing
1909
2033
  for (const f of pkgConfigToParse) {
1910
2034
  if (DEBUG_MODE) {
@@ -1912,11 +2036,11 @@ export async function createCsharpBom(path, options) {
1912
2036
  }
1913
2037
  pkgData = readFileSync(f, { encoding: "utf-8" });
1914
2038
  const dlist = parseCsPkgData(pkgData, f, resolvedVersions);
2039
+ const projectDir = dotnetManifestProjectDir(f);
2040
+ recordProjectVersions(projectDir, dlist);
1915
2041
  if (dlist?.length) {
1916
2042
  pkgList = pkgList.concat(dlist);
1917
- for (const d of dlist) {
1918
- parentDependsOn.add(d["bom-ref"]);
1919
- }
2043
+ recordRoots(projectDir, dlist);
1920
2044
  }
1921
2045
  }
1922
2046
  }
@@ -1939,27 +2063,26 @@ export async function createCsharpBom(path, options) {
1939
2063
  }
1940
2064
  }
1941
2065
  }
1942
- const pkgNameVersions = {};
2066
+ const projectRefByDir = {};
1943
2067
  let services = [];
1944
2068
  if (csProjFiles.length) {
1945
2069
  manifestFiles = manifestFiles.concat(csProjFiles);
1946
- // Parsing csproj is quite error-prone. Some project files may not have versions specified
1947
- // To work around this, we make use of the version from the existing list
1948
- for (const p of pkgList) {
1949
- if (p.version) {
1950
- pkgNameVersions[p.name] = p.version;
1951
- }
1952
- }
2070
+ // Parsing csproj is quite error-prone. Some project files may not have
2071
+ // versions specified. Such a version comes from what this project restored,
2072
+ // then from central package management, and only then from a version the
2073
+ // whole scan agrees on.
2074
+ const agreedVersions = agreedNugetVersions(pkgList);
1953
2075
  let msbuildInstalled = false;
1954
- let pkgVersionLabelCandidates = {};
1955
- const result = safeSpawnSync(DOTNET_CMD, ["msbuild --version"], {
2076
+ const result = safeSpawnSync(DOTNET_CMD, ["msbuild", "--version"], {
1956
2077
  shell: isWin,
1957
2078
  });
1958
- if (result.status === 0 && !result.error && result.stdout.trim()) {
2079
+ if (result.status === 0 && !result.error && result.stdout?.trim()) {
1959
2080
  msbuildInstalled = true;
1960
- } else {
1961
- pkgVersionLabelCandidates = getPropertyGroupTextNodes(propsFiles);
1962
2081
  }
2082
+ // `$(Name)` version labels resolve from the props files that can apply to
2083
+ // each project (beside and above it), read once for the whole scan.
2084
+ // msbuild evaluates only what they leave unresolved.
2085
+ const propsIndex = indexPropsProperties(propsFiles);
1963
2086
  // Directory.Packages.props can sit above the directory being scanned, so it is
1964
2087
  // resolved per project file by walking up rather than from the props glob below.
1965
2088
  // The cache keeps a repository with many projects from re-parsing the same file.
@@ -1970,15 +2093,19 @@ export async function createCsharpBom(path, options) {
1970
2093
  console.log(`Parsing ${f}`);
1971
2094
  }
1972
2095
  const csProjData = readFileSync(f, { encoding: "utf-8" });
2096
+ const projectDir = dotnetManifestProjectDir(f);
1973
2097
  const retMap = parseCsProjData(
1974
2098
  csProjData,
1975
2099
  f,
1976
- pkgNameVersions,
2100
+ projectVersions[projectDir] || {},
1977
2101
  msbuildInstalled,
1978
- pkgVersionLabelCandidates,
2102
+ projectPropertyCandidates(f, propsIndex),
1979
2103
  getCentralPackageVersions(f, centralVersionsCache),
2104
+ agreedVersions,
2105
+ projectNames[projectDir] || {},
1980
2106
  );
1981
2107
  if (retMap?.parentComponent?.purl) {
2108
+ projectRefByDir[projectDir] ??= retMap.parentComponent["bom-ref"];
1982
2109
  // If there are multiple project files, track the parent components using nested components
1983
2110
  if (csProjFiles.length > 1) {
1984
2111
  if (!parentComponent.components) {
@@ -2002,46 +2129,103 @@ export async function createCsharpBom(path, options) {
2002
2129
  }
2003
2130
  }
2004
2131
  }
2132
+ // A lock file's or packages.config's direct dependencies belong to the
2133
+ // project in that directory; without a project file there, to the parent.
2134
+ for (const [projectDir, roots] of Object.entries(rootsByProjectDir)) {
2135
+ if (!roots.size) {
2136
+ continue;
2137
+ }
2138
+ const projectRef = projectRefByDir[projectDir];
2139
+ if (projectRef && projectRef !== parentComponent?.["bom-ref"]) {
2140
+ dependencies = mergeDependencies(
2141
+ dependencies,
2142
+ [{ ref: projectRef, dependsOn: [...roots].sort() }],
2143
+ parentComponent,
2144
+ );
2145
+ } else {
2146
+ for (const ref of roots) {
2147
+ parentDependsOn.add(ref);
2148
+ }
2149
+ }
2150
+ }
2005
2151
  if (pkgList.length) {
2006
2152
  pkgList = trimComponents(pkgList);
2007
2153
  // Perform deep analysis using dosai
2008
2154
  if (options.deep) {
2009
- const slicesFile = resolve(
2010
- options.depsSlicesFile
2011
- ? join(path, options.depsSlicesFile)
2012
- : join(getTmpDir(), "dosai.json"),
2013
- );
2014
- // Create the slices file if it doesn't exist
2015
- if (!safeExistsSync(slicesFile)) {
2016
- thoughtLog(
2017
- "Alright, the next step is to invoke the dosai command to identify evidence of occurrences for various components.",
2018
- );
2019
- const sliceResult = createDosaiMethodsSlice(
2020
- resolve(path),
2021
- resolve(slicesFile),
2022
- options,
2023
- );
2024
- if (!sliceResult && DEBUG_MODE) {
2155
+ // resolve (not join) so a fully-qualified --deps-slices-file is used
2156
+ // as-is: join would append it to the scan path, which is never a valid
2157
+ // path on Windows and silently loses the .NET evidence elsewhere
2158
+ // (issue 4394). A relative value still resolves against the scan path.
2159
+ // Without --deps-slices-file the slice goes to a directory of this run:
2160
+ // a fixed name in the shared temp directory was reused by every later
2161
+ // scan, of any project, as if it were that project's evidence.
2162
+ const slicesDir = options.depsSlicesFile
2163
+ ? undefined
2164
+ : safeMkdtempSync(join(getTmpDir(), "cdxgen-dosai-"));
2165
+ const slicesFile = options.depsSlicesFile
2166
+ ? resolve(path, options.depsSlicesFile)
2167
+ : join(slicesDir, "dosai.json");
2168
+ try {
2169
+ // Create the slices file if it doesn't exist
2170
+ if (!safeExistsSync(slicesFile)) {
2171
+ thoughtLog(
2172
+ "Alright, the next step is to invoke the dosai command to identify evidence of occurrences for various components.",
2173
+ );
2174
+ const sliceResult = createDosaiMethodsSlice(
2175
+ resolve(path),
2176
+ resolve(slicesFile),
2177
+ options,
2178
+ );
2179
+ // A stopped run has already said so and what to change; the debug
2180
+ // output would show no dosai error for it (issue 4438).
2181
+ if (!sliceResult && !dosaiRunWasStopped(resolve(slicesFile))) {
2182
+ // Visible without debug output: otherwise the components silently
2183
+ // lose the dependency-slice evidence while the scan still exits 0.
2184
+ // --evidence (which implies --deep) runs dosai again during evinse
2185
+ // and can still add occurrences, so this names only what is lost.
2186
+ // Debug output comes from the environment, not a CLI flag (issue
2187
+ // 4438).
2188
+ console.warn(
2189
+ "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.",
2190
+ );
2191
+ }
2192
+ } else if (DEBUG_MODE) {
2025
2193
  console.log(
2026
- "Slicing with dosai was unsuccessful. Check the errors reported in the logs above.",
2194
+ `Reusing the existing dosai methods slice "${slicesFile}" without checking whether it honours the current --exclude patterns. Delete the file to force a fresh run.`,
2027
2195
  );
2028
2196
  }
2197
+ pkgList = addEvidenceForDotnet(pkgList, slicesFile, {
2198
+ srcPath: resolve(path),
2199
+ });
2200
+ const methodsSlice = readDosaiMethodsReport(slicesFile, {
2201
+ sections: ["Metadata", "Services", "ApiEndpoints"],
2202
+ });
2203
+ // Provider-backed Services[] first so bom-refs, trust zones, and data
2204
+ // classifications survive; ApiEndpoints derivation fills in the rest.
2205
+ const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
2206
+ collectDosaiServicesFromMethods(methodsSlice, servicesMap);
2207
+ services = normalizeDosaiServiceMap(servicesMap);
2208
+ } finally {
2209
+ if (slicesDir) {
2210
+ safeRmSync(slicesDir, { recursive: true, force: true });
2211
+ }
2029
2212
  }
2030
- pkgList = addEvidenceForDotnet(pkgList, slicesFile);
2031
- const methodsSlice = readDosaiJsonFile(slicesFile);
2032
- // Provider-backed Services[] first so bom-refs, trust zones, and data
2033
- // classifications survive; ApiEndpoints derivation fills in the rest.
2034
- const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
2035
- collectDosaiServicesFromMethods(methodsSlice, servicesMap);
2036
- services = normalizeDosaiServiceMap(servicesMap);
2037
2213
  }
2038
2214
  }
2039
- // Parent dependency tree
2215
+ // Parent dependency tree. A single project file already contributed an
2216
+ // entry for the same parent, so the lock file roots are merged into it
2217
+ // rather than added as a second entry for the same ref.
2040
2218
  if (parentDependsOn.size && parentComponent?.["bom-ref"]) {
2041
- dependencies.splice(0, 0, {
2042
- ref: parentComponent["bom-ref"],
2043
- dependsOn: Array.from(parentDependsOn).sort(),
2044
- });
2219
+ dependencies = mergeDependencies(
2220
+ [
2221
+ {
2222
+ ref: parentComponent["bom-ref"],
2223
+ dependsOn: Array.from(parentDependsOn).sort(),
2224
+ },
2225
+ ],
2226
+ dependencies,
2227
+ parentComponent,
2228
+ );
2045
2229
  }
2046
2230
  if (shouldFetchLicense()) {
2047
2231
  const retMap = await getNugetMetadata(pkgList, dependencies);
@@ -2271,34 +2455,41 @@ export function createPerlBom(path, options) {
2271
2455
  /**
2272
2456
  * Function to create bom string for Terraform and OpenTofu projects.
2273
2457
  *
2274
- * The `.terraform.lock.hcl` file pins the providers the configuration
2275
- * resolves to, with content hashes suitable for the CycloneDX hashes array.
2458
+ * The workspace assembly turns the provider lock file, the module manifest
2459
+ * `init` writes and the configuration files into module and provider
2460
+ * components with a dependency graph and offline licenses. When metadata
2461
+ * fetching is enabled, the public registries add licenses, repository URLs
2462
+ * and registry flags to components the offline pass could not resolve.
2276
2463
  *
2277
2464
  * @param {string} path to the project
2278
2465
  * @param {Object} options Parse options from the cli
2279
- * @returns {Object} BOM object
2466
+ * @returns {Promise<Object>} BOM object
2280
2467
  */
2281
- export function createTerraformBom(path, options) {
2282
- let pkgList = [];
2283
- const lockFiles = getAllFiles(
2468
+ export async function createTerraformBom(path, options) {
2469
+ const parentComponent = createDefaultParentComponent(
2284
2470
  path,
2285
- `${options.multiProject ? "**/" : ""}.terraform.lock.hcl`,
2471
+ "generic",
2286
2472
  options,
2287
2473
  );
2288
- for (const lockFile of lockFiles) {
2289
- if (DEBUG_MODE) {
2290
- console.log(`Parsing ${lockFile}`);
2291
- }
2292
- const result = parseTerraformLockFile(lockFile);
2293
- if (result.pkgList?.length) {
2294
- pkgList = pkgList.concat(result.pkgList);
2295
- }
2474
+ const { pkgList, dependencies, parentProperties, srcFiles } =
2475
+ parseTerraformWorkspace(path, options, parentComponent["bom-ref"]);
2476
+ // No module or provider found (for example, resources only, or a stray
2477
+ // .tf in a JS repo): emit nothing, including no root applications, so
2478
+ // multi-type scans do not gain an empty Terraform parent.
2479
+ if (!pkgList.some((p) => p.type === "library")) {
2480
+ return {};
2296
2481
  }
2297
- if (pkgList.length) {
2298
- return buildBomNSData(options, pkgList, "terraform", {
2299
- src: path,
2300
- filename: lockFiles.join(", "),
2301
- });
2482
+ parentComponent.properties = [
2483
+ ...(parentComponent.properties || []),
2484
+ ...parentProperties,
2485
+ ];
2486
+ if (pkgList.length && shouldFetchPackageMetadata()) {
2487
+ await getTerraformRegistryMetadata(pkgList);
2302
2488
  }
2303
- return {};
2489
+ return buildBomNSData(options, pkgList, "terraform", {
2490
+ src: path,
2491
+ filename: srcFiles.join(", "),
2492
+ dependencies,
2493
+ parentComponent,
2494
+ });
2304
2495
  }