Files
ja4-platform/docs/services/bot-detector.md
Jacquin Antoine f88b739992 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>
2026-04-15 00:09:32 +02:00

734 lines
35 KiB
Markdown
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.

# Bot Detector
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 + 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.
---
## Architecture des modules
Le service est découpé en **16 modules** organisés ainsi :
```
__main__.py Point d'entrée (python -m bot_detector)
└─ cycle.py Boucle principale : requête ClickHouse → pipeline → insertion
├─ config.py Variables d'environnement, flags de disponibilité
├─ log.py Journalisation structurée JSON (structlog + RotatingFileHandler)
├─ infra.py Client ClickHouse (via ja4_common), health check HTTP, arrêt propre
├─ 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, 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)
├─ profile_builder.py Profiling HDBSCAN hors-ligne, centroïdes, lifecycle (cron quotidien)
├─ fleet.py Graphe bipartite JA4×ASN (NetworkX), fleet_score, fleet_detections
├─ metrics.py Métriques de cycle, alertes, ml_performance_metrics
└─ (insère dans ml_all_scores + ml_detected_anomalies + fleet_detections + ml_performance_metrics)
```
| Module | Lignes | Rôle |
|--------|--------|------|
| `config.py` | 154 | Toute la configuration via `os.getenv()`, flags de disponibilité des librairies |
| `log.py` | 65 | `log_info()`, `log_decision()`, `append_training_history()` — JSONL rotatif |
| `infra.py` | 89 | Client ClickHouse (délègue à `ja4_common`), `score_to_threat_level()`, serveur de santé en thread daemon |
| `browser.py` | 191 | Détection multifactorielle des navigateurs sur **6 axes** pondérés (ajout `axis_h2_coherence`) |
| `browser_matcher.py` | 498 | Scoring H2 statique à 7 dimensions pondérées (SETTINGS, WINDOW_UPDATE, pseudo-order, etc.) |
| `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` 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()` — 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` |
---
## Configuration
Toute la configuration est lue via `os.getenv()` dans `config.py`. Aucun fichier YAML ni pydantic-settings.
### Connexion ClickHouse
| Variable | Type | Défaut | Description |
|----------|------|--------|-------------|
| `CLICKHOUSE_HOST` | str | `clickhouse` | Nom d'hôte du serveur ClickHouse (via `ja4_common`) |
| `CLICKHOUSE_PORT` | int | `8123` | Port HTTP ClickHouse (via `ja4_common`) |
| `CLICKHOUSE_USER` | str | `admin` | Utilisateur ClickHouse (via `ja4_common`) |
| `CLICKHOUSE_PASSWORD` | str | `""` | Mot de passe ClickHouse (via `ja4_common`) |
| `CLICKHOUSE_DB_PROCESSING` | str | `ja4_processing` | Base de données ML/agrégations (fallback : `CLICKHOUSE_DB`) |
| `CLICKHOUSE_DB_LOGS` | str | `ja4_logs` | Base de données des logs HTTP |
### Modèle et entraînement
| Variable | Type | Défaut | Description |
|----------|------|--------|-------------|
| `N_ESTIMATORS` | int | `300` | Nombre d'arbres pour l'Extended Isolation Forest |
| `ISOLATION_CONTAMINATION` | float | `0.001` | Paramètre de contamination EIF (plage ]0, 0.5[) |
| `ANOMALY_THRESHOLD` | float | `-0.05` | Seuil de score brut pour la détection d'anomalie |
| `ANOMALY_PERCENTILE` | int | `5` | Percentile pour le seuil adaptatif |
| `MODEL_DIR` | str | `/var/lib/bot_detector` | Répertoire de persistance des modèles |
| `MODEL_HISTORY_COUNT` | int | `10` | Nombre de versions de modèle conservées |
| `RETRAIN_INTERVAL_HOURS` | int | `24` | Intervalle de réentraînement EIF (heures) |
| `DRIFT_THRESHOLD` | float | `0.30` | Seuil de dérive KS (fraction de features driftées) |
| `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 |
### NFEnsemble (Deep Ensemble M=5 Normalizing Flows)
| Variable | Type | Défaut | Description |
|----------|------|--------|-------------|
| `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
| Variable | Type | Défaut | Description |
|----------|------|--------|-------------|
| `XGB_WEIGHT` | float | `0.20` | Poids de XGBoost dans le score final (plage ]0, 1[) |
| `XGB_MIN_LABELS` | int | `100` | Nombre minimal de labels SOC pour activer XGBoost |
| `XGB_RETRAIN_INTERVAL_HOURS` | int | `168` | Intervalle de réentraînement XGBoost (7 jours) |
### Détection navigateur
| Variable | Type | Défaut | Description |
|----------|------|--------|-------------|
| `BROWSER_CONFIDENCE_THRESHOLD` | float | `0.55` | Confiance minimale pour classifier `LEGITIMATE_BROWSER` |
| `BROWSER_COHORT_RATIO` | float | `0.70` | Si ≥ 70 % des sessions d'un JA4 sont navigateur → propagation |
### Clustering et explicabilité
| Variable | Type | Défaut | Description |
|----------|------|--------|-------------|
| `ENABLE_CLUSTERING` | bool | `true` | Activer le clustering HDBSCAN |
| `CLUSTERING_MIN_SAMPLES` | int | `3` | Taille minimale de cluster |
| `ENABLE_SHAP` | bool | `true` | Activer l'explicabilité SHAP (requiert `shap` installé) |
### Cycle et opérations
| Variable | Type | Défaut | Description |
|----------|------|--------|-------------|
| `CYCLE_INTERVAL_SEC` | int | `300` | Intervalle entre les cycles (secondes) |
| `MAX_CONSECUTIVE_FAILURES` | int | `3` | Échecs consécutifs avant `healthy=False` |
| `DEDUP_TTL_MIN` | int | `60` | TTL de déduplication inter-cycle (minutes) |
| `RECURRENCE_WEIGHT` | float | `0.005` | Poids de la récurrence dans le score brut |
| `ENABLE_MULTIWINDOW` | bool | `false` | Activer l'analyse multi-fenêtre 24h |
| `MULTIWINDOW_VIEW` | str | `view_ai_features_24h` | Vue ClickHouse pour le mode multi-fenêtre |
| `ENABLE_FEEDBACK` | bool | `true` | Activer l'intégration du feedback SOC |
| `FEEDBACK_WINDOW_DAYS` | int | `7` | Fenêtre de feedback SOC (jours) |
### Journalisation et santé
| Variable | Type | Défaut | Description |
|----------|------|--------|-------------|
| `BOT_DETECTOR_LOG` | str | `/var/log/bot_detector/decisions.jsonl` | Chemin du fichier de décisions |
| `LOG_BACKUP_COUNT` | int | `7` | Nombre de fichiers de rotation conservés |
| `HEALTH_PORT` | int | `8080` | Port du serveur de santé HTTP |
---
## Pipeline ML — Ensemble à triple voix
### Vue d'ensemble
Le bot-detector utilise trois modèles en parallèle, combinés par une pondération configurable :
```
┌──────────────────────┐
│ Extended Isolation │
│ Forest (isotree) │──→ eif_norm (01)
└──────────────────────┘ │
× (1 AE_WEIGHT)
┌──────────────────────┐ │
│ NFEnsemble (M=5 NF) │ ├──→ combined_norm
│ (PyTorch RealNVP) │──→ ae_norm (01)
└──────────────────────┘ × AE_WEIGHT
× (1 XGB_WEIGHT)
┌──────────────────────┐ │
│ XGBoost │ ├──→ anomaly_score
│ (supervisé, labels │──→ xgb_prob (01)
│ SOC + Cleanlab) │ × XGB_WEIGHT
└──────────────────────┘
```
**Formule du score final :**
```
combined_norm = (1 AE_WEIGHT) × eif_norm + AE_WEIGHT × ae_norm
anomaly_score = (1 XGB_WEIGHT) × combined_norm + XGB_WEIGHT × xgb_prob
```
Avec les poids par défaut (`AE_WEIGHT=0.30`, `XGB_WEIGHT=0.20`) :
```
anomaly_score = 0.56 × eif_norm + 0.24 × ae_norm + 0.20 × xgb_prob
```
### Architecture duale
Deux modèles indépendants tournent sur chaque cycle :
| Modèle | Condition | Features | Données |
|--------|-----------|----------|---------|
| **Complet** | `correlated = 1` | 85 (`FEATURES_COMPLET`) | HTTP + TCP + TLS (L3→L7) |
| **Applicatif** | `correlated = 0` | 73 (`FEATURES`) | HTTP seul (L7 pur) |
En mode multi-fenêtre (`ENABLE_MULTIWINDOW=true`), deux variantes supplémentaires sont exécutées sur la vue 24h : `Complet_24h` et `Applicatif_24h`.
### Extended Isolation Forest (EIF)
Modèle principal non supervisé. Utilise `isotree.IsolationForest` :
```python
isotree.IsolationForest(
ntrees=300, # N_ESTIMATORS
ndim=min(3, n_features),
sample_size='auto',
missing_action='impute',
random_seed=42,
nthreads=-1,
)
```
**Fallback** si `isotree` n'est pas disponible : `sklearn.ensemble.IsolationForest(n_estimators=300, contamination=CONTAMINATION)`.
**Calibration** : le score isotree brut (∈ [0, 1], >0.5 = anomalous) est converti en convention sklearn : `sklearn_equiv = 0.5 isotree_score`.
### NFEnsemble — Deep Ensemble M=5 Normalizing Flows (PyTorch)
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) :
```
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
```
- **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é)
Entraîné sur les labels issus du feedback SOC (table `soc_feedback`), filtrés par Cleanlab :
```python
xgb.XGBClassifier(
n_estimators=200,
max_depth=6,
learning_rate=0.1,
scale_pos_weight=auto, # max(1, n_neg / n_pos)
eval_metric='logloss',
tree_method='hist',
random_state=42,
n_jobs=-1,
)
```
- Labels positifs (bot) : `HIGH`, `CRITICAL`, `ANUBIS_DENY`, `KNOWN_BOT`
- Labels négatifs (légitime) : `NORMAL`, `LEGITIMATE_BROWSER`
- Activation requiert ≥ `XGB_MIN_LABELS` (100) labels
- Réentraînement tous les `XGB_RETRAIN_INTERVAL_HOURS` (168h = 7 jours)
- **Filtrage Cleanlab** : avant l'entraînement, un XGBoost rapide (80 arbres, 3-fold CV) produit des `pred_probs` qui alimentent `cleanlab.filter.find_label_issues()`. Les exemples identifiés comme bruités sont exclus du jeu d'entraînement. En cas d'échec, les labels bruts sont conservés.
---
## Détection multifactorielle des navigateurs
Module `browser.py` — classifie chaque session sur 5 axes pondérés :
| Axe | Clé | Poids | Composantes |
|-----|-----|-------|-------------|
| **1 — JA4 connu** | `axis_ja4_known` | 0.25 | Famille navigateur identifiée dans `dict_browser_ja4` → 1.0, sinon 0.0 |
| **2 — Structure JA4** | `axis_ja4_struct` | 0.15 | TLS 1.3 (×0.35), h2/h3 (×0.25), nb ciphers 1025 (×0.20), nb extensions 1025 (×0.20) |
| **3 — HTTP moderne** | `axis_http_modern` | 0.25 | modern_browser_score ≥ 50 (×0.35), Accept-Language (×0.20), Sec-Fetch < 0.3 (×0.25), generic_accept < 0.3 (×0.10), pas de ua_ch_mismatch (×0.10) |
| **4 — Comportement navigation** | `axis_nav_behavior` | 0.15 | has_cookie (×0.25), has_referer (×0.25), asset_ratio > 0.15 (×0.25), direct_access < 0.5 (×0.25) |
| **5 — Cohérence TLS/TCP** | `axis_tls_coherence` | 0.20 | Pas d'alpn_mismatch (×0.25), window_scale OK (×0.20), tls12 < 0.1 (×0.20), pas d'http10 (×0.15), ALPN présent (×0.20) |
**Seuil** : `browser_confidence ≥ 0.55` + famille identifiée `LEGITIMATE_BROWSER`
**Propagation par cohorte** : si 70 % des sessions partageant un JA4 sont classées navigateur, les sessions `NORMAL`/`LOW` restantes avec le même JA4 sont aussi classées `LEGITIMATE_BROWSER`.
**Inférence de famille** : pour les JA4 inconnus, correspondance structurelle avec les profils `_BROWSER_JA4_PROFILES` (Chromium, Firefox, Safari, Tor_Browser) requiert `browser_confidence ≥ 0.45`.
---
## Scoring et normalisation
### Normalisation des scores (`normalize_scores`)
Les scores bruts (négatifs = anomalous) sont mappés vers **[0, 1]** avec **1 = le plus anomalous** :
```
result[mask] = clip(scores / (s_min + 1e-9), 0, 1)
```
Les scores 0 sont mis à 0.
### Seuil adaptatif (`compute_adaptive_threshold`)
```
threshold = min(percentile(scores_négatifs, ANOMALY_PERCENTILE), ANOMALY_THRESHOLD)
```
Avec `ANOMALY_PERCENTILE=5` et `ANOMALY_THRESHOLD=-0.05`.
### Pénalité de récurrence
Appliquée au `raw_anomaly_score` :
```
raw_anomaly_score = log1p(recurrence_count) × RECURRENCE_WEIGHT
```
### Niveaux de menace
| Plage de score brut | Niveau | Interprétation |
|---------------------|--------|----------------|
| `< 0.30` | **CRITICAL** | Comportement extrêmement anomalous |
| `< 0.15` | **HIGH** | Signal d'anomalie fort |
| `< 0.05` | **MEDIUM** | Anomalie modérée |
| `< 0` | **LOW** | Légèrement inhabituel |
| `≥ 0` | **NORMAL** | Trafic normal |
---
## Clustering HDBSCAN des campagnes
Lorsque `ENABLE_CLUSTERING=true`, les anomalies sont regroupées en campagnes par HDBSCAN :
```python
hdbscan.HDBSCAN(
min_cluster_size=CLUSTERING_MIN_SAMPLES, # défaut : 3
min_samples=max(2, CLUSTERING_MIN_SAMPLES 1), # défaut : 2
cluster_selection_method='eom',
)
```
**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)`.
Chaque anomalie reçoit un `campaign_id` (1 = pas de cluster).
---
## Liste des features
### Features communes — modèle Applicatif (73 features)
#### Comportement HTTP
| Feature | Description |
|---------|-------------|
| `hits` | Nombre de requêtes dans la fenêtre |
| `hit_velocity` | Requêtes par seconde |
| `fuzzing_index` | Score de diversité chemins/paramètres |
| `post_ratio` | Fraction de requêtes POST |
| `port_exhaustion_ratio` | Fraction de ports source distincts / total |
| `orphan_ratio` | Requêtes sans corrélation TLS |
| `head_ratio` | Fraction de requêtes HEAD |
| `http10_ratio` | Fraction de requêtes HTTP/1.0 |
| `generic_accept_ratio` | Fraction d'en-têtes Accept courts |
| `sec_fetch_absence_rate` | Fraction sans Sec-Fetch-Site |
| `missing_accept_enc_ratio` | Fraction sans Accept-Encoding |
| `http_scheme_ratio` | Fraction utilisant HTTP (pas HTTPS) |
#### Gestion de connexion
| Feature | Description |
|---------|-------------|
| `max_keepalives` | Max de requêtes sur une seule connexion Keep-Alive |
| `tcp_shared_count` | Connexions TCP partagées entre sessions |
| `multiplexing_efficiency` | Efficacité du multiplexage HTTP/2 |
#### Empreinte navigateur
| Feature | Description |
|---------|-------------|
| `header_count` | Nombre d'en-têtes HTTP envoyés |
| `has_accept_language` | Présence de l'en-tête Accept-Language |
| `has_cookie` | Présence de l'en-tête Cookie |
| `has_referer` | Présence de l'en-tête Referer |
| `modern_browser_score` | Score composite de conformité navigateur (0100) |
| `ua_ch_mismatch` | Incohérence User-Agent vs Client Hints |
| `ip_id_zero_ratio` | Paquets IP avec ID=0 (pile minimaliste/headless) |
| `header_order_shared_count` | IPs partageant le même ordre d'en-têtes |
| `header_order_confidence` | Entropie normalisée de l'ordre des en-têtes |
| `distinct_header_orders` | Ordres d'en-têtes distincts par IP |
| `is_fake_navigation` | Sec-Fetch-Mode=navigate avec destination non-document |
#### Comportement de navigation
| Feature | Description |
|---------|-------------|
| `request_size_variance` | Variance de la taille des requêtes |
| `mss_mobile_mismatch` | Incohérence TCP MSS vs profil mobile |
| `asset_ratio` | Fraction de requêtes de ressources statiques |
| `direct_access_ratio` | Accès directs (sans referer) |
| `is_ua_rotating` | Rotation de User-Agent détectée |
| `distinct_ja4_count` | Empreintes JA4 distinctes par IP |
| `anomalous_payload_ratio` | Fraction de charges utiles anomalous |
#### Concentration et rareté
| Feature | Description |
|---------|-------------|
| `src_port_density` | Entropie des ports source |
| `ja4_asn_concentration` | Concentration JA4 au sein de l'ASN |
| `ja4_country_concentration` | Concentration JA4 par pays |
| `is_rare_ja4` | Empreinte JA4 rare (< 100 hits totaux) |
#### Temporel et diversité
| Feature | Description |
|---------|-------------|
| `temporal_entropy` | Entropie de la distribution temporelle |
| `path_diversity_ratio` | Diversité des chemins URL |
| `url_depth_variance` | Variance de la profondeur des URL |
#### Anubis
| Feature | Description |
|---------|-------------|
| `anubis_is_flagged` | Signal de suspicion Anubis (bot détecté, action ni ALLOW/DENY/vide) |
#### Navigateur multifactoriel — 6 axes
| Feature | Description |
|---------|-------------|
| `is_known_browser` | JA4 correspond à un navigateur connu |
| `browser_consistency_score` | Score composite de cohérence navigateur |
| `browser_confidence` | Confiance globale de l'identification navigateur (seuil 0.55) |
| `axis_ja4_known` | Axe 1 : JA4 reconnu dans `dict_browser_ja4` |
| `axis_ja4_struct` | Axe 2 : structure JA4 cohérente (chiffrement, extensions TLS) |
| `axis_http_modern` | Axe 3 : comportement HTTP moderne (Sec-Fetch-*, Accept-Language) |
| `axis_nav_behavior` | Axe 4 : comportement de navigation (asset_ratio, referer, cascade) |
| `axis_tls_coherence` | Axe 5 : cohérence TLS/TCP (ALPN, SNI, MSS mobile) |
| `axis_h2_coherence` | Axe 6 : cohérence HTTP/2 (h2_fingerprint famille navigateur déclarée) |
#### Thèse §5 — Features avancées
| Feature | Description |
|---------|-------------|
| `seq_emb_0`..`seq_emb_31` | Embeddings séquentiels via Transformer 5.2, remplace path_transition_entropy + cadence_cv) |
| `burst_ratio` | Fraction de requêtes en rafale 5.3) |
| `pause_ratio` | Fraction de pauses longues 5.3) |
| `lag1_autocorrelation` | Autocorrélation lag-1 des inter-arrivées 5.3) |
| `benford_deviation` | Déviation par rapport à la loi de Benford |
| `root_to_first_asset_delay` | Délai HTML premier asset 5.4, détection headless) |
| `asset_load_stddev` | Écart-type des intervalles de chargement d'assets 5.4) |
| `host_diversity` | Diversité des hôtes ciblés 5.8) |
| `host_sweep_speed` | Vitesse de balayage des hôtes 5.8) |
| `host_coverage_uniformity` | Uniformité de couverture des hôtes 5.8) |
| `cross_domain_path_similarity` | Similarité Jaccard inter-hôte des chemins scanner latéral 5.8) |
#### Features HTTP/2
| Feature | Description |
|---------|-------------|
| `h2_settings_known` | Fingerprint HTTP/2 reconnu dans `dict_browser_h2` (0/1) |
| `h2_pseudo_order_match` | Ordre des pseudo-headers cohérent avec le navigateur déclaré (0/1) |
| `h2_ja4_coherence` | Cohérence HTTP/2 JA4 (même famille) (0/1) |
| `h2_settings_rare` | Fingerprint HTTP/2 avec <100 occurrences globales (0/1) |
#### Score de cohérence cross-layer
| Feature | Description |
|---------|-------------|
| `fingerprint_coherence_score` | Score composite (0.01.0) combinant : JA4UA, H2JA4, TCPUA, langueASN, Sec-CH-UAUA |
#### Features TCP fenêtre
| Feature | Description |
|---------|-------------|
| `true_window_size` | Taille réelle de la fenêtre TCP |
| `window_mss_ratio` | Ratio fenêtre TCP / MSS |
#### Features de détection avancées
| Feature | Description |
|---------|-------------|
| `has_xff` | Présence de l'en-tête X-Forwarded-For |
| `unusual_content_type_ratio` | Fraction de Content-Type inhabituels |
| `non_standard_port_ratio` | Fraction de ports non standards |
| `login_post_concentration` | Concentration de POST sur les pages de login |
| `sec_ch_mobile_mismatch` | Incohérence Sec-CH-UA-Mobile |
### Features supplémentaires — modèle Complet (+12 features TCP/TLS)
| Feature | Description |
|---------|-------------|
| `tcp_jitter_variance` | Variance du jitter TCP inter-paquets |
| `alpn_http_mismatch` | Incohérence ALPN vs protocole HTTP réel |
| `is_alpn_missing` | ALPN absent dans le ClientHello |
| `sni_host_mismatch` | Incohérence TLS SNI vs en-tête Host HTTP |
| `ja3_diversity_ratio` | Ratio de diversité JA3 par IP |
| `syn_timing_cv` | Coefficient de variation du timing SYN |
| `tls12_ratio` | Fraction de connexions TLS 1.2 |
| `ip_df_variance` | Variance du flag Don't-Fragment IP |
| `avg_ttl` | TTL IP moyen (empreinte OS) |
| `ttl_std` | Écart-type du TTL |
| `no_window_scale_ratio` | Fraction sans TCP Window Scale |
| `ja4_drift_ratio` | Dérive JA4 intra-session 5.5) |
---
## Pipeline de détection
```
1. Requête view_ai_features_1h → DataFrame
2. Enrichissement optionnel view_thesis_features_1h (features thèse §5)
3. Prétraitement : preprocess_df() (nettoyage, browser axes 6, imputation)
4. Chargement du feedback SOC → reclassification
5. Chargement de la carte de récurrence (view_ip_recurrence)
6. Séparation par correlated = 1 / correlated = 0
7. Pour chaque modèle (Complet, Applicatif) :
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 + 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 NF par feature
m. SHAP top-5 TreeExplainer
n. HDBSCAN clustering → campaign_id
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)
12. Déduplication intra-cycle (garder raw_anomaly_score le plus bas par IP)
13. Déduplication inter-cycle (TTL, skip si détecté récemment sauf aggravation ≥ 0.05)
14. Insertion → ml_detected_anomalies (anomalies filtrées)
15. Insertion → fleet_detections (flottes détectées avec fleet_score)
16. Enregistrement → ml_performance_metrics (métriques de cycle + alertes)
```
---
## Détection de dérive (ADWIN + KS + KL divergence)
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)
- Feature driftée si `p_value < 0.05`
**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`. Également déclenché quand `nf_uncertainty > NF_UNCERTAINTY_THRESHOLD` alerte `ADVERSARIAL_DRIFT_NF`
**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
**Fallback** (sans `scipy`) : méthode Z-score feature driftée si `|moyenne_courante moyenne_entraînement| / std_entraînement > 2.0`.
---
## 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) = 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
- **Validation** : comparaison MetaLearner vs poids fixes sur split temporel (données postérieures à l'entraînement)
---
## ExIFFI et explicabilité augmentée
En complément de SHAP, le module expose deux méthodes d'importance de features spécifiques aux modèles utilisés :
**ExIFFI** (`compute_exiffi_importance`) :
- Importance calculée par la profondeur moyenne d'isolation par feature dans l'EIF
- 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 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 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.
---
## Détection de flottes (fleet.py)
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. **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
Résultats stockés dans `fleet_detections` (TTL 7 jours). Exposés dans le dashboard via la page `/fleet`.
---
## Métriques de performance (metrics.py)
Enregistre par cycle dans `ml_performance_metrics` :
| Métrique | Description |
|----------|-------------|
| `anomaly_rate` | Taux d'anomalies détectées (cible : 0.5%10%) |
| `known_bot_rate` | Fraction KNOWN_BOT dans le cycle |
| `legit_browser_rate` | Fraction LEGITIMATE_BROWSER |
| `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, ADVERSARIAL_DRIFT_NF) |
**Seuils d'alerte** :
- `anomaly_rate > 10%` `CALIBRATION_HIGH`
- `anomaly_rate < 0.5%` `CALIBRATION_LOW`
- `drift_rate > 30%` `DRIFT_ALERT`
- `corr_rate < 50%` `CORRELATION_ALERT`
- `cycle_latency_s > 300` `LATENCY_ALERT`
---
## Enrichissement Anubis
La vue `view_ai_features_1h` enrichit chaque IP via les dictionnaires Anubis selon une cascade de priorité :
1. **IP/CIDR** correspondance exacte d'adresse ou de sous-réseau
2. **ASN** correspondance par numéro d'ASN
---
## Tables de sortie
### ml_detected_anomalies
Détections d'anomalies au-dessus du seuil de menace. Engine : `ReplacingMergeTree(detected_at)`, ORDER BY `(src_ip, model_name)`, TTL 7 jours.
Colonnes clés : `detected_at`, `src_ip`, `ja4`, `host`, `bot_name`, `anomaly_score`, `raw_anomaly_score`, `threat_level`, `model_name`, `recurrence`, `campaign_id`, `reason`, `anubis_bot_name`, `anubis_bot_action`, `anubis_bot_category`, plus toutes les features ML.
### ml_all_scores
Toutes les classifications (sans filtre de seuil) pour l'observabilité. Engine : `ReplacingMergeTree(detected_at)`, ORDER BY `(window_start, src_ip, ja4, host, model_name)`, TTL 7 jours.
---
## Format du journal de décisions
Le fichier `decisions.jsonl` contient des entrées JSONL structurées :
```json
{"event": "CYCLE_START", "cycle_id": "20260309T143000", "total": 5000, "human": 1500, "known_bot": 200, "correlated": 3000}
{"event": "ANOMALY", "src_ip": "203.0.113.42", "score": -0.25, "threat_level": "HIGH", "reason": "hit_velocity=45.2, fuzzing_index=0.8, ...", "campaign_id": 3}
{"event": "KNOWN_BOT", "src_ip": "198.51.100.10", "bot_name": "AhrefsBot"}
{"event": "CYCLE_END", "cycle_id": "20260309T143000", "anomalies": 15, "known_bots": 200, "duration_sec": 12.5}
```
Rotation des logs : 50 Mo max × `LOG_BACKUP_COUNT` sauvegardes (défaut : 7).
---
## Point de santé
- **URL** : `GET http://localhost:8080/`
- **Réponses** : `200 OK` (corps `OK`) ou `503 Service Unavailable` (corps `DEGRADED`)
- Exécuté dans un thread daemon au démarrage
- État mis à `False` après `MAX_CONSECUTIVE_FAILURES` (3) échecs consécutifs ClickHouse, remis à `True` dès le premier succès
---
## Persistance des modèles
| Fichier | Description |
|---------|-------------|
| `model_<name>_<version>.joblib` | EIF sérialisé (joblib) |
| `model_<name>_<version>.meta.json` | Métadonnées (features, seuils, statistiques d'entraînement, digest quantile) |
| `model_<name>.current` | Pointeur vers la version active |
| `training_history.jsonl` | Historique d'entraînement |
Rotation automatique : seules les `MODEL_HISTORY_COUNT` dernières versions (défaut : 10) sont conservées.
---
## Déploiement Docker
```bash
# Construction de l'image
make build-bot-detector
# Exécution avec docker-compose
cd services/bot-detector
docker-compose up -d
# Tests
make test-bot-detector
```
### Volumes
| Chemin hôte | Chemin conteneur | Description |
|-------------|-----------------|-------------|
| `./bot_detector_logs` | `/var/log/bot_detector` | Journaux de décisions (JSONL) |
| `./bot_detector_models` | `/var/lib/bot_detector` | Modèles ML persistés |
| `./reputation/data/user_files/bot_ip.csv` | `/data/bot_ip.csv` (ro) | Liste d'IPs de bots connus |
| `./reputation/data/user_files/bot_ja4.csv` | `/data/bot_ja4.csv` (ro) | Liste de JA4 de bots connus |
| `./reputation/data/user_files/asn_reputation.csv` | `/data/asn_reputation.csv` (ro) | Labels de réputation ASN |