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.
- package/CHANGELOG.md +90 -1
- package/README.md +38 -5
- package/bin/agent-shield.js +19 -0
- package/package.json +8 -4
- package/src/attack-genome.js +536 -0
- package/src/attack-replay.js +246 -0
- package/src/audit.js +619 -0
- package/src/behavioral-dna.js +762 -0
- package/src/circuit-breaker.js +321 -321
- package/src/compliance-authority.js +803 -0
- package/src/detector-core.js +3 -3
- package/src/distributed.js +403 -359
- package/src/errors.js +9 -0
- package/src/evolution-simulator.js +650 -0
- package/src/flight-recorder.js +379 -0
- package/src/fuzzer.js +764 -764
- package/src/herd-immunity.js +521 -0
- package/src/index.js +28 -11
- package/src/intent-firewall.js +775 -0
- package/src/main.js +135 -2
- package/src/mcp-security-runtime.js +36 -10
- package/src/mcp-server.js +12 -8
- package/src/middleware.js +306 -208
- package/src/multi-agent.js +421 -404
- package/src/pii.js +404 -390
- package/src/real-attack-datasets.js +246 -0
- package/src/report-generator.js +640 -0
- package/src/soc-dashboard.js +394 -0
- package/src/stream-scanner.js +34 -4
- package/src/supply-chain.js +667 -0
- package/src/testing.js +505 -505
- package/src/threat-intel-federation.js +343 -0
- package/src/utils.js +199 -83
- package/types/index.d.ts +374 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield -- Cross-Agent Herd Immunity and Agent Immune Memory (v7.4)
|
|
5
|
+
*
|
|
6
|
+
* When one agent in the network encounters a new attack, the system extracts
|
|
7
|
+
* the pattern, generates variants, and pushes updated immunity to all other
|
|
8
|
+
* agents. New agents inherit the collective memory from day one.
|
|
9
|
+
*
|
|
10
|
+
* Uses MemoryAdapter from distributed.js for the broadcast mechanism.
|
|
11
|
+
* All processing runs locally -- no data ever leaves your environment.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const { PatternGenerator } = require('./self-healing');
|
|
16
|
+
const { MemoryAdapter } = require('./distributed');
|
|
17
|
+
|
|
18
|
+
const LOG_PREFIX = '[Agent Shield]';
|
|
19
|
+
const CHANNEL_IMMUNITY = 'herd:immunity';
|
|
20
|
+
const CHANNEL_CONNECT = 'herd:connect';
|
|
21
|
+
|
|
22
|
+
// =========================================================================
|
|
23
|
+
// HERD IMMUNITY
|
|
24
|
+
// =========================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Cross-agent herd immunity. When one agent blocks an attack, the extracted
|
|
28
|
+
* pattern is broadcast to every connected agent so they are all protected.
|
|
29
|
+
*/
|
|
30
|
+
class HerdImmunity {
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} [options]
|
|
33
|
+
* @param {object} [options.broadcastAdapter] - Adapter for pub/sub (defaults to MemoryAdapter).
|
|
34
|
+
* @param {boolean} [options.anonymize=true] - Strip raw text before broadcast.
|
|
35
|
+
* @param {number} [options.maxPatterns=500] - Maximum patterns to store.
|
|
36
|
+
*/
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this._adapter = options.broadcastAdapter || new MemoryAdapter();
|
|
39
|
+
this._anonymize = options.anonymize !== false;
|
|
40
|
+
this._maxPatterns = options.maxPatterns || 500;
|
|
41
|
+
|
|
42
|
+
this._generator = new PatternGenerator();
|
|
43
|
+
this._patterns = []; // locally held herd patterns
|
|
44
|
+
this._connectedAgents = new Set();
|
|
45
|
+
this._subscribed = false;
|
|
46
|
+
|
|
47
|
+
this.stats = {
|
|
48
|
+
patternsShared: 0,
|
|
49
|
+
attacksBlocked: 0
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
console.log('%s HerdImmunity initialized (anonymize: %s, maxPatterns: %d)',
|
|
53
|
+
LOG_PREFIX, this._anonymize, this._maxPatterns);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// -----------------------------------------------------------------------
|
|
57
|
+
// Agent management
|
|
58
|
+
// -----------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Register an agent in the herd network.
|
|
62
|
+
* @param {string} agentId
|
|
63
|
+
*/
|
|
64
|
+
connect(agentId) {
|
|
65
|
+
if (!agentId) return;
|
|
66
|
+
this._connectedAgents.add(agentId);
|
|
67
|
+
this._ensureSubscription();
|
|
68
|
+
this._adapter.publish(CHANNEL_CONNECT, { type: 'join', agentId, timestamp: Date.now() });
|
|
69
|
+
console.log('%s Agent "%s" connected to herd (%d total)',
|
|
70
|
+
LOG_PREFIX, agentId, this._connectedAgents.size);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Remove an agent from the herd network.
|
|
75
|
+
* @param {string} agentId
|
|
76
|
+
*/
|
|
77
|
+
disconnect(agentId) {
|
|
78
|
+
if (!agentId) return;
|
|
79
|
+
this._connectedAgents.delete(agentId);
|
|
80
|
+
this._adapter.publish(CHANNEL_CONNECT, { type: 'leave', agentId, timestamp: Date.now() });
|
|
81
|
+
console.log('%s Agent "%s" disconnected from herd (%d remaining)',
|
|
82
|
+
LOG_PREFIX, agentId, this._connectedAgents.size);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// -----------------------------------------------------------------------
|
|
86
|
+
// Core API
|
|
87
|
+
// -----------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Agent reports a blocked attack. The system extracts its signature,
|
|
91
|
+
* generates a detection pattern, and broadcasts to all connected agents.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} attack
|
|
94
|
+
* @param {string} attack.text - The blocked attack text.
|
|
95
|
+
* @param {string} [attack.category] - Threat category.
|
|
96
|
+
* @param {string} [attack.agentId] - Reporting agent.
|
|
97
|
+
* @returns {{ signature: string, pattern: object|null, broadcastedTo: number }}
|
|
98
|
+
*/
|
|
99
|
+
reportAttack(attack) {
|
|
100
|
+
if (!attack || !attack.text) {
|
|
101
|
+
return { signature: '', pattern: null, broadcastedTo: 0 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 1. Extract signature (hash of normalized text)
|
|
105
|
+
const normalized = attack.text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
106
|
+
const signature = crypto.createHash('sha256').update(normalized).digest('hex').substring(0, 16);
|
|
107
|
+
|
|
108
|
+
// Skip duplicates
|
|
109
|
+
if (this._patterns.some(p => p.signature === signature)) {
|
|
110
|
+
return { signature, pattern: null, broadcastedTo: 0 };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Generate a detection pattern
|
|
114
|
+
const generated = this._generator.generate(attack.text, {
|
|
115
|
+
category: attack.category
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!generated) {
|
|
119
|
+
return { signature, pattern: null, broadcastedTo: 0 };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Build the portable pattern payload
|
|
123
|
+
const pattern = {
|
|
124
|
+
signature,
|
|
125
|
+
regex: generated.regex.source,
|
|
126
|
+
flags: generated.regex.flags,
|
|
127
|
+
severity: generated.severity,
|
|
128
|
+
category: generated.category,
|
|
129
|
+
description: generated.description,
|
|
130
|
+
source: 'herd_immunity',
|
|
131
|
+
reportedBy: this._anonymize ? undefined : (attack.agentId || 'unknown'),
|
|
132
|
+
timestamp: Date.now()
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// 3. Store locally
|
|
136
|
+
this._addPattern(pattern);
|
|
137
|
+
|
|
138
|
+
// 4. Broadcast to all connected agents
|
|
139
|
+
const broadcastedTo = this._connectedAgents.size;
|
|
140
|
+
this._adapter.publish(CHANNEL_IMMUNITY, pattern);
|
|
141
|
+
this.stats.patternsShared++;
|
|
142
|
+
|
|
143
|
+
console.log('%s Attack reported -- signature %s, broadcast to %d agent(s)',
|
|
144
|
+
LOG_PREFIX, signature, broadcastedTo);
|
|
145
|
+
|
|
146
|
+
return { signature, pattern, broadcastedTo };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Receive a pattern from the herd network and add it to local detection.
|
|
151
|
+
* @param {object} pattern - Pattern object from the network.
|
|
152
|
+
*/
|
|
153
|
+
receiveImmunity(pattern) {
|
|
154
|
+
if (!pattern || !pattern.signature || !pattern.regex) return;
|
|
155
|
+
|
|
156
|
+
// Avoid duplicates
|
|
157
|
+
if (this._patterns.some(p => p.signature === pattern.signature)) return;
|
|
158
|
+
|
|
159
|
+
this._addPattern(pattern);
|
|
160
|
+
console.log('%s Immunity received -- signature %s, category %s',
|
|
161
|
+
LOG_PREFIX, pattern.signature, pattern.category || 'unknown');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check text against all learned herd patterns.
|
|
166
|
+
* @param {string} text
|
|
167
|
+
* @returns {{ immune: boolean, matches: Array<{ signature: string, category: string, severity: string }> }}
|
|
168
|
+
*/
|
|
169
|
+
checkImmunity(text) {
|
|
170
|
+
if (!text) return { immune: false, matches: [] };
|
|
171
|
+
|
|
172
|
+
const matches = [];
|
|
173
|
+
for (const pattern of this._patterns) {
|
|
174
|
+
try {
|
|
175
|
+
const re = new RegExp(pattern.regex, pattern.flags || 'i');
|
|
176
|
+
if (re.test(text)) {
|
|
177
|
+
matches.push({
|
|
178
|
+
signature: pattern.signature,
|
|
179
|
+
category: pattern.category,
|
|
180
|
+
severity: pattern.severity
|
|
181
|
+
});
|
|
182
|
+
this.stats.attacksBlocked++;
|
|
183
|
+
}
|
|
184
|
+
} catch (_e) {
|
|
185
|
+
// Skip broken regex patterns
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { immune: matches.length > 0, matches };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Return network statistics.
|
|
194
|
+
* @returns {{ patternsShared: number, agentsProtected: number, attacksBlocked: number }}
|
|
195
|
+
*/
|
|
196
|
+
getNetworkStats() {
|
|
197
|
+
return {
|
|
198
|
+
patternsShared: this.stats.patternsShared,
|
|
199
|
+
agentsProtected: this._connectedAgents.size,
|
|
200
|
+
attacksBlocked: this.stats.attacksBlocked
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// -----------------------------------------------------------------------
|
|
205
|
+
// Internal helpers
|
|
206
|
+
// -----------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
/** @private */
|
|
209
|
+
_addPattern(pattern) {
|
|
210
|
+
if (this._patterns.length >= this._maxPatterns) {
|
|
211
|
+
this._patterns.shift();
|
|
212
|
+
}
|
|
213
|
+
this._patterns.push(pattern);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** @private */
|
|
217
|
+
_ensureSubscription() {
|
|
218
|
+
if (this._subscribed) return;
|
|
219
|
+
this._subscribed = true;
|
|
220
|
+
|
|
221
|
+
this._adapter.subscribe(CHANNEL_IMMUNITY, (pattern) => {
|
|
222
|
+
this.receiveImmunity(pattern);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// =========================================================================
|
|
228
|
+
// IMMUNE MEMORY
|
|
229
|
+
// =========================================================================
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Persistent immune memory for agents. Stores attack/pattern pairs with
|
|
233
|
+
* time-based decay so stale patterns fade while active threats stay sharp.
|
|
234
|
+
* New agents can import the full collective memory to be protected from day one.
|
|
235
|
+
*/
|
|
236
|
+
class ImmuneMemory {
|
|
237
|
+
/**
|
|
238
|
+
* @param {object} [options]
|
|
239
|
+
* @param {number} [options.maxMemory=1000] - Maximum memory entries.
|
|
240
|
+
* @param {number} [options.decayRate=0.05] - Fraction of strength lost per decay cycle.
|
|
241
|
+
*/
|
|
242
|
+
constructor(options = {}) {
|
|
243
|
+
this._maxMemory = options.maxMemory || 1000;
|
|
244
|
+
this._decayRate = options.decayRate || 0.05;
|
|
245
|
+
this._entries = []; // { attack, pattern, strength, learnedAt, lastMatchedAt }
|
|
246
|
+
this._createdAt = Date.now();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Add an attack/pattern pair to memory.
|
|
251
|
+
* @param {string} attack - The attack text (or its hash).
|
|
252
|
+
* @param {object} pattern - The detection pattern.
|
|
253
|
+
* @returns {{ stored: boolean, totalPatterns: number }}
|
|
254
|
+
*/
|
|
255
|
+
learn(attack, pattern) {
|
|
256
|
+
if (!attack || !pattern) return { stored: false, totalPatterns: this._entries.length };
|
|
257
|
+
|
|
258
|
+
const hash = crypto.createHash('sha256')
|
|
259
|
+
.update(typeof attack === 'string' ? attack : JSON.stringify(attack))
|
|
260
|
+
.digest('hex').substring(0, 16);
|
|
261
|
+
|
|
262
|
+
// Reinforce if already known
|
|
263
|
+
const existing = this._entries.find(e => e.hash === hash);
|
|
264
|
+
if (existing) {
|
|
265
|
+
existing.strength = Math.min(1.0, existing.strength + 0.1);
|
|
266
|
+
existing.lastMatchedAt = Date.now();
|
|
267
|
+
return { stored: true, totalPatterns: this._entries.length };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Evict weakest if at capacity
|
|
271
|
+
if (this._entries.length >= this._maxMemory) {
|
|
272
|
+
let weakestIdx = 0;
|
|
273
|
+
let weakestStrength = Infinity;
|
|
274
|
+
for (let i = 0; i < this._entries.length; i++) {
|
|
275
|
+
if (this._entries[i].strength < weakestStrength) {
|
|
276
|
+
weakestStrength = this._entries[i].strength;
|
|
277
|
+
weakestIdx = i;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
this._entries.splice(weakestIdx, 1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this._entries.push({
|
|
284
|
+
hash,
|
|
285
|
+
attack: typeof attack === 'string' ? attack.substring(0, 200) : String(attack).substring(0, 200),
|
|
286
|
+
pattern: {
|
|
287
|
+
regex: pattern.regex instanceof RegExp ? pattern.regex.source : (pattern.regex || ''),
|
|
288
|
+
flags: pattern.regex instanceof RegExp ? pattern.regex.flags : (pattern.flags || 'i'),
|
|
289
|
+
severity: pattern.severity || 'medium',
|
|
290
|
+
category: pattern.category || 'unknown',
|
|
291
|
+
description: pattern.description || ''
|
|
292
|
+
},
|
|
293
|
+
strength: 1.0,
|
|
294
|
+
learnedAt: Date.now(),
|
|
295
|
+
lastMatchedAt: Date.now()
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return { stored: true, totalPatterns: this._entries.length };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check if text matches any remembered pattern.
|
|
303
|
+
* @param {string} text
|
|
304
|
+
* @returns {{ matched: boolean, matches: Array<{ hash: string, category: string, severity: string, strength: number }> }}
|
|
305
|
+
*/
|
|
306
|
+
recall(text) {
|
|
307
|
+
if (!text) return { matched: false, matches: [] };
|
|
308
|
+
|
|
309
|
+
const matches = [];
|
|
310
|
+
for (const entry of this._entries) {
|
|
311
|
+
if (entry.strength <= 0) continue;
|
|
312
|
+
try {
|
|
313
|
+
const re = new RegExp(entry.pattern.regex, entry.pattern.flags || 'i');
|
|
314
|
+
if (re.test(text)) {
|
|
315
|
+
entry.lastMatchedAt = Date.now();
|
|
316
|
+
entry.strength = Math.min(1.0, entry.strength + 0.02);
|
|
317
|
+
matches.push({
|
|
318
|
+
hash: entry.hash,
|
|
319
|
+
category: entry.pattern.category,
|
|
320
|
+
severity: entry.pattern.severity,
|
|
321
|
+
strength: entry.strength
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
} catch (_e) {
|
|
325
|
+
// Skip broken patterns
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { matched: matches.length > 0, matches };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Export memory as portable JSON for sharing with new agents.
|
|
334
|
+
* @returns {object}
|
|
335
|
+
*/
|
|
336
|
+
export() {
|
|
337
|
+
return {
|
|
338
|
+
version: '1.0',
|
|
339
|
+
exportedAt: Date.now(),
|
|
340
|
+
createdAt: this._createdAt,
|
|
341
|
+
decayRate: this._decayRate,
|
|
342
|
+
entries: this._entries.filter(e => e.strength > 0).map(e => ({
|
|
343
|
+
hash: e.hash,
|
|
344
|
+
pattern: { ...e.pattern },
|
|
345
|
+
strength: e.strength,
|
|
346
|
+
learnedAt: e.learnedAt,
|
|
347
|
+
lastMatchedAt: e.lastMatchedAt
|
|
348
|
+
}))
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Import memory from another agent or a collective export.
|
|
354
|
+
* New agents call this to inherit the herd's collective memory.
|
|
355
|
+
* @param {object} data - Output of export().
|
|
356
|
+
* @returns {{ imported: number, skipped: number }}
|
|
357
|
+
*/
|
|
358
|
+
import(data) {
|
|
359
|
+
if (!data || !Array.isArray(data.entries)) {
|
|
360
|
+
return { imported: 0, skipped: 0 };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let imported = 0;
|
|
364
|
+
let skipped = 0;
|
|
365
|
+
|
|
366
|
+
for (const entry of data.entries) {
|
|
367
|
+
if (!entry.hash || !entry.pattern || !entry.pattern.regex) {
|
|
368
|
+
skipped++;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Skip if already known
|
|
373
|
+
if (this._entries.some(e => e.hash === entry.hash)) {
|
|
374
|
+
skipped++;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (this._entries.length >= this._maxMemory) {
|
|
379
|
+
skipped++;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this._entries.push({
|
|
384
|
+
hash: entry.hash,
|
|
385
|
+
attack: '',
|
|
386
|
+
pattern: {
|
|
387
|
+
regex: entry.pattern.regex,
|
|
388
|
+
flags: entry.pattern.flags || 'i',
|
|
389
|
+
severity: entry.pattern.severity || 'medium',
|
|
390
|
+
category: entry.pattern.category || 'unknown',
|
|
391
|
+
description: entry.pattern.description || ''
|
|
392
|
+
},
|
|
393
|
+
strength: Math.min(1.0, entry.strength || 0.5),
|
|
394
|
+
learnedAt: entry.learnedAt || Date.now(),
|
|
395
|
+
lastMatchedAt: entry.lastMatchedAt || Date.now()
|
|
396
|
+
});
|
|
397
|
+
imported++;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
console.log('%s ImmuneMemory imported %d pattern(s), skipped %d',
|
|
401
|
+
LOG_PREFIX, imported, skipped);
|
|
402
|
+
|
|
403
|
+
return { imported, skipped };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Apply time-based decay to reduce stale patterns. Patterns that have not
|
|
408
|
+
* been matched recently lose strength. When strength reaches zero the
|
|
409
|
+
* pattern is effectively dormant but still retained for reference.
|
|
410
|
+
* @returns {{ decayed: number, removed: number }}
|
|
411
|
+
*/
|
|
412
|
+
decay() {
|
|
413
|
+
let decayed = 0;
|
|
414
|
+
let removed = 0;
|
|
415
|
+
|
|
416
|
+
for (const entry of this._entries) {
|
|
417
|
+
if (entry.strength <= 0) continue;
|
|
418
|
+
|
|
419
|
+
const age = Date.now() - entry.lastMatchedAt;
|
|
420
|
+
// Decay faster for patterns that have not matched in a long time
|
|
421
|
+
const ageFactor = Math.min(3.0, 1.0 + (age / 3600000)); // up to 3x after 2+ hours
|
|
422
|
+
const loss = this._decayRate * ageFactor;
|
|
423
|
+
|
|
424
|
+
entry.strength = Math.max(0, entry.strength - loss);
|
|
425
|
+
if (entry.strength <= 0) {
|
|
426
|
+
removed++;
|
|
427
|
+
}
|
|
428
|
+
decayed++;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return { decayed, removed };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Return memory statistics.
|
|
436
|
+
* @returns {{ totalPatterns: number, activePatterns: number, decayedPatterns: number, memoryAge: number }}
|
|
437
|
+
*/
|
|
438
|
+
getMemoryStats() {
|
|
439
|
+
const active = this._entries.filter(e => e.strength > 0).length;
|
|
440
|
+
return {
|
|
441
|
+
totalPatterns: this._entries.length,
|
|
442
|
+
activePatterns: active,
|
|
443
|
+
decayedPatterns: this._entries.length - active,
|
|
444
|
+
memoryAge: Date.now() - this._createdAt
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// =========================================================================
|
|
450
|
+
// HERD NETWORK HELPER
|
|
451
|
+
// =========================================================================
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Connect multiple AgentShield instances into a herd immunity network.
|
|
455
|
+
* When any agent detects an attack, all others receive the pattern within
|
|
456
|
+
* the same tick (synchronous via shared MemoryAdapter).
|
|
457
|
+
*
|
|
458
|
+
* @param {Array<{ id: string, shield?: object }>} agents - Array of agent descriptors.
|
|
459
|
+
* Each must have an `id` property. Optionally include a `shield` (AgentShield instance).
|
|
460
|
+
* @param {object} [options] - Options forwarded to HerdImmunity constructor.
|
|
461
|
+
* @returns {{ herd: HerdImmunity, memory: ImmuneMemory, reportAttack: Function, checkAll: Function }}
|
|
462
|
+
*/
|
|
463
|
+
function createHerdNetwork(agents, options = {}) {
|
|
464
|
+
if (!Array.isArray(agents) || agents.length === 0) {
|
|
465
|
+
throw new Error(LOG_PREFIX + ' createHerdNetwork requires a non-empty array of agents');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const adapter = options.broadcastAdapter || new MemoryAdapter();
|
|
469
|
+
const herd = new HerdImmunity({ ...options, broadcastAdapter: adapter });
|
|
470
|
+
const memory = new ImmuneMemory({
|
|
471
|
+
maxMemory: options.maxMemory,
|
|
472
|
+
decayRate: options.decayRate
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Connect all agents
|
|
476
|
+
for (const agent of agents) {
|
|
477
|
+
const agentId = agent.id || agent.agentId;
|
|
478
|
+
if (agentId) {
|
|
479
|
+
herd.connect(agentId);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Report an attack on behalf of any agent in the network.
|
|
485
|
+
* The pattern is immediately available to all other agents.
|
|
486
|
+
* @param {object} attack - { text, category, agentId }
|
|
487
|
+
* @returns {object}
|
|
488
|
+
*/
|
|
489
|
+
function reportAttack(attack) {
|
|
490
|
+
const result = herd.reportAttack(attack);
|
|
491
|
+
if (result.pattern) {
|
|
492
|
+
memory.learn(attack.text, result.pattern);
|
|
493
|
+
}
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Check text against the herd's collective immunity.
|
|
499
|
+
* @param {string} text
|
|
500
|
+
* @returns {object}
|
|
501
|
+
*/
|
|
502
|
+
function checkAll(text) {
|
|
503
|
+
const herdResult = herd.checkImmunity(text);
|
|
504
|
+
const memoryResult = memory.recall(text);
|
|
505
|
+
return {
|
|
506
|
+
immune: herdResult.immune || memoryResult.matched,
|
|
507
|
+
herdMatches: herdResult.matches,
|
|
508
|
+
memoryMatches: memoryResult.matches
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
console.log('%s Herd network created with %d agent(s)', LOG_PREFIX, agents.length);
|
|
513
|
+
|
|
514
|
+
return { herd, memory, reportAttack, checkAll };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// =========================================================================
|
|
518
|
+
// EXPORTS
|
|
519
|
+
// =========================================================================
|
|
520
|
+
|
|
521
|
+
module.exports = { HerdImmunity, ImmuneMemory, createHerdNetwork };
|
package/src/index.js
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
const { scanText, getPatterns, SEVERITY_ORDER } = require('./detector-core');
|
|
36
|
+
const { createShieldError } = require('./errors');
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Default configuration for AgentShield.
|
|
@@ -53,6 +54,15 @@ const DEFAULT_CONFIG = {
|
|
|
53
54
|
/** Custom callback when a threat is detected. */
|
|
54
55
|
onThreat: null,
|
|
55
56
|
|
|
57
|
+
/** Maximum input size in bytes before truncation warning. */
|
|
58
|
+
maxInputSize: 1_000_000,
|
|
59
|
+
|
|
60
|
+
/** Maximum number of scan history entries to retain. */
|
|
61
|
+
maxScanHistory: 100,
|
|
62
|
+
|
|
63
|
+
/** Maximum recursion depth when flattening tool arguments. */
|
|
64
|
+
maxArgDepth: 10,
|
|
65
|
+
|
|
56
66
|
/** Dangerous tool names that should be scrutinized more carefully. */
|
|
57
67
|
dangerousTools: [
|
|
58
68
|
'bash', 'shell', 'terminal', 'exec', 'execute',
|
|
@@ -111,10 +121,10 @@ class AgentShield {
|
|
|
111
121
|
*/
|
|
112
122
|
scan(text, options = {}) {
|
|
113
123
|
if (typeof text !== 'string') {
|
|
114
|
-
throw
|
|
124
|
+
throw createShieldError('AS-DET-002', { method: 'scan', received: typeof text });
|
|
115
125
|
}
|
|
116
|
-
if (text.length >
|
|
117
|
-
console.warn('[Agent Shield] Input exceeds
|
|
126
|
+
if (text.length > this.config.maxInputSize) {
|
|
127
|
+
console.warn('[Agent Shield] Input exceeds configured maxInputSize - consider scanning in chunks');
|
|
118
128
|
}
|
|
119
129
|
const result = scanText(text, {
|
|
120
130
|
source: options.source || 'unknown',
|
|
@@ -133,7 +143,7 @@ class AgentShield {
|
|
|
133
143
|
threatCount: result.threats.length,
|
|
134
144
|
source: options.source || 'unknown'
|
|
135
145
|
});
|
|
136
|
-
if (this.stats.scanHistory.length >
|
|
146
|
+
if (this.stats.scanHistory.length > this.config.maxScanHistory) {
|
|
137
147
|
this.stats.scanHistory.shift();
|
|
138
148
|
}
|
|
139
149
|
|
|
@@ -197,10 +207,11 @@ class AgentShield {
|
|
|
197
207
|
* @param {object} [options] - Options.
|
|
198
208
|
* @param {string} [options.source='user_input'] - Where the input came from.
|
|
199
209
|
* @returns {object} Scan result with additional `blocked` field.
|
|
210
|
+
* @throws {TypeError} If text is not a string.
|
|
200
211
|
*/
|
|
201
212
|
scanInput(text, options = {}) {
|
|
202
213
|
if (typeof text !== 'string') {
|
|
203
|
-
throw
|
|
214
|
+
throw createShieldError('AS-DET-002', { method: 'scanInput', received: typeof text });
|
|
204
215
|
}
|
|
205
216
|
return this._scanWithBlocking(text, 'user_input', 'INPUT', options);
|
|
206
217
|
}
|
|
@@ -214,10 +225,11 @@ class AgentShield {
|
|
|
214
225
|
* @param {object} [options] - Options.
|
|
215
226
|
* @param {string} [options.source='agent_output'] - Source label.
|
|
216
227
|
* @returns {object} Scan result with additional `blocked` field.
|
|
228
|
+
* @throws {TypeError} If text is not a string.
|
|
217
229
|
*/
|
|
218
230
|
scanOutput(text, options = {}) {
|
|
219
231
|
if (typeof text !== 'string') {
|
|
220
|
-
throw
|
|
232
|
+
throw createShieldError('AS-DET-002', { method: 'scanOutput', received: typeof text });
|
|
221
233
|
}
|
|
222
234
|
return this._scanWithBlocking(text, 'agent_output', 'OUTPUT', options);
|
|
223
235
|
}
|
|
@@ -232,8 +244,11 @@ class AgentShield {
|
|
|
232
244
|
* @returns {object} Scan result with `blocked` and `warnings` fields.
|
|
233
245
|
*/
|
|
234
246
|
scanToolCall(toolName, args = {}, options = {}) {
|
|
235
|
-
if (
|
|
236
|
-
|
|
247
|
+
if (typeof toolName !== 'string') {
|
|
248
|
+
throw createShieldError('AS-DET-006', { method: 'scanToolCall', received: typeof toolName });
|
|
249
|
+
}
|
|
250
|
+
if (!toolName) {
|
|
251
|
+
return { status: 'safe', toolName: '', threats: [], warnings: ['Empty tool name'], blocked: false, isDangerousTool: false, timestamp: Date.now() };
|
|
237
252
|
}
|
|
238
253
|
const warnings = [];
|
|
239
254
|
const allThreats = [];
|
|
@@ -315,7 +330,7 @@ class AgentShield {
|
|
|
315
330
|
*/
|
|
316
331
|
scanBatch(items) {
|
|
317
332
|
if (!Array.isArray(items)) {
|
|
318
|
-
throw
|
|
333
|
+
throw createShieldError('AS-DET-007', { method: 'scanBatch', received: typeof items });
|
|
319
334
|
}
|
|
320
335
|
const results = items.map(item =>
|
|
321
336
|
this.scan(item.text, { source: item.source || 'batch' })
|
|
@@ -371,7 +386,8 @@ class AgentShield {
|
|
|
371
386
|
* @param {object} args
|
|
372
387
|
* @returns {string}
|
|
373
388
|
*/
|
|
374
|
-
_flattenArgs(args, maxDepth
|
|
389
|
+
_flattenArgs(args, maxDepth) {
|
|
390
|
+
if (maxDepth == null) maxDepth = this.config.maxArgDepth;
|
|
375
391
|
const parts = [];
|
|
376
392
|
const flatten = (obj, depth) => {
|
|
377
393
|
if (depth > maxDepth) return;
|
|
@@ -393,7 +409,8 @@ class AgentShield {
|
|
|
393
409
|
* @param {object} args
|
|
394
410
|
* @returns {Array<string>}
|
|
395
411
|
*/
|
|
396
|
-
_extractFilePaths(args, maxDepth
|
|
412
|
+
_extractFilePaths(args, maxDepth) {
|
|
413
|
+
if (maxDepth == null) maxDepth = this.config.maxArgDepth;
|
|
397
414
|
const paths = [];
|
|
398
415
|
const fileKeys = [
|
|
399
416
|
'file', 'path', 'file_path', 'filepath', 'filename', 'target',
|