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:
@ -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 (0–1)
|
||||
│ NFEnsemble (M=5 NF) │ ├──→ combined_norm
|
||||
│ (PyTorch RealNVP) │──→ ae_norm (0–1)
|
||||
└──────────────────────┘ × 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
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user