@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.
- package/README.md +8 -7
- package/bin/cdxgen.js +16 -3
- package/bin/evinse.js +2 -0
- package/data/crypto-oid.json +88 -12
- package/data/remediations.json +23 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +12 -0
- package/lib/cli/index.js +192 -1
- package/lib/cli/jsBom.js +59 -2
- package/lib/cli/jvmBom.js +22 -0
- package/lib/cli/managedBom.js +152 -0
- package/lib/cli/nativeBom.js +798 -1
- package/lib/core/activity.js +120 -2
- package/lib/core/env.js +39 -1
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +13 -5
- package/lib/ecosystems/cppEvidence.js +40 -2
- package/lib/ecosystems/ecosystems.js +241 -91
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +143 -2
- package/lib/ecosystems/kosi.js +1256 -0
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +69 -39
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +172 -0
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +1 -1
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +932 -17
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/rusi.js +126 -6
- package/lib/evinser/evinser.js +111 -1
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/auditCategories.js +8 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +7 -2
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/dosai.js +77 -0
- package/lib/inventory/envcontext.js +9 -6
- package/lib/inventory/evidenceUtils.js +13 -0
- package/lib/inventory/fetchBatch.js +74 -2
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +6 -5
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +54 -88
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/source.js +10 -4
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/stages/postgen/introspection/markers.js +29 -6
- package/lib/stages/postgen/postgen.js +169 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/package.json +25 -25
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +21 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +20 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +73 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +29 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +4 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +23 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +34 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +33 -0
- 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
|
+
};
|