agentshield-sdk 7.0.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 (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. package/types/index.d.ts +2088 -0
@@ -0,0 +1,1017 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Privacy-Preserving Threat Intelligence Network
5
+ *
6
+ * Anonymous pattern sharing network that allows Agent Shield nodes to
7
+ * collaboratively improve detection without exposing raw user data.
8
+ *
9
+ * Key principles:
10
+ * - All sharing is opt-in
11
+ * - No raw user data is ever shared — only anonymized detection patterns
12
+ * - Differential privacy noise on all statistics (Laplace mechanism)
13
+ * - Pattern generalization strips org-specific details
14
+ * - Consensus mechanism prevents poisoning (need minConsensus votes)
15
+ * - Reputation system reduces weight of low-quality contributors
16
+ */
17
+
18
+ const crypto = require('crypto');
19
+
20
+ // =========================================================================
21
+ // DEFAULTS
22
+ // =========================================================================
23
+
24
+ /**
25
+ * Default configuration values for the threat intel network.
26
+ * @type {Object}
27
+ */
28
+ const NETWORK_DEFAULTS = {
29
+ networkName: 'agent-shield-global',
30
+ sharePatternsEnabled: true,
31
+ receiveEnabled: true,
32
+ minConsensus: 3,
33
+ anonymizationLevel: 'high',
34
+ syncIntervalMs: 300000,
35
+ maxPeers: 50,
36
+ heartbeatIntervalMs: 60000,
37
+ heartbeatTimeoutMs: 180000,
38
+ maxFeedSize: 10000,
39
+ pruneMaxAgeMs: 7 * 24 * 60 * 60 * 1000, // 7 days
40
+ reputationDecayRate: 0.01,
41
+ laplaceSensitivity: 1.0,
42
+ laplaceEpsilon: 1.0
43
+ };
44
+
45
+ // =========================================================================
46
+ // PATTERN ANONYMIZER
47
+ // =========================================================================
48
+
49
+ /**
50
+ * Privacy-preserving pattern transformation.
51
+ * Strips identifying information and generalizes patterns before sharing.
52
+ */
53
+ class PatternAnonymizer {
54
+ /**
55
+ * @param {'low'|'medium'|'high'} level - Anonymization level.
56
+ */
57
+ constructor(level = 'high') {
58
+ const validLevels = ['low', 'medium', 'high'];
59
+ if (!validLevels.includes(level)) {
60
+ throw new Error(`Invalid anonymization level: ${level}. Must be one of: ${validLevels.join(', ')}`);
61
+ }
62
+ this.level = level;
63
+ }
64
+
65
+ /**
66
+ * Anonymize a detection pattern for sharing.
67
+ * Strips identifying info and generalizes based on anonymization level.
68
+ *
69
+ * @param {Object} pattern - The detection pattern to anonymize.
70
+ * @param {string} pattern.regex - The regex pattern string.
71
+ * @param {string} [pattern.category] - Threat category.
72
+ * @param {string} [pattern.severity] - Severity level.
73
+ * @param {Object} [pattern.metadata] - Additional metadata.
74
+ * @param {Object} [pattern.stats] - Frequency statistics.
75
+ * @returns {Object} Anonymized pattern safe for sharing.
76
+ */
77
+ anonymize(pattern) {
78
+ if (!pattern || typeof pattern !== 'object') {
79
+ throw new Error('Pattern must be a non-null object');
80
+ }
81
+
82
+ const anonymized = {
83
+ id: this.hashPattern(pattern),
84
+ category: pattern.category || 'unknown',
85
+ severity: pattern.severity || 'medium',
86
+ anonymizedAt: new Date().toISOString()
87
+ };
88
+
89
+ // Generalize regex based on level
90
+ if (pattern.regex) {
91
+ anonymized.regex = this.generalize(pattern.regex);
92
+ }
93
+
94
+ // Add noise to stats if present
95
+ if (pattern.stats && typeof pattern.stats === 'object') {
96
+ anonymized.stats = this.addNoise(pattern.stats);
97
+ }
98
+
99
+ // Strip metadata based on level
100
+ if (pattern.metadata) {
101
+ anonymized.metadata = this.stripMetadata(pattern.metadata);
102
+ }
103
+
104
+ return anonymized;
105
+ }
106
+
107
+ /**
108
+ * Create a privacy-preserving hash for deduplication.
109
+ * Uses SHA-256 with a salt to prevent rainbow table attacks.
110
+ *
111
+ * @param {Object} pattern - The pattern to hash.
112
+ * @returns {string} A hex hash string.
113
+ */
114
+ hashPattern(pattern) {
115
+ const canonical = JSON.stringify({
116
+ regex: pattern.regex || '',
117
+ category: pattern.category || ''
118
+ });
119
+ return crypto.createHash('sha256')
120
+ .update('agent-shield-pattern:' + canonical)
121
+ .digest('hex')
122
+ .substring(0, 16);
123
+ }
124
+
125
+ /**
126
+ * Generalize a regex pattern to remove org-specific details.
127
+ * Replaces specific strings, domains, IPs with generic character classes.
128
+ *
129
+ * @param {string} regex - The regex pattern string to generalize.
130
+ * @returns {string} A more general version of the regex.
131
+ */
132
+ generalize(regex) {
133
+ if (typeof regex !== 'string') return '';
134
+
135
+ let generalized = regex;
136
+
137
+ if (this.level === 'high') {
138
+ // Replace specific domain names with generic pattern
139
+ generalized = generalized.replace(
140
+ /[a-zA-Z0-9][-a-zA-Z0-9]*\.(com|org|net|io|dev|ai)/g,
141
+ '\\w+\\.\\w+'
142
+ );
143
+ // Replace IP addresses with generic pattern
144
+ generalized = generalized.replace(
145
+ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g,
146
+ '\\d+\\.\\d+\\.\\d+\\.\\d+'
147
+ );
148
+ // Replace quoted strings (potential org-specific identifiers)
149
+ generalized = generalized.replace(
150
+ /["'][^"']{4,}["']/g,
151
+ '["\']\\w+["\']'
152
+ );
153
+ // Replace specific file paths
154
+ generalized = generalized.replace(
155
+ /\/[a-zA-Z0-9_/-]{3,}/g,
156
+ '/\\S+'
157
+ );
158
+ } else if (this.level === 'medium') {
159
+ // Replace IP addresses only
160
+ generalized = generalized.replace(
161
+ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g,
162
+ '\\d+\\.\\d+\\.\\d+\\.\\d+'
163
+ );
164
+ // Replace email-like patterns
165
+ generalized = generalized.replace(
166
+ /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+/g,
167
+ '\\S+@\\S+'
168
+ );
169
+ }
170
+ // 'low' level: minimal generalization, keep pattern mostly intact
171
+
172
+ return generalized;
173
+ }
174
+
175
+ /**
176
+ * Remove org-specific metadata fields.
177
+ *
178
+ * @param {Object} metadata - The metadata object to strip.
179
+ * @returns {Object} Stripped metadata with only safe fields.
180
+ */
181
+ stripMetadata(metadata) {
182
+ if (!metadata || typeof metadata !== 'object') return {};
183
+
184
+ // Safe fields that can be shared
185
+ const safeFields = ['category', 'severity', 'type', 'tags', 'version'];
186
+ const stripped = {};
187
+
188
+ for (const field of safeFields) {
189
+ if (metadata[field] !== undefined) {
190
+ stripped[field] = metadata[field];
191
+ }
192
+ }
193
+
194
+ return stripped;
195
+ }
196
+
197
+ /**
198
+ * Add differential privacy noise to frequency statistics using Laplace mechanism.
199
+ *
200
+ * @param {Object} stats - Statistics object with numeric values.
201
+ * @param {number} [sensitivity=1.0] - Query sensitivity.
202
+ * @param {number} [epsilon=1.0] - Privacy budget (lower = more noise).
203
+ * @returns {Object} Stats with Laplace noise added.
204
+ */
205
+ addNoise(stats, sensitivity, epsilon) {
206
+ if (!stats || typeof stats !== 'object') return {};
207
+
208
+ const s = sensitivity || NETWORK_DEFAULTS.laplaceSensitivity;
209
+ const e = epsilon || NETWORK_DEFAULTS.laplaceEpsilon;
210
+ const scale = s / e;
211
+
212
+ const noisy = {};
213
+ for (const [key, value] of Object.entries(stats)) {
214
+ if (typeof value === 'number') {
215
+ // Laplace noise: sample from Laplace(0, scale)
216
+ const noise = this._laplaceSample(scale);
217
+ noisy[key] = Math.max(0, Math.round(value + noise));
218
+ } else {
219
+ noisy[key] = value;
220
+ }
221
+ }
222
+
223
+ return noisy;
224
+ }
225
+
226
+ /**
227
+ * Sample from a Laplace distribution using inverse CDF.
228
+ * @param {number} scale - Scale parameter (b) of the Laplace distribution.
229
+ * @returns {number} A random sample.
230
+ * @private
231
+ */
232
+ _laplaceSample(scale) {
233
+ const u = Math.random() - 0.5;
234
+ return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
235
+ }
236
+ }
237
+
238
+ // =========================================================================
239
+ // PEER NODE
240
+ // =========================================================================
241
+
242
+ /**
243
+ * Represents a network peer in the threat intelligence network.
244
+ */
245
+ class PeerNode {
246
+ /**
247
+ * @param {string} nodeId - Unique identifier for this peer.
248
+ * @param {Object} [config] - Peer configuration.
249
+ * @param {number} [config.heartbeatIntervalMs] - Heartbeat interval.
250
+ * @param {number} [config.heartbeatTimeoutMs] - Heartbeat timeout.
251
+ */
252
+ constructor(nodeId, config = {}) {
253
+ if (!nodeId) {
254
+ throw new Error('nodeId is required');
255
+ }
256
+ this.id = nodeId;
257
+ this.lastSeen = Date.now();
258
+ this.reputation = 1.0;
259
+ this.sharedCount = 0;
260
+ this.falsePositiveCount = 0;
261
+ this.falsePositiveRate = 0;
262
+ this.connected = false;
263
+ this.messageQueue = [];
264
+ this.heartbeatIntervalMs = config.heartbeatIntervalMs || NETWORK_DEFAULTS.heartbeatIntervalMs;
265
+ this.heartbeatTimeoutMs = config.heartbeatTimeoutMs || NETWORK_DEFAULTS.heartbeatTimeoutMs;
266
+ }
267
+
268
+ /**
269
+ * Establish a peer connection (simulated locally).
270
+ *
271
+ * @param {Object} peerInfo - Information about the remote peer.
272
+ * @param {string} peerInfo.id - Peer identifier.
273
+ * @param {string} [peerInfo.address] - Peer address.
274
+ * @returns {boolean} True if connection was established.
275
+ */
276
+ connect(peerInfo) {
277
+ if (!peerInfo || !peerInfo.id) {
278
+ throw new Error('peerInfo with id is required');
279
+ }
280
+ this.connected = true;
281
+ this.lastSeen = Date.now();
282
+ console.log(`[Agent Shield] Peer ${this.id} connected to ${peerInfo.id}`);
283
+ return true;
284
+ }
285
+
286
+ /**
287
+ * Send a message to this peer (simulated locally).
288
+ *
289
+ * @param {Object} message - Message payload.
290
+ * @param {string} message.type - Message type (e.g., 'pattern', 'heartbeat', 'falsePositive').
291
+ * @param {*} message.data - Message data.
292
+ * @returns {boolean} True if message was queued.
293
+ */
294
+ send(message) {
295
+ if (!message || typeof message !== 'object') {
296
+ throw new Error('Message must be a non-null object');
297
+ }
298
+ this.messageQueue.push({
299
+ ...message,
300
+ timestamp: Date.now(),
301
+ from: this.id
302
+ });
303
+ return true;
304
+ }
305
+
306
+ /**
307
+ * Process an incoming message from this peer.
308
+ *
309
+ * @param {Object} message - Incoming message.
310
+ * @param {string} message.type - Message type.
311
+ * @param {*} message.data - Message data.
312
+ * @returns {Object} Processing result with {accepted, reason}.
313
+ */
314
+ receive(message) {
315
+ if (!message || typeof message !== 'object') {
316
+ return { accepted: false, reason: 'invalid message' };
317
+ }
318
+
319
+ this.lastSeen = Date.now();
320
+
321
+ if (message.type === 'heartbeat') {
322
+ return { accepted: true, reason: 'heartbeat acknowledged' };
323
+ }
324
+
325
+ if (message.type === 'pattern') {
326
+ this.sharedCount++;
327
+ this._updateFalsePositiveRate();
328
+ return { accepted: true, reason: 'pattern received' };
329
+ }
330
+
331
+ if (message.type === 'falsePositive') {
332
+ this.falsePositiveCount++;
333
+ this._updateFalsePositiveRate();
334
+ return { accepted: true, reason: 'false positive recorded' };
335
+ }
336
+
337
+ return { accepted: true, reason: 'message received' };
338
+ }
339
+
340
+ /**
341
+ * Compute the reputation score for this peer.
342
+ * Based on shared pattern count, false positive rate, and activity.
343
+ *
344
+ * @returns {number} Reputation score between 0 and 1.
345
+ */
346
+ getReputation() {
347
+ // Base reputation decays toward false positive penalty
348
+ const fpPenalty = this.falsePositiveRate * 2;
349
+ const activityBonus = Math.min(0.2, this.sharedCount * 0.01);
350
+ const freshness = this.isActive() ? 0.1 : -0.1;
351
+
352
+ this.reputation = Math.max(0, Math.min(1,
353
+ 1.0 - fpPenalty + activityBonus + freshness
354
+ ));
355
+
356
+ return this.reputation;
357
+ }
358
+
359
+ /**
360
+ * Check if this peer is still alive based on heartbeat timeout.
361
+ *
362
+ * @returns {boolean} True if peer was seen within the timeout window.
363
+ */
364
+ isActive() {
365
+ return (Date.now() - this.lastSeen) < this.heartbeatTimeoutMs;
366
+ }
367
+
368
+ /**
369
+ * Update the false positive rate.
370
+ * @private
371
+ */
372
+ _updateFalsePositiveRate() {
373
+ const total = this.sharedCount + this.falsePositiveCount;
374
+ this.falsePositiveRate = total > 0 ? this.falsePositiveCount / total : 0;
375
+ }
376
+ }
377
+
378
+ // =========================================================================
379
+ // CONSENSUS ENGINE
380
+ // =========================================================================
381
+
382
+ /**
383
+ * Validates shared patterns through a voting consensus mechanism.
384
+ * Prevents poisoning by requiring multiple independent confirmations.
385
+ */
386
+ class ConsensusEngine {
387
+ /**
388
+ * @param {number} [minConsensus=3] - Minimum votes required for consensus.
389
+ */
390
+ constructor(minConsensus) {
391
+ this.minConsensus = minConsensus || NETWORK_DEFAULTS.minConsensus;
392
+ this.entries = new Map(); // patternHash -> { votes: Set, falsePositives: Set, createdAt }
393
+ }
394
+
395
+ /**
396
+ * Record a vote for a pattern from a node.
397
+ *
398
+ * @param {string} patternHash - The hash of the pattern being voted on.
399
+ * @param {string} nodeId - The voting node's identifier.
400
+ * @returns {Object} Updated consensus info {votes, consensus, confidence}.
401
+ */
402
+ submit(patternHash, nodeId) {
403
+ if (!patternHash || !nodeId) {
404
+ throw new Error('patternHash and nodeId are required');
405
+ }
406
+
407
+ if (!this.entries.has(patternHash)) {
408
+ this.entries.set(patternHash, {
409
+ votes: new Set(),
410
+ falsePositives: new Set(),
411
+ createdAt: Date.now()
412
+ });
413
+ }
414
+
415
+ const entry = this.entries.get(patternHash);
416
+ entry.votes.add(nodeId);
417
+
418
+ return this.getConsensus(patternHash);
419
+ }
420
+
421
+ /**
422
+ * Get the current consensus status for a pattern.
423
+ *
424
+ * @param {string} patternHash - The pattern hash to check.
425
+ * @returns {Object} Consensus info: {votes, consensus, confidence}.
426
+ */
427
+ getConsensus(patternHash) {
428
+ const entry = this.entries.get(patternHash);
429
+ if (!entry) {
430
+ return { votes: 0, consensus: false, confidence: 0 };
431
+ }
432
+
433
+ const voteCount = entry.votes.size;
434
+ const fpCount = entry.falsePositives.size;
435
+ const netVotes = voteCount - fpCount;
436
+ const consensus = netVotes >= this.minConsensus;
437
+ // Confidence: ratio of net positive votes to minimum required, capped at 1
438
+ const confidence = Math.min(1, Math.max(0, netVotes / this.minConsensus));
439
+
440
+ return { votes: voteCount, consensus, confidence };
441
+ }
442
+
443
+ /**
444
+ * Record a false positive report for a pattern.
445
+ *
446
+ * @param {string} patternHash - The pattern hash being reported.
447
+ * @param {string} nodeId - The reporting node's identifier.
448
+ * @returns {Object} Updated consensus info.
449
+ */
450
+ reportFalsePositive(patternHash, nodeId) {
451
+ if (!patternHash || !nodeId) {
452
+ throw new Error('patternHash and nodeId are required');
453
+ }
454
+
455
+ if (!this.entries.has(patternHash)) {
456
+ this.entries.set(patternHash, {
457
+ votes: new Set(),
458
+ falsePositives: new Set(),
459
+ createdAt: Date.now()
460
+ });
461
+ }
462
+
463
+ const entry = this.entries.get(patternHash);
464
+ entry.falsePositives.add(nodeId);
465
+
466
+ return this.getConsensus(patternHash);
467
+ }
468
+
469
+ /**
470
+ * Get the quality score for a pattern.
471
+ * Computed as (votes - falsePositives) / totalReports.
472
+ *
473
+ * @param {string} patternHash - The pattern hash to score.
474
+ * @returns {number} Quality score between -1 and 1.
475
+ */
476
+ getQualityScore(patternHash) {
477
+ const entry = this.entries.get(patternHash);
478
+ if (!entry) return 0;
479
+
480
+ const totalReports = entry.votes.size + entry.falsePositives.size;
481
+ if (totalReports === 0) return 0;
482
+
483
+ return (entry.votes.size - entry.falsePositives.size) / totalReports;
484
+ }
485
+
486
+ /**
487
+ * Remove stale entries older than maxAge.
488
+ *
489
+ * @param {number} [maxAge] - Maximum age in milliseconds. Defaults to 7 days.
490
+ * @returns {number} Number of entries pruned.
491
+ */
492
+ prune(maxAge) {
493
+ const cutoff = Date.now() - (maxAge || NETWORK_DEFAULTS.pruneMaxAgeMs);
494
+ let pruned = 0;
495
+
496
+ for (const [hash, entry] of this.entries) {
497
+ if (entry.createdAt < cutoff) {
498
+ this.entries.delete(hash);
499
+ pruned++;
500
+ }
501
+ }
502
+
503
+ return pruned;
504
+ }
505
+ }
506
+
507
+ // =========================================================================
508
+ // THREAT FEED
509
+ // =========================================================================
510
+
511
+ /**
512
+ * Aggregated threat intelligence feed.
513
+ * Collects anonymized patterns with timestamps and supports querying.
514
+ */
515
+ class ThreatFeed {
516
+ constructor() {
517
+ this.patterns = [];
518
+ this.maxSize = NETWORK_DEFAULTS.maxFeedSize;
519
+ }
520
+
521
+ /**
522
+ * Add a pattern to the feed.
523
+ *
524
+ * @param {Object} pattern - The anonymized pattern to add.
525
+ * @param {string} [source='unknown'] - Source identifier.
526
+ * @returns {Object} The stored feed entry.
527
+ */
528
+ addPattern(pattern, source) {
529
+ if (!pattern || typeof pattern !== 'object') {
530
+ throw new Error('Pattern must be a non-null object');
531
+ }
532
+
533
+ const entry = {
534
+ ...pattern,
535
+ source: source || 'unknown',
536
+ addedAt: new Date().toISOString(),
537
+ addedTimestamp: Date.now()
538
+ };
539
+
540
+ this.patterns.push(entry);
541
+
542
+ // Evict oldest if over limit
543
+ if (this.patterns.length > this.maxSize) {
544
+ this.patterns = this.patterns.slice(-this.maxSize);
545
+ }
546
+
547
+ return entry;
548
+ }
549
+
550
+ /**
551
+ * Query the feed with filters.
552
+ *
553
+ * @param {Object} [filters] - Query filters.
554
+ * @param {string} [filters.category] - Filter by category.
555
+ * @param {string} [filters.severity] - Filter by severity.
556
+ * @param {string} [filters.since] - ISO date string; only patterns after this date.
557
+ * @param {string} [filters.until] - ISO date string; only patterns before this date.
558
+ * @param {number} [filters.limit] - Maximum results to return.
559
+ * @returns {Object[]} Matching feed entries.
560
+ */
561
+ query(filters = {}) {
562
+ let results = [...this.patterns];
563
+
564
+ if (filters.category) {
565
+ results = results.filter(p => p.category === filters.category);
566
+ }
567
+
568
+ if (filters.severity) {
569
+ results = results.filter(p => p.severity === filters.severity);
570
+ }
571
+
572
+ if (filters.since) {
573
+ const sinceTs = new Date(filters.since).getTime();
574
+ results = results.filter(p => p.addedTimestamp >= sinceTs);
575
+ }
576
+
577
+ if (filters.until) {
578
+ const untilTs = new Date(filters.until).getTime();
579
+ results = results.filter(p => p.addedTimestamp <= untilTs);
580
+ }
581
+
582
+ if (filters.limit && filters.limit > 0) {
583
+ results = results.slice(0, filters.limit);
584
+ }
585
+
586
+ return results;
587
+ }
588
+
589
+ /**
590
+ * Get the top N most reported patterns by frequency.
591
+ *
592
+ * @param {number} [n=10] - Number of top threats to return.
593
+ * @returns {Object[]} Top patterns sorted by report count.
594
+ */
595
+ getTopThreats(n = 10) {
596
+ // Count occurrences by pattern id
597
+ const counts = new Map();
598
+ for (const entry of this.patterns) {
599
+ const id = entry.id || 'unknown';
600
+ if (!counts.has(id)) {
601
+ counts.set(id, { pattern: entry, count: 0 });
602
+ }
603
+ counts.get(id).count++;
604
+ }
605
+
606
+ return Array.from(counts.values())
607
+ .sort((a, b) => b.count - a.count)
608
+ .slice(0, n)
609
+ .map(item => ({
610
+ ...item.pattern,
611
+ reportCount: item.count
612
+ }));
613
+ }
614
+
615
+ /**
616
+ * Get emerging pattern trends — patterns with increasing frequency over time.
617
+ *
618
+ * @returns {Object[]} Trending patterns with frequency delta.
619
+ */
620
+ getTrends() {
621
+ const now = Date.now();
622
+ const recentWindow = 24 * 60 * 60 * 1000; // 24 hours
623
+ const priorWindow = 7 * 24 * 60 * 60 * 1000; // 7 days
624
+
625
+ // Count patterns in recent vs prior windows
626
+ const recentCounts = new Map();
627
+ const priorCounts = new Map();
628
+
629
+ for (const entry of this.patterns) {
630
+ const id = entry.id || 'unknown';
631
+ const age = now - entry.addedTimestamp;
632
+
633
+ if (age <= recentWindow) {
634
+ recentCounts.set(id, (recentCounts.get(id) || 0) + 1);
635
+ } else if (age <= priorWindow) {
636
+ priorCounts.set(id, (priorCounts.get(id) || 0) + 1);
637
+ }
638
+ }
639
+
640
+ // Calculate trends: patterns appearing more in recent window
641
+ const trends = [];
642
+ const allIds = new Set([...recentCounts.keys(), ...priorCounts.keys()]);
643
+
644
+ for (const id of allIds) {
645
+ const recent = recentCounts.get(id) || 0;
646
+ const prior = priorCounts.get(id) || 0;
647
+ // Normalize prior to daily rate for fair comparison
648
+ const priorDaily = prior / 6; // 6 remaining days (7 day window minus 1 day recent)
649
+ const delta = recent - (priorDaily || 0);
650
+
651
+ if (delta > 0) {
652
+ // Find the pattern entry for metadata
653
+ const entry = this.patterns.find(p => (p.id || 'unknown') === id);
654
+ trends.push({
655
+ id,
656
+ category: entry ? entry.category : 'unknown',
657
+ severity: entry ? entry.severity : 'unknown',
658
+ recentCount: recent,
659
+ priorDailyAvg: Math.round(priorDaily * 100) / 100,
660
+ delta: Math.round(delta * 100) / 100,
661
+ trending: 'up'
662
+ });
663
+ }
664
+ }
665
+
666
+ return trends.sort((a, b) => b.delta - a.delta);
667
+ }
668
+
669
+ /**
670
+ * Export the feed in JSON or STIX-like format.
671
+ *
672
+ * @param {'json'|'stix'} [format='json'] - Export format.
673
+ * @returns {Object|string} Exported feed data.
674
+ */
675
+ export(format = 'json') {
676
+ if (format === 'stix') {
677
+ return {
678
+ type: 'bundle',
679
+ id: `bundle--${crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex')}`,
680
+ spec_version: '2.1',
681
+ created: new Date().toISOString(),
682
+ objects: this.patterns.map(entry => ({
683
+ type: 'indicator',
684
+ id: `indicator--${entry.id || crypto.randomBytes(8).toString('hex')}`,
685
+ created: entry.addedAt,
686
+ modified: entry.addedAt,
687
+ name: `${entry.category || 'unknown'} detection pattern`,
688
+ description: `Anonymized threat pattern — severity: ${entry.severity || 'unknown'}`,
689
+ pattern_type: 'agent-shield',
690
+ pattern: entry.regex || '',
691
+ indicator_types: [entry.category || 'unknown'],
692
+ valid_from: entry.addedAt,
693
+ labels: [entry.severity || 'medium', entry.category || 'unknown'],
694
+ confidence: entry.stats ? (entry.stats.confidence || 50) : 50
695
+ }))
696
+ };
697
+ }
698
+
699
+ // Default JSON export
700
+ return {
701
+ format: 'agent-shield-feed',
702
+ version: '1.0',
703
+ exported: new Date().toISOString(),
704
+ count: this.patterns.length,
705
+ patterns: this.patterns
706
+ };
707
+ }
708
+
709
+ /**
710
+ * Get feed statistics.
711
+ *
712
+ * @returns {Object} Feed stats including counts, categories, and severity breakdown.
713
+ */
714
+ getStats() {
715
+ const categories = {};
716
+ const severities = {};
717
+
718
+ for (const entry of this.patterns) {
719
+ const cat = entry.category || 'unknown';
720
+ const sev = entry.severity || 'unknown';
721
+ categories[cat] = (categories[cat] || 0) + 1;
722
+ severities[sev] = (severities[sev] || 0) + 1;
723
+ }
724
+
725
+ return {
726
+ totalPatterns: this.patterns.length,
727
+ categories,
728
+ severities,
729
+ oldestEntry: this.patterns.length > 0 ? this.patterns[0].addedAt : null,
730
+ newestEntry: this.patterns.length > 0 ? this.patterns[this.patterns.length - 1].addedAt : null
731
+ };
732
+ }
733
+ }
734
+
735
+ // =========================================================================
736
+ // THREAT INTEL NETWORK
737
+ // =========================================================================
738
+
739
+ /**
740
+ * Main network coordinator for privacy-preserving threat intelligence sharing.
741
+ * Manages peers, pattern anonymization, consensus, and the local threat feed.
742
+ */
743
+ class ThreatIntelNetwork {
744
+ /**
745
+ * @param {Object} [config] - Network configuration.
746
+ * @param {string} [config.nodeId] - This node's unique identifier (auto-generated if omitted).
747
+ * @param {string} [config.networkName='agent-shield-global'] - Network name.
748
+ * @param {boolean} [config.sharePatternsEnabled=true] - Whether to share patterns.
749
+ * @param {boolean} [config.receiveEnabled=true] - Whether to receive patterns.
750
+ * @param {number} [config.minConsensus=3] - Minimum votes for consensus.
751
+ * @param {'low'|'medium'|'high'} [config.anonymizationLevel='high'] - Anonymization level.
752
+ * @param {number} [config.syncIntervalMs=300000] - Sync interval in milliseconds.
753
+ */
754
+ constructor(config = {}) {
755
+ this.nodeId = config.nodeId || crypto.randomBytes(16).toString('hex');
756
+ this.networkName = config.networkName || NETWORK_DEFAULTS.networkName;
757
+ this.sharePatternsEnabled = config.sharePatternsEnabled !== undefined
758
+ ? config.sharePatternsEnabled
759
+ : NETWORK_DEFAULTS.sharePatternsEnabled;
760
+ this.receiveEnabled = config.receiveEnabled !== undefined
761
+ ? config.receiveEnabled
762
+ : NETWORK_DEFAULTS.receiveEnabled;
763
+ this.syncIntervalMs = config.syncIntervalMs || NETWORK_DEFAULTS.syncIntervalMs;
764
+
765
+ this.anonymizer = new PatternAnonymizer(
766
+ config.anonymizationLevel || NETWORK_DEFAULTS.anonymizationLevel
767
+ );
768
+ this.consensus = new ConsensusEngine(
769
+ config.minConsensus || NETWORK_DEFAULTS.minConsensus
770
+ );
771
+ this.feed = new ThreatFeed();
772
+
773
+ this.peers = new Map(); // peerId -> PeerNode
774
+ this.sharedPatterns = new Map(); // patternHash -> anonymized pattern
775
+ this.receivedPatterns = new Map(); // patternHash -> pattern
776
+ this.running = false;
777
+ this._syncTimer = null;
778
+ }
779
+
780
+ /**
781
+ * Initialize the node and connect to the network.
782
+ *
783
+ * @returns {Object} Startup info: {nodeId, networkName, status}.
784
+ */
785
+ start() {
786
+ if (this.running) {
787
+ return { nodeId: this.nodeId, networkName: this.networkName, status: 'already running' };
788
+ }
789
+
790
+ this.running = true;
791
+
792
+ // Set up periodic sync (simulated)
793
+ this._syncTimer = setInterval(() => {
794
+ this._sync();
795
+ }, this.syncIntervalMs);
796
+
797
+ // Prevent timer from keeping process alive
798
+ if (this._syncTimer && typeof this._syncTimer.unref === 'function') {
799
+ this._syncTimer.unref();
800
+ }
801
+
802
+ console.log(`[Agent Shield] Threat Intel Network started — node: ${this.nodeId}, network: ${this.networkName}`);
803
+
804
+ return {
805
+ nodeId: this.nodeId,
806
+ networkName: this.networkName,
807
+ status: 'running'
808
+ };
809
+ }
810
+
811
+ /**
812
+ * Graceful shutdown of the network node.
813
+ *
814
+ * @returns {Object} Shutdown info.
815
+ */
816
+ stop() {
817
+ if (this._syncTimer) {
818
+ clearInterval(this._syncTimer);
819
+ this._syncTimer = null;
820
+ }
821
+
822
+ this.running = false;
823
+
824
+ // Disconnect all peers
825
+ for (const peer of this.peers.values()) {
826
+ peer.connected = false;
827
+ }
828
+
829
+ console.log(`[Agent Shield] Threat Intel Network stopped — node: ${this.nodeId}`);
830
+
831
+ return {
832
+ nodeId: this.nodeId,
833
+ status: 'stopped',
834
+ sharedPatterns: this.sharedPatterns.size,
835
+ receivedPatterns: this.receivedPatterns.size
836
+ };
837
+ }
838
+
839
+ /**
840
+ * Anonymize and share a detection pattern with the network.
841
+ * Only shares if sharePatternsEnabled is true.
842
+ *
843
+ * @param {Object} pattern - The detection pattern to share.
844
+ * @param {string} pattern.regex - The regex pattern string.
845
+ * @param {string} [pattern.category] - Threat category.
846
+ * @param {string} [pattern.severity] - Severity level.
847
+ * @returns {Object} Result: {shared, patternHash, anonymizedPattern}.
848
+ */
849
+ sharePattern(pattern) {
850
+ if (!this.sharePatternsEnabled) {
851
+ return { shared: false, reason: 'pattern sharing is disabled' };
852
+ }
853
+
854
+ if (!this.running) {
855
+ return { shared: false, reason: 'network not running' };
856
+ }
857
+
858
+ // Anonymize the pattern
859
+ const anonymized = this.anonymizer.anonymize(pattern);
860
+ const hash = anonymized.id;
861
+
862
+ // Store locally
863
+ this.sharedPatterns.set(hash, anonymized);
864
+
865
+ // Submit to consensus
866
+ this.consensus.submit(hash, this.nodeId);
867
+
868
+ // Add to local feed
869
+ this.feed.addPattern(anonymized, this.nodeId);
870
+
871
+ // Broadcast to connected peers (simulated)
872
+ for (const peer of this.peers.values()) {
873
+ if (peer.connected && peer.isActive()) {
874
+ peer.send({ type: 'pattern', data: anonymized });
875
+ }
876
+ }
877
+
878
+ console.log(`[Agent Shield] Shared anonymized pattern: ${hash}`);
879
+
880
+ return {
881
+ shared: true,
882
+ patternHash: hash,
883
+ anonymizedPattern: anonymized
884
+ };
885
+ }
886
+
887
+ /**
888
+ * Pull new patterns from the network feed.
889
+ * Only receives if receiveEnabled is true.
890
+ *
891
+ * @returns {Object[]} Newly received patterns that have reached consensus.
892
+ */
893
+ receivePatterns() {
894
+ if (!this.receiveEnabled) {
895
+ return [];
896
+ }
897
+
898
+ if (!this.running) {
899
+ return [];
900
+ }
901
+
902
+ // Collect patterns from peer message queues
903
+ const newPatterns = [];
904
+
905
+ for (const peer of this.peers.values()) {
906
+ if (!peer.connected) continue;
907
+
908
+ while (peer.messageQueue.length > 0) {
909
+ const msg = peer.messageQueue.shift();
910
+ if (msg.type === 'pattern' && msg.data) {
911
+ const hash = msg.data.id;
912
+ if (hash && !this.receivedPatterns.has(hash)) {
913
+ // Submit to consensus from the sending peer
914
+ this.consensus.submit(hash, msg.from || peer.id);
915
+
916
+ // Check if pattern has reached consensus
917
+ const consensusResult = this.consensus.getConsensus(hash);
918
+ if (consensusResult.consensus) {
919
+ this.receivedPatterns.set(hash, msg.data);
920
+ this.feed.addPattern(msg.data, peer.id);
921
+ newPatterns.push(msg.data);
922
+ }
923
+ }
924
+ }
925
+ }
926
+ }
927
+
928
+ return newPatterns;
929
+ }
930
+
931
+ /**
932
+ * Get network statistics.
933
+ *
934
+ * @returns {Object} Stats: {connectedPeers, sharedPatterns, receivedPatterns, consensusScore}.
935
+ */
936
+ getNetworkStats() {
937
+ const activePeers = Array.from(this.peers.values()).filter(p => p.connected && p.isActive());
938
+
939
+ // Average consensus score across all shared patterns
940
+ let totalScore = 0;
941
+ let scoreCount = 0;
942
+ for (const hash of this.sharedPatterns.keys()) {
943
+ totalScore += this.consensus.getQualityScore(hash);
944
+ scoreCount++;
945
+ }
946
+
947
+ return {
948
+ connectedPeers: activePeers.length,
949
+ sharedPatterns: this.sharedPatterns.size,
950
+ receivedPatterns: this.receivedPatterns.size,
951
+ consensusScore: scoreCount > 0 ? Math.round((totalScore / scoreCount) * 100) / 100 : 0
952
+ };
953
+ }
954
+
955
+ /**
956
+ * Get the local ThreatFeed instance.
957
+ *
958
+ * @returns {ThreatFeed} The threat feed.
959
+ */
960
+ getThreatFeed() {
961
+ return this.feed;
962
+ }
963
+
964
+ /**
965
+ * Report a false positive to reduce consensus for a pattern.
966
+ *
967
+ * @param {string} patternId - The pattern hash to report.
968
+ * @returns {Object} Updated consensus info.
969
+ */
970
+ submitFalsePositive(patternId) {
971
+ if (!patternId) {
972
+ throw new Error('patternId is required');
973
+ }
974
+
975
+ const result = this.consensus.reportFalsePositive(patternId, this.nodeId);
976
+
977
+ // Broadcast to peers
978
+ for (const peer of this.peers.values()) {
979
+ if (peer.connected && peer.isActive()) {
980
+ peer.send({ type: 'falsePositive', data: { patternId } });
981
+ }
982
+ }
983
+
984
+ console.log(`[Agent Shield] Reported false positive for pattern: ${patternId}`);
985
+
986
+ return result;
987
+ }
988
+
989
+ /**
990
+ * Internal sync loop (simulated).
991
+ * @private
992
+ */
993
+ _sync() {
994
+ // Prune stale consensus entries
995
+ this.consensus.prune();
996
+
997
+ // Check peer health
998
+ for (const [id, peer] of this.peers) {
999
+ if (!peer.isActive()) {
1000
+ peer.connected = false;
1001
+ }
1002
+ }
1003
+ }
1004
+ }
1005
+
1006
+ // =========================================================================
1007
+ // EXPORTS
1008
+ // =========================================================================
1009
+
1010
+ module.exports = {
1011
+ ThreatIntelNetwork,
1012
+ PeerNode,
1013
+ PatternAnonymizer,
1014
+ ConsensusEngine,
1015
+ ThreatFeed,
1016
+ NETWORK_DEFAULTS
1017
+ };