@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
@@ -15,6 +15,11 @@ import {
15
15
  safeRmSync,
16
16
  safeWriteSync,
17
17
  } from "../core/fs.js";
18
+ import { hasCppSources } from "../ecosystems/cppEvidence.js";
19
+ import {
20
+ createIncludeAttributor,
21
+ parseCIncludeSlices,
22
+ } from "../ecosystems/cppIncludeResolver.js";
18
23
  import {
19
24
  analyzeGolemProject,
20
25
  collectGolemEvidence,
@@ -39,6 +44,13 @@ import {
39
44
  collectRusiEvidence,
40
45
  isRusiRustLanguage,
41
46
  } from "../ecosystems/rusi.js";
47
+ import {
48
+ analyzeScalaProject,
49
+ collectScalasemApiEndpoints,
50
+ collectScalasemEvidence,
51
+ collectScalasemServices,
52
+ isScalasemLanguage,
53
+ } from "../ecosystems/scalasem.js";
42
54
  import {
43
55
  atomCompileCommandsArgs,
44
56
  buildAtomCommandEnv,
@@ -53,6 +65,7 @@ import {
53
65
  collectDosaiCryptoComponents,
54
66
  findCryptoAlgos,
55
67
  } from "../inventory/cbomutils.js";
68
+ import { cmakeBuildDirCandidates } from "../inventory/cmakeBuildDirs.js";
56
69
  import { collectMvnDependencies } from "../inventory/deps.js";
57
70
  import { mergeServices } from "../inventory/depsUtils.js";
58
71
  import {
@@ -64,6 +77,7 @@ import {
64
77
  collectDosaiServicesFromMethods,
65
78
  createDosaiDataFlowSlice,
66
79
  createDosaiMethodsSlice,
80
+ dosaiRunWasStopped,
67
81
  isDosaiDotnetLanguage,
68
82
  normalizeDosaiServiceMap,
69
83
  persistDosaiSemanticsReport,
@@ -141,6 +155,81 @@ function filterAtomSliceData(sliceData, options = {}) {
141
155
  return filterAtomSlicesByExcludePatterns(sliceData, options.exclude);
142
156
  }
143
157
 
158
+ /**
159
+ * The project type whose evidence a package URL type belongs to. Generic
160
+ * purls are left out because many collectors use that type; they count as C
161
+ * only when the tree holds C or C++ sources.
162
+ */
163
+ const EVIDENCE_LANGUAGE_BY_PURL_TYPE = {
164
+ cargo: "rust",
165
+ cocoapods: "swift",
166
+ composer: "php",
167
+ conan: "c",
168
+ gem: "ruby",
169
+ golang: "go",
170
+ maven: "java",
171
+ npm: "js",
172
+ nuget: "csharp",
173
+ pypi: "python",
174
+ swift: "swift",
175
+ };
176
+
177
+ /**
178
+ * The evidence language of a `cdxgen --evidence` run that names no project
179
+ * type.
180
+ *
181
+ * A directory with a Scala build takes the scalasem path. Otherwise the
182
+ * generated BOM decides: Java when it lists Maven packages, as before, and
183
+ * else the language most of its packages belong to. A BOM with no package in
184
+ * a language the evidence step analyses gives no language, so no analyzer
185
+ * runs over sources it cannot read.
186
+ *
187
+ * @param {string} dirPath Project directory
188
+ * @param {Object} [bomJson] The BOM generated for the directory
189
+ * @param {Object} [options] CLI options, for the exclude patterns
190
+ * @returns {string|undefined} Project type for the evidence phase
191
+ */
192
+ export function defaultEvidenceLanguage(dirPath, bomJson, options = {}) {
193
+ const scalaBuildFiles = [
194
+ "build.sbt",
195
+ "build.sc",
196
+ "build.mill",
197
+ "project.scala",
198
+ ];
199
+ if (
200
+ scalaBuildFiles.some((file) => safeExistsSync(join(dirPath || ".", file)))
201
+ ) {
202
+ return "scala";
203
+ }
204
+ const counts = new Map();
205
+ let cppSources;
206
+ for (const component of bomJson?.components || []) {
207
+ const purl = `${component?.purl || ""}`;
208
+ if (!purl.startsWith("pkg:") || !purl.includes("/")) {
209
+ continue;
210
+ }
211
+ const purlType = purl.slice(4, purl.indexOf("/")).toLowerCase();
212
+ let language = EVIDENCE_LANGUAGE_BY_PURL_TYPE[purlType];
213
+ if (!language && purlType === "generic") {
214
+ cppSources ??= hasCppSources(dirPath || ".", options);
215
+ language = cppSources ? "c" : undefined;
216
+ }
217
+ if (language) {
218
+ counts.set(language, (counts.get(language) || 0) + 1);
219
+ }
220
+ }
221
+ if (counts.has("java")) {
222
+ return "java";
223
+ }
224
+ let chosen;
225
+ for (const [language, count] of counts) {
226
+ if (!chosen || count > counts.get(chosen)) {
227
+ chosen = language;
228
+ }
229
+ }
230
+ return chosen;
231
+ }
232
+
144
233
  /**
145
234
  * Build the options for the evinse phase (prepareDB, analyzeProject, and
146
235
  * createEvinseFile) from the cdxgen CLI options.
@@ -178,6 +267,18 @@ export function buildEvinseOptions(options, args, input) {
178
267
  profile: options.profile,
179
268
  jsonPretty: options.jsonPretty,
180
269
  exclude: options.exclude,
270
+ // Forwarded so a failed rusi/golem run can claim the --fail-on-error exit
271
+ // instead of degrading the reachability evidence in silence.
272
+ failOnError: options.failOnError,
273
+ // The scala analyzer reads these: a command override, the test-source
274
+ // switch and the install policy that decides whether builds may run.
275
+ scalasemCommand: options.scalasemCommand,
276
+ scalasemIncludeTests: options.scalasemIncludeTests,
277
+ scalasem: options.scalasem,
278
+ installDeps: options.installDeps,
279
+ requiredOnly: options.requiredOnly,
280
+ // --deep publishes the namespaces the Scala classpath join reads.
281
+ scalasemNamespaces: options.deep,
181
282
  // Absolute path of the deps slice createCsharpBom writes for --deep runs,
182
283
  // so analyzeProject can reuse it instead of rerunning dosai methods.
183
284
  depsSlicesFile: options.depsSlicesFile
@@ -239,7 +340,7 @@ export async function prepareDB(options) {
239
340
  await catalogMavenDeps(dirPath, purlsJars, Namespaces, options);
240
341
  }
241
342
  if (gradleFiles?.length) {
242
- await catalogGradleDeps(dirPath, purlsJars, Namespaces);
343
+ await catalogGradleDeps(dirPath, purlsJars, Namespaces, options);
243
344
  }
244
345
  }
245
346
  for (const purl of Object.keys(purlsToSlice)) {
@@ -248,11 +349,42 @@ export async function prepareDB(options) {
248
349
  return { sequelize, Namespaces, Usages, DataFlows };
249
350
  }
250
351
 
352
+ /**
353
+ * Read the jar namespace map cdxgen writes next to the BOM it generates
354
+ * (`<bom>.map`). The map for the BOM passed as `--input` is preferred; a
355
+ * `bom.json.map` in the project directory is the fallback.
356
+ *
357
+ * @param {string} dirPath Project directory scanned by evinse
358
+ * @param {Object} [options] CLI options, for `input`
359
+ * @returns {Object|undefined} The mapping, when a non-empty one exists.
360
+ */
361
+ export function readNamespaceMap(dirPath, options = {}) {
362
+ const candidates = [
363
+ options?.input ? `${options.input}.map` : undefined,
364
+ join(dirPath, "bom.json.map"),
365
+ ];
366
+ for (const mapFile of candidates) {
367
+ if (!mapFile || !safeExistsSync(mapFile)) {
368
+ continue;
369
+ }
370
+ try {
371
+ const mapData = JSON.parse(fs.readFileSync(mapFile, "utf-8"));
372
+ if (mapData && Object.keys(mapData).length) {
373
+ return mapData;
374
+ }
375
+ } catch (_err) {
376
+ // An unreadable map is the same as no map.
377
+ }
378
+ }
379
+ return undefined;
380
+ }
381
+
251
382
  /**
252
383
  * Collect Maven jar namespace mappings for the purls found in the SBOM.
253
384
  *
254
- * If a previously generated `bom.json.map` is present in `dirPath` it is reused;
255
- * otherwise the maven command is invoked to collect the jar dependencies. Each
385
+ * If cdxgen left a namespace map next to the BOM (see {@link readNamespaceMap})
386
+ * it is reused; otherwise the maven command is invoked to collect the jar
387
+ * dependencies. Each
256
388
  * resolved purl is recorded in the `purlsJars` map and persisted in the
257
389
  * `Namespaces` model.
258
390
  *
@@ -268,19 +400,7 @@ export async function catalogMavenDeps(
268
400
  Namespaces,
269
401
  options = {},
270
402
  ) {
271
- let jarNSMapping;
272
- if (safeExistsSync(join(dirPath, "bom.json.map"))) {
273
- try {
274
- const mapData = JSON.parse(
275
- fs.readFileSync(join(dirPath, "bom.json.map"), "utf-8"),
276
- );
277
- if (mapData && Object.keys(mapData).length) {
278
- jarNSMapping = mapData;
279
- }
280
- } catch (_err) {
281
- // ignore
282
- }
283
- }
403
+ let jarNSMapping = readNamespaceMap(dirPath, options);
284
404
  if (!jarNSMapping) {
285
405
  console.log("About to collect jar dependencies for the path", dirPath);
286
406
  const mavenCmd = getMavenCommand(dirPath, dirPath);
@@ -324,18 +444,28 @@ export async function catalogMavenDeps(
324
444
  * @param {Object} Namespaces Sequelize-like Namespaces model used to persist pom and namespaces
325
445
  * @returns {Promise<void>}
326
446
  */
327
- export async function catalogGradleDeps(dirPath, purlsJars, Namespaces) {
328
- console.log(
329
- "About to collect jar dependencies from the gradle cache. This would take a while ...",
330
- );
331
- const gradleCmd = getGradleCommand(dirPath, dirPath);
332
- // collect all jars including from the cache if data-flow mode is enabled
333
- const jarNSMapping = await collectGradleDependencies(
334
- gradleCmd,
335
- dirPath,
336
- false,
337
- true,
338
- );
447
+ export async function catalogGradleDeps(
448
+ dirPath,
449
+ purlsJars,
450
+ Namespaces,
451
+ options = {},
452
+ ) {
453
+ // The map cdxgen wrote next to the BOM already holds the namespaces; the
454
+ // Gradle cache is walked only without it.
455
+ let jarNSMapping = readNamespaceMap(dirPath, options);
456
+ if (!jarNSMapping) {
457
+ console.log(
458
+ "About to collect jar dependencies from the gradle cache. This would take a while ...",
459
+ );
460
+ const gradleCmd = getGradleCommand(dirPath, dirPath);
461
+ // collect all jars including from the cache if data-flow mode is enabled
462
+ jarNSMapping = await collectGradleDependencies(
463
+ gradleCmd,
464
+ dirPath,
465
+ false,
466
+ true,
467
+ );
468
+ }
339
469
  if (jarNSMapping) {
340
470
  for (const purl of Object.keys(jarNSMapping)) {
341
471
  purlsJars[purl] = jarNSMapping[purl].jarFile;
@@ -515,7 +645,9 @@ export async function createSlice(
515
645
  resolve(slicesFile),
516
646
  options,
517
647
  );
518
- if (!sliceResult) {
648
+ // A stopped run has already said why; it says nothing about whether the
649
+ // project is supported (issue 4438).
650
+ if (!sliceResult && !dosaiRunWasStopped(resolve(slicesFile))) {
519
651
  console.warn(
520
652
  `Unable to generate ${sliceType} slice using dosai. Check if this is a supported .NET project.`,
521
653
  );
@@ -552,15 +684,9 @@ export async function createSlice(
552
684
  "Use an official cdxgen container image to improve the precision of endpoints detection (for SaaSBOM).",
553
685
  );
554
686
  }
555
- if (["ruby", "scala"].includes(language)) {
687
+ if (language === "ruby") {
556
688
  args.push("--remove-atom");
557
689
  }
558
- if (["scala"].includes(language)) {
559
- semanticsSlicesFile = join(
560
- sliceOutputDir,
561
- basename(options.semanticsSlicesFile || "semantics.slices.json"),
562
- );
563
- }
564
690
  }
565
691
  args = args.concat([
566
692
  "-l",
@@ -632,15 +758,6 @@ export async function createSlice(
632
758
  `openapi spec file "${join(filePath, openapiSpecFile)}" was not generated successfully. Check if atom-tools pypi package is installed and available in PATH.`,
633
759
  );
634
760
  }
635
- if (
636
- ["scala"].includes(language) &&
637
- sliceType === "usages" &&
638
- !safeExistsSync(semanticsSlicesFile)
639
- ) {
640
- console.log(
641
- `Semantics slices file "${semanticsSlicesFile}" was not generated successfully. Try running atom cli in Java mode.`,
642
- );
643
- }
644
761
  return {
645
762
  tempDir: sliceOutputDir,
646
763
  tempDirOwned,
@@ -769,8 +886,8 @@ function usableSlicesFile(slicesFile) {
769
886
  * describe the same project directory and must not predate the input SBOM:
770
887
  * `cdxgen --evidence` writes a fresh SBOM before evinse runs, so an older
771
888
  * slice was made from an earlier state of the sources and is regenerated.
772
- * Scala slices cannot be regenerated by evinse and are reused whenever their
773
- * shape matches.
889
+ * Scala reports are handled by the scalasem analyzer, which applies its own
890
+ * rules.
774
891
  *
775
892
  * @param {string} language Project language
776
893
  * @param {string} slicesFile Candidate semantics slices file
@@ -797,10 +914,7 @@ export function loadReusableSemanticsSlice(
797
914
  return undefined;
798
915
  }
799
916
  if (!PROJECT_TYPE_ALIASES.swift.includes(language)) {
800
- // Scala semantics slices are keyed by .scala source file names
801
- return Object.keys(parsed).some((key) => key.endsWith(".scala"))
802
- ? parsed
803
- : undefined;
917
+ return undefined;
804
918
  }
805
919
  if (!(parsed.fileIndexes || parsed.buildSymbols || parsed.moduleInfos)) {
806
920
  return undefined;
@@ -818,6 +932,217 @@ export function loadReusableSemanticsSlice(
818
932
  return parsed;
819
933
  }
820
934
 
935
+ /**
936
+ * The Scala evidence path: scalasem only, never atom. The branch returns
937
+ * early for every entry point (evinse, --evidence, cbom, saasbom) and every
938
+ * Scala project type, so no usages, reachables, data-flow or atom-run
939
+ * scalasem happens here. Slices the user passes are still read: a version 1
940
+ * semantics slice keeps its usedTypes join, usages and reachables slices
941
+ * keep their own evidence, and an OpenAPI spec keeps its services.
942
+ *
943
+ * @param {string} language Project language, normalized to `scala`.
944
+ * @param {string} dirPath Project directory.
945
+ * @param {Object} options Command line options.
946
+ * @param {Object} bomJson Parsed input SBOM.
947
+ * @param {Object[]} components Components of the input SBOM.
948
+ * @param {Object} purlLocationMap Occurrences seeded from the input SBOM.
949
+ * @param {Object} purlImportsMap Imports seeded from the input SBOM.
950
+ * @param {Object} dbObjMap The evinse database objects.
951
+ * @returns {Promise<Object>} Slice artefacts for `createEvinseFile`.
952
+ */
953
+ async function analyzeScalaEvidenceProject(
954
+ language,
955
+ dirPath,
956
+ options,
957
+ bomJson,
958
+ components,
959
+ purlLocationMap,
960
+ purlImportsMap,
961
+ dbObjMap,
962
+ ) {
963
+ let servicesMap = {};
964
+ const dataFlowFrames = {};
965
+ let componentPropertiesMap = {};
966
+ let metadataProperties = [];
967
+ let cryptoComponents = [];
968
+ const cryptoGeneratePurls = {};
969
+ const mergeLocations = (locationMap) => {
970
+ for (const [purl, locations] of Object.entries(locationMap || {})) {
971
+ purlLocationMap[purl] ??= new Set();
972
+ for (const location of locations) {
973
+ purlLocationMap[purl].add(location);
974
+ }
975
+ }
976
+ };
977
+ const mergeFrames = (framesMap) => {
978
+ for (const [purl, stacks] of Object.entries(framesMap || {})) {
979
+ dataFlowFrames[purl] = [...(dataFlowFrames[purl] || []), ...stacks];
980
+ }
981
+ };
982
+ const mergeProvides = (providesMap) => {
983
+ for (const [purl, refs] of Object.entries(providesMap || {})) {
984
+ cryptoGeneratePurls[purl] ??= new Set();
985
+ for (const ref of refs || []) {
986
+ cryptoGeneratePurls[purl].add(ref);
987
+ }
988
+ }
989
+ };
990
+ const analysis = analyzeScalaProject(dirPath, options);
991
+ if (analysis?.report) {
992
+ const evidence = await collectScalasemEvidence(
993
+ analysis.report,
994
+ [bomJson.metadata?.component, ...components].filter(Boolean),
995
+ { ...options, scalaNamespaceMap: readNamespaceMap(dirPath, options) },
996
+ );
997
+ mergeLocations(evidence.purlLocationMap);
998
+ mergeFrames(evidence.dataFlowFrames);
999
+ mergeProvides(evidence.cryptoGeneratePurls);
1000
+ componentPropertiesMap = evidence.componentPropertiesMap;
1001
+ cryptoComponents = evidence.cryptoComponents;
1002
+ metadataProperties = evidence.metadataProperties;
1003
+ servicesMap = collectScalasemServices(analysis.report, servicesMap);
1004
+ servicesMap = collectScalasemApiEndpoints(analysis.report, servicesMap);
1005
+ }
1006
+ if (analysis?.metadataProperties?.length) {
1007
+ for (const property of analysis.metadataProperties) {
1008
+ if (
1009
+ !metadataProperties.some(
1010
+ (known) =>
1011
+ known.name === property.name && known.value === property.value,
1012
+ )
1013
+ ) {
1014
+ metadataProperties.push(property);
1015
+ }
1016
+ }
1017
+ }
1018
+ // A version 1 semantics slice still goes through the usedTypes join, merged
1019
+ // with the report evidence rather than replacing it.
1020
+ if (analysis?.v1Slice) {
1021
+ mergeLocations(
1022
+ findPurlLocations(components, analysis.v1Slice).purlLocationMap,
1023
+ );
1024
+ }
1025
+ // Usages and reachables slices the user made, with atom or otherwise, are
1026
+ // read as they are for any other language; none is ever generated here.
1027
+ let usagesSlicesFile;
1028
+ let reachablesSlicesFile;
1029
+ if (
1030
+ (options.withReachables || options.includeCrypto) &&
1031
+ usableSlicesFile(options.reachablesSlicesFile)
1032
+ ) {
1033
+ reachablesSlicesFile = options.reachablesSlicesFile;
1034
+ const reachables = collectReachableFrames(
1035
+ language,
1036
+ filterAtomSliceData(readReachablesSlices(reachablesSlicesFile), options),
1037
+ );
1038
+ mergeFrames(reachables.dataFlowFrames);
1039
+ mergeProvides(reachables.cryptoGeneratePurls);
1040
+ const knownRefs = new Set(
1041
+ cryptoComponents.map((asset) => asset["bom-ref"]),
1042
+ );
1043
+ cryptoComponents = cryptoComponents.concat(
1044
+ (reachables.cryptoComponents || []).filter(
1045
+ (asset) => !knownRefs.has(asset["bom-ref"]),
1046
+ ),
1047
+ );
1048
+ }
1049
+ let userDefinedTypesMap = {};
1050
+ if (usableSlicesFile(options.usagesSlicesFile)) {
1051
+ usagesSlicesFile = options.usagesSlicesFile;
1052
+ const usageSlice = filterAtomSliceData(
1053
+ readSlicesFile(usagesSlicesFile, "usages"),
1054
+ options,
1055
+ );
1056
+ if (usageSlice && Object.keys(usageSlice).length) {
1057
+ const retMap = await parseObjectSlices(
1058
+ language,
1059
+ usageSlice,
1060
+ dbObjMap,
1061
+ servicesMap,
1062
+ purlLocationMap,
1063
+ purlImportsMap,
1064
+ );
1065
+ servicesMap = retMap.servicesMap || servicesMap;
1066
+ userDefinedTypesMap = retMap.userDefinedTypesMap || {};
1067
+ }
1068
+ }
1069
+ // An OpenAPI spec the user has (atom-tools' conversion of this report, or
1070
+ // the project's own) adds its routes to the report's: a route both name is
1071
+ // one service.
1072
+ const openapiSpecFile = options.openapiSpecFile
1073
+ ? resolve(dirPath, options.openapiSpecFile)
1074
+ : undefined;
1075
+ if (openapiSpecFile && safeExistsSync(openapiSpecFile)) {
1076
+ const openapiServices = {};
1077
+ detectServicesFromOpenAPI(language, openapiSpecFile, openapiServices);
1078
+ mergeServiceDefinitions(servicesMap, openapiServices);
1079
+ }
1080
+ // A BOM without a project component still needs somewhere for the run's
1081
+ // metadata properties to live; a nameless entry would explain nothing.
1082
+ let workspaceComponent;
1083
+ if (!bomJson.metadata?.component) {
1084
+ workspaceComponent = {
1085
+ type: "application",
1086
+ name: basename(resolve(dirPath)),
1087
+ };
1088
+ }
1089
+ return {
1090
+ purlLocationMap,
1091
+ servicesMap,
1092
+ dataFlowFrames,
1093
+ componentPropertiesMap,
1094
+ metadataProperties,
1095
+ cryptoComponents,
1096
+ cryptoGeneratePurls,
1097
+ usagesSlicesFile,
1098
+ reachablesSlicesFile,
1099
+ semanticsSlicesFile: analysis?.reportFile,
1100
+ userDefinedTypesMap,
1101
+ workspaceComponent,
1102
+ };
1103
+ }
1104
+
1105
+ /**
1106
+ * Merge service definitions into a services map: endpoints join, and a
1107
+ * property another source already carries is not added again, whatever the
1108
+ * case of an HTTP method.
1109
+ *
1110
+ * @param {Object} target Services map that receives the definitions.
1111
+ * @param {Object} source Services map to merge in.
1112
+ * @returns {Object} The target map.
1113
+ */
1114
+ export function mergeServiceDefinitions(target, source) {
1115
+ const sameProperty = (left, right) =>
1116
+ left.name === right.name &&
1117
+ (left.name === "cdx:service:httpMethod"
1118
+ ? String(left.value).toUpperCase() === String(right.value).toUpperCase()
1119
+ : left.value === right.value);
1120
+ for (const [name, definition] of Object.entries(source || {})) {
1121
+ const existing = target[name];
1122
+ if (!existing) {
1123
+ target[name] = definition;
1124
+ continue;
1125
+ }
1126
+ existing.endpoints ??= new Set();
1127
+ for (const endpoint of definition.endpoints || []) {
1128
+ existing.endpoints.add(endpoint);
1129
+ }
1130
+ existing.properties ??= [];
1131
+ for (const property of definition.properties || []) {
1132
+ if (!existing.properties.some((known) => sameProperty(known, property))) {
1133
+ existing.properties.push(property);
1134
+ }
1135
+ }
1136
+ if (definition.authenticated) {
1137
+ existing.authenticated = true;
1138
+ }
1139
+ if (definition.xTrustBoundary) {
1140
+ existing.xTrustBoundary = true;
1141
+ }
1142
+ }
1143
+ return target;
1144
+ }
1145
+
821
1146
  /**
822
1147
  * Function to analyze the project
823
1148
  *
@@ -827,7 +1152,12 @@ export function loadReusableSemanticsSlice(
827
1152
  export async function analyzeProject(dbObjMap, options) {
828
1153
  const dirPath = options._[0] || ".";
829
1154
  const languages = options.language;
830
- const language = Array.isArray(languages) ? languages[0] : languages;
1155
+ let language = Array.isArray(languages) ? languages[0] : languages;
1156
+ // cdxgen --evidence forwards the raw project type, so -t sbt, -t mill and
1157
+ // -t scala3 must be normalized once to behave like -l scala everywhere.
1158
+ if (PROJECT_TYPE_ALIASES.scala.includes(language)) {
1159
+ language = "scala";
1160
+ }
831
1161
  let usageSlice;
832
1162
  let dataFlowSlice;
833
1163
  let reachablesSlice;
@@ -981,6 +1311,19 @@ export async function analyzeProject(dbObjMap, options) {
981
1311
  };
982
1312
  }
983
1313
 
1314
+ if (isScalasemLanguage(language)) {
1315
+ return analyzeScalaEvidenceProject(
1316
+ language,
1317
+ dirPath,
1318
+ options,
1319
+ bomJson,
1320
+ components,
1321
+ purlLocationMap,
1322
+ purlImportsMap,
1323
+ dbObjMap,
1324
+ );
1325
+ }
1326
+
984
1327
  if (isDosaiDotnetLanguage(language)) {
985
1328
  if (options.profile === "research") {
986
1329
  options.withDataFlow = true;
@@ -1011,11 +1354,17 @@ export async function analyzeProject(dbObjMap, options) {
1011
1354
  }
1012
1355
  }
1013
1356
  if (usageSlice && Object.keys(usageSlice).length) {
1014
- const dosaiEvidence = collectDosaiPurlEvidence(usageSlice, components);
1357
+ // dosai locations are relative to the analyzed directory; a package the
1358
+ // BOM holds in several versions is matched by the project of the file.
1359
+ const dosaiEvidence = collectDosaiPurlEvidence(usageSlice, components, {
1360
+ srcPath: resolve(dirPath),
1361
+ });
1015
1362
  // Confidence/evidence facts ride along as properties so an unbuilt tree (Low
1016
1363
  // confidence, unresolved evidence) stays distinguishable from an only-imported
1017
1364
  // package in the emitted BOM.
1018
- applyDosaiReachabilityEvidence(usageSlice, components);
1365
+ applyDosaiReachabilityEvidence(usageSlice, components, {
1366
+ srcPath: resolve(dirPath),
1367
+ });
1019
1368
  for (const [purl, locations] of Object.entries(
1020
1369
  dosaiEvidence.purlLocationMap,
1021
1370
  )) {
@@ -1049,7 +1398,9 @@ export async function analyzeProject(dbObjMap, options) {
1049
1398
  }
1050
1399
  }
1051
1400
  if (dataFlowSlice && Object.keys(dataFlowSlice).length) {
1052
- dataFlowFrames = collectDosaiDataFlowFrames(dataFlowSlice, components);
1401
+ dataFlowFrames = collectDosaiDataFlowFrames(dataFlowSlice, components, {
1402
+ srcPath: resolve(dirPath),
1403
+ });
1053
1404
  }
1054
1405
  }
1055
1406
  if (options.includeCrypto) {
@@ -1145,12 +1496,8 @@ export async function analyzeProject(dbObjMap, options) {
1145
1496
  }
1146
1497
  }
1147
1498
  // Support for semantics slicing
1148
- if (
1149
- (PROJECT_TYPE_ALIASES.swift.includes(language) ||
1150
- PROJECT_TYPE_ALIASES.scala.includes(language)) &&
1151
- components.length
1152
- ) {
1153
- // Reuse existing semantics slices for swift and scala
1499
+ if (PROJECT_TYPE_ALIASES.swift.includes(language) && components.length) {
1500
+ // Reuse existing semantics slices for swift
1154
1501
  const reusableSlice = loadReusableSemanticsSlice(
1155
1502
  language,
1156
1503
  options.semanticsSlicesFile,
@@ -1160,7 +1507,7 @@ export async function analyzeProject(dbObjMap, options) {
1160
1507
  if (reusableSlice) {
1161
1508
  semanticsSlice = reusableSlice;
1162
1509
  semanticsSlicesFile = options.semanticsSlicesFile;
1163
- } else if (PROJECT_TYPE_ALIASES.swift.includes(language)) {
1510
+ } else {
1164
1511
  // Generate our own slices for swift
1165
1512
  retMap = await createSlice(language, dirPath, "semantics", options);
1166
1513
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
@@ -1169,6 +1516,28 @@ export async function analyzeProject(dbObjMap, options) {
1169
1516
  }
1170
1517
  }
1171
1518
  }
1519
+ let cIncludeLocations = new Map();
1520
+ // A C/C++ include names the file it resolved to and the functions the
1521
+ // including file calls from it (atom 4 and later): those functions are the
1522
+ // usages of the component that provides the file
1523
+ if (
1524
+ ["c", "cpp", "c++"].includes(language) &&
1525
+ usageSlice &&
1526
+ Object.keys(usageSlice).length
1527
+ ) {
1528
+ const attributed = attributeCIncludes(
1529
+ usageSlice,
1530
+ components,
1531
+ dirPath,
1532
+ options,
1533
+ );
1534
+ for (const [purl, names] of attributed.symbols) {
1535
+ purlImportsMap[purl] = Array.from(
1536
+ new Set([...(purlImportsMap[purl] || []), ...names]),
1537
+ );
1538
+ }
1539
+ cIncludeLocations = attributed.locations;
1540
+ }
1172
1541
  // Parse usage slices
1173
1542
  if (usageSlice && Object.keys(usageSlice).length) {
1174
1543
  const retMap = await parseObjectSlices(
@@ -1184,7 +1553,14 @@ export async function analyzeProject(dbObjMap, options) {
1184
1553
  servicesMap = retMap.servicesMap;
1185
1554
  userDefinedTypesMap = retMap.userDefinedTypesMap;
1186
1555
  }
1187
- // Parse the semantics slices for swift and scala
1556
+ // A component whose calls the slices do not show (code inside the project,
1557
+ // such as a vendored library) is still used where its header is included
1558
+ for (const [purl, includeLocations] of cIncludeLocations) {
1559
+ if (!purlLocationMap[purl]?.size) {
1560
+ purlLocationMap[purl] = new Set(includeLocations);
1561
+ }
1562
+ }
1563
+ // Parse the semantics slices for swift
1188
1564
  if (
1189
1565
  semanticsSlice &&
1190
1566
  Object.keys(semanticsSlice).length &&
@@ -1192,7 +1568,25 @@ export async function analyzeProject(dbObjMap, options) {
1192
1568
  ) {
1193
1569
  // Identify the purl locations
1194
1570
  const retMap = parseSemanticSlices(language, components, semanticsSlice);
1195
- purlLocationMap = retMap.purlLocationMap;
1571
+ // The usages step may already have locations for a purl; the semantics
1572
+ // pass adds its own rather than replacing them.
1573
+ for (const [apurl, locations] of Object.entries(
1574
+ retMap.purlLocationMap || {},
1575
+ )) {
1576
+ purlLocationMap[apurl] ??= new Set();
1577
+ for (const alocation of locations) {
1578
+ purlLocationMap[apurl].add(alocation);
1579
+ }
1580
+ }
1581
+ }
1582
+ // A user-supplied OpenAPI spec must yield services even when the usages
1583
+ // slice is empty.
1584
+ if (
1585
+ openapiSpecFile &&
1586
+ safeExistsSync(openapiSpecFile) &&
1587
+ !Object.keys(servicesMap).length
1588
+ ) {
1589
+ detectServicesFromOpenAPI(language, openapiSpecFile, servicesMap);
1196
1590
  }
1197
1591
  if (options.withDataFlow) {
1198
1592
  if (
@@ -1261,6 +1655,125 @@ export async function analyzeProject(dbObjMap, options) {
1261
1655
  * @param {string} [openapiSpecFile=undefined] Optional path to an OpenAPI spec used for service detection
1262
1656
  * @returns {Promise<Object>} Map with `purlLocationMap`, `servicesMap`, and `userDefinedTypesMap`
1263
1657
  */
1658
+ /**
1659
+ * Attribute the include slices of a C/C++ usages report to the BOM
1660
+ * components that provide the files they resolved to: each header's imported
1661
+ * symbols (the functions the including file calls that it declares), and the
1662
+ * include directive's location. Reports from atom releases that do not name
1663
+ * the resolved file attribute nothing.
1664
+ *
1665
+ * @param {Object} usageSlice Parsed usages report
1666
+ * @param {Object[]} components Components of the input BOM
1667
+ * @param {string} dirPath Source directory
1668
+ * @param {Object} options CLI options
1669
+ * @returns {{symbols: Map<string, Set<string>>, locations: Map<string, Set<string>>}}
1670
+ * Imported symbols and include locations (`file#line`) by purl
1671
+ */
1672
+ export function attributeCIncludes(
1673
+ usageSlice,
1674
+ components,
1675
+ dirPath,
1676
+ options = {},
1677
+ ) {
1678
+ const symbols = new Map();
1679
+ const locations = new Map();
1680
+ const includes = [...parseCIncludeSlices(usageSlice)].filter(
1681
+ ([, e]) => e.paths.size && e.symbols.size,
1682
+ );
1683
+ if (!includes.length || !components?.length) {
1684
+ return { symbols, locations };
1685
+ }
1686
+ const attribute = createIncludeAttributor({
1687
+ src: dirPath,
1688
+ components,
1689
+ buildDirs: cmakeBuildDirCandidates(dirPath, options),
1690
+ });
1691
+ const withPurl = components.filter((c) => c?.purl);
1692
+ const byName = (name, purlType) =>
1693
+ withPurl.find(
1694
+ (c) =>
1695
+ c.name?.toLowerCase() === `${name}`.toLowerCase() &&
1696
+ (!purlType || c.purl.startsWith(`pkg:${purlType}/`)),
1697
+ );
1698
+ const purlByHeader = new Map();
1699
+ for (const [header, entry] of includes) {
1700
+ const target = attribute(entry.paths);
1701
+ let component;
1702
+ if (target?.kind === "component") {
1703
+ component = target.component;
1704
+ } else if (target?.kind === "os") {
1705
+ component =
1706
+ withPurl.find((c) => c.purl === target.pkgInfo.purl) ||
1707
+ byName(target.pkgInfo.name);
1708
+ } else if (target?.kind === "vcpkg") {
1709
+ component = byName(target.port);
1710
+ } else if (target?.kind === "conan") {
1711
+ component = byName(target.name, "conan") || byName(target.name);
1712
+ }
1713
+ if (!component?.purl) {
1714
+ continue;
1715
+ }
1716
+ purlByHeader.set(header, component.purl);
1717
+ const known = symbols.get(component.purl) || new Set();
1718
+ for (const symbol of entry.symbols) {
1719
+ known.add(symbol);
1720
+ }
1721
+ symbols.set(component.purl, known);
1722
+ }
1723
+ // the directives that include an attributed header and use it
1724
+ for (const slice of usageSlice?.objectSlices || []) {
1725
+ const purl = purlByHeader.get(slice?.fullName);
1726
+ if (
1727
+ !purl ||
1728
+ !`${slice.code || ""}`.startsWith("#include") ||
1729
+ !slice.fileName ||
1730
+ !slice.importedSymbols?.length
1731
+ ) {
1732
+ continue;
1733
+ }
1734
+ const known = locations.get(purl) || new Set();
1735
+ known.add(
1736
+ slice.lineNumber
1737
+ ? `${slice.fileName}#${slice.lineNumber}`
1738
+ : slice.fileName,
1739
+ );
1740
+ locations.set(purl, known);
1741
+ }
1742
+ return { symbols, locations };
1743
+ }
1744
+
1745
+ /**
1746
+ * Seed the C/C++ imports from the include slices of a usages report (see
1747
+ * `attributeCIncludes`).
1748
+ *
1749
+ * @param {Object} usageSlice Parsed usages report
1750
+ * @param {Object[]} components Components of the input BOM
1751
+ * @param {string} dirPath Source directory
1752
+ * @param {Object} purlImportsMap Imports by purl, extended in place
1753
+ * @param {Object} options CLI options
1754
+ * @returns {Object} purlImportsMap
1755
+ */
1756
+ export function seedCImportsFromIncludes(
1757
+ usageSlice,
1758
+ components,
1759
+ dirPath,
1760
+ purlImportsMap = {},
1761
+ options = {},
1762
+ ) {
1763
+ const { symbols } = attributeCIncludes(
1764
+ usageSlice,
1765
+ components,
1766
+ dirPath,
1767
+ options,
1768
+ );
1769
+ for (const [purl, names] of symbols) {
1770
+ purlImportsMap[purl] = Array.from(
1771
+ new Set([...(purlImportsMap[purl] || []), ...names]),
1772
+ );
1773
+ }
1774
+ return purlImportsMap;
1775
+ }
1776
+
1264
1777
  export async function parseObjectSlices(
1265
1778
  language,
1266
1779
  usageSlice,
@@ -1581,11 +2094,7 @@ export async function parseSliceUsages(
1581
2094
  * @param {Object} semanticsSlice Semantic slice data
1582
2095
  * @returns {Object} Parsed metadata
1583
2096
  */
1584
- export function parseSemanticSlices(language, components, semanticsSlice) {
1585
- // For scala, use the dedicated scalasem module.
1586
- if (language === "scala") {
1587
- return findPurlLocations(components, semanticsSlice);
1588
- }
2097
+ export function parseSemanticSlices(_language, components, semanticsSlice) {
1589
2098
  if (!components) {
1590
2099
  return undefined;
1591
2100
  }