agentshield-sdk 13.3.0 → 14.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.
- package/CHANGELOG.md +161 -0
- package/README.md +13 -2
- package/package.json +2 -2
- package/src/audit-immutable.js +59 -1
- package/src/audit.js +1 -1
- package/src/cross-turn.js +25 -1
- package/src/detector-core.js +333 -51
- package/src/document-scanner.js +20 -0
- package/src/enterprise.js +127 -12
- package/src/integrations-frameworks.js +373 -0
- package/src/integrations.js +207 -0
- package/src/main.js +10 -14
- package/src/memory-guard.js +60 -0
- package/src/middleware.js +107 -2
- package/src/native-scanner.js +104 -0
- package/src/plugin-system.js +422 -6
- package/src/supply-chain-scanner.js +112 -2
- package/src/sybil-detector.js +3 -6
- package/src/persistent-learning.js +0 -161
- package/src/threat-intel-federation.js +0 -343
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Agent Shield — Federated Threat Intelligence Node (v12)
|
|
5
|
-
*
|
|
6
|
-
* A local threat intelligence node that can share and receive
|
|
7
|
-
* anonymized attack patterns with differential privacy.
|
|
8
|
-
*
|
|
9
|
-
* All processing runs locally — no data ever leaves your environment
|
|
10
|
-
* unless explicitly exported via exportPatterns().
|
|
11
|
-
*
|
|
12
|
-
* @module persistent-learning
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const crypto = require('crypto');
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Local threat intelligence node.
|
|
19
|
-
*/
|
|
20
|
-
class ThreatIntelNode {
|
|
21
|
-
/**
|
|
22
|
-
* @param {object} [options]
|
|
23
|
-
* @param {string} [options.nodeId] - Unique node identifier.
|
|
24
|
-
* @param {number} [options.noiseLevel=0.1] - Differential privacy noise level (0-1).
|
|
25
|
-
*/
|
|
26
|
-
constructor(options = {}) {
|
|
27
|
-
this.nodeId = options.nodeId || crypto.randomBytes(4).toString('hex');
|
|
28
|
-
this.noiseLevel = options.noiseLevel || 0.1;
|
|
29
|
-
|
|
30
|
-
/** @type {Map<string, { pattern: string, hash: string, count: number, confidence: number, firstSeen: number, lastSeen: number, category: string }>} */
|
|
31
|
-
this.patterns = new Map();
|
|
32
|
-
this.stats = { reported: 0, imported: 0, exported: 0 };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Report a locally observed attack pattern.
|
|
37
|
-
* @param {object} attack
|
|
38
|
-
* @param {string} attack.text - Attack text.
|
|
39
|
-
* @param {string} attack.category - Attack category.
|
|
40
|
-
* @param {string} [attack.severity] - Severity level.
|
|
41
|
-
* @returns {{ hash: string, isNew: boolean }}
|
|
42
|
-
*/
|
|
43
|
-
reportAttack(attack) {
|
|
44
|
-
const hash = crypto.createHash('sha256').update(attack.text || '').digest('hex').substring(0, 16);
|
|
45
|
-
const existing = this.patterns.get(hash);
|
|
46
|
-
|
|
47
|
-
if (existing) {
|
|
48
|
-
existing.count++;
|
|
49
|
-
existing.lastSeen = Date.now();
|
|
50
|
-
existing.confidence = Math.min(1, existing.confidence + 0.1);
|
|
51
|
-
this.stats.reported++;
|
|
52
|
-
return { hash, isNew: false };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
this.patterns.set(hash, {
|
|
56
|
-
pattern: (attack.text || '').substring(0, 200),
|
|
57
|
-
hash,
|
|
58
|
-
count: 1,
|
|
59
|
-
confidence: 0.5,
|
|
60
|
-
firstSeen: Date.now(),
|
|
61
|
-
lastSeen: Date.now(),
|
|
62
|
-
category: attack.category || 'unknown',
|
|
63
|
-
severity: attack.severity || 'medium'
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
this.stats.reported++;
|
|
67
|
-
return { hash, isNew: true };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Export anonymized patterns with differential privacy.
|
|
72
|
-
* @returns {Array<object>} Anonymized patterns.
|
|
73
|
-
*/
|
|
74
|
-
exportPatterns() {
|
|
75
|
-
const exported = [];
|
|
76
|
-
for (const [, p] of this.patterns) {
|
|
77
|
-
// Differential privacy: add noise to counts, truncate patterns
|
|
78
|
-
const noisyCount = Math.max(1, Math.round(p.count + (Math.random() - 0.5) * p.count * this.noiseLevel));
|
|
79
|
-
const noisyConfidence = Math.min(1, Math.max(0, p.confidence + (Math.random() - 0.5) * this.noiseLevel));
|
|
80
|
-
|
|
81
|
-
exported.push({
|
|
82
|
-
hash: p.hash,
|
|
83
|
-
category: p.category,
|
|
84
|
-
severity: p.severity,
|
|
85
|
-
count: noisyCount,
|
|
86
|
-
confidence: Math.round(noisyConfidence * 100) / 100,
|
|
87
|
-
// Do NOT export the actual pattern text — only hash + metadata
|
|
88
|
-
sourceNode: this.nodeId
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
this.stats.exported += exported.length;
|
|
92
|
-
return exported;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Import patterns from another node.
|
|
97
|
-
* @param {Array<object>} patterns - Patterns from exportPatterns().
|
|
98
|
-
* @returns {{ imported: number, merged: number, new: number }}
|
|
99
|
-
*/
|
|
100
|
-
importPatterns(patterns) {
|
|
101
|
-
let merged = 0;
|
|
102
|
-
let newPatterns = 0;
|
|
103
|
-
|
|
104
|
-
for (const p of (patterns || [])) {
|
|
105
|
-
if (!p.hash) continue;
|
|
106
|
-
const existing = this.patterns.get(p.hash);
|
|
107
|
-
|
|
108
|
-
if (existing) {
|
|
109
|
-
// Merge: average confidence, sum counts
|
|
110
|
-
existing.confidence = (existing.confidence + (p.confidence || 0.5)) / 2;
|
|
111
|
-
existing.count += p.count || 1;
|
|
112
|
-
existing.lastSeen = Date.now();
|
|
113
|
-
merged++;
|
|
114
|
-
} else {
|
|
115
|
-
this.patterns.set(p.hash, {
|
|
116
|
-
pattern: '[imported]',
|
|
117
|
-
hash: p.hash,
|
|
118
|
-
count: p.count || 1,
|
|
119
|
-
confidence: p.confidence || 0.5,
|
|
120
|
-
firstSeen: Date.now(),
|
|
121
|
-
lastSeen: Date.now(),
|
|
122
|
-
category: p.category || 'unknown',
|
|
123
|
-
severity: p.severity || 'medium'
|
|
124
|
-
});
|
|
125
|
-
newPatterns++;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
this.stats.imported += merged + newPatterns;
|
|
130
|
-
return { imported: merged + newPatterns, merged, new: newPatterns };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Get all known patterns.
|
|
135
|
-
* @returns {Array<object>}
|
|
136
|
-
*/
|
|
137
|
-
getKnownPatterns() {
|
|
138
|
-
return [...this.patterns.values()].sort((a, b) => b.confidence - a.confidence);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Check if a text matches any known pattern.
|
|
143
|
-
* @param {string} text
|
|
144
|
-
* @returns {{ matches: boolean, pattern: object|null }}
|
|
145
|
-
*/
|
|
146
|
-
checkAgainstKnown(text) {
|
|
147
|
-
const hash = crypto.createHash('sha256').update(text || '').digest('hex').substring(0, 16);
|
|
148
|
-
const match = this.patterns.get(hash);
|
|
149
|
-
return { matches: !!match, pattern: match || null };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Get stats.
|
|
154
|
-
* @returns {object}
|
|
155
|
-
*/
|
|
156
|
-
getStats() {
|
|
157
|
-
return { ...this.stats, totalPatterns: this.patterns.size, nodeId: this.nodeId };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
module.exports = { ThreatIntelNode };
|
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Agent Shield - Federated Threat Intelligence Network
|
|
5
|
-
*
|
|
6
|
-
* The CrowdStrike model for AI agents. Every deployment anonymously
|
|
7
|
-
* contributes attack patterns back to a shared threat database.
|
|
8
|
-
* More customers = better detection = more customers.
|
|
9
|
-
*
|
|
10
|
-
* All sharing uses differential privacy - no customer data is ever
|
|
11
|
-
* exposed. Only anonymized attack signatures are shared.
|
|
12
|
-
*
|
|
13
|
-
* @module threat-intel-federation
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const crypto = require('crypto');
|
|
17
|
-
const { EventEmitter } = require('events');
|
|
18
|
-
|
|
19
|
-
// =========================================================================
|
|
20
|
-
// ThreatIntelFederation - The network coordinator
|
|
21
|
-
// =========================================================================
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Coordinates threat intelligence sharing across multiple
|
|
25
|
-
* Agent Shield deployments with differential privacy.
|
|
26
|
-
*/
|
|
27
|
-
class ThreatIntelFederation extends EventEmitter {
|
|
28
|
-
/**
|
|
29
|
-
* @param {object} [options]
|
|
30
|
-
* @param {string} [options.nodeId] - This node's identity.
|
|
31
|
-
* @param {number} [options.minConfidence=0.7] - Min confidence to share a pattern.
|
|
32
|
-
* @param {number} [options.noiseLevel=0.1] - Differential privacy noise level.
|
|
33
|
-
* @param {number} [options.maxPatterns=5000] - Max patterns in the network database.
|
|
34
|
-
* @param {number} [options.consensusThreshold=3] - Reports needed before pattern is promoted.
|
|
35
|
-
*/
|
|
36
|
-
constructor(options = {}) {
|
|
37
|
-
super();
|
|
38
|
-
this.nodeId = options.nodeId || `node_${crypto.randomBytes(4).toString('hex')}`;
|
|
39
|
-
this.minConfidence = options.minConfidence || 0.7;
|
|
40
|
-
this.noiseLevel = options.noiseLevel || 0.1;
|
|
41
|
-
this.maxPatterns = options.maxPatterns || 5000;
|
|
42
|
-
this.consensusThreshold = options.consensusThreshold || 3;
|
|
43
|
-
|
|
44
|
-
this._peers = new Map(); // peerId -> { lastSeen, patternsShared }
|
|
45
|
-
this._patterns = new Map(); // signature -> ThreatPattern
|
|
46
|
-
this._candidates = new Map(); // signature -> { reports, firstSeen }
|
|
47
|
-
this._stats = {
|
|
48
|
-
patternsReceived: 0,
|
|
49
|
-
patternsShared: 0,
|
|
50
|
-
patternsPromoted: 0,
|
|
51
|
-
patternsRejected: 0,
|
|
52
|
-
peersConnected: 0,
|
|
53
|
-
attacksBlockedByNetwork: 0,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Register a peer node in the federation.
|
|
59
|
-
* @param {string} peerId
|
|
60
|
-
* @param {object} [metadata]
|
|
61
|
-
*/
|
|
62
|
-
addPeer(peerId, metadata = {}) {
|
|
63
|
-
this._peers.set(peerId, {
|
|
64
|
-
id: peerId,
|
|
65
|
-
lastSeen: Date.now(),
|
|
66
|
-
patternsShared: 0,
|
|
67
|
-
...metadata,
|
|
68
|
-
});
|
|
69
|
-
this._stats.peersConnected = this._peers.size;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Remove a peer from the federation.
|
|
74
|
-
* @param {string} peerId
|
|
75
|
-
*/
|
|
76
|
-
removePeer(peerId) {
|
|
77
|
-
this._peers.delete(peerId);
|
|
78
|
-
this._stats.peersConnected = this._peers.size;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Submit a detected attack to the federation.
|
|
83
|
-
* The attack text is anonymized into a signature before sharing.
|
|
84
|
-
*
|
|
85
|
-
* @param {object} report
|
|
86
|
-
* @param {string} report.text - The attack text (kept local, never shared).
|
|
87
|
-
* @param {string} report.category - Threat category.
|
|
88
|
-
* @param {string} report.severity - Threat severity.
|
|
89
|
-
* @param {number} [report.confidence] - Detection confidence 0-1.
|
|
90
|
-
* @returns {object} { signature, status: 'promoted'|'candidate'|'rejected' }
|
|
91
|
-
*/
|
|
92
|
-
submitThreat(report) {
|
|
93
|
-
if (!report.text || !report.category) return { signature: null, status: 'rejected' };
|
|
94
|
-
|
|
95
|
-
// Anonymize: hash the normalized text (never share raw text)
|
|
96
|
-
const normalized = report.text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
97
|
-
const signature = crypto.createHash('sha256').update(normalized).digest('hex').substring(0, 16);
|
|
98
|
-
|
|
99
|
-
// Add differential privacy noise to confidence
|
|
100
|
-
const rawConfidence = report.confidence || 0.8;
|
|
101
|
-
const noise = (Math.random() - 0.5) * 2 * this.noiseLevel;
|
|
102
|
-
const confidence = Math.max(0, Math.min(1, rawConfidence + noise));
|
|
103
|
-
|
|
104
|
-
if (confidence < this.minConfidence) {
|
|
105
|
-
this._stats.patternsRejected++;
|
|
106
|
-
return { signature, status: 'rejected', reason: 'Below confidence threshold' };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Check if this pattern is already promoted
|
|
110
|
-
if (this._patterns.has(signature)) {
|
|
111
|
-
const existing = this._patterns.get(signature);
|
|
112
|
-
existing.reportCount++;
|
|
113
|
-
existing.lastReported = Date.now();
|
|
114
|
-
existing.confidence = Math.min(1, existing.confidence + 0.05);
|
|
115
|
-
return { signature, status: 'already_known' };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Add to candidates
|
|
119
|
-
if (!this._candidates.has(signature)) {
|
|
120
|
-
this._candidates.set(signature, {
|
|
121
|
-
signature,
|
|
122
|
-
category: report.category,
|
|
123
|
-
severity: report.severity,
|
|
124
|
-
confidence,
|
|
125
|
-
reports: 1,
|
|
126
|
-
firstSeen: Date.now(),
|
|
127
|
-
reporters: new Set([this.nodeId]),
|
|
128
|
-
});
|
|
129
|
-
} else {
|
|
130
|
-
const candidate = this._candidates.get(signature);
|
|
131
|
-
candidate.reports++;
|
|
132
|
-
candidate.reporters.add(this.nodeId);
|
|
133
|
-
candidate.confidence = Math.max(candidate.confidence, confidence);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
this._stats.patternsReceived++;
|
|
137
|
-
|
|
138
|
-
// Check consensus
|
|
139
|
-
const candidate = this._candidates.get(signature);
|
|
140
|
-
if (candidate.reports >= this.consensusThreshold) {
|
|
141
|
-
return this._promotePattern(candidate);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return { signature, status: 'candidate', reportsNeeded: this.consensusThreshold - candidate.reports };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Check text against the federated pattern database.
|
|
149
|
-
* @param {string} text
|
|
150
|
-
* @returns {{ matches: Array, blocked: boolean }}
|
|
151
|
-
*/
|
|
152
|
-
check(text) {
|
|
153
|
-
const normalized = text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
154
|
-
const signature = crypto.createHash('sha256').update(normalized).digest('hex').substring(0, 16);
|
|
155
|
-
|
|
156
|
-
const matches = [];
|
|
157
|
-
|
|
158
|
-
// Direct signature match
|
|
159
|
-
if (this._patterns.has(signature)) {
|
|
160
|
-
const pattern = this._patterns.get(signature);
|
|
161
|
-
matches.push({
|
|
162
|
-
signature,
|
|
163
|
-
category: pattern.category,
|
|
164
|
-
severity: pattern.severity,
|
|
165
|
-
confidence: pattern.confidence,
|
|
166
|
-
source: 'federation_exact',
|
|
167
|
-
});
|
|
168
|
-
this._stats.attacksBlockedByNetwork++;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Partial keyword matching against promoted patterns
|
|
172
|
-
const words = normalized.split(' ').filter(w => w.length > 3);
|
|
173
|
-
for (const [sig, pattern] of this._patterns) {
|
|
174
|
-
if (sig === signature) continue; // Already matched
|
|
175
|
-
if (pattern.keywords && pattern.keywords.length > 0) {
|
|
176
|
-
const overlap = pattern.keywords.filter(k => words.includes(k)).length;
|
|
177
|
-
const similarity = pattern.keywords.length > 0 ? overlap / pattern.keywords.length : 0;
|
|
178
|
-
if (similarity >= 0.6) {
|
|
179
|
-
matches.push({
|
|
180
|
-
signature: sig,
|
|
181
|
-
category: pattern.category,
|
|
182
|
-
severity: pattern.severity,
|
|
183
|
-
confidence: pattern.confidence * similarity,
|
|
184
|
-
source: 'federation_similar',
|
|
185
|
-
});
|
|
186
|
-
this._stats.attacksBlockedByNetwork++;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
matches,
|
|
193
|
-
blocked: matches.length > 0,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Receive patterns from another federation node.
|
|
199
|
-
* @param {Array} patterns - Array of promoted patterns.
|
|
200
|
-
* @returns {number} Number of new patterns accepted.
|
|
201
|
-
*/
|
|
202
|
-
receivePatterns(patterns) {
|
|
203
|
-
let accepted = 0;
|
|
204
|
-
for (const p of patterns) {
|
|
205
|
-
if (!this._patterns.has(p.signature)) {
|
|
206
|
-
this._patterns.set(p.signature, {
|
|
207
|
-
...p,
|
|
208
|
-
receivedAt: Date.now(),
|
|
209
|
-
source: 'federation_peer',
|
|
210
|
-
});
|
|
211
|
-
accepted++;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
this._stats.patternsReceived += accepted;
|
|
215
|
-
return accepted;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Export promoted patterns for sharing with peers.
|
|
220
|
-
* @param {number} [limit=100]
|
|
221
|
-
* @returns {Array}
|
|
222
|
-
*/
|
|
223
|
-
exportPatterns(limit = 100) {
|
|
224
|
-
const patterns = [...this._patterns.values()]
|
|
225
|
-
.sort((a, b) => b.confidence - a.confidence)
|
|
226
|
-
.slice(0, limit)
|
|
227
|
-
.map(p => ({
|
|
228
|
-
signature: p.signature,
|
|
229
|
-
category: p.category,
|
|
230
|
-
severity: p.severity,
|
|
231
|
-
confidence: p.confidence,
|
|
232
|
-
keywords: p.keywords,
|
|
233
|
-
reportCount: p.reportCount,
|
|
234
|
-
firstSeen: p.firstSeen,
|
|
235
|
-
}));
|
|
236
|
-
this._stats.patternsShared += patterns.length;
|
|
237
|
-
return patterns;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Get network statistics.
|
|
242
|
-
* @returns {object}
|
|
243
|
-
*/
|
|
244
|
-
getStats() {
|
|
245
|
-
return {
|
|
246
|
-
...this._stats,
|
|
247
|
-
promotedPatterns: this._patterns.size,
|
|
248
|
-
candidatePatterns: this._candidates.size,
|
|
249
|
-
peers: [...this._peers.values()].map(p => ({ id: p.id, lastSeen: p.lastSeen })),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Get the health of the federation network.
|
|
255
|
-
* @returns {object}
|
|
256
|
-
*/
|
|
257
|
-
getHealth() {
|
|
258
|
-
const now = Date.now();
|
|
259
|
-
const activePeers = [...this._peers.values()].filter(p => now - p.lastSeen < 300000).length;
|
|
260
|
-
return {
|
|
261
|
-
healthy: activePeers > 0 || this._patterns.size > 0,
|
|
262
|
-
activePeers,
|
|
263
|
-
totalPeers: this._peers.size,
|
|
264
|
-
patternCoverage: this._patterns.size,
|
|
265
|
-
networkAge: this._patterns.size > 0
|
|
266
|
-
? now - Math.min(...[...this._patterns.values()].map(p => p.firstSeen))
|
|
267
|
-
: 0,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** @private */
|
|
272
|
-
_promotePattern(candidate) {
|
|
273
|
-
const keywords = candidate.category ? [candidate.category] : [];
|
|
274
|
-
// Extract keywords from reporters (if we had the text, but we only have signature)
|
|
275
|
-
// In production this would use the genome sequencer
|
|
276
|
-
|
|
277
|
-
const promoted = {
|
|
278
|
-
signature: candidate.signature,
|
|
279
|
-
category: candidate.category,
|
|
280
|
-
severity: candidate.severity,
|
|
281
|
-
confidence: candidate.confidence,
|
|
282
|
-
keywords,
|
|
283
|
-
reportCount: candidate.reports,
|
|
284
|
-
reporterCount: candidate.reporters.size,
|
|
285
|
-
firstSeen: candidate.firstSeen,
|
|
286
|
-
promotedAt: Date.now(),
|
|
287
|
-
lastReported: Date.now(),
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
this._patterns.set(candidate.signature, promoted);
|
|
291
|
-
this._candidates.delete(candidate.signature);
|
|
292
|
-
this._stats.patternsPromoted++;
|
|
293
|
-
|
|
294
|
-
// Enforce max patterns
|
|
295
|
-
if (this._patterns.size > this.maxPatterns) {
|
|
296
|
-
const oldest = [...this._patterns.entries()]
|
|
297
|
-
.sort((a, b) => a[1].lastReported - b[1].lastReported)[0];
|
|
298
|
-
if (oldest) this._patterns.delete(oldest[0]);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
this.emit('pattern_promoted', promoted);
|
|
302
|
-
return { signature: candidate.signature, status: 'promoted' };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Connect multiple ThreatIntelFederation nodes into a mesh.
|
|
308
|
-
* When any node promotes a pattern, all others receive it.
|
|
309
|
-
* @param {ThreatIntelFederation[]} nodes
|
|
310
|
-
* @returns {{ nodes, broadcast }}
|
|
311
|
-
*/
|
|
312
|
-
function createFederationMesh(nodes) {
|
|
313
|
-
// Wire up: when any node promotes, share with all others
|
|
314
|
-
for (const node of nodes) {
|
|
315
|
-
node.on('pattern_promoted', (pattern) => {
|
|
316
|
-
for (const peer of nodes) {
|
|
317
|
-
if (peer.nodeId !== node.nodeId) {
|
|
318
|
-
peer.receivePatterns([pattern]);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
// Register all peers
|
|
323
|
-
for (const peer of nodes) {
|
|
324
|
-
if (peer.nodeId !== node.nodeId) {
|
|
325
|
-
node.addPeer(peer.nodeId);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return {
|
|
331
|
-
nodes,
|
|
332
|
-
broadcast: (pattern) => {
|
|
333
|
-
for (const node of nodes) {
|
|
334
|
-
node.receivePatterns([pattern]);
|
|
335
|
-
}
|
|
336
|
-
},
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
module.exports = {
|
|
341
|
-
ThreatIntelFederation,
|
|
342
|
-
createFederationMesh,
|
|
343
|
-
};
|