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.
212 lines
7.3 KiB
JavaScript
212 lines
7.3 KiB
JavaScript
/* ESPILON Honeypot Dashboard — Sidebar Renders */
|
|
|
|
import { S } from './state.js';
|
|
import { $id, escHtml, formatTime, countryFlag, sevColor, layerForType, layerColor, emptyState } from './utils.js';
|
|
import { renderSevDonut } from './charts.js';
|
|
|
|
// ── Severity Stats ──────────────────────────────────────────
|
|
|
|
export function renderSevStats() {
|
|
const el = $id('sev-stats');
|
|
if (!el) return;
|
|
const bySev = (S.stats && S.stats.by_severity) ? S.stats.by_severity : {};
|
|
const sevs = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'];
|
|
const counts = [];
|
|
let total = 0;
|
|
for (let i = 0; i < sevs.length; i++) {
|
|
const c = bySev[sevs[i]] || 0;
|
|
counts.push(c);
|
|
total += c;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
// Donut placeholder
|
|
html += '<div id="sev-donut-sb" class="text-center mb-sm"></div>';
|
|
|
|
// Count list
|
|
html += '<div>';
|
|
for (let i = 0; i < sevs.length; i++) {
|
|
html += '<div class="sev-stat-row">';
|
|
html += '<span><span class="sev-stat-dot" style="background:' + sevColor(sevs[i]) + '"></span>' + sevs[i] + '</span>';
|
|
html += '<span class="sev-stat-count">' + counts[i] + '</span>';
|
|
html += '</div>';
|
|
}
|
|
html += '</div>';
|
|
|
|
el.innerHTML = html;
|
|
|
|
// Render donut into the container
|
|
const donutEl = $id('sev-donut-sb');
|
|
if (donutEl) {
|
|
const statsObj = {};
|
|
for (let i = 0; i < sevs.length; i++) statsObj[sevs[i]] = counts[i];
|
|
renderSevDonut(donutEl, statsObj);
|
|
}
|
|
}
|
|
|
|
// ── Layer Stats ─────────────────────────────────────────────
|
|
|
|
export function renderLayerStats() {
|
|
const el = $id('layer-stats');
|
|
if (!el) return;
|
|
|
|
const layerCounts = { L2: 0, L3: 0, L4: 0, L7: 0 };
|
|
if (S.events) {
|
|
for (let i = 0; i < S.events.length; i++) {
|
|
const l = layerForType(S.events[i].event_type);
|
|
if (layerCounts[l] !== undefined) layerCounts[l]++;
|
|
}
|
|
}
|
|
|
|
const layers = ['L2', 'L3', 'L4', 'L7'];
|
|
let html = '<div class="layer-grid">';
|
|
for (let i = 0; i < layers.length; i++) {
|
|
const l = layers[i];
|
|
html += '<div class="layer-box" style="border-top-color:' + layerColor(l) + '">';
|
|
html += '<div class="layer-box-val">' + layerCounts[l] + '</div>';
|
|
html += '<div class="layer-box-label">' + l + '</div>';
|
|
html += '</div>';
|
|
}
|
|
html += '</div>';
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
// ── Attackers ───────────────────────────────────────────────
|
|
|
|
export function renderAttackers() {
|
|
const el = $id('attackers-list');
|
|
if (!el) return;
|
|
|
|
const list = S.attackers || [];
|
|
const top = list.slice(0, 10);
|
|
if (!top.length) {
|
|
el.innerHTML = emptyState('\u{1F464}', 'No attackers yet', '');
|
|
return;
|
|
}
|
|
|
|
const maxCount = top[0].total_events || top[0].count || 1;
|
|
|
|
let html = '';
|
|
for (let i = 0; i < top.length; i++) {
|
|
const a = top[i];
|
|
const cnt = a.total_events || a.count || 0;
|
|
const pct = Math.round((cnt / maxCount) * 100);
|
|
const flag = a.country_code ? countryFlag(a.country_code) + ' ' : '';
|
|
const vendor = a.vendor || '';
|
|
|
|
html += '<div class="atk-row" data-action="attacker" data-ip="' + escHtml(a.ip) + '">';
|
|
html += '<div class="atk-row-top">';
|
|
html += '<span class="atk-row-ip">' + flag + escHtml(a.ip) + '</span>';
|
|
html += '<span class="atk-row-count">' + cnt + '</span>';
|
|
html += '</div>';
|
|
if (vendor) {
|
|
html += '<div class="atk-row-vendor">' + escHtml(vendor) + '</div>';
|
|
}
|
|
html += '<div class="atk-row-bar">';
|
|
html += '<div class="atk-row-bar-fill" style="width:' + pct + '%"></div>';
|
|
html += '</div>';
|
|
html += '</div>';
|
|
}
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
// ── Alerts ──────────────────────────────────────────────────
|
|
|
|
export function renderAlerts() {
|
|
const el = $id('alerts-list');
|
|
const badge = $id('alert-badge');
|
|
const banner = $id('alert-banner');
|
|
const bannerCount = $id('alert-banner-count');
|
|
|
|
const alerts = S.alerts || [];
|
|
const unacked = alerts.filter(function(a) { return !a.acknowledged; });
|
|
|
|
// Badge
|
|
if (badge) {
|
|
badge.textContent = unacked.length;
|
|
badge.style.display = unacked.length > 0 ? '' : 'none';
|
|
}
|
|
|
|
// Banner
|
|
if (banner) {
|
|
banner.style.display = unacked.length > 0 ? '' : 'none';
|
|
}
|
|
if (bannerCount) {
|
|
bannerCount.textContent = unacked.length;
|
|
}
|
|
|
|
// List
|
|
if (!el) return;
|
|
if (!alerts.length) {
|
|
el.innerHTML = emptyState('\u{1F514}', 'No alerts', '');
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
for (let i = 0; i < alerts.length; i++) {
|
|
const a = alerts[i];
|
|
const borderCol = sevColor(a.severity || 'MEDIUM');
|
|
const acked = a.acknowledged;
|
|
html += '<div class="alert-item' + (acked ? ' acked' : '') + '" style="border-left-color:' + borderCol + '">';
|
|
html += '<div class="alert-item-top">';
|
|
html += '<div class="alert-item-msg">' + escHtml(a.message || a.detail || 'Alert') + '</div>';
|
|
if (!acked) {
|
|
html += '<button class="btn-ack" data-action="ack-alert" data-id="' + a.id + '">ACK</button>';
|
|
}
|
|
html += '</div>';
|
|
if (a.timestamp) {
|
|
html += '<div class="alert-item-time">' + formatTime(a.timestamp) + '</div>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
// ── Command History ─────────────────────────────────────────
|
|
|
|
export function renderHistory() {
|
|
const el = $id('cmd-history');
|
|
if (!el) return;
|
|
|
|
const list = S.history || [];
|
|
const items = list.slice(0, 20);
|
|
if (!items.length) {
|
|
el.innerHTML = emptyState('\u{2328}\u{FE0F}', 'No commands yet', '');
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
for (let i = 0; i < items.length; i++) {
|
|
const h = items[i];
|
|
let statusDot;
|
|
if (h.status === 'completed') {
|
|
statusDot = 'var(--sev-low)';
|
|
} else if (h.status === 'pending' || h.status === 'sent') {
|
|
statusDot = 'var(--sev-med)';
|
|
} else {
|
|
statusDot = 'var(--sev-high)';
|
|
}
|
|
let cmdName = h.command_name || h.command || '?';
|
|
if (cmdName.length > 24) cmdName = cmdName.substring(0, 24) + '\u2026';
|
|
const timeStr = h.sent_at ? formatTime(h.sent_at) : '';
|
|
|
|
html += '<div class="hist-item">';
|
|
html += '<span class="hist-dot" style="background:' + statusDot + '"></span>';
|
|
html += '<span class="hist-cmd" title="' + escHtml(h.command_name || h.command || '') + '">' + escHtml(cmdName) + '</span>';
|
|
html += '<span class="hist-time">' + timeStr + '</span>';
|
|
html += '</div>';
|
|
}
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
// ── Full Sidebar Render ─────────────────────────────────────
|
|
|
|
export function renderSidebar() {
|
|
renderSevStats();
|
|
renderLayerStats();
|
|
renderAttackers();
|
|
renderAlerts();
|
|
renderHistory();
|
|
}
|