# 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 + 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
la surveillance de performance par cycle.

---

## Architecture des modules

Le service est découpé en **12 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, TrafficAutoEncoder (PyTorch), XGBoost
       │    └─ scoring.py    Normalisation, MetaLearner, seuil adaptatif, ExIFFI, SHAP, HDBSCAN, dérive KS+KL
       ├─ 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`) |
| `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 |
| `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 |
| `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 |

### Autoencoder

| 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_LEARNING_RATE` | float | `1e-3` | Taux d'apprentissage Adam |

### 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 (0–1)
                  └──────────────────────┘        │
                                                  │ × (1 − AE_WEIGHT)
                  ┌──────────────────────┐        │
                  │  TrafficAutoEncoder   │        ├──→ combined_norm
                  │  (PyTorch)            │──→ ae_norm  (0–1)
                  └──────────────────────┘   × AE_WEIGHT
                                                  │ × (1 − XGB_WEIGHT)
                  ┌──────────────────────┐        │
                  │  XGBoost             │        ├──→ anomaly_score
                  │  (supervisé, labels  │──→ xgb_prob  (0–1)
                  │   SOC)               │   × 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`.

### TrafficAutoEncoder (PyTorch)

Architecture symétrique encodeur-décodeur :

```
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))
```

- 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

### XGBoost (supervisé)

Entraîné sur les labels issus du feedback SOC (table `soc_feedback`) :

```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)

---

## 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 10–25 (×0.20), nb extensions 10–25 (×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 Autoencoder est disponible, le clustering s'effectue dans l'**espace latent 16-dim** de l'AE. 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 (0–100) |
| `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 |
|---------|-------------|
| `path_transition_entropy` | Entropie des transitions de chemins (§5.1) |
| `cadence_cv` | Coefficient de variation de la cadence de requêtes (§5.3) |
| `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.0–1.0) combinant : JA4↔UA, H2↔JA4, TCP↔UA, langue↔ASN, Sec-CH-UA↔UA |

#### 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 + AE
   e. Scoring du trafic inconnu (EIF + AE)
   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
   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
9. Mode multi-fenêtre (si activé) : idem sur view_ai_features_24h
10. Insertion → ml_all_scores (toutes les sessions scorées)
11. Déduplication intra-cycle (garder raw_anomaly_score le plus bas par IP)
12. Déduplication inter-cycle (TTL, skip si détecté récemment sauf aggravation ≥ 0.05)
13. Insertion → ml_detected_anomalies (anomalies filtrées)
14. Insertion → fleet_detections (flottes détectées avec fleet_score)
15. Enregistrement → ml_performance_metrics (métriques de cycle + alertes)
```

---

## Détection de dérive (KS + KL divergence)

Par feature, deux tests comparent la distribution courante avec la distribution d'entraînement :

**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`

**Règle de décision** : une feature est en drift 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 une régression logistique apprise (`scoring.MetaLearner`) :

```
P(bot) = logistic(w1×eif + w2×ae + w3×xgb + w4×volume + w5×correlated + bias)
```

- **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 AE par feature** (`compute_ae_feature_errors`) :
- Reconstruction PyTorch feature par feature : `err_i = (x_i - x̂_i)²`
- Pour chaque anomalie, les features avec la plus grande erreur de reconstruction sont identifiées
- Expose quelles dimensions l'autoencoder 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. **Projection** : projection du graphe bipartite sur les nœuds JA4
3. **Communautés** : algorithme de Louvain (NetworkX) sur le graphe projeté
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) |

**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. **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**

---

## 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 |
