feat: nouvelles techniques de détection et page tactiques SOC

SQL:
- Ajout 5 colonnes d'agrégation (count_xff, count_unusual_ct,
  count_non_std_port, count_login_post, sec_ch_mobile_mismatch)
- Exposition de 5 features calculées dans view_ai_features_1h
- Migration ALTER TABLE pour déploiements existants

Bot-detector:
- 7 nouvelles features ML (has_xff, unusual_content_type_ratio,
  non_standard_port_ratio, login_post_concentration,
  sec_ch_mobile_mismatch, true_window_size, window_mss_ratio)
- Propagation campaign_id vers ml_all_scores (était toujours -1)
- Escalade campagne : HIGH→CRITICAL si cluster ≥5 membres

Dashboard:
- Page Tactiques SOC : brute-force, rotation JA4, récurrence,
  alertes temps réel — 4 KPIs + 4 panneaux + infobulles doc
- Ajout fmtDate() helper global
- Navigation sidebar mise à jour

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
toto
2026-04-09 14:29:18 +02:00
parent 702c0d5edb
commit 039086a0b3
9 changed files with 295 additions and 5 deletions

View File

@ -0,0 +1,199 @@
{% extends "base.html" %}
{% block page_title %}Tactiques d'attaque{% endblock %}
{% block content %}
<!-- ═══ Tactiques d'attaque — 4 panneaux de détection spécialisés ═══ -->
<!-- ── KPIs ── -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
<div class="kpi-card"><div class="kpi-label">Brute-force IPs</div><div class="kpi-value text-red-400" id="kpi-bf"></div></div>
<div class="kpi-card"><div class="kpi-label">JA4 rotation IPs</div><div class="kpi-value text-orange-400" id="kpi-rot"></div></div>
<div class="kpi-card"><div class="kpi-label">IPs récurrentes</div><div class="kpi-value text-yellow-400" id="kpi-rec"></div></div>
<div class="kpi-card"><div class="kpi-label">Alertes 24h</div><div class="kpi-value text-purple-400" id="kpi-alerts"></div></div>
</div>
<!-- ── Brute-force / Credential stuffing ── -->
<div class="section-card border-red-600/40 mb-4">
<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>
<div class="relative">
<button class="doc-btn"></button>
<div class="doc-panel right-0 w-72">
<strong>Détection brute-force</strong><br>
IPs envoyant ≥10 requêtes POST/heure sur des endpoints d'authentification.
Signale les tentatives de credential stuffing, brute-force de mots de passe,
ou abus d'API. Données issues de <code>view_form_bruteforce_detected</code>.
</div>
</div>
</div>
<div class="section-body p-0">
<div class="overflow-x-auto">
<table class="data-table">
<thead><tr><th>IP</th><th>Host</th><th class="text-right">POST/h</th><th class="text-right">Paths distincts</th><th>Première vue</th><th>Dernière vue</th></tr></thead>
<tbody id="bf-body"><tr><td colspan="6" class="text-center text-gray-500 py-8">Chargement…</td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- ── JA4 Rotation (évasion) ── -->
<div class="section-card border-orange-600/40 mb-4">
<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="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 de fingerprint JA4
</span>
<div class="relative">
<button class="doc-btn"></button>
<div class="doc-panel right-0 w-72">
<strong>Détection d'évasion JA4</strong><br>
IPs utilisant ≥2 fingerprints JA4 distincts en 24h. Technique d'évasion
classique : rotation de la configuration TLS pour contourner les blocages
par fingerprint. Données issues de <code>view_host_ip_ja4_rotation</code>.
</div>
</div>
</div>
<div class="section-body p-0">
<div class="overflow-x-auto">
<table class="data-table">
<thead><tr><th>IP</th><th>Host</th><th class="text-right">JA4 distincts</th><th>Fingerprints</th><th class="text-right">Hits</th><th>Fenêtre</th></tr></thead>
<tbody id="rot-body"><tr><td colspan="6" class="text-center text-gray-500 py-8">Chargement…</td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- ── Récurrence (menaces persistantes) ── -->
<div class="section-card border-yellow-600/40 mb-4">
<div class="section-header">
<span class="section-title">
<svg class="w-4 h-4 text-yellow-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 (récurrence)
</span>
<div class="relative">
<button class="doc-btn"></button>
<div class="doc-panel right-0 w-72">
<strong>IPs récurrentes</strong><br>
IPs détectées comme anomalies sur plusieurs fenêtres temporelles distinctes.
La récurrence augmente la confiance dans la classification malveillante.
Score agravé par <code>log1p(recurrence) × 0.005</code>.
Données issues de <code>view_ip_recurrence</code>.
</div>
</div>
</div>
<div class="section-body p-0">
<div class="overflow-x-auto">
<table class="data-table">
<thead><tr><th>IP</th><th class="text-right">Récurrence</th><th class="text-right">Pire score</th><th>Pire menace</th><th>Première vue</th><th>Dernière vue</th><th>JA4 top</th><th>Host top</th></tr></thead>
<tbody id="rec-body"><tr><td colspan="8" class="text-center text-gray-500 py-8">Chargement…</td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- ── Alertes temps réel ── -->
<div class="section-card border-purple-600/40 mb-4">
<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="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
Alertes récentes (24h)
</span>
<div class="relative">
<button class="doc-btn"></button>
<div class="doc-panel right-0 w-72">
<strong>Flux d'alertes</strong><br>
Dernières détections CRITICAL, HIGH et KNOWN_BOT sur les 24 dernières heures.
Chaque alerte inclut le score, la raison SHAP, et le lien vers l'investigation IP.
</div>
</div>
</div>
<div class="section-body p-0">
<div class="overflow-x-auto">
<table class="data-table">
<thead><tr><th>Date</th><th>IP</th><th class="text-right">Score</th><th>Menace</th><th>JA4</th><th>Host</th><th>ASN</th><th class="text-right">Hits</th><th>Raison</th></tr></thead>
<tbody id="alert-body"><tr><td colspan="9" class="text-center text-gray-500 py-8">Chargement…</td></tr></tbody>
</table>
</div>
</div>
</div>
<script>
/* ═══════════════════════════════════════════════════════════════════════
* Tactiques — chargement des 4 sources de données
* ═══════════════════════════════════════════════════════════════════════ */
document.addEventListener('DOMContentLoaded', () => {
Promise.all([
fetch('/api/brute-force').then(r => r.json()),
fetch('/api/ja4-rotation').then(r => r.json()),
fetch('/api/recurrence').then(r => r.json()),
fetch('/api/alerts?limit=50').then(r => r.json()),
]).then(([bf, rot, rec, alerts]) => {
renderBruteForce(bf.data || []);
renderRotation(rot.data || []);
renderRecurrence(rec.data || []);
renderAlerts(alerts.alerts || []);
}).catch(err => console.error('Tactics load error:', err));
});
function renderBruteForce(data) {
document.getElementById('kpi-bf').textContent = data.length;
const body = document.getElementById('bf-body');
if (!data.length) { body.innerHTML = '<tr><td colspan="6" class="text-center text-gray-500 py-8">Aucune tentative de brute-force détectée</td></tr>'; return; }
body.innerHTML = data.map(r => `<tr class="cursor-pointer hover:bg-gray-800/60" onclick="location.href='/ip/${r.src_ip}'">
<td>${fmtIP(r.src_ip)}</td><td class="text-gray-300">${escapeHtml(r.host||'')}</td>
<td class="text-right font-mono text-red-400 font-bold">${r.post_count}</td>
<td class="text-right">${r.distinct_paths}</td>
<td class="text-gray-400 text-xs">${fmtDate(r.first_seen)}</td>
<td class="text-gray-400 text-xs">${fmtDate(r.last_seen)}</td></tr>`).join('');
}
function renderRotation(data) {
document.getElementById('kpi-rot').textContent = data.length;
const body = document.getElementById('rot-body');
if (!data.length) { body.innerHTML = '<tr><td colspan="6" class="text-center text-gray-500 py-8">Aucune rotation JA4 détectée</td></tr>'; return; }
body.innerHTML = data.map(r => {
const ja4s = (r.ja4_list || []).map(j => fmtJA4(j)).join(', ');
return `<tr class="cursor-pointer hover:bg-gray-800/60" onclick="location.href='/ip/${r.src_ip}'">
<td>${fmtIP(r.src_ip)}</td><td class="text-gray-300">${escapeHtml(r.host||'')}</td>
<td class="text-right font-mono text-orange-400 font-bold">${r.distinct_ja4}</td>
<td class="max-w-xs truncate">${ja4s}</td>
<td class="text-right">${r.total_hits}</td>
<td class="text-gray-400 text-xs">${fmtDate(r.window_start)}</td></tr>`;
}).join('');
}
function renderRecurrence(data) {
document.getElementById('kpi-rec').textContent = data.length;
const body = document.getElementById('rec-body');
if (!data.length) { body.innerHTML = '<tr><td colspan="8" class="text-center text-gray-500 py-8">Aucune IP récurrente</td></tr>'; return; }
body.innerHTML = data.map(r => `<tr class="cursor-pointer hover:bg-gray-800/60" onclick="location.href='/ip/${r.src_ip}'">
<td>${fmtIP(r.src_ip)}</td>
<td class="text-right font-mono text-yellow-400 font-bold">${r.recurrence}×</td>
<td class="text-right font-mono">${fmtScore(r.worst_score)}</td>
<td>${threatBadge(r.worst_threat||r.worst_threat_level||'')}</td>
<td class="text-gray-400 text-xs">${fmtDate(r.first_seen)}</td>
<td class="text-gray-400 text-xs">${fmtDate(r.last_seen)}</td>
<td>${r.top_ja4 ? fmtJA4(r.top_ja4) : ''}</td>
<td class="text-gray-300">${escapeHtml(r.top_host||'')}</td></tr>`).join('');
}
function renderAlerts(data) {
document.getElementById('kpi-alerts').textContent = data.length;
const body = document.getElementById('alert-body');
if (!data.length) { body.innerHTML = '<tr><td colspan="9" class="text-center text-gray-500 py-8">Aucune alerte récente</td></tr>'; return; }
body.innerHTML = data.map(r => `<tr class="cursor-pointer hover:bg-gray-800/60" onclick="location.href='/ip/${r.src_ip}'">
<td class="text-gray-400 text-xs whitespace-nowrap">${fmtDate(r.detected_at)}</td>
<td>${fmtIP(r.src_ip)}</td>
<td class="text-right font-mono">${fmtScore(r.anomaly_score)}</td>
<td>${threatBadge(r.threat_level)}</td>
<td>${fmtJA4(r.ja4)}</td>
<td class="text-gray-300">${escapeHtml(r.host||'')}</td>
<td class="text-gray-400 text-xs">${escapeHtml(r.asn_org||'')}</td>
<td class="text-right">${r.hits||''}</td>
<td class="text-gray-400 text-xs max-w-[200px] truncate" title="${escapeHtml(r.reason||'')}">${escapeHtml((r.reason||'').substring(0,60))}</td></tr>`).join('');
}
</script>
{% endblock %}