agentshield-sdk 13.5.0 → 14.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 +97 -0
- package/README.md +12 -1
- package/package.json +2 -2
- package/src/detector-core.js +135 -51
- package/src/enterprise.js +127 -12
- package/src/integrations-frameworks.js +373 -0
- package/src/integrations.js +207 -0
- package/src/main.js +10 -14
- package/src/middleware.js +107 -2
- package/src/native-scanner.js +104 -0
- package/src/plugin-system.js +422 -6
- package/src/persistent-learning.js +0 -161
- package/src/threat-intel-federation.js +0 -343
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 = {
|
|
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
|
+
};
|