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.
216 lines
8.6 KiB
JavaScript
216 lines
8.6 KiB
JavaScript
/* ESPILON Honeypot Dashboard — Application Entry Point */
|
|
|
|
import { S } from './state.js';
|
|
import { $id } from './utils.js';
|
|
import { setToastHandler, setDefaultRender, fetchAll } from './api.js';
|
|
import { connectSSE, setSSECallbacks } from './sse.js';
|
|
import { showToast, closeDetail, closeModal, toggleSidebar, toggleNavMenu, toggleSbSection,
|
|
scrollToAlerts, restoreSidebarState, registerActions, setupEventDelegation, setupResponsive } from './ui.js';
|
|
import { setTabRenderers, switchTab, setupKeyboardShortcuts } from './router.js';
|
|
import { renderOverview, updateHeaderKpis, updateStatusBar, updateAlertBanner, renderDeviceSelect,
|
|
toggleService, startAll, stopAll, refreshStatus, ackAlert } from './overview.js';
|
|
import { renderEvents, prependEventRow, showDetail, showAttackerModal,
|
|
switchModalTab, onSearchKey, applyFilters, toggleFilters, exportData, loadMoreEvents } from './events.js';
|
|
import { renderSessions, openReplay, toggleReplayPlayback, replayStep, replayReset,
|
|
seekReplay, updateReplaySpeed, closeReplay } from './sessions.js';
|
|
import { renderCredentials } from './credentials.js';
|
|
import { renderKillChain, showKillChainDetail } from './killchain.js';
|
|
import { renderMitre } from './mitre.js';
|
|
import { renderSidebar } from './sidebar.js';
|
|
|
|
// ── Wire Dependencies ───────────────────────────────────────
|
|
|
|
// Break circular: api.js needs showToast from ui.js
|
|
setToastHandler(showToast);
|
|
|
|
// Default render callback for fetchAll() (so calls without explicit callback still re-render)
|
|
setDefaultRender(renderAll);
|
|
|
|
// Wire tab renderers into the router
|
|
setTabRenderers({
|
|
overview: renderAll,
|
|
timeline: renderEvents,
|
|
sessions: renderSessions,
|
|
credentials: renderCredentials,
|
|
killchain: renderKillChain,
|
|
mitre: renderMitre,
|
|
});
|
|
|
|
// Wire SSE callbacks
|
|
setSSECallbacks({
|
|
onNewEvent(evt) {
|
|
// Prepend row if on events tab
|
|
prependEventRow(evt);
|
|
// Update sidebar
|
|
renderSidebar();
|
|
// Update header KPIs
|
|
updateHeaderKpis();
|
|
updateAlertBanner();
|
|
},
|
|
onStatsUpdate() {
|
|
// Refresh overview KPIs without full re-render
|
|
updateHeaderKpis();
|
|
}
|
|
});
|
|
|
|
// ── Tab content renderers (main-list area) ──────────────────
|
|
|
|
const _contentRenderers = {
|
|
overview: renderOverview,
|
|
timeline: renderEvents,
|
|
sessions: renderSessions,
|
|
credentials: renderCredentials,
|
|
killchain: renderKillChain,
|
|
mitre: renderMitre,
|
|
};
|
|
|
|
// ── Render All (after fetchAll) ─────────────────────────────
|
|
|
|
function renderAll() {
|
|
renderDeviceSelect();
|
|
// Render the currently active tab, not always overview
|
|
const renderer = _contentRenderers[S.tab] || renderOverview;
|
|
renderer();
|
|
renderSidebar();
|
|
updateHeaderKpis();
|
|
updateAlertBanner();
|
|
updateStatusBar();
|
|
}
|
|
|
|
// ── Event Delegation: register all actions ──────────────────
|
|
|
|
registerActions({
|
|
'tab': (el) => switchTab(el.dataset.tab),
|
|
'detail': (el) => showDetail(parseInt(el.dataset.id)),
|
|
'attacker': (el) => showAttackerModal(el.dataset.ip),
|
|
'replay': (el) => openReplay(el.dataset.session, el.dataset.ip, el.dataset.service),
|
|
'killchain-detail': (el) => showKillChainDetail(el.dataset.ip),
|
|
'toggle-sidebar': () => toggleSidebar(),
|
|
'toggle-nav': () => toggleNavMenu(),
|
|
'toggle-section': (el) => toggleSbSection(el),
|
|
'close-detail': () => closeDetail(),
|
|
'close-modal': () => closeModal(),
|
|
'close-replay': () => closeReplay(),
|
|
'scroll-alerts': () => scrollToAlerts(),
|
|
'toggle-sound': () => {
|
|
S.soundEnabled = !S.soundEnabled;
|
|
const btn = $id('sound-btn');
|
|
if (btn) btn.style.opacity = S.soundEnabled ? '1' : '0.4';
|
|
showToast('Sound', S.soundEnabled ? 'Alert sounds enabled' : 'Alert sounds muted', '', 'info');
|
|
},
|
|
'toggle-notif': () => {
|
|
if (!S.notifEnabled && typeof Notification !== 'undefined' && Notification.permission !== 'granted') {
|
|
Notification.requestPermission().then(function(perm) {
|
|
if (perm === 'granted') {
|
|
S.notifEnabled = true;
|
|
var btn = $id('notif-btn');
|
|
if (btn) btn.style.opacity = '1';
|
|
showToast('Notifications', 'Browser notifications enabled', '', 'success');
|
|
} else {
|
|
showToast('Notifications', 'Permission denied by browser', '', 'warning');
|
|
}
|
|
});
|
|
} else {
|
|
S.notifEnabled = !S.notifEnabled;
|
|
var btn = $id('notif-btn');
|
|
if (btn) btn.style.opacity = S.notifEnabled ? '1' : '0.4';
|
|
showToast('Notifications', S.notifEnabled ? 'Browser notifications enabled' : 'Browser notifications disabled', '', 'info');
|
|
}
|
|
},
|
|
'toggle-service': (el) => toggleService(el.dataset.name, el.dataset.running === '1'),
|
|
'start-all': () => startAll(),
|
|
'stop-all': () => stopAll(),
|
|
'refresh-status': () => refreshStatus(),
|
|
'ack-alert': (el) => ackAlert(parseInt(el.dataset.id)),
|
|
'toggle-filters': () => toggleFilters(),
|
|
'load-more-events': () => loadMoreEvents(),
|
|
'export-csv': () => exportData('csv'),
|
|
'export-json': () => exportData('json'),
|
|
'dismiss-banner': (el, e) => {
|
|
e.stopPropagation();
|
|
$id('alert-banner')?.classList.remove('active');
|
|
},
|
|
'replay-play': () => toggleReplayPlayback(),
|
|
'replay-step': () => replayStep(),
|
|
'replay-reset': () => replayReset(),
|
|
'replay-seek': (el, e) => seekReplay(e),
|
|
'modal-tab': (el) => switchModalTab(el.dataset.tab),
|
|
'copy-json': () => {
|
|
if (window._detailEvtJson) {
|
|
navigator.clipboard.writeText(window._detailEvtJson);
|
|
showToast('Copied', 'Event JSON copied', '', 'success');
|
|
}
|
|
},
|
|
});
|
|
|
|
// ── Init ────────────────────────────────────────────────────
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
restoreSidebarState();
|
|
setupEventDelegation();
|
|
setupResponsive();
|
|
|
|
// Form element listeners (change/keyup — not click-based)
|
|
const devSel = $id('device-select');
|
|
if (devSel) devSel.addEventListener('change', () => {
|
|
S.selectedDevice = devSel.value;
|
|
fetchAll(renderAll);
|
|
});
|
|
|
|
const searchIn = $id('search-input');
|
|
if (searchIn) searchIn.addEventListener('keyup', () => onSearchKey());
|
|
|
|
const searchClear = $id('search-clear');
|
|
if (searchClear) searchClear.addEventListener('click', () => {
|
|
if (searchIn) { searchIn.value = ''; onSearchKey(); searchIn.focus(); }
|
|
});
|
|
|
|
['f-type', 'f-sev', 'f-service'].forEach(id => {
|
|
const el = $id(id);
|
|
if (el) el.addEventListener('change', () => applyFilters());
|
|
});
|
|
|
|
const fIp = $id('f-ip');
|
|
if (fIp) fIp.addEventListener('change', () => applyFilters());
|
|
|
|
const replaySpd = $id('replay-speed');
|
|
if (replaySpd) replaySpd.addEventListener('change', () => updateReplaySpeed());
|
|
|
|
const sevFilter = $id('sse-sev-filter');
|
|
if (sevFilter) sevFilter.addEventListener('change', () => {
|
|
S.minSeverity = sevFilter.value;
|
|
connectSSE();
|
|
});
|
|
|
|
setupKeyboardShortcuts({
|
|
onEscape() { closeDetail(); closeModal(); closeReplay(); },
|
|
onRefresh() { refreshStatus(); },
|
|
onToggleSound() {
|
|
S.soundEnabled = !S.soundEnabled;
|
|
const btn = $id('sound-btn');
|
|
if (btn) btn.style.opacity = S.soundEnabled ? '1' : '0.4';
|
|
showToast('Sound', S.soundEnabled ? 'Alert sounds enabled' : 'Alert sounds muted', '', 'info');
|
|
}
|
|
});
|
|
fetchAll(renderAll);
|
|
connectSSE();
|
|
|
|
/* Conditional auto-refresh: only re-render if data actually changed */
|
|
let _lastDataHash = '';
|
|
function renderIfChanged() {
|
|
const hash = JSON.stringify([
|
|
S.stats.total_events,
|
|
S.events.length,
|
|
S.alerts.length,
|
|
S.devices.length,
|
|
Object.keys(S.services).length
|
|
]);
|
|
if (hash !== _lastDataHash) {
|
|
_lastDataHash = hash;
|
|
renderAll();
|
|
}
|
|
}
|
|
S._refreshTimer = setInterval(() => fetchAll(renderIfChanged), 30000);
|
|
setInterval(updateStatusBar, 1000);
|
|
});
|