espilon-source/tools/C3PO/hp_dashboard/static/hp/js/killchain.js
Eun0us 79c2a4d4bf c3po: full server rewrite with modular routes and honeypot dashboard
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.
2026-02-28 20:12:27 +01:00

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 &middot; 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');
}
}