@weave_protocol/mund 0.1.9 → 0.1.11
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/dist/analyzers/index.d.ts +1 -0
- package/dist/analyzers/index.d.ts.map +1 -1
- package/dist/analyzers/index.js +4 -1
- package/dist/analyzers/index.js.map +1 -1
- package/dist/analyzers/mcp-server-analyzer.d.ts +146 -0
- package/dist/analyzers/mcp-server-analyzer.d.ts.map +1 -0
- package/dist/analyzers/mcp-server-analyzer.js +634 -0
- package/dist/analyzers/mcp-server-analyzer.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp-scanning.d.ts +16 -0
- package/dist/tools/mcp-scanning.d.ts.map +1 -0
- package/dist/tools/mcp-scanning.js +325 -0
- package/dist/tools/mcp-scanning.js.map +1 -0
- package/package.json +2 -13
- package/src/analyzers/index.ts +4 -1
- package/src/analyzers/mcp-server-analyzer.ts +788 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/mcp-scanning.ts +382 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mund - The Guardian Protocol
|
|
3
|
+
* MCP Server Analyzer - Scans MCP server manifests for security issues
|
|
4
|
+
*
|
|
5
|
+
* Detects:
|
|
6
|
+
* - Prompt injection in tool descriptions ("line jumping" attacks)
|
|
7
|
+
* - Hidden Unicode characters
|
|
8
|
+
* - Dangerous permission patterns
|
|
9
|
+
* - Embedded secrets
|
|
10
|
+
* - Typosquatting of legitimate server names
|
|
11
|
+
*/
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Constants
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/** Known legitimate MCP server names for typosquatting detection */
|
|
16
|
+
const LEGITIMATE_SERVERS = [
|
|
17
|
+
// Official/common servers
|
|
18
|
+
'filesystem', 'github', 'gitlab', 'slack', 'notion', 'linear',
|
|
19
|
+
'postgres', 'postgresql', 'sqlite', 'mysql', 'redis', 'mongodb',
|
|
20
|
+
'elasticsearch', 'google-drive', 'dropbox', 'aws', 'azure', 'gcp',
|
|
21
|
+
'docker', 'kubernetes', 'terraform', 'ansible', 'jenkins',
|
|
22
|
+
'jira', 'confluence', 'asana', 'trello', 'monday',
|
|
23
|
+
'stripe', 'twilio', 'sendgrid', 'mailchimp',
|
|
24
|
+
'openai', 'anthropic', 'huggingface', 'replicate',
|
|
25
|
+
'vercel', 'netlify', 'heroku', 'railway',
|
|
26
|
+
'supabase', 'firebase', 'planetscale', 'neon',
|
|
27
|
+
'brave-search', 'puppeteer', 'playwright', 'selenium',
|
|
28
|
+
'fetch', 'memory', 'time', 'sequential-thinking',
|
|
29
|
+
// Weave Protocol
|
|
30
|
+
'mund', 'hord', 'domere', 'witan'
|
|
31
|
+
];
|
|
32
|
+
/** Injection patterns commonly found in "line jumping" attacks */
|
|
33
|
+
const INJECTION_PATTERNS = [
|
|
34
|
+
// Direct instruction override
|
|
35
|
+
{ pattern: /ignore\s+(previous|all|prior|above|earlier)\s+(instructions?|prompts?|rules?|guidelines?)/i, name: 'Instruction Override' },
|
|
36
|
+
{ pattern: /disregard\s+(your|the|all|any)\s+(rules?|guidelines?|instructions?|constraints?)/i, name: 'Rule Disregard' },
|
|
37
|
+
{ pattern: /forget\s+(everything|all|your)\s+(previous|prior|above)?/i, name: 'Memory Wipe' },
|
|
38
|
+
// Role manipulation
|
|
39
|
+
{ pattern: /you\s+are\s+now\s+(a|an|the)?\s*[a-z]+/i, name: 'Role Reassignment' },
|
|
40
|
+
{ pattern: /act\s+as\s+(if\s+you\s+are\s+)?(a|an|the)?\s*[a-z]+/i, name: 'Role Acting' },
|
|
41
|
+
{ pattern: /pretend\s+(to\s+be|you\s+are)/i, name: 'Role Pretend' },
|
|
42
|
+
{ pattern: /from\s+now\s+on,?\s+you/i, name: 'Behavior Override' },
|
|
43
|
+
// System prompt extraction
|
|
44
|
+
{ pattern: /reveal\s+(your|the)\s+(system|initial)\s*(prompt|instructions?)/i, name: 'Prompt Extraction' },
|
|
45
|
+
{ pattern: /what\s+(is|are)\s+your\s+(system|initial|original)\s*(prompt|instructions?)/i, name: 'Prompt Query' },
|
|
46
|
+
{ pattern: /show\s+me\s+(your|the)\s+(system|hidden)/i, name: 'System Reveal' },
|
|
47
|
+
// Markup injection
|
|
48
|
+
{ pattern: /\[SYSTEM\]/i, name: 'System Tag Injection' },
|
|
49
|
+
{ pattern: /<\|?(system|assistant|user)\|?>/i, name: 'Role Tag Injection' },
|
|
50
|
+
{ pattern: /```\s*(system|python.*exec|bash|sh)\s/i, name: 'Code Block Injection' },
|
|
51
|
+
{ pattern: /\{\{system/i, name: 'Template Injection' },
|
|
52
|
+
// Jailbreak patterns
|
|
53
|
+
{ pattern: /DAN\s*mode/i, name: 'DAN Jailbreak' },
|
|
54
|
+
{ pattern: /developer\s+mode\s+(enabled|on|active)/i, name: 'Developer Mode Jailbreak' },
|
|
55
|
+
{ pattern: /bypass\s+(your|the|all)?\s*(safety|security|filter|restriction)/i, name: 'Safety Bypass' },
|
|
56
|
+
{ pattern: /override\s+(safety|security|content)\s*(filter|policy|check)/i, name: 'Filter Override' },
|
|
57
|
+
// Hypothetical framing (common evasion)
|
|
58
|
+
{ pattern: /hypothetically,?\s+(if|what\s+if)\s+you\s+(could|were|had)/i, name: 'Hypothetical Evasion' },
|
|
59
|
+
{ pattern: /in\s+a\s+fictional\s+(world|scenario|story)/i, name: 'Fiction Framing' },
|
|
60
|
+
];
|
|
61
|
+
/** Patterns indicating dangerous capabilities */
|
|
62
|
+
const DANGEROUS_CAPABILITY_PATTERNS = [
|
|
63
|
+
// Command execution
|
|
64
|
+
{ pattern: /\b(exec|execute|shell|command|bash|sh|cmd|spawn|fork)\b/i, capability: 'execution', risk: 'Command execution' },
|
|
65
|
+
{ pattern: /\b(child_process|subprocess|system\(|popen)\b/i, capability: 'execution', risk: 'Process spawning' },
|
|
66
|
+
// Network access
|
|
67
|
+
{ pattern: /\b(fetch|request|http|https|curl|wget|axios|got)\b/i, capability: 'network', risk: 'Network requests' },
|
|
68
|
+
{ pattern: /\b(socket|websocket|tcp|udp|net\.connect)\b/i, capability: 'network', risk: 'Raw socket access' },
|
|
69
|
+
{ pattern: /\b(dns|resolve|lookup)\b/i, capability: 'network', risk: 'DNS operations' },
|
|
70
|
+
// Filesystem
|
|
71
|
+
{ pattern: /\b(file|read|write|unlink|rmdir|mkdir|chmod|chown)\b/i, capability: 'filesystem', risk: 'Filesystem access' },
|
|
72
|
+
{ pattern: /\b(path|directory|folder|fs\.|fopen|fwrite)\b/i, capability: 'filesystem', risk: 'Path operations' },
|
|
73
|
+
// Code evaluation
|
|
74
|
+
{ pattern: /\b(eval|Function\(|vm\.run|new\s+Function)\b/i, capability: 'execution', risk: 'Code evaluation' },
|
|
75
|
+
{ pattern: /\b(require|import|__import__|importlib)\b/i, capability: 'execution', risk: 'Dynamic imports' },
|
|
76
|
+
// Environment/secrets
|
|
77
|
+
{ pattern: /\b(env|environment|process\.env|os\.environ|getenv)\b/i, capability: 'environment', risk: 'Environment access' },
|
|
78
|
+
{ pattern: /\b(secret|credential|password|token|apikey|api_key)\b/i, capability: 'environment', risk: 'Credential access' },
|
|
79
|
+
// Database
|
|
80
|
+
{ pattern: /\b(sql|query|database|db|postgres|mysql|mongo|redis)\b/i, capability: 'database', risk: 'Database operations' },
|
|
81
|
+
{ pattern: /\b(select|insert|update|delete|drop|truncate)\b/i, capability: 'database', risk: 'SQL operations' },
|
|
82
|
+
// Crypto (not inherently bad, but noteworthy)
|
|
83
|
+
{ pattern: /\b(crypto|encrypt|decrypt|hash|sign|verify|cipher)\b/i, capability: 'crypto', risk: 'Cryptographic operations' },
|
|
84
|
+
];
|
|
85
|
+
/** Secret patterns to detect in manifests */
|
|
86
|
+
const SECRET_PATTERNS = [
|
|
87
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/, name: 'OpenAI API Key' },
|
|
88
|
+
{ pattern: /sk-ant-[a-zA-Z0-9-]{90,}/, name: 'Anthropic API Key' },
|
|
89
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/, name: 'GitHub Personal Access Token' },
|
|
90
|
+
{ pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/, name: 'GitHub Fine-Grained Token' },
|
|
91
|
+
{ pattern: /gho_[a-zA-Z0-9]{36}/, name: 'GitHub OAuth Token' },
|
|
92
|
+
{ pattern: /xox[baprs]-[a-zA-Z0-9-]{10,}/, name: 'Slack Token' },
|
|
93
|
+
{ pattern: /https:\/\/hooks\.slack\.com\/services\/[A-Z0-9/]+/, name: 'Slack Webhook' },
|
|
94
|
+
{ pattern: /sk_live_[a-zA-Z0-9]{24,}/, name: 'Stripe Live Key' },
|
|
95
|
+
{ pattern: /sk_test_[a-zA-Z0-9]{24,}/, name: 'Stripe Test Key' },
|
|
96
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, name: 'AWS Access Key ID' },
|
|
97
|
+
{ pattern: /[a-zA-Z0-9/+=]{40}/, name: 'Potential AWS Secret Key', entropyCheck: true },
|
|
98
|
+
{ pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/, name: 'JWT Token' },
|
|
99
|
+
{ pattern: /-----BEGIN\s+(RSA|DSA|EC|OPENSSH|PGP)?\s*PRIVATE\s+KEY-----/, name: 'Private Key' },
|
|
100
|
+
{ pattern: /mongodb(\+srv)?:\/\/[^:]+:[^@]+@/, name: 'MongoDB Connection String' },
|
|
101
|
+
{ pattern: /postgres(ql)?:\/\/[^:]+:[^@]+@/, name: 'PostgreSQL Connection String' },
|
|
102
|
+
{ pattern: /mysql:\/\/[^:]+:[^@]+@/, name: 'MySQL Connection String' },
|
|
103
|
+
{ pattern: /redis:\/\/[^:]+:[^@]+@/, name: 'Redis Connection String' },
|
|
104
|
+
];
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Analyzer Implementation
|
|
107
|
+
// ============================================================================
|
|
108
|
+
export class McpServerAnalyzer {
|
|
109
|
+
name = 'McpServerAnalyzer';
|
|
110
|
+
type = 'mcp_server';
|
|
111
|
+
async analyze(content, _rules) {
|
|
112
|
+
const issues = [];
|
|
113
|
+
// Attempt to parse as JSON
|
|
114
|
+
let manifest;
|
|
115
|
+
try {
|
|
116
|
+
manifest = JSON.parse(content);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Not valid JSON - might be a file path or URL, skip analysis
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
// Validate it looks like an MCP manifest
|
|
123
|
+
if (!manifest.name && !manifest.tools && !manifest.resources) {
|
|
124
|
+
return []; // Doesn't appear to be an MCP manifest
|
|
125
|
+
}
|
|
126
|
+
// Run all security checks
|
|
127
|
+
issues.push(...this.scanToolDescriptions(manifest));
|
|
128
|
+
issues.push(...this.scanResourceDescriptions(manifest));
|
|
129
|
+
issues.push(...this.scanPromptDescriptions(manifest));
|
|
130
|
+
issues.push(...this.auditPermissions(manifest));
|
|
131
|
+
issues.push(...this.scanForSecrets(content));
|
|
132
|
+
issues.push(...this.checkTyposquatting(manifest));
|
|
133
|
+
issues.push(...this.checkMetadata(manifest));
|
|
134
|
+
return issues;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Scan tool descriptions for injection attacks
|
|
138
|
+
*/
|
|
139
|
+
scanToolDescriptions(manifest) {
|
|
140
|
+
const issues = [];
|
|
141
|
+
for (const tool of manifest.tools || []) {
|
|
142
|
+
const desc = tool.description || '';
|
|
143
|
+
const name = tool.name || '';
|
|
144
|
+
const combined = `${name} ${desc}`;
|
|
145
|
+
// Check for injection patterns
|
|
146
|
+
for (const { pattern, name: patternName } of INJECTION_PATTERNS) {
|
|
147
|
+
const match = combined.match(pattern);
|
|
148
|
+
if (match) {
|
|
149
|
+
issues.push({
|
|
150
|
+
rule_id: 'mcp_tool_injection',
|
|
151
|
+
rule_name: `Injection Pattern: ${patternName}`,
|
|
152
|
+
type: 'mcp_server',
|
|
153
|
+
severity: 'critical',
|
|
154
|
+
action: 'block',
|
|
155
|
+
match: `Tool "${tool.name}": "${this.truncate(match[0], 50)}"`,
|
|
156
|
+
location: this.createLocation(desc, match.index || 0, match[0].length),
|
|
157
|
+
suggestion: `Tool description contains "${patternName}" injection pattern. DO NOT install this server.`
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Check for hidden Unicode
|
|
162
|
+
const hiddenUnicode = this.findHiddenUnicode(desc);
|
|
163
|
+
if (hiddenUnicode.length > 0) {
|
|
164
|
+
issues.push({
|
|
165
|
+
rule_id: 'mcp_hidden_unicode',
|
|
166
|
+
rule_name: 'Hidden Unicode Characters',
|
|
167
|
+
type: 'mcp_server',
|
|
168
|
+
severity: 'high',
|
|
169
|
+
action: 'alert',
|
|
170
|
+
match: `Tool "${tool.name}" contains ${hiddenUnicode.length} hidden character(s): ${hiddenUnicode.map(c => `U+${c.charCodeAt(0).toString(16).toUpperCase()}`).join(', ')}`,
|
|
171
|
+
suggestion: 'Hidden Unicode characters may conceal malicious instructions. Inspect the raw content carefully.'
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Check for suspicious length (very long descriptions may hide content)
|
|
175
|
+
if (desc.length > 2000) {
|
|
176
|
+
issues.push({
|
|
177
|
+
rule_id: 'mcp_suspicious_length',
|
|
178
|
+
rule_name: 'Suspiciously Long Description',
|
|
179
|
+
type: 'mcp_server',
|
|
180
|
+
severity: 'medium',
|
|
181
|
+
action: 'alert',
|
|
182
|
+
match: `Tool "${tool.name}" has ${desc.length} character description`,
|
|
183
|
+
suggestion: 'Unusually long tool descriptions may hide malicious content. Review the full description carefully.'
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Check inputSchema for suspicious defaults
|
|
187
|
+
if (tool.inputSchema) {
|
|
188
|
+
const schemaIssues = this.scanInputSchema(tool.name, tool.inputSchema);
|
|
189
|
+
issues.push(...schemaIssues);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return issues;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Scan resource descriptions
|
|
196
|
+
*/
|
|
197
|
+
scanResourceDescriptions(manifest) {
|
|
198
|
+
const issues = [];
|
|
199
|
+
for (const resource of manifest.resources || []) {
|
|
200
|
+
const desc = resource.description || '';
|
|
201
|
+
// Check for injection patterns
|
|
202
|
+
for (const { pattern, name: patternName } of INJECTION_PATTERNS) {
|
|
203
|
+
if (pattern.test(desc)) {
|
|
204
|
+
issues.push({
|
|
205
|
+
rule_id: 'mcp_resource_injection',
|
|
206
|
+
rule_name: `Injection in Resource: ${patternName}`,
|
|
207
|
+
type: 'mcp_server',
|
|
208
|
+
severity: 'critical',
|
|
209
|
+
action: 'block',
|
|
210
|
+
match: `Resource "${resource.name}"`,
|
|
211
|
+
suggestion: `Resource description contains injection pattern. DO NOT install this server.`
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Check resource URI for suspicious patterns
|
|
216
|
+
if (resource.uri) {
|
|
217
|
+
if (/^(file|data|javascript):/i.test(resource.uri)) {
|
|
218
|
+
issues.push({
|
|
219
|
+
rule_id: 'mcp_suspicious_uri',
|
|
220
|
+
rule_name: 'Suspicious Resource URI',
|
|
221
|
+
type: 'mcp_server',
|
|
222
|
+
severity: 'high',
|
|
223
|
+
action: 'alert',
|
|
224
|
+
match: `Resource "${resource.name}" uses ${resource.uri.split(':')[0]}: URI scheme`,
|
|
225
|
+
suggestion: 'This URI scheme may allow access to local files or execute code.'
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return issues;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Scan prompt template descriptions
|
|
234
|
+
*/
|
|
235
|
+
scanPromptDescriptions(manifest) {
|
|
236
|
+
const issues = [];
|
|
237
|
+
for (const prompt of manifest.prompts || []) {
|
|
238
|
+
const desc = prompt.description || '';
|
|
239
|
+
for (const { pattern, name: patternName } of INJECTION_PATTERNS) {
|
|
240
|
+
if (pattern.test(desc)) {
|
|
241
|
+
issues.push({
|
|
242
|
+
rule_id: 'mcp_prompt_injection',
|
|
243
|
+
rule_name: `Injection in Prompt: ${patternName}`,
|
|
244
|
+
type: 'mcp_server',
|
|
245
|
+
severity: 'critical',
|
|
246
|
+
action: 'block',
|
|
247
|
+
match: `Prompt "${prompt.name}"`,
|
|
248
|
+
suggestion: `Prompt description contains injection pattern. DO NOT install this server.`
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return issues;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Scan input schema for suspicious default values
|
|
257
|
+
*/
|
|
258
|
+
scanInputSchema(toolName, schema) {
|
|
259
|
+
const issues = [];
|
|
260
|
+
const schemaStr = JSON.stringify(schema);
|
|
261
|
+
// Check for injection in schema defaults
|
|
262
|
+
for (const { pattern, name: patternName } of INJECTION_PATTERNS) {
|
|
263
|
+
if (pattern.test(schemaStr)) {
|
|
264
|
+
issues.push({
|
|
265
|
+
rule_id: 'mcp_schema_injection',
|
|
266
|
+
rule_name: `Injection in Schema: ${patternName}`,
|
|
267
|
+
type: 'mcp_server',
|
|
268
|
+
severity: 'critical',
|
|
269
|
+
action: 'block',
|
|
270
|
+
match: `Tool "${toolName}" input schema`,
|
|
271
|
+
suggestion: 'Input schema contains injection patterns in defaults or descriptions.'
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return issues;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Audit tool permissions and capabilities
|
|
279
|
+
*/
|
|
280
|
+
auditPermissions(manifest) {
|
|
281
|
+
const issues = [];
|
|
282
|
+
const capabilities = {
|
|
283
|
+
network: false,
|
|
284
|
+
filesystem: false,
|
|
285
|
+
execution: false,
|
|
286
|
+
environment: false,
|
|
287
|
+
database: false,
|
|
288
|
+
crypto: false
|
|
289
|
+
};
|
|
290
|
+
for (const tool of manifest.tools || []) {
|
|
291
|
+
const combined = `${tool.name} ${tool.description || ''}`.toLowerCase();
|
|
292
|
+
for (const { pattern, capability, risk } of DANGEROUS_CAPABILITY_PATTERNS) {
|
|
293
|
+
if (pattern.test(combined)) {
|
|
294
|
+
capabilities[capability] = true;
|
|
295
|
+
// Only flag execution as high severity
|
|
296
|
+
const severity = capability === 'execution' ? 'high' : 'medium';
|
|
297
|
+
issues.push({
|
|
298
|
+
rule_id: `mcp_capability_${capability}`,
|
|
299
|
+
rule_name: `Capability: ${risk}`,
|
|
300
|
+
type: 'mcp_server',
|
|
301
|
+
severity: severity,
|
|
302
|
+
action: 'alert',
|
|
303
|
+
match: `Tool "${tool.name}" may have ${risk.toLowerCase()}`,
|
|
304
|
+
suggestion: `Verify this capability is necessary and from a trusted source.`
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Flag servers with execution capability
|
|
310
|
+
if (capabilities.execution) {
|
|
311
|
+
issues.push({
|
|
312
|
+
rule_id: 'mcp_execution_warning',
|
|
313
|
+
rule_name: 'Command Execution Capability',
|
|
314
|
+
type: 'mcp_server',
|
|
315
|
+
severity: 'high',
|
|
316
|
+
action: 'alert',
|
|
317
|
+
match: `Server "${manifest.name}" can execute commands`,
|
|
318
|
+
suggestion: 'This server can execute arbitrary commands on your system. Only install from highly trusted sources.'
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
// Check for excessive tool count
|
|
322
|
+
const toolCount = manifest.tools?.length || 0;
|
|
323
|
+
if (toolCount > 25) {
|
|
324
|
+
issues.push({
|
|
325
|
+
rule_id: 'mcp_excessive_tools',
|
|
326
|
+
rule_name: 'Excessive Tool Count',
|
|
327
|
+
type: 'mcp_server',
|
|
328
|
+
severity: 'medium',
|
|
329
|
+
action: 'alert',
|
|
330
|
+
match: `${toolCount} tools defined`,
|
|
331
|
+
suggestion: 'Servers with many tools have larger attack surfaces. Consider if all tools are necessary.'
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return issues;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Scan manifest content for embedded secrets
|
|
338
|
+
*/
|
|
339
|
+
scanForSecrets(content) {
|
|
340
|
+
const issues = [];
|
|
341
|
+
for (const { pattern, name, entropyCheck } of SECRET_PATTERNS) {
|
|
342
|
+
const regex = new RegExp(pattern, 'g');
|
|
343
|
+
let match;
|
|
344
|
+
while ((match = regex.exec(content)) !== null) {
|
|
345
|
+
// Skip if entropy check is required and entropy is low
|
|
346
|
+
if (entropyCheck && this.calculateEntropy(match[0]) < 4.0) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
issues.push({
|
|
350
|
+
rule_id: 'mcp_embedded_secret',
|
|
351
|
+
rule_name: `Embedded Secret: ${name}`,
|
|
352
|
+
type: 'mcp_server',
|
|
353
|
+
severity: 'critical',
|
|
354
|
+
action: 'block',
|
|
355
|
+
match: this.redactSecret(match[0]),
|
|
356
|
+
location: this.createLocation(content, match.index, match[0].length),
|
|
357
|
+
suggestion: `Server manifest contains hardcoded ${name}. This is a critical security risk. DO NOT install.`
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return issues;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Check for typosquatting of known legitimate servers
|
|
365
|
+
*/
|
|
366
|
+
checkTyposquatting(manifest) {
|
|
367
|
+
const issues = [];
|
|
368
|
+
const name = (manifest.name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
369
|
+
if (!name)
|
|
370
|
+
return issues;
|
|
371
|
+
for (const legitimate of LEGITIMATE_SERVERS) {
|
|
372
|
+
const legitNormalized = legitimate.replace(/[^a-z0-9]/g, '');
|
|
373
|
+
// Skip exact matches
|
|
374
|
+
if (name === legitNormalized)
|
|
375
|
+
continue;
|
|
376
|
+
const distance = this.levenshteinDistance(name, legitNormalized);
|
|
377
|
+
// Flag if very similar (1-2 edits) or if it's a substring with additions
|
|
378
|
+
if (distance > 0 && distance <= 2) {
|
|
379
|
+
issues.push({
|
|
380
|
+
rule_id: 'mcp_typosquatting',
|
|
381
|
+
rule_name: 'Potential Typosquatting',
|
|
382
|
+
type: 'mcp_server',
|
|
383
|
+
severity: 'high',
|
|
384
|
+
action: 'alert',
|
|
385
|
+
match: `"${manifest.name}" is ${distance} edit(s) away from "${legitimate}"`,
|
|
386
|
+
suggestion: `This server name is suspiciously similar to the legitimate "${legitimate}" server. Verify you have the correct server from a trusted source.`
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// Check for common typosquatting patterns
|
|
390
|
+
if (this.hasTyposquatPattern(name, legitNormalized)) {
|
|
391
|
+
issues.push({
|
|
392
|
+
rule_id: 'mcp_typosquatting_pattern',
|
|
393
|
+
rule_name: 'Typosquatting Pattern Detected',
|
|
394
|
+
type: 'mcp_server',
|
|
395
|
+
severity: 'high',
|
|
396
|
+
action: 'alert',
|
|
397
|
+
match: `"${manifest.name}" uses common typosquatting pattern of "${legitimate}"`,
|
|
398
|
+
suggestion: 'This name uses a known typosquatting technique. Verify the source carefully.'
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return issues;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Check metadata for suspicious patterns
|
|
406
|
+
*/
|
|
407
|
+
checkMetadata(manifest) {
|
|
408
|
+
const issues = [];
|
|
409
|
+
// Check for missing metadata
|
|
410
|
+
if (!manifest.version) {
|
|
411
|
+
issues.push({
|
|
412
|
+
rule_id: 'mcp_missing_version',
|
|
413
|
+
rule_name: 'Missing Version',
|
|
414
|
+
type: 'mcp_server',
|
|
415
|
+
severity: 'low',
|
|
416
|
+
action: 'log',
|
|
417
|
+
match: 'No version specified',
|
|
418
|
+
suggestion: 'Legitimate servers typically include version information.'
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
// Check for suspicious repository URLs
|
|
422
|
+
if (manifest.repository) {
|
|
423
|
+
// Check for URL shorteners or suspicious domains
|
|
424
|
+
const suspiciousDomains = ['bit.ly', 'tinyurl', 'goo.gl', 't.co', 'shorturl'];
|
|
425
|
+
for (const domain of suspiciousDomains) {
|
|
426
|
+
if (manifest.repository.includes(domain)) {
|
|
427
|
+
issues.push({
|
|
428
|
+
rule_id: 'mcp_suspicious_repo',
|
|
429
|
+
rule_name: 'Suspicious Repository URL',
|
|
430
|
+
type: 'mcp_server',
|
|
431
|
+
severity: 'high',
|
|
432
|
+
action: 'alert',
|
|
433
|
+
match: `Repository uses URL shortener: ${domain}`,
|
|
434
|
+
suggestion: 'Legitimate servers link directly to their repository. URL shorteners can hide malicious destinations.'
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return issues;
|
|
440
|
+
}
|
|
441
|
+
// ============================================================================
|
|
442
|
+
// Helper Methods
|
|
443
|
+
// ============================================================================
|
|
444
|
+
/**
|
|
445
|
+
* Find hidden Unicode characters in a string
|
|
446
|
+
*/
|
|
447
|
+
findHiddenUnicode(str) {
|
|
448
|
+
const hiddenChars = [];
|
|
449
|
+
const hiddenPatterns = [
|
|
450
|
+
'\u200B', // Zero-width space
|
|
451
|
+
'\u200C', // Zero-width non-joiner
|
|
452
|
+
'\u200D', // Zero-width joiner
|
|
453
|
+
'\u2060', // Word joiner
|
|
454
|
+
'\u2061', // Function application
|
|
455
|
+
'\u2062', // Invisible times
|
|
456
|
+
'\u2063', // Invisible separator
|
|
457
|
+
'\u2064', // Invisible plus
|
|
458
|
+
'\uFEFF', // Zero-width no-break space (BOM)
|
|
459
|
+
'\u00AD', // Soft hyphen
|
|
460
|
+
'\u034F', // Combining grapheme joiner
|
|
461
|
+
'\u061C', // Arabic letter mark
|
|
462
|
+
'\u115F', // Hangul choseong filler
|
|
463
|
+
'\u1160', // Hangul jungseong filler
|
|
464
|
+
'\u17B4', // Khmer vowel inherent aq
|
|
465
|
+
'\u17B5', // Khmer vowel inherent aa
|
|
466
|
+
'\u180E', // Mongolian vowel separator
|
|
467
|
+
];
|
|
468
|
+
for (const char of str) {
|
|
469
|
+
if (hiddenPatterns.includes(char)) {
|
|
470
|
+
hiddenChars.push(char);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return hiddenChars;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Check for common typosquatting patterns
|
|
477
|
+
*/
|
|
478
|
+
hasTyposquatPattern(suspect, legitimate) {
|
|
479
|
+
// Character substitution (0 for o, 1 for l, etc.)
|
|
480
|
+
const substitutions = {
|
|
481
|
+
'o': ['0'],
|
|
482
|
+
'l': ['1', 'i'],
|
|
483
|
+
'i': ['1', 'l'],
|
|
484
|
+
'e': ['3'],
|
|
485
|
+
'a': ['4', '@'],
|
|
486
|
+
's': ['5', '$'],
|
|
487
|
+
'g': ['9', 'q'],
|
|
488
|
+
'b': ['8'],
|
|
489
|
+
};
|
|
490
|
+
// Check if suspect is legitimate with common substitutions
|
|
491
|
+
let normalized = suspect;
|
|
492
|
+
for (const [char, subs] of Object.entries(substitutions)) {
|
|
493
|
+
for (const sub of subs) {
|
|
494
|
+
normalized = normalized.replace(new RegExp(sub, 'g'), char);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (normalized === legitimate)
|
|
498
|
+
return true;
|
|
499
|
+
// Check for doubled characters (githubb, slackk)
|
|
500
|
+
const deduped = suspect.replace(/(.)\1+/g, '$1');
|
|
501
|
+
if (deduped === legitimate)
|
|
502
|
+
return true;
|
|
503
|
+
// Check for prefix/suffix additions (xgithub, githubx, my-github)
|
|
504
|
+
if (suspect.includes(legitimate) && suspect !== legitimate)
|
|
505
|
+
return true;
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Calculate Shannon entropy of a string
|
|
510
|
+
*/
|
|
511
|
+
calculateEntropy(str) {
|
|
512
|
+
const len = str.length;
|
|
513
|
+
if (len === 0)
|
|
514
|
+
return 0;
|
|
515
|
+
const frequencies = {};
|
|
516
|
+
for (const char of str) {
|
|
517
|
+
frequencies[char] = (frequencies[char] || 0) + 1;
|
|
518
|
+
}
|
|
519
|
+
let entropy = 0;
|
|
520
|
+
for (const count of Object.values(frequencies)) {
|
|
521
|
+
const p = count / len;
|
|
522
|
+
entropy -= p * Math.log2(p);
|
|
523
|
+
}
|
|
524
|
+
return entropy;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Calculate Levenshtein distance between two strings
|
|
528
|
+
*/
|
|
529
|
+
levenshteinDistance(a, b) {
|
|
530
|
+
const matrix = [];
|
|
531
|
+
for (let i = 0; i <= b.length; i++) {
|
|
532
|
+
matrix[i] = [i];
|
|
533
|
+
}
|
|
534
|
+
for (let j = 0; j <= a.length; j++) {
|
|
535
|
+
matrix[0][j] = j;
|
|
536
|
+
}
|
|
537
|
+
for (let i = 1; i <= b.length; i++) {
|
|
538
|
+
for (let j = 1; j <= a.length; j++) {
|
|
539
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
540
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
|
|
544
|
+
matrix[i][j - 1] + 1, // insertion
|
|
545
|
+
matrix[i - 1][j] + 1 // deletion
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return matrix[b.length][a.length];
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Create a location object
|
|
554
|
+
*/
|
|
555
|
+
createLocation(content, start, length) {
|
|
556
|
+
const end = start + length;
|
|
557
|
+
const lines = content.substring(0, start).split('\n');
|
|
558
|
+
const line = lines.length;
|
|
559
|
+
const column = lines[lines.length - 1].length + 1;
|
|
560
|
+
return { start, end, line, column };
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Truncate string for display
|
|
564
|
+
*/
|
|
565
|
+
truncate(str, maxLength = 100) {
|
|
566
|
+
if (str.length <= maxLength)
|
|
567
|
+
return str;
|
|
568
|
+
return str.substring(0, maxLength) + '...';
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Redact a secret for safe display
|
|
572
|
+
*/
|
|
573
|
+
redactSecret(secret) {
|
|
574
|
+
if (secret.length <= 8)
|
|
575
|
+
return '****';
|
|
576
|
+
const visibleStart = Math.min(4, Math.floor(secret.length * 0.15));
|
|
577
|
+
const visibleEnd = Math.min(4, Math.floor(secret.length * 0.15));
|
|
578
|
+
return `${secret.substring(0, visibleStart)}${'*'.repeat(8)}${secret.substring(secret.length - visibleEnd)}`;
|
|
579
|
+
}
|
|
580
|
+
// ============================================================================
|
|
581
|
+
// Public Utility Methods
|
|
582
|
+
// ============================================================================
|
|
583
|
+
/**
|
|
584
|
+
* Parse and validate an MCP manifest
|
|
585
|
+
*/
|
|
586
|
+
static parseManifest(content) {
|
|
587
|
+
try {
|
|
588
|
+
const parsed = JSON.parse(content);
|
|
589
|
+
if (parsed.name || parsed.tools || parsed.resources) {
|
|
590
|
+
return parsed;
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Analyze capabilities of a manifest
|
|
600
|
+
*/
|
|
601
|
+
static analyzeCapabilities(manifest) {
|
|
602
|
+
const capabilities = {
|
|
603
|
+
network: false,
|
|
604
|
+
filesystem: false,
|
|
605
|
+
execution: false,
|
|
606
|
+
environment: false,
|
|
607
|
+
database: false,
|
|
608
|
+
crypto: false
|
|
609
|
+
};
|
|
610
|
+
for (const tool of manifest.tools || []) {
|
|
611
|
+
const combined = `${tool.name} ${tool.description || ''}`.toLowerCase();
|
|
612
|
+
for (const { pattern, capability } of DANGEROUS_CAPABILITY_PATTERNS) {
|
|
613
|
+
if (pattern.test(combined)) {
|
|
614
|
+
capabilities[capability] = true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return capabilities;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Get risk level based on capabilities
|
|
622
|
+
*/
|
|
623
|
+
static getRiskLevel(capabilities) {
|
|
624
|
+
if (capabilities.execution)
|
|
625
|
+
return 'CRITICAL';
|
|
626
|
+
if (capabilities.filesystem && capabilities.network)
|
|
627
|
+
return 'HIGH';
|
|
628
|
+
if (capabilities.filesystem || capabilities.network || capabilities.environment)
|
|
629
|
+
return 'MEDIUM';
|
|
630
|
+
return 'LOW';
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
export default McpServerAnalyzer;
|
|
634
|
+
//# sourceMappingURL=mcp-server-analyzer.js.map
|