agentshield-sdk 7.3.0 → 7.4.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +36 -7
  3. package/package.json +7 -3
  4. package/src/agent-protocol.js +4 -0
  5. package/src/allowlist.js +605 -603
  6. package/src/audit-streaming.js +486 -469
  7. package/src/audit.js +1 -1
  8. package/src/behavior-profiling.js +299 -289
  9. package/src/behavioral-dna.js +4 -9
  10. package/src/canary.js +273 -271
  11. package/src/compliance.js +619 -617
  12. package/src/confidence-tuning.js +328 -324
  13. package/src/context-scoring.js +362 -360
  14. package/src/cost-optimizer.js +1024 -1024
  15. package/src/detector-core.js +186 -0
  16. package/src/distributed.js +5 -1
  17. package/src/embedding.js +310 -307
  18. package/src/herd-immunity.js +12 -12
  19. package/src/honeypot.js +332 -328
  20. package/src/integrations.js +1 -2
  21. package/src/intent-firewall.js +14 -14
  22. package/src/llm-redteam.js +678 -670
  23. package/src/main.js +10 -0
  24. package/src/middleware.js +5 -2
  25. package/src/model-fingerprint.js +1059 -1042
  26. package/src/multi-agent-trust.js +459 -453
  27. package/src/multi-agent.js +1 -1
  28. package/src/normalizer.js +734 -0
  29. package/src/pii.js +4 -0
  30. package/src/policy-dsl.js +775 -775
  31. package/src/presets.js +409 -409
  32. package/src/production.js +22 -9
  33. package/src/redteam.js +475 -475
  34. package/src/response-handler.js +436 -429
  35. package/src/scanners.js +358 -357
  36. package/src/self-healing.js +368 -363
  37. package/src/semantic.js +339 -339
  38. package/src/shield-score.js +250 -250
  39. package/src/sso-saml.js +8 -4
  40. package/src/testing.js +24 -2
  41. package/src/tool-guard.js +412 -412
  42. package/src/watermark.js +242 -235
  43. package/src/worker-scanner.js +608 -601
@@ -1,469 +1,486 @@
1
- 'use strict';
2
-
3
- /**
4
- * Agent Shield — Audit Log Streaming (v2.1)
5
- *
6
- * Stream security audit events to external logging/SIEM systems.
7
- * Supports Splunk HEC, Elasticsearch, file-based logging, and custom transports.
8
- *
9
- * Zero dependencies — uses Node.js built-in http/https/fs modules.
10
- */
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
- const http = require('http');
15
- const https = require('https');
16
-
17
- // =========================================================================
18
- // TRANSPORT INTERFACE
19
- // =========================================================================
20
-
21
- /**
22
- * Base transport class. Extend for custom destinations.
23
- */
24
- class AuditTransport {
25
- /**
26
- * Send an event to the transport.
27
- * @param {object} event - Audit event.
28
- * @returns {Promise<void>}
29
- */
30
- async send(event) { throw new Error('Not implemented'); }
31
-
32
- /**
33
- * Send multiple events in a batch.
34
- * @param {Array<object>} events
35
- * @returns {Promise<void>}
36
- */
37
- async sendBatch(events) {
38
- for (const event of events) {
39
- await this.send(event);
40
- }
41
- }
42
-
43
- /**
44
- * Flush any buffered events.
45
- * @returns {Promise<void>}
46
- */
47
- async flush() {}
48
-
49
- /**
50
- * Close the transport.
51
- * @returns {Promise<void>}
52
- */
53
- async close() {}
54
- }
55
-
56
- // =========================================================================
57
- // FILE TRANSPORT
58
- // =========================================================================
59
-
60
- /**
61
- * Writes audit events to a local file (JSONL format).
62
- */
63
- class FileTransport extends AuditTransport {
64
- /**
65
- * @param {object} [options]
66
- * @param {string} [options.filePath='./agent-shield-audit.log'] - Log file path.
67
- * @param {number} [options.maxSizeMB=100] - Max file size before rotation.
68
- * @param {number} [options.maxFiles=5] - Number of rotated files to keep.
69
- */
70
- constructor(options = {}) {
71
- super();
72
- this.filePath = options.filePath || './agent-shield-audit.log';
73
- this.maxSizeBytes = (options.maxSizeMB || 100) * 1024 * 1024;
74
- this.maxFiles = options.maxFiles || 5;
75
- this._buffer = [];
76
- this._bufferSize = 0;
77
- this._flushInterval = setInterval(() => this.flush(), 5000);
78
- }
79
-
80
- async send(event) {
81
- const line = JSON.stringify(event) + '\n';
82
- this._buffer.push(line);
83
- this._bufferSize += line.length;
84
-
85
- if (this._bufferSize >= 64 * 1024) {
86
- await this.flush();
87
- }
88
- }
89
-
90
- async flush() {
91
- if (this._buffer.length === 0) return;
92
-
93
- const data = this._buffer.join('');
94
- this._buffer = [];
95
- this._bufferSize = 0;
96
-
97
- try {
98
- // Check rotation
99
- if (fs.existsSync(this.filePath)) {
100
- const stat = fs.statSync(this.filePath);
101
- if (stat.size + data.length > this.maxSizeBytes) {
102
- this._rotate();
103
- }
104
- }
105
-
106
- fs.appendFileSync(this.filePath, data);
107
- } catch (e) {
108
- console.warn('[Agent Shield] FileTransport write error:', e.message);
109
- }
110
- }
111
-
112
- async close() {
113
- clearInterval(this._flushInterval);
114
- await this.flush();
115
- }
116
-
117
- /** @private */
118
- _rotate() {
119
- for (let i = this.maxFiles - 1; i >= 1; i--) {
120
- const from = `${this.filePath}.${i}`;
121
- const to = `${this.filePath}.${i + 1}`;
122
- if (fs.existsSync(from)) {
123
- if (i + 1 > this.maxFiles) fs.unlinkSync(from);
124
- else fs.renameSync(from, to);
125
- }
126
- }
127
- if (fs.existsSync(this.filePath)) {
128
- fs.renameSync(this.filePath, `${this.filePath}.1`);
129
- }
130
- }
131
- }
132
-
133
- // =========================================================================
134
- // SPLUNK HEC TRANSPORT
135
- // =========================================================================
136
-
137
- /**
138
- * Sends audit events to Splunk via HTTP Event Collector (HEC).
139
- */
140
- class SplunkTransport extends AuditTransport {
141
- /**
142
- * @param {object} options
143
- * @param {string} options.url - Splunk HEC URL (e.g., https://splunk:8088/services/collector/event).
144
- * @param {string} options.token - HEC token.
145
- * @param {string} [options.index='main'] - Splunk index.
146
- * @param {string} [options.source='agent-shield'] - Event source.
147
- * @param {string} [options.sourcetype='_json'] - Source type.
148
- * @param {number} [options.batchSize=50] - Events per batch.
149
- * @param {number} [options.flushIntervalMs=5000] - Auto-flush interval.
150
- */
151
- constructor(options = {}) {
152
- super();
153
- this.url = options.url;
154
- this.token = options.token;
155
- this.index = options.index || 'main';
156
- this.source = options.source || 'agent-shield';
157
- this.sourcetype = options.sourcetype || '_json';
158
- this.batchSize = options.batchSize || 50;
159
- this._buffer = [];
160
- this._flushInterval = setInterval(() => this.flush(), options.flushIntervalMs || 5000);
161
- this._stats = { sent: 0, errors: 0 };
162
-
163
- if (!this.url || !this.token) {
164
- console.warn('[Agent Shield] SplunkTransport: url and token are required.');
165
- }
166
- }
167
-
168
- async send(event) {
169
- this._buffer.push({
170
- time: event.timestamp ? event.timestamp / 1000 : Date.now() / 1000,
171
- source: this.source,
172
- sourcetype: this.sourcetype,
173
- index: this.index,
174
- event
175
- });
176
-
177
- if (this._buffer.length >= this.batchSize) {
178
- await this.flush();
179
- }
180
- }
181
-
182
- async flush() {
183
- if (this._buffer.length === 0 || !this.url || !this.token) return;
184
-
185
- const events = this._buffer.splice(0, this.batchSize);
186
- const payload = events.map(e => JSON.stringify(e)).join('\n');
187
-
188
- try {
189
- await this._post(this.url, payload, {
190
- 'Authorization': `Splunk ${this.token}`,
191
- 'Content-Type': 'application/json'
192
- });
193
- this._stats.sent += events.length;
194
- } catch (e) {
195
- this._stats.errors += events.length;
196
- console.warn('[Agent Shield] SplunkTransport error:', e.message);
197
- // Re-queue failed events
198
- this._buffer.unshift(...events);
199
- }
200
- }
201
-
202
- async close() {
203
- clearInterval(this._flushInterval);
204
- await this.flush();
205
- }
206
-
207
- getStats() { return { ...this._stats, buffered: this._buffer.length }; }
208
-
209
- /** @private */
210
- _post(url, body, headers) {
211
- return new Promise((resolve, reject) => {
212
- const parsed = new URL(url);
213
- const lib = parsed.protocol === 'https:' ? https : http;
214
- const req = lib.request({
215
- hostname: parsed.hostname, port: parsed.port,
216
- path: parsed.pathname, method: 'POST',
217
- headers: { ...headers, 'Content-Length': Buffer.byteLength(body) },
218
- rejectUnauthorized: false, timeout: 10000
219
- }, (res) => {
220
- let data = '';
221
- res.on('data', c => data += c);
222
- res.on('end', () => resolve(data));
223
- });
224
- req.on('error', reject);
225
- req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
226
- req.write(body);
227
- req.end();
228
- });
229
- }
230
- }
231
-
232
- // =========================================================================
233
- // ELASTICSEARCH TRANSPORT
234
- // =========================================================================
235
-
236
- /**
237
- * Sends audit events to Elasticsearch.
238
- */
239
- class ElasticsearchTransport extends AuditTransport {
240
- /**
241
- * @param {object} options
242
- * @param {string} options.url - Elasticsearch URL (e.g., http://localhost:9200).
243
- * @param {string} [options.index='agent-shield-audit'] - Index name.
244
- * @param {string} [options.apiKey] - API key for authentication.
245
- * @param {number} [options.batchSize=100] - Events per bulk request.
246
- * @param {number} [options.flushIntervalMs=5000]
247
- */
248
- constructor(options = {}) {
249
- super();
250
- this.url = options.url;
251
- this.index = options.index || 'agent-shield-audit';
252
- this.apiKey = options.apiKey || null;
253
- this.batchSize = options.batchSize || 100;
254
- this._buffer = [];
255
- this._flushInterval = setInterval(() => this.flush(), options.flushIntervalMs || 5000);
256
- this._stats = { sent: 0, errors: 0 };
257
-
258
- if (!this.url) {
259
- console.warn('[Agent Shield] ElasticsearchTransport: url is required.');
260
- }
261
- }
262
-
263
- async send(event) {
264
- this._buffer.push(event);
265
- if (this._buffer.length >= this.batchSize) {
266
- await this.flush();
267
- }
268
- }
269
-
270
- async flush() {
271
- if (this._buffer.length === 0 || !this.url) return;
272
-
273
- const events = this._buffer.splice(0, this.batchSize);
274
- const dateStr = new Date().toISOString().split('T')[0].replace(/-/g, '.');
275
- const indexName = `${this.index}-${dateStr}`;
276
-
277
- // Build NDJSON bulk payload
278
- const lines = [];
279
- for (const event of events) {
280
- lines.push(JSON.stringify({ index: { _index: indexName } }));
281
- lines.push(JSON.stringify({ ...event, '@timestamp': event.timestamp || Date.now() }));
282
- }
283
- const payload = lines.join('\n') + '\n';
284
-
285
- const headers = { 'Content-Type': 'application/x-ndjson' };
286
- if (this.apiKey) headers['Authorization'] = `ApiKey ${this.apiKey}`;
287
-
288
- try {
289
- await this._post(`${this.url}/_bulk`, payload, headers);
290
- this._stats.sent += events.length;
291
- } catch (e) {
292
- this._stats.errors += events.length;
293
- console.warn('[Agent Shield] ElasticsearchTransport error:', e.message);
294
- this._buffer.unshift(...events);
295
- }
296
- }
297
-
298
- async close() {
299
- clearInterval(this._flushInterval);
300
- await this.flush();
301
- }
302
-
303
- getStats() { return { ...this._stats, buffered: this._buffer.length }; }
304
-
305
- /** @private */
306
- _post(url, body, headers) {
307
- return new Promise((resolve, reject) => {
308
- const parsed = new URL(url);
309
- const lib = parsed.protocol === 'https:' ? https : http;
310
- const req = lib.request({
311
- hostname: parsed.hostname, port: parsed.port,
312
- path: parsed.pathname, method: 'POST',
313
- headers: { ...headers, 'Content-Length': Buffer.byteLength(body) },
314
- timeout: 10000
315
- }, (res) => {
316
- let data = '';
317
- res.on('data', c => data += c);
318
- res.on('end', () => resolve(data));
319
- });
320
- req.on('error', reject);
321
- req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
322
- req.write(body);
323
- req.end();
324
- });
325
- }
326
- }
327
-
328
- // =========================================================================
329
- // AUDIT STREAM MANAGER
330
- // =========================================================================
331
-
332
- /**
333
- * Manages multiple audit transports and routes events to all of them.
334
- */
335
- class AuditStreamManager {
336
- /**
337
- * @param {object} [options]
338
- * @param {Array<AuditTransport>} [options.transports] - Initial transports.
339
- * @param {boolean} [options.includeMetadata=true] - Add instance metadata to events.
340
- * @param {string} [options.environment='production'] - Environment label.
341
- */
342
- constructor(options = {}) {
343
- this._transports = options.transports || [];
344
- this.includeMetadata = options.includeMetadata !== false;
345
- this.environment = options.environment || 'production';
346
- this._eventCount = 0;
347
-
348
- console.log('[Agent Shield] AuditStreamManager initialized (%d transports)', this._transports.length);
349
- }
350
-
351
- /**
352
- * Add a transport.
353
- * @param {AuditTransport} transport
354
- */
355
- addTransport(transport) {
356
- this._transports.push(transport);
357
- }
358
-
359
- /**
360
- * Emit an audit event to all transports.
361
- * @param {string} type - Event type (e.g., 'scan', 'threat', 'block', 'config_change').
362
- * @param {object} data - Event data.
363
- * @returns {Promise<void>}
364
- */
365
- async emit(type, data = {}) {
366
- this._eventCount++;
367
-
368
- const event = {
369
- type,
370
- ...data,
371
- timestamp: Date.now(),
372
- eventId: `evt_${this._eventCount}_${Date.now()}`
373
- };
374
-
375
- if (this.includeMetadata) {
376
- event.environment = this.environment;
377
- event.agentShieldVersion = '2.1.0';
378
- }
379
-
380
- const promises = this._transports.map(t =>
381
- t.send(event).catch(e => console.warn('[Agent Shield] Transport error:', e.message))
382
- );
383
-
384
- await Promise.all(promises);
385
- }
386
-
387
- /**
388
- * Emit a scan event.
389
- * @param {object} scanResult - Result from scanText/AgentShield.
390
- * @param {object} [context] - Additional context.
391
- */
392
- async emitScan(scanResult, context = {}) {
393
- await this.emit('scan', {
394
- status: scanResult.status,
395
- threatCount: scanResult.threats ? scanResult.threats.length : 0,
396
- categories: scanResult.threats ? [...new Set(scanResult.threats.map(t => t.category))] : [],
397
- scanTimeMs: scanResult.stats ? scanResult.stats.scanTimeMs : 0,
398
- ...context
399
- });
400
- }
401
-
402
- /**
403
- * Emit a threat event.
404
- * @param {object} threat - Individual threat object.
405
- * @param {object} [context]
406
- */
407
- async emitThreat(threat, context = {}) {
408
- await this.emit('threat', {
409
- severity: threat.severity,
410
- category: threat.category,
411
- description: threat.description,
412
- confidence: threat.confidence,
413
- ...context
414
- });
415
- }
416
-
417
- /**
418
- * Emit a block event.
419
- * @param {string} reason
420
- * @param {object} [context]
421
- */
422
- async emitBlock(reason, context = {}) {
423
- await this.emit('block', { reason, ...context });
424
- }
425
-
426
- /**
427
- * Flush all transports.
428
- * @returns {Promise<void>}
429
- */
430
- async flush() {
431
- await Promise.all(this._transports.map(t => t.flush()));
432
- }
433
-
434
- /**
435
- * Close all transports.
436
- * @returns {Promise<void>}
437
- */
438
- async close() {
439
- await Promise.all(this._transports.map(t => t.close()));
440
- }
441
-
442
- /**
443
- * Get streaming statistics.
444
- * @returns {object}
445
- */
446
- getStats() {
447
- return {
448
- eventCount: this._eventCount,
449
- transports: this._transports.length,
450
- transportStats: this._transports.map((t, i) => ({
451
- index: i,
452
- type: t.constructor.name,
453
- stats: typeof t.getStats === 'function' ? t.getStats() : null
454
- }))
455
- };
456
- }
457
- }
458
-
459
- // =========================================================================
460
- // EXPORTS
461
- // =========================================================================
462
-
463
- module.exports = {
464
- AuditStreamManager,
465
- AuditTransport,
466
- FileTransport,
467
- SplunkTransport,
468
- ElasticsearchTransport
469
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Audit Log Streaming (v2.1)
5
+ *
6
+ * Stream security audit events to external logging/SIEM systems.
7
+ * Supports Splunk HEC, Elasticsearch, file-based logging, and custom transports.
8
+ *
9
+ * Zero dependencies — uses Node.js built-in http/https/fs modules.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const http = require('http');
15
+ const https = require('https');
16
+
17
+ // =========================================================================
18
+ // TRANSPORT INTERFACE
19
+ // =========================================================================
20
+
21
+ /**
22
+ * Base transport class. Extend for custom destinations.
23
+ */
24
+ class AuditTransport {
25
+ /**
26
+ * Send an event to the transport.
27
+ * @param {object} event - Audit event.
28
+ * @returns {Promise<void>}
29
+ */
30
+ async send(event) { throw new Error('Not implemented'); }
31
+
32
+ /**
33
+ * Send multiple events in a batch.
34
+ * @param {Array<object>} events
35
+ * @returns {Promise<void>}
36
+ */
37
+ async sendBatch(events) {
38
+ for (const event of events) {
39
+ await this.send(event);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Flush any buffered events.
45
+ * @returns {Promise<void>}
46
+ */
47
+ async flush() {}
48
+
49
+ /**
50
+ * Close the transport.
51
+ * @returns {Promise<void>}
52
+ */
53
+ async close() {}
54
+ }
55
+
56
+ // =========================================================================
57
+ // FILE TRANSPORT
58
+ // =========================================================================
59
+
60
+ /**
61
+ * Writes audit events to a local file (JSONL format).
62
+ */
63
+ class FileTransport extends AuditTransport {
64
+ /**
65
+ * @param {object} [options]
66
+ * @param {string} [options.filePath='./agent-shield-audit.log'] - Log file path.
67
+ * @param {number} [options.maxSizeMB=100] - Max file size before rotation.
68
+ * @param {number} [options.maxFiles=5] - Number of rotated files to keep.
69
+ */
70
+ constructor(options = {}) {
71
+ super();
72
+ this.filePath = options.filePath || './agent-shield-audit.log';
73
+ this.maxSizeBytes = (options.maxSizeMB || 100) * 1024 * 1024;
74
+ this.maxFiles = options.maxFiles || 5;
75
+ this._buffer = [];
76
+ this._bufferSize = 0;
77
+ this._flushInterval = setInterval(() => this.flush(), 5000);
78
+ if (this._flushInterval.unref) this._flushInterval.unref();
79
+ }
80
+
81
+ async send(event) {
82
+ const line = JSON.stringify(event) + '\n';
83
+ this._buffer.push(line);
84
+ this._bufferSize += line.length;
85
+
86
+ if (this._bufferSize >= 64 * 1024) {
87
+ await this.flush();
88
+ }
89
+ }
90
+
91
+ async flush() {
92
+ if (this._buffer.length === 0) return;
93
+
94
+ const data = this._buffer.join('');
95
+ this._buffer = [];
96
+ this._bufferSize = 0;
97
+
98
+ try {
99
+ // Check rotation
100
+ if (fs.existsSync(this.filePath)) {
101
+ const stat = fs.statSync(this.filePath);
102
+ if (stat.size + data.length > this.maxSizeBytes) {
103
+ this._rotate();
104
+ }
105
+ }
106
+
107
+ fs.appendFileSync(this.filePath, data);
108
+ } catch (e) {
109
+ console.warn('[Agent Shield] FileTransport write error:', e.message);
110
+ }
111
+ }
112
+
113
+ async close() {
114
+ clearInterval(this._flushInterval);
115
+ await this.flush();
116
+ }
117
+
118
+ /** @private */
119
+ _rotate() {
120
+ // Delete the oldest rotated file if it exists
121
+ const oldest = `${this.filePath}.${this.maxFiles}`;
122
+ if (fs.existsSync(oldest)) fs.unlinkSync(oldest);
123
+
124
+ for (let i = this.maxFiles - 1; i >= 1; i--) {
125
+ const from = `${this.filePath}.${i}`;
126
+ const to = `${this.filePath}.${i + 1}`;
127
+ if (fs.existsSync(from)) {
128
+ fs.renameSync(from, to);
129
+ }
130
+ }
131
+ if (fs.existsSync(this.filePath)) {
132
+ fs.renameSync(this.filePath, `${this.filePath}.1`);
133
+ }
134
+ }
135
+ }
136
+
137
+ // =========================================================================
138
+ // SPLUNK HEC TRANSPORT
139
+ // =========================================================================
140
+
141
+ /**
142
+ * Sends audit events to Splunk via HTTP Event Collector (HEC).
143
+ */
144
+ class SplunkTransport extends AuditTransport {
145
+ /**
146
+ * @param {object} options
147
+ * @param {string} options.url - Splunk HEC URL (e.g., https://splunk:8088/services/collector/event).
148
+ * @param {string} options.token - HEC token.
149
+ * @param {string} [options.index='main'] - Splunk index.
150
+ * @param {string} [options.source='agent-shield'] - Event source.
151
+ * @param {string} [options.sourcetype='_json'] - Source type.
152
+ * @param {number} [options.batchSize=50] - Events per batch.
153
+ * @param {number} [options.flushIntervalMs=5000] - Auto-flush interval.
154
+ */
155
+ constructor(options = {}) {
156
+ super();
157
+ this.url = options.url;
158
+ this.token = options.token;
159
+ this.index = options.index || 'main';
160
+ this.source = options.source || 'agent-shield';
161
+ this.sourcetype = options.sourcetype || '_json';
162
+ this.batchSize = options.batchSize || 50;
163
+ this._buffer = [];
164
+ this._flushInterval = setInterval(() => this.flush(), options.flushIntervalMs || 5000);
165
+ if (this._flushInterval.unref) this._flushInterval.unref();
166
+ this._stats = { sent: 0, errors: 0 };
167
+
168
+ if (!this.url || !this.token) {
169
+ console.warn('[Agent Shield] SplunkTransport: url and token are required.');
170
+ }
171
+ }
172
+
173
+ async send(event) {
174
+ this._buffer.push({
175
+ time: event.timestamp ? event.timestamp / 1000 : Date.now() / 1000,
176
+ source: this.source,
177
+ sourcetype: this.sourcetype,
178
+ index: this.index,
179
+ event
180
+ });
181
+
182
+ if (this._buffer.length >= this.batchSize) {
183
+ await this.flush();
184
+ }
185
+ }
186
+
187
+ async flush() {
188
+ if (this._buffer.length === 0 || !this.url || !this.token) return;
189
+
190
+ const events = this._buffer.splice(0, this.batchSize);
191
+ const payload = events.map(e => JSON.stringify(e)).join('\n');
192
+
193
+ try {
194
+ await this._post(this.url, payload, {
195
+ 'Authorization': `Splunk ${this.token}`,
196
+ 'Content-Type': 'application/json'
197
+ });
198
+ this._stats.sent += events.length;
199
+ } catch (e) {
200
+ this._stats.errors += events.length;
201
+ console.warn('[Agent Shield] SplunkTransport error:', e.message);
202
+ // Re-queue failed events (cap buffer to prevent unbounded growth)
203
+ this._buffer.unshift(...events);
204
+ if (this._buffer.length > this.batchSize * 20) {
205
+ const dropped = this._buffer.length - this.batchSize * 10;
206
+ this._buffer.splice(0, dropped);
207
+ console.warn('[Agent Shield] SplunkTransport buffer overflow, dropped %d events', dropped);
208
+ }
209
+ }
210
+ }
211
+
212
+ async close() {
213
+ clearInterval(this._flushInterval);
214
+ await this.flush();
215
+ }
216
+
217
+ getStats() { return { ...this._stats, buffered: this._buffer.length }; }
218
+
219
+ /** @private */
220
+ _post(url, body, headers) {
221
+ return new Promise((resolve, reject) => {
222
+ const parsed = new URL(url);
223
+ const lib = parsed.protocol === 'https:' ? https : http;
224
+ const req = lib.request({
225
+ hostname: parsed.hostname, port: parsed.port,
226
+ path: parsed.pathname, method: 'POST',
227
+ headers: { ...headers, 'Content-Length': Buffer.byteLength(body) },
228
+ rejectUnauthorized: false, timeout: 10000
229
+ }, (res) => {
230
+ let data = '';
231
+ res.on('data', c => data += c);
232
+ res.on('end', () => resolve(data));
233
+ });
234
+ req.on('error', reject);
235
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
236
+ req.write(body);
237
+ req.end();
238
+ });
239
+ }
240
+ }
241
+
242
+ // =========================================================================
243
+ // ELASTICSEARCH TRANSPORT
244
+ // =========================================================================
245
+
246
+ /**
247
+ * Sends audit events to Elasticsearch.
248
+ */
249
+ class ElasticsearchTransport extends AuditTransport {
250
+ /**
251
+ * @param {object} options
252
+ * @param {string} options.url - Elasticsearch URL (e.g., http://localhost:9200).
253
+ * @param {string} [options.index='agent-shield-audit'] - Index name.
254
+ * @param {string} [options.apiKey] - API key for authentication.
255
+ * @param {number} [options.batchSize=100] - Events per bulk request.
256
+ * @param {number} [options.flushIntervalMs=5000]
257
+ */
258
+ constructor(options = {}) {
259
+ super();
260
+ this.url = options.url;
261
+ this.index = options.index || 'agent-shield-audit';
262
+ this.apiKey = options.apiKey || null;
263
+ this.batchSize = options.batchSize || 100;
264
+ this._buffer = [];
265
+ this._flushInterval = setInterval(() => this.flush(), options.flushIntervalMs || 5000);
266
+ if (this._flushInterval.unref) this._flushInterval.unref();
267
+ this._stats = { sent: 0, errors: 0 };
268
+
269
+ if (!this.url) {
270
+ console.warn('[Agent Shield] ElasticsearchTransport: url is required.');
271
+ }
272
+ }
273
+
274
+ async send(event) {
275
+ this._buffer.push(event);
276
+ if (this._buffer.length >= this.batchSize) {
277
+ await this.flush();
278
+ }
279
+ }
280
+
281
+ async flush() {
282
+ if (this._buffer.length === 0 || !this.url) return;
283
+
284
+ const events = this._buffer.splice(0, this.batchSize);
285
+ const dateStr = new Date().toISOString().split('T')[0].replace(/-/g, '.');
286
+ const indexName = `${this.index}-${dateStr}`;
287
+
288
+ // Build NDJSON bulk payload
289
+ const lines = [];
290
+ for (const event of events) {
291
+ lines.push(JSON.stringify({ index: { _index: indexName } }));
292
+ lines.push(JSON.stringify({ ...event, '@timestamp': event.timestamp || Date.now() }));
293
+ }
294
+ const payload = lines.join('\n') + '\n';
295
+
296
+ const headers = { 'Content-Type': 'application/x-ndjson' };
297
+ if (this.apiKey) headers['Authorization'] = `ApiKey ${this.apiKey}`;
298
+
299
+ try {
300
+ await this._post(`${this.url}/_bulk`, payload, headers);
301
+ this._stats.sent += events.length;
302
+ } catch (e) {
303
+ this._stats.errors += events.length;
304
+ console.warn('[Agent Shield] ElasticsearchTransport error:', e.message);
305
+ // Re-queue failed events (cap buffer to prevent unbounded growth)
306
+ this._buffer.unshift(...events);
307
+ if (this._buffer.length > this.batchSize * 20) {
308
+ const dropped = this._buffer.length - this.batchSize * 10;
309
+ this._buffer.splice(0, dropped);
310
+ console.warn('[Agent Shield] ElasticsearchTransport buffer overflow, dropped %d events', dropped);
311
+ }
312
+ }
313
+ }
314
+
315
+ async close() {
316
+ clearInterval(this._flushInterval);
317
+ await this.flush();
318
+ }
319
+
320
+ getStats() { return { ...this._stats, buffered: this._buffer.length }; }
321
+
322
+ /** @private */
323
+ _post(url, body, headers) {
324
+ return new Promise((resolve, reject) => {
325
+ const parsed = new URL(url);
326
+ const lib = parsed.protocol === 'https:' ? https : http;
327
+ const req = lib.request({
328
+ hostname: parsed.hostname, port: parsed.port,
329
+ path: parsed.pathname, method: 'POST',
330
+ headers: { ...headers, 'Content-Length': Buffer.byteLength(body) },
331
+ timeout: 10000
332
+ }, (res) => {
333
+ let data = '';
334
+ res.on('data', c => data += c);
335
+ res.on('end', () => resolve(data));
336
+ });
337
+ req.on('error', reject);
338
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
339
+ req.write(body);
340
+ req.end();
341
+ });
342
+ }
343
+ }
344
+
345
+ // =========================================================================
346
+ // AUDIT STREAM MANAGER
347
+ // =========================================================================
348
+
349
+ /**
350
+ * Manages multiple audit transports and routes events to all of them.
351
+ */
352
+ class AuditStreamManager {
353
+ /**
354
+ * @param {object} [options]
355
+ * @param {Array<AuditTransport>} [options.transports] - Initial transports.
356
+ * @param {boolean} [options.includeMetadata=true] - Add instance metadata to events.
357
+ * @param {string} [options.environment='production'] - Environment label.
358
+ */
359
+ constructor(options = {}) {
360
+ this._transports = options.transports || [];
361
+ this.includeMetadata = options.includeMetadata !== false;
362
+ this.environment = options.environment || 'production';
363
+ this._eventCount = 0;
364
+
365
+ console.log('[Agent Shield] AuditStreamManager initialized (%d transports)', this._transports.length);
366
+ }
367
+
368
+ /**
369
+ * Add a transport.
370
+ * @param {AuditTransport} transport
371
+ */
372
+ addTransport(transport) {
373
+ this._transports.push(transport);
374
+ }
375
+
376
+ /**
377
+ * Emit an audit event to all transports.
378
+ * @param {string} type - Event type (e.g., 'scan', 'threat', 'block', 'config_change').
379
+ * @param {object} data - Event data.
380
+ * @returns {Promise<void>}
381
+ */
382
+ async emit(type, data = {}) {
383
+ this._eventCount++;
384
+
385
+ const event = {
386
+ type,
387
+ ...data,
388
+ timestamp: Date.now(),
389
+ eventId: `evt_${this._eventCount}_${Date.now()}`
390
+ };
391
+
392
+ if (this.includeMetadata) {
393
+ event.environment = this.environment;
394
+ event.agentShieldVersion = '2.1.0';
395
+ }
396
+
397
+ const promises = this._transports.map(t =>
398
+ t.send(event).catch(e => console.warn('[Agent Shield] Transport error:', e.message))
399
+ );
400
+
401
+ await Promise.all(promises);
402
+ }
403
+
404
+ /**
405
+ * Emit a scan event.
406
+ * @param {object} scanResult - Result from scanText/AgentShield.
407
+ * @param {object} [context] - Additional context.
408
+ */
409
+ async emitScan(scanResult, context = {}) {
410
+ await this.emit('scan', {
411
+ status: scanResult.status,
412
+ threatCount: scanResult.threats ? scanResult.threats.length : 0,
413
+ categories: scanResult.threats ? [...new Set(scanResult.threats.map(t => t.category))] : [],
414
+ scanTimeMs: scanResult.stats ? scanResult.stats.scanTimeMs : 0,
415
+ ...context
416
+ });
417
+ }
418
+
419
+ /**
420
+ * Emit a threat event.
421
+ * @param {object} threat - Individual threat object.
422
+ * @param {object} [context]
423
+ */
424
+ async emitThreat(threat, context = {}) {
425
+ await this.emit('threat', {
426
+ severity: threat.severity,
427
+ category: threat.category,
428
+ description: threat.description,
429
+ confidence: threat.confidence,
430
+ ...context
431
+ });
432
+ }
433
+
434
+ /**
435
+ * Emit a block event.
436
+ * @param {string} reason
437
+ * @param {object} [context]
438
+ */
439
+ async emitBlock(reason, context = {}) {
440
+ await this.emit('block', { reason, ...context });
441
+ }
442
+
443
+ /**
444
+ * Flush all transports.
445
+ * @returns {Promise<void>}
446
+ */
447
+ async flush() {
448
+ await Promise.all(this._transports.map(t => t.flush()));
449
+ }
450
+
451
+ /**
452
+ * Close all transports.
453
+ * @returns {Promise<void>}
454
+ */
455
+ async close() {
456
+ await Promise.all(this._transports.map(t => t.close()));
457
+ }
458
+
459
+ /**
460
+ * Get streaming statistics.
461
+ * @returns {object}
462
+ */
463
+ getStats() {
464
+ return {
465
+ eventCount: this._eventCount,
466
+ transports: this._transports.length,
467
+ transportStats: this._transports.map((t, i) => ({
468
+ index: i,
469
+ type: t.constructor.name,
470
+ stats: typeof t.getStats === 'function' ? t.getStats() : null
471
+ }))
472
+ };
473
+ }
474
+ }
475
+
476
+ // =========================================================================
477
+ // EXPORTS
478
+ // =========================================================================
479
+
480
+ module.exports = {
481
+ AuditStreamManager,
482
+ AuditTransport,
483
+ FileTransport,
484
+ SplunkTransport,
485
+ ElasticsearchTransport
486
+ };