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>
278 lines
13 KiB
HTML
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 %}
|