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
package/dist/init.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Amir Mironi
|
|
4
|
+
*
|
|
5
|
+
* contextguard init — automatically patch claude_desktop_config.json
|
|
6
|
+
* so that every MCP server is wrapped with ContextGuard.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.runInit = runInit;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
// ── Config file detection ────────────────────────────────────────────────────
|
|
47
|
+
function findConfigPath() {
|
|
48
|
+
const home = os.homedir();
|
|
49
|
+
let candidates = [];
|
|
50
|
+
switch (process.platform) {
|
|
51
|
+
case "darwin":
|
|
52
|
+
candidates = [
|
|
53
|
+
path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
54
|
+
];
|
|
55
|
+
break;
|
|
56
|
+
case "win32":
|
|
57
|
+
candidates = [
|
|
58
|
+
path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json"),
|
|
59
|
+
];
|
|
60
|
+
break;
|
|
61
|
+
default: // Linux
|
|
62
|
+
candidates = [
|
|
63
|
+
path.join(home, ".config", "Claude", "claude_desktop_config.json"),
|
|
64
|
+
path.join(home, ".config", "claude", "claude_desktop_config.json"),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
return candidates.find((p) => fs.existsSync(p)) ?? null;
|
|
68
|
+
}
|
|
69
|
+
// ── Wrapping helpers ─────────────────────────────────────────────────────────
|
|
70
|
+
function isAlreadyWrapped(server) {
|
|
71
|
+
if (server.command === "npx" && server.args?.[1] === "contextguard") {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (server.command === "contextguard") {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Wrap an MCP server entry:
|
|
81
|
+
* { command, args } → { command: "npx", args: ["-y", "contextguard", "--", <original command+args>] }
|
|
82
|
+
*/
|
|
83
|
+
function wrapServer(server, apiKey) {
|
|
84
|
+
const originalCmd = [server.command, ...(server.args ?? [])];
|
|
85
|
+
return {
|
|
86
|
+
command: "npx",
|
|
87
|
+
args: ["-y", "contextguard", "--", ...originalCmd],
|
|
88
|
+
env: {
|
|
89
|
+
...server.env,
|
|
90
|
+
CONTEXTGUARD_API_KEY: apiKey,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function parseInitArgs(argv) {
|
|
95
|
+
let apiKey = "";
|
|
96
|
+
let configPath;
|
|
97
|
+
let dryRun = false;
|
|
98
|
+
for (let i = 0; i < argv.length; i++) {
|
|
99
|
+
const arg = argv[i];
|
|
100
|
+
if ((arg === "--api-key" || arg === "-k") && argv[i + 1]) {
|
|
101
|
+
apiKey = argv[++i];
|
|
102
|
+
}
|
|
103
|
+
else if ((arg === "--config" || arg === "-c") && argv[i + 1]) {
|
|
104
|
+
configPath = argv[++i];
|
|
105
|
+
}
|
|
106
|
+
else if (arg === "--dry-run") {
|
|
107
|
+
dryRun = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { apiKey, configPath, dryRun };
|
|
111
|
+
}
|
|
112
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
113
|
+
function runInit(argv) {
|
|
114
|
+
const { apiKey, configPath: customPath, dryRun } = parseInitArgs(argv);
|
|
115
|
+
if (!apiKey) {
|
|
116
|
+
console.error("\n❌ Missing required argument: --api-key <key>\n" +
|
|
117
|
+
" Get your key from the ContextGuard dashboard → Settings → API Keys\n");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
// Locate config
|
|
121
|
+
const configPath = customPath ?? findConfigPath();
|
|
122
|
+
if (!configPath) {
|
|
123
|
+
console.error("\n❌ Could not find claude_desktop_config.json.\n" +
|
|
124
|
+
" Is Claude Desktop installed? Or specify the path with --config <path>\n");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
console.log(`\n🔍 Config found: ${configPath}`);
|
|
128
|
+
// Read and parse
|
|
129
|
+
let config;
|
|
130
|
+
try {
|
|
131
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.error(`\n❌ Failed to parse config: ${configPath}`);
|
|
135
|
+
console.error(` Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
const servers = { ...(config.mcpServers ?? {}) };
|
|
139
|
+
const names = Object.keys(servers);
|
|
140
|
+
if (names.length === 0) {
|
|
141
|
+
console.log("\n⚠️ No MCP servers found in config — nothing to wrap.\n");
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
console.log(`\n📋 Found ${names.length} MCP server(s):\n`);
|
|
145
|
+
let wrapped = 0;
|
|
146
|
+
let skipped = 0;
|
|
147
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
148
|
+
if (isAlreadyWrapped(server)) {
|
|
149
|
+
console.log(` ⏭ ${name} (already wrapped, skipping)`);
|
|
150
|
+
skipped++;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
servers[name] = wrapServer(server, apiKey);
|
|
154
|
+
console.log(` ✅ ${name} → wrapped`);
|
|
155
|
+
wrapped++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
console.log(`\n📝 ${wrapped} wrapped, ${skipped} already configured.\n`);
|
|
159
|
+
if (wrapped === 0) {
|
|
160
|
+
console.log("✨ Nothing to do — all servers are already protected.\n");
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
const updated = { ...config, mcpServers: servers };
|
|
164
|
+
if (dryRun) {
|
|
165
|
+
console.log("⚠️ --dry-run: no changes written. Preview:\n");
|
|
166
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
// Backup
|
|
170
|
+
const backupPath = configPath + ".bak";
|
|
171
|
+
fs.copyFileSync(configPath, backupPath);
|
|
172
|
+
console.log(`💾 Backup saved: ${backupPath}`);
|
|
173
|
+
// Write
|
|
174
|
+
fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
|
|
175
|
+
console.log(`✅ Config updated: ${configPath}`);
|
|
176
|
+
console.log("\n🚀 Done! Restart Claude Desktop to activate ContextGuard.\n" +
|
|
177
|
+
" Dashboard: https://contextguard.dev/dashboard\n");
|
|
178
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Amir Mironi
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentPolicyRow, SecurityEventRow } from "../types/database.types";
|
|
8
|
+
/**
|
|
9
|
+
* Supabase configuration
|
|
10
|
+
*/
|
|
11
|
+
export interface SupabaseConfig {
|
|
12
|
+
url: string;
|
|
13
|
+
serviceKey: string;
|
|
14
|
+
agentId?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Supabase client for agent-dashboard communication
|
|
18
|
+
*/
|
|
19
|
+
export interface SupabaseClient {
|
|
20
|
+
reportEvent: (event: SecurityEventRow) => Promise<void>;
|
|
21
|
+
fetchPolicy: (agentId: string) => Promise<AgentPolicyRow | null>;
|
|
22
|
+
updateAgentStatus: (agentId: string, status: string) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a Supabase client
|
|
26
|
+
*/
|
|
27
|
+
export declare const createSupabaseClient: (config: SupabaseConfig) => SupabaseClient;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Amir Mironi
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createSupabaseClient = void 0;
|
|
10
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
11
|
+
/**
|
|
12
|
+
* Create a Supabase client
|
|
13
|
+
*/
|
|
14
|
+
const createSupabaseClient = (config) => {
|
|
15
|
+
const { url, serviceKey } = config;
|
|
16
|
+
// Create Supabase client using the official SDK
|
|
17
|
+
const supabase = (0, supabase_js_1.createClient)(url, serviceKey, {
|
|
18
|
+
auth: {
|
|
19
|
+
persistSession: false,
|
|
20
|
+
autoRefreshToken: false,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
/**
|
|
25
|
+
* Report a security event to Supabase
|
|
26
|
+
*/
|
|
27
|
+
reportEvent: async (event) => {
|
|
28
|
+
try {
|
|
29
|
+
const eventData = {
|
|
30
|
+
agent_id: config.agentId || "unknown",
|
|
31
|
+
event_type: event.event_type,
|
|
32
|
+
severity: event.severity,
|
|
33
|
+
details: event.details,
|
|
34
|
+
session_id: event.session_id,
|
|
35
|
+
timestamp: event.timestamp,
|
|
36
|
+
};
|
|
37
|
+
const { error } = await supabase
|
|
38
|
+
.from("events")
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
.insert(eventData);
|
|
41
|
+
if (error) {
|
|
42
|
+
console.error("Failed to report event to Supabase:", error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error("Failed to report event to Supabase:", error);
|
|
47
|
+
// Don't throw - we don't want to break the agent if reporting fails
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* Fetch policy configuration for an agent
|
|
52
|
+
*/
|
|
53
|
+
fetchPolicy: async (agentId) => {
|
|
54
|
+
try {
|
|
55
|
+
const { data, error } = await supabase
|
|
56
|
+
.from("policies")
|
|
57
|
+
.select("*")
|
|
58
|
+
.eq("agent_id", agentId)
|
|
59
|
+
.single();
|
|
60
|
+
if (error) {
|
|
61
|
+
console.error("Failed to fetch policy from Supabase:", error);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return data || null;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error("Failed to fetch policy from Supabase:", error);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Update agent status
|
|
73
|
+
*/
|
|
74
|
+
updateAgentStatus: async (agentId, status) => {
|
|
75
|
+
try {
|
|
76
|
+
const statusData = {
|
|
77
|
+
agent_id: agentId,
|
|
78
|
+
status: status,
|
|
79
|
+
last_heartbeat: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
const { error } = await supabase
|
|
82
|
+
.from("agents")
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
+
.upsert(statusData, {
|
|
85
|
+
onConflict: "agent_id",
|
|
86
|
+
});
|
|
87
|
+
if (error) {
|
|
88
|
+
console.error("Failed to update agent status:", error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error("Failed to update agent status:", error);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
exports.createSupabaseClient = createSupabaseClient;
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Amir Mironi
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { SupabaseClient } from "./lib/supabase-client";
|
|
8
|
+
import { SecurityEventRow, SecuritySeverity } from "./types/database.types";
|
|
9
|
+
/**
|
|
10
|
+
* Security statistics interface
|
|
11
|
+
*/
|
|
12
|
+
export interface SecurityStatistics {
|
|
13
|
+
totalEvents: number;
|
|
14
|
+
eventsByType: Record<string, number>;
|
|
15
|
+
eventsBySeverity: Record<string, number>;
|
|
16
|
+
recentEvents: SecurityEventRow[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Logger interface
|
|
20
|
+
*/
|
|
21
|
+
export interface Logger {
|
|
22
|
+
logEvent: (eventType: string, severity: SecuritySeverity, details: Record<string, unknown>, sessionId: string) => void;
|
|
23
|
+
getStatistics: () => SecurityStatistics;
|
|
24
|
+
clearEvents: () => void;
|
|
25
|
+
getAllEvents: () => SecurityEventRow[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a security event logger
|
|
29
|
+
* @param logFile - Path to log file
|
|
30
|
+
* @param supabaseClient - Optional Supabase client for remote logging
|
|
31
|
+
* @param agentId - Agent identifier for event attribution
|
|
32
|
+
* @param alertWebhook - Optional webhook URL for HIGH/CRITICAL alerts
|
|
33
|
+
* @param alertOnSeverity - Severity levels that trigger webhook alerts
|
|
34
|
+
* @returns Logger functions
|
|
35
|
+
*/
|
|
36
|
+
export declare const createLogger: (logFile?: string, supabaseClient?: SupabaseClient, agentId?: string, alertWebhook?: string, alertOnSeverity?: string[]) => Logger;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Amir Mironi
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.createLogger = void 0;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const MAX_STORED_EVENTS = 1000;
|
|
45
|
+
/**
|
|
46
|
+
* Count events by a specific field
|
|
47
|
+
*/
|
|
48
|
+
const countByField = (events, field) => {
|
|
49
|
+
const counts = {};
|
|
50
|
+
for (const event of events) {
|
|
51
|
+
const value = String(event[field]);
|
|
52
|
+
counts[value] = (counts[value] || 0) + 1;
|
|
53
|
+
}
|
|
54
|
+
return counts;
|
|
55
|
+
};
|
|
56
|
+
const fireWebhookAlert = (webhookUrl, event) => {
|
|
57
|
+
fetch(webhookUrl, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
text: `[ContextGuard] ${event.severity} — ${event.event_type}`,
|
|
62
|
+
agent_id: event.agent_id,
|
|
63
|
+
session_id: event.session_id,
|
|
64
|
+
details: event.details,
|
|
65
|
+
timestamp: event.timestamp,
|
|
66
|
+
}),
|
|
67
|
+
}).catch((err) => {
|
|
68
|
+
console.error("Failed to fire webhook alert:", err);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Create a security event logger
|
|
73
|
+
* @param logFile - Path to log file
|
|
74
|
+
* @param supabaseClient - Optional Supabase client for remote logging
|
|
75
|
+
* @param agentId - Agent identifier for event attribution
|
|
76
|
+
* @param alertWebhook - Optional webhook URL for HIGH/CRITICAL alerts
|
|
77
|
+
* @param alertOnSeverity - Severity levels that trigger webhook alerts
|
|
78
|
+
* @returns Logger functions
|
|
79
|
+
*/
|
|
80
|
+
const createLogger = (logFile = "mcp_security.log", supabaseClient, agentId = "", alertWebhook, alertOnSeverity = ["HIGH", "CRITICAL"]) => {
|
|
81
|
+
let events = [];
|
|
82
|
+
return {
|
|
83
|
+
/**
|
|
84
|
+
* Log a security event
|
|
85
|
+
*/
|
|
86
|
+
logEvent: (eventType, severity, details, sessionId) => {
|
|
87
|
+
const event = {
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
event_type: eventType,
|
|
90
|
+
severity,
|
|
91
|
+
details,
|
|
92
|
+
session_id: sessionId,
|
|
93
|
+
agent_id: agentId,
|
|
94
|
+
created_at: new Date().toISOString(),
|
|
95
|
+
id: "",
|
|
96
|
+
};
|
|
97
|
+
events.push(event);
|
|
98
|
+
// Keep only recent events in memory
|
|
99
|
+
if (events.length > MAX_STORED_EVENTS) {
|
|
100
|
+
events = events.slice(-MAX_STORED_EVENTS);
|
|
101
|
+
}
|
|
102
|
+
// Write to log file
|
|
103
|
+
try {
|
|
104
|
+
fs.appendFileSync(logFile, JSON.stringify(event) + "\n");
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error("Failed to write to log file:", error);
|
|
108
|
+
}
|
|
109
|
+
// Alert on high/critical severity
|
|
110
|
+
if (severity === "HIGH" || severity === "CRITICAL") {
|
|
111
|
+
console.error(`[SECURITY ALERT] ${eventType}: ${JSON.stringify(details)}`);
|
|
112
|
+
}
|
|
113
|
+
// Fire webhook alert if configured and severity matches
|
|
114
|
+
if (alertWebhook && alertOnSeverity.includes(severity)) {
|
|
115
|
+
fireWebhookAlert(alertWebhook, event);
|
|
116
|
+
}
|
|
117
|
+
// Report to Supabase if client is provided
|
|
118
|
+
if (supabaseClient) {
|
|
119
|
+
supabaseClient.reportEvent(event).catch((err) => {
|
|
120
|
+
console.error("Failed to report event to Supabase:", err);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
/**
|
|
125
|
+
* Get security statistics
|
|
126
|
+
*/
|
|
127
|
+
getStatistics: () => ({
|
|
128
|
+
totalEvents: events.length,
|
|
129
|
+
eventsByType: countByField(events, "event_type"),
|
|
130
|
+
eventsBySeverity: countByField(events, "severity"),
|
|
131
|
+
recentEvents: events.slice(-10),
|
|
132
|
+
}),
|
|
133
|
+
/**
|
|
134
|
+
* Clear all logged events
|
|
135
|
+
*/
|
|
136
|
+
clearEvents: () => {
|
|
137
|
+
events = [];
|
|
138
|
+
},
|
|
139
|
+
/**
|
|
140
|
+
* Get all events
|
|
141
|
+
*/
|
|
142
|
+
getAllEvents: () => [...events],
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
exports.createLogger = createLogger;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Amir Mironi
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
interface SecurityConfig {
|
|
9
|
+
maxToolCallsPerMinute?: number;
|
|
10
|
+
enablePromptInjectionDetection?: boolean;
|
|
11
|
+
enableSensitiveDataDetection?: boolean;
|
|
12
|
+
enablePathTraversalPrevention?: boolean;
|
|
13
|
+
mode?: "monitor" | "block";
|
|
14
|
+
blockedPatterns?: string[];
|
|
15
|
+
allowedFilePaths?: string[];
|
|
16
|
+
alertThreshold?: number;
|
|
17
|
+
logPath?: string;
|
|
18
|
+
logLevel?: "debug" | "info" | "warn" | "error";
|
|
19
|
+
transport?: "stdio" | "sse" | "http";
|
|
20
|
+
port?: number;
|
|
21
|
+
targetUrl?: string;
|
|
22
|
+
}
|
|
23
|
+
interface SecurityEvent {
|
|
24
|
+
timestamp: string;
|
|
25
|
+
eventType: string;
|
|
26
|
+
severity: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
|
|
27
|
+
details: Record<string, unknown>;
|
|
28
|
+
sessionId: string;
|
|
29
|
+
}
|
|
30
|
+
declare class SecurityPolicy {
|
|
31
|
+
private config;
|
|
32
|
+
private sensitiveDataPatterns;
|
|
33
|
+
private promptInjectionPatterns;
|
|
34
|
+
constructor(config: SecurityConfig);
|
|
35
|
+
get isBlockingMode(): boolean;
|
|
36
|
+
get rawConfig(): Required<SecurityConfig>;
|
|
37
|
+
checkPromptInjection(text: string): string[];
|
|
38
|
+
checkSensitiveData(text: string): string[];
|
|
39
|
+
checkFileAccess(filePath: string): string[];
|
|
40
|
+
checkRateLimit(timestamps: number[]): boolean;
|
|
41
|
+
}
|
|
42
|
+
declare class SecurityLogger {
|
|
43
|
+
private logFile;
|
|
44
|
+
private events;
|
|
45
|
+
constructor(logFile?: string);
|
|
46
|
+
logEvent(eventType: string, severity: SecurityEvent["severity"], details: Record<string, unknown>, sessionId: string): void;
|
|
47
|
+
getStatistics(): Record<string, unknown>;
|
|
48
|
+
private countByField;
|
|
49
|
+
}
|
|
50
|
+
declare class MCPSecurityWrapper {
|
|
51
|
+
private serverCommand;
|
|
52
|
+
private policy;
|
|
53
|
+
private logger;
|
|
54
|
+
private process;
|
|
55
|
+
private toolCallTimestamps;
|
|
56
|
+
private sessionId;
|
|
57
|
+
private clientMessageBuffer;
|
|
58
|
+
private serverMessageBuffer;
|
|
59
|
+
constructor(serverCommand: string[], policy: SecurityPolicy, logger: SecurityLogger);
|
|
60
|
+
start(): Promise<void>;
|
|
61
|
+
private handleClientInput;
|
|
62
|
+
private processClientMessage;
|
|
63
|
+
/** Returns violations + whether to block (block only if mode==="block") */
|
|
64
|
+
private inspectOutbound;
|
|
65
|
+
private handleServerOutput;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* SSE proxy: listens on `port`, forwards every request to `targetUrl`,
|
|
69
|
+
* scanning both request body (outbound) and response body (inbound).
|
|
70
|
+
*
|
|
71
|
+
* Supports:
|
|
72
|
+
* - Standard JSON-RPC over HTTP POST (MCP HTTP transport)
|
|
73
|
+
* - Server-Sent Events streams (MCP SSE transport)
|
|
74
|
+
*/
|
|
75
|
+
declare class MCPSSEProxy {
|
|
76
|
+
private policy;
|
|
77
|
+
private logger;
|
|
78
|
+
private sessionId;
|
|
79
|
+
private toolCallTimestamps;
|
|
80
|
+
constructor(policy: SecurityPolicy, logger: SecurityLogger);
|
|
81
|
+
start(port: number, targetUrl: string): void;
|
|
82
|
+
private inspectOutbound;
|
|
83
|
+
}
|
|
84
|
+
export { MCPSecurityWrapper, MCPSSEProxy, SecurityPolicy, SecurityLogger };
|