@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
@@ -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) {
@@ -481,16 +744,115 @@ function propertyGroupText(propertyGroup, key) {
481
744
  return text.trim();
482
745
  }
483
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
+
484
833
  /**
485
834
  * Method to parse .csproj like xml files
486
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
+ *
487
842
  * @param {String} csProjData Raw data
488
843
  * @param {String} projFile File name
489
- * @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
490
846
  * @param {Boolean} msbuildInstalled Whether msbuild is available to resolve properties
491
- * @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
492
849
  * @param {Object} centralVersions Versions declared centrally in Directory.Packages.props,
493
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.
494
856
  *
495
857
  * @returns {Object} Containing parent component, package, and dependencies
496
858
  */
@@ -501,6 +863,8 @@ export function parseCsProjData(
501
863
  msbuildInstalled = false,
502
864
  pkgVersionLabelCandidates = {},
503
865
  centralVersions = {},
866
+ fallbackVersions = {},
867
+ restoredNames = {},
504
868
  ) {
505
869
  const pkgList = [];
506
870
  const parentComponent = { type: "application", properties: [] };
@@ -529,6 +893,39 @@ export function parseCsProjData(
529
893
  return pkgList;
530
894
  }
531
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
+ }
532
929
  // SDK-style projects declare their version through the MSBuild properties
533
930
  // that `dotnet pack` reads: PackageVersion, which defaults to Version, which
534
931
  // in turn defaults to VersionPrefix plus an optional `-VersionSuffix`. A
@@ -726,53 +1123,30 @@ export function parseCsProjData(
726
1123
  if (!pref.Include || pref.Include.includes(".csproj")) {
727
1124
  continue;
728
1125
  }
729
- pkg.name = pref.Include;
1126
+ pkg.name = restoredName(pref.Include);
730
1127
  // Under central package management a PackageReference carries no Version at
731
1128
  // all, and VersionOverride is the documented way for a project to opt out of
732
- // the central version, so it wins over everything else. The central version
733
- // is consulted only after pkgNameVersions, which holds versions already
734
- // resolved from project.assets.json / packages.lock.json and is therefore
735
- // 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.
736
1132
  pkg.version =
737
1133
  pref.VersionOverride ||
738
1134
  item.PackageReference[j].VersionOverride?.[0]._?.[0] ||
739
1135
  pref.Version ||
740
1136
  item.PackageReference[j].Version?.[0]._?.[0] ||
741
- pkgNameVersions[pkg.name] ||
742
- centralVersions[pkg.name.toLowerCase()];
743
- if (pkg.version) {
744
- const version_label_match = pkg.version.match(/^\$\((.*)\)$/)?.[1];
745
- if (version_label_match) {
746
- let version_resolved = false;
747
- if (msbuildInstalled) {
748
- const result = safeSpawnSync(
749
- DOTNET_CMD,
750
- [
751
- `msbuild ${projFile} -nologo -getProperty:${version_label_match}`,
752
- ],
753
- { shell: isWin },
754
- );
755
- if (
756
- result.status === 0 &&
757
- !result.error &&
758
- result.stdout.trim()
759
- ) {
760
- pkg.version = result.stdout.trim();
761
- version_resolved = true;
762
- }
763
- }
764
- if (!version_resolved) {
765
- if (
766
- pkgVersionLabelCandidates[version_label_match]?.length === 1
767
- ) {
768
- // Prioritizing correctness over completeness: reject resolved package version label if it has more than one possible values.
769
- pkg.version = pkgVersionLabelCandidates[version_label_match][0];
770
- } else if (DEBUG_MODE) {
771
- console.log(
772
- `Could not resolve package version label ${version_label_match} for ${pkg.name}`,
773
- );
774
- }
775
- }
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
+ );
776
1150
  }
777
1151
  }
778
1152
  // A `Version` attribute holds whatever the project declares, which may
@@ -789,9 +1163,7 @@ export function parseCsProjData(
789
1163
  pkg.version = exactPin[1];
790
1164
  } else if (!concreteVersion(pkg.version)) {
791
1165
  declaredRange = pkg.version;
792
- pkg.version =
793
- pkgNameVersions[pkg.name] ||
794
- centralVersions[pkg.name.toLowerCase()];
1166
+ pkg.version = resolvedVersion(pkg.name);
795
1167
  }
796
1168
  }
797
1169
  // A version that could not be resolved from any source must not be
@@ -866,21 +1238,20 @@ export function parseCsProjData(
866
1238
  }
867
1239
  }
868
1240
  }
1241
+ pkg.name = restoredName(pkg.name);
869
1242
  let declaredAssemblyVersion;
870
1243
  if (incParts.length > 1 && incParts[1].includes("Version")) {
871
1244
  const declared = incParts[1].replace("Version=", "").trim();
872
- // An assembly reference carries the version inline, and that value can
873
- // be an MSBuild property such as `Version=$(TargetFSharpCoreVersion)`
874
- // which only a build evaluates. Anything that is not a concrete
875
- // version is resolved from the manifests that pin one, keeping the
876
- // 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.
877
1251
  if (declared && !concreteVersion(declared)) {
878
1252
  declaredAssemblyVersion = declared;
879
- assemblyVersion =
880
- pkgNameVersions[pkg.name] ||
881
- centralVersions[pkg.name.toLowerCase()];
882
- } else {
883
- assemblyVersion = declared || pkgNameVersions[pkg.name];
1253
+ } else if (declared) {
1254
+ assemblyVersion = declared;
884
1255
  }
885
1256
  }
886
1257
  if (declaredAssemblyVersion) {
@@ -889,7 +1260,11 @@ export function parseCsProjData(
889
1260
  value: declaredAssemblyVersion,
890
1261
  });
891
1262
  }
892
- 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);
893
1268
  // An assembly reference that states no version anywhere - no HintPath to
894
1269
  // read one from and no `Version=` on the Include - leaves the component
895
1270
  // versionless in both fields. The registry's latest release is not a
@@ -917,7 +1292,7 @@ export function parseCsProjData(
917
1292
  value: assemblyName,
918
1293
  });
919
1294
  }
920
- if (hintVersion && assemblyVersion) {
1295
+ if (assemblyVersion) {
921
1296
  pkg.properties.push({
922
1297
  name: "cdx:dotnet:assembly_version",
923
1298
  value: assemblyVersion,
@@ -966,14 +1341,21 @@ export function parseCsProjData(
966
1341
  }
967
1342
  if (sdkVersion) {
968
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";
969
1353
  }
970
1354
  // The purl is built once every PropertyGroup has been read, so a version
971
- // declared after OutputType is not lost.
1355
+ // declared after OutputType is not lost. A project that states no version
1356
+ // gets a versionless purl: "latest" is not a version.
972
1357
  if (parentComponent.name) {
973
- applyPurl(
974
- parentComponent,
975
- nugetPurl(parentComponent.name, parentComponent.version || "latest"),
976
- );
1358
+ applyNugetVersion(parentComponent, parentComponent.version);
977
1359
  }
978
1360
  let dependencies = [];
979
1361
  if (parentComponent?.["bom-ref"]) {
@@ -991,6 +1373,41 @@ export function parseCsProjData(
991
1373
  };
992
1374
  }
993
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
+
994
1411
  /**
995
1412
  * Parse a .NET project.assets.json file and return the package list and dependency tree.
996
1413
  *
@@ -1000,7 +1417,8 @@ export function parseCsProjData(
1000
1417
  *
1001
1418
  * @param {string} csProjData Raw JSON string of the project.assets.json file
1002
1419
  * @param {string} assetsJsonFile Path to the project.assets.json file, used for evidence properties
1003
- * @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
1004
1422
  */
1005
1423
  export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
1006
1424
  // extract name, operator, version from .NET package representation
@@ -1040,22 +1458,18 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
1040
1458
  return { pkgList, dependenciesList };
1041
1459
  }
1042
1460
  csProjData = JSON.parse(csProjData);
1043
- let purlString;
1461
+ const projectPath = csProjData.project?.restore?.projectPath;
1462
+ let rootRef;
1044
1463
  if (csProjData.project?.restore?.projectName) {
1045
- purlString = build({
1046
- type: "nuget",
1047
- namespace: "" || null,
1048
- name: csProjData.project?.restore?.projectName,
1049
- version: csProjData.project.version || "latest" || null,
1050
- });
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.
1051
1466
  rootPkg = {
1052
1467
  group: "",
1053
1468
  name: csProjData.project.restore.projectName,
1054
- version: csProjData.project.version || "latest",
1055
1469
  type: "application",
1056
- purl: purlString,
1057
- "bom-ref": decodeURIComponent(purlString),
1058
1470
  };
1471
+ applyNugetVersion(rootPkg, csProjData.project.version);
1472
+ rootRef = rootPkg["bom-ref"];
1059
1473
  pkgList.push(rootPkg);
1060
1474
  }
1061
1475
  const rootPkgDeps = new Set();
@@ -1069,53 +1483,42 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
1069
1483
  if (nameOperatorVersion == null) {
1070
1484
  continue;
1071
1485
  }
1072
- const targetNameVersion = `${nameOperatorVersion.name}/${nameOperatorVersion.version}`;
1073
- let nameToUse = nameOperatorVersion.name;
1074
- // Due to the difference in casing, we might arrive this case where a simple lookup doesn't succeed.
1075
- // Instead of skipping, let's work harder to find a match.
1076
- if (!csProjData.targets[frameworkTarget][targetNameVersion]) {
1077
- let matchFound = false;
1078
- for (const fkeys of Object.keys(
1079
- csProjData.targets[frameworkTarget],
1080
- )) {
1081
- const tmpParts = fkeys.split("/");
1082
- const tname = tmpParts[0];
1083
- const tversion = tmpParts[1];
1084
- if (
1085
- tname.toLowerCase() === nameOperatorVersion.name.toLowerCase() &&
1086
- tversion === nameOperatorVersion.version.replace("-*", "")
1087
- ) {
1088
- nameToUse = tname;
1089
- matchFound = true;
1090
- break;
1091
- }
1092
- }
1093
- if (!matchFound) {
1094
- if (DEBUG_MODE) {
1095
- console.log(
1096
- "Unable to match",
1097
- dependencyName,
1098
- "with a target name. The dependency tree will be imprecise.",
1099
- );
1100
- }
1101
- 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
+ );
1102
1504
  }
1505
+ continue;
1103
1506
  }
1104
-
1507
+ const [nameToUse, resolvedVersion] = resolvedTarget.split("/");
1105
1508
  const dpurl = decodeURIComponent(
1106
1509
  build({
1107
1510
  type: "nuget",
1108
1511
  namespace: "" || null,
1109
1512
  name: nameToUse,
1110
- version: nameOperatorVersion.version || null,
1513
+ version: resolvedVersion || null,
1111
1514
  }),
1112
1515
  );
1113
1516
  rootPkgDeps.add(dpurl);
1114
1517
  }
1115
1518
  }
1116
- if (purlString && rootPkgDeps.size) {
1519
+ if (rootRef && rootPkgDeps.size) {
1117
1520
  dependenciesList.push({
1118
- ref: purlString,
1521
+ ref: rootRef,
1119
1522
  dependsOn: Array.from(rootPkgDeps).sort(),
1120
1523
  });
1121
1524
  }
@@ -1154,12 +1557,8 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
1154
1557
  if (lib[rootDep].files && Array.isArray(lib[rootDep].files)) {
1155
1558
  const dllFiles = new Set();
1156
1559
  lib[rootDep].files.forEach((f) => {
1157
- if (
1158
- f.endsWith(".dll") ||
1159
- f.endsWith(".exe") ||
1160
- f.endsWith(".so")
1161
- ) {
1162
- dllFiles.add(basename(f));
1560
+ if (isReferencedDotnetPackageFile(f)) {
1561
+ dllFiles.add(basename(f.replaceAll("\\", "/")));
1163
1562
  }
1164
1563
  });
1165
1564
  pkg.properties = [
@@ -1246,6 +1645,7 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
1246
1645
  return {
1247
1646
  pkgList,
1248
1647
  dependenciesList,
1648
+ projectPath,
1249
1649
  };
1250
1650
  }
1251
1651
 
@@ -1321,39 +1721,55 @@ export function parseCsPkgLockData(csLockData, pkgLockFile) {
1321
1721
  }
1322
1722
  const dependsOn = new Set();
1323
1723
  if (libData.dependencies) {
1324
- for (let adep of Object.keys(libData.dependencies)) {
1325
- let adepResolvedVersion = libData.dependencies[adep];
1326
- const aversionNoRuntime = aversion.split("/")[0];
1327
- // Try to get the resolved version of the dependency. See #930 and #937
1328
- if (assetData.dependencies[aversion]?.[adep]?.resolved) {
1329
- adepResolvedVersion =
1330
- assetData.dependencies[aversion][adep].resolved;
1331
- } else if (
1332
- aversion.includes("/") &&
1333
- assetData.dependencies[aversionNoRuntime]?.[adep]?.resolved
1334
- ) {
1335
- adepResolvedVersion =
1336
- assetData.dependencies[aversionNoRuntime][adep].resolved;
1337
- } else if (
1338
- (assetData.dependencies[aversion]?.[adep.toLowerCase()] &&
1339
- assetData.dependencies[aversion][adep.toLowerCase()].type ===
1340
- "Project") ||
1341
- (assetData.dependencies[aversionNoRuntime]?.[adep.toLowerCase()] &&
1342
- assetData.dependencies[aversionNoRuntime][adep.toLowerCase()]
1343
- .type === "Project")
1344
- ) {
1345
- adepResolvedVersion = undefined;
1346
- adep = adep.toLowerCase();
1347
- } else if (DEBUG_MODE) {
1348
- console.warn(
1349
- `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(),
1350
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;
1351
1767
  }
1352
1768
  const adpurl = build({
1353
1769
  type: "nuget",
1354
1770
  namespace: "" || null,
1355
- name: adep,
1356
- version: adepResolvedVersion || null,
1771
+ name: depName,
1772
+ version: depVersion || null,
1357
1773
  });
1358
1774
  dependsOn.add(decodeURIComponent(adpurl));
1359
1775
  }