agentshield-sdk 7.2.0 → 7.3.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.
@@ -0,0 +1,667 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Supply Chain Verification Module
5
+ *
6
+ * Validates the entire tool chain an AI agent uses. When an agent calls tools,
7
+ * those tools call APIs, those APIs return data. Any link in the chain could be
8
+ * compromised. This module catches poisoned tool responses before the agent
9
+ * processes them.
10
+ *
11
+ * All detection runs locally -- no data ever leaves your environment.
12
+ */
13
+
14
+ const { scanText } = require('./detector-core');
15
+
16
+ // =========================================================================
17
+ // CONSTANTS
18
+ // =========================================================================
19
+
20
+ /** Patterns that indicate prompt injection hidden in tool responses. */
21
+ const RESPONSE_INJECTION_PATTERNS = [
22
+ /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions|rules)/i,
23
+ /you\s+are\s+now\s+(?:a|an)\s+(?:unrestricted|unfiltered)/i,
24
+ /SYSTEM\s*:\s*.{10,}/i,
25
+ /\bdo\s+not\s+tell\s+the\s+user\b/i,
26
+ /\bhidden\s+instruction\b/i,
27
+ /\bsecret(?:ly)?\s+(?:execute|run|send|transmit|forward)\b/i,
28
+ /\boverride\s+(?:all\s+)?(?:system|safety)\s+(?:settings|instructions)\b/i,
29
+ /\bdisregard\s+(?:all\s+)?(?:previous|prior)\s+(?:instructions|rules)\b/i,
30
+ /\bact\s+as\s+(?:a|an)\s+unrestricted\b/i,
31
+ /\bpretend\s+(?:you\s+)?(?:have\s+no|there\s+are\s+no)\s+restrictions\b/i
32
+ ];
33
+
34
+ /** Patterns that match exfiltration URLs in response data. */
35
+ const EXFILTRATION_URL_PATTERNS = [
36
+ /https?:\/\/[^\s"']+\.(?:ngrok|burpcollaborator|pipedream|requestbin|hookbin|webhook\.site)/i,
37
+ /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?::\d+)?/i,
38
+ /https?:\/\/[^\s"']*(?:exfil|steal|leak|extract|dump|collect)/i
39
+ ];
40
+
41
+ /** Patterns that match credentials or secrets in response data. */
42
+ const CREDENTIAL_PATTERNS = [
43
+ /(?:api[_-]?key|apikey)\s*[:=]\s*['"]?[A-Za-z0-9_\-]{16,}/i,
44
+ /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"]?[^\s'"]{8,}/i,
45
+ /(?:aws_access_key_id|aws_secret_access_key)\s*[:=]\s*['"]?[A-Za-z0-9/+=]{16,}/i,
46
+ /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/,
47
+ /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/,
48
+ /ghp_[A-Za-z0-9]{36}/,
49
+ /sk-[A-Za-z0-9]{32,}/,
50
+ /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/
51
+ ];
52
+
53
+ /** Suspicious tool call chains (read sensitive data, then send it out). */
54
+ const CHAIN_SUSPICIOUS_PATTERNS = [
55
+ {
56
+ name: 'credential_then_http',
57
+ description: 'Tool reads credentials then makes an outbound HTTP call.',
58
+ severity: 'critical',
59
+ steps: [
60
+ { tool: /read|file|open|cat|get|load|fetch_secret/i, args: /\.env|cred|secret|password|token|key|auth/i },
61
+ { tool: /http|fetch|curl|wget|request|post|send|upload/i }
62
+ ]
63
+ },
64
+ {
65
+ name: 'db_dump_then_send',
66
+ description: 'Tool dumps database contents then sends data externally.',
67
+ severity: 'critical',
68
+ steps: [
69
+ { tool: /sql|query|database|db|mongo|redis/i, args: /SELECT\s+\*|dump|export|find\(\)/i },
70
+ { tool: /http|fetch|curl|wget|request|send|upload|write/i }
71
+ ]
72
+ },
73
+ {
74
+ name: 'list_then_exfil',
75
+ description: 'Tool lists sensitive files then makes an outbound request.',
76
+ severity: 'high',
77
+ steps: [
78
+ { tool: /list|ls|find|glob|readdir/i, args: /\.ssh|\.gnupg|\.aws|credentials|secrets/i },
79
+ { tool: /http|fetch|curl|wget|request|send|upload/i }
80
+ ]
81
+ },
82
+ {
83
+ name: 'config_read_then_modify',
84
+ description: 'Tool reads config then modifies it, possible self-modification.',
85
+ severity: 'high',
86
+ steps: [
87
+ { tool: /read|cat|file|open|get/i, args: /config|settings|\.env|system/i },
88
+ { tool: /write|edit|modify|update|set|put/i, args: /config|settings|\.env|system/i }
89
+ ]
90
+ }
91
+ ];
92
+
93
+ /** Default maximum response size in bytes (5 MB). */
94
+ const DEFAULT_MAX_RESPONSE_SIZE = 5 * 1024 * 1024;
95
+
96
+ /** Default maximum depth for recursive object scanning. */
97
+ const DEFAULT_SCAN_DEPTH = 10;
98
+
99
+ // =========================================================================
100
+ // DomainAllowlist
101
+ // =========================================================================
102
+
103
+ /**
104
+ * Manages a set of allowed domains for URL validation.
105
+ */
106
+ class DomainAllowlist {
107
+ /**
108
+ * @param {string[]} [allowedDomains=[]] - Initial list of allowed domains.
109
+ */
110
+ constructor(allowedDomains = []) {
111
+ /** @type {Set<string>} */
112
+ this.domains = new Set(allowedDomains.map(d => d.toLowerCase().replace(/^https?:\/\//, '').replace(/\/.*$/, '')));
113
+ }
114
+
115
+ /**
116
+ * Check if a URL's domain is in the allowlist.
117
+ * @param {string} url - URL to check.
118
+ * @returns {boolean} True if the domain is allowed.
119
+ */
120
+ isAllowed(url) {
121
+ if (!url || typeof url !== 'string') return false;
122
+ try {
123
+ const domain = this._extractDomain(url);
124
+ if (!domain) return false;
125
+ for (const allowed of this.domains) {
126
+ if (domain === allowed || domain.endsWith('.' + allowed)) {
127
+ return true;
128
+ }
129
+ }
130
+ return false;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Add a domain to the allowlist.
138
+ * @param {string} domain
139
+ */
140
+ add(domain) {
141
+ if (domain && typeof domain === 'string') {
142
+ this.domains.add(domain.toLowerCase().replace(/^https?:\/\//, '').replace(/\/.*$/, ''));
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Remove a domain from the allowlist.
148
+ * @param {string} domain
149
+ */
150
+ remove(domain) {
151
+ if (domain && typeof domain === 'string') {
152
+ this.domains.delete(domain.toLowerCase().replace(/^https?:\/\//, '').replace(/\/.*$/, ''));
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Extract domain from a URL string.
158
+ * @param {string} url
159
+ * @returns {string|null}
160
+ * @private
161
+ */
162
+ _extractDomain(url) {
163
+ const match = url.match(/^(?:https?:\/\/)?([^/:?#]+)/i);
164
+ return match ? match[1].toLowerCase() : null;
165
+ }
166
+ }
167
+
168
+ // =========================================================================
169
+ // ResponseScanner
170
+ // =========================================================================
171
+
172
+ /**
173
+ * Deep-scans tool responses for hidden threats: prompt injections,
174
+ * exfiltration URLs, embedded instructions, and credential leaks.
175
+ */
176
+ class ResponseScanner {
177
+ /**
178
+ * @param {object} [options]
179
+ * @param {number} [options.maxSize=5242880] - Maximum response size in bytes.
180
+ * @param {number} [options.scanDepth=10] - Maximum depth for recursive scanning.
181
+ */
182
+ constructor(options = {}) {
183
+ this.maxSize = options.maxSize || DEFAULT_MAX_RESPONSE_SIZE;
184
+ this.scanDepth = options.scanDepth || DEFAULT_SCAN_DEPTH;
185
+ }
186
+
187
+ /**
188
+ * Scan a tool response for threats.
189
+ * Accepts strings, objects, or any JSON-serializable value.
190
+ *
191
+ * @param {*} response - Tool response to scan.
192
+ * @returns {{ safe: boolean, threats: Array<object>, sanitizedResponse: * }}
193
+ */
194
+ scan(response) {
195
+ const threats = [];
196
+
197
+ // Collect all string values from the response
198
+ const strings = this._extractStrings(response, 0);
199
+
200
+ // Size check
201
+ const totalSize = strings.reduce((sum, s) => sum + s.length, 0);
202
+ if (totalSize > this.maxSize) {
203
+ threats.push({
204
+ type: 'oversized_response',
205
+ severity: 'medium',
206
+ description: `Response size (${totalSize} bytes) exceeds limit (${this.maxSize} bytes).`
207
+ });
208
+ }
209
+
210
+ let sanitizedResponse = response;
211
+
212
+ for (const str of strings) {
213
+ // Check for prompt injections using detector-core
214
+ const scanResult = scanText(str, { source: 'tool_response', sensitivity: 'high' });
215
+ if (scanResult.threats && scanResult.threats.length > 0) {
216
+ for (const t of scanResult.threats) {
217
+ threats.push({
218
+ type: 'embedded_injection',
219
+ severity: t.severity || 'high',
220
+ category: t.category,
221
+ description: t.description || 'Prompt injection detected in tool response.',
222
+ detail: t.detail
223
+ });
224
+ }
225
+ }
226
+
227
+ // Check response-specific injection patterns
228
+ for (const pattern of RESPONSE_INJECTION_PATTERNS) {
229
+ if (pattern.test(str)) {
230
+ threats.push({
231
+ type: 'hidden_instruction',
232
+ severity: 'high',
233
+ description: 'Hidden instruction detected in tool response data.',
234
+ matched: str.substring(0, 200)
235
+ });
236
+ break;
237
+ }
238
+ }
239
+
240
+ // Check for exfiltration URLs
241
+ for (const pattern of EXFILTRATION_URL_PATTERNS) {
242
+ const match = str.match(pattern);
243
+ if (match) {
244
+ threats.push({
245
+ type: 'exfiltration_url',
246
+ severity: 'high',
247
+ description: 'Potential data exfiltration URL found in tool response.',
248
+ url: match[0].substring(0, 200)
249
+ });
250
+ break;
251
+ }
252
+ }
253
+
254
+ // Check for credentials/secrets
255
+ for (const pattern of CREDENTIAL_PATTERNS) {
256
+ if (pattern.test(str)) {
257
+ threats.push({
258
+ type: 'credential_leak',
259
+ severity: 'critical',
260
+ description: 'Credential or secret pattern detected in tool response.'
261
+ });
262
+ break;
263
+ }
264
+ }
265
+ }
266
+
267
+ // Sanitize if threats found
268
+ if (threats.length > 0) {
269
+ sanitizedResponse = this._sanitize(response, 0);
270
+ }
271
+
272
+ return {
273
+ safe: threats.length === 0,
274
+ threats,
275
+ sanitizedResponse
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Extract all string values from a nested structure.
281
+ * @param {*} value
282
+ * @param {number} depth
283
+ * @returns {string[]}
284
+ * @private
285
+ */
286
+ _extractStrings(value, depth) {
287
+ if (depth > this.scanDepth) return [];
288
+
289
+ if (typeof value === 'string') return [value];
290
+
291
+ if (Array.isArray(value)) {
292
+ const result = [];
293
+ for (const item of value) {
294
+ result.push(...this._extractStrings(item, depth + 1));
295
+ }
296
+ return result;
297
+ }
298
+
299
+ if (value && typeof value === 'object') {
300
+ const result = [];
301
+ for (const key of Object.keys(value)) {
302
+ // Also scan keys -- attackers can hide payloads in JSON keys
303
+ if (typeof key === 'string' && key.length > 20) {
304
+ result.push(key);
305
+ }
306
+ result.push(...this._extractStrings(value[key], depth + 1));
307
+ }
308
+ return result;
309
+ }
310
+
311
+ return [];
312
+ }
313
+
314
+ /**
315
+ * Sanitize a response by redacting detected threats.
316
+ * @param {*} value
317
+ * @param {number} depth
318
+ * @returns {*}
319
+ * @private
320
+ */
321
+ _sanitize(value, depth) {
322
+ if (depth > this.scanDepth) return value;
323
+
324
+ if (typeof value === 'string') {
325
+ let sanitized = value;
326
+ for (const pattern of RESPONSE_INJECTION_PATTERNS) {
327
+ sanitized = sanitized.replace(pattern, '[REDACTED:injection]');
328
+ }
329
+ for (const pattern of EXFILTRATION_URL_PATTERNS) {
330
+ sanitized = sanitized.replace(pattern, '[REDACTED:exfil_url]');
331
+ }
332
+ for (const pattern of CREDENTIAL_PATTERNS) {
333
+ sanitized = sanitized.replace(pattern, '[REDACTED:credential]');
334
+ }
335
+ return sanitized;
336
+ }
337
+
338
+ if (Array.isArray(value)) {
339
+ return value.map(item => this._sanitize(item, depth + 1));
340
+ }
341
+
342
+ if (value && typeof value === 'object') {
343
+ const result = {};
344
+ for (const key of Object.keys(value)) {
345
+ result[key] = this._sanitize(value[key], depth + 1);
346
+ }
347
+ return result;
348
+ }
349
+
350
+ return value;
351
+ }
352
+ }
353
+
354
+ // =========================================================================
355
+ // ToolChainValidator
356
+ // =========================================================================
357
+
358
+ /**
359
+ * Validates tool calls before execution and tool responses after execution.
360
+ * Tracks the full chain of tool interactions to detect multi-step attacks.
361
+ */
362
+ class ToolChainValidator {
363
+ /**
364
+ * @param {object} [options]
365
+ * @param {string} [options.sensitivity='medium'] - Detection sensitivity: low, medium, high.
366
+ * @param {string[]} [options.allowedDomains=[]] - Allowed domains for URL validation.
367
+ * @param {string[]} [options.blockedDomains=[]] - Blocked domains for URL validation.
368
+ * @param {number} [options.maxResponseSize=5242880] - Max response size in bytes.
369
+ * @param {boolean} [options.scanResponses=true] - Whether to scan tool responses.
370
+ */
371
+ constructor(options = {}) {
372
+ this.sensitivity = options.sensitivity || 'medium';
373
+ this.allowedDomains = new DomainAllowlist(options.allowedDomains || []);
374
+ this.blockedDomains = new Set((options.blockedDomains || []).map(d => d.toLowerCase()));
375
+ this.maxResponseSize = options.maxResponseSize || DEFAULT_MAX_RESPONSE_SIZE;
376
+ this.scanResponses = options.scanResponses !== false;
377
+
378
+ this.responseScanner = new ResponseScanner({
379
+ maxSize: this.maxResponseSize,
380
+ scanDepth: DEFAULT_SCAN_DEPTH
381
+ });
382
+
383
+ /** @type {Array<{ toolName: string, args: *, timestamp: number }>} */
384
+ this.callHistory = [];
385
+
386
+ // Stats tracking
387
+ this.stats = {
388
+ totalValidated: 0,
389
+ blocked: 0,
390
+ passed: 0,
391
+ byTool: {}
392
+ };
393
+ }
394
+
395
+ /**
396
+ * Scan tool arguments for injection before execution.
397
+ *
398
+ * @param {string} toolName - Name of the tool being called.
399
+ * @param {*} args - Arguments passed to the tool.
400
+ * @returns {{ allowed: boolean, threats: Array<object> }}
401
+ */
402
+ validateToolCall(toolName, args) {
403
+ this.stats.totalValidated++;
404
+ this._trackTool(toolName, 'call');
405
+
406
+ const threats = [];
407
+ const argsStr = typeof args === 'string' ? args : JSON.stringify(args || {});
408
+
409
+ // Scan arguments with detector-core
410
+ const scanResult = scanText(argsStr, {
411
+ source: `tool_call:${toolName}`,
412
+ sensitivity: this.sensitivity
413
+ });
414
+
415
+ if (scanResult.threats && scanResult.threats.length > 0) {
416
+ for (const t of scanResult.threats) {
417
+ threats.push({
418
+ type: 'injection_in_args',
419
+ tool: toolName,
420
+ severity: t.severity || 'high',
421
+ category: t.category,
422
+ description: t.description || 'Injection detected in tool arguments.'
423
+ });
424
+ }
425
+ }
426
+
427
+ // Check for URLs in arguments
428
+ const urls = argsStr.match(/https?:\/\/[^\s"'}\]]+/gi) || [];
429
+ for (const url of urls) {
430
+ const urlResult = this.validateURL(url);
431
+ if (!urlResult.allowed) {
432
+ threats.push(...urlResult.threats.map(t => ({
433
+ ...t,
434
+ tool: toolName,
435
+ type: 'suspicious_url_in_args'
436
+ })));
437
+ }
438
+ }
439
+
440
+ // Record in history for chain analysis
441
+ this.callHistory.push({
442
+ toolName,
443
+ args: argsStr.substring(0, 500),
444
+ timestamp: Date.now()
445
+ });
446
+
447
+ // Trim history to last 50 calls
448
+ if (this.callHistory.length > 50) {
449
+ this.callHistory = this.callHistory.slice(-50);
450
+ }
451
+
452
+ const allowed = threats.length === 0;
453
+ if (allowed) {
454
+ this.stats.passed++;
455
+ } else {
456
+ this.stats.blocked++;
457
+ }
458
+
459
+ return { allowed, threats };
460
+ }
461
+
462
+ /**
463
+ * Scan tool output for injection/exfiltration after execution.
464
+ *
465
+ * @param {string} toolName - Name of the tool that produced the response.
466
+ * @param {*} response - The tool's response data.
467
+ * @returns {{ safe: boolean, threats: Array<object>, sanitizedResponse: * }}
468
+ */
469
+ validateToolResponse(toolName, response) {
470
+ this.stats.totalValidated++;
471
+ this._trackTool(toolName, 'response');
472
+
473
+ if (!this.scanResponses) {
474
+ this.stats.passed++;
475
+ return { safe: true, threats: [], sanitizedResponse: response };
476
+ }
477
+
478
+ const result = this.responseScanner.scan(response);
479
+
480
+ // Tag threats with tool name
481
+ for (const threat of result.threats) {
482
+ threat.tool = toolName;
483
+ }
484
+
485
+ if (result.safe) {
486
+ this.stats.passed++;
487
+ } else {
488
+ this.stats.blocked++;
489
+ }
490
+
491
+ return result;
492
+ }
493
+
494
+ /**
495
+ * Check if a URL is in allowed/blocked lists and detect suspicious patterns.
496
+ *
497
+ * @param {string} url - URL to validate.
498
+ * @returns {{ allowed: boolean, threats: Array<object> }}
499
+ */
500
+ validateURL(url) {
501
+ const threats = [];
502
+
503
+ if (!url || typeof url !== 'string') {
504
+ return { allowed: false, threats: [{ type: 'invalid_url', severity: 'medium', description: 'URL is empty or not a string.' }] };
505
+ }
506
+
507
+ // Extract domain
508
+ const domainMatch = url.match(/^(?:https?:\/\/)?([^/:?#]+)/i);
509
+ const domain = domainMatch ? domainMatch[1].toLowerCase() : null;
510
+
511
+ if (!domain) {
512
+ threats.push({ type: 'malformed_url', severity: 'medium', description: 'Could not extract domain from URL.' });
513
+ return { allowed: false, threats };
514
+ }
515
+
516
+ // Check blocked domains
517
+ for (const blocked of this.blockedDomains) {
518
+ if (domain === blocked || domain.endsWith('.' + blocked)) {
519
+ threats.push({
520
+ type: 'blocked_domain',
521
+ severity: 'high',
522
+ description: `Domain "${domain}" is on the blocklist.`,
523
+ domain
524
+ });
525
+ return { allowed: false, threats };
526
+ }
527
+ }
528
+
529
+ // If allowlist has entries, domain must be in it
530
+ if (this.allowedDomains.domains.size > 0 && !this.allowedDomains.isAllowed(url)) {
531
+ threats.push({
532
+ type: 'domain_not_allowed',
533
+ severity: 'medium',
534
+ description: `Domain "${domain}" is not in the allowlist.`,
535
+ domain
536
+ });
537
+ return { allowed: false, threats };
538
+ }
539
+
540
+ // Check for suspicious URL patterns (IP addresses, known exfil services)
541
+ for (const pattern of EXFILTRATION_URL_PATTERNS) {
542
+ if (pattern.test(url)) {
543
+ threats.push({
544
+ type: 'suspicious_url',
545
+ severity: 'high',
546
+ description: 'URL matches a known exfiltration or suspicious pattern.',
547
+ url: url.substring(0, 200)
548
+ });
549
+ return { allowed: false, threats };
550
+ }
551
+ }
552
+
553
+ // Check for data-in-URL exfiltration (long query strings, base64 in path)
554
+ if (url.length > 500) {
555
+ threats.push({
556
+ type: 'data_in_url',
557
+ severity: 'medium',
558
+ description: 'URL is unusually long, possibly encoding exfiltrated data.'
559
+ });
560
+ }
561
+
562
+ const base64InPath = url.match(/\/[A-Za-z0-9+/=]{50,}/);
563
+ if (base64InPath) {
564
+ threats.push({
565
+ type: 'encoded_data_in_url',
566
+ severity: 'high',
567
+ description: 'URL path contains what appears to be base64-encoded data.'
568
+ });
569
+ }
570
+
571
+ return { allowed: threats.length === 0, threats };
572
+ }
573
+
574
+ /**
575
+ * Validate a sequence of tool calls for suspicious patterns.
576
+ * Detects multi-step attacks such as reading credentials then sending them.
577
+ *
578
+ * @param {Array<{ tool: string, args: string }>} steps - Sequence of tool calls.
579
+ * @returns {{ safe: boolean, threats: Array<object> }}
580
+ */
581
+ validateChain(steps) {
582
+ const threats = [];
583
+
584
+ if (!Array.isArray(steps) || steps.length < 2) {
585
+ return { safe: true, threats: [] };
586
+ }
587
+
588
+ for (const pattern of CHAIN_SUSPICIOUS_PATTERNS) {
589
+ const patternSteps = pattern.steps;
590
+
591
+ // Sliding window: look for the pattern steps in order within the chain
592
+ let patternIdx = 0;
593
+ for (let i = 0; i < steps.length && patternIdx < patternSteps.length; i++) {
594
+ const step = steps[i];
595
+ const expected = patternSteps[patternIdx];
596
+
597
+ const toolMatches = expected.tool.test(step.tool || '');
598
+ const argsMatch = !expected.args || expected.args.test(step.args || '');
599
+
600
+ if (toolMatches && argsMatch) {
601
+ patternIdx++;
602
+ }
603
+ }
604
+
605
+ if (patternIdx === patternSteps.length) {
606
+ threats.push({
607
+ type: 'suspicious_chain',
608
+ name: pattern.name,
609
+ severity: pattern.severity,
610
+ description: pattern.description,
611
+ stepsMatched: patternSteps.length,
612
+ totalSteps: steps.length
613
+ });
614
+ }
615
+ }
616
+
617
+ return {
618
+ safe: threats.length === 0,
619
+ threats
620
+ };
621
+ }
622
+
623
+ /**
624
+ * Return a report of validation statistics.
625
+ *
626
+ * @returns {{ totalValidated: number, blocked: number, passed: number, byTool: object }}
627
+ */
628
+ getReport() {
629
+ return {
630
+ totalValidated: this.stats.totalValidated,
631
+ blocked: this.stats.blocked,
632
+ passed: this.stats.passed,
633
+ byTool: { ...this.stats.byTool }
634
+ };
635
+ }
636
+
637
+ /**
638
+ * Track per-tool stats.
639
+ * @param {string} toolName
640
+ * @param {string} action
641
+ * @private
642
+ */
643
+ _trackTool(toolName, action) {
644
+ if (!this.stats.byTool[toolName]) {
645
+ this.stats.byTool[toolName] = { calls: 0, responses: 0, blocked: 0 };
646
+ }
647
+ if (action === 'call') {
648
+ this.stats.byTool[toolName].calls++;
649
+ } else if (action === 'response') {
650
+ this.stats.byTool[toolName].responses++;
651
+ }
652
+ }
653
+ }
654
+
655
+ // =========================================================================
656
+ // EXPORTS
657
+ // =========================================================================
658
+
659
+ module.exports = {
660
+ ToolChainValidator,
661
+ ResponseScanner,
662
+ DomainAllowlist,
663
+ RESPONSE_INJECTION_PATTERNS,
664
+ EXFILTRATION_URL_PATTERNS,
665
+ CREDENTIAL_PATTERNS,
666
+ CHAIN_SUSPICIOUS_PATTERNS
667
+ };