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.
341 lines
18 KiB
JavaScript
341 lines
18 KiB
JavaScript
/* ESPILON Honeypot Dashboard — Events Tab + Attacker Modal */
|
|
|
|
import { S } from './state.js';
|
|
import { $id, escHtml, formatTime, sevClass, layerForType, layerColor, countryFlag, debounce, emptyState, skeletonRows } from './utils.js';
|
|
import { api } from './api.js';
|
|
import { showToast, closeDetail, closeModal } from './ui.js';
|
|
|
|
// ── Events Tab ──────────────────────────────────────────────
|
|
|
|
const EVENTS_PAGE_SIZE = 50;
|
|
let _eventsHasMore = true;
|
|
|
|
export function renderEvents() {
|
|
const sb = $id('search-bar');
|
|
if (sb) sb.style.display = '';
|
|
const ml = $id('main-list');
|
|
if (!ml) return;
|
|
if (!S.events || !S.events.length) {
|
|
ml.innerHTML = emptyState('\uD83D\uDCCA', 'No events recorded', 'Events will appear here as they are detected');
|
|
return;
|
|
}
|
|
let html = '';
|
|
for (let i = 0; i < S.events.length; i++) {
|
|
html += buildEventRow(S.events[i]);
|
|
}
|
|
if (_eventsHasMore) {
|
|
html += '<div class="ev-load-more"><button class="btn btn-sm" data-action="load-more-events">Load More</button></div>';
|
|
}
|
|
ml.innerHTML = html;
|
|
}
|
|
|
|
export function buildEventRow(evt) {
|
|
const layer = layerForType(evt.event_type);
|
|
let detail = evt.detail || '';
|
|
if (detail.length > 80) detail = detail.substring(0, 80) + '\u2026';
|
|
let mitre = '';
|
|
if (evt.mitre_techniques) {
|
|
mitre = buildMitreBadges(evt.mitre_techniques);
|
|
}
|
|
const safeId = parseInt(evt.id) || 0;
|
|
return '<div class="ev-row" data-action="detail" data-id="' + safeId + '">'
|
|
+ '<div class="ev-row-line1">'
|
|
+ '<span class="ev-time">' + formatTime(evt.timestamp) + '</span>'
|
|
+ '<span class="ev-layer" style="color:' + layerColor(layer) + '">[' + layer + ']</span>'
|
|
+ '<span class="ev-service">' + escHtml(evt.service || '') + '</span>'
|
|
+ '<span class="ev-type">' + escHtml(evt.event_type) + '</span>'
|
|
+ '<span class="ev-severity ' + sevClass(evt.severity) + '">' + escHtml(evt.severity) + '</span>'
|
|
+ '</div>'
|
|
+ '<div class="ev-row-line2">'
|
|
+ '<span class="ev-ip" data-action="attacker" data-ip="' + escHtml(evt.src_ip) + '">' + escHtml(evt.src_ip) + '</span>'
|
|
+ '<span class="ev-detail">' + escHtml(detail) + '</span>'
|
|
+ '<span class="ev-mitre">' + mitre + '</span>'
|
|
+ '</div>'
|
|
+ '</div>';
|
|
}
|
|
|
|
export function buildMitreBadges(mitreJson) {
|
|
let techniques;
|
|
if (!mitreJson) return '';
|
|
try {
|
|
if (typeof mitreJson === 'string') {
|
|
techniques = JSON.parse(mitreJson);
|
|
} else {
|
|
techniques = mitreJson;
|
|
}
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
if (!techniques || !techniques.length) return '';
|
|
let html = '';
|
|
for (let i = 0; i < techniques.length; i++) {
|
|
const t = techniques[i];
|
|
const tid = typeof t === 'string' ? t : (t.technique_id || t.id || t);
|
|
const name = typeof t === 'object' ? (t.name || t.technique_name || '') : '';
|
|
const tactic = typeof t === 'object' ? (t.tactic || '') : '';
|
|
let title = tid;
|
|
if (name) title += ' - ' + name;
|
|
if (tactic) title += ' (' + tactic + ')';
|
|
html += '<span class="mitre-tag" title="' + escHtml(title) + '">' + escHtml(String(tid)) + '</span>';
|
|
}
|
|
return html;
|
|
}
|
|
|
|
export function prependEventRow(evt) {
|
|
if (S.tab !== 'timeline') return;
|
|
const ml = $id('main-list');
|
|
if (!ml) return;
|
|
const empty = ml.querySelector('.empty-state');
|
|
if (empty) ml.innerHTML = '';
|
|
const wrapper = document.createElement('div');
|
|
wrapper.innerHTML = buildEventRow(evt);
|
|
const el = wrapper.firstChild;
|
|
if (el) {
|
|
el.classList.add('flash');
|
|
ml.insertBefore(el, ml.firstChild);
|
|
setTimeout(function() { el.classList.remove('flash'); }, 2000);
|
|
}
|
|
}
|
|
|
|
// ── Search & Filters ────────────────────────────────────────
|
|
|
|
const _debouncedFilter = debounce(() => applyFilters(), 300);
|
|
|
|
export function onSearchKey(e) {
|
|
if (!e) { applyFilters(); return; }
|
|
if (e.key === 'Enter') applyFilters();
|
|
else if (e.key === 'Escape') { e.target.value = ''; applyFilters(); }
|
|
else _debouncedFilter();
|
|
}
|
|
|
|
function _buildFilterParams() {
|
|
const params = new URLSearchParams();
|
|
const q = ($id('search-input')?.value || '').trim();
|
|
const type = $id('f-type')?.value;
|
|
const sev = $id('f-sev')?.value;
|
|
const service = $id('f-service')?.value;
|
|
const ip = ($id('f-ip')?.value || '').trim();
|
|
if (q) params.set('q', q);
|
|
if (type) params.set('type', type);
|
|
if (sev) params.set('severity', sev);
|
|
if (service) params.set('service', service);
|
|
if (ip) params.set('ip', ip);
|
|
return params;
|
|
}
|
|
|
|
export async function applyFilters() {
|
|
const params = _buildFilterParams();
|
|
params.set('limit', String(EVENTS_PAGE_SIZE));
|
|
const data = await api('/api/honeypot/search?' + params.toString());
|
|
if (data) {
|
|
S.events = data.events || [];
|
|
_eventsHasMore = S.events.length >= EVENTS_PAGE_SIZE;
|
|
renderEvents();
|
|
}
|
|
}
|
|
|
|
export async function loadMoreEvents() {
|
|
const params = _buildFilterParams();
|
|
params.set('limit', String(EVENTS_PAGE_SIZE));
|
|
params.set('offset', String(S.events.length));
|
|
const data = await api('/api/honeypot/search?' + params.toString());
|
|
if (data && data.events && data.events.length) {
|
|
S.events.push(...data.events);
|
|
_eventsHasMore = data.events.length >= EVENTS_PAGE_SIZE;
|
|
renderEvents();
|
|
} else {
|
|
_eventsHasMore = false;
|
|
/* Remove the Load More button */
|
|
const btn = document.querySelector('[data-action="load-more-events"]');
|
|
if (btn && btn.parentElement) btn.parentElement.remove();
|
|
}
|
|
}
|
|
|
|
export function toggleFilters() {
|
|
$id('filter-panel')?.classList.toggle('active');
|
|
}
|
|
|
|
// ── Event Detail Panel ──────────────────────────────────────
|
|
|
|
export async function showDetail(eventId) {
|
|
const panel = $id('detail-panel'), body = $id('detail-body');
|
|
if (!panel || !body) return;
|
|
panel.classList.add('open');
|
|
body.innerHTML = skeletonRows(6);
|
|
const evt = await api('/api/honeypot/events/' + eventId);
|
|
if (!evt) { body.innerHTML = '<p class="text-error">Failed to load event</p>'; return; }
|
|
let html = '';
|
|
// Connection
|
|
html += '<div class="detail-group"><h4 class="detail-group-title">Connection</h4><div class="detail-fields">';
|
|
if (evt.src_ip) html += '<div class="detail-field detail-field-col"><span class="detail-label">Source IP</span><span class="detail-value clickable" data-action="attacker" data-ip="' + escHtml(evt.src_ip) + '">' + escHtml(evt.src_ip) + '</span></div>';
|
|
if (evt.src_port) html += '<div class="detail-field detail-field-col"><span class="detail-label">Src Port</span><span class="detail-value">' + escHtml(String(evt.src_port)) + '</span></div>';
|
|
if (evt.dst_port) html += '<div class="detail-field detail-field-col"><span class="detail-label">Dst Port</span><span class="detail-value">' + escHtml(String(evt.dst_port)) + '</span></div>';
|
|
if (evt.service) html += '<div class="detail-field detail-field-col"><span class="detail-label">Service</span><span class="detail-value">' + escHtml(evt.service) + '</span></div>';
|
|
if (evt.session_id) html += '<div class="detail-field detail-field-col"><span class="detail-label">Session</span><span class="detail-value">' + escHtml(evt.session_id) + '</span></div>';
|
|
html += '</div></div>';
|
|
// Event
|
|
html += '<div class="detail-group"><h4 class="detail-group-title">Event</h4><div class="detail-fields">';
|
|
html += '<div class="detail-field detail-field-col"><span class="detail-label">Type</span><span class="detail-value">' + escHtml(evt.event_type) + '</span></div>';
|
|
html += '<div class="detail-field detail-field-col"><span class="detail-label">Severity</span><span class="detail-value ' + sevClass(evt.severity) + '">' + escHtml(evt.severity) + '</span></div>';
|
|
html += '<div class="detail-field detail-field-col"><span class="detail-label">Time</span><span class="detail-value">' + formatTime(evt.timestamp) + '</span></div>';
|
|
if (evt.device_id) html += '<div class="detail-field detail-field-col"><span class="detail-label">Device</span><span class="detail-value">' + escHtml(evt.device_id) + '</span></div>';
|
|
if (evt.detail) html += '<div class="detail-field detail-field-full"><span class="detail-label">Detail</span><span class="detail-value">' + escHtml(evt.detail) + '</span></div>';
|
|
html += '</div></div>';
|
|
// Auth
|
|
if (evt.username) {
|
|
html += '<div class="detail-group"><h4 class="detail-group-title">Authentication</h4><div class="detail-fields">';
|
|
html += '<div class="detail-field detail-field-col"><span class="detail-label">Username</span><span class="detail-value">' + escHtml(evt.username) + '</span></div>';
|
|
if (evt.password) html += '<div class="detail-field detail-field-col"><span class="detail-label">Password</span><span class="detail-value">' + escHtml(evt.password) + '</span></div>';
|
|
html += '</div></div>';
|
|
}
|
|
// Payload
|
|
if (evt.command || evt.url || evt.path) {
|
|
html += '<div class="detail-group"><h4 class="detail-group-title">Payload</h4><div class="detail-fields">';
|
|
if (evt.command) html += '<div class="detail-field detail-field-full"><span class="detail-label">Command</span><span class="detail-value text-accent">' + escHtml(evt.command) + '</span></div>';
|
|
if (evt.url) html += '<div class="detail-field detail-field-full"><span class="detail-label">URL</span><span class="detail-value">' + escHtml(evt.url) + '</span></div>';
|
|
if (evt.path) html += '<div class="detail-field detail-field-col"><span class="detail-label">Path</span><span class="detail-value">' + escHtml(evt.path) + '</span></div>';
|
|
html += '</div></div>';
|
|
}
|
|
// Tags
|
|
if (evt.malware_tag || evt.os_tag) {
|
|
html += '<div class="detail-group"><h4 class="detail-group-title">Tags</h4><div class="detail-fields">';
|
|
if (evt.malware_tag) html += '<span class="mitre-tag mitre-tag-malware">' + escHtml(evt.malware_tag) + '</span>';
|
|
if (evt.os_tag) html += '<span class="mitre-tag">' + escHtml(evt.os_tag) + '</span>';
|
|
html += '</div></div>';
|
|
}
|
|
// MITRE
|
|
if (evt.mitre_techniques) {
|
|
html += '<div class="detail-group"><h4 class="detail-group-title">MITRE ATT&CK</h4><div class="detail-fields">';
|
|
html += buildMitreBadges(evt.mitre_techniques);
|
|
html += '</div></div>';
|
|
}
|
|
// Related
|
|
if (evt.related && evt.related.length) {
|
|
html += '<div class="detail-group"><h4 class="detail-group-title">Related Events</h4>';
|
|
evt.related.slice(0, 10).forEach(r => {
|
|
html += '<div class="ev-row" data-action="detail" data-id="' + (parseInt(r.id) || 0) + '">#' + (parseInt(r.id) || 0) + ' ' + escHtml(r.event_type) + ' \u2014 ' + escHtml(r.severity) + ' \u2014 ' + formatTime(r.timestamp) + '</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
// Copy JSON
|
|
window._detailEvtJson = JSON.stringify(evt, null, 2);
|
|
html += '<div class="detail-copy-row"><button class="btn btn-sm btn-ghost" data-action="copy-json">Copy JSON</button></div>';
|
|
body.innerHTML = html;
|
|
}
|
|
|
|
// ── Export ───────────────────────────────────────────────────
|
|
|
|
export function exportData(format) {
|
|
const params = new URLSearchParams();
|
|
params.set('format', format);
|
|
const q = ($id('search-input')?.value || '').trim();
|
|
if (q) params.set('q', q);
|
|
if ($id('f-type')?.value) params.set('type', $id('f-type').value);
|
|
if ($id('f-sev')?.value) params.set('severity', $id('f-sev').value);
|
|
if ($id('f-service')?.value) params.set('service', $id('f-service').value);
|
|
const ip = ($id('f-ip')?.value || '').trim();
|
|
if (ip) params.set('ip', ip);
|
|
window.open('/api/honeypot/export?' + params.toString());
|
|
}
|
|
|
|
// ── Attacker Modal ──────────────────────────────────────────
|
|
|
|
export async function showAttackerModal(ip) {
|
|
const modal = $id('attacker-modal'), body = $id('modal-body');
|
|
if (!modal || !body) return;
|
|
modal.classList.add('open');
|
|
$id('modal-title').textContent = ip;
|
|
body.innerHTML = skeletonRows(5);
|
|
const data = await api('/api/honeypot/attacker/' + ip);
|
|
if (!data) { body.innerHTML = '<p>Not found</p>'; return; }
|
|
const credCount = data.credentials?.length || 0;
|
|
const cmdCount = data.commands?.length || 0;
|
|
const sessCount = data.sessions?.length || 0;
|
|
let html = '';
|
|
// Header badges
|
|
html += '<div class="modal-header-badges">';
|
|
html += '<span class="modal-header-ip">' + escHtml(data.ip) + '</span>';
|
|
if (data.vendor) html += '<span class="modal-header-vendor">' + escHtml(data.vendor) + '</span>';
|
|
if (data.country_code) html += '<span class="modal-header-country">' + countryFlag(data.country_code) + ' ' + escHtml(data.country || data.country_code) + '</span>';
|
|
html += '</div>';
|
|
// Stats grid
|
|
html += '<div class="modal-stat-grid">';
|
|
[{v: data.total_events || 0, l: 'Events'}, {v: credCount, l: 'Credentials'}, {v: cmdCount, l: 'Commands'}, {v: sessCount, l: 'Sessions'}].forEach(s => {
|
|
html += '<div class="modal-stat-card"><div class="modal-stat-val">' + s.v + '</div><div class="modal-stat-label">' + s.l + '</div></div>';
|
|
});
|
|
html += '</div>';
|
|
// Geo
|
|
if (data.is_private) {
|
|
html += '<div class="modal-info-block"><div class="modal-info-label">Network</div><div class="modal-info-row">Private Network (LAN)</div>';
|
|
if (data.vendor) html += '<div class="modal-info-row">Vendor: ' + escHtml(data.vendor) + '</div>';
|
|
html += '</div>';
|
|
} else if (data.country || data.city || data.isp) {
|
|
html += '<div class="modal-info-block"><div class="modal-info-label">Geo / Threat Intel</div>';
|
|
if (data.country) html += '<div class="modal-info-row">' + countryFlag(data.country_code) + ' ' + escHtml(data.country) + '</div>';
|
|
if (data.city) html += '<div class="modal-info-row">City: ' + escHtml(data.city) + '</div>';
|
|
if (data.isp) html += '<div class="modal-info-row">ISP: ' + escHtml(data.isp) + '</div>';
|
|
if (data.vendor) html += '<div class="modal-info-row">Vendor: ' + escHtml(data.vendor) + '</div>';
|
|
html += '</div>';
|
|
}
|
|
// Tabs
|
|
const defaultTab = credCount > 0 ? 'creds' : 'events';
|
|
html += '<div class="modal-tabs-bar">';
|
|
['creds', 'cmds', 'sess', 'events'].forEach(t => {
|
|
const labels = {creds: 'Credentials (' + credCount + ')', cmds: 'Commands (' + cmdCount + ')', sess: 'Sessions (' + sessCount + ')', events: 'Events'};
|
|
html += '<button class="modal-tab-btn' + (t === defaultTab ? ' active' : '') + '" data-tab="' + t + '" data-action="modal-tab">' + labels[t] + '</button>';
|
|
});
|
|
html += '</div>';
|
|
// Creds panel
|
|
html += '<div class="modal-tab-panel" data-tab="creds" style="display:' + (defaultTab === 'creds' ? '' : 'none') + '">';
|
|
if (credCount) {
|
|
html += '<table class="ov-table"><thead><tr><th>User</th><th>Pass</th><th>Svc</th><th>Time</th></tr></thead><tbody>';
|
|
data.credentials.forEach(c => {
|
|
html += '<tr><td class="font-mono">' + escHtml(c.username || '') + '</td><td class="font-mono">' + escHtml(c.password || '') + '</td><td>' + escHtml(c.service || '') + '</td><td>' + formatTime(c.timestamp) + '</td></tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
} else html += emptyState('\uD83D\uDD12', 'No credentials', '');
|
|
html += '</div>';
|
|
// Cmds panel
|
|
html += '<div class="modal-tab-panel" data-tab="cmds" style="display:' + (defaultTab === 'cmds' ? '' : 'none') + '">';
|
|
if (cmdCount) {
|
|
html += '<table class="ov-table"><thead><tr><th>Command</th><th>Svc</th><th>Time</th></tr></thead><tbody>';
|
|
data.commands.forEach(c => {
|
|
html += '<tr><td class="font-mono text-accent">' + escHtml(c.command || '') + '</td><td>' + escHtml(c.service || '') + '</td><td>' + formatTime(c.timestamp) + '</td></tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
} else html += emptyState('\uD83D\uDCBB', 'No commands', '');
|
|
html += '</div>';
|
|
// Sessions panel
|
|
html += '<div class="modal-tab-panel" data-tab="sess" style="display:' + (defaultTab === 'sess' ? '' : 'none') + '">';
|
|
if (sessCount) {
|
|
data.sessions.forEach(s => {
|
|
html += '<div class="modal-session-card" data-action="replay" data-session="' + escHtml(s.session_id) + '" data-ip="' + escHtml(data.ip) + '" data-service="' + escHtml(s.service || '') + '">'
|
|
+ '<div class="modal-session-card-top"><span class="fw-600">' + escHtml(s.service || '') + ' session</span><span class="' + sevClass(s.max_sev) + '">' + (s.event_count || 0) + ' events</span></div>'
|
|
+ '<div class="modal-session-card-time">' + formatTime(s.start) + ' \u2014 ' + formatTime(s.end) + '</div></div>';
|
|
});
|
|
} else html += emptyState('\uD83D\uDD17', 'No sessions', '');
|
|
html += '</div>';
|
|
// Events panel
|
|
html += '<div class="modal-tab-panel" data-tab="events" style="display:' + (defaultTab === 'events' ? '' : 'none') + '">';
|
|
if (data.events?.length) {
|
|
data.events.slice(0, 20).forEach(ev => {
|
|
html += '<div class="ev-row" data-action="detail" data-id="' + (parseInt(ev.id) || 0) + '">'
|
|
+ '<span class="' + sevClass(ev.severity) + ' modal-ev-sev">' + escHtml(ev.severity || '') + '</span>'
|
|
+ '<span class="modal-ev-type">' + escHtml(ev.event_type || '') + ' ' + escHtml((ev.detail || '').substring(0, 50)) + '</span>'
|
|
+ '<span class="modal-ev-time">' + formatTime(ev.timestamp) + '</span></div>';
|
|
});
|
|
} else html += emptyState('\uD83D\uDCCB', 'No events', '');
|
|
html += '</div>';
|
|
body.innerHTML = html;
|
|
}
|
|
|
|
export function switchModalTab(tabName) {
|
|
document.querySelectorAll('.modal-tab-btn').forEach(b => {
|
|
const active = b.dataset.tab === tabName;
|
|
b.classList.toggle('active', active);
|
|
});
|
|
document.querySelectorAll('.modal-tab-panel').forEach(p => {
|
|
p.style.display = p.dataset.tab === tabName ? '' : 'none';
|
|
});
|
|
}
|