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,394 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield - Enterprise SOC Dashboard Backend
|
|
5
|
+
*
|
|
6
|
+
* Real-time attack visibility across all agents in an organization.
|
|
7
|
+
* Aggregates threat data, provides drill-down by agent/category/time,
|
|
8
|
+
* and supports alert routing to PagerDuty, Slack, and Microsoft Teams.
|
|
9
|
+
*
|
|
10
|
+
* @module soc-dashboard
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { EventEmitter } = require('events');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
// =========================================================================
|
|
17
|
+
// SOCDashboard - Aggregation and alerting engine
|
|
18
|
+
// =========================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Enterprise SOC dashboard backend.
|
|
22
|
+
* Aggregates threat events from multiple agents and provides
|
|
23
|
+
* real-time alerting and drill-down capabilities.
|
|
24
|
+
*/
|
|
25
|
+
class SOCDashboard extends EventEmitter {
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} [options]
|
|
28
|
+
* @param {number} [options.maxEvents=50000] - Max events retained.
|
|
29
|
+
* @param {number} [options.alertCooldownMs=300000] - Min time between duplicate alerts (5 min).
|
|
30
|
+
* @param {Array} [options.alertChannels] - Array of AlertChannel instances.
|
|
31
|
+
*/
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
super();
|
|
34
|
+
this.maxEvents = options.maxEvents || 50000;
|
|
35
|
+
this.alertCooldownMs = options.alertCooldownMs || 300000;
|
|
36
|
+
this._events = [];
|
|
37
|
+
this._agents = new Map(); // agentId -> agent metadata
|
|
38
|
+
this._alertChannels = options.alertChannels || [];
|
|
39
|
+
this._lastAlerts = new Map(); // alertKey -> timestamp
|
|
40
|
+
this._stats = {
|
|
41
|
+
totalEvents: 0,
|
|
42
|
+
totalThreats: 0,
|
|
43
|
+
totalBlocked: 0,
|
|
44
|
+
agentCount: 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register an agent with the dashboard.
|
|
50
|
+
* @param {string} agentId
|
|
51
|
+
* @param {object} [metadata] - Agent metadata (name, environment, team, etc.)
|
|
52
|
+
*/
|
|
53
|
+
registerAgent(agentId, metadata = {}) {
|
|
54
|
+
this._agents.set(agentId, {
|
|
55
|
+
id: agentId,
|
|
56
|
+
name: metadata.name || agentId,
|
|
57
|
+
environment: metadata.environment || 'production',
|
|
58
|
+
team: metadata.team || 'default',
|
|
59
|
+
registeredAt: Date.now(),
|
|
60
|
+
lastActivity: Date.now(),
|
|
61
|
+
eventCount: 0,
|
|
62
|
+
threatCount: 0,
|
|
63
|
+
});
|
|
64
|
+
this._stats.agentCount = this._agents.size;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Ingest a security event from an agent.
|
|
69
|
+
* @param {object} event
|
|
70
|
+
* @param {string} event.agentId - Source agent.
|
|
71
|
+
* @param {string} event.type - Event type: 'scan', 'threat', 'block', 'anomaly'.
|
|
72
|
+
* @param {string} [event.category] - Threat category if applicable.
|
|
73
|
+
* @param {string} [event.severity] - Threat severity.
|
|
74
|
+
* @param {string} [event.description] - Event description.
|
|
75
|
+
* @param {boolean} [event.blocked] - Whether the threat was blocked.
|
|
76
|
+
* @param {object} [event.metadata] - Additional data.
|
|
77
|
+
*/
|
|
78
|
+
ingest(event) {
|
|
79
|
+
const enriched = {
|
|
80
|
+
id: crypto.randomBytes(8).toString('hex'),
|
|
81
|
+
agentId: event.agentId,
|
|
82
|
+
type: event.type || 'scan',
|
|
83
|
+
category: event.category || null,
|
|
84
|
+
severity: event.severity || null,
|
|
85
|
+
description: event.description || '',
|
|
86
|
+
blocked: event.blocked || false,
|
|
87
|
+
metadata: event.metadata || {},
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this._events.push(enriched);
|
|
92
|
+
this._stats.totalEvents++;
|
|
93
|
+
|
|
94
|
+
if (enriched.type === 'threat' || enriched.severity) {
|
|
95
|
+
this._stats.totalThreats++;
|
|
96
|
+
}
|
|
97
|
+
if (enriched.blocked) {
|
|
98
|
+
this._stats.totalBlocked++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Update agent stats
|
|
102
|
+
const agent = this._agents.get(enriched.agentId);
|
|
103
|
+
if (agent) {
|
|
104
|
+
agent.lastActivity = Date.now();
|
|
105
|
+
agent.eventCount++;
|
|
106
|
+
if (enriched.severity) agent.threatCount++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Rotate events
|
|
110
|
+
if (this._events.length > this.maxEvents) {
|
|
111
|
+
this._events = this._events.slice(-Math.floor(this.maxEvents * 0.75));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Emit for real-time listeners
|
|
115
|
+
this.emit('event', enriched);
|
|
116
|
+
|
|
117
|
+
// Check alert conditions
|
|
118
|
+
if (enriched.severity === 'critical') {
|
|
119
|
+
this._sendAlert({
|
|
120
|
+
level: 'critical',
|
|
121
|
+
title: `Critical threat on ${enriched.agentId}`,
|
|
122
|
+
message: enriched.description,
|
|
123
|
+
event: enriched,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return enriched;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// =======================================================================
|
|
131
|
+
// Querying
|
|
132
|
+
// =======================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get events with optional filters.
|
|
136
|
+
* @param {object} [filters]
|
|
137
|
+
* @param {string} [filters.agentId]
|
|
138
|
+
* @param {string} [filters.type]
|
|
139
|
+
* @param {string} [filters.category]
|
|
140
|
+
* @param {string} [filters.severity]
|
|
141
|
+
* @param {number} [filters.since] - Timestamp.
|
|
142
|
+
* @param {number} [filters.limit=100]
|
|
143
|
+
* @returns {Array}
|
|
144
|
+
*/
|
|
145
|
+
query(filters = {}) {
|
|
146
|
+
let results = this._events;
|
|
147
|
+
|
|
148
|
+
if (filters.agentId) results = results.filter(e => e.agentId === filters.agentId);
|
|
149
|
+
if (filters.type) results = results.filter(e => e.type === filters.type);
|
|
150
|
+
if (filters.category) results = results.filter(e => e.category === filters.category);
|
|
151
|
+
if (filters.severity) results = results.filter(e => e.severity === filters.severity);
|
|
152
|
+
if (filters.since) results = results.filter(e => e.timestamp >= filters.since);
|
|
153
|
+
|
|
154
|
+
const limit = filters.limit || 100;
|
|
155
|
+
return results.slice(-limit);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get threat distribution by category.
|
|
160
|
+
* @param {number} [sinceMs] - Time window in ms (e.g., 3600000 for last hour).
|
|
161
|
+
* @returns {object} Category -> count mapping.
|
|
162
|
+
*/
|
|
163
|
+
getThreatDistribution(sinceMs) {
|
|
164
|
+
const cutoff = sinceMs ? Date.now() - sinceMs : 0;
|
|
165
|
+
const dist = {};
|
|
166
|
+
for (const e of this._events) {
|
|
167
|
+
if (e.timestamp >= cutoff && e.category) {
|
|
168
|
+
dist[e.category] = (dist[e.category] || 0) + 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return dist;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get severity distribution.
|
|
176
|
+
* @param {number} [sinceMs]
|
|
177
|
+
* @returns {object}
|
|
178
|
+
*/
|
|
179
|
+
getSeverityDistribution(sinceMs) {
|
|
180
|
+
const cutoff = sinceMs ? Date.now() - sinceMs : 0;
|
|
181
|
+
const dist = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
182
|
+
for (const e of this._events) {
|
|
183
|
+
if (e.timestamp >= cutoff && e.severity && dist[e.severity] !== undefined) {
|
|
184
|
+
dist[e.severity]++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return dist;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get per-agent threat summary.
|
|
192
|
+
* @returns {Array}
|
|
193
|
+
*/
|
|
194
|
+
getAgentSummary() {
|
|
195
|
+
const summaries = [];
|
|
196
|
+
for (const [id, agent] of this._agents) {
|
|
197
|
+
const recentThreats = this._events.filter(
|
|
198
|
+
e => e.agentId === id && e.severity && e.timestamp > Date.now() - 3600000
|
|
199
|
+
).length;
|
|
200
|
+
summaries.push({
|
|
201
|
+
...agent,
|
|
202
|
+
recentThreats,
|
|
203
|
+
status: recentThreats > 10 ? 'critical' : recentThreats > 3 ? 'elevated' : 'normal',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return summaries.sort((a, b) => b.threatCount - a.threatCount);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get timeline data for charting (bucketed by interval).
|
|
211
|
+
* @param {number} [intervalMs=60000] - Bucket size in ms (default: 1 minute).
|
|
212
|
+
* @param {number} [windowMs=3600000] - Total window (default: 1 hour).
|
|
213
|
+
* @returns {Array} Array of { timestamp, threats, blocked, scans }.
|
|
214
|
+
*/
|
|
215
|
+
getTimeline(intervalMs = 60000, windowMs = 3600000) {
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
const start = now - windowMs;
|
|
218
|
+
const buckets = [];
|
|
219
|
+
|
|
220
|
+
for (let t = start; t < now; t += intervalMs) {
|
|
221
|
+
const bucketEnd = t + intervalMs;
|
|
222
|
+
const events = this._events.filter(e => e.timestamp >= t && e.timestamp < bucketEnd);
|
|
223
|
+
buckets.push({
|
|
224
|
+
timestamp: t,
|
|
225
|
+
threats: events.filter(e => e.severity).length,
|
|
226
|
+
blocked: events.filter(e => e.blocked).length,
|
|
227
|
+
scans: events.filter(e => e.type === 'scan').length,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return buckets;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get dashboard summary.
|
|
236
|
+
* @returns {object}
|
|
237
|
+
*/
|
|
238
|
+
getSummary() {
|
|
239
|
+
return {
|
|
240
|
+
stats: { ...this._stats },
|
|
241
|
+
agents: this.getAgentSummary(),
|
|
242
|
+
threatDistribution: this.getThreatDistribution(3600000),
|
|
243
|
+
severityDistribution: this.getSeverityDistribution(3600000),
|
|
244
|
+
recentEvents: this._events.slice(-20),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// =======================================================================
|
|
249
|
+
// Alerting
|
|
250
|
+
// =======================================================================
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Add an alert channel.
|
|
254
|
+
* @param {AlertChannel} channel
|
|
255
|
+
*/
|
|
256
|
+
addAlertChannel(channel) {
|
|
257
|
+
this._alertChannels.push(channel);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** @private */
|
|
261
|
+
_sendAlert(alert) {
|
|
262
|
+
const key = `${alert.level}:${alert.event.agentId}:${alert.event.category}`;
|
|
263
|
+
const lastSent = this._lastAlerts.get(key);
|
|
264
|
+
|
|
265
|
+
// Cooldown to prevent alert storms
|
|
266
|
+
if (lastSent && Date.now() - lastSent < this.alertCooldownMs) return;
|
|
267
|
+
this._lastAlerts.set(key, Date.now());
|
|
268
|
+
|
|
269
|
+
this.emit('alert', alert);
|
|
270
|
+
for (const channel of this._alertChannels) {
|
|
271
|
+
try { channel.send(alert); } catch (_) { /* channel error */ }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// =========================================================================
|
|
277
|
+
// Alert Channels
|
|
278
|
+
// =========================================================================
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Base alert channel. Extend for specific integrations.
|
|
282
|
+
*/
|
|
283
|
+
class AlertChannel {
|
|
284
|
+
send(alert) { throw new Error('Not implemented'); }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Slack webhook alert channel.
|
|
289
|
+
*/
|
|
290
|
+
class SlackAlertChannel extends AlertChannel {
|
|
291
|
+
/**
|
|
292
|
+
* @param {object} options
|
|
293
|
+
* @param {string} options.webhookUrl - Slack webhook URL.
|
|
294
|
+
* @param {string} [options.channel] - Override channel.
|
|
295
|
+
*/
|
|
296
|
+
constructor(options = {}) {
|
|
297
|
+
super();
|
|
298
|
+
this.webhookUrl = options.webhookUrl;
|
|
299
|
+
this.channel = options.channel;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
send(alert) {
|
|
303
|
+
const payload = {
|
|
304
|
+
text: `[Agent Shield ${alert.level.toUpperCase()}] ${alert.title}`,
|
|
305
|
+
blocks: [
|
|
306
|
+
{ type: 'header', text: { type: 'plain_text', text: `Agent Shield Alert: ${alert.title}` } },
|
|
307
|
+
{ type: 'section', text: { type: 'mrkdwn', text: alert.message } },
|
|
308
|
+
{ type: 'context', elements: [
|
|
309
|
+
{ type: 'mrkdwn', text: `*Agent:* ${alert.event.agentId} | *Severity:* ${alert.event.severity} | *Category:* ${alert.event.category}` }
|
|
310
|
+
]},
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
if (this.channel) payload.channel = this.channel;
|
|
314
|
+
|
|
315
|
+
// In production, this would be: fetch(this.webhookUrl, { method: 'POST', body: JSON.stringify(payload) })
|
|
316
|
+
// Stored for retrieval/testing
|
|
317
|
+
this.lastPayload = payload;
|
|
318
|
+
return payload;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* PagerDuty alert channel.
|
|
324
|
+
*/
|
|
325
|
+
class PagerDutyAlertChannel extends AlertChannel {
|
|
326
|
+
/**
|
|
327
|
+
* @param {object} options
|
|
328
|
+
* @param {string} options.routingKey - PagerDuty integration key.
|
|
329
|
+
*/
|
|
330
|
+
constructor(options = {}) {
|
|
331
|
+
super();
|
|
332
|
+
this.routingKey = options.routingKey;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
send(alert) {
|
|
336
|
+
const payload = {
|
|
337
|
+
routing_key: this.routingKey,
|
|
338
|
+
event_action: alert.level === 'critical' ? 'trigger' : 'acknowledge',
|
|
339
|
+
payload: {
|
|
340
|
+
summary: `[Agent Shield] ${alert.title}`,
|
|
341
|
+
severity: alert.level === 'critical' ? 'critical' : 'warning',
|
|
342
|
+
source: alert.event.agentId,
|
|
343
|
+
component: 'agent-shield',
|
|
344
|
+
group: alert.event.category,
|
|
345
|
+
custom_details: {
|
|
346
|
+
description: alert.message,
|
|
347
|
+
severity: alert.event.severity,
|
|
348
|
+
category: alert.event.category,
|
|
349
|
+
timestamp: new Date().toISOString(),
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
this.lastPayload = payload;
|
|
354
|
+
return payload;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Microsoft Teams webhook alert channel.
|
|
360
|
+
*/
|
|
361
|
+
class TeamsAlertChannel extends AlertChannel {
|
|
362
|
+
constructor(options = {}) {
|
|
363
|
+
super();
|
|
364
|
+
this.webhookUrl = options.webhookUrl;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
send(alert) {
|
|
368
|
+
const color = alert.level === 'critical' ? 'FF0000' : 'FFA500';
|
|
369
|
+
const payload = {
|
|
370
|
+
'@type': 'MessageCard',
|
|
371
|
+
themeColor: color,
|
|
372
|
+
summary: `Agent Shield: ${alert.title}`,
|
|
373
|
+
sections: [{
|
|
374
|
+
activityTitle: `Agent Shield Alert: ${alert.title}`,
|
|
375
|
+
facts: [
|
|
376
|
+
{ name: 'Agent', value: alert.event.agentId },
|
|
377
|
+
{ name: 'Severity', value: alert.event.severity },
|
|
378
|
+
{ name: 'Category', value: alert.event.category },
|
|
379
|
+
],
|
|
380
|
+
text: alert.message,
|
|
381
|
+
}],
|
|
382
|
+
};
|
|
383
|
+
this.lastPayload = payload;
|
|
384
|
+
return payload;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = {
|
|
389
|
+
SOCDashboard,
|
|
390
|
+
AlertChannel,
|
|
391
|
+
SlackAlertChannel,
|
|
392
|
+
PagerDutyAlertChannel,
|
|
393
|
+
TeamsAlertChannel,
|
|
394
|
+
};
|
package/src/stream-scanner.js
CHANGED
|
@@ -551,6 +551,26 @@ class StreamScanner extends EventEmitter {
|
|
|
551
551
|
const iterable = _eventEmitterToAsyncIterable(emitter);
|
|
552
552
|
return this.wrap(iterable);
|
|
553
553
|
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Wrap a Promise that resolves to a stream/async iterable.
|
|
557
|
+
* Safely handles rejection before and during iteration.
|
|
558
|
+
*
|
|
559
|
+
* @param {Promise<AsyncIterable>} streamPromise - A Promise resolving to an async iterable.
|
|
560
|
+
* @param {object} [options] - Options passed to wrap().
|
|
561
|
+
* @returns {AsyncGenerator} Yields chunks with scanning applied.
|
|
562
|
+
*/
|
|
563
|
+
async *wrapPromise(streamPromise, options = {}) {
|
|
564
|
+
let stream;
|
|
565
|
+
try {
|
|
566
|
+
stream = await streamPromise;
|
|
567
|
+
} catch (err) {
|
|
568
|
+
this._streamError = err;
|
|
569
|
+
this.finalize();
|
|
570
|
+
throw err;
|
|
571
|
+
}
|
|
572
|
+
yield* this.wrap(stream, options);
|
|
573
|
+
}
|
|
554
574
|
}
|
|
555
575
|
|
|
556
576
|
// =========================================================================
|
|
@@ -782,10 +802,20 @@ async function scanAsyncIterator(iterator, options = {}) {
|
|
|
782
802
|
const scanner = new StreamScanner(scannerOpts);
|
|
783
803
|
const wrapped = scanner.wrap(iterator, { extractText });
|
|
784
804
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
805
|
+
try {
|
|
806
|
+
// Consume the wrapped iterator fully
|
|
807
|
+
// eslint-disable-next-line no-unused-vars
|
|
808
|
+
for await (const _chunk of wrapped) {
|
|
809
|
+
// consumed - side effect is the scanning
|
|
810
|
+
}
|
|
811
|
+
} catch (err) {
|
|
812
|
+
// Ensure finalization on error and attach the error to result
|
|
813
|
+
scanner._streamError = scanner._streamError || err;
|
|
814
|
+
if (!scanner._ended) {
|
|
815
|
+
scanner.finalize();
|
|
816
|
+
}
|
|
817
|
+
// Re-throw so callers know the stream failed
|
|
818
|
+
throw err;
|
|
789
819
|
}
|
|
790
820
|
|
|
791
821
|
return {
|