Replace monolithic CLI and web server with route-based Flask API. New routes: api_commands, api_build, api_can, api_monitor, api_ota, api_tunnel. Add honeypot security dashboard with real-time SSE, MITRE ATT&CK mapping, kill chain analysis. New TUI with commander/help modules. Add session management, tunnel proxy core, CAN bus data store. Docker support.
107 lines
4.1 KiB
JavaScript
107 lines
4.1 KiB
JavaScript
/* ESPILON Honeypot Dashboard — Server-Sent Events */
|
|
|
|
import { S } from './state.js';
|
|
import { $id } from './utils.js';
|
|
import { playAlertSound } from './audio.js';
|
|
import { showToast } from './ui.js';
|
|
|
|
// Callbacks set by app.js to avoid circular imports.
|
|
// onNewEvent(evt) — called for every new event (render row, update sidebar, etc.)
|
|
// onStatsUpdate() — called after in-memory stats are patched
|
|
let _onNewEvent = null;
|
|
let _onStatsUpdate = null;
|
|
|
|
export function setSSECallbacks({ onNewEvent, onStatsUpdate }) {
|
|
_onNewEvent = onNewEvent;
|
|
_onStatsUpdate = onStatsUpdate;
|
|
}
|
|
|
|
export function connectSSE() {
|
|
if (S._eventSource) {
|
|
try { S._eventSource.close(); } catch (x) { /* ignore */ }
|
|
}
|
|
const sev = S.minSeverity || 'MEDIUM';
|
|
const es = new EventSource('/api/honeypot/stream?min_severity=' + sev);
|
|
S._eventSource = es;
|
|
|
|
es.onopen = function() {
|
|
S.sseConnected = true;
|
|
S._sseRetry = 0;
|
|
const d1 = $id('conn-dot'), d2 = $id('status-conn-dot');
|
|
if (d1) d1.classList.add('connected');
|
|
if (d2) d2.classList.add('connected');
|
|
const l1 = $id('conn-label'), l2 = $id('status-conn-text');
|
|
if (l1) l1.textContent = 'Live';
|
|
if (l2) l2.textContent = 'Connected';
|
|
};
|
|
|
|
es.onmessage = function(e) {
|
|
let evt;
|
|
try { evt = JSON.parse(e.data); } catch (x) { return; }
|
|
if (evt.type === 'connected' || evt.type === 'keepalive') return;
|
|
|
|
// Buffer into events array
|
|
S.events.unshift(evt);
|
|
if (S.events.length > 500) S.events.length = 500;
|
|
if (evt.id && evt.id > S.lastId) S.lastId = evt.id;
|
|
|
|
// Track event times for rate calculation
|
|
S._eventTimes.push(Date.now());
|
|
if (S._eventTimes.length > 1000) S._eventTimes = S._eventTimes.slice(-500);
|
|
|
|
// Update in-memory stats
|
|
if (S.stats.by_severity) {
|
|
S.stats.by_severity[evt.severity] = (S.stats.by_severity[evt.severity] || 0) + 1;
|
|
}
|
|
if (S.stats.by_type) {
|
|
S.stats.by_type[evt.event_type] = (S.stats.by_type[evt.event_type] || 0) + 1;
|
|
}
|
|
S.stats.total_events = (S.stats.total_events || 0) + 1;
|
|
|
|
// Notify app-level callbacks
|
|
if (_onNewEvent) _onNewEvent(evt);
|
|
if (_onStatsUpdate) _onStatsUpdate();
|
|
|
|
// Update events badge
|
|
const badge = $id('nav-badge-events');
|
|
if (badge && S.tab !== 'timeline') {
|
|
const n = parseInt(badge.textContent || '0') + 1;
|
|
badge.textContent = n;
|
|
badge.style.display = 'inline-flex';
|
|
}
|
|
|
|
// Alert sounds
|
|
if (S.soundEnabled && (evt.severity === 'CRITICAL' || evt.severity === 'HIGH')) {
|
|
playAlertSound(evt.severity);
|
|
}
|
|
|
|
// Browser notifications
|
|
if (S.notifEnabled && (evt.severity === 'CRITICAL' || evt.severity === 'HIGH')) {
|
|
try {
|
|
if (Notification.permission === 'granted') {
|
|
new Notification('ESPILON Alert — ' + evt.severity, {
|
|
body: (evt.detail || evt.event_type || 'New event').substring(0, 120),
|
|
icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="%231a1a28"/><circle cx="16" cy="16" r="9" fill="none" stroke="%23a855f7" stroke-width="1.5"/></svg>',
|
|
tag: 'espilon-' + (evt.id || Date.now()),
|
|
silent: true
|
|
});
|
|
}
|
|
} catch (x) { /* Notification API not available */ }
|
|
}
|
|
};
|
|
|
|
es.onerror = function() {
|
|
S.sseConnected = false;
|
|
es.close();
|
|
const d1 = $id('conn-dot'), d2 = $id('status-conn-dot');
|
|
if (d1) d1.classList.remove('connected');
|
|
if (d2) d2.classList.remove('connected');
|
|
const l1 = $id('conn-label'), l2 = $id('status-conn-text');
|
|
if (l1) l1.textContent = 'SSE';
|
|
if (l2) l2.textContent = 'Disconnected';
|
|
S._sseRetry++;
|
|
const delay = Math.min(1000 * Math.pow(2, S._sseRetry), 30000);
|
|
setTimeout(connectSSE, delay);
|
|
};
|
|
}
|