espilon-source/tools/C3PO/hp_dashboard/static/hp/js/overview.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

308 lines
15 KiB
JavaScript

/* ESPILON Honeypot Dashboard — Overview Tab */
import { S, SERVICES, MONITORS } from './state.js';
import { $id, escHtml, formatTime, countryFlag, sevClass, layerForType, layerColor, animateCounter, emptyState } from './utils.js';
import { renderTimeline, renderSevDonut } from './charts.js';
import { postApi, fetchAll } from './api.js';
import { showToast } from './ui.js';
const DEVICE_IMG = '/hp-static/hp/img/floating.png';
// ── Device Cards ────────────────────────────────────────────
function renderDeviceCards() {
if (!S.devices.length) {
return emptyState('\uD83D\uDCE1', 'No honeypot devices connected', 'Connect an ESP32 honeypot to start monitoring');
}
let html = '';
S.devices.forEach(d => {
// BUG FIX: case-insensitive status comparison
const st = (d.status || '').toLowerCase();
const isOnline = st === 'online' || st === 'connected';
const dotColor = isOnline ? 'var(--status-success)' : 'var(--sev-high)';
const statusTxt = isOnline ? 'Online' : 'Offline';
const lastSeen = d.last_seen ? formatTime(d.last_seen) : '--';
let runCount = 0, totalSvc = 0;
const devSvc = S.services[d.id] || {};
for (const sn in devSvc) { totalSvc++; if (devSvc[sn] && devSvc[sn].running) runCount++; }
let evtCount = 0;
S.events.forEach(ev => { if (ev.device_id === d.id) evtCount++; });
const sevCounts = {CRITICAL:0, HIGH:0, MEDIUM:0, LOW:0};
S.events.forEach(ev => { if (ev.device_id === d.id && sevCounts[ev.severity] !== undefined) sevCounts[ev.severity]++; });
html += '<div class="device-card">';
html += '<div class="dev-card-header">';
html += '<div class="dev-card-thumb">';
html += '<img src="' + DEVICE_IMG + '" alt="Honeypot">';
html += '<div class="dev-card-dot" style="background:' + dotColor + ';box-shadow:0 0 6px ' + dotColor + '"></div>';
html += '</div>';
html += '<div class="dev-card-info">';
html += '<div class="dev-card-name">' + escHtml(d.id) + '</div>';
html += '<div class="dev-card-ip">' + escHtml(d.ip || 'unknown') + '</div>';
html += '<div class="dev-card-status"><span class="dev-card-status-dot" style="background:' + dotColor + '"></span>' + statusTxt + '</div>';
html += '<div class="dev-card-lastseen">Last seen: ' + lastSeen + '</div>';
html += '</div></div>';
// Stats bar
html += '<div class="dev-card-stats">';
html += '<div class="dev-card-stat"><div class="dev-card-stat-val">' + runCount + '/' + totalSvc + '</div><div class="dev-card-stat-label">Services</div></div>';
html += '<div class="dev-card-stat"><div class="dev-card-stat-val text-accent">' + evtCount + '</div><div class="dev-card-stat-label">Events</div></div>';
html += '<div class="dev-card-stat"><div class="dev-card-stat-val text-crit">' + sevCounts.CRITICAL + '</div><div class="dev-card-stat-label">Critical</div></div>';
html += '</div>';
// Severity mini-bar
const sevTotal = Object.values(sevCounts).reduce((a, b) => a + b, 0) || 1;
html += '<div class="dev-card-severity-bar">';
if (sevCounts.CRITICAL) html += '<div style="flex:' + (sevCounts.CRITICAL / sevTotal) + ';background:var(--sev-crit)"></div>';
if (sevCounts.HIGH) html += '<div style="flex:' + (sevCounts.HIGH / sevTotal) + ';background:var(--sev-high)"></div>';
if (sevCounts.MEDIUM) html += '<div style="flex:' + (sevCounts.MEDIUM / sevTotal) + ';background:var(--sev-med)"></div>';
if (sevCounts.LOW) html += '<div style="flex:' + (sevCounts.LOW / sevTotal) + ';background:var(--sev-low)"></div>';
html += '</div>';
html += '</div>';
});
return html;
}
// ── Top Attackers Table ─────────────────────────────────────
function renderTopAttackersTable() {
const top = S.attackers.slice(0, 8);
if (!top.length) return emptyState('\uD83D\uDC64', 'No attackers', 'Attacker data will appear here');
let html = '<table class="ov-table"><tr><th>#</th><th>IP</th><th>Vendor</th><th>Count</th></tr>';
top.forEach((a, i) => {
const flag = countryFlag(a.country_code);
html += '<tr class="clickable" data-action="attacker" data-ip="' + escHtml(a.ip) + '">'
+ '<td>' + (i + 1) + '</td>'
+ '<td class="font-mono">' + (flag ? flag + ' ' : '') + escHtml(a.ip) + '</td>'
+ '<td>' + escHtml(a.vendor || '-') + '</td>'
+ '<td class="text-right text-accent fw-600">' + (a.count || 0) + '</td></tr>';
});
return html + '</table>';
}
// ── Service Grid ────────────────────────────────────────────
function renderServiceGrid() {
let svcStatus = {};
for (const devId in S.services) {
const ds = S.services[devId];
for (const sn in ds) if (!svcStatus[sn] || ds[sn].running) svcStatus[sn] = ds[sn];
}
let html = '<div class="svc-grid">';
Object.keys(SERVICES).forEach(name => {
const running = svcStatus[name]?.running;
html += '<div class="svc-card">'
+ '<span class="svc-indicator ' + (running ? 'on' : 'off') + '"></span>'
+ '<span class="svc-name">' + escHtml(name) + '</span>'
+ '<span class="svc-port">' + escHtml(String(SERVICES[name].port)) + '</span>'
+ '<button class="svc-toggle ' + (running ? 'svc-toggle-stop' : 'svc-toggle-start') + '" '
+ 'data-action="toggle-service" data-name="' + escHtml(name) + '" '
+ 'data-running="' + (running ? '1' : '0') + '">'
+ (running ? 'Stop' : 'Start') + '</button></div>';
});
MONITORS.forEach(name => {
const running = svcStatus[name]?.running;
html += '<div class="svc-card">'
+ '<span class="svc-indicator ' + (running ? 'on' : 'off') + '"></span>'
+ '<span class="svc-name">' + escHtml(name) + '</span>'
+ '<span class="svc-port">mon</span>'
+ '<button class="svc-toggle ' + (running ? 'svc-toggle-stop' : 'svc-toggle-start') + '" '
+ 'data-action="toggle-service" data-name="' + escHtml(name) + '" '
+ 'data-running="' + (running ? '1' : '0') + '">'
+ (running ? 'Stop' : 'Start') + '</button></div>';
});
return html + '</div>';
}
// ── Recent Events ───────────────────────────────────────────
function renderRecentEvents() {
const recent = S.events.slice(0, 10);
if (!recent.length) return emptyState('\uD83D\uDCCA', 'No events', 'Events will appear here as they are detected');
return '<div>' + recent.map(ev => {
const layer = layerForType(ev.event_type);
const detail = ev.detail ? (ev.detail.length > 60 ? ev.detail.slice(0, 60) + '...' : ev.detail) : '';
return '<div class="ev-row" data-action="detail" data-id="' + (parseInt(ev.id) || 0) + '">'
+ '<div class="ev-row-line1">'
+ '<span class="ev-time">' + formatTime(ev.timestamp) + '</span>'
+ '<span class="ev-layer" style="color:' + layerColor(layer) + '">' + layer + '</span>'
+ '<span class="ev-service">' + escHtml(ev.service || '-') + '</span>'
+ '<span class="ev-severity ' + sevClass(ev.severity) + '">' + escHtml(ev.severity) + '</span>'
+ '<span class="ev-ip">' + escHtml(ev.src_ip || '') + '</span>'
+ '<span class="ev-type">' + escHtml(detail) + '</span>'
+ '</div></div>';
}).join('') + '</div>';
}
// ── Overview Render ─────────────────────────────────────────
export function renderOverview() {
const ml = $id('main-list');
if (!ml) return;
const bs = S.stats.by_severity || {};
const totalEvts = S.stats.total_events || 0;
const critCount = bs.CRITICAL || 0;
let activeCount = 0;
for (const devId in S.services) for (const sn in S.services[devId]) if (S.services[devId][sn]?.running) activeCount++;
const kpis = [
{val: totalEvts, label: 'Total Events', color: 'var(--accent-primary)'},
{val: critCount, label: 'Critical', color: 'var(--sev-crit)'},
{val: S.attackers.length, label: 'Attackers', color: 'var(--status-warning)'},
{val: activeCount, label: 'Services', color: 'var(--status-success)'},
{val: S.alerts.filter(a => !a.acknowledged).length, label: 'Alerts', color: 'var(--accent-secondary)'}
];
let html = '<div class="overview-grid">';
html += '<div class="ov-kpi-row">';
kpis.forEach(k => {
html += '<div class="ov-kpi-card">'
+ '<div class="ov-kpi-val" style="color:' + k.color + '">' + k.val + '</div>'
+ '<div class="ov-kpi-label">' + k.label + '</div></div>';
});
html += '</div>';
// Device cards row
if (S.devices.length) {
html += '<div class="ov-section">';
html += '<div class="ov-section-title">\uD83D\uDCE1 Honeypot Devices</div>';
html += '<div class="ov-devices-grid">';
html += renderDeviceCards();
html += '</div></div>';
}
// Charts row
html += '<div class="ov-chart-row">';
html += '<div class="ov-section">';
html += '<div class="ov-section-title">Activity Timeline</div>';
html += '<div id="overview-timeline"></div></div>';
html += '<div class="ov-section">';
html += '<div class="ov-section-title">Severity</div>';
html += '<div id="overview-donut"></div></div></div>';
// Data row
html += '<div class="ov-2col">';
html += '<div class="ov-section">';
html += '<div class="ov-section-title">Top Attackers</div>';
html += renderTopAttackersTable() + '</div>';
html += '<div class="ov-section">';
html += '<div class="ov-section-title" style="display:flex;align-items:center;justify-content:space-between">Services';
html += '<span style="display:flex;gap:0.5rem">';
html += '<button data-action="start-all" class="btn btn-sm btn-start">Start All</button>';
html += '<button data-action="stop-all" class="btn btn-sm btn-stop">Stop All</button>';
html += '<button data-action="refresh-status" class="btn btn-sm btn-ghost">Refresh</button>';
html += '</span></div>';
html += renderServiceGrid() + '</div></div>';
// Recent events
html += '<div class="ov-section">';
html += '<div class="ov-section-title">Recent Events</div>';
html += renderRecentEvents() + '</div></div>';
ml.innerHTML = html;
renderTimeline($id('overview-timeline'), S.timeline, 100);
renderSevDonut($id('overview-donut'), bs);
}
// ── Header KPI Updates ──────────────────────────────────────
export function updateHeaderKpis() {
const bs = S.stats.by_severity || {};
animateCounter($id('kpi-events'), S.stats.total_events || 0);
animateCounter($id('kpi-critical'), bs.CRITICAL || 0);
animateCounter($id('kpi-attackers'), S.attackers.length);
const unacked = S.alerts.filter(a => !a.acknowledged).length;
animateCounter($id('kpi-alerts'), unacked);
}
// ── Device Select ───────────────────────────────────────────
export function renderDeviceSelect() {
const sel = $id('device-select');
if (!sel) return;
const cur = sel.value;
let html = '<option value="">All Devices</option>';
S.devices.forEach(d => {
html += '<option value="' + escHtml(d.id) + '"' + (d.id === cur ? ' selected' : '') + '>' + escHtml(d.id) + ' (' + escHtml(d.ip || '?') + ')</option>';
});
sel.innerHTML = html;
}
// ── Status Bar ──────────────────────────────────────────────
export function updateStatusBar() {
const dot = $id('status-conn-dot'), text = $id('status-conn-text');
if (dot) dot.classList.toggle('connected', S.sseConnected);
if (text) text.textContent = S.sseConnected ? 'Connected' : 'Disconnected';
const ref = $id('status-refresh');
if (ref) ref.textContent = new Date().toLocaleTimeString('en-GB');
const dev = $id('status-device');
if (dev) dev.textContent = S.selectedDevice || 'All Devices';
// Rate calc
const now = Date.now(), cutoff = now - 60000;
S._eventTimes = S._eventTimes.filter(t => t > cutoff);
S.eventRate = S._eventTimes.length;
const rEl = $id('status-rate');
if (rEl) rEl.textContent = S.eventRate + ' evt/min';
const dbEl = $id('status-db-count');
if (dbEl) dbEl.textContent = (S.stats.total_events || 0).toLocaleString() + ' stored';
}
// ── Alert Banner ────────────────────────────────────────────
export function updateAlertBanner() {
const banner = $id('alert-banner');
if (!banner) return;
const unacked = S.alerts.filter(a => !a.acknowledged);
if (!unacked.length) { banner.classList.remove('active'); return; }
banner.classList.add('active');
const txt = $id('alert-banner-text'), cnt = $id('alert-banner-count');
if (txt) txt.textContent = unacked[0].message || unacked[0].rule_name || 'Alert';
if (cnt) cnt.textContent = unacked.length > 1 ? '+' + (unacked.length - 1) + ' more' : '';
}
// ── Service Control Functions (migrated from config.js) ─────
export async function toggleService(name, currentlyRunning) {
if (!S.selectedDevice) {
showToast('Error', 'No device selected', 'Select a device first.', 'error');
return;
}
/* Loading state feedback */
const btn = document.querySelector('[data-action="toggle-service"][data-name="' + name + '"]');
if (btn) { btn.disabled = true; btn.textContent = '...'; }
const action = currentlyRunning ? 'stop' : 'start';
const cmd = 'hp_' + name + '_' + action;
const res = await postApi('/api/honeypot/command', {
device_id: S.selectedDevice,
command: cmd,
argv: []
});
if (res && res.ok) {
showToast('Command Sent', cmd, 'Request ID: ' + (res.request_id || '?'), 'success');
} else {
showToast('Error', 'Failed to send ' + cmd, '', 'error');
}
setTimeout(() => { if (btn) btn.disabled = false; fetchAll(); }, 2000);
}
export async function startAll() {
await postApi('/api/honeypot/start_all', { device_id: S.selectedDevice });
showToast('Services', 'Start all command sent', '', 'info');
setTimeout(fetchAll, 2000);
}
export async function stopAll() {
await postApi('/api/honeypot/stop_all', { device_id: S.selectedDevice });
showToast('Services', 'Stop all command sent', '', 'info');
setTimeout(fetchAll, 2000);
}
export async function refreshStatus() {
await postApi('/api/honeypot/refresh_status', { device_id: S.selectedDevice });
showToast('Status', 'Refresh requested', '', 'info');
setTimeout(fetchAll, 1000);
}
export async function ackAlert(id) {
const res = await postApi('/api/honeypot/alerts/ack/' + id, {});
if (res && res.ok) {
S.alerts = S.alerts.filter(a => a.id !== id);
showToast('Alert', 'Alert acknowledged', '', 'success');
} else {
showToast('Error', 'Failed to acknowledge alert', '', 'error');
}
}