Architecture: - ja4_logs: raw log ingestion (http_logs_raw, http_logs, mv_http_logs) - ja4_processing: analytics, aggregation, ML, dictionaries, audit Configuration (env vars): - CLICKHOUSE_DB_LOGS (default: ja4_logs) - CLICKHOUSE_DB_PROCESSING (default: ja4_processing) Changes: - SQL migrations (10 files): all mabase_prod refs → ja4_logs or ja4_processing with correct cross-database references (MVs, views, dicts) - deploy_schema.sh: substitutes DB names from env vars at deploy time - Python shared settings: added CLICKHOUSE_DB_LOGS + CLICKHOUSE_DB_PROCESSING - Dashboard routes (19 files): replaced ~80 hardcoded mabase_prod refs with settings.CLICKHOUSE_DB_LOGS / settings.CLICKHOUSE_DB_PROCESSING - Bot-detector: DB → CLICKHOUSE_DB_PROCESSING, fetch_rules.py configurable - Correlator: DSN example updated to ja4_logs - Docker-compose + .env files: new env vars with defaults - All documentation updated (14 markdown files) All tests pass: sentinel 10/10, correlator 67.1%, bot-detector 11, dashboard 20, ja4_common 18 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
711 lines
32 KiB
Markdown
711 lines
32 KiB
Markdown
# Bot Detector IA — Documentation Technique
|
||
|
||
> Version du code : v11 | Dernière mise à jour : 2026-03-17
|
||
|
||
---
|
||
|
||
## Table des matières
|
||
|
||
1. [Vue d'ensemble](#1-vue-densemble)
|
||
2. [Architecture système](#2-architecture-système)
|
||
3. [Pipeline de détection](#3-pipeline-de-détection)
|
||
4. [Modèles et features](#4-modèles-et-features)
|
||
5. [Approche semi-supervisée](#5-approche-semi-supervisée)
|
||
6. [Gestion des modèles](#6-gestion-des-modèles)
|
||
7. [Données d'entrée — vue ClickHouse](#7-données-dentrée--vue-clickhouse)
|
||
8. [Données de sortie](#8-données-de-sortie)
|
||
9. [Configuration](#9-configuration)
|
||
10. [Observabilité](#10-observabilité)
|
||
11. [Réputation et enrichissement](#11-réputation-et-enrichissement)
|
||
12. [Fondements scientifiques](#12-fondements-scientifiques)
|
||
13. [Améliorations implémentées (v11)](#13-améliorations-implémentées-v11)
|
||
14. [Migration de schéma ClickHouse](#14-migration-de-schéma-clickhouse)
|
||
|
||
---
|
||
|
||
## 1. Vue d'ensemble
|
||
|
||
Le **Bot Detector IA** est un service de détection d'activité suspecte et de bots sur un trafic HTTP. Il tourne en boucle continue (toutes les 5 minutes par défaut) et analyse des données agrégées issues de ClickHouse.
|
||
|
||
### Principe général
|
||
|
||
```
|
||
ClickHouse (view_ai_features_1h)
|
||
│
|
||
▼
|
||
┌───────────────────────┐
|
||
│ Séparation du trafic │
|
||
│ ├─ Bots connus │ → Étiquetés via réputation IP / JA4 / ASN
|
||
│ ├─ Trafic humain │ → Sert de baseline d'entraînement pour l'IF
|
||
│ └─ Trafic inconnu │ → Scoré par Isolation Forest
|
||
└───────────────────────┘
|
||
│
|
||
▼
|
||
┌───────────────────────┐
|
||
│ Isolation Forest │
|
||
│ (semi-supervisé) │
|
||
│ ├─ Modèle Complet │ TCP + TLS + HTTP (35 features, correlated=1)
|
||
│ └─ Modèle Applicatif │ HTTP seul (31 features, correlated=0)
|
||
└───────────────────────┘
|
||
│
|
||
▼
|
||
ClickHouse (ml_detected_anomalies)
|
||
```
|
||
|
||
### Caractéristiques clés
|
||
|
||
| Propriété | Valeur |
|
||
|-----------|--------|
|
||
| Algorithme | Isolation Forest (sklearn) |
|
||
| Supervision | Semi-supervisée (baseline humain + réputation) |
|
||
| Fenêtre d'analyse | 1 heure glissante (optionnel : 24h avec `ENABLE_MULTIWINDOW`) |
|
||
| Cycle d'exécution | 300 s (configurable) |
|
||
| Re-entraînement | Toutes les 1 h (configurable) + retrain forcé sur dérive conceptuelle |
|
||
| Contamination | 2 % (fraction d'anomalies attendues dans la baseline) |
|
||
| Seuil d'anomalie | Adaptatif : min(percentile_5, -0.03) |
|
||
|
||
---
|
||
|
||
## 2. Architecture système
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Docker Compose │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||
│ │ bot_detector_ai │ │
|
||
│ │ │ │
|
||
│ │ ┌────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │
|
||
│ │ │ Health │ │ Main Loop │ │ ClickHouse │ │ │
|
||
│ │ │ :8080 │ │ (300s cycle)│ │ Client │ │ │
|
||
│ │ │ (thread) │ │ │ │ (reconnect) │ │ │
|
||
│ │ └────────────┘ └──────────────┘ └─────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ Volumes: │ │
|
||
│ │ ├─ ./bot_detector_logs → /var/log/bot_detector │ │
|
||
│ │ ├─ ./bot_detector_models → /var/lib/bot_detector │ │
|
||
│ │ ├─ ./reputation/data/user_files/bot_ip.csv (ro) │ │
|
||
│ │ ├─ ./reputation/data/user_files/bot_ja4.csv (ro) │ │
|
||
│ │ └─ ./reputation/data/user_files/asn_reputation.csv (ro) │ │
|
||
│ └──────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└────────────────────────────┬────────────────────────────────────┘
|
||
│ HTTP :8123
|
||
▼
|
||
ClickHouse externe
|
||
(test-sdv-anubis.sdv.fr)
|
||
```
|
||
|
||
### Fichiers et répertoires
|
||
|
||
| Chemin | Rôle |
|
||
|--------|------|
|
||
| `bot_detector/bot_detector.py` | Code source principal |
|
||
| `bot_detector/requirements.txt` | Dépendances Python |
|
||
| `bot_detector/Dockerfile` | Image Python 3.11-slim |
|
||
| `docker-compose.yml` | Orchestration Docker |
|
||
| `.env` | Variables d'environnement (non commité) |
|
||
| `bot_detector_logs/decisions.jsonl` | Journal JSONL structuré (rotation 50 MB × 7) |
|
||
| `bot_detector_models/model_<name>_<version>.joblib` | Modèle sérialisé |
|
||
| `bot_detector_models/model_<name>_<version>.meta.json` | Métadonnées du modèle |
|
||
| `bot_detector_models/model_<name>.current` | Pointeur vers la version active |
|
||
| `bot_detector_models/training_history.jsonl` | Historique des entraînements |
|
||
| `reputation/bot_ip.csv` | ~288 k entrées IP/CIDR de bots connus |
|
||
| `reputation/bot_ja4.csv` | Empreintes JA4 de bots |
|
||
| `reputation/asn_reputation.csv` | Labels ASN (human / bot) |
|
||
|
||
---
|
||
|
||
## 3. Pipeline de détection
|
||
|
||
### 3.1 Cycle principal (`fetch_and_analyze`)
|
||
|
||
```
|
||
1. Génération d'un cycle_id (timestamp)
|
||
2. Requête view_ai_features_1h → DataFrame df
|
||
3. Requête view_ip_recurrence → recurrence_map {src_ip: count}
|
||
4. Nettoyage des colonnes (fillna, astype)
|
||
5. Log CYCLE_START (total, human, known_bot, correlated)
|
||
6. Séparation df → correlated=1 / correlated=0
|
||
7. Appel run_semi_supervised_logic() × 2 (modèle Complet + Applicatif)
|
||
8. Concaténation, déduplication par src_ip (score le plus bas)
|
||
9. Insertion dans ml_detected_anomalies
|
||
10. Log CYCLE_END
|
||
11. Attente CYCLE_INTERVAL secondes
|
||
```
|
||
|
||
### 3.2 Logique semi-supervisée (`run_semi_supervised_logic`)
|
||
|
||
```
|
||
df (trafic de la fenêtre 1h)
|
||
│
|
||
├─ A7 → validate_features() : exclusion des features manquantes ou constantes
|
||
│
|
||
├─ bot_name != '' → known_bots → KNOWN_BOT (log + insertion)
|
||
│
|
||
└─ bot_name == '' → unknown_traffic
|
||
│
|
||
├─ asn_label == 'human' → human_baseline
|
||
│ (min. 500 sessions requis)
|
||
│ └──► load_or_train_model()
|
||
│ ├─ A1 : drift check (z-score / features)
|
||
│ └─ Si drift ≥ DRIFT_THRESHOLD : retrain forcé
|
||
│
|
||
└─ reste du trafic inconnu
|
||
│
|
||
▼
|
||
IsolationForest.decision_function() → raw_scores
|
||
│
|
||
A10 : normalize_scores() → anomaly_score [-1, 0]
|
||
│
|
||
A2 : effective_threshold = min(percentile_5, ANOMALY_THRESHOLD)
|
||
│
|
||
A6 : raw_score -= log1p(recurrence) × RECURRENCE_WEIGHT
|
||
│
|
||
raw_score < effective_threshold ?
|
||
│
|
||
YES → A4 : SHAP top-5 features → reason
|
||
A8 : DBSCAN clustering → campaign_id
|
||
ANOMALY (log + insertion)
|
||
│
|
||
NO → ignoré
|
||
```
|
||
|
||
### 3.3 Niveaux de menace
|
||
|
||
| Score | Niveau | Interprétation |
|
||
|-------|--------|----------------|
|
||
| `< -0.30` | **CRITICAL** | Comportement extrêmement anormal |
|
||
| `< -0.15` | **HIGH** | Fort signal d'anomalie |
|
||
| `< -0.05` | **MEDIUM** | Anomalie modérée |
|
||
| `≥ -0.05` | **LOW** | Légèrement inhabituel |
|
||
|
||
> Le seuil d'insertion (`ANOMALY_THRESHOLD = -0.03`) est plus permissif que LOW. Toutes les IP dont le score passe sous ce seuil sont insérées, quelle que soit leur catégorie de niveau.
|
||
|
||
---
|
||
|
||
## 4. Modèles et features
|
||
|
||
### 4.1 Architecture à deux niveaux
|
||
|
||
| Modèle | Condition | Nb features | Données utilisées |
|
||
|--------|-----------|-------------|-------------------|
|
||
| **Complet** | `correlated = 1` | 35 | HTTP + TCP + TLS |
|
||
| **Applicatif** | `correlated = 0` | 31 | HTTP uniquement |
|
||
|
||
La corrélation (`correlated`) indique si les logs HTTP ont pu être enrichis avec les données TCP/TLS de la même connexion. En l'absence de corrélation (capture incomplète ou trafic chiffré sans inspection), seul le modèle Applicatif est utilisé.
|
||
|
||
### 4.2 Features communes (31 — modèle Applicatif)
|
||
|
||
#### Comportement HTTP de base
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `hits` | Nombre de requêtes sur la fenêtre |
|
||
| `hit_velocity` | Requêtes par seconde |
|
||
| `fuzzing_index` | Score de diversité anormale des chemins/paramètres |
|
||
| `post_ratio` | Fraction de requêtes POST |
|
||
| `port_exhaustion_ratio` | Fraction de ports sources différents / total ports |
|
||
| `orphan_ratio` | Requêtes sans réponse associée |
|
||
|
||
#### Gestion des connexions
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `max_keepalives` | Nb max de requêtes sur une même connexion keep-alive |
|
||
| `tcp_shared_count` | Connexions TCP partagées entre plusieurs sessions HTTP |
|
||
|
||
#### Empreinte navigateur (Browser Fingerprint)
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `header_count` | Nombre d'en-têtes HTTP envoyés |
|
||
| `has_accept_language` | Présence de Accept-Language |
|
||
| `has_cookie` | Présence de Cookie |
|
||
| `has_referer` | Présence de Referer |
|
||
| `modern_browser_score` | Score composite de conformité navigateur moderne |
|
||
| `ua_ch_mismatch` | Incohérence entre User-Agent et Client Hints |
|
||
| `ip_id_zero_ratio` | Ratio de paquets IP avec ID=0 (headless / stack minimale) |
|
||
| `header_order_shared_count` | Partage d'un même ordre d'en-têtes entre IPs |
|
||
| `header_order_confidence` | Confiance dans l'ordre d'en-têtes (entropie normalisée) |
|
||
| `distinct_header_orders` | Nombre d'ordres d'en-têtes distincts observés |
|
||
|
||
#### Patterns de navigation
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `request_size_variance` | Variance de la taille des requêtes |
|
||
| `multiplexing_efficiency` | Efficacité du multiplexage HTTP/2 |
|
||
| `mss_mobile_mismatch` | Incohérence MSS TCP / profil mobile annoncé |
|
||
| `asset_ratio` | Fraction de requêtes vers des ressources statiques |
|
||
| `direct_access_ratio` | Fraction d'accès directs (sans referer) |
|
||
| `is_ua_rotating` | Rotation de User-Agent détectée (flag 0/1) |
|
||
| `distinct_ja4_count` | Nombre de fingerprints JA4 distincts par IP |
|
||
|
||
#### Concentration et rareté
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `src_port_density` | Densité des ports sources (entropy) |
|
||
| `ja4_asn_concentration` | Concentration d'un même JA4 dans un ASN |
|
||
| `ja4_country_concentration` | Concentration d'un même JA4 par pays |
|
||
| `is_rare_ja4` | JA4 peu commun dans la population (flag 0/1) |
|
||
|
||
#### Dimensions temporelles et de diversité (académiques)
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `temporal_entropy` | Entropie de la distribution temporelle des requêtes |
|
||
| `path_diversity_ratio` | Diversité des chemins URL accédés |
|
||
| `url_depth_variance` | Variance de la profondeur des URL |
|
||
| `anomalous_payload_ratio` | Fraction de payloads avec patterns anormaux |
|
||
|
||
### 4.3 Features additionnelles TCP/TLS (modèle Complet uniquement)
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| `tcp_jitter_variance` | Variance de la gigue inter-paquets TCP |
|
||
| `alpn_http_mismatch` | Incohérence entre ALPN négocié et protocole HTTP effectif |
|
||
| `is_alpn_missing` | ALPN absent dans le TLS ClientHello |
|
||
| `sni_host_mismatch` | Incohérence entre SNI TLS et Host HTTP |
|
||
|
||
---
|
||
|
||
## 5. Approche semi-supervisée
|
||
|
||
### 5.1 Fondement théorique
|
||
|
||
L'**Isolation Forest** (Liu, Ting & Zhou, 2008) est un algorithme d'apprentissage non supervisé conçu pour la détection d'anomalies. Son principe : les anomalies, étant rares et différentes, sont **isolées en moins de partitions** dans un arbre de décision aléatoire que les points normaux.
|
||
|
||
Le score de décision (`decision_function`) est normalisé entre -1 (très anormal) et +1 (très normal). Le paramètre `contamination` fixe la fraction de points considérés comme anomalies dans l'ensemble d'entraînement.
|
||
|
||
### 5.2 Dimension semi-supervisée
|
||
|
||
L'approche est **semi-supervisée** car :
|
||
|
||
1. **Étiquetage partiel** : Les bots connus (via réputation IP/JA4) et les humains (via réputation ASN) sont identifiés *a priori*.
|
||
2. **Entraînement sur la classe normale uniquement** : L'IF est entraîné **exclusivement sur la baseline humaine** (`asn_label = 'human'`, `bot_name = ''`). Il apprend ainsi le profil du trafic légitime.
|
||
3. **Détection par déviation** : Tout trafic inconnu qui s'éloigne du profil humain est scoré négativement.
|
||
|
||
Cette approche suit le paradigme **One-Class Classification** (Tax & Duin, 2004) appliqué à la détection de bots, proche des travaux de Kruegel & Vigna (2003) sur la détection d'anomalies réseau.
|
||
|
||
### 5.3 Qualité de la baseline humaine
|
||
|
||
Le minimum de 500 sessions humaines est une garde-fou empirique. En dessous de ce seuil, l'IF ne dispose pas de suffisamment d'exemples pour définir un profil normal robuste, augmentant le risque de faux positifs.
|
||
|
||
En pratique, les cycles observés montrent entre **1 264** et **1 725** sessions humaines par fenêtre d'une heure.
|
||
|
||
---
|
||
|
||
## 6. Gestion des modèles
|
||
|
||
### 6.1 Cycle de vie d'un modèle
|
||
|
||
```
|
||
Démarrage cycle
|
||
│
|
||
▼
|
||
Existe un .current ? ──NON──► Entraîner nouveau modèle
|
||
│
|
||
OUI
|
||
│
|
||
▼
|
||
Âge < RETRAIN_INTERVAL_H ?
|
||
│ │
|
||
OUI NON
|
||
│ │
|
||
▼ └──► Entraîner nouveau modèle
|
||
A1 : Drift check (MODEL_TRAINED)
|
||
(z-score vs baseline_stats)
|
||
│
|
||
Drift ≥ DRIFT_THRESHOLD ?
|
||
│ │
|
||
NON OUI
|
||
│ │
|
||
Charger modèle Entraîner nouveau modèle
|
||
(MODEL_LOADED) (DRIFT_DETECTED + MODEL_TRAINED)
|
||
```
|
||
|
||
### 6.2 Versioning des modèles
|
||
|
||
Chaque modèle est identifié par un `version_id` au format `YYYYMMDD_HHMMSS`. Les fichiers associés sont :
|
||
|
||
- `model_{name}_{version_id}.joblib` — modèle sérialisé (joblib/pickle)
|
||
- `model_{name}_{version_id}.meta.json` — métadonnées (features, contamination, nb samples, etc.)
|
||
- `model_{name}.current` — pointeur atomique vers la version active
|
||
|
||
L'historique est limité à `MODEL_HISTORY_COUNT` versions (72 en production = 3 jours à 1 h de retrain).
|
||
|
||
Le fichier `.meta.json` contient maintenant un champ `baseline_stats` avec les statistiques de distribution (mean, std, p25, p75) de chaque feature, utilisées pour la détection de dérive (A1).
|
||
|
||
### 6.3 Paramètres Isolation Forest
|
||
|
||
```python
|
||
IsolationForest(
|
||
n_estimators=300, # Nombre d'arbres (compromis précision/temps)
|
||
contamination=0.02, # 2% d'anomalies estimées dans la baseline
|
||
random_state=42, # Reproductibilité
|
||
n_jobs=-1 # Parallélisation sur tous les cores
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Données d'entrée — vue ClickHouse
|
||
|
||
### 7.1 Vue principale : `view_ai_features_1h`
|
||
|
||
Agrégation sur 1 heure glissante, une ligne par `src_ip`. Colonnes clés :
|
||
|
||
| Colonne | Type | Source |
|
||
|---------|------|--------|
|
||
| `src_ip` | String | TCP/IP |
|
||
| `ja4` | String | TLS fingerprint (JA4+) |
|
||
| `host` | String | HTTP Host header |
|
||
| `bot_name` | String | Réputation IP/JA4 (vide si inconnu) |
|
||
| `asn_number` | String | GeoIP/ASN lookup |
|
||
| `asn_org` | String | Organisation ASN |
|
||
| `asn_domain` | String | Domaine ASN |
|
||
| `country_code` | String | Pays source |
|
||
| `asn_label` | String | `human` / `bot` / `unknown` |
|
||
| `correlated` | Int | 1 si TCP/TLS disponible, 0 sinon |
|
||
| `hits` | Float | Nb requêtes |
|
||
| `hit_velocity` | Float | Req/s |
|
||
| *…(26+ features)* | Float | Voir section 4.2 |
|
||
|
||
### 7.2 Vue de récurrence : `view_ip_recurrence`
|
||
|
||
```sql
|
||
SELECT src_ip, recurrence FROM {DB}.view_ip_recurrence
|
||
```
|
||
|
||
Donne le nombre de fois qu'une IP a déjà été détectée comme menace dans l'historique. Enrichit le champ `recurrence` dans la sortie.
|
||
|
||
---
|
||
|
||
## 8. Données de sortie
|
||
|
||
### 8.1 Table ClickHouse : `ml_detected_anomalies`
|
||
|
||
Toutes les anomalies et bots connus détectés sont insérés dans cette table. Colonnes notables :
|
||
|
||
| Colonne | Description |
|
||
|---------|-------------|
|
||
| `detected_at` | Timestamp de détection |
|
||
| `src_ip` | IP source |
|
||
| `ja4` | Fingerprint TLS/JA4 (`HTTP_CLEAR_TEXT` si absent) |
|
||
| `host` | Vhost ciblé |
|
||
| `bot_name` | Nom du bot (vide si anomalie IF) |
|
||
| `anomaly_score` | Score IF (0.0 pour bots connus) |
|
||
| `threat_level` | `CRITICAL` / `HIGH` / `MEDIUM` / `LOW` / `KNOWN_BOT` |
|
||
| `model_name` | `Complet` ou `Applicatif` |
|
||
| `recurrence` | Nb d'apparitions historiques + 1 |
|
||
| `reason` | Description textuelle de l'anomalie |
|
||
| `is_headless` | Dérivé de `is_fake_navigation` |
|
||
| *…(toutes les features)* | Pour analyse post-mortem |
|
||
|
||
### 8.2 Journal JSONL : `decisions.jsonl`
|
||
|
||
Événements structurés en JSON Lines, rotatifs (50 MB × 7 fichiers).
|
||
|
||
| Événement | Déclencheur |
|
||
|-----------|-------------|
|
||
| `SERVICE_START` | Démarrage du conteneur |
|
||
| `SERVICE_STOP` | Arrêt propre (SIGTERM/SIGINT) |
|
||
| `CYCLE_START` | Début d'un cycle d'analyse |
|
||
| `CYCLE_END` | Fin du cycle (résumé inserés) |
|
||
| `MODEL_LOADED` | Réutilisation d'un modèle existant |
|
||
| `MODEL_TRAINED` | Nouvel entraînement |
|
||
| `KNOWN_BOT` | Bot connu identifié |
|
||
| `ANOMALY` | Anomalie IF détectée |
|
||
| `SKIPPED_LOW_DATA` | Cycle ignoré (baseline < 500) |
|
||
| `CONSECUTIVE_FAILURES` | Erreur ClickHouse répétée |
|
||
|
||
---
|
||
|
||
## 9. Configuration
|
||
|
||
Toutes les valeurs sont passées via variables d'environnement (fichier `.env`).
|
||
|
||
| Variable | Défaut | Description |
|
||
|----------|--------|-------------|
|
||
| `CLICKHOUSE_HOST` | `clickhouse` | Hôte ClickHouse |
|
||
| `CLICKHOUSE_DB` | `ja4_processing` | Base de données |
|
||
| `CLICKHOUSE_USER` | `default` | Utilisateur |
|
||
| `CLICKHOUSE_PASSWORD` | *(vide)* | Mot de passe |
|
||
| `ISOLATION_CONTAMINATION` | `0.001` | Fraction d'anomalies attendues (0 < x < 0.5) |
|
||
| `ANOMALY_THRESHOLD` | `-0.05` | Seuil statique de score pour insertion |
|
||
| `CYCLE_INTERVAL_SEC` | `300` | Délai entre cycles (secondes) |
|
||
| `MAX_CONSECUTIVE_FAILURES` | `3` | Échecs avant passage en DEGRADED |
|
||
| `BOT_DETECTOR_LOG` | `/var/log/bot_detector/decisions.jsonl` | Fichier de log |
|
||
| `LOG_BACKUP_COUNT` | `7` | Nb de rotations conservées |
|
||
| `MODEL_DIR` | `/var/lib/bot_detector` | Répertoire des modèles |
|
||
| `RETRAIN_INTERVAL_HOURS` | `24` | Fréquence de re-entraînement |
|
||
| `MODEL_HISTORY_COUNT` | `10` | Nb de versions de modèles conservées |
|
||
| `HEALTH_PORT` | `8080` | Port du health check HTTP |
|
||
| **A1** `DRIFT_THRESHOLD` | `0.30` | Fraction de features déroutantes déclenchant un retrain forcé |
|
||
| **A2** `ANOMALY_PERCENTILE` | `5` | Percentile pour le seuil adaptatif (0–20) |
|
||
| **A3** `ENABLE_MULTIWINDOW` | `false` | Active l'analyse sur fenêtre 24h |
|
||
| **A3** `MULTIWINDOW_VIEW` | `view_ai_features_24h` | Nom de la vue 24h dans ClickHouse |
|
||
| **A4** `ENABLE_SHAP` | `true` | Active le calcul SHAP (désactivé si shap non installé) |
|
||
| **A5** `DEDUP_TTL_MIN` | `60` | TTL de déduplication inter-cycles (0 = désactivé) |
|
||
| **A6** `RECURRENCE_WEIGHT` | `0.005` | Pénalité de score par log(récurrence) |
|
||
| **A7** `MIN_VALID_FEATURE_RATIO` | `0.50` | Ratio minimum de features valides pour procéder |
|
||
| **A8** `ENABLE_CLUSTERING` | `true` | Active le clustering DBSCAN des anomalies |
|
||
| **A8** `CLUSTERING_MIN_SAMPLES` | `3` | Taille minimale d'un cluster DBSCAN |
|
||
|
||
---
|
||
|
||
## 10. Observabilité
|
||
|
||
### 10.1 Health check
|
||
|
||
```bash
|
||
GET http://localhost:8080/
|
||
# → 200 OK service opérationnel
|
||
# → 503 DEGRADED ≥ MAX_CONSECUTIVE_FAILURES échecs ClickHouse consécutifs
|
||
```
|
||
|
||
### 10.2 Logs opérationnels
|
||
|
||
Les logs console suivent le format `[YYYY-MM-DD HH:MM:SS] message`. Le fichier JSONL permet des analyses post-mortem avec des outils comme `jq` :
|
||
|
||
```bash
|
||
# Voir les dernières anomalies CRITICAL
|
||
jq 'select(.event=="ANOMALY" and .threat_level=="CRITICAL")' decisions.jsonl
|
||
|
||
# Voir les top features SHAP pour les anomalies HIGH
|
||
jq 'select(.event=="ANOMALY" and .threat_level=="HIGH") | .reason' decisions.jsonl
|
||
|
||
# Détecter les dérives de distribution
|
||
jq 'select(.event=="DRIFT_DETECTED")' decisions.jsonl
|
||
|
||
# Voir les campagnes coordonnées (campaign_id >= 0)
|
||
jq 'select(.event=="ANOMALY" and .campaign_id >= 0) | {src_ip, campaign_id, threat_level}' decisions.jsonl
|
||
|
||
# Compter les bots connus par nom
|
||
jq -r 'select(.event=="KNOWN_BOT") | .bot_name' decisions.jsonl | sort | uniq -c | sort -rn
|
||
|
||
# Résumé des cycles
|
||
jq 'select(.event=="CYCLE_END")' decisions.jsonl
|
||
```
|
||
|
||
| Événement | Déclencheur |
|
||
|-----------|-------------|
|
||
| `SERVICE_START` | Démarrage du conteneur |
|
||
| `SERVICE_STOP` | Arrêt propre (SIGTERM/SIGINT) |
|
||
| `CYCLE_START` | Début d'un cycle d'analyse |
|
||
| `CYCLE_END` | Fin du cycle (résumé insertés + dedup_ttl_min) |
|
||
| `MODEL_LOADED` | Réutilisation d'un modèle existant (+ drift_score) |
|
||
| `MODEL_TRAINED` | Nouvel entraînement |
|
||
| `DRIFT_DETECTED` | Dérive conceptuelle détectée → retrain forcé |
|
||
| `FEATURE_WARNING` | Features manquantes / constantes / agrégats globaux détectés (loggué uniquement si la situation change) |
|
||
| `SKIPPED_INVALID_FEATURES` | Cycle ignoré (trop peu de features valides) |
|
||
| `KNOWN_BOT` | Bot connu identifié |
|
||
| `ANOMALY` | Anomalie IF détectée (+ effective_threshold, campaign_id, raw_anomaly_score) |
|
||
| `SKIPPED_LOW_DATA` | Cycle ignoré (baseline < 500) |
|
||
| `CONSECUTIVE_FAILURES` | Erreur ClickHouse répétée |
|
||
|
||
### 10.3 Avertissements sur les features (A7)
|
||
|
||
Les avertissements de features ne sont affichés en console **qu'une seule fois** (à la première détection ou lors d'un changement). Les cycles suivants avec la même situation ne génèrent pas de bruit. L'événement `FEATURE_WARNING` reste dans le JSONL pour traçabilité.
|
||
|
||
| Catégorie | Message console | Cause typique |
|
||
|-----------|-----------------|---------------|
|
||
| `zero` | `Features à 0 (pipeline non-alimenté)` | Table source vide / LEFT JOIN sans match |
|
||
| `unique_nonzero` | `Features non-discriminantes (agrégat global)` | `PARTITION BY` sur valeur NULL → partition unique |
|
||
| `missing` | `Features absentes du schéma` | Colonne manquante dans la vue ClickHouse |
|
||
|
||
Voir [`CLICKHOUSE_FEATURES_DIAGNOSTIC.md`](CLICKHOUSE_FEATURES_DIAGNOSTIC.md) pour le détail des corrections ClickHouse nécessaires.
|
||
|
||
### 11.1 Sources de réputation
|
||
|
||
| Fichier | Format | Contenu |
|
||
|---------|--------|---------|
|
||
| `bot_ip.csv` | `ip_cidr,bot_name` | ~288 k IP/CIDR de bots référencés |
|
||
| `bot_ja4.csv` | `ja4,bot_name` | Fingerprints JA4 de bots |
|
||
| `asn_reputation.csv` | `asn_number,label` | Labels ASN (human/bot) |
|
||
|
||
Ces fichiers sont montés en lecture seule dans le conteneur. Ils sont écrits par ClickHouse (FILE engine) et partagés via volume Docker.
|
||
|
||
### 11.2 Hiérarchie de classification
|
||
|
||
```
|
||
1. bot_name != '' (depuis view_ai_features_1h)
|
||
→ KNOWN_BOT : bot identifié par réputation IP ou JA4
|
||
|
||
2. asn_label == 'human' (depuis view_ai_features_1h)
|
||
→ Utilisé pour la baseline d'entraînement de l'IF
|
||
|
||
3. Trafic restant
|
||
→ Scoré par Isolation Forest
|
||
→ Anomalie si score < ANOMALY_THRESHOLD
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Fondements scientifiques
|
||
|
||
### 12.1 Isolation Forest (Liu et al., 2008)
|
||
|
||
L'algorithme repose sur la propriété que les anomalies sont **isolées plus rapidement** dans des arbres de partitionnement aléatoire. La longueur moyenne du chemin d'isolation est normalisée pour produire un score entre 0 et 1 (transposé ici en -1 à +1 par `decision_function`).
|
||
|
||
**Propriétés clés :**
|
||
- Complexité O(n log n) pour l'entraînement
|
||
- Robuste aux données de haute dimensionnalité (31–35 features ici)
|
||
- Pas d'hypothèse sur la distribution des données
|
||
- Efficace sur de grands volumes (n_estimators=300, n_jobs=-1)
|
||
|
||
### 12.2 JA4+ Fingerprinting (FoxIO, 2023)
|
||
|
||
JA4 est la 4e génération de fingerprints TLS/QUIC/HTTP, successeur de JA3. Il capture les caractéristiques du ClientHello TLS (versions, ciphers, extensions) en une empreinte compacte permettant d'identifier des familles de clients (navigateurs, bots, outils). L'utilisation de `is_rare_ja4`, `distinct_ja4_count` et `ja4_asn_concentration` exploite cette propriété.
|
||
|
||
### 12.3 One-Class Classification appliquée aux bots
|
||
|
||
L'approche s'inscrit dans la lignée des travaux sur la détection de bots web :
|
||
- **Stevanovic et al. (2013)** : détection de bots par analyse comportementale de flux HTTP
|
||
- **Kruegel & Vigna (2003)** : détection d'anomalies réseau par profils normaux
|
||
- **Barford & Yegneswaran (2007)** : classification comportementale des botnets
|
||
|
||
La combinaison de features HTTP comportementales (velocity, fuzzing, post_ratio), de features d'empreinte (JA4, headers), et de features TCP/TLS (jitter, ALPN, SNI) reproduit l'approche multi-couche recommandée par la littérature récente.
|
||
|
||
### 12.4 Entropie temporelle comme signal d'anomalie
|
||
|
||
Le feature `temporal_entropy` mesure l'entropie de Shannon sur la distribution temporelle des requêtes dans la fenêtre. Un bot avec un timing régulier (scripted polling) produit une entropie faible, tandis qu'un humain naviguant naturellement produit une distribution plus aléatoire. Ce signal est utilisé dans les travaux de **Wang et al. (2014)** sur la détection de crawlers web.
|
||
|
||
---
|
||
|
||
## 13. Améliorations implémentées (v11)
|
||
|
||
### A1 — Détection de dérive conceptuelle
|
||
|
||
**Fonctionnement** : À chaque cycle, avant de décider de charger ou de réentraîner le modèle, on compare la distribution courante de la baseline humaine avec celle sauvegardée lors du dernier entraînement. Pour chaque feature, un z-score est calculé :
|
||
|
||
```
|
||
z = |mean_current - mean_trained| / std_trained
|
||
```
|
||
|
||
Si la fraction de features avec `z > 2.0` dépasse `DRIFT_THRESHOLD` (30% par défaut), un re-entraînement est forcé et l'événement `DRIFT_DETECTED` est loggué.
|
||
|
||
**Métadonnées sauvegardées** : `baseline_stats` dans le `.meta.json` contient `{mean, std, p25, p75}` par feature.
|
||
|
||
**Références** : Gama et al. (2014) — *A Survey on Concept Drift Adaptation*
|
||
|
||
---
|
||
|
||
### A2 — Seuil adaptatif par percentile
|
||
|
||
**Fonctionnement** :
|
||
|
||
```python
|
||
effective_threshold = min(np.percentile(raw_scores[raw_scores < 0], ANOMALY_PERCENTILE),
|
||
ANOMALY_THRESHOLD)
|
||
```
|
||
|
||
Le seuil effectif est le minimum entre le `ANOMALY_PERCENTILE`-ème percentile des scores négatifs et le seuil statique. Cela garantit que le seuil ne peut pas remonter au-dessus du seuil configuré, mais peut s'adapter vers le bas selon la distribution courante.
|
||
|
||
Le seuil utilisé est loggué dans chaque événement `ANOMALY`.
|
||
|
||
---
|
||
|
||
### A3 — Analyse multi-fenêtres (optionnelle)
|
||
|
||
**Activation** : `ENABLE_MULTIWINDOW=true` + une vue `view_ai_features_24h` dans ClickHouse.
|
||
|
||
**Fonctionnement** : Deux paires de modèles supplémentaires (`Complet_24h`, `Applicatif_24h`) tournent sur la fenêtre de 24h. Les anomalies des deux fenêtres sont fusionnées via une logique OR : une IP est flaggée si elle est anormale dans au moins une fenêtre. En cas de doublon, le score le plus bas (le plus anormal) est conservé.
|
||
|
||
**Utilité** : Détection des bots low-and-slow invisibles sur 1h mais clairement anormaux sur 24h.
|
||
|
||
---
|
||
|
||
### A4 — Explainabilité par SHAP
|
||
|
||
**Fonctionnement** : Pour chaque anomalie détectée, `shap.TreeExplainer` calcule la contribution de chaque feature au score d'anomalie. Les 5 features les plus négatives (les plus responsables de l'anomalie) sont incluses dans le champ `reason` :
|
||
|
||
```
|
||
[Complet] Score: -0.112 | SHAP: is_alpn_missing(-1.081) | tcp_jitter_variance(-1.073) |
|
||
ja4_asn_concentration(-1.062) | temporal_entropy(-0.887) |
|
||
direct_access_ratio(-0.886) | Threat: MEDIUM
|
||
```
|
||
|
||
**Désactivation** : `ENABLE_SHAP=false` ou si le package `shap` n'est pas installé.
|
||
|
||
**Références** : Lundberg & Lee (2017) — *A Unified Approach to Interpreting Model Predictions*
|
||
|
||
---
|
||
|
||
### A5 — Déduplication inter-cycles avec TTL
|
||
|
||
**Fonctionnement** : Avant chaque insertion, la table `ml_detected_anomalies` est interrogée pour identifier les IPs déjà insérées dans les `DEDUP_TTL_MIN` dernières minutes. Une IP est réinsérée uniquement si son score brut s'est dégradé d'au moins 0.05 points.
|
||
|
||
**Désactivation** : `DEDUP_TTL_MIN=0`
|
||
|
||
---
|
||
|
||
### A6 — Pondération du score par récurrence
|
||
|
||
**Fonctionnement** :
|
||
|
||
```python
|
||
raw_score_adjusted = raw_score - log1p(recurrence) × RECURRENCE_WEIGHT
|
||
```
|
||
|
||
Une IP détectée 10 fois reçoit une pénalité de `log(11) × 0.005 ≈ 0.012` sur son score brut, ce qui la rapproche du seuil de détection. Ce mécanisme simule un prior bayésien : les IPs récidivistes sont plus probablement malveillantes.
|
||
|
||
---
|
||
|
||
### A7 — Validation de complétude des features
|
||
|
||
**Fonctionnement** : Avant entraînement et scoring, `validate_features()` détecte :
|
||
- Les features absentes de la vue ClickHouse
|
||
- Les features constantes (std = 0, donc non discriminantes)
|
||
|
||
Les features invalides sont exclues du modèle. Si la fraction de features valides est inférieure à `MIN_VALID_FEATURE_RATIO` (50%), le cycle est ignoré.
|
||
|
||
**Bénéfice** : Les features constantes (souvent dues à des colonnes non encore implémentées dans la vue) ne biaisent plus le modèle.
|
||
|
||
---
|
||
|
||
### A8 — Clustering comportemental (DBSCAN)
|
||
|
||
**Fonctionnement** : Après détection, DBSCAN est appliqué sur les features normalisées des anomalies :
|
||
|
||
```python
|
||
X_scaled = StandardScaler().fit_transform(anomalies[valid_features])
|
||
labels = DBSCAN(eps=0.5, min_samples=CLUSTERING_MIN_SAMPLES).fit_predict(X_scaled)
|
||
```
|
||
|
||
- `campaign_id = -1` : IP isolée (comportement unique)
|
||
- `campaign_id >= 0` : membre d'une campagne coordonnée
|
||
|
||
Le `campaign_id` est loggué dans les événements `ANOMALY` (JSONL). Il n'est pas encore dans le schéma ClickHouse (voir §14).
|
||
|
||
**Références** : Ester et al. (1996) — *A Density-Based Algorithm for Discovering Clusters*
|
||
|
||
---
|
||
|
||
### A10 — Normalisation des scores entre modèles
|
||
|
||
**Fonctionnement** :
|
||
|
||
```python
|
||
# Scores négatifs normalisés en [-1, 0], scores positifs inchangés
|
||
anomaly_score_normalized = normalize_scores(raw_score)
|
||
```
|
||
|
||
Le champ `anomaly_score` dans ClickHouse contient désormais le score normalisé, permettant une comparaison cohérente entre le modèle Complet (35 features) et le modèle Applicatif (31 features). Le score brut IF est conservé dans `raw_anomaly_score` (logs JSONL uniquement) et est utilisé pour l'assignation du threat level.
|
||
|
||
---
|
||
|
||
## 14. Migration de schéma ClickHouse
|
||
|
||
Les nouvelles colonnes suivantes sont disponibles dans les logs JSONL mais pas encore dans la table `ml_detected_anomalies`. Pour les activer :
|
||
|
||
```sql
|
||
ALTER TABLE ja4_processing.ml_detected_anomalies
|
||
ADD COLUMN IF NOT EXISTS campaign_id Int32 DEFAULT -1,
|
||
ADD COLUMN IF NOT EXISTS raw_anomaly_score Float32 DEFAULT 0;
|
||
```
|
||
|
||
Après cette migration, ajouter ces colonnes à la liste `cols` dans `fetch_and_analyze()` (elles sont déjà calculées en mémoire).
|