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>
This commit is contained in:
166
services/bot-detector/bot_detector/metrics.py
Normal file
166
services/bot-detector/bot_detector/metrics.py
Normal file
@ -0,0 +1,166 @@
|
||||
"""Module de métriques de performance du pipeline ML.
|
||||
|
||||
Enregistre un résumé de chaque cycle dans ml_performance_metrics :
|
||||
- Taux d'anomalie par niveau (CRITICAL/HIGH/MEDIUM/LOW)
|
||||
- Taux de corrélation (correlated=1 vs 0)
|
||||
- Drift rate, latence, taille baseline, seuil adaptatif
|
||||
- Alertes automatiques sur calibration, drift, corrélation, latence
|
||||
|
||||
Utilisation dans cycle.py :
|
||||
from .metrics import record_cycle_metrics
|
||||
record_cycle_metrics(client, cycle_id, model_name, ...)
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import pandas as pd
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Seuils d'alerte (configurables via env vars)
|
||||
import os
|
||||
ALERT_ANOMALY_RATE_HIGH = float(os.getenv('ALERT_ANOMALY_RATE_HIGH', '0.10'))
|
||||
ALERT_ANOMALY_RATE_LOW = float(os.getenv('ALERT_ANOMALY_RATE_LOW', '0.005'))
|
||||
ALERT_DRIFT_RATE = float(os.getenv('ALERT_DRIFT_RATE', '0.30'))
|
||||
ALERT_CORRELATION_RATE = float(os.getenv('ALERT_CORRELATION_RATE', '0.50'))
|
||||
ALERT_LATENCY_MS = int(os.getenv('ALERT_LATENCY_MS', '300000'))
|
||||
|
||||
|
||||
def record_cycle_metrics(
|
||||
client,
|
||||
db: str,
|
||||
cycle_id: str,
|
||||
model_name: str,
|
||||
df_all: pd.DataFrame,
|
||||
anomalies: pd.DataFrame,
|
||||
all_scored: pd.DataFrame,
|
||||
drift_rate: float,
|
||||
cycle_start_time: float,
|
||||
baseline_size: int,
|
||||
threshold: float,
|
||||
valid_features: int,
|
||||
total_features: int,
|
||||
meta_learner_active: bool = False,
|
||||
) -> None:
|
||||
"""Enregistre les métriques d'un cycle dans ml_performance_metrics.
|
||||
|
||||
Émet également des alertes dans les logs si les seuils sont dépassés.
|
||||
|
||||
Args:
|
||||
client : ClickHouseClient actif
|
||||
db : nom de la base ja4_processing
|
||||
cycle_id : identifiant du cycle (timestamp)
|
||||
model_name : 'Complet' ou 'Applicatif' (ou variante 24h)
|
||||
df_all : DataFrame complet du cycle (avant scoring)
|
||||
anomalies : DataFrame des anomalies détectées
|
||||
all_scored : DataFrame de tous les scores
|
||||
drift_rate : taux de features en dérive [0, 1]
|
||||
cycle_start_time : timestamp (time.time()) du début du cycle
|
||||
baseline_size : nombre de sessions dans la baseline humaine
|
||||
threshold : seuil adaptatif utilisé
|
||||
valid_features : nombre de features valides
|
||||
total_features : nombre total de features demandées
|
||||
meta_learner_active : True si le meta-learner a été utilisé ce cycle
|
||||
"""
|
||||
now = datetime.utcnow()
|
||||
latency_ms = int((time.time() - cycle_start_time) * 1000)
|
||||
|
||||
n_total = max(len(df_all), 1)
|
||||
n_scored = max(len(all_scored), 1)
|
||||
|
||||
# Taux de corrélation
|
||||
if 'correlated' in df_all.columns:
|
||||
n_correlated = int((df_all['correlated'] == 1).sum())
|
||||
correlated_rate = n_correlated / n_total
|
||||
else:
|
||||
correlated_rate = 0.0
|
||||
|
||||
# Comptage par niveau de menace
|
||||
n_critical = n_high = n_medium = n_low = 0
|
||||
if not anomalies.empty and 'threat_level' in anomalies.columns:
|
||||
levels = anomalies['threat_level'].value_counts()
|
||||
n_critical = int(levels.get('CRITICAL', 0))
|
||||
n_high = int(levels.get('HIGH', 0))
|
||||
n_medium = int(levels.get('MEDIUM', 0))
|
||||
n_low = int(levels.get('LOW', 0))
|
||||
|
||||
# Bots connus et navigateurs légitimes
|
||||
n_known_bot = 0
|
||||
n_anubis_deny = 0
|
||||
n_legit_browser = 0
|
||||
if not df_all.empty:
|
||||
if 'bot_name' in df_all.columns:
|
||||
n_known_bot = int((df_all['bot_name'].fillna('') != '').sum())
|
||||
if 'anubis_bot_action' in df_all.columns:
|
||||
n_anubis_deny = int((df_all['anubis_bot_action'] == 'DENY').sum())
|
||||
if 'browser_confidence' in df_all.columns:
|
||||
from .config import BROWSER_CONFIDENCE_THRESHOLD
|
||||
n_legit_browser = int((df_all['browser_confidence'] >= BROWSER_CONFIDENCE_THRESHOLD).sum())
|
||||
|
||||
anomaly_rate = (n_critical + n_high + n_medium + n_low) / n_scored
|
||||
drift_alert = 1 if drift_rate > ALERT_DRIFT_RATE else 0
|
||||
|
||||
# Alertes
|
||||
_emit_alerts(model_name, anomaly_rate, drift_rate, correlated_rate, latency_ms, drift_alert)
|
||||
|
||||
try:
|
||||
client.execute(
|
||||
f"INSERT INTO {db}.ml_performance_metrics VALUES",
|
||||
[{
|
||||
'cycle_at': now,
|
||||
'model_name': model_name,
|
||||
'total_sessions': n_total,
|
||||
'correlated_rate': round(float(correlated_rate), 4),
|
||||
'anomaly_rate': round(float(anomaly_rate), 4),
|
||||
'critical_count': n_critical,
|
||||
'high_count': n_high,
|
||||
'medium_count': n_medium,
|
||||
'low_count': n_low,
|
||||
'known_bot_count': n_known_bot,
|
||||
'anubis_deny_count': n_anubis_deny,
|
||||
'legit_browser_count': n_legit_browser,
|
||||
'drift_rate': round(float(drift_rate), 4),
|
||||
'drift_alert': drift_alert,
|
||||
'cycle_latency_ms': latency_ms,
|
||||
'features_valid': valid_features,
|
||||
'features_total': total_features,
|
||||
'baseline_size': baseline_size,
|
||||
'threshold': round(float(threshold), 6),
|
||||
'meta_learner_active': 1 if meta_learner_active else 0,
|
||||
}]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Metrics] Erreur d'enregistrement des métriques : {e}")
|
||||
|
||||
|
||||
def _emit_alerts(model_name: str, anomaly_rate: float, drift_rate: float,
|
||||
correlated_rate: float, latency_ms: int, drift_alert: int) -> None:
|
||||
"""Émet des alertes dans les logs si les seuils sont dépassés."""
|
||||
if anomaly_rate > ALERT_ANOMALY_RATE_HIGH:
|
||||
logger.warning(
|
||||
f"[{model_name}] ⚠ ALERTE CALIBRATION : taux d'anomalie élevé "
|
||||
f"({anomaly_rate:.1%} > {ALERT_ANOMALY_RATE_HIGH:.1%})"
|
||||
)
|
||||
elif anomaly_rate < ALERT_ANOMALY_RATE_LOW and anomaly_rate > 0:
|
||||
logger.warning(
|
||||
f"[{model_name}] ⚠ ALERTE CALIBRATION : taux d'anomalie très bas "
|
||||
f"({anomaly_rate:.3%} < {ALERT_ANOMALY_RATE_LOW:.1%})"
|
||||
)
|
||||
if drift_alert:
|
||||
logger.warning(
|
||||
f"[{model_name}] ⚠ ALERTE DRIFT : {drift_rate:.1%} des features en dérive "
|
||||
f"(seuil {ALERT_DRIFT_RATE:.1%})"
|
||||
)
|
||||
if correlated_rate < ALERT_CORRELATION_RATE:
|
||||
logger.warning(
|
||||
f"[{model_name}] ⚠ ALERTE CORRÉLATION : taux de corrélation bas "
|
||||
f"({correlated_rate:.1%} < {ALERT_CORRELATION_RATE:.1%}) — "
|
||||
"vérifier ja4sentinel/logcorrelator"
|
||||
)
|
||||
if latency_ms > ALERT_LATENCY_MS:
|
||||
logger.warning(
|
||||
f"[{model_name}] ⚠ ALERTE PERFORMANCE : latence cycle {latency_ms}ms "
|
||||
f"> {ALERT_LATENCY_MS}ms"
|
||||
)
|
||||
Reference in New Issue
Block a user