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>
This commit is contained in:
82
services/dashboard/backend/templates/overview.html
Normal file
82
services/dashboard/backend/templates/overview.html
Normal file
@ -0,0 +1,82 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user