sovr-mcp-proxy 7.0.0 → 7.1.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/dist/auditDashboard.d.mts +208 -0
- package/dist/auditDashboard.d.ts +208 -0
- package/dist/auditDashboard.js +398 -0
- package/dist/auditDashboard.mjs +370 -0
- package/dist/mcpProxyInterceptor.d.mts +256 -0
- package/dist/mcpProxyInterceptor.d.ts +256 -0
- package/dist/mcpProxyInterceptor.js +579 -0
- package/dist/mcpProxyInterceptor.mjs +552 -0
- package/dist/semanticAnalyzer.d.mts +133 -0
- package/dist/semanticAnalyzer.d.ts +133 -0
- package/dist/semanticAnalyzer.js +701 -0
- package/dist/semanticAnalyzer.mjs +674 -0
- package/dist/teamPolicyManager.d.mts +202 -0
- package/dist/teamPolicyManager.d.ts +202 -0
- package/dist/teamPolicyManager.js +529 -0
- package/dist/teamPolicyManager.mjs +502 -0
- package/package.json +2 -2
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
// src/mcpProxyInterceptor.ts
|
|
2
|
+
var SlidingWindowRateLimiter = class {
|
|
3
|
+
windows = /* @__PURE__ */ new Map();
|
|
4
|
+
windowSizeMs = 6e4;
|
|
5
|
+
// 1 minute
|
|
6
|
+
check(key, limit) {
|
|
7
|
+
const now = Date.now();
|
|
8
|
+
const cutoff = now - this.windowSizeMs;
|
|
9
|
+
const timestamps = this.windows.get(key) || [];
|
|
10
|
+
const recent = timestamps.filter((t) => t > cutoff);
|
|
11
|
+
this.windows.set(key, recent);
|
|
12
|
+
return recent.length < limit;
|
|
13
|
+
}
|
|
14
|
+
record(key) {
|
|
15
|
+
const timestamps = this.windows.get(key) || [];
|
|
16
|
+
timestamps.push(Date.now());
|
|
17
|
+
this.windows.set(key, timestamps);
|
|
18
|
+
}
|
|
19
|
+
getCount(key) {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const cutoff = now - this.windowSizeMs;
|
|
22
|
+
const timestamps = this.windows.get(key) || [];
|
|
23
|
+
return timestamps.filter((t) => t > cutoff).length;
|
|
24
|
+
}
|
|
25
|
+
cleanup() {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const cutoff = now - this.windowSizeMs;
|
|
28
|
+
for (const [key, timestamps] of this.windows) {
|
|
29
|
+
const recent = timestamps.filter((t) => t > cutoff);
|
|
30
|
+
if (recent.length === 0) {
|
|
31
|
+
this.windows.delete(key);
|
|
32
|
+
} else {
|
|
33
|
+
this.windows.set(key, recent);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function matchGlob(pattern, text) {
|
|
39
|
+
const regex = new RegExp(
|
|
40
|
+
"^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
|
|
41
|
+
);
|
|
42
|
+
return regex.test(text);
|
|
43
|
+
}
|
|
44
|
+
function evaluateCondition(condition, args) {
|
|
45
|
+
const value = getNestedValue(args, condition.field);
|
|
46
|
+
switch (condition.operator) {
|
|
47
|
+
case "exists":
|
|
48
|
+
return value !== void 0 && value !== null;
|
|
49
|
+
case "not-exists":
|
|
50
|
+
return value === void 0 || value === null;
|
|
51
|
+
case "equals":
|
|
52
|
+
return value === condition.value;
|
|
53
|
+
case "contains":
|
|
54
|
+
return typeof value === "string" && value.includes(String(condition.value));
|
|
55
|
+
case "matches":
|
|
56
|
+
return typeof value === "string" && new RegExp(String(condition.value)).test(value);
|
|
57
|
+
case "gt":
|
|
58
|
+
return typeof value === "number" && value > Number(condition.value);
|
|
59
|
+
case "lt":
|
|
60
|
+
return typeof value === "number" && value < Number(condition.value);
|
|
61
|
+
default:
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function getNestedValue(obj, path) {
|
|
66
|
+
return path.split(".").reduce((current, key) => {
|
|
67
|
+
if (current && typeof current === "object" && key in current) {
|
|
68
|
+
return current[key];
|
|
69
|
+
}
|
|
70
|
+
return void 0;
|
|
71
|
+
}, obj);
|
|
72
|
+
}
|
|
73
|
+
function applyTransform(args, transform) {
|
|
74
|
+
const result = JSON.parse(JSON.stringify(args));
|
|
75
|
+
const parts = transform.field.split(".");
|
|
76
|
+
const lastKey = parts.pop();
|
|
77
|
+
let target = result;
|
|
78
|
+
for (const part of parts) {
|
|
79
|
+
if (target[part] && typeof target[part] === "object") {
|
|
80
|
+
target = target[part];
|
|
81
|
+
} else {
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
switch (transform.type) {
|
|
86
|
+
case "redact":
|
|
87
|
+
target[lastKey] = "[REDACTED]";
|
|
88
|
+
break;
|
|
89
|
+
case "mask":
|
|
90
|
+
if (typeof target[lastKey] === "string") {
|
|
91
|
+
const val = target[lastKey];
|
|
92
|
+
target[lastKey] = val.slice(0, 2) + "*".repeat(Math.max(val.length - 4, 0)) + val.slice(-2);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case "replace":
|
|
96
|
+
target[lastKey] = transform.replacement ?? "";
|
|
97
|
+
break;
|
|
98
|
+
case "remove":
|
|
99
|
+
delete target[lastKey];
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
var HIGH_RISK_TOOLS = /* @__PURE__ */ new Set([
|
|
105
|
+
"bash",
|
|
106
|
+
"shell",
|
|
107
|
+
"exec",
|
|
108
|
+
"run_command",
|
|
109
|
+
"execute",
|
|
110
|
+
"write_file",
|
|
111
|
+
"delete_file",
|
|
112
|
+
"move_file",
|
|
113
|
+
"sql_query",
|
|
114
|
+
"database_query",
|
|
115
|
+
"send_email",
|
|
116
|
+
"send_message",
|
|
117
|
+
"deploy",
|
|
118
|
+
"publish",
|
|
119
|
+
"payment",
|
|
120
|
+
"transfer",
|
|
121
|
+
"checkout"
|
|
122
|
+
]);
|
|
123
|
+
var DANGEROUS_ARG_PATTERNS = [
|
|
124
|
+
/rm\s+-rf/i,
|
|
125
|
+
/DROP\s+TABLE/i,
|
|
126
|
+
/DELETE\s+FROM/i,
|
|
127
|
+
/TRUNCATE/i,
|
|
128
|
+
/sudo\s+/i,
|
|
129
|
+
/chmod\s+777/i,
|
|
130
|
+
/curl\s+.*\|\s*sh/i,
|
|
131
|
+
/eval\s*\(/i,
|
|
132
|
+
/exec\s*\(/i,
|
|
133
|
+
/password|secret|token|api.?key/i
|
|
134
|
+
];
|
|
135
|
+
function calculateRiskScore(toolName, args) {
|
|
136
|
+
let score = 0;
|
|
137
|
+
if (HIGH_RISK_TOOLS.has(toolName.toLowerCase())) {
|
|
138
|
+
score += 30;
|
|
139
|
+
}
|
|
140
|
+
const argsStr = JSON.stringify(args);
|
|
141
|
+
for (const pattern of DANGEROUS_ARG_PATTERNS) {
|
|
142
|
+
if (pattern.test(argsStr)) {
|
|
143
|
+
score += 20;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (argsStr.length > 5e3) {
|
|
147
|
+
score += 10;
|
|
148
|
+
}
|
|
149
|
+
const pipeCount = (argsStr.match(/\|/g) || []).length;
|
|
150
|
+
if (pipeCount > 2) {
|
|
151
|
+
score += 15;
|
|
152
|
+
}
|
|
153
|
+
return Math.min(score, 100);
|
|
154
|
+
}
|
|
155
|
+
var McpProxyInterceptor = class {
|
|
156
|
+
config;
|
|
157
|
+
rateLimiter;
|
|
158
|
+
stats;
|
|
159
|
+
auditLog = [];
|
|
160
|
+
maxAuditLogSize = 1e4;
|
|
161
|
+
activeCalls = 0;
|
|
162
|
+
cleanupInterval = null;
|
|
163
|
+
constructor(config = {}) {
|
|
164
|
+
this.config = {
|
|
165
|
+
mode: config.mode ?? "block",
|
|
166
|
+
maxConcurrent: config.maxConcurrent ?? 50,
|
|
167
|
+
rateLimits: config.rateLimits ?? {},
|
|
168
|
+
globalRateLimit: config.globalRateLimit ?? 200,
|
|
169
|
+
toolPolicies: config.toolPolicies ?? [],
|
|
170
|
+
enableSemanticAnalysis: config.enableSemanticAnalysis ?? false,
|
|
171
|
+
onApprovalRequired: config.onApprovalRequired,
|
|
172
|
+
onAuditEvent: config.onAuditEvent,
|
|
173
|
+
upstreamTimeout: config.upstreamTimeout ?? 3e4,
|
|
174
|
+
maxRetries: config.maxRetries ?? 2
|
|
175
|
+
};
|
|
176
|
+
this.rateLimiter = new SlidingWindowRateLimiter();
|
|
177
|
+
this.stats = {
|
|
178
|
+
totalCalls: 0,
|
|
179
|
+
allowed: 0,
|
|
180
|
+
blocked: 0,
|
|
181
|
+
approvalRequested: 0,
|
|
182
|
+
rateLimited: 0,
|
|
183
|
+
errors: 0,
|
|
184
|
+
avgLatencyMs: 0,
|
|
185
|
+
callsByTool: {},
|
|
186
|
+
blocksByPolicy: {},
|
|
187
|
+
riskDistribution: { safe: 0, suspicious: 0, dangerous: 0, critical: 0 }
|
|
188
|
+
};
|
|
189
|
+
this.cleanupInterval = setInterval(() => {
|
|
190
|
+
this.rateLimiter.cleanup();
|
|
191
|
+
this.trimAuditLog();
|
|
192
|
+
}, 6e4);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Intercept a tool call — the core entry point.
|
|
196
|
+
* Returns the decision and optionally transformed arguments.
|
|
197
|
+
*/
|
|
198
|
+
async intercept(toolName, args) {
|
|
199
|
+
const startTime = Date.now();
|
|
200
|
+
const callId = `ic_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
201
|
+
this.stats.totalCalls++;
|
|
202
|
+
this.stats.callsByTool[toolName] = (this.stats.callsByTool[toolName] || 0) + 1;
|
|
203
|
+
if (this.activeCalls >= this.config.maxConcurrent) {
|
|
204
|
+
const decision2 = {
|
|
205
|
+
action: "block",
|
|
206
|
+
reason: `Concurrency limit exceeded (${this.config.maxConcurrent})`,
|
|
207
|
+
riskScore: 0
|
|
208
|
+
};
|
|
209
|
+
this.stats.blocked++;
|
|
210
|
+
this.emitAudit(callId, "block", toolName, args, decision2, Date.now() - startTime);
|
|
211
|
+
return { id: callId, timestamp: startTime, toolName, arguments: args, matchedPolicies: [], decision: decision2 };
|
|
212
|
+
}
|
|
213
|
+
if (!this.rateLimiter.check("__global__", this.config.globalRateLimit)) {
|
|
214
|
+
const decision2 = {
|
|
215
|
+
action: "block",
|
|
216
|
+
reason: `Global rate limit exceeded (${this.config.globalRateLimit}/min)`,
|
|
217
|
+
riskScore: 0
|
|
218
|
+
};
|
|
219
|
+
this.stats.rateLimited++;
|
|
220
|
+
this.emitAudit(callId, "rate-limited", toolName, args, decision2, Date.now() - startTime);
|
|
221
|
+
return { id: callId, timestamp: startTime, toolName, arguments: args, matchedPolicies: [], decision: decision2 };
|
|
222
|
+
}
|
|
223
|
+
const toolLimit = this.config.rateLimits[toolName];
|
|
224
|
+
if (toolLimit && !this.rateLimiter.check(`tool:${toolName}`, toolLimit)) {
|
|
225
|
+
const decision2 = {
|
|
226
|
+
action: "block",
|
|
227
|
+
reason: `Tool rate limit exceeded for '${toolName}' (${toolLimit}/min)`,
|
|
228
|
+
riskScore: 0
|
|
229
|
+
};
|
|
230
|
+
this.stats.rateLimited++;
|
|
231
|
+
this.emitAudit(callId, "rate-limited", toolName, args, decision2, Date.now() - startTime);
|
|
232
|
+
return { id: callId, timestamp: startTime, toolName, arguments: args, matchedPolicies: [], decision: decision2 };
|
|
233
|
+
}
|
|
234
|
+
const matchedPolicies = this.config.toolPolicies.filter((p) => matchGlob(p.toolPattern, toolName)).filter((p) => {
|
|
235
|
+
if (!p.conditions || p.conditions.length === 0) return true;
|
|
236
|
+
return p.conditions.every((c) => evaluateCondition(c, args));
|
|
237
|
+
}).sort((a, b) => b.priority - a.priority);
|
|
238
|
+
const riskScore = calculateRiskScore(toolName, args);
|
|
239
|
+
let decision;
|
|
240
|
+
if (matchedPolicies.length > 0) {
|
|
241
|
+
const topPolicy = matchedPolicies[0];
|
|
242
|
+
switch (topPolicy.action) {
|
|
243
|
+
case "block":
|
|
244
|
+
decision = {
|
|
245
|
+
action: "block",
|
|
246
|
+
reason: topPolicy.description,
|
|
247
|
+
triggeringPolicy: topPolicy,
|
|
248
|
+
riskScore
|
|
249
|
+
};
|
|
250
|
+
this.stats.blocked++;
|
|
251
|
+
this.stats.blocksByPolicy[topPolicy.description] = (this.stats.blocksByPolicy[topPolicy.description] || 0) + 1;
|
|
252
|
+
break;
|
|
253
|
+
case "require-approval":
|
|
254
|
+
decision = {
|
|
255
|
+
action: "require-approval",
|
|
256
|
+
reason: topPolicy.description,
|
|
257
|
+
triggeringPolicy: topPolicy,
|
|
258
|
+
riskScore
|
|
259
|
+
};
|
|
260
|
+
this.stats.approvalRequested++;
|
|
261
|
+
if (this.config.onApprovalRequired) {
|
|
262
|
+
const call = {
|
|
263
|
+
id: callId,
|
|
264
|
+
timestamp: startTime,
|
|
265
|
+
toolName,
|
|
266
|
+
arguments: args,
|
|
267
|
+
matchedPolicies,
|
|
268
|
+
decision
|
|
269
|
+
};
|
|
270
|
+
try {
|
|
271
|
+
const approval = await this.config.onApprovalRequired(call);
|
|
272
|
+
if (approval.approved) {
|
|
273
|
+
decision = { action: "allow", reason: `Approved by ${approval.approver || "human"}`, riskScore };
|
|
274
|
+
this.emitAudit(callId, "approval-granted", toolName, args, decision, Date.now() - startTime);
|
|
275
|
+
} else {
|
|
276
|
+
decision = { action: "block", reason: `Denied by ${approval.approver || "human"}: ${approval.reason || "no reason"}`, riskScore };
|
|
277
|
+
this.emitAudit(callId, "approval-denied", toolName, args, decision, Date.now() - startTime);
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
decision = { action: "block", reason: "Approval system unavailable (fail-closed)", riskScore };
|
|
281
|
+
this.stats.errors++;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
case "transform":
|
|
286
|
+
let transformed = { ...args };
|
|
287
|
+
for (const transform of topPolicy.transforms || []) {
|
|
288
|
+
transformed = applyTransform(transformed, transform);
|
|
289
|
+
}
|
|
290
|
+
decision = {
|
|
291
|
+
action: "transform",
|
|
292
|
+
reason: topPolicy.description,
|
|
293
|
+
triggeringPolicy: topPolicy,
|
|
294
|
+
riskScore,
|
|
295
|
+
transformedArguments: transformed
|
|
296
|
+
};
|
|
297
|
+
this.stats.allowed++;
|
|
298
|
+
break;
|
|
299
|
+
case "allow":
|
|
300
|
+
default:
|
|
301
|
+
decision = {
|
|
302
|
+
action: "allow",
|
|
303
|
+
reason: topPolicy.description,
|
|
304
|
+
triggeringPolicy: topPolicy,
|
|
305
|
+
riskScore
|
|
306
|
+
};
|
|
307
|
+
this.stats.allowed++;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
switch (this.config.mode) {
|
|
312
|
+
case "block":
|
|
313
|
+
decision = {
|
|
314
|
+
action: "block",
|
|
315
|
+
reason: "No matching policy (default deny)",
|
|
316
|
+
riskScore
|
|
317
|
+
};
|
|
318
|
+
this.stats.blocked++;
|
|
319
|
+
break;
|
|
320
|
+
case "warn":
|
|
321
|
+
decision = {
|
|
322
|
+
action: "allow",
|
|
323
|
+
reason: "No matching policy (warn mode \u2014 allowed with warning)",
|
|
324
|
+
riskScore
|
|
325
|
+
};
|
|
326
|
+
this.stats.allowed++;
|
|
327
|
+
break;
|
|
328
|
+
case "audit":
|
|
329
|
+
decision = {
|
|
330
|
+
action: "allow",
|
|
331
|
+
reason: "No matching policy (audit mode \u2014 allowed, logged)",
|
|
332
|
+
riskScore
|
|
333
|
+
};
|
|
334
|
+
this.stats.allowed++;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
this.rateLimiter.record("__global__");
|
|
339
|
+
this.rateLimiter.record(`tool:${toolName}`);
|
|
340
|
+
if (riskScore < 25) this.stats.riskDistribution.safe++;
|
|
341
|
+
else if (riskScore < 50) this.stats.riskDistribution.suspicious++;
|
|
342
|
+
else if (riskScore < 75) this.stats.riskDistribution.dangerous++;
|
|
343
|
+
else this.stats.riskDistribution.critical++;
|
|
344
|
+
const duration = Date.now() - startTime;
|
|
345
|
+
this.stats.avgLatencyMs = (this.stats.avgLatencyMs * (this.stats.totalCalls - 1) + duration) / this.stats.totalCalls;
|
|
346
|
+
const eventType = decision.action === "allow" || decision.action === "transform" ? "allow" : "block";
|
|
347
|
+
this.emitAudit(callId, eventType, toolName, args, decision, duration);
|
|
348
|
+
return {
|
|
349
|
+
id: callId,
|
|
350
|
+
timestamp: startTime,
|
|
351
|
+
toolName,
|
|
352
|
+
arguments: args,
|
|
353
|
+
matchedPolicies,
|
|
354
|
+
decision
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Wrap an upstream tool call with interception.
|
|
359
|
+
* If allowed, executes the upstream function; if blocked, returns error.
|
|
360
|
+
*/
|
|
361
|
+
async wrapToolCall(toolName, args, upstream) {
|
|
362
|
+
const intercepted = await this.intercept(toolName, args);
|
|
363
|
+
if (intercepted.decision.action === "block" || intercepted.decision.action === "require-approval") {
|
|
364
|
+
return {
|
|
365
|
+
intercepted,
|
|
366
|
+
error: `SOVR blocked: ${intercepted.decision.reason}`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const finalArgs = intercepted.decision.transformedArguments || args;
|
|
370
|
+
this.activeCalls++;
|
|
371
|
+
try {
|
|
372
|
+
const result = await Promise.race([
|
|
373
|
+
upstream(finalArgs),
|
|
374
|
+
new Promise(
|
|
375
|
+
(_, reject) => setTimeout(() => reject(new Error("Upstream timeout")), this.config.upstreamTimeout)
|
|
376
|
+
)
|
|
377
|
+
]);
|
|
378
|
+
return { result, intercepted };
|
|
379
|
+
} catch (err) {
|
|
380
|
+
this.stats.errors++;
|
|
381
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
382
|
+
this.emitAudit(
|
|
383
|
+
intercepted.id,
|
|
384
|
+
"error",
|
|
385
|
+
toolName,
|
|
386
|
+
args,
|
|
387
|
+
intercepted.decision,
|
|
388
|
+
0,
|
|
389
|
+
errorMsg
|
|
390
|
+
);
|
|
391
|
+
return { intercepted, error: errorMsg };
|
|
392
|
+
} finally {
|
|
393
|
+
this.activeCalls--;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** Get current statistics */
|
|
397
|
+
getStats() {
|
|
398
|
+
return { ...this.stats };
|
|
399
|
+
}
|
|
400
|
+
/** Get recent audit log */
|
|
401
|
+
getAuditLog(limit = 100) {
|
|
402
|
+
return this.auditLog.slice(-limit);
|
|
403
|
+
}
|
|
404
|
+
/** Export audit log as CSV */
|
|
405
|
+
exportAuditCSV() {
|
|
406
|
+
const headers = ["id", "timestamp", "type", "toolName", "decision", "riskScore", "reason", "duration", "error"];
|
|
407
|
+
const rows = this.auditLog.map((e) => [
|
|
408
|
+
e.id,
|
|
409
|
+
new Date(e.timestamp).toISOString(),
|
|
410
|
+
e.type,
|
|
411
|
+
e.toolName,
|
|
412
|
+
e.decision.action,
|
|
413
|
+
e.decision.riskScore,
|
|
414
|
+
`"${e.decision.reason.replace(/"/g, '""')}"`,
|
|
415
|
+
e.duration ?? "",
|
|
416
|
+
e.error ? `"${e.error.replace(/"/g, '""')}"` : ""
|
|
417
|
+
].join(","));
|
|
418
|
+
return [headers.join(","), ...rows].join("\n");
|
|
419
|
+
}
|
|
420
|
+
/** Add a policy at runtime */
|
|
421
|
+
addPolicy(policy) {
|
|
422
|
+
this.config.toolPolicies.push(policy);
|
|
423
|
+
this.config.toolPolicies.sort((a, b) => b.priority - a.priority);
|
|
424
|
+
}
|
|
425
|
+
/** Remove policies matching a pattern */
|
|
426
|
+
removePolicies(toolPattern) {
|
|
427
|
+
const before = this.config.toolPolicies.length;
|
|
428
|
+
this.config.toolPolicies = this.config.toolPolicies.filter((p) => p.toolPattern !== toolPattern);
|
|
429
|
+
return before - this.config.toolPolicies.length;
|
|
430
|
+
}
|
|
431
|
+
/** Reset statistics */
|
|
432
|
+
resetStats() {
|
|
433
|
+
this.stats = {
|
|
434
|
+
totalCalls: 0,
|
|
435
|
+
allowed: 0,
|
|
436
|
+
blocked: 0,
|
|
437
|
+
approvalRequested: 0,
|
|
438
|
+
rateLimited: 0,
|
|
439
|
+
errors: 0,
|
|
440
|
+
avgLatencyMs: 0,
|
|
441
|
+
callsByTool: {},
|
|
442
|
+
blocksByPolicy: {},
|
|
443
|
+
riskDistribution: { safe: 0, suspicious: 0, dangerous: 0, critical: 0 }
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/** Cleanup resources */
|
|
447
|
+
destroy() {
|
|
448
|
+
if (this.cleanupInterval) {
|
|
449
|
+
clearInterval(this.cleanupInterval);
|
|
450
|
+
this.cleanupInterval = null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ─── Private ─────────────────────────────────────────────────────────────
|
|
454
|
+
emitAudit(id, type, toolName, args, decision, duration, error) {
|
|
455
|
+
const event = {
|
|
456
|
+
id,
|
|
457
|
+
timestamp: Date.now(),
|
|
458
|
+
type,
|
|
459
|
+
toolName,
|
|
460
|
+
arguments: args,
|
|
461
|
+
decision,
|
|
462
|
+
duration,
|
|
463
|
+
error
|
|
464
|
+
};
|
|
465
|
+
this.auditLog.push(event);
|
|
466
|
+
this.config.onAuditEvent?.(event);
|
|
467
|
+
}
|
|
468
|
+
trimAuditLog() {
|
|
469
|
+
if (this.auditLog.length > this.maxAuditLogSize) {
|
|
470
|
+
this.auditLog = this.auditLog.slice(-this.maxAuditLogSize);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
var PRESET_POLICIES = {
|
|
475
|
+
/** Block all destructive file operations */
|
|
476
|
+
noDestructiveFiles: {
|
|
477
|
+
toolPattern: "*",
|
|
478
|
+
action: "block",
|
|
479
|
+
conditions: [
|
|
480
|
+
{ field: "command", operator: "matches", value: "rm\\s+-rf|rmdir|del\\s+/|Remove-Item" }
|
|
481
|
+
],
|
|
482
|
+
priority: 100,
|
|
483
|
+
description: "Block destructive file deletion commands"
|
|
484
|
+
},
|
|
485
|
+
/** Block all database write operations */
|
|
486
|
+
readOnlyDatabase: {
|
|
487
|
+
toolPattern: "*sql*",
|
|
488
|
+
action: "block",
|
|
489
|
+
conditions: [
|
|
490
|
+
{ field: "query", operator: "matches", value: "INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE" }
|
|
491
|
+
],
|
|
492
|
+
priority: 90,
|
|
493
|
+
description: "Block database write operations (read-only mode)"
|
|
494
|
+
},
|
|
495
|
+
/** Require approval for payment operations */
|
|
496
|
+
approvePayments: {
|
|
497
|
+
toolPattern: "*payment*",
|
|
498
|
+
action: "require-approval",
|
|
499
|
+
priority: 95,
|
|
500
|
+
description: "Require human approval for payment operations"
|
|
501
|
+
},
|
|
502
|
+
/** Redact secrets in tool arguments */
|
|
503
|
+
redactSecrets: {
|
|
504
|
+
toolPattern: "*",
|
|
505
|
+
action: "transform",
|
|
506
|
+
conditions: [
|
|
507
|
+
{ field: "command", operator: "matches", value: "password|secret|token|api.?key" }
|
|
508
|
+
],
|
|
509
|
+
transforms: [
|
|
510
|
+
{ field: "password", type: "redact" },
|
|
511
|
+
{ field: "secret", type: "redact" },
|
|
512
|
+
{ field: "token", type: "mask" }
|
|
513
|
+
],
|
|
514
|
+
priority: 80,
|
|
515
|
+
description: "Redact sensitive values in tool arguments"
|
|
516
|
+
},
|
|
517
|
+
/** Rate limit shell commands */
|
|
518
|
+
rateLimitShell: {
|
|
519
|
+
toolPattern: "bash",
|
|
520
|
+
action: "rate-limit",
|
|
521
|
+
priority: 70,
|
|
522
|
+
description: "Rate limit shell command execution"
|
|
523
|
+
},
|
|
524
|
+
/** Allow read-only operations */
|
|
525
|
+
allowReadOnly: {
|
|
526
|
+
toolPattern: "*",
|
|
527
|
+
action: "allow",
|
|
528
|
+
conditions: [
|
|
529
|
+
{ field: "command", operator: "matches", value: "^(ls|cat|head|tail|grep|find|echo|pwd|whoami|date|wc)\\b" }
|
|
530
|
+
],
|
|
531
|
+
priority: 60,
|
|
532
|
+
description: "Allow read-only shell commands"
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
function createSecureInterceptor(overrides = {}) {
|
|
536
|
+
return new McpProxyInterceptor({
|
|
537
|
+
mode: "block",
|
|
538
|
+
maxConcurrent: 20,
|
|
539
|
+
globalRateLimit: 100,
|
|
540
|
+
rateLimits: { bash: 30, shell: 30, exec: 30 },
|
|
541
|
+
toolPolicies: Object.values(PRESET_POLICIES),
|
|
542
|
+
enableSemanticAnalysis: false,
|
|
543
|
+
upstreamTimeout: 3e4,
|
|
544
|
+
maxRetries: 2,
|
|
545
|
+
...overrides
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
export {
|
|
549
|
+
McpProxyInterceptor,
|
|
550
|
+
PRESET_POLICIES,
|
|
551
|
+
createSecureInterceptor
|
|
552
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SOVR Semantic Analyzer — Multi-Layer Intent Detection Engine
|
|
3
|
+
*
|
|
4
|
+
* P1-2: Goes beyond regex pattern matching to understand command INTENT.
|
|
5
|
+
*
|
|
6
|
+
* Three analysis layers (evidence-based first, LLM last):
|
|
7
|
+
* Layer 1: Rule-based pattern matching (fast, deterministic)
|
|
8
|
+
* Layer 2: Structural analysis (AST-like decomposition of commands)
|
|
9
|
+
* Layer 3: LLM-as-Judge (optional, for ambiguous cases only)
|
|
10
|
+
*
|
|
11
|
+
* Design principle: "Evidence-based over LLM semantic" — per SOVR ADR.
|
|
12
|
+
* LLM is only used as a tiebreaker, never as the sole decision maker.
|
|
13
|
+
*/
|
|
14
|
+
interface SemanticAnalysisConfig {
|
|
15
|
+
/** Enable Layer 3 (LLM-as-Judge) */
|
|
16
|
+
enableLLM: boolean;
|
|
17
|
+
/** LLM provider function (optional) */
|
|
18
|
+
llmProvider?: LLMProvider;
|
|
19
|
+
/** Risk threshold to trigger LLM analysis (0-100) */
|
|
20
|
+
llmTriggerThreshold: number;
|
|
21
|
+
/** Maximum time for LLM analysis (ms) */
|
|
22
|
+
llmTimeout: number;
|
|
23
|
+
/** Custom intent rules */
|
|
24
|
+
customRules: IntentRule[];
|
|
25
|
+
/** Enable structural analysis */
|
|
26
|
+
enableStructural: boolean;
|
|
27
|
+
/** Sensitivity level */
|
|
28
|
+
sensitivity: 'low' | 'medium' | 'high' | 'paranoid';
|
|
29
|
+
}
|
|
30
|
+
interface LLMProvider {
|
|
31
|
+
analyze(prompt: string, timeout: number): Promise<LLMJudgment>;
|
|
32
|
+
}
|
|
33
|
+
interface LLMJudgment {
|
|
34
|
+
intent: string;
|
|
35
|
+
riskLevel: 'safe' | 'suspicious' | 'dangerous' | 'critical';
|
|
36
|
+
confidence: number;
|
|
37
|
+
reasoning: string;
|
|
38
|
+
}
|
|
39
|
+
interface IntentRule {
|
|
40
|
+
/** Rule ID */
|
|
41
|
+
id: string;
|
|
42
|
+
/** Human-readable name */
|
|
43
|
+
name: string;
|
|
44
|
+
/** Category of intent */
|
|
45
|
+
category: IntentCategory;
|
|
46
|
+
/** Patterns to match (any match triggers) */
|
|
47
|
+
patterns: IntentPattern[];
|
|
48
|
+
/** Risk level when matched */
|
|
49
|
+
riskLevel: 'safe' | 'suspicious' | 'dangerous' | 'critical';
|
|
50
|
+
/** Priority (higher = evaluated first) */
|
|
51
|
+
priority: number;
|
|
52
|
+
/** Whether this rule is a positive (allow) or negative (block) indicator */
|
|
53
|
+
polarity: 'positive' | 'negative';
|
|
54
|
+
}
|
|
55
|
+
interface IntentPattern {
|
|
56
|
+
/** Pattern type */
|
|
57
|
+
type: 'regex' | 'keyword' | 'structural' | 'sequence';
|
|
58
|
+
/** The pattern value */
|
|
59
|
+
value: string;
|
|
60
|
+
/** Where to look */
|
|
61
|
+
target: 'command' | 'arguments' | 'tool_name' | 'full_context';
|
|
62
|
+
/** Case sensitive */
|
|
63
|
+
caseSensitive?: boolean;
|
|
64
|
+
}
|
|
65
|
+
type IntentCategory = 'data_destruction' | 'data_exfiltration' | 'privilege_escalation' | 'code_execution' | 'network_access' | 'file_modification' | 'credential_access' | 'system_modification' | 'financial_operation' | 'communication' | 'read_only' | 'benign';
|
|
66
|
+
interface AnalysisResult {
|
|
67
|
+
/** Overall risk level */
|
|
68
|
+
riskLevel: 'safe' | 'suspicious' | 'dangerous' | 'critical';
|
|
69
|
+
/** Overall risk score (0-100) */
|
|
70
|
+
riskScore: number;
|
|
71
|
+
/** Confidence in the analysis (0-1) */
|
|
72
|
+
confidence: number;
|
|
73
|
+
/** Detected intents */
|
|
74
|
+
intents: DetectedIntent[];
|
|
75
|
+
/** Layer results */
|
|
76
|
+
layers: {
|
|
77
|
+
rules: LayerResult;
|
|
78
|
+
structural: LayerResult;
|
|
79
|
+
llm?: LayerResult;
|
|
80
|
+
};
|
|
81
|
+
/** Recommended action */
|
|
82
|
+
recommendation: 'allow' | 'warn' | 'block' | 'require-approval';
|
|
83
|
+
/** Human-readable explanation */
|
|
84
|
+
explanation: string;
|
|
85
|
+
/** Analysis duration (ms) */
|
|
86
|
+
durationMs: number;
|
|
87
|
+
}
|
|
88
|
+
interface DetectedIntent {
|
|
89
|
+
category: IntentCategory;
|
|
90
|
+
description: string;
|
|
91
|
+
confidence: number;
|
|
92
|
+
source: 'rule' | 'structural' | 'llm';
|
|
93
|
+
evidence: string[];
|
|
94
|
+
}
|
|
95
|
+
interface LayerResult {
|
|
96
|
+
riskLevel: 'safe' | 'suspicious' | 'dangerous' | 'critical';
|
|
97
|
+
riskScore: number;
|
|
98
|
+
confidence: number;
|
|
99
|
+
findings: string[];
|
|
100
|
+
}
|
|
101
|
+
declare class SemanticAnalyzer {
|
|
102
|
+
private config;
|
|
103
|
+
private rules;
|
|
104
|
+
constructor(config?: Partial<SemanticAnalysisConfig>);
|
|
105
|
+
/**
|
|
106
|
+
* Analyze a tool call for security risks.
|
|
107
|
+
* Returns a comprehensive analysis result with multi-layer findings.
|
|
108
|
+
*/
|
|
109
|
+
analyze(toolName: string, args: Record<string, unknown>): Promise<AnalysisResult>;
|
|
110
|
+
/** Quick synchronous check (Layer 1 only, for hot path) */
|
|
111
|
+
quickCheck(toolName: string, args: Record<string, unknown>): {
|
|
112
|
+
riskLevel: 'safe' | 'suspicious' | 'dangerous' | 'critical';
|
|
113
|
+
riskScore: number;
|
|
114
|
+
topFinding: string;
|
|
115
|
+
};
|
|
116
|
+
/** Add custom rules at runtime */
|
|
117
|
+
addRule(rule: IntentRule): void;
|
|
118
|
+
/** Get all active rules */
|
|
119
|
+
getRules(): IntentRule[];
|
|
120
|
+
private analyzeWithRules;
|
|
121
|
+
private extractCommand;
|
|
122
|
+
private getPatternTarget;
|
|
123
|
+
private riskLevelToScore;
|
|
124
|
+
private applySensitivity;
|
|
125
|
+
private collectIntents;
|
|
126
|
+
private combineResults;
|
|
127
|
+
}
|
|
128
|
+
/** Create a semantic analyzer with default settings */
|
|
129
|
+
declare function createSemanticAnalyzer(overrides?: Partial<SemanticAnalysisConfig>): SemanticAnalyzer;
|
|
130
|
+
/** Create a paranoid analyzer (highest sensitivity, all layers) */
|
|
131
|
+
declare function createParanoidAnalyzer(llmProvider?: LLMProvider): SemanticAnalyzer;
|
|
132
|
+
|
|
133
|
+
export { type AnalysisResult, type DetectedIntent, type IntentCategory, type IntentPattern, type IntentRule, type LLMJudgment, type LLMProvider, type LayerResult, type SemanticAnalysisConfig, SemanticAnalyzer, createParanoidAnalyzer, createSemanticAnalyzer };
|