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,480 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Alert Fatigue Scoring & Auto-Tuning
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Alert fatigue analysis: identifies noisy patterns and high false-positive sources
|
|
8
|
+
* - Auto-tuning: applies suggestions to shield config to reduce alert noise
|
|
9
|
+
* - Alert correlation: groups related alerts by time proximity and category
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// =========================================================================
|
|
13
|
+
// Alert Fatigue Analyzer
|
|
14
|
+
// =========================================================================
|
|
15
|
+
|
|
16
|
+
class AlertFatigueAnalyzer {
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} [options]
|
|
19
|
+
* @param {number} [options.windowSize=1000] - Maximum number of events in the analysis window.
|
|
20
|
+
* @param {number} [options.fatigueThreshold=0.7] - Score above which alerting is considered fatigued.
|
|
21
|
+
*/
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.windowSize = options.windowSize || 1000;
|
|
24
|
+
this.fatigueThreshold = options.fatigueThreshold || 0.7;
|
|
25
|
+
this.events = [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Record an alert event for fatigue analysis.
|
|
30
|
+
* @param {Object} alertEvent - The alert event to record.
|
|
31
|
+
* @param {string} alertEvent.category - Alert category (e.g., 'prompt_injection').
|
|
32
|
+
* @param {string} alertEvent.severity - Severity level ('critical', 'high', 'medium', 'low').
|
|
33
|
+
* @param {string} alertEvent.pattern - The pattern or rule that fired.
|
|
34
|
+
* @param {boolean} [alertEvent.falsePositive=false] - Whether this was a false positive.
|
|
35
|
+
*/
|
|
36
|
+
record(alertEvent) {
|
|
37
|
+
const event = {
|
|
38
|
+
category: alertEvent.category || 'unknown',
|
|
39
|
+
severity: alertEvent.severity || 'medium',
|
|
40
|
+
pattern: alertEvent.pattern || 'unknown',
|
|
41
|
+
falsePositive: alertEvent.falsePositive === true,
|
|
42
|
+
timestamp: Date.now()
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this.events.push(event);
|
|
46
|
+
|
|
47
|
+
// Trim to window size
|
|
48
|
+
if (this.events.length > this.windowSize) {
|
|
49
|
+
this.events = this.events.slice(this.events.length - this.windowSize);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return event;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return the patterns/categories with highest fire rate and lowest true-positive rate.
|
|
57
|
+
* @returns {Array<Object>} Sorted array of noisy sources [{pattern, category, fireCount, falsePositiveRate, truePositiveRate}].
|
|
58
|
+
*/
|
|
59
|
+
getTopNoisy() {
|
|
60
|
+
if (this.events.length === 0) return [];
|
|
61
|
+
|
|
62
|
+
const patternStats = {};
|
|
63
|
+
|
|
64
|
+
for (const event of this.events) {
|
|
65
|
+
const key = `${event.category}::${event.pattern}`;
|
|
66
|
+
if (!patternStats[key]) {
|
|
67
|
+
patternStats[key] = {
|
|
68
|
+
pattern: event.pattern,
|
|
69
|
+
category: event.category,
|
|
70
|
+
fireCount: 0,
|
|
71
|
+
falsePositives: 0
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
patternStats[key].fireCount++;
|
|
75
|
+
if (event.falsePositive) {
|
|
76
|
+
patternStats[key].falsePositives++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const results = Object.values(patternStats).map(stat => ({
|
|
81
|
+
pattern: stat.pattern,
|
|
82
|
+
category: stat.category,
|
|
83
|
+
fireCount: stat.fireCount,
|
|
84
|
+
falsePositiveRate: stat.fireCount > 0 ? stat.falsePositives / stat.fireCount : 0,
|
|
85
|
+
truePositiveRate: stat.fireCount > 0 ? (stat.fireCount - stat.falsePositives) / stat.fireCount : 1
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
// Sort by highest fire rate and lowest true-positive rate (noisiest first)
|
|
89
|
+
results.sort((a, b) => {
|
|
90
|
+
const noiseA = a.fireCount * a.falsePositiveRate;
|
|
91
|
+
const noiseB = b.fireCount * b.falsePositiveRate;
|
|
92
|
+
return noiseB - noiseA;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Calculate a fatigue score from 0 to 1.
|
|
100
|
+
* High fire rate combined with many false positives produces a high fatigue score.
|
|
101
|
+
* @returns {number} Fatigue score between 0 (healthy) and 1 (severely fatigued).
|
|
102
|
+
*/
|
|
103
|
+
getFatigueScore() {
|
|
104
|
+
if (this.events.length === 0) return 0;
|
|
105
|
+
|
|
106
|
+
const totalEvents = this.events.length;
|
|
107
|
+
const totalFalsePositives = this.events.filter(e => e.falsePositive).length;
|
|
108
|
+
const fpRate = totalFalsePositives / totalEvents;
|
|
109
|
+
|
|
110
|
+
// Fire rate: how full is the window relative to capacity
|
|
111
|
+
const fillRate = totalEvents / this.windowSize;
|
|
112
|
+
|
|
113
|
+
// Unique pattern count — fewer unique patterns with high volume = more fatigued
|
|
114
|
+
const uniquePatterns = new Set(this.events.map(e => `${e.category}::${e.pattern}`)).size;
|
|
115
|
+
const repetitionFactor = totalEvents > 0 ? 1 - (uniquePatterns / totalEvents) : 0;
|
|
116
|
+
|
|
117
|
+
// Weighted combination
|
|
118
|
+
const score = (fpRate * 0.5) + (fillRate * 0.25) + (repetitionFactor * 0.25);
|
|
119
|
+
|
|
120
|
+
return Math.min(1, Math.max(0, score));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate tuning suggestions to reduce alert fatigue.
|
|
125
|
+
* @returns {Array<Object>} Array of suggestions [{action, target, reason}].
|
|
126
|
+
*/
|
|
127
|
+
suggest() {
|
|
128
|
+
const suggestions = [];
|
|
129
|
+
const noisy = this.getTopNoisy();
|
|
130
|
+
const fatigueScore = this.getFatigueScore();
|
|
131
|
+
|
|
132
|
+
if (fatigueScore < 0.3) {
|
|
133
|
+
return suggestions; // Alerting is healthy
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const source of noisy) {
|
|
137
|
+
if (source.falsePositiveRate > 0.8) {
|
|
138
|
+
// Mostly false positives — suggest disabling
|
|
139
|
+
suggestions.push({
|
|
140
|
+
action: 'disable',
|
|
141
|
+
target: source.pattern,
|
|
142
|
+
reason: `Pattern "${source.pattern}" in category "${source.category}" has ${(source.falsePositiveRate * 100).toFixed(0)}% false positive rate (${source.fireCount} fires).`
|
|
143
|
+
});
|
|
144
|
+
} else if (source.falsePositiveRate > 0.5) {
|
|
145
|
+
// High FP rate — suggest adding allowlist
|
|
146
|
+
suggestions.push({
|
|
147
|
+
action: 'add_allowlist',
|
|
148
|
+
target: source.pattern,
|
|
149
|
+
reason: `Pattern "${source.pattern}" has ${(source.falsePositiveRate * 100).toFixed(0)}% false positive rate. Consider allowlisting common benign matches.`
|
|
150
|
+
});
|
|
151
|
+
} else if (source.fireCount > this.windowSize * 0.1 && source.falsePositiveRate > 0.2) {
|
|
152
|
+
// High volume with moderate FPs — refine the pattern
|
|
153
|
+
suggestions.push({
|
|
154
|
+
action: 'refine_pattern',
|
|
155
|
+
target: source.pattern,
|
|
156
|
+
reason: `Pattern "${source.pattern}" fires frequently (${source.fireCount} times) with ${(source.falsePositiveRate * 100).toFixed(0)}% false positives. Consider tightening the pattern.`
|
|
157
|
+
});
|
|
158
|
+
} else if (source.fireCount > this.windowSize * 0.15) {
|
|
159
|
+
// Very high volume but mostly true positives — lower severity to reduce noise
|
|
160
|
+
suggestions.push({
|
|
161
|
+
action: 'lower_severity',
|
|
162
|
+
target: source.pattern,
|
|
163
|
+
reason: `Pattern "${source.pattern}" fires very frequently (${source.fireCount} times). Consider lowering severity to reduce alert noise.`
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return suggestions;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reset all recorded events.
|
|
173
|
+
*/
|
|
174
|
+
reset() {
|
|
175
|
+
this.events = [];
|
|
176
|
+
console.log('[Agent Shield] AlertFatigueAnalyzer reset.');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// =========================================================================
|
|
181
|
+
// Auto-Tuner
|
|
182
|
+
// =========================================================================
|
|
183
|
+
|
|
184
|
+
class AutoTuner {
|
|
185
|
+
/**
|
|
186
|
+
* @param {Object} shield - AgentShield instance to tune.
|
|
187
|
+
* @param {Object} [options]
|
|
188
|
+
* @param {boolean} [options.autoApply=false] - Whether to automatically apply suggestions.
|
|
189
|
+
*/
|
|
190
|
+
constructor(shield, options = {}) {
|
|
191
|
+
this.shield = shield;
|
|
192
|
+
this.autoApply = options.autoApply || false;
|
|
193
|
+
this.analyzer = new AlertFatigueAnalyzer(options);
|
|
194
|
+
this.history = [];
|
|
195
|
+
this._lastApplied = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Run fatigue analysis on the shield's scan history.
|
|
200
|
+
* @returns {Object} Analysis result {fatigueScore, suggestions, topNoisy}.
|
|
201
|
+
*/
|
|
202
|
+
analyze() {
|
|
203
|
+
// Pull scan history from the shield if available
|
|
204
|
+
const scanHistory = (this.shield && this.shield.stats && this.shield.stats.history) || [];
|
|
205
|
+
|
|
206
|
+
// Feed events into the analyzer
|
|
207
|
+
for (const entry of scanHistory) {
|
|
208
|
+
if (entry.threats && entry.threats.length > 0) {
|
|
209
|
+
for (const threat of entry.threats) {
|
|
210
|
+
this.analyzer.record({
|
|
211
|
+
category: threat.category || 'unknown',
|
|
212
|
+
severity: threat.severity || 'medium',
|
|
213
|
+
pattern: threat.description || threat.pattern || 'unknown',
|
|
214
|
+
falsePositive: threat.falsePositive === true
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const fatigueScore = this.analyzer.getFatigueScore();
|
|
221
|
+
const suggestions = this.analyzer.suggest();
|
|
222
|
+
const topNoisy = this.analyzer.getTopNoisy();
|
|
223
|
+
|
|
224
|
+
const result = {
|
|
225
|
+
fatigueScore,
|
|
226
|
+
suggestions,
|
|
227
|
+
topNoisy,
|
|
228
|
+
timestamp: new Date().toISOString()
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (this.autoApply && suggestions.length > 0) {
|
|
232
|
+
this.apply(suggestions);
|
|
233
|
+
console.log(`[Agent Shield] AutoTuner auto-applied ${suggestions.length} suggestion(s).`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Apply tuning suggestions to the shield config.
|
|
241
|
+
* @param {Array<Object>} suggestions - Array of suggestions from analyze().
|
|
242
|
+
* @returns {Object} Applied changes record.
|
|
243
|
+
*/
|
|
244
|
+
apply(suggestions) {
|
|
245
|
+
const applied = {
|
|
246
|
+
timestamp: new Date().toISOString(),
|
|
247
|
+
changes: [],
|
|
248
|
+
previousConfig: this.shield.config ? JSON.parse(JSON.stringify(this.shield.config)) : {}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
for (const suggestion of suggestions) {
|
|
252
|
+
const change = {
|
|
253
|
+
action: suggestion.action,
|
|
254
|
+
target: suggestion.target,
|
|
255
|
+
reason: suggestion.reason
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
switch (suggestion.action) {
|
|
259
|
+
case 'disable':
|
|
260
|
+
if (this.shield.config && this.shield.config.disabledPatterns) {
|
|
261
|
+
this.shield.config.disabledPatterns.push(suggestion.target);
|
|
262
|
+
}
|
|
263
|
+
change.applied = true;
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case 'lower_severity':
|
|
267
|
+
if (this.shield.config && this.shield.config.severityOverrides) {
|
|
268
|
+
this.shield.config.severityOverrides[suggestion.target] = 'low';
|
|
269
|
+
}
|
|
270
|
+
change.applied = true;
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case 'add_allowlist':
|
|
274
|
+
if (this.shield.config && this.shield.config.allowlist) {
|
|
275
|
+
this.shield.config.allowlist.push({ pattern: suggestion.target, reason: suggestion.reason });
|
|
276
|
+
}
|
|
277
|
+
change.applied = true;
|
|
278
|
+
break;
|
|
279
|
+
|
|
280
|
+
case 'refine_pattern':
|
|
281
|
+
// Cannot auto-apply pattern refinement — requires manual review
|
|
282
|
+
change.applied = false;
|
|
283
|
+
change.note = 'Pattern refinement requires manual review.';
|
|
284
|
+
break;
|
|
285
|
+
|
|
286
|
+
default:
|
|
287
|
+
change.applied = false;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
applied.changes.push(change);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this._lastApplied = applied;
|
|
295
|
+
this.history.push(applied);
|
|
296
|
+
console.log(`[Agent Shield] AutoTuner applied ${applied.changes.filter(c => c.applied).length} change(s).`);
|
|
297
|
+
|
|
298
|
+
return applied;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Undo the last applied tuning.
|
|
303
|
+
* @returns {boolean} Whether the revert was successful.
|
|
304
|
+
*/
|
|
305
|
+
revert() {
|
|
306
|
+
if (!this._lastApplied) {
|
|
307
|
+
console.log('[Agent Shield] AutoTuner: nothing to revert.');
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (this.shield.config && this._lastApplied.previousConfig) {
|
|
312
|
+
Object.assign(this.shield.config, this._lastApplied.previousConfig);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const revertRecord = {
|
|
316
|
+
timestamp: new Date().toISOString(),
|
|
317
|
+
action: 'revert',
|
|
318
|
+
reverted: this._lastApplied.timestamp
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
this.history.push(revertRecord);
|
|
322
|
+
this._lastApplied = null;
|
|
323
|
+
console.log('[Agent Shield] AutoTuner reverted last tuning.');
|
|
324
|
+
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get the full tuning history.
|
|
330
|
+
* @returns {Array<Object>} Array of applied/reverted tuning records.
|
|
331
|
+
*/
|
|
332
|
+
getHistory() {
|
|
333
|
+
return [...this.history];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// =========================================================================
|
|
338
|
+
// Alert Correlator
|
|
339
|
+
// =========================================================================
|
|
340
|
+
|
|
341
|
+
class AlertCorrelator {
|
|
342
|
+
/**
|
|
343
|
+
* @param {Object} [options]
|
|
344
|
+
* @param {number} [options.timeWindowMs=60000] - Time window for correlating alerts (default: 1 minute).
|
|
345
|
+
*/
|
|
346
|
+
constructor(options = {}) {
|
|
347
|
+
this.timeWindowMs = options.timeWindowMs || 60000;
|
|
348
|
+
this._internalPatterns = [];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Group related alerts by time proximity and category.
|
|
353
|
+
* @param {Array<Object>} alerts - Array of alert objects with timestamp and category.
|
|
354
|
+
* @returns {Array<Object>} Array of correlated groups [{alerts, category, startTime, endTime, count}].
|
|
355
|
+
*/
|
|
356
|
+
correlate(alerts) {
|
|
357
|
+
if (!alerts || alerts.length === 0) return [];
|
|
358
|
+
|
|
359
|
+
// Sort by timestamp
|
|
360
|
+
const sorted = [...alerts].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
361
|
+
|
|
362
|
+
const groups = [];
|
|
363
|
+
let currentGroup = null;
|
|
364
|
+
|
|
365
|
+
for (const alert of sorted) {
|
|
366
|
+
const ts = alert.timestamp || 0;
|
|
367
|
+
const cat = alert.category || 'unknown';
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
currentGroup &&
|
|
371
|
+
currentGroup.category === cat &&
|
|
372
|
+
ts - currentGroup.endTime <= this.timeWindowMs
|
|
373
|
+
) {
|
|
374
|
+
// Extend current group
|
|
375
|
+
currentGroup.alerts.push(alert);
|
|
376
|
+
currentGroup.endTime = ts;
|
|
377
|
+
currentGroup.count++;
|
|
378
|
+
} else {
|
|
379
|
+
// Start a new group
|
|
380
|
+
if (currentGroup) groups.push(currentGroup);
|
|
381
|
+
currentGroup = {
|
|
382
|
+
alerts: [alert],
|
|
383
|
+
category: cat,
|
|
384
|
+
startTime: ts,
|
|
385
|
+
endTime: ts,
|
|
386
|
+
count: 1
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (currentGroup) groups.push(currentGroup);
|
|
392
|
+
|
|
393
|
+
// Track patterns for getPatterns()
|
|
394
|
+
this._updatePatterns(groups);
|
|
395
|
+
|
|
396
|
+
return groups;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Remove duplicate alerts within the time window.
|
|
401
|
+
* Alerts are considered duplicates if they share the same category and pattern within the window.
|
|
402
|
+
* @param {Array<Object>} alerts - Array of alert objects.
|
|
403
|
+
* @returns {Array<Object>} Deduplicated alerts.
|
|
404
|
+
*/
|
|
405
|
+
deduplicate(alerts) {
|
|
406
|
+
if (!alerts || alerts.length === 0) return [];
|
|
407
|
+
|
|
408
|
+
const sorted = [...alerts].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
409
|
+
const result = [];
|
|
410
|
+
const seen = new Map();
|
|
411
|
+
|
|
412
|
+
for (const alert of sorted) {
|
|
413
|
+
const key = `${alert.category || 'unknown'}::${alert.pattern || 'unknown'}`;
|
|
414
|
+
const ts = alert.timestamp || 0;
|
|
415
|
+
|
|
416
|
+
const lastSeen = seen.get(key);
|
|
417
|
+
if (lastSeen === undefined || ts - lastSeen > this.timeWindowMs) {
|
|
418
|
+
result.push(alert);
|
|
419
|
+
seen.set(key, ts);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Return recurring alert patterns observed from correlated groups.
|
|
428
|
+
* @returns {Array<Object>} Recurring patterns [{category, avgGroupSize, frequency, firstSeen, lastSeen}].
|
|
429
|
+
*/
|
|
430
|
+
getPatterns() {
|
|
431
|
+
return [...this._internalPatterns];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Update internal pattern tracking from correlated groups.
|
|
436
|
+
* @private
|
|
437
|
+
* @param {Array<Object>} groups - Correlated alert groups.
|
|
438
|
+
*/
|
|
439
|
+
_updatePatterns(groups) {
|
|
440
|
+
const patternMap = {};
|
|
441
|
+
|
|
442
|
+
for (const group of groups) {
|
|
443
|
+
const cat = group.category;
|
|
444
|
+
if (!patternMap[cat]) {
|
|
445
|
+
patternMap[cat] = {
|
|
446
|
+
category: cat,
|
|
447
|
+
groupSizes: [],
|
|
448
|
+
firstSeen: group.startTime,
|
|
449
|
+
lastSeen: group.endTime
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
patternMap[cat].groupSizes.push(group.count);
|
|
454
|
+
if (group.startTime < patternMap[cat].firstSeen) {
|
|
455
|
+
patternMap[cat].firstSeen = group.startTime;
|
|
456
|
+
}
|
|
457
|
+
if (group.endTime > patternMap[cat].lastSeen) {
|
|
458
|
+
patternMap[cat].lastSeen = group.endTime;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
this._internalPatterns = Object.values(patternMap).map(p => ({
|
|
463
|
+
category: p.category,
|
|
464
|
+
avgGroupSize: p.groupSizes.length > 0 ? p.groupSizes.reduce((a, b) => a + b, 0) / p.groupSizes.length : 0,
|
|
465
|
+
frequency: p.groupSizes.length,
|
|
466
|
+
firstSeen: p.firstSeen,
|
|
467
|
+
lastSeen: p.lastSeen
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// =========================================================================
|
|
473
|
+
// Exports
|
|
474
|
+
// =========================================================================
|
|
475
|
+
|
|
476
|
+
module.exports = {
|
|
477
|
+
AlertFatigueAnalyzer,
|
|
478
|
+
AutoTuner,
|
|
479
|
+
AlertCorrelator
|
|
480
|
+
};
|