@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
@@ -0,0 +1,389 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+
4
+ import { sanitizeBomPropertyValue } from "../core/propertySanitizer.js";
5
+ import { parseJsonLike } from "./jsonLike.js";
6
+ import { isLocalHost, sanitizeMcpRefToken } from "./mcpDiscovery.js";
7
+ import { tryBuildPurl } from "./purl.js";
8
+ import { scanTextForHiddenUnicode } from "./unicodeScan.js";
9
+
10
+ const SERVER_MANIFEST_PATTERNS = ["server.json", "**/server.json"];
11
+
12
+ // registryType values understood by the official MCP Registry, mapped to the
13
+ // package-url ecosystem cdxgen uses when a component purl can be built.
14
+ const REGISTRY_TYPE_PURL_TYPES = {
15
+ npm: "npm",
16
+ pypi: "pypi",
17
+ nuget: "nuget",
18
+ oci: "oci",
19
+ mcpb: undefined,
20
+ };
21
+
22
+ function addUniqueProperty(properties, name, value) {
23
+ const sanitizedValue = sanitizeBomPropertyValue(name, value);
24
+ if (
25
+ sanitizedValue === undefined ||
26
+ sanitizedValue === null ||
27
+ sanitizedValue === ""
28
+ ) {
29
+ return;
30
+ }
31
+ if (
32
+ properties.some(
33
+ (property) =>
34
+ property.name === name && property.value === String(sanitizedValue),
35
+ )
36
+ ) {
37
+ return;
38
+ }
39
+ properties.push({ name, value: String(sanitizedValue) });
40
+ }
41
+
42
+ /**
43
+ * Determine whether a parsed object looks like an MCP Registry server manifest.
44
+ *
45
+ * The heuristic keeps unrelated `server.json` files (for example dev-server
46
+ * configuration) out of the MCP inventory by requiring the manifest to carry a
47
+ * server name plus at least one `packages[]` or `remotes[]` entry, or an
48
+ * explicit MCP Registry `$schema` reference.
49
+ *
50
+ * @param {Object} manifest Parsed JSON object
51
+ * @returns {boolean} true when the object resembles a registry manifest
52
+ */
53
+ function looksLikeServerManifest(manifest) {
54
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
55
+ return false;
56
+ }
57
+ const schema = String(manifest.$schema || "").toLowerCase();
58
+ if (schema.includes("modelcontextprotocol")) {
59
+ return true;
60
+ }
61
+ const hasName = typeof manifest.name === "string" && manifest.name.length > 0;
62
+ const hasPackages =
63
+ Array.isArray(manifest.packages) && manifest.packages.length > 0;
64
+ const hasRemotes =
65
+ Array.isArray(manifest.remotes) && manifest.remotes.length > 0;
66
+ return hasName && (hasPackages || hasRemotes);
67
+ }
68
+
69
+ /**
70
+ * Build a package-url string for a registry package entry when the
71
+ * registryType maps to a supported ecosystem and enough coordinates exist.
72
+ *
73
+ * @param {Object} pkg Registry package entry
74
+ * @returns {string|undefined} purl string, or undefined when not derivable
75
+ */
76
+ function purlForPackage(pkg) {
77
+ const registryType = String(pkg?.registryType || pkg?.registry_type || "")
78
+ .trim()
79
+ .toLowerCase();
80
+ const purlType = REGISTRY_TYPE_PURL_TYPES[registryType];
81
+ if (!purlType) {
82
+ return undefined;
83
+ }
84
+ const identifier = String(pkg?.identifier || pkg?.name || "").trim();
85
+ if (!identifier) {
86
+ return undefined;
87
+ }
88
+ const version = pkg?.version ? String(pkg.version) : undefined;
89
+ let namespace;
90
+ let name = identifier;
91
+ const parts = { type: purlType, version };
92
+ if (purlType === "npm" && identifier.startsWith("@")) {
93
+ const [scope, unscoped] = identifier.split("/");
94
+ namespace = scope;
95
+ name = unscoped || identifier;
96
+ } else if (purlType === "pypi") {
97
+ // PyPI purls require the normalized project name: lowercase with runs of
98
+ // [-_.] collapsed to a single hyphen.
99
+ name = identifier.toLowerCase().replace(/[-_.]+/gu, "-");
100
+ } else if (purlType === "oci") {
101
+ // OCI purls are only meaningful with a lowercase name, a digest version,
102
+ // and a repository_url qualifier. A tag-only reference is not resolvable,
103
+ // so skip the purl rather than emit a misleading one.
104
+ const digest =
105
+ pkg?.digest || (version?.startsWith("sha256:") ? version : undefined);
106
+ const repositoryUrl = pkg?.registryBaseUrl || pkg?.repository_url;
107
+ if (!digest || !repositoryUrl) {
108
+ return undefined;
109
+ }
110
+ name = identifier.toLowerCase().split("/").pop();
111
+ parts.version = digest;
112
+ parts.qualifiers = { repository_url: String(repositoryUrl) };
113
+ }
114
+ parts.namespace = namespace;
115
+ parts.name = name;
116
+ return tryBuildPurl(parts)?.toString() || undefined;
117
+ }
118
+
119
+ /**
120
+ * Convert one registry package entry into a CycloneDX component describing the
121
+ * distributable that hosts the MCP server.
122
+ *
123
+ * @param {string} filePath Manifest path
124
+ * @param {string} serverName MCP server name
125
+ * @param {Object} pkg Registry package entry
126
+ * @param {number} index Package index for stable identity
127
+ * @returns {Object|undefined} component or undefined when not usable
128
+ */
129
+ function componentForPackage(filePath, serverName, pkg, index) {
130
+ const registryType = String(pkg?.registryType || pkg?.registry_type || "")
131
+ .trim()
132
+ .toLowerCase();
133
+ const identifier = String(pkg?.identifier || pkg?.name || "").trim();
134
+ if (!registryType && !identifier) {
135
+ return undefined;
136
+ }
137
+ const purl = purlForPackage(pkg);
138
+ const properties = [
139
+ { name: "internal:SrcFile", value: filePath },
140
+ { name: "cdx:mcp:inventorySource", value: "server-manifest" },
141
+ { name: "cdx:registry:server:packageRegistryType", value: registryType },
142
+ ];
143
+ if (pkg?.transport?.type || pkg?.transport) {
144
+ addUniqueProperty(
145
+ properties,
146
+ "cdx:registry:server:packageTransport",
147
+ typeof pkg.transport === "string" ? pkg.transport : pkg.transport?.type,
148
+ );
149
+ }
150
+ // Record only the counts and field names the server will be handed at launch,
151
+ // never their values. These describe the launch surface without leaking
152
+ // secrets that a registry manifest may reference through them.
153
+ const secretInputFields = new Set();
154
+ for (const [inputKey, property] of [
155
+ ["environmentVariables", "cdx:registry:server:environmentVariableCount"],
156
+ ["runtimeArguments", "cdx:registry:server:runtimeArgumentCount"],
157
+ ["packageArguments", "cdx:registry:server:packageArgumentCount"],
158
+ ]) {
159
+ const entries = pkg?.[inputKey];
160
+ if (Array.isArray(entries) && entries.length) {
161
+ addUniqueProperty(properties, property, String(entries.length));
162
+ for (const entry of entries) {
163
+ const fieldName = entry?.name || entry?.variable;
164
+ if (
165
+ entry?.isSecret === true ||
166
+ entry?.secret === true ||
167
+ (typeof fieldName === "string" &&
168
+ /(token|secret|password|api[_-]?key|credential)/iu.test(fieldName))
169
+ ) {
170
+ secretInputFields.add(String(fieldName || "secret"));
171
+ }
172
+ }
173
+ }
174
+ }
175
+ if (secretInputFields.size) {
176
+ addUniqueProperty(
177
+ properties,
178
+ "cdx:registry:server:secretInputCount",
179
+ String(secretInputFields.size),
180
+ );
181
+ }
182
+ const component = {
183
+ "bom-ref":
184
+ purl ||
185
+ `urn:mcp:server-package:${sanitizeMcpRefToken(serverName)}:${sanitizeMcpRefToken(identifier || registryType)}:${index}`,
186
+ type: "application",
187
+ name: identifier || `${serverName}-package-${index}`,
188
+ properties,
189
+ };
190
+ if (purl) {
191
+ component.purl = purl;
192
+ }
193
+ if (pkg?.version) {
194
+ component.version = String(pkg.version);
195
+ }
196
+ return component;
197
+ }
198
+
199
+ /**
200
+ * Convert one registry remote entry into a CycloneDX service describing the
201
+ * remotely hosted MCP endpoint.
202
+ *
203
+ * @param {string} filePath Manifest path
204
+ * @param {string} serverName MCP server name
205
+ * @param {Object} remote Registry remote entry
206
+ * @param {number} index Remote index for stable identity
207
+ * @returns {Object|undefined} service or undefined when no URL is present
208
+ */
209
+ function serviceForRemote(filePath, serverName, remote, index) {
210
+ const url = typeof remote?.url === "string" ? remote.url : undefined;
211
+ if (!url) {
212
+ return undefined;
213
+ }
214
+ let hostname;
215
+ try {
216
+ hostname = new URL(url).hostname;
217
+ } catch {
218
+ return undefined;
219
+ }
220
+ const transport = String(remote?.type || remote?.transport || "")
221
+ .trim()
222
+ .toLowerCase();
223
+ const exposureType = isLocalHost(hostname)
224
+ ? "local-only"
225
+ : "networked-public";
226
+ const properties = [
227
+ { name: "internal:SrcFile", value: filePath },
228
+ { name: "cdx:mcp:serviceType", value: "registry-remote" },
229
+ { name: "cdx:mcp:inventorySource", value: "server-manifest" },
230
+ { name: "cdx:mcp:exposureType", value: exposureType },
231
+ // A remote server has no resolvable local package, so its composition
232
+ // cannot be verified from the manifest alone.
233
+ { name: "cdx:mcp:composition", value: "unknown" },
234
+ { name: "cdx:mcp:reviewNeeded", value: "true" },
235
+ ];
236
+ if (transport) {
237
+ addUniqueProperty(
238
+ properties,
239
+ "cdx:mcp:transport",
240
+ transport.includes("sse")
241
+ ? "sse"
242
+ : transport.includes("stream") || transport.includes("http")
243
+ ? "streamable-http"
244
+ : transport,
245
+ );
246
+ }
247
+ return {
248
+ "bom-ref": `urn:service:mcp:${sanitizeMcpRefToken(serverName)}:remote:${index}`,
249
+ group: "mcp",
250
+ name: serverName,
251
+ endpoints: [url],
252
+ properties,
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Build the file component that represents the discovered manifest itself and
258
+ * summarizes the packages and remotes it declares.
259
+ *
260
+ * @param {string} filePath Manifest path
261
+ * @param {Object} manifest Parsed manifest
262
+ * @param {string} raw Raw file contents
263
+ * @param {number} packageCount Number of package components emitted
264
+ * @param {number} remoteCount Number of remote services emitted
265
+ * @returns {Object} file component
266
+ */
267
+ function manifestFileComponent(
268
+ filePath,
269
+ manifest,
270
+ raw,
271
+ packageCount,
272
+ remoteCount,
273
+ ) {
274
+ const properties = [
275
+ { name: "internal:SrcFile", value: filePath },
276
+ { name: "cdx:file:kind", value: "mcp-server-manifest" },
277
+ { name: "cdx:mcp:inventorySource", value: "server-manifest" },
278
+ {
279
+ name: "cdx:registry:server:packageCount",
280
+ value: String(packageCount),
281
+ },
282
+ { name: "cdx:registry:server:remoteCount", value: String(remoteCount) },
283
+ ];
284
+ addUniqueProperty(
285
+ properties,
286
+ "cdx:registry:server:name",
287
+ typeof manifest.name === "string" ? manifest.name : undefined,
288
+ );
289
+ addUniqueProperty(
290
+ properties,
291
+ "cdx:registry:server:version",
292
+ manifest.version ? String(manifest.version) : undefined,
293
+ );
294
+ const hiddenUnicodeScan = scanTextForHiddenUnicode(raw, { syntax: "json" });
295
+ if (hiddenUnicodeScan.hasHiddenUnicode) {
296
+ addUniqueProperty(properties, "cdx:file:hasHiddenUnicode", "true");
297
+ addUniqueProperty(
298
+ properties,
299
+ "cdx:file:hiddenUnicodeCodePoints",
300
+ hiddenUnicodeScan.codePoints.join(","),
301
+ );
302
+ }
303
+ return {
304
+ "bom-ref": `file:${filePath}`,
305
+ type: "file",
306
+ name: basename(filePath),
307
+ properties,
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Parser for MCP Registry server manifests (`server.json`).
313
+ *
314
+ * Converts each declared distributable package into a CycloneDX component
315
+ * (with a purl when the registry type maps to a supported ecosystem) and each
316
+ * remotely hosted endpoint into a service, alongside a file component that
317
+ * summarizes the manifest.
318
+ *
319
+ * @type {{id: string, patterns: string[], parse(files: string[], options?: Object): {components: Object[], services: Object[]}}}
320
+ */
321
+ export const mcpServerManifestParser = {
322
+ id: "mcp-server-manifest",
323
+ patterns: SERVER_MANIFEST_PATTERNS,
324
+ parse(files, _options = {}) {
325
+ const components = [];
326
+ const services = [];
327
+ for (const filePath of [...new Set(files || [])]) {
328
+ let raw;
329
+ try {
330
+ raw = readFileSync(filePath, "utf-8");
331
+ } catch {
332
+ continue;
333
+ }
334
+ let manifest;
335
+ try {
336
+ manifest = parseJsonLike(raw);
337
+ } catch {
338
+ continue;
339
+ }
340
+ if (!looksLikeServerManifest(manifest)) {
341
+ continue;
342
+ }
343
+ const serverName =
344
+ typeof manifest.name === "string" && manifest.name.length
345
+ ? manifest.name
346
+ : basename(filePath);
347
+ const packageComponents = [];
348
+ const packages = Array.isArray(manifest.packages)
349
+ ? manifest.packages
350
+ : [];
351
+ for (let index = 0; index < packages.length; index++) {
352
+ const component = componentForPackage(
353
+ filePath,
354
+ serverName,
355
+ packages[index],
356
+ index,
357
+ );
358
+ if (component) {
359
+ packageComponents.push(component);
360
+ }
361
+ }
362
+ const remoteServices = [];
363
+ const remotes = Array.isArray(manifest.remotes) ? manifest.remotes : [];
364
+ for (let index = 0; index < remotes.length; index++) {
365
+ const service = serviceForRemote(
366
+ filePath,
367
+ serverName,
368
+ remotes[index],
369
+ index,
370
+ );
371
+ if (service) {
372
+ remoteServices.push(service);
373
+ }
374
+ }
375
+ components.push(
376
+ manifestFileComponent(
377
+ filePath,
378
+ manifest,
379
+ raw,
380
+ packageComponents.length,
381
+ remoteServices.length,
382
+ ),
383
+ );
384
+ components.push(...packageComponents);
385
+ services.push(...remoteServices);
386
+ }
387
+ return { components, services };
388
+ },
389
+ };