agentshield-sdk 7.0.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/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- package/types/index.d.ts +2088 -0
package/src/main.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Shield — ESM entry point
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { AgentShield, scanText, expressMiddleware } from 'agent-shield';
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const shield = require('./main.js');
|
|
10
|
+
|
|
11
|
+
// Re-export all named exports
|
|
12
|
+
export const {
|
|
13
|
+
AgentShield,
|
|
14
|
+
scanText,
|
|
15
|
+
getPatterns,
|
|
16
|
+
SEVERITY_ORDER,
|
|
17
|
+
expressMiddleware,
|
|
18
|
+
wrapAgent,
|
|
19
|
+
shieldTools,
|
|
20
|
+
extractTextFromBody,
|
|
21
|
+
shieldAnthropicClient,
|
|
22
|
+
shieldOpenAIClient,
|
|
23
|
+
ShieldCallbackHandler,
|
|
24
|
+
shieldVercelAI,
|
|
25
|
+
CanaryTokenGenerator,
|
|
26
|
+
PromptLeakDetector,
|
|
27
|
+
PIIRedactor,
|
|
28
|
+
ToolSequenceAnalyzer,
|
|
29
|
+
PermissionBoundary,
|
|
30
|
+
CircuitBreaker,
|
|
31
|
+
RateLimiter,
|
|
32
|
+
ShadowMode,
|
|
33
|
+
createShieldError,
|
|
34
|
+
deprecationWarning,
|
|
35
|
+
ERROR_CODES,
|
|
36
|
+
} = shield;
|
|
37
|
+
|
|
38
|
+
export default shield;
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — MCP (Model Context Protocol) Bridge
|
|
5
|
+
*
|
|
6
|
+
* Native integration with MCP tool chains. Scans tool calls, tool results,
|
|
7
|
+
* resources, and prompt templates for security threats. Enforces per-session
|
|
8
|
+
* budgets and tool policies.
|
|
9
|
+
*
|
|
10
|
+
* All processing runs locally — no data ever leaves your environment.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
// =========================================================================
|
|
16
|
+
// Dangerous tool patterns
|
|
17
|
+
// =========================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tool name patterns that are inherently dangerous.
|
|
21
|
+
* @type {Array<object>}
|
|
22
|
+
*/
|
|
23
|
+
const MCP_DANGEROUS_TOOLS = [
|
|
24
|
+
{ pattern: /(?:exec|spawn|run|shell|bash|cmd|powershell|terminal)/i, category: 'code_execution', severity: 'critical', description: 'Code/command execution tool' },
|
|
25
|
+
{ pattern: /(?:file|fs|read|write|delete|remove|mkdir|rmdir|unlink)/i, category: 'filesystem', severity: 'high', description: 'Filesystem access tool' },
|
|
26
|
+
{ pattern: /(?:http|fetch|request|curl|wget|socket|net|dns)/i, category: 'network', severity: 'high', description: 'Network access tool' },
|
|
27
|
+
{ pattern: /(?:sql|query|database|db|mongo|redis|postgres|mysql)/i, category: 'database', severity: 'high', description: 'Database access tool' },
|
|
28
|
+
{ pattern: /(?:env|process|os|system|config|secret|credential|key)/i, category: 'system', severity: 'high', description: 'System/environment access tool' },
|
|
29
|
+
{ pattern: /(?:email|smtp|send|notify|publish|post|tweet|slack)/i, category: 'communication', severity: 'medium', description: 'External communication tool' },
|
|
30
|
+
{ pattern: /(?:install|npm|pip|apt|brew|package|deploy)/i, category: 'package_management', severity: 'high', description: 'Package management tool' },
|
|
31
|
+
{ pattern: /(?:cron|schedule|timer|interval|daemon)/i, category: 'scheduling', severity: 'medium', description: 'Task scheduling tool' }
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Patterns that indicate injection in tool arguments.
|
|
36
|
+
* @type {Array<object>}
|
|
37
|
+
*/
|
|
38
|
+
const ARG_INJECTION_PATTERNS = [
|
|
39
|
+
{ pattern: /;\s*(?:rm|del|drop|shutdown|kill|curl|wget)\b/i, severity: 'critical', description: 'Command chaining in argument' },
|
|
40
|
+
{ pattern: /\$\{.{0,500}\}|\$\(.{0,500}\)|`.{0,500}`/s, severity: 'high', description: 'Shell expansion in argument' },
|
|
41
|
+
{ pattern: /(?:\.\.\/){2,}|(?:\.\.\\){2,}/i, severity: 'high', description: 'Path traversal in argument' },
|
|
42
|
+
{ pattern: /(?:ignore|override|forget)\s+(?:previous|all|system)\s+(?:instructions|rules)/i, severity: 'critical', description: 'Injection in tool argument' },
|
|
43
|
+
{ pattern: /<script[^>]*>|javascript:/i, severity: 'high', description: 'XSS in tool argument' },
|
|
44
|
+
{ pattern: /(?:union\s+select|;\s*drop\s+table|'\s*or\s+'1'\s*=\s*'1)/i, severity: 'critical', description: 'SQL injection in tool argument' }
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns the default scanner (detector-core.scanText) or a safe fallback.
|
|
49
|
+
* @returns {Function}
|
|
50
|
+
*/
|
|
51
|
+
function getDefaultScanner() {
|
|
52
|
+
try {
|
|
53
|
+
const { scanText } = require('./detector-core');
|
|
54
|
+
return (text) => scanText(text);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return () => ({ threats: [], severity: 'safe' });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =========================================================================
|
|
61
|
+
// MCPBridge — Main integration point
|
|
62
|
+
// =========================================================================
|
|
63
|
+
|
|
64
|
+
class MCPBridge {
|
|
65
|
+
/**
|
|
66
|
+
* @param {object} [options]
|
|
67
|
+
* @param {Function} [options.scanner] - Custom scan function (defaults to detector-core.scanText)
|
|
68
|
+
* @param {string[]} [options.allowedTools] - Whitelist of allowed tool names
|
|
69
|
+
* @param {string[]} [options.blockedTools] - Blacklist of blocked tool names
|
|
70
|
+
* @param {boolean} [options.scanInputs=true] - Scan tool call arguments
|
|
71
|
+
* @param {boolean} [options.scanOutputs=true] - Scan tool results
|
|
72
|
+
* @param {number} [options.maxToolCallsPerMinute=60] - Rate limit
|
|
73
|
+
*/
|
|
74
|
+
constructor(options = {}) {
|
|
75
|
+
this.scanner = options.scanner || getDefaultScanner();
|
|
76
|
+
this.allowedTools = options.allowedTools ? new Set(options.allowedTools) : null;
|
|
77
|
+
this.blockedTools = new Set(options.blockedTools || []);
|
|
78
|
+
this.scanInputs = options.scanInputs !== false;
|
|
79
|
+
this.scanOutputs = options.scanOutputs !== false;
|
|
80
|
+
this.maxToolCallsPerMinute = options.maxToolCallsPerMinute || 60;
|
|
81
|
+
|
|
82
|
+
this.stats = {
|
|
83
|
+
toolCallsScanned: 0,
|
|
84
|
+
toolResultsScanned: 0,
|
|
85
|
+
blocked: 0,
|
|
86
|
+
threats: {},
|
|
87
|
+
callTimestamps: []
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Scans tool call arguments for injection before execution.
|
|
93
|
+
* @param {string} toolName - MCP tool name
|
|
94
|
+
* @param {object} args - Tool call arguments
|
|
95
|
+
* @returns {{ allowed: boolean, threats: Array, sanitizedArgs: object, reason: string|null }}
|
|
96
|
+
*/
|
|
97
|
+
wrapToolCall(toolName, args = {}) {
|
|
98
|
+
this.stats.toolCallsScanned++;
|
|
99
|
+
const threats = [];
|
|
100
|
+
let reason = null;
|
|
101
|
+
|
|
102
|
+
// Check blocked tools
|
|
103
|
+
if (this.blockedTools.has(toolName)) {
|
|
104
|
+
this.stats.blocked++;
|
|
105
|
+
return { allowed: false, threats: [{ severity: 'high', category: 'blocked_tool', description: `Tool "${toolName}" is blocked by policy` }], sanitizedArgs: args, reason: 'Tool is blocked by policy' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check allowlist
|
|
109
|
+
if (this.allowedTools && !this.allowedTools.has(toolName)) {
|
|
110
|
+
this.stats.blocked++;
|
|
111
|
+
return { allowed: false, threats: [{ severity: 'medium', category: 'unlisted_tool', description: `Tool "${toolName}" is not in the allowed list` }], sanitizedArgs: args, reason: 'Tool is not in allowed list' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check rate limit
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
this.stats.callTimestamps = this.stats.callTimestamps.filter(t => now - t < 60000);
|
|
117
|
+
if (this.stats.callTimestamps.length >= this.maxToolCallsPerMinute) {
|
|
118
|
+
this.stats.blocked++;
|
|
119
|
+
return { allowed: false, threats: [{ severity: 'medium', category: 'rate_limit', description: 'Tool call rate limit exceeded' }], sanitizedArgs: args, reason: 'Rate limit exceeded' };
|
|
120
|
+
}
|
|
121
|
+
this.stats.callTimestamps.push(now);
|
|
122
|
+
|
|
123
|
+
// Check dangerous tool patterns
|
|
124
|
+
for (const dt of MCP_DANGEROUS_TOOLS) {
|
|
125
|
+
if (dt.pattern.test(toolName)) {
|
|
126
|
+
threats.push({ severity: dt.severity, category: dt.category, description: dt.description, tool: toolName });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Scan arguments for injection
|
|
131
|
+
if (this.scanInputs) {
|
|
132
|
+
const argText = JSON.stringify(args);
|
|
133
|
+
for (const ap of ARG_INJECTION_PATTERNS) {
|
|
134
|
+
if (ap.pattern.test(argText)) {
|
|
135
|
+
threats.push({ severity: ap.severity, category: 'arg_injection', description: ap.description, tool: toolName });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Run general scanner
|
|
140
|
+
const scanResult = this.scanner(argText);
|
|
141
|
+
if (scanResult.threats && scanResult.threats.length > 0) {
|
|
142
|
+
threats.push(...scanResult.threats.map(t => ({ ...t, tool: toolName, source: 'general_scanner' })));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const hasCritical = threats.some(t => t.severity === 'critical');
|
|
147
|
+
if (hasCritical) {
|
|
148
|
+
this.stats.blocked++;
|
|
149
|
+
reason = 'Critical threat detected in tool call';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Track threat categories
|
|
153
|
+
for (const t of threats) {
|
|
154
|
+
this.stats.threats[t.category] = (this.stats.threats[t.category] || 0) + 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { allowed: !hasCritical, threats, sanitizedArgs: args, reason };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Scans tool results for exfiltration/injection before returning to model.
|
|
162
|
+
* @param {string} toolName - MCP tool name
|
|
163
|
+
* @param {*} result - Tool result
|
|
164
|
+
* @returns {{ safe: boolean, threats: Array, sanitizedResult: * }}
|
|
165
|
+
*/
|
|
166
|
+
wrapToolResult(toolName, result) {
|
|
167
|
+
this.stats.toolResultsScanned++;
|
|
168
|
+
const threats = [];
|
|
169
|
+
|
|
170
|
+
if (!this.scanOutputs) {
|
|
171
|
+
return { safe: true, threats: [], sanitizedResult: result };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const resultText = typeof result === 'string' ? result : JSON.stringify(result);
|
|
175
|
+
|
|
176
|
+
// Run general scanner on output
|
|
177
|
+
const scanResult = this.scanner(resultText);
|
|
178
|
+
if (scanResult.threats && scanResult.threats.length > 0) {
|
|
179
|
+
threats.push(...scanResult.threats.map(t => ({ ...t, tool: toolName, source: 'output_scan' })));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for potential data exfiltration markers
|
|
183
|
+
const exfilPatterns = [
|
|
184
|
+
{ pattern: /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key)\s*[:=]\s*\S+/i, description: 'Credential in tool output' },
|
|
185
|
+
{ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/i, description: 'Private key in tool output' },
|
|
186
|
+
{ pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/i, description: 'JWT token in tool output' }
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
for (const ep of exfilPatterns) {
|
|
190
|
+
if (ep.pattern.test(resultText)) {
|
|
191
|
+
threats.push({ severity: 'high', category: 'data_exfiltration', description: ep.description, tool: toolName });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const safe = !threats.some(t => t.severity === 'critical' || t.severity === 'high');
|
|
196
|
+
return { safe, threats, sanitizedResult: result };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validates an MCP tool schema for dangerous patterns.
|
|
201
|
+
* @param {object} schema - MCP tool schema
|
|
202
|
+
* @returns {{ valid: boolean, warnings: Array, risks: Array }}
|
|
203
|
+
*/
|
|
204
|
+
validateToolSchema(schema = {}) {
|
|
205
|
+
const warnings = [];
|
|
206
|
+
const risks = [];
|
|
207
|
+
|
|
208
|
+
if (!schema.name) {
|
|
209
|
+
warnings.push({ field: 'name', message: 'Tool schema missing name' });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!schema.description) {
|
|
213
|
+
warnings.push({ field: 'description', message: 'Tool schema missing description' });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for dangerous tool names
|
|
217
|
+
if (schema.name) {
|
|
218
|
+
for (const dt of MCP_DANGEROUS_TOOLS) {
|
|
219
|
+
if (dt.pattern.test(schema.name)) {
|
|
220
|
+
risks.push({ severity: dt.severity, category: dt.category, description: `Tool "${schema.name}": ${dt.description}` });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check input schema for overly permissive types
|
|
226
|
+
if (schema.inputSchema) {
|
|
227
|
+
const inputStr = JSON.stringify(schema.inputSchema);
|
|
228
|
+
if (!schema.inputSchema.properties || Object.keys(schema.inputSchema.properties).length === 0) {
|
|
229
|
+
warnings.push({ field: 'inputSchema', message: 'Tool accepts arbitrary input (no properties defined)' });
|
|
230
|
+
}
|
|
231
|
+
if (inputStr.includes('"additionalProperties":true') || !inputStr.includes('additionalProperties')) {
|
|
232
|
+
warnings.push({ field: 'inputSchema', message: 'Tool allows additional properties — may accept unexpected input' });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { valid: risks.length === 0, warnings, risks };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Returns scan statistics.
|
|
241
|
+
* @returns {object}
|
|
242
|
+
*/
|
|
243
|
+
getStats() {
|
|
244
|
+
return { ...this.stats, callTimestamps: this.stats.callTimestamps.length };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// =========================================================================
|
|
249
|
+
// MCPToolPolicy — Policy engine for MCP tools
|
|
250
|
+
// =========================================================================
|
|
251
|
+
|
|
252
|
+
class MCPToolPolicy {
|
|
253
|
+
/**
|
|
254
|
+
* @param {Array<object>} [rules] - Policy rules: { id, tool, action, conditions }
|
|
255
|
+
*/
|
|
256
|
+
constructor(rules = []) {
|
|
257
|
+
this.rules = rules.map((r, i) => ({ id: r.id || `rule_${i}`, ...r }));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Evaluates a tool call against the policy.
|
|
262
|
+
* @param {string} toolName
|
|
263
|
+
* @param {object} args
|
|
264
|
+
* @param {object} [context] - Session context
|
|
265
|
+
* @returns {{ action: string, reason: string, matchedRule: object|null }}
|
|
266
|
+
*/
|
|
267
|
+
evaluate(toolName, args = {}, context = {}) {
|
|
268
|
+
for (const rule of this.rules) {
|
|
269
|
+
if (this._matchesRule(rule, toolName, args, context)) {
|
|
270
|
+
return { action: rule.action, reason: rule.reason || `Matched rule ${rule.id}`, matchedRule: rule };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { action: 'scan', reason: 'No matching rule — default to scan', matchedRule: null };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Adds a policy rule.
|
|
278
|
+
* @param {object} rule
|
|
279
|
+
*/
|
|
280
|
+
addRule(rule) {
|
|
281
|
+
const id = rule.id || `rule_${this.rules.length}`;
|
|
282
|
+
this.rules.push({ id, ...rule });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Removes a rule by ID.
|
|
287
|
+
* @param {string} ruleId
|
|
288
|
+
*/
|
|
289
|
+
removeRule(ruleId) {
|
|
290
|
+
this.rules = this.rules.filter(r => r.id !== ruleId);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Serializes policy to JSON.
|
|
295
|
+
* @returns {object}
|
|
296
|
+
*/
|
|
297
|
+
toJSON() {
|
|
298
|
+
return { version: '1.0', rules: this.rules };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Deserializes policy from JSON.
|
|
303
|
+
* @param {object} json
|
|
304
|
+
* @returns {MCPToolPolicy}
|
|
305
|
+
*/
|
|
306
|
+
static fromJSON(json) {
|
|
307
|
+
return new MCPToolPolicy(json.rules || []);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** @private */
|
|
311
|
+
_matchesRule(rule, toolName, args, context) {
|
|
312
|
+
if (rule.tool) {
|
|
313
|
+
const toolMatch = rule.tool instanceof RegExp ? rule.tool.test(toolName) : rule.tool === toolName;
|
|
314
|
+
if (!toolMatch) return false;
|
|
315
|
+
}
|
|
316
|
+
if (rule.conditions) {
|
|
317
|
+
if (rule.conditions.maxArgLength && JSON.stringify(args).length > rule.conditions.maxArgLength) return true;
|
|
318
|
+
if (rule.conditions.requiresAuth && !context.authenticated) return true;
|
|
319
|
+
if (rule.conditions.roles && context.role && !rule.conditions.roles.includes(context.role)) return true;
|
|
320
|
+
}
|
|
321
|
+
return !rule.conditions || Object.keys(rule.conditions).length === 0;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// =========================================================================
|
|
326
|
+
// MCPSessionGuard — Per-session security state
|
|
327
|
+
// =========================================================================
|
|
328
|
+
|
|
329
|
+
class MCPSessionGuard {
|
|
330
|
+
/**
|
|
331
|
+
* @param {string} sessionId
|
|
332
|
+
* @param {object} [options]
|
|
333
|
+
* @param {number} [options.maxToolCalls=100] - Max tool calls per session
|
|
334
|
+
* @param {number} [options.maxTokenBudget=100000] - Max tokens per session
|
|
335
|
+
* @param {string[]} [options.allowedTools] - Per-session tool whitelist
|
|
336
|
+
*/
|
|
337
|
+
constructor(sessionId, options = {}) {
|
|
338
|
+
this.sessionId = sessionId;
|
|
339
|
+
this.maxToolCalls = options.maxToolCalls || 100;
|
|
340
|
+
this.maxTokenBudget = options.maxTokenBudget || 100000;
|
|
341
|
+
this.allowedTools = options.allowedTools ? new Set(options.allowedTools) : null;
|
|
342
|
+
|
|
343
|
+
this.callCount = 0;
|
|
344
|
+
this.tokenCount = 0;
|
|
345
|
+
this.toolUsage = {};
|
|
346
|
+
this.threats = [];
|
|
347
|
+
this.startedAt = Date.now();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Tracks a tool call, enforcing session limits.
|
|
352
|
+
* @param {string} toolName
|
|
353
|
+
* @param {object} args
|
|
354
|
+
* @returns {{ allowed: boolean, reason: string|null }}
|
|
355
|
+
*/
|
|
356
|
+
trackToolCall(toolName, args = {}) {
|
|
357
|
+
// Validate before mutating state
|
|
358
|
+
if (this.allowedTools && !this.allowedTools.has(toolName)) {
|
|
359
|
+
return { allowed: false, reason: `Tool "${toolName}" not allowed in this session` };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (this.callCount >= this.maxToolCalls) {
|
|
363
|
+
return { allowed: false, reason: `Session tool call limit exceeded (${this.maxToolCalls})` };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this.callCount++;
|
|
367
|
+
this.toolUsage[toolName] = (this.toolUsage[toolName] || 0) + 1;
|
|
368
|
+
this.tokenCount += JSON.stringify(args).length;
|
|
369
|
+
|
|
370
|
+
return { allowed: true, reason: null };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Checks if the session budget is exceeded.
|
|
375
|
+
* @returns {{ exceeded: boolean, callsRemaining: number, tokensRemaining: number }}
|
|
376
|
+
*/
|
|
377
|
+
checkBudget() {
|
|
378
|
+
return {
|
|
379
|
+
exceeded: this.callCount >= this.maxToolCalls || this.tokenCount >= this.maxTokenBudget,
|
|
380
|
+
callsRemaining: Math.max(0, this.maxToolCalls - this.callCount),
|
|
381
|
+
tokensRemaining: Math.max(0, this.maxTokenBudget - this.tokenCount)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Returns session security summary.
|
|
387
|
+
* @returns {object}
|
|
388
|
+
*/
|
|
389
|
+
getSessionReport() {
|
|
390
|
+
return {
|
|
391
|
+
sessionId: this.sessionId,
|
|
392
|
+
duration: Date.now() - this.startedAt,
|
|
393
|
+
callCount: this.callCount,
|
|
394
|
+
tokenCount: this.tokenCount,
|
|
395
|
+
uniqueTools: Object.keys(this.toolUsage).length,
|
|
396
|
+
toolUsage: { ...this.toolUsage },
|
|
397
|
+
threats: this.threats.length,
|
|
398
|
+
budget: this.checkBudget()
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Resets session state.
|
|
404
|
+
*/
|
|
405
|
+
reset() {
|
|
406
|
+
this.callCount = 0;
|
|
407
|
+
this.tokenCount = 0;
|
|
408
|
+
this.toolUsage = {};
|
|
409
|
+
this.threats = [];
|
|
410
|
+
this.startedAt = Date.now();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// =========================================================================
|
|
415
|
+
// MCPResourceScanner — Scan MCP resources
|
|
416
|
+
// =========================================================================
|
|
417
|
+
|
|
418
|
+
class MCPResourceScanner {
|
|
419
|
+
/**
|
|
420
|
+
* @param {object} [options]
|
|
421
|
+
* @param {Function} [options.scanner] - Custom scan function
|
|
422
|
+
*/
|
|
423
|
+
constructor(options = {}) {
|
|
424
|
+
this.scanner = options.scanner || getDefaultScanner();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Scans MCP resource content for threats.
|
|
429
|
+
* @param {string} uri - Resource URI
|
|
430
|
+
* @param {string} content - Resource content
|
|
431
|
+
* @param {string} [mimeType='text/plain'] - MIME type
|
|
432
|
+
* @returns {{ safe: boolean, threats: Array, uri: string }}
|
|
433
|
+
*/
|
|
434
|
+
scanResource(uri, content, mimeType = 'text/plain') {
|
|
435
|
+
const threats = [];
|
|
436
|
+
const text = typeof content === 'string' ? content : JSON.stringify(content);
|
|
437
|
+
const scanResult = this.scanner(text);
|
|
438
|
+
|
|
439
|
+
if (scanResult.threats && scanResult.threats.length > 0) {
|
|
440
|
+
threats.push(...scanResult.threats.map(t => ({ ...t, uri, mimeType })));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { safe: threats.length === 0, threats, uri };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Scans an MCP prompt template for injection vectors.
|
|
448
|
+
* @param {string} template - Prompt template text
|
|
449
|
+
* @returns {{ safe: boolean, threats: Array, recommendations: Array }}
|
|
450
|
+
*/
|
|
451
|
+
scanPromptTemplate(template) {
|
|
452
|
+
const threats = [];
|
|
453
|
+
const recommendations = [];
|
|
454
|
+
|
|
455
|
+
// Check for unescaped user input slots
|
|
456
|
+
const slotPattern = /\{\{?\s*(\w+)\s*\}?\}/g;
|
|
457
|
+
let match;
|
|
458
|
+
while ((match = slotPattern.exec(template)) !== null) {
|
|
459
|
+
const varName = match[1];
|
|
460
|
+
if (/user|input|query|message|prompt/i.test(varName)) {
|
|
461
|
+
recommendations.push(`Variable "${varName}" accepts user input — ensure it is sanitized before interpolation`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Run general scanner
|
|
466
|
+
const scanResult = this.scanner(template);
|
|
467
|
+
if (scanResult.threats && scanResult.threats.length > 0) {
|
|
468
|
+
threats.push(...scanResult.threats.map(t => ({ ...t, source: 'prompt_template' })));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Check for missing safety instructions
|
|
472
|
+
if (!/(?:do not|never|must not|should not)\s+(?:reveal|disclose|output|share)/i.test(template)) {
|
|
473
|
+
recommendations.push('Prompt template lacks defensive instructions against information disclosure');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return { safe: threats.length === 0, threats, recommendations };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// =========================================================================
|
|
481
|
+
// Factory middleware
|
|
482
|
+
// =========================================================================
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Creates an MCP middleware object with security handlers.
|
|
486
|
+
* @param {object} [options] - MCPBridge options
|
|
487
|
+
* @returns {{ onToolCall: Function, onToolResult: Function, onResourceAccess: Function }}
|
|
488
|
+
*/
|
|
489
|
+
function createMCPMiddleware(options = {}) {
|
|
490
|
+
const bridge = new MCPBridge(options);
|
|
491
|
+
const resourceScanner = new MCPResourceScanner(options);
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
/**
|
|
495
|
+
* Handler for tool calls.
|
|
496
|
+
* @param {string} toolName
|
|
497
|
+
* @param {object} args
|
|
498
|
+
* @returns {{ allowed: boolean, threats: Array }}
|
|
499
|
+
*/
|
|
500
|
+
onToolCall(toolName, args) {
|
|
501
|
+
return bridge.wrapToolCall(toolName, args);
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Handler for tool results.
|
|
506
|
+
* @param {string} toolName
|
|
507
|
+
* @param {*} result
|
|
508
|
+
* @returns {{ safe: boolean, threats: Array }}
|
|
509
|
+
*/
|
|
510
|
+
onToolResult(toolName, result) {
|
|
511
|
+
return bridge.wrapToolResult(toolName, result);
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Handler for resource access.
|
|
516
|
+
* @param {string} uri
|
|
517
|
+
* @param {string} content
|
|
518
|
+
* @param {string} mimeType
|
|
519
|
+
* @returns {{ safe: boolean, threats: Array }}
|
|
520
|
+
*/
|
|
521
|
+
onResourceAccess(uri, content, mimeType) {
|
|
522
|
+
return resourceScanner.scanResource(uri, content, mimeType);
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
/** Returns the underlying bridge for stats/config */
|
|
526
|
+
getBridge() { return bridge; }
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// =========================================================================
|
|
531
|
+
// Exports
|
|
532
|
+
// =========================================================================
|
|
533
|
+
|
|
534
|
+
module.exports = {
|
|
535
|
+
MCPBridge,
|
|
536
|
+
MCPToolPolicy,
|
|
537
|
+
MCPSessionGuard,
|
|
538
|
+
MCPResourceScanner,
|
|
539
|
+
MCP_DANGEROUS_TOOLS,
|
|
540
|
+
ARG_INJECTION_PATTERNS,
|
|
541
|
+
createMCPMiddleware
|
|
542
|
+
};
|