Files
ja4-platform/services/dashboard/backend/templates/network.html
toto c6ca352db9 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>
2026-04-08 14:58:48 +02:00

278 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}JA4 SOC — Analyse Réseau{% endblock %}
{% block content %}
<div class="space-y-6">
<h2 class="text-lg font-semibold text-white">Analyse Réseau</h2>
<!-- KPI Row -->
<div class="grid grid-cols-2 md:grid-cols-4 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-indigo-400"></span><span class="text-xs text-gray-500">Pays</span></div>
<div class="text-2xl font-bold text-brand-500" id="kpi-countries"></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">ASNs</span></div>
<div class="text-2xl font-bold text-yellow-400" id="kpi-asns"></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">Sessions humaines</span></div>
<div class="text-2xl font-bold text-green-400" id="kpi-human"></div>
</div>
<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">Sessions datacenter</span></div>
<div class="text-2xl font-bold text-red-400" id="kpi-datacenter"></div>
</div>
</div>
<!-- Row 2: ASN Treemap + Country Sunburst -->
<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">Treemap ASN (par label)</h3>
<div id="chart-treemap" style="height:380px"></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">Sunburst Pays → Label</h3>
<div id="chart-sunburst" style="height:380px"></div>
</div>
</div>
<!-- Row 3: JA4 fingerprint 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">Empreintes JA4</h3>
<div class="overflow-x-auto" style="max-height:420px; overflow-y:auto">
<table class="data-table" id="ja4-table">
<thead><tr>
<th class="cursor-pointer select-none" data-col="0">JA4 ▸</th>
<th class="cursor-pointer select-none" data-col="1">Sessions ▸</th>
<th class="cursor-pointer select-none" data-col="2">Hits ▸</th>
<th class="cursor-pointer select-none" data-col="3">Avg Velocity ▸</th>
<th class="cursor-pointer select-none" data-col="4">Avg Fuzz ▸</th>
<th class="cursor-pointer select-none" data-col="5">Browser Score ▸</th>
<th>Label</th>
<th>Bot</th>
</tr></thead>
<tbody id="ja4-body"></tbody>
</table>
</div>
</div>
<!-- Row 4: ASN detail table + Bot pie -->
<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étail ASN</h3>
<div class="overflow-x-auto" style="max-height:400px; overflow-y:auto">
<table class="data-table">
<thead><tr>
<th>ASN Org</th><th>Label</th><th>Pays</th>
<th>Sessions</th><th>Hits</th><th>Avg Velocity</th><th>Avg Fuzz</th>
</tr></thead>
<tbody id="asn-body"></tbody>
</table>
</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 par empreinte</h3>
<div id="chart-botpie" style="height:340px"></div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const LABEL_COLORS = {human:'#22c55e', datacenter:'#ef4444', hosting:'#f97316', unknown:'#6b7280'};
function labelBadge(label) {
const colors = {
human: 'bg-green-500/20 text-green-400',
datacenter: 'bg-red-500/20 text-red-400',
hosting: 'bg-orange-500/20 text-orange-400',
unknown: 'bg-gray-500/20 text-gray-400',
};
return `<span class="badge ${colors[label] || colors.unknown}">${label || 'unknown'}</span>`;
}
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];
}
/* ── Sortable JA4 table ── */
let ja4Rows = [];
let sortCol = 1, sortAsc = false;
function renderJA4Table() {
const sorted = [...ja4Rows].sort((a,b) => {
const va = a[sortCol], vb = b[sortCol];
if (typeof va === 'number' && typeof vb === 'number') return sortAsc ? va - vb : vb - va;
return sortAsc ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va));
});
document.getElementById('ja4-body').innerHTML = sorted.map(r =>
`<tr>
<td class="font-mono text-xs">${fmtJA4Full(r[0])}</td>
<td>${r[1]}</td><td>${r[2]}</td>
<td>${r[3].toFixed(3)}</td><td>${r[4].toFixed(3)}</td><td>${r[5].toFixed(2)}</td>
<td>${fmtLabel(r[6])}</td>
<td class="text-xs">${fmtBotName(r[7])}</td>
</tr>`
).join('');
}
document.getElementById('ja4-table').querySelector('thead').addEventListener('click', e => {
const th = e.target.closest('th[data-col]');
if (!th) return;
const col = parseInt(th.dataset.col);
if (col === sortCol) { sortAsc = !sortAsc; } else { sortCol = col; sortAsc = false; }
renderJA4Table();
});
async function loadAll() {
try {
const [geo, fp] = await Promise.all([
fetch('/api/geo').then(r => r.json()),
fetch('/api/fingerprints').then(r => r.json()),
]);
const countries = geo.countries || [];
const asns = geo.asns || [];
const ja4Stats = fp.ja4_stats || [];
const botJa4 = fp.bot_ja4 || [];
// ── KPIs ──
const uniqueCountries = new Set(countries.map(c => c.country_code)).size;
const uniqueAsns = new Set(asns.map(a => a.asn_org)).size;
const humanSessions = asns.filter(a => a.asn_label === 'human').reduce((s,a) => s + (a.sessions||0), 0);
const dcSessions = asns.filter(a => a.asn_label === 'datacenter').reduce((s,a) => s + (a.sessions||0), 0);
document.getElementById('kpi-countries').textContent = uniqueCountries.toLocaleString();
document.getElementById('kpi-asns').textContent = uniqueAsns.toLocaleString();
document.getElementById('kpi-human').textContent = humanSessions.toLocaleString();
document.getElementById('kpi-datacenter').textContent = dcSessions.toLocaleString();
// ── ASN Treemap grouped by asn_label ──
const treemapChart = initChart('chart-treemap');
if (treemapChart && asns.length) {
const byLabel = {};
asns.forEach(a => {
const lbl = a.asn_label || 'unknown';
if (!byLabel[lbl]) byLabel[lbl] = {name:lbl, value:0, children:[], itemStyle:{color:LABEL_COLORS[lbl]||'#6b7280'}};
byLabel[lbl].children.push({name:a.asn_org, value:a.sessions||0});
byLabel[lbl].value += a.sessions || 0;
});
treemapChart.setOption(ecBase({
tooltip: ecTooltip({formatter: i => `${i.name}<br>Sessions: <b>${(i.value||0).toLocaleString()}</b>`}),
series:[{
type:'treemap',
data: Object.values(byLabel).sort((a,b) => b.value - a.value),
width:'100%', height:'100%',
label:{show:true, fontSize:11, color:'#fff'},
upperLabel:{show:true, height:22, fontSize:12, color:'#fff', fontWeight:'bold',
backgroundColor:'transparent'},
itemStyle:{borderColor:'#111827', borderWidth:2, gapWidth:2},
levels:[
{itemStyle:{borderColor:'#1f2937', borderWidth:3, gapWidth:3},
upperLabel:{show:true}},
{colorSaturation:[0.4,0.8],
itemStyle:{borderColorSaturation:0.5, gapWidth:1, borderWidth:1}},
],
}]
}));
treemapChart.on('click', params => {
if (params.data?.name && params.treePathInfo?.length > 2) window.location.href = '/detections?asn_org=' + encodeURIComponent(params.data.name);
});
}
// ── Country Sunburst ──
const sunburstChart = initChart('chart-sunburst');
if (sunburstChart && countries.length) {
const byCountry = {};
countries.forEach(c => {
if (!byCountry[c.country_code]) byCountry[c.country_code] = {name:c.country_code, children:[]};
byCountry[c.country_code].children.push({
name: c.asn_label || 'unknown',
value: c.sessions || 0,
itemStyle:{color: LABEL_COLORS[c.asn_label] || '#6b7280'},
});
});
sunburstChart.setOption(ecBase({
tooltip: ecTooltip({formatter: i => `${i.name}<br>Sessions: <b>${(i.value||0).toLocaleString()}</b>`}),
series:[{
type:'sunburst',
data: Object.values(byCountry).sort((a,b) => {
const va = a.children.reduce((s,c) => s+c.value,0);
const vb = b.children.reduce((s,c) => s+c.value,0);
return vb - va;
}),
radius:['15%','90%'],
label:{color:'#e5e7eb', fontSize:11, rotate:'radial'},
itemStyle:{borderColor:'#111827', borderWidth:1},
levels:[
{},
{r0:'15%', r:'50%', label:{fontSize:13, fontWeight:'bold'},
itemStyle:{borderWidth:2}},
{r0:'50%', r:'90%', label:{fontSize:10}},
],
}]
}));
sunburstChart.on('click', params => {
if (params.data?.name && params.data.name.length <= 3) window.location.href = '/detections?country_code=' + encodeURIComponent(params.data.name);
});
}
// ── JA4 Fingerprint Table ──
const botMap = {};
botJa4.forEach(b => { botMap[b.ja4] = b.bot_name; });
ja4Rows = ja4Stats.map(j => [
j.ja4,
j.sessions || 0,
j.total_hits || 0,
j.avg_velocity || 0,
j.avg_fuzz || 0,
j.avg_browser_score || 0,
j.asn_label || 'unknown',
botMap[j.ja4] || '',
]);
renderJA4Table();
// ── ASN Detail Table ──
document.getElementById('asn-body').innerHTML = asns
.sort((a,b) => (b.sessions||0) - (a.sessions||0))
.map(a => `<tr>
<td class="text-xs">${fmtASN(a.asn_org)}</td>
<td>${fmtLabel(a.asn_label)}</td>
<td>${fmtCountry(a.country_code)}</td>
<td>${(a.sessions||0).toLocaleString()}</td>
<td>${(a.total_hits||0).toLocaleString()}</td>
<td>${(a.avg_velocity||0).toFixed(3)}</td>
<td>${(a.avg_fuzz||0).toFixed(3)}</td>
</tr>`).join('');
// ── Bot Fingerprints Pie ──
const botPieChart = initChart('chart-botpie');
if (botPieChart && botJa4.length) {
const byBot = {};
botJa4.forEach(b => { byBot[b.bot_name] = (byBot[b.bot_name]||0) + (b.sessions||0); });
botPieChart.setOption(ecBase({
tooltip: ecTooltip({trigger:'item', formatter:'{b}: {c} ({d}%)'}),
series:[{
type:'pie', radius:['35%','75%'], center:['50%','55%'],
label:{color:EC_TEXT, fontSize:11, formatter:'{b}\n{d}%'},
data: Object.entries(byBot)
.map(([name,value],i) => ({name, value, itemStyle:{color:EC_COLORS[i%EC_COLORS.length]}}))
.sort((a,b) => b.value - a.value),
emphasis:{itemStyle:{shadowBlur:10, shadowColor:'rgba(0,0,0,0.5)'}},
}]
}));
}
} catch(e) { console.error('Network load error:', e); }
}
loadAll();
setInterval(loadAll, 60000);
window.addEventListener('resize', () => Object.values(charts).forEach(c => c?.resize()));
</script>
{% endblock %}