agentshield-sdk 7.2.1 → 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 +33 -1
- package/bin/agent-shield.js +19 -0
- package/package.json +5 -2
- 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/compliance-authority.js +803 -0
- package/src/distributed.js +2 -1
- package/src/errors.js +9 -0
- package/src/evolution-simulator.js +650 -0
- package/src/flight-recorder.js +379 -0
- package/src/herd-immunity.js +521 -0
- package/src/index.js +6 -5
- package/src/intent-firewall.js +775 -0
- package/src/main.js +129 -0
- package/src/mcp-security-runtime.js +6 -5
- package/src/middleware.js +6 -3
- package/src/pii.js +4 -1
- package/src/real-attack-datasets.js +246 -0
- package/src/report-generator.js +640 -0
- package/src/soc-dashboard.js +394 -0
- package/src/supply-chain.js +667 -0
- package/src/threat-intel-federation.js +343 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield - Agent Flight Recorder
|
|
5
|
+
*
|
|
6
|
+
* Records every interaction an agent has, creating a forensic timeline.
|
|
7
|
+
* When an agent gets compromised, the Flight Recorder provides:
|
|
8
|
+
* - The exact conversation that led to compromise
|
|
9
|
+
* - The exact moment the attack succeeded
|
|
10
|
+
* - What defense should have caught it
|
|
11
|
+
* - Auto-generated pattern to prevent recurrence
|
|
12
|
+
*
|
|
13
|
+
* Like a black box from aviation, applied to AI agents.
|
|
14
|
+
*
|
|
15
|
+
* @module flight-recorder
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
const { scanText } = require('./detector-core');
|
|
20
|
+
|
|
21
|
+
// =========================================================================
|
|
22
|
+
// FlightRecorder - Core recording engine
|
|
23
|
+
// =========================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Records agent interactions and provides forensic analysis.
|
|
27
|
+
*/
|
|
28
|
+
class FlightRecorder {
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} [options]
|
|
31
|
+
* @param {string} [options.agentId] - Agent identifier.
|
|
32
|
+
* @param {number} [options.maxEntries=10000] - Max entries before rotation.
|
|
33
|
+
* @param {boolean} [options.scanOnRecord=true] - Scan each entry as it's recorded.
|
|
34
|
+
* @param {string} [options.sensitivity='high'] - Detection sensitivity.
|
|
35
|
+
*/
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
this.agentId = options.agentId || `agent_${crypto.randomBytes(4).toString('hex')}`;
|
|
38
|
+
this.maxEntries = options.maxEntries || 10000;
|
|
39
|
+
this.scanOnRecord = options.scanOnRecord !== false;
|
|
40
|
+
this.sensitivity = options.sensitivity || 'high';
|
|
41
|
+
|
|
42
|
+
/** @type {Array} The flight log - ordered list of all interactions. */
|
|
43
|
+
this._log = [];
|
|
44
|
+
|
|
45
|
+
/** @type {Array} Detected incidents - moments where threats were found. */
|
|
46
|
+
this._incidents = [];
|
|
47
|
+
|
|
48
|
+
/** @type {number} Sequence counter. */
|
|
49
|
+
this._seq = 0;
|
|
50
|
+
|
|
51
|
+
/** @type {string|null} Current session ID. */
|
|
52
|
+
this._sessionId = null;
|
|
53
|
+
|
|
54
|
+
this._startSession();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start a new recording session.
|
|
59
|
+
* @returns {string} Session ID.
|
|
60
|
+
*/
|
|
61
|
+
_startSession() {
|
|
62
|
+
this._sessionId = `session_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
63
|
+
return this._sessionId;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Record an interaction entry.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} entry
|
|
70
|
+
* @param {string} entry.role - 'user', 'assistant', 'system', 'tool'
|
|
71
|
+
* @param {string} entry.content - The message content.
|
|
72
|
+
* @param {string} [entry.toolName] - Tool name if role is 'tool'.
|
|
73
|
+
* @param {object} [entry.metadata] - Additional metadata.
|
|
74
|
+
* @returns {object} The recorded entry with scan results.
|
|
75
|
+
*/
|
|
76
|
+
record(entry) {
|
|
77
|
+
const record = {
|
|
78
|
+
seq: this._seq++,
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
sessionId: this._sessionId,
|
|
81
|
+
agentId: this.agentId,
|
|
82
|
+
role: entry.role || 'unknown',
|
|
83
|
+
content: entry.content || '',
|
|
84
|
+
toolName: entry.toolName || null,
|
|
85
|
+
metadata: entry.metadata || {},
|
|
86
|
+
threats: [],
|
|
87
|
+
compromised: false,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Scan the content if enabled
|
|
91
|
+
if (this.scanOnRecord && record.content) {
|
|
92
|
+
const scanResult = scanText(record.content, {
|
|
93
|
+
sensitivity: this.sensitivity,
|
|
94
|
+
source: `flight_recorder:${record.role}`
|
|
95
|
+
});
|
|
96
|
+
record.threats = scanResult.threats;
|
|
97
|
+
record.status = scanResult.status;
|
|
98
|
+
|
|
99
|
+
if (scanResult.threats.length > 0) {
|
|
100
|
+
record.compromised = true;
|
|
101
|
+
this._incidents.push({
|
|
102
|
+
seq: record.seq,
|
|
103
|
+
timestamp: record.timestamp,
|
|
104
|
+
role: record.role,
|
|
105
|
+
threatCount: scanResult.threats.length,
|
|
106
|
+
maxSeverity: scanResult.threats[0].severity,
|
|
107
|
+
categories: [...new Set(scanResult.threats.map(t => t.category))],
|
|
108
|
+
preview: record.content.substring(0, 100),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this._log.push(record);
|
|
114
|
+
|
|
115
|
+
// Rotate if over limit
|
|
116
|
+
if (this._log.length > this.maxEntries) {
|
|
117
|
+
this._log = this._log.slice(-Math.floor(this.maxEntries * 0.75));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return record;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Record a user message.
|
|
125
|
+
* @param {string} content
|
|
126
|
+
* @param {object} [metadata]
|
|
127
|
+
* @returns {object}
|
|
128
|
+
*/
|
|
129
|
+
recordUser(content, metadata) {
|
|
130
|
+
return this.record({ role: 'user', content, metadata });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Record an assistant response.
|
|
135
|
+
* @param {string} content
|
|
136
|
+
* @param {object} [metadata]
|
|
137
|
+
* @returns {object}
|
|
138
|
+
*/
|
|
139
|
+
recordAssistant(content, metadata) {
|
|
140
|
+
return this.record({ role: 'assistant', content, metadata });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Record a tool call.
|
|
145
|
+
* @param {string} toolName
|
|
146
|
+
* @param {string} content - Tool arguments or result as string.
|
|
147
|
+
* @param {object} [metadata]
|
|
148
|
+
* @returns {object}
|
|
149
|
+
*/
|
|
150
|
+
recordTool(toolName, content, metadata) {
|
|
151
|
+
return this.record({ role: 'tool', content, toolName, metadata });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// =======================================================================
|
|
155
|
+
// Forensic Analysis
|
|
156
|
+
// =======================================================================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get the full flight log.
|
|
160
|
+
* @param {number} [limit] - Return only the last N entries.
|
|
161
|
+
* @returns {Array}
|
|
162
|
+
*/
|
|
163
|
+
getLog(limit) {
|
|
164
|
+
if (limit) return this._log.slice(-limit);
|
|
165
|
+
return [...this._log];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get all recorded incidents (moments where threats were detected).
|
|
170
|
+
* @returns {Array}
|
|
171
|
+
*/
|
|
172
|
+
getIncidents() {
|
|
173
|
+
return [...this._incidents];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Analyze a specific incident - reconstruct what happened.
|
|
178
|
+
*
|
|
179
|
+
* @param {number} incidentSeq - The sequence number of the incident.
|
|
180
|
+
* @param {number} [contextBefore=5] - Number of entries before the incident to include.
|
|
181
|
+
* @param {number} [contextAfter=3] - Number of entries after.
|
|
182
|
+
* @returns {object} Forensic analysis.
|
|
183
|
+
*/
|
|
184
|
+
analyze(incidentSeq, contextBefore = 5, contextAfter = 3) {
|
|
185
|
+
const incidentIdx = this._log.findIndex(e => e.seq === incidentSeq);
|
|
186
|
+
if (incidentIdx === -1) return null;
|
|
187
|
+
|
|
188
|
+
const incident = this._log[incidentIdx];
|
|
189
|
+
const startIdx = Math.max(0, incidentIdx - contextBefore);
|
|
190
|
+
const endIdx = Math.min(this._log.length, incidentIdx + contextAfter + 1);
|
|
191
|
+
const timeline = this._log.slice(startIdx, endIdx);
|
|
192
|
+
|
|
193
|
+
// Find the escalation path - when did things start going wrong?
|
|
194
|
+
let firstSuspiciousIdx = incidentIdx;
|
|
195
|
+
for (let i = incidentIdx - 1; i >= startIdx; i--) {
|
|
196
|
+
if (this._log[i].threats && this._log[i].threats.length > 0) {
|
|
197
|
+
firstSuspiciousIdx = i;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const escalationPath = this._log.slice(firstSuspiciousIdx, incidentIdx + 1);
|
|
202
|
+
|
|
203
|
+
// Determine what defense should have caught it
|
|
204
|
+
const missedBy = [];
|
|
205
|
+
if (incident.threats.length > 0) {
|
|
206
|
+
for (const threat of incident.threats) {
|
|
207
|
+
if (threat.severity === 'critical' || threat.severity === 'high') {
|
|
208
|
+
missedBy.push({
|
|
209
|
+
category: threat.category,
|
|
210
|
+
severity: threat.severity,
|
|
211
|
+
recommendation: this._getDefenseRecommendation(threat.category),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
incident: {
|
|
219
|
+
seq: incident.seq,
|
|
220
|
+
timestamp: incident.timestamp,
|
|
221
|
+
role: incident.role,
|
|
222
|
+
content: incident.content,
|
|
223
|
+
threats: incident.threats,
|
|
224
|
+
},
|
|
225
|
+
timeline: timeline.map(e => ({
|
|
226
|
+
seq: e.seq,
|
|
227
|
+
timestamp: e.timestamp,
|
|
228
|
+
role: e.role,
|
|
229
|
+
preview: e.content.substring(0, 120),
|
|
230
|
+
threatCount: e.threats ? e.threats.length : 0,
|
|
231
|
+
isIncident: e.seq === incidentSeq,
|
|
232
|
+
})),
|
|
233
|
+
escalationPath: escalationPath.map(e => ({
|
|
234
|
+
seq: e.seq,
|
|
235
|
+
role: e.role,
|
|
236
|
+
preview: e.content.substring(0, 120),
|
|
237
|
+
threats: e.threats,
|
|
238
|
+
})),
|
|
239
|
+
missedBy,
|
|
240
|
+
rootCause: this._determineRootCause(incident, escalationPath),
|
|
241
|
+
autoFix: this._generateAutoFix(incident),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Generate a full forensic report for all incidents.
|
|
247
|
+
* @returns {object}
|
|
248
|
+
*/
|
|
249
|
+
getForensicReport() {
|
|
250
|
+
const analyses = this._incidents.map(i => this.analyze(i.seq));
|
|
251
|
+
|
|
252
|
+
// Attack technique distribution
|
|
253
|
+
const techniques = {};
|
|
254
|
+
for (const incident of this._incidents) {
|
|
255
|
+
for (const cat of incident.categories) {
|
|
256
|
+
techniques[cat] = (techniques[cat] || 0) + 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
agentId: this.agentId,
|
|
262
|
+
sessionId: this._sessionId,
|
|
263
|
+
totalEntries: this._log.length,
|
|
264
|
+
totalIncidents: this._incidents.length,
|
|
265
|
+
compromiseRate: this._log.length > 0
|
|
266
|
+
? ((this._incidents.length / this._log.length) * 100).toFixed(1) + '%'
|
|
267
|
+
: '0%',
|
|
268
|
+
techniques,
|
|
269
|
+
incidents: analyses.filter(Boolean),
|
|
270
|
+
timeline: this._log.map(e => ({
|
|
271
|
+
seq: e.seq,
|
|
272
|
+
timestamp: e.timestamp,
|
|
273
|
+
role: e.role,
|
|
274
|
+
compromised: e.compromised,
|
|
275
|
+
})),
|
|
276
|
+
generatedAt: Date.now(),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Export the flight log as JSON for archival.
|
|
282
|
+
* @returns {string}
|
|
283
|
+
*/
|
|
284
|
+
exportJSON() {
|
|
285
|
+
return JSON.stringify({
|
|
286
|
+
agentId: this.agentId,
|
|
287
|
+
sessionId: this._sessionId,
|
|
288
|
+
exportedAt: Date.now(),
|
|
289
|
+
entries: this._log,
|
|
290
|
+
incidents: this._incidents,
|
|
291
|
+
}, null, 2);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Import a previously exported flight log.
|
|
296
|
+
* @param {string} json
|
|
297
|
+
*/
|
|
298
|
+
importJSON(json) {
|
|
299
|
+
const data = JSON.parse(json);
|
|
300
|
+
this._log = data.entries || [];
|
|
301
|
+
this._incidents = data.incidents || [];
|
|
302
|
+
this._seq = this._log.length > 0 ? this._log[this._log.length - 1].seq + 1 : 0;
|
|
303
|
+
if (data.sessionId) this._sessionId = data.sessionId;
|
|
304
|
+
if (data.agentId) this.agentId = data.agentId;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Clear all recorded data.
|
|
309
|
+
*/
|
|
310
|
+
clear() {
|
|
311
|
+
this._log = [];
|
|
312
|
+
this._incidents = [];
|
|
313
|
+
this._seq = 0;
|
|
314
|
+
this._startSession();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// =======================================================================
|
|
318
|
+
// Internal helpers
|
|
319
|
+
// =======================================================================
|
|
320
|
+
|
|
321
|
+
/** @private */
|
|
322
|
+
_determineRootCause(incident, escalationPath) {
|
|
323
|
+
if (escalationPath.length <= 1) {
|
|
324
|
+
return `Direct ${incident.threats[0]?.category || 'unknown'} attack in a single message.`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const roles = escalationPath.map(e => e.role);
|
|
328
|
+
if (roles.filter(r => r === 'user').length >= 3) {
|
|
329
|
+
return 'Multi-turn escalation attack. The attacker built context across multiple messages before the payload.';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (escalationPath.some(e => e.role === 'tool')) {
|
|
333
|
+
return 'Tool-chain compromise. The attack flowed through a tool call, possibly poisoned tool output.';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return `Escalation from ${escalationPath[0].role} input leading to compromise at message ${incident.seq}.`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** @private */
|
|
340
|
+
_generateAutoFix(incident) {
|
|
341
|
+
if (!incident.threats || incident.threats.length === 0) return null;
|
|
342
|
+
|
|
343
|
+
const threat = incident.threats[0];
|
|
344
|
+
const words = incident.content.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
345
|
+
const keyTerms = words.slice(0, 5).join('|');
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
description: `Auto-generated pattern to catch "${threat.category}" attacks similar to this incident.`,
|
|
349
|
+
pattern: `(?:${keyTerms})`,
|
|
350
|
+
severity: threat.severity,
|
|
351
|
+
category: threat.category,
|
|
352
|
+
source: 'flight_recorder_autofix',
|
|
353
|
+
generatedFrom: incident.seq,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** @private */
|
|
358
|
+
_getDefenseRecommendation(category) {
|
|
359
|
+
const recs = {
|
|
360
|
+
instruction_override: 'Enable InstructionHierarchy to enforce system prompt priority.',
|
|
361
|
+
role_hijack: 'Use PermissionBoundary to restrict role changes. Add role anchoring to system prompt.',
|
|
362
|
+
prompt_injection: 'Scan for ChatML/LLaMA delimiters. Block fake system directives.',
|
|
363
|
+
data_exfiltration: 'Use CanaryTokens to detect prompt leaks. Block external URL generation.',
|
|
364
|
+
social_engineering: 'Never bypass safety for urgency claims. Validate authority out-of-band.',
|
|
365
|
+
tool_abuse: 'Use ToolSequenceAnalyzer. Enforce PermissionBoundary on all tool calls.',
|
|
366
|
+
malicious_plugin: 'Only load verified plugins. Scan manifests with ToolSchemaValidator.',
|
|
367
|
+
ai_phishing: 'Never ask for credentials through the agent. Train users on AI phishing.',
|
|
368
|
+
};
|
|
369
|
+
return recs[category] || 'Review and strengthen detection for this attack category.';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// =========================================================================
|
|
374
|
+
// EXPORTS
|
|
375
|
+
// =========================================================================
|
|
376
|
+
|
|
377
|
+
module.exports = {
|
|
378
|
+
FlightRecorder,
|
|
379
|
+
};
|