@cdxgen/cdxgen 13.3.0 → 13.5.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 (372) hide show
  1. package/README.md +12 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +115 -31
  4. package/bin/evinse.js +23 -1
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +115 -0
  7. package/bin/sign.js +45 -15
  8. package/bin/tracebom.js +5 -1
  9. package/bin/validate.js +84 -18
  10. package/bin/verify.js +155 -86
  11. package/data/cbomosdb-queries.json +13 -1
  12. package/data/component-tags.json +8 -0
  13. package/data/helpers/init.gradle +46 -21
  14. package/data/queries-darwin.json +25 -1
  15. package/data/queries-win.json +19 -1
  16. package/data/queries.json +41 -0
  17. package/data/remediations.json +344 -1
  18. package/data/rules/cbom-security.yaml +235 -4
  19. package/data/rules/dependency-sources.yaml +67 -0
  20. package/data/rules/go-binaries.yaml +46 -0
  21. package/data/rules/golem-go.yaml +80 -0
  22. package/data/rules/hbom-compliance.yaml +40 -22
  23. package/data/rules/hbom-performance.yaml +7 -1
  24. package/data/rules/host-topology.yaml +1 -1
  25. package/data/rules/obom-runtime.yaml +31 -2
  26. package/data/rules/php-runtime.yaml +143 -0
  27. package/data/rules/scala-evidence.yaml +115 -0
  28. package/lib/audit/index.js +52 -6
  29. package/lib/cli/bomAssembly.js +28 -3
  30. package/lib/cli/cliOptions.js +110 -3
  31. package/lib/cli/index.js +186 -58
  32. package/lib/cli/jsBom.js +50 -29
  33. package/lib/cli/jvmBom.js +779 -146
  34. package/lib/cli/managedBom.js +1021 -261
  35. package/lib/cli/nativeBom.js +364 -58
  36. package/lib/core/activity.js +12 -0
  37. package/lib/core/env.js +47 -17
  38. package/lib/core/fs.js +31 -21
  39. package/lib/core/httpClient.js +233 -69
  40. package/lib/core/pluginRun.js +184 -0
  41. package/lib/core/processTree.js +100 -0
  42. package/lib/core/projectConfig.js +1 -0
  43. package/lib/core/projectTooling.js +97 -0
  44. package/lib/core/propertySanitizer.js +72 -0
  45. package/lib/core/runState.js +39 -0
  46. package/lib/core/toml.js +72 -0
  47. package/lib/ecosystems/asarutils.js +4 -2
  48. package/lib/ecosystems/bunutils.js +5 -7
  49. package/lib/ecosystems/cargoCache.js +231 -8
  50. package/lib/ecosystems/cmakeResolver.js +10 -21
  51. package/lib/ecosystems/cppBuildContext.js +631 -0
  52. package/lib/ecosystems/cppEvidence.js +337 -22
  53. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  54. package/lib/ecosystems/denoutils.js +93 -66
  55. package/lib/ecosystems/ecosystems.js +1967 -788
  56. package/lib/ecosystems/golem.js +578 -18
  57. package/lib/ecosystems/gradleutils.js +340 -46
  58. package/lib/ecosystems/jsEvidence.js +85 -24
  59. package/lib/ecosystems/kosi.js +21 -4
  60. package/lib/ecosystems/npmutils.js +64 -0
  61. package/lib/ecosystems/parsers-dotnet.js +668 -153
  62. package/lib/ecosystems/parsers-elm.js +46 -28
  63. package/lib/ecosystems/parsers-gleam.js +3 -4
  64. package/lib/ecosystems/parsers-go.js +1283 -297
  65. package/lib/ecosystems/parsers-js.js +92 -26
  66. package/lib/ecosystems/parsers-julia.js +2 -3
  67. package/lib/ecosystems/parsers-jvm.js +463 -29
  68. package/lib/ecosystems/parsers-misc.js +405 -93
  69. package/lib/ecosystems/parsers-python.js +152 -20
  70. package/lib/ecosystems/parsers-rust.js +4 -4
  71. package/lib/ecosystems/parsers-terraform.js +1573 -19
  72. package/lib/ecosystems/pythonutils.js +102 -6
  73. package/lib/ecosystems/remote/huggingface.js +3 -0
  74. package/lib/ecosystems/rubyutils.js +73 -20
  75. package/lib/ecosystems/rusi.js +32 -9
  76. package/lib/ecosystems/sbtutils.js +243 -187
  77. package/lib/ecosystems/scalacliutils.js +343 -0
  78. package/lib/ecosystems/scalasem.js +2620 -0
  79. package/lib/ecosystems/terraformRegistry.js +288 -0
  80. package/lib/ecosystems/utils.js +2 -0
  81. package/lib/ecosystems/vendoredCode.js +156 -0
  82. package/lib/evinser/evinser.js +695 -78
  83. package/lib/evinser/swiftsem.js +68 -5
  84. package/lib/helpers/agenticToolutils.js +2 -2
  85. package/lib/helpers/bomSigner.js +576 -128
  86. package/lib/inventory/agentSkillSpec.js +17 -0
  87. package/lib/inventory/aiCollector.js +4 -1
  88. package/lib/inventory/analyzer.js +110 -127
  89. package/lib/inventory/apkInstalledDb.js +243 -0
  90. package/lib/inventory/atomUtils.js +204 -48
  91. package/lib/inventory/auditCategories.js +10 -3
  92. package/lib/inventory/buildToolRateLimit.js +144 -0
  93. package/lib/inventory/cacheCommands.js +2 -1
  94. package/lib/inventory/cacheDir.js +52 -9
  95. package/lib/inventory/certificates.js +2371 -0
  96. package/lib/inventory/cmakeBuildDirs.js +254 -0
  97. package/lib/inventory/communityAiConfigParser.js +4 -9
  98. package/lib/inventory/deps.js +507 -128
  99. package/lib/inventory/display.js +200 -3
  100. package/lib/inventory/dosai.js +747 -101
  101. package/lib/inventory/dosaiParsers.js +310 -15
  102. package/lib/inventory/dynamic.js +96 -8
  103. package/lib/inventory/envcontext.js +168 -5
  104. package/lib/inventory/evidenceUtils.js +590 -132
  105. package/lib/inventory/fetchBatch.js +574 -38
  106. package/lib/inventory/fetchDiskCache.js +597 -0
  107. package/lib/inventory/forgeEnricher.js +14 -5
  108. package/lib/inventory/formulationParsers.js +3 -2
  109. package/lib/inventory/goBuildInfo.js +140 -0
  110. package/lib/inventory/hbom.js +67 -7
  111. package/lib/inventory/hbomAnalysis.js +16 -0
  112. package/lib/inventory/hostTopology.js +21 -0
  113. package/lib/inventory/jvmLocalRepos.js +917 -0
  114. package/lib/inventory/mcpConfigParser.js +2 -3
  115. package/lib/inventory/osPackagePurl.js +244 -0
  116. package/lib/inventory/osPackageResolver.js +89 -87
  117. package/lib/inventory/osinfo.js +79 -31
  118. package/lib/inventory/osqueryTransform.js +114 -5
  119. package/lib/inventory/parentSelection.js +146 -0
  120. package/lib/inventory/phpBuildInfo.js +487 -0
  121. package/lib/inventory/phpHostProbe.js +641 -0
  122. package/lib/inventory/rootfsCerts.js +426 -0
  123. package/lib/inventory/rustFormulationParser.js +3 -4
  124. package/lib/inventory/scalaCoords.js +153 -0
  125. package/lib/inventory/source.js +215 -11
  126. package/lib/inventory/spdx.js +245 -0
  127. package/lib/inventory/traceRunner.js +93 -8
  128. package/lib/inventory/tracedCrypto.js +423 -0
  129. package/lib/managers/binary.js +529 -183
  130. package/lib/managers/docker.js +199 -44
  131. package/lib/managers/oci.js +24 -2
  132. package/lib/parsers/cmakeCache.js +69 -0
  133. package/lib/parsers/cmakeLists.js +16 -2
  134. package/lib/parsers/cmakePresets.js +400 -0
  135. package/lib/parsers/compileCommands.js +557 -0
  136. package/lib/parsers/email.js +277 -0
  137. package/lib/parsers/largeJson.js +34 -2
  138. package/lib/parsers/strictJson.js +322 -0
  139. package/lib/parsers/terraformConfig.js +1073 -0
  140. package/lib/parsers/terraformSource.js +652 -0
  141. package/lib/server/server.js +209 -1
  142. package/lib/stages/postgen/annotator.js +17 -1
  143. package/lib/stages/postgen/introspection/markers.js +1 -1
  144. package/lib/stages/postgen/introspection/reflect.js +4 -1
  145. package/lib/stages/postgen/postgen.js +20 -6
  146. package/lib/stages/postgen/ruleEngine.js +58 -1
  147. package/lib/stages/postgen/specVersionCompat.js +62 -0
  148. package/lib/stages/pregen/pregen.js +117 -5
  149. package/lib/validator/bomValidator.js +26 -5
  150. package/lib/validator/generated/runtime.mjs +13 -0
  151. package/lib/validator/generated/validate-1.6.mjs +26 -7
  152. package/lib/validator/generated/validate-1.7.mjs +31 -12
  153. package/lib/validator/generated/validate-2.0.mjs +30 -11
  154. package/lib/validator/index.js +25 -10
  155. package/lib/validator/reporters/json.js +1 -0
  156. package/package.json +23 -23
  157. package/types/bin/cdxgen-secure.d.ts +3 -0
  158. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  159. package/types/bin/repl.d.ts.map +1 -1
  160. package/types/lib/audit/index.d.ts.map +1 -1
  161. package/types/lib/cli/bomAssembly.d.ts +9 -2
  162. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  163. package/types/lib/cli/cliOptions.d.ts +43 -0
  164. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  165. package/types/lib/cli/index.d.ts.map +1 -1
  166. package/types/lib/cli/jsBom.d.ts.map +1 -1
  167. package/types/lib/cli/jvmBom.d.ts +44 -1
  168. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  169. package/types/lib/cli/managedBom.d.ts +7 -4
  170. package/types/lib/cli/managedBom.d.ts.map +1 -1
  171. package/types/lib/cli/nativeBom.d.ts +3 -1
  172. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  173. package/types/lib/core/activity.d.ts.map +1 -1
  174. package/types/lib/core/env.d.ts +27 -2
  175. package/types/lib/core/env.d.ts.map +1 -1
  176. package/types/lib/core/fs.d.ts.map +1 -1
  177. package/types/lib/core/httpClient.d.ts +13 -8
  178. package/types/lib/core/httpClient.d.ts.map +1 -1
  179. package/types/lib/core/pluginRun.d.ts +97 -0
  180. package/types/lib/core/pluginRun.d.ts.map +1 -0
  181. package/types/lib/core/processTree.d.ts +36 -0
  182. package/types/lib/core/processTree.d.ts.map +1 -0
  183. package/types/lib/core/projectConfig.d.ts.map +1 -1
  184. package/types/lib/core/projectTooling.d.ts +41 -0
  185. package/types/lib/core/projectTooling.d.ts.map +1 -0
  186. package/types/lib/core/propertySanitizer.d.ts +26 -0
  187. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  188. package/types/lib/core/runState.d.ts +29 -0
  189. package/types/lib/core/runState.d.ts.map +1 -0
  190. package/types/lib/core/toml.d.ts +20 -0
  191. package/types/lib/core/toml.d.ts.map +1 -0
  192. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  193. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  194. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  195. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  196. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  197. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  198. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  199. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  200. package/types/lib/ecosystems/cppEvidence.d.ts +19 -3
  201. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  202. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  203. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  204. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  205. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  207. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  208. package/types/lib/ecosystems/golem.d.ts +81 -1
  209. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  210. package/types/lib/ecosystems/gradleutils.d.ts +59 -7
  211. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  212. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  213. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  214. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  215. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  216. package/types/lib/ecosystems/parsers-dotnet.d.ts +155 -5
  217. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  218. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  219. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  220. package/types/lib/ecosystems/parsers-go.d.ts +119 -5
  221. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  222. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  223. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  224. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  225. package/types/lib/ecosystems/parsers-jvm.d.ts +74 -3
  226. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  227. package/types/lib/ecosystems/parsers-misc.d.ts +21 -59
  228. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  229. package/types/lib/ecosystems/parsers-python.d.ts +57 -6
  230. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  231. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  232. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  233. package/types/lib/ecosystems/pythonutils.d.ts +26 -1
  234. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  235. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  236. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  237. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  238. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  239. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  240. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  241. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  242. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  243. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  244. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  245. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  246. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  247. package/types/lib/ecosystems/utils.d.ts +2 -2
  248. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  249. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  250. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  251. package/types/lib/evinser/evinser.d.ts +100 -8
  252. package/types/lib/evinser/evinser.d.ts.map +1 -1
  253. package/types/lib/evinser/swiftsem.d.ts +10 -0
  254. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  255. package/types/lib/helpers/bomSigner.d.ts +80 -4
  256. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  257. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  258. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  259. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  260. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  261. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  262. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  263. package/types/lib/inventory/atomUtils.d.ts +96 -10
  264. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  265. package/types/lib/inventory/auditCategories.d.ts +3 -0
  266. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  267. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  268. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  269. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  270. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  271. package/types/lib/inventory/cacheDir.d.ts +12 -4
  272. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  273. package/types/lib/inventory/certificates.d.ts +205 -0
  274. package/types/lib/inventory/certificates.d.ts.map +1 -0
  275. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  276. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  277. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  278. package/types/lib/inventory/deps.d.ts +61 -2
  279. package/types/lib/inventory/deps.d.ts.map +1 -1
  280. package/types/lib/inventory/display.d.ts +18 -0
  281. package/types/lib/inventory/display.d.ts.map +1 -1
  282. package/types/lib/inventory/dosai.d.ts +202 -14
  283. package/types/lib/inventory/dosai.d.ts.map +1 -1
  284. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  285. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  286. package/types/lib/inventory/dynamic.d.ts +16 -1
  287. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  288. package/types/lib/inventory/envcontext.d.ts +40 -2
  289. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  290. package/types/lib/inventory/evidenceUtils.d.ts +48 -14
  291. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  292. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  293. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  294. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  295. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  296. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  297. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  298. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  299. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  300. package/types/lib/inventory/hbom.d.ts.map +1 -1
  301. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  302. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  303. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  304. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  305. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  306. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  307. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  308. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  309. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  310. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  311. package/types/lib/inventory/osinfo.d.ts +31 -6
  312. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  313. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  314. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  315. package/types/lib/inventory/parentSelection.d.ts +34 -0
  316. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  317. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  318. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  319. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  320. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  321. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  322. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  323. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  324. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  325. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  326. package/types/lib/inventory/source.d.ts +28 -0
  327. package/types/lib/inventory/source.d.ts.map +1 -1
  328. package/types/lib/inventory/spdx.d.ts +24 -0
  329. package/types/lib/inventory/spdx.d.ts.map +1 -1
  330. package/types/lib/inventory/traceRunner.d.ts +16 -1
  331. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  332. package/types/lib/inventory/tracedCrypto.d.ts +64 -0
  333. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  334. package/types/lib/managers/binary.d.ts +10 -0
  335. package/types/lib/managers/binary.d.ts.map +1 -1
  336. package/types/lib/managers/docker.d.ts +12 -0
  337. package/types/lib/managers/docker.d.ts.map +1 -1
  338. package/types/lib/managers/oci.d.ts +3 -1
  339. package/types/lib/managers/oci.d.ts.map +1 -1
  340. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  341. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  342. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  343. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  344. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  345. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  346. package/types/lib/parsers/compileCommands.d.ts +115 -0
  347. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  348. package/types/lib/parsers/email.d.ts +19 -0
  349. package/types/lib/parsers/email.d.ts.map +1 -0
  350. package/types/lib/parsers/largeJson.d.ts +13 -0
  351. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  352. package/types/lib/parsers/strictJson.d.ts +46 -0
  353. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  354. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  355. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  356. package/types/lib/parsers/terraformSource.d.ts +66 -0
  357. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  358. package/types/lib/server/server.d.ts +8 -0
  359. package/types/lib/server/server.d.ts.map +1 -1
  360. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  361. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  362. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  363. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  364. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  365. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  366. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  367. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  368. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  369. package/types/lib/validator/index.d.ts +8 -0
  370. package/types/lib/validator/index.d.ts.map +1 -1
  371. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  372. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -10,7 +10,7 @@ import {
10
10
  import { basename, delimiter, isAbsolute, join, resolve } from "node:path";
11
11
  import process from "node:process";
12
12
 
13
- import { DEBUG_MODE } from "../core/activity.js";
13
+ import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
14
14
  import { recordDegradation } from "../core/buildLedger.js";
15
15
  import {
16
16
  getTmpDir,
@@ -20,10 +20,14 @@ import {
20
20
  safeSpawnSync,
21
21
  safeWriteChunksSync,
22
22
  safeWriteSync,
23
+ TIMEOUT_MS,
23
24
  } from "../core/fs.js";
25
+ import { formatDuration, reapProcessTree } from "../core/processTree.js";
26
+ import { encodeUriReference } from "../core/propertySanitizer.js";
24
27
  import {
25
28
  indexJsonObject,
26
29
  MAX_JSON_TEXT_BYTES,
30
+ readJsonMember,
27
31
  readJsonRange,
28
32
  } from "../parsers/largeJson.js";
29
33
  import {
@@ -152,11 +156,7 @@ function sanitizeEndpoint(endpoint) {
152
156
  // CycloneDX types `services[].endpoints[]` as an iri-reference, where square
153
157
  // brackets are reserved for IPv6 literals and braces are excluded outright,
154
158
  // so the template is percent-encoded before it reaches the BOM.
155
- const encodedPath = routePath.replace(
156
- /[[\]{}]/g,
157
- (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
158
- );
159
- return encodedPath;
159
+ return encodeUriReference(routePath);
160
160
  }
161
161
 
162
162
  function serviceNameFromEndpoint(endpoint) {
@@ -167,15 +167,67 @@ function serviceNameFromEndpoint(endpoint) {
167
167
  .replace(/-+/g, "-");
168
168
  }
169
169
 
170
- function dosaiSdkMessage(result) {
171
- return (
172
- result?.stdout?.includes(
173
- "You must install or update .NET to run this application",
174
- ) ||
175
- result?.stderr?.includes(
176
- "You must install or update .NET to run this application",
177
- )
178
- );
170
+ // The .NET host prints "You must install .NET" when it finds no .NET at all,
171
+ // and "You must install or update .NET" when no installed runtime is new
172
+ // enough for the app.
173
+ const DOTNET_HOST_LAUNCH_FAILURE =
174
+ /You must install (?:or update )?\.NET to run this application/;
175
+
176
+ /**
177
+ * The .NET runtime a framework-dependent dosai needs, read from the error the
178
+ * .NET host prints when it refuses to start dosai.
179
+ *
180
+ * When an older runtime is installed, the host names the exact framework
181
+ * version it looked for. When it finds no .NET at all, it names only its own
182
+ * version. The app host comes from the pack of the runtime the app targets, so
183
+ * its major version is still the .NET release to install.
184
+ *
185
+ * @param {Object|string} resultOrOutput spawnSync result, or the text it printed
186
+ * @returns {{version: (string|undefined), major: (string|undefined)}|undefined}
187
+ * The framework version, when the host named it, and the .NET major version;
188
+ * undefined when the output is not a .NET host launch failure
189
+ */
190
+ export function dosaiDotnetRequirement(resultOrOutput) {
191
+ const output =
192
+ typeof resultOrOutput === "string"
193
+ ? resultOrOutput
194
+ : `${resultOrOutput?.stdout || ""}\n${resultOrOutput?.stderr || ""}`;
195
+ if (!DOTNET_HOST_LAUNCH_FAILURE.test(output)) {
196
+ return undefined;
197
+ }
198
+ const version = output.match(
199
+ /Framework: 'Microsoft\.NETCore\.App', version '([^']+)'/,
200
+ )?.[1];
201
+ const major =
202
+ version?.match(/^(\d+)\./)?.[1] ||
203
+ output.match(/App host version: (\d+)\./)?.[1];
204
+ return { version, major };
205
+ }
206
+
207
+ /**
208
+ * What stopped dosai from starting, for warnings and ledger details.
209
+ *
210
+ * @param {{version: (string|undefined), major: (string|undefined)}} [requirement] Result of dosaiDotnetRequirement
211
+ * @returns {string} A clause naming the missing .NET runtime when it is known
212
+ */
213
+ export function dosaiDotnetRequirementDetail(requirement) {
214
+ if (requirement?.version) {
215
+ return `dosai needs the .NET runtime ${requirement.version} or newer, which is not installed`;
216
+ }
217
+ if (requirement?.major) {
218
+ return `dosai needs the .NET ${requirement.major} runtime, and no .NET installation was found`;
219
+ }
220
+ return "dosai could not start because the .NET runtime it needs is not installed";
221
+ }
222
+
223
+ /**
224
+ * How to get dosai running when the .NET host refused to start it.
225
+ *
226
+ * @param {{version: (string|undefined), major: (string|undefined)}} [requirement] Result of dosaiDotnetRequirement
227
+ * @returns {string} Install instructions, naming the .NET major version when it is known
228
+ */
229
+ export function dosaiDotnetRequirementAdvice(requirement) {
230
+ return `Install the .NET${requirement?.major ? ` ${requirement.major}` : ""} runtime or SDK from https://dotnet.microsoft.com/download, or download the self-contained dosai (-full suffix) from https://github.com/owasp-dep-scan/dosai/releases and set DOSAI_CMD to its path.`;
179
231
  }
180
232
 
181
233
  function safeDosaiPath(value) {
@@ -196,6 +248,16 @@ function safeDosaiPatternPacks(value) {
196
248
  .join(delimiter);
197
249
  }
198
250
 
251
+ // A --sections value names top-level report sections, which are plain
252
+ // identifiers in dosai's report contract. Anything else is not passed.
253
+ function safeDosaiSections(value) {
254
+ if (!value || typeof value !== "string") {
255
+ return undefined;
256
+ }
257
+ const names = value.split(",");
258
+ return names.every((name) => /^[A-Za-z]+$/.test(name)) ? value : undefined;
259
+ }
260
+
199
261
  const MAX_DOSAI_EXCLUDES = 256;
200
262
  const MAX_BRACE_RANGE = 64;
201
263
  const GLOB_ESCAPABLE = "*?[]{}()!+@,";
@@ -207,7 +269,10 @@ const SAFE_DOSAI_GLOB = /^[\p{L}\p{N} _.+@#%~=/*?-]+$/u;
207
269
  // letters and "Program Files (x86)". Shell metacharacters are not allowed.
208
270
  const SAFE_DOSAI_EXECUTABLE = /^[\p{L}\p{N} _.,+@#%~=/\\:()-]+$/u;
209
271
  const warnedDosaiMessages = new Set();
210
- const dosaiWithoutExclude = new Set();
272
+ // Optional dosai flags this executable has rejected, so later runs drop them
273
+ // without the doomed first attempt. dosai reports every unknown token in one
274
+ // run, so one rejection teaches the memo about every flag of that run.
275
+ const dosaiRejectedFlagMemo = new Map();
211
276
 
212
277
  function warnDosaiOnce(message) {
213
278
  if (!warnedDosaiMessages.has(message)) {
@@ -494,9 +559,25 @@ function dosaiExcludeArgs(excludes, srcPath) {
494
559
  return patterns.flatMap((pattern) => ["--exclude", pattern]);
495
560
  }
496
561
 
497
- function dosaiRejectedExclude(result) {
562
+ /**
563
+ * Every flag dosai reported as unknown in a run.
564
+ *
565
+ * System.CommandLine prints one `Unrecognized command or argument '--x'` line
566
+ * per unknown token (and a second line for the value that follows it, which
567
+ * carries no leading dashes and so never matches), so one failed run names
568
+ * every rejected flag.
569
+ *
570
+ * @param {Object} result spawnSync result
571
+ * @returns {Set<string>} The rejected flag literals, e.g. `{"--exclude"}`
572
+ */
573
+ function dosaiRejectedFlags(result) {
498
574
  const output = `${result?.stdout || ""}${result?.stderr || ""}`;
499
- return output.includes("Unrecognized command or argument '--exclude'");
575
+ const rejected = new Set();
576
+ const pattern = /Unrecognized command or argument '(--[^\s']+)'/g;
577
+ for (const match of output.matchAll(pattern)) {
578
+ rejected.add(match[1]);
579
+ }
580
+ return rejected;
500
581
  }
501
582
 
502
583
  function safeDosaiExecutable(value) {
@@ -537,9 +618,10 @@ export function readDosaiJsonFile(jsonFile) {
537
618
  }
538
619
  }
539
620
 
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.
621
+ // The sections of a dosai methods report that cdxgen reads. A run that will
622
+ // not run evinse asks dosai to write only these (--sections;
623
+ // Metadata.OmittedSections names the rest), so the complete report reaches
624
+ // downstream tools through the semantics slices file of an evidence run.
543
625
  const DOSAI_METHODS_SECTIONS = [
544
626
  "Metadata",
545
627
  "Dependencies",
@@ -552,6 +634,84 @@ const DOSAI_METHODS_SECTIONS = [
552
634
  "AiComponents",
553
635
  ];
554
636
 
637
+ /**
638
+ * The `--sections` value that writes exactly the sections cdxgen reads.
639
+ *
640
+ * Passed as `dosaiMethodsSections` by callers that know their run will not
641
+ * need the whole report (see {@link runsEvidenceAnalysis}); an older dosai
642
+ * that rejects `--sections` falls back to the full report on its own.
643
+ *
644
+ * @returns {string} Comma-separated section list for dosai `--sections`
645
+ */
646
+ export function dosaiMethodsSectionsArgument() {
647
+ return DOSAI_METHODS_SECTIONS.join(",");
648
+ }
649
+
650
+ /**
651
+ * Whether this scan will run the evidence (evinse) analysis.
652
+ *
653
+ * One place answers for both the trim decision in `createCsharpBom` and the
654
+ * evinse gate in `bin/cdxgen.js`: `--evidence` alone runs it, and so does
655
+ * `--include-crypto` alone, after profiles are applied (`--profile research`
656
+ * turns on both). Testing `options.evidence` alone would trim a report that
657
+ * `--include-crypto` then needs full.
658
+ *
659
+ * @param {Object} options Scan options after profile expansion
660
+ * @returns {boolean} True when the scan will run evinse
661
+ */
662
+ export function runsEvidenceAnalysis(options) {
663
+ return Boolean(options?.evidence || options?.includeCrypto);
664
+ }
665
+
666
+ /**
667
+ * The top-level sections a dosai methods report left out, empty for a full
668
+ * report. dosai v4 reports have no `OmittedSections`, so they count as full.
669
+ *
670
+ * @param {Object} [report] A dosai methods report as read by readDosaiMethodsReport
671
+ * @returns {string[]} The omitted section names
672
+ */
673
+ export function dosaiOmittedSections(report) {
674
+ const omitted = report?.Metadata?.OmittedSections;
675
+ return Array.isArray(omitted)
676
+ ? omitted.filter((name) => typeof name === "string" && name)
677
+ : [];
678
+ }
679
+
680
+ /**
681
+ * The top-level sections a dosai methods report file left out, read from its
682
+ * Metadata alone. dosai writes Metadata first, so this reads only the head of
683
+ * the file, however large the report is. A missing or unreadable file counts
684
+ * as full: the read that follows reports it.
685
+ *
686
+ * @param {string} reportFile Path to the dosai methods JSON
687
+ * @returns {string[]} The omitted section names
688
+ */
689
+ export function dosaiReportFileOmittedSections(reportFile) {
690
+ if (!reportFile || !safeExistsSync(reportFile)) {
691
+ return [];
692
+ }
693
+ try {
694
+ return dosaiOmittedSections({
695
+ Metadata: readJsonMember(reportFile, "Metadata"),
696
+ });
697
+ } catch (_err) {
698
+ return [];
699
+ }
700
+ }
701
+
702
+ /**
703
+ * The omitted sections of a dosai methods report that cdxgen reads, so the
704
+ * evidence built from the report lacks their input.
705
+ *
706
+ * @param {string[]} omittedSections Sections the report left out
707
+ * @returns {string[]} The ones cdxgen reads
708
+ */
709
+ export function dosaiSectionsCdxgenReads(omittedSections) {
710
+ return omittedSections.filter((name) =>
711
+ DOSAI_METHODS_SECTIONS.includes(name),
712
+ );
713
+ }
714
+
555
715
  // The file each report returned by readDosaiReport came from, and whether it
556
716
  // was trimmed to what cdxgen reads. persistDosaiSemanticsReport copies the
557
717
  // file rather than re-serialising a trimmed or oversized report.
@@ -675,14 +835,123 @@ export function readDosaiDataFlowReport(reportFile, options = {}) {
675
835
  });
676
836
  }
677
837
 
838
+ /**
839
+ * Whether a dosai run ended because it ran out of time.
840
+ *
841
+ * dosai has no limit of its own: cdxgen's spawn timeout
842
+ * (CDXGEN_TIMEOUT_MS) stops the process, which reaches the caller only as an
843
+ * ETIMEDOUT error looking like any other failure (issue 4438).
844
+ *
845
+ * @param {Object} result spawnSync result
846
+ * @returns {boolean} true when the run was stopped by the spawn timeout
847
+ */
848
+ export function dosaiRunTimedOut(result) {
849
+ return result?.error?.code === "ETIMEDOUT";
850
+ }
851
+
852
+ /**
853
+ * How a dosai run was stopped before it could finish, if it was.
854
+ *
855
+ * Besides the spawn timeout, a run is stopped when its console output outgrows
856
+ * CDXGEN_MAX_BUFFER (ENOBUFS) or a signal ends it, such as the kernel's
857
+ * out-of-memory killer. A stopped run can leave its slice half written.
858
+ *
859
+ * @param {Object} result spawnSync result
860
+ * @returns {"timeout"|"buffer"|"signal"|undefined} Why the run was stopped, or undefined when it ended by itself
861
+ */
862
+ export function dosaiRunStopReason(result) {
863
+ if (dosaiRunTimedOut(result)) {
864
+ return "timeout";
865
+ }
866
+ if (result?.error?.code === "ENOBUFS") {
867
+ return "buffer";
868
+ }
869
+ if (result?.signal) {
870
+ return "signal";
871
+ }
872
+ return undefined;
873
+ }
874
+
875
+ // The output files of the dosai runs of this process that were stopped, so a
876
+ // caller does not follow the stop warning with advice for another failure.
877
+ const stoppedDosaiOutputs = new Set();
878
+ // The output files of the dosai runs the .NET host refused to start; the
879
+ // warning naming the missing runtime already says why they are absent.
880
+ const unstartedDosaiOutputs = new Set();
881
+ // The executable that wrote each output file of this process, and whether it
882
+ // was the cdxgen-plugins-bin build rather than an override.
883
+ const dosaiOutputProducers = new Map();
884
+ // Executables the .NET host refused to start, once that has been reported to
885
+ // the user, by the pregen check or by a dosai run.
886
+ const dosaiLaunchFailuresReported = new Set();
887
+
888
+ /**
889
+ * Note that the .NET host's refusal to start a dosai executable has been
890
+ * reported, so dosai runs of the same executable do not repeat the warning.
891
+ *
892
+ * @param {string} executable dosai executable
893
+ */
894
+ export function markDosaiLaunchFailureReported(executable) {
895
+ const safeExecutable = safeDosaiExecutable(executable);
896
+ if (safeExecutable) {
897
+ dosaiLaunchFailuresReported.add(safeExecutable);
898
+ }
899
+ }
900
+
901
+ /**
902
+ * Whether the dosai run that should have written a file was stopped (by the
903
+ * time limit, the output buffer limit, or a signal) and already reported so.
904
+ *
905
+ * @param {string} outputFile Output file passed to the dosai run
906
+ * @returns {boolean} true when the run was stopped
907
+ */
908
+ export function dosaiRunWasStopped(outputFile) {
909
+ const outputPath = safeDosaiPath(outputFile);
910
+ return Boolean(outputPath && stoppedDosaiOutputs.has(outputPath));
911
+ }
912
+
913
+ /**
914
+ * Whether the dosai run that should have written a file failed in a way it has
915
+ * already reported: it was stopped (see dosaiRunWasStopped), or the .NET host
916
+ * refused to start dosai and the warning named the missing runtime.
917
+ *
918
+ * @param {string} outputFile Output file passed to the dosai run
919
+ * @returns {boolean} true when the failure has been reported
920
+ */
921
+ export function dosaiFailureReported(outputFile) {
922
+ const outputPath = safeDosaiPath(outputFile);
923
+ return Boolean(
924
+ outputPath &&
925
+ (stoppedDosaiOutputs.has(outputPath) ||
926
+ unstartedDosaiOutputs.has(outputPath)),
927
+ );
928
+ }
929
+
930
+ function dosaiStopMessage(reason, signal) {
931
+ if (reason === "timeout") {
932
+ return `WARN: dosai did not finish within ${formatDuration(TIMEOUT_MS)} and was stopped, so its slices are missing or incomplete. Allow more time with CDXGEN_TIMEOUT_MS (milliseconds, every external command).`;
933
+ }
934
+ if (reason === "buffer") {
935
+ return "WARN: dosai wrote more console output than CDXGEN_MAX_BUFFER allows and was stopped, so its slices are missing or incomplete. Raise CDXGEN_MAX_BUFFER (bytes).";
936
+ }
937
+ return `WARN: dosai was stopped by ${signal || "a signal"} before it finished, so its slices are missing or incomplete. A SIGKILL usually means the system ran out of memory.`;
938
+ }
939
+
678
940
  /**
679
941
  * Run a dosai subcommand ("methods", "dataflows", or "crypto") against a source
680
942
  * tree and write its JSON output to the given file.
681
943
  *
944
+ * Flags cdxgen adds for its own benefit that an older dosai may not know
945
+ * (--exclude, --sections, --crypto-dataflows) are optional: when dosai reports
946
+ * them as unknown, every rejected one is dropped in a single retry, later runs
947
+ * of the same executable skip them up front, and dropping is reported once
948
+ * (a warning for --exclude, whose loss changes results; a debug line for the
949
+ * size-only flags). A run that rejects anything else has simply failed.
950
+ *
682
951
  * @param {string} command Dosai subcommand to execute
683
952
  * @param {string} src Source directory to analyze
684
953
  * @param {string} outputFile Path where the dosai JSON output is written
685
- * @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, patternPacks, or exclude globs
954
+ * @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, patternPacks, exclude globs, or dosaiMethodsSections
686
955
  * @returns {boolean} True when the command succeeded and produced the output file, false otherwise
687
956
  */
688
957
  export function runDosaiCommand(command, src, outputFile, options = {}) {
@@ -714,47 +983,138 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
714
983
  } else if (command === "crypto") {
715
984
  args.push("--format", "dosai");
716
985
  }
717
- const excludeArgs = dosaiWithoutExclude.has(executable)
718
- ? []
719
- : dosaiExcludeArgs(options.exclude, srcPath);
986
+ // Optional flag groups: the flag literal, the tokens to append while it is
987
+ // active, and whether dropping it changes what dosai analyses (true, only
988
+ // --exclude) or only the size of the output (false).
989
+ const flagGroups = [];
990
+ if (command === "crypto") {
991
+ // cdxgen reads only Assets, Operations, and Materials from the crypto
992
+ // report, so the data-flow graph it never reads is omitted.
993
+ flagGroups.push({
994
+ flag: "--crypto-dataflows",
995
+ tokens: ["--crypto-dataflows", "none"],
996
+ changesResults: false,
997
+ });
998
+ }
999
+ const sections =
1000
+ command === "methods"
1001
+ ? safeDosaiSections(options.dosaiMethodsSections)
1002
+ : undefined;
1003
+ if (sections) {
1004
+ flagGroups.push({
1005
+ flag: "--sections",
1006
+ tokens: ["--sections", sections],
1007
+ changesResults: false,
1008
+ });
1009
+ }
1010
+ const rejectedBefore = dosaiRejectedFlagMemo.get(executable);
1011
+ // An executable that rejected --exclude gets no exclude translation at all:
1012
+ // computing it would print warnings about patterns that are never passed.
1013
+ const excludeTokens =
1014
+ rejectedBefore?.has("--exclude") || !options.exclude
1015
+ ? []
1016
+ : dosaiExcludeArgs(options.exclude, srcPath);
1017
+ if (excludeTokens.length) {
1018
+ flagGroups.push({
1019
+ flag: "--exclude",
1020
+ tokens: excludeTokens,
1021
+ changesResults: true,
1022
+ });
1023
+ }
1024
+ const activeGroups = flagGroups.filter(
1025
+ (group) => !rejectedBefore?.has(group.flag),
1026
+ );
1027
+ const commandArgs = [
1028
+ ...args,
1029
+ ...activeGroups.flatMap((group) => group.tokens),
1030
+ ];
720
1031
  if (DEBUG_MODE) {
721
- console.log("Executing", executable, [...args, ...excludeArgs].join(" "));
1032
+ console.log("Executing", executable, commandArgs.join(" "));
722
1033
  }
723
- let result = safeSpawnSync(executable, [...args, ...excludeArgs], {
1034
+ stoppedDosaiOutputs.delete(outputPath);
1035
+ unstartedDosaiOutputs.delete(outputPath);
1036
+ dosaiOutputProducers.delete(outputPath);
1037
+ let startedAt = Date.now();
1038
+ let result = safeSpawnSync(executable, commandArgs, {
724
1039
  cwd: srcPath,
725
1040
  shell: false,
726
1041
  });
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
- });
1042
+ // Dosai releases that predate an optional flag reject it. Rerun once without
1043
+ // every flag this run rejected, so a newer cdxgen flag never turns into a
1044
+ // missing slice on an older dosai.
1045
+ if (result?.status !== 0) {
1046
+ const rejected = dosaiRejectedFlags(result);
1047
+ if (
1048
+ rejected.size &&
1049
+ [...rejected].every((flag) =>
1050
+ activeGroups.some((group) => group.flag === flag),
1051
+ )
1052
+ ) {
1053
+ const dropped = flagGroups.filter((group) => rejected.has(group.flag));
1054
+ const memo = dosaiRejectedFlagMemo.get(executable) ?? new Set();
1055
+ for (const group of dropped) {
1056
+ memo.add(group.flag);
1057
+ }
1058
+ dosaiRejectedFlagMemo.set(executable, memo);
1059
+ const droppedExclude = dropped.some((group) => group.changesResults);
1060
+ if (droppedExclude) {
1061
+ warnDosaiOnce(
1062
+ "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.",
1063
+ );
1064
+ }
1065
+ if (DEBUG_MODE) {
1066
+ console.log(
1067
+ `Retrying without the optional dosai flag(s) it rejected: ${dropped.map((group) => group.flag).join(", ")}.`,
1068
+ );
1069
+ }
1070
+ const retryArgs = [
1071
+ ...args,
1072
+ ...activeGroups
1073
+ .filter((group) => !rejected.has(group.flag))
1074
+ .flatMap((group) => group.tokens),
1075
+ ];
1076
+ startedAt = Date.now();
1077
+ result = safeSpawnSync(executable, retryArgs, {
1078
+ cwd: srcPath,
1079
+ shell: false,
1080
+ });
1081
+ }
742
1082
  }
743
- if (dosaiSdkMessage(result)) {
1083
+ const dotnetRequirement = dosaiDotnetRequirement(result);
1084
+ if (dotnetRequirement) {
1085
+ const detail = dosaiDotnetRequirementDetail(dotnetRequirement);
744
1086
  recordDegradation("dotnet.sdk.missing", {
745
1087
  ecosystem: "csharp",
746
1088
  tool: "dotnet",
747
1089
  impact: "none",
748
1090
  command: `${basename(executable)} ${args.join(" ")}`,
749
- detail:
750
- "dosai reported that no .NET SDK is installed, so the requested dosai analysis could not run.",
1091
+ detail: `${detail}, so the requested dosai analysis could not run.`,
751
1092
  });
752
- console.log(
753
- "Dotnet SDK is not installed. Please use the cdxgen dotnet container images to analyze this project with dosai.",
754
- );
755
- console.log(
756
- "Alternatively, download the dosai self-contained binary (-full suffix) from https://github.com/owasp-dep-scan/dosai/releases and set DOSAI_CMD to its location.",
757
- );
1093
+ // Visible without debug output, and once per scan: every later run of
1094
+ // this executable fails the same way.
1095
+ if (!dosaiLaunchFailuresReported.has(executable)) {
1096
+ dosaiLaunchFailuresReported.add(executable);
1097
+ console.warn(
1098
+ `WARNING: ${detail}, so dosai could not run and the .NET evidence it provides (call graph, reachability, services, crypto) is missing. ${dosaiDotnetRequirementAdvice(dotnetRequirement)}`,
1099
+ );
1100
+ }
1101
+ unstartedDosaiOutputs.add(outputPath);
1102
+ }
1103
+ const stopReason = dosaiRunStopReason(result);
1104
+ if (stopReason) {
1105
+ // Visible without debug output: otherwise a stopped dosai reads like any
1106
+ // other failure while the scan silently loses its .NET evidence.
1107
+ console.warn(dosaiStopMessage(stopReason, result.signal));
1108
+ // Windows kills only the dosai process itself, and the build hosts it
1109
+ // started would keep running.
1110
+ reapProcessTree(result.pid, startedAt, Date.now(), "dosai");
1111
+ // A stopped dosai can leave its output half written, and the caller would
1112
+ // read the truncated file back as a slice.
1113
+ if (safeExistsSync(outputPath)) {
1114
+ safeRmSync(outputPath, { force: true });
1115
+ }
1116
+ stoppedDosaiOutputs.add(outputPath);
1117
+ return false;
758
1118
  }
759
1119
  if (result?.status !== 0 || result?.error || !safeExistsSync(outputPath)) {
760
1120
  if (DEBUG_MODE) {
@@ -766,6 +1126,10 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
766
1126
  }
767
1127
  return false;
768
1128
  }
1129
+ dosaiOutputProducers.set(outputPath, {
1130
+ executable,
1131
+ bundled: !options.dosaiCommand && !readEnvironmentVariable("DOSAI_CMD"),
1132
+ });
769
1133
  return true;
770
1134
  }
771
1135
 
@@ -774,7 +1138,7 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
774
1138
  *
775
1139
  * @param {string} src Source directory to analyze
776
1140
  * @param {string} outputFile Path where the methods slice JSON is written
777
- * @param {Object} [options] Options forwarded to runDosaiCommand
1141
+ * @param {Object} [options] Options forwarded to runDosaiCommand; `dosaiMethodsSections` names the top-level sections to write (see dosaiMethodsSectionsArgument), omitted for the full report
778
1142
  * @returns {boolean} True when the slice was produced successfully
779
1143
  */
780
1144
  export function createDosaiMethodsSlice(src, outputFile, options = {}) {
@@ -827,6 +1191,141 @@ export function analyzeDosaiCrypto(src, options = {}) {
827
1191
  }
828
1192
  }
829
1193
 
1194
+ const DOSAI_PURL = "pkg:github/owasp-dep-scan/dosai";
1195
+ const DOSAI_VERSION_PATTERN = /^\d+(?:\.\d+){1,3}(?:-[0-9A-Za-z.-]+)?$/;
1196
+ // The version each dosai executable reported for itself, by executable.
1197
+ const dosaiExecutableVersions = new Map();
1198
+
1199
+ /**
1200
+ * The version a dosai executable reports with `--version`, without its build
1201
+ * metadata: `5.0.0-rc.2+e42b205` reads as `5.0.0-rc.2`. Each executable is
1202
+ * asked once per process.
1203
+ *
1204
+ * @param {string} executable dosai executable
1205
+ * @returns {string|undefined} The version, or undefined when dosai reported none
1206
+ */
1207
+ function dosaiExecutableVersion(executable) {
1208
+ const safeExecutable = safeDosaiExecutable(executable);
1209
+ if (!safeExecutable) {
1210
+ return undefined;
1211
+ }
1212
+ if (!dosaiExecutableVersions.has(safeExecutable)) {
1213
+ const result = safeSpawnSync(safeExecutable, ["--version"], {
1214
+ shell: false,
1215
+ });
1216
+ const reported =
1217
+ result?.status === 0 && !result.error
1218
+ ? `${result.stdout || ""}`.trim().split(/\s+/)[0].split("+")[0]
1219
+ : "";
1220
+ dosaiExecutableVersions.set(
1221
+ safeExecutable,
1222
+ DOSAI_VERSION_PATTERN.test(reported) ? reported : undefined,
1223
+ );
1224
+ }
1225
+ return dosaiExecutableVersions.get(safeExecutable);
1226
+ }
1227
+
1228
+ /**
1229
+ * The dosai version a report records about its producer. AnalyzerVersion is
1230
+ * the four-part assembly version, so `4.1.0.0` reads as `4.1.0`, and a
1231
+ * prerelease build reads as the release it precedes.
1232
+ *
1233
+ * @param {Object} [report] A dosai report
1234
+ * @returns {string|undefined} The version, or undefined when the report names no dosai version
1235
+ */
1236
+ function dosaiReportVersion(report) {
1237
+ const metadata = report?.Metadata;
1238
+ if (`${metadata?.Tool || ""}`.toLowerCase() !== "dosai") {
1239
+ return undefined;
1240
+ }
1241
+ const match = `${metadata.AnalyzerVersion || ""}`.match(
1242
+ /^(\d+\.\d+\.\d+)(?:\.(\d+))?$/,
1243
+ );
1244
+ if (!match) {
1245
+ return undefined;
1246
+ }
1247
+ return match[2] && match[2] !== "0" ? match[0] : match[1];
1248
+ }
1249
+
1250
+ function dosaiToolComponent(version, manifestComponent, bundled) {
1251
+ // The manifest component describes the bundled binary, hashes included
1252
+ if (bundled && version && manifestComponent?.version === version) {
1253
+ return structuredClone(manifestComponent);
1254
+ }
1255
+ const purl = version ? `${DOSAI_PURL}@${version}` : DOSAI_PURL;
1256
+ const component = { type: "application", name: "dosai" };
1257
+ if (version) {
1258
+ component.version = version;
1259
+ }
1260
+ if (manifestComponent?.description) {
1261
+ component.description = manifestComponent.description;
1262
+ }
1263
+ component.purl = purl;
1264
+ component["bom-ref"] = purl;
1265
+ for (const field of ["licenses", "externalReferences"]) {
1266
+ if (manifestComponent?.[field]) {
1267
+ component[field] = structuredClone(manifestComponent[field]);
1268
+ }
1269
+ }
1270
+ return component;
1271
+ }
1272
+
1273
+ /**
1274
+ * The tool component naming the dosai behind a methods report.
1275
+ *
1276
+ * A report that a dosai run of this process wrote names the executable that
1277
+ * wrote it, with the version that executable reports. A report reused from an
1278
+ * earlier run names the version recorded in its Metadata, or the configured
1279
+ * dosai's own version when that is a build of the same release. No component
1280
+ * is returned for a report that is missing or that no dosai wrote, such as
1281
+ * when the .NET host refused to start dosai.
1282
+ *
1283
+ * The cdxgen-plugins-bin manifest component is returned as-is only for its
1284
+ * own binary at its own version. Any other dosai gets a component built from
1285
+ * the manifest's project fields (description, licenses, references), without
1286
+ * the bundled binary's hashes or path.
1287
+ *
1288
+ * @param {string} reportFile Path of the dosai methods report
1289
+ * @param {Object} [report] The report as read, for its Metadata
1290
+ * @param {Object} [manifestComponent] dosai component of the cdxgen-plugins-bin manifest
1291
+ * @returns {Object[]} The dosai tool component, or an empty array
1292
+ */
1293
+ export function dosaiToolComponents(reportFile, report, manifestComponent) {
1294
+ const reportPath = safeDosaiPath(reportFile);
1295
+ if (!reportPath) {
1296
+ return [];
1297
+ }
1298
+ const producer = dosaiOutputProducers.get(reportPath);
1299
+ if (producer) {
1300
+ return [
1301
+ dosaiToolComponent(
1302
+ dosaiExecutableVersion(producer.executable) ||
1303
+ dosaiReportVersion(report),
1304
+ manifestComponent,
1305
+ producer.bundled,
1306
+ ),
1307
+ ];
1308
+ }
1309
+ const recorded = dosaiReportVersion(report);
1310
+ if (!recorded) {
1311
+ return [];
1312
+ }
1313
+ const executable = dosaiBin();
1314
+ const configured = executable
1315
+ ? dosaiExecutableVersion(executable)
1316
+ : undefined;
1317
+ if (configured?.split("-")[0] === recorded) {
1318
+ return [
1319
+ dosaiToolComponent(
1320
+ configured,
1321
+ manifestComponent,
1322
+ !readEnvironmentVariable("DOSAI_CMD"),
1323
+ ),
1324
+ ];
1325
+ }
1326
+ return [dosaiToolComponent(recorded, manifestComponent, false)];
1327
+ }
1328
+
830
1329
  /**
831
1330
  * Build the combined native dosai report object persisted for downstream tools.
832
1331
  *
@@ -887,10 +1386,12 @@ function* combinedDosaiReportChunks(combined, parts) {
887
1386
  * analyzeGolemProject on branch feat/rusi-persist-report): when a semantics-
888
1387
  * slices path is provided, the FULL native report is written there and kept so
889
1388
  * downstream tools (depscan) can consume the complete methods + data-flow
890
- * facts that cdxgen only projects a subset of into the SBOM evidence. dotnet
891
- * does not otherwise use the semantics slice (atom is never run for dotnet),
892
- * so the path is free to carry the combined dosai report. Returns the resolved
893
- * durable path when something was persisted, otherwise undefined.
1389
+ * facts that cdxgen only projects a subset of into the SBOM evidence. A
1390
+ * methods report the user supplied with --usages-slices-file is written as
1391
+ * given, with a recorded degradation when it was trimmed with --sections.
1392
+ * dotnet does not otherwise use the semantics slice (atom is never run for
1393
+ * dotnet), so the path is free to carry the combined dosai report. Returns the
1394
+ * resolved durable path when something was persisted, otherwise undefined.
894
1395
  */
895
1396
  export function persistDosaiSemanticsReport(
896
1397
  options,
@@ -912,6 +1413,19 @@ export function persistDosaiSemanticsReport(
912
1413
  const combined = buildCombinedDosaiReport(methodsSlice, dataFlowSlice);
913
1414
  const methodsSource = dosaiReportSources.get(methodsSlice);
914
1415
  const dataFlowSource = dosaiReportSources.get(dataFlowSlice);
1416
+ // A methods report written with --sections is partial. Persist it as given
1417
+ // (a user-supplied usages slice is their choice), but say so, because the
1418
+ // semantics file is the source of truth for downstream tools.
1419
+ const omittedSections = dosaiOmittedSections(methodsSlice);
1420
+ if (omittedSections.length) {
1421
+ recordDegradation("dosai.semantics.trimmed-report", {
1422
+ ecosystem: "csharp",
1423
+ tool: "dosai",
1424
+ impact: "none",
1425
+ command: `persistDosaiSemanticsReport ${durablePath}`,
1426
+ detail: `The dosai methods report was written with --sections, so the persisted report omits: ${omittedSections.join(", ")}. Run dosai methods without --sections for the complete report.`,
1427
+ });
1428
+ }
915
1429
  try {
916
1430
  let text;
917
1431
  if (!methodsSource?.trimmed && !dataFlowSource?.trimmed) {
@@ -958,21 +1472,63 @@ export function persistDosaiSemanticsReport(
958
1472
  * Build a purl alias map for a list of components.
959
1473
  *
960
1474
  * @param {Object[]} [components] Component objects with purl fields
961
- * @returns {Map<string, string>} Map of exact purls and normalized type/namespace/name keys to canonical purls
1475
+ * @param {Object} [options] Options passed to {@link buildDosaiPurlAliasMap} (`srcPath`)
1476
+ * @returns {Map<string, string>} Map of component purls, carrying the version-free identity index
962
1477
  */
963
- export function buildPurlAliasMap(components = []) {
964
- return buildDosaiPurlAliasMap(components);
1478
+ export function buildPurlAliasMap(components = [], options = {}) {
1479
+ return buildDosaiPurlAliasMap(components, options);
965
1480
  }
966
1481
 
967
1482
  /**
968
- * Resolve a possibly-aliased purl to the canonical component purl.
1483
+ * Resolve a dosai purl to the canonical component purl, by version, and for a
1484
+ * package the BOM holds in several versions by the record's location.
969
1485
  *
970
1486
  * @param {string} purl Purl from a dosai report
971
1487
  * @param {Map<string, string>} purlAliasMap Alias map built by buildPurlAliasMap
972
- * @returns {string|undefined} Canonical component purl, the input purl when unaliased, or undefined when empty
1488
+ * @param {string} [location] Source location of the dosai record
1489
+ * @returns {string|undefined} Canonical component purl, the input purl when the BOM has no such package,
1490
+ * or undefined when empty or ambiguous
973
1491
  */
974
- export function resolveComponentPurl(purl, purlAliasMap) {
975
- return resolveDosaiComponentPurl(purl, purlAliasMap);
1492
+ export function resolveComponentPurl(purl, purlAliasMap, location) {
1493
+ return resolveDosaiComponentPurl(purl, purlAliasMap, location);
1494
+ }
1495
+
1496
+ /**
1497
+ * The component purls one PackageReachability fact belongs to: each of its
1498
+ * locations resolved in its own project (a versionless purl from unrestored
1499
+ * projects spans several), else the purl alone.
1500
+ *
1501
+ * @param {Object} reachability dosai PackageReachability entry
1502
+ * @param {Map<string, string>} purlAliasMap Alias map built by buildPurlAliasMap
1503
+ * @param {Object} [graph] Call graph lookups `{ edgesById, nodesById }`
1504
+ * @returns {string[]} Component purls
1505
+ */
1506
+ function reachabilityComponentPurls(reachability, purlAliasMap, graph = {}) {
1507
+ const locations = [
1508
+ ...(reachability.SourceLocations || []).map((location) =>
1509
+ dosaiSourceLocation(location),
1510
+ ),
1511
+ ...(reachability.EdgeIds || []).map((edgeId) =>
1512
+ dosaiSourceLocation(graph.edgesById?.get(edgeId)),
1513
+ ),
1514
+ ...(reachability.NodeIds || []).map((nodeId) =>
1515
+ dosaiSourceLocationFromNode(graph.nodesById?.get(nodeId)),
1516
+ ),
1517
+ ].filter(Boolean);
1518
+ const purls = new Set(
1519
+ locations
1520
+ .map((location) =>
1521
+ resolveComponentPurl(reachability.Purl, purlAliasMap, location),
1522
+ )
1523
+ .filter(Boolean),
1524
+ );
1525
+ if (!purls.size) {
1526
+ const purl = resolveComponentPurl(reachability.Purl, purlAliasMap);
1527
+ if (purl) {
1528
+ purls.add(purl);
1529
+ }
1530
+ }
1531
+ return [...purls];
976
1532
  }
977
1533
 
978
1534
  /**
@@ -1021,24 +1577,51 @@ export function addDosaiReachabilityProperties(component, reachability) {
1021
1577
  *
1022
1578
  * @param {Object} methodsSlice Parsed dosai methods slice JSON
1023
1579
  * @param {Object[]} [components] BOM components used to resolve purl aliases and mutate
1580
+ * @param {Object} [options] Options
1581
+ * @param {string} [options.srcPath] Directory dosai analyzed; a package the BOM holds in several
1582
+ * versions is matched by the project of the record's file
1024
1583
  * @returns {number} Number of distinct components enriched
1025
1584
  */
1026
- export function applyDosaiReachabilityEvidence(methodsSlice, components = []) {
1027
- const purlAliasMap = buildPurlAliasMap(components);
1585
+ export function applyDosaiReachabilityEvidence(
1586
+ methodsSlice,
1587
+ components = [],
1588
+ options = {},
1589
+ ) {
1590
+ const purlAliasMap = buildPurlAliasMap(components, options);
1028
1591
  const componentsByPurl = new Map(
1029
1592
  components
1030
1593
  .filter((component) => component?.purl)
1031
1594
  .map((component) => [component.purl, component]),
1032
1595
  );
1596
+ const graph = {
1597
+ edgesById: new Map(
1598
+ (methodsSlice?.CallGraph?.Edges || []).map((edge) => [edge.Id, edge]),
1599
+ ),
1600
+ nodesById: new Map(
1601
+ (methodsSlice?.CallGraph?.Nodes || []).map((node) => [node.Id, node]),
1602
+ ),
1603
+ };
1033
1604
  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;
1605
+ // The first fact applied to a component wins, so a fact that names the
1606
+ // component's exact purl goes before one that reached it by name.
1607
+ const facts = [...(methodsSlice?.PackageReachability || [])].sort(
1608
+ (a, b) =>
1609
+ Number(!componentsByPurl.has(a.Purl)) -
1610
+ Number(!componentsByPurl.has(b.Purl)),
1611
+ );
1612
+ for (const reachability of facts) {
1613
+ for (const purl of reachabilityComponentPurls(
1614
+ reachability,
1615
+ purlAliasMap,
1616
+ graph,
1617
+ )) {
1618
+ const component = componentsByPurl.get(purl);
1619
+ if (!component) {
1620
+ continue;
1621
+ }
1622
+ addDosaiReachabilityProperties(component, reachability);
1623
+ enriched.add(component);
1039
1624
  }
1040
- addDosaiReachabilityProperties(component, reachability);
1041
- enriched.add(component);
1042
1625
  }
1043
1626
  return enriched.size;
1044
1627
  }
@@ -1051,10 +1634,17 @@ export function applyDosaiReachabilityEvidence(methodsSlice, components = []) {
1051
1634
  *
1052
1635
  * @param {Object} methodsSlice Parsed dosai methods slice JSON
1053
1636
  * @param {Object[]} [components] BOM components used to resolve purl aliases
1637
+ * @param {Object} [options] Options
1638
+ * @param {string} [options.srcPath] Directory dosai analyzed; a package the BOM holds in several
1639
+ * versions is matched by the project of the record's file
1054
1640
  * @returns {Object} Object with purlLocationMap, purlModulesMap, and purlMethodsMap keyed by purl
1055
1641
  */
1056
- export function collectDosaiPurlEvidence(methodsSlice, components = []) {
1057
- const purlAliasMap = buildPurlAliasMap(components);
1642
+ export function collectDosaiPurlEvidence(
1643
+ methodsSlice,
1644
+ components = [],
1645
+ options = {},
1646
+ ) {
1647
+ const purlAliasMap = buildPurlAliasMap(components, options);
1058
1648
  const purlLocationMap = {};
1059
1649
  const purlModulesMap = {};
1060
1650
  const purlMethodsMap = {};
@@ -1066,7 +1656,11 @@ export function collectDosaiPurlEvidence(methodsSlice, components = []) {
1066
1656
  );
1067
1657
 
1068
1658
  for (const dependency of methodsSlice?.Dependencies || []) {
1069
- const purl = resolveComponentPurl(dependency.Purl, purlAliasMap);
1659
+ const purl = resolveComponentPurl(
1660
+ dependency.Purl,
1661
+ purlAliasMap,
1662
+ dependency.Path,
1663
+ );
1070
1664
  if (!purl) {
1071
1665
  continue;
1072
1666
  }
@@ -1079,42 +1673,61 @@ export function collectDosaiPurlEvidence(methodsSlice, components = []) {
1079
1673
  }
1080
1674
 
1081
1675
  for (const reachability of methodsSlice?.PackageReachability || []) {
1082
- const purl = resolveComponentPurl(reachability.Purl, purlAliasMap);
1083
- if (!purl) {
1084
- continue;
1085
- }
1676
+ // Every location resolves in its own project: a versionless purl from
1677
+ // unrestored projects spans projects restored at different versions.
1678
+ const resolveAt = (location) =>
1679
+ resolveComponentPurl(reachability.Purl, purlAliasMap, location);
1680
+ const factPurls = new Set();
1681
+ // Modules and methods of call-graph items that name no source file
1682
+ // (frames inside the package), held for the fact's own version.
1683
+ const unplaced = [];
1086
1684
  let hasExplicitSourceLocations = false;
1087
1685
  for (const sourceLocation of reachability.SourceLocations || []) {
1088
1686
  const location = dosaiSourceLocation(sourceLocation);
1687
+ const purl = resolveAt(location);
1089
1688
  addDosaiSetValue(purlLocationMap, purl, location);
1689
+ if (purl) {
1690
+ factPurls.add(purl);
1691
+ }
1090
1692
  hasExplicitSourceLocations ||= Boolean(location);
1091
1693
  }
1092
1694
  for (const edgeId of reachability.EdgeIds || []) {
1093
1695
  const edge = edgesById.get(edgeId);
1696
+ const location = dosaiSourceLocation(edge);
1697
+ const purl = resolveAt(location ?? edge?.Path);
1698
+ const method = edge?.CalledMethodName || edge?.TargetName;
1699
+ if (!purl) {
1700
+ unplaced.push([purlMethodsMap, method]);
1701
+ continue;
1702
+ }
1703
+ factPurls.add(purl);
1094
1704
  if (!hasExplicitSourceLocations) {
1095
- addDosaiSetValue(purlLocationMap, purl, dosaiSourceLocation(edge));
1705
+ addDosaiSetValue(purlLocationMap, purl, location);
1096
1706
  }
1097
- addDosaiSetValue(
1098
- purlMethodsMap,
1099
- purl,
1100
- edge?.CalledMethodName || edge?.TargetName,
1101
- );
1707
+ addDosaiSetValue(purlMethodsMap, purl, method);
1102
1708
  }
1103
1709
  for (const nodeId of reachability.NodeIds || []) {
1104
1710
  const node = nodesById.get(nodeId);
1711
+ const location = dosaiSourceLocationFromNode(node);
1712
+ const purl = resolveAt(location);
1713
+ const module = node?.ClassName || node?.Module;
1714
+ const method = node?.Name || node?.Identity?.MethodName;
1715
+ if (!purl) {
1716
+ unplaced.push([purlModulesMap, module], [purlMethodsMap, method]);
1717
+ continue;
1718
+ }
1719
+ factPurls.add(purl);
1105
1720
  if (!hasExplicitSourceLocations) {
1106
- addDosaiSetValue(
1107
- purlLocationMap,
1108
- purl,
1109
- dosaiSourceLocationFromNode(node),
1110
- );
1721
+ addDosaiSetValue(purlLocationMap, purl, location);
1722
+ }
1723
+ addDosaiSetValue(purlModulesMap, purl, module);
1724
+ addDosaiSetValue(purlMethodsMap, purl, method);
1725
+ }
1726
+ if (factPurls.size === 1) {
1727
+ const [purl] = factPurls;
1728
+ for (const [map, value] of unplaced) {
1729
+ addDosaiSetValue(map, purl, value);
1111
1730
  }
1112
- addDosaiSetValue(purlModulesMap, purl, node?.ClassName || node?.Module);
1113
- addDosaiSetValue(
1114
- purlMethodsMap,
1115
- purl,
1116
- node?.Name || node?.Identity?.MethodName,
1117
- );
1118
1731
  }
1119
1732
  }
1120
1733
  return { purlLocationMap, purlModulesMap, purlMethodsMap };
@@ -1128,17 +1741,38 @@ export function collectDosaiPurlEvidence(methodsSlice, components = []) {
1128
1741
  *
1129
1742
  * @param {Object} dataFlowResult Parsed dosai data-flow slice JSON
1130
1743
  * @param {Object[]} [components] BOM components used to resolve purl aliases
1744
+ * @param {Object} [options] Options
1745
+ * @param {string} [options.srcPath] Directory dosai analyzed; a package the BOM holds in several
1746
+ * versions is matched by the project of the record's file
1131
1747
  * @returns {Object} Map of canonical purl to arrays of call-stack frame objects
1132
1748
  */
1133
- export function collectDosaiDataFlowFrames(dataFlowResult, components = []) {
1134
- const purlAliasMap = buildPurlAliasMap(components);
1749
+ export function collectDosaiDataFlowFrames(
1750
+ dataFlowResult,
1751
+ components = [],
1752
+ options = {},
1753
+ ) {
1754
+ const purlAliasMap = buildPurlAliasMap(components, options);
1135
1755
  const nodesById = new Map(
1136
1756
  (dataFlowResult?.Nodes || []).map((node) => [node.Id, node]),
1137
1757
  );
1138
1758
  const dataFlowFrames = {};
1139
1759
  const addFramesForPurl = (purl, frames) => {
1140
- const componentPurl = resolveComponentPurl(purl, purlAliasMap);
1141
- if (!componentPurl || !frames.length) {
1760
+ if (!frames.length) {
1761
+ return;
1762
+ }
1763
+ // The flow runs in the project of its first frame that settles one.
1764
+ let componentPurl;
1765
+ for (const frame of frames) {
1766
+ componentPurl = resolveComponentPurl(
1767
+ purl,
1768
+ purlAliasMap,
1769
+ frame.fullFilename,
1770
+ );
1771
+ if (componentPurl) {
1772
+ break;
1773
+ }
1774
+ }
1775
+ if (!componentPurl) {
1142
1776
  return;
1143
1777
  }
1144
1778
  dataFlowFrames[componentPurl] ??= [];
@@ -1575,6 +2209,12 @@ export function collectDosaiServicesFromMethods(
1575
2209
  /**
1576
2210
  * Normalize a services map into a sorted array of CycloneDX service objects.
1577
2211
  *
2212
+ * This is where evinse's service maps — dosai, golem, kosi, scalasem, an
2213
+ * OpenAPI document, and the atom usage and type detectors — become
2214
+ * `services[]`, so it is also where every endpoint is percent-encoded into the
2215
+ * iri-reference CycloneDX requires. The collectors keep the readable template
2216
+ * (`/users/{id}`); the BOM gets `/users/%7Bid%7D`.
2217
+ *
1578
2218
  * @param {Object} [servicesMap] Map of service name to service definition with Set-backed endpoints
1579
2219
  * @returns {Object[]} Array of service objects with sorted endpoint arrays and properties
1580
2220
  */
@@ -1582,12 +2222,18 @@ export function normalizeDosaiServiceMap(servicesMap = {}) {
1582
2222
  return Object.keys(servicesMap)
1583
2223
  .map((serviceName) => {
1584
2224
  const definition = servicesMap[serviceName];
2225
+ const endpoints = new Set();
2226
+ for (const endpoint of definition.endpoints || []) {
2227
+ if (typeof endpoint === "string" && endpoint) {
2228
+ endpoints.add(encodeUriReference(endpoint));
2229
+ }
2230
+ }
1585
2231
  const service = {
1586
2232
  name:
1587
2233
  definition.name ||
1588
2234
  serviceName ||
1589
2235
  `dosai-${basename(serviceName)}-service`,
1590
- endpoints: Array.from(definition.endpoints || []).sort(),
2236
+ endpoints: Array.from(endpoints).sort(),
1591
2237
  authenticated: definition.authenticated,
1592
2238
  // Accept both spellings. Every pre-4.0.0 producer — collectDosaiServicesFromMethods here, and
1593
2239
  // the OpenAPI/Java/Python paths in evinser.js — sets the camelCase `xTrustBoundary`; only the