feat(dashboard): SOC workflow overhaul — sidebar nav, doc tooltips, full-width layout
- base.html: collapsible sidebar navigation, doc tooltip system, JS helpers (fmtNum, fmtPct, fmtDuration, ecGrid, buildTable, docHTML) - overview.html: SOC command center with stacked timeline, live alerts, campaigns panel, browser donut, 6 KPIs - detections.html: threat color dots, raw score column, click-to-navigate rows - network.html: JA4 rotation, brute-force, persistent threats tables, 6 KPIs - ip_detail.html: ASN/country KPIs, AE/XGB/campaign columns, enriched features - scores/traffic/features/models/classify: page_title blocks + doc tooltips - api.py: 9 new endpoints (campaigns, brute-force, ja4-rotation, recurrence, cascade, alerts, timeline-detail, ua-rotation) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -1,95 +1,163 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}JA4 SOC — Analyse Réseau{% endblock %}
|
||||
{% block page_title %}
|
||||
Analyse Réseau
|
||||
<span class="relative inline-block ml-1"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
|
||||
<h4>Analyse réseau</h4>
|
||||
<p>Vue complète de l'infrastructure réseau : ASN, pays, fingerprints JA4, rotation de fingerprints, brute-force et menaces persistantes.</p>
|
||||
<p><strong>Workflow :</strong> Identifiez les ASN suspects → vérifiez la rotation JA4 → contrôlez le brute-force → investiguez les IPs récurrentes.</p>
|
||||
<p class="doc-source">Sources : view_ai_features_1h, view_host_ip_ja4_rotation, view_form_bruteforce_detected, view_ip_recurrence</p>
|
||||
</div></span>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-lg font-semibold text-white">Analyse Réseau</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 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 class="grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-3">
|
||||
<div class="kpi-card"><div class="text-[11px] text-gray-500 mb-1">Pays</div><div class="text-xl font-bold text-brand-500" id="kpi-countries">—</div></div>
|
||||
<div class="kpi-card"><div class="text-[11px] text-gray-500 mb-1">ASNs</div><div class="text-xl font-bold text-yellow-400" id="kpi-asns">—</div></div>
|
||||
<div class="kpi-card"><div class="text-[11px] text-gray-500 mb-1">Sessions ISP</div><div class="text-xl font-bold text-green-400" id="kpi-human">—</div></div>
|
||||
<div class="kpi-card"><div class="text-[11px] text-gray-500 mb-1">Sessions DC</div><div class="text-xl font-bold text-red-400" id="kpi-datacenter">—</div></div>
|
||||
<div class="kpi-card"><div class="text-[11px] text-gray-500 mb-1">Rotation JA4</div><div class="text-xl font-bold text-purple-400" id="kpi-rotation">—</div></div>
|
||||
<div class="kpi-card"><div class="text-[11px] text-gray-500 mb-1">Brute-force</div><div class="text-xl font-bold text-red-400" id="kpi-brute">—</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: ASN Treemap + Country Sunburst -->
|
||||
<!-- Row: ASN Treemap + 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 class="section-card">
|
||||
<div class="section-header"><span class="section-title">Treemap ASN
|
||||
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
|
||||
<h4>Treemap ASN</h4>
|
||||
<p>Taille = nombre de sessions. Regroupé par type : <span class="text-green-400">ISP</span> (résidentiel), <span class="text-red-400">Datacenter</span>, <span class="text-orange-400">Hosting</span>, <span class="text-cyan-400">CDN</span>.</p>
|
||||
<p><strong>Action :</strong> Un ASN datacenter avec beaucoup de sessions mérite investigation.</p>
|
||||
<p class="doc-source">Source : view_ai_features_1h</p>
|
||||
</div></span>
|
||||
</span></div>
|
||||
<div class="section-body"><div id="chart-treemap" style="height:340px"></div></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 class="section-card">
|
||||
<div class="section-header"><span class="section-title">Pays → Type ASN
|
||||
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
|
||||
<h4>Sunburst géographique</h4>
|
||||
<p>Niveau 1 : pays. Niveau 2 : type d'ASN. Identifiez les pays avec forte proportion datacenter.</p>
|
||||
<p class="doc-source">Source : view_ai_features_1h</p>
|
||||
</div></span>
|
||||
</span></div>
|
||||
<div class="section-body"><div id="chart-sunburst" style="height:340px"></div></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>
|
||||
<!-- Row: JA4 Rotation + Brute-force -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- JA4 Rotation -->
|
||||
<div class="section-card">
|
||||
<div class="section-header"><span class="section-title">
|
||||
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
Rotation JA4 (évasion TLS)
|
||||
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
|
||||
<h4>Rotation de fingerprints JA4</h4>
|
||||
<p>IPs utilisant plusieurs fingerprints TLS distinctes sur une fenêtre horaire. Indique une tentative d'évasion de détection (rotation de client TLS).</p>
|
||||
<p><strong>Seuil critique :</strong> ≥ 3 JA4 distincts par IP/host/heure.</p>
|
||||
<p class="doc-source">Source : view_host_ip_ja4_rotation</p>
|
||||
</div></span>
|
||||
</span></div>
|
||||
<div class="section-body p-0">
|
||||
<div class="overflow-x-auto" style="max-height:300px; overflow-y:auto">
|
||||
<table class="data-table"><thead><tr>
|
||||
<th>IP</th><th>Host</th><th>JA4 distincts</th><th>Hits</th><th>Fenêtre</th>
|
||||
</tr></thead><tbody id="rotation-body"></tbody></table>
|
||||
</div>
|
||||
</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>
|
||||
<!-- Brute-force -->
|
||||
<div class="section-card">
|
||||
<div class="section-header"><span class="section-title">
|
||||
<svg class="w-4 h-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>
|
||||
Brute-force / Credential stuffing
|
||||
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
|
||||
<h4>Détection brute-force</h4>
|
||||
<p>IPs envoyant ≥10 requêtes POST par host sur 24h. Indique du credential stuffing ou du brute-force de formulaires.</p>
|
||||
<p class="doc-source">Source : view_form_bruteforce_detected</p>
|
||||
</div></span>
|
||||
</span></div>
|
||||
<div class="section-body p-0">
|
||||
<div class="overflow-x-auto" style="max-height:300px; overflow-y:auto">
|
||||
<table class="data-table"><thead><tr>
|
||||
<th>IP</th><th>Host</th><th>POSTs</th><th>Paths</th><th>Première</th><th>Dernière</th>
|
||||
</tr></thead><tbody id="brute-body"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row: Persistent threats + JA4 table -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<!-- Persistent threats -->
|
||||
<div class="section-card">
|
||||
<div class="section-header"><span class="section-title">
|
||||
<svg class="w-4 h-4 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
Menaces persistantes
|
||||
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
|
||||
<h4>IPs récurrentes</h4>
|
||||
<p>IPs détectées sur plusieurs fenêtres horaires. Récurrence élevée = acteur persistant. Le score indique le pire score observé.</p>
|
||||
<p class="doc-source">Source : view_ip_recurrence</p>
|
||||
</div></span>
|
||||
</span></div>
|
||||
<div class="section-body p-0">
|
||||
<div class="overflow-y-auto" style="max-height:360px">
|
||||
<table class="data-table"><thead><tr>
|
||||
<th>IP</th><th>Réc.</th><th>Score</th><th>Threat</th>
|
||||
</tr></thead><tbody id="recurrence-body"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- JA4 Fingerprints -->
|
||||
<div class="lg:col-span-2 section-card">
|
||||
<div class="section-header"><span class="section-title">Empreintes JA4
|
||||
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">?</button><div class="doc-panel">
|
||||
<h4>Fingerprints TLS (JA4)</h4>
|
||||
<p>Chaque combinaison unique de paramètres TLS génère un hash JA4. Les navigateurs courants partagent des fingerprints connues.</p>
|
||||
<p><strong>Indicateurs :</strong> Velocity élevée + browser score bas = bot probable.</p>
|
||||
<p class="doc-source">Source : view_ai_features_1h GROUP BY ja4</p>
|
||||
</div></span>
|
||||
</span></div>
|
||||
<div class="section-body p-0">
|
||||
<div class="overflow-x-auto" style="max-height:360px; overflow-y:auto">
|
||||
<table class="data-table" id="ja4-table"><thead><tr>
|
||||
<th class="cursor-pointer" data-col="0">JA4</th>
|
||||
<th class="cursor-pointer" data-col="1">Sessions</th>
|
||||
<th class="cursor-pointer" data-col="2">Hits</th>
|
||||
<th class="cursor-pointer" data-col="3">Velocity</th>
|
||||
<th class="cursor-pointer" data-col="4">Fuzz</th>
|
||||
<th class="cursor-pointer" data-col="5">Browser</th>
|
||||
<th>Label</th><th>Bot</th>
|
||||
</tr></thead><tbody id="ja4-body"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row: ASN table + Bot pie -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div class="lg:col-span-2 section-card">
|
||||
<div class="section-header"><span class="section-title">Détail ASN</span></div>
|
||||
<div class="section-body p-0">
|
||||
<div class="overflow-x-auto" style="max-height:360px; 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>Velocity</th><th>Fuzz</th>
|
||||
</tr></thead><tbody id="asn-body"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-card">
|
||||
<div class="section-header"><span class="section-title">Bots par empreinte</span></div>
|
||||
<div class="section-body"><div id="chart-botpie" style="height:300px"></div></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>`;
|
||||
}
|
||||
const LABEL_COLORS = {isp:'#22c55e', datacenter:'#ef4444', hosting:'#f97316', cdn:'#06b6d4', unknown:'#6b7280'};
|
||||
|
||||
let charts = {};
|
||||
function initChart(id) {
|
||||
@ -100,178 +168,147 @@ function initChart(id) {
|
||||
return charts[id];
|
||||
}
|
||||
|
||||
/* ── Sortable JA4 table ── */
|
||||
let ja4Rows = [];
|
||||
let sortCol = 1, sortAsc = false;
|
||||
|
||||
let ja4Rows = [], 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;
|
||||
if (typeof va === '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('');
|
||||
`<tr onclick="window.location='/detections?ja4=${encodeURIComponent(r[0])}'">
|
||||
<td class="font-mono text-[11px]">${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 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; }
|
||||
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 [geo, fp, rot, bf, rec] = await Promise.all([
|
||||
fetch('/api/geo').then(r=>r.json()),
|
||||
fetch('/api/fingerprints').then(r=>r.json()),
|
||||
fetch('/api/ja4-rotation').then(r=>r.json()),
|
||||
fetch('/api/brute-force').then(r=>r.json()),
|
||||
fetch('/api/recurrence').then(r=>r.json()),
|
||||
]);
|
||||
|
||||
const countries = geo.countries || [];
|
||||
const asns = geo.asns || [];
|
||||
const ja4Stats = fp.ja4_stats || [];
|
||||
const botJa4 = fp.bot_ja4 || [];
|
||||
const countries = geo.countries||[], asns = geo.asns||[];
|
||||
const ja4Stats = fp.ja4_stats||[], botJa4 = fp.bot_ja4||[];
|
||||
const rotData = rot.data||[], bfData = bf.data||[], recData = rec.data||[];
|
||||
|
||||
// ── 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();
|
||||
// KPIs
|
||||
document.getElementById('kpi-countries').textContent = new Set(countries.map(c=>c.country_code)).size;
|
||||
document.getElementById('kpi-asns').textContent = new Set(asns.map(a=>a.asn_org)).size;
|
||||
document.getElementById('kpi-human').textContent = fmtNum(asns.filter(a=>a.asn_label==='isp').reduce((s,a)=>s+(a.sessions||0),0));
|
||||
document.getElementById('kpi-datacenter').textContent = fmtNum(asns.filter(a=>a.asn_label==='datacenter').reduce((s,a)=>s+(a.sessions||0),0));
|
||||
document.getElementById('kpi-rotation').textContent = rotData.length;
|
||||
document.getElementById('kpi-brute').textContent = bfData.length;
|
||||
|
||||
// ── ASN Treemap grouped by asn_label ──
|
||||
// ASN Treemap
|
||||
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'}};
|
||||
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;
|
||||
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}},
|
||||
],
|
||||
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:20,fontSize:11,color:'#fff',fontWeight:'bold',backgroundColor:'transparent'},
|
||||
itemStyle:{borderColor:'#111827',borderWidth:2,gapWidth:1},
|
||||
levels:[{itemStyle:{borderColor:'#1f2937',borderWidth:3},upperLabel:{show:true}},{colorSaturation:[0.4,0.8]}],
|
||||
}]
|
||||
}));
|
||||
treemapChart.on('click', params => {
|
||||
if (params.data?.name && params.treePathInfo?.length > 2) window.location.href = '/detections?asn_org=' + encodeURIComponent(params.data.name);
|
||||
});
|
||||
treemapChart.on('click', p => { if(p.data?.name && p.treePathInfo?.length>2) window.location.href='/detections?asn_org='+encodeURIComponent(p.data.name); });
|
||||
}
|
||||
|
||||
// ── Country Sunburst ──
|
||||
const sunburstChart = initChart('chart-sunburst');
|
||||
if (sunburstChart && countries.length) {
|
||||
const byCountry = {};
|
||||
// Sunburst
|
||||
const sunChart = initChart('chart-sunburst');
|
||||
if (sunChart && countries.length) {
|
||||
const byC = {};
|
||||
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'},
|
||||
});
|
||||
if(!byC[c.country_code]) byC[c.country_code] = {name:c.country_code, children:[]};
|
||||
byC[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}},
|
||||
],
|
||||
sunChart.setOption(ecBase({
|
||||
tooltip:ecTooltip({formatter:i=>`${i.name}: ${(i.value||0).toLocaleString()}`}),
|
||||
series:[{type:'sunburst', data:Object.values(byC).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:10,rotate:'radial'},
|
||||
itemStyle:{borderColor:'#111827',borderWidth:1},
|
||||
levels:[{},{r0:'15%',r:'50%',label:{fontSize:12,fontWeight:'bold'}},{r0:'50%',r:'90%',label:{fontSize:9}}],
|
||||
}]
|
||||
}));
|
||||
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 ──
|
||||
// JA4 Rotation table
|
||||
document.getElementById('rotation-body').innerHTML = rotData.map(r =>
|
||||
`<tr onclick="window.location='/ip/${encodeURIComponent(String(r.src_ip).replace('::ffff:',''))}'">
|
||||
<td>${fmtIP(r.src_ip)}</td>
|
||||
<td class="text-xs">${escapeHtml(r.host||'')}</td>
|
||||
<td class="text-center"><span class="badge badge-high">${r.distinct_ja4}</span></td>
|
||||
<td class="font-mono text-xs">${r.total_hits||0}</td>
|
||||
<td class="text-[11px] text-gray-400">${(r.window_start||'').substring(0,16)}</td>
|
||||
</tr>`).join('') || '<tr><td colspan="5" class="text-center py-4 text-gray-500">Aucune rotation détectée</td></tr>';
|
||||
|
||||
// Brute-force table
|
||||
document.getElementById('brute-body').innerHTML = bfData.map(r =>
|
||||
`<tr onclick="window.location='/ip/${encodeURIComponent(String(r.src_ip).replace('::ffff:',''))}'">
|
||||
<td>${fmtIP(r.src_ip)}</td>
|
||||
<td class="text-xs">${escapeHtml(r.host||'')}</td>
|
||||
<td class="font-mono text-red-400">${r.post_count}</td>
|
||||
<td class="text-xs">${r.distinct_paths}</td>
|
||||
<td class="text-[11px] text-gray-400">${(r.first_seen||'').substring(0,16)}</td>
|
||||
<td class="text-[11px] text-gray-400">${(r.last_seen||'').substring(0,16)}</td>
|
||||
</tr>`).join('') || '<tr><td colspan="6" class="text-center py-4 text-gray-500">Aucun brute-force détecté</td></tr>';
|
||||
|
||||
// Recurrence table
|
||||
document.getElementById('recurrence-body').innerHTML = recData.map(r =>
|
||||
`<tr onclick="window.location='/ip/${encodeURIComponent(String(r.src_ip).replace('::ffff:',''))}'">
|
||||
<td>${fmtIP(r.src_ip)}</td>
|
||||
<td class="text-center font-bold text-orange-400">${r.recurrence}</td>
|
||||
<td>${fmtScore(r.worst_score)}</td>
|
||||
<td>${threatBadge(r.worst_threat)}</td>
|
||||
</tr>`).join('') || '<tr><td colspan="4" class="text-center py-4 text-gray-500">Aucune récurrence</td></tr>';
|
||||
|
||||
// JA4 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] || '',
|
||||
]);
|
||||
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('');
|
||||
// ASN 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) {
|
||||
// Bot pie
|
||||
const bpChart = initChart('chart-botpie');
|
||||
if (bpChart && 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)'}},
|
||||
botJa4.forEach(b => { byBot[b.bot_name] = (byBot[b.bot_name]||0)+(b.sessions||0); });
|
||||
bpChart.setOption(ecBase({
|
||||
tooltip:ecTooltip({trigger:'item',formatter:'{b}: {c} ({d}%)'}),
|
||||
series:[{type:'pie',radius:['35%','75%'],center:['50%','55%'],
|
||||
label:{color:EC_TEXT,fontSize:10,formatter:'{b}\n{d}%'},
|
||||
data:Object.entries(byBot).map(([n,v],i)=>({name:n,value:v,itemStyle:{color:EC_COLORS[i%EC_COLORS.length]}})).sort((a,b)=>b.value-a.value),
|
||||
}]
|
||||
}));
|
||||
}
|
||||
|
||||
} catch(e) { console.error('Network load error:', e); }
|
||||
}
|
||||
|
||||
loadAll();
|
||||
setInterval(loadAll, 60000);
|
||||
window.addEventListener('resize', () => Object.values(charts).forEach(c => c?.resize()));
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user