@cdxgen/cdxgen 13.1.0 → 13.3.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 (283) hide show
  1. package/README.md +32 -15
  2. package/bin/cdxgen.js +243 -122
  3. package/bin/evinse.js +7 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/crypto-oid.json +88 -12
  9. package/data/remediations.json +47 -0
  10. package/data/rules/ai-bom.yaml +78 -0
  11. package/data/rules/cargo-rust.yaml +132 -0
  12. package/data/rules/mcp-servers.yaml +122 -0
  13. package/lib/audit/targets.js +6 -1
  14. package/lib/cli/bomAssembly.js +117 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +235 -5
  17. package/lib/cli/jsBom.js +108 -26
  18. package/lib/cli/jvmBom.js +68 -12
  19. package/lib/cli/managedBom.js +264 -41
  20. package/lib/cli/nativeBom.js +923 -15
  21. package/lib/core/activity.js +120 -2
  22. package/lib/core/env.js +44 -7
  23. package/lib/core/fs.js +174 -6
  24. package/lib/core/metadataProperties.js +97 -0
  25. package/lib/core/projectConfig.js +31 -4
  26. package/lib/core/userAgent.js +151 -0
  27. package/lib/ecosystems/bunutils.js +269 -101
  28. package/lib/ecosystems/cargoCache.js +364 -0
  29. package/lib/ecosystems/caxa.js +447 -7
  30. package/lib/ecosystems/cmakeResolver.js +66 -0
  31. package/lib/ecosystems/cppEvidence.js +104 -33
  32. package/lib/ecosystems/ecosystems.js +260 -99
  33. package/lib/ecosystems/golem.js +11 -1
  34. package/lib/ecosystems/gradleutils.js +185 -9
  35. package/lib/ecosystems/jsEvidence.js +19 -20
  36. package/lib/ecosystems/kosi.js +1263 -0
  37. package/lib/ecosystems/npmutils.js +110 -9
  38. package/lib/ecosystems/parsers-crystal.js +179 -0
  39. package/lib/ecosystems/parsers-dotnet.js +120 -49
  40. package/lib/ecosystems/parsers-elm.js +816 -0
  41. package/lib/ecosystems/parsers-gleam.js +1 -1
  42. package/lib/ecosystems/parsers-go.js +47 -9
  43. package/lib/ecosystems/parsers-js.js +171 -50
  44. package/lib/ecosystems/parsers-julia.js +268 -0
  45. package/lib/ecosystems/parsers-lua.js +218 -0
  46. package/lib/ecosystems/parsers-misc.js +919 -161
  47. package/lib/ecosystems/parsers-nim.js +221 -0
  48. package/lib/ecosystems/parsers-opam.js +180 -0
  49. package/lib/ecosystems/parsers-perl.js +277 -0
  50. package/lib/ecosystems/parsers-python.js +27 -15
  51. package/lib/ecosystems/parsers-r.js +148 -0
  52. package/lib/ecosystems/parsers-rebar.js +411 -0
  53. package/lib/ecosystems/parsers-rust.js +954 -33
  54. package/lib/ecosystems/parsers-spack.js +204 -0
  55. package/lib/ecosystems/parsers-stack.js +264 -0
  56. package/lib/ecosystems/parsers-terraform.js +194 -0
  57. package/lib/ecosystems/rubyutils.js +1426 -166
  58. package/lib/ecosystems/rusi.js +158 -7
  59. package/lib/evinser/evinser.js +547 -129
  60. package/lib/evinser/swiftsem.js +1914 -302
  61. package/lib/helpers/agenticToolutils.js +925 -0
  62. package/lib/helpers/bomSigner.js +652 -221
  63. package/lib/inventory/agentFormulationParser.js +57 -4
  64. package/lib/inventory/aiCollector.js +504 -10
  65. package/lib/inventory/aiInventory.js +12 -0
  66. package/lib/inventory/analyzer.js +488 -30
  67. package/lib/inventory/astWorker.js +6 -0
  68. package/lib/inventory/atomUtils.js +568 -31
  69. package/lib/inventory/auditCategories.js +8 -0
  70. package/lib/inventory/cbomutils.js +30 -0
  71. package/lib/inventory/cdxrs.js +110 -14
  72. package/lib/inventory/communityAiConfigParser.js +215 -2
  73. package/lib/inventory/depsUtils.js +183 -11
  74. package/lib/inventory/display.js +4 -5
  75. package/lib/inventory/dosai.js +627 -13
  76. package/lib/inventory/dynamic.js +39 -1
  77. package/lib/inventory/envcontext.js +9 -6
  78. package/lib/inventory/evidenceUtils.js +26 -5
  79. package/lib/inventory/fetchBatch.js +190 -19
  80. package/lib/inventory/fetchRate.js +74 -11
  81. package/lib/inventory/forgeEnricher.js +3 -2
  82. package/lib/inventory/jvmToolEnv.js +6 -5
  83. package/lib/inventory/mcpConfigParser.js +166 -2
  84. package/lib/inventory/mcpDiscovery.js +11 -0
  85. package/lib/inventory/mcpServerManifest.js +389 -0
  86. package/lib/inventory/mcpbBundle.js +419 -0
  87. package/lib/inventory/plugins.js +3 -0
  88. package/lib/inventory/protobom.js +62 -89
  89. package/lib/inventory/protobomLoader.js +4 -0
  90. package/lib/inventory/purl.js +152 -3
  91. package/lib/inventory/rustFormulationParser.js +1 -1
  92. package/lib/inventory/sbomDocument.js +2 -70
  93. package/lib/inventory/source.js +47 -23
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/traceRunner.js +210 -21
  97. package/lib/managers/binary.js +115 -24
  98. package/lib/managers/docker.js +440 -20
  99. package/lib/managers/oci.js +39 -1
  100. package/lib/parsers/cmakeLists.js +452 -0
  101. package/lib/parsers/huggingfaceUtils.js +15 -7
  102. package/lib/parsers/largeJson.js +454 -0
  103. package/lib/parsers/npmrc.js +19 -1
  104. package/lib/parsers/onnx.js +294 -0
  105. package/lib/parsers/safetensors.js +114 -0
  106. package/lib/stages/postgen/introspection/markers.js +29 -6
  107. package/lib/stages/postgen/postgen.js +237 -16
  108. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  109. package/lib/validator/bomValidator.js +6 -16
  110. package/lib/validator/index.js +48 -10
  111. package/lib/validator/reporters/console.js +3 -0
  112. package/lib/validator/reporters/json.js +1 -0
  113. package/lib/validator/reporters/sarif.js +7 -0
  114. package/package.json +27 -27
  115. package/types/lib/audit/targets.d.ts.map +1 -1
  116. package/types/lib/cli/bomAssembly.d.ts +2 -1
  117. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  118. package/types/lib/cli/cliOptions.d.ts +33 -1
  119. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  120. package/types/lib/cli/index.d.ts +2 -2
  121. package/types/lib/cli/index.d.ts.map +1 -1
  122. package/types/lib/cli/jsBom.d.ts +23 -0
  123. package/types/lib/cli/jsBom.d.ts.map +1 -1
  124. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  125. package/types/lib/cli/managedBom.d.ts +33 -0
  126. package/types/lib/cli/managedBom.d.ts.map +1 -1
  127. package/types/lib/cli/nativeBom.d.ts +88 -8
  128. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  129. package/types/lib/core/activity.d.ts.map +1 -1
  130. package/types/lib/core/env.d.ts +21 -0
  131. package/types/lib/core/env.d.ts.map +1 -1
  132. package/types/lib/core/fs.d.ts +55 -0
  133. package/types/lib/core/fs.d.ts.map +1 -1
  134. package/types/lib/core/metadataProperties.d.ts +39 -0
  135. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  136. package/types/lib/core/projectConfig.d.ts +6 -2
  137. package/types/lib/core/projectConfig.d.ts.map +1 -1
  138. package/types/lib/core/userAgent.d.ts +56 -0
  139. package/types/lib/core/userAgent.d.ts.map +1 -0
  140. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  141. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  143. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  144. package/types/lib/ecosystems/caxa.d.ts +14 -0
  145. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  146. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  147. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  148. package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
  149. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  150. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  151. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  152. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  153. package/types/lib/ecosystems/gradleutils.d.ts +40 -4
  154. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  155. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  156. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  157. package/types/lib/ecosystems/kosi.d.ts +86 -0
  158. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  159. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  160. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  161. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  162. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  163. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  164. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  165. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  166. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  167. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  168. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  169. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  170. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  171. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  172. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  173. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  174. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
  176. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  177. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  178. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  179. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  180. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  181. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  182. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  183. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  184. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  185. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  187. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  188. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  189. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  191. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  192. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  193. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  194. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  195. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  196. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  197. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  198. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  199. package/types/lib/ecosystems/rusi.d.ts +13 -0
  200. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  201. package/types/lib/evinser/evinser.d.ts +81 -2
  202. package/types/lib/evinser/evinser.d.ts.map +1 -1
  203. package/types/lib/evinser/swiftsem.d.ts +280 -15
  204. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  205. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  206. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  207. package/types/lib/helpers/bomSigner.d.ts +106 -13
  208. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  209. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  210. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  211. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  212. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  213. package/types/lib/inventory/atomUtils.d.ts +145 -3
  214. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +4 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/cdxrs.d.ts +8 -3
  219. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  220. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  221. package/types/lib/inventory/depsUtils.d.ts +15 -0
  222. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/display.d.ts.map +1 -1
  224. package/types/lib/inventory/dosai.d.ts +87 -1
  225. package/types/lib/inventory/dosai.d.ts.map +1 -1
  226. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  227. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  228. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  229. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  230. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  231. package/types/lib/inventory/fetchRate.d.ts +9 -6
  232. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  233. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  234. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  235. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  236. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  237. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  238. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  239. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  240. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  241. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  242. package/types/lib/inventory/plugins.d.ts.map +1 -1
  243. package/types/lib/inventory/protobom.d.ts +18 -13
  244. package/types/lib/inventory/protobom.d.ts.map +1 -1
  245. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  246. package/types/lib/inventory/purl.d.ts +71 -2
  247. package/types/lib/inventory/purl.d.ts.map +1 -1
  248. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  249. package/types/lib/inventory/source.d.ts.map +1 -1
  250. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  251. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  252. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  253. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  254. package/types/lib/inventory/traceRunner.d.ts +47 -1
  255. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  256. package/types/lib/managers/binary.d.ts +30 -1
  257. package/types/lib/managers/binary.d.ts.map +1 -1
  258. package/types/lib/managers/docker.d.ts +29 -0
  259. package/types/lib/managers/docker.d.ts.map +1 -1
  260. package/types/lib/managers/oci.d.ts +14 -0
  261. package/types/lib/managers/oci.d.ts.map +1 -1
  262. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  263. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  264. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  265. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  266. package/types/lib/parsers/largeJson.d.ts +52 -0
  267. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  268. package/types/lib/parsers/npmrc.d.ts +5 -0
  269. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  270. package/types/lib/parsers/onnx.d.ts +28 -0
  271. package/types/lib/parsers/onnx.d.ts.map +1 -0
  272. package/types/lib/parsers/safetensors.d.ts +22 -0
  273. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  275. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  276. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  277. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  278. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  279. package/types/lib/validator/index.d.ts +10 -1
  280. package/types/lib/validator/index.d.ts.map +1 -1
  281. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  282. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  283. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -25,16 +25,30 @@ import {
25
25
  getGradleCommand,
26
26
  getMavenCommand,
27
27
  } from "../ecosystems/gradleutils.js";
28
+ import {
29
+ analyzeKosiProject,
30
+ collectKosiApiEndpoints,
31
+ collectKosiEvidence,
32
+ collectKosiServices,
33
+ isKosiKotlinLanguage,
34
+ kosiWorkspaceComponent,
35
+ mergeKosiEvidence,
36
+ } from "../ecosystems/kosi.js";
28
37
  import {
29
38
  analyzeRusiProject,
30
39
  collectRusiEvidence,
31
40
  isRusiRustLanguage,
32
41
  } from "../ecosystems/rusi.js";
33
42
  import {
43
+ atomCompileCommandsArgs,
34
44
  buildAtomCommandEnv,
35
45
  executeAtom,
36
46
  filterAtomSlicesByExcludePatterns,
47
+ readReachablesSlices,
48
+ readSlicesFile,
49
+ removeReachablesChunkFiles,
37
50
  } from "../inventory/atomUtils.js";
51
+ import { isCycloneDxComponentTypeEnabled } from "../inventory/bomUtils.js";
38
52
  import {
39
53
  collectDosaiCryptoComponents,
40
54
  findCryptoAlgos,
@@ -42,6 +56,7 @@ import {
42
56
  import { collectMvnDependencies } from "../inventory/deps.js";
43
57
  import { mergeServices } from "../inventory/depsUtils.js";
44
58
  import {
59
+ applyDosaiReachabilityEvidence,
45
60
  collectDosaiAiComponents,
46
61
  collectDosaiDataFlowFrames,
47
62
  collectDosaiPurlEvidence,
@@ -52,9 +67,12 @@ import {
52
67
  isDosaiDotnetLanguage,
53
68
  normalizeDosaiServiceMap,
54
69
  persistDosaiSemanticsReport,
70
+ readDosaiDataFlowReport,
71
+ readDosaiMethodsReport,
55
72
  } from "../inventory/dosai.js";
56
73
  import { parseOccurrenceEvidenceLocation } from "../inventory/evidenceUtils.js";
57
74
  import { executeOsQuery } from "../managers/binary.js";
75
+ import { MAX_JSON_TEXT_BYTES, readJsonFile } from "../parsers/largeJson.js";
58
76
  import { postProcess } from "../stages/postgen/postgen.js";
59
77
  import { createOrLoad } from "./db.js";
60
78
  import { findPurlLocations } from "./scalasem.js";
@@ -86,10 +104,88 @@ function appendComponentProperties(component, properties = []) {
86
104
  }
87
105
  }
88
106
 
107
+ /**
108
+ * Merges one analyzer run's metadata properties into the BOM's metadata
109
+ * component. Analyzer metadata describes a single run, so when the input BOM
110
+ * already carries facts from an earlier run (cdxgen --profile research, then
111
+ * evinse), everything under the analyzer's own `cdx:<analyzer>:` prefix is
112
+ * replaced. Replacing by exact name would keep facts the new run no longer
113
+ * emits, such as a cdx:rusi:requestedBackend left from a fallback run.
114
+ *
115
+ * @param {Object} component Metadata component to update in place
116
+ * @param {Object[]} metadataProperties Properties from the analyzer run
117
+ */
118
+ export function mergeAnalyzerMetadataProperties(
119
+ component,
120
+ metadataProperties = [],
121
+ ) {
122
+ if (!component || !metadataProperties.length) {
123
+ return;
124
+ }
125
+ if (component.properties?.length) {
126
+ const analyzerPrefixes = [
127
+ ...new Set(
128
+ metadataProperties
129
+ .map((p) => /^cdx:[^:]+:/.exec(p.name)?.[0])
130
+ .filter(Boolean),
131
+ ),
132
+ ];
133
+ component.properties = component.properties.filter(
134
+ (p) => !analyzerPrefixes.some((prefix) => p.name?.startsWith(prefix)),
135
+ );
136
+ }
137
+ appendComponentProperties(component, metadataProperties);
138
+ }
139
+
89
140
  function filterAtomSliceData(sliceData, options = {}) {
90
141
  return filterAtomSlicesByExcludePatterns(sliceData, options.exclude);
91
142
  }
92
143
 
144
+ /**
145
+ * Build the options for the evinse phase (prepareDB, analyzeProject, and
146
+ * createEvinseFile) from the cdxgen CLI options.
147
+ *
148
+ * This object used to be assembled inline in bin/cdxgen.js, which dropped
149
+ * fields the analyzers read - most notably `exclude`, so every dosai and atom
150
+ * run triggered by `--evidence` ignored the user's `--exclude` patterns. Keep
151
+ * the wiring in one place so the forwarded fields stay testable.
152
+ *
153
+ * @param {Object} options CLI options
154
+ * @param {Object} args Parsed CLI arguments; `args._[0]` is the source directory
155
+ * @param {string} input Path of the BOM file the evinse phase enriches
156
+ * @returns {Object} Options for prepareDB, analyzeProject, and createEvinseFile
157
+ */
158
+ export function buildEvinseOptions(options, args, input) {
159
+ const sourceDir = args._?.[0] || ".";
160
+ return {
161
+ _: args._,
162
+ input,
163
+ output: options.evinseOutput,
164
+ language: options.projectType,
165
+ skipMavenCollector: false,
166
+ force: false,
167
+ withReachables: options.deep,
168
+ usagesSlicesFile: options.usagesSlicesFile,
169
+ dataFlowSlicesFile: options.dataFlowSlicesFile,
170
+ reachablesSlicesFile: options.reachablesSlicesFile,
171
+ semanticsSlicesFile: options.semanticsSlicesFile,
172
+ openapiSpecFile: options.openapiSpecFile,
173
+ componentType: options.componentType,
174
+ includeCrypto:
175
+ options.includeCrypto &&
176
+ isCycloneDxComponentTypeEnabled("cryptographic-asset", options),
177
+ specVersion: options.specVersion,
178
+ profile: options.profile,
179
+ jsonPretty: options.jsonPretty,
180
+ exclude: options.exclude,
181
+ // Absolute path of the deps slice createCsharpBom writes for --deep runs,
182
+ // so analyzeProject can reuse it instead of rerunning dosai methods.
183
+ depsSlicesFile: options.depsSlicesFile
184
+ ? resolve(sourceDir, options.depsSlicesFile)
185
+ : undefined,
186
+ };
187
+ }
188
+
93
189
  /**
94
190
  * Function to create the db for the libraries referred in the sbom.
95
191
  *
@@ -434,6 +530,11 @@ export async function createSlice(
434
530
  `Creating ${sliceType} slice for ${resolve(filePath)}. Please wait ...`,
435
531
  );
436
532
  const atomFile = join(sliceOutputDir, `${language}-app.atom`);
533
+ if (sliceType === "reachables") {
534
+ // atom writes reachables in numbered chunks next to the slices file; one
535
+ // left by an earlier run must not be read back as part of this one.
536
+ removeReachablesChunkFiles(slicesFile);
537
+ }
437
538
  let args = [sliceType];
438
539
  // Support for crypto slices aka CBOM
439
540
  if (sliceType === "reachables" && options.includeCrypto) {
@@ -478,6 +579,7 @@ export async function createSlice(
478
579
  args.push("--slice-depth");
479
580
  args.push(readEnvironmentVariable("ATOM_SLICE_DEPTH"));
480
581
  }
582
+ args.push(...atomCompileCommandsArgs(filePath, language, options));
481
583
  args.push(resolve(filePath));
482
584
  const atomExcludeEnv = buildAtomCommandEnv(options, language);
483
585
  // Execute atom
@@ -588,8 +690,9 @@ export function purlToLanguage(purl, filePath) {
588
690
  * Seed purl-location and import maps from SBOM component evidence.
589
691
  *
590
692
  * Walks the supplied components reading `internal:ImportedModules` (or
591
- * `internal:Namespaces` for php/ruby) and `evidence.occurrences` properties
592
- * to construct lookup maps used during slice analysis.
693
+ * `internal:Namespaces` for php/ruby, plus the `internal:ImportedSymbols` the
694
+ * C/C++ collector records) and `evidence.occurrences` properties to construct
695
+ * lookup maps used during slice analysis.
593
696
  *
594
697
  * @param {Object[]} components CycloneDX components from the input SBOM
595
698
  * @param {string} language Application language used to select the import property name
@@ -614,10 +717,27 @@ export function initFromSbom(components, language) {
614
717
  .forEach((v) => {
615
718
  purlImportsMap[comp.purl] = (v.value || "").split(",");
616
719
  });
720
+ // A C/C++ component records the symbols the project uses from it, which
721
+ // are what C usages are matched against. The list is pipe-separated:
722
+ // C++ symbols may contain commas.
723
+ if (["c", "cpp", "c++"].includes(language)) {
724
+ (comp.properties || [])
725
+ .filter((v) => v.name === "internal:ImportedSymbols")
726
+ .forEach((v) => {
727
+ purlImportsMap[comp.purl] = (
728
+ purlImportsMap[comp.purl] || []
729
+ ).concat((v.value || "").split("|").filter(Boolean));
730
+ });
731
+ }
617
732
  }
618
733
  if (comp.evidence.occurrences) {
734
+ // Keep the `location#line` shape so a re-run over a cdxgen BOM does not
735
+ // degrade seeded occurrences to file-only entries and duplicate the
736
+ // richer locations the fresh slice merge adds to the same set.
619
737
  purlLocationMap[comp.purl] = new Set(
620
- comp.evidence.occurrences.map((v) => v.location),
738
+ comp.evidence.occurrences.map((v) =>
739
+ v.line ? `${v.location}#${v.line}` : v.location,
740
+ ),
621
741
  );
622
742
  }
623
743
  }
@@ -639,6 +759,65 @@ function usableSlicesFile(slicesFile) {
639
759
  return fileSizeInBytes > 1024;
640
760
  }
641
761
 
762
+ /**
763
+ * Load a semantics slices file for reuse when it belongs to this project.
764
+ *
765
+ * `--semantics-slices-file` defaults to `semantics.slices.json` in the working
766
+ * directory, which is shared by every project scanned from there and also
767
+ * receives the reports of other analyzers (dosai, rusi, golem). A file is
768
+ * only reused when its shape matches the language. Swift slices must also
769
+ * describe the same project directory and must not predate the input SBOM:
770
+ * `cdxgen --evidence` writes a fresh SBOM before evinse runs, so an older
771
+ * 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.
774
+ *
775
+ * @param {string} language Project language
776
+ * @param {string} slicesFile Candidate semantics slices file
777
+ * @param {string} bomFile Input SBOM
778
+ * @param {string} dirPath Project directory
779
+ * @returns {undefined|Object} Parsed slice when it can be reused
780
+ */
781
+ export function loadReusableSemanticsSlice(
782
+ language,
783
+ slicesFile,
784
+ bomFile,
785
+ dirPath,
786
+ ) {
787
+ if (!usableSlicesFile(slicesFile)) {
788
+ return undefined;
789
+ }
790
+ let parsed;
791
+ try {
792
+ parsed = readJsonFile(slicesFile);
793
+ } catch (_e) {
794
+ return undefined;
795
+ }
796
+ if (!parsed || typeof parsed !== "object") {
797
+ return undefined;
798
+ }
799
+ 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;
804
+ }
805
+ if (!(parsed.fileIndexes || parsed.buildSymbols || parsed.moduleInfos)) {
806
+ return undefined;
807
+ }
808
+ if (parsed.projectPath && resolve(parsed.projectPath) !== resolve(dirPath)) {
809
+ return undefined;
810
+ }
811
+ try {
812
+ if (fs.statSync(slicesFile).mtimeMs < fs.statSync(bomFile).mtimeMs) {
813
+ return undefined;
814
+ }
815
+ } catch (_e) {
816
+ // Without timestamps the shape and project checks decide
817
+ }
818
+ return parsed;
819
+ }
820
+
642
821
  /**
643
822
  * Function to analyze the project
644
823
  *
@@ -724,6 +903,84 @@ export async function analyzeProject(dbObjMap, options) {
724
903
  };
725
904
  }
726
905
 
906
+ if (isKosiKotlinLanguage(language)) {
907
+ const kosiReports = analyzeKosiProject(dirPath, options);
908
+ if (kosiReports?.report && Object.keys(kosiReports.report).length) {
909
+ // The workspace component: cdxgen's Kotlin BOM carries no project
910
+ // component, so every workspace-anchored slice (reachability, the
911
+ // app's own flows) would have nothing to attach to. createEvinseFile
912
+ // re-reads the BOM from disk, so the candidate rides the artefacts
913
+ // instead of a mutation here — applied when the BOM lacks a project
914
+ // component OR carries one with no purl: every kosi evidence join is
915
+ // keyed by purl, so a purl-less component anchors nothing and is, for
916
+ // this purpose, no anchor at all (found on the sample app: Gradle
917
+ // identity with empty group and version "latest" emits no purl, and
918
+ // the reachableFromRoots/cryptoFlow evidence silently attached to
919
+ // nothing while the dependency-joined kinds landed).
920
+ const metadataComponentHasPurl = Boolean(
921
+ bomJson.metadata?.component?.purl,
922
+ );
923
+ let workspaceComponent;
924
+ if (!metadataComponentHasPurl) {
925
+ // kosi's modules[] holds the project's OWN Gradle/Maven modules
926
+ // (dependencies never appear there), so the project component is
927
+ // its root: the shortest modulePath, ties broken by name so the
928
+ // choice is deterministic. `workspaceMember` is not the selector —
929
+ // it is a String naming the owning workspace member in kosi's
930
+ // schema, empty for a single-module build and then omitted from the
931
+ // JSON entirely, so testing it against `undefined` picked nothing
932
+ // on exactly the single-module projects this fallback exists for.
933
+ const workspaceModule = (kosiReports.report.modules || [])
934
+ .filter((candidate) => candidate?.purl)
935
+ .sort(
936
+ (a, b) =>
937
+ (a.modulePath || "").length - (b.modulePath || "").length ||
938
+ (a.name || "").localeCompare(b.name || ""),
939
+ )[0];
940
+ if (workspaceModule?.purl) {
941
+ workspaceComponent = kosiWorkspaceComponent(workspaceModule);
942
+ }
943
+ }
944
+ const analysisComponents = [
945
+ bomJson.metadata?.component,
946
+ workspaceComponent,
947
+ ...components,
948
+ ].filter(Boolean);
949
+ const kosiEvidence = collectKosiEvidence(
950
+ kosiReports.report,
951
+ analysisComponents,
952
+ );
953
+ mergeKosiEvidence(
954
+ kosiEvidence,
955
+ collectKosiEvidence(
956
+ kosiReports.reachableReport || {},
957
+ analysisComponents,
958
+ ),
959
+ );
960
+ servicesMap = collectKosiServices(kosiReports.report, servicesMap);
961
+ // The INBOUND surface: the routes the app serves, with the
962
+ // authentication kosi found declared on them.
963
+ servicesMap = collectKosiApiEndpoints(kosiReports.report, servicesMap);
964
+ return {
965
+ purlLocationMap: kosiEvidence.purlLocationMap,
966
+ dataFlowFrames: kosiEvidence.dataFlowFrames,
967
+ componentPropertiesMap: kosiEvidence.componentPropertiesMap,
968
+ metadataProperties: kosiEvidence.metadataProperties,
969
+ cryptoComponents: kosiEvidence.cryptoComponents,
970
+ cryptoGeneratePurls: kosiEvidence.cryptoGeneratePurls,
971
+ servicesMap,
972
+ userDefinedTypesMap,
973
+ workspaceComponent,
974
+ };
975
+ }
976
+ return {
977
+ purlLocationMap,
978
+ servicesMap,
979
+ dataFlowFrames,
980
+ userDefinedTypesMap,
981
+ };
982
+ }
983
+
727
984
  if (isDosaiDotnetLanguage(language)) {
728
985
  if (options.profile === "research") {
729
986
  options.withDataFlow = true;
@@ -733,19 +990,32 @@ export async function analyzeProject(dbObjMap, options) {
733
990
  options.usagesSlicesFile &&
734
991
  usableSlicesFile(options.usagesSlicesFile)
735
992
  ) {
736
- usageSlice = JSON.parse(
737
- fs.readFileSync(options.usagesSlicesFile, "utf-8"),
738
- );
993
+ usageSlice = readDosaiMethodsReport(options.usagesSlicesFile);
739
994
  usagesSlicesFile = options.usagesSlicesFile;
995
+ } else if (usableSlicesFile(options.depsSlicesFile)) {
996
+ // createCsharpBom already ran dosai methods on this tree for --deep runs
997
+ // and wrote the deps slice. For dosai, that output IS the usages slice,
998
+ // so reuse it instead of invoking dosai a second time.
999
+ if (DEBUG_MODE) {
1000
+ console.log(
1001
+ `Reusing the dosai deps slices file "${options.depsSlicesFile}" for the usages analysis.`,
1002
+ );
1003
+ }
1004
+ usageSlice = readDosaiMethodsReport(options.depsSlicesFile);
1005
+ usagesSlicesFile = options.depsSlicesFile;
740
1006
  } else {
741
1007
  retMap = await createSlice(language, dirPath, "usages", options);
742
1008
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
743
- usageSlice = JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8"));
1009
+ usageSlice = readDosaiMethodsReport(retMap.slicesFile);
744
1010
  usagesSlicesFile = retMap.slicesFile;
745
1011
  }
746
1012
  }
747
1013
  if (usageSlice && Object.keys(usageSlice).length) {
748
1014
  const dosaiEvidence = collectDosaiPurlEvidence(usageSlice, components);
1015
+ // Confidence/evidence facts ride along as properties so an unbuilt tree (Low
1016
+ // confidence, unresolved evidence) stays distinguishable from an only-imported
1017
+ // package in the emitted BOM.
1018
+ applyDosaiReachabilityEvidence(usageSlice, components);
749
1019
  for (const [purl, locations] of Object.entries(
750
1020
  dosaiEvidence.purlLocationMap,
751
1021
  )) {
@@ -770,16 +1040,12 @@ export async function analyzeProject(dbObjMap, options) {
770
1040
  safeExistsSync(options.dataFlowSlicesFile)
771
1041
  ) {
772
1042
  dataFlowSlicesFile = options.dataFlowSlicesFile;
773
- dataFlowSlice = JSON.parse(
774
- fs.readFileSync(options.dataFlowSlicesFile, "utf-8"),
775
- );
1043
+ dataFlowSlice = readDosaiDataFlowReport(options.dataFlowSlicesFile);
776
1044
  } else {
777
1045
  retMap = await createSlice(language, dirPath, "data-flow", options);
778
1046
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
779
1047
  dataFlowSlicesFile = retMap.slicesFile;
780
- dataFlowSlice = JSON.parse(
781
- fs.readFileSync(retMap.slicesFile, "utf-8"),
782
- );
1048
+ dataFlowSlice = readDosaiDataFlowReport(retMap.slicesFile);
783
1049
  }
784
1050
  }
785
1051
  if (dataFlowSlice && Object.keys(dataFlowSlice).length) {
@@ -826,7 +1092,7 @@ export async function analyzeProject(dbObjMap, options) {
826
1092
  ) {
827
1093
  reachablesSlicesFile = options.reachablesSlicesFile;
828
1094
  reachablesSlice = filterAtomSliceData(
829
- JSON.parse(fs.readFileSync(options.reachablesSlicesFile, "utf-8")),
1095
+ readReachablesSlices(options.reachablesSlicesFile),
830
1096
  options,
831
1097
  );
832
1098
  } else {
@@ -834,7 +1100,7 @@ export async function analyzeProject(dbObjMap, options) {
834
1100
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
835
1101
  reachablesSlicesFile = retMap.slicesFile;
836
1102
  reachablesSlice = filterAtomSliceData(
837
- JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8")),
1103
+ readReachablesSlices(retMap.slicesFile),
838
1104
  options,
839
1105
  );
840
1106
  }
@@ -850,7 +1116,7 @@ export async function analyzeProject(dbObjMap, options) {
850
1116
  // Reuse existing usages slices
851
1117
  if (options.usagesSlicesFile && usableSlicesFile(options.usagesSlicesFile)) {
852
1118
  usageSlice = filterAtomSliceData(
853
- JSON.parse(fs.readFileSync(options.usagesSlicesFile, "utf-8")),
1119
+ readSlicesFile(options.usagesSlicesFile, "usages"),
854
1120
  options,
855
1121
  );
856
1122
  usagesSlicesFile = options.usagesSlicesFile;
@@ -859,7 +1125,7 @@ export async function analyzeProject(dbObjMap, options) {
859
1125
  retMap = await createSlice(language, dirPath, "usages", options);
860
1126
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
861
1127
  usageSlice = filterAtomSliceData(
862
- JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8")),
1128
+ readSlicesFile(retMap.slicesFile, "usages"),
863
1129
  options,
864
1130
  );
865
1131
  usagesSlicesFile = retMap.slicesFile;
@@ -885,21 +1151,20 @@ export async function analyzeProject(dbObjMap, options) {
885
1151
  components.length
886
1152
  ) {
887
1153
  // Reuse existing semantics slices for swift and scala
888
- if (
889
- options.semanticsSlicesFile &&
890
- safeExistsSync(options.semanticsSlicesFile)
891
- ) {
892
- semanticsSlice = JSON.parse(
893
- fs.readFileSync(options.semanticsSlicesFile, "utf-8"),
894
- );
1154
+ const reusableSlice = loadReusableSemanticsSlice(
1155
+ language,
1156
+ options.semanticsSlicesFile,
1157
+ bomFile,
1158
+ dirPath,
1159
+ );
1160
+ if (reusableSlice) {
1161
+ semanticsSlice = reusableSlice;
895
1162
  semanticsSlicesFile = options.semanticsSlicesFile;
896
1163
  } else if (PROJECT_TYPE_ALIASES.swift.includes(language)) {
897
1164
  // Generate our own slices for swift
898
1165
  retMap = await createSlice(language, dirPath, "semantics", options);
899
1166
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
900
- semanticsSlice = JSON.parse(
901
- fs.readFileSync(retMap.slicesFile, "utf-8"),
902
- );
1167
+ semanticsSlice = readSlicesFile(retMap.slicesFile, "semantics");
903
1168
  semanticsSlicesFile = retMap.slicesFile;
904
1169
  }
905
1170
  }
@@ -936,7 +1201,7 @@ export async function analyzeProject(dbObjMap, options) {
936
1201
  ) {
937
1202
  dataFlowSlicesFile = options.dataFlowSlicesFile;
938
1203
  dataFlowSlice = filterAtomSliceData(
939
- JSON.parse(fs.readFileSync(options.dataFlowSlicesFile, "utf-8")),
1204
+ readSlicesFile(options.dataFlowSlicesFile, "data-flow"),
940
1205
  options,
941
1206
  );
942
1207
  } else if (!PROJECT_TYPE_ALIASES.scala.includes(language)) {
@@ -944,7 +1209,7 @@ export async function analyzeProject(dbObjMap, options) {
944
1209
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
945
1210
  dataFlowSlicesFile = retMap.slicesFile;
946
1211
  dataFlowSlice = filterAtomSliceData(
947
- JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8")),
1212
+ readSlicesFile(retMap.slicesFile, "data-flow"),
948
1213
  options,
949
1214
  );
950
1215
  }
@@ -1303,6 +1568,14 @@ export async function parseSliceUsages(
1303
1568
  /**
1304
1569
  * Method to parse semantic slice data. Currently supported for swift and scala languages.
1305
1570
  *
1571
+ * Swift slices created by cdxgen record, for every indexed source file, the
1572
+ * lines that reference each dependency module (`moduleReferences`); the module
1573
+ * of a reference is the declaring module recovered from the compiler-resolved
1574
+ * USR. Components are mapped to their package's modules through the
1575
+ * `packages` slice, so every occurrence is backed by a resolved reference.
1576
+ * Slices without `moduleReferences` (created by older cdxgen versions) fall
1577
+ * back to matching symbol names.
1578
+ *
1306
1579
  * @param {String} language Project language.
1307
1580
  * @param {Array} components Components from the input SBOM
1308
1581
  * @param {Object} semanticsSlice Semantic slice data
@@ -1316,86 +1589,47 @@ export function parseSemanticSlices(language, components, semanticsSlice) {
1316
1589
  if (!components) {
1317
1590
  return undefined;
1318
1591
  }
1319
- const componentNamePurlMap = {};
1320
- const componentSymbolsMap = {};
1321
- const allObfuscationsMap = {};
1322
- // We have two attributes in the semantics slice to expand a given module to its constituent symbols
1323
- // - A less precise buildSymbols, which is obtained by parsing the various output-file-map.json files
1324
- // - A granular and precise moduleInfos, which has the exact classes, protocols, enums etc belonging to each module
1325
- // The objective then is to build the purlLocationMap, which is the list of locations where a given purl is used
1326
- // For this, we have two attributes:
1327
- // - A simpler fileStructures attribute that has the mapping between a swift file and the list of referenced types
1328
- // - A granular fileIndexes attribute that contains information about the clang modules and line number information
1329
-
1330
- // We first need to map out the component names to their purls
1331
- // This is because the semantics slice use the module names everywhere
1592
+ const fileIndexes = semanticsSlice?.fileIndexes || {};
1593
+ const hasModuleReferences = Object.values(fileIndexes).some(
1594
+ (afileIndex) => afileIndex?.moduleReferences,
1595
+ );
1596
+ const purlLocationsSet = {};
1332
1597
  for (const comp of components) {
1333
- componentNamePurlMap[comp.name] = comp.purl;
1334
- if (!componentSymbolsMap[comp.name]) {
1335
- componentSymbolsMap[comp.name] = new Set();
1336
- }
1337
- if (semanticsSlice?.buildSymbols[comp.name]) {
1338
- for (const asym of semanticsSlice.buildSymbols[comp.name]) {
1339
- componentSymbolsMap[comp.name].add(asym);
1340
- }
1341
- }
1342
- const moduleInfo = semanticsSlice?.moduleInfos[comp.name] || {};
1343
- if (moduleInfo.classes) {
1344
- for (const asym of moduleInfo.classes) {
1345
- componentSymbolsMap[comp.name].add(asym);
1346
- }
1347
- }
1348
- if (moduleInfo.protocols) {
1349
- for (const asym of moduleInfo.protocols) {
1350
- componentSymbolsMap[comp.name].add(asym);
1351
- }
1352
- }
1353
- if (moduleInfo.enums) {
1354
- for (const asym of moduleInfo.enums) {
1355
- componentSymbolsMap[comp.name].add(asym);
1356
- }
1598
+ // Local packages and the root application have no purl, so there is
1599
+ // nothing the evidence could be attached to downstream
1600
+ if (!comp?.purl) {
1601
+ continue;
1357
1602
  }
1358
- // Now collect the method signatures from class and protocol methods
1359
- if (moduleInfo.classMethods) {
1360
- for (const aclassName of Object.keys(moduleInfo.classMethods)) {
1361
- for (const asym of moduleInfo.classMethods[aclassName]) {
1362
- componentSymbolsMap[comp.name].add(asym);
1363
- }
1364
- }
1603
+ const moduleNames = resolveSwiftComponentModules(comp, semanticsSlice);
1604
+ if (!moduleNames.size) {
1605
+ continue;
1365
1606
  }
1366
- if (moduleInfo.protocolMethods) {
1367
- for (const aclassName of Object.keys(moduleInfo.protocolMethods)) {
1368
- for (const asym of moduleInfo.protocolMethods[aclassName]) {
1369
- componentSymbolsMap[comp.name].add(asym);
1607
+ const locations = purlLocationsSet[comp.purl] || new Set();
1608
+ if (hasModuleReferences) {
1609
+ for (const [aswiftFile, afileIndex] of Object.entries(fileIndexes)) {
1610
+ for (const [amodule, lines] of Object.entries(
1611
+ afileIndex?.moduleReferences || {},
1612
+ )) {
1613
+ if (!moduleNames.has(amodule)) {
1614
+ continue;
1615
+ }
1616
+ for (const aline of lines) {
1617
+ locations.add(`${aswiftFile}#${aline}`);
1618
+ }
1370
1619
  }
1371
1620
  }
1372
- }
1373
- // Build a large obfuscation map
1374
- if (moduleInfo.obfuscationMap) {
1375
- for (const asym of Object.keys(moduleInfo.obfuscationMap)) {
1376
- allObfuscationsMap[asym] = moduleInfo.obfuscationMap[asym];
1377
- }
1378
- }
1379
- }
1380
- const purlLocationsSet = {};
1381
- // We now have a good set of data in componentSymbolsMap and allObfuscationsMap
1382
- // We can iterate these symbols and check if they exist under fileIndexes.symbolLocations
1383
- for (const compName of Object.keys(componentSymbolsMap)) {
1384
- const compSymbols = Array.from(componentSymbolsMap[compName]);
1385
- const searchHits = searchSymbolLocations(
1386
- compSymbols,
1387
- semanticsSlice?.fileIndexes,
1388
- );
1389
- // We have an occurrence hit. Let's populate the purlLocationMap
1390
- if (searchHits?.length) {
1391
- const locations =
1392
- purlLocationsSet[componentNamePurlMap[compName]] || new Set();
1393
- for (const ahit of searchHits) {
1621
+ } else {
1622
+ for (const ahit of searchSymbolLocations(
1623
+ collectLegacySwiftSymbols(moduleNames, semanticsSlice),
1624
+ fileIndexes,
1625
+ )) {
1394
1626
  for (const aline of ahit.lineNumbers) {
1395
1627
  locations.add(`${ahit.file}#${aline}`);
1396
1628
  }
1397
1629
  }
1398
- purlLocationsSet[componentNamePurlMap[compName]] = locations;
1630
+ }
1631
+ if (locations.size) {
1632
+ purlLocationsSet[comp.purl] = locations;
1399
1633
  }
1400
1634
  }
1401
1635
  const purlLocationMap = {};
@@ -1405,22 +1639,184 @@ export function parseSemanticSlices(language, components, semanticsSlice) {
1405
1639
  return { purlLocationMap };
1406
1640
  }
1407
1641
 
1408
- function searchSymbolLocations(compSymbols, fileIndexes) {
1409
- const searchHits = [];
1410
- if (!fileIndexes) {
1642
+ /**
1643
+ * Normalise a repository location or Swift purl into `host/owner/name`.
1644
+ *
1645
+ * @param {String} value Repository URL, scp-style git location, or swift purl
1646
+ * @returns {undefined|String} Lowercase `host/path` without scheme and `.git`
1647
+ */
1648
+ function normalizeSwiftPackageLocation(value) {
1649
+ if (!value || typeof value !== "string") {
1411
1650
  return undefined;
1412
1651
  }
1413
- for (const aswiftFile of Object.keys(fileIndexes)) {
1414
- if (!fileIndexes[aswiftFile].symbolLocations) {
1652
+ let text = value.trim();
1653
+ if (text.startsWith("pkg:swift/")) {
1654
+ text = text.slice("pkg:swift/".length).split("?")[0].split("#")[0];
1655
+ const at = text.lastIndexOf("@");
1656
+ if (at > 0) {
1657
+ text = text.slice(0, at);
1658
+ }
1659
+ try {
1660
+ text = decodeURIComponent(text);
1661
+ } catch (_e) {
1662
+ // Keep the raw value
1663
+ }
1664
+ } else {
1665
+ text = text.split("?")[0].split("#")[0];
1666
+ const scheme = text.indexOf("://");
1667
+ if (scheme >= 0) {
1668
+ text = text.slice(scheme + 3);
1669
+ // Drop userinfo such as git@
1670
+ const slash = text.indexOf("/");
1671
+ const at = text.indexOf("@");
1672
+ if (at >= 0 && (slash < 0 || at < slash)) {
1673
+ text = text.slice(at + 1);
1674
+ }
1675
+ } else if (text.includes("@") && text.includes(":")) {
1676
+ // scp-style git@github.com:owner/name.git
1677
+ text = text.slice(text.indexOf("@") + 1).replace(":", "/");
1678
+ } else {
1679
+ return undefined;
1680
+ }
1681
+ }
1682
+ text = text.replace(/\/+$/, "");
1683
+ if (text.endsWith(".git")) {
1684
+ text = text.slice(0, -4);
1685
+ }
1686
+ if (text.startsWith("www.")) {
1687
+ text = text.slice(4);
1688
+ }
1689
+ return text.toLowerCase() || undefined;
1690
+ }
1691
+
1692
+ /**
1693
+ * Resolve the Swift module names that belong to a SBOM component.
1694
+ *
1695
+ * A component is named after its package repository or identity while the
1696
+ * semantic slices are keyed by module names. With the `packages` slice, a
1697
+ * package cloned from a repository matches the component whose purl names
1698
+ * the same repository, so a fork with the same name never inherits another
1699
+ * package's evidence; registry and local packages, which have no repository
1700
+ * URL, match by identity or name. Older slices only carry the
1701
+ * `packageModules` aliases (identity, package name, repository name), which
1702
+ * are matched against the component name and the `cdx:swift:packageName`
1703
+ * property, so that `swift-argument-parser` resolves to its `ArgumentParser`
1704
+ * module.
1705
+ *
1706
+ * @param {Object} comp SBOM component
1707
+ * @param {Object} semanticsSlice Semantic slice data
1708
+ * @returns {Set<string>} Module names associated with the component
1709
+ */
1710
+ function resolveSwiftComponentModules(comp, semanticsSlice) {
1711
+ const moduleNames = new Set();
1712
+ if (!comp?.name) {
1713
+ return moduleNames;
1714
+ }
1715
+ const aliases = new Set(
1716
+ [comp.name, getPropertyValue(comp, "cdx:swift:packageName")]
1717
+ .filter(Boolean)
1718
+ .map((alias) => String(alias).toLowerCase()),
1719
+ );
1720
+ if (Array.isArray(semanticsSlice?.packages)) {
1721
+ const componentLocation = normalizeSwiftPackageLocation(comp.purl);
1722
+ for (const apkg of semanticsSlice.packages) {
1723
+ const packageLocation = normalizeSwiftPackageLocation(apkg?.location);
1724
+ const matches =
1725
+ packageLocation && componentLocation
1726
+ ? packageLocation === componentLocation
1727
+ : [apkg?.identity, apkg?.name].some(
1728
+ (alias) => alias && aliases.has(String(alias).toLowerCase()),
1729
+ );
1730
+ if (matches) {
1731
+ for (const amodule of apkg.modules || []) {
1732
+ moduleNames.add(amodule);
1733
+ }
1734
+ }
1735
+ }
1736
+ return moduleNames;
1737
+ }
1738
+ const packageModules = semanticsSlice?.packageModules || {};
1739
+ for (const alias of new Set([comp.name, ...aliases])) {
1740
+ if (Array.isArray(packageModules[alias])) {
1741
+ for (const amodule of packageModules[alias]) {
1742
+ moduleNames.add(amodule);
1743
+ }
1744
+ }
1745
+ }
1746
+ // Modules named after the package (Yams, SWXMLHash) still match
1747
+ if (!moduleNames.size) {
1748
+ moduleNames.add(comp.name);
1749
+ }
1750
+ return moduleNames;
1751
+ }
1752
+
1753
+ function getPropertyValue(comp, propertyName) {
1754
+ for (const aproperty of comp?.properties || []) {
1755
+ if (aproperty?.name === propertyName) {
1756
+ return aproperty.value;
1757
+ }
1758
+ }
1759
+ return undefined;
1760
+ }
1761
+
1762
+ /**
1763
+ * Collect the symbol names of a component's modules from slices that predate
1764
+ * `moduleReferences`, using the moduleInfos data and the coarser buildSymbols
1765
+ * derived from the build's output file maps.
1766
+ *
1767
+ * @param {Set<String>} moduleNames Module names of the component
1768
+ * @param {Object} semanticsSlice Semantic slice data
1769
+ * @returns {Object} `{ symbols, freeFunctions }` name sets
1770
+ */
1771
+ function collectLegacySwiftSymbols(moduleNames, semanticsSlice) {
1772
+ const symbols = new Set();
1773
+ const freeFunctions = new Set();
1774
+ for (const moduleName of moduleNames) {
1775
+ for (const asym of semanticsSlice?.buildSymbols?.[moduleName] || []) {
1776
+ symbols.add(asym);
1777
+ }
1778
+ const moduleInfo = semanticsSlice?.moduleInfos?.[moduleName] || {};
1779
+ for (const asym of [
1780
+ ...(moduleInfo.classes || []),
1781
+ ...(moduleInfo.protocols || []),
1782
+ ...(moduleInfo.enums || []),
1783
+ ]) {
1784
+ symbols.add(asym);
1785
+ }
1786
+ for (const methods of [
1787
+ ...Object.values(moduleInfo.classMethods || {}),
1788
+ ...Object.values(moduleInfo.protocolMethods || {}),
1789
+ ]) {
1790
+ for (const asym of methods || []) {
1791
+ symbols.add(asym);
1792
+ }
1793
+ }
1794
+ for (const asym of moduleInfo.functions || []) {
1795
+ freeFunctions.add(asym);
1796
+ }
1797
+ }
1798
+ return { symbols, freeFunctions };
1799
+ }
1800
+
1801
+ function searchSymbolLocations(componentSymbols, fileIndexes) {
1802
+ const searchHits = [];
1803
+ const { symbols, freeFunctions } = componentSymbols;
1804
+ for (const aswiftFile of Object.keys(fileIndexes || {})) {
1805
+ const symbolLocations = fileIndexes[aswiftFile]?.symbolLocations;
1806
+ if (!symbolLocations) {
1415
1807
  continue;
1416
1808
  }
1417
- for (const asym of Object.keys(fileIndexes[aswiftFile].symbolLocations)) {
1418
- const lineNumbers = fileIndexes[aswiftFile].symbolLocations[asym];
1419
- if (compSymbols.includes(asym)) {
1809
+ for (const asym of Object.keys(symbolLocations)) {
1810
+ // The index records free functions by their full selector
1811
+ // (`dump(object:canonical:...)`) while module interfaces give the base
1812
+ // name. Only top-level functions are matched by base name: members such
1813
+ // as `encode(to:)` exist in nearly every module
1814
+ const baseName = asym.includes("(") ? asym.split("(")[0] : undefined;
1815
+ if (symbols.has(asym) || (baseName && freeFunctions.has(baseName))) {
1420
1816
  searchHits.push({
1421
1817
  file: aswiftFile,
1422
1818
  symbol: asym,
1423
- lineNumbers,
1819
+ lineNumbers: symbolLocations[asym],
1424
1820
  });
1425
1821
  }
1426
1822
  }
@@ -1833,6 +2229,7 @@ export async function createEvinseFile(sliceArtefacts, options) {
1833
2229
  cryptoComponents,
1834
2230
  aiComponents,
1835
2231
  cryptoGeneratePurls,
2232
+ workspaceComponent,
1836
2233
  } = sliceArtefacts;
1837
2234
  const bomFile = options.input;
1838
2235
  const evinseOutFile = options.output;
@@ -1840,7 +2237,22 @@ export async function createEvinseFile(sliceArtefacts, options) {
1840
2237
  const components = bomJson.components || [];
1841
2238
  // Clear existing annotations
1842
2239
  bomJson.annotations = [];
1843
- appendComponentProperties(bomJson.metadata?.component, metadataProperties);
2240
+ if (workspaceComponent && !bomJson.metadata?.component?.purl) {
2241
+ if (bomJson.metadata?.component) {
2242
+ // A purl-less project component: keep cdxgen's own identity fields
2243
+ // and supply the purl kosi's module discovery published, so the
2244
+ // evidence loop below can anchor workspace evidence to it.
2245
+ bomJson.metadata.component.purl = workspaceComponent.purl;
2246
+ bomJson.metadata.component["bom-ref"] ??= workspaceComponent.purl;
2247
+ } else {
2248
+ bomJson.metadata ??= {};
2249
+ bomJson.metadata.component = workspaceComponent;
2250
+ }
2251
+ }
2252
+ mergeAnalyzerMetadataProperties(
2253
+ bomJson.metadata?.component,
2254
+ metadataProperties,
2255
+ );
1844
2256
  let occEvidencePresent = false;
1845
2257
  let csEvidencePresent = false;
1846
2258
  let servicesPresent = false;
@@ -1899,8 +2311,15 @@ export async function createEvinseFile(sliceArtefacts, options) {
1899
2311
  servicesPresent = true;
1900
2312
  }
1901
2313
  // Add the crypto components to the components list
2314
+ // Multi-type scans may have collected the same crypto assets during BOM
2315
+ // generation, so skip any bom-ref already present to keep the BOM valid.
1902
2316
  if (cryptoComponents?.length) {
1903
- bomJson.components = bomJson.components.concat(cryptoComponents);
2317
+ const existingRefs = new Set(
2318
+ bomJson.components.map((comp) => comp["bom-ref"]).filter(Boolean),
2319
+ );
2320
+ bomJson.components = bomJson.components.concat(
2321
+ cryptoComponents.filter((comp) => !existingRefs.has(comp["bom-ref"])),
2322
+ );
1904
2323
  }
1905
2324
  // Add the AI components (machine-learning-model, dataset) discovered by dosai
1906
2325
  if (aiComponents?.length) {
@@ -1925,28 +2344,27 @@ export async function createEvinseFile(sliceArtefacts, options) {
1925
2344
  bomJson.dependencies = newDependencies;
1926
2345
  }
1927
2346
  if (options.annotate) {
1928
- if (usagesSlicesFile && safeExistsSync(usagesSlicesFile)) {
1929
- bomJson.annotations.push({
1930
- subjects: [bomJson.serialNumber],
1931
- annotator: { component: bomJson.metadata.tools.components[0] },
1932
- timestamp: getTimestamp(),
1933
- text: fs.readFileSync(usagesSlicesFile, "utf8"),
1934
- });
1935
- }
1936
- if (dataFlowSlicesFile && safeExistsSync(dataFlowSlicesFile)) {
1937
- bomJson.annotations.push({
1938
- subjects: [bomJson.serialNumber],
1939
- annotator: { component: bomJson.metadata.tools.components[0] },
1940
- timestamp: getTimestamp(),
1941
- text: fs.readFileSync(dataFlowSlicesFile, "utf8"),
1942
- });
1943
- }
1944
- if (reachablesSlicesFile && safeExistsSync(reachablesSlicesFile)) {
2347
+ for (const slicesFile of [
2348
+ usagesSlicesFile,
2349
+ dataFlowSlicesFile,
2350
+ reachablesSlicesFile,
2351
+ ]) {
2352
+ if (!slicesFile || !safeExistsSync(slicesFile)) {
2353
+ continue;
2354
+ }
2355
+ // The annotation embeds the slice as one string, which a file this
2356
+ // large cannot become.
2357
+ if (fs.statSync(slicesFile).size > MAX_JSON_TEXT_BYTES) {
2358
+ console.warn(
2359
+ `Not annotating the BOM with the slices file "${slicesFile}", which is too large to embed as text.`,
2360
+ );
2361
+ continue;
2362
+ }
1945
2363
  bomJson.annotations.push({
1946
2364
  subjects: [bomJson.serialNumber],
1947
2365
  annotator: { component: bomJson.metadata.tools.components[0] },
1948
2366
  timestamp: getTimestamp(),
1949
- text: fs.readFileSync(reachablesSlicesFile, "utf8"),
2367
+ text: fs.readFileSync(slicesFile, "utf8"),
1950
2368
  });
1951
2369
  }
1952
2370
  }