@cdxgen/cdxgen 13.4.0 → 13.6.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 (305) hide show
  1. package/README.md +9 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +29 -5
  4. package/bin/evinse.js +8 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/bin/validate.js +6 -0
  8. package/data/cbomosdb-queries.json +13 -1
  9. package/data/component-tags.json +8 -0
  10. package/data/license-aliases.json +24 -2
  11. package/data/queries-darwin.json +25 -1
  12. package/data/queries-win.json +19 -1
  13. package/data/queries.json +41 -0
  14. package/data/remediations.json +407 -1
  15. package/data/rules/ai-bom.yaml +1 -1
  16. package/data/rules/brotli-decoder.yaml +107 -0
  17. package/data/rules/cbom-security.yaml +235 -4
  18. package/data/rules/go-binaries.yaml +46 -0
  19. package/data/rules/golem-go.yaml +80 -0
  20. package/data/rules/hbom-compliance.yaml +40 -22
  21. package/data/rules/hbom-performance.yaml +7 -1
  22. package/data/rules/host-topology.yaml +1 -1
  23. package/data/rules/obom-runtime.yaml +31 -2
  24. package/data/rules/php-runtime.yaml +143 -0
  25. package/data/rules/vscode-extensions.yaml +75 -0
  26. package/data/spdx-license-list.json +121 -7
  27. package/data/spdx-licenses.json +69 -0
  28. package/data/spdx.schema.json +845 -2
  29. package/lib/audit/index.js +14 -0
  30. package/lib/cli/bomAssembly.js +120 -6
  31. package/lib/cli/cliOptions.js +110 -3
  32. package/lib/cli/index.js +182 -6
  33. package/lib/cli/jsBom.js +153 -64
  34. package/lib/cli/jvmBom.js +363 -66
  35. package/lib/cli/managedBom.js +853 -87
  36. package/lib/cli/nativeBom.js +267 -33
  37. package/lib/core/boundedFile.js +344 -0
  38. package/lib/core/brotliProbe.js +182 -0
  39. package/lib/core/env.js +38 -8
  40. package/lib/core/fs.js +40 -19
  41. package/lib/core/httpClient.js +172 -46
  42. package/lib/core/lockPaths.js +38 -0
  43. package/lib/core/paths.js +24 -1
  44. package/lib/core/processTree.js +5 -0
  45. package/lib/core/projectConfig.js +1 -0
  46. package/lib/core/projectTooling.js +97 -0
  47. package/lib/core/propertySanitizer.js +72 -0
  48. package/lib/core/toolRegistry.js +170 -0
  49. package/lib/ecosystems/cppEvidence.js +263 -106
  50. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  51. package/lib/ecosystems/ecosystems.js +25 -10
  52. package/lib/ecosystems/gemHostProbe.js +301 -0
  53. package/lib/ecosystems/golem.js +550 -6
  54. package/lib/ecosystems/gradleutils.js +858 -244
  55. package/lib/ecosystems/parsers-bazel.js +107 -6
  56. package/lib/ecosystems/parsers-dotnet.js +117 -18
  57. package/lib/ecosystems/parsers-go.js +1090 -367
  58. package/lib/ecosystems/parsers-js.js +505 -22
  59. package/lib/ecosystems/parsers-jvm.js +259 -17
  60. package/lib/ecosystems/parsers-misc.js +438 -64
  61. package/lib/ecosystems/parsers-python.js +100 -12
  62. package/lib/ecosystems/pythonutils.js +128 -2
  63. package/lib/ecosystems/rubyutils.js +544 -34
  64. package/lib/ecosystems/rusi.js +10 -0
  65. package/lib/ecosystems/scalacliutils.js +4 -1
  66. package/lib/ecosystems/scalasem.js +798 -151
  67. package/lib/ecosystems/vendoredCode.js +237 -3
  68. package/lib/ecosystems/venvProbe.js +360 -0
  69. package/lib/evinser/evinser.js +472 -61
  70. package/lib/evinser/swiftsem.js +68 -5
  71. package/lib/helpers/agenticToolutils.js +413 -93
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentSkillSpec.js +17 -0
  74. package/lib/inventory/apkInstalledDb.js +243 -0
  75. package/lib/inventory/atomUtils.js +110 -0
  76. package/lib/inventory/auditCategories.js +3 -3
  77. package/lib/inventory/brotliDecoder.js +142 -0
  78. package/lib/inventory/buildToolRateLimit.js +1 -1
  79. package/lib/inventory/cacheDir.js +52 -9
  80. package/lib/inventory/cbomutils.js +56 -7
  81. package/lib/inventory/certificates.js +2371 -0
  82. package/lib/inventory/communityAiConfigParser.js +4 -9
  83. package/lib/inventory/deps.js +41 -4
  84. package/lib/inventory/display.js +200 -3
  85. package/lib/inventory/dosai.js +639 -63
  86. package/lib/inventory/dotnetRuntime.js +217 -0
  87. package/lib/inventory/dynamic.js +45 -3
  88. package/lib/inventory/envcontext.js +475 -18
  89. package/lib/inventory/evidenceUtils.js +447 -23
  90. package/lib/inventory/fetchBatch.js +1 -1
  91. package/lib/inventory/formulationParsers.js +32 -3
  92. package/lib/inventory/goBuildInfo.js +140 -0
  93. package/lib/inventory/gtfobins.js +13 -0
  94. package/lib/inventory/hbom.js +67 -7
  95. package/lib/inventory/hbomAnalysis.js +16 -0
  96. package/lib/inventory/hostRuntimeProbe.js +686 -0
  97. package/lib/inventory/hostTopology.js +21 -0
  98. package/lib/inventory/jvmLocalRepos.js +9 -2
  99. package/lib/inventory/jvmToolEnv.js +29 -0
  100. package/lib/inventory/licenseEnhancer.js +12 -0
  101. package/lib/inventory/mavenCentralMirror.js +534 -0
  102. package/lib/inventory/nodeBuildInfo.js +218 -0
  103. package/lib/inventory/nodeHostProbe.js +202 -0
  104. package/lib/inventory/osPackagePurl.js +244 -0
  105. package/lib/inventory/osPackageResolver.js +271 -87
  106. package/lib/inventory/osinfo.js +79 -31
  107. package/lib/inventory/osqueryTransform.js +114 -5
  108. package/lib/inventory/phpBuildInfo.js +438 -0
  109. package/lib/inventory/phpHostProbe.js +185 -0
  110. package/lib/inventory/rootfsCerts.js +426 -0
  111. package/lib/inventory/rubyBuildInfo.js +972 -0
  112. package/lib/inventory/rubyHostProbe.js +218 -0
  113. package/lib/inventory/scalaCoords.js +4 -3
  114. package/lib/inventory/scalaDedupe.js +440 -0
  115. package/lib/inventory/source.js +256 -5
  116. package/lib/inventory/spdx.js +144 -0
  117. package/lib/inventory/traceRunner.js +5 -1
  118. package/lib/inventory/tracedCrypto.js +129 -0
  119. package/lib/managers/binary.js +866 -186
  120. package/lib/managers/docker.js +245 -12
  121. package/lib/parsers/email.js +277 -0
  122. package/lib/parsers/largeJson.js +62 -13
  123. package/lib/parsers/vscodeExtensionId.js +183 -0
  124. package/lib/server/server.js +198 -2
  125. package/lib/stages/postgen/annotator.js +17 -1
  126. package/lib/stages/postgen/introspection/reflect.js +4 -1
  127. package/lib/stages/postgen/postgen.js +5 -5
  128. package/lib/stages/postgen/ruleEngine.js +71 -6
  129. package/lib/stages/postgen/specVersionCompat.js +30 -0
  130. package/lib/stages/pregen/envAudit.js +1 -0
  131. package/lib/stages/pregen/pregen.js +231 -62
  132. package/lib/validator/bomValidator.js +32 -6
  133. package/lib/validator/generated/runtime.mjs +13 -0
  134. package/lib/validator/generated/validate-1.6.mjs +27 -8
  135. package/lib/validator/generated/validate-1.7.mjs +32 -13
  136. package/lib/validator/generated/validate-2.0.mjs +31 -12
  137. package/lib/validator/index.js +1 -1
  138. package/package.json +19 -18
  139. package/types/bin/cdxgen-secure.d.ts +3 -0
  140. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  141. package/types/bin/repl.d.ts.map +1 -1
  142. package/types/lib/audit/index.d.ts.map +1 -1
  143. package/types/lib/cli/bomAssembly.d.ts +22 -3
  144. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  145. package/types/lib/cli/cliOptions.d.ts +43 -0
  146. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  147. package/types/lib/cli/index.d.ts.map +1 -1
  148. package/types/lib/cli/jsBom.d.ts +1 -17
  149. package/types/lib/cli/jsBom.d.ts.map +1 -1
  150. package/types/lib/cli/jvmBom.d.ts +20 -0
  151. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  152. package/types/lib/cli/managedBom.d.ts.map +1 -1
  153. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  154. package/types/lib/core/boundedFile.d.ts +70 -0
  155. package/types/lib/core/boundedFile.d.ts.map +1 -0
  156. package/types/lib/core/brotliProbe.d.ts +37 -0
  157. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  158. package/types/lib/core/env.d.ts +22 -2
  159. package/types/lib/core/env.d.ts.map +1 -1
  160. package/types/lib/core/fs.d.ts.map +1 -1
  161. package/types/lib/core/httpClient.d.ts +24 -0
  162. package/types/lib/core/httpClient.d.ts.map +1 -1
  163. package/types/lib/core/lockPaths.d.ts +19 -0
  164. package/types/lib/core/lockPaths.d.ts.map +1 -0
  165. package/types/lib/core/processTree.d.ts +2 -0
  166. package/types/lib/core/processTree.d.ts.map +1 -1
  167. package/types/lib/core/projectConfig.d.ts.map +1 -1
  168. package/types/lib/core/projectTooling.d.ts +41 -0
  169. package/types/lib/core/projectTooling.d.ts.map +1 -0
  170. package/types/lib/core/propertySanitizer.d.ts +26 -0
  171. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  172. package/types/lib/core/toolRegistry.d.ts +36 -0
  173. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  174. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  175. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  176. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  177. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  178. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  179. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  180. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  181. package/types/lib/ecosystems/golem.d.ts +81 -1
  182. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  183. package/types/lib/ecosystems/gradleutils.d.ts +57 -21
  184. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  187. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  188. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  189. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  191. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +14 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  199. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  200. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  201. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  202. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  203. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  205. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  206. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  207. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  208. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  209. package/types/lib/evinser/evinser.d.ts +88 -8
  210. package/types/lib/evinser/evinser.d.ts.map +1 -1
  211. package/types/lib/evinser/swiftsem.d.ts +10 -0
  212. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  213. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  214. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  215. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  216. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  217. package/types/lib/inventory/atomUtils.d.ts +33 -0
  218. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  219. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  220. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  221. package/types/lib/inventory/cacheDir.d.ts +12 -4
  222. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  223. package/types/lib/inventory/cbomutils.d.ts +17 -0
  224. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  225. package/types/lib/inventory/certificates.d.ts +205 -0
  226. package/types/lib/inventory/certificates.d.ts.map +1 -0
  227. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  228. package/types/lib/inventory/deps.d.ts +2 -1
  229. package/types/lib/inventory/deps.d.ts.map +1 -1
  230. package/types/lib/inventory/display.d.ts +18 -0
  231. package/types/lib/inventory/display.d.ts.map +1 -1
  232. package/types/lib/inventory/dosai.d.ts +162 -7
  233. package/types/lib/inventory/dosai.d.ts.map +1 -1
  234. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  235. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  236. package/types/lib/inventory/dynamic.d.ts +6 -1
  237. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  238. package/types/lib/inventory/envcontext.d.ts +92 -8
  239. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  240. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  241. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  242. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  243. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  244. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  245. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  246. package/types/lib/inventory/hbom.d.ts.map +1 -1
  247. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  248. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  249. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  250. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  251. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  252. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  253. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  254. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  255. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  256. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  257. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  258. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  259. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  260. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  261. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  262. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  263. package/types/lib/inventory/osPackageResolver.d.ts +44 -2
  264. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  265. package/types/lib/inventory/osinfo.d.ts +31 -6
  266. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  267. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  268. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  269. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  270. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  271. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  272. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  273. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  274. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  275. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  276. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  277. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  278. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  279. package/types/lib/inventory/source.d.ts +38 -0
  280. package/types/lib/inventory/source.d.ts.map +1 -1
  281. package/types/lib/inventory/spdx.d.ts +15 -0
  282. package/types/lib/inventory/spdx.d.ts.map +1 -1
  283. package/types/lib/inventory/traceRunner.d.ts +4 -1
  284. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  285. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  286. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  287. package/types/lib/managers/binary.d.ts +10 -0
  288. package/types/lib/managers/binary.d.ts.map +1 -1
  289. package/types/lib/managers/docker.d.ts +24 -0
  290. package/types/lib/managers/docker.d.ts.map +1 -1
  291. package/types/lib/parsers/email.d.ts +19 -0
  292. package/types/lib/parsers/email.d.ts.map +1 -0
  293. package/types/lib/parsers/largeJson.d.ts +17 -3
  294. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  295. package/types/lib/server/server.d.ts +8 -0
  296. package/types/lib/server/server.d.ts.map +1 -1
  297. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  298. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  299. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  300. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  301. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  302. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  303. package/types/lib/stages/pregen/pregen.d.ts +26 -9
  304. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -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,
@@ -23,9 +23,11 @@ import {
23
23
  TIMEOUT_MS,
24
24
  } from "../core/fs.js";
25
25
  import { formatDuration, reapProcessTree } from "../core/processTree.js";
26
+ import { encodeUriReference } from "../core/propertySanitizer.js";
26
27
  import {
27
28
  indexJsonObject,
28
29
  MAX_JSON_TEXT_BYTES,
30
+ readJsonMember,
29
31
  readJsonRange,
30
32
  } from "../parsers/largeJson.js";
31
33
  import {
@@ -154,11 +156,7 @@ function sanitizeEndpoint(endpoint) {
154
156
  // CycloneDX types `services[].endpoints[]` as an iri-reference, where square
155
157
  // brackets are reserved for IPv6 literals and braces are excluded outright,
156
158
  // so the template is percent-encoded before it reaches the BOM.
157
- const encodedPath = routePath.replace(
158
- /[[\]{}]/g,
159
- (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
160
- );
161
- return encodedPath;
159
+ return encodeUriReference(routePath);
162
160
  }
163
161
 
164
162
  function serviceNameFromEndpoint(endpoint) {
@@ -169,15 +167,67 @@ function serviceNameFromEndpoint(endpoint) {
169
167
  .replace(/-+/g, "-");
170
168
  }
171
169
 
172
- function dosaiSdkMessage(result) {
173
- return (
174
- result?.stdout?.includes(
175
- "You must install or update .NET to run this application",
176
- ) ||
177
- result?.stderr?.includes(
178
- "You must install or update .NET to run this application",
179
- )
180
- );
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.`;
181
231
  }
182
232
 
183
233
  function safeDosaiPath(value) {
@@ -198,6 +248,16 @@ function safeDosaiPatternPacks(value) {
198
248
  .join(delimiter);
199
249
  }
200
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
+
201
261
  const MAX_DOSAI_EXCLUDES = 256;
202
262
  const MAX_BRACE_RANGE = 64;
203
263
  const GLOB_ESCAPABLE = "*?[]{}()!+@,";
@@ -209,7 +269,10 @@ const SAFE_DOSAI_GLOB = /^[\p{L}\p{N} _.+@#%~=/*?-]+$/u;
209
269
  // letters and "Program Files (x86)". Shell metacharacters are not allowed.
210
270
  const SAFE_DOSAI_EXECUTABLE = /^[\p{L}\p{N} _.,+@#%~=/\\:()-]+$/u;
211
271
  const warnedDosaiMessages = new Set();
212
- 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();
213
276
 
214
277
  function warnDosaiOnce(message) {
215
278
  if (!warnedDosaiMessages.has(message)) {
@@ -496,9 +559,25 @@ function dosaiExcludeArgs(excludes, srcPath) {
496
559
  return patterns.flatMap((pattern) => ["--exclude", pattern]);
497
560
  }
498
561
 
499
- 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) {
500
574
  const output = `${result?.stdout || ""}${result?.stderr || ""}`;
501
- 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;
502
581
  }
503
582
 
504
583
  function safeDosaiExecutable(value) {
@@ -539,9 +618,10 @@ export function readDosaiJsonFile(jsonFile) {
539
618
  }
540
619
  }
541
620
 
542
- // The sections of a dosai methods report that cdxgen reads. The rest (Methods,
543
- // Properties, Fields, Reachability, DeadCode, ...) only reaches downstream
544
- // 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.
545
625
  const DOSAI_METHODS_SECTIONS = [
546
626
  "Metadata",
547
627
  "Dependencies",
@@ -554,6 +634,84 @@ const DOSAI_METHODS_SECTIONS = [
554
634
  "AiComponents",
555
635
  ];
556
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
+
557
715
  // The file each report returned by readDosaiReport came from, and whether it
558
716
  // was trimmed to what cdxgen reads. persistDosaiSemanticsReport copies the
559
717
  // file rather than re-serialising a trimmed or oversized report.
@@ -569,6 +727,105 @@ function referencedIds(entries, field) {
569
727
  return ids;
570
728
  }
571
729
 
730
+ // The fields of a MethodCalls row cdxgen reads: the component it names (Purl
731
+ // or Module), where the call site is (Path, FileName, LineNumber) and what it
732
+ // calls (ClassName, CalledMethod). Everything else a report row carries -
733
+ // arguments, evidence kinds, caller identities - no cdxgen consumer reads,
734
+ // and a report with millions of calls cannot afford to hold them.
735
+ const DOSAI_METHOD_CALL_FIELDS = [
736
+ "Purl",
737
+ "Module",
738
+ "Path",
739
+ "FileName",
740
+ "LineNumber",
741
+ "ClassName",
742
+ "CalledMethod",
743
+ ];
744
+
745
+ /**
746
+ * The largest number of distinct method calls cdxgen keeps from one report.
747
+ *
748
+ * Reports in the millions of calls repeat the same call sites heavily, so the
749
+ * aggregated rows stay far below this; the cap exists so a report whose calls
750
+ * are all distinct degrades the occurrence evidence instead of the process.
751
+ *
752
+ * @returns {number} Cap on distinct kept rows
753
+ */
754
+ export function maxDosaiMethodCalls() {
755
+ const configured = Number.parseInt(
756
+ readEnvironmentVariable("CDXGEN_DOSAI_MAX_METHOD_CALLS") || "",
757
+ 10,
758
+ );
759
+ if (Number.isInteger(configured) && configured > 0) {
760
+ return configured;
761
+ }
762
+ return 1_000_000;
763
+ }
764
+
765
+ /**
766
+ * Build the `map` that turns report MethodCalls rows into what cdxgen keeps:
767
+ * rows the caller rejects are dropped, the rest are reduced to
768
+ * {@link DOSAI_METHOD_CALL_FIELDS} and aggregated by those fields, so a call
769
+ * site reached a million times is held once with a `CallCount`. Past
770
+ * {@link maxDosaiMethodCalls} distinct rows, further rows are dropped and one
771
+ * warning names the cap.
772
+ *
773
+ * @param {string} reportFile Report the rows come from, named in the warning
774
+ * @param {Object} [options] Options
775
+ * @param {Function} [options.keepMethodCall] Predicate choosing the rows to keep
776
+ * @param {number} [options.maxMethodCalls] Cap override, for tests
777
+ * @returns {{map: Function, finish: Function}} The row mapper, and the
778
+ * `finish` that reports a cap reached
779
+ */
780
+ function dosaiMethodCallCollector(reportFile, options = {}) {
781
+ const keep = options.keepMethodCall;
782
+ const max = options.maxMethodCalls ?? maxDosaiMethodCalls();
783
+ const keptByKey = new Map();
784
+ let dropped = 0;
785
+ return {
786
+ map(methodCall) {
787
+ if (keep && !keep(methodCall)) {
788
+ return undefined;
789
+ }
790
+ const compact = {};
791
+ for (const field of DOSAI_METHOD_CALL_FIELDS) {
792
+ if (methodCall?.[field] !== undefined) {
793
+ compact[field] = methodCall[field];
794
+ }
795
+ }
796
+ const key = DOSAI_METHOD_CALL_FIELDS.map((field) => compact[field]).join(
797
+ "\u0000",
798
+ );
799
+ const kept = keptByKey.get(key);
800
+ if (kept) {
801
+ kept.CallCount = (kept.CallCount ?? 1) + 1;
802
+ return undefined;
803
+ }
804
+ if (keptByKey.size >= max) {
805
+ dropped++;
806
+ return undefined;
807
+ }
808
+ keptByKey.set(key, compact);
809
+ return compact;
810
+ },
811
+ finish() {
812
+ if (!dropped) {
813
+ return;
814
+ }
815
+ console.warn(
816
+ `The dosai report "${reportFile}" holds more distinct method calls than cdxgen keeps (${max}); ${dropped} call sites were dropped, so occurrence evidence may be incomplete. Raise the cap with CDXGEN_DOSAI_MAX_METHOD_CALLS.`,
817
+ );
818
+ recordDegradation("dosai.method-calls-capped", {
819
+ ecosystem: "csharp",
820
+ tool: "dosai",
821
+ impact: "components",
822
+ command: reportFile,
823
+ detail: `The dosai report holds more distinct method calls than the cap of ${max}, so ${dropped} call sites were dropped from the occurrence evidence.`,
824
+ });
825
+ },
826
+ };
827
+ }
828
+
572
829
  function readDosaiReport(reportFile, options, readTrimmed) {
573
830
  if (!reportFile || !safeExistsSync(reportFile)) {
574
831
  return undefined;
@@ -603,7 +860,13 @@ function readDosaiReport(reportFile, options, readTrimmed) {
603
860
  }
604
861
  return report;
605
862
  } catch (err) {
606
- if (DEBUG_MODE) {
863
+ if (err instanceof RangeError) {
864
+ // A heap or string limit is a report-size problem the user can act on,
865
+ // unlike a malformed report, so it is named even without debug output.
866
+ console.warn(
867
+ `The dosai report "${reportFile}" is too large to read: ${err.message}. Raise the Node.js heap, for example NODE_OPTIONS="--max-old-space-size=8192", or write the report with dosai's --sections so it holds less.`,
868
+ );
869
+ } else if (DEBUG_MODE) {
607
870
  console.log(
608
871
  `Unable to read the dosai report "${reportFile}": ${err.message}`,
609
872
  );
@@ -618,25 +881,34 @@ function readDosaiReport(reportFile, options, readTrimmed) {
618
881
  * A report that fits in one JavaScript string is parsed whole. A larger one is
619
882
  * read in bounded runs, keeping the requested sections, the CallGraph nodes and
620
883
  * edges that PackageReachability references, and the MethodCalls accepted by
621
- * `keepMethodCall` (none when it is omitted). Nothing cdxgen reads is lost.
884
+ * `keepMethodCall` (none when it is omitted). A kept MethodCalls row is
885
+ * reduced to the fields cdxgen reads and aggregated by them, so a call site
886
+ * reached many times is held once with a `CallCount`; distinct rows past
887
+ * {@link maxDosaiMethodCalls} are dropped with one warning. Nothing cdxgen
888
+ * reads is lost.
622
889
  *
623
890
  * @param {string} reportFile Path to the dosai methods JSON
624
891
  * @param {Object} [options] Options
625
892
  * @param {string[]} [options.sections] Sections to keep from a large report (default: every section cdxgen reads)
626
893
  * @param {Function} [options.keepMethodCall] Predicate choosing the MethodCalls to keep from a large report
894
+ * @param {number} [options.maxMethodCalls] Cap on distinct kept MethodCalls, for tests
627
895
  * @param {number} [options.maxTextBytes] Largest report parsed whole, for tests
628
896
  * @returns {Object|undefined} Parsed report, or undefined when missing or invalid
629
897
  */
630
898
  export function readDosaiMethodsReport(reportFile, options = {}) {
631
899
  const sections = options.sections || DOSAI_METHODS_SECTIONS;
632
- return readDosaiReport(reportFile, options, (report, read) => {
900
+ const collector =
901
+ sections.includes("MethodCalls") && options.keepMethodCall
902
+ ? dosaiMethodCallCollector(reportFile, options)
903
+ : undefined;
904
+ const report = readDosaiReport(reportFile, options, (report, read) => {
633
905
  for (const section of sections) {
634
906
  if (section !== "CallGraph" && section !== "MethodCalls") {
635
907
  read(section);
636
908
  }
637
909
  }
638
- if (sections.includes("MethodCalls") && options.keepMethodCall) {
639
- read("MethodCalls", { filter: options.keepMethodCall });
910
+ if (collector) {
911
+ read("MethodCalls", { map: collector.map });
640
912
  }
641
913
  if (sections.includes("CallGraph")) {
642
914
  // CallGraph precedes PackageReachability in the file, which is why the
@@ -651,6 +923,26 @@ export function readDosaiMethodsReport(reportFile, options = {}) {
651
923
  });
652
924
  }
653
925
  });
926
+ // A report parsed whole kept every row, as it always has: reduce them to
927
+ // the fields cdxgen reads and aggregate repeats, without the caller's
928
+ // predicate, which the whole parse has never applied. A report that took
929
+ // the trimmed path already went through the collector above.
930
+ if (report && !dosaiReportSources.get(report)?.trimmed) {
931
+ const wholeCollector = dosaiMethodCallCollector(reportFile, {
932
+ maxMethodCalls: options.maxMethodCalls,
933
+ });
934
+ const reduced = [];
935
+ for (const methodCall of report.MethodCalls || []) {
936
+ const kept = wholeCollector.map(methodCall);
937
+ if (kept !== undefined) {
938
+ reduced.push(kept);
939
+ }
940
+ }
941
+ report.MethodCalls = reduced;
942
+ wholeCollector.finish();
943
+ }
944
+ collector?.finish();
945
+ return report;
654
946
  }
655
947
 
656
948
  /**
@@ -717,6 +1009,28 @@ export function dosaiRunStopReason(result) {
717
1009
  // The output files of the dosai runs of this process that were stopped, so a
718
1010
  // caller does not follow the stop warning with advice for another failure.
719
1011
  const stoppedDosaiOutputs = new Set();
1012
+ // The output files of the dosai runs the .NET host refused to start; the
1013
+ // warning naming the missing runtime already says why they are absent.
1014
+ const unstartedDosaiOutputs = new Set();
1015
+ // The executable that wrote each output file of this process, and whether it
1016
+ // was the cdxgen-plugins-bin build rather than an override.
1017
+ const dosaiOutputProducers = new Map();
1018
+ // Executables the .NET host refused to start, once that has been reported to
1019
+ // the user, by the pregen check or by a dosai run.
1020
+ const dosaiLaunchFailuresReported = new Set();
1021
+
1022
+ /**
1023
+ * Note that the .NET host's refusal to start a dosai executable has been
1024
+ * reported, so dosai runs of the same executable do not repeat the warning.
1025
+ *
1026
+ * @param {string} executable dosai executable
1027
+ */
1028
+ export function markDosaiLaunchFailureReported(executable) {
1029
+ const safeExecutable = safeDosaiExecutable(executable);
1030
+ if (safeExecutable) {
1031
+ dosaiLaunchFailuresReported.add(safeExecutable);
1032
+ }
1033
+ }
720
1034
 
721
1035
  /**
722
1036
  * Whether the dosai run that should have written a file was stopped (by the
@@ -730,6 +1044,23 @@ export function dosaiRunWasStopped(outputFile) {
730
1044
  return Boolean(outputPath && stoppedDosaiOutputs.has(outputPath));
731
1045
  }
732
1046
 
1047
+ /**
1048
+ * Whether the dosai run that should have written a file failed in a way it has
1049
+ * already reported: it was stopped (see dosaiRunWasStopped), or the .NET host
1050
+ * refused to start dosai and the warning named the missing runtime.
1051
+ *
1052
+ * @param {string} outputFile Output file passed to the dosai run
1053
+ * @returns {boolean} true when the failure has been reported
1054
+ */
1055
+ export function dosaiFailureReported(outputFile) {
1056
+ const outputPath = safeDosaiPath(outputFile);
1057
+ return Boolean(
1058
+ outputPath &&
1059
+ (stoppedDosaiOutputs.has(outputPath) ||
1060
+ unstartedDosaiOutputs.has(outputPath)),
1061
+ );
1062
+ }
1063
+
733
1064
  function dosaiStopMessage(reason, signal) {
734
1065
  if (reason === "timeout") {
735
1066
  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).`;
@@ -744,10 +1075,17 @@ function dosaiStopMessage(reason, signal) {
744
1075
  * Run a dosai subcommand ("methods", "dataflows", or "crypto") against a source
745
1076
  * tree and write its JSON output to the given file.
746
1077
  *
1078
+ * Flags cdxgen adds for its own benefit that an older dosai may not know
1079
+ * (--exclude, --sections, --crypto-dataflows) are optional: when dosai reports
1080
+ * them as unknown, every rejected one is dropped in a single retry, later runs
1081
+ * of the same executable skip them up front, and dropping is reported once
1082
+ * (a warning for --exclude, whose loss changes results; a debug line for the
1083
+ * size-only flags). A run that rejects anything else has simply failed.
1084
+ *
747
1085
  * @param {string} command Dosai subcommand to execute
748
1086
  * @param {string} src Source directory to analyze
749
1087
  * @param {string} outputFile Path where the dosai JSON output is written
750
- * @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, patternPacks, or exclude globs
1088
+ * @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, patternPacks, exclude globs, or dosaiMethodsSections
751
1089
  * @returns {boolean} True when the command succeeded and produced the output file, false otherwise
752
1090
  */
753
1091
  export function runDosaiCommand(command, src, outputFile, options = {}) {
@@ -779,50 +1117,122 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
779
1117
  } else if (command === "crypto") {
780
1118
  args.push("--format", "dosai");
781
1119
  }
782
- const excludeArgs = dosaiWithoutExclude.has(executable)
783
- ? []
784
- : dosaiExcludeArgs(options.exclude, srcPath);
1120
+ // Optional flag groups: the flag literal, the tokens to append while it is
1121
+ // active, and whether dropping it changes what dosai analyses (true, only
1122
+ // --exclude) or only the size of the output (false).
1123
+ const flagGroups = [];
1124
+ if (command === "crypto") {
1125
+ // cdxgen reads only Assets, Operations, and Materials from the crypto
1126
+ // report, so the data-flow graph it never reads is omitted.
1127
+ flagGroups.push({
1128
+ flag: "--crypto-dataflows",
1129
+ tokens: ["--crypto-dataflows", "none"],
1130
+ changesResults: false,
1131
+ });
1132
+ }
1133
+ const sections =
1134
+ command === "methods"
1135
+ ? safeDosaiSections(options.dosaiMethodsSections)
1136
+ : undefined;
1137
+ if (sections) {
1138
+ flagGroups.push({
1139
+ flag: "--sections",
1140
+ tokens: ["--sections", sections],
1141
+ changesResults: false,
1142
+ });
1143
+ }
1144
+ const rejectedBefore = dosaiRejectedFlagMemo.get(executable);
1145
+ // An executable that rejected --exclude gets no exclude translation at all:
1146
+ // computing it would print warnings about patterns that are never passed.
1147
+ const excludeTokens =
1148
+ rejectedBefore?.has("--exclude") || !options.exclude
1149
+ ? []
1150
+ : dosaiExcludeArgs(options.exclude, srcPath);
1151
+ if (excludeTokens.length) {
1152
+ flagGroups.push({
1153
+ flag: "--exclude",
1154
+ tokens: excludeTokens,
1155
+ changesResults: true,
1156
+ });
1157
+ }
1158
+ const activeGroups = flagGroups.filter(
1159
+ (group) => !rejectedBefore?.has(group.flag),
1160
+ );
1161
+ const commandArgs = [
1162
+ ...args,
1163
+ ...activeGroups.flatMap((group) => group.tokens),
1164
+ ];
785
1165
  if (DEBUG_MODE) {
786
- console.log("Executing", executable, [...args, ...excludeArgs].join(" "));
1166
+ console.log("Executing", executable, commandArgs.join(" "));
787
1167
  }
788
1168
  stoppedDosaiOutputs.delete(outputPath);
1169
+ unstartedDosaiOutputs.delete(outputPath);
1170
+ dosaiOutputProducers.delete(outputPath);
789
1171
  let startedAt = Date.now();
790
- let result = safeSpawnSync(executable, [...args, ...excludeArgs], {
1172
+ let result = safeSpawnSync(executable, commandArgs, {
791
1173
  cwd: srcPath,
792
1174
  shell: false,
793
1175
  });
794
- // Dosai releases that predate --exclude reject it. Rerun without it so a
795
- // user supplied exclude never turns into a missing slice.
796
- if (
797
- excludeArgs.length &&
798
- result?.status !== 0 &&
799
- dosaiRejectedExclude(result)
800
- ) {
801
- dosaiWithoutExclude.add(executable);
802
- warnDosaiOnce(
803
- "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.",
804
- );
805
- startedAt = Date.now();
806
- result = safeSpawnSync(executable, args, {
807
- cwd: srcPath,
808
- shell: false,
809
- });
1176
+ // Dosai releases that predate an optional flag reject it. Rerun once without
1177
+ // every flag this run rejected, so a newer cdxgen flag never turns into a
1178
+ // missing slice on an older dosai.
1179
+ if (result?.status !== 0) {
1180
+ const rejected = dosaiRejectedFlags(result);
1181
+ if (
1182
+ rejected.size &&
1183
+ [...rejected].every((flag) =>
1184
+ activeGroups.some((group) => group.flag === flag),
1185
+ )
1186
+ ) {
1187
+ const dropped = flagGroups.filter((group) => rejected.has(group.flag));
1188
+ const memo = dosaiRejectedFlagMemo.get(executable) ?? new Set();
1189
+ for (const group of dropped) {
1190
+ memo.add(group.flag);
1191
+ }
1192
+ dosaiRejectedFlagMemo.set(executable, memo);
1193
+ const droppedExclude = dropped.some((group) => group.changesResults);
1194
+ if (droppedExclude) {
1195
+ warnDosaiOnce(
1196
+ "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.",
1197
+ );
1198
+ }
1199
+ if (DEBUG_MODE) {
1200
+ console.log(
1201
+ `Retrying without the optional dosai flag(s) it rejected: ${dropped.map((group) => group.flag).join(", ")}.`,
1202
+ );
1203
+ }
1204
+ const retryArgs = [
1205
+ ...args,
1206
+ ...activeGroups
1207
+ .filter((group) => !rejected.has(group.flag))
1208
+ .flatMap((group) => group.tokens),
1209
+ ];
1210
+ startedAt = Date.now();
1211
+ result = safeSpawnSync(executable, retryArgs, {
1212
+ cwd: srcPath,
1213
+ shell: false,
1214
+ });
1215
+ }
810
1216
  }
811
- if (dosaiSdkMessage(result)) {
1217
+ const dotnetRequirement = dosaiDotnetRequirement(result);
1218
+ if (dotnetRequirement) {
1219
+ const detail = dosaiDotnetRequirementDetail(dotnetRequirement);
812
1220
  recordDegradation("dotnet.sdk.missing", {
813
1221
  ecosystem: "csharp",
814
1222
  tool: "dotnet",
815
1223
  impact: "none",
816
1224
  command: `${basename(executable)} ${args.join(" ")}`,
817
- detail:
818
- "dosai reported that no .NET SDK is installed, so the requested dosai analysis could not run.",
1225
+ detail: `${detail}, so the requested dosai analysis could not run.`,
819
1226
  });
820
- console.log(
821
- "Dotnet SDK is not installed. Please use the cdxgen dotnet container images to analyze this project with dosai.",
822
- );
823
- console.log(
824
- "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.",
825
- );
1227
+ // Visible without debug output, and once per scan: every later run of
1228
+ // this executable fails the same way.
1229
+ if (!dosaiLaunchFailuresReported.has(executable)) {
1230
+ dosaiLaunchFailuresReported.add(executable);
1231
+ console.warn(
1232
+ `WARNING: ${detail}, so dosai could not run and the .NET evidence it provides (call graph, reachability, services, crypto) is missing. ${dosaiDotnetRequirementAdvice(dotnetRequirement)}`,
1233
+ );
1234
+ }
1235
+ unstartedDosaiOutputs.add(outputPath);
826
1236
  }
827
1237
  const stopReason = dosaiRunStopReason(result);
828
1238
  if (stopReason) {
@@ -850,6 +1260,10 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
850
1260
  }
851
1261
  return false;
852
1262
  }
1263
+ dosaiOutputProducers.set(outputPath, {
1264
+ executable,
1265
+ bundled: !options.dosaiCommand && !readEnvironmentVariable("DOSAI_CMD"),
1266
+ });
853
1267
  return true;
854
1268
  }
855
1269
 
@@ -858,7 +1272,7 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
858
1272
  *
859
1273
  * @param {string} src Source directory to analyze
860
1274
  * @param {string} outputFile Path where the methods slice JSON is written
861
- * @param {Object} [options] Options forwarded to runDosaiCommand
1275
+ * @param {Object} [options] Options forwarded to runDosaiCommand; `dosaiMethodsSections` names the top-level sections to write (see dosaiMethodsSectionsArgument), omitted for the full report
862
1276
  * @returns {boolean} True when the slice was produced successfully
863
1277
  */
864
1278
  export function createDosaiMethodsSlice(src, outputFile, options = {}) {
@@ -911,6 +1325,141 @@ export function analyzeDosaiCrypto(src, options = {}) {
911
1325
  }
912
1326
  }
913
1327
 
1328
+ const DOSAI_PURL = "pkg:github/owasp-dep-scan/dosai";
1329
+ const DOSAI_VERSION_PATTERN = /^\d+(?:\.\d+){1,3}(?:-[0-9A-Za-z.-]+)?$/;
1330
+ // The version each dosai executable reported for itself, by executable.
1331
+ const dosaiExecutableVersions = new Map();
1332
+
1333
+ /**
1334
+ * The version a dosai executable reports with `--version`, without its build
1335
+ * metadata: `5.0.0-rc.2+e42b205` reads as `5.0.0-rc.2`. Each executable is
1336
+ * asked once per process.
1337
+ *
1338
+ * @param {string} executable dosai executable
1339
+ * @returns {string|undefined} The version, or undefined when dosai reported none
1340
+ */
1341
+ function dosaiExecutableVersion(executable) {
1342
+ const safeExecutable = safeDosaiExecutable(executable);
1343
+ if (!safeExecutable) {
1344
+ return undefined;
1345
+ }
1346
+ if (!dosaiExecutableVersions.has(safeExecutable)) {
1347
+ const result = safeSpawnSync(safeExecutable, ["--version"], {
1348
+ shell: false,
1349
+ });
1350
+ const reported =
1351
+ result?.status === 0 && !result.error
1352
+ ? `${result.stdout || ""}`.trim().split(/\s+/)[0].split("+")[0]
1353
+ : "";
1354
+ dosaiExecutableVersions.set(
1355
+ safeExecutable,
1356
+ DOSAI_VERSION_PATTERN.test(reported) ? reported : undefined,
1357
+ );
1358
+ }
1359
+ return dosaiExecutableVersions.get(safeExecutable);
1360
+ }
1361
+
1362
+ /**
1363
+ * The dosai version a report records about its producer. AnalyzerVersion is
1364
+ * the four-part assembly version, so `4.1.0.0` reads as `4.1.0`, and a
1365
+ * prerelease build reads as the release it precedes.
1366
+ *
1367
+ * @param {Object} [report] A dosai report
1368
+ * @returns {string|undefined} The version, or undefined when the report names no dosai version
1369
+ */
1370
+ function dosaiReportVersion(report) {
1371
+ const metadata = report?.Metadata;
1372
+ if (`${metadata?.Tool || ""}`.toLowerCase() !== "dosai") {
1373
+ return undefined;
1374
+ }
1375
+ const match = `${metadata.AnalyzerVersion || ""}`.match(
1376
+ /^(\d+\.\d+\.\d+)(?:\.(\d+))?$/,
1377
+ );
1378
+ if (!match) {
1379
+ return undefined;
1380
+ }
1381
+ return match[2] && match[2] !== "0" ? match[0] : match[1];
1382
+ }
1383
+
1384
+ function dosaiToolComponent(version, manifestComponent, bundled) {
1385
+ // The manifest component describes the bundled binary, hashes included
1386
+ if (bundled && version && manifestComponent?.version === version) {
1387
+ return structuredClone(manifestComponent);
1388
+ }
1389
+ const purl = version ? `${DOSAI_PURL}@${version}` : DOSAI_PURL;
1390
+ const component = { type: "application", name: "dosai" };
1391
+ if (version) {
1392
+ component.version = version;
1393
+ }
1394
+ if (manifestComponent?.description) {
1395
+ component.description = manifestComponent.description;
1396
+ }
1397
+ component.purl = purl;
1398
+ component["bom-ref"] = purl;
1399
+ for (const field of ["licenses", "externalReferences"]) {
1400
+ if (manifestComponent?.[field]) {
1401
+ component[field] = structuredClone(manifestComponent[field]);
1402
+ }
1403
+ }
1404
+ return component;
1405
+ }
1406
+
1407
+ /**
1408
+ * The tool component naming the dosai behind a methods report.
1409
+ *
1410
+ * A report that a dosai run of this process wrote names the executable that
1411
+ * wrote it, with the version that executable reports. A report reused from an
1412
+ * earlier run names the version recorded in its Metadata, or the configured
1413
+ * dosai's own version when that is a build of the same release. No component
1414
+ * is returned for a report that is missing or that no dosai wrote, such as
1415
+ * when the .NET host refused to start dosai.
1416
+ *
1417
+ * The cdxgen-plugins-bin manifest component is returned as-is only for its
1418
+ * own binary at its own version. Any other dosai gets a component built from
1419
+ * the manifest's project fields (description, licenses, references), without
1420
+ * the bundled binary's hashes or path.
1421
+ *
1422
+ * @param {string} reportFile Path of the dosai methods report
1423
+ * @param {Object} [report] The report as read, for its Metadata
1424
+ * @param {Object} [manifestComponent] dosai component of the cdxgen-plugins-bin manifest
1425
+ * @returns {Object[]} The dosai tool component, or an empty array
1426
+ */
1427
+ export function dosaiToolComponents(reportFile, report, manifestComponent) {
1428
+ const reportPath = safeDosaiPath(reportFile);
1429
+ if (!reportPath) {
1430
+ return [];
1431
+ }
1432
+ const producer = dosaiOutputProducers.get(reportPath);
1433
+ if (producer) {
1434
+ return [
1435
+ dosaiToolComponent(
1436
+ dosaiExecutableVersion(producer.executable) ||
1437
+ dosaiReportVersion(report),
1438
+ manifestComponent,
1439
+ producer.bundled,
1440
+ ),
1441
+ ];
1442
+ }
1443
+ const recorded = dosaiReportVersion(report);
1444
+ if (!recorded) {
1445
+ return [];
1446
+ }
1447
+ const executable = dosaiBin();
1448
+ const configured = executable
1449
+ ? dosaiExecutableVersion(executable)
1450
+ : undefined;
1451
+ if (configured?.split("-")[0] === recorded) {
1452
+ return [
1453
+ dosaiToolComponent(
1454
+ configured,
1455
+ manifestComponent,
1456
+ !readEnvironmentVariable("DOSAI_CMD"),
1457
+ ),
1458
+ ];
1459
+ }
1460
+ return [dosaiToolComponent(recorded, manifestComponent, false)];
1461
+ }
1462
+
914
1463
  /**
915
1464
  * Build the combined native dosai report object persisted for downstream tools.
916
1465
  *
@@ -971,10 +1520,12 @@ function* combinedDosaiReportChunks(combined, parts) {
971
1520
  * analyzeGolemProject on branch feat/rusi-persist-report): when a semantics-
972
1521
  * slices path is provided, the FULL native report is written there and kept so
973
1522
  * downstream tools (depscan) can consume the complete methods + data-flow
974
- * facts that cdxgen only projects a subset of into the SBOM evidence. dotnet
975
- * does not otherwise use the semantics slice (atom is never run for dotnet),
976
- * so the path is free to carry the combined dosai report. Returns the resolved
977
- * durable path when something was persisted, otherwise undefined.
1523
+ * facts that cdxgen only projects a subset of into the SBOM evidence. A
1524
+ * methods report the user supplied with --usages-slices-file is written as
1525
+ * given, with a recorded degradation when it was trimmed with --sections.
1526
+ * dotnet does not otherwise use the semantics slice (atom is never run for
1527
+ * dotnet), so the path is free to carry the combined dosai report. Returns the
1528
+ * resolved durable path when something was persisted, otherwise undefined.
978
1529
  */
979
1530
  export function persistDosaiSemanticsReport(
980
1531
  options,
@@ -996,6 +1547,19 @@ export function persistDosaiSemanticsReport(
996
1547
  const combined = buildCombinedDosaiReport(methodsSlice, dataFlowSlice);
997
1548
  const methodsSource = dosaiReportSources.get(methodsSlice);
998
1549
  const dataFlowSource = dosaiReportSources.get(dataFlowSlice);
1550
+ // A methods report written with --sections is partial. Persist it as given
1551
+ // (a user-supplied usages slice is their choice), but say so, because the
1552
+ // semantics file is the source of truth for downstream tools.
1553
+ const omittedSections = dosaiOmittedSections(methodsSlice);
1554
+ if (omittedSections.length) {
1555
+ recordDegradation("dosai.semantics.trimmed-report", {
1556
+ ecosystem: "csharp",
1557
+ tool: "dosai",
1558
+ impact: "none",
1559
+ command: `persistDosaiSemanticsReport ${durablePath}`,
1560
+ 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.`,
1561
+ });
1562
+ }
999
1563
  try {
1000
1564
  let text;
1001
1565
  if (!methodsSource?.trimmed && !dataFlowSource?.trimmed) {
@@ -1779,6 +2343,12 @@ export function collectDosaiServicesFromMethods(
1779
2343
  /**
1780
2344
  * Normalize a services map into a sorted array of CycloneDX service objects.
1781
2345
  *
2346
+ * This is where evinse's service maps — dosai, golem, kosi, scalasem, an
2347
+ * OpenAPI document, and the atom usage and type detectors — become
2348
+ * `services[]`, so it is also where every endpoint is percent-encoded into the
2349
+ * iri-reference CycloneDX requires. The collectors keep the readable template
2350
+ * (`/users/{id}`); the BOM gets `/users/%7Bid%7D`.
2351
+ *
1782
2352
  * @param {Object} [servicesMap] Map of service name to service definition with Set-backed endpoints
1783
2353
  * @returns {Object[]} Array of service objects with sorted endpoint arrays and properties
1784
2354
  */
@@ -1786,12 +2356,18 @@ export function normalizeDosaiServiceMap(servicesMap = {}) {
1786
2356
  return Object.keys(servicesMap)
1787
2357
  .map((serviceName) => {
1788
2358
  const definition = servicesMap[serviceName];
2359
+ const endpoints = new Set();
2360
+ for (const endpoint of definition.endpoints || []) {
2361
+ if (typeof endpoint === "string" && endpoint) {
2362
+ endpoints.add(encodeUriReference(endpoint));
2363
+ }
2364
+ }
1789
2365
  const service = {
1790
2366
  name:
1791
2367
  definition.name ||
1792
2368
  serviceName ||
1793
2369
  `dosai-${basename(serviceName)}-service`,
1794
- endpoints: Array.from(definition.endpoints || []).sort(),
2370
+ endpoints: Array.from(endpoints).sort(),
1795
2371
  authenticated: definition.authenticated,
1796
2372
  // Accept both spellings. Every pre-4.0.0 producer — collectDosaiServicesFromMethods here, and
1797
2373
  // the OpenAPI/Java/Python paths in evinser.js — sets the camelCase `xTrustBoundary`; only the