# 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), enrichi par l'explicabilité SHAP, le clustering HDBSCAN et la détection multifactorielle des navigateurs. --- ## Architecture des modules Le service est découpé en **11 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 │ └─ browser.py Identification multifactorielle des navigateurs (5 axes) ├─ pipeline.py Orchestration : filtrage → entraînement → scoring → fusion │ ├─ models.py EIF, TrafficAutoEncoder (PyTorch), XGBoost │ └─ scoring.py Normalisation, seuil adaptatif, SHAP, HDBSCAN, dérive └─ (insère dans ml_all_scores + ml_detected_anomalies) ``` | 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` | 170 | Détection multifactorielle des navigateurs sur 5 axes pondérés | | `scoring.py` | 279 | Normalisation, seuil adaptatif, SHAP top-3, HDBSCAN, détection de dérive | | `models.py` | 478 | `TrafficAutoEncoder`, entraînement/chargement EIF, XGBoost, élagage de features | | `preprocessing.py` | 117 | `preprocess_df()` — nettoyage, typage, imputation, listes `FEATURES` / `FEATURES_COMPLET` | | `pipeline.py` | 378 | `run_semi_supervised_logic()` — orchestration complète d'un modèle | | `cycle.py` | 371 | `fetch_and_analyze()` — boucle principale, feedback SOC, multiwindow | | `__main__.py` | 41 | Point d'entrée, bannière de démarrage, boucle `while True` | | `__init__.py` | 1 | Docstring du package | --- ## 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` | 77 (`FEATURES_COMPLET`) | HTTP + TCP + TLS (L3→L7) | | **Applicatif** | `correlated = 0` | 65 (`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 (65 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 | 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 | | `axis_ja4_known` | Score de l'axe 1 (JA4 connu) | | `axis_ja4_struct` | Score de l'axe 2 (structure JA4) | | `axis_http_modern` | Score de l'axe 3 (HTTP moderne) | | `axis_nav_behavior` | Score de l'axe 4 (comportement navigation) | | `axis_tls_coherence` | Score de l'axe 5 (cohérence TLS/TCP) | #### Thèse §5 — Features avancées | Feature | Description | |---------|-------------| | `path_transition_entropy` | Entropie des transitions de chemins | | `cadence_cv` | Coefficient de variation de la cadence de requêtes | | `burst_ratio` | Fraction de requêtes en rafale | | `pause_ratio` | Fraction de pauses longues | | `lag1_autocorrelation` | Autocorrélation lag-1 des inter-arrivées | | `benford_deviation` | Déviation par rapport à la loi de Benford | | `host_diversity` | Diversité des hôtes ciblés | | `host_sweep_speed` | Vitesse de balayage des hôtes | | `host_coverage_uniformity` | Uniformité de couverture des hôtes | #### 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, 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') d. Chargement ou entraînement EIF + AE e. Scoring du trafic inconnu (EIF + AE) f. Chargement ou entraînement XGBoost (si labels disponibles) g. Combinaison des scores (formule triple voix) h. Normalisation [0, 1] i. Seuil adaptatif j. Pénalité de récurrence k. SHAP (top-3 features) l. HDBSCAN clustering → campaign_id m. Détection de dérive (KS test) 8. Mode multi-fenêtre (si activé) : idem sur view_ai_features_24h 9. Insertion → ml_all_scores (toutes les sessions scorées) 10. Déduplication intra-cycle (garder raw_anomaly_score le plus bas par IP) 11. Déduplication inter-cycle (TTL, skip si détecté récemment sauf aggravation ≥ 0.05) 12. Insertion → ml_detected_anomalies (anomalies filtrées) ``` --- ## Détection de dérive (Kolmogorov-Smirnov) Par feature, un **test KS bilatéral** compare la distribution courante avec la distribution d'entraînement (reconstruite par interpolation à partir d'un digest quantile p10/p25/p50/p75/p90) : - Feature driftée si `p_value < 0.05` - 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`. --- ## Explicabilité SHAP Lorsque `ENABLE_SHAP=true` et que la librairie `shap` est disponible : - Calcul des valeurs SHAP via `TreeExplainer` sur le modèle EIF - Les **3 features les plus contributives** sont stockées dans le champ `reason` - Format : `"feature1=valeur (±shap), feature2=valeur (±shap), feature3=valeur (±shap)"` --- ## 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__.joblib` | EIF sérialisé (joblib) | | `model__.meta.json` | Métadonnées (features, seuils, statistiques d'entraînement, digest quantile) | | `model_.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 |