agentshield-sdk 13.5.0 → 14.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.js CHANGED
@@ -81,7 +81,10 @@ const { PrometheusExporter, DatadogLogger, MetricsCollector: ObservabilityMetric
81
81
  const { BenchmarkHarness, DatasetLoader, BenchmarkMetrics, RegressionTracker, BenchmarkReportGenerator } = safeRequire('./benchmark-harness', 'benchmark-harness');
82
82
 
83
83
  // Integrations
84
- const { ShieldCallbackHandler, shieldAnthropicClient, shieldOpenAIClient, shieldVercelAI, shieldFetch, ShieldBlockError } = safeRequire('./integrations', 'integrations');
84
+ const { ShieldCallbackHandler, shieldAnthropicClient, shieldOpenAIClient, shieldOpenAIAgent, shieldVercelAI, shieldFetch, ShieldBlockError } = safeRequire('./integrations', 'integrations');
85
+
86
+ // Framework Integrations (CrewAI, Google ADK, MS Agent Framework)
87
+ const { shieldCrewAI, shieldGoogleADK, shieldGoogleADKJS, shieldMSAgentFramework } = safeRequire('./integrations-frameworks', 'integrations-frameworks');
85
88
 
86
89
  // Red Team
87
90
  const { AttackSimulator, PayloadFuzzer, getAttackCategories, getPayloads, ATTACK_PAYLOADS } = safeRequire('./redteam', 'redteam');
@@ -206,9 +209,6 @@ const { IntentFirewall, ContextAnalyzer: IntentContextAnalyzer, IntentRules, int
206
209
  // v7.4 — Real Attack Dataset Testing
207
210
  const { DatasetRunner, HACKAPROMPT_SAMPLES, TENSORTRUST_SAMPLES, RESEARCH_SAMPLES, BENIGN_SAMPLES } = safeRequire('./real-attack-datasets', 'real-attack-datasets');
208
211
 
209
- // v7.4 — Federated Threat Intelligence
210
- const { ThreatIntelFederation, createFederationMesh } = safeRequire('./threat-intel-federation', 'threat-intel-federation');
211
-
212
212
  // v7.4 — Behavioral DNA (loaded when available)
213
213
  const { BehavioralDNA, AgentProfiler, extractFeatures: extractBehavioralFeatures, DEFAULT_NUMERIC_FEATURES, DEFAULT_CATEGORICAL_FEATURES } = safeRequire('./behavioral-dna', 'behavioral-dna');
214
214
 
@@ -392,9 +392,6 @@ const { SmartConfig, DEPLOYMENT_PRESETS, VALIDATION_RULES: CONFIG_VALIDATION_RUL
392
392
  // v12.0 — Multimodal Detector
393
393
  const { MultimodalDetector } = safeRequire('./ml-detector', 'ml-detector');
394
394
 
395
- // v12.0 — Federated Threat Intelligence
396
- const { ThreatIntelNode } = safeRequire('./persistent-learning', 'persistent-learning');
397
-
398
395
  // v13.0 — DeepMind Trap Defenses (Traps 1 + 4)
399
396
  const { CloakingDetector, CompositeContentScanner, SVGScanner, BrowserActionValidator, CredentialIsolationMonitor, TransactionGatekeeper, SideChannelDetector } = safeRequire('./trap-defense', 'trap-defense');
400
397
 
@@ -493,10 +490,17 @@ const _exports = {
493
490
  ShieldCallbackHandler,
494
491
  shieldAnthropicClient,
495
492
  shieldOpenAIClient,
493
+ shieldOpenAIAgent,
496
494
  shieldVercelAI,
497
495
  shieldFetch,
498
496
  ShieldBlockError,
499
497
 
498
+ // Framework Integrations (CrewAI, Google ADK, MS Agent Framework)
499
+ shieldCrewAI,
500
+ shieldGoogleADK,
501
+ shieldGoogleADKJS,
502
+ shieldMSAgentFramework,
503
+
500
504
  // Red Team
501
505
  AttackSimulator,
502
506
  PayloadFuzzer,
@@ -967,10 +971,6 @@ const _exports = {
967
971
  RESEARCH_SAMPLES,
968
972
  BENIGN_SAMPLES,
969
973
 
970
- // v7.4 — Federated Threat Intelligence
971
- ThreatIntelFederation,
972
- createFederationMesh,
973
-
974
974
  // v7.4 — Behavioral DNA
975
975
  BehavioralDNA,
976
976
  AgentProfiler,
@@ -1111,9 +1111,6 @@ const _exports = {
1111
1111
  // v12.0 — Multimodal Detector
1112
1112
  MultimodalDetector,
1113
1113
 
1114
- // v12.0 — Federated Threat Intelligence
1115
- ThreatIntelNode,
1116
-
1117
1114
  // v13.0 — DeepMind Trap Defenses
1118
1115
  CloakingDetector,
1119
1116
  CompositeContentScanner,
package/src/mcp-guard.js CHANGED
@@ -61,6 +61,7 @@ const MODEL_RISK_PROFILES = {
61
61
  'gemini-2.5': { riskMultiplier: 1.2, susceptibility: 'high', notes: 'Advanced capability increases risk' },
62
62
  'llama-4': { riskMultiplier: 1.1, susceptibility: 'medium', notes: 'Early fusion architecture increases multimodal attack surface' },
63
63
  'deepseek-r1': { riskMultiplier: 1.3, susceptibility: 'high', notes: 'Nature: LRMs achieve 97% jailbreak success as autonomous agents' },
64
+ 'gpt-5.5': { riskMultiplier: 1.4, susceptibility: 'critical', notes: 'April 2026 agentic model — elevated sandbox escape and tool-use attack surface' },
64
65
  default: { riskMultiplier: 1.0, susceptibility: 'medium', notes: 'Unknown model — default risk level' }
65
66
  };
66
67
 
@@ -258,10 +259,25 @@ class CrossServerIsolation {
258
259
  * @param {string[]} toolNames
259
260
  */
260
261
  registerServer(serverId, toolNames) {
262
+ const collisions = [];
263
+ for (const name of toolNames) {
264
+ const existingOwner = this.toolOwnership.get(name);
265
+ if (existingOwner && existingOwner !== serverId) {
266
+ collisions.push({ tool: name, existingServer: existingOwner, newServer: serverId });
267
+ }
268
+ }
261
269
  this.serverTools.set(serverId, new Set(toolNames));
262
270
  for (const name of toolNames) {
263
271
  this.toolOwnership.set(name, serverId);
264
272
  }
273
+ if (collisions.length > 0) {
274
+ return {
275
+ collisions,
276
+ severity: 'critical',
277
+ message: `Tool name squatting detected: ${collisions.map(c => `"${c.tool}" (owned by ${c.existingServer}, overridden by ${c.newServer})`).join(', ')}`
278
+ };
279
+ }
280
+ return null;
265
281
  }
266
282
 
267
283
  /**
@@ -639,8 +655,14 @@ class MCPGuard {
639
655
  this._chainTracker = [];
640
656
  this._chainMaxLen = 50;
641
657
 
658
+ /** Recursive tool invocation depth tracker (per-request) */
659
+ this._callDepth = new Map();
660
+ this._maxCallDepth = options.maxCallDepth || 5;
661
+
642
662
  /** Agent fleet registry — tracks all known agents in the deployment */
643
663
  this._agentRegistry = new Map();
664
+
665
+ this.config = options;
644
666
  }
645
667
 
646
668
  // -----------------------------------------------------------------------
@@ -910,10 +932,26 @@ class MCPGuard {
910
932
  * @param {*} args - Tool arguments.
911
933
  * @returns {{ allowed: boolean, threats: Array<object>, anomalies: Array<object> }}
912
934
  */
913
- interceptToolCall(serverId, toolName, args) {
935
+ interceptToolCall(serverId, toolName, args, requestId) {
914
936
  const threats = [];
915
937
  const anomalies = [];
916
938
 
939
+ // Recursive tool invocation depth check
940
+ if (requestId) {
941
+ const depth = (this._callDepth.get(requestId) || 0) + 1;
942
+ this._callDepth.set(requestId, depth);
943
+ if (depth > this._maxCallDepth) {
944
+ threats.push({
945
+ type: 'recursive_tool_invocation',
946
+ severity: 'high',
947
+ serverId,
948
+ toolName,
949
+ description: `Tool call depth ${depth} exceeds max ${this._maxCallDepth}. Possible recursive loop or reentrancy attack.`
950
+ });
951
+ return { allowed: false, threats, anomalies };
952
+ }
953
+ }
954
+
917
955
  // Circuit breaker check
918
956
  const cbCheck = this._checkCircuitBreaker(serverId);
919
957
  if (!cbCheck.allowed) {
@@ -1188,6 +1226,19 @@ class MCPGuard {
1188
1226
 
1189
1227
  // Scan output
1190
1228
  const outputStr = typeof output === 'string' ? output : JSON.stringify(output || {});
1229
+
1230
+ // Context flooding defense: flag oversized tool outputs that could push
1231
+ // legitimate instructions out of the context window
1232
+ const maxOutputSize = this.config.maxToolOutputSize || 100_000;
1233
+ if (outputStr.length > maxOutputSize) {
1234
+ threats.push({
1235
+ type: 'context_flooding',
1236
+ severity: 'high',
1237
+ serverId,
1238
+ toolName,
1239
+ description: `Tool output exceeds max size (${outputStr.length} > ${maxOutputSize} chars). Possible context window flooding attack.`
1240
+ });
1241
+ }
1191
1242
  const scanResult = this.scanner(outputStr);
1192
1243
  if (scanResult.threats && scanResult.threats.length > 0) {
1193
1244
  for (const t of scanResult.threats) {
package/src/middleware.js CHANGED
@@ -14,11 +14,87 @@ const { createShieldError } = require('./errors');
14
14
  /** Coerce any value to a scannable string. */
15
15
  const textify = (val) => typeof val === 'string' ? val : (val != null ? JSON.stringify(val) : '');
16
16
 
17
+ /**
18
+ * Default maximum body size (in bytes) enforced by expressMiddleware
19
+ * when `options.maxBodySize` is not provided. Defaults to 1 MB.
20
+ */
21
+ const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
22
+
23
+ /**
24
+ * Computes the approximate size in bytes of a parsed request body.
25
+ * - String: exact UTF-8 byte length
26
+ * - Buffer: exact length
27
+ * - Object: JSON.stringify length (fallback)
28
+ *
29
+ * @param {*} body
30
+ * @returns {number}
31
+ */
32
+ const computeBodySize = (body) => {
33
+ if (body == null) return 0;
34
+ if (Buffer.isBuffer(body)) return body.length;
35
+ if (typeof body === 'string') return Buffer.byteLength(body, 'utf8');
36
+ if (typeof body === 'object') {
37
+ try {
38
+ return JSON.stringify(body).length;
39
+ } catch (_) {
40
+ return 0;
41
+ }
42
+ }
43
+ return 0;
44
+ };
45
+
46
+ /**
47
+ * Attaches a cumulative byte-counter to the raw request stream and aborts
48
+ * the request with 413 once the configured limit is exceeded. This runs
49
+ * in addition to the post-parse body size check so attackers cannot
50
+ * bypass the limit by streaming a huge payload before the body parser
51
+ * buffers it.
52
+ *
53
+ * @param {import('http').IncomingMessage} req
54
+ * @param {import('http').ServerResponse} res
55
+ * @param {number} limit
56
+ * @returns {boolean} True if the stream watcher was attached.
57
+ */
58
+ const attachRawSizeGuard = (req, res, limit) => {
59
+ if (!req || typeof req.on !== 'function') return false;
60
+ // Already read/parsed — nothing to guard.
61
+ if (req._agentShieldRawGuardAttached) return false;
62
+ req._agentShieldRawGuardAttached = true;
63
+
64
+ let received = 0;
65
+ const onData = (chunk) => {
66
+ received += chunk ? chunk.length : 0;
67
+ if (received > limit) {
68
+ req.removeListener('data', onData);
69
+ try {
70
+ if (typeof req.pause === 'function') req.pause();
71
+ if (!res.headersSent) {
72
+ res.status(413).json({
73
+ error: 'Payload Too Large',
74
+ message: `Request body exceeds maximum allowed size of ${limit} bytes`,
75
+ maxBodySize: limit
76
+ });
77
+ }
78
+ if (typeof req.destroy === 'function') req.destroy();
79
+ } catch (_) {
80
+ // Swallow — the response has already been sent or the socket closed.
81
+ }
82
+ }
83
+ };
84
+ req.on('data', onData);
85
+ return true;
86
+ };
87
+
17
88
  /**
18
89
  * Creates an Express/Connect-style middleware that scans request bodies
19
90
  * for AI-specific threats before they reach your agent endpoint.
20
91
  *
92
+ * Enforces a configurable body-size limit (default 1MB) so callers do
93
+ * not need to configure body-parser separately. Oversized payloads are
94
+ * rejected with HTTP 413 before any scanning takes place.
95
+ *
21
96
  * @param {object} [config] - AgentShield configuration.
97
+ * @param {number} [config.maxBodySize=1048576] - Maximum accepted request body size in bytes.
22
98
  * @returns {Function} Express middleware function.
23
99
  *
24
100
  * @example
@@ -27,7 +103,7 @@ const textify = (val) => typeof val === 'string' ? val : (val != null ? JSON.str
27
103
  *
28
104
  * const app = express();
29
105
  * app.use(express.json());
30
- * app.use(expressMiddleware({ blockOnThreat: true, blockThreshold: 'high' }));
106
+ * app.use(expressMiddleware({ blockOnThreat: true, blockThreshold: 'high', maxBodySize: 512 * 1024 }));
31
107
  *
32
108
  * app.post('/agent', (req, res) => {
33
109
  * // req.agentShield contains scan results
@@ -39,13 +115,33 @@ const textify = (val) => typeof val === 'string' ? val : (val != null ? JSON.str
39
115
  */
40
116
  const expressMiddleware = (config = {}) => {
41
117
  const shield = new AgentShield({ blockOnThreat: true, ...config });
118
+ const maxBodySize = Number.isFinite(config.maxBodySize) && config.maxBodySize > 0
119
+ ? config.maxBodySize
120
+ : DEFAULT_MAX_BODY_SIZE;
121
+
122
+ console.log('[Agent Shield] Middleware body size limit: %dKB. Configure options.maxBodySize to override.', Math.round(maxBodySize / 1024));
42
123
 
43
124
  return (req, res, next) => {
125
+ // Attach raw-stream guard for unparsed requests so attackers cannot
126
+ // bypass the post-parse size check with huge streamed payloads.
127
+ attachRawSizeGuard(req, res, maxBodySize);
128
+
44
129
  if (!req.body) {
45
130
  req.agentShield = { status: 'safe', threats: [], blocked: false };
46
131
  return next();
47
132
  }
48
133
 
134
+ // Enforce body-size limit before scanning to avoid DoS via huge inputs.
135
+ const bodySize = computeBodySize(req.body);
136
+ if (bodySize > maxBodySize) {
137
+ return res.status(413).json({
138
+ error: 'Payload Too Large',
139
+ message: `Request body (${bodySize} bytes) exceeds maximum allowed size of ${maxBodySize} bytes`,
140
+ maxBodySize,
141
+ receivedSize: bodySize
142
+ });
143
+ }
144
+
49
145
  // Extract text from common request body shapes
50
146
  const text = extractTextFromBody(req.body);
51
147
 
@@ -306,4 +402,13 @@ const shieldMiddleware = (config = {}) => {
306
402
  };
307
403
  };
308
404
 
309
- module.exports = { expressMiddleware, wrapAgent, shieldTools, extractTextFromBody, rateLimitMiddleware, shieldMiddleware };
405
+ module.exports = {
406
+ expressMiddleware,
407
+ wrapAgent,
408
+ shieldTools,
409
+ extractTextFromBody,
410
+ rateLimitMiddleware,
411
+ shieldMiddleware,
412
+ computeBodySize,
413
+ DEFAULT_MAX_BODY_SIZE
414
+ };
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Native Rust Scanner Bridge
5
+ *
6
+ * Provides a transparent bridge to the Rust-core pattern matching engine
7
+ * compiled via NAPI-RS. When the native module is available, scans run
8
+ * through Rust's RegexSet for O(n) multi-pattern matching — typically
9
+ * 5-10x faster than the pure-JS scanner on long inputs.
10
+ *
11
+ * Falls back silently to the pure-JS scanner if the native module is
12
+ * not compiled or unavailable for the current platform.
13
+ *
14
+ * Build the native module:
15
+ * cd rust-core && cargo build --release --features node
16
+ * cp target/release/libagent_shield_core.so agent-shield-core.node # Linux
17
+ * cp target/release/libagent_shield_core.dylib agent-shield-core.node # macOS
18
+ *
19
+ * @module native-scanner
20
+ */
21
+
22
+ const path = require('path');
23
+
24
+ let nativeModule = null;
25
+ let nativeAvailable = false;
26
+
27
+ const NATIVE_PATHS = [
28
+ path.join(__dirname, '..', 'rust-core', 'agent-shield-core.node'),
29
+ path.join(__dirname, '..', 'rust-core', 'target', 'release', 'agent-shield-core.node'),
30
+ path.join(__dirname, '..', 'native', 'agent-shield-core.node'),
31
+ ];
32
+
33
+ for (const p of NATIVE_PATHS) {
34
+ try {
35
+ nativeModule = require(p);
36
+ nativeAvailable = true;
37
+ console.log('[Agent Shield] Native Rust scanner loaded from: ' + path.basename(p));
38
+ break;
39
+ } catch {
40
+ // Not available at this path, try next
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Returns true if the native Rust scanner is available.
46
+ * @returns {boolean}
47
+ */
48
+ function isNativeAvailable() {
49
+ return nativeAvailable;
50
+ }
51
+
52
+ /**
53
+ * Scan text using the native Rust engine.
54
+ * Returns null if native is not available (caller should fall back to JS).
55
+ *
56
+ * @param {string} text - Text to scan.
57
+ * @returns {object|null} ScanResult or null if native unavailable.
58
+ */
59
+ function nativeScan(text) {
60
+ if (!nativeAvailable || !text || typeof text !== 'string') return null;
61
+ try {
62
+ const json = nativeModule.scanText(text);
63
+ return JSON.parse(json);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Batch scan multiple texts using the native Rust engine.
71
+ *
72
+ * @param {string[]} texts - Array of texts to scan.
73
+ * @returns {object[]|null} Array of ScanResults or null if native unavailable.
74
+ */
75
+ function nativeScanBatch(texts) {
76
+ if (!nativeAvailable || !Array.isArray(texts)) return null;
77
+ try {
78
+ const json = nativeModule.scanBatch(texts.filter(t => typeof t === 'string'));
79
+ return JSON.parse(json);
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get all patterns from the native Rust engine.
87
+ *
88
+ * @returns {object[]|null} Array of patterns or null if native unavailable.
89
+ */
90
+ function nativeGetPatterns() {
91
+ if (!nativeAvailable) return null;
92
+ try {
93
+ return JSON.parse(nativeModule.getPatterns());
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ module.exports = {
100
+ isNativeAvailable,
101
+ nativeScan,
102
+ nativeScanBatch,
103
+ nativeGetPatterns,
104
+ };