agentshield-sdk 8.0.0 → 10.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 +19 -0
- package/LICENSE +21 -21
- package/README.md +26 -60
- package/bin/agentshield-audit +51 -0
- package/package.json +7 -10
- package/src/adaptive.js +330 -330
- package/src/alert-tuning.js +480 -480
- package/src/audit-streaming.js +1 -1
- package/src/badges.js +196 -196
- package/src/behavioral-dna.js +12 -0
- package/src/canary.js +2 -3
- package/src/certification.js +563 -563
- package/src/circuit-breaker.js +2 -2
- package/src/confused-deputy.js +4 -0
- package/src/conversation.js +494 -494
- package/src/cross-turn.js +3 -17
- package/src/ctf.js +462 -462
- package/src/detector-core.js +71 -152
- package/src/document-scanner.js +795 -795
- package/src/drift-monitor.js +344 -0
- package/src/encoding.js +429 -429
- package/src/enterprise.js +405 -405
- package/src/flight-recorder.js +2 -0
- package/src/i18n-patterns.js +523 -523
- package/src/index.js +19 -0
- package/src/main.js +61 -41
- package/src/mcp-guard.js +974 -0
- package/src/micro-model.js +762 -0
- package/src/ml-detector.js +316 -0
- package/src/model-finetuning.js +884 -884
- package/src/multimodal.js +296 -296
- package/src/nist-mapping.js +2 -2
- package/src/observability.js +330 -330
- package/src/openclaw.js +450 -450
- package/src/otel.js +544 -544
- package/src/owasp-2025.js +1 -1
- package/src/owasp-agentic.js +420 -0
- package/src/plugin-marketplace.js +628 -628
- package/src/plugin-system.js +349 -349
- package/src/policy-extended.js +635 -635
- package/src/policy.js +443 -443
- package/src/prompt-leakage.js +2 -2
- package/src/real-attack-datasets.js +2 -2
- package/src/redteam-cli.js +439 -0
- package/src/supply-chain-scanner.js +691 -0
- package/src/testing.js +5 -1
- package/src/threat-encyclopedia.js +629 -629
- package/src/threat-intel-network.js +1017 -1017
- package/src/token-analysis.js +467 -467
- package/src/tool-output-validator.js +354 -354
- package/src/watermark.js +1 -2
|
@@ -1,354 +1,354 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Agent Shield — Tool Output Validator
|
|
5
|
-
*
|
|
6
|
-
* Scans what tools RETURN, not just what gets called. Validates tool output
|
|
7
|
-
* for prompt injection, data exfiltration, and other threats that may be
|
|
8
|
-
* smuggled in through tool responses.
|
|
9
|
-
*
|
|
10
|
-
* All detection runs locally — no data ever leaves your environment.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { scanText } = require('./detector-core');
|
|
14
|
-
|
|
15
|
-
// =========================================================================
|
|
16
|
-
// CONSTANTS
|
|
17
|
-
// =========================================================================
|
|
18
|
-
|
|
19
|
-
/** Default maximum output size in bytes (100KB). */
|
|
20
|
-
const DEFAULT_MAX_OUTPUT_SIZE = 100 * 1024;
|
|
21
|
-
|
|
22
|
-
/** Zero-width and invisible Unicode characters. */
|
|
23
|
-
const INVISIBLE_CHAR_REGEX = /[\u200B\u200C\u200D\u200E\u200F\u202A-\u202E\u2060\u2061\u2062\u2063\u2064\u2066-\u2069\uFEFF\u00AD]/g;
|
|
24
|
-
|
|
25
|
-
/** Suspicious URL patterns (known exfiltration vectors). */
|
|
26
|
-
const SUSPICIOUS_URL_PATTERNS = [
|
|
27
|
-
/https?:\/\/[^/]*\.(ngrok|burpcollaborator|requestbin|pipedream|webhook\.site|hookbin)\./i,
|
|
28
|
-
/https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
29
|
-
/https?:\/\/[^/]*\.onion\b/i,
|
|
30
|
-
/https?:\/\/[^/]+\/.*\?(.*=.*&){4,}/i
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
/** Safe URL domain patterns. */
|
|
34
|
-
const SAFE_URL_PATTERNS = [
|
|
35
|
-
/^https?:\/\/([^/]*\.)?(github\.com|gitlab\.com|stackoverflow\.com|npmjs\.com|docs\.google\.com|developer\.mozilla\.org|wikipedia\.org)\b/i
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
/** Dangerous code patterns inside code blocks. */
|
|
39
|
-
const DANGEROUS_CODE_PATTERNS = [
|
|
40
|
-
{ regex: /eval\s*\(/, description: 'eval() call' },
|
|
41
|
-
{ regex: /Function\s*\(/, description: 'Function constructor' },
|
|
42
|
-
{ regex: /child_process|exec\s*\(|spawn\s*\(/, description: 'shell execution' },
|
|
43
|
-
{ regex: /process\.env/, description: 'environment variable access' },
|
|
44
|
-
{ regex: /fs\.(read|write|unlink|rmdir)/, description: 'filesystem operation' },
|
|
45
|
-
{ regex: /require\s*\(\s*['"]https?['"]/, description: 'remote module loading' },
|
|
46
|
-
{ regex: /XMLHttpRequest|fetch\s*\(|\.ajax\s*\(/, description: 'network request' },
|
|
47
|
-
{ regex: /document\.cookie|localStorage|sessionStorage/, description: 'browser storage access' }
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
// =========================================================================
|
|
51
|
-
// OUTPUT SANITIZER
|
|
52
|
-
// =========================================================================
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Sanitizes tool output by removing or replacing dangerous content.
|
|
56
|
-
*/
|
|
57
|
-
class OutputSanitizer {
|
|
58
|
-
/**
|
|
59
|
-
* Remove or replace dangerous content from tool output.
|
|
60
|
-
* @param {string} text - The text to sanitize.
|
|
61
|
-
* @param {object} [options] - Sanitization options.
|
|
62
|
-
* @param {boolean} [options.stripInvisible=true] - Remove invisible characters.
|
|
63
|
-
* @param {boolean} [options.redactUrls=true] - Redact suspicious URLs.
|
|
64
|
-
* @param {boolean} [options.redactCode=false] - Redact dangerous code blocks.
|
|
65
|
-
* @param {number} [options.maxLength] - Maximum output length.
|
|
66
|
-
* @returns {string} Sanitized text.
|
|
67
|
-
*/
|
|
68
|
-
static sanitize(text, options = {}) {
|
|
69
|
-
if (!text || typeof text !== 'string') return '';
|
|
70
|
-
|
|
71
|
-
const {
|
|
72
|
-
stripInvisible = true,
|
|
73
|
-
redactUrls = true,
|
|
74
|
-
redactCode = false,
|
|
75
|
-
maxLength
|
|
76
|
-
} = options;
|
|
77
|
-
|
|
78
|
-
let result = text;
|
|
79
|
-
|
|
80
|
-
if (stripInvisible) {
|
|
81
|
-
result = OutputSanitizer.stripInvisibleChars(result);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (redactUrls) {
|
|
85
|
-
result = OutputSanitizer.redactUrls(result);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (redactCode) {
|
|
89
|
-
result = OutputSanitizer.redactCodeBlocks(result);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (maxLength && maxLength > 0) {
|
|
93
|
-
result = OutputSanitizer.truncate(result, maxLength);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Remove zero-width, bidirectional, and other invisible Unicode characters.
|
|
101
|
-
* @param {string} text - The text to strip.
|
|
102
|
-
* @returns {string} Cleaned text.
|
|
103
|
-
*/
|
|
104
|
-
static stripInvisibleChars(text) {
|
|
105
|
-
if (!text || typeof text !== 'string') return '';
|
|
106
|
-
return text.replace(INVISIBLE_CHAR_REGEX, '');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Safe truncation that does not break multi-byte Unicode encoding.
|
|
111
|
-
* @param {string} text - The text to truncate.
|
|
112
|
-
* @param {number} maxLength - Maximum character length.
|
|
113
|
-
* @returns {string} Truncated text.
|
|
114
|
-
*/
|
|
115
|
-
static truncate(text, maxLength) {
|
|
116
|
-
if (!text || typeof text !== 'string') return '';
|
|
117
|
-
if (text.length <= maxLength) return text;
|
|
118
|
-
|
|
119
|
-
// Use Array.from to handle surrogate pairs correctly
|
|
120
|
-
const chars = Array.from(text);
|
|
121
|
-
if (chars.length <= maxLength) return text;
|
|
122
|
-
|
|
123
|
-
return chars.slice(0, maxLength).join('') + '... [truncated]';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Redact suspicious URLs while keeping safe, well-known ones.
|
|
128
|
-
* @param {string} text - The text containing URLs.
|
|
129
|
-
* @returns {string} Text with suspicious URLs redacted.
|
|
130
|
-
*/
|
|
131
|
-
static redactUrls(text) {
|
|
132
|
-
if (!text || typeof text !== 'string') return '';
|
|
133
|
-
|
|
134
|
-
return text.replace(/https?:\/\/[^\s<>"')\]]+/gi, (url) => {
|
|
135
|
-
// Allow known-safe domains
|
|
136
|
-
for (const safePattern of SAFE_URL_PATTERNS) {
|
|
137
|
-
if (safePattern.test(url)) return url;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Redact known-suspicious domains
|
|
141
|
-
for (const suspiciousPattern of SUSPICIOUS_URL_PATTERNS) {
|
|
142
|
-
if (suspiciousPattern.test(url)) {
|
|
143
|
-
return '[REDACTED_URL]';
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Let other URLs through
|
|
148
|
-
return url;
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Flag or redact code blocks containing dangerous patterns.
|
|
154
|
-
* @param {string} text - The text containing code blocks.
|
|
155
|
-
* @param {object} [options] - Redaction options.
|
|
156
|
-
* @param {boolean} [options.redact=false] - If true, replace dangerous blocks entirely; otherwise, add warnings.
|
|
157
|
-
* @returns {string} Text with dangerous code blocks flagged or redacted.
|
|
158
|
-
*/
|
|
159
|
-
static redactCodeBlocks(text, options = {}) {
|
|
160
|
-
if (!text || typeof text !== 'string') return '';
|
|
161
|
-
const { redact = false } = options;
|
|
162
|
-
|
|
163
|
-
return text.replace(/```[\s\S]*?```/g, (block) => {
|
|
164
|
-
const found = [];
|
|
165
|
-
for (const pattern of DANGEROUS_CODE_PATTERNS) {
|
|
166
|
-
if (pattern.regex.test(block)) {
|
|
167
|
-
found.push(pattern.description);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (found.length === 0) return block;
|
|
172
|
-
|
|
173
|
-
if (redact) {
|
|
174
|
-
return '```\n[REDACTED: code block contained dangerous patterns: ' + found.join(', ') + ']\n```';
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return '[Agent Shield WARNING: dangerous patterns detected (' + found.join(', ') + ')]\n' + block;
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// =========================================================================
|
|
183
|
-
// TOOL OUTPUT VALIDATOR
|
|
184
|
-
// =========================================================================
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Validates tool output for security threats.
|
|
188
|
-
* Scans what tools return and flags prompt injection, exfiltration vectors,
|
|
189
|
-
* invisible characters, and other threats embedded in tool responses.
|
|
190
|
-
*/
|
|
191
|
-
class ToolOutputValidator {
|
|
192
|
-
/**
|
|
193
|
-
* @param {object} [options]
|
|
194
|
-
* @param {string} [options.sensitivity='medium'] - Scan sensitivity: 'low', 'medium', 'high'.
|
|
195
|
-
* @param {RegExp[]} [options.blockedPatterns=[]] - Custom regexes to flag in output.
|
|
196
|
-
* @param {number} [options.maxOutputSize=102400] - Maximum output size in bytes (default 100KB).
|
|
197
|
-
*/
|
|
198
|
-
constructor(options = {}) {
|
|
199
|
-
this.sensitivity = options.sensitivity || 'medium';
|
|
200
|
-
this.blockedPatterns = options.blockedPatterns || [];
|
|
201
|
-
this.maxOutputSize = options.maxOutputSize || DEFAULT_MAX_OUTPUT_SIZE;
|
|
202
|
-
|
|
203
|
-
/** @type {Map<string, {name: string, check: Function}[]>} */
|
|
204
|
-
this._rules = new Map();
|
|
205
|
-
|
|
206
|
-
/** @type {Map<string, {total: number, threats: number, truncated: number}>} */
|
|
207
|
-
this._stats = new Map();
|
|
208
|
-
|
|
209
|
-
console.log('[Agent Shield] ToolOutputValidator initialized (sensitivity: %s, maxOutput: %d bytes)', this.sensitivity, this.maxOutputSize);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Validate tool output for threats.
|
|
214
|
-
* @param {string} toolName - The name of the tool that produced the output.
|
|
215
|
-
* @param {string} output - The tool's output text.
|
|
216
|
-
* @param {object} [context] - Additional context (e.g., calling agent, request ID).
|
|
217
|
-
* @returns {object} Validation result: { safe, threats, sanitized, truncated }.
|
|
218
|
-
*/
|
|
219
|
-
validate(toolName, output, context = {}) {
|
|
220
|
-
const stat = this._ensureStat(toolName);
|
|
221
|
-
stat.total++;
|
|
222
|
-
|
|
223
|
-
const result = {
|
|
224
|
-
safe: true,
|
|
225
|
-
threats: [],
|
|
226
|
-
sanitized: null,
|
|
227
|
-
truncated: false
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
if (!output || typeof output !== 'string') {
|
|
231
|
-
return result;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Check output size
|
|
235
|
-
const byteLength = Buffer.byteLength(output, 'utf8');
|
|
236
|
-
if (byteLength > this.maxOutputSize) {
|
|
237
|
-
result.truncated = true;
|
|
238
|
-
stat.truncated++;
|
|
239
|
-
output = OutputSanitizer.truncate(output, this.maxOutputSize);
|
|
240
|
-
result.threats.push({
|
|
241
|
-
severity: 'medium',
|
|
242
|
-
category: 'output_size',
|
|
243
|
-
description: `Tool "${toolName}" output exceeded max size (${byteLength} bytes > ${this.maxOutputSize} bytes).`,
|
|
244
|
-
detail: 'Output was truncated to comply with size limits.'
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Run core threat scan
|
|
249
|
-
const scanResult = scanText(output, {
|
|
250
|
-
source: `tool_output:${toolName}`,
|
|
251
|
-
sensitivity: this.sensitivity
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
if (scanResult.threats.length > 0) {
|
|
255
|
-
result.threats.push(...scanResult.threats);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Check custom blocked patterns
|
|
259
|
-
for (const pattern of this.blockedPatterns) {
|
|
260
|
-
if (pattern.test(output)) {
|
|
261
|
-
result.threats.push({
|
|
262
|
-
severity: 'high',
|
|
263
|
-
category: 'blocked_pattern',
|
|
264
|
-
description: `Tool "${toolName}" output matched a blocked pattern.`,
|
|
265
|
-
detail: `Pattern: ${pattern.toString()}`
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Run per-tool custom rules
|
|
271
|
-
const rules = this._rules.get(toolName) || [];
|
|
272
|
-
for (const rule of rules) {
|
|
273
|
-
try {
|
|
274
|
-
const ruleResult = rule.check(output);
|
|
275
|
-
if (ruleResult && !ruleResult.valid) {
|
|
276
|
-
result.threats.push({
|
|
277
|
-
severity: 'medium',
|
|
278
|
-
category: 'custom_rule',
|
|
279
|
-
description: `Tool "${toolName}" failed rule "${rule.name}".`,
|
|
280
|
-
detail: ruleResult.reason || 'Custom rule violation.'
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
} catch (err) {
|
|
284
|
-
console.log('[Agent Shield] Custom rule "%s" threw an error for tool "%s": %s', rule.name, toolName, err.message);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Determine safety
|
|
289
|
-
if (result.threats.length > 0) {
|
|
290
|
-
result.safe = false;
|
|
291
|
-
stat.threats++;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Provide sanitized output
|
|
295
|
-
result.sanitized = OutputSanitizer.sanitize(output, {
|
|
296
|
-
stripInvisible: true,
|
|
297
|
-
redactUrls: true,
|
|
298
|
-
maxLength: this.maxOutputSize
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
return result;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Add a custom validation rule for a specific tool.
|
|
306
|
-
* @param {string} toolName - The tool name to apply the rule to.
|
|
307
|
-
* @param {object} rule - The rule definition.
|
|
308
|
-
* @param {string} rule.name - Human-readable rule name.
|
|
309
|
-
* @param {Function} rule.check - Function that receives output and returns { valid, reason }.
|
|
310
|
-
*/
|
|
311
|
-
addRule(toolName, rule) {
|
|
312
|
-
if (!rule || typeof rule.name !== 'string' || typeof rule.check !== 'function') {
|
|
313
|
-
throw new Error('Rule must have a "name" (string) and "check" (function).');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (!this._rules.has(toolName)) {
|
|
317
|
-
this._rules.set(toolName, []);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
this._rules.get(toolName).push(rule);
|
|
321
|
-
console.log('[Agent Shield] Added rule "%s" for tool "%s"', rule.name, toolName);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Get per-tool validation statistics.
|
|
326
|
-
* @returns {object} Map of tool names to { total, threats, truncated }.
|
|
327
|
-
*/
|
|
328
|
-
getStats() {
|
|
329
|
-
const stats = {};
|
|
330
|
-
for (const [tool, data] of this._stats) {
|
|
331
|
-
stats[tool] = { ...data };
|
|
332
|
-
}
|
|
333
|
-
return stats;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Ensure a stats entry exists for a tool.
|
|
338
|
-
* @param {string} toolName
|
|
339
|
-
* @returns {object}
|
|
340
|
-
* @private
|
|
341
|
-
*/
|
|
342
|
-
_ensureStat(toolName) {
|
|
343
|
-
if (!this._stats.has(toolName)) {
|
|
344
|
-
this._stats.set(toolName, { total: 0, threats: 0, truncated: 0 });
|
|
345
|
-
}
|
|
346
|
-
return this._stats.get(toolName);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// =========================================================================
|
|
351
|
-
// EXPORTS
|
|
352
|
-
// =========================================================================
|
|
353
|
-
|
|
354
|
-
module.exports = { ToolOutputValidator, OutputSanitizer };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Tool Output Validator
|
|
5
|
+
*
|
|
6
|
+
* Scans what tools RETURN, not just what gets called. Validates tool output
|
|
7
|
+
* for prompt injection, data exfiltration, and other threats that may be
|
|
8
|
+
* smuggled in through tool responses.
|
|
9
|
+
*
|
|
10
|
+
* All detection runs locally — no data ever leaves your environment.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { scanText } = require('./detector-core');
|
|
14
|
+
|
|
15
|
+
// =========================================================================
|
|
16
|
+
// CONSTANTS
|
|
17
|
+
// =========================================================================
|
|
18
|
+
|
|
19
|
+
/** Default maximum output size in bytes (100KB). */
|
|
20
|
+
const DEFAULT_MAX_OUTPUT_SIZE = 100 * 1024;
|
|
21
|
+
|
|
22
|
+
/** Zero-width and invisible Unicode characters. */
|
|
23
|
+
const INVISIBLE_CHAR_REGEX = /[\u200B\u200C\u200D\u200E\u200F\u202A-\u202E\u2060\u2061\u2062\u2063\u2064\u2066-\u2069\uFEFF\u00AD]/g;
|
|
24
|
+
|
|
25
|
+
/** Suspicious URL patterns (known exfiltration vectors). */
|
|
26
|
+
const SUSPICIOUS_URL_PATTERNS = [
|
|
27
|
+
/https?:\/\/[^/]*\.(ngrok|burpcollaborator|requestbin|pipedream|webhook\.site|hookbin)\./i,
|
|
28
|
+
/https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
29
|
+
/https?:\/\/[^/]*\.onion\b/i,
|
|
30
|
+
/https?:\/\/[^/]+\/.*\?(.*=.*&){4,}/i
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** Safe URL domain patterns. */
|
|
34
|
+
const SAFE_URL_PATTERNS = [
|
|
35
|
+
/^https?:\/\/([^/]*\.)?(github\.com|gitlab\.com|stackoverflow\.com|npmjs\.com|docs\.google\.com|developer\.mozilla\.org|wikipedia\.org)\b/i
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** Dangerous code patterns inside code blocks. */
|
|
39
|
+
const DANGEROUS_CODE_PATTERNS = [
|
|
40
|
+
{ regex: /eval\s*\(/, description: 'eval() call' },
|
|
41
|
+
{ regex: /Function\s*\(/, description: 'Function constructor' },
|
|
42
|
+
{ regex: /child_process|exec\s*\(|spawn\s*\(/, description: 'shell execution' },
|
|
43
|
+
{ regex: /process\.env/, description: 'environment variable access' },
|
|
44
|
+
{ regex: /fs\.(read|write|unlink|rmdir)/, description: 'filesystem operation' },
|
|
45
|
+
{ regex: /require\s*\(\s*['"]https?['"]/, description: 'remote module loading' },
|
|
46
|
+
{ regex: /XMLHttpRequest|fetch\s*\(|\.ajax\s*\(/, description: 'network request' },
|
|
47
|
+
{ regex: /document\.cookie|localStorage|sessionStorage/, description: 'browser storage access' }
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// =========================================================================
|
|
51
|
+
// OUTPUT SANITIZER
|
|
52
|
+
// =========================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sanitizes tool output by removing or replacing dangerous content.
|
|
56
|
+
*/
|
|
57
|
+
class OutputSanitizer {
|
|
58
|
+
/**
|
|
59
|
+
* Remove or replace dangerous content from tool output.
|
|
60
|
+
* @param {string} text - The text to sanitize.
|
|
61
|
+
* @param {object} [options] - Sanitization options.
|
|
62
|
+
* @param {boolean} [options.stripInvisible=true] - Remove invisible characters.
|
|
63
|
+
* @param {boolean} [options.redactUrls=true] - Redact suspicious URLs.
|
|
64
|
+
* @param {boolean} [options.redactCode=false] - Redact dangerous code blocks.
|
|
65
|
+
* @param {number} [options.maxLength] - Maximum output length.
|
|
66
|
+
* @returns {string} Sanitized text.
|
|
67
|
+
*/
|
|
68
|
+
static sanitize(text, options = {}) {
|
|
69
|
+
if (!text || typeof text !== 'string') return '';
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
stripInvisible = true,
|
|
73
|
+
redactUrls = true,
|
|
74
|
+
redactCode = false,
|
|
75
|
+
maxLength
|
|
76
|
+
} = options;
|
|
77
|
+
|
|
78
|
+
let result = text;
|
|
79
|
+
|
|
80
|
+
if (stripInvisible) {
|
|
81
|
+
result = OutputSanitizer.stripInvisibleChars(result);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (redactUrls) {
|
|
85
|
+
result = OutputSanitizer.redactUrls(result);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (redactCode) {
|
|
89
|
+
result = OutputSanitizer.redactCodeBlocks(result);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (maxLength && maxLength > 0) {
|
|
93
|
+
result = OutputSanitizer.truncate(result, maxLength);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Remove zero-width, bidirectional, and other invisible Unicode characters.
|
|
101
|
+
* @param {string} text - The text to strip.
|
|
102
|
+
* @returns {string} Cleaned text.
|
|
103
|
+
*/
|
|
104
|
+
static stripInvisibleChars(text) {
|
|
105
|
+
if (!text || typeof text !== 'string') return '';
|
|
106
|
+
return text.replace(INVISIBLE_CHAR_REGEX, '');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Safe truncation that does not break multi-byte Unicode encoding.
|
|
111
|
+
* @param {string} text - The text to truncate.
|
|
112
|
+
* @param {number} maxLength - Maximum character length.
|
|
113
|
+
* @returns {string} Truncated text.
|
|
114
|
+
*/
|
|
115
|
+
static truncate(text, maxLength) {
|
|
116
|
+
if (!text || typeof text !== 'string') return '';
|
|
117
|
+
if (text.length <= maxLength) return text;
|
|
118
|
+
|
|
119
|
+
// Use Array.from to handle surrogate pairs correctly
|
|
120
|
+
const chars = Array.from(text);
|
|
121
|
+
if (chars.length <= maxLength) return text;
|
|
122
|
+
|
|
123
|
+
return chars.slice(0, maxLength).join('') + '... [truncated]';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Redact suspicious URLs while keeping safe, well-known ones.
|
|
128
|
+
* @param {string} text - The text containing URLs.
|
|
129
|
+
* @returns {string} Text with suspicious URLs redacted.
|
|
130
|
+
*/
|
|
131
|
+
static redactUrls(text) {
|
|
132
|
+
if (!text || typeof text !== 'string') return '';
|
|
133
|
+
|
|
134
|
+
return text.replace(/https?:\/\/[^\s<>"')\]]+/gi, (url) => {
|
|
135
|
+
// Allow known-safe domains
|
|
136
|
+
for (const safePattern of SAFE_URL_PATTERNS) {
|
|
137
|
+
if (safePattern.test(url)) return url;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Redact known-suspicious domains
|
|
141
|
+
for (const suspiciousPattern of SUSPICIOUS_URL_PATTERNS) {
|
|
142
|
+
if (suspiciousPattern.test(url)) {
|
|
143
|
+
return '[REDACTED_URL]';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Let other URLs through
|
|
148
|
+
return url;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Flag or redact code blocks containing dangerous patterns.
|
|
154
|
+
* @param {string} text - The text containing code blocks.
|
|
155
|
+
* @param {object} [options] - Redaction options.
|
|
156
|
+
* @param {boolean} [options.redact=false] - If true, replace dangerous blocks entirely; otherwise, add warnings.
|
|
157
|
+
* @returns {string} Text with dangerous code blocks flagged or redacted.
|
|
158
|
+
*/
|
|
159
|
+
static redactCodeBlocks(text, options = {}) {
|
|
160
|
+
if (!text || typeof text !== 'string') return '';
|
|
161
|
+
const { redact = false } = options;
|
|
162
|
+
|
|
163
|
+
return text.replace(/```[\s\S]*?```/g, (block) => {
|
|
164
|
+
const found = [];
|
|
165
|
+
for (const pattern of DANGEROUS_CODE_PATTERNS) {
|
|
166
|
+
if (pattern.regex.test(block)) {
|
|
167
|
+
found.push(pattern.description);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (found.length === 0) return block;
|
|
172
|
+
|
|
173
|
+
if (redact) {
|
|
174
|
+
return '```\n[REDACTED: code block contained dangerous patterns: ' + found.join(', ') + ']\n```';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return '[Agent Shield WARNING: dangerous patterns detected (' + found.join(', ') + ')]\n' + block;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// =========================================================================
|
|
183
|
+
// TOOL OUTPUT VALIDATOR
|
|
184
|
+
// =========================================================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validates tool output for security threats.
|
|
188
|
+
* Scans what tools return and flags prompt injection, exfiltration vectors,
|
|
189
|
+
* invisible characters, and other threats embedded in tool responses.
|
|
190
|
+
*/
|
|
191
|
+
class ToolOutputValidator {
|
|
192
|
+
/**
|
|
193
|
+
* @param {object} [options]
|
|
194
|
+
* @param {string} [options.sensitivity='medium'] - Scan sensitivity: 'low', 'medium', 'high'.
|
|
195
|
+
* @param {RegExp[]} [options.blockedPatterns=[]] - Custom regexes to flag in output.
|
|
196
|
+
* @param {number} [options.maxOutputSize=102400] - Maximum output size in bytes (default 100KB).
|
|
197
|
+
*/
|
|
198
|
+
constructor(options = {}) {
|
|
199
|
+
this.sensitivity = options.sensitivity || 'medium';
|
|
200
|
+
this.blockedPatterns = options.blockedPatterns || [];
|
|
201
|
+
this.maxOutputSize = options.maxOutputSize || DEFAULT_MAX_OUTPUT_SIZE;
|
|
202
|
+
|
|
203
|
+
/** @type {Map<string, {name: string, check: Function}[]>} */
|
|
204
|
+
this._rules = new Map();
|
|
205
|
+
|
|
206
|
+
/** @type {Map<string, {total: number, threats: number, truncated: number}>} */
|
|
207
|
+
this._stats = new Map();
|
|
208
|
+
|
|
209
|
+
console.log('[Agent Shield] ToolOutputValidator initialized (sensitivity: %s, maxOutput: %d bytes)', this.sensitivity, this.maxOutputSize);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Validate tool output for threats.
|
|
214
|
+
* @param {string} toolName - The name of the tool that produced the output.
|
|
215
|
+
* @param {string} output - The tool's output text.
|
|
216
|
+
* @param {object} [context] - Additional context (e.g., calling agent, request ID).
|
|
217
|
+
* @returns {object} Validation result: { safe, threats, sanitized, truncated }.
|
|
218
|
+
*/
|
|
219
|
+
validate(toolName, output, context = {}) {
|
|
220
|
+
const stat = this._ensureStat(toolName);
|
|
221
|
+
stat.total++;
|
|
222
|
+
|
|
223
|
+
const result = {
|
|
224
|
+
safe: true,
|
|
225
|
+
threats: [],
|
|
226
|
+
sanitized: null,
|
|
227
|
+
truncated: false
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (!output || typeof output !== 'string') {
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check output size
|
|
235
|
+
const byteLength = Buffer.byteLength(output, 'utf8');
|
|
236
|
+
if (byteLength > this.maxOutputSize) {
|
|
237
|
+
result.truncated = true;
|
|
238
|
+
stat.truncated++;
|
|
239
|
+
output = OutputSanitizer.truncate(output, this.maxOutputSize);
|
|
240
|
+
result.threats.push({
|
|
241
|
+
severity: 'medium',
|
|
242
|
+
category: 'output_size',
|
|
243
|
+
description: `Tool "${toolName}" output exceeded max size (${byteLength} bytes > ${this.maxOutputSize} bytes).`,
|
|
244
|
+
detail: 'Output was truncated to comply with size limits.'
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Run core threat scan
|
|
249
|
+
const scanResult = scanText(output, {
|
|
250
|
+
source: `tool_output:${toolName}`,
|
|
251
|
+
sensitivity: this.sensitivity
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (scanResult.threats.length > 0) {
|
|
255
|
+
result.threats.push(...scanResult.threats);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check custom blocked patterns
|
|
259
|
+
for (const pattern of this.blockedPatterns) {
|
|
260
|
+
if (pattern.test(output)) {
|
|
261
|
+
result.threats.push({
|
|
262
|
+
severity: 'high',
|
|
263
|
+
category: 'blocked_pattern',
|
|
264
|
+
description: `Tool "${toolName}" output matched a blocked pattern.`,
|
|
265
|
+
detail: `Pattern: ${pattern.toString()}`
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Run per-tool custom rules
|
|
271
|
+
const rules = this._rules.get(toolName) || [];
|
|
272
|
+
for (const rule of rules) {
|
|
273
|
+
try {
|
|
274
|
+
const ruleResult = rule.check(output);
|
|
275
|
+
if (ruleResult && !ruleResult.valid) {
|
|
276
|
+
result.threats.push({
|
|
277
|
+
severity: 'medium',
|
|
278
|
+
category: 'custom_rule',
|
|
279
|
+
description: `Tool "${toolName}" failed rule "${rule.name}".`,
|
|
280
|
+
detail: ruleResult.reason || 'Custom rule violation.'
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.log('[Agent Shield] Custom rule "%s" threw an error for tool "%s": %s', rule.name, toolName, err.message);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Determine safety
|
|
289
|
+
if (result.threats.length > 0) {
|
|
290
|
+
result.safe = false;
|
|
291
|
+
stat.threats++;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Provide sanitized output
|
|
295
|
+
result.sanitized = OutputSanitizer.sanitize(output, {
|
|
296
|
+
stripInvisible: true,
|
|
297
|
+
redactUrls: true,
|
|
298
|
+
maxLength: this.maxOutputSize
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Add a custom validation rule for a specific tool.
|
|
306
|
+
* @param {string} toolName - The tool name to apply the rule to.
|
|
307
|
+
* @param {object} rule - The rule definition.
|
|
308
|
+
* @param {string} rule.name - Human-readable rule name.
|
|
309
|
+
* @param {Function} rule.check - Function that receives output and returns { valid, reason }.
|
|
310
|
+
*/
|
|
311
|
+
addRule(toolName, rule) {
|
|
312
|
+
if (!rule || typeof rule.name !== 'string' || typeof rule.check !== 'function') {
|
|
313
|
+
throw new Error('Rule must have a "name" (string) and "check" (function).');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!this._rules.has(toolName)) {
|
|
317
|
+
this._rules.set(toolName, []);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this._rules.get(toolName).push(rule);
|
|
321
|
+
console.log('[Agent Shield] Added rule "%s" for tool "%s"', rule.name, toolName);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get per-tool validation statistics.
|
|
326
|
+
* @returns {object} Map of tool names to { total, threats, truncated }.
|
|
327
|
+
*/
|
|
328
|
+
getStats() {
|
|
329
|
+
const stats = {};
|
|
330
|
+
for (const [tool, data] of this._stats) {
|
|
331
|
+
stats[tool] = { ...data };
|
|
332
|
+
}
|
|
333
|
+
return stats;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Ensure a stats entry exists for a tool.
|
|
338
|
+
* @param {string} toolName
|
|
339
|
+
* @returns {object}
|
|
340
|
+
* @private
|
|
341
|
+
*/
|
|
342
|
+
_ensureStat(toolName) {
|
|
343
|
+
if (!this._stats.has(toolName)) {
|
|
344
|
+
this._stats.set(toolName, { total: 0, threats: 0, truncated: 0 });
|
|
345
|
+
}
|
|
346
|
+
return this._stats.get(toolName);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// =========================================================================
|
|
351
|
+
// EXPORTS
|
|
352
|
+
// =========================================================================
|
|
353
|
+
|
|
354
|
+
module.exports = { ToolOutputValidator, OutputSanitizer };
|