Files
ja4-platform/services/dashboard/backend/templates/overview.html
toto b735bab5a5 feat(dashboard): rebuild SOC dashboard + fix ClickHouse SQL
Complete rewrite of the SOC dashboard using FastAPI + Jinja2 + htmx + Chart.js + Tailwind CSS.
Replaces the old React/Vite frontend with server-rendered templates.

Dashboard pages:
- Overview: KPIs, timeline chart, threat distribution, top IPs
- Detections: paginated/filterable anomaly table
- Scores: ml_all_scores with AE error & XGB prob columns
- Traffic: HTTP logs with method/host filters
- IP Investigation: full deep-dive (scores, features, HTTP logs, classify)
- Classification: SOC feedback form + history
- Features: AI + thesis feature stats
- Models: scoring stats + model metadata

API: 9 JSON endpoints with parameterized queries, sort whitelists

SQL fixes:
- 05_aggregation_tables: add deduplicate_merge_projection_mode
- 11_views: fix nested aggregate (argMax inside sum)
- 12_thesis_features: remove invalid 'let' bindings, fix groupArrayIf type

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-08 03:21:05 +02:00

83 lines
5.1 KiB
HTML

{% extends "base.html" %}
{% block title %}JA4 SOC — Overview{% endblock %}
{% block content %}
<div class="space-y-6">
<!-- KPI Row -->
<div class="grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-4" id="kpi-grid">
<div class="kpi-card"><div class="text-xs text-gray-500 mb-1">Détections 24h</div><div class="text-2xl font-bold text-red-400" id="kpi-detections"></div></div>
<div class="kpi-card"><div class="text-xs text-gray-500 mb-1">Sessions scorées 24h</div><div class="text-2xl font-bold text-brand-500" id="kpi-scored"></div></div>
<div class="kpi-card"><div class="text-xs text-gray-500 mb-1">Trafic total 24h</div><div class="text-2xl font-bold text-gray-200" id="kpi-traffic"></div></div>
<div class="kpi-card"><div class="text-xs text-gray-500 mb-1">IPs uniques</div><div class="text-2xl font-bold text-yellow-400" id="kpi-ips"></div></div>
<div class="kpi-card"><div class="text-xs text-gray-500 mb-1">Critical/High</div><div class="text-2xl font-bold text-orange-400" id="kpi-critical"></div></div>
<div class="kpi-card"><div class="text-xs text-gray-500 mb-1">Modèles actifs</div><div class="text-2xl font-bold text-green-400" id="kpi-models"></div></div>
</div>
<!-- Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-sm font-medium text-gray-400 mb-3">Détections par heure (24h)</h3>
<canvas id="chart-timeline" height="200"></canvas>
</div>
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-sm font-medium text-gray-400 mb-3">Distribution des threat levels</h3>
<canvas id="chart-threats" height="200"></canvas>
</div>
</div>
<!-- Top IPs -->
<div class="bg-gray-900 rounded-xl p-5 border border-gray-800">
<h3 class="text-sm font-medium text-gray-400 mb-3">Top 10 IPs détectées (24h)</h3>
<div class="overflow-x-auto">
<table class="data-table" id="top-ips-table">
<thead><tr><th>IP</th><th>Détections</th><th>Pire score</th><th>Threat Level</th><th>ASN</th><th>Pays</th></tr></thead>
<tbody id="top-ips-body"></tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let timelineChart, threatsChart;
async function loadOverview() {
try {
const r = await fetch('/api/overview');
const d = await r.json();
document.getElementById('kpi-detections').textContent = (d.detections_24h ?? 0).toLocaleString();
document.getElementById('kpi-scored').textContent = (d.scored_24h ?? 0).toLocaleString();
document.getElementById('kpi-traffic').textContent = (d.traffic_24h ?? 0).toLocaleString();
document.getElementById('kpi-ips').textContent = (d.unique_ips ?? 0).toLocaleString();
document.getElementById('kpi-critical').textContent = ((d.critical_count ?? 0) + (d.high_count ?? 0)).toLocaleString();
document.getElementById('kpi-models').textContent = d.models?.length ?? 0;
// Timeline chart
if (d.timeline && d.timeline.length) {
const labels = d.timeline.map(t => t.hour?.substring(11,16) || '');
const data = d.timeline.map(t => t.cnt);
if (timelineChart) timelineChart.destroy();
timelineChart = new Chart(document.getElementById('chart-timeline'), {
type: 'bar', data: { labels, datasets: [{ label:'Détections', data, backgroundColor:'rgba(99,102,241,0.6)', borderColor:'#6366f1', borderWidth:1 }] },
options: { responsive:true, plugins:{legend:{display:false}}, scales:{ y:{beginAtZero:true,ticks:{color:'#9ca3af'}}, x:{ticks:{color:'#9ca3af'}} } }
});
}
// Threats donut
if (d.threat_distribution && d.threat_distribution.length) {
const labels = d.threat_distribution.map(t => t.threat_level);
const data = d.threat_distribution.map(t => t.cnt);
const colors = labels.map(l => ({CRITICAL:'#ef4444',HIGH:'#f97316',MEDIUM:'#eab308',LOW:'#22c55e',NORMAL:'#6b7280',KNOWN_BOT:'#3b82f6',ANUBIS_DENY:'#dc2626'}[l]||'#6b7280'));
if (threatsChart) threatsChart.destroy();
threatsChart = new Chart(document.getElementById('chart-threats'), {
type:'doughnut', data:{labels,datasets:[{data,backgroundColor:colors}]},
options:{responsive:true,plugins:{legend:{position:'right',labels:{color:'#9ca3af',font:{size:11}}}}}
});
}
// Top IPs table
const tbody = document.getElementById('top-ips-body');
tbody.innerHTML = (d.top_ips||[]).map(ip => `<tr>
<td>${fmtIP(ip.src_ip)}</td><td>${ip.cnt}</td><td>${fmtScore(ip.worst_score)}</td>
<td>${threatBadge(ip.threat_level||'')}</td><td class="text-xs">${ip.asn_org||''}</td><td>${ip.country_code||''}</td>
</tr>`).join('');
} catch(e) { console.error('Overview load error:', e); }
}
loadOverview();
setInterval(loadOverview, 30000);
</script>
{% endblock %}