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
package/src/distributed.js
CHANGED
|
@@ -1,359 +1,403 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Agent Shield — Distributed Scanning (v2.1)
|
|
5
|
-
*
|
|
6
|
-
* Share threat state, scan results, and pattern updates across multiple
|
|
7
|
-
* Agent Shield instances. Includes an in-memory adapter for single-process
|
|
8
|
-
* use and adapter interfaces for Redis, Memcached, etc.
|
|
9
|
-
*
|
|
10
|
-
* Zero dependencies — uses Node.js built-in modules only.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const crypto = require('crypto');
|
|
14
|
-
const { EventEmitter } = require('events');
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
* @param {
|
|
28
|
-
* @param {
|
|
29
|
-
* @
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
* @
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
* @
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
* @param {
|
|
51
|
-
* @
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
* @param {
|
|
59
|
-
* @
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
*
|
|
66
|
-
* @param {
|
|
67
|
-
* @
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.
|
|
84
|
-
this.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this._timers.
|
|
92
|
-
|
|
93
|
-
this.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this._timers.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
*
|
|
132
|
-
* @
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
this._timers.
|
|
141
|
-
this.
|
|
142
|
-
this.
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
* @param {object} options
|
|
158
|
-
* @param {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
subscriber.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
* @param {
|
|
221
|
-
* @param {
|
|
222
|
-
* @param {
|
|
223
|
-
* @param {number} [options.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
this.
|
|
228
|
-
this.
|
|
229
|
-
this.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
this.
|
|
233
|
-
this.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
await this.adapter.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Distributed Scanning (v2.1)
|
|
5
|
+
*
|
|
6
|
+
* Share threat state, scan results, and pattern updates across multiple
|
|
7
|
+
* Agent Shield instances. Includes an in-memory adapter for single-process
|
|
8
|
+
* use and adapter interfaces for Redis, Memcached, etc.
|
|
9
|
+
*
|
|
10
|
+
* Zero dependencies — uses Node.js built-in modules only.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const { EventEmitter } = require('events');
|
|
15
|
+
const { createShieldError } = require('./errors');
|
|
16
|
+
|
|
17
|
+
// =========================================================================
|
|
18
|
+
// ADAPTER INTERFACE
|
|
19
|
+
// =========================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Base adapter class. Extend this for custom backends (Redis, Memcached, etc.).
|
|
23
|
+
*/
|
|
24
|
+
class DistributedAdapter extends EventEmitter {
|
|
25
|
+
/**
|
|
26
|
+
* Store a value.
|
|
27
|
+
* @param {string} key
|
|
28
|
+
* @param {*} value
|
|
29
|
+
* @param {number} [ttlMs] - Time-to-live in milliseconds.
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
async set(key, value, ttlMs) { throw new Error('Not implemented'); }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Retrieve a value.
|
|
36
|
+
* @param {string} key
|
|
37
|
+
* @returns {Promise<*>}
|
|
38
|
+
*/
|
|
39
|
+
async get(key) { throw new Error('Not implemented'); }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Delete a key.
|
|
43
|
+
* @param {string} key
|
|
44
|
+
* @returns {Promise<boolean>}
|
|
45
|
+
*/
|
|
46
|
+
async del(key) { throw new Error('Not implemented'); }
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Publish a message to a channel.
|
|
50
|
+
* @param {string} channel
|
|
51
|
+
* @param {*} message
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
54
|
+
async publish(channel, message) { throw new Error('Not implemented'); }
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Subscribe to a channel.
|
|
58
|
+
* @param {string} channel
|
|
59
|
+
* @param {Function} handler
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
async subscribe(channel, handler) { throw new Error('Not implemented'); }
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Increment a counter atomically.
|
|
66
|
+
* @param {string} key
|
|
67
|
+
* @param {number} [amount=1]
|
|
68
|
+
* @returns {Promise<number>} New value.
|
|
69
|
+
*/
|
|
70
|
+
async incr(key, amount = 1) { throw new Error('Not implemented'); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// =========================================================================
|
|
74
|
+
// IN-MEMORY ADAPTER
|
|
75
|
+
// =========================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* In-memory adapter for single-process or testing use.
|
|
79
|
+
*/
|
|
80
|
+
class MemoryAdapter extends DistributedAdapter {
|
|
81
|
+
constructor() {
|
|
82
|
+
super();
|
|
83
|
+
this._store = new Map();
|
|
84
|
+
this._subscriptions = new Map();
|
|
85
|
+
this._timers = new Map();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async set(key, value, ttlMs) {
|
|
89
|
+
this._store.set(key, value);
|
|
90
|
+
if (ttlMs) {
|
|
91
|
+
if (this._timers.has(key)) clearTimeout(this._timers.get(key));
|
|
92
|
+
this._timers.set(key, setTimeout(() => {
|
|
93
|
+
this._store.delete(key);
|
|
94
|
+
this._timers.delete(key);
|
|
95
|
+
}, ttlMs));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async get(key) {
|
|
100
|
+
return this._store.get(key) || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async del(key) {
|
|
104
|
+
if (this._timers.has(key)) clearTimeout(this._timers.get(key));
|
|
105
|
+
this._timers.delete(key);
|
|
106
|
+
return this._store.delete(key);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async publish(channel, message) {
|
|
110
|
+
const handlers = this._subscriptions.get(channel) || [];
|
|
111
|
+
for (const handler of handlers) {
|
|
112
|
+
handler(message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async subscribe(channel, handler) {
|
|
117
|
+
if (!this._subscriptions.has(channel)) {
|
|
118
|
+
this._subscriptions.set(channel, []);
|
|
119
|
+
}
|
|
120
|
+
this._subscriptions.get(channel).push(handler);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async incr(key, amount = 1) {
|
|
124
|
+
const current = this._store.get(key) || 0;
|
|
125
|
+
const newVal = current + amount;
|
|
126
|
+
this._store.set(key, newVal);
|
|
127
|
+
return newVal;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get all keys matching a prefix.
|
|
132
|
+
* @param {string} prefix
|
|
133
|
+
* @returns {Promise<string[]>}
|
|
134
|
+
*/
|
|
135
|
+
async keys(prefix) {
|
|
136
|
+
return [...this._store.keys()].filter(k => k.startsWith(prefix));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
destroy() {
|
|
140
|
+
for (const timer of this._timers.values()) clearTimeout(timer);
|
|
141
|
+
this._timers.clear();
|
|
142
|
+
this._store.clear();
|
|
143
|
+
this._subscriptions.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// =========================================================================
|
|
148
|
+
// REDIS ADAPTER TEMPLATE
|
|
149
|
+
// =========================================================================
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Redis adapter template. Users provide their own redis client instance.
|
|
153
|
+
* Usage: new RedisAdapter({ client: require('redis').createClient() })
|
|
154
|
+
*/
|
|
155
|
+
class RedisAdapter extends DistributedAdapter {
|
|
156
|
+
/**
|
|
157
|
+
* @param {object} options
|
|
158
|
+
* @param {object} options.client - A Redis client instance (e.g., ioredis or node-redis).
|
|
159
|
+
* @param {string} [options.prefix='agent-shield:'] - Key prefix.
|
|
160
|
+
*/
|
|
161
|
+
constructor(options = {}) {
|
|
162
|
+
super();
|
|
163
|
+
this.client = options.client;
|
|
164
|
+
this.prefix = options.prefix || 'agent-shield:';
|
|
165
|
+
|
|
166
|
+
if (!this.client) {
|
|
167
|
+
throw createShieldError('AS-NET-003', { reason: 'RedisAdapter requires a Redis client instance. Pass { client: redisClient }.' });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async set(key, value, ttlMs) {
|
|
172
|
+
const serialized = JSON.stringify(value);
|
|
173
|
+
if (ttlMs) {
|
|
174
|
+
await this.client.set(this.prefix + key, serialized, 'PX', ttlMs);
|
|
175
|
+
} else {
|
|
176
|
+
await this.client.set(this.prefix + key, serialized);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async get(key) {
|
|
181
|
+
const result = await this.client.get(this.prefix + key);
|
|
182
|
+
return result ? JSON.parse(result) : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async del(key) {
|
|
186
|
+
const result = await this.client.del(this.prefix + key);
|
|
187
|
+
return result > 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async publish(channel, message) {
|
|
191
|
+
await this.client.publish(this.prefix + channel, JSON.stringify(message));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async subscribe(channel, handler) {
|
|
195
|
+
const subscriber = this.client.duplicate();
|
|
196
|
+
await subscriber.subscribe(this.prefix + channel);
|
|
197
|
+
subscriber.on('message', (ch, msg) => {
|
|
198
|
+
try {
|
|
199
|
+
handler(JSON.parse(msg));
|
|
200
|
+
} catch (e) {
|
|
201
|
+
handler(msg);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async incr(key, amount = 1) {
|
|
207
|
+
return this.client.incrby(this.prefix + key, amount);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// =========================================================================
|
|
212
|
+
// DISTRIBUTED SHIELD
|
|
213
|
+
// =========================================================================
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Coordinates multiple Agent Shield instances sharing threat state.
|
|
217
|
+
*/
|
|
218
|
+
class DistributedShield {
|
|
219
|
+
/**
|
|
220
|
+
* @param {object} [options]
|
|
221
|
+
* @param {DistributedAdapter} [options.adapter] - Storage adapter (defaults to MemoryAdapter).
|
|
222
|
+
* @param {string} [options.instanceId] - Unique ID for this instance.
|
|
223
|
+
* @param {number} [options.syncIntervalMs=30000] - How often to sync state.
|
|
224
|
+
* @param {number} [options.threatTTLMs=3600000] - How long threats persist (1 hour default).
|
|
225
|
+
*/
|
|
226
|
+
constructor(options = {}) {
|
|
227
|
+
this.adapter = options.adapter || new MemoryAdapter();
|
|
228
|
+
this.instanceId = options.instanceId || crypto.randomBytes(8).toString('hex');
|
|
229
|
+
this.syncIntervalMs = options.syncIntervalMs || 30000;
|
|
230
|
+
this.threatTTLMs = options.threatTTLMs || 3600000;
|
|
231
|
+
|
|
232
|
+
this._localThreats = [];
|
|
233
|
+
this._maxLocalThreats = options.maxLocalThreats || 1000;
|
|
234
|
+
this._syncTimer = null;
|
|
235
|
+
this._started = false;
|
|
236
|
+
|
|
237
|
+
// Queue depth monitoring
|
|
238
|
+
this._pendingOps = 0;
|
|
239
|
+
this._peakQueueDepth = 0;
|
|
240
|
+
this._totalOpsQueued = 0;
|
|
241
|
+
|
|
242
|
+
console.log('[Agent Shield] DistributedShield initialized (instance: %s)', this.instanceId);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Start distributed coordination.
|
|
247
|
+
* @returns {Promise<void>}
|
|
248
|
+
*/
|
|
249
|
+
async start() {
|
|
250
|
+
if (this._started) return;
|
|
251
|
+
this._started = true;
|
|
252
|
+
|
|
253
|
+
// Register this instance
|
|
254
|
+
await this.adapter.set(`instance:${this.instanceId}`, {
|
|
255
|
+
id: this.instanceId,
|
|
256
|
+
startedAt: Date.now(),
|
|
257
|
+
lastHeartbeat: Date.now()
|
|
258
|
+
}, this.threatTTLMs);
|
|
259
|
+
|
|
260
|
+
// Subscribe to threat broadcasts
|
|
261
|
+
await this.adapter.subscribe('threats', (threat) => {
|
|
262
|
+
if (threat.instanceId !== this.instanceId) {
|
|
263
|
+
this._localThreats.push(threat);
|
|
264
|
+
if (this._localThreats.length > this._maxLocalThreats) {
|
|
265
|
+
this._localThreats = this._localThreats.slice(-Math.floor(this._maxLocalThreats * 0.75));
|
|
266
|
+
}
|
|
267
|
+
console.log('[Agent Shield] Received threat from instance %s: %s', threat.instanceId, threat.category);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Heartbeat
|
|
272
|
+
this._syncTimer = setInterval(async () => {
|
|
273
|
+
await this.adapter.set(`instance:${this.instanceId}`, {
|
|
274
|
+
id: this.instanceId,
|
|
275
|
+
startedAt: Date.now(),
|
|
276
|
+
lastHeartbeat: Date.now()
|
|
277
|
+
}, this.threatTTLMs);
|
|
278
|
+
}, this.syncIntervalMs);
|
|
279
|
+
if (this._syncTimer.unref) this._syncTimer.unref();
|
|
280
|
+
|
|
281
|
+
console.log('[Agent Shield] DistributedShield started');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Report a detected threat to all instances.
|
|
286
|
+
* @param {object} threat - Threat object from a scan result.
|
|
287
|
+
* @returns {Promise<void>}
|
|
288
|
+
*/
|
|
289
|
+
async reportThreat(threat) {
|
|
290
|
+
this._trackOp(1);
|
|
291
|
+
try {
|
|
292
|
+
const entry = {
|
|
293
|
+
...threat,
|
|
294
|
+
instanceId: this.instanceId,
|
|
295
|
+
timestamp: Date.now(),
|
|
296
|
+
id: crypto.randomBytes(8).toString('hex')
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Store in shared state
|
|
300
|
+
await this.adapter.set(`threat:${entry.id}`, entry, this.threatTTLMs);
|
|
301
|
+
await this.adapter.incr('stats:totalThreats');
|
|
302
|
+
await this.adapter.incr(`stats:category:${threat.category || 'unknown'}`);
|
|
303
|
+
|
|
304
|
+
// Broadcast to other instances
|
|
305
|
+
await this.adapter.publish('threats', entry);
|
|
306
|
+
|
|
307
|
+
this._localThreats.push(entry);
|
|
308
|
+
if (this._localThreats.length > this._maxLocalThreats) {
|
|
309
|
+
this._localThreats = this._localThreats.slice(-Math.floor(this._maxLocalThreats * 0.75));
|
|
310
|
+
}
|
|
311
|
+
} finally {
|
|
312
|
+
this._trackOp(-1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get aggregated threat statistics across all instances.
|
|
318
|
+
* @returns {Promise<object>}
|
|
319
|
+
*/
|
|
320
|
+
async getGlobalStats() {
|
|
321
|
+
const totalThreats = await this.adapter.get('stats:totalThreats') || 0;
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
instanceId: this.instanceId,
|
|
325
|
+
totalThreats,
|
|
326
|
+
localThreats: this._localThreats.length,
|
|
327
|
+
started: this._started
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Check if a threat signature has been seen by any instance.
|
|
333
|
+
* @param {string} signature - Threat hash/signature.
|
|
334
|
+
* @returns {Promise<boolean>}
|
|
335
|
+
*/
|
|
336
|
+
async isKnownThreat(signature) {
|
|
337
|
+
const result = await this.adapter.get(`threat:sig:${signature}`);
|
|
338
|
+
return result !== null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Mark a threat signature as known.
|
|
343
|
+
* @param {string} signature
|
|
344
|
+
* @param {object} [metadata]
|
|
345
|
+
* @returns {Promise<void>}
|
|
346
|
+
*/
|
|
347
|
+
async markKnownThreat(signature, metadata = {}) {
|
|
348
|
+
await this.adapter.set(`threat:sig:${signature}`, {
|
|
349
|
+
...metadata,
|
|
350
|
+
firstSeen: Date.now(),
|
|
351
|
+
reportedBy: this.instanceId
|
|
352
|
+
}, this.threatTTLMs);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Returns queue depth metrics for monitoring.
|
|
357
|
+
* @returns {{ pending: number, peak: number, totalQueued: number }}
|
|
358
|
+
*/
|
|
359
|
+
getQueueDepth() {
|
|
360
|
+
return {
|
|
361
|
+
pending: this._pendingOps,
|
|
362
|
+
peak: this._peakQueueDepth,
|
|
363
|
+
totalQueued: this._totalOpsQueued
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Track pending async operations for queue depth monitoring.
|
|
369
|
+
* @param {number} delta - +1 when starting, -1 when completing.
|
|
370
|
+
* @private
|
|
371
|
+
*/
|
|
372
|
+
_trackOp(delta) {
|
|
373
|
+
this._pendingOps += delta;
|
|
374
|
+
if (delta > 0) this._totalOpsQueued++;
|
|
375
|
+
if (this._pendingOps > this._peakQueueDepth) {
|
|
376
|
+
this._peakQueueDepth = this._pendingOps;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Stop distributed coordination.
|
|
382
|
+
*/
|
|
383
|
+
async stop() {
|
|
384
|
+
if (this._syncTimer) {
|
|
385
|
+
clearInterval(this._syncTimer);
|
|
386
|
+
this._syncTimer = null;
|
|
387
|
+
}
|
|
388
|
+
this._started = false;
|
|
389
|
+
await this.adapter.del(`instance:${this.instanceId}`);
|
|
390
|
+
console.log('[Agent Shield] DistributedShield stopped');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// =========================================================================
|
|
395
|
+
// EXPORTS
|
|
396
|
+
// =========================================================================
|
|
397
|
+
|
|
398
|
+
module.exports = {
|
|
399
|
+
DistributedShield,
|
|
400
|
+
DistributedAdapter,
|
|
401
|
+
MemoryAdapter,
|
|
402
|
+
RedisAdapter
|
|
403
|
+
};
|