-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathalert-manager.js
More file actions
314 lines (270 loc) · 10 KB
/
alert-manager.js
File metadata and controls
314 lines (270 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
// Hera Alert Manager - Tiered Alerting with Confidence Scoring
// Prevents alert fatigue by showing only high-confidence, high-severity findings
class AlertManager {
constructor() {
this.alertThresholds = {
CRITICAL: { minConfidence: 70, action: 'PAGE_OVERLAY' },
HIGH: { minConfidence: 60, action: 'NOTIFICATION' },
MEDIUM: { minConfidence: 50, action: 'BADGE' },
LOW: { minConfidence: 40, action: 'POPUP_ONLY' },
INFO: { minConfidence: 0, action: 'LOG_ONLY' }
};
// CRITICAL FIX P0: Persistent storage for alert deduplication
this._alertHistory = new Map();
this.initialized = false;
this.initPromise = this.initialize();
// P0-SEVENTH-1 FIX: Maximum alert history size to prevent storage quota exhaustion
this.MAX_ALERT_HISTORY_SIZE = 1000;
this.ALERT_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
}
async initialize() {
if (this.initialized) return;
try {
// CRITICAL FIX P0: Use chrome.storage.local for alert history (survives browser restart)
// Alert deduplication must persist across browser sessions
const data = await chrome.storage.local.get(['heraAlertHistory']);
if (data.heraAlertHistory) {
for (const [key, value] of Object.entries(data.heraAlertHistory)) {
this._alertHistory.set(key, value);
}
console.log(`Hera: Restored ${this._alertHistory.size} alert history entries`);
}
this.initialized = true;
} catch (error) {
console.error('Hera: Failed to initialize alert manager:', error);
this.initialized = true;
}
}
async _syncToStorage() {
try {
await this.initPromise;
// P0-SIXTEENTH-2 FIX: Check quota before writing
const bytesInUse = await chrome.storage.local.getBytesInUse();
const quota = chrome.storage.local.QUOTA_BYTES || 10485760;
if (bytesInUse / quota > 0.90) {
console.warn('Hera: Alert history sync skipped - quota >90%');
return;
}
const historyObj = Object.fromEntries(this._alertHistory.entries());
// CRITICAL FIX P0: Use chrome.storage.local (survives browser restart)
await chrome.storage.local.set({ heraAlertHistory: historyObj });
} catch (error) {
if (error.message?.includes('QUOTA')) {
console.error('Hera: Alert history quota exceeded, forcing cleanup');
this.cleanupAlertHistory();
} else {
console.error('Hera: Failed to sync alert history:', error);
}
}
}
_debouncedSync() {
// P3-SIXTEENTH-2: DEBOUNCE TIMING - 1000ms (vs memory-manager's 100ms)
// Alert history is low-priority data (used only for deduplication)
// Longer debounce reduces storage quota pressure
// Acceptable data loss: Worst case = duplicate alerts shown after browser restart
if (this._syncTimeout) clearTimeout(this._syncTimeout);
this._syncTimeout = setTimeout(() => {
this._syncToStorage().catch(err => console.error('Alert history sync failed:', err));
}, 1000); // 1 second debounce - see rationale above
}
get alertHistory() {
return this._alertHistory;
}
/**
* Calculate confidence score for a security finding
* @param {Object} finding - Security finding with evidence
* @returns {number} Confidence score 0-100
*/
calculateConfidence(finding) {
let confidence = 50; // Base confidence
// Evidence-based confidence boosters
if (finding.evidence) {
// Direct evidence (response body, headers)
if (finding.evidence.responseBody) confidence += 20;
if (finding.evidence.verification) confidence += 15;
// Known provider reduces confidence of false positives
if (finding.evidence.isKnownProvider) confidence -= 10;
// Multiple corroborating indicators
if (finding.evidence.correlatedFindings > 1) {
confidence += finding.evidence.correlatedFindings * 5;
}
}
// Severity-based adjustments
if (finding.severity === 'CRITICAL') {
confidence += 10; // Critical findings need high bar
}
// Protocol-specific confidence
if (finding.protocol) {
if (['OAuth2', 'OIDC'].includes(finding.protocol)) {
confidence += 5; // Well-understood protocols
}
}
// Pattern matching confidence
if (finding.patternMatches > 2) {
confidence += 10; // Multiple patterns matched
}
// Entropy-based confidence (for state parameters)
if (finding.entropyPerChar !== undefined) {
if (finding.entropyPerChar < 1) confidence += 20; // Very low entropy = high confidence
else if (finding.entropyPerChar < 2) confidence += 10;
}
// Cap at 0-100
return Math.max(0, Math.min(100, confidence));
}
/**
* Determine if an alert should be shown based on severity and confidence
* @param {Object} finding - Security finding
* @returns {Object} Alert decision { show, action, reason }
*/
shouldShowAlert(finding) {
const confidence = this.calculateConfidence(finding);
finding.confidence = confidence; // Attach confidence to finding
const threshold = this.alertThresholds[finding.severity];
if (!threshold) {
return { show: false, action: 'UNKNOWN_SEVERITY', reason: 'Unknown severity level' };
}
// Check confidence threshold
if (confidence < threshold.minConfidence) {
return {
show: false,
action: 'BELOW_THRESHOLD',
reason: `Confidence ${confidence}% below threshold ${threshold.minConfidence}%`
};
}
// Check for duplicate alerts (same finding on same domain within 1 hour)
const alertKey = `${finding.type}_${finding.url}_${finding.severity}`;
const lastShown = this.alertHistory.get(alertKey);
if (lastShown && (Date.now() - lastShown) < 60 * 60 * 1000) {
return {
show: false,
action: 'DUPLICATE',
reason: 'Same alert shown within last hour'
};
}
// Show the alert
this.alertHistory.set(alertKey, Date.now());
// CRITICAL FIX: Persist to storage.session
this._debouncedSync();
return {
show: true,
action: threshold.action,
confidence: confidence,
reason: `Severity ${finding.severity}, confidence ${confidence}%`
};
}
/**
* Execute the appropriate alert action
* @param {Object} finding - Security finding with confidence
* @param {string} action - Alert action type
*/
async executeAlert(finding, action) {
switch (action) {
case 'PAGE_OVERLAY':
// Send to content script for branded alert
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
chrome.tabs.sendMessage(tabs[0].id, {
action: 'showPageAlert',
finding: finding
});
}
});
break;
case 'NOTIFICATION':
// Chrome notification
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title: `🔒 Hera Security Alert (${finding.confidence}% confident)`,
message: `${finding.type}: ${finding.message}`,
priority: 2
});
break;
case 'BADGE':
// Update badge with severity indicator
const badgeColors = {
CRITICAL: '#dc3545',
HIGH: '#fd7e14',
MEDIUM: '#ffc107',
LOW: '#6c757d'
};
chrome.action.setBadgeText({ text: '!' });
chrome.action.setBadgeBackgroundColor({ color: badgeColors[finding.severity] || '#6c757d' });
break;
case 'POPUP_ONLY':
case 'LOG_ONLY':
// Just log for review in popup
console.log(`Hera Finding (${finding.confidence}% confidence):`, finding);
break;
}
}
/**
* Process a security finding with tiered alerting
* @param {Object} finding - Raw security finding
*/
processFinding(finding) {
const decision = this.shouldShowAlert(finding);
console.log(`Alert Decision for ${finding.type}:`, decision);
if (decision.show) {
this.executeAlert(finding, decision.action);
}
// Always store for popup display
this.storeFindingForPopup(finding, decision);
}
/**
* Store finding for display in popup
* @param {Object} finding - Security finding
* @param {Object} decision - Alert decision
*/
storeFindingForPopup(finding, decision) {
chrome.storage.local.get(['heraFindings'], (result) => {
const findings = result.heraFindings || [];
findings.push({
...finding,
confidence: finding.confidence,
alertAction: decision.action,
timestamp: Date.now()
});
// Keep only last 500 findings
if (findings.length > 500) {
findings.splice(0, findings.length - 500);
}
chrome.storage.local.set({ heraFindings: findings });
});
}
/**
* Clear old alert history (run periodically)
* P0-SEVENTH-1 FIX: Added LRU eviction to prevent unbounded growth
*/
cleanupAlertHistory() {
const now = Date.now();
let cleaned = 0;
// 1. Remove expired entries (older than 24 hours)
for (const [key, timestamp] of this.alertHistory.entries()) {
if (now - timestamp > this.ALERT_EXPIRY_MS) {
this.alertHistory.delete(key);
cleaned++;
}
}
// 2. P0-SEVENTH-1 FIX: If still too large, LRU eviction
if (this.alertHistory.size > this.MAX_ALERT_HISTORY_SIZE) {
// Sort by timestamp (oldest first)
const entries = Array.from(this.alertHistory.entries())
.sort((a, b) => a[1] - b[1]);
// Remove oldest entries until size is acceptable
const toRemove = this.alertHistory.size - this.MAX_ALERT_HISTORY_SIZE;
for (let i = 0; i < toRemove; i++) {
this.alertHistory.delete(entries[i][0]);
cleaned++;
}
console.log(`Hera: Alert history LRU eviction removed ${toRemove} oldest entries`);
}
if (cleaned > 0) {
console.log(`Hera: Cleaned ${cleaned} total alert history entries (size now: ${this.alertHistory.size}/${this.MAX_ALERT_HISTORY_SIZE})`);
// CRITICAL FIX: Persist deletion to storage.session
this._debouncedSync();
}
}
}
// Export for ES modules
export { AlertManager };