@cdxgen/cdxgen 13.3.0 → 13.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/README.md +9 -6
  2. package/bin/cdxgen.js +90 -26
  3. package/bin/evinse.js +18 -1
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +45 -15
  6. package/bin/tracebom.js +5 -1
  7. package/bin/validate.js +84 -18
  8. package/bin/verify.js +155 -86
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +61 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/index.js +113 -54
  15. package/lib/cli/jsBom.js +50 -29
  16. package/lib/cli/jvmBom.js +665 -138
  17. package/lib/cli/managedBom.js +320 -200
  18. package/lib/cli/nativeBom.js +139 -30
  19. package/lib/core/activity.js +12 -0
  20. package/lib/core/env.js +17 -9
  21. package/lib/core/fs.js +3 -2
  22. package/lib/core/httpClient.js +159 -24
  23. package/lib/core/pluginRun.js +184 -0
  24. package/lib/core/processTree.js +100 -0
  25. package/lib/core/runState.js +39 -0
  26. package/lib/core/toml.js +72 -0
  27. package/lib/ecosystems/asarutils.js +4 -2
  28. package/lib/ecosystems/bunutils.js +5 -7
  29. package/lib/ecosystems/cargoCache.js +231 -8
  30. package/lib/ecosystems/cmakeResolver.js +10 -21
  31. package/lib/ecosystems/cppBuildContext.js +631 -0
  32. package/lib/ecosystems/cppEvidence.js +337 -22
  33. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  34. package/lib/ecosystems/denoutils.js +93 -66
  35. package/lib/ecosystems/ecosystems.js +1967 -788
  36. package/lib/ecosystems/golem.js +28 -12
  37. package/lib/ecosystems/gradleutils.js +314 -30
  38. package/lib/ecosystems/jsEvidence.js +85 -24
  39. package/lib/ecosystems/kosi.js +21 -4
  40. package/lib/ecosystems/npmutils.js +64 -0
  41. package/lib/ecosystems/parsers-dotnet.js +567 -151
  42. package/lib/ecosystems/parsers-elm.js +46 -28
  43. package/lib/ecosystems/parsers-gleam.js +3 -4
  44. package/lib/ecosystems/parsers-go.js +324 -61
  45. package/lib/ecosystems/parsers-js.js +92 -26
  46. package/lib/ecosystems/parsers-julia.js +2 -3
  47. package/lib/ecosystems/parsers-jvm.js +204 -12
  48. package/lib/ecosystems/parsers-misc.js +182 -55
  49. package/lib/ecosystems/parsers-python.js +141 -10
  50. package/lib/ecosystems/parsers-rust.js +4 -4
  51. package/lib/ecosystems/parsers-terraform.js +1573 -19
  52. package/lib/ecosystems/pythonutils.js +102 -6
  53. package/lib/ecosystems/remote/huggingface.js +3 -0
  54. package/lib/ecosystems/rubyutils.js +67 -17
  55. package/lib/ecosystems/rusi.js +22 -9
  56. package/lib/ecosystems/sbtutils.js +243 -187
  57. package/lib/ecosystems/scalacliutils.js +342 -0
  58. package/lib/ecosystems/scalasem.js +2618 -0
  59. package/lib/ecosystems/terraformRegistry.js +288 -0
  60. package/lib/ecosystems/utils.js +2 -0
  61. package/lib/ecosystems/vendoredCode.js +156 -0
  62. package/lib/evinser/evinser.js +578 -69
  63. package/lib/helpers/agenticToolutils.js +2 -2
  64. package/lib/helpers/bomSigner.js +576 -128
  65. package/lib/inventory/aiCollector.js +4 -1
  66. package/lib/inventory/analyzer.js +110 -127
  67. package/lib/inventory/atomUtils.js +171 -48
  68. package/lib/inventory/auditCategories.js +7 -0
  69. package/lib/inventory/buildToolRateLimit.js +144 -0
  70. package/lib/inventory/cacheCommands.js +2 -1
  71. package/lib/inventory/cmakeBuildDirs.js +254 -0
  72. package/lib/inventory/deps.js +501 -124
  73. package/lib/inventory/dosai.js +248 -44
  74. package/lib/inventory/dosaiParsers.js +310 -15
  75. package/lib/inventory/dynamic.js +52 -6
  76. package/lib/inventory/evidenceUtils.js +154 -109
  77. package/lib/inventory/fetchBatch.js +574 -38
  78. package/lib/inventory/fetchDiskCache.js +597 -0
  79. package/lib/inventory/forgeEnricher.js +14 -5
  80. package/lib/inventory/jvmLocalRepos.js +917 -0
  81. package/lib/inventory/mcpConfigParser.js +2 -3
  82. package/lib/inventory/parentSelection.js +146 -0
  83. package/lib/inventory/rustFormulationParser.js +3 -4
  84. package/lib/inventory/scalaCoords.js +153 -0
  85. package/lib/inventory/source.js +8 -8
  86. package/lib/inventory/spdx.js +101 -0
  87. package/lib/inventory/traceRunner.js +88 -7
  88. package/lib/inventory/tracedCrypto.js +294 -0
  89. package/lib/managers/docker.js +33 -32
  90. package/lib/managers/oci.js +24 -2
  91. package/lib/parsers/cmakeCache.js +69 -0
  92. package/lib/parsers/cmakeLists.js +16 -2
  93. package/lib/parsers/cmakePresets.js +400 -0
  94. package/lib/parsers/compileCommands.js +557 -0
  95. package/lib/parsers/strictJson.js +322 -0
  96. package/lib/parsers/terraformConfig.js +1073 -0
  97. package/lib/parsers/terraformSource.js +652 -0
  98. package/lib/server/server.js +23 -0
  99. package/lib/stages/postgen/introspection/markers.js +1 -1
  100. package/lib/stages/postgen/postgen.js +15 -1
  101. package/lib/stages/postgen/specVersionCompat.js +32 -0
  102. package/lib/validator/index.js +25 -10
  103. package/lib/validator/reporters/json.js +1 -0
  104. package/package.json +21 -21
  105. package/types/lib/audit/index.d.ts.map +1 -1
  106. package/types/lib/cli/index.d.ts.map +1 -1
  107. package/types/lib/cli/jsBom.d.ts.map +1 -1
  108. package/types/lib/cli/jvmBom.d.ts +24 -1
  109. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  110. package/types/lib/cli/managedBom.d.ts +7 -4
  111. package/types/lib/cli/managedBom.d.ts.map +1 -1
  112. package/types/lib/cli/nativeBom.d.ts +3 -1
  113. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  114. package/types/lib/core/activity.d.ts.map +1 -1
  115. package/types/lib/core/env.d.ts +9 -0
  116. package/types/lib/core/env.d.ts.map +1 -1
  117. package/types/lib/core/fs.d.ts.map +1 -1
  118. package/types/lib/core/httpClient.d.ts +13 -8
  119. package/types/lib/core/httpClient.d.ts.map +1 -1
  120. package/types/lib/core/pluginRun.d.ts +97 -0
  121. package/types/lib/core/pluginRun.d.ts.map +1 -0
  122. package/types/lib/core/processTree.d.ts +36 -0
  123. package/types/lib/core/processTree.d.ts.map +1 -0
  124. package/types/lib/core/runState.d.ts +29 -0
  125. package/types/lib/core/runState.d.ts.map +1 -0
  126. package/types/lib/core/toml.d.ts +20 -0
  127. package/types/lib/core/toml.d.ts.map +1 -0
  128. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  129. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  131. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  132. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  133. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  134. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  135. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  136. package/types/lib/ecosystems/cppEvidence.d.ts +18 -2
  137. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  139. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  140. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  141. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  143. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  144. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  145. package/types/lib/ecosystems/gradleutils.d.ts +50 -0
  146. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  147. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  148. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  149. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  150. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  151. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  152. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  153. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  154. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  155. package/types/lib/ecosystems/parsers-go.d.ts +44 -2
  156. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  157. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  158. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  159. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  160. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  161. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  162. package/types/lib/ecosystems/parsers-misc.d.ts +3 -1
  163. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  164. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  165. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  166. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  167. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  168. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  169. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  170. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  171. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  172. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  173. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  174. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  175. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  176. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  177. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  178. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  179. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  180. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  181. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  182. package/types/lib/ecosystems/utils.d.ts +2 -2
  183. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  184. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  185. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  186. package/types/lib/evinser/evinser.d.ts +75 -8
  187. package/types/lib/evinser/evinser.d.ts.map +1 -1
  188. package/types/lib/helpers/bomSigner.d.ts +80 -4
  189. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  190. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  191. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  192. package/types/lib/inventory/atomUtils.d.ts +82 -10
  193. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  194. package/types/lib/inventory/auditCategories.d.ts +3 -0
  195. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  196. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  197. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  198. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  199. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  200. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  201. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  202. package/types/lib/inventory/deps.d.ts +59 -1
  203. package/types/lib/inventory/deps.d.ts.map +1 -1
  204. package/types/lib/inventory/dosai.d.ts +57 -8
  205. package/types/lib/inventory/dosai.d.ts.map +1 -1
  206. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  207. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  208. package/types/lib/inventory/dynamic.d.ts +10 -0
  209. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  210. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  211. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  212. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  213. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  214. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  215. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  216. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  217. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  218. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  219. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  220. package/types/lib/inventory/parentSelection.d.ts +34 -0
  221. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  222. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  223. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  224. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  225. package/types/lib/inventory/source.d.ts.map +1 -1
  226. package/types/lib/inventory/spdx.d.ts +9 -0
  227. package/types/lib/inventory/spdx.d.ts.map +1 -1
  228. package/types/lib/inventory/traceRunner.d.ts +12 -0
  229. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  230. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  231. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  232. package/types/lib/managers/docker.d.ts.map +1 -1
  233. package/types/lib/managers/oci.d.ts +3 -1
  234. package/types/lib/managers/oci.d.ts.map +1 -1
  235. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  236. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  237. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  238. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  239. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  240. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  241. package/types/lib/parsers/compileCommands.d.ts +115 -0
  242. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  243. package/types/lib/parsers/strictJson.d.ts +46 -0
  244. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  245. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  246. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  247. package/types/lib/parsers/terraformSource.d.ts +66 -0
  248. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  249. package/types/lib/server/server.d.ts.map +1 -1
  250. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  252. package/types/lib/validator/index.d.ts +8 -0
  253. package/types/lib/validator/index.d.ts.map +1 -1
  254. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  255. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -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 {
@@ -82,6 +89,7 @@ import {
82
89
  parseGemfileLockData,
83
90
  parseGemspecData,
84
91
  } from "../ecosystems/rubyutils.js";
92
+ import { getTerraformRegistryMetadata } from "../ecosystems/terraformRegistry.js";
85
93
  import {
86
94
  collectAiInventory,
87
95
  summarizeAiInventory,
@@ -105,6 +113,7 @@ import {
105
113
  collectDosaiServiceComponents,
106
114
  collectDosaiServicesFromMethods,
107
115
  createDosaiMethodsSlice,
116
+ dosaiRunWasStopped,
108
117
  isDosaiDotnetLanguage,
109
118
  normalizeDosaiServiceMap,
110
119
  readDosaiMethodsReport,
@@ -404,6 +413,17 @@ export async function createPythonBom(path, options) {
404
413
  !pyLockFiles?.length;
405
414
  const poetryMode = poetryFiles?.length;
406
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
+
407
427
  // TODO: Support for nested directories
408
428
  const setupPy = join(path, "setup.py");
409
429
  const setupPyMode = safeExistsSync(setupPy);
@@ -416,7 +436,9 @@ export async function createPythonBom(path, options) {
416
436
  if (DEBUG_MODE) {
417
437
  console.log(`Parsing ${f}`);
418
438
  }
419
- let retMap = await parsePyLockData(lockData, f);
439
+ let retMap = await parsePyLockData(lockData, f, undefined, {
440
+ installedMetadata,
441
+ });
420
442
  if (retMap?.workspaceWarningShown) {
421
443
  deferFailOnError(options, {
422
444
  ecosystem: "python",
@@ -508,7 +530,11 @@ export async function createPythonBom(path, options) {
508
530
  `exported-${basename(basePath)}-reqs.txt`,
509
531
  );
510
532
  safeWriteSync(tmpReqFile, exportedReqs);
511
- const dlist = await parseReqFile(tmpReqFile, false);
533
+ const dlist = await parseReqFile(
534
+ tmpReqFile,
535
+ false,
536
+ installedMetadata,
537
+ );
512
538
  if (dlist?.length) {
513
539
  pkgList = pkgList.concat(dlist);
514
540
  }
@@ -518,15 +544,6 @@ export async function createPythonBom(path, options) {
518
544
  }
519
545
  options.parentComponent = parentComponent;
520
546
  } // 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
547
  // .whl files. Zip file containing dist-info directory
531
548
  if (whlFiles?.length) {
532
549
  for (const wf of whlFiles) {
@@ -592,7 +609,7 @@ export async function createPythonBom(path, options) {
592
609
  let dlist;
593
610
  try {
594
611
  const lockData = JSON.parse(readFileSync(piplockFile));
595
- dlist = await parsePiplockData(lockData);
612
+ dlist = await parsePiplockData(lockData, installedMetadata);
596
613
  } catch (err) {
597
614
  recordDegradation("python.lockfile-unparseable.pipenv", {
598
615
  ecosystem: "python",
@@ -617,7 +634,7 @@ export async function createPythonBom(path, options) {
617
634
  } else if (reqDirFiles?.length) {
618
635
  for (const j in reqDirFiles) {
619
636
  const f = reqDirFiles[j];
620
- const dlist = await parseReqFile(f, false);
637
+ const dlist = await parseReqFile(f, false, installedMetadata);
621
638
  if (dlist?.length) {
622
639
  pkgList = pkgList.concat(dlist);
623
640
  }
@@ -625,13 +642,30 @@ export async function createPythonBom(path, options) {
625
642
  metadataFilename = reqDirFiles.join(", ");
626
643
  } else if (reqFiles?.length) {
627
644
  for (const f of reqFiles) {
628
- const dlist = await parseReqFile(f, false);
645
+ const dlist = await parseReqFile(f, false, installedMetadata);
629
646
  if (dlist?.length) {
630
647
  pkgList = pkgList.concat(dlist);
631
648
  }
632
649
  }
633
650
  }
634
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
+ }
635
669
 
636
670
  const parentDependsOn = new Set();
637
671
 
@@ -742,79 +776,81 @@ export async function createPythonBom(path, options) {
742
776
  );
743
777
  }
744
778
  }
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
- }
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
+ });
772
807
  }
773
808
  }
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));
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;
787
820
  }
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 };
821
+ parentDependsOn.add(pypiBomRef(p.name, p.version));
798
822
  }
799
823
  }
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
- }
824
+ if (retMap.dependenciesList) {
825
+ dependencies = mergeDependencies(
826
+ dependencies,
827
+ retMap.dependenciesList,
828
+ parentComponent,
829
+ );
807
830
  }
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);
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;
816
841
  }
817
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
+ }
818
854
  }
819
855
  // Final fallback is to manually parse setup.py if we still
820
856
  // have an empty list
@@ -1403,7 +1439,7 @@ export async function createRubyBom(path, options) {
1403
1439
  console.log(`Parsing ${f}`);
1404
1440
  }
1405
1441
  const gemLockData = readFileSync(f, { encoding: "utf-8" });
1406
- const retMap = await parseGemfileLockData(gemLockData, f);
1442
+ const retMap = await parseGemfileLockData(gemLockData, f, { gemHome });
1407
1443
  if (retMap.pkgList?.length) {
1408
1444
  pkgList = pkgList.concat(retMap.pkgList);
1409
1445
  pkgList = trimComponents(pkgList);
@@ -1457,7 +1493,7 @@ export async function createRubyBom(path, options) {
1457
1493
  const gpkgList = await parseGemspecData(
1458
1494
  gemspecData,
1459
1495
  f,
1460
- isRootGemspec ? { lockedVersions } : {},
1496
+ isRootGemspec ? { lockedVersions, projectGemspec: true } : {},
1461
1497
  );
1462
1498
  if (!gpkgList.length) {
1463
1499
  continue;
@@ -1821,49 +1857,88 @@ export async function createCsharpBom(path, options) {
1821
1857
  const parentDependsOn = new Set();
1822
1858
  if (nupkgFiles.length && projAssetsFiles.length === 0) {
1823
1859
  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 = {};
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 = {};
1828
1867
  for (const nf of nupkgFiles) {
1829
1868
  if (DEBUG_MODE) {
1830
1869
  console.log(`Parsing ${nf}`);
1831
1870
  }
1832
1871
  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;
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);
1838
1881
  }
1839
1882
  }
1840
- if (retMap?.dependenciesMap) {
1841
- dependenciesMap = { ...dependenciesMap, ...retMap.dependenciesMap };
1883
+ if (retMap?.dependencyRanges) {
1884
+ dependencyRanges = { ...dependencyRanges, ...retMap.dependencyRanges };
1842
1885
  }
1843
1886
  } // 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() });
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() });
1851
1901
  }
1852
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
+ };
1853
1935
  // In a multi-project scan, different projects may use different manifest
1854
1936
  // formats: modern SDK-style (project.assets.json / packages.lock.json) or
1855
1937
  // legacy (packages.config). Apply the assets > lock > config precedence per
1856
1938
  // project directory instead of across the whole scan, so dependencies from
1857
1939
  // one project's manifest are not dropped just because another format exists
1858
1940
  // 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)));
1941
+ const assetsProjectDirs = new Set();
1867
1942
  // project.assets.json parsing
1868
1943
  if (projAssetsFiles.length) {
1869
1944
  manifestFiles = manifestFiles.concat(projAssetsFiles);
@@ -1875,6 +1950,11 @@ export async function createCsharpBom(path, options) {
1875
1950
  const results = parseCsProjAssetsData(pkgData, af);
1876
1951
  const deps = results["dependenciesList"];
1877
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);
1878
1958
  if (dlist?.length) {
1879
1959
  pkgList = pkgList.concat(dlist);
1880
1960
  }
@@ -1907,9 +1987,12 @@ export async function createCsharpBom(path, options) {
1907
1987
  });
1908
1988
  }
1909
1989
  }
1990
+ const lockProjectDirs = new Set(
1991
+ pkgLockFiles.map((af) => dotnetManifestProjectDir(af)),
1992
+ );
1910
1993
  // packages.lock.json parsing for any project without a project.assets.json
1911
1994
  const pkgLockToParse = pkgLockFiles.filter(
1912
- (af) => !assetsProjectDirs.has(dirname(af)),
1995
+ (af) => !assetsProjectDirs.has(dotnetManifestProjectDir(af)),
1913
1996
  );
1914
1997
  if (pkgLockToParse.length) {
1915
1998
  manifestFiles = manifestFiles.concat(pkgLockToParse);
@@ -1923,37 +2006,29 @@ export async function createCsharpBom(path, options) {
1923
2006
  const deps = results["dependenciesList"];
1924
2007
  const dlist = results["pkgList"];
1925
2008
  const rootList = results["rootList"];
2009
+ const projectDir = dotnetManifestProjectDir(af);
2010
+ recordProjectVersions(projectDir, dlist);
1926
2011
  if (dlist?.length) {
1927
2012
  pkgList = pkgList.concat(dlist);
1928
2013
  }
1929
2014
  if (deps?.length) {
1930
2015
  dependencies = mergeDependencies(dependencies, deps, parentComponent);
1931
2016
  }
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
- }
2017
+ recordRoots(projectDir, rootList);
1939
2018
  }
1940
2019
  }
1941
2020
  // packages.config parsing for any project without a modern manifest
1942
2021
  // (project.assets.json / packages.lock.json) covering the same directory
1943
2022
  const pkgConfigToParse = pkgConfigFiles.filter((f) => {
1944
- const projDir = dirname(f);
2023
+ const projDir = dotnetManifestProjectDir(f);
1945
2024
  return !assetsProjectDirs.has(projDir) && !lockProjectDirs.has(projDir);
1946
2025
  });
1947
2026
  if (pkgConfigToParse.length) {
1948
2027
  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
- }
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);
1957
2032
  // packages.config parsing
1958
2033
  for (const f of pkgConfigToParse) {
1959
2034
  if (DEBUG_MODE) {
@@ -1961,11 +2036,11 @@ export async function createCsharpBom(path, options) {
1961
2036
  }
1962
2037
  pkgData = readFileSync(f, { encoding: "utf-8" });
1963
2038
  const dlist = parseCsPkgData(pkgData, f, resolvedVersions);
2039
+ const projectDir = dotnetManifestProjectDir(f);
2040
+ recordProjectVersions(projectDir, dlist);
1964
2041
  if (dlist?.length) {
1965
2042
  pkgList = pkgList.concat(dlist);
1966
- for (const d of dlist) {
1967
- parentDependsOn.add(d["bom-ref"]);
1968
- }
2043
+ recordRoots(projectDir, dlist);
1969
2044
  }
1970
2045
  }
1971
2046
  }
@@ -1988,27 +2063,26 @@ export async function createCsharpBom(path, options) {
1988
2063
  }
1989
2064
  }
1990
2065
  }
1991
- const pkgNameVersions = {};
2066
+ const projectRefByDir = {};
1992
2067
  let services = [];
1993
2068
  if (csProjFiles.length) {
1994
2069
  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
- }
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);
2002
2075
  let msbuildInstalled = false;
2003
- let pkgVersionLabelCandidates = {};
2004
- const result = safeSpawnSync(DOTNET_CMD, ["msbuild --version"], {
2076
+ const result = safeSpawnSync(DOTNET_CMD, ["msbuild", "--version"], {
2005
2077
  shell: isWin,
2006
2078
  });
2007
- if (result.status === 0 && !result.error && result.stdout.trim()) {
2079
+ if (result.status === 0 && !result.error && result.stdout?.trim()) {
2008
2080
  msbuildInstalled = true;
2009
- } else {
2010
- pkgVersionLabelCandidates = getPropertyGroupTextNodes(propsFiles);
2011
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);
2012
2086
  // Directory.Packages.props can sit above the directory being scanned, so it is
2013
2087
  // resolved per project file by walking up rather than from the props glob below.
2014
2088
  // The cache keeps a repository with many projects from re-parsing the same file.
@@ -2019,15 +2093,19 @@ export async function createCsharpBom(path, options) {
2019
2093
  console.log(`Parsing ${f}`);
2020
2094
  }
2021
2095
  const csProjData = readFileSync(f, { encoding: "utf-8" });
2096
+ const projectDir = dotnetManifestProjectDir(f);
2022
2097
  const retMap = parseCsProjData(
2023
2098
  csProjData,
2024
2099
  f,
2025
- pkgNameVersions,
2100
+ projectVersions[projectDir] || {},
2026
2101
  msbuildInstalled,
2027
- pkgVersionLabelCandidates,
2102
+ projectPropertyCandidates(f, propsIndex),
2028
2103
  getCentralPackageVersions(f, centralVersionsCache),
2104
+ agreedVersions,
2105
+ projectNames[projectDir] || {},
2029
2106
  );
2030
2107
  if (retMap?.parentComponent?.purl) {
2108
+ projectRefByDir[projectDir] ??= retMap.parentComponent["bom-ref"];
2031
2109
  // If there are multiple project files, track the parent components using nested components
2032
2110
  if (csProjFiles.length > 1) {
2033
2111
  if (!parentComponent.components) {
@@ -2051,6 +2129,25 @@ export async function createCsharpBom(path, options) {
2051
2129
  }
2052
2130
  }
2053
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
+ }
2054
2151
  if (pkgList.length) {
2055
2152
  pkgList = trimComponents(pkgList);
2056
2153
  // Perform deep analysis using dosai
@@ -2059,44 +2156,60 @@ export async function createCsharpBom(path, options) {
2059
2156
  // as-is: join would append it to the scan path, which is never a valid
2060
2157
  // path on Windows and silently loses the .NET evidence elsewhere
2061
2158
  // (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.",
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) {
2193
+ console.log(
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.`,
2084
2195
  );
2085
2196
  }
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.`,
2089
- );
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
+ }
2090
2212
  }
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
2213
  }
2101
2214
  }
2102
2215
  // Parent dependency tree. A single project file already contributed an
@@ -2342,34 +2455,41 @@ export function createPerlBom(path, options) {
2342
2455
  /**
2343
2456
  * Function to create bom string for Terraform and OpenTofu projects.
2344
2457
  *
2345
- * The `.terraform.lock.hcl` file pins the providers the configuration
2346
- * 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.
2347
2463
  *
2348
2464
  * @param {string} path to the project
2349
2465
  * @param {Object} options Parse options from the cli
2350
- * @returns {Object} BOM object
2466
+ * @returns {Promise<Object>} BOM object
2351
2467
  */
2352
- export function createTerraformBom(path, options) {
2353
- let pkgList = [];
2354
- const lockFiles = getAllFiles(
2468
+ export async function createTerraformBom(path, options) {
2469
+ const parentComponent = createDefaultParentComponent(
2355
2470
  path,
2356
- `${options.multiProject ? "**/" : ""}.terraform.lock.hcl`,
2471
+ "generic",
2357
2472
  options,
2358
2473
  );
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
- }
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 {};
2367
2481
  }
2368
- if (pkgList.length) {
2369
- return buildBomNSData(options, pkgList, "terraform", {
2370
- src: path,
2371
- filename: lockFiles.join(", "),
2372
- });
2482
+ parentComponent.properties = [
2483
+ ...(parentComponent.properties || []),
2484
+ ...parentProperties,
2485
+ ];
2486
+ if (pkgList.length && shouldFetchPackageMetadata()) {
2487
+ await getTerraformRegistryMetadata(pkgList);
2373
2488
  }
2374
- return {};
2489
+ return buildBomNSData(options, pkgList, "terraform", {
2490
+ src: path,
2491
+ filename: srcFiles.join(", "),
2492
+ dependencies,
2493
+ parentComponent,
2494
+ });
2375
2495
  }