contextguard 0.1.8 → 0.2.1
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/LICENSE +23 -17
- package/README.md +157 -109
- package/dist/agent.d.ts +24 -0
- package/dist/agent.js +376 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +298 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +56 -0
- package/dist/database.d.ts +116 -0
- package/dist/database.js +291 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +18 -0
- package/dist/init.d.ts +7 -0
- package/dist/init.js +178 -0
- package/dist/lib/supabase-client.d.ts +27 -0
- package/dist/lib/supabase-client.js +97 -0
- package/dist/logger.d.ts +36 -0
- package/dist/logger.js +145 -0
- package/dist/mcp-security-wrapper.d.ts +84 -0
- package/dist/mcp-security-wrapper.js +389 -147
- package/dist/mcp-traceability-integration.d.ts +118 -0
- package/dist/mcp-traceability-integration.js +302 -0
- package/dist/policy.d.ts +30 -0
- package/dist/policy.js +273 -0
- package/dist/premium-features.d.ts +364 -0
- package/dist/premium-features.js +950 -0
- package/dist/security-logger.d.ts +45 -0
- package/dist/security-logger.js +125 -0
- package/dist/security-policy.d.ts +55 -0
- package/dist/security-policy.js +140 -0
- package/dist/semantic-detector.d.ts +21 -0
- package/dist/semantic-detector.js +49 -0
- package/dist/sse-proxy.d.ts +21 -0
- package/dist/sse-proxy.js +276 -0
- package/dist/supabase-client.d.ts +27 -0
- package/dist/supabase-client.js +89 -0
- package/dist/types/database.types.d.ts +220 -0
- package/dist/types/database.types.js +8 -0
- package/dist/types/mcp.d.ts +27 -0
- package/dist/types/mcp.js +15 -0
- package/dist/types/types.d.ts +65 -0
- package/dist/types/types.js +8 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.js +8 -0
- package/dist/wrapper.d.ts +115 -0
- package/dist/wrapper.js +417 -0
- package/package.json +35 -11
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -57
- package/CONTRIBUTING.md +0 -532
- package/SECURITY.md +0 -254
- package/assets/demo.mp4 +0 -0
- package/eslint.config.mts +0 -23
- package/examples/config/config.json +0 -19
- package/examples/mcp-server/demo.js +0 -228
- package/examples/mcp-server/package-lock.json +0 -978
- package/examples/mcp-server/package.json +0 -16
- package/examples/mcp-server/pnpm-lock.yaml +0 -745
- package/src/mcp-security-wrapper.ts +0 -570
- package/test/test-server.ts +0 -295
- package/tsconfig.json +0 -16
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
|
-
* Copyright (c)
|
|
4
|
+
* Copyright (c) 2026 Amir Mironi
|
|
5
5
|
*
|
|
6
6
|
* This source code is licensed under the MIT license found in the
|
|
7
7
|
* LICENSE file in the root directory of this source tree.
|
|
@@ -40,31 +40,40 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
40
40
|
};
|
|
41
41
|
})();
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
-
exports.SecurityLogger = exports.SecurityPolicy = exports.MCPSecurityWrapper = void 0;
|
|
43
|
+
exports.SecurityLogger = exports.SecurityPolicy = exports.MCPSSEProxy = exports.MCPSecurityWrapper = void 0;
|
|
44
44
|
const child_process_1 = require("child_process");
|
|
45
45
|
const fs = __importStar(require("fs"));
|
|
46
|
+
const http = __importStar(require("http"));
|
|
47
|
+
const https = __importStar(require("https"));
|
|
46
48
|
const crypto_1 = require("crypto");
|
|
49
|
+
// ─── Security Policy ─────────────────────────────────────────────────────────
|
|
47
50
|
class SecurityPolicy {
|
|
48
51
|
constructor(config) {
|
|
49
52
|
this.config = {
|
|
50
53
|
maxToolCallsPerMinute: 30,
|
|
54
|
+
enablePromptInjectionDetection: true,
|
|
55
|
+
enableSensitiveDataDetection: true,
|
|
56
|
+
enablePathTraversalPrevention: true,
|
|
57
|
+
mode: "monitor", // Safe default — just log, don't block
|
|
51
58
|
blockedPatterns: [],
|
|
52
59
|
allowedFilePaths: [],
|
|
53
60
|
alertThreshold: 5,
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
logPath: "./mcp_security.log",
|
|
62
|
+
logLevel: "info",
|
|
63
|
+
transport: "stdio",
|
|
64
|
+
port: 3100,
|
|
65
|
+
targetUrl: "",
|
|
56
66
|
...config,
|
|
57
67
|
};
|
|
58
|
-
// Sensitive data patterns
|
|
59
68
|
this.sensitiveDataPatterns = [
|
|
60
69
|
/(?:password|secret|api[_-]?key|token)\s*[:=]\s*['"]?[\w\-.]+['"]?/gi,
|
|
61
|
-
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
62
|
-
/\b\d{3}-\d{2}-\d{4}\b/g,
|
|
63
|
-
/sk-[a-zA-Z0-9]{20,}/g,
|
|
64
|
-
/ghp_[a-zA-Z0-9]{36}/g,
|
|
65
|
-
/AKIA[0-9A-Z]{16}/g,
|
|
70
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
71
|
+
/\b\d{3}-\d{2}-\d{4}\b/g,
|
|
72
|
+
/sk-[a-zA-Z0-9]{20,}/g,
|
|
73
|
+
/ghp_[a-zA-Z0-9]{36}/g,
|
|
74
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
75
|
+
/sk_(live|test)_[a-zA-Z0-9]{24,}/g,
|
|
66
76
|
];
|
|
67
|
-
// Prompt injection patterns
|
|
68
77
|
this.promptInjectionPatterns = [
|
|
69
78
|
/ignore\s+(previous|all)\s+(instructions|prompts)/gi,
|
|
70
79
|
/system:\s*you\s+are\s+now/gi,
|
|
@@ -76,6 +85,12 @@ class SecurityPolicy {
|
|
|
76
85
|
/override\s+previous/gi,
|
|
77
86
|
];
|
|
78
87
|
}
|
|
88
|
+
get isBlockingMode() {
|
|
89
|
+
return this.config.mode === "block";
|
|
90
|
+
}
|
|
91
|
+
get rawConfig() {
|
|
92
|
+
return this.config;
|
|
93
|
+
}
|
|
79
94
|
checkPromptInjection(text) {
|
|
80
95
|
if (!this.config.enablePromptInjectionDetection)
|
|
81
96
|
return [];
|
|
@@ -101,6 +116,8 @@ class SecurityPolicy {
|
|
|
101
116
|
return violations;
|
|
102
117
|
}
|
|
103
118
|
checkFileAccess(filePath) {
|
|
119
|
+
if (!this.config.enablePathTraversalPrevention)
|
|
120
|
+
return [];
|
|
104
121
|
const violations = [];
|
|
105
122
|
if (filePath.includes("..")) {
|
|
106
123
|
violations.push(`Path traversal attempt detected: ${filePath}`);
|
|
@@ -112,11 +129,10 @@ class SecurityPolicy {
|
|
|
112
129
|
"/proc",
|
|
113
130
|
"C:\\Windows\\System32",
|
|
114
131
|
];
|
|
115
|
-
if (dangerousPaths.some((
|
|
132
|
+
if (dangerousPaths.some((d) => filePath.startsWith(d))) {
|
|
116
133
|
violations.push(`Access to dangerous path detected: ${filePath}`);
|
|
117
134
|
}
|
|
118
|
-
if (this.config.allowedFilePaths
|
|
119
|
-
this.config.allowedFilePaths.length > 0) {
|
|
135
|
+
if (this.config.allowedFilePaths.length > 0) {
|
|
120
136
|
const isAllowed = this.config.allowedFilePaths.some((allowed) => filePath.startsWith(allowed));
|
|
121
137
|
if (!isAllowed) {
|
|
122
138
|
violations.push(`File path not in allowed list: ${filePath}`);
|
|
@@ -127,12 +143,13 @@ class SecurityPolicy {
|
|
|
127
143
|
checkRateLimit(timestamps) {
|
|
128
144
|
const oneMinuteAgo = Date.now() - 60000;
|
|
129
145
|
const recentCalls = timestamps.filter((t) => t > oneMinuteAgo);
|
|
130
|
-
return recentCalls.length <
|
|
146
|
+
return recentCalls.length < this.config.maxToolCallsPerMinute;
|
|
131
147
|
}
|
|
132
148
|
}
|
|
133
149
|
exports.SecurityPolicy = SecurityPolicy;
|
|
150
|
+
// ─── Security Logger ──────────────────────────────────────────────────────────
|
|
134
151
|
class SecurityLogger {
|
|
135
|
-
constructor(logFile = "mcp_security.log") {
|
|
152
|
+
constructor(logFile = "./mcp_security.log") {
|
|
136
153
|
this.events = [];
|
|
137
154
|
this.logFile = logFile;
|
|
138
155
|
}
|
|
@@ -168,6 +185,15 @@ class SecurityLogger {
|
|
|
168
185
|
}
|
|
169
186
|
}
|
|
170
187
|
exports.SecurityLogger = SecurityLogger;
|
|
188
|
+
// ─── Violation Handler (respects monitor vs block mode) ───────────────────────
|
|
189
|
+
function buildErrorResponse(msg, code, message, violations) {
|
|
190
|
+
return (JSON.stringify({
|
|
191
|
+
jsonrpc: msg.jsonrpc,
|
|
192
|
+
id: msg.id,
|
|
193
|
+
error: { code, message, data: { violations } },
|
|
194
|
+
}) + "\n");
|
|
195
|
+
}
|
|
196
|
+
// ─── stdio Transport ─────────────────────────────────────────────────────────
|
|
171
197
|
class MCPSecurityWrapper {
|
|
172
198
|
constructor(serverCommand, policy, logger) {
|
|
173
199
|
this.process = null;
|
|
@@ -192,31 +218,23 @@ class MCPSecurityWrapper {
|
|
|
192
218
|
this.logger.logEvent("SERVER_START", "LOW", {
|
|
193
219
|
command: this.serverCommand.join(" "),
|
|
194
220
|
pid: this.process.pid,
|
|
221
|
+
mode: this.policy.rawConfig.mode,
|
|
195
222
|
}, this.sessionId);
|
|
196
223
|
this.process.stderr.pipe(process.stderr);
|
|
197
224
|
this.process.stdout.on("data", (data) => {
|
|
198
|
-
|
|
199
|
-
this.handleServerOutput(output);
|
|
225
|
+
this.handleServerOutput(data.toString());
|
|
200
226
|
});
|
|
201
227
|
process.stdin.on("data", (data) => {
|
|
202
|
-
|
|
203
|
-
this.handleClientInput(input);
|
|
228
|
+
this.handleClientInput(data.toString());
|
|
204
229
|
});
|
|
205
230
|
this.process.on("exit", (code) => {
|
|
206
|
-
this.logger.logEvent("SERVER_EXIT", "MEDIUM", {
|
|
207
|
-
exitCode: code,
|
|
208
|
-
}, this.sessionId);
|
|
231
|
+
this.logger.logEvent("SERVER_EXIT", "MEDIUM", { exitCode: code }, this.sessionId);
|
|
209
232
|
console.error("\n=== MCP Security Statistics ===");
|
|
210
233
|
console.error(JSON.stringify(this.logger.getStatistics(), null, 2));
|
|
211
|
-
|
|
212
|
-
setImmediate(() => {
|
|
213
|
-
process.exit(code || 0);
|
|
214
|
-
});
|
|
234
|
+
setImmediate(() => process.exit(code || 0));
|
|
215
235
|
});
|
|
216
236
|
this.process.on("error", (err) => {
|
|
217
|
-
this.logger.logEvent("SERVER_ERROR", "HIGH", {
|
|
218
|
-
error: err.message,
|
|
219
|
-
}, this.sessionId);
|
|
237
|
+
this.logger.logEvent("SERVER_ERROR", "HIGH", { error: err.message }, this.sessionId);
|
|
220
238
|
console.error("Server process error:", err);
|
|
221
239
|
});
|
|
222
240
|
}
|
|
@@ -225,9 +243,8 @@ class MCPSecurityWrapper {
|
|
|
225
243
|
const lines = this.clientMessageBuffer.split("\n");
|
|
226
244
|
this.clientMessageBuffer = lines.pop() || "";
|
|
227
245
|
for (const line of lines) {
|
|
228
|
-
if (line.trim())
|
|
246
|
+
if (line.trim())
|
|
229
247
|
this.processClientMessage(line);
|
|
230
|
-
}
|
|
231
248
|
}
|
|
232
249
|
}
|
|
233
250
|
processClientMessage(line) {
|
|
@@ -237,189 +254,414 @@ class MCPSecurityWrapper {
|
|
|
237
254
|
method: message.method,
|
|
238
255
|
id: message.id,
|
|
239
256
|
}, this.sessionId);
|
|
240
|
-
const violations =
|
|
241
|
-
let shouldBlock = false;
|
|
242
|
-
if (message.method === "tools/call") {
|
|
243
|
-
const now = Date.now();
|
|
244
|
-
this.toolCallTimestamps.push(now);
|
|
245
|
-
// Clean up old timestamps to prevent memory leak
|
|
246
|
-
const oneMinuteAgo = now - 60000;
|
|
247
|
-
this.toolCallTimestamps = this.toolCallTimestamps.filter((t) => t > oneMinuteAgo);
|
|
248
|
-
if (!this.policy.checkRateLimit(this.toolCallTimestamps)) {
|
|
249
|
-
violations.push("Rate limit exceeded for tool calls");
|
|
250
|
-
shouldBlock = true;
|
|
251
|
-
this.logger.logEvent("RATE_LIMIT_EXCEEDED", "HIGH", {
|
|
252
|
-
method: message.method,
|
|
253
|
-
toolName: message.params?.name,
|
|
254
|
-
}, this.sessionId);
|
|
255
|
-
}
|
|
256
|
-
const paramsStr = JSON.stringify(message.params);
|
|
257
|
-
const injectionViolations = this.policy.checkPromptInjection(paramsStr);
|
|
258
|
-
violations.push(...injectionViolations);
|
|
259
|
-
const sensitiveViolations = this.policy.checkSensitiveData(paramsStr);
|
|
260
|
-
violations.push(...sensitiveViolations);
|
|
261
|
-
// Check multiple possible file path parameter locations
|
|
262
|
-
const filePathParams = [
|
|
263
|
-
message.params?.arguments?.path,
|
|
264
|
-
message.params?.arguments?.filePath,
|
|
265
|
-
message.params?.arguments?.file,
|
|
266
|
-
message.params?.arguments?.directory,
|
|
267
|
-
message.params?.path,
|
|
268
|
-
message.params?.filePath,
|
|
269
|
-
].filter((path) => typeof path === "string");
|
|
270
|
-
for (const filePath of filePathParams) {
|
|
271
|
-
const fileViolations = this.policy.checkFileAccess(filePath);
|
|
272
|
-
violations.push(...fileViolations);
|
|
273
|
-
}
|
|
274
|
-
this.logger.logEvent("TOOL_CALL", violations.length > 0 ? "HIGH" : "LOW", {
|
|
275
|
-
toolName: message.params?.name,
|
|
276
|
-
hasViolations: violations.length > 0,
|
|
277
|
-
violations,
|
|
278
|
-
}, this.sessionId);
|
|
279
|
-
}
|
|
257
|
+
const { violations, shouldBlock } = this.inspectOutbound(message);
|
|
280
258
|
if (violations.length > 0) {
|
|
281
259
|
this.logger.logEvent("SECURITY_VIOLATION", "CRITICAL", {
|
|
282
260
|
violations,
|
|
283
|
-
message: message,
|
|
284
261
|
blocked: shouldBlock,
|
|
285
262
|
}, this.sessionId);
|
|
286
263
|
console.error(`\n⚠️ SECURITY VIOLATIONS DETECTED:\n${violations.join("\n")}\n`);
|
|
287
264
|
if (shouldBlock) {
|
|
288
265
|
console.error("🚫 REQUEST BLOCKED\n");
|
|
289
|
-
// Send error response back to client
|
|
290
266
|
if (message.id !== undefined) {
|
|
291
|
-
|
|
292
|
-
jsonrpc: message.jsonrpc,
|
|
293
|
-
id: message.id,
|
|
294
|
-
error: {
|
|
295
|
-
code: -32000,
|
|
296
|
-
message: "Security violation: Request blocked",
|
|
297
|
-
data: { violations },
|
|
298
|
-
},
|
|
299
|
-
};
|
|
300
|
-
process.stdout.write(JSON.stringify(errorResponse) + "\n");
|
|
267
|
+
process.stdout.write(buildErrorResponse(message, -32000, "Security violation: Request blocked", violations));
|
|
301
268
|
}
|
|
302
|
-
return;
|
|
269
|
+
return;
|
|
303
270
|
}
|
|
271
|
+
// monitor mode: log and warn but still forward
|
|
272
|
+
console.error('⚠️ MONITOR MODE — request forwarded (set mode: "block" to block)\n');
|
|
304
273
|
}
|
|
305
|
-
|
|
306
|
-
this.process.stdin.write(line + "\n");
|
|
307
|
-
}
|
|
274
|
+
this.process?.stdin?.write(line + "\n");
|
|
308
275
|
}
|
|
309
276
|
catch (err) {
|
|
310
277
|
this.logger.logEvent("PARSE_ERROR", "MEDIUM", {
|
|
311
278
|
error: err instanceof Error ? err.message : String(err),
|
|
312
279
|
line: line.substring(0, 100),
|
|
313
280
|
}, this.sessionId);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
281
|
+
this.process?.stdin?.write(line + "\n");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** Returns violations + whether to block (block only if mode==="block") */
|
|
285
|
+
inspectOutbound(message) {
|
|
286
|
+
const violations = [];
|
|
287
|
+
let shouldBlock = false;
|
|
288
|
+
if (message.method === "tools/call") {
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
this.toolCallTimestamps.push(now);
|
|
291
|
+
this.toolCallTimestamps = this.toolCallTimestamps.filter((t) => t > now - 60000);
|
|
292
|
+
if (!this.policy.checkRateLimit(this.toolCallTimestamps)) {
|
|
293
|
+
violations.push("Rate limit exceeded for tool calls");
|
|
294
|
+
shouldBlock = this.policy.isBlockingMode; // only hard-block in block mode
|
|
295
|
+
this.logger.logEvent("RATE_LIMIT_EXCEEDED", "HIGH", {
|
|
296
|
+
toolName: message.params?.name,
|
|
297
|
+
}, this.sessionId);
|
|
298
|
+
}
|
|
299
|
+
const paramsStr = JSON.stringify(message.params);
|
|
300
|
+
violations.push(...this.policy.checkPromptInjection(paramsStr));
|
|
301
|
+
violations.push(...this.policy.checkSensitiveData(paramsStr));
|
|
302
|
+
const filePathParams = [
|
|
303
|
+
message.params?.arguments?.path,
|
|
304
|
+
message.params?.arguments?.filePath,
|
|
305
|
+
message.params?.arguments?.file,
|
|
306
|
+
message.params?.arguments?.directory,
|
|
307
|
+
message.params?.path,
|
|
308
|
+
message.params?.filePath,
|
|
309
|
+
].filter((p) => typeof p === "string");
|
|
310
|
+
for (const fp of filePathParams) {
|
|
311
|
+
violations.push(...this.policy.checkFileAccess(fp));
|
|
312
|
+
}
|
|
313
|
+
this.logger.logEvent("TOOL_CALL", violations.length > 0 ? "HIGH" : "LOW", {
|
|
314
|
+
toolName: message.params?.name,
|
|
315
|
+
hasViolations: violations.length > 0,
|
|
316
|
+
violations,
|
|
317
|
+
}, this.sessionId);
|
|
318
|
+
// In blocking mode, any violation blocks
|
|
319
|
+
if (violations.length > 0 && this.policy.isBlockingMode) {
|
|
320
|
+
shouldBlock = true;
|
|
318
321
|
}
|
|
319
322
|
}
|
|
323
|
+
return { violations, shouldBlock };
|
|
320
324
|
}
|
|
321
325
|
handleServerOutput(output) {
|
|
322
|
-
// Buffer and parse for security scanning
|
|
323
326
|
this.serverMessageBuffer += output;
|
|
324
327
|
const lines = this.serverMessageBuffer.split("\n");
|
|
325
328
|
this.serverMessageBuffer = lines.pop() || "";
|
|
326
329
|
for (const line of lines) {
|
|
327
|
-
if (line.trim())
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}, this.sessionId);
|
|
341
|
-
console.error(`\n🚨 SENSITIVE DATA DETECTED IN RESPONSE:\n${violations.join("\n")}\n`);
|
|
330
|
+
if (!line.trim())
|
|
331
|
+
continue;
|
|
332
|
+
let shouldForward = true;
|
|
333
|
+
try {
|
|
334
|
+
const message = JSON.parse(line);
|
|
335
|
+
const violations = this.policy.checkSensitiveData(JSON.stringify(message.result || message));
|
|
336
|
+
if (violations.length > 0) {
|
|
337
|
+
this.logger.logEvent("SENSITIVE_DATA_LEAK", "CRITICAL", {
|
|
338
|
+
violations,
|
|
339
|
+
responseId: message.id,
|
|
340
|
+
}, this.sessionId);
|
|
341
|
+
console.error(`\n🚨 SENSITIVE DATA DETECTED IN RESPONSE:\n${violations.join("\n")}\n`);
|
|
342
|
+
if (this.policy.isBlockingMode) {
|
|
342
343
|
console.error("🚫 RESPONSE BLOCKED\n");
|
|
343
|
-
// Send sanitized error response instead
|
|
344
344
|
if (message.id !== undefined) {
|
|
345
|
-
|
|
346
|
-
jsonrpc: message.jsonrpc,
|
|
347
|
-
id: message.id,
|
|
348
|
-
error: {
|
|
349
|
-
code: -32001,
|
|
350
|
-
message: "Security violation: Response contains sensitive data",
|
|
351
|
-
data: { violations },
|
|
352
|
-
},
|
|
353
|
-
};
|
|
354
|
-
process.stdout.write(JSON.stringify(errorResponse) + "\n");
|
|
345
|
+
process.stdout.write(buildErrorResponse(message, -32001, "Security violation: Response contains sensitive data", violations));
|
|
355
346
|
}
|
|
356
347
|
shouldForward = false;
|
|
357
348
|
}
|
|
358
349
|
else {
|
|
359
|
-
|
|
360
|
-
id: message.id,
|
|
361
|
-
hasError: !!message.error,
|
|
362
|
-
}, this.sessionId);
|
|
350
|
+
console.error("⚠️ MONITOR MODE — response forwarded\n");
|
|
363
351
|
}
|
|
364
352
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
line: line.substring(0, 100),
|
|
353
|
+
else {
|
|
354
|
+
this.logger.logEvent("SERVER_RESPONSE", "LOW", {
|
|
355
|
+
id: message.id,
|
|
356
|
+
hasError: !!message.error,
|
|
370
357
|
}, this.sessionId);
|
|
371
358
|
}
|
|
372
|
-
// Forward the line if not blocked
|
|
373
|
-
if (shouldForward) {
|
|
374
|
-
process.stdout.write(line + "\n");
|
|
375
|
-
}
|
|
376
359
|
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
this.logger.logEvent("SERVER_PARSE_ERROR", "LOW", {
|
|
362
|
+
error: err instanceof Error ? err.message : String(err),
|
|
363
|
+
line: line.substring(0, 100),
|
|
364
|
+
}, this.sessionId);
|
|
365
|
+
}
|
|
366
|
+
if (shouldForward)
|
|
367
|
+
process.stdout.write(line + "\n");
|
|
377
368
|
}
|
|
378
369
|
}
|
|
379
370
|
}
|
|
380
371
|
exports.MCPSecurityWrapper = MCPSecurityWrapper;
|
|
372
|
+
// ─── SSE / HTTP Transport ────────────────────────────────────────────────────
|
|
373
|
+
/**
|
|
374
|
+
* SSE proxy: listens on `port`, forwards every request to `targetUrl`,
|
|
375
|
+
* scanning both request body (outbound) and response body (inbound).
|
|
376
|
+
*
|
|
377
|
+
* Supports:
|
|
378
|
+
* - Standard JSON-RPC over HTTP POST (MCP HTTP transport)
|
|
379
|
+
* - Server-Sent Events streams (MCP SSE transport)
|
|
380
|
+
*/
|
|
381
|
+
class MCPSSEProxy {
|
|
382
|
+
constructor(policy, logger) {
|
|
383
|
+
this.toolCallTimestamps = [];
|
|
384
|
+
this.policy = policy;
|
|
385
|
+
this.logger = logger;
|
|
386
|
+
this.sessionId = (0, crypto_1.createHash)("md5")
|
|
387
|
+
.update(Date.now().toString())
|
|
388
|
+
.digest("hex")
|
|
389
|
+
.substring(0, 8);
|
|
390
|
+
}
|
|
391
|
+
start(port, targetUrl) {
|
|
392
|
+
const target = new URL(targetUrl);
|
|
393
|
+
const isHttps = target.protocol === "https:";
|
|
394
|
+
const server = http.createServer((clientReq, clientRes) => {
|
|
395
|
+
// ── 1. Collect request body ──────────────────────────────────────────
|
|
396
|
+
const chunks = [];
|
|
397
|
+
clientReq.on("data", (chunk) => chunks.push(chunk));
|
|
398
|
+
clientReq.on("end", () => {
|
|
399
|
+
const rawBody = Buffer.concat(chunks);
|
|
400
|
+
const bodyStr = rawBody.toString();
|
|
401
|
+
// ── 2. Inspect request ──────────────────────────────────────────────
|
|
402
|
+
let violations = [];
|
|
403
|
+
let shouldBlock = false;
|
|
404
|
+
if (bodyStr) {
|
|
405
|
+
try {
|
|
406
|
+
const msg = JSON.parse(bodyStr);
|
|
407
|
+
const result = this.inspectOutbound(msg);
|
|
408
|
+
violations = result.violations;
|
|
409
|
+
shouldBlock = result.shouldBlock;
|
|
410
|
+
if (violations.length > 0) {
|
|
411
|
+
this.logger.logEvent("SECURITY_VIOLATION", "CRITICAL", {
|
|
412
|
+
violations,
|
|
413
|
+
blocked: shouldBlock,
|
|
414
|
+
}, this.sessionId);
|
|
415
|
+
console.error(`\n⚠️ SECURITY VIOLATIONS DETECTED:\n${violations.join("\n")}\n`);
|
|
416
|
+
}
|
|
417
|
+
if (shouldBlock) {
|
|
418
|
+
console.error("🚫 REQUEST BLOCKED\n");
|
|
419
|
+
clientRes.writeHead(403, { "Content-Type": "application/json" });
|
|
420
|
+
clientRes.end(JSON.stringify({
|
|
421
|
+
jsonrpc: "2.0",
|
|
422
|
+
id: msg.id,
|
|
423
|
+
error: {
|
|
424
|
+
code: -32000,
|
|
425
|
+
message: "Security violation: Request blocked",
|
|
426
|
+
data: { violations },
|
|
427
|
+
},
|
|
428
|
+
}));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// non-JSON body — forward as-is
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// ── 3. Forward to upstream ──────────────────────────────────────────
|
|
437
|
+
const options = {
|
|
438
|
+
hostname: target.hostname,
|
|
439
|
+
port: target.port || (isHttps ? 443 : 80),
|
|
440
|
+
path: target.pathname + clientReq.url?.replace(/^[^?]*/, "") ||
|
|
441
|
+
target.pathname,
|
|
442
|
+
method: clientReq.method,
|
|
443
|
+
headers: {
|
|
444
|
+
...clientReq.headers,
|
|
445
|
+
host: target.host,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
const lib = isHttps ? https : http;
|
|
449
|
+
const proxyReq = lib.request(options, (proxyRes) => {
|
|
450
|
+
const isSSE = proxyRes.headers["content-type"]?.includes("text/event-stream");
|
|
451
|
+
clientRes.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
452
|
+
if (isSSE) {
|
|
453
|
+
// Stream SSE events, scanning each data line
|
|
454
|
+
let sseBuffer = "";
|
|
455
|
+
proxyRes.on("data", (chunk) => {
|
|
456
|
+
sseBuffer += chunk.toString();
|
|
457
|
+
const lines = sseBuffer.split("\n");
|
|
458
|
+
sseBuffer = lines.pop() || "";
|
|
459
|
+
for (const line of lines) {
|
|
460
|
+
if (line.startsWith("data:")) {
|
|
461
|
+
const data = line.slice(5).trim();
|
|
462
|
+
try {
|
|
463
|
+
const msg = JSON.parse(data);
|
|
464
|
+
const leakViolations = this.policy.checkSensitiveData(JSON.stringify(msg.result || msg));
|
|
465
|
+
if (leakViolations.length > 0) {
|
|
466
|
+
this.logger.logEvent("SENSITIVE_DATA_LEAK", "CRITICAL", {
|
|
467
|
+
violations: leakViolations,
|
|
468
|
+
}, this.sessionId);
|
|
469
|
+
console.error(`\n🚨 SENSITIVE DATA IN SSE STREAM:\n${leakViolations.join("\n")}\n`);
|
|
470
|
+
if (this.policy.isBlockingMode) {
|
|
471
|
+
// Replace event with error event
|
|
472
|
+
clientRes.write(`data: ${JSON.stringify({
|
|
473
|
+
jsonrpc: "2.0",
|
|
474
|
+
id: msg.id,
|
|
475
|
+
error: {
|
|
476
|
+
code: -32001,
|
|
477
|
+
message: "Sensitive data blocked",
|
|
478
|
+
},
|
|
479
|
+
})}\n\n`);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
/* non-JSON SSE data */
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
clientRes.write(line + "\n");
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
proxyRes.on("end", () => {
|
|
492
|
+
if (sseBuffer)
|
|
493
|
+
clientRes.write(sseBuffer);
|
|
494
|
+
clientRes.end();
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
// Regular HTTP — collect response, scan, forward
|
|
499
|
+
const resChunks = [];
|
|
500
|
+
proxyRes.on("data", (chunk) => resChunks.push(chunk));
|
|
501
|
+
proxyRes.on("end", () => {
|
|
502
|
+
const resBody = Buffer.concat(resChunks).toString();
|
|
503
|
+
try {
|
|
504
|
+
const msg = JSON.parse(resBody);
|
|
505
|
+
const leakViolations = this.policy.checkSensitiveData(JSON.stringify(msg.result || msg));
|
|
506
|
+
if (leakViolations.length > 0) {
|
|
507
|
+
this.logger.logEvent("SENSITIVE_DATA_LEAK", "CRITICAL", {
|
|
508
|
+
violations: leakViolations,
|
|
509
|
+
}, this.sessionId);
|
|
510
|
+
if (this.policy.isBlockingMode && msg.id !== undefined) {
|
|
511
|
+
clientRes.end(JSON.stringify({
|
|
512
|
+
jsonrpc: "2.0",
|
|
513
|
+
id: msg.id,
|
|
514
|
+
error: {
|
|
515
|
+
code: -32001,
|
|
516
|
+
message: "Sensitive data blocked",
|
|
517
|
+
},
|
|
518
|
+
}));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
/* non-JSON */
|
|
525
|
+
}
|
|
526
|
+
clientRes.end(resBody);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
proxyReq.on("error", (err) => {
|
|
531
|
+
this.logger.logEvent("PROXY_ERROR", "HIGH", { error: err.message }, this.sessionId);
|
|
532
|
+
clientRes.writeHead(502);
|
|
533
|
+
clientRes.end("Bad Gateway");
|
|
534
|
+
});
|
|
535
|
+
if (rawBody.length > 0)
|
|
536
|
+
proxyReq.write(rawBody);
|
|
537
|
+
proxyReq.end();
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
server.listen(port, () => {
|
|
541
|
+
this.logger.logEvent("SSE_PROXY_START", "LOW", {
|
|
542
|
+
port,
|
|
543
|
+
targetUrl,
|
|
544
|
+
mode: this.policy.rawConfig.mode,
|
|
545
|
+
}, this.sessionId);
|
|
546
|
+
console.error(`🛡️ ContextGuard SSE/HTTP proxy listening on port ${port}`);
|
|
547
|
+
console.error(` → Forwarding to: ${targetUrl}`);
|
|
548
|
+
console.error(` → Mode: ${this.policy.rawConfig.mode}\n`);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
inspectOutbound(message) {
|
|
552
|
+
const violations = [];
|
|
553
|
+
let shouldBlock = false;
|
|
554
|
+
if (message.method === "tools/call") {
|
|
555
|
+
const now = Date.now();
|
|
556
|
+
this.toolCallTimestamps.push(now);
|
|
557
|
+
this.toolCallTimestamps = this.toolCallTimestamps.filter((t) => t > now - 60000);
|
|
558
|
+
if (!this.policy.checkRateLimit(this.toolCallTimestamps)) {
|
|
559
|
+
violations.push("Rate limit exceeded");
|
|
560
|
+
if (this.policy.isBlockingMode)
|
|
561
|
+
shouldBlock = true;
|
|
562
|
+
}
|
|
563
|
+
const paramsStr = JSON.stringify(message.params);
|
|
564
|
+
violations.push(...this.policy.checkPromptInjection(paramsStr));
|
|
565
|
+
violations.push(...this.policy.checkSensitiveData(paramsStr));
|
|
566
|
+
const filePathParams = [
|
|
567
|
+
message.params?.arguments?.path,
|
|
568
|
+
message.params?.arguments?.filePath,
|
|
569
|
+
].filter((p) => typeof p === "string");
|
|
570
|
+
for (const fp of filePathParams) {
|
|
571
|
+
violations.push(...this.policy.checkFileAccess(fp));
|
|
572
|
+
}
|
|
573
|
+
if (violations.length > 0 && this.policy.isBlockingMode)
|
|
574
|
+
shouldBlock = true;
|
|
575
|
+
}
|
|
576
|
+
return { violations, shouldBlock };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
exports.MCPSSEProxy = MCPSSEProxy;
|
|
580
|
+
// ─── CLI Entry Point ──────────────────────────────────────────────────────────
|
|
381
581
|
async function main() {
|
|
382
582
|
const args = process.argv.slice(2);
|
|
383
583
|
if (args.length === 0 || args.includes("--help")) {
|
|
384
584
|
console.log(`
|
|
385
|
-
MCP Security Wrapper
|
|
585
|
+
MCP Security Wrapper
|
|
386
586
|
|
|
387
587
|
Usage:
|
|
588
|
+
# stdio transport (Claude Desktop)
|
|
388
589
|
contextguard --server "node server.js" --config config.json
|
|
389
590
|
|
|
591
|
+
# SSE/HTTP transport proxy
|
|
592
|
+
contextguard --transport sse --port 3100 --target http://localhost:3000 --config config.json
|
|
593
|
+
|
|
390
594
|
Options:
|
|
391
|
-
--server <
|
|
392
|
-
--
|
|
393
|
-
--
|
|
595
|
+
--server <cmd> Command to start MCP server (stdio mode)
|
|
596
|
+
--transport <type> stdio | sse | http (default: stdio)
|
|
597
|
+
--port <n> Proxy listen port (SSE/HTTP mode, default: 3100)
|
|
598
|
+
--target <url> Upstream server URL (SSE/HTTP mode)
|
|
599
|
+
--config <file> Path to security config JSON (optional)
|
|
600
|
+
--help Show this help
|
|
394
601
|
|
|
602
|
+
Config options (config.json):
|
|
603
|
+
mode "monitor" (log only) | "block" (block + log) default: monitor
|
|
604
|
+
maxToolCallsPerMinute number default: 30
|
|
605
|
+
enablePromptInjectionDetection boolean default: true
|
|
606
|
+
enableSensitiveDataDetection boolean default: true
|
|
607
|
+
enablePathTraversalPrevention boolean default: true
|
|
608
|
+
allowedFilePaths string[]
|
|
609
|
+
logPath string default: ./mcp_security.log
|
|
395
610
|
`);
|
|
396
611
|
process.exit(0);
|
|
397
612
|
}
|
|
398
613
|
let serverCommand = "";
|
|
399
614
|
let configFile = "";
|
|
615
|
+
let transport = "stdio";
|
|
616
|
+
let port = 3100;
|
|
617
|
+
let targetUrl = "";
|
|
400
618
|
for (let i = 0; i < args.length; i++) {
|
|
401
619
|
if (args[i] === "--server" && args[i + 1]) {
|
|
402
|
-
serverCommand = args[i
|
|
403
|
-
i++;
|
|
620
|
+
serverCommand = args[++i];
|
|
404
621
|
}
|
|
405
622
|
else if (args[i] === "--config" && args[i + 1]) {
|
|
406
|
-
configFile = args[i
|
|
407
|
-
|
|
623
|
+
configFile = args[++i];
|
|
624
|
+
}
|
|
625
|
+
else if (args[i] === "--transport" && args[i + 1]) {
|
|
626
|
+
transport = args[++i];
|
|
627
|
+
}
|
|
628
|
+
else if (args[i] === "--port" && args[i + 1]) {
|
|
629
|
+
port = parseInt(args[++i], 10);
|
|
630
|
+
}
|
|
631
|
+
else if (args[i] === "--target" && args[i + 1]) {
|
|
632
|
+
targetUrl = args[++i];
|
|
408
633
|
}
|
|
409
|
-
}
|
|
410
|
-
if (!serverCommand) {
|
|
411
|
-
console.error("Error: --server argument is required");
|
|
412
|
-
process.exit(1);
|
|
413
634
|
}
|
|
414
635
|
let config = {};
|
|
415
636
|
if (configFile && fs.existsSync(configFile)) {
|
|
416
637
|
config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
417
638
|
}
|
|
639
|
+
// CLI flags override config file
|
|
640
|
+
if (transport !== "stdio")
|
|
641
|
+
config.transport = transport;
|
|
642
|
+
if (port !== 3100)
|
|
643
|
+
config.port = port;
|
|
644
|
+
if (targetUrl)
|
|
645
|
+
config.targetUrl = targetUrl;
|
|
418
646
|
const policy = new SecurityPolicy(config);
|
|
419
647
|
const logger = new SecurityLogger(config.logPath);
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
648
|
+
const effectiveTransport = config.transport || transport;
|
|
649
|
+
if (effectiveTransport === "sse" || effectiveTransport === "http") {
|
|
650
|
+
if (!config.targetUrl && !targetUrl) {
|
|
651
|
+
console.error("Error: --target <url> is required for SSE/HTTP transport");
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
const proxy = new MCPSSEProxy(policy, logger);
|
|
655
|
+
proxy.start(config.port || port, config.targetUrl || targetUrl);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
if (!serverCommand) {
|
|
659
|
+
console.error("Error: --server argument is required for stdio transport");
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
const wrapper = new MCPSecurityWrapper(serverCommand.split(" "), policy, logger);
|
|
663
|
+
await wrapper.start();
|
|
664
|
+
}
|
|
423
665
|
}
|
|
424
666
|
if (require.main === module) {
|
|
425
667
|
main().catch((err) => {
|