@cdxgen/cdxgen 13.2.0 → 13.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/README.md +33 -14
  2. package/bin/cdxgen.js +310 -138
  3. package/bin/evinse.js +22 -0
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +119 -15
  6. package/bin/tracebom.js +69 -5
  7. package/bin/validate.js +107 -19
  8. package/bin/verify.js +172 -63
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +85 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/bomAssembly.js +105 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +156 -58
  17. package/lib/cli/jsBom.js +99 -53
  18. package/lib/cli/jvmBom.js +708 -147
  19. package/lib/cli/managedBom.js +415 -224
  20. package/lib/cli/nativeBom.js +264 -44
  21. package/lib/core/activity.js +12 -0
  22. package/lib/core/env.js +22 -15
  23. package/lib/core/fs.js +176 -7
  24. package/lib/core/httpClient.js +159 -24
  25. package/lib/core/metadataProperties.js +97 -0
  26. package/lib/core/pluginRun.js +184 -0
  27. package/lib/core/processTree.js +100 -0
  28. package/lib/core/projectConfig.js +31 -4
  29. package/lib/core/runState.js +39 -0
  30. package/lib/core/toml.js +72 -0
  31. package/lib/ecosystems/asarutils.js +4 -2
  32. package/lib/ecosystems/bunutils.js +274 -108
  33. package/lib/ecosystems/cargoCache.js +231 -8
  34. package/lib/ecosystems/caxa.js +434 -2
  35. package/lib/ecosystems/cmakeResolver.js +76 -21
  36. package/lib/ecosystems/cppBuildContext.js +631 -0
  37. package/lib/ecosystems/cppEvidence.js +399 -51
  38. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  39. package/lib/ecosystems/denoutils.js +93 -66
  40. package/lib/ecosystems/ecosystems.js +1983 -793
  41. package/lib/ecosystems/golem.js +28 -12
  42. package/lib/ecosystems/gradleutils.js +356 -37
  43. package/lib/ecosystems/jsEvidence.js +104 -44
  44. package/lib/ecosystems/kosi.js +31 -7
  45. package/lib/ecosystems/npmutils.js +174 -9
  46. package/lib/ecosystems/parsers-dotnet.js +616 -159
  47. package/lib/ecosystems/parsers-elm.js +46 -28
  48. package/lib/ecosystems/parsers-gleam.js +3 -4
  49. package/lib/ecosystems/parsers-go.js +371 -70
  50. package/lib/ecosystems/parsers-js.js +263 -76
  51. package/lib/ecosystems/parsers-julia.js +2 -3
  52. package/lib/ecosystems/parsers-jvm.js +204 -12
  53. package/lib/ecosystems/parsers-misc.js +929 -216
  54. package/lib/ecosystems/parsers-python.js +167 -24
  55. package/lib/ecosystems/parsers-rust.js +28 -22
  56. package/lib/ecosystems/parsers-terraform.js +1573 -19
  57. package/lib/ecosystems/pythonutils.js +102 -6
  58. package/lib/ecosystems/remote/huggingface.js +3 -0
  59. package/lib/ecosystems/rubyutils.js +1492 -182
  60. package/lib/ecosystems/rusi.js +54 -10
  61. package/lib/ecosystems/sbtutils.js +243 -187
  62. package/lib/ecosystems/scalacliutils.js +342 -0
  63. package/lib/ecosystems/scalasem.js +2618 -0
  64. package/lib/ecosystems/terraformRegistry.js +288 -0
  65. package/lib/ecosystems/utils.js +2 -0
  66. package/lib/ecosystems/vendoredCode.js +156 -0
  67. package/lib/evinser/evinser.js +1008 -191
  68. package/lib/evinser/swiftsem.js +1914 -302
  69. package/lib/helpers/agenticToolutils.js +2 -2
  70. package/lib/helpers/bomSigner.js +1101 -222
  71. package/lib/inventory/aiCollector.js +4 -1
  72. package/lib/inventory/analyzer.js +110 -127
  73. package/lib/inventory/atomUtils.js +703 -43
  74. package/lib/inventory/auditCategories.js +7 -0
  75. package/lib/inventory/buildToolRateLimit.js +144 -0
  76. package/lib/inventory/cacheCommands.js +2 -1
  77. package/lib/inventory/cdxrs.js +103 -12
  78. package/lib/inventory/cmakeBuildDirs.js +254 -0
  79. package/lib/inventory/deps.js +501 -124
  80. package/lib/inventory/depsUtils.js +183 -11
  81. package/lib/inventory/display.js +4 -5
  82. package/lib/inventory/dosai.js +798 -57
  83. package/lib/inventory/dosaiParsers.js +310 -15
  84. package/lib/inventory/dynamic.js +90 -6
  85. package/lib/inventory/evidenceUtils.js +166 -113
  86. package/lib/inventory/fetchBatch.js +690 -55
  87. package/lib/inventory/fetchDiskCache.js +597 -0
  88. package/lib/inventory/forgeEnricher.js +14 -5
  89. package/lib/inventory/jvmLocalRepos.js +917 -0
  90. package/lib/inventory/mcpConfigParser.js +2 -3
  91. package/lib/inventory/parentSelection.js +146 -0
  92. package/lib/inventory/protobom.js +8 -1
  93. package/lib/inventory/purl.js +152 -3
  94. package/lib/inventory/rustFormulationParser.js +3 -4
  95. package/lib/inventory/sbomDocument.js +2 -70
  96. package/lib/inventory/scalaCoords.js +153 -0
  97. package/lib/inventory/source.js +45 -27
  98. package/lib/inventory/spdx.js +101 -0
  99. package/lib/inventory/traceRunner.js +295 -25
  100. package/lib/inventory/tracedCrypto.js +294 -0
  101. package/lib/managers/binary.js +115 -24
  102. package/lib/managers/docker.js +467 -46
  103. package/lib/managers/oci.js +63 -3
  104. package/lib/parsers/cmakeCache.js +69 -0
  105. package/lib/parsers/cmakeLists.js +466 -0
  106. package/lib/parsers/cmakePresets.js +400 -0
  107. package/lib/parsers/compileCommands.js +557 -0
  108. package/lib/parsers/largeJson.js +454 -0
  109. package/lib/parsers/npmrc.js +19 -1
  110. package/lib/parsers/strictJson.js +322 -0
  111. package/lib/parsers/terraformConfig.js +1073 -0
  112. package/lib/parsers/terraformSource.js +652 -0
  113. package/lib/server/server.js +23 -0
  114. package/lib/stages/postgen/introspection/markers.js +1 -1
  115. package/lib/stages/postgen/postgen.js +83 -16
  116. package/lib/stages/postgen/specVersionCompat.js +32 -0
  117. package/lib/validator/bomValidator.js +6 -16
  118. package/lib/validator/index.js +64 -11
  119. package/lib/validator/reporters/console.js +3 -0
  120. package/lib/validator/reporters/json.js +2 -0
  121. package/lib/validator/reporters/sarif.js +7 -0
  122. package/package.json +23 -23
  123. package/types/lib/audit/index.d.ts.map +1 -1
  124. package/types/lib/cli/bomAssembly.d.ts +2 -1
  125. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  126. package/types/lib/cli/cliOptions.d.ts +33 -1
  127. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  128. package/types/lib/cli/index.d.ts.map +1 -1
  129. package/types/lib/cli/jsBom.d.ts.map +1 -1
  130. package/types/lib/cli/jvmBom.d.ts +24 -1
  131. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  132. package/types/lib/cli/managedBom.d.ts +7 -4
  133. package/types/lib/cli/managedBom.d.ts.map +1 -1
  134. package/types/lib/cli/nativeBom.d.ts +3 -1
  135. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  136. package/types/lib/core/activity.d.ts.map +1 -1
  137. package/types/lib/core/env.d.ts +9 -0
  138. package/types/lib/core/env.d.ts.map +1 -1
  139. package/types/lib/core/fs.d.ts +55 -0
  140. package/types/lib/core/fs.d.ts.map +1 -1
  141. package/types/lib/core/httpClient.d.ts +13 -8
  142. package/types/lib/core/httpClient.d.ts.map +1 -1
  143. package/types/lib/core/metadataProperties.d.ts +39 -0
  144. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  145. package/types/lib/core/pluginRun.d.ts +97 -0
  146. package/types/lib/core/pluginRun.d.ts.map +1 -0
  147. package/types/lib/core/processTree.d.ts +36 -0
  148. package/types/lib/core/processTree.d.ts.map +1 -0
  149. package/types/lib/core/projectConfig.d.ts +6 -2
  150. package/types/lib/core/projectConfig.d.ts.map +1 -1
  151. package/types/lib/core/runState.d.ts +29 -0
  152. package/types/lib/core/runState.d.ts.map +1 -0
  153. package/types/lib/core/toml.d.ts +20 -0
  154. package/types/lib/core/toml.d.ts.map +1 -0
  155. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  156. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  157. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  159. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  160. package/types/lib/ecosystems/caxa.d.ts +14 -0
  161. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  162. package/types/lib/ecosystems/cmakeResolver.d.ts +28 -3
  163. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  164. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  165. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  166. package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
  167. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  168. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  169. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  170. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  171. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  172. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  173. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  174. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  175. package/types/lib/ecosystems/gradleutils.d.ts +70 -4
  176. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  177. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  178. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  179. package/types/lib/ecosystems/kosi.d.ts +13 -0
  180. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  181. package/types/lib/ecosystems/npmutils.d.ts +57 -0
  182. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  183. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  184. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-go.d.ts +54 -2
  188. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  189. package/types/lib/ecosystems/parsers-js.d.ts +16 -5
  190. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  191. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  199. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  200. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  201. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rubyutils.d.ts +164 -3
  205. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/rusi.d.ts +13 -0
  207. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  208. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  209. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  210. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  211. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  212. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  213. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  214. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  215. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  216. package/types/lib/ecosystems/utils.d.ts +2 -2
  217. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  218. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  219. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  220. package/types/lib/evinser/evinser.d.ts +132 -15
  221. package/types/lib/evinser/evinser.d.ts.map +1 -1
  222. package/types/lib/evinser/swiftsem.d.ts +280 -15
  223. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  224. package/types/lib/helpers/bomSigner.d.ts +182 -13
  225. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  226. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  227. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  228. package/types/lib/inventory/atomUtils.d.ts +219 -5
  229. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  230. package/types/lib/inventory/auditCategories.d.ts +3 -0
  231. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  232. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  233. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  234. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  235. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  236. package/types/lib/inventory/cdxrs.d.ts +8 -3
  237. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  238. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  239. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  240. package/types/lib/inventory/deps.d.ts +59 -1
  241. package/types/lib/inventory/deps.d.ts.map +1 -1
  242. package/types/lib/inventory/depsUtils.d.ts +15 -0
  243. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  244. package/types/lib/inventory/display.d.ts.map +1 -1
  245. package/types/lib/inventory/dosai.d.ts +121 -9
  246. package/types/lib/inventory/dosai.d.ts.map +1 -1
  247. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  248. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  249. package/types/lib/inventory/dynamic.d.ts +10 -0
  250. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  251. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  252. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  253. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  254. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  255. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  256. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  257. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  258. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  259. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  260. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  261. package/types/lib/inventory/parentSelection.d.ts +34 -0
  262. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  263. package/types/lib/inventory/plugins.d.ts.map +1 -1
  264. package/types/lib/inventory/protobom.d.ts.map +1 -1
  265. package/types/lib/inventory/purl.d.ts +71 -2
  266. package/types/lib/inventory/purl.d.ts.map +1 -1
  267. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  268. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  269. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  270. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  271. package/types/lib/inventory/source.d.ts.map +1 -1
  272. package/types/lib/inventory/spdx.d.ts +9 -0
  273. package/types/lib/inventory/spdx.d.ts.map +1 -1
  274. package/types/lib/inventory/traceRunner.d.ts +59 -1
  275. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  276. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  277. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  278. package/types/lib/managers/binary.d.ts +30 -1
  279. package/types/lib/managers/binary.d.ts.map +1 -1
  280. package/types/lib/managers/docker.d.ts +29 -0
  281. package/types/lib/managers/docker.d.ts.map +1 -1
  282. package/types/lib/managers/oci.d.ts +17 -1
  283. package/types/lib/managers/oci.d.ts.map +1 -1
  284. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  285. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  286. package/types/lib/parsers/cmakeLists.d.ts +77 -0
  287. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  288. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  289. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  290. package/types/lib/parsers/compileCommands.d.ts +115 -0
  291. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  292. package/types/lib/parsers/largeJson.d.ts +52 -0
  293. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  294. package/types/lib/parsers/npmrc.d.ts +5 -0
  295. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  296. package/types/lib/parsers/strictJson.d.ts +46 -0
  297. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  298. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  299. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  300. package/types/lib/parsers/terraformSource.d.ts +66 -0
  301. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  302. package/types/lib/server/server.d.ts.map +1 -1
  303. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  304. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  306. package/types/lib/validator/index.d.ts +18 -1
  307. package/types/lib/validator/index.d.ts.map +1 -1
  308. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  309. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  310. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
  311. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -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,
@@ -40,14 +45,27 @@ import {
40
45
  isRusiRustLanguage,
41
46
  } from "../ecosystems/rusi.js";
42
47
  import {
48
+ analyzeScalaProject,
49
+ collectScalasemApiEndpoints,
50
+ collectScalasemEvidence,
51
+ collectScalasemServices,
52
+ isScalasemLanguage,
53
+ } from "../ecosystems/scalasem.js";
54
+ import {
55
+ atomCompileCommandsArgs,
43
56
  buildAtomCommandEnv,
44
57
  executeAtom,
45
58
  filterAtomSlicesByExcludePatterns,
59
+ readReachablesSlices,
60
+ readSlicesFile,
61
+ removeReachablesChunkFiles,
46
62
  } from "../inventory/atomUtils.js";
63
+ import { isCycloneDxComponentTypeEnabled } from "../inventory/bomUtils.js";
47
64
  import {
48
65
  collectDosaiCryptoComponents,
49
66
  findCryptoAlgos,
50
67
  } from "../inventory/cbomutils.js";
68
+ import { cmakeBuildDirCandidates } from "../inventory/cmakeBuildDirs.js";
51
69
  import { collectMvnDependencies } from "../inventory/deps.js";
52
70
  import { mergeServices } from "../inventory/depsUtils.js";
53
71
  import {
@@ -59,12 +77,16 @@ import {
59
77
  collectDosaiServicesFromMethods,
60
78
  createDosaiDataFlowSlice,
61
79
  createDosaiMethodsSlice,
80
+ dosaiRunWasStopped,
62
81
  isDosaiDotnetLanguage,
63
82
  normalizeDosaiServiceMap,
64
83
  persistDosaiSemanticsReport,
84
+ readDosaiDataFlowReport,
85
+ readDosaiMethodsReport,
65
86
  } from "../inventory/dosai.js";
66
87
  import { parseOccurrenceEvidenceLocation } from "../inventory/evidenceUtils.js";
67
88
  import { executeOsQuery } from "../managers/binary.js";
89
+ import { MAX_JSON_TEXT_BYTES, readJsonFile } from "../parsers/largeJson.js";
68
90
  import { postProcess } from "../stages/postgen/postgen.js";
69
91
  import { createOrLoad } from "./db.js";
70
92
  import { findPurlLocations } from "./scalasem.js";
@@ -96,10 +118,175 @@ function appendComponentProperties(component, properties = []) {
96
118
  }
97
119
  }
98
120
 
121
+ /**
122
+ * Merges one analyzer run's metadata properties into the BOM's metadata
123
+ * component. Analyzer metadata describes a single run, so when the input BOM
124
+ * already carries facts from an earlier run (cdxgen --profile research, then
125
+ * evinse), everything under the analyzer's own `cdx:<analyzer>:` prefix is
126
+ * replaced. Replacing by exact name would keep facts the new run no longer
127
+ * emits, such as a cdx:rusi:requestedBackend left from a fallback run.
128
+ *
129
+ * @param {Object} component Metadata component to update in place
130
+ * @param {Object[]} metadataProperties Properties from the analyzer run
131
+ */
132
+ export function mergeAnalyzerMetadataProperties(
133
+ component,
134
+ metadataProperties = [],
135
+ ) {
136
+ if (!component || !metadataProperties.length) {
137
+ return;
138
+ }
139
+ if (component.properties?.length) {
140
+ const analyzerPrefixes = [
141
+ ...new Set(
142
+ metadataProperties
143
+ .map((p) => /^cdx:[^:]+:/.exec(p.name)?.[0])
144
+ .filter(Boolean),
145
+ ),
146
+ ];
147
+ component.properties = component.properties.filter(
148
+ (p) => !analyzerPrefixes.some((prefix) => p.name?.startsWith(prefix)),
149
+ );
150
+ }
151
+ appendComponentProperties(component, metadataProperties);
152
+ }
153
+
99
154
  function filterAtomSliceData(sliceData, options = {}) {
100
155
  return filterAtomSlicesByExcludePatterns(sliceData, options.exclude);
101
156
  }
102
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
+
233
+ /**
234
+ * Build the options for the evinse phase (prepareDB, analyzeProject, and
235
+ * createEvinseFile) from the cdxgen CLI options.
236
+ *
237
+ * This object used to be assembled inline in bin/cdxgen.js, which dropped
238
+ * fields the analyzers read - most notably `exclude`, so every dosai and atom
239
+ * run triggered by `--evidence` ignored the user's `--exclude` patterns. Keep
240
+ * the wiring in one place so the forwarded fields stay testable.
241
+ *
242
+ * @param {Object} options CLI options
243
+ * @param {Object} args Parsed CLI arguments; `args._[0]` is the source directory
244
+ * @param {string} input Path of the BOM file the evinse phase enriches
245
+ * @returns {Object} Options for prepareDB, analyzeProject, and createEvinseFile
246
+ */
247
+ export function buildEvinseOptions(options, args, input) {
248
+ const sourceDir = args._?.[0] || ".";
249
+ return {
250
+ _: args._,
251
+ input,
252
+ output: options.evinseOutput,
253
+ language: options.projectType,
254
+ skipMavenCollector: false,
255
+ force: false,
256
+ withReachables: options.deep,
257
+ usagesSlicesFile: options.usagesSlicesFile,
258
+ dataFlowSlicesFile: options.dataFlowSlicesFile,
259
+ reachablesSlicesFile: options.reachablesSlicesFile,
260
+ semanticsSlicesFile: options.semanticsSlicesFile,
261
+ openapiSpecFile: options.openapiSpecFile,
262
+ componentType: options.componentType,
263
+ includeCrypto:
264
+ options.includeCrypto &&
265
+ isCycloneDxComponentTypeEnabled("cryptographic-asset", options),
266
+ specVersion: options.specVersion,
267
+ profile: options.profile,
268
+ jsonPretty: options.jsonPretty,
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,
282
+ // Absolute path of the deps slice createCsharpBom writes for --deep runs,
283
+ // so analyzeProject can reuse it instead of rerunning dosai methods.
284
+ depsSlicesFile: options.depsSlicesFile
285
+ ? resolve(sourceDir, options.depsSlicesFile)
286
+ : undefined,
287
+ };
288
+ }
289
+
103
290
  /**
104
291
  * Function to create the db for the libraries referred in the sbom.
105
292
  *
@@ -153,7 +340,7 @@ export async function prepareDB(options) {
153
340
  await catalogMavenDeps(dirPath, purlsJars, Namespaces, options);
154
341
  }
155
342
  if (gradleFiles?.length) {
156
- await catalogGradleDeps(dirPath, purlsJars, Namespaces);
343
+ await catalogGradleDeps(dirPath, purlsJars, Namespaces, options);
157
344
  }
158
345
  }
159
346
  for (const purl of Object.keys(purlsToSlice)) {
@@ -162,11 +349,42 @@ export async function prepareDB(options) {
162
349
  return { sequelize, Namespaces, Usages, DataFlows };
163
350
  }
164
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
+
165
382
  /**
166
383
  * Collect Maven jar namespace mappings for the purls found in the SBOM.
167
384
  *
168
- * If a previously generated `bom.json.map` is present in `dirPath` it is reused;
169
- * 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
170
388
  * resolved purl is recorded in the `purlsJars` map and persisted in the
171
389
  * `Namespaces` model.
172
390
  *
@@ -182,19 +400,7 @@ export async function catalogMavenDeps(
182
400
  Namespaces,
183
401
  options = {},
184
402
  ) {
185
- let jarNSMapping;
186
- if (safeExistsSync(join(dirPath, "bom.json.map"))) {
187
- try {
188
- const mapData = JSON.parse(
189
- fs.readFileSync(join(dirPath, "bom.json.map"), "utf-8"),
190
- );
191
- if (mapData && Object.keys(mapData).length) {
192
- jarNSMapping = mapData;
193
- }
194
- } catch (_err) {
195
- // ignore
196
- }
197
- }
403
+ let jarNSMapping = readNamespaceMap(dirPath, options);
198
404
  if (!jarNSMapping) {
199
405
  console.log("About to collect jar dependencies for the path", dirPath);
200
406
  const mavenCmd = getMavenCommand(dirPath, dirPath);
@@ -238,18 +444,28 @@ export async function catalogMavenDeps(
238
444
  * @param {Object} Namespaces Sequelize-like Namespaces model used to persist pom and namespaces
239
445
  * @returns {Promise<void>}
240
446
  */
241
- export async function catalogGradleDeps(dirPath, purlsJars, Namespaces) {
242
- console.log(
243
- "About to collect jar dependencies from the gradle cache. This would take a while ...",
244
- );
245
- const gradleCmd = getGradleCommand(dirPath, dirPath);
246
- // collect all jars including from the cache if data-flow mode is enabled
247
- const jarNSMapping = await collectGradleDependencies(
248
- gradleCmd,
249
- dirPath,
250
- false,
251
- true,
252
- );
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
+ }
253
469
  if (jarNSMapping) {
254
470
  for (const purl of Object.keys(jarNSMapping)) {
255
471
  purlsJars[purl] = jarNSMapping[purl].jarFile;
@@ -429,7 +645,9 @@ export async function createSlice(
429
645
  resolve(slicesFile),
430
646
  options,
431
647
  );
432
- 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))) {
433
651
  console.warn(
434
652
  `Unable to generate ${sliceType} slice using dosai. Check if this is a supported .NET project.`,
435
653
  );
@@ -444,6 +662,11 @@ export async function createSlice(
444
662
  `Creating ${sliceType} slice for ${resolve(filePath)}. Please wait ...`,
445
663
  );
446
664
  const atomFile = join(sliceOutputDir, `${language}-app.atom`);
665
+ if (sliceType === "reachables") {
666
+ // atom writes reachables in numbered chunks next to the slices file; one
667
+ // left by an earlier run must not be read back as part of this one.
668
+ removeReachablesChunkFiles(slicesFile);
669
+ }
447
670
  let args = [sliceType];
448
671
  // Support for crypto slices aka CBOM
449
672
  if (sliceType === "reachables" && options.includeCrypto) {
@@ -461,15 +684,9 @@ export async function createSlice(
461
684
  "Use an official cdxgen container image to improve the precision of endpoints detection (for SaaSBOM).",
462
685
  );
463
686
  }
464
- if (["ruby", "scala"].includes(language)) {
687
+ if (language === "ruby") {
465
688
  args.push("--remove-atom");
466
689
  }
467
- if (["scala"].includes(language)) {
468
- semanticsSlicesFile = join(
469
- sliceOutputDir,
470
- basename(options.semanticsSlicesFile || "semantics.slices.json"),
471
- );
472
- }
473
690
  }
474
691
  args = args.concat([
475
692
  "-l",
@@ -488,6 +705,7 @@ export async function createSlice(
488
705
  args.push("--slice-depth");
489
706
  args.push(readEnvironmentVariable("ATOM_SLICE_DEPTH"));
490
707
  }
708
+ args.push(...atomCompileCommandsArgs(filePath, language, options));
491
709
  args.push(resolve(filePath));
492
710
  const atomExcludeEnv = buildAtomCommandEnv(options, language);
493
711
  // Execute atom
@@ -540,15 +758,6 @@ export async function createSlice(
540
758
  `openapi spec file "${join(filePath, openapiSpecFile)}" was not generated successfully. Check if atom-tools pypi package is installed and available in PATH.`,
541
759
  );
542
760
  }
543
- if (
544
- ["scala"].includes(language) &&
545
- sliceType === "usages" &&
546
- !safeExistsSync(semanticsSlicesFile)
547
- ) {
548
- console.log(
549
- `Semantics slices file "${semanticsSlicesFile}" was not generated successfully. Try running atom cli in Java mode.`,
550
- );
551
- }
552
761
  return {
553
762
  tempDir: sliceOutputDir,
554
763
  tempDirOwned,
@@ -598,8 +807,9 @@ export function purlToLanguage(purl, filePath) {
598
807
  * Seed purl-location and import maps from SBOM component evidence.
599
808
  *
600
809
  * Walks the supplied components reading `internal:ImportedModules` (or
601
- * `internal:Namespaces` for php/ruby) and `evidence.occurrences` properties
602
- * to construct lookup maps used during slice analysis.
810
+ * `internal:Namespaces` for php/ruby, plus the `internal:ImportedSymbols` the
811
+ * C/C++ collector records) and `evidence.occurrences` properties to construct
812
+ * lookup maps used during slice analysis.
603
813
  *
604
814
  * @param {Object[]} components CycloneDX components from the input SBOM
605
815
  * @param {string} language Application language used to select the import property name
@@ -624,6 +834,18 @@ export function initFromSbom(components, language) {
624
834
  .forEach((v) => {
625
835
  purlImportsMap[comp.purl] = (v.value || "").split(",");
626
836
  });
837
+ // A C/C++ component records the symbols the project uses from it, which
838
+ // are what C usages are matched against. The list is pipe-separated:
839
+ // C++ symbols may contain commas.
840
+ if (["c", "cpp", "c++"].includes(language)) {
841
+ (comp.properties || [])
842
+ .filter((v) => v.name === "internal:ImportedSymbols")
843
+ .forEach((v) => {
844
+ purlImportsMap[comp.purl] = (
845
+ purlImportsMap[comp.purl] || []
846
+ ).concat((v.value || "").split("|").filter(Boolean));
847
+ });
848
+ }
627
849
  }
628
850
  if (comp.evidence.occurrences) {
629
851
  // Keep the `location#line` shape so a re-run over a cdxgen BOM does not
@@ -654,6 +876,273 @@ function usableSlicesFile(slicesFile) {
654
876
  return fileSizeInBytes > 1024;
655
877
  }
656
878
 
879
+ /**
880
+ * Load a semantics slices file for reuse when it belongs to this project.
881
+ *
882
+ * `--semantics-slices-file` defaults to `semantics.slices.json` in the working
883
+ * directory, which is shared by every project scanned from there and also
884
+ * receives the reports of other analyzers (dosai, rusi, golem). A file is
885
+ * only reused when its shape matches the language. Swift slices must also
886
+ * describe the same project directory and must not predate the input SBOM:
887
+ * `cdxgen --evidence` writes a fresh SBOM before evinse runs, so an older
888
+ * slice was made from an earlier state of the sources and is regenerated.
889
+ * Scala reports are handled by the scalasem analyzer, which applies its own
890
+ * rules.
891
+ *
892
+ * @param {string} language Project language
893
+ * @param {string} slicesFile Candidate semantics slices file
894
+ * @param {string} bomFile Input SBOM
895
+ * @param {string} dirPath Project directory
896
+ * @returns {undefined|Object} Parsed slice when it can be reused
897
+ */
898
+ export function loadReusableSemanticsSlice(
899
+ language,
900
+ slicesFile,
901
+ bomFile,
902
+ dirPath,
903
+ ) {
904
+ if (!usableSlicesFile(slicesFile)) {
905
+ return undefined;
906
+ }
907
+ let parsed;
908
+ try {
909
+ parsed = readJsonFile(slicesFile);
910
+ } catch (_e) {
911
+ return undefined;
912
+ }
913
+ if (!parsed || typeof parsed !== "object") {
914
+ return undefined;
915
+ }
916
+ if (!PROJECT_TYPE_ALIASES.swift.includes(language)) {
917
+ return undefined;
918
+ }
919
+ if (!(parsed.fileIndexes || parsed.buildSymbols || parsed.moduleInfos)) {
920
+ return undefined;
921
+ }
922
+ if (parsed.projectPath && resolve(parsed.projectPath) !== resolve(dirPath)) {
923
+ return undefined;
924
+ }
925
+ try {
926
+ if (fs.statSync(slicesFile).mtimeMs < fs.statSync(bomFile).mtimeMs) {
927
+ return undefined;
928
+ }
929
+ } catch (_e) {
930
+ // Without timestamps the shape and project checks decide
931
+ }
932
+ return parsed;
933
+ }
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
+
657
1146
  /**
658
1147
  * Function to analyze the project
659
1148
  *
@@ -663,7 +1152,12 @@ function usableSlicesFile(slicesFile) {
663
1152
  export async function analyzeProject(dbObjMap, options) {
664
1153
  const dirPath = options._[0] || ".";
665
1154
  const languages = options.language;
666
- 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
+ }
667
1161
  let usageSlice;
668
1162
  let dataFlowSlice;
669
1163
  let reachablesSlice;
@@ -817,6 +1311,19 @@ export async function analyzeProject(dbObjMap, options) {
817
1311
  };
818
1312
  }
819
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
+
820
1327
  if (isDosaiDotnetLanguage(language)) {
821
1328
  if (options.profile === "research") {
822
1329
  options.withDataFlow = true;
@@ -826,23 +1333,38 @@ export async function analyzeProject(dbObjMap, options) {
826
1333
  options.usagesSlicesFile &&
827
1334
  usableSlicesFile(options.usagesSlicesFile)
828
1335
  ) {
829
- usageSlice = JSON.parse(
830
- fs.readFileSync(options.usagesSlicesFile, "utf-8"),
831
- );
1336
+ usageSlice = readDosaiMethodsReport(options.usagesSlicesFile);
832
1337
  usagesSlicesFile = options.usagesSlicesFile;
1338
+ } else if (usableSlicesFile(options.depsSlicesFile)) {
1339
+ // createCsharpBom already ran dosai methods on this tree for --deep runs
1340
+ // and wrote the deps slice. For dosai, that output IS the usages slice,
1341
+ // so reuse it instead of invoking dosai a second time.
1342
+ if (DEBUG_MODE) {
1343
+ console.log(
1344
+ `Reusing the dosai deps slices file "${options.depsSlicesFile}" for the usages analysis.`,
1345
+ );
1346
+ }
1347
+ usageSlice = readDosaiMethodsReport(options.depsSlicesFile);
1348
+ usagesSlicesFile = options.depsSlicesFile;
833
1349
  } else {
834
1350
  retMap = await createSlice(language, dirPath, "usages", options);
835
1351
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
836
- usageSlice = JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8"));
1352
+ usageSlice = readDosaiMethodsReport(retMap.slicesFile);
837
1353
  usagesSlicesFile = retMap.slicesFile;
838
1354
  }
839
1355
  }
840
1356
  if (usageSlice && Object.keys(usageSlice).length) {
841
- 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
+ });
842
1362
  // Confidence/evidence facts ride along as properties so an unbuilt tree (Low
843
1363
  // confidence, unresolved evidence) stays distinguishable from an only-imported
844
1364
  // package in the emitted BOM.
845
- applyDosaiReachabilityEvidence(usageSlice, components);
1365
+ applyDosaiReachabilityEvidence(usageSlice, components, {
1366
+ srcPath: resolve(dirPath),
1367
+ });
846
1368
  for (const [purl, locations] of Object.entries(
847
1369
  dosaiEvidence.purlLocationMap,
848
1370
  )) {
@@ -867,20 +1389,18 @@ export async function analyzeProject(dbObjMap, options) {
867
1389
  safeExistsSync(options.dataFlowSlicesFile)
868
1390
  ) {
869
1391
  dataFlowSlicesFile = options.dataFlowSlicesFile;
870
- dataFlowSlice = JSON.parse(
871
- fs.readFileSync(options.dataFlowSlicesFile, "utf-8"),
872
- );
1392
+ dataFlowSlice = readDosaiDataFlowReport(options.dataFlowSlicesFile);
873
1393
  } else {
874
1394
  retMap = await createSlice(language, dirPath, "data-flow", options);
875
1395
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
876
1396
  dataFlowSlicesFile = retMap.slicesFile;
877
- dataFlowSlice = JSON.parse(
878
- fs.readFileSync(retMap.slicesFile, "utf-8"),
879
- );
1397
+ dataFlowSlice = readDosaiDataFlowReport(retMap.slicesFile);
880
1398
  }
881
1399
  }
882
1400
  if (dataFlowSlice && Object.keys(dataFlowSlice).length) {
883
- dataFlowFrames = collectDosaiDataFlowFrames(dataFlowSlice, components);
1401
+ dataFlowFrames = collectDosaiDataFlowFrames(dataFlowSlice, components, {
1402
+ srcPath: resolve(dirPath),
1403
+ });
884
1404
  }
885
1405
  }
886
1406
  if (options.includeCrypto) {
@@ -923,7 +1443,7 @@ export async function analyzeProject(dbObjMap, options) {
923
1443
  ) {
924
1444
  reachablesSlicesFile = options.reachablesSlicesFile;
925
1445
  reachablesSlice = filterAtomSliceData(
926
- JSON.parse(fs.readFileSync(options.reachablesSlicesFile, "utf-8")),
1446
+ readReachablesSlices(options.reachablesSlicesFile),
927
1447
  options,
928
1448
  );
929
1449
  } else {
@@ -931,7 +1451,7 @@ export async function analyzeProject(dbObjMap, options) {
931
1451
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
932
1452
  reachablesSlicesFile = retMap.slicesFile;
933
1453
  reachablesSlice = filterAtomSliceData(
934
- JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8")),
1454
+ readReachablesSlices(retMap.slicesFile),
935
1455
  options,
936
1456
  );
937
1457
  }
@@ -947,7 +1467,7 @@ export async function analyzeProject(dbObjMap, options) {
947
1467
  // Reuse existing usages slices
948
1468
  if (options.usagesSlicesFile && usableSlicesFile(options.usagesSlicesFile)) {
949
1469
  usageSlice = filterAtomSliceData(
950
- JSON.parse(fs.readFileSync(options.usagesSlicesFile, "utf-8")),
1470
+ readSlicesFile(options.usagesSlicesFile, "usages"),
951
1471
  options,
952
1472
  );
953
1473
  usagesSlicesFile = options.usagesSlicesFile;
@@ -956,7 +1476,7 @@ export async function analyzeProject(dbObjMap, options) {
956
1476
  retMap = await createSlice(language, dirPath, "usages", options);
957
1477
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
958
1478
  usageSlice = filterAtomSliceData(
959
- JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8")),
1479
+ readSlicesFile(retMap.slicesFile, "usages"),
960
1480
  options,
961
1481
  );
962
1482
  usagesSlicesFile = retMap.slicesFile;
@@ -976,31 +1496,48 @@ export async function analyzeProject(dbObjMap, options) {
976
1496
  }
977
1497
  }
978
1498
  // Support for semantics slicing
979
- if (
980
- (PROJECT_TYPE_ALIASES.swift.includes(language) ||
981
- PROJECT_TYPE_ALIASES.scala.includes(language)) &&
982
- components.length
983
- ) {
984
- // Reuse existing semantics slices for swift and scala
985
- if (
986
- options.semanticsSlicesFile &&
987
- safeExistsSync(options.semanticsSlicesFile)
988
- ) {
989
- semanticsSlice = JSON.parse(
990
- fs.readFileSync(options.semanticsSlicesFile, "utf-8"),
991
- );
1499
+ if (PROJECT_TYPE_ALIASES.swift.includes(language) && components.length) {
1500
+ // Reuse existing semantics slices for swift
1501
+ const reusableSlice = loadReusableSemanticsSlice(
1502
+ language,
1503
+ options.semanticsSlicesFile,
1504
+ bomFile,
1505
+ dirPath,
1506
+ );
1507
+ if (reusableSlice) {
1508
+ semanticsSlice = reusableSlice;
992
1509
  semanticsSlicesFile = options.semanticsSlicesFile;
993
- } else if (PROJECT_TYPE_ALIASES.swift.includes(language)) {
1510
+ } else {
994
1511
  // Generate our own slices for swift
995
1512
  retMap = await createSlice(language, dirPath, "semantics", options);
996
1513
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
997
- semanticsSlice = JSON.parse(
998
- fs.readFileSync(retMap.slicesFile, "utf-8"),
999
- );
1514
+ semanticsSlice = readSlicesFile(retMap.slicesFile, "semantics");
1000
1515
  semanticsSlicesFile = retMap.slicesFile;
1001
1516
  }
1002
1517
  }
1003
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
+ }
1004
1541
  // Parse usage slices
1005
1542
  if (usageSlice && Object.keys(usageSlice).length) {
1006
1543
  const retMap = await parseObjectSlices(
@@ -1016,7 +1553,14 @@ export async function analyzeProject(dbObjMap, options) {
1016
1553
  servicesMap = retMap.servicesMap;
1017
1554
  userDefinedTypesMap = retMap.userDefinedTypesMap;
1018
1555
  }
1019
- // 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
1020
1564
  if (
1021
1565
  semanticsSlice &&
1022
1566
  Object.keys(semanticsSlice).length &&
@@ -1024,7 +1568,25 @@ export async function analyzeProject(dbObjMap, options) {
1024
1568
  ) {
1025
1569
  // Identify the purl locations
1026
1570
  const retMap = parseSemanticSlices(language, components, semanticsSlice);
1027
- 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);
1028
1590
  }
1029
1591
  if (options.withDataFlow) {
1030
1592
  if (
@@ -1033,7 +1595,7 @@ export async function analyzeProject(dbObjMap, options) {
1033
1595
  ) {
1034
1596
  dataFlowSlicesFile = options.dataFlowSlicesFile;
1035
1597
  dataFlowSlice = filterAtomSliceData(
1036
- JSON.parse(fs.readFileSync(options.dataFlowSlicesFile, "utf-8")),
1598
+ readSlicesFile(options.dataFlowSlicesFile, "data-flow"),
1037
1599
  options,
1038
1600
  );
1039
1601
  } else if (!PROJECT_TYPE_ALIASES.scala.includes(language)) {
@@ -1041,7 +1603,7 @@ export async function analyzeProject(dbObjMap, options) {
1041
1603
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
1042
1604
  dataFlowSlicesFile = retMap.slicesFile;
1043
1605
  dataFlowSlice = filterAtomSliceData(
1044
- JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8")),
1606
+ readSlicesFile(retMap.slicesFile, "data-flow"),
1045
1607
  options,
1046
1608
  );
1047
1609
  }
@@ -1093,6 +1655,125 @@ export async function analyzeProject(dbObjMap, options) {
1093
1655
  * @param {string} [openapiSpecFile=undefined] Optional path to an OpenAPI spec used for service detection
1094
1656
  * @returns {Promise<Object>} Map with `purlLocationMap`, `servicesMap`, and `userDefinedTypesMap`
1095
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
+
1096
1777
  export async function parseObjectSlices(
1097
1778
  language,
1098
1779
  usageSlice,
@@ -1400,99 +2081,64 @@ export async function parseSliceUsages(
1400
2081
  /**
1401
2082
  * Method to parse semantic slice data. Currently supported for swift and scala languages.
1402
2083
  *
2084
+ * Swift slices created by cdxgen record, for every indexed source file, the
2085
+ * lines that reference each dependency module (`moduleReferences`); the module
2086
+ * of a reference is the declaring module recovered from the compiler-resolved
2087
+ * USR. Components are mapped to their package's modules through the
2088
+ * `packages` slice, so every occurrence is backed by a resolved reference.
2089
+ * Slices without `moduleReferences` (created by older cdxgen versions) fall
2090
+ * back to matching symbol names.
2091
+ *
1403
2092
  * @param {String} language Project language.
1404
2093
  * @param {Array} components Components from the input SBOM
1405
2094
  * @param {Object} semanticsSlice Semantic slice data
1406
2095
  * @returns {Object} Parsed metadata
1407
2096
  */
1408
- export function parseSemanticSlices(language, components, semanticsSlice) {
1409
- // For scala, use the dedicated scalasem module.
1410
- if (language === "scala") {
1411
- return findPurlLocations(components, semanticsSlice);
1412
- }
2097
+ export function parseSemanticSlices(_language, components, semanticsSlice) {
1413
2098
  if (!components) {
1414
2099
  return undefined;
1415
2100
  }
1416
- const componentNamePurlMap = {};
1417
- const componentSymbolsMap = {};
1418
- const allObfuscationsMap = {};
1419
- // We have two attributes in the semantics slice to expand a given module to its constituent symbols
1420
- // - A less precise buildSymbols, which is obtained by parsing the various output-file-map.json files
1421
- // - A granular and precise moduleInfos, which has the exact classes, protocols, enums etc belonging to each module
1422
- // The objective then is to build the purlLocationMap, which is the list of locations where a given purl is used
1423
- // For this, we have two attributes:
1424
- // - A simpler fileStructures attribute that has the mapping between a swift file and the list of referenced types
1425
- // - A granular fileIndexes attribute that contains information about the clang modules and line number information
1426
-
1427
- // We first need to map out the component names to their purls
1428
- // This is because the semantics slice use the module names everywhere
2101
+ const fileIndexes = semanticsSlice?.fileIndexes || {};
2102
+ const hasModuleReferences = Object.values(fileIndexes).some(
2103
+ (afileIndex) => afileIndex?.moduleReferences,
2104
+ );
2105
+ const purlLocationsSet = {};
1429
2106
  for (const comp of components) {
1430
- componentNamePurlMap[comp.name] = comp.purl;
1431
- if (!componentSymbolsMap[comp.name]) {
1432
- componentSymbolsMap[comp.name] = new Set();
1433
- }
1434
- if (semanticsSlice?.buildSymbols[comp.name]) {
1435
- for (const asym of semanticsSlice.buildSymbols[comp.name]) {
1436
- componentSymbolsMap[comp.name].add(asym);
1437
- }
1438
- }
1439
- const moduleInfo = semanticsSlice?.moduleInfos[comp.name] || {};
1440
- if (moduleInfo.classes) {
1441
- for (const asym of moduleInfo.classes) {
1442
- componentSymbolsMap[comp.name].add(asym);
1443
- }
1444
- }
1445
- if (moduleInfo.protocols) {
1446
- for (const asym of moduleInfo.protocols) {
1447
- componentSymbolsMap[comp.name].add(asym);
1448
- }
1449
- }
1450
- if (moduleInfo.enums) {
1451
- for (const asym of moduleInfo.enums) {
1452
- componentSymbolsMap[comp.name].add(asym);
1453
- }
2107
+ // Local packages and the root application have no purl, so there is
2108
+ // nothing the evidence could be attached to downstream
2109
+ if (!comp?.purl) {
2110
+ continue;
1454
2111
  }
1455
- // Now collect the method signatures from class and protocol methods
1456
- if (moduleInfo.classMethods) {
1457
- for (const aclassName of Object.keys(moduleInfo.classMethods)) {
1458
- for (const asym of moduleInfo.classMethods[aclassName]) {
1459
- componentSymbolsMap[comp.name].add(asym);
1460
- }
1461
- }
2112
+ const moduleNames = resolveSwiftComponentModules(comp, semanticsSlice);
2113
+ if (!moduleNames.size) {
2114
+ continue;
1462
2115
  }
1463
- if (moduleInfo.protocolMethods) {
1464
- for (const aclassName of Object.keys(moduleInfo.protocolMethods)) {
1465
- for (const asym of moduleInfo.protocolMethods[aclassName]) {
1466
- componentSymbolsMap[comp.name].add(asym);
2116
+ const locations = purlLocationsSet[comp.purl] || new Set();
2117
+ if (hasModuleReferences) {
2118
+ for (const [aswiftFile, afileIndex] of Object.entries(fileIndexes)) {
2119
+ for (const [amodule, lines] of Object.entries(
2120
+ afileIndex?.moduleReferences || {},
2121
+ )) {
2122
+ if (!moduleNames.has(amodule)) {
2123
+ continue;
2124
+ }
2125
+ for (const aline of lines) {
2126
+ locations.add(`${aswiftFile}#${aline}`);
2127
+ }
1467
2128
  }
1468
2129
  }
1469
- }
1470
- // Build a large obfuscation map
1471
- if (moduleInfo.obfuscationMap) {
1472
- for (const asym of Object.keys(moduleInfo.obfuscationMap)) {
1473
- allObfuscationsMap[asym] = moduleInfo.obfuscationMap[asym];
1474
- }
1475
- }
1476
- }
1477
- const purlLocationsSet = {};
1478
- // We now have a good set of data in componentSymbolsMap and allObfuscationsMap
1479
- // We can iterate these symbols and check if they exist under fileIndexes.symbolLocations
1480
- for (const compName of Object.keys(componentSymbolsMap)) {
1481
- const compSymbols = Array.from(componentSymbolsMap[compName]);
1482
- const searchHits = searchSymbolLocations(
1483
- compSymbols,
1484
- semanticsSlice?.fileIndexes,
1485
- );
1486
- // We have an occurrence hit. Let's populate the purlLocationMap
1487
- if (searchHits?.length) {
1488
- const locations =
1489
- purlLocationsSet[componentNamePurlMap[compName]] || new Set();
1490
- for (const ahit of searchHits) {
2130
+ } else {
2131
+ for (const ahit of searchSymbolLocations(
2132
+ collectLegacySwiftSymbols(moduleNames, semanticsSlice),
2133
+ fileIndexes,
2134
+ )) {
1491
2135
  for (const aline of ahit.lineNumbers) {
1492
2136
  locations.add(`${ahit.file}#${aline}`);
1493
2137
  }
1494
2138
  }
1495
- purlLocationsSet[componentNamePurlMap[compName]] = locations;
2139
+ }
2140
+ if (locations.size) {
2141
+ purlLocationsSet[comp.purl] = locations;
1496
2142
  }
1497
2143
  }
1498
2144
  const purlLocationMap = {};
@@ -1502,22 +2148,184 @@ export function parseSemanticSlices(language, components, semanticsSlice) {
1502
2148
  return { purlLocationMap };
1503
2149
  }
1504
2150
 
1505
- function searchSymbolLocations(compSymbols, fileIndexes) {
1506
- const searchHits = [];
1507
- if (!fileIndexes) {
2151
+ /**
2152
+ * Normalise a repository location or Swift purl into `host/owner/name`.
2153
+ *
2154
+ * @param {String} value Repository URL, scp-style git location, or swift purl
2155
+ * @returns {undefined|String} Lowercase `host/path` without scheme and `.git`
2156
+ */
2157
+ function normalizeSwiftPackageLocation(value) {
2158
+ if (!value || typeof value !== "string") {
1508
2159
  return undefined;
1509
2160
  }
1510
- for (const aswiftFile of Object.keys(fileIndexes)) {
1511
- if (!fileIndexes[aswiftFile].symbolLocations) {
2161
+ let text = value.trim();
2162
+ if (text.startsWith("pkg:swift/")) {
2163
+ text = text.slice("pkg:swift/".length).split("?")[0].split("#")[0];
2164
+ const at = text.lastIndexOf("@");
2165
+ if (at > 0) {
2166
+ text = text.slice(0, at);
2167
+ }
2168
+ try {
2169
+ text = decodeURIComponent(text);
2170
+ } catch (_e) {
2171
+ // Keep the raw value
2172
+ }
2173
+ } else {
2174
+ text = text.split("?")[0].split("#")[0];
2175
+ const scheme = text.indexOf("://");
2176
+ if (scheme >= 0) {
2177
+ text = text.slice(scheme + 3);
2178
+ // Drop userinfo such as git@
2179
+ const slash = text.indexOf("/");
2180
+ const at = text.indexOf("@");
2181
+ if (at >= 0 && (slash < 0 || at < slash)) {
2182
+ text = text.slice(at + 1);
2183
+ }
2184
+ } else if (text.includes("@") && text.includes(":")) {
2185
+ // scp-style git@github.com:owner/name.git
2186
+ text = text.slice(text.indexOf("@") + 1).replace(":", "/");
2187
+ } else {
2188
+ return undefined;
2189
+ }
2190
+ }
2191
+ text = text.replace(/\/+$/, "");
2192
+ if (text.endsWith(".git")) {
2193
+ text = text.slice(0, -4);
2194
+ }
2195
+ if (text.startsWith("www.")) {
2196
+ text = text.slice(4);
2197
+ }
2198
+ return text.toLowerCase() || undefined;
2199
+ }
2200
+
2201
+ /**
2202
+ * Resolve the Swift module names that belong to a SBOM component.
2203
+ *
2204
+ * A component is named after its package repository or identity while the
2205
+ * semantic slices are keyed by module names. With the `packages` slice, a
2206
+ * package cloned from a repository matches the component whose purl names
2207
+ * the same repository, so a fork with the same name never inherits another
2208
+ * package's evidence; registry and local packages, which have no repository
2209
+ * URL, match by identity or name. Older slices only carry the
2210
+ * `packageModules` aliases (identity, package name, repository name), which
2211
+ * are matched against the component name and the `cdx:swift:packageName`
2212
+ * property, so that `swift-argument-parser` resolves to its `ArgumentParser`
2213
+ * module.
2214
+ *
2215
+ * @param {Object} comp SBOM component
2216
+ * @param {Object} semanticsSlice Semantic slice data
2217
+ * @returns {Set<string>} Module names associated with the component
2218
+ */
2219
+ function resolveSwiftComponentModules(comp, semanticsSlice) {
2220
+ const moduleNames = new Set();
2221
+ if (!comp?.name) {
2222
+ return moduleNames;
2223
+ }
2224
+ const aliases = new Set(
2225
+ [comp.name, getPropertyValue(comp, "cdx:swift:packageName")]
2226
+ .filter(Boolean)
2227
+ .map((alias) => String(alias).toLowerCase()),
2228
+ );
2229
+ if (Array.isArray(semanticsSlice?.packages)) {
2230
+ const componentLocation = normalizeSwiftPackageLocation(comp.purl);
2231
+ for (const apkg of semanticsSlice.packages) {
2232
+ const packageLocation = normalizeSwiftPackageLocation(apkg?.location);
2233
+ const matches =
2234
+ packageLocation && componentLocation
2235
+ ? packageLocation === componentLocation
2236
+ : [apkg?.identity, apkg?.name].some(
2237
+ (alias) => alias && aliases.has(String(alias).toLowerCase()),
2238
+ );
2239
+ if (matches) {
2240
+ for (const amodule of apkg.modules || []) {
2241
+ moduleNames.add(amodule);
2242
+ }
2243
+ }
2244
+ }
2245
+ return moduleNames;
2246
+ }
2247
+ const packageModules = semanticsSlice?.packageModules || {};
2248
+ for (const alias of new Set([comp.name, ...aliases])) {
2249
+ if (Array.isArray(packageModules[alias])) {
2250
+ for (const amodule of packageModules[alias]) {
2251
+ moduleNames.add(amodule);
2252
+ }
2253
+ }
2254
+ }
2255
+ // Modules named after the package (Yams, SWXMLHash) still match
2256
+ if (!moduleNames.size) {
2257
+ moduleNames.add(comp.name);
2258
+ }
2259
+ return moduleNames;
2260
+ }
2261
+
2262
+ function getPropertyValue(comp, propertyName) {
2263
+ for (const aproperty of comp?.properties || []) {
2264
+ if (aproperty?.name === propertyName) {
2265
+ return aproperty.value;
2266
+ }
2267
+ }
2268
+ return undefined;
2269
+ }
2270
+
2271
+ /**
2272
+ * Collect the symbol names of a component's modules from slices that predate
2273
+ * `moduleReferences`, using the moduleInfos data and the coarser buildSymbols
2274
+ * derived from the build's output file maps.
2275
+ *
2276
+ * @param {Set<String>} moduleNames Module names of the component
2277
+ * @param {Object} semanticsSlice Semantic slice data
2278
+ * @returns {Object} `{ symbols, freeFunctions }` name sets
2279
+ */
2280
+ function collectLegacySwiftSymbols(moduleNames, semanticsSlice) {
2281
+ const symbols = new Set();
2282
+ const freeFunctions = new Set();
2283
+ for (const moduleName of moduleNames) {
2284
+ for (const asym of semanticsSlice?.buildSymbols?.[moduleName] || []) {
2285
+ symbols.add(asym);
2286
+ }
2287
+ const moduleInfo = semanticsSlice?.moduleInfos?.[moduleName] || {};
2288
+ for (const asym of [
2289
+ ...(moduleInfo.classes || []),
2290
+ ...(moduleInfo.protocols || []),
2291
+ ...(moduleInfo.enums || []),
2292
+ ]) {
2293
+ symbols.add(asym);
2294
+ }
2295
+ for (const methods of [
2296
+ ...Object.values(moduleInfo.classMethods || {}),
2297
+ ...Object.values(moduleInfo.protocolMethods || {}),
2298
+ ]) {
2299
+ for (const asym of methods || []) {
2300
+ symbols.add(asym);
2301
+ }
2302
+ }
2303
+ for (const asym of moduleInfo.functions || []) {
2304
+ freeFunctions.add(asym);
2305
+ }
2306
+ }
2307
+ return { symbols, freeFunctions };
2308
+ }
2309
+
2310
+ function searchSymbolLocations(componentSymbols, fileIndexes) {
2311
+ const searchHits = [];
2312
+ const { symbols, freeFunctions } = componentSymbols;
2313
+ for (const aswiftFile of Object.keys(fileIndexes || {})) {
2314
+ const symbolLocations = fileIndexes[aswiftFile]?.symbolLocations;
2315
+ if (!symbolLocations) {
1512
2316
  continue;
1513
2317
  }
1514
- for (const asym of Object.keys(fileIndexes[aswiftFile].symbolLocations)) {
1515
- const lineNumbers = fileIndexes[aswiftFile].symbolLocations[asym];
1516
- if (compSymbols.includes(asym)) {
2318
+ for (const asym of Object.keys(symbolLocations)) {
2319
+ // The index records free functions by their full selector
2320
+ // (`dump(object:canonical:...)`) while module interfaces give the base
2321
+ // name. Only top-level functions are matched by base name: members such
2322
+ // as `encode(to:)` exist in nearly every module
2323
+ const baseName = asym.includes("(") ? asym.split("(")[0] : undefined;
2324
+ if (symbols.has(asym) || (baseName && freeFunctions.has(baseName))) {
1517
2325
  searchHits.push({
1518
2326
  file: aswiftFile,
1519
2327
  symbol: asym,
1520
- lineNumbers,
2328
+ lineNumbers: symbolLocations[asym],
1521
2329
  });
1522
2330
  }
1523
2331
  }
@@ -1950,7 +2758,10 @@ export async function createEvinseFile(sliceArtefacts, options) {
1950
2758
  bomJson.metadata.component = workspaceComponent;
1951
2759
  }
1952
2760
  }
1953
- appendComponentProperties(bomJson.metadata?.component, metadataProperties);
2761
+ mergeAnalyzerMetadataProperties(
2762
+ bomJson.metadata?.component,
2763
+ metadataProperties,
2764
+ );
1954
2765
  let occEvidencePresent = false;
1955
2766
  let csEvidencePresent = false;
1956
2767
  let servicesPresent = false;
@@ -2009,8 +2820,15 @@ export async function createEvinseFile(sliceArtefacts, options) {
2009
2820
  servicesPresent = true;
2010
2821
  }
2011
2822
  // Add the crypto components to the components list
2823
+ // Multi-type scans may have collected the same crypto assets during BOM
2824
+ // generation, so skip any bom-ref already present to keep the BOM valid.
2012
2825
  if (cryptoComponents?.length) {
2013
- bomJson.components = bomJson.components.concat(cryptoComponents);
2826
+ const existingRefs = new Set(
2827
+ bomJson.components.map((comp) => comp["bom-ref"]).filter(Boolean),
2828
+ );
2829
+ bomJson.components = bomJson.components.concat(
2830
+ cryptoComponents.filter((comp) => !existingRefs.has(comp["bom-ref"])),
2831
+ );
2014
2832
  }
2015
2833
  // Add the AI components (machine-learning-model, dataset) discovered by dosai
2016
2834
  if (aiComponents?.length) {
@@ -2035,28 +2853,27 @@ export async function createEvinseFile(sliceArtefacts, options) {
2035
2853
  bomJson.dependencies = newDependencies;
2036
2854
  }
2037
2855
  if (options.annotate) {
2038
- if (usagesSlicesFile && safeExistsSync(usagesSlicesFile)) {
2039
- bomJson.annotations.push({
2040
- subjects: [bomJson.serialNumber],
2041
- annotator: { component: bomJson.metadata.tools.components[0] },
2042
- timestamp: getTimestamp(),
2043
- text: fs.readFileSync(usagesSlicesFile, "utf8"),
2044
- });
2045
- }
2046
- if (dataFlowSlicesFile && safeExistsSync(dataFlowSlicesFile)) {
2047
- bomJson.annotations.push({
2048
- subjects: [bomJson.serialNumber],
2049
- annotator: { component: bomJson.metadata.tools.components[0] },
2050
- timestamp: getTimestamp(),
2051
- text: fs.readFileSync(dataFlowSlicesFile, "utf8"),
2052
- });
2053
- }
2054
- if (reachablesSlicesFile && safeExistsSync(reachablesSlicesFile)) {
2856
+ for (const slicesFile of [
2857
+ usagesSlicesFile,
2858
+ dataFlowSlicesFile,
2859
+ reachablesSlicesFile,
2860
+ ]) {
2861
+ if (!slicesFile || !safeExistsSync(slicesFile)) {
2862
+ continue;
2863
+ }
2864
+ // The annotation embeds the slice as one string, which a file this
2865
+ // large cannot become.
2866
+ if (fs.statSync(slicesFile).size > MAX_JSON_TEXT_BYTES) {
2867
+ console.warn(
2868
+ `Not annotating the BOM with the slices file "${slicesFile}", which is too large to embed as text.`,
2869
+ );
2870
+ continue;
2871
+ }
2055
2872
  bomJson.annotations.push({
2056
2873
  subjects: [bomJson.serialNumber],
2057
2874
  annotator: { component: bomJson.metadata.tools.components[0] },
2058
2875
  timestamp: getTimestamp(),
2059
- text: fs.readFileSync(reachablesSlicesFile, "utf8"),
2876
+ text: fs.readFileSync(slicesFile, "utf8"),
2060
2877
  });
2061
2878
  }
2062
2879
  }