@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
@@ -1,5 +1,14 @@
1
- import { readFileSync } from "node:fs";
2
- import { basename, delimiter, join, resolve } from "node:path";
1
+ import { Buffer } from "node:buffer";
2
+ import {
3
+ closeSync,
4
+ openSync,
5
+ readFileSync,
6
+ readSync,
7
+ realpathSync,
8
+ statSync,
9
+ } from "node:fs";
10
+ import { basename, delimiter, isAbsolute, join, resolve } from "node:path";
11
+ import process from "node:process";
3
12
 
4
13
  import { DEBUG_MODE } from "../core/activity.js";
5
14
  import { recordDegradation } from "../core/buildLedger.js";
@@ -9,8 +18,14 @@ import {
9
18
  safeMkdtempSync,
10
19
  safeRmSync,
11
20
  safeSpawnSync,
21
+ safeWriteChunksSync,
12
22
  safeWriteSync,
13
23
  } from "../core/fs.js";
24
+ import {
25
+ indexJsonObject,
26
+ MAX_JSON_TEXT_BYTES,
27
+ readJsonRange,
28
+ } from "../parsers/largeJson.js";
14
29
  import {
15
30
  addDosaiSetValue,
16
31
  buildDosaiPurlAliasMap,
@@ -26,6 +41,15 @@ const DOTNET_LANGUAGES = new Set([
26
41
  "cs",
27
42
  "dotnet",
28
43
  "dotnet-framework",
44
+ // Versioned CLI types (`-t dotnet11`): the project-type aliases map them to
45
+ // csharp for dispatch, but gates such as shouldCollectDosaiCrypto inspect the
46
+ // raw options.projectType, so the versioned spellings must resolve here too.
47
+ "dotnet6",
48
+ "dotnet7",
49
+ "dotnet8",
50
+ "dotnet9",
51
+ "dotnet10",
52
+ "dotnet11",
29
53
  "f#",
30
54
  "fsharp",
31
55
  "fs",
@@ -172,11 +196,318 @@ function safeDosaiPatternPacks(value) {
172
196
  .join(delimiter);
173
197
  }
174
198
 
199
+ const MAX_DOSAI_EXCLUDES = 256;
200
+ const MAX_BRACE_RANGE = 64;
201
+ const GLOB_ESCAPABLE = "*?[]{}()!+@,";
202
+ // Characters a translated dosai glob may contain. dosai runs without a shell,
203
+ // but keeping shell metacharacters out means a pattern can never change
204
+ // meaning even if that ever changes. Anything else is reported as skipped.
205
+ const SAFE_DOSAI_GLOB = /^[\p{L}\p{N} _.+@#%~=/*?-]+$/u;
206
+ // Characters a dosai executable path may contain, including Windows drive
207
+ // letters and "Program Files (x86)". Shell metacharacters are not allowed.
208
+ const SAFE_DOSAI_EXECUTABLE = /^[\p{L}\p{N} _.,+@#%~=/\\:()-]+$/u;
209
+ const warnedDosaiMessages = new Set();
210
+ const dosaiWithoutExclude = new Set();
211
+
212
+ function warnDosaiOnce(message) {
213
+ if (!warnedDosaiMessages.has(message)) {
214
+ warnedDosaiMessages.add(message);
215
+ console.warn(message);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Find the brace group starting at `open` and return its closing index and the
221
+ * top-level comma positions, or undefined when the brace is never closed.
222
+ */
223
+ function scanBraceGroup(pattern, open) {
224
+ let depth = 0;
225
+ const commas = [];
226
+ for (let i = open; i < pattern.length; i++) {
227
+ const char = pattern[i];
228
+ if (char === "{") {
229
+ depth++;
230
+ } else if (char === "}") {
231
+ depth--;
232
+ if (depth === 0) {
233
+ return { close: i, commas };
234
+ }
235
+ } else if (char === "," && depth === 1) {
236
+ commas.push(i);
237
+ }
238
+ }
239
+ return undefined;
240
+ }
241
+
242
+ function braceAlternatives(pattern, open, group) {
243
+ const body = pattern.slice(open + 1, group.close);
244
+ if (group.commas.length) {
245
+ const bounds = [open, ...group.commas, group.close];
246
+ const alternatives = [];
247
+ for (let i = 0; i < bounds.length - 1; i++) {
248
+ alternatives.push(pattern.slice(bounds[i] + 1, bounds[i + 1]));
249
+ }
250
+ return alternatives;
251
+ }
252
+ // minimatch also expands small integer ranges such as {1..3}.
253
+ const rangeParts = body.split("..");
254
+ if (
255
+ rangeParts.length === 2 &&
256
+ /^-?\d+$/.test(rangeParts[0]) &&
257
+ /^-?\d+$/.test(rangeParts[1])
258
+ ) {
259
+ const from = Number.parseInt(rangeParts[0], 10);
260
+ const to = Number.parseInt(rangeParts[1], 10);
261
+ if (Math.abs(to - from) < MAX_BRACE_RANGE) {
262
+ const step = from <= to ? 1 : -1;
263
+ const alternatives = [];
264
+ for (let value = from; value !== to + step; value += step) {
265
+ alternatives.push(String(value));
266
+ }
267
+ return alternatives;
268
+ }
269
+ }
270
+ return undefined;
271
+ }
272
+
273
+ /**
274
+ * Expand brace groups the way minimatch does. A group without a comma or a
275
+ * numeric range, and a group written as "${...}", stays literal text.
276
+ */
277
+ function expandGlobBraces(pattern, limit = MAX_DOSAI_EXCLUDES) {
278
+ let open = pattern.indexOf("{");
279
+ while (open !== -1) {
280
+ const group = scanBraceGroup(pattern, open);
281
+ if (!group) {
282
+ return [pattern];
283
+ }
284
+ const alternatives =
285
+ pattern[open - 1] === "$"
286
+ ? undefined
287
+ : braceAlternatives(pattern, open, group);
288
+ if (alternatives) {
289
+ const head = pattern.slice(0, open);
290
+ const tail = pattern.slice(group.close + 1);
291
+ const expanded = [];
292
+ for (const alternative of alternatives) {
293
+ for (const entry of expandGlobBraces(
294
+ `${head}${alternative}${tail}`,
295
+ limit,
296
+ )) {
297
+ expanded.push(entry);
298
+ if (expanded.length >= limit) {
299
+ return expanded;
300
+ }
301
+ }
302
+ }
303
+ return expanded;
304
+ }
305
+ open = pattern.indexOf("{", group.close + 1);
306
+ }
307
+ return [pattern];
308
+ }
309
+
310
+ /**
311
+ * Normalise separators the way the atom exclude filter does: a backslash
312
+ * before a glob character is an escape, any other backslash is a Windows path
313
+ * separator. Returns undefined for escapes, which dosai cannot express.
314
+ */
315
+ function normalizeDosaiGlobSeparators(pattern) {
316
+ let normalized = "";
317
+ for (let i = 0; i < pattern.length; i++) {
318
+ const char = pattern[i];
319
+ if (char !== "\\") {
320
+ normalized += char;
321
+ continue;
322
+ }
323
+ if (GLOB_ESCAPABLE.includes(pattern[i + 1] || "")) {
324
+ return undefined;
325
+ }
326
+ normalized += "/";
327
+ }
328
+ return normalized;
329
+ }
330
+
331
+ function hasUnsupportedGlobSyntax(glob) {
332
+ if (glob.includes("[") || glob.includes("]")) {
333
+ return true;
334
+ }
335
+ for (let i = 1; i < glob.length; i++) {
336
+ if (glob[i] === "(" && "@?+*!".includes(glob[i - 1])) {
337
+ return true;
338
+ }
339
+ }
340
+ return false;
341
+ }
342
+
343
+ /**
344
+ * Rebase an absolute glob onto one of the scanned roots. Matching ignores case
345
+ * on macOS and Windows, as cdxgen's own glob does there.
346
+ */
347
+ function rebaseAbsoluteGlob(glob, roots) {
348
+ const ignoreCase =
349
+ process.platform === "darwin" || process.platform === "win32";
350
+ const candidate = ignoreCase ? glob.toLowerCase() : glob;
351
+ for (const root of roots) {
352
+ let rootPath = root.replaceAll("\\", "/");
353
+ while (rootPath.endsWith("/")) {
354
+ rootPath = rootPath.slice(0, -1);
355
+ }
356
+ const rootPrefix = `${rootPath}/`;
357
+ const prefix = ignoreCase ? rootPrefix.toLowerCase() : rootPrefix;
358
+ if (candidate.startsWith(prefix)) {
359
+ const rest = glob.slice(prefix.length);
360
+ return rest ? rest : undefined;
361
+ }
362
+ }
363
+ return undefined;
364
+ }
365
+
366
+ /**
367
+ * Translate cdxgen --exclude globs into dosai --exclude globs.
368
+ *
369
+ * Dosai is an evidence analyzer, so the translation follows the exclude
370
+ * filter cdxgen already applies to atom evidence slices
371
+ * (`globPatternsToAtomIgnoreRegex`): a relative pattern matches at any depth,
372
+ * and an absolute pattern is anchored to the scanned directory. Dosai itself
373
+ * uses gitignore conventions, where a pattern with a slash is anchored, so
374
+ * relative patterns get a leading "**\/". Like the atom filter, and unlike
375
+ * cdxgen's file discovery, excluding a directory also excludes everything
376
+ * beneath it.
377
+ *
378
+ * Brace groups and numeric ranges are expanded, and comma separated lists are
379
+ * split, since dosai reads both as literal text. Character classes, extglobs,
380
+ * escapes, negation, and patterns that step outside the scanned directory
381
+ * have no dosai equivalent; they are reported as skipped rather than passed
382
+ * on as literals that match nothing. So is any pattern that still carries a
383
+ * character outside a small allowlist, such as a literal brace group or a
384
+ * shell metacharacter. Dosai matches case-sensitively on Linux.
385
+ *
386
+ * @param {string[]} excludes cdxgen exclude globs
387
+ * @param {string} srcPath Absolute directory dosai scans
388
+ * @returns {{patterns: string[], skipped: string[], truncated: number}} Translated patterns, dropped patterns, and how many were left out by the cap
389
+ */
390
+ export function toDosaiExcludePatterns(excludes, srcPath) {
391
+ const patterns = new Set();
392
+ const skipped = [];
393
+ let truncated = 0;
394
+ if (!Array.isArray(excludes)) {
395
+ return { patterns: [], skipped, truncated };
396
+ }
397
+ const roots = [srcPath];
398
+ try {
399
+ const realRoot = realpathSync(srcPath);
400
+ if (realRoot !== srcPath) {
401
+ roots.push(realRoot);
402
+ }
403
+ } catch (_err) {
404
+ // The directory may not exist yet in tests; the given path still applies.
405
+ }
406
+ const candidates = [];
407
+ for (const pattern of excludes) {
408
+ if (typeof pattern !== "string" || /[\0\r\n]/.test(pattern)) {
409
+ continue;
410
+ }
411
+ const entries =
412
+ pattern.includes(",") && !pattern.includes("{")
413
+ ? pattern.split(",")
414
+ : [pattern];
415
+ for (const entry of entries) {
416
+ const trimmed = entry.trim();
417
+ if (trimmed) {
418
+ candidates.push(trimmed);
419
+ }
420
+ }
421
+ }
422
+ for (const original of candidates) {
423
+ const normalized = original.startsWith("!")
424
+ ? undefined
425
+ : normalizeDosaiGlobSeparators(original);
426
+ if (!normalized) {
427
+ skipped.push(original);
428
+ continue;
429
+ }
430
+ for (const expanded of expandGlobBraces(normalized)) {
431
+ let glob = expanded;
432
+ while (glob.includes("//")) {
433
+ glob = glob.replaceAll("//", "/");
434
+ }
435
+ if (hasUnsupportedGlobSyntax(glob)) {
436
+ skipped.push(expanded);
437
+ continue;
438
+ }
439
+ if (isAbsolute(glob) || /^[A-Za-z]:\//.test(glob)) {
440
+ const rebased = rebaseAbsoluteGlob(glob, roots);
441
+ if (!rebased) {
442
+ skipped.push(expanded);
443
+ continue;
444
+ }
445
+ glob = `/${rebased}`;
446
+ } else {
447
+ while (glob.startsWith("./")) {
448
+ glob = glob.slice(2);
449
+ }
450
+ if (glob && !glob.startsWith("**/") && glob !== "**") {
451
+ glob = `**/${glob}`;
452
+ }
453
+ }
454
+ const segments = glob.split("/").filter(Boolean);
455
+ if (
456
+ !segments.length ||
457
+ segments.includes("..") ||
458
+ segments.every((segment) => segment === ".")
459
+ ) {
460
+ skipped.push(expanded);
461
+ continue;
462
+ }
463
+ if (!SAFE_DOSAI_GLOB.test(glob)) {
464
+ skipped.push(expanded);
465
+ continue;
466
+ }
467
+ if (patterns.size >= MAX_DOSAI_EXCLUDES) {
468
+ if (!patterns.has(glob)) {
469
+ truncated++;
470
+ }
471
+ continue;
472
+ }
473
+ patterns.add(glob);
474
+ }
475
+ }
476
+ return { patterns: Array.from(patterns), skipped, truncated };
477
+ }
478
+
479
+ function dosaiExcludeArgs(excludes, srcPath) {
480
+ const { patterns, skipped, truncated } = toDosaiExcludePatterns(
481
+ excludes,
482
+ srcPath,
483
+ );
484
+ if (skipped.length) {
485
+ warnDosaiOnce(
486
+ `dosai cannot express these exclude patterns and will not apply them: ${skipped.join(", ")}`,
487
+ );
488
+ }
489
+ if (truncated) {
490
+ warnDosaiOnce(
491
+ `Only the first ${MAX_DOSAI_EXCLUDES} exclude patterns are passed to dosai; ${truncated} more were left out.`,
492
+ );
493
+ }
494
+ return patterns.flatMap((pattern) => ["--exclude", pattern]);
495
+ }
496
+
497
+ function dosaiRejectedExclude(result) {
498
+ const output = `${result?.stdout || ""}${result?.stderr || ""}`;
499
+ return output.includes("Unrecognized command or argument '--exclude'");
500
+ }
501
+
175
502
  function safeDosaiExecutable(value) {
176
- if (!value || typeof value !== "string" || /[\0\r\n]/.test(value)) {
503
+ if (!value || typeof value !== "string") {
504
+ return undefined;
505
+ }
506
+ const executable = value.trim();
507
+ if (!SAFE_DOSAI_EXECUTABLE.test(executable)) {
177
508
  return undefined;
178
509
  }
179
- return value.trim();
510
+ return executable;
180
511
  }
181
512
 
182
513
  /**
@@ -206,6 +537,144 @@ export function readDosaiJsonFile(jsonFile) {
206
537
  }
207
538
  }
208
539
 
540
+ // The sections of a dosai methods report that cdxgen reads. The rest (Methods,
541
+ // Properties, Fields, Reachability, DeadCode, ...) only reaches downstream
542
+ // tools through the persisted native report.
543
+ const DOSAI_METHODS_SECTIONS = [
544
+ "Metadata",
545
+ "Dependencies",
546
+ "MethodCalls",
547
+ "AssemblyInformation",
548
+ "CallGraph",
549
+ "PackageReachability",
550
+ "Services",
551
+ "ApiEndpoints",
552
+ "AiComponents",
553
+ ];
554
+
555
+ // The file each report returned by readDosaiReport came from, and whether it
556
+ // was trimmed to what cdxgen reads. persistDosaiSemanticsReport copies the
557
+ // file rather than re-serialising a trimmed or oversized report.
558
+ const dosaiReportSources = new WeakMap();
559
+
560
+ function referencedIds(entries, field) {
561
+ const ids = new Set();
562
+ for (const entry of entries || []) {
563
+ for (const id of entry?.[field] || []) {
564
+ ids.add(id);
565
+ }
566
+ }
567
+ return ids;
568
+ }
569
+
570
+ function readDosaiReport(reportFile, options, readTrimmed) {
571
+ if (!reportFile || !safeExistsSync(reportFile)) {
572
+ return undefined;
573
+ }
574
+ try {
575
+ const size = statSync(reportFile).size;
576
+ let report;
577
+ let trimmed = false;
578
+ if (size <= (options.maxTextBytes ?? MAX_JSON_TEXT_BYTES)) {
579
+ report = JSON.parse(readFileSync(reportFile, "utf-8"));
580
+ } else {
581
+ // dotnet/efcore produces a 1.9 GB methods report (issue 1033). Read it
582
+ // in bounded runs and keep only what cdxgen uses, which is a small
583
+ // fraction of the file.
584
+ if (DEBUG_MODE) {
585
+ console.log(
586
+ `The dosai report "${reportFile}" is ${size} bytes, more than one JavaScript string can hold, so only the parts cdxgen uses are read from it.`,
587
+ );
588
+ }
589
+ const index = indexJsonObject(reportFile, options);
590
+ report = {};
591
+ readTrimmed(report, (section, spec) => {
592
+ const range = index.get(section);
593
+ if (range) {
594
+ report[section] = readJsonRange(reportFile, range, spec, options);
595
+ }
596
+ });
597
+ trimmed = true;
598
+ }
599
+ if (report && typeof report === "object") {
600
+ dosaiReportSources.set(report, { file: reportFile, trimmed });
601
+ }
602
+ return report;
603
+ } catch (err) {
604
+ if (DEBUG_MODE) {
605
+ console.log(
606
+ `Unable to read the dosai report "${reportFile}": ${err.message}`,
607
+ );
608
+ }
609
+ return undefined;
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Read a dosai methods report of any size.
615
+ *
616
+ * A report that fits in one JavaScript string is parsed whole. A larger one is
617
+ * read in bounded runs, keeping the requested sections, the CallGraph nodes and
618
+ * edges that PackageReachability references, and the MethodCalls accepted by
619
+ * `keepMethodCall` (none when it is omitted). Nothing cdxgen reads is lost.
620
+ *
621
+ * @param {string} reportFile Path to the dosai methods JSON
622
+ * @param {Object} [options] Options
623
+ * @param {string[]} [options.sections] Sections to keep from a large report (default: every section cdxgen reads)
624
+ * @param {Function} [options.keepMethodCall] Predicate choosing the MethodCalls to keep from a large report
625
+ * @param {number} [options.maxTextBytes] Largest report parsed whole, for tests
626
+ * @returns {Object|undefined} Parsed report, or undefined when missing or invalid
627
+ */
628
+ export function readDosaiMethodsReport(reportFile, options = {}) {
629
+ const sections = options.sections || DOSAI_METHODS_SECTIONS;
630
+ return readDosaiReport(reportFile, options, (report, read) => {
631
+ for (const section of sections) {
632
+ if (section !== "CallGraph" && section !== "MethodCalls") {
633
+ read(section);
634
+ }
635
+ }
636
+ if (sections.includes("MethodCalls") && options.keepMethodCall) {
637
+ read("MethodCalls", { filter: options.keepMethodCall });
638
+ }
639
+ if (sections.includes("CallGraph")) {
640
+ // CallGraph precedes PackageReachability in the file, which is why the
641
+ // sections are read by range rather than in one pass.
642
+ const edgeIds = referencedIds(report.PackageReachability, "EdgeIds");
643
+ const nodeIds = referencedIds(report.PackageReachability, "NodeIds");
644
+ read("CallGraph", {
645
+ members: {
646
+ Edges: { filter: (edge) => edgeIds.has(edge?.Id) },
647
+ Nodes: { filter: (node) => nodeIds.has(node?.Id) },
648
+ },
649
+ });
650
+ }
651
+ });
652
+ }
653
+
654
+ /**
655
+ * Read a dosai data-flow report of any size.
656
+ *
657
+ * A report that fits in one JavaScript string is parsed whole. A larger one
658
+ * keeps Metadata, Slices, PackageReachability, and the Nodes those reference.
659
+ *
660
+ * @param {string} reportFile Path to the dosai data-flow JSON
661
+ * @param {Object} [options] Options
662
+ * @param {number} [options.maxTextBytes] Largest report parsed whole, for tests
663
+ * @returns {Object|undefined} Parsed report, or undefined when missing or invalid
664
+ */
665
+ export function readDosaiDataFlowReport(reportFile, options = {}) {
666
+ return readDosaiReport(reportFile, options, (report, read) => {
667
+ read("Metadata");
668
+ read("Slices");
669
+ read("PackageReachability");
670
+ const nodeIds = referencedIds(report.Slices, "NodeIds");
671
+ for (const id of referencedIds(report.PackageReachability, "NodeIds")) {
672
+ nodeIds.add(id);
673
+ }
674
+ read("Nodes", { filter: (node) => nodeIds.has(node?.Id) });
675
+ });
676
+ }
677
+
209
678
  /**
210
679
  * Run a dosai subcommand ("methods", "dataflows", or "crypto") against a source
211
680
  * tree and write its JSON output to the given file.
@@ -213,7 +682,7 @@ export function readDosaiJsonFile(jsonFile) {
213
682
  * @param {string} command Dosai subcommand to execute
214
683
  * @param {string} src Source directory to analyze
215
684
  * @param {string} outputFile Path where the dosai JSON output is written
216
- * @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, or patternPacks overrides
685
+ * @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, patternPacks, or exclude globs
217
686
  * @returns {boolean} True when the command succeeded and produced the output file, false otherwise
218
687
  */
219
688
  export function runDosaiCommand(command, src, outputFile, options = {}) {
@@ -245,13 +714,32 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
245
714
  } else if (command === "crypto") {
246
715
  args.push("--format", "dosai");
247
716
  }
717
+ const excludeArgs = dosaiWithoutExclude.has(executable)
718
+ ? []
719
+ : dosaiExcludeArgs(options.exclude, srcPath);
248
720
  if (DEBUG_MODE) {
249
- console.log("Executing", executable, args.join(" "));
721
+ console.log("Executing", executable, [...args, ...excludeArgs].join(" "));
250
722
  }
251
- const result = safeSpawnSync(executable, args, {
723
+ let result = safeSpawnSync(executable, [...args, ...excludeArgs], {
252
724
  cwd: srcPath,
253
725
  shell: false,
254
726
  });
727
+ // Dosai releases that predate --exclude reject it. Rerun without it so a
728
+ // user supplied exclude never turns into a missing slice.
729
+ if (
730
+ excludeArgs.length &&
731
+ result?.status !== 0 &&
732
+ dosaiRejectedExclude(result)
733
+ ) {
734
+ dosaiWithoutExclude.add(executable);
735
+ warnDosaiOnce(
736
+ "This dosai build does not support --exclude, so it runs without the exclude patterns. Upgrade dosai or set DOSAI_CMD to a newer build to honour them.",
737
+ );
738
+ result = safeSpawnSync(executable, args, {
739
+ cwd: srcPath,
740
+ shell: false,
741
+ });
742
+ }
255
743
  if (dosaiSdkMessage(result)) {
256
744
  recordDegradation("dotnet.sdk.missing", {
257
745
  ecosystem: "csharp",
@@ -360,6 +848,38 @@ function buildCombinedDosaiReport(methodsSlice, dataFlowSlice) {
360
848
  };
361
849
  }
362
850
 
851
+ function* fileChunks(filePath) {
852
+ const fd = openSync(filePath, "r");
853
+ try {
854
+ const buffer = Buffer.allocUnsafe(8 * 1024 * 1024);
855
+ for (;;) {
856
+ const count = readSync(fd, buffer, 0, buffer.length, null);
857
+ if (!count) {
858
+ return;
859
+ }
860
+ yield buffer.subarray(0, count);
861
+ }
862
+ } finally {
863
+ closeSync(fd);
864
+ }
865
+ }
866
+
867
+ // The combined report as JSON text in pieces, with each part copied from its
868
+ // native file when there is one. Keys and their order match
869
+ // buildCombinedDosaiReport.
870
+ function* combinedDosaiReportChunks(combined, parts) {
871
+ yield `{"Metadata":${JSON.stringify(combined.Metadata)}`;
872
+ for (const [key, source] of parts) {
873
+ yield `,${JSON.stringify(key)}:`;
874
+ if (source) {
875
+ yield* fileChunks(source.file);
876
+ } else {
877
+ yield JSON.stringify(combined[key]);
878
+ }
879
+ }
880
+ yield "}";
881
+ }
882
+
363
883
  /**
364
884
  * Persist the combined native dosai report to options.semanticsSlicesFile.
365
885
  *
@@ -390,16 +910,42 @@ export function persistDosaiSemanticsReport(
390
910
  return undefined;
391
911
  }
392
912
  const combined = buildCombinedDosaiReport(methodsSlice, dataFlowSlice);
913
+ const methodsSource = dosaiReportSources.get(methodsSlice);
914
+ const dataFlowSource = dosaiReportSources.get(dataFlowSlice);
393
915
  try {
394
- safeWriteSync(durablePath, JSON.stringify(combined));
916
+ let text;
917
+ if (!methodsSource?.trimmed && !dataFlowSource?.trimmed) {
918
+ try {
919
+ text = JSON.stringify(combined);
920
+ } catch (err) {
921
+ // Too large for one string: write it in parts below instead.
922
+ if (!(err instanceof RangeError)) {
923
+ throw err;
924
+ }
925
+ }
926
+ }
927
+ if (text !== undefined) {
928
+ safeWriteSync(durablePath, text);
929
+ } else {
930
+ // A trimmed report lacks the sections cdxgen skipped, so each part is
931
+ // copied from its native file when it has one, and serialised on its
932
+ // own otherwise.
933
+ safeWriteChunksSync(
934
+ durablePath,
935
+ combinedDosaiReportChunks(combined, [
936
+ ["methods", methodsSource],
937
+ ["dataflows", dataFlowSource],
938
+ ]),
939
+ );
940
+ }
395
941
  } catch (err) {
396
- // Combined methods + data-flow reports for large solutions can exceed the
397
- // JS maximum string length. The SBOM evidence is already collected by this
398
- // point, so skip the durable copy instead of losing the whole analysis.
399
- recordDegradation("dosai.semantics.persistFailed", {
942
+ // A part with no native file can still be too large to serialise, and the
943
+ // write can fail. The SBOM evidence is already collected by this point, so
944
+ // skip the durable copy instead of losing the whole analysis.
945
+ recordDegradation("dosai.semantics.persist-failed", {
400
946
  ecosystem: "csharp",
401
947
  tool: "dosai",
402
- impact: "minor",
948
+ impact: "none",
403
949
  command: `persistDosaiSemanticsReport ${durablePath}`,
404
950
  detail: `The combined dosai report could not be persisted: ${err.message}. The SBOM evidence is unaffected.`,
405
951
  });
@@ -429,6 +975,74 @@ export function resolveComponentPurl(purl, purlAliasMap) {
429
975
  return resolveDosaiComponentPurl(purl, purlAliasMap);
430
976
  }
431
977
 
978
+ /**
979
+ * Copy one dosai PackageReachability fact onto a component as properties.
980
+ *
981
+ * The occurrence/location consumers of PackageReachability read SourceLocations, EdgeIds,
982
+ * and NodeIds only, so dosai's Confidence, EvidenceKinds, ReachabilityKind, and
983
+ * ConfidenceReasons never reached the BOM: an unbuilt tree (Low confidence, unresolved
984
+ * evidence) was indistinguishable from a package that really is only imported. These
985
+ * properties carry that distinction to BOM consumers. They use the consumer-facing
986
+ * `cdx:dosai:reachability:*` namespace - not `internal:`, which is cdxgen's private
987
+ * bookkeeping namespace stripped or ignored by downstream tooling.
988
+ *
989
+ * @param {Object} component BOM component object (mutated; properties created on demand)
990
+ * @param {Object} reachability dosai PackageReachability entry
991
+ */
992
+ export function addDosaiReachabilityProperties(component, reachability) {
993
+ if (!component || !reachability) {
994
+ return;
995
+ }
996
+ component.properties = component.properties || [];
997
+ const values = {
998
+ "cdx:dosai:reachability:kind": reachability.ReachabilityKind,
999
+ "cdx:dosai:reachability:confidence": reachability.Confidence,
1000
+ "cdx:dosai:reachability:evidence": (reachability.EvidenceKinds || []).join(
1001
+ ", ",
1002
+ ),
1003
+ "cdx:dosai:reachability:reasons": (
1004
+ reachability.ConfidenceReasons || []
1005
+ ).join(" | "),
1006
+ };
1007
+ for (const [name, value] of Object.entries(values)) {
1008
+ if (!value) {
1009
+ continue;
1010
+ }
1011
+ // First fact wins so a repeated enrichment pass cannot stack duplicate properties.
1012
+ if (component.properties.some((p) => p.name === name)) {
1013
+ continue;
1014
+ }
1015
+ component.properties.push({ name, value });
1016
+ }
1017
+ }
1018
+
1019
+ /**
1020
+ * Attach dosai PackageReachability confidence facts to matching BOM components.
1021
+ *
1022
+ * @param {Object} methodsSlice Parsed dosai methods slice JSON
1023
+ * @param {Object[]} [components] BOM components used to resolve purl aliases and mutate
1024
+ * @returns {number} Number of distinct components enriched
1025
+ */
1026
+ export function applyDosaiReachabilityEvidence(methodsSlice, components = []) {
1027
+ const purlAliasMap = buildPurlAliasMap(components);
1028
+ const componentsByPurl = new Map(
1029
+ components
1030
+ .filter((component) => component?.purl)
1031
+ .map((component) => [component.purl, component]),
1032
+ );
1033
+ const enriched = new Set();
1034
+ for (const reachability of methodsSlice?.PackageReachability || []) {
1035
+ const purl = resolveComponentPurl(reachability.Purl, purlAliasMap);
1036
+ const component = purl && componentsByPurl.get(purl);
1037
+ if (!component) {
1038
+ continue;
1039
+ }
1040
+ addDosaiReachabilityProperties(component, reachability);
1041
+ enriched.add(component);
1042
+ }
1043
+ return enriched.size;
1044
+ }
1045
+
432
1046
  /**
433
1047
  * Map a dosai methods slice to per-purl occurrence evidence.
434
1048
  *