@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
package/src/tools/index.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
export { registerMonitoringTools } from './monitoring.js';
|
|
7
7
|
export { registerConfigurationTools } from './configuration.js';
|
|
8
|
+
export { registerMcpScanningTools } from './mcp-scanning.js';
|
|
8
9
|
|
|
9
10
|
import { registerMonitoringTools } from './monitoring.js';
|
|
10
11
|
import { registerConfigurationTools } from './configuration.js';
|
|
12
|
+
import { registerMcpScanningTools } from './mcp-scanning.js';
|
|
11
13
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
14
|
import type { IStorage, DetectionRule } from '../types.js';
|
|
13
15
|
import { AnalyzerEngine } from '../analyzers/index.js';
|
|
@@ -24,6 +26,12 @@ export function registerAllTools(
|
|
|
24
26
|
notificationHub: NotificationHub,
|
|
25
27
|
blockMode: boolean
|
|
26
28
|
): void {
|
|
29
|
+
// Core monitoring tools
|
|
27
30
|
registerMonitoringTools(server, storage, rules, analyzer, notificationHub, blockMode);
|
|
31
|
+
|
|
32
|
+
// Configuration management tools
|
|
28
33
|
registerConfigurationTools(server, storage, rules);
|
|
34
|
+
|
|
35
|
+
// MCP server scanning tools (new)
|
|
36
|
+
registerMcpScanningTools(server);
|
|
29
37
|
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mund - The Guardian Protocol
|
|
3
|
+
* MCP Scanning Tools - Tools for scanning MCP servers before installation
|
|
4
|
+
*
|
|
5
|
+
* Tools:
|
|
6
|
+
* - mund_scan_mcp_server: Full security scan of a server manifest
|
|
7
|
+
* - mund_check_typosquatting: Check if a name is typosquatting a known server
|
|
8
|
+
* - mund_audit_mcp_permissions: Audit the permission scope of server tools
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
|
+
import {
|
|
14
|
+
McpServerAnalyzer,
|
|
15
|
+
type McpServerManifest,
|
|
16
|
+
type McpCapabilities,
|
|
17
|
+
type McpScanResult
|
|
18
|
+
} from '../analyzers/mcp-server-analyzer.js';
|
|
19
|
+
import type { SecurityIssue } from '../types.js';
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Tool Schemas
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
const ScanMcpServerSchema = z.object({
|
|
26
|
+
manifest: z.string().describe('JSON content of server.json manifest to scan'),
|
|
27
|
+
source: z.string().optional().describe('Source URL, registry name, or file path for context')
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const CheckTyposquattingSchema = z.object({
|
|
31
|
+
name: z.string().describe('MCP server name to check for typosquatting')
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const AuditPermissionsSchema = z.object({
|
|
35
|
+
manifest: z.string().describe('JSON content of server.json manifest to audit')
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Tool Registration
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register all MCP scanning tools
|
|
44
|
+
*/
|
|
45
|
+
export function registerMcpScanningTools(server: McpServer): void {
|
|
46
|
+
const analyzer = new McpServerAnalyzer();
|
|
47
|
+
|
|
48
|
+
// -------------------------------------------------------------------------
|
|
49
|
+
// Tool: mund_scan_mcp_server
|
|
50
|
+
// -------------------------------------------------------------------------
|
|
51
|
+
server.tool(
|
|
52
|
+
'mund_scan_mcp_server',
|
|
53
|
+
'Scan an MCP server manifest (server.json) for security issues before installation. Detects prompt injection in tool descriptions, typosquatting, embedded secrets, and dangerous permissions.',
|
|
54
|
+
ScanMcpServerSchema.shape,
|
|
55
|
+
async ({ manifest, source }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => {
|
|
56
|
+
try {
|
|
57
|
+
// Parse manifest
|
|
58
|
+
const parsed = McpServerAnalyzer.parseManifest(manifest);
|
|
59
|
+
if (!parsed) {
|
|
60
|
+
return {
|
|
61
|
+
content: [{
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: JSON.stringify({
|
|
64
|
+
error: 'Invalid manifest',
|
|
65
|
+
message: 'Could not parse the provided content as a valid MCP server manifest. Ensure it is valid JSON with name, tools, or resources fields.',
|
|
66
|
+
recommendation: 'CANNOT_ANALYZE'
|
|
67
|
+
}, null, 2)
|
|
68
|
+
}]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Run full analysis
|
|
73
|
+
const issues = await analyzer.analyze(manifest, []);
|
|
74
|
+
|
|
75
|
+
// Analyze capabilities
|
|
76
|
+
const capabilities = McpServerAnalyzer.analyzeCapabilities(parsed);
|
|
77
|
+
const riskLevel = McpServerAnalyzer.getRiskLevel(capabilities);
|
|
78
|
+
|
|
79
|
+
// Count by severity
|
|
80
|
+
const bySeverity = {
|
|
81
|
+
critical: issues.filter(i => i.severity === 'critical').length,
|
|
82
|
+
high: issues.filter(i => i.severity === 'high').length,
|
|
83
|
+
medium: issues.filter(i => i.severity === 'medium').length,
|
|
84
|
+
low: issues.filter(i => i.severity === 'low').length,
|
|
85
|
+
info: issues.filter(i => i.severity === 'info').length
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Determine recommendation
|
|
89
|
+
let recommendation: McpScanResult['recommendation'];
|
|
90
|
+
if (bySeverity.critical > 0) {
|
|
91
|
+
recommendation = 'DO_NOT_INSTALL';
|
|
92
|
+
} else if (bySeverity.high > 0) {
|
|
93
|
+
recommendation = 'REVIEW_CAREFULLY';
|
|
94
|
+
} else if (bySeverity.medium > 0 || riskLevel === 'HIGH') {
|
|
95
|
+
recommendation = 'CAUTION';
|
|
96
|
+
} else {
|
|
97
|
+
recommendation = 'APPEARS_SAFE';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result: McpScanResult = {
|
|
101
|
+
server_name: parsed.name || 'unknown',
|
|
102
|
+
version: parsed.version || 'unknown',
|
|
103
|
+
scanned_at: new Date().toISOString(),
|
|
104
|
+
source,
|
|
105
|
+
issue_count: issues.length,
|
|
106
|
+
by_severity: bySeverity,
|
|
107
|
+
recommendation,
|
|
108
|
+
capabilities,
|
|
109
|
+
issues: issues.map(formatIssue)
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
content: [{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: JSON.stringify(result, null, 2)
|
|
116
|
+
}]
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return {
|
|
120
|
+
content: [{
|
|
121
|
+
type: 'text',
|
|
122
|
+
text: JSON.stringify({
|
|
123
|
+
error: 'Scan failed',
|
|
124
|
+
message: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
125
|
+
recommendation: 'CANNOT_ANALYZE'
|
|
126
|
+
}, null, 2)
|
|
127
|
+
}]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// -------------------------------------------------------------------------
|
|
134
|
+
// Tool: mund_check_typosquatting
|
|
135
|
+
// -------------------------------------------------------------------------
|
|
136
|
+
server.tool(
|
|
137
|
+
'mund_check_typosquatting',
|
|
138
|
+
'Check if an MCP server name is potentially typosquatting a known legitimate server. Compares against a list of official and common MCP servers.',
|
|
139
|
+
CheckTyposquattingSchema.shape,
|
|
140
|
+
async ({ name }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => {
|
|
141
|
+
try {
|
|
142
|
+
// Create a minimal manifest for the check
|
|
143
|
+
const fakeManifest: McpServerManifest = { name, version: '0.0.0' };
|
|
144
|
+
const issues = analyzer.checkTyposquatting(fakeManifest);
|
|
145
|
+
|
|
146
|
+
const similarTo = issues.map(i => {
|
|
147
|
+
// Extract the legitimate name from the match string
|
|
148
|
+
const match = i.match?.match(/"([^"]+)"$/);
|
|
149
|
+
return match ? match[1] : i.match;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const result = {
|
|
153
|
+
name,
|
|
154
|
+
checked_at: new Date().toISOString(),
|
|
155
|
+
is_suspicious: issues.length > 0,
|
|
156
|
+
risk_level: issues.length > 0 ? 'HIGH' : 'NONE',
|
|
157
|
+
similar_to: similarTo,
|
|
158
|
+
details: issues.map(i => ({
|
|
159
|
+
pattern: i.rule_name,
|
|
160
|
+
match: i.match,
|
|
161
|
+
suggestion: i.suggestion
|
|
162
|
+
})),
|
|
163
|
+
recommendation: issues.length > 0
|
|
164
|
+
? 'This name is suspiciously similar to a known legitimate server. Verify you have the correct server from a trusted source before installing.'
|
|
165
|
+
: 'No typosquatting patterns detected. This does not guarantee the server is safe - always verify the source.'
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
content: [{
|
|
170
|
+
type: 'text',
|
|
171
|
+
text: JSON.stringify(result, null, 2)
|
|
172
|
+
}]
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
content: [{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: JSON.stringify({
|
|
179
|
+
error: 'Check failed',
|
|
180
|
+
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
181
|
+
}, null, 2)
|
|
182
|
+
}]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// -------------------------------------------------------------------------
|
|
189
|
+
// Tool: mund_audit_mcp_permissions
|
|
190
|
+
// -------------------------------------------------------------------------
|
|
191
|
+
server.tool(
|
|
192
|
+
'mund_audit_mcp_permissions',
|
|
193
|
+
'Analyze the permission scope and capabilities of an MCP server\'s tools. Identifies network access, filesystem operations, command execution, and other potentially dangerous capabilities.',
|
|
194
|
+
AuditPermissionsSchema.shape,
|
|
195
|
+
async ({ manifest }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => {
|
|
196
|
+
try {
|
|
197
|
+
// Parse manifest
|
|
198
|
+
const parsed = McpServerAnalyzer.parseManifest(manifest);
|
|
199
|
+
if (!parsed) {
|
|
200
|
+
return {
|
|
201
|
+
content: [{
|
|
202
|
+
type: 'text',
|
|
203
|
+
text: JSON.stringify({
|
|
204
|
+
error: 'Invalid manifest',
|
|
205
|
+
message: 'Could not parse as MCP server manifest'
|
|
206
|
+
}, null, 2)
|
|
207
|
+
}]
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Get capabilities
|
|
212
|
+
const capabilities = McpServerAnalyzer.analyzeCapabilities(parsed);
|
|
213
|
+
const riskLevel = McpServerAnalyzer.getRiskLevel(capabilities);
|
|
214
|
+
|
|
215
|
+
// Get permission-related issues
|
|
216
|
+
const allIssues = await analyzer.analyze(manifest, []);
|
|
217
|
+
const permissionIssues = allIssues.filter(i =>
|
|
218
|
+
i.rule_id.startsWith('mcp_capability_') ||
|
|
219
|
+
i.rule_id === 'mcp_execution_warning' ||
|
|
220
|
+
i.rule_id === 'mcp_excessive_tools'
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Analyze each tool
|
|
224
|
+
const toolAnalysis = (parsed.tools || []).map(tool => {
|
|
225
|
+
const combined = `${tool.name} ${tool.description || ''}`.toLowerCase();
|
|
226
|
+
const permissions: string[] = [];
|
|
227
|
+
|
|
228
|
+
if (/\b(fetch|request|http|https|curl|wget|axios|socket|websocket)\b/i.test(combined)) {
|
|
229
|
+
permissions.push('network');
|
|
230
|
+
}
|
|
231
|
+
if (/\b(file|read|write|path|directory|fs|fopen)\b/i.test(combined)) {
|
|
232
|
+
permissions.push('filesystem');
|
|
233
|
+
}
|
|
234
|
+
if (/\b(exec|execute|shell|command|bash|spawn|eval)\b/i.test(combined)) {
|
|
235
|
+
permissions.push('execution');
|
|
236
|
+
}
|
|
237
|
+
if (/\b(env|environment|process\.env|secret|credential)\b/i.test(combined)) {
|
|
238
|
+
permissions.push('environment');
|
|
239
|
+
}
|
|
240
|
+
if (/\b(sql|query|database|db|postgres|mysql|mongo|redis)\b/i.test(combined)) {
|
|
241
|
+
permissions.push('database');
|
|
242
|
+
}
|
|
243
|
+
if (/\b(crypto|encrypt|decrypt|hash|sign)\b/i.test(combined)) {
|
|
244
|
+
permissions.push('crypto');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
name: tool.name,
|
|
249
|
+
description: truncate(tool.description || '', 100),
|
|
250
|
+
detected_permissions: permissions,
|
|
251
|
+
risk: permissions.includes('execution') ? 'HIGH' :
|
|
252
|
+
permissions.length > 1 ? 'MEDIUM' :
|
|
253
|
+
permissions.length > 0 ? 'LOW' : 'NONE'
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const result = {
|
|
258
|
+
server_name: parsed.name || 'unknown',
|
|
259
|
+
version: parsed.version || 'unknown',
|
|
260
|
+
audited_at: new Date().toISOString(),
|
|
261
|
+
tool_count: parsed.tools?.length || 0,
|
|
262
|
+
resource_count: parsed.resources?.length || 0,
|
|
263
|
+
prompt_count: parsed.prompts?.length || 0,
|
|
264
|
+
overall_risk_level: riskLevel,
|
|
265
|
+
capabilities,
|
|
266
|
+
capability_summary: formatCapabilitySummary(capabilities),
|
|
267
|
+
tools: toolAnalysis,
|
|
268
|
+
permission_issues: permissionIssues.map(formatIssue),
|
|
269
|
+
recommendations: generatePermissionRecommendations(capabilities, parsed.tools?.length || 0)
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
content: [{
|
|
274
|
+
type: 'text',
|
|
275
|
+
text: JSON.stringify(result, null, 2)
|
|
276
|
+
}]
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return {
|
|
280
|
+
content: [{
|
|
281
|
+
type: 'text',
|
|
282
|
+
text: JSON.stringify({
|
|
283
|
+
error: 'Audit failed',
|
|
284
|
+
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
285
|
+
}, null, 2)
|
|
286
|
+
}]
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ============================================================================
|
|
294
|
+
// Helper Functions
|
|
295
|
+
// ============================================================================
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Format a security issue for JSON output
|
|
299
|
+
*/
|
|
300
|
+
function formatIssue(issue: SecurityIssue): SecurityIssue {
|
|
301
|
+
return {
|
|
302
|
+
rule_id: issue.rule_id,
|
|
303
|
+
rule_name: issue.rule_name,
|
|
304
|
+
type: issue.type,
|
|
305
|
+
severity: issue.severity,
|
|
306
|
+
action: issue.action,
|
|
307
|
+
match: issue.match,
|
|
308
|
+
suggestion: issue.suggestion,
|
|
309
|
+
location: issue.location
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Truncate a string
|
|
315
|
+
*/
|
|
316
|
+
function truncate(str: string, maxLength: number): string {
|
|
317
|
+
if (str.length <= maxLength) return str;
|
|
318
|
+
return str.substring(0, maxLength) + '...';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Format capabilities into a human-readable summary
|
|
323
|
+
*/
|
|
324
|
+
function formatCapabilitySummary(capabilities: McpCapabilities): string[] {
|
|
325
|
+
const summary: string[] = [];
|
|
326
|
+
|
|
327
|
+
if (capabilities.execution) {
|
|
328
|
+
summary.push('⚠️ Can execute commands/code on your system');
|
|
329
|
+
}
|
|
330
|
+
if (capabilities.filesystem) {
|
|
331
|
+
summary.push('📁 Can read/write files');
|
|
332
|
+
}
|
|
333
|
+
if (capabilities.network) {
|
|
334
|
+
summary.push('🌐 Can make network requests');
|
|
335
|
+
}
|
|
336
|
+
if (capabilities.environment) {
|
|
337
|
+
summary.push('🔑 Can access environment variables/secrets');
|
|
338
|
+
}
|
|
339
|
+
if (capabilities.database) {
|
|
340
|
+
summary.push('🗄️ Can access databases');
|
|
341
|
+
}
|
|
342
|
+
if (capabilities.crypto) {
|
|
343
|
+
summary.push('🔐 Uses cryptographic operations');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (summary.length === 0) {
|
|
347
|
+
summary.push('✅ No dangerous capabilities detected');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return summary;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Generate permission-based recommendations
|
|
355
|
+
*/
|
|
356
|
+
function generatePermissionRecommendations(capabilities: McpCapabilities, toolCount: number): string[] {
|
|
357
|
+
const recommendations: string[] = [];
|
|
358
|
+
|
|
359
|
+
if (capabilities.execution) {
|
|
360
|
+
recommendations.push('CRITICAL: This server can execute arbitrary commands. Only install if you fully trust the source and understand what commands it will run.');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (capabilities.filesystem && capabilities.network) {
|
|
364
|
+
recommendations.push('HIGH RISK: This server can both access files and make network requests. This combination could be used to exfiltrate data.');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (capabilities.environment) {
|
|
368
|
+
recommendations.push('This server can access environment variables, which may include API keys and secrets. Ensure you trust the source.');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (toolCount > 20) {
|
|
372
|
+
recommendations.push(`This server has ${toolCount} tools, which is a large attack surface. Consider whether you need all of them.`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (recommendations.length === 0) {
|
|
376
|
+
recommendations.push('This server has a relatively low-risk permission profile, but always verify the source before installing.');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return recommendations;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export default registerMcpScanningTools;
|