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.
153 lines
7.0 KiB
JavaScript
153 lines
7.0 KiB
JavaScript
/* ESPILON Honeypot Dashboard — Sessions Tab + Replay */
|
|
|
|
import { S } from './state.js';
|
|
import { $id, escHtml, formatTime, formatDuration, sevClass, svcIcon, emptyState, skeletonRows } from './utils.js';
|
|
import { api } from './api.js';
|
|
|
|
// ── Sessions Tab ────────────────────────────────────────────
|
|
|
|
export async function renderSessions() {
|
|
const ml = $id('main-list');
|
|
if (!ml) return;
|
|
ml.innerHTML = skeletonRows(5);
|
|
|
|
const url = '/api/honeypot/sessions?limit=50';
|
|
const data = await api(url);
|
|
S.sessions = (data && data.sessions) ? data.sessions : [];
|
|
|
|
if (!S.sessions.length) {
|
|
ml.innerHTML = emptyState('\uD83D\uDC64', 'No sessions recorded', 'Session data appears when attackers interact with services');
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="session-list">';
|
|
for (let i = 0; i < S.sessions.length; i++) {
|
|
const s = S.sessions[i];
|
|
const sevCls = sevClass(s.max_severity || 'LOW');
|
|
const startT = formatTime(s.start_time);
|
|
const endT = s.end_time ? formatTime(s.end_time) : 'active';
|
|
const dur = s.end_time && s.start_time
|
|
? formatDuration(Math.round((new Date(s.end_time) - new Date(s.start_time)) / 1000))
|
|
: 'ongoing';
|
|
const port = s.dst_port ? ':' + s.dst_port : '';
|
|
|
|
html += '<div class="ev-row session-row" data-action="replay" data-session="' + escHtml(s.session_id) + '" data-ip="' + escHtml(s.src_ip) + '" data-service="' + escHtml(s.service || '') + '">'
|
|
+ '<div class="ev-row-line1">'
|
|
+ '<span class="ev-service">' + svcIcon(s.service || '') + ' ' + escHtml(s.service || '?') + escHtml(port) + '</span>'
|
|
+ '<span class="ev-ip" data-action="attacker" data-ip="' + escHtml(s.src_ip) + '">' + escHtml(s.src_ip) + '</span>'
|
|
+ '<span class="ev-time">' + startT + ' \u2192 ' + endT + '</span>'
|
|
+ '<span class="ev-severity ' + sevCls + '">' + escHtml(s.max_severity || 'LOW') + '</span>'
|
|
+ '</div>'
|
|
+ '<div class="ev-row-line2">'
|
|
+ '<span>\uD83D\uDCC4 ' + (s.event_count || 0) + ' events</span>'
|
|
+ '<span>' + dur + '</span>'
|
|
+ (s.auth_count ? '<span>\uD83D\uDD11 ' + s.auth_count + '</span>' : '')
|
|
+ (s.cmd_count ? '<span>\u2328 ' + s.cmd_count + '</span>' : '')
|
|
+ (s.malware_count ? '<span class="text-crit">malware: ' + s.malware_count + '</span>' : '')
|
|
+ '</div>'
|
|
+ '</div>';
|
|
}
|
|
html += '</div>';
|
|
ml.innerHTML = html;
|
|
}
|
|
|
|
// ── Session Replay ──────────────────────────────────────────
|
|
|
|
export async function openReplay(sessionId, ip, service) {
|
|
const modal = $id('replay-modal');
|
|
modal.classList.add('open');
|
|
$id('replay-title').textContent = 'Session ' + sessionId;
|
|
$id('replay-meta').textContent = ip + ' — ' + service;
|
|
$id('replay-terminal').innerHTML = '';
|
|
$id('replay-progress').style.width = '0%';
|
|
$id('replay-counter').textContent = '0/0';
|
|
$id('replay-play-btn').textContent = 'Play';
|
|
S._replayEvents = [];
|
|
S._replayIdx = 0;
|
|
S._replayPlaying = false;
|
|
if (S._replayInterval) { clearInterval(S._replayInterval); S._replayInterval = null; }
|
|
const data = await api('/api/honeypot/sessions/' + sessionId);
|
|
if (data && data.events) S._replayEvents = data.events;
|
|
$id('replay-counter').textContent = '0/' + S._replayEvents.length;
|
|
}
|
|
|
|
export function toggleReplayPlayback() {
|
|
S._replayPlaying = !S._replayPlaying;
|
|
$id('replay-play-btn').textContent = S._replayPlaying ? 'Pause' : 'Play';
|
|
if (S._replayPlaying) {
|
|
const speed = parseInt($id('replay-speed').value) || 500;
|
|
S._replayInterval = setInterval(replayStep, speed);
|
|
} else if (S._replayInterval) {
|
|
clearInterval(S._replayInterval);
|
|
S._replayInterval = null;
|
|
}
|
|
}
|
|
|
|
export function replayStep() {
|
|
if (S._replayIdx >= S._replayEvents.length) {
|
|
S._replayPlaying = false;
|
|
$id('replay-play-btn').textContent = 'Play';
|
|
if (S._replayInterval) { clearInterval(S._replayInterval); S._replayInterval = null; }
|
|
return;
|
|
}
|
|
const evt = S._replayEvents[S._replayIdx];
|
|
const term = $id('replay-terminal');
|
|
term.innerHTML += renderReplayLine(evt);
|
|
term.scrollTop = term.scrollHeight;
|
|
S._replayIdx++;
|
|
const total = S._replayEvents.length;
|
|
$id('replay-progress').style.width = (S._replayIdx / total * 100) + '%';
|
|
$id('replay-counter').textContent = S._replayIdx + '/' + total;
|
|
}
|
|
|
|
function renderReplayLine(evt) {
|
|
const type = evt.event_type || '', detail = evt.detail || '';
|
|
if (type === 'SVC_CONNECT')
|
|
return '<div class="replay-line replay-line-connect">Connected from ' + escHtml(evt.src_ip || '') + '</div>';
|
|
if (type === 'SVC_AUTH_ATTEMPT') {
|
|
const ok = detail.toLowerCase().includes('success');
|
|
return '<div class="replay-line ' + (ok ? 'replay-line-auth-ok' : 'replay-line-auth-fail') + '">AUTH user=\'' + escHtml(evt.username || '') + '\' pass=\'' + escHtml(evt.password || '') + '\' [' + (ok ? 'OK' : 'FAIL') + ']</div>';
|
|
}
|
|
if (type === 'SVC_COMMAND')
|
|
return '<div class="replay-line replay-line-cmd">$ ' + escHtml(evt.command || detail) + '</div>';
|
|
if (type === 'SVC_HTTP_REQUEST')
|
|
return '<div class="replay-line replay-line-http">' + escHtml(detail) + '</div>';
|
|
return '<div class="replay-line">' + escHtml(detail) + '</div>';
|
|
}
|
|
|
|
export function replayReset() {
|
|
S._replayIdx = 0;
|
|
S._replayPlaying = false;
|
|
if (S._replayInterval) { clearInterval(S._replayInterval); S._replayInterval = null; }
|
|
$id('replay-play-btn').textContent = 'Play';
|
|
$id('replay-terminal').innerHTML = '';
|
|
$id('replay-progress').style.width = '0%';
|
|
$id('replay-counter').textContent = '0/' + S._replayEvents.length;
|
|
}
|
|
|
|
export function seekReplay(e) {
|
|
const bar = e.currentTarget, rect = bar.getBoundingClientRect();
|
|
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
const total = S._replayEvents.length, target = Math.round(pct * total);
|
|
const term = $id('replay-terminal');
|
|
term.innerHTML = '';
|
|
for (let i = 0; i < target && i < total; i++) term.innerHTML += renderReplayLine(S._replayEvents[i]);
|
|
term.scrollTop = term.scrollHeight;
|
|
S._replayIdx = target;
|
|
$id('replay-progress').style.width = (target / total * 100) + '%';
|
|
$id('replay-counter').textContent = target + '/' + total;
|
|
}
|
|
|
|
export function updateReplaySpeed() {
|
|
if (S._replayPlaying && S._replayInterval) {
|
|
clearInterval(S._replayInterval);
|
|
S._replayInterval = setInterval(replayStep, parseInt($id('replay-speed').value) || 500);
|
|
}
|
|
}
|
|
|
|
export function closeReplay() {
|
|
S._replayPlaying = false;
|
|
if (S._replayInterval) { clearInterval(S._replayInterval); S._replayInterval = null; }
|
|
$id('replay-modal')?.classList.remove('open');
|
|
}
|