docs: réécriture complète de la documentation des services en français

- bot-detector.md : architecture 11 modules, 77/65 features,
  ensemble triple voix (EIF+AE+XGBoost), browser 5 axes, HDBSCAN,
  toutes les variables d'environnement vérifiées depuis le code source
- dashboard.md : corrigé stack (Jinja2+htmx, pas React+Vite),
  14 pages + 35 API routes + health, dual-database, IPv4/IPv6
- python-ja4common.md : ajouté CLICKHOUSE_DB_PROCESSING/LOGS,
  schéma dual-database, note dashboard n'utilise pas ja4_common

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
toto
2026-04-09 22:04:58 +02:00
parent 8f5e771096
commit c96c41fb45
3 changed files with 1006 additions and 542 deletions

View File

@ -1,221 +1,549 @@
# Bot Detector
The bot-detector is a Python service that performs machine-learning anomaly detection on aggregated HTTP/TLS traffic features stored in ClickHouse. It runs on a continuous cycle (default: every 5 minutes), using Isolation Forest to identify suspicious traffic patterns, enriched with SHAP explainability, DBSCAN clustering, and Anubis bot-rule enrichment.
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.
## ML Algorithm
---
### Isolation Forest (Semi-Supervised)
## Architecture des modules
The core algorithm is **Isolation Forest** (Liu, Ting & Zhou, 2008) — an unsupervised anomaly detection algorithm that isolates anomalies by randomly partitioning feature space. Anomalies require fewer partitions to isolate than normal points.
The approach is **semi-supervised** because:
1. **Known bots** are identified a priori via reputation dictionaries (IP, JA4, ASN)
2. **Human baseline** is identified via ASN reputation labels (`asn_label = 'human'`)
3. The model trains **only on human-baseline traffic** (minimum 500 sessions required)
4. Unknown traffic is scored by deviation from the human profile
### Two-Model Architecture
| Model | Condition | Features | Data |
|-------|-----------|----------|------|
| **Complet** | `correlated = 1` | 35 | HTTP + TCP + TLS (full pipeline data) |
| **Applicatif** | `correlated = 0` | 31 | HTTP only (no TLS correlation available) |
### Threat Levels
| Score Range | Level | Interpretation |
|------------|-------|----------------|
| `< -0.30` | **CRITICAL** | Extremely anomalous behavior |
| `< -0.15` | **HIGH** | Strong anomaly signal |
| `< -0.05` | **MEDIUM** | Moderate anomaly |
| `≥ -0.05` | **LOW** | Slightly unusual |
## Feature List
### Common Features (31 — Applicatif model)
#### HTTP Behavior
| Feature | Description |
|---------|-------------|
| `hits` | Request count in the window |
| `hit_velocity` | Requests per second |
| `fuzzing_index` | Path/parameter diversity anomaly score |
| `post_ratio` | Fraction of POST requests |
| `port_exhaustion_ratio` | Fraction of distinct source ports / total |
| `orphan_ratio` | Requests without TLS correlation |
| `head_ratio` | Fraction of HEAD requests |
| `http10_ratio` | Fraction of HTTP/1.0 requests |
| `generic_accept_ratio` | Fraction of short Accept headers |
| `sec_fetch_absence_rate` | Fraction missing Sec-Fetch-Site |
| `missing_accept_enc_ratio` | Fraction missing Accept-Encoding |
| `http_scheme_ratio` | Fraction using HTTP (not HTTPS) |
#### Connection Management
| Feature | Description |
|---------|-------------|
| `max_keepalives` | Max requests on a single Keep-Alive connection |
| `tcp_shared_count` | TCP connections shared between sessions |
| `multiplexing_efficiency` | HTTP/2 multiplexing efficiency |
#### Browser Fingerprint
| Feature | Description |
|---------|-------------|
| `header_count` | HTTP headers sent |
| `has_accept_language` | Accept-Language header presence |
| `has_cookie` | Cookie header presence |
| `has_referer` | Referer header presence |
| `modern_browser_score` | Composite browser compliance score (0100) |
| `ua_ch_mismatch` | User-Agent vs Client Hints inconsistency |
| `ip_id_zero_ratio` | IP packets with ID=0 (headless/minimal stack) |
| `header_order_shared_count` | IPs sharing same header order |
| `header_order_confidence` | Normalized entropy of header order |
| `distinct_header_orders` | Distinct header orderings per IP |
| `is_fake_navigation` | Sec-Fetch-Mode=navigate with non-document dest |
#### Navigation Patterns
| Feature | Description |
|---------|-------------|
| `request_size_variance` | Variance of request sizes |
| `mss_mobile_mismatch` | TCP MSS vs mobile profile inconsistency |
| `asset_ratio` | Static asset request fraction |
| `direct_access_ratio` | Direct accesses (no referer) |
| `is_ua_rotating` | User-Agent rotation detected (flag) |
| `distinct_ja4_count` | Distinct JA4 fingerprints per IP |
| `anomalous_payload_ratio` | Anomalous payload size fraction |
#### Concentration & Rarity
| Feature | Description |
|---------|-------------|
| `src_port_density` | Source port entropy |
| `ja4_asn_concentration` | JA4 concentration within ASN |
| `ja4_country_concentration` | JA4 concentration per country |
| `is_rare_ja4` | Rare JA4 fingerprint (< 100 total hits) |
#### Temporal & Diversity
| Feature | Description |
|---------|-------------|
| `temporal_entropy` | Temporal distribution entropy |
| `path_diversity_ratio` | URL path diversity |
| `url_depth_variance` | URL depth variance |
| `ja3_diversity_ratio` | JA3 diversity ratio per IP |
### Additional TCP/TLS Features (Complet model only — 4 extra)
| Feature | Description |
|---------|-------------|
| `tcp_jitter_variance` | TCP inter-packet jitter variance |
| `alpn_http_mismatch` | ALPN vs actual HTTP protocol mismatch |
| `is_alpn_missing` | ALPN absent in ClientHello |
| `sni_host_mismatch` | TLS SNI vs HTTP Host mismatch |
### L4 Fingerprint Features (Complet model)
| Feature | Description |
|---------|-------------|
| `avg_ttl` | Average IP TTL (OS fingerprint) |
| `ttl_std` | TTL standard deviation |
| `no_window_scale_ratio` | Fraction without TCP window scale |
| `syn_timing_cv` | SYN timing coefficient of variation |
| `tls12_ratio` | Fraction of TLS 1.2 connections |
| `ip_df_variance` | IP Don't-Fragment flag variance |
## Detection Pipeline
Le service est découpé en **11 modules** organisés ainsi :
```
1. Read view_ai_features_1h (last 24h) → DataFrame
2. Read view_ip_recurrence → recurrence map
3. Clean columns (fillna, astype)
4. Split by correlated=1 / correlated=0
5. For each model (Complet, Applicatif):
a. A7: Validate features (exclude missing/constant)
b. Separate known bots → log as KNOWN_BOT
c. Filter human baseline (asn_label='human', min 500 sessions)
d. Load or train Isolation Forest model
e. A1: Check concept drift (KS test on features)
f. Score unknown traffic
g. A10: Normalize scores to [-1, 0]
h. A2: Compute adaptive threshold = min(percentile_5, ANOMALY_THRESHOLD)
i. A6: Apply recurrence weighting
j. Filter scores below threshold
k. A4: SHAP explainability (top 5 features)
l. A8: DBSCAN clustering (campaign detection)
6. Concatenate results, deduplicate by src_ip (keep lowest score)
7. A5: Deduplication with TTL (skip recently reported IPs)
8. Insert into ml_detected_anomalies + ml_all_scores
__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)
```
## Concept Drift Detection (A1)
| 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 |
Uses the **Kolmogorov-Smirnov test** to compare feature distributions between the current data and the training data. If the fraction of drifted features exceeds `DRIFT_THRESHOLD` (default: 0.30), the model is retrained.
---
## SHAP Explainability (A4)
## Configuration
When enabled (`ENABLE_SHAP=true`), computes SHAP values for each detected anomaly using `shap.TreeExplainer`. The top 5 contributing features are stored in the `reason` field.
Toute la configuration est lue via `os.getenv()` dans `config.py`. Aucun fichier YAML ni pydantic-settings.
## DBSCAN Clustering (A8)
### Connexion ClickHouse
When enabled (`ENABLE_CLUSTERING=true`), applies DBSCAN on anomaly feature vectors to group related anomalies into campaigns. Each anomaly gets a `campaign_id` (-1 = no cluster).
| 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 |
## Anubis Bot-Rule Enrichment
### Modèle et entraînement
The `view_ai_features_1h` view enriches each IP with Anubis bot detection using a priority cascade:
1. **UA + IP combined** (same `rule_id`) highest confidence
2. **UA only** (no IP requirement)
3. **IP only** (no UA requirement)
4. **ASN match**
5. **Country match**
| 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 |
## Environment Variables
### Autoencoder
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `CLICKHOUSE_HOST` | string | `clickhouse` | ClickHouse server hostname |
| `CLICKHOUSE_PORT` | int | `8123` | ClickHouse HTTP port |
| `CLICKHOUSE_DB` | string | `ja4_processing` | Database name |
| `CLICKHOUSE_USER` | string | `admin` | ClickHouse username |
| `CLICKHOUSE_PASSWORD` | string | `""` | ClickHouse password |
| `ISOLATION_CONTAMINATION` | float | `0.02` | Contamination parameter for Isolation Forest |
| `ANOMALY_THRESHOLD` | float | `-0.03` | Score threshold for anomaly detection |
| `ANOMALY_PERCENTILE` | int | `5` | Percentile for adaptive threshold (A2) |
| `CYCLE_INTERVAL_SEC` | int | `300` | Seconds between detection cycles |
| `MAX_CONSECUTIVE_FAILURES` | int | `3` | Max consecutive failures before exit |
| `BOT_DETECTOR_LOG` | string | `/var/log/bot_detector/decisions.jsonl` | Decision log file path |
| `LOG_BACKUP_COUNT` | int | `7` | Number of rotated log backups |
| `MODEL_DIR` | string | `/var/lib/bot_detector` | Model persistence directory |
| `RETRAIN_INTERVAL_HOURS` | int | `24` | Hours between model retraining |
| `MODEL_HISTORY_COUNT` | int | `10` | Number of model versions to keep |
| `DRIFT_THRESHOLD` | float | `0.30` | KS-test drift threshold (A1) |
| `ENABLE_MULTIWINDOW` | bool | `false` | Enable 24h multi-window analysis (A3) |
| `MULTIWINDOW_VIEW` | string | `view_ai_features_24h` | View for multi-window mode |
| `ENABLE_SHAP` | bool | `true` | Enable SHAP explainability (A4) |
| `DEDUP_TTL_MIN` | int | `60` | Deduplication TTL in minutes (A5) |
| `RECURRENCE_WEIGHT` | float | `0.005` | Recurrence score weighting factor (A6) |
| `MIN_VALID_FEATURE_RATIO` | float | `0.50` | Min valid feature ratio (A7) |
| `ENABLE_CLUSTERING` | bool | `true` | Enable DBSCAN clustering (A8) |
| `CLUSTERING_MIN_SAMPLES` | int | `3` | DBSCAN min samples per cluster |
| `HEALTH_PORT` | int | `8080` | Health check HTTP server port |
| 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 |
## Output Tables
### 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 (01)
└──────────────────────┘ │
× (1 AE_WEIGHT)
┌──────────────────────┐ │
│ TrafficAutoEncoder │ ├──→ combined_norm
│ (PyTorch) │──→ ae_norm (01)
└──────────────────────┘ × AE_WEIGHT
× (1 XGB_WEIGHT)
┌──────────────────────┐ │
│ XGBoost │ ├──→ anomaly_score
│ (supervisé, labels │──→ xgb_prob (01)
│ 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 1025 (×0.20), nb extensions 1025 (×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 (0100) |
| `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
Anomaly detections above the threat threshold. Engine: `ReplacingMergeTree(detected_at)`, ORDER BY `(src_ip)`, TTL 30 days.
Détections d'anomalies au-dessus du seuil de menace. Engine : `ReplacingMergeTree(detected_at)`, ORDER BY `(src_ip, model_name)`, TTL 7 jours.
Key columns: `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 all ML features.
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
All classifications (no threshold filter) for observability. Engine: `ReplacingMergeTree(detected_at)`, ORDER BY `(window_start, src_ip, ja4, host, model_name)`, TTL 3 days.
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.
## Decision Log Format
---
The `decisions.jsonl` file contains structured JSONL entries:
## 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}
@ -224,42 +552,52 @@ The `decisions.jsonl` file contains structured JSONL entries:
{"event": "CYCLE_END", "cycle_id": "20260309T143000", "anomalies": 15, "known_bots": 200, "duration_sec": 12.5}
```
Log rotation: 50 MB max size × `LOG_BACKUP_COUNT` backups (default 7).
Rotation des logs : 50 Mo max × `LOG_BACKUP_COUNT` sauvegardes (défaut : 7).
## Health Check Endpoint
---
- **URL**: `GET http://localhost:8080/`
- **Response**: `200 OK` with status JSON
- Runs in a separate thread
## Point de santé
## Model Persistence
- **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
| File | Description |
|------|-------------|
| `model_<name>_<version>.joblib` | Serialized Isolation Forest (joblib) |
| `model_<name>_<version>.meta.json` | Model metadata (features, thresholds, training stats) |
| `model_<name>.current` | Pointer to active model version |
| `training_history.jsonl` | Training history log |
---
Models are rotated: only the last `MODEL_HISTORY_COUNT` versions (default 10) are kept.
## Persistance des modèles
## Docker Deployment
| 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
# Build
# Construction de l'image
make build-bot-detector
# Run with docker-compose
# Exécution avec docker-compose
cd services/bot-detector
docker-compose up -d
# Tests
make test-bot-detector
```
### Volumes
| Host Path | Container Path | Description |
|-----------|---------------|-------------|
| `./bot_detector_logs` | `/var/log/bot_detector` | Decision logs (JSONL) |
| `./bot_detector_models` | `/var/lib/bot_detector` | Persisted ML models |
| `./reputation/data/user_files/bot_ip.csv` | `/data/bot_ip.csv` (ro) | Known bot IP list |
| `./reputation/data/user_files/bot_ja4.csv` | `/data/bot_ja4.csv` (ro) | Known bot JA4 list |
| `./reputation/data/user_files/asn_reputation.csv` | `/data/asn_reputation.csv` (ro) | ASN reputation labels |
| 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 |