Skip to content

Commit 6aacac2

Browse files
Merge pull request #3 from CodeMonkeyCybersecurity/claude/adversarial-codebase-analysis-011CV5BDnZ7okdzi7ZCPpYDR
Claude/adversarial codebase analysis 011 cv5 b dn z7okdzi7 zc pp ydr
2 parents 1a9c499 + 8463857 commit 6aacac2

11 files changed

+4199
-67
lines changed

evidence-collector.js

Lines changed: 231 additions & 43 deletions
Large diffs are not rendered by default.

modules/auth/rfc9700-compliance-checker.js

Lines changed: 640 additions & 0 deletions
Large diffs are not rendered by default.

modules/notification-manager.js

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
/**
2+
* Notification Manager
3+
*
4+
* PURPOSE: Notify users when high-confidence security findings are detected
5+
* - Chrome notifications for immediate awareness
6+
* - Badge updates with finding counts
7+
* - Configurable notification thresholds
8+
*
9+
* IMPLEMENTATION: P1-1 (Evidence Export Notifications)
10+
* @see ROADMAP.md P1-1: Evidence Export Notifications
11+
*/
12+
13+
export class NotificationManager {
14+
constructor() {
15+
this.notifiedFindings = new Set(); // Track notified findings to avoid duplicates
16+
this.findingCounts = new Map(); // domain → count
17+
18+
// Notification thresholds
19+
this.MIN_CONFIDENCE = 'MEDIUM'; // Minimum confidence for notifications
20+
this.MIN_SEVERITY = 'MEDIUM'; // Minimum severity for notifications
21+
22+
// Confidence levels (for comparison)
23+
this.confidenceLevels = {
24+
'SPECULATIVE': 0,
25+
'LOW': 1,
26+
'MEDIUM': 2,
27+
'HIGH': 3
28+
};
29+
30+
// Severity levels (for comparison)
31+
this.severityLevels = {
32+
'INFO': 0,
33+
'LOW': 1,
34+
'MEDIUM': 2,
35+
'HIGH': 3,
36+
'CRITICAL': 4
37+
};
38+
}
39+
40+
/**
41+
* Notify user of a high-confidence security finding
42+
*
43+
* @param {Object} finding - Security finding object
44+
* @param {string} domain - Domain where finding was detected
45+
* @returns {Promise<string|null>} Notification ID or null if not notified
46+
*/
47+
async notifyFinding(finding, domain) {
48+
// Check if we should notify
49+
if (!this._shouldNotify(finding)) {
50+
return null;
51+
}
52+
53+
// Check if already notified
54+
const findingKey = `${domain}:${finding.type}`;
55+
if (this.notifiedFindings.has(findingKey)) {
56+
return null;
57+
}
58+
59+
// Mark as notified
60+
this.notifiedFindings.add(findingKey);
61+
62+
// Update finding count
63+
const currentCount = this.findingCounts.get(domain) || 0;
64+
this.findingCounts.set(domain, currentCount + 1);
65+
66+
// Create notification
67+
try {
68+
const notificationId = await this._createNotification(finding, domain);
69+
70+
// Update badge
71+
await this._updateBadge(domain);
72+
73+
return notificationId;
74+
} catch (error) {
75+
console.error('[NotificationManager] Failed to create notification:', error);
76+
return null;
77+
}
78+
}
79+
80+
/**
81+
* Check if finding meets notification criteria
82+
*
83+
* @param {Object} finding - Security finding
84+
* @returns {boolean} True if should notify
85+
* @private
86+
*/
87+
_shouldNotify(finding) {
88+
// Check confidence level
89+
const confidenceLevel = this.confidenceLevels[finding.confidence] || 0;
90+
const minConfidenceLevel = this.confidenceLevels[this.MIN_CONFIDENCE];
91+
92+
if (confidenceLevel < minConfidenceLevel) {
93+
return false;
94+
}
95+
96+
// Check severity level
97+
const severityLevel = this.severityLevels[finding.severity] || 0;
98+
const minSeverityLevel = this.severityLevels[this.MIN_SEVERITY];
99+
100+
if (severityLevel < minSeverityLevel) {
101+
return false;
102+
}
103+
104+
return true;
105+
}
106+
107+
/**
108+
* Create Chrome notification
109+
*
110+
* @param {Object} finding - Security finding
111+
* @param {string} domain - Domain
112+
* @returns {Promise<string>} Notification ID
113+
* @private
114+
*/
115+
async _createNotification(finding, domain) {
116+
const severityEmoji = this._getSeverityEmoji(finding.severity);
117+
const confidenceBadge = this._getConfidenceBadge(finding.confidence);
118+
119+
const notificationOptions = {
120+
type: 'basic',
121+
iconUrl: this._getIconUrl(finding.severity),
122+
title: `Hera: ${severityEmoji} ${finding.severity} Finding`,
123+
message: `${confidenceBadge} ${this._formatFindingMessage(finding)} on ${domain}`,
124+
priority: this._getPriority(finding.severity),
125+
requireInteraction: finding.severity === 'CRITICAL',
126+
buttons: [
127+
{ title: 'View Evidence' },
128+
{ title: 'Export Report' }
129+
]
130+
};
131+
132+
return new Promise((resolve, reject) => {
133+
chrome.notifications.create(notificationOptions, (notificationId) => {
134+
if (chrome.runtime.lastError) {
135+
reject(chrome.runtime.lastError);
136+
} else {
137+
// Register notification click handler
138+
this._registerClickHandler(notificationId, domain);
139+
resolve(notificationId);
140+
}
141+
});
142+
});
143+
}
144+
145+
/**
146+
* Format finding message for notification
147+
*
148+
* @param {Object} finding - Security finding
149+
* @returns {string} Formatted message
150+
* @private
151+
*/
152+
_formatFindingMessage(finding) {
153+
// Convert finding type to human-readable message
154+
const messages = {
155+
'MISSING_STATE_PARAMETER': 'Missing CSRF protection (state parameter)',
156+
'WEAK_STATE_PARAMETER': 'Weak CSRF protection (predictable state)',
157+
'MISSING_PKCE': 'Missing PKCE (authorization code interception risk)',
158+
'MISSING_SECURE_FLAG': 'Missing Secure flag on auth cookie',
159+
'MISSING_HTTPONLY_FLAG': 'Missing HttpOnly flag on auth cookie',
160+
'SESSION_FIXATION': 'Session fixation vulnerability',
161+
'ALG_NONE_VULNERABILITY': 'JWT algorithm confusion (alg=none)',
162+
'WEAK_JWT_SECRET': 'Weak JWT secret detected',
163+
'TOKEN_IN_URL': 'Token leaked in URL',
164+
'CREDENTIALS_IN_URL': 'Credentials leaked in URL',
165+
'NO_HSTS': 'Missing HSTS header',
166+
'REFRESH_TOKEN_NOT_ROTATED': 'Refresh token not rotated'
167+
};
168+
169+
return messages[finding.type] || finding.type.replace(/_/g, ' ').toLowerCase();
170+
}
171+
172+
/**
173+
* Get icon URL based on severity
174+
*
175+
* @param {string} severity - Finding severity
176+
* @returns {string} Icon URL
177+
* @private
178+
*/
179+
_getIconUrl(severity) {
180+
const icons = {
181+
'CRITICAL': 'icons/icon-critical-128.png',
182+
'HIGH': 'icons/icon-warning-128.png',
183+
'MEDIUM': 'icons/icon-info-128.png',
184+
'LOW': 'icons/icon-info-128.png'
185+
};
186+
187+
return icons[severity] || 'icons/icon-128.png';
188+
}
189+
190+
/**
191+
* Get severity emoji
192+
*
193+
* @param {string} severity - Finding severity
194+
* @returns {string} Emoji
195+
* @private
196+
*/
197+
_getSeverityEmoji(severity) {
198+
const emojis = {
199+
'CRITICAL': '🔴',
200+
'HIGH': '🟠',
201+
'MEDIUM': '🟡',
202+
'LOW': '🔵',
203+
'INFO': 'ℹ️'
204+
};
205+
206+
return emojis[severity] || '⚠️';
207+
}
208+
209+
/**
210+
* Get confidence badge
211+
*
212+
* @param {string} confidence - Finding confidence
213+
* @returns {string} Badge text
214+
* @private
215+
*/
216+
_getConfidenceBadge(confidence) {
217+
const badges = {
218+
'HIGH': '[✓ High Confidence]',
219+
'MEDIUM': '[~ Medium Confidence]',
220+
'LOW': '[? Low Confidence]',
221+
'SPECULATIVE': '[! Speculative]'
222+
};
223+
224+
return badges[confidence] || '';
225+
}
226+
227+
/**
228+
* Get notification priority
229+
*
230+
* @param {string} severity - Finding severity
231+
* @returns {number} Priority (0-2)
232+
* @private
233+
*/
234+
_getPriority(severity) {
235+
const priorities = {
236+
'CRITICAL': 2,
237+
'HIGH': 2,
238+
'MEDIUM': 1,
239+
'LOW': 0
240+
};
241+
242+
return priorities[severity] || 0;
243+
}
244+
245+
/**
246+
* Update extension badge with finding count
247+
*
248+
* @param {string} domain - Domain
249+
* @private
250+
*/
251+
async _updateBadge(domain) {
252+
const count = this.findingCounts.get(domain) || 0;
253+
254+
if (count === 0) {
255+
// Clear badge
256+
await chrome.action.setBadgeText({ text: '' });
257+
return;
258+
}
259+
260+
// Set badge text
261+
const badgeText = count > 99 ? '99+' : count.toString();
262+
await chrome.action.setBadgeText({ text: badgeText });
263+
264+
// Set badge color based on highest severity
265+
const color = this._getBadgeColor(count);
266+
await chrome.action.setBadgeBackgroundColor({ color: color });
267+
}
268+
269+
/**
270+
* Get badge background color
271+
*
272+
* @param {number} count - Finding count
273+
* @returns {string} Color hex code
274+
* @private
275+
*/
276+
_getBadgeColor(count) {
277+
if (count >= 5) return '#DC2626'; // Red (Critical)
278+
if (count >= 3) return '#F59E0B'; // Orange (High)
279+
if (count >= 1) return '#FBBF24'; // Yellow (Medium)
280+
return '#60A5FA'; // Blue (Low)
281+
}
282+
283+
/**
284+
* Register notification click handler
285+
*
286+
* @param {string} notificationId - Notification ID
287+
* @param {string} domain - Domain
288+
* @private
289+
*/
290+
_registerClickHandler(notificationId, domain) {
291+
chrome.notifications.onClicked.addListener((clickedId) => {
292+
if (clickedId === notificationId) {
293+
// Open popup to view evidence
294+
chrome.action.openPopup();
295+
}
296+
});
297+
298+
chrome.notifications.onButtonClicked.addListener((clickedId, buttonIndex) => {
299+
if (clickedId === notificationId) {
300+
if (buttonIndex === 0) {
301+
// View Evidence button
302+
chrome.action.openPopup();
303+
} else if (buttonIndex === 1) {
304+
// Export Report button
305+
this._triggerExport(domain);
306+
}
307+
}
308+
});
309+
}
310+
311+
/**
312+
* Trigger evidence export
313+
*
314+
* @param {string} domain - Domain
315+
* @private
316+
*/
317+
async _triggerExport(domain) {
318+
// Send message to background to trigger export
319+
try {
320+
await chrome.runtime.sendMessage({
321+
action: 'exportEvidence',
322+
domain: domain,
323+
format: 'enhanced'
324+
});
325+
} catch (error) {
326+
console.error('[NotificationManager] Failed to trigger export:', error);
327+
}
328+
}
329+
330+
/**
331+
* Clear notifications for a domain
332+
*
333+
* @param {string} domain - Domain
334+
*/
335+
clearDomain(domain) {
336+
// Clear notified findings for domain
337+
for (const key of this.notifiedFindings) {
338+
if (key.startsWith(`${domain}:`)) {
339+
this.notifiedFindings.delete(key);
340+
}
341+
}
342+
343+
// Reset count
344+
this.findingCounts.delete(domain);
345+
346+
// Update badge
347+
this._updateBadge(domain);
348+
}
349+
350+
/**
351+
* Clear all notifications
352+
*/
353+
clearAll() {
354+
this.notifiedFindings.clear();
355+
this.findingCounts.clear();
356+
chrome.action.setBadgeText({ text: '' });
357+
}
358+
359+
/**
360+
* Get notification statistics
361+
*
362+
* @returns {Object} Statistics
363+
*/
364+
getStats() {
365+
return {
366+
totalNotified: this.notifiedFindings.size,
367+
domains: Array.from(this.findingCounts.keys()),
368+
findingsByDomain: Object.fromEntries(this.findingCounts)
369+
};
370+
}
371+
}

0 commit comments

Comments
 (0)