@weave_protocol/mund 0.1.10 → 0.1.12

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.
@@ -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