@cdxgen/cdxgen 13.4.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 (208) hide show
  1. package/README.md +7 -7
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +25 -5
  4. package/bin/evinse.js +5 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/data/cbomosdb-queries.json +13 -1
  8. package/data/component-tags.json +8 -0
  9. package/data/queries-darwin.json +25 -1
  10. package/data/queries-win.json +19 -1
  11. package/data/queries.json +41 -0
  12. package/data/remediations.json +283 -1
  13. package/data/rules/cbom-security.yaml +235 -4
  14. package/data/rules/go-binaries.yaml +46 -0
  15. package/data/rules/golem-go.yaml +80 -0
  16. package/data/rules/hbom-compliance.yaml +40 -22
  17. package/data/rules/hbom-performance.yaml +7 -1
  18. package/data/rules/host-topology.yaml +1 -1
  19. package/data/rules/obom-runtime.yaml +31 -2
  20. package/data/rules/php-runtime.yaml +143 -0
  21. package/lib/audit/index.js +14 -0
  22. package/lib/cli/bomAssembly.js +28 -3
  23. package/lib/cli/cliOptions.js +110 -3
  24. package/lib/cli/index.js +73 -4
  25. package/lib/cli/jvmBom.js +126 -20
  26. package/lib/cli/managedBom.js +711 -71
  27. package/lib/cli/nativeBom.js +225 -28
  28. package/lib/core/env.js +30 -8
  29. package/lib/core/fs.js +28 -19
  30. package/lib/core/httpClient.js +74 -45
  31. package/lib/core/projectConfig.js +1 -0
  32. package/lib/core/projectTooling.js +97 -0
  33. package/lib/core/propertySanitizer.js +72 -0
  34. package/lib/ecosystems/golem.js +550 -6
  35. package/lib/ecosystems/gradleutils.js +30 -20
  36. package/lib/ecosystems/parsers-dotnet.js +117 -18
  37. package/lib/ecosystems/parsers-go.js +1090 -367
  38. package/lib/ecosystems/parsers-jvm.js +259 -17
  39. package/lib/ecosystems/parsers-misc.js +229 -44
  40. package/lib/ecosystems/parsers-python.js +11 -10
  41. package/lib/ecosystems/rubyutils.js +6 -3
  42. package/lib/ecosystems/rusi.js +10 -0
  43. package/lib/ecosystems/scalacliutils.js +2 -1
  44. package/lib/ecosystems/scalasem.js +3 -1
  45. package/lib/evinser/evinser.js +126 -18
  46. package/lib/evinser/swiftsem.js +68 -5
  47. package/lib/inventory/agentSkillSpec.js +17 -0
  48. package/lib/inventory/apkInstalledDb.js +243 -0
  49. package/lib/inventory/atomUtils.js +33 -0
  50. package/lib/inventory/auditCategories.js +3 -3
  51. package/lib/inventory/cacheDir.js +52 -9
  52. package/lib/inventory/certificates.js +2371 -0
  53. package/lib/inventory/communityAiConfigParser.js +4 -9
  54. package/lib/inventory/deps.js +6 -4
  55. package/lib/inventory/display.js +200 -3
  56. package/lib/inventory/dosai.js +500 -58
  57. package/lib/inventory/dynamic.js +45 -3
  58. package/lib/inventory/envcontext.js +168 -5
  59. package/lib/inventory/evidenceUtils.js +436 -23
  60. package/lib/inventory/formulationParsers.js +3 -2
  61. package/lib/inventory/goBuildInfo.js +140 -0
  62. package/lib/inventory/hbom.js +67 -7
  63. package/lib/inventory/hbomAnalysis.js +16 -0
  64. package/lib/inventory/hostTopology.js +21 -0
  65. package/lib/inventory/osPackagePurl.js +244 -0
  66. package/lib/inventory/osPackageResolver.js +89 -87
  67. package/lib/inventory/osinfo.js +79 -31
  68. package/lib/inventory/osqueryTransform.js +114 -5
  69. package/lib/inventory/phpBuildInfo.js +487 -0
  70. package/lib/inventory/phpHostProbe.js +641 -0
  71. package/lib/inventory/rootfsCerts.js +426 -0
  72. package/lib/inventory/source.js +207 -3
  73. package/lib/inventory/spdx.js +144 -0
  74. package/lib/inventory/traceRunner.js +5 -1
  75. package/lib/inventory/tracedCrypto.js +129 -0
  76. package/lib/managers/binary.js +529 -183
  77. package/lib/managers/docker.js +166 -12
  78. package/lib/parsers/email.js +277 -0
  79. package/lib/parsers/largeJson.js +34 -2
  80. package/lib/server/server.js +186 -1
  81. package/lib/stages/postgen/annotator.js +17 -1
  82. package/lib/stages/postgen/introspection/reflect.js +4 -1
  83. package/lib/stages/postgen/postgen.js +5 -5
  84. package/lib/stages/postgen/ruleEngine.js +58 -1
  85. package/lib/stages/postgen/specVersionCompat.js +30 -0
  86. package/lib/stages/pregen/pregen.js +117 -5
  87. package/lib/validator/bomValidator.js +26 -5
  88. package/lib/validator/generated/runtime.mjs +13 -0
  89. package/lib/validator/generated/validate-1.6.mjs +26 -7
  90. package/lib/validator/generated/validate-1.7.mjs +31 -12
  91. package/lib/validator/generated/validate-2.0.mjs +30 -11
  92. package/package.json +17 -17
  93. package/types/bin/cdxgen-secure.d.ts +3 -0
  94. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  95. package/types/bin/repl.d.ts.map +1 -1
  96. package/types/lib/audit/index.d.ts.map +1 -1
  97. package/types/lib/cli/bomAssembly.d.ts +9 -2
  98. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  99. package/types/lib/cli/cliOptions.d.ts +43 -0
  100. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  101. package/types/lib/cli/index.d.ts.map +1 -1
  102. package/types/lib/cli/jvmBom.d.ts +20 -0
  103. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  104. package/types/lib/cli/managedBom.d.ts.map +1 -1
  105. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  106. package/types/lib/core/env.d.ts +18 -2
  107. package/types/lib/core/env.d.ts.map +1 -1
  108. package/types/lib/core/fs.d.ts.map +1 -1
  109. package/types/lib/core/httpClient.d.ts.map +1 -1
  110. package/types/lib/core/projectConfig.d.ts.map +1 -1
  111. package/types/lib/core/projectTooling.d.ts +41 -0
  112. package/types/lib/core/projectTooling.d.ts.map +1 -0
  113. package/types/lib/core/propertySanitizer.d.ts +26 -0
  114. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  115. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  116. package/types/lib/ecosystems/golem.d.ts +81 -1
  117. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  118. package/types/lib/ecosystems/gradleutils.d.ts +10 -8
  119. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  121. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  123. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  125. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  126. package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
  127. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  128. package/types/lib/ecosystems/parsers-python.d.ts +4 -3
  129. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  130. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  131. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  132. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  133. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  134. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  135. package/types/lib/evinser/evinser.d.ts +25 -0
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/evinser/swiftsem.d.ts +10 -0
  138. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  139. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  140. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  141. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  142. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  143. package/types/lib/inventory/atomUtils.d.ts +14 -0
  144. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  145. package/types/lib/inventory/cacheDir.d.ts +12 -4
  146. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  147. package/types/lib/inventory/certificates.d.ts +205 -0
  148. package/types/lib/inventory/certificates.d.ts.map +1 -0
  149. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  150. package/types/lib/inventory/deps.d.ts +2 -1
  151. package/types/lib/inventory/deps.d.ts.map +1 -1
  152. package/types/lib/inventory/display.d.ts +18 -0
  153. package/types/lib/inventory/display.d.ts.map +1 -1
  154. package/types/lib/inventory/dosai.d.ts +145 -6
  155. package/types/lib/inventory/dosai.d.ts.map +1 -1
  156. package/types/lib/inventory/dynamic.d.ts +6 -1
  157. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  158. package/types/lib/inventory/envcontext.d.ts +40 -2
  159. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  160. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  161. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  162. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  163. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  164. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  165. package/types/lib/inventory/hbom.d.ts.map +1 -1
  166. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  167. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  168. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  169. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  170. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  171. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  172. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  173. package/types/lib/inventory/osinfo.d.ts +31 -6
  174. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  175. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  176. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  177. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  178. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  179. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  180. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  181. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  182. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  183. package/types/lib/inventory/source.d.ts +28 -0
  184. package/types/lib/inventory/source.d.ts.map +1 -1
  185. package/types/lib/inventory/spdx.d.ts +15 -0
  186. package/types/lib/inventory/spdx.d.ts.map +1 -1
  187. package/types/lib/inventory/traceRunner.d.ts +4 -1
  188. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  189. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  190. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  191. package/types/lib/managers/binary.d.ts +10 -0
  192. package/types/lib/managers/binary.d.ts.map +1 -1
  193. package/types/lib/managers/docker.d.ts +12 -0
  194. package/types/lib/managers/docker.d.ts.map +1 -1
  195. package/types/lib/parsers/email.d.ts +19 -0
  196. package/types/lib/parsers/email.d.ts.map +1 -0
  197. package/types/lib/parsers/largeJson.d.ts +13 -0
  198. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  199. package/types/lib/server/server.d.ts +8 -0
  200. package/types/lib/server/server.d.ts.map +1 -1
  201. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  202. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  203. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  204. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  205. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  206. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  207. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  208. 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.
@@ -717,6 +875,28 @@ export function dosaiRunStopReason(result) {
717
875
  // The output files of the dosai runs of this process that were stopped, so a
718
876
  // caller does not follow the stop warning with advice for another failure.
719
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
+ }
720
900
 
721
901
  /**
722
902
  * Whether the dosai run that should have written a file was stopped (by the
@@ -730,6 +910,23 @@ export function dosaiRunWasStopped(outputFile) {
730
910
  return Boolean(outputPath && stoppedDosaiOutputs.has(outputPath));
731
911
  }
732
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
+
733
930
  function dosaiStopMessage(reason, signal) {
734
931
  if (reason === "timeout") {
735
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).`;
@@ -744,10 +941,17 @@ function dosaiStopMessage(reason, signal) {
744
941
  * Run a dosai subcommand ("methods", "dataflows", or "crypto") against a source
745
942
  * tree and write its JSON output to the given file.
746
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
+ *
747
951
  * @param {string} command Dosai subcommand to execute
748
952
  * @param {string} src Source directory to analyze
749
953
  * @param {string} outputFile Path where the dosai JSON output is written
750
- * @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, patternPacks, or exclude globs
954
+ * @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, patternPacks, exclude globs, or dosaiMethodsSections
751
955
  * @returns {boolean} True when the command succeeded and produced the output file, false otherwise
752
956
  */
753
957
  export function runDosaiCommand(command, src, outputFile, options = {}) {
@@ -779,50 +983,122 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
779
983
  } else if (command === "crypto") {
780
984
  args.push("--format", "dosai");
781
985
  }
782
- const excludeArgs = dosaiWithoutExclude.has(executable)
783
- ? []
784
- : 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
+ ];
785
1031
  if (DEBUG_MODE) {
786
- console.log("Executing", executable, [...args, ...excludeArgs].join(" "));
1032
+ console.log("Executing", executable, commandArgs.join(" "));
787
1033
  }
788
1034
  stoppedDosaiOutputs.delete(outputPath);
1035
+ unstartedDosaiOutputs.delete(outputPath);
1036
+ dosaiOutputProducers.delete(outputPath);
789
1037
  let startedAt = Date.now();
790
- let result = safeSpawnSync(executable, [...args, ...excludeArgs], {
1038
+ let result = safeSpawnSync(executable, commandArgs, {
791
1039
  cwd: srcPath,
792
1040
  shell: false,
793
1041
  });
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
- });
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
+ }
810
1082
  }
811
- if (dosaiSdkMessage(result)) {
1083
+ const dotnetRequirement = dosaiDotnetRequirement(result);
1084
+ if (dotnetRequirement) {
1085
+ const detail = dosaiDotnetRequirementDetail(dotnetRequirement);
812
1086
  recordDegradation("dotnet.sdk.missing", {
813
1087
  ecosystem: "csharp",
814
1088
  tool: "dotnet",
815
1089
  impact: "none",
816
1090
  command: `${basename(executable)} ${args.join(" ")}`,
817
- detail:
818
- "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.`,
819
1092
  });
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
- );
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);
826
1102
  }
827
1103
  const stopReason = dosaiRunStopReason(result);
828
1104
  if (stopReason) {
@@ -850,6 +1126,10 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
850
1126
  }
851
1127
  return false;
852
1128
  }
1129
+ dosaiOutputProducers.set(outputPath, {
1130
+ executable,
1131
+ bundled: !options.dosaiCommand && !readEnvironmentVariable("DOSAI_CMD"),
1132
+ });
853
1133
  return true;
854
1134
  }
855
1135
 
@@ -858,7 +1138,7 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
858
1138
  *
859
1139
  * @param {string} src Source directory to analyze
860
1140
  * @param {string} outputFile Path where the methods slice JSON is written
861
- * @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
862
1142
  * @returns {boolean} True when the slice was produced successfully
863
1143
  */
864
1144
  export function createDosaiMethodsSlice(src, outputFile, options = {}) {
@@ -911,6 +1191,141 @@ export function analyzeDosaiCrypto(src, options = {}) {
911
1191
  }
912
1192
  }
913
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
+
914
1329
  /**
915
1330
  * Build the combined native dosai report object persisted for downstream tools.
916
1331
  *
@@ -971,10 +1386,12 @@ function* combinedDosaiReportChunks(combined, parts) {
971
1386
  * analyzeGolemProject on branch feat/rusi-persist-report): when a semantics-
972
1387
  * slices path is provided, the FULL native report is written there and kept so
973
1388
  * 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.
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.
978
1395
  */
979
1396
  export function persistDosaiSemanticsReport(
980
1397
  options,
@@ -996,6 +1413,19 @@ export function persistDosaiSemanticsReport(
996
1413
  const combined = buildCombinedDosaiReport(methodsSlice, dataFlowSlice);
997
1414
  const methodsSource = dosaiReportSources.get(methodsSlice);
998
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
+ }
999
1429
  try {
1000
1430
  let text;
1001
1431
  if (!methodsSource?.trimmed && !dataFlowSource?.trimmed) {
@@ -1779,6 +2209,12 @@ export function collectDosaiServicesFromMethods(
1779
2209
  /**
1780
2210
  * Normalize a services map into a sorted array of CycloneDX service objects.
1781
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
+ *
1782
2218
  * @param {Object} [servicesMap] Map of service name to service definition with Set-backed endpoints
1783
2219
  * @returns {Object[]} Array of service objects with sorted endpoint arrays and properties
1784
2220
  */
@@ -1786,12 +2222,18 @@ export function normalizeDosaiServiceMap(servicesMap = {}) {
1786
2222
  return Object.keys(servicesMap)
1787
2223
  .map((serviceName) => {
1788
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
+ }
1789
2231
  const service = {
1790
2232
  name:
1791
2233
  definition.name ||
1792
2234
  serviceName ||
1793
2235
  `dosai-${basename(serviceName)}-service`,
1794
- endpoints: Array.from(definition.endpoints || []).sort(),
2236
+ endpoints: Array.from(endpoints).sort(),
1795
2237
  authenticated: definition.authenticated,
1796
2238
  // Accept both spellings. Every pre-4.0.0 producer — collectDosaiServicesFromMethods here, and
1797
2239
  // the OpenAPI/Java/Python paths in evinser.js — sets the camelCase `xTrustBoundary`; only the