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.
- package/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- 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
|
+
};
|