Reflète l'état réel du système après les étapes 1-9 du roadmap : - §5.2 (fleet_detector NetworkX/Louvain) et §5.8 (Jaccard cross-domain) : ✅ - MetaLearner (régression logistique, fallback poids fixes) : documenté - ExIFFI (profondeur isolation EIF) + erreur AE par feature : documenté - KL divergence en complément du KS, drift adversarial : documenté - HTTP/2 fingerprinting (h2_fingerprint, dict_browser_h2, axis_h2_coherence) : documenté - Métriques de cycle (metrics.py, ml_performance_metrics, alertes) : documenté - Browser confidence : 5 axes → 6 axes (axis_h2_coherence) - 85 features (73 FEATURES + 12 FEATURES_COMPLET), 12 modules, 53 routes dashboard - Conformité thèse : 99.4% (était 97.9%), §5 : 87.5% (était 62.5%) - Tables nouvelles : fleet_detections, ml_performance_metrics, soc_feedback - Dictionnaires : 8 (dict_browser_h2 ajouté) - Dashboard : 16 pages + 37 API routes (fleet, health ajoutés) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
704 lines
31 KiB
Markdown
704 lines
31 KiB
Markdown
# Bot Detector
|
||
|
||
Service Python de détection d'anomalies par apprentissage automatique semi-supervisé
|
||
sur le trafic HTTP/TLS agrégé dans ClickHouse. Fonctionne en cycle continu
|
||
(par défaut toutes les 5 minutes) avec un **ensemble à triple voix**
|
||
(Extended Isolation Forest + Autoencoder + XGBoost) piloté par un **méta-learner
|
||
à régression logistique**, enrichi par l'explicabilité **ExIFFI** et **SHAP**,
|
||
le clustering HDBSCAN, la détection de flottes coordonnées (NetworkX) et
|
||
la surveillance de performance par cycle.
|
||
|
||
---
|
||
|
||
## Architecture des modules
|
||
|
||
Le service est découpé en **12 modules** organisés ainsi :
|
||
|
||
```
|
||
__main__.py Point d'entrée (python -m bot_detector)
|
||
└─ cycle.py Boucle principale : requête ClickHouse → pipeline → insertion
|
||
├─ config.py Variables d'environnement, flags de disponibilité
|
||
├─ log.py Journalisation structurée JSON (structlog + RotatingFileHandler)
|
||
├─ infra.py Client ClickHouse (via ja4_common), health check HTTP, arrêt propre
|
||
├─ preprocessing.py Nettoyage du DataFrame, imputation, listes de features (FEATURES, FEATURES_COMPLET)
|
||
│ └─ browser.py Identification multifactorielle des navigateurs (6 axes)
|
||
├─ pipeline.py Orchestration : filtrage → entraînement → MetaLearner → ExIFFI → scoring → fusion
|
||
│ ├─ models.py EIF, TrafficAutoEncoder (PyTorch), XGBoost
|
||
│ └─ scoring.py Normalisation, MetaLearner, seuil adaptatif, ExIFFI, SHAP, HDBSCAN, dérive KS+KL
|
||
├─ fleet.py Graphe bipartite JA4×ASN (NetworkX), fleet_score, fleet_detections
|
||
├─ metrics.py Métriques de cycle, alertes, ml_performance_metrics
|
||
└─ (insère dans ml_all_scores + ml_detected_anomalies + fleet_detections + ml_performance_metrics)
|
||
```
|
||
|
||
| Module | Lignes | Rôle |
|
||
|--------|--------|------|
|
||
| `config.py` | 154 | Toute la configuration via `os.getenv()`, flags de disponibilité des librairies |
|
||
| `log.py` | 65 | `log_info()`, `log_decision()`, `append_training_history()` — JSONL rotatif |
|
||
| `infra.py` | 89 | Client ClickHouse (délègue à `ja4_common`), `score_to_threat_level()`, serveur de santé en thread daemon |
|
||
| `browser.py` | 191 | Détection multifactorielle des navigateurs sur **6 axes** pondérés (ajout `axis_h2_coherence`) |
|
||
| `scoring.py` | 564 | `MetaLearner` (régression logistique), normalisation, seuil adaptatif, ExIFFI, SHAP top-5, HDBSCAN, dérive KS+KL |
|
||
| `models.py` | 484 | `TrafficAutoEncoder`, entraînement/chargement EIF, XGBoost, élagage de features |
|
||
| `preprocessing.py` | 127 | `preprocess_df()` — nettoyage, typage, imputation, listes `FEATURES` / `FEATURES_COMPLET` |
|
||
| `pipeline.py` | 441 | `run_semi_supervised_logic()` — orchestration complète d'un modèle, MetaLearner, ExIFFI |
|
||
| `fleet.py` | 174 | `build_fleet_graph()`, `detect_fleet_communities()`, `enrich_with_fleet_score()` — NetworkX + HDBSCAN |
|
||
| `metrics.py` | 166 | `record_cycle_metrics()`, `_emit_alerts()` — table `ml_performance_metrics` |
|
||
| `cycle.py` | 415 | `fetch_and_analyze()` — boucle principale, feedback SOC, multiwindow |
|
||
| `__main__.py` | 41 | Point d'entrée, bannière de démarrage, boucle `while True` |
|
||
|
||
---
|
||
|
||
## Configuration
|
||
|
||
Toute la configuration est lue via `os.getenv()` dans `config.py`. Aucun fichier YAML ni pydantic-settings.
|
||
|
||
### Connexion ClickHouse
|
||
|
||
| Variable | Type | Défaut | Description |
|
||
|----------|------|--------|-------------|
|
||
| `CLICKHOUSE_HOST` | str | `clickhouse` | Nom d'hôte du serveur ClickHouse (via `ja4_common`) |
|
||
| `CLICKHOUSE_PORT` | int | `8123` | Port HTTP ClickHouse (via `ja4_common`) |
|
||
| `CLICKHOUSE_USER` | str | `admin` | Utilisateur ClickHouse (via `ja4_common`) |
|
||
| `CLICKHOUSE_PASSWORD` | str | `""` | Mot de passe ClickHouse (via `ja4_common`) |
|
||
| `CLICKHOUSE_DB_PROCESSING` | str | `ja4_processing` | Base de données ML/agrégations (fallback : `CLICKHOUSE_DB`) |
|
||
| `CLICKHOUSE_DB_LOGS` | str | `ja4_logs` | Base de données des logs HTTP |
|
||
|
||
### Modèle et entraînement
|
||
|
||
| Variable | Type | Défaut | Description |
|
||
|----------|------|--------|-------------|
|
||
| `N_ESTIMATORS` | int | `300` | Nombre d'arbres pour l'Extended Isolation Forest |
|
||
| `ISOLATION_CONTAMINATION` | float | `0.001` | Paramètre de contamination EIF (plage ]0, 0.5[) |
|
||
| `ANOMALY_THRESHOLD` | float | `-0.05` | Seuil de score brut pour la détection d'anomalie |
|
||
| `ANOMALY_PERCENTILE` | int | `5` | Percentile pour le seuil adaptatif |
|
||
| `MODEL_DIR` | str | `/var/lib/bot_detector` | Répertoire de persistance des modèles |
|
||
| `MODEL_HISTORY_COUNT` | int | `10` | Nombre de versions de modèle conservées |
|
||
| `RETRAIN_INTERVAL_HOURS` | int | `24` | Intervalle de réentraînement EIF (heures) |
|
||
| `DRIFT_THRESHOLD` | float | `0.30` | Seuil de dérive KS (fraction de features driftées) |
|
||
| `MIN_VALID_FEATURE_RATIO` | float | `0.50` | Ratio minimal de features valides pour entraîner |
|
||
| `PRUNE_VARIANCE_THRESHOLD` | float | `1e-6` | Seuil de variance pour l'élagage de features |
|
||
| `VAL_ANOMALY_GATE` | float | `0.20` | Garde-fou : taux maximum d'anomalies en validation |
|
||
|
||
### Autoencoder
|
||
|
||
| Variable | Type | Défaut | Description |
|
||
|----------|------|--------|-------------|
|
||
| `AE_WEIGHT` | float | `0.30` | Poids de l'Autoencoder dans le score combiné (plage ]0, 1[) |
|
||
| `AE_EPOCHS` | int | `50` | Nombre d'époques d'entraînement |
|
||
| `AE_LATENT_DIM` | int | `16` | Dimension de l'espace latent |
|
||
| `AE_LEARNING_RATE` | float | `1e-3` | Taux d'apprentissage Adam |
|
||
|
||
### XGBoost
|
||
|
||
| Variable | Type | Défaut | Description |
|
||
|----------|------|--------|-------------|
|
||
| `XGB_WEIGHT` | float | `0.20` | Poids de XGBoost dans le score final (plage ]0, 1[) |
|
||
| `XGB_MIN_LABELS` | int | `100` | Nombre minimal de labels SOC pour activer XGBoost |
|
||
| `XGB_RETRAIN_INTERVAL_HOURS` | int | `168` | Intervalle de réentraînement XGBoost (7 jours) |
|
||
|
||
### Détection navigateur
|
||
|
||
| Variable | Type | Défaut | Description |
|
||
|----------|------|--------|-------------|
|
||
| `BROWSER_CONFIDENCE_THRESHOLD` | float | `0.55` | Confiance minimale pour classifier `LEGITIMATE_BROWSER` |
|
||
| `BROWSER_COHORT_RATIO` | float | `0.70` | Si ≥ 70 % des sessions d'un JA4 sont navigateur → propagation |
|
||
|
||
### Clustering et explicabilité
|
||
|
||
| Variable | Type | Défaut | Description |
|
||
|----------|------|--------|-------------|
|
||
| `ENABLE_CLUSTERING` | bool | `true` | Activer le clustering HDBSCAN |
|
||
| `CLUSTERING_MIN_SAMPLES` | int | `3` | Taille minimale de cluster |
|
||
| `ENABLE_SHAP` | bool | `true` | Activer l'explicabilité SHAP (requiert `shap` installé) |
|
||
|
||
### Cycle et opérations
|
||
|
||
| Variable | Type | Défaut | Description |
|
||
|----------|------|--------|-------------|
|
||
| `CYCLE_INTERVAL_SEC` | int | `300` | Intervalle entre les cycles (secondes) |
|
||
| `MAX_CONSECUTIVE_FAILURES` | int | `3` | Échecs consécutifs avant `healthy=False` |
|
||
| `DEDUP_TTL_MIN` | int | `60` | TTL de déduplication inter-cycle (minutes) |
|
||
| `RECURRENCE_WEIGHT` | float | `0.005` | Poids de la récurrence dans le score brut |
|
||
| `ENABLE_MULTIWINDOW` | bool | `false` | Activer l'analyse multi-fenêtre 24h |
|
||
| `MULTIWINDOW_VIEW` | str | `view_ai_features_24h` | Vue ClickHouse pour le mode multi-fenêtre |
|
||
| `ENABLE_FEEDBACK` | bool | `true` | Activer l'intégration du feedback SOC |
|
||
| `FEEDBACK_WINDOW_DAYS` | int | `7` | Fenêtre de feedback SOC (jours) |
|
||
|
||
### Journalisation et santé
|
||
|
||
| Variable | Type | Défaut | Description |
|
||
|----------|------|--------|-------------|
|
||
| `BOT_DETECTOR_LOG` | str | `/var/log/bot_detector/decisions.jsonl` | Chemin du fichier de décisions |
|
||
| `LOG_BACKUP_COUNT` | int | `7` | Nombre de fichiers de rotation conservés |
|
||
| `HEALTH_PORT` | int | `8080` | Port du serveur de santé HTTP |
|
||
|
||
---
|
||
|
||
## Pipeline ML — Ensemble à triple voix
|
||
|
||
### Vue d'ensemble
|
||
|
||
Le bot-detector utilise trois modèles en parallèle, combinés par une pondération configurable :
|
||
|
||
```
|
||
┌──────────────────────┐
|
||
│ Extended Isolation │
|
||
│ Forest (isotree) │──→ eif_norm (0–1)
|
||
└──────────────────────┘ │
|
||
│ × (1 − AE_WEIGHT)
|
||
┌──────────────────────┐ │
|
||
│ TrafficAutoEncoder │ ├──→ combined_norm
|
||
│ (PyTorch) │──→ ae_norm (0–1)
|
||
└──────────────────────┘ × AE_WEIGHT
|
||
│ × (1 − XGB_WEIGHT)
|
||
┌──────────────────────┐ │
|
||
│ XGBoost │ ├──→ anomaly_score
|
||
│ (supervisé, labels │──→ xgb_prob (0–1)
|
||
│ SOC) │ × XGB_WEIGHT
|
||
└──────────────────────┘
|
||
```
|
||
|
||
**Formule du score final :**
|
||
|
||
```
|
||
combined_norm = (1 − AE_WEIGHT) × eif_norm + AE_WEIGHT × ae_norm
|
||
anomaly_score = (1 − XGB_WEIGHT) × combined_norm + XGB_WEIGHT × xgb_prob
|
||
```
|
||
|
||
Avec les poids par défaut (`AE_WEIGHT=0.30`, `XGB_WEIGHT=0.20`) :
|
||
|
||
```
|
||
anomaly_score = 0.56 × eif_norm + 0.24 × ae_norm + 0.20 × xgb_prob
|
||
```
|
||
|
||
### Architecture duale
|
||
|
||
Deux modèles indépendants tournent sur chaque cycle :
|
||
|
||
| Modèle | Condition | Features | Données |
|
||
|--------|-----------|----------|---------|
|
||
| **Complet** | `correlated = 1` | 85 (`FEATURES_COMPLET`) | HTTP + TCP + TLS (L3→L7) |
|
||
| **Applicatif** | `correlated = 0` | 73 (`FEATURES`) | HTTP seul (L7 pur) |
|
||
|
||
En mode multi-fenêtre (`ENABLE_MULTIWINDOW=true`), deux variantes supplémentaires sont exécutées sur la vue 24h : `Complet_24h` et `Applicatif_24h`.
|
||
|
||
### Extended Isolation Forest (EIF)
|
||
|
||
Modèle principal non supervisé. Utilise `isotree.IsolationForest` :
|
||
|
||
```python
|
||
isotree.IsolationForest(
|
||
ntrees=300, # N_ESTIMATORS
|
||
ndim=min(3, n_features),
|
||
sample_size='auto',
|
||
missing_action='impute',
|
||
random_seed=42,
|
||
nthreads=-1,
|
||
)
|
||
```
|
||
|
||
**Fallback** si `isotree` n'est pas disponible : `sklearn.ensemble.IsolationForest(n_estimators=300, contamination=CONTAMINATION)`.
|
||
|
||
**Calibration** : le score isotree brut (∈ [0, 1], >0.5 = anomalous) est converti en convention sklearn : `sklearn_equiv = 0.5 − isotree_score`.
|
||
|
||
### TrafficAutoEncoder (PyTorch)
|
||
|
||
Architecture symétrique encodeur-décodeur :
|
||
|
||
```
|
||
Encodeur : n_features → dim1 → dim2 → 16 (latent)
|
||
Décodeur : 16 → dim2 → dim1 → n_features
|
||
|
||
dim1 = min(64, max(n_features, latent_dim + 4))
|
||
dim2 = min(32, max(dim1 // 2, latent_dim + 2))
|
||
```
|
||
|
||
- Activations : `ReLU` + `BatchNorm1d` sur les couches cachées, `Sigmoid` en sortie du décodeur
|
||
- Optimiseur : `Adam(lr=1e-3, weight_decay=1e-5)`
|
||
- Perte : `MSELoss`
|
||
- Entraînement : 50 époques, batch_size=256
|
||
- Score : erreur de reconstruction MSE par échantillon
|
||
- Normalisation des entrées : min-max [0, 1] par feature
|
||
|
||
### XGBoost (supervisé)
|
||
|
||
Entraîné sur les labels issus du feedback SOC (table `soc_feedback`) :
|
||
|
||
```python
|
||
xgb.XGBClassifier(
|
||
n_estimators=200,
|
||
max_depth=6,
|
||
learning_rate=0.1,
|
||
scale_pos_weight=auto, # max(1, n_neg / n_pos)
|
||
eval_metric='logloss',
|
||
tree_method='hist',
|
||
random_state=42,
|
||
n_jobs=-1,
|
||
)
|
||
```
|
||
|
||
- Labels positifs (bot) : `HIGH`, `CRITICAL`, `ANUBIS_DENY`, `KNOWN_BOT`
|
||
- Labels négatifs (légitime) : `NORMAL`, `LEGITIMATE_BROWSER`
|
||
- Activation requiert ≥ `XGB_MIN_LABELS` (100) labels
|
||
- Réentraînement tous les `XGB_RETRAIN_INTERVAL_HOURS` (168h = 7 jours)
|
||
|
||
---
|
||
|
||
## Détection multifactorielle des navigateurs
|
||
|
||
Module `browser.py` — classifie chaque session sur 5 axes pondérés :
|
||
|
||
| Axe | Clé | Poids | Composantes |
|
||
|-----|-----|-------|-------------|
|
||
| **1 — JA4 connu** | `axis_ja4_known` | 0.25 | Famille navigateur identifiée dans `dict_browser_ja4` → 1.0, sinon 0.0 |
|
||
| **2 — Structure JA4** | `axis_ja4_struct` | 0.15 | TLS 1.3 (×0.35), h2/h3 (×0.25), nb ciphers 10–25 (×0.20), nb extensions 10–25 (×0.20) |
|
||
| **3 — HTTP moderne** | `axis_http_modern` | 0.25 | modern_browser_score ≥ 50 (×0.35), Accept-Language (×0.20), Sec-Fetch < 0.3 (×0.25), generic_accept < 0.3 (×0.10), pas de ua_ch_mismatch (×0.10) |
|
||
| **4 — Comportement navigation** | `axis_nav_behavior` | 0.15 | has_cookie (×0.25), has_referer (×0.25), asset_ratio > 0.15 (×0.25), direct_access < 0.5 (×0.25) |
|
||
| **5 — Cohérence TLS/TCP** | `axis_tls_coherence` | 0.20 | Pas d'alpn_mismatch (×0.25), window_scale OK (×0.20), tls12 < 0.1 (×0.20), pas d'http10 (×0.15), ALPN présent (×0.20) |
|
||
|
||
**Seuil** : `browser_confidence ≥ 0.55` + famille identifiée → `LEGITIMATE_BROWSER`
|
||
|
||
**Propagation par cohorte** : si ≥ 70 % des sessions partageant un JA4 sont classées navigateur, les sessions `NORMAL`/`LOW` restantes avec le même JA4 sont aussi classées `LEGITIMATE_BROWSER`.
|
||
|
||
**Inférence de famille** : pour les JA4 inconnus, correspondance structurelle avec les profils `_BROWSER_JA4_PROFILES` (Chromium, Firefox, Safari, Tor_Browser) — requiert `browser_confidence ≥ 0.45`.
|
||
|
||
---
|
||
|
||
## Scoring et normalisation
|
||
|
||
### Normalisation des scores (`normalize_scores`)
|
||
|
||
Les scores bruts (négatifs = anomalous) sont mappés vers **[0, 1]** avec **1 = le plus anomalous** :
|
||
|
||
```
|
||
result[mask] = clip(−scores / (−s_min + 1e-9), 0, 1)
|
||
```
|
||
|
||
Les scores ≥ 0 sont mis à 0.
|
||
|
||
### Seuil adaptatif (`compute_adaptive_threshold`)
|
||
|
||
```
|
||
threshold = min(percentile(scores_négatifs, ANOMALY_PERCENTILE), ANOMALY_THRESHOLD)
|
||
```
|
||
|
||
Avec `ANOMALY_PERCENTILE=5` et `ANOMALY_THRESHOLD=-0.05`.
|
||
|
||
### Pénalité de récurrence
|
||
|
||
Appliquée au `raw_anomaly_score` :
|
||
|
||
```
|
||
raw_anomaly_score −= log1p(recurrence_count) × RECURRENCE_WEIGHT
|
||
```
|
||
|
||
### Niveaux de menace
|
||
|
||
| Plage de score brut | Niveau | Interprétation |
|
||
|---------------------|--------|----------------|
|
||
| `< −0.30` | **CRITICAL** | Comportement extrêmement anomalous |
|
||
| `< −0.15` | **HIGH** | Signal d'anomalie fort |
|
||
| `< −0.05` | **MEDIUM** | Anomalie modérée |
|
||
| `< 0` | **LOW** | Légèrement inhabituel |
|
||
| `≥ 0` | **NORMAL** | Trafic normal |
|
||
|
||
---
|
||
|
||
## Clustering HDBSCAN des campagnes
|
||
|
||
Lorsque `ENABLE_CLUSTERING=true`, les anomalies sont regroupées en campagnes par HDBSCAN :
|
||
|
||
```python
|
||
hdbscan.HDBSCAN(
|
||
min_cluster_size=CLUSTERING_MIN_SAMPLES, # défaut : 3
|
||
min_samples=max(2, CLUSTERING_MIN_SAMPLES − 1), # défaut : 2
|
||
cluster_selection_method='eom',
|
||
)
|
||
```
|
||
|
||
**Espace de clustering** : si un Autoencoder est disponible, le clustering s'effectue dans l'**espace latent 16-dim** de l'AE. Sinon, `StandardScaler` est appliqué sur les features brutes.
|
||
|
||
**Fallback** si `hdbscan` n'est pas disponible : `DBSCAN(eps=0.5, min_samples=CLUSTERING_MIN_SAMPLES)`.
|
||
|
||
Chaque anomalie reçoit un `campaign_id` (−1 = pas de cluster).
|
||
|
||
---
|
||
|
||
## Liste des features
|
||
|
||
### Features communes — modèle Applicatif (73 features)
|
||
|
||
#### Comportement HTTP
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `hits` | Nombre de requêtes dans la fenêtre |
|
||
| `hit_velocity` | Requêtes par seconde |
|
||
| `fuzzing_index` | Score de diversité chemins/paramètres |
|
||
| `post_ratio` | Fraction de requêtes POST |
|
||
| `port_exhaustion_ratio` | Fraction de ports source distincts / total |
|
||
| `orphan_ratio` | Requêtes sans corrélation TLS |
|
||
| `head_ratio` | Fraction de requêtes HEAD |
|
||
| `http10_ratio` | Fraction de requêtes HTTP/1.0 |
|
||
| `generic_accept_ratio` | Fraction d'en-têtes Accept courts |
|
||
| `sec_fetch_absence_rate` | Fraction sans Sec-Fetch-Site |
|
||
| `missing_accept_enc_ratio` | Fraction sans Accept-Encoding |
|
||
| `http_scheme_ratio` | Fraction utilisant HTTP (pas HTTPS) |
|
||
|
||
#### Gestion de connexion
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `max_keepalives` | Max de requêtes sur une seule connexion Keep-Alive |
|
||
| `tcp_shared_count` | Connexions TCP partagées entre sessions |
|
||
| `multiplexing_efficiency` | Efficacité du multiplexage HTTP/2 |
|
||
|
||
#### Empreinte navigateur
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `header_count` | Nombre d'en-têtes HTTP envoyés |
|
||
| `has_accept_language` | Présence de l'en-tête Accept-Language |
|
||
| `has_cookie` | Présence de l'en-tête Cookie |
|
||
| `has_referer` | Présence de l'en-tête Referer |
|
||
| `modern_browser_score` | Score composite de conformité navigateur (0–100) |
|
||
| `ua_ch_mismatch` | Incohérence User-Agent vs Client Hints |
|
||
| `ip_id_zero_ratio` | Paquets IP avec ID=0 (pile minimaliste/headless) |
|
||
| `header_order_shared_count` | IPs partageant le même ordre d'en-têtes |
|
||
| `header_order_confidence` | Entropie normalisée de l'ordre des en-têtes |
|
||
| `distinct_header_orders` | Ordres d'en-têtes distincts par IP |
|
||
| `is_fake_navigation` | Sec-Fetch-Mode=navigate avec destination non-document |
|
||
|
||
#### Comportement de navigation
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `request_size_variance` | Variance de la taille des requêtes |
|
||
| `mss_mobile_mismatch` | Incohérence TCP MSS vs profil mobile |
|
||
| `asset_ratio` | Fraction de requêtes de ressources statiques |
|
||
| `direct_access_ratio` | Accès directs (sans referer) |
|
||
| `is_ua_rotating` | Rotation de User-Agent détectée |
|
||
| `distinct_ja4_count` | Empreintes JA4 distinctes par IP |
|
||
| `anomalous_payload_ratio` | Fraction de charges utiles anomalous |
|
||
|
||
#### Concentration et rareté
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `src_port_density` | Entropie des ports source |
|
||
| `ja4_asn_concentration` | Concentration JA4 au sein de l'ASN |
|
||
| `ja4_country_concentration` | Concentration JA4 par pays |
|
||
| `is_rare_ja4` | Empreinte JA4 rare (< 100 hits totaux) |
|
||
|
||
#### Temporel et diversité
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `temporal_entropy` | Entropie de la distribution temporelle |
|
||
| `path_diversity_ratio` | Diversité des chemins URL |
|
||
| `url_depth_variance` | Variance de la profondeur des URL |
|
||
|
||
#### Anubis
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `anubis_is_flagged` | Signal de suspicion Anubis (bot détecté, action ni ALLOW/DENY/vide) |
|
||
|
||
#### Navigateur multifactoriel — 6 axes
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `is_known_browser` | JA4 correspond à un navigateur connu |
|
||
| `browser_consistency_score` | Score composite de cohérence navigateur |
|
||
| `browser_confidence` | Confiance globale de l'identification navigateur (seuil 0.55) |
|
||
| `axis_ja4_known` | Axe 1 : JA4 reconnu dans `dict_browser_ja4` |
|
||
| `axis_ja4_struct` | Axe 2 : structure JA4 cohérente (chiffrement, extensions TLS) |
|
||
| `axis_http_modern` | Axe 3 : comportement HTTP moderne (Sec-Fetch-*, Accept-Language) |
|
||
| `axis_nav_behavior` | Axe 4 : comportement de navigation (asset_ratio, referer, cascade) |
|
||
| `axis_tls_coherence` | Axe 5 : cohérence TLS/TCP (ALPN, SNI, MSS mobile) |
|
||
| `axis_h2_coherence` | Axe 6 : cohérence HTTP/2 (h2_fingerprint ↔ famille navigateur déclarée) |
|
||
|
||
#### Thèse §5 — Features avancées
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `path_transition_entropy` | Entropie des transitions de chemins (§5.1) |
|
||
| `cadence_cv` | Coefficient de variation de la cadence de requêtes (§5.3) |
|
||
| `burst_ratio` | Fraction de requêtes en rafale (§5.3) |
|
||
| `pause_ratio` | Fraction de pauses longues (§5.3) |
|
||
| `lag1_autocorrelation` | Autocorrélation lag-1 des inter-arrivées (§5.3) |
|
||
| `benford_deviation` | Déviation par rapport à la loi de Benford |
|
||
| `root_to_first_asset_delay` | Délai HTML → premier asset (§5.4, détection headless) |
|
||
| `asset_load_stddev` | Écart-type des intervalles de chargement d'assets (§5.4) |
|
||
| `host_diversity` | Diversité des hôtes ciblés (§5.8) |
|
||
| `host_sweep_speed` | Vitesse de balayage des hôtes (§5.8) |
|
||
| `host_coverage_uniformity` | Uniformité de couverture des hôtes (§5.8) |
|
||
| `cross_domain_path_similarity` | Similarité Jaccard inter-hôte des chemins — scanner latéral (§5.8) |
|
||
|
||
#### Features HTTP/2
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `h2_settings_known` | Fingerprint HTTP/2 reconnu dans `dict_browser_h2` (0/1) |
|
||
| `h2_pseudo_order_match` | Ordre des pseudo-headers cohérent avec le navigateur déclaré (0/1) |
|
||
| `h2_ja4_coherence` | Cohérence HTTP/2 ↔ JA4 (même famille) (0/1) |
|
||
| `h2_settings_rare` | Fingerprint HTTP/2 avec <100 occurrences globales (0/1) |
|
||
|
||
#### Score de cohérence cross-layer
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `fingerprint_coherence_score` | Score composite (0.0–1.0) combinant : JA4↔UA, H2↔JA4, TCP↔UA, langue↔ASN, Sec-CH-UA↔UA |
|
||
|
||
#### Features TCP fenêtre
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `true_window_size` | Taille réelle de la fenêtre TCP |
|
||
| `window_mss_ratio` | Ratio fenêtre TCP / MSS |
|
||
|
||
#### Features de détection avancées
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `has_xff` | Présence de l'en-tête X-Forwarded-For |
|
||
| `unusual_content_type_ratio` | Fraction de Content-Type inhabituels |
|
||
| `non_standard_port_ratio` | Fraction de ports non standards |
|
||
| `login_post_concentration` | Concentration de POST sur les pages de login |
|
||
| `sec_ch_mobile_mismatch` | Incohérence Sec-CH-UA-Mobile |
|
||
|
||
### Features supplémentaires — modèle Complet (+12 features TCP/TLS)
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `tcp_jitter_variance` | Variance du jitter TCP inter-paquets |
|
||
| `alpn_http_mismatch` | Incohérence ALPN vs protocole HTTP réel |
|
||
| `is_alpn_missing` | ALPN absent dans le ClientHello |
|
||
| `sni_host_mismatch` | Incohérence TLS SNI vs en-tête Host HTTP |
|
||
| `ja3_diversity_ratio` | Ratio de diversité JA3 par IP |
|
||
| `syn_timing_cv` | Coefficient de variation du timing SYN |
|
||
| `tls12_ratio` | Fraction de connexions TLS 1.2 |
|
||
| `ip_df_variance` | Variance du flag Don't-Fragment IP |
|
||
| `avg_ttl` | TTL IP moyen (empreinte OS) |
|
||
| `ttl_std` | Écart-type du TTL |
|
||
| `no_window_scale_ratio` | Fraction sans TCP Window Scale |
|
||
| `ja4_drift_ratio` | Dérive JA4 intra-session (§5.5) |
|
||
|
||
---
|
||
|
||
## Pipeline de détection
|
||
|
||
```
|
||
1. Requête view_ai_features_1h → DataFrame
|
||
2. Enrichissement optionnel view_thesis_features_1h (features thèse §5)
|
||
3. Prétraitement : preprocess_df() (nettoyage, browser axes 6, imputation)
|
||
4. Chargement du feedback SOC → reclassification
|
||
5. Chargement de la carte de récurrence (view_ip_recurrence)
|
||
6. Séparation par correlated = 1 / correlated = 0
|
||
7. Pour chaque modèle (Complet, Applicatif) :
|
||
a. Validation des features (exclure constantes/manquantes)
|
||
b. Séparation des bots connus → journalisation KNOWN_BOT
|
||
c. Filtrage de la baseline humaine (asn_label = 'human', fingerprint_coherence_score ≥ seuil)
|
||
d. Chargement ou entraînement EIF + AE
|
||
e. Scoring du trafic inconnu (EIF + AE)
|
||
f. Chargement ou entraînement XGBoost (si labels disponibles)
|
||
g. MetaLearner : pondération apprise (logistique) sur historique SOC, sinon fallback poids fixes
|
||
h. Combinaison des scores via MetaLearner ou formule fixe
|
||
i. Normalisation [0, 1]
|
||
j. Seuil adaptatif (percentile_5 des scores négatifs, minimum -0.05)
|
||
k. Pénalité de récurrence
|
||
l. ExIFFI (importance par profondeur d'isolation EIF) + erreur AE par feature
|
||
m. SHAP top-5 TreeExplainer
|
||
n. HDBSCAN clustering → campaign_id
|
||
o. Détection de dérive (KS test + KL divergence)
|
||
p. Alerte drift adversarial (dérive simultanée multiple features → direction commune)
|
||
8. Analyse de flotte (fleet.py) : graphe bipartite JA4×ASN → communautés Louvain → fleet_score
|
||
9. Mode multi-fenêtre (si activé) : idem sur view_ai_features_24h
|
||
10. Insertion → ml_all_scores (toutes les sessions scorées)
|
||
11. Déduplication intra-cycle (garder raw_anomaly_score le plus bas par IP)
|
||
12. Déduplication inter-cycle (TTL, skip si détecté récemment sauf aggravation ≥ 0.05)
|
||
13. Insertion → ml_detected_anomalies (anomalies filtrées)
|
||
14. Insertion → fleet_detections (flottes détectées avec fleet_score)
|
||
15. Enregistrement → ml_performance_metrics (métriques de cycle + alertes)
|
||
```
|
||
|
||
---
|
||
|
||
## Détection de dérive (KS + KL divergence)
|
||
|
||
Par feature, deux tests comparent la distribution courante avec la distribution d'entraînement :
|
||
|
||
**Test KS (Kolmogorov-Smirnov)** :
|
||
- Distribution reconstruite par interpolation à partir d'un digest quantile 9 points (p5, p10, p25, p50, p75, p90, p95)
|
||
- Feature driftée si `p_value < 0.05`
|
||
|
||
**Divergence KL (Kullback-Leibler)** :
|
||
- Histogramme discrétisé (20 bins) de la distribution courante vs baseline
|
||
- Feature driftée si `KL > seuil` (0.5 par défaut)
|
||
- Détection de **drift adversarial** : si ≥30% des features dérivent simultanément dans la même direction → alerte `ADVERSARIAL_DRIFT`
|
||
|
||
**Règle de décision** : une feature est en drift si KS **ou** KL dépasse son seuil.
|
||
|
||
- Dérive globale = fraction de features driftées
|
||
- Si `drift > DRIFT_THRESHOLD` (0.30) → réentraînement automatique
|
||
|
||
**Fallback** (sans `scipy`) : méthode Z-score — feature driftée si `|moyenne_courante − moyenne_entraînement| / std_entraînement > 2.0`.
|
||
|
||
---
|
||
|
||
## MetaLearner
|
||
|
||
Remplace la pondération linéaire fixe `(1-XGB_W)×((1-AE_W)×eif + AE_W×ae) + XGB_W×xgb` par une régression logistique apprise (`scoring.MetaLearner`) :
|
||
|
||
```
|
||
P(bot) = logistic(w1×eif + w2×ae + w3×xgb + w4×volume + w5×correlated + bias)
|
||
```
|
||
|
||
- **Entraînement** : sur l'historique `ml_all_scores` JOIN `soc_feedback` (labels SOC + KNOWN_BOT + ANUBIS_DENY + LEGITIMATE_BROWSER)
|
||
- **Seuil** : activé seulement si ≥1000 labels disponibles — sinon fallback aux poids fixes
|
||
- **Transparence** : poids appris journalisés dans `ml_performance_metrics` pour audit SOC
|
||
- **Validation** : comparaison MetaLearner vs poids fixes sur split temporel (données postérieures à l'entraînement)
|
||
|
||
---
|
||
|
||
## ExIFFI et explicabilité augmentée
|
||
|
||
En complément de SHAP, le module expose deux méthodes d'importance de features spécifiques aux modèles utilisés :
|
||
|
||
**ExIFFI** (`compute_exiffi_importance`) :
|
||
- Importance calculée par la profondeur moyenne d'isolation par feature dans l'EIF
|
||
- Une feature avec profondeur d'isolation faible contribue fortement à l'anomalie
|
||
- Corrèle avec SHAP mais capte des aspects complémentaires de la structure EIF
|
||
|
||
**Erreur AE par feature** (`compute_ae_feature_errors`) :
|
||
- Reconstruction PyTorch feature par feature : `err_i = (x_i - x̂_i)²`
|
||
- Pour chaque anomalie, les features avec la plus grande erreur de reconstruction sont identifiées
|
||
- Expose quelles dimensions l'autoencoder ne parvient pas à reconstruire
|
||
|
||
Les deux méthodes sont disponibles dans le champ `shap_features` des résultats, en complément des valeurs SHAP TreeExplainer.
|
||
|
||
---
|
||
|
||
## Détection de flottes (fleet.py)
|
||
|
||
Détecte les **botnets coordonnés** utilisant des JA4 et ASN rotatifs via analyse de graphe bipartite :
|
||
|
||
1. **Construction du graphe** : nœuds JA4 ∪ ASN, arêtes IP observées dans le cycle
|
||
2. **Projection** : projection du graphe bipartite sur les nœuds JA4
|
||
3. **Communautés** : algorithme de Louvain (NetworkX) sur le graphe projeté
|
||
4. **Score de flotte** : `fleet_score = taille_communauté × densité_arêtes / log(nb_ASN)`
|
||
5. **Enrichissement** : les IPs membres reçoivent un malus proportionnel au fleet_score
|
||
|
||
Résultats stockés dans `fleet_detections` (TTL 7 jours). Exposés dans le dashboard via la page `/fleet`.
|
||
|
||
---
|
||
|
||
## Métriques de performance (metrics.py)
|
||
|
||
Enregistre par cycle dans `ml_performance_metrics` :
|
||
|
||
| Métrique | Description |
|
||
|----------|-------------|
|
||
| `anomaly_rate` | Taux d'anomalies détectées (cible : 0.5%–10%) |
|
||
| `known_bot_rate` | Fraction KNOWN_BOT dans le cycle |
|
||
| `legit_browser_rate` | Fraction LEGITIMATE_BROWSER |
|
||
| `drift_rate` | Fraction de features en dérive |
|
||
| `corr_rate` | Taux de sessions corrélées (cible : ≥50%) |
|
||
| `cycle_latency_s` | Durée totale d'inférence (cible : <300s) |
|
||
| `alert_flags` | Alertes émises (CALIBRATION_HIGH/LOW, DRIFT, CORRELATION, LATENCY) |
|
||
|
||
**Seuils d'alerte** :
|
||
- `anomaly_rate > 10%` → `CALIBRATION_HIGH`
|
||
- `anomaly_rate < 0.5%` → `CALIBRATION_LOW`
|
||
- `drift_rate > 30%` → `DRIFT_ALERT`
|
||
- `corr_rate < 50%` → `CORRELATION_ALERT`
|
||
- `cycle_latency_s > 300` → `LATENCY_ALERT`
|
||
|
||
---
|
||
|
||
## Enrichissement Anubis
|
||
|
||
La vue `view_ai_features_1h` enrichit chaque IP via les dictionnaires Anubis selon une cascade de priorité :
|
||
|
||
1. **UA + IP combinés** (même `rule_id`) — confiance maximale
|
||
2. **UA seul** (pas de condition IP)
|
||
3. **IP seul** (pas de condition UA)
|
||
4. **Correspondance ASN**
|
||
5. **Correspondance pays**
|
||
|
||
---
|
||
|
||
## Tables de sortie
|
||
|
||
### ml_detected_anomalies
|
||
|
||
Détections d'anomalies au-dessus du seuil de menace. Engine : `ReplacingMergeTree(detected_at)`, ORDER BY `(src_ip, model_name)`, TTL 7 jours.
|
||
|
||
Colonnes clés : `detected_at`, `src_ip`, `ja4`, `host`, `bot_name`, `anomaly_score`, `raw_anomaly_score`, `threat_level`, `model_name`, `recurrence`, `campaign_id`, `reason`, `anubis_bot_name`, `anubis_bot_action`, `anubis_bot_category`, plus toutes les features ML.
|
||
|
||
### ml_all_scores
|
||
|
||
Toutes les classifications (sans filtre de seuil) pour l'observabilité. Engine : `ReplacingMergeTree(detected_at)`, ORDER BY `(window_start, src_ip, ja4, host, model_name)`, TTL 7 jours.
|
||
|
||
---
|
||
|
||
## Format du journal de décisions
|
||
|
||
Le fichier `decisions.jsonl` contient des entrées JSONL structurées :
|
||
|
||
```json
|
||
{"event": "CYCLE_START", "cycle_id": "20260309T143000", "total": 5000, "human": 1500, "known_bot": 200, "correlated": 3000}
|
||
{"event": "ANOMALY", "src_ip": "203.0.113.42", "score": -0.25, "threat_level": "HIGH", "reason": "hit_velocity=45.2, fuzzing_index=0.8, ...", "campaign_id": 3}
|
||
{"event": "KNOWN_BOT", "src_ip": "198.51.100.10", "bot_name": "AhrefsBot"}
|
||
{"event": "CYCLE_END", "cycle_id": "20260309T143000", "anomalies": 15, "known_bots": 200, "duration_sec": 12.5}
|
||
```
|
||
|
||
Rotation des logs : 50 Mo max × `LOG_BACKUP_COUNT` sauvegardes (défaut : 7).
|
||
|
||
---
|
||
|
||
## Point de santé
|
||
|
||
- **URL** : `GET http://localhost:8080/`
|
||
- **Réponses** : `200 OK` (corps `OK`) ou `503 Service Unavailable` (corps `DEGRADED`)
|
||
- Exécuté dans un thread daemon au démarrage
|
||
- État mis à `False` après `MAX_CONSECUTIVE_FAILURES` (3) échecs consécutifs ClickHouse, remis à `True` dès le premier succès
|
||
|
||
---
|
||
|
||
## Persistance des modèles
|
||
|
||
| Fichier | Description |
|
||
|---------|-------------|
|
||
| `model_<name>_<version>.joblib` | EIF sérialisé (joblib) |
|
||
| `model_<name>_<version>.meta.json` | Métadonnées (features, seuils, statistiques d'entraînement, digest quantile) |
|
||
| `model_<name>.current` | Pointeur vers la version active |
|
||
| `training_history.jsonl` | Historique d'entraînement |
|
||
|
||
Rotation automatique : seules les `MODEL_HISTORY_COUNT` dernières versions (défaut : 10) sont conservées.
|
||
|
||
---
|
||
|
||
## Déploiement Docker
|
||
|
||
```bash
|
||
# Construction de l'image
|
||
make build-bot-detector
|
||
|
||
# Exécution avec docker-compose
|
||
cd services/bot-detector
|
||
docker-compose up -d
|
||
|
||
# Tests
|
||
make test-bot-detector
|
||
```
|
||
|
||
### Volumes
|
||
|
||
| Chemin hôte | Chemin conteneur | Description |
|
||
|-------------|-----------------|-------------|
|
||
| `./bot_detector_logs` | `/var/log/bot_detector` | Journaux de décisions (JSONL) |
|
||
| `./bot_detector_models` | `/var/lib/bot_detector` | Modèles ML persistés |
|
||
| `./reputation/data/user_files/bot_ip.csv` | `/data/bot_ip.csv` (ro) | Liste d'IPs de bots connus |
|
||
| `./reputation/data/user_files/bot_ja4.csv` | `/data/bot_ja4.csv` (ro) | Liste de JA4 de bots connus |
|
||
| `./reputation/data/user_files/asn_reputation.csv` | `/data/asn_reputation.csv` (ro) | Labels de réputation ASN |
|