feat(dashboard): add clickable drill-down to all data elements

Add navigation helpers (fmtASN, fmtCountry, fmtJA4, fmtBotName,
fmtThreatLink, fmtLabel) to base.html for SOC analyst drill-down.

Update all templates:
- overview.html: clickable table cells + ECharts click handlers for
  ASN, country, JA4, bot, and threat charts
- detections.html: URL param pre-filters, active filter bar with
  clear buttons, clickable ASN/country/JA4/threat in table
- scores.html: URL param pre-filters, clickable threat/JA4/country
- traffic.html: clickable JA4 and country columns
- ip_detail.html: clickable threat/JA4 in detections, clickable
  asn_org/country_code/asn_label in AI features grid
- network.html: click handlers on ASN treemap and country sunburst,
  fmtJA4Full/fmtLabel/fmtBotName/fmtASN in tables
- features.html: scatter plot click navigates to /ip/{ip}

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
toto
2026-04-08 14:58:48 +02:00
parent fc882dd3e7
commit c6ca352db9
8 changed files with 1148 additions and 112 deletions

View File

@ -3,30 +3,74 @@
{% 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 class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-4" id="kpi-grid">
<div class="kpi-card">
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-red-400"></span><span class="text-xs text-gray-500">Détections 24h</span></div>
<div class="text-2xl font-bold text-red-400" id="kpi-detections"></div>
</div>
<div class="kpi-card">
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-indigo-400"></span><span class="text-xs text-gray-500">Sessions scorées</span></div>
<div class="text-2xl font-bold text-brand-500" id="kpi-scored"></div>
</div>
<div class="kpi-card">
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-gray-400"></span><span class="text-xs text-gray-500">Trafic total 24h</span></div>
<div class="text-2xl font-bold text-gray-200" id="kpi-traffic"></div>
</div>
<div class="kpi-card">
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-yellow-400"></span><span class="text-xs text-gray-500">IPs uniques</span></div>
<div class="text-2xl font-bold text-yellow-400" id="kpi-ips"></div>
</div>
<div class="kpi-card">
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-orange-400"></span><span class="text-xs text-gray-500">Critical / High</span></div>
<div class="text-2xl font-bold text-orange-400" id="kpi-critical"></div>
</div>
<div class="kpi-card">
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-green-400"></span><span class="text-xs text-gray-500">Modèles actifs</span></div>
<div class="text-2xl font-bold text-green-400" id="kpi-models"></div>
</div>
</div>
<!-- Charts Row -->
<!-- Charts Row 1: Timeline + Threats -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="lg:col-span-2 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>
<div id="chart-timeline" style="height:280px"></div>
</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">Threat levels</h3>
<div id="chart-threats" style="height:280px"></div>
</div>
</div>
<!-- Charts Row 2: ASN Treemap + Country -->
<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>
<h3 class="text-sm font-medium text-gray-400 mb-3">Répartition par ASN</h3>
<div id="chart-asn" style="height:300px"></div>
</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>
<h3 class="text-sm font-medium text-gray-400 mb-3">Répartition géographique</h3>
<div id="chart-geo" style="height:300px"></div>
</div>
</div>
<!-- Top IPs -->
<!-- Charts Row 3: JA4 Diversity + Bot names -->
<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">Empreintes JA4 (top 15)</h3>
<div id="chart-ja4" style="height:300px"></div>
</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">Bots identifiés</h3>
<div id="chart-bots" style="height:300px"></div>
</div>
</div>
<!-- Top IPs table -->
<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">
<table class="data-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>
@ -36,47 +80,168 @@
{% endblock %}
{% block scripts %}
<script>
let timelineChart, threatsChart;
const THREAT_COLORS = {CRITICAL:'#ef4444',HIGH:'#f97316',MEDIUM:'#eab308',LOW:'#22c55e',NORMAL:'#6b7280',KNOWN_BOT:'#3b82f6',ANUBIS_DENY:'#dc2626'};
const LABEL_COLORS = {human:'#22c55e',datacenter:'#ef4444',hosting:'#f97316',unknown:'#6b7280'};
let charts = {};
function initChart(id) {
const el = document.getElementById(id);
if (!el) return null;
if (charts[id]) charts[id].dispose();
charts[id] = echarts.init(el);
return charts[id];
}
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'}} } }
const [ov, geo, fp] = await Promise.all([
fetch('/api/overview').then(r=>r.json()),
fetch('/api/geo').then(r=>r.json()),
fetch('/api/fingerprints').then(r=>r.json()),
]);
// KPIs
document.getElementById('kpi-detections').textContent = (ov.detections_24h??0).toLocaleString();
document.getElementById('kpi-scored').textContent = (ov.scored_24h??0).toLocaleString();
document.getElementById('kpi-traffic').textContent = (ov.traffic_24h??0).toLocaleString();
document.getElementById('kpi-ips').textContent = (ov.unique_ips??0).toLocaleString();
document.getElementById('kpi-critical').textContent = ((ov.critical_count??0)+(ov.high_count??0)).toLocaleString();
document.getElementById('kpi-models').textContent = ov.models?.length ?? 0;
// Timeline area chart
if (ov.timeline?.length) {
const ch = initChart('chart-timeline');
ch.setOption(ecBase({
tooltip: ecTooltip({trigger:'axis'}),
grid: {left:50,right:20,top:20,bottom:30},
xAxis: {type:'category', data: ov.timeline.map(t=>t.hour?.substring(11,16)||''), axisLine:{lineStyle:{color:EC_GRID}}, axisLabel:{color:EC_TEXT}},
yAxis: {type:'value', splitLine:{lineStyle:{color:EC_GRID,type:'dashed'}}, axisLabel:{color:EC_TEXT}},
series: [{
type:'line', data: ov.timeline.map(t=>t.cnt), smooth:true,
areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(99,102,241,0.4)'},{offset:1,color:'rgba(99,102,241,0.02)'}])},
lineStyle:{color:'#6366f1',width:2}, itemStyle:{color:'#6366f1'}, symbol:'circle', symbolSize:6,
}]
}));
}
// Threats pie
if (ov.threat_distribution?.length) {
const ch = initChart('chart-threats');
ch.setOption(ecBase({
tooltip: ecTooltip({trigger:'item', formatter:'{b}: {c} ({d}%)'}),
series: [{
type:'pie', radius:['45%','75%'], center:['50%','55%'],
label:{color:EC_TEXT, fontSize:11},
data: ov.threat_distribution.map(t=>({name:t.threat_level, value:t.cnt, itemStyle:{color:THREAT_COLORS[t.threat_level]||'#6b7280'}})),
emphasis:{itemStyle:{shadowBlur:10,shadowColor:'rgba(0,0,0,0.5)'}},
}]
}));
ch.on('click', params => {
if (params.name) window.location.href = '/detections?threat_level=' + encodeURIComponent(params.name);
});
}
// 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}}}}}
// ASN horizontal bar
if (geo.asns?.length) {
const top = geo.asns.slice(0,12);
const ch = initChart('chart-asn');
ch.setOption(ecBase({
tooltip: ecTooltip({trigger:'axis',axisPointer:{type:'shadow'}}),
grid: {left:160,right:30,top:10,bottom:30},
yAxis: {type:'category', data: top.map(a=>a.asn_org).reverse(), axisLine:{lineStyle:{color:EC_GRID}}, axisLabel:{color:EC_TEXT,fontSize:11,width:140,overflow:'truncate'}},
xAxis: {type:'value', splitLine:{lineStyle:{color:EC_GRID,type:'dashed'}}, axisLabel:{color:EC_TEXT}},
series: [{
type:'bar', data: top.map(a=>({value:a.sessions,itemStyle:{color:LABEL_COLORS[a.asn_label]||'#6b7280'}})).reverse(),
barWidth:'60%', label:{show:true,position:'right',color:EC_TEXT,fontSize:11},
}]
}));
ch.on('click', params => {
if (params.name) window.location.href = '/detections?asn_org=' + encodeURIComponent(params.name);
});
}
// Country treemap
if (geo.countries?.length) {
const byCountry = {};
geo.countries.forEach(c => {
if (!byCountry[c.country_code]) byCountry[c.country_code] = {name:c.country_code, value:0, children:[]};
byCountry[c.country_code].value += c.sessions;
byCountry[c.country_code].children.push({name:c.asn_label, value:c.sessions});
});
const ch = initChart('chart-geo');
ch.setOption(ecBase({
tooltip: ecTooltip({formatter:i => `${i.name}: ${i.value} sessions`}),
series: [{
type:'treemap', data: Object.values(byCountry).sort((a,b)=>b.value-a.value).slice(0,20),
width:'100%', height:'100%',
label:{show:true,fontSize:13,color:'#fff',fontWeight:'bold'},
upperLabel:{show:false},
itemStyle:{borderColor:'#111827',borderWidth:2,gapWidth:2},
levels:[
{itemStyle:{borderColor:'#1f2937',borderWidth:3,gapWidth:3},upperLabel:{show:false}},
{colorSaturation:[0.3,0.7],itemStyle:{borderColorSaturation:0.6,gapWidth:1,borderWidth:1}},
],
colorMappingBy:'value',
color: EC_COLORS,
}]
}));
ch.on('click', params => {
if (params.data?.name && params.data.name.length <= 3) window.location.href = '/detections?country_code=' + encodeURIComponent(params.data.name);
});
}
// JA4 bar chart
if (fp.ja4_stats?.length) {
const top15 = fp.ja4_stats.slice(0,15);
const ch = initChart('chart-ja4');
ch.setOption(ecBase({
tooltip: ecTooltip({trigger:'axis',axisPointer:{type:'shadow'}}),
grid: {left:50,right:20,top:10,bottom:80},
xAxis: {type:'category', data:top15.map(j=>j.ja4.substring(0,16)+'…'), axisLabel:{color:EC_TEXT,rotate:45,fontSize:10}, axisLine:{lineStyle:{color:EC_GRID}}},
yAxis: {type:'value', splitLine:{lineStyle:{color:EC_GRID,type:'dashed'}}, axisLabel:{color:EC_TEXT}},
series: [{
type:'bar', data:top15.map(j=>({value:j.sessions,itemStyle:{color:LABEL_COLORS[j.asn_label]||EC_COLORS[0]}})),
barWidth:'65%',
}]
}));
ch.on('click', params => {
if (params.dataIndex !== undefined) {
const ja4 = fp.ja4_stats[params.dataIndex]?.ja4;
if (ja4) window.location.href = '/detections?ja4=' + encodeURIComponent(ja4);
}
});
}
// Bots pie
if (fp.bot_ja4?.length) {
const ch = initChart('chart-bots');
const byBot = {};
fp.bot_ja4.forEach(b => { byBot[b.bot_name] = (byBot[b.bot_name]||0) + b.sessions; });
ch.setOption(ecBase({
tooltip: ecTooltip({trigger:'item', formatter:'{b}: {c} ({d}%)'}),
series: [{
type:'pie', radius:['35%','70%'], center:['50%','55%'],
label:{color:EC_TEXT,fontSize:11,formatter:'{b}\n{d}%'},
data: Object.entries(byBot).map(([k,v],i)=>({name:k,value:v,itemStyle:{color:EC_COLORS[i%EC_COLORS.length]}}))
.sort((a,b)=>b.value-a.value),
emphasis:{itemStyle:{shadowBlur:10}},
}]
}));
ch.on('click', params => {
if (params.name) window.location.href = '/detections?bot_name=' + encodeURIComponent(params.name);
});
}
// Top IPs table
const tbody = document.getElementById('top-ips-body');
tbody.innerHTML = (d.top_ips||[]).map(ip => `<tr>
document.getElementById('top-ips-body').innerHTML = (ov.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>
<td>${fmtThreatLink(ip.threat_level)}</td><td class="text-xs">${fmtASN(ip.asn_org)}</td><td>${fmtCountry(ip.country_code)}</td>
</tr>`).join('');
} catch(e) { console.error('Overview load error:', e); }
}
loadOverview();
setInterval(loadOverview, 30000);
setInterval(loadOverview, 60000);
window.addEventListener('resize', () => Object.values(charts).forEach(c => c?.resize()));
</script>
{% endblock %}