@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
@@ -1,5 +1,18 @@
1
+ import { readFileSync } from "node:fs";
2
+ import {
3
+ basename,
4
+ dirname,
5
+ isAbsolute,
6
+ relative,
7
+ resolve,
8
+ sep,
9
+ win32,
10
+ } from "node:path";
11
+
1
12
  import { Purl } from "@cdxgen/cdx-purl";
2
13
 
14
+ import { safeExistsSync } from "../core/fs.js";
15
+
3
16
  /**
4
17
  * Build a lowercase type/namespace/name lookup key for a purl.
5
18
  *
@@ -73,6 +86,62 @@ function dosaiSourceLineNumber(item) {
73
86
  return location?.LineNumber || item?.LineNumber;
74
87
  }
75
88
 
89
+ /**
90
+ * Format the `file#line` location of a dosai MethodCalls row, relative to the
91
+ * scanned directory like dosai's source rows.
92
+ *
93
+ * A call site found in assembly IL names its file through the PDB. dosai 5
94
+ * writes it relative to the scan root and leaves Path out when the file has
95
+ * no place in the tree; dosai 4 writes the PDB path as it is: absolute for a
96
+ * local build, `/_/` for a deterministic one (owasp-dep-scan/dosai#79). An
97
+ * absolute path under the scanned directory is made relative to it, and any
98
+ * other build path, or a missing one, gives way to the file name, as for call
99
+ * graph edges, so both versions list a call site found in source and in IL
100
+ * once. Where no sequence point covers a call, the row names the assembly and
101
+ * its line is an IL offset, which is no source location.
102
+ *
103
+ * @param {Object} methodCall Dosai MethodCalls row
104
+ * @param {string} [srcPath] Directory dosai analyzed
105
+ * @returns {string|undefined} Location string, or undefined when the row names no source file
106
+ */
107
+ export function dosaiCallSiteLocation(methodCall, srcPath) {
108
+ const file = dosaiCallSiteFile(methodCall, srcPath);
109
+ const lowerFile = file?.toLowerCase();
110
+ if (!file || lowerFile.endsWith(".dll") || lowerFile.endsWith(".exe")) {
111
+ return undefined;
112
+ }
113
+ const lineNumber = methodCall?.LineNumber;
114
+ return lineNumber > 0 ? `${file}#${lineNumber}` : file;
115
+ }
116
+
117
+ function dosaiCallSiteFile(methodCall, srcPath) {
118
+ const filePath = methodCall?.Path;
119
+ if (filePath && filePath !== "<unknown>") {
120
+ if (!isAbsolute(filePath) && !win32.isAbsolute(filePath)) {
121
+ return filePath;
122
+ }
123
+ if (srcPath && isAbsolute(filePath)) {
124
+ const relativePath = relative(resolve(srcPath), filePath);
125
+ if (
126
+ relativePath &&
127
+ relativePath !== ".." &&
128
+ !relativePath.startsWith(`..${sep}`) &&
129
+ !isAbsolute(relativePath)
130
+ ) {
131
+ return relativePath;
132
+ }
133
+ }
134
+ }
135
+ const fileName = methodCall?.FileName || filePath;
136
+ if (!fileName || fileName === "<unknown>") {
137
+ return undefined;
138
+ }
139
+ // dosai 4 kept a Windows build's whole path as the file name on Linux.
140
+ return fileName.slice(
141
+ Math.max(fileName.lastIndexOf("/"), fileName.lastIndexOf("\\")) + 1,
142
+ );
143
+ }
144
+
76
145
  /**
77
146
  * Return a validated source location for .NET source extensions, from a call graph node.
78
147
  *
@@ -111,44 +180,270 @@ export function dosaiSourceLocation(location) {
111
180
  return sourceLocation;
112
181
  }
113
182
 
183
+ /**
184
+ * Split a purl into its version-free identity key and its version.
185
+ *
186
+ * @param {string} purl Package URL string
187
+ * @returns {{key: string, version: (string|undefined)}|undefined} Parts, or undefined for an empty input
188
+ */
189
+ function dosaiPurlParts(purl) {
190
+ const key = normalizeDosaiPurlKey(purl);
191
+ if (!key) {
192
+ return undefined;
193
+ }
194
+ let version;
195
+ try {
196
+ version = Purl.parse(purl).version || undefined;
197
+ } catch (_err) {
198
+ const at = purl.split("?")[0].split("#")[0].lastIndexOf("@");
199
+ version = at > 0 ? purl.slice(at + 1).split("?")[0] : undefined;
200
+ }
201
+ return { key, version };
202
+ }
203
+
204
+ /**
205
+ * The manifests a component was identified from, as recorded in its
206
+ * `internal:SrcFile` values.
207
+ *
208
+ * @param {Object} component BOM component
209
+ * @returns {string[]} Manifest paths as recorded
210
+ */
211
+ function componentSrcFiles(component) {
212
+ const files = new Set();
213
+ for (const property of component?.properties || []) {
214
+ if (property?.name === "internal:SrcFile" && property.value) {
215
+ files.add(`${property.value}`);
216
+ }
217
+ }
218
+ return [...files];
219
+ }
220
+
221
+ /**
222
+ * Resolve a recorded manifest path. A BOM records it relative to the scanned
223
+ * directory, a scan in progress as found; a relative path that does not exist
224
+ * under the scanned directory is tried against the working directory.
225
+ *
226
+ * @param {string} file Manifest path as recorded
227
+ * @param {string} [srcPath] Scanned directory
228
+ * @returns {string} Absolute manifest path
229
+ */
230
+ function resolveManifestPath(file, srcPath) {
231
+ if (isAbsolute(file)) {
232
+ return file;
233
+ }
234
+ const fromSrcPath = resolve(srcPath || ".", file);
235
+ if (!srcPath || safeExistsSync(fromSrcPath)) {
236
+ return fromSrcPath;
237
+ }
238
+ const fromCwd = resolve(file);
239
+ return safeExistsSync(fromCwd) ? fromCwd : fromSrcPath;
240
+ }
241
+
242
+ /**
243
+ * The project directory a .NET manifest belongs to. A project.assets.json
244
+ * names its project, which an artifacts layout keeps outside the `obj/`
245
+ * holding the file; otherwise it sits in the project's `obj/`, and the other
246
+ * manifests in the project directory.
247
+ *
248
+ * @param {string} file Absolute manifest path
249
+ * @returns {string} Absolute project directory
250
+ */
251
+ function manifestProjectDir(file) {
252
+ const dir = dirname(file);
253
+ if (basename(file).toLowerCase() === "project.assets.json") {
254
+ try {
255
+ const projectPath = JSON.parse(readFileSync(file, "utf-8"))?.project
256
+ ?.restore?.projectPath;
257
+ const projectDir = projectPath && dirname(resolve(projectPath));
258
+ if (projectDir && safeExistsSync(projectDir)) {
259
+ return projectDir;
260
+ }
261
+ } catch (_err) {
262
+ // An unreadable file falls back to its location
263
+ }
264
+ }
265
+ return basename(dir) === "obj" ? dirname(dir) : dir;
266
+ }
267
+
268
+ /**
269
+ * The project directories a candidate's manifests belong to, worked out on
270
+ * first use: only a record that names a version the BOM lacks, of a package
271
+ * the BOM holds in several versions, needs them.
272
+ *
273
+ * @param {Object} candidate Candidate built by buildDosaiPurlAliasMap
274
+ * @param {Map<string, string>} purlAliasMap Alias map carrying srcPath and the directory cache
275
+ * @returns {string[]} Absolute project directories
276
+ */
277
+ function candidateProjectDirs(candidate, purlAliasMap) {
278
+ if (!candidate.projectDirs) {
279
+ const dirs = new Set();
280
+ for (const file of candidate.srcFiles) {
281
+ const path = resolveManifestPath(file, purlAliasMap?.srcPath);
282
+ let dir = purlAliasMap?.projectDirCache?.get(path);
283
+ if (!dir) {
284
+ dir = manifestProjectDir(path);
285
+ purlAliasMap?.projectDirCache?.set(path, dir);
286
+ }
287
+ dirs.add(dir);
288
+ }
289
+ candidate.projectDirs = [...dirs];
290
+ }
291
+ return candidate.projectDirs;
292
+ }
293
+
114
294
  /**
115
295
  * Build a purl alias map from BOM components.
116
296
  *
117
- * Maps both exact component purls and normalized type/namespace/name keys to
118
- * the canonical component purl so dosai-reported purls can be reconciled.
297
+ * Maps each component purl to itself, and indexes the components by their
298
+ * version-free identity, so a dosai-reported purl can be reconciled by version
299
+ * (see {@link resolveDosaiComponentPurl}). Two versions of one package are two
300
+ * components, and nothing here picks one of them on a name alone.
119
301
  *
120
302
  * @param {Object[]} [components] Component objects with purl fields
121
- * @returns {Map<string, string>} Map of purl or normalized key to canonical component purl
303
+ * @param {Object} [options] Options
304
+ * @param {string} [options.srcPath] Directory dosai analyzed, which its relative locations
305
+ * and the BOM's relative manifest paths are relative to
306
+ * @returns {Map<string, string>} Map of purl to canonical component purl, carrying the identity index
122
307
  */
123
- export function buildDosaiPurlAliasMap(components = []) {
308
+ export function buildDosaiPurlAliasMap(components = [], options = {}) {
124
309
  const purlAliasMap = new Map();
310
+ const candidatesByKey = new Map();
125
311
  for (const component of components) {
126
312
  if (!component?.purl) {
127
313
  continue;
128
314
  }
129
315
  purlAliasMap.set(component.purl, component.purl);
130
- const key = normalizeDosaiPurlKey(component.purl);
131
- if (key && !purlAliasMap.has(key)) {
132
- purlAliasMap.set(key, component.purl);
316
+ const parts = dosaiPurlParts(component.purl);
317
+ if (!parts) {
318
+ continue;
319
+ }
320
+ const candidates = candidatesByKey.get(parts.key) || [];
321
+ const existing = candidates.find(
322
+ (candidate) => candidate.purl === component.purl,
323
+ );
324
+ if (existing) {
325
+ for (const file of componentSrcFiles(component)) {
326
+ if (!existing.srcFiles.includes(file)) {
327
+ existing.srcFiles.push(file);
328
+ }
329
+ }
330
+ } else {
331
+ candidates.push({
332
+ purl: component.purl,
333
+ version: parts.version,
334
+ srcFiles: componentSrcFiles(component),
335
+ });
133
336
  }
337
+ candidatesByKey.set(parts.key, candidates);
134
338
  }
339
+ purlAliasMap.candidatesByKey = candidatesByKey;
340
+ purlAliasMap.srcPath = options.srcPath;
341
+ purlAliasMap.projectDirCache = new Map();
135
342
  return purlAliasMap;
136
343
  }
137
344
 
138
345
  /**
139
- * Resolve a purl to the canonical component purl via the alias map.
346
+ * The candidate whose project directory is the nearest one holding a file.
347
+ *
348
+ * @param {Object[]} candidates Candidates of one package identity
349
+ * @param {string} location Location string (`path` or `path#line`), relative to srcPath or absolute
350
+ * @param {Map<string, string>} purlAliasMap Alias map built by buildDosaiPurlAliasMap
351
+ * @returns {string|undefined} Candidate purl, or undefined when no single candidate is nearest
352
+ */
353
+ function candidateForLocation(candidates, location, purlAliasMap) {
354
+ if (!location) {
355
+ return undefined;
356
+ }
357
+ const file = resolve(
358
+ purlAliasMap?.srcPath || ".",
359
+ `${location}`.split("#")[0],
360
+ );
361
+ let best;
362
+ let bestLength = -1;
363
+ let tied = false;
364
+ for (const candidate of candidates) {
365
+ for (const dir of candidateProjectDirs(candidate, purlAliasMap)) {
366
+ if (file !== dir && !file.startsWith(`${dir}${sep}`)) {
367
+ continue;
368
+ }
369
+ if (dir.length > bestLength) {
370
+ best = candidate.purl;
371
+ bestLength = dir.length;
372
+ tied = false;
373
+ } else if (dir.length === bestLength && best !== candidate.purl) {
374
+ tied = true;
375
+ }
376
+ }
377
+ }
378
+ return tied ? undefined : best;
379
+ }
380
+
381
+ /**
382
+ * Choose among the components that share a file name (two versions of a
383
+ * package ship the same DLLs): the only one, else the one whose project
384
+ * directory is nearest the record's location.
385
+ *
386
+ * @param {Iterable<string>} purls Candidate component purls
387
+ * @param {Map<string, string>} purlAliasMap Alias map built by buildDosaiPurlAliasMap
388
+ * @param {string} [location] Source location of the dosai record (`path` or `path#line`)
389
+ * @returns {string|undefined} The component purl, or undefined when it cannot be told
390
+ */
391
+ export function pickDosaiComponentByLocation(purls, purlAliasMap, location) {
392
+ const wanted = new Set(purls);
393
+ if (wanted.size <= 1) {
394
+ return [...wanted][0];
395
+ }
396
+ const candidates = [];
397
+ for (const list of purlAliasMap?.candidatesByKey?.values() || []) {
398
+ for (const candidate of list) {
399
+ if (wanted.has(candidate.purl)) {
400
+ candidates.push(candidate);
401
+ }
402
+ }
403
+ }
404
+ return candidateForLocation(candidates, location, purlAliasMap);
405
+ }
406
+
407
+ /**
408
+ * Resolve a dosai-reported purl to a BOM component purl.
409
+ *
410
+ * The exact purl wins, then the same package at the same version (ids compare
411
+ * case-insensitively). A versionless purl, or a version the BOM does not hold,
412
+ * maps to the package's only component when it has one. When the BOM holds
413
+ * several versions, the record's location picks the version of the project
414
+ * that file belongs to; without a location that settles it the purl maps to
415
+ * nothing rather than to an arbitrary version (issue dosai#72).
140
416
  *
141
417
  * @param {string} purl Purl reported by dosai
142
418
  * @param {Map<string, string>} purlAliasMap Alias map built by buildDosaiPurlAliasMap
143
- * @returns {string|undefined} Canonical component purl, the input purl when unaliased, or undefined when empty
419
+ * @param {string} [location] Source location of the dosai record (`path` or `path#line`)
420
+ * @returns {string|undefined} Canonical component purl, the input purl when the BOM has no
421
+ * component of that package, or undefined when empty or ambiguous
144
422
  */
145
- export function resolveDosaiComponentPurl(purl, purlAliasMap) {
423
+ export function resolveDosaiComponentPurl(purl, purlAliasMap, location) {
146
424
  if (!purl) {
147
425
  return undefined;
148
426
  }
149
- return (
150
- purlAliasMap.get(purl) ||
151
- purlAliasMap.get(normalizeDosaiPurlKey(purl)) ||
152
- purl
153
- );
427
+ const exact = purlAliasMap?.get(purl);
428
+ if (exact) {
429
+ return exact;
430
+ }
431
+ const parts = dosaiPurlParts(purl);
432
+ const candidates = parts && purlAliasMap?.candidatesByKey?.get(parts.key);
433
+ if (!candidates?.length) {
434
+ return purl;
435
+ }
436
+ if (parts.version) {
437
+ const sameVersion = candidates.filter(
438
+ (candidate) =>
439
+ candidate.version?.toLowerCase() === parts.version.toLowerCase(),
440
+ );
441
+ if (sameVersion.length === 1) {
442
+ return sameVersion[0].purl;
443
+ }
444
+ }
445
+ if (candidates.length === 1) {
446
+ return candidates[0].purl;
447
+ }
448
+ return candidateForLocation(candidates, location, purlAliasMap);
154
449
  }
@@ -4,8 +4,36 @@ import { checksumFile } from "../core/fs.js";
4
4
  import { mapWithConcurrency } from "../core/parallel.js";
5
5
  import { resolvePackageForFile } from "./osPackageResolver.js";
6
6
  import { applyPurl, tryBuildPurl } from "./purl.js";
7
+ import {
8
+ cryptoLibrariesByPath,
9
+ normalizeTracedCryptoComponents,
10
+ } from "./tracedCrypto.js";
7
11
  import { executeAndTrace, groupHttpEntriesToServices } from "./traceRunner.js";
8
12
 
13
+ /**
14
+ * Converts a traced library path into a purl subpath. Windows paths
15
+ * ("C:\Windows\System32\bcrypt.dll", "\\server\share\x.dll") use forward
16
+ * slashes and drop the drive or UNC prefix, which a purl subpath cannot carry;
17
+ * the full path stays in the cdx:dynamic:filePath property.
18
+ *
19
+ * @param {string} libPath - Absolute path of the loaded library
20
+ * @returns {string|null} Subpath without leading separators, or null
21
+ */
22
+ export function librarySubpath(libPath) {
23
+ if (!libPath) {
24
+ return null;
25
+ }
26
+ let subpath = libPath;
27
+ const isDrivePath = /^[A-Za-z]:[\\/]/.test(subpath);
28
+ if (isDrivePath || subpath.startsWith("\\\\")) {
29
+ subpath = subpath.replaceAll("\\", "/");
30
+ if (isDrivePath) {
31
+ subpath = subpath.substring(3);
32
+ }
33
+ }
34
+ return subpath.replace(/^\/+/, "") || null;
35
+ }
36
+
9
37
  /**
10
38
  * Builds a flat list of CycloneDX component objects and services by executing
11
39
  * the given command and inspecting which shared libraries it loads at runtime
@@ -32,7 +60,12 @@ export async function buildDynamicComponents(
32
60
  traceOptions = {},
33
61
  ) {
34
62
  const result = await executeAndTrace(commandStr, workingDir, traceOptions);
35
- const libPaths = result.libPaths || [];
63
+ // A crypto library is a loaded library too, even when library tracing
64
+ // missed it.
65
+ const cryptoLibraries = cryptoLibrariesByPath(result.cryptoEntries);
66
+ const libPaths = Array.from(
67
+ new Set([...(result.libPaths || []), ...cryptoLibraries.keys()]),
68
+ );
36
69
  const httpAccessEntries = result.httpAccessEntries || [];
37
70
  const egressEntries = result.egressEntries || [];
38
71
  const cryptoComponents = result.cryptoComponents || [];
@@ -50,7 +83,10 @@ export async function buildDynamicComponents(
50
83
  }
51
84
 
52
85
  const pkgInfo = resolvePackageForFile(libPath);
53
- let name = basename(libPath);
86
+ const cryptoLibrary = cryptoLibraries.get(libPath);
87
+ // The subpath uses forward slashes, so this also names a Windows DLL
88
+ // correctly when the BOM is built on another host.
89
+ let name = basename(librarySubpath(libPath) || libPath);
54
90
  let version = "";
55
91
  let purlStr = "";
56
92
  let confidence = 0.5;
@@ -71,12 +107,16 @@ export async function buildDynamicComponents(
71
107
  qualifiers: pkgInfo.arch ? { arch: pkgInfo.arch } : null,
72
108
  });
73
109
  } else {
110
+ // A crypto library's version is its file version (Windows reports one
111
+ // for each DLL it classifies).
112
+ version = cryptoLibrary?.version || "";
74
113
  // Fall back to a generic purl carrying the file path as a subpath. It was
75
114
  // previously a `path` qualifier, which generic does not permit.
76
115
  purlStr = tryBuildPurl({
77
116
  type: "generic",
78
117
  name: name,
79
- subpath: libPath ? libPath.replace(/^\/+/, "") : null,
118
+ version: version || null,
119
+ subpath: librarySubpath(libPath),
80
120
  });
81
121
  }
82
122
 
@@ -117,6 +157,12 @@ export async function buildDynamicComponents(
117
157
  if (version) {
118
158
  component.version = version;
119
159
  }
160
+ if (cryptoLibrary) {
161
+ component.properties.push({
162
+ name: "cdx:dynamic:cryptoLibrary",
163
+ value: cryptoLibrary.name,
164
+ });
165
+ }
120
166
  // applyPurl sets a unique fallback bom-ref when no valid purl exists, so a
121
167
  // library that cannot be expressed as a purl still participates correctly in
122
168
  // the dependency graph.
@@ -126,7 +172,7 @@ export async function buildDynamicComponents(
126
172
  }
127
173
 
128
174
  // Append cryptographic assets
129
- for (const comp of cryptoComponents) {
175
+ for (const comp of normalizeTracedCryptoComponents(cryptoComponents)) {
130
176
  components.push(comp);
131
177
  }
132
178
 
@@ -136,7 +182,7 @@ export async function buildDynamicComponents(
136
182
  const entry = servicesMap[serviceName];
137
183
  return {
138
184
  name: serviceName,
139
- bomRef: `urn:service:dynamic:${serviceName}`,
185
+ "bom-ref": `urn:service:dynamic:${serviceName}`,
140
186
  endpoints: Array.from(entry.endpoints).sort(),
141
187
  properties: entry.properties,
142
188
  };
@@ -154,7 +200,7 @@ export async function buildDynamicComponents(
154
200
  if (!svc) {
155
201
  svc = {
156
202
  name,
157
- bomRef: `urn:service:dynamic:${name}`,
203
+ "bom-ref": `urn:service:dynamic:${name}`,
158
204
  endpoints: [],
159
205
  properties: [],
160
206
  };