espilon-source/tools/C3PO/hp_dashboard/static/hp/js/app.js
Eun0us 79c2a4d4bf c3po: full server rewrite with modular routes and honeypot dashboard
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.
2026-02-28 20:12:27 +01:00

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);
});