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.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. 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
+ };