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.
308 lines
15 KiB
JavaScript
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');
|
|
}
|
|
}
|