@cyclonedx/cdxgen 12.3.1 → 12.3.3
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 +6 -0
- package/bin/cdxgen.js +1 -2
- package/data/rules/ai-agent-governance.yaml +43 -0
- package/data/rules/ci-permissions.yaml +132 -0
- package/data/rules/dependency-sources.yaml +65 -5
- package/data/rules/mcp-servers.yaml +36 -2
- package/data/rules/package-integrity.yaml +22 -0
- package/lib/cli/index.js +436 -56
- package/lib/cli/index.poku.js +875 -2
- package/lib/helpers/agentFormulationParser.js +10 -3
- package/lib/helpers/agentFormulationParser.poku.js +42 -0
- package/lib/helpers/aiInventory.js +262 -0
- package/lib/helpers/aiInventory.poku.js +111 -0
- package/lib/helpers/analyzer.js +413 -54
- package/lib/helpers/analyzer.poku.js +117 -0
- package/lib/helpers/auditCategories.js +76 -0
- package/lib/helpers/chromextutils.js +25 -3
- package/lib/helpers/chromextutils.poku.js +68 -0
- package/lib/helpers/ciParsers/githubActions.js +79 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
- package/lib/helpers/communityAiConfigParser.js +15 -5
- package/lib/helpers/communityAiConfigParser.poku.js +71 -0
- package/lib/helpers/depsUtils.js +5 -0
- package/lib/helpers/depsUtils.poku.js +55 -0
- package/lib/helpers/display.js +50 -24
- package/lib/helpers/display.poku.js +70 -58
- package/lib/helpers/formulationParsers.js +26 -6
- package/lib/helpers/jsonLike.js +21 -20
- package/lib/helpers/jsonLike.poku.js +34 -0
- package/lib/helpers/mcpConfigParser.js +32 -16
- package/lib/helpers/mcpConfigParser.poku.js +104 -0
- package/lib/helpers/mcpDiscovery.js +13 -23
- package/lib/helpers/mcpDiscovery.poku.js +21 -0
- package/lib/helpers/propertySanitizer.js +121 -0
- package/lib/helpers/utils.js +953 -41
- package/lib/helpers/utils.poku.js +901 -1
- package/lib/managers/binary.js +16 -0
- package/lib/managers/binary.poku.js +1 -0
- package/lib/managers/docker.js +240 -16
- package/lib/managers/docker.poku.js +1142 -2
- package/lib/server/server.js +7 -4
- package/lib/server/server.poku.js +36 -1
- package/lib/stages/postgen/annotator.js +2 -1
- package/lib/stages/postgen/annotator.poku.js +15 -0
- package/lib/stages/postgen/auditBom.js +12 -6
- package/lib/stages/postgen/auditBom.poku.js +755 -6
- package/lib/stages/postgen/postgen.js +229 -6
- package/lib/stages/postgen/postgen.poku.js +180 -0
- package/package.json +2 -1
- package/types/lib/cli/index.d.ts +1 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
- package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
- package/types/lib/helpers/aiInventory.d.ts +23 -0
- package/types/lib/helpers/aiInventory.d.ts.map +1 -0
- package/types/lib/helpers/analyzer.d.ts +5 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/auditCategories.d.ts +12 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -1
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +1 -0
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/jsonLike.d.ts +4 -0
- package/types/lib/helpers/jsonLike.d.ts.map +1 -0
- package/types/lib/helpers/mcp.d.ts +29 -0
- package/types/lib/helpers/mcp.d.ts.map +1 -0
- package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
- package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
- package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
- package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
- package/types/lib/helpers/propertySanitizer.d.ts +3 -0
- package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +31 -0
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +3 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
package/lib/helpers/analyzer.js
CHANGED
|
@@ -8,6 +8,10 @@ import traverse from "@babel/traverse";
|
|
|
8
8
|
|
|
9
9
|
import { classifyMcpReference } from "./mcp.js";
|
|
10
10
|
import { isLocalHost, sanitizeMcpRefToken } from "./mcpDiscovery.js";
|
|
11
|
+
import {
|
|
12
|
+
sanitizeBomPropertyValue,
|
|
13
|
+
sanitizeBomUrl,
|
|
14
|
+
} from "./propertySanitizer.js";
|
|
11
15
|
|
|
12
16
|
const IGNORE_DIRS = process.env.ASTGEN_IGNORE_DIRS
|
|
13
17
|
? process.env.ASTGEN_IGNORE_DIRS.split(",")
|
|
@@ -1509,13 +1513,26 @@ const providerFamilyFromModelName = (modelName) => {
|
|
|
1509
1513
|
};
|
|
1510
1514
|
|
|
1511
1515
|
const addUniqueProperty = (properties, name, value) => {
|
|
1512
|
-
|
|
1516
|
+
const sanitizedValue = sanitizeBomPropertyValue(name, value);
|
|
1517
|
+
if (
|
|
1518
|
+
sanitizedValue === undefined ||
|
|
1519
|
+
sanitizedValue === null ||
|
|
1520
|
+
sanitizedValue === ""
|
|
1521
|
+
) {
|
|
1513
1522
|
return;
|
|
1514
1523
|
}
|
|
1515
|
-
|
|
1524
|
+
const normalizedValue =
|
|
1525
|
+
typeof sanitizedValue === "string"
|
|
1526
|
+
? sanitizedValue
|
|
1527
|
+
: String(sanitizedValue);
|
|
1528
|
+
if (
|
|
1529
|
+
properties.some(
|
|
1530
|
+
(prop) => prop.name === name && prop.value === normalizedValue,
|
|
1531
|
+
)
|
|
1532
|
+
) {
|
|
1516
1533
|
return;
|
|
1517
1534
|
}
|
|
1518
|
-
properties.push({ name, value });
|
|
1535
|
+
properties.push({ name, value: normalizedValue });
|
|
1519
1536
|
};
|
|
1520
1537
|
|
|
1521
1538
|
const rootMemberName = (value) => String(value || "").split(".")[0];
|
|
@@ -1823,14 +1840,18 @@ const primitiveComponentForMcp = (serviceInfo, primitive) => {
|
|
|
1823
1840
|
addUniqueProperty(
|
|
1824
1841
|
properties,
|
|
1825
1842
|
"cdx:mcp:toolAnnotations",
|
|
1826
|
-
|
|
1843
|
+
primitive.annotations,
|
|
1827
1844
|
);
|
|
1828
1845
|
}
|
|
1829
1846
|
return {
|
|
1830
1847
|
"bom-ref": primitiveRef,
|
|
1831
|
-
description:
|
|
1832
|
-
|
|
1833
|
-
|
|
1848
|
+
description: String(
|
|
1849
|
+
sanitizeBomPropertyValue(
|
|
1850
|
+
"cdx:mcp:description",
|
|
1851
|
+
primitive.description ||
|
|
1852
|
+
`${primitive.role} exposed by ${serviceInfo.name || "mcp-server"}`,
|
|
1853
|
+
) || "",
|
|
1854
|
+
),
|
|
1834
1855
|
name: primitiveName,
|
|
1835
1856
|
properties,
|
|
1836
1857
|
scope: "required",
|
|
@@ -2056,8 +2077,16 @@ const serviceObjectForMcp = (serviceInfo) => {
|
|
|
2056
2077
|
return {
|
|
2057
2078
|
"bom-ref": serviceRef,
|
|
2058
2079
|
authenticated: serviceInfo.authenticated,
|
|
2059
|
-
description:
|
|
2060
|
-
|
|
2080
|
+
description: String(
|
|
2081
|
+
sanitizeBomPropertyValue(
|
|
2082
|
+
"cdx:mcp:description",
|
|
2083
|
+
serviceInfo.description || "",
|
|
2084
|
+
) || "",
|
|
2085
|
+
),
|
|
2086
|
+
endpoints: Array.from(serviceInfo.endpoints)
|
|
2087
|
+
.map((endpoint) => sanitizeBomUrl(endpoint))
|
|
2088
|
+
.filter(Boolean)
|
|
2089
|
+
.sort(),
|
|
2061
2090
|
group: "mcp",
|
|
2062
2091
|
name: serviceName,
|
|
2063
2092
|
properties,
|
|
@@ -2066,6 +2095,380 @@ const serviceObjectForMcp = (serviceInfo) => {
|
|
|
2066
2095
|
};
|
|
2067
2096
|
};
|
|
2068
2097
|
|
|
2098
|
+
const buildMcpInventoryFromServices = (servicesByKey) => {
|
|
2099
|
+
const services = [];
|
|
2100
|
+
const components = [];
|
|
2101
|
+
const dependencies = [];
|
|
2102
|
+
for (const serviceInfo of servicesByKey.values()) {
|
|
2103
|
+
if (
|
|
2104
|
+
!serviceInfo.primitives.length &&
|
|
2105
|
+
!serviceInfo.transports.size &&
|
|
2106
|
+
!serviceInfo.endpoints.size &&
|
|
2107
|
+
!serviceInfo.capabilities.size &&
|
|
2108
|
+
!serviceInfo.sourceLine &&
|
|
2109
|
+
!serviceInfo.outboundHosts.size &&
|
|
2110
|
+
!serviceInfo.usageSignals.size
|
|
2111
|
+
) {
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
if (
|
|
2115
|
+
serviceInfo.endpoints.size &&
|
|
2116
|
+
!serviceInfo.transports.has("stdio") &&
|
|
2117
|
+
!serviceInfo.transports.size
|
|
2118
|
+
) {
|
|
2119
|
+
serviceInfo.transports.add("streamable-http");
|
|
2120
|
+
}
|
|
2121
|
+
if (
|
|
2122
|
+
serviceInfo.transports.has("streamable-http") &&
|
|
2123
|
+
typeof serviceInfo.authenticated === "undefined"
|
|
2124
|
+
) {
|
|
2125
|
+
serviceInfo.authenticated = false;
|
|
2126
|
+
}
|
|
2127
|
+
const service = serviceObjectForMcp(serviceInfo);
|
|
2128
|
+
services.push(service);
|
|
2129
|
+
const providedRefs = [];
|
|
2130
|
+
for (const primitive of serviceInfo.primitives) {
|
|
2131
|
+
const component = primitiveComponentForMcp(serviceInfo, primitive);
|
|
2132
|
+
components.push(component);
|
|
2133
|
+
providedRefs.push(component["bom-ref"]);
|
|
2134
|
+
}
|
|
2135
|
+
if (providedRefs.length) {
|
|
2136
|
+
dependencies.push({
|
|
2137
|
+
ref: service["bom-ref"],
|
|
2138
|
+
dependsOn: [],
|
|
2139
|
+
provides: providedRefs.sort(),
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return { components, dependencies, services };
|
|
2144
|
+
};
|
|
2145
|
+
|
|
2146
|
+
// Capture groups:
|
|
2147
|
+
// 1 = module path in `from x import y`
|
|
2148
|
+
// 2 = imported symbols in `from x import y`
|
|
2149
|
+
// 3 = imported modules in `import x, y`
|
|
2150
|
+
const PYTHON_IMPORT_PATTERN =
|
|
2151
|
+
/^\s*(?:from\s+([a-zA-Z0-9_.]+)\s+import\s+([^\n#]+)|import\s+([^\n#]+))/gmu;
|
|
2152
|
+
const PYTHON_DECORATOR_PATTERN = /@([a-zA-Z_][a-zA-Z0-9_]*)\.(\w+)\s*\(/gmu;
|
|
2153
|
+
const PYTHON_STDIO_PATTERN = /\bstdio_server\s*\(/u;
|
|
2154
|
+
const PYTHON_HTTP_TRANSPORT_PATTERN = /\b(streamable|sse|http)\b/iu;
|
|
2155
|
+
|
|
2156
|
+
const PYTHON_DECORATOR_ROLE_MAP = new Map([
|
|
2157
|
+
["call_tool", "tool"],
|
|
2158
|
+
["get_prompt", "prompt"],
|
|
2159
|
+
["list_prompts", "prompt"],
|
|
2160
|
+
["list_resources", "resource"],
|
|
2161
|
+
["list_tools", "tool"],
|
|
2162
|
+
["prompt", "prompt"],
|
|
2163
|
+
["read_resource", "resource"],
|
|
2164
|
+
["resource", "resource"],
|
|
2165
|
+
["resource_template", "resource-template"],
|
|
2166
|
+
["tool", "tool"],
|
|
2167
|
+
]);
|
|
2168
|
+
|
|
2169
|
+
const lineNumberForIndex = (text, index) =>
|
|
2170
|
+
text.slice(0, index).split("\n").length || 1;
|
|
2171
|
+
|
|
2172
|
+
const extractPythonNamedString = (argumentText, key) => {
|
|
2173
|
+
const directPattern = new RegExp(`${key}\\s*=\\s*["']([^"'\\n]+)["']`, "u");
|
|
2174
|
+
const directMatch = argumentText.match(directPattern);
|
|
2175
|
+
if (directMatch?.[1]) {
|
|
2176
|
+
return directMatch[1];
|
|
2177
|
+
}
|
|
2178
|
+
const wrappedPattern = new RegExp(
|
|
2179
|
+
`${key}\\s*=\\s*[a-zA-Z_][a-zA-Z0-9_.]*\\(\\s*["']([^"'\\n]+)["']`,
|
|
2180
|
+
"u",
|
|
2181
|
+
);
|
|
2182
|
+
return argumentText.match(wrappedPattern)?.[1];
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
const extractFirstPythonString = (argumentText) =>
|
|
2186
|
+
argumentText.match(/^\s*["']([^"'\n]+)["']/u)?.[1];
|
|
2187
|
+
|
|
2188
|
+
const extractPythonCallArguments = (raw, alias) => {
|
|
2189
|
+
const aliasPattern = new RegExp(
|
|
2190
|
+
`(\\w+)\\s*=\\s*${alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`,
|
|
2191
|
+
"gmu",
|
|
2192
|
+
);
|
|
2193
|
+
const calls = [];
|
|
2194
|
+
for (const match of raw.matchAll(aliasPattern)) {
|
|
2195
|
+
let callStart = -1;
|
|
2196
|
+
for (let index = match.index; index < raw.length; index++) {
|
|
2197
|
+
if (raw[index] === "(") {
|
|
2198
|
+
callStart = index;
|
|
2199
|
+
break;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
if (callStart === -1) {
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
let depth = 0;
|
|
2206
|
+
let callEnd = -1;
|
|
2207
|
+
for (let index = callStart; index < raw.length; index++) {
|
|
2208
|
+
if (raw[index] === "(") {
|
|
2209
|
+
depth += 1;
|
|
2210
|
+
} else if (raw[index] === ")") {
|
|
2211
|
+
depth -= 1;
|
|
2212
|
+
if (depth === 0) {
|
|
2213
|
+
callEnd = index;
|
|
2214
|
+
break;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
if (callEnd === -1) {
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
calls.push({
|
|
2222
|
+
argumentText: raw.slice(callStart + 1, callEnd),
|
|
2223
|
+
index: match.index,
|
|
2224
|
+
serviceVarName: match[1],
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
return calls;
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
const extractPythonFunctionCalls = (raw, callName) => {
|
|
2231
|
+
const callPattern = new RegExp(
|
|
2232
|
+
`${callName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`,
|
|
2233
|
+
"gmu",
|
|
2234
|
+
);
|
|
2235
|
+
const calls = [];
|
|
2236
|
+
for (const match of raw.matchAll(callPattern)) {
|
|
2237
|
+
const callStart = match.index + match[0].lastIndexOf("(");
|
|
2238
|
+
let depth = 0;
|
|
2239
|
+
let callEnd = -1;
|
|
2240
|
+
for (let index = callStart; index < raw.length; index++) {
|
|
2241
|
+
if (raw[index] === "(") {
|
|
2242
|
+
depth += 1;
|
|
2243
|
+
} else if (raw[index] === ")") {
|
|
2244
|
+
depth -= 1;
|
|
2245
|
+
if (depth === 0) {
|
|
2246
|
+
callEnd = index;
|
|
2247
|
+
break;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
if (callEnd === -1) {
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
calls.push({
|
|
2255
|
+
argumentText: raw.slice(callStart + 1, callEnd),
|
|
2256
|
+
index: match.index,
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
return calls;
|
|
2260
|
+
};
|
|
2261
|
+
|
|
2262
|
+
const parsePythonImports = (raw) => {
|
|
2263
|
+
const imports = [];
|
|
2264
|
+
for (const match of raw.matchAll(PYTHON_IMPORT_PATTERN)) {
|
|
2265
|
+
const fromSource = match[1];
|
|
2266
|
+
const fromImports = match[2];
|
|
2267
|
+
const directImports = match[3];
|
|
2268
|
+
if (fromSource && fromImports) {
|
|
2269
|
+
for (const importEntry of fromImports.split(",")) {
|
|
2270
|
+
const [importedName, localName] = importEntry
|
|
2271
|
+
.trim()
|
|
2272
|
+
.split(/\s+as\s+/u)
|
|
2273
|
+
.map((value) => value?.trim());
|
|
2274
|
+
if (importedName) {
|
|
2275
|
+
imports.push({
|
|
2276
|
+
importedName,
|
|
2277
|
+
localName: localName || importedName,
|
|
2278
|
+
sourceValue: fromSource,
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
for (const importEntry of (directImports || "").split(",")) {
|
|
2285
|
+
const [sourceValue, localName] = importEntry
|
|
2286
|
+
.trim()
|
|
2287
|
+
.split(/\s+as\s+/u)
|
|
2288
|
+
.map((value) => value?.trim());
|
|
2289
|
+
if (sourceValue) {
|
|
2290
|
+
imports.push({
|
|
2291
|
+
importedName: sourceValue.split(".").pop(),
|
|
2292
|
+
localName: localName || sourceValue.split(".").pop(),
|
|
2293
|
+
sourceValue,
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
return imports;
|
|
2299
|
+
};
|
|
2300
|
+
|
|
2301
|
+
const registerPythonPrimitive = (
|
|
2302
|
+
serviceInfo,
|
|
2303
|
+
role,
|
|
2304
|
+
name,
|
|
2305
|
+
description,
|
|
2306
|
+
uri,
|
|
2307
|
+
sourceLine,
|
|
2308
|
+
) => {
|
|
2309
|
+
if (!role) {
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
const primitiveName =
|
|
2313
|
+
name ||
|
|
2314
|
+
uri ||
|
|
2315
|
+
`${role}-${serviceInfo.primitives.filter((item) => item.role === role).length + 1}`;
|
|
2316
|
+
serviceInfo.primitives.push({
|
|
2317
|
+
description,
|
|
2318
|
+
name: primitiveName,
|
|
2319
|
+
role,
|
|
2320
|
+
sourceLine,
|
|
2321
|
+
uri,
|
|
2322
|
+
});
|
|
2323
|
+
if (role === "tool") {
|
|
2324
|
+
serviceInfo.usageSignals.add("registered-tool");
|
|
2325
|
+
}
|
|
2326
|
+
if (["resource", "resource-template"].includes(role)) {
|
|
2327
|
+
serviceInfo.usageSignals.add("registered-resource");
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* Detect MCP server inventory from Python source using import and decorator heuristics.
|
|
2333
|
+
*
|
|
2334
|
+
* @param {string} src Absolute or relative path to the project source directory
|
|
2335
|
+
* @param {boolean} deep When true, also scans nested paths more aggressively
|
|
2336
|
+
* @returns {{components: Object[], dependencies: Object[], services: Object[]}}
|
|
2337
|
+
*/
|
|
2338
|
+
export const detectPythonMcpInventory = (src, deep = false) => {
|
|
2339
|
+
const servicesByKey = new Map();
|
|
2340
|
+
let srcFiles = [];
|
|
2341
|
+
try {
|
|
2342
|
+
srcFiles = getAllFiles(deep, src, ".py");
|
|
2343
|
+
} catch {
|
|
2344
|
+
return { components: [], dependencies: [], services: [] };
|
|
2345
|
+
}
|
|
2346
|
+
for (const file of srcFiles) {
|
|
2347
|
+
let raw;
|
|
2348
|
+
try {
|
|
2349
|
+
raw = readFileSync(file, "utf-8");
|
|
2350
|
+
} catch {
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
const fileRelativeLoc = relative(src, file);
|
|
2354
|
+
const serverConstructorAliases = new Set(["Server", "FastMCP"]);
|
|
2355
|
+
const importEntries = parsePythonImports(raw);
|
|
2356
|
+
let fileHasMcpImports = false;
|
|
2357
|
+
for (const importEntry of importEntries) {
|
|
2358
|
+
const sourceValue = importEntry.sourceValue;
|
|
2359
|
+
const classification = classifyMcpReference(sourceValue);
|
|
2360
|
+
if (!classification.isMcp && !sourceValue.startsWith("mcp")) {
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
fileHasMcpImports = true;
|
|
2364
|
+
const serviceInfo = ensureMcpService(
|
|
2365
|
+
servicesByKey,
|
|
2366
|
+
file,
|
|
2367
|
+
fileRelativeLoc,
|
|
2368
|
+
);
|
|
2369
|
+
if (sourceValue.startsWith("mcp")) {
|
|
2370
|
+
serviceInfo.sdkImports.add(sourceValue);
|
|
2371
|
+
serviceInfo.usageSignals.add("mcp-sdk-import");
|
|
2372
|
+
serviceInfo.officialSdk = true;
|
|
2373
|
+
} else {
|
|
2374
|
+
recordMcpSdkImport(serviceInfo, sourceValue);
|
|
2375
|
+
}
|
|
2376
|
+
if (
|
|
2377
|
+
sourceValue.startsWith("mcp.server") ||
|
|
2378
|
+
sourceValue === "fastmcp" ||
|
|
2379
|
+
/server/i.test(importEntry.importedName || "") ||
|
|
2380
|
+
/server/i.test(importEntry.localName || "")
|
|
2381
|
+
) {
|
|
2382
|
+
serverConstructorAliases.add(importEntry.localName);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
for (const alias of serverConstructorAliases) {
|
|
2386
|
+
for (const match of extractPythonCallArguments(raw, alias)) {
|
|
2387
|
+
const serviceVarName = match.serviceVarName;
|
|
2388
|
+
const argumentText = match.argumentText || "";
|
|
2389
|
+
const serviceInfo = ensureMcpService(
|
|
2390
|
+
servicesByKey,
|
|
2391
|
+
file,
|
|
2392
|
+
fileRelativeLoc,
|
|
2393
|
+
);
|
|
2394
|
+
serviceInfo.name =
|
|
2395
|
+
extractPythonNamedString(argumentText, "name") ||
|
|
2396
|
+
extractFirstPythonString(argumentText) ||
|
|
2397
|
+
serviceInfo.name;
|
|
2398
|
+
serviceInfo.version =
|
|
2399
|
+
extractPythonNamedString(argumentText, "version") ||
|
|
2400
|
+
serviceInfo.version;
|
|
2401
|
+
serviceInfo.description =
|
|
2402
|
+
extractPythonNamedString(argumentText, "instructions") ||
|
|
2403
|
+
extractPythonNamedString(argumentText, "description") ||
|
|
2404
|
+
serviceInfo.description;
|
|
2405
|
+
serviceInfo.serviceKinds.add("server");
|
|
2406
|
+
serviceInfo.usageSignals.add("server-constructor");
|
|
2407
|
+
serviceInfo.sourceLine = lineNumberForIndex(raw, match.index);
|
|
2408
|
+
for (const decoratorMatch of raw.matchAll(PYTHON_DECORATOR_PATTERN)) {
|
|
2409
|
+
if (decoratorMatch[1] !== serviceVarName) {
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
const primitiveRole = PYTHON_DECORATOR_ROLE_MAP.get(
|
|
2413
|
+
decoratorMatch[2],
|
|
2414
|
+
);
|
|
2415
|
+
if (!primitiveRole) {
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
if (primitiveRole === "tool") {
|
|
2419
|
+
serviceInfo.capabilities.add("tools");
|
|
2420
|
+
} else if (primitiveRole === "prompt") {
|
|
2421
|
+
serviceInfo.capabilities.add("prompts");
|
|
2422
|
+
} else {
|
|
2423
|
+
serviceInfo.capabilities.add("resources");
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
if (PYTHON_STDIO_PATTERN.test(raw)) {
|
|
2429
|
+
const serviceInfo = ensureMcpService(
|
|
2430
|
+
servicesByKey,
|
|
2431
|
+
file,
|
|
2432
|
+
fileRelativeLoc,
|
|
2433
|
+
);
|
|
2434
|
+
serviceInfo.transports.add("stdio");
|
|
2435
|
+
} else if (fileHasMcpImports && PYTHON_HTTP_TRANSPORT_PATTERN.test(raw)) {
|
|
2436
|
+
const serviceInfo = ensureMcpService(
|
|
2437
|
+
servicesByKey,
|
|
2438
|
+
file,
|
|
2439
|
+
fileRelativeLoc,
|
|
2440
|
+
);
|
|
2441
|
+
serviceInfo.transports.add("streamable-http");
|
|
2442
|
+
}
|
|
2443
|
+
const primitivePatterns = [
|
|
2444
|
+
["mtypes.Tool", "tool"],
|
|
2445
|
+
["mtypes.Prompt", "prompt"],
|
|
2446
|
+
["mtypes.Resource", "resource"],
|
|
2447
|
+
];
|
|
2448
|
+
for (const [callName, role] of primitivePatterns) {
|
|
2449
|
+
for (const match of extractPythonFunctionCalls(raw, callName)) {
|
|
2450
|
+
const serviceInfo = ensureMcpService(
|
|
2451
|
+
servicesByKey,
|
|
2452
|
+
file,
|
|
2453
|
+
fileRelativeLoc,
|
|
2454
|
+
);
|
|
2455
|
+
registerPythonPrimitive(
|
|
2456
|
+
serviceInfo,
|
|
2457
|
+
role,
|
|
2458
|
+
extractPythonNamedString(match.argumentText || "", "name"),
|
|
2459
|
+
extractPythonNamedString(match.argumentText || "", "description"),
|
|
2460
|
+
extractPythonNamedString(match.argumentText || "", "uri"),
|
|
2461
|
+
lineNumberForIndex(raw, match.index),
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
if (fileHasMcpImports) {
|
|
2466
|
+
ensureMcpService(servicesByKey, file, fileRelativeLoc);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
return buildMcpInventoryFromServices(servicesByKey);
|
|
2470
|
+
};
|
|
2471
|
+
|
|
2069
2472
|
/**
|
|
2070
2473
|
* Detect MCP server inventory from JavaScript/TypeScript source using AST analysis.
|
|
2071
2474
|
*
|
|
@@ -2748,49 +3151,5 @@ export const detectMcpInventory = (src, deep = false) => {
|
|
|
2748
3151
|
ensureMcpService(servicesByKey, file, fileRelativeLoc);
|
|
2749
3152
|
}
|
|
2750
3153
|
}
|
|
2751
|
-
|
|
2752
|
-
const components = [];
|
|
2753
|
-
const dependencies = [];
|
|
2754
|
-
for (const serviceInfo of servicesByKey.values()) {
|
|
2755
|
-
if (
|
|
2756
|
-
!serviceInfo.primitives.length &&
|
|
2757
|
-
!serviceInfo.transports.size &&
|
|
2758
|
-
!serviceInfo.endpoints.size &&
|
|
2759
|
-
!serviceInfo.capabilities.size &&
|
|
2760
|
-
!serviceInfo.sourceLine &&
|
|
2761
|
-
!serviceInfo.outboundHosts.size &&
|
|
2762
|
-
!serviceInfo.usageSignals.size
|
|
2763
|
-
) {
|
|
2764
|
-
continue;
|
|
2765
|
-
}
|
|
2766
|
-
if (
|
|
2767
|
-
serviceInfo.endpoints.size &&
|
|
2768
|
-
!serviceInfo.transports.has("stdio") &&
|
|
2769
|
-
!serviceInfo.transports.size
|
|
2770
|
-
) {
|
|
2771
|
-
serviceInfo.transports.add("streamable-http");
|
|
2772
|
-
}
|
|
2773
|
-
if (
|
|
2774
|
-
serviceInfo.transports.has("streamable-http") &&
|
|
2775
|
-
typeof serviceInfo.authenticated === "undefined"
|
|
2776
|
-
) {
|
|
2777
|
-
serviceInfo.authenticated = false;
|
|
2778
|
-
}
|
|
2779
|
-
const service = serviceObjectForMcp(serviceInfo);
|
|
2780
|
-
services.push(service);
|
|
2781
|
-
const providedRefs = [];
|
|
2782
|
-
for (const primitive of serviceInfo.primitives) {
|
|
2783
|
-
const component = primitiveComponentForMcp(serviceInfo, primitive);
|
|
2784
|
-
components.push(component);
|
|
2785
|
-
providedRefs.push(component["bom-ref"]);
|
|
2786
|
-
}
|
|
2787
|
-
if (providedRefs.length) {
|
|
2788
|
-
dependencies.push({
|
|
2789
|
-
ref: service["bom-ref"],
|
|
2790
|
-
dependsOn: [],
|
|
2791
|
-
provides: providedRefs.sort(),
|
|
2792
|
-
});
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
return { components, dependencies, services };
|
|
3154
|
+
return buildMcpInventoryFromServices(servicesByKey);
|
|
2796
3155
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
|
+
import { URL } from "node:url";
|
|
10
11
|
|
|
11
12
|
import { assert, describe, it } from "poku";
|
|
12
13
|
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
analyzeSuspiciousJsFile,
|
|
15
16
|
detectExtensionCapabilities,
|
|
16
17
|
detectMcpInventory,
|
|
18
|
+
detectPythonMcpInventory,
|
|
17
19
|
findJSImportsExports,
|
|
18
20
|
} from "./analyzer.js";
|
|
19
21
|
|
|
@@ -529,4 +531,119 @@ describe("detectMcpInventory()", () => {
|
|
|
529
531
|
),
|
|
530
532
|
);
|
|
531
533
|
});
|
|
534
|
+
|
|
535
|
+
it("sanitizes source-code-analysis MCP metadata before emission", () => {
|
|
536
|
+
const projectDir = createProjectFiles("mcp-sanitized-source-analysis", {
|
|
537
|
+
"src/server.ts": [
|
|
538
|
+
"import { McpServer } from '@modelcontextprotocol/server';",
|
|
539
|
+
"import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client';",
|
|
540
|
+
"const server = new McpServer({",
|
|
541
|
+
" name: 'sanitized-server',",
|
|
542
|
+
" version: '0.3.0',",
|
|
543
|
+
" description: 'Use https://user:pass@example.com/mcp?token=abc#frag and Bearer sk_test_super_secret_value',",
|
|
544
|
+
"});",
|
|
545
|
+
"server.registerTool(",
|
|
546
|
+
" 'download',",
|
|
547
|
+
" {",
|
|
548
|
+
" description: 'Download from https://user:pass@example.com/tool?token=abc#frag',",
|
|
549
|
+
" annotations: {",
|
|
550
|
+
" Authorization: 'Bearer sk_test_super_secret_value',",
|
|
551
|
+
" nested: { __proto__: 'polluted', endpoint: 'https://user:pass@example.com/tool?token=abc#frag' },",
|
|
552
|
+
" },",
|
|
553
|
+
" },",
|
|
554
|
+
" async () => ({ content: [] }),",
|
|
555
|
+
");",
|
|
556
|
+
"server.registerResource(",
|
|
557
|
+
" 'private-docs',",
|
|
558
|
+
" 'https://user:pass@example.com/docs?token=abc#frag',",
|
|
559
|
+
" { description: 'Private docs' },",
|
|
560
|
+
" async () => ({ contents: [] }),",
|
|
561
|
+
");",
|
|
562
|
+
"const transport = new StreamableHTTPClientTransport(new URL('https://user:pass@example.com/mcp?access_token=secret#frag'));",
|
|
563
|
+
"void transport;",
|
|
564
|
+
].join("\n"),
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const inventory = detectMcpInventory(projectDir);
|
|
568
|
+
const service = inventory.services[0];
|
|
569
|
+
const toolComponent = inventory.components.find(
|
|
570
|
+
(component) => component.name === "download",
|
|
571
|
+
);
|
|
572
|
+
const resourceComponent = inventory.components.find(
|
|
573
|
+
(component) => component.name === "private-docs",
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
assert.strictEqual(
|
|
577
|
+
service.description,
|
|
578
|
+
"Use https://example.com/mcp and [redacted]",
|
|
579
|
+
);
|
|
580
|
+
const serviceEndpoint = new URL(service.endpoints[0]);
|
|
581
|
+
assert.strictEqual(serviceEndpoint.hostname, "example.com");
|
|
582
|
+
assert.strictEqual(serviceEndpoint.pathname, "/mcp");
|
|
583
|
+
assert.strictEqual(
|
|
584
|
+
getProp(resourceComponent, "cdx:mcp:resourceUri"),
|
|
585
|
+
"https://example.com/docs",
|
|
586
|
+
);
|
|
587
|
+
assert.strictEqual(
|
|
588
|
+
toolComponent.description,
|
|
589
|
+
"Download from https://example.com/tool",
|
|
590
|
+
);
|
|
591
|
+
const toolAnnotations = JSON.parse(
|
|
592
|
+
getProp(toolComponent, "cdx:mcp:toolAnnotations"),
|
|
593
|
+
);
|
|
594
|
+
assert.strictEqual(toolAnnotations.Authorization, "[redacted]");
|
|
595
|
+
assert.ok(
|
|
596
|
+
!JSON.stringify(toolAnnotations).includes("sk_test_super_secret_value"),
|
|
597
|
+
);
|
|
598
|
+
assert.ok(!JSON.stringify(toolAnnotations).includes("__proto__"));
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
describe("detectPythonMcpInventory()", () => {
|
|
603
|
+
it("detects a Python stdio MCP server and exported primitives", () => {
|
|
604
|
+
const projectDir = createProjectFiles("mcp-python-server", {
|
|
605
|
+
"src/server.py": [
|
|
606
|
+
"import mcp.server.stdio",
|
|
607
|
+
"import mcp.types as mtypes",
|
|
608
|
+
"from mcp.server import NotificationOptions, Server",
|
|
609
|
+
"",
|
|
610
|
+
'server = Server("appthreat-vulnerability-db", version="1.0.1")',
|
|
611
|
+
"",
|
|
612
|
+
"@server.list_resources()",
|
|
613
|
+
"async def handle_list_resources():",
|
|
614
|
+
' return [mtypes.Resource(uri=mtypes.AnyUrl("cve://"), name="CVE Information", description="Get detailed information about a CVE")]',
|
|
615
|
+
"",
|
|
616
|
+
"@server.list_tools()",
|
|
617
|
+
"async def handle_list_tools():",
|
|
618
|
+
' return [mtypes.Tool(name="search_by_purl_like", description="Search by purl", inputSchema={"type": "object"})]',
|
|
619
|
+
"",
|
|
620
|
+
"async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):",
|
|
621
|
+
" await server.run(",
|
|
622
|
+
" read_stream,",
|
|
623
|
+
" write_stream,",
|
|
624
|
+
' InitializationOptions(server_name="appthreat-vulnerability-db", server_version="1.0.1", capabilities=server.get_capabilities(notification_options=NotificationOptions(), experimental_capabilities={}))',
|
|
625
|
+
" )",
|
|
626
|
+
].join("\n"),
|
|
627
|
+
});
|
|
628
|
+
const inventory = detectPythonMcpInventory(projectDir);
|
|
629
|
+
assert.strictEqual(inventory.services.length, 1);
|
|
630
|
+
assert.strictEqual(inventory.components.length, 2);
|
|
631
|
+
const service = inventory.services[0];
|
|
632
|
+
assert.strictEqual(service.name, "appthreat-vulnerability-db");
|
|
633
|
+
assert.strictEqual(service.version, "1.0.1");
|
|
634
|
+
assert.strictEqual(getProp(service, "cdx:mcp:transport"), "stdio");
|
|
635
|
+
assert.strictEqual(getProp(service, "cdx:mcp:officialSdk"), "true");
|
|
636
|
+
assert.strictEqual(getProp(service, "cdx:mcp:toolCount"), "1");
|
|
637
|
+
assert.strictEqual(getProp(service, "cdx:mcp:resourceCount"), "1");
|
|
638
|
+
assert.ok(
|
|
639
|
+
inventory.components.some(
|
|
640
|
+
(component) => component.name === "search_by_purl_like",
|
|
641
|
+
),
|
|
642
|
+
);
|
|
643
|
+
assert.ok(
|
|
644
|
+
inventory.components.some(
|
|
645
|
+
(component) => component.name === "CVE Information",
|
|
646
|
+
),
|
|
647
|
+
);
|
|
648
|
+
});
|
|
532
649
|
});
|