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.
161 lines
6.6 KiB
JavaScript
161 lines
6.6 KiB
JavaScript
/* ESPILON Honeypot Dashboard — Kill Chain Tab */
|
|
|
|
import { S, KC_PHASES } from './state.js';
|
|
import { $id, escHtml, formatTime, formatDuration, emptyState, skeletonRows } from './utils.js';
|
|
import { api } from './api.js';
|
|
import { showToast } from './ui.js';
|
|
|
|
export function scoreColor(score) {
|
|
if (score >= 150) return 'var(--sev-crit)';
|
|
if (score >= 80) return 'var(--sev-high)';
|
|
if (score >= 40) return 'var(--sev-med)';
|
|
return 'var(--sev-low)';
|
|
}
|
|
|
|
export function phaseColor(order, total) {
|
|
const lightness = 65 - (order / total) * 35;
|
|
const saturation = 50 + (order / total) * 30;
|
|
return 'hsl(0,' + saturation + '%,' + lightness + '%)';
|
|
}
|
|
|
|
export async function renderKillChain() {
|
|
const ml = $id('main-list');
|
|
if (!ml) return;
|
|
ml.innerHTML = skeletonRows(5);
|
|
|
|
S.killchain = await api('/api/honeypot/killchain?limit=20');
|
|
if (!S.killchain || !S.killchain.attackers || !S.killchain.attackers.length) {
|
|
ml.innerHTML = emptyState('\uD83D\uDEE1\uFE0F', 'No kill chain data', 'Attack progression will be tracked here');
|
|
return;
|
|
}
|
|
|
|
const phases = S.killchain.phases || KC_PHASES;
|
|
const attackers = S.killchain.attackers;
|
|
|
|
let html = '<div class="kc-table-wrapper">';
|
|
|
|
// Header row with phase names
|
|
html += '<div class="kc-row kc-row-header">';
|
|
html += '<span class="kc-row-ip">Attacker</span>';
|
|
html += '<span class="kc-row-score">Score</span>';
|
|
html += '<div class="kc-phases">';
|
|
for (let p = 0; p < phases.length; p++) {
|
|
html += '<span class="kc-phase-label" title="' + escHtml(phases[p].label) + '">' + escHtml(phases[p].label) + '</span>';
|
|
}
|
|
html += '</div>';
|
|
html += '<span class="kc-row-events">Events</span>';
|
|
html += '<span class="kc-row-dur">Duration</span>';
|
|
html += '</div>';
|
|
|
|
// Attacker rows
|
|
for (let i = 0; i < attackers.length; i++) {
|
|
const a = attackers[i];
|
|
|
|
html += '<div class="kc-row ev-row" data-action="killchain-detail" data-ip="' + escHtml(a.ip) + '">';
|
|
|
|
// IP + full chain badge
|
|
html += '<span class="kc-row-ip">';
|
|
if (a.is_full_chain) html += '<span title="Full kill chain" class="kc-full-chain-badge">\u26A0</span>';
|
|
html += escHtml(a.ip) + '</span>';
|
|
|
|
// Score
|
|
html += '<span class="kc-row-score" style="color:' + scoreColor(a.score) + '">' + (a.score || 0) + '</span>';
|
|
|
|
// Phase progression bar
|
|
html += '<div class="kc-phases">';
|
|
for (let p = 0; p < phases.length; p++) {
|
|
const phaseId = phases[p].id;
|
|
const hasPhase = a.phases && a.phases[phaseId] && a.phases[phaseId].count > 0;
|
|
const segColor = hasPhase ? phaseColor(phases[p].order, phases.length) : 'var(--bg-secondary)';
|
|
const segTitle = phases[p].label + (hasPhase ? ' (' + a.phases[phaseId].count + ' events)' : ' (none)');
|
|
html += '<div class="kc-bar' + (hasPhase ? '' : ' empty') + '"' + (hasPhase ? ' style="background:' + segColor + '"' : '') + ' title="' + escHtml(segTitle) + '"></div>';
|
|
}
|
|
html += '</div>';
|
|
|
|
// Event count
|
|
html += '<span class="kc-row-events">' + (a.total_events || 0) + '</span>';
|
|
|
|
// Duration
|
|
const dur = a.duration_seconds ? formatDuration(a.duration_seconds) : '-';
|
|
html += '<span class="kc-row-dur">' + dur + '</span>';
|
|
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
ml.innerHTML = html;
|
|
}
|
|
|
|
export async function showKillChainDetail(ip) {
|
|
const data = await api('/api/honeypot/killchain/' + encodeURIComponent(ip));
|
|
if (!data) {
|
|
showToast('Error', 'Failed to load kill chain for ' + ip, '', 'error');
|
|
return;
|
|
}
|
|
|
|
const phases = data.phase_defs || KC_PHASES;
|
|
let html = '<div class="kc-detail-content">';
|
|
|
|
// Header
|
|
html += '<div class="kc-detail-header">';
|
|
html += '<div><span class="kc-detail-ip">' + escHtml(data.ip) + '</span></div>';
|
|
html += '<div class="kc-detail-score" style="color:' + scoreColor(data.score) + '">Score: ' + (data.score || 0) + '</div>';
|
|
html += '</div>';
|
|
|
|
// Duration + progression
|
|
html += '<div class="kc-detail-meta">';
|
|
html += '<span>Max phase: ' + escHtml(data.max_phase || '-') + '</span>';
|
|
html += '<span>Progression: ' + (data.progression_pct || 0) + '%</span>';
|
|
if (data.duration_seconds) html += '<span>Duration: ' + formatDuration(data.duration_seconds) + '</span>';
|
|
if (data.is_full_chain) html += '<span class="text-crit fw-700">\u26A0 Full Kill Chain</span>';
|
|
html += '</div>';
|
|
|
|
// Visual progression bar
|
|
html += '<div class="kc-detail-bar">';
|
|
for (let p = 0; p < phases.length; p++) {
|
|
const phaseId = phases[p].id;
|
|
const hasPhase = data.phases && data.phases[phaseId] && data.phases[phaseId].count > 0;
|
|
const segColor = hasPhase ? phaseColor(phases[p].order, phases.length) : 'var(--bg-secondary)';
|
|
const segBorder = hasPhase ? 'none' : '1px solid var(--border-color)';
|
|
html += '<div style="flex:1;background:' + segColor + ';border:' + segBorder + ';border-radius:3px"></div>';
|
|
}
|
|
html += '</div>';
|
|
|
|
// Phase details
|
|
for (let p = 0; p < phases.length; p++) {
|
|
const phaseId = phases[p].id;
|
|
const phaseData = (data.phases && data.phases[phaseId]) ? data.phases[phaseId] : null;
|
|
const active = phaseData && phaseData.count > 0;
|
|
const color = active ? phaseColor(phases[p].order, phases.length) : 'var(--border-color)';
|
|
|
|
html += '<div class="kc-detail-phase ' + (active ? 'active' : 'inactive') + '" style="border-left-color:' + color + '">';
|
|
html += '<div class="kc-detail-phase-header">';
|
|
html += '<span class="kc-detail-phase-name">' + escHtml(phases[p].label) + '</span>';
|
|
if (active) {
|
|
html += '<span class="kc-detail-phase-info">' + phaseData.count + ' events · first seen ' + formatTime(phaseData.first_seen) + '</span>';
|
|
}
|
|
html += '</div>';
|
|
|
|
if (active && phaseData.techniques && phaseData.techniques.length) {
|
|
html += '<div class="kc-mitre-tags">';
|
|
for (let t = 0; t < phaseData.techniques.length; t++) {
|
|
const tech = phaseData.techniques[t];
|
|
const tid = typeof tech === 'string' ? tech : (tech.technique_id || tech.id || tech);
|
|
html += '<span class="mitre-tag">' + escHtml(String(tid)) + '</span>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
|
|
// Show in detail panel
|
|
const panel = $id('detail-panel');
|
|
if (panel) {
|
|
const panelBody = panel.querySelector('.detail-body') || panel;
|
|
panelBody.innerHTML = html;
|
|
panel.classList.add('open');
|
|
}
|
|
}
|