@cdxgen/cdxgen 13.1.0 → 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 (186) hide show
  1. package/README.md +8 -7
  2. package/bin/cdxgen.js +16 -3
  3. package/bin/evinse.js +2 -0
  4. package/data/crypto-oid.json +88 -12
  5. package/data/remediations.json +23 -0
  6. package/data/rules/ai-bom.yaml +78 -0
  7. package/data/rules/cargo-rust.yaml +132 -0
  8. package/data/rules/mcp-servers.yaml +122 -0
  9. package/lib/audit/targets.js +6 -1
  10. package/lib/cli/bomAssembly.js +12 -0
  11. package/lib/cli/index.js +192 -1
  12. package/lib/cli/jsBom.js +59 -2
  13. package/lib/cli/jvmBom.js +22 -0
  14. package/lib/cli/managedBom.js +152 -0
  15. package/lib/cli/nativeBom.js +798 -1
  16. package/lib/core/activity.js +120 -2
  17. package/lib/core/env.js +39 -1
  18. package/lib/core/userAgent.js +151 -0
  19. package/lib/ecosystems/cargoCache.js +364 -0
  20. package/lib/ecosystems/caxa.js +13 -5
  21. package/lib/ecosystems/cppEvidence.js +40 -2
  22. package/lib/ecosystems/ecosystems.js +241 -91
  23. package/lib/ecosystems/golem.js +11 -1
  24. package/lib/ecosystems/gradleutils.js +143 -2
  25. package/lib/ecosystems/kosi.js +1256 -0
  26. package/lib/ecosystems/parsers-crystal.js +179 -0
  27. package/lib/ecosystems/parsers-dotnet.js +69 -39
  28. package/lib/ecosystems/parsers-elm.js +816 -0
  29. package/lib/ecosystems/parsers-gleam.js +1 -1
  30. package/lib/ecosystems/parsers-julia.js +268 -0
  31. package/lib/ecosystems/parsers-lua.js +218 -0
  32. package/lib/ecosystems/parsers-misc.js +172 -0
  33. package/lib/ecosystems/parsers-nim.js +221 -0
  34. package/lib/ecosystems/parsers-opam.js +180 -0
  35. package/lib/ecosystems/parsers-perl.js +277 -0
  36. package/lib/ecosystems/parsers-python.js +1 -1
  37. package/lib/ecosystems/parsers-r.js +148 -0
  38. package/lib/ecosystems/parsers-rebar.js +411 -0
  39. package/lib/ecosystems/parsers-rust.js +932 -17
  40. package/lib/ecosystems/parsers-spack.js +204 -0
  41. package/lib/ecosystems/parsers-stack.js +264 -0
  42. package/lib/ecosystems/parsers-terraform.js +194 -0
  43. package/lib/ecosystems/rusi.js +126 -6
  44. package/lib/evinser/evinser.js +111 -1
  45. package/lib/helpers/agenticToolutils.js +925 -0
  46. package/lib/inventory/agentFormulationParser.js +57 -4
  47. package/lib/inventory/aiCollector.js +504 -10
  48. package/lib/inventory/aiInventory.js +12 -0
  49. package/lib/inventory/analyzer.js +488 -30
  50. package/lib/inventory/astWorker.js +6 -0
  51. package/lib/inventory/auditCategories.js +8 -0
  52. package/lib/inventory/cbomutils.js +30 -0
  53. package/lib/inventory/cdxrs.js +7 -2
  54. package/lib/inventory/communityAiConfigParser.js +215 -2
  55. package/lib/inventory/dosai.js +77 -0
  56. package/lib/inventory/envcontext.js +9 -6
  57. package/lib/inventory/evidenceUtils.js +13 -0
  58. package/lib/inventory/fetchBatch.js +74 -2
  59. package/lib/inventory/fetchRate.js +74 -11
  60. package/lib/inventory/forgeEnricher.js +3 -2
  61. package/lib/inventory/jvmToolEnv.js +6 -5
  62. package/lib/inventory/mcpConfigParser.js +166 -2
  63. package/lib/inventory/mcpDiscovery.js +11 -0
  64. package/lib/inventory/mcpServerManifest.js +389 -0
  65. package/lib/inventory/mcpbBundle.js +419 -0
  66. package/lib/inventory/plugins.js +3 -0
  67. package/lib/inventory/protobom.js +54 -88
  68. package/lib/inventory/protobomLoader.js +4 -0
  69. package/lib/inventory/rustFormulationParser.js +1 -1
  70. package/lib/inventory/source.js +10 -4
  71. package/lib/inventory/stylePackageRefs.js +41 -0
  72. package/lib/inventory/svelteUtils.js +524 -0
  73. package/lib/parsers/huggingfaceUtils.js +15 -7
  74. package/lib/parsers/onnx.js +294 -0
  75. package/lib/parsers/safetensors.js +114 -0
  76. package/lib/stages/postgen/introspection/markers.js +29 -6
  77. package/lib/stages/postgen/postgen.js +169 -1
  78. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  79. package/package.json +25 -25
  80. package/types/lib/audit/targets.d.ts.map +1 -1
  81. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  82. package/types/lib/cli/index.d.ts +2 -2
  83. package/types/lib/cli/index.d.ts.map +1 -1
  84. package/types/lib/cli/jsBom.d.ts +23 -0
  85. package/types/lib/cli/jsBom.d.ts.map +1 -1
  86. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  87. package/types/lib/cli/managedBom.d.ts +33 -0
  88. package/types/lib/cli/managedBom.d.ts.map +1 -1
  89. package/types/lib/cli/nativeBom.d.ts +88 -8
  90. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  91. package/types/lib/core/activity.d.ts.map +1 -1
  92. package/types/lib/core/env.d.ts +21 -0
  93. package/types/lib/core/env.d.ts.map +1 -1
  94. package/types/lib/core/userAgent.d.ts +56 -0
  95. package/types/lib/core/userAgent.d.ts.map +1 -0
  96. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  97. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  98. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  99. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  100. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  101. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  102. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  103. package/types/lib/ecosystems/gradleutils.d.ts +20 -0
  104. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  105. package/types/lib/ecosystems/kosi.d.ts +73 -0
  106. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  107. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  108. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  109. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  110. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  111. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  112. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  113. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  114. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  115. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  116. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  117. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  118. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  120. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  121. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  122. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  123. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  124. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  125. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  126. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  127. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  128. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  129. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  130. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  131. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  132. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  133. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  134. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  135. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  136. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  137. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  138. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  139. package/types/lib/evinser/evinser.d.ts +29 -0
  140. package/types/lib/evinser/evinser.d.ts.map +1 -1
  141. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  142. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  143. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  144. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  145. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  146. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  147. package/types/lib/inventory/auditCategories.d.ts +4 -0
  148. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  149. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  150. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  151. package/types/lib/inventory/dosai.d.ts +23 -0
  152. package/types/lib/inventory/dosai.d.ts.map +1 -1
  153. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  154. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  155. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  156. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  157. package/types/lib/inventory/fetchRate.d.ts +9 -6
  158. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  159. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  160. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  161. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  162. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  163. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  164. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  165. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  166. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  167. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  168. package/types/lib/inventory/plugins.d.ts.map +1 -1
  169. package/types/lib/inventory/protobom.d.ts +18 -13
  170. package/types/lib/inventory/protobom.d.ts.map +1 -1
  171. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  172. package/types/lib/inventory/source.d.ts.map +1 -1
  173. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  174. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  175. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  176. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  177. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  178. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  179. package/types/lib/parsers/onnx.d.ts +28 -0
  180. package/types/lib/parsers/onnx.d.ts.map +1 -0
  181. package/types/lib/parsers/safetensors.d.ts +22 -0
  182. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  183. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  184. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  185. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  186. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
@@ -9,6 +9,12 @@
9
9
  import { parentPort } from "node:worker_threads";
10
10
 
11
11
  import { parseFileASTTreeCollected } from "./analyzer.js";
12
+ import { getSvelteTemplateParser } from "./svelteUtils.js";
13
+
14
+ // The parse runs synchronously on the worker thread, so the optional Svelte
15
+ // template parser is resolved eagerly here (a silent no-op when the optional
16
+ // package is absent; the built-in script mask covers all scope evidence).
17
+ getSvelteTemplateParser();
12
18
 
13
19
  parentPort.on("message", (task) => {
14
20
  try {
@@ -23,6 +23,12 @@ export const GOLEM_AUDIT_CATEGORIES = Object.freeze([
23
23
  "golem-compliance",
24
24
  ]);
25
25
 
26
+ /** Frozen list of Cargo (Rust) audit categories. */
27
+ export const CARGO_AUDIT_CATEGORIES = Object.freeze([
28
+ "cargo-security",
29
+ "cargo-compliance",
30
+ ]);
31
+
26
32
  /**
27
33
  * Frozen list of audit categories that never activate on their own. These
28
34
  * rule packs serve a specific feature (`--introspect` for build-fidelity) and
@@ -52,10 +58,12 @@ export const BOM_AUDIT_CATEGORY_ALIASES = Object.freeze({
52
58
  "ai-bom": [...AI_BOM_AUDIT_CATEGORIES, "ai-agent", "mcp-server"],
53
59
  "ai-provenance": ["ai-provenance", "ai-oversight"],
54
60
  "ai-oversight": ["ai-oversight"],
61
+ cargo: [...CARGO_AUDIT_CATEGORIES],
55
62
  cbom: [...CBOM_AUDIT_CATEGORIES],
56
63
  "crypto-bom": [...CBOM_AUDIT_CATEGORIES],
57
64
  golem: [...GOLEM_AUDIT_CATEGORIES],
58
65
  hbom: [...HBOM_AUDIT_CATEGORIES],
66
+ rust: [...CARGO_AUDIT_CATEGORIES],
59
67
  host: [...HBOM_AUDIT_CATEGORIES, ...HOST_TOPOLOGY_AUDIT_CATEGORIES],
60
68
  });
61
69
 
@@ -345,6 +345,36 @@ function normalizeDosaiCryptoNames(cryptoObject) {
345
345
  names.add("sha-512");
346
346
  }
347
347
  }
348
+ // dosai reports the post-quantum families under their generic names and no
349
+ // generic OID exists for any of them, so the parameter set has to come from
350
+ // the call-site context (dosai's Code field carries the source line, e.g.
351
+ // "MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65)"). Without a parameter set the
352
+ // asset stays unmapped and is skipped rather than emitted with a wrong OID.
353
+ if (/^ML-?DSA$/i.test(cleanName)) {
354
+ const parameterSet = context.match(/(?:MLDsa|ML-DSA-)(44|65|87)\b/i);
355
+ if (parameterSet) {
356
+ names.add(`ml-dsa-${parameterSet[1]}`);
357
+ }
358
+ }
359
+ if (/^ML-?KEM$/i.test(cleanName)) {
360
+ const parameterSet = context.match(/(?:MLKem|ML-KEM-)(512|768|1024)\b/i);
361
+ if (parameterSet) {
362
+ names.add(`ml-kem-${parameterSet[1]}`);
363
+ }
364
+ }
365
+ if (/^SLH-?DSA$/i.test(cleanName)) {
366
+ // .NET spells it SlhDsaSha2_128s; NIST SLH-DSA-SHA2-128s. The hash family
367
+ // is required: SHA2 and SHAKE have distinct OIDs, so a context naming only
368
+ // the size, such as SLH-DSA-192f, resolves to no single parameter set.
369
+ const parameterSet = context.match(
370
+ /(?:SlhDsa|SLH-DSA-)(SHA2|SHAKE)[-_]?(128|192|256)(s|f)\b/i,
371
+ );
372
+ if (parameterSet) {
373
+ names.add(
374
+ `slh-dsa-${parameterSet[1].toLowerCase()}-${parameterSet[2]}${parameterSet[3].toLowerCase()}`,
375
+ );
376
+ }
377
+ }
348
378
  return Array.from(names);
349
379
  }
350
380
 
@@ -25,8 +25,13 @@ import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
25
25
  import { safeExistsSync } from "../core/fs.js";
26
26
  import { getDefaultPluginRuntime, resolvePluginBinary } from "./plugins.js";
27
27
 
28
- /** Major version this bridge expects from the cdxrs binary. */
29
- const CDXRS_VERSION_MAJOR = "3";
28
+ /**
29
+ * Major version this bridge expects from the cdxrs binary. cdxgen-plugins-bin
30
+ * versions every tool in lockstep, so this moves with
31
+ * `@cdxgen/cdxgen-plugins-bin` in package.json even when cdxrs itself carries
32
+ * no breaking change (4.0.0 is 3.2.0 renumbered by the release).
33
+ */
34
+ const CDXRS_VERSION_MAJOR = "4";
30
35
 
31
36
  /** Default timeout for a cdxrs invocation. */
32
37
  const DEFAULT_TIMEOUT_MS = 30_000;
@@ -1,8 +1,9 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { basename, dirname, extname } from "node:path";
2
+ import { basename, dirname, extname, join } from "node:path";
3
3
 
4
4
  import { parse as loadYaml } from "yaml";
5
5
 
6
+ import { safeExistsSync } from "../core/fs.js";
6
7
  import { sanitizeBomPropertyValue } from "../core/propertySanitizer.js";
7
8
  import { parseJsonLike } from "./jsonLike.js";
8
9
  import {
@@ -28,6 +29,18 @@ const COMMUNITY_AI_PATTERNS = [
28
29
  "**/.claude/skills/*/SKILL.md",
29
30
  ".agents/skills/*/SKILL.md",
30
31
  "**/.agents/skills/*/SKILL.md",
32
+ ".codex/skills/*/SKILL.md",
33
+ "**/.codex/skills/*/SKILL.md",
34
+ "skills/*/SKILL.md",
35
+ "**/skills/*/SKILL.md",
36
+ ".claude-plugin/plugin.json",
37
+ "**/.claude-plugin/plugin.json",
38
+ ".claude-plugin/marketplace.json",
39
+ "**/.claude-plugin/marketplace.json",
40
+ ".claude/agents/*.md",
41
+ "**/.claude/agents/*.md",
42
+ ".claude/commands/**/*.md",
43
+ "**/.claude/commands/**/*.md",
31
44
  ".cursor/rules/*.{md,mdc}",
32
45
  "**/.cursor/rules/*.{md,mdc}",
33
46
  ".windsurf/rules/**/*.md",
@@ -145,9 +158,24 @@ function frameworkForFile(filePath, raw = "") {
145
158
  if (lowerPath.includes("/.claude/skills/")) {
146
159
  return "claude-compatible";
147
160
  }
161
+ if (lowerPath.includes("/.claude-plugin/")) {
162
+ return "claude-plugin";
163
+ }
164
+ if (lowerPath.includes("/.claude/agents/")) {
165
+ return "claude-subagent";
166
+ }
167
+ if (lowerPath.includes("/.claude/commands/")) {
168
+ return "claude-command";
169
+ }
170
+ if (lowerPath.includes("/.codex/skills/")) {
171
+ return "codex";
172
+ }
148
173
  if (lowerPath.includes("/.agents/skills/")) {
149
174
  return "agent-compatible";
150
175
  }
176
+ if (/(?:^|\/)skills\/[^/]+\/skill\.md$/u.test(lowerPath)) {
177
+ return "agent-skills";
178
+ }
151
179
  return "community-ai";
152
180
  }
153
181
 
@@ -250,6 +278,173 @@ function parseSkillFile(filePath, raw) {
250
278
  metadata.metadata,
251
279
  );
252
280
  }
281
+ // Record whether the frontmatter satisfies the open Agent Skills constraints
282
+ // (name: 1-64 lowercase-alnum-hyphen; description: 1-1024 chars) as a single
283
+ // reviewer-friendly boolean rather than copying the raw values twice.
284
+ const specValid =
285
+ typeof metadata?.name === "string" &&
286
+ /^[a-z0-9-]{1,64}$/u.test(metadata.name) &&
287
+ typeof metadata?.description === "string" &&
288
+ metadata.description.length >= 1 &&
289
+ metadata.description.length <= 1024;
290
+ addUniqueProperty(
291
+ component.properties,
292
+ "cdx:skill:specValid",
293
+ specValid ? "true" : "false",
294
+ );
295
+ const skillDir = dirname(filePath);
296
+ for (const [subdir, property] of [
297
+ ["scripts", "cdx:skill:hasScripts"],
298
+ ["references", "cdx:skill:hasReferences"],
299
+ ["assets", "cdx:skill:hasAssets"],
300
+ ]) {
301
+ if (safeExistsSync(join(skillDir, subdir))) {
302
+ addUniqueProperty(component.properties, property, "true");
303
+ }
304
+ }
305
+ maybeAddFileSignals(component.properties, filePath, raw);
306
+ return [component];
307
+ }
308
+
309
+ /**
310
+ * Parse a Claude Code plugin manifest (`.claude-plugin/plugin.json`) into a
311
+ * single application component describing the plugin.
312
+ *
313
+ * @param {string} filePath Manifest path
314
+ * @param {string} raw File contents
315
+ * @returns {Object[]} Component list (one plugin component or empty)
316
+ */
317
+ function parsePluginManifest(filePath, raw) {
318
+ let manifest;
319
+ try {
320
+ manifest = parseJsonLike(raw);
321
+ } catch {
322
+ return [];
323
+ }
324
+ if (!manifest?.name) {
325
+ return [];
326
+ }
327
+ const component = createComponent(
328
+ filePath,
329
+ "claude-plugin",
330
+ "agent-plugin",
331
+ manifest.name,
332
+ );
333
+ if (manifest.version) {
334
+ component.version = String(manifest.version);
335
+ }
336
+ addUniqueProperty(
337
+ component.properties,
338
+ "cdx:plugin:description",
339
+ manifest.description,
340
+ );
341
+ addUniqueProperty(
342
+ component.properties,
343
+ "cdx:plugin:author",
344
+ typeof manifest.author === "string"
345
+ ? manifest.author
346
+ : manifest.author?.name,
347
+ );
348
+ for (const [key, property] of [
349
+ ["commands", "cdx:plugin:commandCount"],
350
+ ["agents", "cdx:plugin:agentCount"],
351
+ ["skills", "cdx:plugin:skillCount"],
352
+ ["hooks", "cdx:plugin:hookCount"],
353
+ ["mcpServers", "cdx:plugin:mcpServerCount"],
354
+ ]) {
355
+ const value = manifest[key];
356
+ if (Array.isArray(value)) {
357
+ addUniqueProperty(component.properties, property, String(value.length));
358
+ } else if (value && typeof value === "object") {
359
+ addUniqueProperty(
360
+ component.properties,
361
+ property,
362
+ String(Object.keys(value).length),
363
+ );
364
+ }
365
+ }
366
+ maybeAddFileSignals(component.properties, filePath, raw);
367
+ return [component];
368
+ }
369
+
370
+ /**
371
+ * Parse a Claude Code plugin marketplace file
372
+ * (`.claude-plugin/marketplace.json`) into one component per listed plugin.
373
+ *
374
+ * @param {string} filePath Marketplace path
375
+ * @param {string} raw File contents
376
+ * @returns {Object[]} Component list
377
+ */
378
+ function parsePluginMarketplace(filePath, raw) {
379
+ let marketplace;
380
+ try {
381
+ marketplace = parseJsonLike(raw);
382
+ } catch {
383
+ return [];
384
+ }
385
+ const plugins = Array.isArray(marketplace?.plugins)
386
+ ? marketplace.plugins
387
+ : [];
388
+ const components = [];
389
+ for (const plugin of plugins) {
390
+ const name = plugin?.name || plugin?.source;
391
+ if (!name) {
392
+ continue;
393
+ }
394
+ const component = createComponent(
395
+ filePath,
396
+ "claude-plugin",
397
+ "agent-plugin-listing",
398
+ String(name),
399
+ );
400
+ addUniqueProperty(
401
+ component.properties,
402
+ "cdx:plugin:description",
403
+ plugin.description,
404
+ );
405
+ addUniqueProperty(
406
+ component.properties,
407
+ "cdx:plugin:source",
408
+ typeof plugin.source === "string" ? plugin.source : undefined,
409
+ );
410
+ components.push(component);
411
+ }
412
+ return components;
413
+ }
414
+
415
+ /**
416
+ * Parse a Claude Code subagent definition (`.claude/agents/*.md`) into an
417
+ * agent-definition component using its markdown frontmatter.
418
+ *
419
+ * @param {string} filePath Subagent markdown path
420
+ * @param {string} raw File contents
421
+ * @returns {Object[]} Component list
422
+ */
423
+ function parseClaudeSubagent(filePath, raw) {
424
+ const { metadata } = parseFrontmatter(raw);
425
+ const name = metadata?.name || basename(filePath, extname(filePath));
426
+ const component = createComponent(
427
+ filePath,
428
+ "claude-subagent",
429
+ "agent-definition",
430
+ name,
431
+ );
432
+ addUniqueProperty(component.properties, "cdx:agent:role", "claude-subagent");
433
+ addUniqueProperty(
434
+ component.properties,
435
+ "cdx:agent:description",
436
+ metadata?.description,
437
+ );
438
+ addUniqueProperty(component.properties, "cdx:agent:model", metadata?.model);
439
+ if (Array.isArray(metadata?.tools)) {
440
+ addUniqueProperty(
441
+ component.properties,
442
+ "cdx:agent:tools",
443
+ metadata.tools.join(","),
444
+ );
445
+ } else if (typeof metadata?.tools === "string") {
446
+ addUniqueProperty(component.properties, "cdx:agent:tools", metadata.tools);
447
+ }
253
448
  maybeAddFileSignals(component.properties, filePath, raw);
254
449
  return [component];
255
450
  }
@@ -708,10 +903,28 @@ export const communityAiConfigParser = {
708
903
  components.push(...parseAgentMarkdown(filePath, raw));
709
904
  continue;
710
905
  }
906
+ if (lowerPath.endsWith("/.claude-plugin/plugin.json")) {
907
+ components.push(...parsePluginManifest(filePath, raw));
908
+ continue;
909
+ }
910
+ if (lowerPath.endsWith("/.claude-plugin/marketplace.json")) {
911
+ components.push(...parsePluginMarketplace(filePath, raw));
912
+ continue;
913
+ }
914
+ if (lowerPath.includes("/.claude/agents/")) {
915
+ components.push(...parseClaudeSubagent(filePath, raw));
916
+ continue;
917
+ }
918
+ if (lowerPath.includes("/.claude/commands/")) {
919
+ components.push(...parseInstructionMarkdown(filePath, raw));
920
+ continue;
921
+ }
711
922
  if (
712
923
  lowerPath.includes("/.opencode/skills/") ||
713
924
  lowerPath.includes("/.claude/skills/") ||
714
- lowerPath.includes("/.agents/skills/")
925
+ lowerPath.includes("/.agents/skills/") ||
926
+ lowerPath.includes("/.codex/skills/") ||
927
+ lowerPath.endsWith("/skill.md")
715
928
  ) {
716
929
  components.push(...parseSkillFile(filePath, raw));
717
930
  continue;
@@ -26,6 +26,15 @@ const DOTNET_LANGUAGES = new Set([
26
26
  "cs",
27
27
  "dotnet",
28
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",
29
38
  "f#",
30
39
  "fsharp",
31
40
  "fs",
@@ -429,6 +438,74 @@ export function resolveComponentPurl(purl, purlAliasMap) {
429
438
  return resolveDosaiComponentPurl(purl, purlAliasMap);
430
439
  }
431
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
+
432
509
  /**
433
510
  * Map a dosai methods slice to per-purl occurrence evidence.
434
511
  *
@@ -101,15 +101,18 @@ function hardenedGitEnv() {
101
101
  * @type {Object<string, string>}
102
102
  */
103
103
  export const SDKMAN_JAVA_TOOL_ALIASES = {
104
- java8: readEnvironmentVariable("JAVA8_TOOL") || "8.0.452-amzn", // Temurin no longer offers java8 :(
105
- java11: readEnvironmentVariable("JAVA11_TOOL") || "11.0.31-tem",
106
- java17: readEnvironmentVariable("JAVA17_TOOL") || "17.0.19-tem",
107
- java21: readEnvironmentVariable("JAVA21_TOOL") || "21.0.11-tem",
104
+ // Identifiers must be downloadable from the sdkman broker: the `sdk list`
105
+ // output advertises Temurin respin ids such as 21.0.12+1.1-tem, but the
106
+ // broker cannot serve ids containing '+', so the plain ids are pinned.
107
+ java8: readEnvironmentVariable("JAVA8_TOOL") || "8.0.504-zulu", // Temurin no longer offers java8 :(
108
+ java11: readEnvironmentVariable("JAVA11_TOOL") || "11.0.32-tem",
109
+ java17: readEnvironmentVariable("JAVA17_TOOL") || "17.0.20-tem",
110
+ java21: readEnvironmentVariable("JAVA21_TOOL") || "21.0.12-tem",
108
111
  java22: readEnvironmentVariable("JAVA22_TOOL") || "22.0.2-tem",
109
112
  java23: readEnvironmentVariable("JAVA23_TOOL") || "23.0.2-tem",
110
113
  java24: readEnvironmentVariable("JAVA24_TOOL") || "24.0.2-tem",
111
- java25: readEnvironmentVariable("JAVA25_TOOL") || "25.0.3-tem",
112
- java26: readEnvironmentVariable("JAVA26_TOOL") || "26.0.1-tem",
114
+ java25: readEnvironmentVariable("JAVA25_TOOL") || "25.0.4-tem",
115
+ java26: readEnvironmentVariable("JAVA26_TOOL") || "26.0.2-tem",
113
116
  };
114
117
 
115
118
  /**
@@ -6,6 +6,7 @@ import { build } from "@cdxgen/cdx-purl";
6
6
 
7
7
  import { safeExistsSync } from "../core/fs.js";
8
8
  import { thoughtLog } from "../core/logger.js";
9
+ import { addDosaiReachabilityProperties } from "./dosai.js";
9
10
  import {
10
11
  addDosaiSetValue,
11
12
  buildDosaiPurlAliasMap,
@@ -256,6 +257,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
256
257
  const purlLocationMap = {};
257
258
  const purlModulesMap = {};
258
259
  const purlMethodsMap = {};
260
+ const purlReachabilityMap = {};
259
261
  const purlAliasMap = buildDosaiPurlAliasMap(pkgList);
260
262
  for (const apkg of pkgList) {
261
263
  if (apkg.properties && Array.isArray(apkg.properties)) {
@@ -340,6 +342,9 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
340
342
  if (!modPurl) {
341
343
  continue;
342
344
  }
345
+ // Confidence/evidence facts for this purl, applied to the component below so an
346
+ // unbuilt tree stays distinguishable from an only-imported package.
347
+ purlReachabilityMap[modPurl] ??= reachability;
343
348
  let hasExplicitSourceLocations = false;
344
349
  for (const sourceLocation of reachability.SourceLocations || []) {
345
350
  const location = dosaiSourceLocation(sourceLocation);
@@ -456,6 +461,14 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
456
461
  "I didn't find any occurrence evidence or detailed imported modules, even though there is good dependency slice data from dosai. This is surprising.",
457
462
  );
458
463
  }
464
+ // Reachability confidence facts (kind, confidence, evidence, reasons) as properties.
465
+ // Applied outside the location guard: a Low-confidence unbuilt tree must stay
466
+ // distinguishable from an only-imported package even when no locations were found.
467
+ for (const apkg of pkgList) {
468
+ if (purlReachabilityMap[apkg.purl]) {
469
+ addDosaiReachabilityProperties(apkg, purlReachabilityMap[apkg.purl]);
470
+ }
471
+ }
459
472
  return pkgList;
460
473
  }
461
474
 
@@ -248,21 +248,58 @@ export async function prefetchJson(requests, options = {}) {
248
248
  * - a non-JSON `responseType`, since the envelope has nowhere to put HTML;
249
249
  * - request-specific `headers`, since cdxrs derives its own Authorization from
250
250
  * `GITHUB_TOKEN` and would silently drop any other credential, turning an
251
- * authenticated lookup into an anonymous one.
251
+ * authenticated lookup into an anonymous one;
252
+ * - anything addressed to a host whose rate policy only one transport knows.
253
+ * crates.io's crawler policy allows one request per second with no
254
+ * parallelism, and package.elm-lang.org has a JS-only allowance because
255
+ * rate.rs carries no row for it. Both transports keep separate gates, so a
256
+ * batch split across the two would issue twice the agreed rate; the JS pool
257
+ * owns these hosts so there is one gate to reason about.
252
258
  *
253
259
  * The pool applies the same global and per-host concurrency and rate policy, so
254
260
  * the batching win is unaffected; only the on-disk cache is skipped.
255
261
  *
256
- * @param {{responseType?: string, headers?: Object}} request
262
+ * @param {{url?: string, responseType?: string, headers?: Object}} request
257
263
  * @returns {boolean}
258
264
  */
259
265
  function cdxrsCanServe(request) {
260
266
  if (request.responseType && request.responseType !== "json") {
261
267
  return false;
262
268
  }
269
+ if (isSingleGateHost(extractHost(request.url))) {
270
+ return false;
271
+ }
263
272
  return !request.headers || !Object.keys(request.headers).length;
264
273
  }
265
274
 
275
+ /**
276
+ * Whether a hostname belongs to crates.io, including its index and CDN hosts.
277
+ *
278
+ * @param {string|null} host Hostname, or null when the URL did not parse.
279
+ * @returns {boolean}
280
+ */
281
+ export function isCratesHost(host) {
282
+ if (!host) {
283
+ return false;
284
+ }
285
+ const lower = host.toLowerCase();
286
+ return lower === "crates.io" || lower.endsWith(".crates.io");
287
+ }
288
+
289
+ /**
290
+ * Whether a host's rate policy lives in the JS table alone and therefore needs
291
+ * every request to pass through the one gate the JS pool holds.
292
+ *
293
+ * @param {string|null} host Hostname, or null when the URL did not parse.
294
+ * @returns {boolean}
295
+ */
296
+ export function isSingleGateHost(host) {
297
+ if (!host) {
298
+ return false;
299
+ }
300
+ return isCratesHost(host) || host.toLowerCase() === "package.elm-lang.org";
301
+ }
302
+
266
303
  /**
267
304
  * Run the batch through the `cdxrs fetch` subprocess and translate its
268
305
  * envelope into the shared result shape.
@@ -700,6 +737,41 @@ async function fetchOne(request, gauge) {
700
737
  }
701
738
  }
702
739
 
740
+ /**
741
+ * Run a caller's own request under the same per-host gate the batch pool uses.
742
+ *
743
+ * Not every registry lookup goes through the pool: a caller that misses the
744
+ * prefetch, or runs with prefetching disabled, issues `cdxgenAgent.get`
745
+ * directly. Those requests reach the same host and count against the same
746
+ * published budget, so they have to queue behind the same limiter rather than
747
+ * a second one — crates.io's one-request-per-second policy is not honoured by
748
+ * two independent gates that each allow one per second.
749
+ *
750
+ * The per-host semaphore and limiter are module state, so this shares one gate
751
+ * per host for the life of the process.
752
+ *
753
+ * @template T
754
+ * @param {string} url The URL about to be requested.
755
+ * @param {() => Promise<T>} issue Issues the request and resolves its result.
756
+ * @returns {Promise<T>} Whatever `issue` resolves to.
757
+ */
758
+ export async function withHostRateLimit(url, issue) {
759
+ const host = extractHost(url);
760
+ if (!host) {
761
+ return await issue();
762
+ }
763
+ const controls = getHostControls(host);
764
+ const releaseGlobal = await _globalSemaphore.acquire();
765
+ const releaseHost = await controls.sem.acquire();
766
+ try {
767
+ await controls.limiter.wait();
768
+ return await issue();
769
+ } finally {
770
+ releaseHost();
771
+ releaseGlobal();
772
+ }
773
+ }
774
+
703
775
  /**
704
776
  * Dedupe key for an in-flight request: the URL, plus everything that changes
705
777
  * which response the caller gets back. That is the headers which change what