@cdxgen/cdxgen 13.0.1 → 13.2.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 (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { basename, delimiter, join, resolve } from "node:path";
3
3
 
4
4
  import { DEBUG_MODE } from "../core/activity.js";
5
+ import { recordDegradation } from "../core/buildLedger.js";
5
6
  import {
6
7
  getTmpDir,
7
8
  safeExistsSync,
@@ -25,6 +26,15 @@ const DOTNET_LANGUAGES = new Set([
25
26
  "cs",
26
27
  "dotnet",
27
28
  "dotnet-framework",
29
+ // Versioned CLI types (`-t dotnet11`): the project-type aliases map them to
30
+ // csharp for dispatch, but gates such as shouldCollectDosaiCrypto inspect the
31
+ // raw options.projectType, so the versioned spellings must resolve here too.
32
+ "dotnet6",
33
+ "dotnet7",
34
+ "dotnet8",
35
+ "dotnet9",
36
+ "dotnet10",
37
+ "dotnet11",
28
38
  "f#",
29
39
  "fsharp",
30
40
  "fs",
@@ -59,6 +69,39 @@ function frameFromDosaiNode(node) {
59
69
  };
60
70
  }
61
71
 
72
+ /**
73
+ * Whether this slice's ApiEndpoints carry a resolved route in `Path`.
74
+ *
75
+ * Schema 4.0.0 introduced `Path` as the resolved route ([controller] substituted, constraints
76
+ * stripped) alongside the verbatim `Route`. In every earlier schema `Path` is the **source file
77
+ * path**, so preferring it unconditionally emits file paths as service endpoints. cdxgen ships a
78
+ * pinned dosai via @cdxgen/cdxgen-plugins-bin and users can point DOSAI_CMD at any build, so both
79
+ * shapes are live and the version has to be checked.
80
+ *
81
+ * @param {Object} methodsSlice Parsed dosai methods slice JSON
82
+ * @returns {boolean} True when `Path` holds a resolved route
83
+ */
84
+ function hasResolvedEndpointPaths(methodsSlice) {
85
+ const version = methodsSlice?.Metadata?.SchemaVersion;
86
+ if (typeof version !== "string") {
87
+ return false;
88
+ }
89
+ const major = Number.parseInt(version.split(".")[0], 10);
90
+ return Number.isFinite(major) && major >= 4;
91
+ }
92
+
93
+ /**
94
+ * Coerce a dosai string list into the array-of-strings shape serviceData.source/destination require.
95
+ * Returns undefined rather than an empty or malformed array so the key is omitted entirely.
96
+ */
97
+ function toStringArray(value) {
98
+ if (!Array.isArray(value)) {
99
+ return undefined;
100
+ }
101
+ const strings = value.filter((entry) => typeof entry === "string" && entry);
102
+ return strings.length ? strings : undefined;
103
+ }
104
+
62
105
  function appendUniqueProperty(properties, name, value) {
63
106
  if (value === undefined || value === null || value === "") {
64
107
  return;
@@ -219,6 +262,14 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
219
262
  shell: false,
220
263
  });
221
264
  if (dosaiSdkMessage(result)) {
265
+ recordDegradation("dotnet.sdk.missing", {
266
+ ecosystem: "csharp",
267
+ tool: "dotnet",
268
+ impact: "none",
269
+ command: `${basename(executable)} ${args.join(" ")}`,
270
+ detail:
271
+ "dosai reported that no .NET SDK is installed, so the requested dosai analysis could not run.",
272
+ });
222
273
  console.log(
223
274
  "Dotnet SDK is not installed. Please use the cdxgen dotnet container images to analyze this project with dosai.",
224
275
  );
@@ -348,7 +399,21 @@ export function persistDosaiSemanticsReport(
348
399
  return undefined;
349
400
  }
350
401
  const combined = buildCombinedDosaiReport(methodsSlice, dataFlowSlice);
351
- safeWriteSync(durablePath, JSON.stringify(combined));
402
+ try {
403
+ safeWriteSync(durablePath, JSON.stringify(combined));
404
+ } catch (err) {
405
+ // Combined methods + data-flow reports for large solutions can exceed the
406
+ // JS maximum string length. The SBOM evidence is already collected by this
407
+ // point, so skip the durable copy instead of losing the whole analysis.
408
+ recordDegradation("dosai.semantics.persistFailed", {
409
+ ecosystem: "csharp",
410
+ tool: "dosai",
411
+ impact: "minor",
412
+ command: `persistDosaiSemanticsReport ${durablePath}`,
413
+ detail: `The combined dosai report could not be persisted: ${err.message}. The SBOM evidence is unaffected.`,
414
+ });
415
+ return undefined;
416
+ }
352
417
  return durablePath;
353
418
  }
354
419
 
@@ -373,6 +438,74 @@ export function resolveComponentPurl(purl, purlAliasMap) {
373
438
  return resolveDosaiComponentPurl(purl, purlAliasMap);
374
439
  }
375
440
 
441
+ /**
442
+ * Copy one dosai PackageReachability fact onto a component as properties.
443
+ *
444
+ * The occurrence/location consumers of PackageReachability read SourceLocations, EdgeIds,
445
+ * and NodeIds only, so dosai's Confidence, EvidenceKinds, ReachabilityKind, and
446
+ * ConfidenceReasons never reached the BOM: an unbuilt tree (Low confidence, unresolved
447
+ * evidence) was indistinguishable from a package that really is only imported. These
448
+ * properties carry that distinction to BOM consumers. They use the consumer-facing
449
+ * `cdx:dosai:reachability:*` namespace - not `internal:`, which is cdxgen's private
450
+ * bookkeeping namespace stripped or ignored by downstream tooling.
451
+ *
452
+ * @param {Object} component BOM component object (mutated; properties created on demand)
453
+ * @param {Object} reachability dosai PackageReachability entry
454
+ */
455
+ export function addDosaiReachabilityProperties(component, reachability) {
456
+ if (!component || !reachability) {
457
+ return;
458
+ }
459
+ component.properties = component.properties || [];
460
+ const values = {
461
+ "cdx:dosai:reachability:kind": reachability.ReachabilityKind,
462
+ "cdx:dosai:reachability:confidence": reachability.Confidence,
463
+ "cdx:dosai:reachability:evidence": (reachability.EvidenceKinds || []).join(
464
+ ", ",
465
+ ),
466
+ "cdx:dosai:reachability:reasons": (
467
+ reachability.ConfidenceReasons || []
468
+ ).join(" | "),
469
+ };
470
+ for (const [name, value] of Object.entries(values)) {
471
+ if (!value) {
472
+ continue;
473
+ }
474
+ // First fact wins so a repeated enrichment pass cannot stack duplicate properties.
475
+ if (component.properties.some((p) => p.name === name)) {
476
+ continue;
477
+ }
478
+ component.properties.push({ name, value });
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Attach dosai PackageReachability confidence facts to matching BOM components.
484
+ *
485
+ * @param {Object} methodsSlice Parsed dosai methods slice JSON
486
+ * @param {Object[]} [components] BOM components used to resolve purl aliases and mutate
487
+ * @returns {number} Number of distinct components enriched
488
+ */
489
+ export function applyDosaiReachabilityEvidence(methodsSlice, components = []) {
490
+ const purlAliasMap = buildPurlAliasMap(components);
491
+ const componentsByPurl = new Map(
492
+ components
493
+ .filter((component) => component?.purl)
494
+ .map((component) => [component.purl, component]),
495
+ );
496
+ const enriched = new Set();
497
+ for (const reachability of methodsSlice?.PackageReachability || []) {
498
+ const purl = resolveComponentPurl(reachability.Purl, purlAliasMap);
499
+ const component = purl && componentsByPurl.get(purl);
500
+ if (!component) {
501
+ continue;
502
+ }
503
+ addDosaiReachabilityProperties(component, reachability);
504
+ enriched.add(component);
505
+ }
506
+ return enriched.size;
507
+ }
508
+
376
509
  /**
377
510
  * Map a dosai methods slice to per-purl occurrence evidence.
378
511
  *
@@ -498,6 +631,288 @@ export function collectDosaiDataFlowFrames(dataFlowResult, components = []) {
498
631
  return dataFlowFrames;
499
632
  }
500
633
 
634
+ /**
635
+ * Consume dosai's AiComponents[] inventory (schema 4.0.0): model identifiers,
636
+ * on-disk model artifacts with hashes, MCP tools, prompts (redacted), and agents
637
+ * become CycloneDX machine-learning-model / data components with modelCard data.
638
+ *
639
+ * @param {Object} methodsSlice Parsed dosai methods slice JSON
640
+ * @param {Array} [components] Component list to mutate in place
641
+ * @returns {Array} The updated component list
642
+ */
643
+ export function collectDosaiAiComponents(methodsSlice, components = []) {
644
+ for (const ai of methodsSlice?.AiComponents || []) {
645
+ if (!ai?.Id || !ai?.Name) {
646
+ continue;
647
+ }
648
+ if (components.some((c) => c["bom-ref"] === ai.Id)) {
649
+ continue;
650
+ }
651
+ if (ai.Kind === "model") {
652
+ const modelCard = {
653
+ modelParameters: {},
654
+ };
655
+ if (ai.Task) {
656
+ modelCard.modelParameters.task = ai.Task;
657
+ }
658
+ if (ai.ArchitectureFamily) {
659
+ modelCard.modelParameters.architectureFamily = ai.ArchitectureFamily;
660
+ }
661
+ if (ai.ModelArchitecture) {
662
+ // modelArchitecture lives under modelParameters; modelCard itself is
663
+ // additionalProperties:false, so placing it at the top level fails validation.
664
+ modelCard.modelParameters.modelArchitecture = ai.ModelArchitecture;
665
+ }
666
+ if (Array.isArray(ai.InputFormats) && ai.InputFormats.length) {
667
+ modelCard.modelParameters.inputs = ai.InputFormats.map((format) => ({
668
+ format,
669
+ }));
670
+ }
671
+ if (Array.isArray(ai.OutputFormats) && ai.OutputFormats.length) {
672
+ modelCard.modelParameters.outputs = ai.OutputFormats.map((format) => ({
673
+ format,
674
+ }));
675
+ }
676
+ const component = {
677
+ "bom-ref": ai.Id,
678
+ type: "machine-learning-model",
679
+ name: ai.Name,
680
+ version: ai.Version,
681
+ purl: ai.Purl,
682
+ modelCard,
683
+ properties: [
684
+ { name: "cdx:ai:provider", value: ai.Provider || "unknown" },
685
+ { name: "cdx:ai:deployment", value: ai.Deployment || "unknown" },
686
+ ],
687
+ };
688
+ if (ai.Sha256) {
689
+ component.hashes = [{ alg: "SHA-256", content: ai.Sha256 }];
690
+ }
691
+ if (ai.FilePath) {
692
+ component.properties.push({
693
+ name: "cdx:ai:modelFile",
694
+ value: ai.FilePath,
695
+ });
696
+ }
697
+ components.push(component);
698
+ } else if (ai.Kind === "prompt" || ai.Kind === "dataset") {
699
+ components.push({
700
+ "bom-ref": ai.Id,
701
+ type: "data",
702
+ name: ai.Name,
703
+ properties: [
704
+ { name: "cdx:ai:kind", value: ai.Kind },
705
+ ...(ai.PromptText
706
+ ? [{ name: "cdx:ai:promptText", value: ai.PromptText }]
707
+ : []),
708
+ ...Object.entries(ai.Properties || {}).map(([name, value]) => ({
709
+ name: `cdx:ai:${name}`,
710
+ value: String(value),
711
+ })),
712
+ ],
713
+ });
714
+ } else if (
715
+ ai.Kind === "tool" ||
716
+ ai.Kind === "agent" ||
717
+ ai.Kind === "guardrail" ||
718
+ ai.Kind === "embedding"
719
+ ) {
720
+ const properties = [
721
+ { name: "cdx:ai:kind", value: ai.Kind },
722
+ { name: "cdx:ai:provider", value: ai.Provider || "unknown" },
723
+ ];
724
+ if (ai.ToolSchema) {
725
+ properties.push({ name: "cdx:ai:toolSchema", value: ai.ToolSchema });
726
+ }
727
+ components.push({
728
+ "bom-ref": ai.Id,
729
+ type: "data",
730
+ name: ai.Name,
731
+ properties,
732
+ });
733
+ }
734
+ }
735
+ return components;
736
+ }
737
+
738
+ /**
739
+ * Consume dosai's first-class Services[] inventory (schema 4.0.0) directly: richer
740
+ * than deriving services from ApiEndpoints alone — stable bom-refs, trust zones,
741
+ * data classifications, providers, and per-service evidence occurrences.
742
+ *
743
+ * @param {Object} methodsSlice Parsed dosai methods slice JSON
744
+ * @param {Object} [servicesMap] Map of service key to service definition, mutated in place
745
+ * @returns {Object} The updated services map
746
+ */
747
+ export function collectDosaiServiceComponents(methodsSlice, servicesMap = {}) {
748
+ for (const service of methodsSlice?.Services || []) {
749
+ if (!service?.Id || !service?.Name) {
750
+ continue;
751
+ }
752
+ const key = service.Id;
753
+ const definition = (servicesMap[key] ??= {
754
+ name: service.Name,
755
+ bomRef: service.Id,
756
+ endpoints: new Set(),
757
+ properties: [],
758
+ });
759
+ definition.group = definition.group || service.Group;
760
+ definition.version = definition.version || service.Version;
761
+ // The same service Id can appear in several Services[] entries (one per
762
+ // implementing type or call site). Scalar facts left unset by earlier
763
+ // entries must still be filled from later ones, in particular the
764
+ // outbound provider: an entry that is only inbound must not lock the
765
+ // definition into an anonymous egress.
766
+ if (
767
+ !definition.provider &&
768
+ service.Provider &&
769
+ service.Direction === "outbound"
770
+ ) {
771
+ definition.provider = { name: service.Provider };
772
+ }
773
+ if (typeof definition.authenticated === "undefined") {
774
+ definition.authenticated = service.Authenticated ?? undefined;
775
+ }
776
+ if (!definition.trustZone) {
777
+ definition.trustZone = service.TrustZone;
778
+ }
779
+ if (!definition["x-trust-boundary"]) {
780
+ definition["x-trust-boundary"] =
781
+ service.CrossesTrustBoundary === true ? true : undefined;
782
+ }
783
+ for (const endpoint of service.Endpoints || []) {
784
+ const sanitized = sanitizeEndpoint(endpoint);
785
+ if (sanitized) {
786
+ definition.endpoints.add(sanitized);
787
+ }
788
+ }
789
+ if (Array.isArray(service.Operations) && service.Operations.length) {
790
+ appendUniqueProperty(
791
+ definition.properties,
792
+ "cdx:dosai:operationCount",
793
+ service.Operations.length,
794
+ );
795
+ for (const operation of service.Operations) {
796
+ // Endpoints[] can miss routes that only Operations carry (e.g. gRPC and
797
+ // messaging operations resolved per method), so both are collected.
798
+ const sanitized = sanitizeEndpoint(operation.Path);
799
+ if (sanitized) {
800
+ definition.endpoints.add(sanitized);
801
+ }
802
+ // Verbs are small enum values; they keep the HTTP surface auditable
803
+ // per endpoint without copying request payloads.
804
+ appendUniqueProperty(
805
+ definition.properties,
806
+ "cdx:service:httpMethod",
807
+ operation.HttpMethod,
808
+ );
809
+ if (operation.RouteTemplate && operation.Path) {
810
+ appendUniqueProperty(
811
+ definition.properties,
812
+ "cdx:service:pathTemplate",
813
+ operation.RouteTemplate,
814
+ );
815
+ }
816
+ }
817
+ }
818
+ if (Array.isArray(service.Data) && service.Data.length) {
819
+ // First population of services[].data[] for .NET: flow + classification
820
+ // with auditable descriptions from dosai.
821
+ definition.data = (definition.data || []).concat(
822
+ service.Data
823
+ // `flow` and `classification` are both required by serviceData in the 1.7 schema; an entry
824
+ // missing either fails validation and would fail the whole BOM, so drop it instead.
825
+ .filter((entry) => entry?.Classification && entry?.Flow)
826
+ .map((entry) => ({
827
+ classification: entry.Classification,
828
+ // "unknown" is a legitimate dataFlowDirection member. Coercing it to "bi-directional"
829
+ // turned an absence of knowledge into a positive claim that data flows both ways.
830
+ flow: ["inbound", "outbound", "bi-directional", "unknown"].includes(
831
+ entry.Flow,
832
+ )
833
+ ? entry.Flow
834
+ : "unknown",
835
+ name: entry.Name,
836
+ description: entry.Description,
837
+ // source/destination are arrays of strings (iri-reference or BOM-link) in the schema.
838
+ source: toStringArray(entry.Source),
839
+ destination: toStringArray(entry.Destination),
840
+ })),
841
+ );
842
+ }
843
+ const properties = definition.properties;
844
+ appendUniqueProperty(properties, "cdx:service:kind", service.ServiceKind);
845
+ appendUniqueProperty(
846
+ properties,
847
+ "cdx:service:direction",
848
+ service.Direction,
849
+ );
850
+ appendUniqueProperty(
851
+ properties,
852
+ "cdx:service:framework",
853
+ service.Framework,
854
+ );
855
+ appendUniqueProperty(
856
+ properties,
857
+ "cdx:dosai:confidence",
858
+ service.Confidence,
859
+ );
860
+ for (const tag of service.Tags || []) {
861
+ appendUniqueProperty(properties, "cdx:dosai:tag", tag);
862
+ }
863
+ // Auth surface as counts and scheme categories only; policies, roles, and
864
+ // claim values never cross into the BOM verbatim.
865
+ appendUniqueProperty(
866
+ properties,
867
+ "cdx:dosai:allowAnonymous",
868
+ service.AllowAnonymous,
869
+ );
870
+ for (const scheme of service.AuthenticationSchemes || []) {
871
+ appendUniqueProperty(properties, "cdx:dosai:authScheme", scheme);
872
+ }
873
+ appendUniqueProperty(
874
+ properties,
875
+ "cdx:dosai:authorizationPolicyCount",
876
+ service.AuthorizationPolicies?.length,
877
+ );
878
+ appendUniqueProperty(
879
+ properties,
880
+ "cdx:dosai:roleCount",
881
+ service.Roles?.length,
882
+ );
883
+ if (service.Location?.Path) {
884
+ // Accumulate call sites: a service implemented across files must keep
885
+ // every occurrence, not just the last one processed.
886
+ definition.evidence ??= { occurrences: [] };
887
+ const location = {
888
+ path: service.Location.Path,
889
+ line: service.Location.LineNumber || undefined,
890
+ column: service.Location.ColumnNumber || undefined,
891
+ };
892
+ const occurrenceKey = `${location.path}#${location.line}#${location.column}`;
893
+ const knownOccurrences = definition.evidence.occurrences;
894
+ if (
895
+ !knownOccurrences.some(
896
+ (known) =>
897
+ `${known?.location?.path}#${known?.location?.line}#${known?.location?.column}` ===
898
+ occurrenceKey,
899
+ )
900
+ ) {
901
+ knownOccurrences.push({ location });
902
+ }
903
+ // services[].evidence is a CycloneDX 2.0 field that spec-version
904
+ // compatibility strips below 2.0, so the location also lands in a
905
+ // property where it survives at 1.6 and 1.7.
906
+ appendUniqueProperty(
907
+ definition.properties,
908
+ "cdx:dosai:location",
909
+ `${location.path}:${location.line || 0}:${location.column || 0}`,
910
+ );
911
+ }
912
+ }
913
+ return servicesMap;
914
+ }
915
+
501
916
  /**
502
917
  * Infer service and endpoint definitions from a dosai methods slice.
503
918
  *
@@ -513,21 +928,40 @@ export function collectDosaiServicesFromMethods(
513
928
  methodsSlice,
514
929
  servicesMap = {},
515
930
  ) {
931
+ const resolvedPaths = hasResolvedEndpointPaths(methodsSlice);
516
932
  for (const endpoint of methodsSlice?.ApiEndpoints || []) {
517
- const route = sanitizeEndpoint(endpoint.Route || endpoint.Path);
518
- if (!route) {
933
+ // Endpoints owned by a provider service (ServiceId set and already
934
+ // collected) enrich the owner instead of being re-derived into a duplicate
935
+ // service. Their method and auth metadata still lands on the owner via the
936
+ // shared property recording below.
937
+ const ownedDefinition =
938
+ endpoint.ServiceId && servicesMap[endpoint.ServiceId]
939
+ ? servicesMap[endpoint.ServiceId]
940
+ : undefined;
941
+ // Schema 4.0.0: Path is the resolved route (tokens substituted, constraints
942
+ // stripped); Route keeps the verbatim template. Preferring Path is the fix for
943
+ // cdxgen discussion #4333, where [controller] shipped as %5Bcontroller%5D.
944
+ const route = sanitizeEndpoint(
945
+ resolvedPaths ? endpoint.Path || endpoint.Route : endpoint.Route,
946
+ );
947
+ if (!ownedDefinition && !route) {
519
948
  continue;
520
949
  }
521
- const serviceName = serviceNameFromEndpoint(endpoint);
522
- servicesMap[serviceName] ??= {
523
- endpoints: new Set(),
524
- authenticated: endpoint.AuthorizationRequired,
525
- xTrustBoundary:
526
- endpoint.AuthorizationRequired === true ? true : undefined,
527
- properties: [],
528
- };
529
- servicesMap[serviceName].endpoints.add(route);
530
- const properties = servicesMap[serviceName].properties;
950
+ let definition = ownedDefinition;
951
+ if (!definition) {
952
+ const serviceName = serviceNameFromEndpoint(endpoint);
953
+ definition = servicesMap[serviceName] ??= {
954
+ endpoints: new Set(),
955
+ authenticated: endpoint.AuthorizationRequired,
956
+ xTrustBoundary:
957
+ endpoint.AuthorizationRequired === true ? true : undefined,
958
+ properties: [],
959
+ };
960
+ }
961
+ if (route) {
962
+ definition.endpoints.add(route);
963
+ }
964
+ const properties = definition.properties;
531
965
  appendUniqueProperty(
532
966
  properties,
533
967
  "cdx:service:httpMethod",
@@ -571,13 +1005,30 @@ export function collectDosaiServicesFromMethods(
571
1005
  appendUniqueProperty(
572
1006
  properties,
573
1007
  "internal:SrcFile",
574
- endpoint.Path || endpoint.FileName,
1008
+ endpoint.FilePath || endpoint.FileName,
575
1009
  );
576
1010
  if (endpoint.LineNumber) {
577
1011
  appendUniqueProperty(
578
1012
  properties,
579
1013
  "cdx:dosai:location",
580
- `${endpoint.Path || endpoint.FileName}:${endpoint.LineNumber}:${endpoint.ColumnNumber || 0}`,
1014
+ // Schema 4.0.0 renamed the ApiEndpoint source file field to FilePath;
1015
+ // Path carries the resolved route there, not a location.
1016
+ `${endpoint.FilePath || endpoint.FileName}:${endpoint.LineNumber}:${endpoint.ColumnNumber || 0}`,
1017
+ );
1018
+ }
1019
+ if (endpoint.Route && endpoint.Path && endpoint.Route !== endpoint.Path) {
1020
+ // The verbatim template (e.g. api/[controller]/{id:int}) alongside the resolved path.
1021
+ appendUniqueProperty(
1022
+ properties,
1023
+ "cdx:service:pathTemplate",
1024
+ endpoint.Route,
1025
+ );
1026
+ }
1027
+ if (endpoint.Confidence) {
1028
+ appendUniqueProperty(
1029
+ properties,
1030
+ "cdx:dosai:confidence",
1031
+ endpoint.Confidence,
581
1032
  );
582
1033
  }
583
1034
  }
@@ -591,11 +1042,46 @@ export function collectDosaiServicesFromMethods(
591
1042
  * @returns {Object[]} Array of service objects with sorted endpoint arrays and properties
592
1043
  */
593
1044
  export function normalizeDosaiServiceMap(servicesMap = {}) {
594
- return Object.keys(servicesMap).map((serviceName) => ({
595
- name: serviceName || `dosai-${basename(serviceName)}-service`,
596
- endpoints: Array.from(servicesMap[serviceName].endpoints || []).sort(),
597
- authenticated: servicesMap[serviceName].authenticated,
598
- "x-trust-boundary": servicesMap[serviceName].xTrustBoundary,
599
- properties: servicesMap[serviceName].properties,
600
- }));
1045
+ return Object.keys(servicesMap)
1046
+ .map((serviceName) => {
1047
+ const definition = servicesMap[serviceName];
1048
+ const service = {
1049
+ name:
1050
+ definition.name ||
1051
+ serviceName ||
1052
+ `dosai-${basename(serviceName)}-service`,
1053
+ endpoints: Array.from(definition.endpoints || []).sort(),
1054
+ authenticated: definition.authenticated,
1055
+ // Accept both spellings. Every pre-4.0.0 producer — collectDosaiServicesFromMethods here, and
1056
+ // the OpenAPI/Java/Python paths in evinser.js — sets the camelCase `xTrustBoundary`; only the
1057
+ // new Services[] path sets the hyphenated CycloneDX key. Reading just the latter silently
1058
+ // dropped the flag for all of them, including languages dosai has nothing to do with.
1059
+ "x-trust-boundary":
1060
+ definition["x-trust-boundary"] ?? definition.xTrustBoundary,
1061
+ properties: definition.properties,
1062
+ };
1063
+ if (definition.bomRef) {
1064
+ service["bom-ref"] = definition.bomRef;
1065
+ }
1066
+ if (definition.group) {
1067
+ service.group = definition.group;
1068
+ }
1069
+ if (definition.version) {
1070
+ service.version = definition.version;
1071
+ }
1072
+ if (definition.trustZone) {
1073
+ service.trustZone = definition.trustZone;
1074
+ }
1075
+ if (definition.provider) {
1076
+ service.provider = definition.provider;
1077
+ }
1078
+ if (definition.data) {
1079
+ service.data = definition.data;
1080
+ }
1081
+ if (definition.evidence) {
1082
+ service.evidence = definition.evidence;
1083
+ }
1084
+ return service;
1085
+ })
1086
+ .filter((service) => service.name);
601
1087
  }