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>
248 lines
13 KiB
HTML
248 lines
13 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-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 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">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">Répartition géographique</h3>
|
|
<div id="chart-geo" style="height:300px"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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">
|
|
<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>
|
|
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 [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);
|
|
});
|
|
}
|
|
|
|
// 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
|
|
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>${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, 60000);
|
|
window.addEventListener('resize', () => Object.values(charts).forEach(c => c?.resize()));
|
|
</script>
|
|
{% endblock %}
|