feat(e2e): add distributed E2E test framework with parametric traffic generation

Add run-e2e-test.sh with CLI parameters (--hits, --http-ratio, --dns, --tls,
--src-ips, --keep-analysis, --up) for configurable traffic generation. Traffic
runs from VM endpoints with multiple source IPs (alias IPs on eth0) to produce
distinct sessions for the ML pipeline. Fix curl TLS flags (--tlsv1.2 instead
of --tls-v1-2), skip redundant local verification in distributed mode, and
fix dashboard is_available() cache that never retried after ClickHouse recovery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-15 00:09:32 +02:00
parent 7894d39f1c
commit f88b739992
40 changed files with 2154 additions and 337 deletions

View File

@ -3,9 +3,9 @@
Service Python de détection d'anomalies par apprentissage automatique semi-supervisé
sur le trafic HTTP/TLS agrégé dans ClickHouse. Fonctionne en cycle continu
(par défaut toutes les 5 minutes) avec un **ensemble à triple voix**
(Extended Isolation Forest + Autoencoder + XGBoost) piloté par un **méta-learner
à régression logistique**, enrichi par l'explicabilité **ExIFFI** et **SHAP**,
le clustering HDBSCAN, la détection de flottes coordonnées (NetworkX) et
(Extended Isolation Forest + NFEnsemble + XGBoost) piloté par un **méta-learner
MLP**, enrichi par l'explicabilité **ExIFFI** et **SHAP**,
le clustering HDBSCAN, la détection de flottes coordonnées (PyTorch Geometric GraphSAGE) et
la surveillance de performance par cycle.
---
@ -23,8 +23,8 @@ __main__.py Point d'entrée (python -m bot_detector)
├─ preprocessing.py Nettoyage du DataFrame, imputation, listes de features (FEATURES, FEATURES_COMPLET)
│ └─ browser.py Identification multifactorielle des navigateurs (6 axes)
├─ pipeline.py Orchestration : filtrage → entraînement → MetaLearner → ExIFFI → scoring → fusion
│ ├─ models.py EIF, TrafficAutoEncoder (PyTorch), XGBoost
│ └─ scoring.py Normalisation, MetaLearner, seuil adaptatif, ExIFFI, SHAP, HDBSCAN, dérive KS+KL
│ ├─ models.py EIF, NFEnsemble/TrafficNormalizingFlow (PyTorch), XGBoost
│ └─ scoring.py Normalisation, MetaLearner MLP, seuil adaptatif, ExIFFI, SHAP, HDBSCAN, dérive ADWIN+KS+KL
├─ browser_matcher.py Scoring H2 statique à 7 dimensions pondérées
│ └─ browser_signatures.py Signatures statiques Chrome/Firefox/Safari + rechargement ClickHouse
├─ browser_matcher_dynamic.py Scoring H2 dynamique temps réel (profils auto-appris)
@ -44,11 +44,11 @@ __main__.py Point d'entrée (python -m bot_detector)
| `browser_signatures.py` | 166 | Signatures statiques Chrome/Firefox/Safari + rechargement dynamique depuis ClickHouse |
| `browser_matcher_dynamic.py` | 387 | Scoring H2 dynamique temps réel contre profils auto-appris (`auto_browser_profiles`) |
| `profile_builder.py` | 614 | Profiling HDBSCAN hors-ligne : clustering, centroïdes, fusion, lifecycle (cron quotidien) |
| `scoring.py` | 564 | `MetaLearner` (régression logistique), normalisation, seuil adaptatif, ExIFFI, SHAP top-5, HDBSCAN, dérive KS+KL |
| `models.py` | 484 | `TrafficAutoEncoder`, entraînement/chargement EIF, XGBoost, élagage de features |
| `scoring.py` | 564 | `MetaLearner` MLP, normalisation, seuil adaptatif, ExIFFI, SHAP top-5, HDBSCAN, dérive ADWIN+KS+KL |
| `models.py` | 484 | `NFEnsemble`/`TrafficNormalizingFlow`, entraînement/chargement EIF, XGBoost, élagage de features |
| `preprocessing.py` | 127 | `preprocess_df()` — nettoyage, typage, imputation, listes `FEATURES` / `FEATURES_COMPLET` |
| `pipeline.py` | 441 | `run_semi_supervised_logic()` — orchestration complète d'un modèle, MetaLearner, ExIFFI |
| `fleet.py` | 174 | `build_fleet_graph()`, `detect_fleet_communities()`, `enrich_with_fleet_score()`NetworkX + HDBSCAN |
| `fleet.py` | 174 | `build_fleet_graph()`, `detect_fleet_communities()`, `enrich_with_fleet_score()`PyTorch Geometric GraphSAGE + HDBSCAN |
| `metrics.py` | 166 | `record_cycle_metrics()`, `_emit_alerts()` — table `ml_performance_metrics` |
| `cycle.py` | 415 | `fetch_and_analyze()` — boucle principale, feedback SOC, multiwindow |
| `__main__.py` | 41 | Point d'entrée, bannière de démarrage, boucle `while True` |
@ -85,15 +85,18 @@ Toute la configuration est lue via `os.getenv()` dans `config.py`. Aucun fichier
| `MIN_VALID_FEATURE_RATIO` | float | `0.50` | Ratio minimal de features valides pour entraîner |
| `PRUNE_VARIANCE_THRESHOLD` | float | `1e-6` | Seuil de variance pour l'élagage de features |
| `VAL_ANOMALY_GATE` | float | `0.20` | Garde-fou : taux maximum d'anomalies en validation |
| `MIN_HUMAN_BASELINE` | int | `500` | Nombre minimum de sessions humaines pour entraîner l'IF |
| `BASELINE_ACCEPT_UNKNOWN` | bool | `false` | Mode test : utiliser ASN `unknown` comme fallback si baseline ISP insuffisante |
### Autoencoder
### NFEnsemble (Deep Ensemble M=5 Normalizing Flows)
| Variable | Type | Défaut | Description |
|----------|------|--------|-------------|
| `AE_WEIGHT` | float | `0.30` | Poids de l'Autoencoder dans le score combiné (plage ]0, 1[) |
| `AE_EPOCHS` | int | `50` | Nombre d'époques d'entraînement |
| `AE_LATENT_DIM` | int | `16` | Dimension de l'espace latent |
| `AE_WEIGHT` | float | `0.30` | Poids du NFEnsemble dans le score combiné (plage ]0, 1[) |
| `AE_EPOCHS` | int | `50` | Nombre d'époques d'entraînement par membre |
| `AE_LATENT_DIM` | int | `16` | Dimension de l'espace latent (coupling layers RealNVP) |
| `AE_LEARNING_RATE` | float | `1e-3` | Taux d'apprentissage Adam |
| `NF_UNCERTAINTY_THRESHOLD` | float | `1.0` | Seuil de variance inter-modèles pour la détection adversariale |
### XGBoost
@ -154,8 +157,8 @@ Le bot-detector utilise trois modèles en parallèle, combinés par une pondéra
└──────────────────────┘ │
× (1 AE_WEIGHT)
┌──────────────────────┐ │
TrafficAutoEncoder │ ├──→ combined_norm
│ (PyTorch) │──→ ae_norm (01)
NFEnsemble (M=5 NF) │ ├──→ combined_norm
│ (PyTorch RealNVP) │──→ ae_norm (01)
└──────────────────────┘ × AE_WEIGHT
× (1 XGB_WEIGHT)
┌──────────────────────┐ │
@ -208,24 +211,38 @@ isotree.IsolationForest(
**Calibration** : le score isotree brut (∈ [0, 1], >0.5 = anomalous) est converti en convention sklearn : `sklearn_equiv = 0.5 isotree_score`.
### TrafficAutoEncoder (PyTorch)
### NFEnsemble — Deep Ensemble M=5 Normalizing Flows (PyTorch)
Architecture symétrique encodeur-décodeur :
Le NFEnsemble est un **deep ensemble** de M=5 `TrafficNormalizingFlow` indépendants, chacun
basé sur l'architecture RealNVP (Dinh et al., 2017) avec couches de coupling affines,
permutations fixes et batch normalization.
**TrafficNormalizingFlow** (membre individuel) :
```
Encodeur : n_features → dim1 → dim2 → 16 (latent)
Décodeur : 16 → dim2 → dim1 → n_features
dim1 = min(64, max(n_features, latent_dim + 4))
dim2 = min(32, max(dim1 // 2, latent_dim + 2))
Couche 1 : Permutation fixe → RealNVP coupling → BatchNorm
Couche 2 : Permutation fixe → RealNVP coupling → BatchNorm
...
Sortie : log-probabilité exacte par la règle de changement de variable
```
- Activations : `ReLU` + `BatchNorm1d` sur les couches cachées, `Sigmoid` en sortie du décodeur
- Optimiseur : `Adam(lr=1e-3, weight_decay=1e-5)`
- Perte : `MSELoss`
- Entraînement : 50 époques, batch_size=256
- Score : erreur de reconstruction MSE par échantillon
- Normalisation des entrées : min-max [0, 1] par feature
- **Coupling layers** : transformations affines conditionnées (scale + shift)
- **Permutations** : permutations fixes alternées entre les couches
- **BatchNorm** : normalisation par batch entre les couches de coupling
- **Optimiseur** : `Adam(lr=1e-3, weight_decay=1e-5)`
- **Perte** : Negative Log-Likelihood (NLL) — `-log p(x)` via la règle de changement de variable
- **Entraînement** : 50 époques, batch_size=256, échantillonnage bootstrap (avec remise) par membre
- **Score** : `-log p(x)` — plus élevé = plus anomalous
- **Normalisation des entrées** : min-max [0, 1] par feature
**NFEnsemble** (M=5, Lakshminarayanan et al., 2017) :
- **Incertitude** : la variance inter-modèles des scores `-log p(x)` quantifie l'incertitude épistémique.
- Faible incertitude → dérive organique (les modèles s'accordent)
- Forte incertitude → dérive adversariale probable (les modèles divergent)
- **Seuil d'incertitude** : `NF_UNCERTAINTY_THRESHOLD` (défaut 1.0) — au-delà, `is_adversarial_drift = True`
- **Score final** : moyenne des `-log p(x)` sur les M modèles (rétro-compatible avec le pipeline)
- **Sérialisation** : `state_dict()` contient `ensemble_size`, `n_features`, et les `state_dict` de chaque membre
### XGBoost (supervisé)
@ -324,7 +341,7 @@ hdbscan.HDBSCAN(
)
```
**Espace de clustering** : si un Autoencoder est disponible, le clustering s'effectue dans l'**espace latent 16-dim** de l'AE. Sinon, `StandardScaler` est appliqué sur les features brutes.
**Espace de clustering** : si un NFEnsemble est disponible, le clustering s'effectue dans l'**espace latent moyen** du Deep Ensemble. Sinon, `StandardScaler` est appliqué sur les features brutes.
**Fallback** si `hdbscan` n'est pas disponible : `DBSCAN(eps=0.5, min_samples=CLUSTERING_MIN_SAMPLES)`.
@ -506,20 +523,20 @@ Chaque anomalie reçoit un `campaign_id` (1 = pas de cluster).
a. Validation des features (exclure constantes/manquantes)
b. Séparation des bots connus → journalisation KNOWN_BOT
c. Filtrage de la baseline humaine (asn_label = 'human', fingerprint_coherence_score ≥ seuil)
d. Chargement ou entraînement EIF + AE
e. Scoring du trafic inconnu (EIF + AE)
d. Chargement ou entraînement EIF + NFEnsemble
e. Scoring du trafic inconnu (EIF + NFEnsemble, incertitude inter-modèles)
f. Chargement ou entraînement XGBoost (si labels disponibles)
g. MetaLearner : pondération apprise (logistique) sur historique SOC, sinon fallback poids fixes
h. Combinaison des scores via MetaLearner ou formule fixe
i. Normalisation [0, 1]
j. Seuil adaptatif (percentile_5 des scores négatifs, minimum -0.05)
k. Pénalité de récurrence
l. ExIFFI (importance par profondeur d'isolation EIF) + erreur AE par feature
l. ExIFFI (importance par profondeur d'isolation EIF) + erreur NF par feature
m. SHAP top-5 TreeExplainer
n. HDBSCAN clustering → campaign_id
o. Détection de dérive (KS test + KL divergence)
p. Alerte drift adversarial (dérive simultanée multiple features → direction commune)
8. Analyse de flotte (fleet.py) : graphe bipartite JA4×ASN → communautés Louvain → fleet_score
o. Détection de dérive (ADWIN + KS test + KL divergence)
p. Alerte drift adversarial (dérive simultanée multiple features → direction commune, ou incertitude NF > NF_UNCERTAINTY_THRESHOLD)
8. Analyse de flotte (fleet.py) : graphe bipartite JA4×ASN → communautés GraphSAGE → fleet_score
9. Scoring dynamique H2 (browser_matcher_dynamic.py) : profils auto-appris vs sessions entrantes
10. Mode multi-fenêtre (si activé) : idem sur view_ai_features_24h
11. Insertion → ml_all_scores (toutes les sessions scorées)
@ -532,9 +549,15 @@ Chaque anomalie reçoit un `campaign_id` (1 = pas de cluster).
---
## Détection de dérive (KS + KL divergence)
## Détection de dérive (ADWIN + KS + KL divergence)
Par feature, deux tests comparent la distribution courante avec la distribution d'entraînement :
Par feature, trois mécanismes comparent la distribution courante avec la distribution d'entraînement :
**ADWIN (Adaptive Windowing)** (River) :
- Détection en ligne par fenêtre adaptative chaque feature est surveillée par un détecteur ADWIN indépendant
- Propriété `drift_detected` (bool) vrai quand un changement de distribution est détecté dans la fenêtre
- Pas de seuil manuel ADWIN ajuste automatiquement la taille de fenêtre
- Remplace la dérive KS+KL pour les features continues en temps réel
**Test KS (Kolmogorov-Smirnov)** :
- Distribution reconstruite par interpolation à partir d'un digest quantile 9 points (p5, p10, p25, p50, p75, p90, p95)
@ -543,9 +566,9 @@ Par feature, deux tests comparent la distribution courante avec la distribution
**Divergence KL (Kullback-Leibler)** :
- Histogramme discrétisé (20 bins) de la distribution courante vs baseline
- Feature driftée si `KL > seuil` (0.5 par défaut)
- Détection de **drift adversarial** : si 30% des features dérivent simultanément dans la même direction alerte `ADVERSARIAL_DRIFT`
- Détection de **drift adversarial** : si 30% des features dérivent simultanément dans la même direction alerte `ADVERSARIAL_DRIFT`. Également déclenché quand `nf_uncertainty > NF_UNCERTAINTY_THRESHOLD` alerte `ADVERSARIAL_DRIFT_NF`
**Règle de décision** : une feature est en drift si KS **ou** KL dépasse son seuil.
**Règle de décision** : une feature est en drift si ADWIN détecte un changement, ou si KS **ou** KL dépasse son seuil.
- Dérive globale = fraction de features driftées
- Si `drift > DRIFT_THRESHOLD` (0.30) réentraînement automatique
@ -556,12 +579,13 @@ Par feature, deux tests comparent la distribution courante avec la distribution
## MetaLearner
Remplace la pondération linéaire fixe `(1-XGB_W)×((1-AE_W)×eif + AE_W×ae) + XGB_W×xgb` par une régression logistique apprise (`scoring.MetaLearner`) :
Remplace la pondération linéaire fixe `(1-XGB_W)×((1-AE_W)×eif + AE_W×ae) + XGB_W×xgb` par un MLP appris (`scoring.MetaLearner`) :
```
P(bot) = logistic(w1×eif + w2×ae + w3×xgb + w4×volume + w5×correlated + bias)
P(bot) = MLP(w1×eif + w2×ae + w3×xgb + w4×volume + w5×correlated + bias)
```
- **Architecture** : MLP (Multi-Layer Perceptron) avec couches cachées remplace la régression logistique
- **Entraînement** : sur l'historique `ml_all_scores` JOIN `soc_feedback` (labels SOC + KNOWN_BOT + ANUBIS_DENY + LEGITIMATE_BROWSER)
- **Seuil** : activé seulement si 1000 labels disponibles sinon fallback aux poids fixes
- **Transparence** : poids appris journalisés dans `ml_performance_metrics` pour audit SOC
@ -578,10 +602,10 @@ En complément de SHAP, le module expose deux méthodes d'importance de features
- Une feature avec profondeur d'isolation faible contribue fortement à l'anomalie
- Corrèle avec SHAP mais capte des aspects complémentaires de la structure EIF
**Erreur AE par feature** (`compute_ae_feature_errors`) :
- Reconstruction PyTorch feature par feature : `err_i = (x_i - x̂_i)²`
**Erreur NF par feature** (`compute_ae_feature_errors`) :
- Score de reconstruction NF feature par feature : `err_i = (x_i - x̂_i)²` dans l'espace latent
- Pour chaque anomalie, les features avec la plus grande erreur de reconstruction sont identifiées
- Expose quelles dimensions l'autoencoder ne parvient pas à reconstruire
- Expose quelles dimensions le Normalizing Flow ne parvient pas à reconstruire
Les deux méthodes sont disponibles dans le champ `shap_features` des résultats, en complément des valeurs SHAP TreeExplainer.
@ -592,8 +616,8 @@ Les deux méthodes sont disponibles dans le champ `shap_features` des résultats
Détecte les **botnets coordonnés** utilisant des JA4 et ASN rotatifs via analyse de graphe bipartite :
1. **Construction du graphe** : nœuds JA4 ASN, arêtes IP observées dans le cycle
2. **Projection** : projection du graphe bipartite sur les nœuds JA4
3. **Communautés** : algorithme de Louvain (NetworkX) sur le graphe projeté
2. **Embedding** : PyTorch Geometric GraphSAGE pour apprendre des embeddings de nœuds
3. **Communautés** : clustering dans l'espace d'embedding GraphSAGE
4. **Score de flotte** : `fleet_score = taille_communauté × densité_arêtes / log(nb_ASN)`
5. **Enrichissement** : les IPs membres reçoivent un malus proportionnel au fleet_score
@ -613,7 +637,7 @@ Enregistre par cycle dans `ml_performance_metrics` :
| `drift_rate` | Fraction de features en dérive |
| `corr_rate` | Taux de sessions corrélées (cible : 50%) |
| `cycle_latency_s` | Durée totale d'inférence (cible : <300s) |
| `alert_flags` | Alertes émises (CALIBRATION_HIGH/LOW, DRIFT, CORRELATION, LATENCY) |
| `alert_flags` | Alertes émises (CALIBRATION_HIGH/LOW, DRIFT, CORRELATION, LATENCY, ADVERSARIAL_DRIFT_NF) |
**Seuils d'alerte** :
- `anomaly_rate > 10%` `CALIBRATION_HIGH`
@ -628,11 +652,8 @@ Enregistre par cycle dans `ml_performance_metrics` :
La vue `view_ai_features_1h` enrichit chaque IP via les dictionnaires Anubis selon une cascade de priorité :
1. **UA + IP combinés** (même `rule_id`) confiance maximale
2. **UA seul** (pas de condition IP)
3. **IP seul** (pas de condition UA)
4. **Correspondance ASN**
5. **Correspondance pays**
1. **IP/CIDR** correspondance exacte d'adresse ou de sous-réseau
2. **ASN** correspondance par numéro d'ASN
---