# Bot Detector IA — Documentation Technique

> Architecture modulaire (11 modules) | Dernière mise à jour : 2025-07-15

---

## Table des matières

1. [Vue d'ensemble](#1-vue-densemble)
2. [Installation et configuration](#2-installation-et-configuration)
3. [Modules](#3-modules)
4. [Features ML](#4-features-ml)
5. [Classification des menaces](#5-classification-des-menaces)
6. [Tests](#6-tests)
7. [Diagnostic](#7-diagnostic)

---

## 1. Vue d'ensemble

Le **Bot Detector IA** est un service de détection d'activité suspecte et de bots sur
un trafic HTTP/TLS. Il tourne en boucle continue (toutes les 5 minutes par défaut),
analyse des données agrégées issues de ClickHouse, et produit des scores d'anomalie
via un **ensemble triple-voix** (Extended Isolation Forest + Autoencoder + XGBoost).

### 1.1 Architecture modulaire

```
services/bot-detector/bot_detector/
├── __init__.py              # Paquetage Python
├── __main__.py         (41) # Point d'entrée, boucle principale
├── config.py          (154) # Variables d'environnement, constantes, imports optionnels
├── log.py              (65) # Journalisation structurée (structlog JSON)
├── infra.py            (89) # Client ClickHouse, health check HTTP, arrêt propre
├── browser.py         (170) # Détection multifactorielle 5 axes des navigateurs
├── scoring.py         (279) # Validation, seuil adaptatif, SHAP, HDBSCAN, dérive
├── models.py          (478) # EIF (isotree), AutoEncoder (PyTorch), XGBoost, persistance
├── preprocessing.py   (117) # Nettoyage, imputation, listes de features
├── pipeline.py        (378) # run_semi_supervised_logic() — orchestrateur ML
├── cycle.py           (371) # fetch_and_analyze(), cycle principal, multi-fenêtres
└── tests/
    ├── test_detector.py      # 36 tests auto-contenus
    └── conftest.py
```

### 1.2 Flux de données

```
ClickHouse                                          ClickHouse
 view_ai_features_1h ──┐                     ┌──► ml_all_scores
 view_thesis_features ──┤    ┌─────────────┐  │
 view_ip_recurrence ────┼──► │  cycle.py   │──┤
 audit_logs (feedback) ─┤    │  pipeline.py│  │
 view_ai_features_24h ──┘    │  models.py  │  └──► ml_detected_anomalies
    (si multiwindow)         └─────────────┘
```

**Étapes du flux :**

1. `cycle.py` récupère les features agrégées sur 1h (et 24h si `ENABLE_MULTIWINDOW=true`)
2. `preprocessing.py` nettoie les colonnes et enrichit via la détection navigateur
3. `pipeline.py` orchestre l'ensemble ML sur deux modèles parallèles :
   - **Complet** (~63 features, `correlated=1`) : couches L3 à L7 (TCP + TLS + HTTP)
   - **Applicatif** (~51 features, `correlated=0`) : couche L7 uniquement (HTTP)
4. `models.py` entraîne/charge les trois voix de l'ensemble (EIF, AE, XGBoost)
5. `scoring.py` normalise, explique (SHAP), clusterise (HDBSCAN) et détecte la dérive
6. Les résultats sont insérés dans `ml_detected_anomalies` et `ml_all_scores`

### 1.3 Caractéristiques clés

| Propriété | Valeur |
|-----------|--------|
| Ensemble ML | EIF + Autoencoder + XGBoost (triple-voix) |
| Supervision | Semi-supervisée (baseline humaine `asn_label='isp'`) |
| Fenêtre d'analyse | 1h glissante (+ 24h optionnel via `ENABLE_MULTIWINDOW`) |
| Cycle d'exécution | 300 s (configurable `CYCLE_INTERVAL_SEC`) |
| Contamination | 0.1 % (`ISOLATION_CONTAMINATION=0.001`) |
| Seuil d'anomalie | Adaptatif : `min(percentile_5(scores_négatifs), -0.05)` |
| Détection navigateur | 5 axes pondérés, seuil 0.55 |
| Explainabilité | SHAP top-5 features par anomalie |
| Clustering | HDBSCAN `min_cluster_size=3` + escalade campagne |

---

## 2. Installation et configuration

### 2.1 Dépendances

Le service est construit via Docker (`Dockerfile`). Dépendances principales :

- Python 3.11+
- `clickhouse-connect` — client ClickHouse
- `pandas`, `numpy` — manipulation de données
- `scikit-learn` — IsolationForest (fallback), StandardScaler, DBSCAN
- `structlog` — journalisation JSON
- `joblib` — sérialisation des modèles EIF

**Dépendances optionnelles** (imports conditionnels dans `config.py`) :

| Paquet | Variable de disponibilité | Rôle |
|--------|---------------------------|------|
| `isotree` | `EIF_AVAILABLE` | Extended Isolation Forest (ndim>1) |
| `torch` | `TORCH_AVAILABLE` | Autoencoder (PyTorch) |
| `xgboost` | `XGB_AVAILABLE` | Modèle supervisé XGBoost |
| `shap` | `SHAP_AVAILABLE` | Explainabilité SHAP |
| `hdbscan` | `HDBSCAN_AVAILABLE` | Clustering hiérarchique |

Si un paquet optionnel est absent, le service tourne en mode dégradé (fallback
sklearn pour EIF, pas d'AE, pas de XGBoost, DBSCAN au lieu de HDBSCAN, pas de SHAP).

### 2.2 Variables d'environnement

Toutes lues via `os.getenv()` dans `config.py` (pas de pydantic-settings).

#### Connexion ClickHouse

| Variable | Défaut | Description |
|----------|--------|-------------|
| `CLICKHOUSE_HOST` | `clickhouse` | Hôte ClickHouse |
| `CLICKHOUSE_PORT` | `8123` | Port HTTP |
| `CLICKHOUSE_USER` | `default` | Utilisateur |
| `CLICKHOUSE_PASSWORD` | *(vide)* | Mot de passe |
| `CLICKHOUSE_DB_PROCESSING` / `CLICKHOUSE_DB` | `ja4_processing` | Base de traitement (tables ML, vues, agrégations) |
| `CLICKHOUSE_DB_LOGS` | `ja4_logs` | Base des logs bruts |

#### Cycle et opération

| Variable | Défaut | Description |
|----------|--------|-------------|
| `CYCLE_INTERVAL_SEC` | `300` | Intervalle entre cycles (secondes) |
| `MAX_CONSECUTIVE_FAILURES` | `3` | Échecs ClickHouse consécutifs avant DEGRADED |
| `HEALTH_PORT` | `8080` | Port du health check HTTP |
| `BOT_DETECTOR_LOG` | `/var/log/bot_detector/decisions.jsonl` | Fichier de journal |
| `LOG_BACKUP_COUNT` | `7` | Nombre de rotations conservées |

#### Modèles ML

| Variable | Défaut | Description |
|----------|--------|-------------|
| `MODEL_DIR` | `/var/lib/bot_detector` | Répertoire de persistance des modèles |
| `MODEL_HISTORY_COUNT` | `10` | Versions conservées par modèle |
| `N_ESTIMATORS` | `300` | Nombre d'arbres EIF/IF |
| `ISOLATION_CONTAMINATION` | `0.001` | Fraction d'anomalies attendues (0 < x < 0.5) |
| `RETRAIN_INTERVAL_HOURS` | `24` | Fréquence de ré-entraînement EIF |
| `ANOMALY_THRESHOLD` | `-0.05` | Seuil statique de score pour insertion |
| `ANOMALY_PERCENTILE` | `5` | Percentile pour le seuil adaptatif (0–20) |
| `DRIFT_THRESHOLD` | `0.30` | Fraction de features en dérive déclenchant un retrain |
| `MIN_VALID_FEATURE_RATIO` | `0.50` | Ratio minimum de features valides (sinon cycle ignoré) |

#### Autoencoder

| Variable | Défaut | Description |
|----------|--------|-------------|
| `AE_WEIGHT` | `0.30` | Poids de l'AE dans la combinaison EIF+AE (`α`) |
| `AE_EPOCHS` | `50` | Nombre d'époques d'entraînement |
| `AE_LATENT_DIM` | `16` | Dimension de l'espace latent |
| `AE_LEARNING_RATE` | `1e-3` | Taux d'apprentissage Adam |

#### XGBoost

| Variable | Défaut | Description |
|----------|--------|-------------|
| `XGB_WEIGHT` | `0.20` | Poids de XGBoost dans le score final (`β`) |
| `XGB_MIN_LABELS` | `100` | Nombre minimum de labels SOC pour entraîner |
| `XGB_RETRAIN_INTERVAL_HOURS` | `168` | Intervalle de ré-entraînement (7 jours) |

#### Détection navigateur

| Variable | Défaut | Description |
|----------|--------|-------------|
| `BROWSER_CONFIDENCE_THRESHOLD` | `0.55` | Seuil de confiance pour LEGITIMATE_BROWSER |
| `BROWSER_COHORT_RATIO` | `0.70` | Ratio de cohorte pour la propagation par JA4 |

#### Fonctionnalités optionnelles

| Variable | Défaut | Description |
|----------|--------|-------------|
| `ENABLE_SHAP` | `true` | Active le calcul SHAP (ET `shap` installé) |
| `ENABLE_CLUSTERING` | `true` | Active le clustering HDBSCAN/DBSCAN des anomalies |
| `CLUSTERING_MIN_SAMPLES` | `3` | Taille minimale d'un cluster |
| `ENABLE_MULTIWINDOW` | `false` | Active l'analyse sur fenêtre 24h |
| `MULTIWINDOW_VIEW` | `view_ai_features_24h` | Nom de la vue 24h dans ClickHouse |
| `ENABLE_FEEDBACK` | `true` | Active l'intégration du feedback SOC |
| `FEEDBACK_WINDOW_DAYS` | `7` | Fenêtre de feedback SOC (jours) |
| `DEDUP_TTL_MIN` | `60` | TTL de déduplication inter-cycles (0 = désactivé) |
| `RECURRENCE_WEIGHT` | `0.005` | Pénalité de score par `log1p(récurrence)` |

### 2.3 Docker

```bash
# Construction de l'image de production
docker build -f services/bot-detector/bot_detector/Dockerfile -t bot-detector .

# Tests
docker build -f services/bot-detector/bot_detector/Dockerfile.tests -t bd-tests .
docker run --rm bd-tests

# Ou via le Makefile racine
make test-bot-detector
```

### 2.4 Systemd / Docker Compose

Le service tourne en boucle infinie. En Docker Compose, il est configuré avec
`restart: unless-stopped`. Le health check est exposé sur le port `HEALTH_PORT` (8080).

---

## 3. Modules

### 3.1 `config.py` — Configuration et imports optionnels

**Rôle** : Centralise toutes les variables d'environnement et gère les imports
conditionnels des dépendances optionnelles.

**Mécanisme d'imports optionnels** :

```python
try:
    from isotree import IsolationForest as ExtendedIsolationForest
    EIF_AVAILABLE = True
except ImportError:
    EIF_AVAILABLE = False
```

Ce patron est répété pour `torch`, `xgboost`, `shap`, et `hdbscan`. Les variables
`*_AVAILABLE` sont importées par les autres modules pour choisir les algorithmes.

**Validation des paramètres** : Les valeurs numériques à plage contrainte
(`CONTAMINATION`, `DRIFT_THRESHOLD`, `AE_WEIGHT`, `XGB_WEIGHT`,
`BROWSER_CONFIDENCE_THRESHOLD`, etc.) sont validées à l'import avec des bornes (0, 1)
ou (0, 0.5) selon le paramètre.

**Exclusions structurelles** (`STRUCTURAL_EXCLUDED_FEATURES`) : Dictionnaire
définissant les features exclues par modèle. Le modèle Applicatif exclut 15 features
TCP/TLS non disponibles sans corrélation (ex. `is_rare_ja4`, `tcp_shared_count`,
`ja3_diversity_ratio`, `tls12_ratio`, etc.).

---

### 3.2 `log.py` — Journalisation structurée

**Rôle** : Fournit la journalisation JSON via `structlog`.

**Fonctions exportées** :

| Fonction | Description |
|----------|-------------|
| `log_info(msg, **kw)` | Message informatif (console + structlog) |
| `log_decision(event, cycle_id, model, data)` | Événement décisionnel (fichier JSONL rotatif) |

Le fichier JSONL est configuré en rotation (50 Mo × `LOG_BACKUP_COUNT` fichiers).
Chaque événement contient : `timestamp`, `event`, `cycle_id`, `model_name`, et les
données spécifiques à l'événement.

**Événements journalisés** :

| Événement | Déclencheur |
|-----------|-------------|
| `SERVICE_START` | Démarrage du service |
| `SERVICE_STOP` | Arrêt propre (SIGTERM/SIGINT) |
| `CYCLE_START` | Début d'un cycle d'analyse |
| `CYCLE_END` | Fin du cycle (résumé des insertions) |
| `MODEL_LOADED` | Réutilisation d'un modèle existant |
| `MODEL_TRAINED` | Nouvel entraînement |
| `DRIFT_DETECTED` | Dérive conceptuelle → retrain forcé |
| `FEATURE_WARNING` | Features manquantes/constantes détectées |
| `KNOWN_BOT` | Bot connu identifié par réputation |
| `ANOMALY` | Anomalie ML détectée (score, SHAP, campaign_id) |
| `ANUBIS_DENY` | IP bloquée par Anubis |
| `LEGITIMATE_BROWSER` | Navigateur légitime confirmé (5 axes) |
| `SKIPPED_LOW_DATA` | Cycle ignoré (baseline < 500 sessions) |
| `SKIPPED_INVALID_FEATURES` | Cycle ignoré (ratio features valides insuffisant) |
| `CONSECUTIVE_FAILURES` | Erreur ClickHouse répétée |

---

### 3.3 `infra.py` — Infrastructure

**Rôle** : Client ClickHouse, health check HTTP, gestion de l'arrêt propre.

**Health check** : Serveur HTTP en thread daemon sur `HEALTH_PORT` (8080).

```
GET /  →  200 OK       (service opérationnel)
GET /  →  503 DEGRADED (≥ MAX_CONSECUTIVE_FAILURES échecs consécutifs)
```

L'état est contrôlé via `set_healthy(bool)` / `is_healthy()` (thread-safe avec
`threading.Lock`).

**Arrêt propre** : Gestionnaires de signaux SIGTERM et SIGINT journalisent
`SERVICE_STOP` et terminent proprement via `sys.exit(0)`.

**Client ClickHouse** : `get_client()` délègue à `ja4_common.clickhouse.get_client()`,
qui gère le singleton de connexion et la reconnexion automatique.

**Classification des menaces** : `score_to_threat_level(score)` convertit le score
brut IF en niveau de menace textuel :

| Condition | Niveau |
|-----------|--------|
| `score < -0.30` | `CRITICAL` |
| `score < -0.15` | `HIGH` |
| `score < -0.05` | `MEDIUM` |
| `score < 0` | `LOW` |
| `score ≥ 0` | `NORMAL` |

> **Note** : Les niveaux `KNOWN_BOT`, `ANUBIS_DENY`, `ANUBIS_ALLOW` et
> `LEGITIMATE_BROWSER` sont attribués en amont par `pipeline.py`, sans passer par
> cette fonction.

---

### 3.4 `browser.py` — Détection multifactorielle des navigateurs

**Rôle** : Évalue la probabilité qu'une session provienne d'un navigateur légitime
via 5 axes pondérés indépendants.

**Fonctions exportées** :

| Fonction | Description |
|----------|-------------|
| `_compute_browser_axes(df)` | Calcule les 5 axes + `browser_confidence` pour chaque ligne |
| `_parse_ja4_columns(ja4_series)` | Parse les champs structurels du fingerprint JA4 |
| `_infer_browser_family(df, ja4_parsed, axes)` | Infère la famille navigateur (Chromium, Firefox, Safari, Tor_Browser) |

#### Axe 1 — JA4 Known (poids 0.25)

Recherche dans le dictionnaire `dict_browser_ja4`. Score binaire : 1.0 si la famille
navigateur est identifiée, 0.0 sinon.

#### Axe 2 — JA4 Structure (poids 0.15)

Analyse structurelle du fingerprint JA4 :

| Composant | Poids | Condition de score 1.0 |
|-----------|-------|------------------------|
| TLS 1.3 | 0.35 | Version TLS ≥ 1.3 |
| Protocole h2/h3 | 0.25 | ALPN = h2 ou h3 |
| Cipher count | 0.20 | 10 ≤ nombre de ciphers ≤ 25 |
| Extension count | 0.20 | 10 ≤ nombre d'extensions ≤ 25 |

#### Axe 3 — HTTP Modern (poids 0.25)

| Composant | Poids | Condition de score 1.0 |
|-----------|-------|------------------------|
| `modern_browser_score` ≥ 50 | 0.35 | Score de conformité navigateur |
| `has_accept_language` | 0.20 | Présence du header Accept-Language |
| `sec_fetch_absence_rate` < 0.3 | 0.25 | Headers Sec-Fetch présents |
| `generic_accept_ratio` < 0.3 | 0.10 | Accept non générique (`*/*`) |
| `ua_ch_mismatch` = 0 | 0.10 | Cohérence UA / Client Hints |

#### Axe 4 — Navigation Behavior (poids 0.15)

| Composant | Poids | Condition de score 1.0 |
|-----------|-------|------------------------|
| `has_cookie` | 0.25 | Présence de cookies |
| `has_referer` | 0.25 | Présence du Referer |
| `asset_ratio` > 0.15 | 0.25 | Chargement de ressources statiques |
| `direct_access_ratio` < 0.5 | 0.25 | Navigation par liens (pas d'accès direct) |

#### Axe 5 — TLS/TCP Coherence (poids 0.20)

| Composant | Poids | Condition de score 1.0 |
|-----------|-------|------------------------|
| `alpn_http_mismatch` = 0 | 0.25 | Cohérence ALPN/HTTP |
| `no_window_scale_ratio` = 0 | 0.20 | Window scaling TCP présent |
| `tls12_ratio` < 0.1 | 0.20 | Peu de TLS 1.2 |
| `http10_ratio` = 0 | 0.15 | Pas de HTTP/1.0 |
| `is_alpn_missing` = 0 | 0.20 | ALPN présent dans le ClientHello |

#### Score final et classification

```
browser_confidence = Σ (axe_i × poids_i)    pour i = 1..5
```

**Condition LEGITIMATE_BROWSER** :
- `browser_confidence ≥ BROWSER_CONFIDENCE_THRESHOLD (0.55)` **ET**
- famille navigateur identifiée (Chromium, Firefox, Safari, ou Tor_Browser)

**Inférence de famille** (`_infer_browser_family`) : Compare les colonnes structurelles
du JA4 avec des profils prédéfinis par famille. Requiert `browser_confidence ≥ 0.45`
pour attribuer une famille.

**Propagation par cohorte** : Les sessions avec le même JA4 héritent de la
classification si `BROWSER_COHORT_RATIO` (70%) des sessions de ce JA4 sont
LEGITIMATE_BROWSER.

---

### 3.5 `preprocessing.py` — Prétraitement des données

**Rôle** : Nettoyage des DataFrames et définition des listes de features.

**Listes de features** :

| Liste | Nombre | Usage |
|-------|--------|-------|
| `FEATURES` | 57 | Modèle Applicatif (L7 pur, `correlated=0`) |
| `FEATURES_COMPLET` | 68 | Modèle Complet (`FEATURES` + 11 features TCP/TLS) |

> **Note** : Le nombre effectif de features utilisées par chaque modèle est inférieur
> aux listes, car `validate_features()` exclut les features manquantes, constantes
> ou à variance nulle, et `STRUCTURAL_EXCLUDED_FEATURES` (dans config.py) exclut
> des features structurellement non pertinentes par modèle.

**`preprocess_df(df)` — Pipeline de nettoyage** :

1. **Nettoyage des noms de colonnes** : supprime les préfixes de table (`c.split('.')[-1]`)
2. **Remplissage des colonnes texte** : `fillna('')` pour `src_ip`, `ja4`, `host`,
   `bot_name`, `anubis_bot_name`, `anubis_bot_action`, etc.
3. **Identification navigateur** : calcule les 5 axes browser via `_compute_browser_axes()`,
   infère la famille navigateur, ajoute `browser_confidence`, `is_known_browser`,
   `browser_consistency_score` et les 5 colonnes `axis_*`
4. **Signal Anubis** : calcule `anubis_is_flagged` (bot nommé par Anubis, action
   ni ALLOW ni DENY)
5. **Imputation intelligente** :
   - Features binaires (`has_cookie`, `is_ua_rotating`, etc.) → `fillna(-1)` (sentinelle
     pour donnée manquante)
   - Features numériques → remplacement des `±inf` par `NaN`, puis `fillna(median)`

---

### 3.6 `models.py` — Modèles ML

**Rôle** : Entraînement, chargement, prédiction et persistance des trois voix
de l'ensemble ML.

#### Extended Isolation Forest (EIF)

**Bibliothèque principale** : `isotree` (fallback : `sklearn.ensemble.IsolationForest`)

```python
# Paramètres isotree
ExtendedIsolationForest(
    ntrees=300,
    ndim=min(3, len(features)),   # partitions multi-dimensionnelles
    sample_size='auto',
    missing_action='impute',
    random_seed=42,
    nthreads=-1,
)

# Fallback sklearn
IsolationForest(
    n_estimators=300,
    contamination=CONTAMINATION,   # 0.001
    random_state=42,
    n_jobs=-1,
)
```

**Calibration des scores isotree** : Les scores isotree sont convertis pour être
comparables à sklearn via `sklearn_equiv = 0.5 - isotree_score`.

**Persistance** : `joblib.dump()` / `joblib.load()` → fichier `.joblib`.

#### Autoencoder (TrafficAutoEncoder)

**Bibliothèque** : PyTorch (`torch.nn.Module`). Disponible uniquement si `TORCH_AVAILABLE=True`.

**Architecture adaptative** :

```
Encoder : input → dim1 → dim2 → latent_dim
Decoder : latent_dim → dim2 → dim1 → input

dim1 = min(64, max(n_features, latent_dim + 4))
dim2 = min(32, max(dim1 // 2, latent_dim + 2))
latent_dim = AE_LATENT_DIM (16 par défaut)
```

Chaque couche utilise `BatchNorm1d + ReLU`. La dernière couche du décodeur utilise
`Sigmoid`. Perte : `MSELoss`. Optimiseur : `Adam(lr=AE_LEARNING_RATE, weight_decay=1e-5)`.
Entraînement sur `AE_EPOCHS` (50) époques, batch_size=256.

**Score** : L'erreur de reconstruction (MSE par échantillon) sert de score d'anomalie.

**Persistance** : `torch.save(state_dict())` → fichier `.pt`.

#### XGBoost (supervisé)

**Bibliothèque** : `xgboost.XGBClassifier`. Disponible uniquement si `XGB_AVAILABLE=True`.

```python
XGBClassifier(
    n_estimators=200,
    max_depth=6,
    learning_rate=0.1,
    scale_pos_weight=<dynamique>,   # ratio négatifs/positifs
    eval_metric='logloss',
    random_state=42,
    n_jobs=-1,
    tree_method='hist',
)
```

**Données d'entraînement** : Labels issus du feedback SOC (`audit_logs`). Les faux
positifs (FP) servent d'exemples négatifs, les vrais positifs (TP) d'exemples positifs.
Requiert ≥ `XGB_MIN_LABELS` (100) labels avant activation.

**Ré-entraînement** : Toutes les `XGB_RETRAIN_INTERVAL_HOURS` (168h = 7 jours).

**Persistance** : `model.save_model()` → fichier `.json`.

#### Cycle de vie des modèles

```
Démarrage cycle
      │
      ▼
Existe un .current ?  ──NON──► Entraîner nouveau modèle
      │
     OUI
      │
      ▼
Âge < RETRAIN_INTERVAL ?
      │                  │
     OUI                NON
      │                  │
      ▼                  └──► Entraîner nouveau modèle
Drift check (scoring.py)
      │
Drift ≥ DRIFT_THRESHOLD ?
      │              │
     NON            OUI
      │              │
Charger modèle  Entraîner nouveau modèle
```

**Versioning** : `model_{name}_{YYYYMMDD_HHMMSS}.{joblib|pt|json}` +
`.meta.json` (features, contamination, nb_samples, `baseline_stats`).
Pointeur atomique : `model_{name}.current`.
Historique limité à `MODEL_HISTORY_COUNT` versions.

**Validation gate** : Un modèle est rejeté si `val_anomaly_rate > 0.20` sur
le jeu de validation (split 80/20).

**Feature pruning** : Les features avec variance < 1e-6 sont éliminées avant
entraînement.

---

### 3.7 `scoring.py` — Scoring et post-traitement

**Rôle** : Validation des features, seuil adaptatif, normalisation, explainabilité
SHAP, clustering HDBSCAN, et détection de dérive conceptuelle.

#### `validate_features(df, features, name, cycle_id)`

Filtre les features avant entraînement et scoring :
- Exclut les features **absentes** du DataFrame
- Exclut les features **constantes** (std = 0, non discriminantes)
- Exclut les features **entièrement à zéro** (pipeline non alimenté)
- Retourne `None` si le ratio de features valides < `MIN_VALID_FEATURE_RATIO` (0.50),
  ce qui fait ignorer le cycle

#### `compute_adaptive_threshold(scores)`

Calcule un seuil d'anomalie dynamique basé sur la distribution courante :

```python
neg_scores = scores[scores < 0]
adaptive = np.percentile(neg_scores, ANOMALY_PERCENTILE)   # défaut : 5e percentile
effective_threshold = min(adaptive, ANOMALY_THRESHOLD)      # garde-fou : -0.05
```

Le seuil ne peut pas remonter au-dessus du seuil statique mais s'adapte vers le bas.

#### `normalize_scores(scores)`

Normalisation min-max des scores négatifs sur l'intervalle [0, 1], où 1 = le plus
anormal. Les scores positifs (normaux) ne sont pas normalisés.

#### `_compute_shap_top_features(model, X, features, n_top=5)`

Calcul des contributions SHAP par anomalie :
- **isotree** : `shap.PermutationExplainer`
- **sklearn** : `shap.TreeExplainer`
- Retourne les top-5 features les plus contributives par ligne

Activé uniquement si `ENABLE_SHAP=true` **ET** `SHAP_AVAILABLE=True`.

#### `_cluster_anomalies(anomalies, features, ae_model=None)`

Clustering des anomalies pour identifier les campagnes coordonnées :
- **HDBSCAN** (si disponible) : `min_cluster_size=CLUSTERING_MIN_SAMPLES (3)`,
  `min_samples=max(2, CLUSTERING_MIN_SAMPLES - 1)`, `cluster_selection_method='eom'`
- **DBSCAN** (fallback) : `eps=0.5`, `min_samples=CLUSTERING_MIN_SAMPLES`
- Si un modèle AE est disponible, le clustering opère dans **l'espace latent** de
  l'autoencoder (dimension 16) plutôt que sur les features brutes

Résultat : colonne `campaign_id` (-1 = isolé, ≥0 = membre d'un cluster).

#### `_compute_drift_score(baseline_stats, current_baseline, features)`

Détection de dérive conceptuelle entre la baseline d'entraînement et la baseline
courante. Deux méthodes :

1. **Méthode principale** : Comparaison par quantiles interpolés (KS-like). Fraction
   de features avec p < 0.05.
2. **Fallback z-score** (`_compute_drift_score_zscore`) :
   `z = |mean_current - mean_trained| / std_trained`. Fraction de features avec z > 2.0.

Si la fraction dépasse `DRIFT_THRESHOLD` (0.30), le modèle est ré-entraîné et
l'événement `DRIFT_DETECTED` est journalisé.

---

### 3.8 `pipeline.py` — Orchestrateur semi-supervisé

**Rôle** : Orchestre le flux ML complet via `run_semi_supervised_logic()`.

**Signature** :

```python
def run_semi_supervised_logic(df, features, name, cycle_id, recurrence_map) -> (threats, all_scored)
```

**Étapes détaillées** (13 phases) :

1. **Triage initial** : Sépare le DataFrame en trois groupes :
   - `known_bots` : IPs avec un `bot_name` renseigné (dictionnaires de réputation)
   - `anubis_allow` : IPs avec `anubis_bot_action='ALLOW'`
   - `unknown_traffic` : tout le reste
   - Baseline humaine : `unknown_traffic` avec `asn_label='isp'`

2. **Validation des features** : `validate_features()` exclut les features invalides.
   Retour anticipé si ratio < `MIN_VALID_FEATURE_RATIO`.

3. **Vérification de la baseline** : Minimum 500 sessions humaines requis.
   Sinon `SKIPPED_LOW_DATA`.

4. **EIF : entraînement ou chargement** : `load_or_train_model()` avec détection de
   dérive intégrée. Entraîné **uniquement sur la baseline humaine**.

5. **EIF : scoring** : `decision_function()` sur tout le trafic inconnu.
   Pour isotree : `score_equiv = 0.5 - isotree_score`.

6. **Autoencoder : entraînement et scoring** : Erreur de reconstruction MSE normalisée.

7. **Combinaison EIF + AE** :
   ```
   combined = (1 - α) × eif_norm + α × ae_norm
   ```
   où `α = AE_WEIGHT = 0.30`. Soit : **70% EIF + 30% AE**.

8. **XGBoost : prédiction supervisée** : `predict_proba()` → probabilité de malveillance.

9. **Fusion EIF+AE+XGB** :
   ```
   final = (1 - β) × combined + β × xgb_prob
   ```
   où `β = XGB_WEIGHT = 0.20`. Le score final se décompose en :
   **56% EIF + 24% AE + 20% XGBoost**.

10. **Seuil adaptatif** : `compute_adaptive_threshold()` sur les scores bruts EIF.

11. **Pénalité de récurrence** :
    ```
    raw_anomaly_score -= log1p(recurrence_count) × RECURRENCE_WEIGHT
    ```

12. **Classification des menaces** :
    - `score_to_threat_level()` pour le score brut
    - Override `ANUBIS_DENY` pour les IPs Anubis deny
    - Classification `LEGITIMATE_BROWSER` si `browser_confidence ≥ 0.55` + famille
      identifiée + threat_level ∈ {NORMAL, LOW} + action ≠ DENY

13. **Post-traitement des anomalies** (score brut < seuil adaptatif) :
    - SHAP : explication top-5 features → champ `reason`
    - HDBSCAN : clustering → `campaign_id`
    - **Escalade campagne** : si un cluster contient ≥ 5 membres, les IPs `HIGH`
      sont escaladées en `CRITICAL`

**Labeling bots connus et Anubis** : Les `known_bots` reçoivent `threat_level='KNOWN_BOT'`,
`anomaly_score=0.0`. Les `anubis_allow` reçoivent `threat_level='KNOWN_BOT'`.
Les IPs Anubis deny sont forcées à `ANUBIS_DENY`.

**Retour** : `(threats, all_scored)` — respectivement les anomalies/bots à insérer
dans `ml_detected_anomalies` et tous les scores pour `ml_all_scores`.

---

### 3.9 `cycle.py` — Cycle principal de détection

**Rôle** : Orchestre l'acquisition de données, l'appel au pipeline ML, et l'insertion
des résultats dans ClickHouse.

**`fetch_and_analyze()` — Flux complet** :

1. Connexion ClickHouse via `get_client()`
2. Requête `{DB}.view_ai_features_1h` → DataFrame principal
3. Enrichissement avec les features thèse §5 : `{DB}.view_thesis_features_1h`
   (jointure sur `src_ip`)
4. `preprocess_df(df)` — nettoyage, imputation, détection navigateur
5. Chargement de la carte de récurrence : `{DB}.view_ip_recurrence`
6. Chargement du feedback SOC : `{DB}.audit_logs` (si `ENABLE_FEEDBACK=true`)
7. Application du feedback : FP → `asn_label='isp'`, TP → `asn_label='soc_confirmed_bot'`
8. Exécution du modèle **Complet** (`correlated=1`, `FEATURES_COMPLET`)
9. Exécution du modèle **Applicatif** (`correlated=0`, `FEATURES`)
10. **Multi-fenêtres** (si `ENABLE_MULTIWINDOW=true`) :
    - Requête `{DB}.{MULTIWINDOW_VIEW}` (défaut `view_ai_features_24h`)
    - Exécution des modèles `Complet_24h` et `Applicatif_24h`
    - Fusion OR : une IP est flaggée si anomalie dans ≥ 1 fenêtre (score le plus bas conservé)
11. **Insertion `ml_all_scores`** : Toutes les sessions scorées
12. **Déduplication intra-cycle** : `drop_duplicates(subset=['src_ip'], keep='first')`
    en gardant le score le plus bas
13. **Déduplication inter-cycles** (`_filter_recent_detections`) : Interroge
    `ml_detected_anomalies` pour les IPs insérées dans les dernières `DEDUP_TTL_MIN`
    minutes. Réinsertion uniquement si le score s'est dégradé de ≥ 0.05
14. **Insertion `ml_detected_anomalies`** : Anomalies et bots connus filtrés

**Gestion des erreurs** : Compteur `_consecutive_failures`. Après `MAX_CONSECUTIVE_FAILURES`
(3) échecs, `set_healthy(False)` → health check renvoie 503.

**Tables d'insertion** :

| Table | Contenu |
|-------|---------|
| `{DB}.ml_all_scores` | Toutes les sessions scorées (anomalies + normales + bots) |
| `{DB}.ml_detected_anomalies` | Anomalies confirmées + bots connus uniquement |

---

### 3.10 `__main__.py` — Point d'entrée

**Rôle** : Point d'entrée du service. Pas d'argparse — le comportement est entièrement
contrôlé par les variables d'environnement.

**Flux** :

1. Import de toute la configuration via `from .config import *`
2. Affichage d'un bandeau de démarrage avec toutes les valeurs de configuration
3. Journalisation de l'événement `SERVICE_START`
4. Boucle infinie :
   - `fetch_and_analyze()`
   - `time.sleep(CYCLE_INTERVAL_SEC)`
   - Les exceptions sont capturées, journalisées, et la boucle continue

**Lancement** :

```bash
python -m bot_detector
```

---

## 4. Features ML

### 4.1 Vue d'ensemble

Le service utilise **7 familles de features** totalisant ~68 features uniques. Le
modèle Complet utilise les 68, le modèle Applicatif en utilise ~57 (sans les features
TCP/TLS).

### 4.2 Famille 1 — Volume et débit

| Feature | Description | Modèle |
|---------|-------------|--------|
| `hits` | Nombre total de requêtes sur la fenêtre | Les deux |
| `hit_velocity` | Requêtes par seconde | Les deux |
| `fuzzing_index` | Score de diversité anormale des chemins/paramètres | Les deux |
| `post_ratio` | Fraction de requêtes POST | Les deux |
| `port_exhaustion_ratio` | Fraction de ports sources distincts / total | Les deux |
| `orphan_ratio` | Requêtes sans réponse associée | Les deux |
| `max_keepalives` | Max requêtes sur une connexion keep-alive | Les deux |
| `tcp_shared_count` | Connexions TCP partagées entre sessions HTTP | Les deux |
| `head_ratio` | Fraction de requêtes HEAD (B4) | Les deux |
| `burst_ratio` | Ratio de requêtes en rafale (thèse §5) | Les deux |
| `pause_ratio` | Ratio de pauses entre rafales (thèse §5) | Les deux |

### 4.3 Famille 2 — Chemins et contenu

| Feature | Description | Modèle |
|---------|-------------|--------|
| `path_diversity_ratio` | Diversité des chemins URL accédés | Les deux |
| `url_depth_variance` | Variance de la profondeur des URL | Les deux |
| `anomalous_payload_ratio` | Fraction de payloads avec patterns anormaux | Les deux |
| `path_transition_entropy` | Entropie des transitions entre chemins (thèse §5) | Les deux |
| `login_post_concentration` | Concentration de POST sur les pages de login (P1) | Les deux |
| `unusual_content_type_ratio` | Ratio de Content-Types inhabituels (P1) | Les deux |
| `non_standard_port_ratio` | Ratio de ports non standard (P1) | Les deux |

### 4.4 Famille 3 — Headers et protocole

| Feature | Description | Modèle |
|---------|-------------|--------|
| `header_count` | Nombre d'en-têtes HTTP envoyés | Les deux |
| `header_order_shared_count` | Partage d'un même ordre d'en-têtes entre IPs | Les deux |
| `header_order_confidence` | Confiance dans l'ordre d'en-têtes | Les deux |
| `distinct_header_orders` | Nombre d'ordres d'en-têtes distincts | Les deux |
| `has_accept_language` | Présence de Accept-Language | Les deux |
| `modern_browser_score` | Score composite de conformité navigateur | Les deux |
| `ua_ch_mismatch` | Incohérence User-Agent / Client Hints | Les deux |
| `sec_fetch_absence_rate` | Absence des headers Sec-Fetch (B5) | Les deux |
| `generic_accept_ratio` | Ratio de headers Accept génériques (B6) | Les deux |
| `http10_ratio` | Ratio de requêtes HTTP/1.0 (B7) | Les deux |
| `missing_accept_enc_ratio` | Ratio de requêtes sans Accept-Encoding | Les deux |
| `http_scheme_ratio` | Ratio de schémas HTTP (vs HTTPS) | Les deux |
| `sec_ch_mobile_mismatch` | Incohérence Sec-CH-UA-Mobile (P1) | Les deux |
| `has_xff` | Présence de X-Forwarded-For (P1) | Les deux |

### 4.5 Famille 4 — Session et navigation

| Feature | Description | Modèle |
|---------|-------------|--------|
| `has_cookie` | Présence de cookies | Les deux |
| `has_referer` | Présence du Referer | Les deux |
| `asset_ratio` | Fraction de requêtes vers des ressources statiques | Les deux |
| `direct_access_ratio` | Fraction d'accès directs (sans Referer) | Les deux |
| `is_ua_rotating` | Rotation de User-Agent détectée | Les deux |
| `multiplexing_efficiency` | Efficacité du multiplexage HTTP/2 | Les deux |
| `request_size_variance` | Variance de la taille des requêtes | Les deux |
| `is_fake_navigation` | Fausse navigation détectée (P0) | Les deux |
| `host_diversity` | Diversité des hosts accédés (thèse §5) | Les deux |
| `host_sweep_speed` | Vitesse de balayage des hosts (thèse §5) | Les deux |
| `host_coverage_uniformity` | Uniformité de couverture des hosts (thèse §5) | Les deux |

### 4.6 Famille 5 — TLS et réseau

| Feature | Description | Modèle |
|---------|-------------|--------|
| `distinct_ja4_count` | Fingerprints JA4 distincts par IP | Les deux |
| `ja4_asn_concentration` | Concentration d'un même JA4 dans un ASN | Les deux |
| `ja4_country_concentration` | Concentration d'un même JA4 par pays | Les deux |
| `is_rare_ja4` | JA4 peu commun dans la population | Les deux |
| `ip_id_zero_ratio` | Ratio de paquets IP avec ID=0 | Les deux |
| `mss_mobile_mismatch` | Incohérence MSS TCP / profil mobile | Les deux |
| `tcp_jitter_variance` | Variance de la gigue inter-paquets TCP | Complet |
| `alpn_http_mismatch` | Incohérence ALPN négocié / protocole HTTP | Complet |
| `is_alpn_missing` | ALPN absent dans le ClientHello | Complet |
| `sni_host_mismatch` | Incohérence SNI TLS / Host HTTP | Complet |
| `ja3_diversity_ratio` | Ratio JA3/JA4 — rotation de fingerprint (B1) | Complet |
| `syn_timing_cv` | Coefficient de variation du timing SYN→ClientHello (B2) | Complet |
| `tls12_ratio` | Ratio de requêtes exclusivement TLS 1.2 (B3) | Complet |
| `ip_df_variance` | Variance du bit DF (Don't Fragment) (B8) | Complet |
| `avg_ttl` | TTL moyen (fingerprinting OS) | Complet |
| `ttl_std` | Écart-type du TTL | Complet |
| `no_window_scale_ratio` | Ratio de sessions sans window scaling TCP | Complet |
| `ja4_drift_ratio` | Dérive JA4 intra-session (thèse §5.5) | Complet |
| `true_window_size` | Taille réelle de la fenêtre TCP (P0) | Complet |
| `window_mss_ratio` | Ratio fenêtre TCP / MSS (P0) | Complet |

### 4.7 Famille 6 — Intelligence et réputation

| Feature | Description | Modèle |
|---------|-------------|--------|
| `anubis_is_flagged` | Signalé par Anubis (action ≠ ALLOW/DENY) | Les deux |
| `is_known_browser` | Browser identifié (axe 1 rétro-compat) | Les deux |
| `browser_consistency_score` | Score de cohérence navigateur (rétro-compat) | Les deux |
| `browser_confidence` | Confiance navigateur (5 axes) | Les deux |
| `src_port_density` | Densité des ports sources (entropie) | Les deux |

### 4.8 Famille 7 — Thèse §5 et temporel

| Feature | Description | Modèle |
|---------|-------------|--------|
| `temporal_entropy` | Entropie de Shannon de la distribution temporelle | Les deux |
| `cadence_cv` | Coefficient de variation de la cadence (thèse §5) | Les deux |
| `lag1_autocorrelation` | Autocorrélation lag-1 des inter-arrivées (thèse §5) | Les deux |
| `benford_deviation` | Déviation par rapport à la loi de Benford (thèse §5) | Les deux |

### 4.9 Axes de détection navigateur (utilisés comme features)

| Feature | Description | Modèle |
|---------|-------------|--------|
| `axis_ja4_known` | Score de l'axe 1 (JA4 connu) | Les deux |
| `axis_ja4_struct` | Score de l'axe 2 (structure JA4) | Les deux |
| `axis_http_modern` | Score de l'axe 3 (HTTP moderne) | Les deux |
| `axis_nav_behavior` | Score de l'axe 4 (comportement navigation) | Les deux |
| `axis_tls_coherence` | Score de l'axe 5 (cohérence TLS/TCP) | Les deux |

---

## 5. Classification des menaces

### 5.1 Arbre de décision complet

La classification des menaces suit un ordre de priorité strict, appliqué dans
`pipeline.py` :

```
1. anubis_bot_action = 'DENY'
   └──► ANUBIS_DENY (override systématique)

2. bot_name != '' (dictionnaires de réputation IP/JA4)
   └──► KNOWN_BOT

3. anubis_bot_action = 'ALLOW'
   └──► KNOWN_BOT (bot identifié mais autorisé)

4. browser_confidence ≥ 0.55 AND famille identifiée AND threat ∈ {NORMAL, LOW}
   └──► LEGITIMATE_BROWSER

5. Score brut IsolationForest < -0.30
   └──► CRITICAL

6. Score brut < -0.15
   └──► HIGH

7. Score brut < -0.05
   └──► MEDIUM

8. Score brut < 0
   └──► LOW

9. Sinon
   └──► NORMAL
```

### 5.2 Escalade par campagne

Après le clustering HDBSCAN, les IPs appartenant à un cluster de **≥ 5 membres**
voient leur threat level escaladé de `HIGH` à `CRITICAL`. Cette logique détecte les
campagnes de botnet coordonnées qui individuellement ne seraient que `HIGH`.

### 5.3 Niveaux de menace

| Niveau | Score brut | Interprétation |
|--------|-----------|----------------|
| `ANUBIS_DENY` | — | Bloqué par la WAF Anubis |
| `KNOWN_BOT` | 0.0 | Bot identifié par réputation |
| `LEGITIMATE_BROWSER` | — | Navigateur humain confirmé (5 axes) |
| `CRITICAL` | < -0.30 | Comportement extrêmement anormal |
| `HIGH` | < -0.15 | Fort signal d'anomalie |
| `MEDIUM` | < -0.05 | Anomalie modérée |
| `LOW` | < 0 | Légèrement inhabituel |
| `NORMAL` | ≥ 0 | Trafic normal |

---

## 6. Tests

### 6.1 Organisation

Les tests sont dans `bot_detector/tests/test_detector.py` (36 tests). Ils suivent un
patron **auto-contenu** : chaque test ré-implémente la logique clé plutôt que
d'importer directement depuis le module principal. Cela évite les chaînes d'imports
lourdes (`joblib`, `sklearn`, `torch`, `xgboost`).

### 6.2 Patron des tests

- **Autoencoder** : Helper local `_make_ae()` crée un modèle minimal en mémoire
- **XGBoost** : Modèles créés in-memory pour chaque test
- **EIF/IF** : Tests de scoring sur des données synthétiques
- **Dépendances optionnelles** : `pytest.skip()` si `torch` ou `xgboost` non installés

### 6.3 Exécution

```bash
# Via Docker (recommandé)
make test-bot-detector

# Ou directement
docker build -f services/bot-detector/bot_detector/Dockerfile.tests -t bd-tests .
docker run --rm bd-tests

# En local (nécessite les dépendances)
cd services/bot-detector
pip install -r bot_detector/requirements.txt pytest pytest-mock
pytest bot_detector/tests/test_detector.py -v

# Un test spécifique
pytest bot_detector/tests/test_detector.py -v -k "test_benford"
```

### 6.4 Couverture des tests

Les 36 tests couvrent :
- Calcul des scores de menace (`score_to_threat_level`)
- Validation des features (manquantes, constantes, zéro)
- Seuil adaptatif par percentile
- Normalisation des scores
- Entraînement et scoring de l'autoencoder
- Prédiction XGBoost
- Détection de dérive conceptuelle
- Clustering HDBSCAN/DBSCAN
- Détection navigateur (5 axes)
- Déviation de Benford, autocorrélation lag-1
- Prétraitement des données

---

## 7. Diagnostic

### 7.1 Requêtes ClickHouse utiles

```sql
-- Dernières anomalies CRITICAL
SELECT detected_at, src_ip, ja4, threat_level, anomaly_score, reason
FROM ja4_processing.ml_detected_anomalies
WHERE threat_level = 'CRITICAL'
ORDER BY detected_at DESC
LIMIT 20;

-- Distribution des threat levels sur la dernière heure
SELECT threat_level, count() AS cnt
FROM ja4_processing.ml_detected_anomalies
WHERE detected_at > now() - INTERVAL 1 HOUR
GROUP BY threat_level
ORDER BY cnt DESC;

-- Campagnes coordonnées (HDBSCAN clusters)
SELECT campaign_id, count() AS members, groupArray(src_ip) AS ips
FROM ja4_processing.ml_detected_anomalies
WHERE campaign_id >= 0 AND detected_at > now() - INTERVAL 1 HOUR
GROUP BY campaign_id
ORDER BY members DESC;

-- Scores moyens par modèle (dernière heure)
SELECT model_name, avg(anomaly_score) AS avg_score, count() AS total
FROM ja4_processing.ml_all_scores
WHERE window_start > now() - INTERVAL 1 HOUR
GROUP BY model_name;

-- Navigateurs légitimes détectés
SELECT src_ip, ja4, inferred_browser_family, browser_confidence
FROM ja4_processing.ml_all_scores
WHERE threat_level = 'LEGITIMATE_BROWSER' AND window_start > now() - INTERVAL 1 HOUR
ORDER BY browser_confidence DESC
LIMIT 20;

-- Volume de la baseline humaine (santé du pipeline)
SELECT count() AS human_sessions
FROM ja4_processing.view_ai_features_1h
WHERE asn_label = 'isp' AND bot_name = '';
```

### 7.2 Commandes jq pour les logs JSONL

```bash
# Anomalies CRITICAL récentes
jq 'select(.event=="ANOMALY" and .threat_level=="CRITICAL")' decisions.jsonl

# Top features SHAP des anomalies HIGH
jq 'select(.event=="ANOMALY" and .threat_level=="HIGH") | .reason' decisions.jsonl

# Dérives de distribution détectées
jq 'select(.event=="DRIFT_DETECTED")' decisions.jsonl

# Campagnes coordonnées (campaign_id ≥ 0)
jq 'select(.event=="ANOMALY" and .campaign_id >= 0) | {src_ip, campaign_id, threat_level}' decisions.jsonl

# Comptage des bots connus par nom
jq -r 'select(.event=="KNOWN_BOT") | .bot_name' decisions.jsonl | sort | uniq -c | sort -rn

# Résumé des cycles (performances)
jq 'select(.event=="CYCLE_END")' decisions.jsonl

# Navigateurs légitimes confirmés
jq 'select(.event=="LEGITIMATE_BROWSER")' decisions.jsonl
```

### 7.3 Problèmes courants

| Symptôme | Cause probable | Solution |
|----------|---------------|----------|
| `SKIPPED_LOW_DATA` à chaque cycle | Baseline humaine < 500 sessions | Vérifier que `view_ai_features_1h` retourne des lignes avec `asn_label='isp'` |
| `SKIPPED_INVALID_FEATURES` | > 50% des features constantes/absentes | Vérifier le schéma de `view_ai_features_1h` — colonnes manquantes ou agrégations NULL |
| `CONSECUTIVE_FAILURES` | ClickHouse inaccessible | Vérifier `CLICKHOUSE_HOST`/`PORT`, réseau Docker, état du service ClickHouse |
| Health check 503 | ≥ 3 échecs consécutifs | Consulter les logs pour l'erreur sous-jacente |
| Pas d'anomalies détectées | Seuil trop strict ou features constantes | Vérifier `ANOMALY_THRESHOLD`, inspecter la distribution des scores dans `ml_all_scores` |
| SHAP non disponible | Paquet `shap` non installé | Installer `shap` ou vérifier `ENABLE_SHAP=true` |
| Autoencoder désactivé | `torch` non installé | Installer `torch` — le service tourne sans AE mais avec moins de précision |
| Trop de `LEGITIMATE_BROWSER` | Seuil navigateur trop bas | Augmenter `BROWSER_CONFIDENCE_THRESHOLD` (défaut 0.55) |
| `FEATURE_WARNING` fréquents | Colonnes non alimentées dans la vue | Voir `CLICKHOUSE_FEATURES_DIAGNOSTIC.md` |

### 7.4 Vérification de l'état du service

```bash
# Health check
curl -s http://localhost:8080/
# → OK ou DEGRADED

# Vérifier les logs Docker
docker logs bot-detector --tail 50

# Vérifier la configuration active (première ligne du démarrage)
docker logs bot-detector 2>&1 | head -30
```
