@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
@@ -41,6 +41,252 @@ export function applyNugetVersion(pkg, version) {
41
41
  return applyPurl(pkg, nugetPurl(pkg.name, concrete));
42
42
  }
43
43
 
44
+ /**
45
+ * Compare two NuGet versions: up to four numeric release parts (missing parts
46
+ * read as 0), then the prerelease label, with a release sorting after its own
47
+ * prereleases. Build metadata after `+` is ignored, as NuGet ignores it.
48
+ *
49
+ * @param {string} a Version
50
+ * @param {string} b Version
51
+ * @returns {number} Negative, zero or positive, like a sort comparator
52
+ */
53
+ export function compareNugetVersions(a, b) {
54
+ const split = (version) => {
55
+ const withoutMetadata = `${version ?? ""}`.trim().split("+")[0];
56
+ const dash = withoutMetadata.indexOf("-");
57
+ const release =
58
+ dash >= 0 ? withoutMetadata.slice(0, dash) : withoutMetadata;
59
+ const prerelease = dash >= 0 ? withoutMetadata.slice(dash + 1) : "";
60
+ const parts = release.split(".").map((part) => Number.parseInt(part, 10));
61
+ return { parts, prerelease };
62
+ };
63
+ const left = split(a);
64
+ const right = split(b);
65
+ for (let index = 0; index < 4; index++) {
66
+ const l = Number.isNaN(left.parts[index]) ? 0 : (left.parts[index] ?? 0);
67
+ const r = Number.isNaN(right.parts[index]) ? 0 : (right.parts[index] ?? 0);
68
+ if (l !== r) {
69
+ return l < r ? -1 : 1;
70
+ }
71
+ }
72
+ if (left.prerelease === right.prerelease) {
73
+ return 0;
74
+ }
75
+ if (!left.prerelease) {
76
+ return 1;
77
+ }
78
+ if (!right.prerelease) {
79
+ return -1;
80
+ }
81
+ const leftLabels = left.prerelease.toLowerCase().split(".");
82
+ const rightLabels = right.prerelease.toLowerCase().split(".");
83
+ for (
84
+ let index = 0;
85
+ index < Math.max(leftLabels.length, rightLabels.length);
86
+ index++
87
+ ) {
88
+ const l = leftLabels[index];
89
+ const r = rightLabels[index];
90
+ if (l === undefined || r === undefined) {
91
+ return l === undefined ? -1 : 1;
92
+ }
93
+ if (l === r) {
94
+ continue;
95
+ }
96
+ const lNumeric = /^\d+$/.test(l);
97
+ const rNumeric = /^\d+$/.test(r);
98
+ if (lNumeric && rNumeric) {
99
+ return Number(l) < Number(r) ? -1 : 1;
100
+ }
101
+ if (lNumeric !== rNumeric) {
102
+ return lNumeric ? -1 : 1;
103
+ }
104
+ return l < r ? -1 : 1;
105
+ }
106
+ return 0;
107
+ }
108
+
109
+ /**
110
+ * Whether a NuGet version range allows a version. A bare version is a minimum
111
+ * (`1.0` means at least 1.0); brackets are inclusive and parentheses exclusive
112
+ * (`[1.0]`, `[1.0,2.0)`, `(,2.0]`). An empty or unparseable range allows
113
+ * anything.
114
+ *
115
+ * @param {string} range Range as a nuspec or lock file states it
116
+ * @param {string} version Version to test
117
+ * @returns {boolean} True when the range allows the version
118
+ */
119
+ export function nugetRangeAllows(range, version) {
120
+ const text = `${range ?? ""}`.trim();
121
+ if (!text || !version) {
122
+ return true;
123
+ }
124
+ const opening = text[0];
125
+ if (opening !== "[" && opening !== "(") {
126
+ return compareNugetVersions(version, text) >= 0;
127
+ }
128
+ const closing = text[text.length - 1];
129
+ if (closing !== "]" && closing !== ")") {
130
+ return true;
131
+ }
132
+ const inner = text.slice(1, -1);
133
+ const comma = inner.indexOf(",");
134
+ if (comma < 0) {
135
+ return compareNugetVersions(version, inner.trim()) === 0;
136
+ }
137
+ const minimum = inner.slice(0, comma).trim();
138
+ const maximum = inner.slice(comma + 1).trim();
139
+ if (minimum) {
140
+ const order = compareNugetVersions(version, minimum);
141
+ if (order < 0 || (order === 0 && opening === "(")) {
142
+ return false;
143
+ }
144
+ }
145
+ if (maximum) {
146
+ const order = compareNugetVersions(version, maximum);
147
+ if (order > 0 || (order === 0 && closing === ")")) {
148
+ return false;
149
+ }
150
+ }
151
+ return true;
152
+ }
153
+
154
+ /**
155
+ * Choose the installed version a declared dependency resolves to: the lowest
156
+ * installed version the range allows, as NuGet picks the lowest applicable
157
+ * version; else the only installed version, which is what binding redirects
158
+ * unify a dependency onto. Undefined when nothing installed can be named.
159
+ *
160
+ * @param {string} range Declared range
161
+ * @param {string[]} installedVersions Versions installed for the package
162
+ * @returns {string|undefined} The chosen version
163
+ */
164
+ export function resolveInstalledNugetVersion(range, installedVersions = []) {
165
+ const allowed = installedVersions
166
+ .filter((version) => nugetRangeAllows(range, version))
167
+ .sort(compareNugetVersions);
168
+ if (allowed.length) {
169
+ return allowed[0];
170
+ }
171
+ return installedVersions.length === 1 ? installedVersions[0] : undefined;
172
+ }
173
+
174
+ /**
175
+ * The project directory a .NET manifest belongs to. Restore output records its
176
+ * project file; otherwise project.assets.json sits in the project's `obj/`,
177
+ * and lock files, packages.config and project files sit in the project
178
+ * directory itself.
179
+ *
180
+ * @param {string} manifestFile Manifest path
181
+ * @param {string} [projectPath] Project file the manifest names, when it does
182
+ * @returns {string} Project directory
183
+ */
184
+ export function dotnetManifestProjectDir(manifestFile, projectPath) {
185
+ if (projectPath) {
186
+ const projectDir = dirname(resolve(projectPath));
187
+ if (safeExistsSync(projectDir)) {
188
+ return projectDir;
189
+ }
190
+ }
191
+ const dir = dirname(resolve(manifestFile));
192
+ return basename(dir) === "obj" ? dirname(dir) : dir;
193
+ }
194
+
195
+ /**
196
+ * Index the MSBuild properties that props files define, by the directory each
197
+ * file sits in and across the whole scan, so the candidates for every project
198
+ * come from one read of each file.
199
+ *
200
+ * @param {string[]} propsFiles Props files of the scan
201
+ * @returns {{byDir: Object, treeWide: Object}} Property values per directory and
202
+ * across the scan, each a map of property name to values
203
+ */
204
+ export function indexPropsProperties(propsFiles = []) {
205
+ const byDir = {};
206
+ const treeWide = {};
207
+ for (const propsFile of propsFiles) {
208
+ if (!propsFile) {
209
+ continue;
210
+ }
211
+ const nodes = getPropertyGroupTextNodes([propsFile]);
212
+ const dir = dirname(resolve(propsFile));
213
+ byDir[dir] ??= {};
214
+ for (const [key, values] of Object.entries(nodes)) {
215
+ byDir[dir][key] ??= [];
216
+ treeWide[key] ??= [];
217
+ for (const value of values) {
218
+ if (!byDir[dir][key].includes(value)) {
219
+ byDir[dir][key].push(value);
220
+ }
221
+ if (!treeWide[key].includes(value)) {
222
+ treeWide[key].push(value);
223
+ }
224
+ }
225
+ }
226
+ }
227
+ return { byDir, treeWide };
228
+ }
229
+
230
+ /**
231
+ * The candidate values of MSBuild properties that can apply to a project file,
232
+ * from the props files beside it and above it: the nearest directory that
233
+ * defines a property wins, as the nearest Directory.Build.props is the one
234
+ * MSBuild imports. A property no such directory defines falls back to the
235
+ * values the whole scan's props files give it, which a caller only trusts when
236
+ * there is exactly one.
237
+ *
238
+ * @param {string} projFile Project file
239
+ * @param {{byDir: Object, treeWide: Object}} propsIndex Index built by {@link indexPropsProperties}
240
+ * @returns {Object} Map of property name to candidate values
241
+ */
242
+ export function projectPropertyCandidates(projFile, propsIndex = {}) {
243
+ const candidates = { ...(propsIndex.treeWide || {}) };
244
+ const settled = new Set();
245
+ let dir = dirname(resolve(projFile));
246
+ for (;;) {
247
+ for (const [key, values] of Object.entries(propsIndex.byDir?.[dir] || {})) {
248
+ if (!settled.has(key)) {
249
+ candidates[key] = values;
250
+ settled.add(key);
251
+ }
252
+ }
253
+ const parent = dirname(dir);
254
+ if (parent === dir) {
255
+ break;
256
+ }
257
+ dir = parent;
258
+ }
259
+ return candidates;
260
+ }
261
+
262
+ /**
263
+ * The version of each package that every manifest of a scan agrees on, keyed by
264
+ * lowercased package id. A package that two manifests give different versions
265
+ * is left out: without knowing which project a declaration belongs to, picking
266
+ * one of them would be a guess.
267
+ *
268
+ * @param {Object[]} pkgList Components collected from the scan's manifests
269
+ * @returns {Object} Map of lowercased package id to version
270
+ */
271
+ export function agreedNugetVersions(pkgList = []) {
272
+ const versions = {};
273
+ for (const p of pkgList) {
274
+ if (!p?.name || !p.version || ["application", "project"].includes(p.type)) {
275
+ continue;
276
+ }
277
+ const key = p.name.toLowerCase();
278
+ versions[key] ??= new Set();
279
+ versions[key].add(p.version);
280
+ }
281
+ const agreed = {};
282
+ for (const [key, values] of Object.entries(versions)) {
283
+ if (values.size === 1) {
284
+ agreed[key] = [...values][0];
285
+ }
286
+ }
287
+ return agreed;
288
+ }
289
+
44
290
  /**
45
291
  * Method to parse .nupkg files
46
292
  *
@@ -70,9 +316,16 @@ export function parseNuspecData(nupkgFile, nuspecData) {
70
316
  const pkg = { group: "" };
71
317
  let npkg;
72
318
  const dependenciesMap = {};
319
+ // The version range each dependency accepts, per depending package, so a
320
+ // caller holding the installed packages can point an edge at one of them.
321
+ const dependencyRanges = {};
73
322
  const addedMap = {};
74
323
  try {
75
- npkg = xml2js(nuspecData, {
324
+ // A nuspec written on Windows often starts with a UTF-8 byte order mark,
325
+ // which the XML parser rejects as text before the declaration.
326
+ const data =
327
+ nuspecData.charCodeAt(0) === 0xfeff ? nuspecData.slice(1) : nuspecData;
328
+ npkg = xml2js(data, {
76
329
  compact: true,
77
330
  alwaysArray: false,
78
331
  spaces: 4,
@@ -94,6 +347,7 @@ export function parseNuspecData(nupkgFile, nuspecData) {
94
347
  return {
95
348
  pkgList,
96
349
  dependenciesMap,
350
+ dependencyRanges,
97
351
  };
98
352
  }
99
353
  const m = npkg.metadata;
@@ -101,7 +355,12 @@ export function parseNuspecData(nupkgFile, nuspecData) {
101
355
  pkg.version = m.version._;
102
356
  pkg.description = m.description._;
103
357
  applyPurl(pkg, nugetPurl(pkg.name, pkg.version));
104
- if (m.licenseUrl) {
358
+ // A licence expression is the machine-readable licence NuGet records; the
359
+ // licence URL is the older, often unresolvable form, so the expression wins.
360
+ if (m.license?.$?.type === "expression" && m.license?._?.trim()) {
361
+ pkg.license = findLicenseId(m.license._.trim()) || m.license._.trim();
362
+ }
363
+ if (!pkg.license && m.licenseUrl) {
105
364
  pkg.license = findLicenseId(m.licenseUrl._);
106
365
  }
107
366
  if (m.authors) {
@@ -128,18 +387,19 @@ export function parseNuspecData(nupkgFile, nuspecData) {
128
387
  };
129
388
  pkg.scope = "required";
130
389
  pkgList.push(pkg);
390
+ const ranges = {};
131
391
  if (m?.dependencies?.dependency) {
132
392
  const dependsOn = [];
133
- if (Array.isArray(m.dependencies.dependency)) {
134
- for (const adep of m.dependencies.dependency) {
135
- const d = adep.$;
136
- dependsOn.push(d.id);
137
- }
138
- } else {
139
- const d = m.dependencies.dependency.$;
393
+ const declared = Array.isArray(m.dependencies.dependency)
394
+ ? m.dependencies.dependency
395
+ : [m.dependencies.dependency];
396
+ for (const adep of declared) {
397
+ const d = adep.$;
140
398
  dependsOn.push(d.id);
399
+ ranges[d.id] ??= d.version;
141
400
  }
142
401
  dependenciesMap[pkg["bom-ref"]] = dependsOn;
402
+ dependencyRanges[pkg["bom-ref"]] = ranges;
143
403
  } else if (m?.dependencies?.group) {
144
404
  let dependencyGroups;
145
405
  if (Array.isArray(m.dependencies.group)) {
@@ -168,6 +428,7 @@ export function parseNuspecData(nupkgFile, nuspecData) {
168
428
  continue;
169
429
  }
170
430
  groupPkg.name = agroupdep.id;
431
+ ranges[agroupdep.id] ??= agroupdep.version;
171
432
  let declaredRange;
172
433
  if (agroupdep?.version) {
173
434
  let versionStr = agroupdep.version;
@@ -225,11 +486,13 @@ export function parseNuspecData(nupkgFile, nuspecData) {
225
486
  } // for
226
487
  } // group dependency block
227
488
  dependenciesMap[pkg["bom-ref"]] = dependsOn;
489
+ dependencyRanges[pkg["bom-ref"]] = ranges;
228
490
  } // for
229
491
  }
230
492
  return {
231
493
  pkgList,
232
494
  dependenciesMap,
495
+ dependencyRanges,
233
496
  };
234
497
  }
235
498
 
@@ -289,7 +552,7 @@ export function parseCsPkgData(pkgData, pkgFile, pkgNameVersions = {}) {
289
552
  // Backfill from a version already resolved by a more precise manifest.
290
553
  // Concrete versions are never overridden - a disagreement means both
291
554
  // versions must be tracked.
292
- pkg.version = pkgNameVersions[pkg.name];
555
+ pkg.version = lookupNugetVersion(pkgNameVersions, pkg.name);
293
556
  }
294
557
  applyNugetVersion(pkg, pkg.version);
295
558
  if (pkgFile) {
@@ -466,16 +729,130 @@ export function getPropertyGroupTextNodes(propsFiles) {
466
729
  return matches;
467
730
  }
468
731
 
732
+ /**
733
+ * Read the trimmed text of an MSBuild property from a parsed PropertyGroup.
734
+ *
735
+ * @param {Object} propertyGroup PropertyGroup parsed by xml2js in compact mode
736
+ * @param {string} key Property name
737
+ * @returns {string|undefined} The text, or undefined when absent or empty
738
+ */
739
+ function propertyGroupText(propertyGroup, key) {
740
+ const text = propertyGroup?.[key]?.[0]?._?.[0];
741
+ if (typeof text !== "string" || !text.trim().length) {
742
+ return undefined;
743
+ }
744
+ return text.trim();
745
+ }
746
+
747
+ /**
748
+ * Look up a package version in a name - version map. NuGet package ids are
749
+ * case-insensitive, so a map may be keyed by the id as written or lowercased.
750
+ *
751
+ * @param {Object} versions Map of package id to version
752
+ * @param {string} name Package id
753
+ * @returns {string|undefined} The version, or undefined when the map has none
754
+ */
755
+ export function lookupNugetVersion(versions, name) {
756
+ if (!versions || !name) {
757
+ return undefined;
758
+ }
759
+ return versions[name] ?? versions[name.toLowerCase()];
760
+ }
761
+
762
+ /**
763
+ * Collect the MSBuild properties a project file defines itself, from every
764
+ * PropertyGroup, keyed by property name. A later definition wins, as it does
765
+ * when MSBuild evaluates the file.
766
+ *
767
+ * @param {Object} project Project element parsed by xml2js in compact mode
768
+ * @returns {Object} Map of property name to text value
769
+ */
770
+ function projectOwnProperties(project) {
771
+ const properties = {};
772
+ for (const propertyGroup of project?.PropertyGroup || []) {
773
+ for (const key of Object.keys(propertyGroup)) {
774
+ if (key === "$" || key === "value") {
775
+ continue;
776
+ }
777
+ const text = propertyGroupText(propertyGroup, key);
778
+ if (text !== undefined) {
779
+ properties[key] = text;
780
+ }
781
+ }
782
+ }
783
+ return properties;
784
+ }
785
+
786
+ /**
787
+ * Evaluate MSBuild properties of one project with a single `dotnet msbuild`
788
+ * call. Asking for every property at once costs one evaluation per project,
789
+ * where one call per package reference evaluated the same project over and
790
+ * over.
791
+ *
792
+ * @param {string} projFile Project file
793
+ * @param {string[]} names Property names
794
+ * @returns {Object} Map of property name to evaluated value, without empty values
795
+ */
796
+ function evaluateMsbuildProperties(projFile, names) {
797
+ const values = {};
798
+ if (!projFile || !names.length) {
799
+ return values;
800
+ }
801
+ const result = safeSpawnSync(
802
+ DOTNET_CMD,
803
+ [
804
+ "msbuild",
805
+ projFile,
806
+ "-nologo",
807
+ ...names.map((name) => `-getProperty:${name}`),
808
+ ],
809
+ { shell: isWin },
810
+ );
811
+ if (result.status !== 0 || result.error || !result.stdout?.trim()) {
812
+ return values;
813
+ }
814
+ const stdout = result.stdout.trim();
815
+ if (names.length === 1) {
816
+ values[names[0]] = stdout;
817
+ return values;
818
+ }
819
+ // Several properties come back as one JSON document.
820
+ try {
821
+ const evaluated = JSON.parse(stdout)?.Properties || {};
822
+ for (const name of names) {
823
+ if (typeof evaluated[name] === "string" && evaluated[name].trim()) {
824
+ values[name] = evaluated[name].trim();
825
+ }
826
+ }
827
+ } catch (_e) {
828
+ // Unexpected output: treat every property as unresolved.
829
+ }
830
+ return values;
831
+ }
832
+
469
833
  /**
470
834
  * Method to parse .csproj like xml files
471
835
  *
836
+ * A version the project file does not state is taken, in order, from the
837
+ * versions this project restored, from central package management, and last
838
+ * from a version the whole scan agrees on. Two projects of one tree routinely
839
+ * restore different versions of a package, so a version taken from another
840
+ * project would describe a package this project does not use.
841
+ *
472
842
  * @param {String} csProjData Raw data
473
843
  * @param {String} projFile File name
474
- * @param {Object} pkgNameVersions Package name - version map object
844
+ * @param {Object} pkgNameVersions Versions this project restored (its project.assets.json,
845
+ * packages.lock.json or packages.config), keyed by package id or lowercased id
475
846
  * @param {Boolean} msbuildInstalled Whether msbuild is available to resolve properties
476
- * @param {Object} pkgVersionLabelCandidates Candidate values for msbuild version properties
847
+ * @param {Object} pkgVersionLabelCandidates Candidate values for msbuild version properties,
848
+ * from the props files that can apply to this project
477
849
  * @param {Object} centralVersions Versions declared centrally in Directory.Packages.props,
478
850
  * keyed by lowercased package id. See {@link getCentralPackageVersions}.
851
+ * @param {Object} fallbackVersions Versions every project of the scan agrees on, keyed by
852
+ * lowercased package id, used only when nothing closer states one
853
+ * @param {Object} restoredNames Package ids as this project's restore output spells them,
854
+ * keyed by lowercased id. NuGet ids are case-insensitive, so a reference spelled
855
+ * differently names the same package and takes the restored spelling.
479
856
  *
480
857
  * @returns {Object} Containing parent component, package, and dependencies
481
858
  */
@@ -486,6 +863,8 @@ export function parseCsProjData(
486
863
  msbuildInstalled = false,
487
864
  pkgVersionLabelCandidates = {},
488
865
  centralVersions = {},
866
+ fallbackVersions = {},
867
+ restoredNames = {},
489
868
  ) {
490
869
  const pkgList = [];
491
870
  const parentComponent = { type: "application", properties: [] };
@@ -514,6 +893,68 @@ export function parseCsProjData(
514
893
  return pkgList;
515
894
  }
516
895
  const project = projects[0];
896
+ const restoredName = (name) => restoredNames[name.toLowerCase()] || name;
897
+ const resolvedVersion = (name) =>
898
+ lookupNugetVersion(pkgNameVersions, name) ||
899
+ lookupNugetVersion(centralVersions, name) ||
900
+ lookupNugetVersion(fallbackVersions, name);
901
+ // `$(Name)` version labels: the project's own properties, then the props
902
+ // files that can apply to it, then (only for what neither settles) one
903
+ // msbuild evaluation of the project.
904
+ const ownProperties = projectOwnProperties(project);
905
+ const labelOf = (version) => version?.match(/^\$\((.*)\)$/)?.[1];
906
+ const staticLabelValue = (label) =>
907
+ ownProperties[label] ??
908
+ (pkgVersionLabelCandidates[label]?.length === 1
909
+ ? pkgVersionLabelCandidates[label][0]
910
+ : undefined);
911
+ let msbuildValues = {};
912
+ if (msbuildInstalled) {
913
+ const unresolved = new Set();
914
+ for (const item of project.ItemGroup || []) {
915
+ for (const reference of item.PackageReference || []) {
916
+ const label = labelOf(
917
+ reference.$?.VersionOverride ||
918
+ reference.VersionOverride?.[0]._?.[0] ||
919
+ reference.$?.Version ||
920
+ reference.Version?.[0]._?.[0],
921
+ );
922
+ if (label && staticLabelValue(label) === undefined) {
923
+ unresolved.add(label);
924
+ }
925
+ }
926
+ }
927
+ msbuildValues = evaluateMsbuildProperties(projFile, [...unresolved].sort());
928
+ }
929
+ // SDK-style projects declare their version through the MSBuild properties
930
+ // that `dotnet pack` reads: PackageVersion, which defaults to Version, which
931
+ // in turn defaults to VersionPrefix plus an optional `-VersionSuffix`. A
932
+ // property may be set in any PropertyGroup, so they are collected first.
933
+ const sdkVersionProps = {};
934
+ for (const apg of project?.PropertyGroup || []) {
935
+ for (const key of [
936
+ "PackageVersion",
937
+ "Version",
938
+ "VersionPrefix",
939
+ "VersionSuffix",
940
+ ]) {
941
+ const value = propertyGroupText(apg, key);
942
+ if (value !== undefined) {
943
+ sdkVersionProps[key] = value;
944
+ }
945
+ }
946
+ }
947
+ let sdkVersion =
948
+ sdkVersionProps.PackageVersion ||
949
+ sdkVersionProps.Version ||
950
+ (sdkVersionProps.VersionPrefix
951
+ ? `${sdkVersionProps.VersionPrefix}${sdkVersionProps.VersionSuffix ? `-${sdkVersionProps.VersionSuffix}` : ""}`
952
+ : undefined);
953
+ // An MSBuild property reference such as $(BuildNumber) is only known at
954
+ // build time, so a version that still contains one is not reported.
955
+ if (sdkVersion?.includes("$(")) {
956
+ sdkVersion = undefined;
957
+ }
517
958
  // First make up a parentcomponent name based on the .csproj file name
518
959
  if (projFile) {
519
960
  parentComponent.name = basename(projFile).replaceAll(
@@ -571,10 +1012,6 @@ export function parseCsProjData(
571
1012
  if (outputType === "Library") {
572
1013
  parentComponent.type = "library";
573
1014
  }
574
- applyPurl(
575
- parentComponent,
576
- nugetPurl(parentComponent.name, parentComponent.version || "latest"),
577
- );
578
1015
  // The MSBuild OutputType (Exe, WinExe, …) used to ride along as an
579
1016
  // `output_type` purl qualifier, which nuget does not define — every such
580
1017
  // purl was invalid. It is project metadata, so it belongs in properties.
@@ -686,53 +1123,30 @@ export function parseCsProjData(
686
1123
  if (!pref.Include || pref.Include.includes(".csproj")) {
687
1124
  continue;
688
1125
  }
689
- pkg.name = pref.Include;
1126
+ pkg.name = restoredName(pref.Include);
690
1127
  // Under central package management a PackageReference carries no Version at
691
1128
  // all, and VersionOverride is the documented way for a project to opt out of
692
- // the central version, so it wins over everything else. The central version
693
- // is consulted only after pkgNameVersions, which holds versions already
694
- // resolved from project.assets.json / packages.lock.json and is therefore
695
- // more precise than a central declaration that may be a floating range.
1129
+ // the central version, so it wins over everything else. Otherwise the version
1130
+ // this project restored is more precise than a central declaration that may
1131
+ // be a floating range.
696
1132
  pkg.version =
697
1133
  pref.VersionOverride ||
698
1134
  item.PackageReference[j].VersionOverride?.[0]._?.[0] ||
699
1135
  pref.Version ||
700
1136
  item.PackageReference[j].Version?.[0]._?.[0] ||
701
- pkgNameVersions[pkg.name] ||
702
- centralVersions[pkg.name.toLowerCase()];
703
- if (pkg.version) {
704
- const version_label_match = pkg.version.match(/^\$\((.*)\)$/)?.[1];
705
- if (version_label_match) {
706
- let version_resolved = false;
707
- if (msbuildInstalled) {
708
- const result = safeSpawnSync(
709
- DOTNET_CMD,
710
- [
711
- `msbuild ${projFile} -nologo -getProperty:${version_label_match}`,
712
- ],
713
- { shell: isWin },
714
- );
715
- if (
716
- result.status === 0 &&
717
- !result.error &&
718
- result.stdout.trim()
719
- ) {
720
- pkg.version = result.stdout.trim();
721
- version_resolved = true;
722
- }
723
- }
724
- if (!version_resolved) {
725
- if (
726
- pkgVersionLabelCandidates[version_label_match]?.length === 1
727
- ) {
728
- // Prioritizing correctness over completeness: reject resolved package version label if it has more than one possible values.
729
- pkg.version = pkgVersionLabelCandidates[version_label_match][0];
730
- } else if (DEBUG_MODE) {
731
- console.log(
732
- `Could not resolve package version label ${version_label_match} for ${pkg.name}`,
733
- );
734
- }
735
- }
1137
+ resolvedVersion(pkg.name);
1138
+ const versionLabel = labelOf(pkg.version);
1139
+ if (versionLabel) {
1140
+ // Prioritizing correctness over completeness: a label with more than one
1141
+ // candidate value across the applicable props files stays unresolved.
1142
+ const labelValue =
1143
+ msbuildValues[versionLabel] ?? staticLabelValue(versionLabel);
1144
+ if (labelValue !== undefined) {
1145
+ pkg.version = labelValue;
1146
+ } else if (DEBUG_MODE) {
1147
+ console.log(
1148
+ `Could not resolve package version label ${versionLabel} for ${pkg.name}`,
1149
+ );
736
1150
  }
737
1151
  }
738
1152
  // A `Version` attribute holds whatever the project declares, which may
@@ -749,9 +1163,7 @@ export function parseCsProjData(
749
1163
  pkg.version = exactPin[1];
750
1164
  } else if (!concreteVersion(pkg.version)) {
751
1165
  declaredRange = pkg.version;
752
- pkg.version =
753
- pkgNameVersions[pkg.name] ||
754
- centralVersions[pkg.name.toLowerCase()];
1166
+ pkg.version = resolvedVersion(pkg.name);
755
1167
  }
756
1168
  }
757
1169
  // A version that could not be resolved from any source must not be
@@ -826,21 +1238,20 @@ export function parseCsProjData(
826
1238
  }
827
1239
  }
828
1240
  }
1241
+ pkg.name = restoredName(pkg.name);
829
1242
  let declaredAssemblyVersion;
830
1243
  if (incParts.length > 1 && incParts[1].includes("Version")) {
831
1244
  const declared = incParts[1].replace("Version=", "").trim();
832
- // An assembly reference carries the version inline, and that value can
833
- // be an MSBuild property such as `Version=$(TargetFSharpCoreVersion)`
834
- // which only a build evaluates. Anything that is not a concrete
835
- // version is resolved from the manifests that pin one, keeping the
836
- // declaration as a property.
1245
+ // An assembly reference carries the assembly's version inline, which is
1246
+ // not the version of the package that ships it (Newtonsoft.Json 13.0.3
1247
+ // ships assembly 13.0.0.0), and can be an MSBuild property such as
1248
+ // `Version=$(TargetFSharpCoreVersion)` that only a build evaluates. A
1249
+ // concrete one is kept as the assembly version; a property as a
1250
+ // declaration.
837
1251
  if (declared && !concreteVersion(declared)) {
838
1252
  declaredAssemblyVersion = declared;
839
- assemblyVersion =
840
- pkgNameVersions[pkg.name] ||
841
- centralVersions[pkg.name.toLowerCase()];
842
- } else {
843
- assemblyVersion = declared || pkgNameVersions[pkg.name];
1253
+ } else if (declared) {
1254
+ assemblyVersion = declared;
844
1255
  }
845
1256
  }
846
1257
  if (declaredAssemblyVersion) {
@@ -849,7 +1260,11 @@ export function parseCsProjData(
849
1260
  value: declaredAssemblyVersion,
850
1261
  });
851
1262
  }
852
- const version = hintVersion ?? assemblyVersion;
1263
+ // The package version comes from the HintPath's package folder, or from
1264
+ // what this project restored; never from the assembly version, and not
1265
+ // from a central pin, which applies to PackageReference items only.
1266
+ const version =
1267
+ hintVersion ?? lookupNugetVersion(pkgNameVersions, pkg.name);
853
1268
  // An assembly reference that states no version anywhere - no HintPath to
854
1269
  // read one from and no `Version=` on the Include - leaves the component
855
1270
  // versionless in both fields. The registry's latest release is not a
@@ -877,7 +1292,7 @@ export function parseCsProjData(
877
1292
  value: assemblyName,
878
1293
  });
879
1294
  }
880
- if (hintVersion && assemblyVersion) {
1295
+ if (assemblyVersion) {
881
1296
  pkg.properties.push({
882
1297
  name: "cdx:dotnet:assembly_version",
883
1298
  value: assemblyVersion,
@@ -924,21 +1339,29 @@ export function parseCsProjData(
924
1339
  }
925
1340
  }
926
1341
  }
927
- // If the parent still lacks a purl, add one based on the name and version
928
- if (parentComponent && !parentComponent.purl && parentComponent.name) {
929
- applyPurl(
930
- parentComponent,
931
- nugetPurl(parentComponent.name, parentComponent.version || "latest"),
932
- );
1342
+ if (sdkVersion) {
1343
+ parentComponent.version = sdkVersion;
1344
+ } else if (
1345
+ !parentComponent.version &&
1346
+ !Object.keys(sdkVersionProps).length &&
1347
+ (project.$?.Sdk || project.Sdk)
1348
+ ) {
1349
+ // An SDK-style project that sets no version builds as 1.0.0, the Version
1350
+ // its project.assets.json records too, so both describe one component. A
1351
+ // version set from a build-time property is unknown, not 1.0.0.
1352
+ parentComponent.version = "1.0.0";
933
1353
  }
934
- if (parentComponent?.purl) {
935
- parentComponent["bom-ref"] = parentComponent.purl;
1354
+ // The purl is built once every PropertyGroup has been read, so a version
1355
+ // declared after OutputType is not lost. A project that states no version
1356
+ // gets a versionless purl: "latest" is not a version.
1357
+ if (parentComponent.name) {
1358
+ applyNugetVersion(parentComponent, parentComponent.version);
936
1359
  }
937
1360
  let dependencies = [];
938
1361
  if (parentComponent?.["bom-ref"]) {
939
1362
  dependencies = [
940
1363
  {
941
- ref: parentComponent.purl,
1364
+ ref: parentComponent["bom-ref"],
942
1365
  dependsOn: [...new Set(pkgList.map((p) => p["bom-ref"]))].sort(),
943
1366
  },
944
1367
  ];
@@ -950,6 +1373,41 @@ export function parseCsProjData(
950
1373
  };
951
1374
  }
952
1375
 
1376
+ // The package folders a consuming project compiles against or loads at run
1377
+ // time. A DLL elsewhere in the package (build/, buildTransitive/, tools/,
1378
+ // analyzers/) is a build-time helper of the package itself, such as the
1379
+ // System.Formats.Asn1.dll coverlet.collector carries for its test-platform
1380
+ // collector: the project's code never calls it, so its name must not credit
1381
+ // the package with the project's framework calls (issue 4441).
1382
+ const DOTNET_REFERENCED_ASSET_FOLDERS = new Set(["lib", "ref", "runtimes"]);
1383
+
1384
+ /**
1385
+ * Whether a file of a NuGet package is an assembly a consuming project can
1386
+ * reference: a .dll, .exe, or .so under lib/, ref/, or runtimes/.
1387
+ *
1388
+ * @param {string} packageFile Path of the file inside the package, as project.assets.json lists it
1389
+ * @returns {boolean} true for a referenced assembly
1390
+ */
1391
+ export function isReferencedDotnetPackageFile(packageFile) {
1392
+ if (typeof packageFile !== "string") {
1393
+ return false;
1394
+ }
1395
+ const lowerFile = packageFile.toLowerCase();
1396
+ if (
1397
+ !lowerFile.endsWith(".dll") &&
1398
+ !lowerFile.endsWith(".exe") &&
1399
+ !lowerFile.endsWith(".so")
1400
+ ) {
1401
+ return false;
1402
+ }
1403
+ const normalized = lowerFile.replaceAll("\\", "/");
1404
+ const slashIndex = normalized.indexOf("/");
1405
+ return (
1406
+ slashIndex > 0 &&
1407
+ DOTNET_REFERENCED_ASSET_FOLDERS.has(normalized.slice(0, slashIndex))
1408
+ );
1409
+ }
1410
+
953
1411
  /**
954
1412
  * Parse a .NET project.assets.json file and return the package list and dependency tree.
955
1413
  *
@@ -959,7 +1417,8 @@ export function parseCsProjData(
959
1417
  *
960
1418
  * @param {string} csProjData Raw JSON string of the project.assets.json file
961
1419
  * @param {string} assetsJsonFile Path to the project.assets.json file, used for evidence properties
962
- * @returns {{ pkgList: Object[], dependenciesList: Object[] }}
1420
+ * @returns {{ pkgList: Object[], dependenciesList: Object[], projectPath: (string|undefined) }}
1421
+ * `projectPath` is the project file the restore output belongs to, as restore recorded it
963
1422
  */
964
1423
  export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
965
1424
  // extract name, operator, version from .NET package representation
@@ -999,22 +1458,18 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
999
1458
  return { pkgList, dependenciesList };
1000
1459
  }
1001
1460
  csProjData = JSON.parse(csProjData);
1002
- let purlString;
1461
+ const projectPath = csProjData.project?.restore?.projectPath;
1462
+ let rootRef;
1003
1463
  if (csProjData.project?.restore?.projectName) {
1004
- purlString = build({
1005
- type: "nuget",
1006
- namespace: "" || null,
1007
- name: csProjData.project?.restore?.projectName,
1008
- version: csProjData.project.version || "latest" || null,
1009
- });
1464
+ // The project's own version as restore evaluated it; "latest" is not a
1465
+ // version, so a project without one gets a versionless purl.
1010
1466
  rootPkg = {
1011
1467
  group: "",
1012
1468
  name: csProjData.project.restore.projectName,
1013
- version: csProjData.project.version || "latest",
1014
1469
  type: "application",
1015
- purl: purlString,
1016
- "bom-ref": decodeURIComponent(purlString),
1017
1470
  };
1471
+ applyNugetVersion(rootPkg, csProjData.project.version);
1472
+ rootRef = rootPkg["bom-ref"];
1018
1473
  pkgList.push(rootPkg);
1019
1474
  }
1020
1475
  const rootPkgDeps = new Set();
@@ -1028,53 +1483,42 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
1028
1483
  if (nameOperatorVersion == null) {
1029
1484
  continue;
1030
1485
  }
1031
- const targetNameVersion = `${nameOperatorVersion.name}/${nameOperatorVersion.version}`;
1032
- let nameToUse = nameOperatorVersion.name;
1033
- // Due to the difference in casing, we might arrive this case where a simple lookup doesn't succeed.
1034
- // Instead of skipping, let's work harder to find a match.
1035
- if (!csProjData.targets[frameworkTarget][targetNameVersion]) {
1036
- let matchFound = false;
1037
- for (const fkeys of Object.keys(
1038
- csProjData.targets[frameworkTarget],
1039
- )) {
1040
- const tmpParts = fkeys.split("/");
1041
- const tname = tmpParts[0];
1042
- const tversion = tmpParts[1];
1043
- if (
1044
- tname.toLowerCase() === nameOperatorVersion.name.toLowerCase() &&
1045
- tversion === nameOperatorVersion.version.replace("-*", "")
1046
- ) {
1047
- nameToUse = tname;
1048
- matchFound = true;
1049
- break;
1050
- }
1051
- }
1052
- if (!matchFound) {
1053
- if (DEBUG_MODE) {
1054
- console.log(
1055
- "Unable to match",
1056
- dependencyName,
1057
- "with a target name. The dependency tree will be imprecise.",
1058
- );
1059
- }
1060
- continue;
1486
+ // A direct dependency is declared as a constraint (`Moq >= 4.17.6`,
1487
+ // `X >= 7.0.0-*`, `[4.0.0, 5.0.0)`), and restore installs one version of
1488
+ // each package per target: the edge points at that installed version,
1489
+ // whatever the constraint's lower bound says.
1490
+ const resolvedTarget = Object.keys(
1491
+ csProjData.targets[frameworkTarget] || {},
1492
+ ).find(
1493
+ (targetKey) =>
1494
+ targetKey.split("/")[0].toLowerCase() ===
1495
+ nameOperatorVersion.name.toLowerCase(),
1496
+ );
1497
+ if (!resolvedTarget) {
1498
+ if (DEBUG_MODE) {
1499
+ console.log(
1500
+ "Unable to match",
1501
+ dependencyName,
1502
+ "with a target name. The dependency tree will be imprecise.",
1503
+ );
1061
1504
  }
1505
+ continue;
1062
1506
  }
1063
-
1507
+ const [nameToUse, resolvedVersion] = resolvedTarget.split("/");
1064
1508
  const dpurl = decodeURIComponent(
1065
1509
  build({
1066
1510
  type: "nuget",
1067
1511
  namespace: "" || null,
1068
1512
  name: nameToUse,
1069
- version: nameOperatorVersion.version || null,
1513
+ version: resolvedVersion || null,
1070
1514
  }),
1071
1515
  );
1072
1516
  rootPkgDeps.add(dpurl);
1073
1517
  }
1074
1518
  }
1075
- if (purlString && rootPkgDeps.size) {
1519
+ if (rootRef && rootPkgDeps.size) {
1076
1520
  dependenciesList.push({
1077
- ref: purlString,
1521
+ ref: rootRef,
1078
1522
  dependsOn: Array.from(rootPkgDeps).sort(),
1079
1523
  });
1080
1524
  }
@@ -1113,12 +1557,8 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
1113
1557
  if (lib[rootDep].files && Array.isArray(lib[rootDep].files)) {
1114
1558
  const dllFiles = new Set();
1115
1559
  lib[rootDep].files.forEach((f) => {
1116
- if (
1117
- f.endsWith(".dll") ||
1118
- f.endsWith(".exe") ||
1119
- f.endsWith(".so")
1120
- ) {
1121
- dllFiles.add(basename(f));
1560
+ if (isReferencedDotnetPackageFile(f)) {
1561
+ dllFiles.add(basename(f.replaceAll("\\", "/")));
1122
1562
  }
1123
1563
  });
1124
1564
  pkg.properties = [
@@ -1205,6 +1645,7 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
1205
1645
  return {
1206
1646
  pkgList,
1207
1647
  dependenciesList,
1648
+ projectPath,
1208
1649
  };
1209
1650
  }
1210
1651
 
@@ -1280,39 +1721,55 @@ export function parseCsPkgLockData(csLockData, pkgLockFile) {
1280
1721
  }
1281
1722
  const dependsOn = new Set();
1282
1723
  if (libData.dependencies) {
1283
- for (let adep of Object.keys(libData.dependencies)) {
1284
- let adepResolvedVersion = libData.dependencies[adep];
1285
- const aversionNoRuntime = aversion.split("/")[0];
1286
- // Try to get the resolved version of the dependency. See #930 and #937
1287
- if (assetData.dependencies[aversion]?.[adep]?.resolved) {
1288
- adepResolvedVersion =
1289
- assetData.dependencies[aversion][adep].resolved;
1290
- } else if (
1291
- aversion.includes("/") &&
1292
- assetData.dependencies[aversionNoRuntime]?.[adep]?.resolved
1293
- ) {
1294
- adepResolvedVersion =
1295
- assetData.dependencies[aversionNoRuntime][adep].resolved;
1296
- } else if (
1297
- (assetData.dependencies[aversion]?.[adep.toLowerCase()] &&
1298
- assetData.dependencies[aversion][adep.toLowerCase()].type ===
1299
- "Project") ||
1300
- (assetData.dependencies[aversionNoRuntime]?.[adep.toLowerCase()] &&
1301
- assetData.dependencies[aversionNoRuntime][adep.toLowerCase()]
1302
- .type === "Project")
1303
- ) {
1304
- adepResolvedVersion = undefined;
1305
- adep = adep.toLowerCase();
1306
- } else if (DEBUG_MODE) {
1307
- console.warn(
1308
- `Unable to find the resolved version for ${adep} ${aversion}. Using ${adepResolvedVersion} which may be incorrect.`,
1724
+ const aversionNoRuntime = aversion.split("/")[0];
1725
+ // The entry for a dependency: in this target, the target without its
1726
+ // runtime identifier, or a runtime-specific target of the same
1727
+ // framework. Package ids are case-insensitive, and the lock file writes
1728
+ // a dependency the way the depending package's nuspec spells it. See
1729
+ // #930 and #937.
1730
+ const sections = [
1731
+ assetData.dependencies[aversion],
1732
+ assetData.dependencies[aversionNoRuntime],
1733
+ ...Object.keys(assetData.dependencies)
1734
+ .filter((key) => key.startsWith(`${aversionNoRuntime}/`))
1735
+ .map((key) => assetData.dependencies[key]),
1736
+ ].filter(Boolean);
1737
+ const entryFor = (name) => {
1738
+ for (const section of sections) {
1739
+ const key = Object.keys(section).find(
1740
+ (candidate) => candidate.toLowerCase() === name.toLowerCase(),
1309
1741
  );
1742
+ if (key) {
1743
+ return { name: key, entry: section[key] };
1744
+ }
1745
+ }
1746
+ return undefined;
1747
+ };
1748
+ for (const adep of Object.keys(libData.dependencies)) {
1749
+ const found = entryFor(adep);
1750
+ let depName;
1751
+ let depVersion;
1752
+ if (found?.entry?.type === "Project") {
1753
+ depName = found.name;
1754
+ } else if (found?.entry?.resolved) {
1755
+ depName = found.name;
1756
+ depVersion = found.entry.resolved;
1757
+ } else {
1758
+ // The dependency value is the declared range (`[6.0.0, )`), which
1759
+ // names no package version: without a resolved entry there is no
1760
+ // component to point at.
1761
+ if (DEBUG_MODE) {
1762
+ console.warn(
1763
+ `Unable to find the resolved version for ${adep} ${aversion}. The dependency tree will be imprecise.`,
1764
+ );
1765
+ }
1766
+ continue;
1310
1767
  }
1311
1768
  const adpurl = build({
1312
1769
  type: "nuget",
1313
1770
  namespace: "" || null,
1314
- name: adep,
1315
- version: adepResolvedVersion || null,
1771
+ name: depName,
1772
+ version: depVersion || null,
1316
1773
  });
1317
1774
  dependsOn.add(decodeURIComponent(adpurl));
1318
1775
  }