Replace the LogisticRegression meta-learner with a PyTorch MetaFusionMLP (Linear(3,16)->BN->ReLU->Dropout->Linear(16,1)->Sigmoid) for non-linear fusion of EIF, NF, and XGBoost scores. Replace KS-test + quantile digest drift detection with ADWIN (adaptive sliding window, Hoeffding bound). Replace weekly XGBoost batch retraining with River HoeffdingAdaptiveTree for incremental online learning (learn_one per cycle). Update all thesis documentation sections (2.4.2c, 2.4.3, 3.8, discussion, conclusion). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1176 lines
47 KiB
Markdown
1176 lines
47 KiB
Markdown
# Bot Detector IA — Documentation Technique
|
||
|
||
> Architecture modulaire (16 modules) | Dernière mise à jour : 2026-04-13
|
||
|
||
---
|
||
|
||
## 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
|
||
├── browser_matcher.py (498) # Scoring H2 statique à 7 dimensions pondérées
|
||
├── browser_signatures.py (166) # Signatures statiques Chrome/Firefox/Safari
|
||
├── browser_matcher_dynamic.py (387) # Scoring H2 dynamique temps réel (profils auto-appris)
|
||
├── profile_builder.py (614) # Profiling HDBSCAN hors-ligne, centroïdes, lifecycle
|
||
├── fleet.py (XXX) # Détection de flottes par graphes bipartis NetworkX
|
||
├── scoring.py (588) # 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.4b `browser_matcher.py` — Scoring H2 statique
|
||
|
||
**Rôle** : Scoring à 7 dimensions pondérées des sessions HTTP/2 contre des
|
||
signatures de navigateurs connues (Chrome, Firefox, Safari).
|
||
|
||
**Fonctions exportées** :
|
||
|
||
| Fonction | Description |
|
||
|----------|-------------|
|
||
| `run_browser_matcher(df)` | Score un batch de sessions, retourne `browser_match_chrome/firefox/safari/max` |
|
||
| `log_dual_mode_comparison(df)` | Compare les scores statique vs confiance browser |
|
||
|
||
**Dimensions de scoring** :
|
||
|
||
| Dimension | Poids | Signal |
|
||
|-----------|-------|--------|
|
||
| D1 — SETTINGS H2 | 0.30 | Correspondance exacte des paramètres SETTINGS |
|
||
| D2 — WINDOW_UPDATE | 0.15 | Valeur de WINDOW_UPDATE ± tolérance |
|
||
| D3 — Pseudo-order | 0.15 | Ordre des pseudo-headers H2 |
|
||
| D4 — PRIORITY frames | 0.10 | Présence de frames PRIORITY |
|
||
| D5 — HTTP headers | 0.15 | Cohérence des headers HTTP |
|
||
| D6 — TLS structure | 0.10 | Famille TLS (JA4) |
|
||
| D7 — JA4 dict | 0.05 | Lookup dans le dictionnaire JA4 navigateurs |
|
||
|
||
**Dépendance** : `browser_signatures.py` (signatures statiques), `config.py` (`BROWSER_CONFIDENCE_THRESHOLD`).
|
||
|
||
---
|
||
|
||
### 3.4c `browser_matcher_dynamic.py` — Scoring H2 dynamique temps réel
|
||
|
||
**Rôle** : Scoring des sessions HTTP/2 contre les profils auto-appris (centroïdes HDBSCAN).
|
||
Remplace le dictionnaire statique pour l'adaptation automatique aux nouvelles versions de navigateurs.
|
||
|
||
**Fonctions exportées** :
|
||
|
||
| Fonction | Description |
|
||
|----------|-------------|
|
||
| `get_dynamic_matcher()` | Singleton du chargeur/scorer |
|
||
| `load_dynamic_profiles(client, force)` | Charge les profils depuis `auto_browser_profiles` (refresh 24h) |
|
||
| `score_session_dynamic(session)` | Score une session → `(famille, score)` |
|
||
| `score_sessions_batch_dynamic(df)` | Score un batch (ajoute `dynamic_family`, `dynamic_score`) |
|
||
|
||
**Pipeline de scoring** :
|
||
|
||
1. Chargement des profils en mémoire depuis `ja4_processing.auto_browser_profiles`
|
||
2. Pour chaque session : rejet rapide (pseudo_order incompatible ou tolérance dépassée)
|
||
3. Similarité pondérée : `h2_window_update` (0.40), `pseudo_order` (0.40), `h2_initial_window_size` (0.10), `h2_has_priority` (0.10)
|
||
4. Confiance volumétrique : `min(1.0, log10(count_ips + 1) / 4)`
|
||
|
||
---
|
||
|
||
### 3.4d `profile_builder.py` — Profiling HDBSCAN hors-ligne
|
||
|
||
**Rôle** : Pipeline quotidien (cron) qui clusterise les sessions H2 similaires,
|
||
calcule les centroïdes, et gère le cycle de vie des profils dynamiques.
|
||
|
||
**Fonction exportée** :
|
||
|
||
| Fonction | Description |
|
||
|----------|-------------|
|
||
| `run_profile_builder(client)` | Pipeline complet : extraction → HDBSCAN → centroïdes → fusion → persistance → lifecycle |
|
||
|
||
**Pipeline interne** :
|
||
|
||
1. `_fetch_profiling_data()` — Lit `view_h2_profiling_raw`, déduplique par IP, limite 2M lignes
|
||
2. `_cluster_sessions()` — HDBSCAN (`min_cluster_size=1000`) sur variables mixtes (StandardScaler + brut)
|
||
3. `_compute_centroids()` — Moyenne + 3σ (tolérance) pour continues, mode pour catégorielles
|
||
4. `_label_family()` — Analyse des UAs → `Auto_Chrome`, `Auto_Firefox`, `Auto_Safari`, `Auto_Unknown`
|
||
5. `_merge_profiles()` — Fusion des clusters redondants (même famille + pseudo_order + WU < 5%)
|
||
6. `_persist_profiles()` — INSERT INTO `auto_browser_profiles` (ReplacingMergeTree)
|
||
7. `_update_last_seen()` — Rafraîchit les profils actifs (IPs vues dans les dernières 24h)
|
||
8. `_purge_stale_profiles()` — Supprime les profils > 14 jours
|
||
|
||
**CLI** : `python -m bot_detector.profile_builder`
|
||
|
||
---
|
||
|
||
### 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.
|
||
|
||
**Filtrage Cleanlab des labels bruités** (`cleanlab>=2.6`, optionnel) :
|
||
|
||
Avant l'entraînement XGBoost, un pipeline de *confident learning* filtre les
|
||
labels SOC probablement erronés :
|
||
|
||
1. Entraînement d'un XGBoost rapide (`n_estimators=80, max_depth=4`) en 3-fold CV
|
||
2. Extraction des `pred_probs` out-of-fold via `cross_val_predict`
|
||
3. Appel à `cleanlab.filter.find_label_issues(labels=y, pred_probs=pred_probs)`
|
||
4. Exclusion des indices identifiés comme bruités du jeu d'entraînement
|
||
5. Recalcul du `scale_pos_weight` sur les données nettoyées
|
||
|
||
Le taux de labels filtrés est loggué (`[XGB][name] Cleanlab : N/M labels bruyants supprimés (X.X%)`).
|
||
En cas d'échec (erreur, dépendance manquante), le pipeline retombe sur les données brutes sans interruption.
|
||
|
||
**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).
|
||
|
||
#### `ADWINDriftMonitor(features, delta=0.002)`
|
||
|
||
Détection de dérive conceptuelle par ADWIN (fenêtre glissante adaptative).
|
||
Maintient un détecteur ADWIN par feature, mis à jour à chaque cycle avec la
|
||
moyenne de la feature sur le trafic baseline.
|
||
|
||
Si la fraction de features en dérive dépasse `DRIFT_THRESHOLD` (0.30), le
|
||
modèle EIF/NF est ré-entraîné. Si > 50% des features dérivent, une alerte
|
||
`ADVERSARIAL_DRIFT` est générée.
|
||
|
||
---
|
||
|
||
### 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 |
|
||
| `seq_emb_0`..`seq_emb_31` | Embeddings séquentiels via SessionTransformer (§5.2, remplace path_transition_entropy + cadence_cv) | 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 |
|
||
| `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
|
||
```
|