contextguard 0.1.7 → 0.2.0
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 +369 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +266 -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 +173 -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 +394 -120
- 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 -10
- 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 -529
- 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,157 +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
|
-
// Forward output immediately
|
|
323
|
-
process.stdout.write(output);
|
|
324
|
-
// Buffer and parse for logging
|
|
325
326
|
this.serverMessageBuffer += output;
|
|
326
327
|
const lines = this.serverMessageBuffer.split("\n");
|
|
327
328
|
this.serverMessageBuffer = lines.pop() || "";
|
|
328
329
|
for (const line of lines) {
|
|
329
|
-
if (line.trim())
|
|
330
|
-
|
|
331
|
-
|
|
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) {
|
|
343
|
+
console.error("🚫 RESPONSE BLOCKED\n");
|
|
344
|
+
if (message.id !== undefined) {
|
|
345
|
+
process.stdout.write(buildErrorResponse(message, -32001, "Security violation: Response contains sensitive data", violations));
|
|
346
|
+
}
|
|
347
|
+
shouldForward = false;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
console.error("⚠️ MONITOR MODE — response forwarded\n");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
332
354
|
this.logger.logEvent("SERVER_RESPONSE", "LOW", {
|
|
333
355
|
id: message.id,
|
|
334
356
|
hasError: !!message.error,
|
|
335
357
|
}, this.sessionId);
|
|
336
358
|
}
|
|
337
|
-
catch (err) {
|
|
338
|
-
// Log parse errors for server output
|
|
339
|
-
this.logger.logEvent("SERVER_PARSE_ERROR", "LOW", {
|
|
340
|
-
error: err instanceof Error ? err.message : String(err),
|
|
341
|
-
line: line.substring(0, 100),
|
|
342
|
-
}, this.sessionId);
|
|
343
|
-
}
|
|
344
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");
|
|
345
368
|
}
|
|
346
369
|
}
|
|
347
370
|
}
|
|
348
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 ──────────────────────────────────────────────────────────
|
|
349
581
|
async function main() {
|
|
350
582
|
const args = process.argv.slice(2);
|
|
351
583
|
if (args.length === 0 || args.includes("--help")) {
|
|
352
584
|
console.log(`
|
|
353
|
-
MCP Security Wrapper
|
|
585
|
+
MCP Security Wrapper
|
|
354
586
|
|
|
355
587
|
Usage:
|
|
588
|
+
# stdio transport (Claude Desktop)
|
|
356
589
|
contextguard --server "node server.js" --config config.json
|
|
357
590
|
|
|
591
|
+
# SSE/HTTP transport proxy
|
|
592
|
+
contextguard --transport sse --port 3100 --target http://localhost:3000 --config config.json
|
|
593
|
+
|
|
358
594
|
Options:
|
|
359
|
-
--server <
|
|
360
|
-
--
|
|
361
|
-
--
|
|
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
|
|
362
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
|
|
363
610
|
`);
|
|
364
611
|
process.exit(0);
|
|
365
612
|
}
|
|
366
613
|
let serverCommand = "";
|
|
367
614
|
let configFile = "";
|
|
615
|
+
let transport = "stdio";
|
|
616
|
+
let port = 3100;
|
|
617
|
+
let targetUrl = "";
|
|
368
618
|
for (let i = 0; i < args.length; i++) {
|
|
369
619
|
if (args[i] === "--server" && args[i + 1]) {
|
|
370
|
-
serverCommand = args[i
|
|
371
|
-
i++;
|
|
620
|
+
serverCommand = args[++i];
|
|
372
621
|
}
|
|
373
622
|
else if (args[i] === "--config" && args[i + 1]) {
|
|
374
|
-
configFile = args[i
|
|
375
|
-
|
|
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];
|
|
376
633
|
}
|
|
377
|
-
}
|
|
378
|
-
if (!serverCommand) {
|
|
379
|
-
console.error("Error: --server argument is required");
|
|
380
|
-
process.exit(1);
|
|
381
634
|
}
|
|
382
635
|
let config = {};
|
|
383
636
|
if (configFile && fs.existsSync(configFile)) {
|
|
384
637
|
config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
385
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;
|
|
386
646
|
const policy = new SecurityPolicy(config);
|
|
387
647
|
const logger = new SecurityLogger(config.logPath);
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}
|
|
391
665
|
}
|
|
392
666
|
if (require.main === module) {
|
|
393
667
|
main().catch((err) => {
|