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