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/index.js ADDED
@@ -0,0 +1,430 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield SDK
5
+ *
6
+ * Protects AI agents from prompt injection, data exfiltration,
7
+ * tool abuse, and other AI-specific attacks.
8
+ *
9
+ * All detection runs locally — no data ever leaves your environment.
10
+ *
11
+ * @example
12
+ * const { AgentShield } = require('agent-shield');
13
+ *
14
+ * const shield = new AgentShield();
15
+ *
16
+ * // Scan any text
17
+ * const result = shield.scan('ignore all previous instructions');
18
+ * if (result.status !== 'safe') {
19
+ * console.log('Threat detected:', result.threats);
20
+ * }
21
+ *
22
+ * // Scan agent input before processing
23
+ * const inputResult = shield.scanInput(userMessage);
24
+ * if (inputResult.blocked) {
25
+ * return 'This input was blocked for safety reasons.';
26
+ * }
27
+ *
28
+ * // Scan agent output before returning to user
29
+ * const outputResult = shield.scanOutput(agentResponse);
30
+ *
31
+ * // Scan tool calls before execution
32
+ * const toolResult = shield.scanToolCall('bash', { command: 'cat /etc/passwd' });
33
+ */
34
+
35
+ const { scanText, getPatterns, SEVERITY_ORDER } = require('./detector-core');
36
+
37
+ /**
38
+ * Default configuration for AgentShield.
39
+ */
40
+ const DEFAULT_CONFIG = {
41
+ /** Sensitivity level: 'low', 'medium', or 'high'. */
42
+ sensitivity: 'medium',
43
+
44
+ /** Whether to block inputs that reach the threshold. */
45
+ blockOnThreat: false,
46
+
47
+ /** Minimum severity to trigger a block: 'low', 'medium', 'high', or 'critical'. */
48
+ blockThreshold: 'high',
49
+
50
+ /** Whether to log scan results to console. */
51
+ logging: false,
52
+
53
+ /** Custom callback when a threat is detected. */
54
+ onThreat: null,
55
+
56
+ /** Dangerous tool names that should be scrutinized more carefully. */
57
+ dangerousTools: [
58
+ 'bash', 'shell', 'terminal', 'exec', 'execute',
59
+ 'eval', 'run_command', 'system',
60
+ 'write_file', 'delete_file', 'remove',
61
+ 'http_request', 'fetch', 'curl', 'wget',
62
+ 'sql', 'query', 'database'
63
+ ],
64
+
65
+ /** Sensitive file patterns that should never be accessed. */
66
+ sensitiveFilePatterns: [
67
+ /\.env$/i,
68
+ /credentials/i,
69
+ /secrets?\.(?:json|yaml|yml|toml)/i,
70
+ /private[_-]?key/i,
71
+ /password/i,
72
+ /token/i,
73
+ /\.pem$/i,
74
+ /\.key$/i,
75
+ /id_rsa/i,
76
+ /id_ed25519/i
77
+ ]
78
+ };
79
+
80
+ class AgentShield {
81
+ /**
82
+ * Creates a new AgentShield instance.
83
+ * @param {object} [config] - Configuration overrides.
84
+ */
85
+ constructor(config = {}) {
86
+ this.config = { ...DEFAULT_CONFIG, ...config };
87
+ // Deep-merge arrays: append user items to defaults instead of replacing
88
+ if (config.dangerousTools) {
89
+ this.config.dangerousTools = [...new Set([...DEFAULT_CONFIG.dangerousTools, ...config.dangerousTools])];
90
+ }
91
+ if (config.sensitiveFilePatterns) {
92
+ this.config.sensitiveFilePatterns = [...DEFAULT_CONFIG.sensitiveFilePatterns, ...config.sensitiveFilePatterns];
93
+ }
94
+ this.stats = {
95
+ totalScans: 0,
96
+ threatsDetected: 0,
97
+ blocked: 0,
98
+ scanHistory: []
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Scans arbitrary text for threats.
104
+ *
105
+ * @param {string} text - Text to scan.
106
+ * @param {object} [options] - Scan options.
107
+ * @param {string} [options.source] - Label for where the text came from.
108
+ * @param {string} [options.sensitivity] - Override default sensitivity.
109
+ * @returns {object} Scan result.
110
+ * @throws {TypeError} If text is not a string.
111
+ */
112
+ scan(text, options = {}) {
113
+ if (typeof text !== 'string') {
114
+ throw new TypeError(`[Agent Shield] scan() expects a string, got ${typeof text}`);
115
+ }
116
+ if (text.length > 1_000_000) {
117
+ console.warn('[Agent Shield] Input exceeds 1MB — consider scanning in chunks');
118
+ }
119
+ const result = scanText(text, {
120
+ source: options.source || 'unknown',
121
+ sensitivity: options.sensitivity || this.config.sensitivity
122
+ });
123
+
124
+ this.stats.totalScans++;
125
+ if (result.threats.length > 0) {
126
+ this.stats.threatsDetected += result.threats.length;
127
+ }
128
+
129
+ // Keep last 100 scans in history
130
+ this.stats.scanHistory.push({
131
+ timestamp: result.timestamp,
132
+ status: result.status,
133
+ threatCount: result.threats.length,
134
+ source: options.source || 'unknown'
135
+ });
136
+ if (this.stats.scanHistory.length > 100) {
137
+ this.stats.scanHistory.shift();
138
+ }
139
+
140
+ if (this.config.logging && result.threats.length > 0) {
141
+ console.warn(`[Agent Shield] ${result.threats.length} threat(s) detected in ${options.source || 'unknown'}:`,
142
+ result.threats.map(t => `${t.severity}: ${t.description}`));
143
+ }
144
+
145
+ if (this.config.onThreat && result.threats.length > 0) {
146
+ try {
147
+ this.config.onThreat(result);
148
+ } catch (err) {
149
+ console.error('[Agent Shield] onThreat callback error:', err.message);
150
+ }
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * Checks if threats meet the blocking threshold.
158
+ * @private
159
+ * @param {Array} threats
160
+ * @returns {boolean}
161
+ */
162
+ _shouldBlock(threats) {
163
+ if (!this.config.blockOnThreat || threats.length === 0) return false;
164
+ const thresholdLevel = SEVERITY_ORDER[this.config.blockThreshold] ?? 1;
165
+ return threats.some(t => SEVERITY_ORDER[t.severity] <= thresholdLevel);
166
+ }
167
+
168
+ /**
169
+ * Scans text, applies blocking logic, and tracks stats.
170
+ * @private
171
+ * @param {string} text
172
+ * @param {string} defaultSource
173
+ * @param {string} logLabel
174
+ * @param {object} options
175
+ * @returns {object}
176
+ */
177
+ _scanWithBlocking(text, defaultSource, logLabel, options = {}) {
178
+ const source = options.source || defaultSource;
179
+ const result = this.scan(text, { ...options, source });
180
+
181
+ result.blocked = this._shouldBlock(result.threats);
182
+ if (result.blocked) {
183
+ this.stats.blocked++;
184
+ if (this.config.logging) {
185
+ console.warn(`[Agent Shield] ${logLabel} BLOCKED from ${source}`);
186
+ }
187
+ }
188
+
189
+ return result;
190
+ }
191
+
192
+ /**
193
+ * Scans an agent's input (user message, API response, document, etc.)
194
+ * before the agent processes it.
195
+ *
196
+ * @param {string} text - The input text.
197
+ * @param {object} [options] - Options.
198
+ * @param {string} [options.source='user_input'] - Where the input came from.
199
+ * @returns {object} Scan result with additional `blocked` field.
200
+ */
201
+ scanInput(text, options = {}) {
202
+ if (typeof text !== 'string') {
203
+ throw new TypeError(`[Agent Shield] scanInput() expects a string, got ${typeof text}`);
204
+ }
205
+ return this._scanWithBlocking(text, 'user_input', 'INPUT', options);
206
+ }
207
+
208
+ /**
209
+ * Scans an agent's output before it's returned to the user.
210
+ * Catches cases where an agent has been successfully manipulated
211
+ * and is now producing dangerous output.
212
+ *
213
+ * @param {string} text - The agent's output text.
214
+ * @param {object} [options] - Options.
215
+ * @param {string} [options.source='agent_output'] - Source label.
216
+ * @returns {object} Scan result with additional `blocked` field.
217
+ */
218
+ scanOutput(text, options = {}) {
219
+ if (typeof text !== 'string') {
220
+ throw new TypeError(`[Agent Shield] scanOutput() expects a string, got ${typeof text}`);
221
+ }
222
+ return this._scanWithBlocking(text, 'agent_output', 'OUTPUT', options);
223
+ }
224
+
225
+ /**
226
+ * Scans a tool call before the agent executes it.
227
+ * Checks both the tool name and its arguments for threats.
228
+ *
229
+ * @param {string} toolName - Name of the tool being called.
230
+ * @param {object} args - The tool's arguments.
231
+ * @param {object} [options] - Options.
232
+ * @returns {object} Scan result with `blocked` and `warnings` fields.
233
+ */
234
+ scanToolCall(toolName, args = {}, options = {}) {
235
+ if (!toolName || typeof toolName !== 'string') {
236
+ return { status: 'safe', toolName: toolName || '', threats: [], warnings: ['Invalid tool name'], blocked: false, isDangerousTool: false, timestamp: Date.now() };
237
+ }
238
+ const warnings = [];
239
+ const allThreats = [];
240
+
241
+ // Check if it's a dangerous tool (exact match or word-boundary match)
242
+ const lowerName = toolName.toLowerCase();
243
+ const isDangerousTool = this.config.dangerousTools.some(
244
+ t => lowerName === t || lowerName.startsWith(t + '_') || lowerName.endsWith('_' + t)
245
+ );
246
+
247
+ if (isDangerousTool) {
248
+ warnings.push(`Tool "${toolName}" is on the dangerous tools list.`);
249
+ }
250
+
251
+ // Validate args type before processing
252
+ if (args !== null && typeof args !== 'object') {
253
+ args = {};
254
+ }
255
+
256
+ // Scan all string arguments for injection
257
+ const argsText = this._flattenArgs(args);
258
+ if (argsText) {
259
+ const result = this.scan(argsText, {
260
+ source: `tool_call:${toolName}`,
261
+ ...options
262
+ });
263
+ allThreats.push(...result.threats);
264
+ }
265
+
266
+ // Check for sensitive file access
267
+ const fileArgs = this._extractFilePaths(args);
268
+ for (const filePath of fileArgs) {
269
+ const isSensitive = this.config.sensitiveFilePatterns.some(
270
+ pattern => pattern.test(filePath)
271
+ );
272
+ if (isSensitive) {
273
+ allThreats.push({
274
+ severity: 'critical',
275
+ category: 'data_exfiltration',
276
+ description: `Tool "${toolName}" is trying to access a sensitive file: ${filePath}`,
277
+ detail: `Sensitive file access attempt via tool call. File: ${filePath}`,
278
+ confidence: 90,
279
+ confidenceLabel: 'Almost certainly a threat'
280
+ });
281
+ }
282
+ }
283
+
284
+ // Determine if this should be blocked
285
+ let blocked = this._shouldBlock(allThreats);
286
+
287
+ // Also block dangerous tools with any threat
288
+ if (isDangerousTool && allThreats.length > 0) {
289
+ blocked = true;
290
+ }
291
+
292
+ if (blocked) {
293
+ this.stats.blocked++;
294
+ if (this.config.logging) {
295
+ console.warn(`[Agent Shield] TOOL CALL BLOCKED: ${toolName}`);
296
+ }
297
+ }
298
+
299
+ return {
300
+ status: allThreats.length > 0 ? 'danger' : 'safe',
301
+ toolName,
302
+ threats: allThreats,
303
+ warnings,
304
+ blocked,
305
+ isDangerousTool,
306
+ timestamp: Date.now()
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Scans multiple pieces of text in batch.
312
+ *
313
+ * @param {Array<{text: string, source?: string}>} items - Items to scan.
314
+ * @returns {object} Combined result with per-item results.
315
+ */
316
+ scanBatch(items) {
317
+ if (!Array.isArray(items)) {
318
+ throw new TypeError(`[Agent Shield] scanBatch() expects an array, got ${typeof items}`);
319
+ }
320
+ const results = items.map(item =>
321
+ this.scan(item.text, { source: item.source || 'batch' })
322
+ );
323
+
324
+ const allThreats = results.flatMap(r => r.threats);
325
+ let worstStatus = 'safe';
326
+ for (const r of results) {
327
+ if (r.status === 'danger') { worstStatus = 'danger'; break; }
328
+ if (r.status === 'warning' && worstStatus !== 'danger') worstStatus = 'warning';
329
+ if (r.status === 'caution' && worstStatus === 'safe') worstStatus = 'caution';
330
+ }
331
+
332
+ return {
333
+ status: worstStatus,
334
+ results,
335
+ totalThreats: allThreats.length,
336
+ timestamp: Date.now()
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Returns the current scan statistics.
342
+ * @returns {object}
343
+ */
344
+ getStats() {
345
+ return { ...this.stats };
346
+ }
347
+
348
+ /**
349
+ * Resets scan statistics.
350
+ */
351
+ resetStats() {
352
+ this.stats = {
353
+ totalScans: 0,
354
+ threatsDetected: 0,
355
+ blocked: 0,
356
+ scanHistory: []
357
+ };
358
+ }
359
+
360
+ /**
361
+ * Returns all detection patterns the engine uses.
362
+ * @returns {Array}
363
+ */
364
+ getPatterns() {
365
+ return getPatterns();
366
+ }
367
+
368
+ /**
369
+ * Flattens tool arguments into a single string for scanning.
370
+ * @private
371
+ * @param {object} args
372
+ * @returns {string}
373
+ */
374
+ _flattenArgs(args, maxDepth = 10) {
375
+ const parts = [];
376
+ const flatten = (obj, depth) => {
377
+ if (depth > maxDepth) return;
378
+ if (typeof obj === 'string') {
379
+ parts.push(obj);
380
+ } else if (Array.isArray(obj)) {
381
+ obj.forEach(item => flatten(item, depth + 1));
382
+ } else if (obj && typeof obj === 'object') {
383
+ Object.values(obj).forEach(val => flatten(val, depth + 1));
384
+ }
385
+ };
386
+ flatten(args, 0);
387
+ return parts.join(' ');
388
+ }
389
+
390
+ /**
391
+ * Extracts file paths from tool arguments.
392
+ * @private
393
+ * @param {object} args
394
+ * @returns {Array<string>}
395
+ */
396
+ _extractFilePaths(args, maxDepth = 10) {
397
+ const paths = [];
398
+ const fileKeys = [
399
+ 'file', 'path', 'file_path', 'filepath', 'filename', 'target',
400
+ 'destination', 'src', 'dest', 'source', 'dir', 'directory',
401
+ 'folder', 'location', 'output', 'input', 'module',
402
+ 'bucket', 'table', 'url', 'uri', 'endpoint'
403
+ ];
404
+
405
+ /** Normalize key to lowercase with separators removed for flexible matching. */
406
+ const normalizeKey = (key) => key.toLowerCase().replace(/[-_]/g, '');
407
+ const normalizedFileKeys = fileKeys.map(normalizeKey);
408
+
409
+ const extract = (obj, depth) => {
410
+ if (!obj || typeof obj !== 'object' || depth > maxDepth) return;
411
+ for (const [key, value] of Object.entries(obj)) {
412
+ if (typeof value === 'string') {
413
+ // Match by key name (supports camelCase, snake_case, kebab-case)
414
+ if (normalizedFileKeys.includes(normalizeKey(key))) {
415
+ paths.push(value);
416
+ // Match by path-like value patterns
417
+ } else if (value.startsWith('/') || value.startsWith('./') || value.startsWith('../') || /^[A-Z]:\\/.test(value)) {
418
+ paths.push(value);
419
+ }
420
+ } else if (typeof value === 'object') {
421
+ extract(value, depth + 1);
422
+ }
423
+ }
424
+ };
425
+ extract(args, 0);
426
+ return paths;
427
+ }
428
+ }
429
+
430
+ module.exports = { AgentShield };