Files
ja4-platform/services/dashboard/backend/templates/fleet.html
toto a108814a56 feat: roadmap détection bots §2-9 — HTTP/2, cohérence, drift, flotte, Jaccard, ExIFFI, méta-learner, métriques
Étape 2 — Fingerprinting HTTP/2 dans le pipeline ML :
- Ajout du dictionnaire dict_browser_h2 (11 familles de navigateurs) dans 05_aggregation_tables.sql
- Ajout du CTE h2_agg et 4 features HTTP/2 dans 07_ai_features_view.sql :
  h2_settings_known, h2_pseudo_order_match, h2_ja4_coherence, h2_settings_rare
- Calcul du fingerprint_coherence_score (5 axes pondérés) dans la vue
- Ajout du 6e axe axis_h2_coherence dans browser.py (poids rééquilibrés)
- browser_h2.csv : 11 fingerprints Akamai → famille navigateur

Étape 3 — Pré-filtre de cohérence sur la baseline humaine :
- pipeline.py exclut les sessions avec fingerprint_coherence_score < seuil de la baseline d'entraînement
- FINGERPRINT_COHERENCE_THRESHOLD configurable via env (défaut 0.25)
- Log des sessions exclues pour analyse SOC

Étape 4 — Détection de drift améliorée :
- scoring.py : passage de 5 à 9 quantiles (p5…p95)
- Ajout de la divergence KL en complément du test KS
- Détection de drift adversarial (≥80% des features dérivent dans la même direction)
- Split temporel strict pour la validation

Étape 5 — Graphe bipartite JA4×ASN (§5.2) :
- fleet.py : détection de flottes via NetworkX + Louvain (imports optionnels)
- enrich_with_fleet_score() : ajout fleet_score + fleet_campaign_flag au DataFrame
- cycle.py : appel après preprocess_df avec log du nombre de sessions en flotte
- SQL migration 05_fleet_metrics_tables.sql : table fleet_detections (TTL 7j)
- Dashboard : /fleet + /api/fleet (communautés détectées) + template fleet.html

Étape 6 — Cross-domain Jaccard §5.8 :
- 12_thesis_features.sql : CTE jaccard_paths → cross_domain_path_similarity
- Signal : même chemins (/admin, /wp-login) sur plusieurs hosts = scanner

Étape 7 — ExIFFI + erreurs AE par feature :
- scoring.py : compute_exiffi_importance() par permutation, compute_ae_feature_errors()
- pipeline.py : calcul ExIFFI sur X_test, mapping index → dict pour anomalies
- build_reason() enrichi avec exiffi_top quand SHAP inactif

Étape 8 — Méta-learner pour la pondération de l'ensemble :
- scoring.py : classe MetaLearner (LogisticRegression, fallback poids fixes <1000 labels)
- Collecte des labels depuis le cycle courant (known_bots, légitimes, Anubis)
- pipeline.py : remplacement des poids fixes par MetaLearner.predict()

Étape 9 — Métriques de performance et monitoring :
- metrics.py : record_cycle_metrics() — taux anomalie, drift, corrélation, latence
- SQL migration 05_fleet_metrics_tables.sql : table ml_performance_metrics (TTL 90j)
- Dashboard : /health + /api/health + template health.html
- cycle.py : appel record_cycle_metrics en fin de cycle (Complet + Applicatif)

Tests : 36/36 bot-detector tests passent

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 00:11:35 +02:00

227 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block page_title %}Flottes JA4×ASN — §5.2{% endblock %}
{% block content %}
<div class="p-4 lg:p-6 space-y-4 max-w-[1920px] mx-auto">
<!-- ═══ Header KPIs ═══ -->
<div class="flex flex-wrap items-center gap-4 mb-2">
<h1 class="text-xl font-bold text-white flex items-center gap-2">
<svg class="w-6 h-6 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Flottes JA4×ASN
</h1>
<div class="ml-auto flex items-center gap-3">
<div class="text-center px-3">
<div class="text-2xl font-bold text-cyan-400" id="kpi-total"></div>
<div class="text-[10px] text-gray-500 uppercase tracking-wider">Flottes</div>
</div>
<div class="text-center px-3 border-l border-gray-700">
<div class="text-2xl font-bold text-red-400" id="kpi-ips"></div>
<div class="text-[10px] text-gray-500 uppercase tracking-wider">IPs impliquées</div>
</div>
<div class="text-center px-3 border-l border-gray-700">
<div class="text-2xl font-bold text-amber-400" id="kpi-maxscore"></div>
<div class="text-[10px] text-gray-500 uppercase tracking-wider">Score max</div>
</div>
</div>
</div>
<!-- ═══ Doc banner ═══ -->
<div class="bg-gray-900/50 border border-gray-800 rounded-lg px-4 py-3 text-xs text-gray-400 leading-relaxed">
<strong class="text-cyan-300">Détection de flottes §5.2</strong> — Analyse du graphe bipartite
<strong>G = (JA4 ASN, E)</strong> pour identifier les flottes de bots coordonnées qui
font tourner leurs fingerprints JA4 et ASN. Les communautés suspectes sont détectées via
l'algorithme Louvain (ou composantes connexes en fallback).
<br><strong>fleet_score = taille × densité / log₂(n_asn + 2).</strong>
Un score ≥ 2.0 indique une communauté coordonnée. Chaque ligne représente une communauté
avec les fingerprints JA4 et ASNs impliqués.
<br><strong>Action SOC :</strong> Bloquer les ASNs ou les JA4 d'une flotte peut neutraliser
des milliers de bots en une seule règle.
</div>
<!-- ═══ Table des flottes détectées ═══ -->
<div class="section-card overflow-hidden">
<div class="section-header">
<span class="section-title">
<svg class="w-4 h-4 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064"/></svg>
Communautés suspectes (7 jours)
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn"></button><div class="doc-panel">
<h4>Flottes JA4×ASN</h4>
<p>Chaque ligne = une communauté du graphe bipartite. Les flottes avec un
<strong>fleet_score</strong> élevé sont les plus coordonnées.</p>
<p><strong>ja4_set :</strong> Fingerprints TLS utilisés par la flotte.</p>
<p><strong>asn_set :</strong> Réseaux (ASN) de la flotte.</p>
<p><strong>n_ips :</strong> Nombre d'IPs distinctes dans la communauté.</p>
<p class="doc-source">Source : fleet_detections (7j)</p>
</div></span>
</span>
<span id="load-status" class="text-[10px] text-gray-500">Chargement…</span>
</div>
<div class="overflow-x-auto">
<table class="data-table">
<thead>
<tr>
<th>Détecté le</th>
<th>Comm.</th>
<th>Score</th>
<th>IPs</th>
<th>JA4 impliqués</th>
<th>ASNs impliqués</th>
<th>Échantillon IPs</th>
</tr>
</thead>
<tbody id="fleet-body">
<tr><td colspan="7" class="text-center text-gray-500 py-8">Chargement…</td></tr>
</tbody>
</table>
</div>
</div>
<!-- ═══ Graphique score distribution ═══ -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="section-card">
<div class="section-header">
<span class="section-title">
<svg class="w-4 h-4 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Distribution des fleet_scores
</span>
</div>
<div class="section-body">
<div id="score-chart" style="height:260px"></div>
</div>
</div>
<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"><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="12" cy="7" r="1"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l3-5 3 5"/></svg>
IPs par flotte
</span>
</div>
<div class="section-body">
<div id="ips-chart" style="height:260px"></div>
</div>
</div>
</div>
</div>
<script>
/* ════════════════════════════════════════════════════════════════════════════
* Page Flottes JA4×ASN — chargement et rendu
* ════════════════════════════════════════════════════════════════════════════ */
function fmtTs(ts) {
if (!ts) return '—';
return new Date(ts).toLocaleString('fr-FR', {dateStyle:'short', timeStyle:'short'});
}
function scoreColor(score) {
if (score >= 5) return 'badge-critical';
if (score >= 3) return 'badge-high';
return 'badge-medium';
}
function renderTags(arr, maxShow = 3) {
if (!arr || arr.length === 0) return '<span class="text-gray-600">—</span>';
const shown = arr.slice(0, maxShow);
const rest = arr.length - shown.length;
const tags = shown.map(v =>
`<span class="inline-block bg-gray-800 text-gray-300 text-[10px] px-1.5 py-0.5 rounded mr-1 mb-1 font-mono">${v}</span>`
).join('');
const more = rest > 0 ? `<span class="text-gray-500 text-[10px]">+${rest}</span>` : '';
return tags + more;
}
async function loadFleet() {
try {
const res = await fetch('/api/fleet');
const data = await res.json();
const fleets = data.fleets || [];
// KPIs
const totalIps = fleets.reduce((s, f) => s + (f.n_ips || 0), 0);
const maxScore = fleets.length ? Math.max(...fleets.map(f => f.fleet_score || 0)) : 0;
document.getElementById('kpi-total').textContent = fleets.length;
document.getElementById('kpi-ips').textContent = totalIps.toLocaleString('fr-FR');
document.getElementById('kpi-maxscore').textContent = maxScore.toFixed(2);
document.getElementById('load-status').textContent =
fleets.length ? `${fleets.length} communauté(s)` : 'Aucune flotte détectée';
// Table
const tbody = document.getElementById('fleet-body');
if (fleets.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-gray-500 py-8">Aucune flotte détectée sur les 7 derniers jours</td></tr>';
} else {
tbody.innerHTML = fleets.map(f => `
<tr>
<td class="text-gray-400 text-xs">${fmtTs(f.detected_at)}</td>
<td class="font-mono text-xs text-gray-300">#${f.community_id}</td>
<td><span class="badge ${scoreColor(f.fleet_score)}">${(f.fleet_score || 0).toFixed(3)}</span></td>
<td class="font-bold text-white">${(f.n_ips || 0).toLocaleString('fr-FR')}</td>
<td class="max-w-xs">${renderTags(f.ja4_set, 2)}</td>
<td class="max-w-xs">${renderTags(f.asn_set, 3)}</td>
<td class="max-w-xs">${renderTags(f.ip_sample, 3)}</td>
</tr>
`).join('');
}
// Graphique distribution des scores
renderScoreChart(fleets);
renderIpsChart(fleets);
} catch (err) {
console.error('Erreur chargement flottes :', err);
document.getElementById('load-status').textContent = 'Erreur de chargement';
document.getElementById('fleet-body').innerHTML =
'<tr><td colspan="7" class="text-center text-red-500 py-8">Erreur de chargement</td></tr>';
}
}
function renderScoreChart(fleets) {
const el = document.getElementById('score-chart');
if (!el || !fleets.length) return;
const chart = echarts.init(el, 'dark');
// Histogramme des fleet_scores
const scores = fleets.map(f => parseFloat((f.fleet_score || 0).toFixed(2)));
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
xAxis: { type:'category', data: fleets.map((f,i) => `#${f.community_id}`),
axisLabel:{color:'#6b7280', fontSize:10, rotate:45} },
yAxis: { type:'value', name:'Fleet Score', nameTextStyle:{color:'#6b7280',fontSize:10},
axisLabel:{color:'#6b7280', fontSize:10} },
series: [{
type: 'bar',
data: scores.map(s => ({
value: s,
itemStyle: { color: s >= 5 ? '#ef4444' : s >= 3 ? '#f97316' : '#eab308' }
})),
barMaxWidth: 40,
}],
});
window.addEventListener('resize', () => chart.resize());
}
function renderIpsChart(fleets) {
const el = document.getElementById('ips-chart');
if (!el || !fleets.length) return;
const chart = echarts.init(el, 'dark');
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'item', formatter: '{b}: {c} IPs' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: fleets.slice(0, 15).map(f => ({
name: `#${f.community_id}`,
value: f.n_ips || 0,
})),
label: { color: '#9ca3af', fontSize: 10 },
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.5)' } },
}],
});
window.addEventListener('resize', () => chart.resize());
}
loadFleet();
</script>
{% endblock %}