Files
ja4-platform/services/bot-detector/IMPROVEMENTS.md
toto d469e39da7 feat: ja4-platform monorepo — 5 services unified, tests & RPM builds standardized
Services:
- ja4sentinel: TLS/JA4 fingerprint capture daemon (Go, libpcap)
- logcorrelator: JA4 log correlation engine (Go, ClickHouse)
- mod_reqin_log: Apache module (C, JSON request logging)
- bot_detector: ML bot detection pipeline (Python)
- dashboard: FastAPI/Streamlit analytics UI (Python)

Shared libraries:
- shared/go/ja4common: logger, config, shutdown, ipfilter (Go module)
- shared/python/ja4_common: ClickHouseClient, ClickHouseSettings (Python package)
- shared/clickhouse/: canonical SQL migrations (10 files)

Build & packaging:
- Unified 3-stage Dockerfile.package for Go RPMs (el8/el9/el10)
- go.work workspace linking sentinel, correlator, ja4common
- Makefile with test-all, build-all, rpm-* targets

Fixes applied:
- go.work: 1.21 → 1.24.6 (required by sentinel)
- correlator Dockerfiles: golang:1.21 → golang:1.24
- replace directives in go.mod for ja4common local path
- pyproject.toml: setuptools.backends → setuptools.build_meta
- Removed static libpcap linking (unavailable on Rocky 9)
- Fixed data races in output/writers_test.go (sync.Mutex + atomic.Int32)
- Rewrote corrupted test files (logger_test.go × 2)

Test coverage:
- correlator: 67.1% total (unixsocket 80.5%, config 91.7%, app 83.3%, multi 87.7%, stdout 100%)
- sentinel: all 10 packages pass (api, capture, config, fingerprint, ipfilter, logging, output, tlsparse)

Documentation:
- README.md + docs/ (architecture, development, 5 services, shared libs, DB schema & migrations)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-07 16:42:59 +02:00

757 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Bot Detector IA — Axes d'amélioration
> Document de propositions techniques — à valider avant implémentation
---
## Résumé des axes proposés
| # | Axe | Impact | Complexité | Priorité suggérée |
|---|-----|--------|------------|-------------------|
| A1 | [Détection de dérive conceptuelle (concept drift)](#a1-détection-de-dérive-conceptuelle) | 🔴 Élevé | Moyenne | ⭐⭐⭐ |
| A2 | [Seuil adaptatif par percentile](#a2-seuil-adaptatif-par-percentile) | 🔴 Élevé | Faible | ⭐⭐⭐ |
| A3 | [Analyse multi-fenêtres temporelles](#a3-analyse-multi-fenêtres-temporelles) | 🔴 Élevé | Élevée | ⭐⭐ |
| A4 | [Explainabilité par SHAP](#a4-explainabilité-par-shap) | 🟠 Moyen | Moyenne | ⭐⭐⭐ |
| A5 | [Déduplication avec TTL inter-cycles](#a5-déduplication-avec-ttl-inter-cycles) | 🟠 Moyen | Faible | ⭐⭐⭐ |
| A6 | [Pondération par récurrence dans le score](#a6-pondération-par-récurrence-dans-le-score) | 🟠 Moyen | Faible | ⭐⭐ |
| A7 | [Validation de complétude des features](#a7-validation-de-complétude-des-features) | 🟠 Moyen | Faible | ⭐⭐⭐ |
| A8 | [Clustering comportemental des anomalies](#a8-clustering-comportemental-des-anomalies) | 🟡 Utile | Moyenne | ⭐⭐ |
| A9 | [Métriques Prometheus / health check enrichi](#a9-métriques-prometheus--health-check-enrichi) | 🟡 Utile | Faible | ⭐⭐ |
| A10 | [Normalisation des scores entre modèles](#a10-normalisation-des-scores-entre-modèles) | 🟡 Utile | Faible | ⭐ |
---
## A1 — Détection de dérive conceptuelle
### Problème
L'Isolation Forest est entraîné sur la baseline humaine courante. Si le profil du trafic légitime évolue graduellement (nouveau navigateur populaire, changement de comportement utilisateur, migration réseau), le modèle vieilli peut :
- Générer des **faux positifs** sur du trafic humain nouvellement apparu
- Rater des **faux négatifs** si les bots imitent les anciens patterns
Le re-entraînement périodique (toutes les X heures) atténue le problème mais ne détecte pas quand une dérive significative a eu lieu **entre deux cycles de retraining**.
### Approche proposée
Calculer à chaque cycle un score de **dérive statistique** entre la baseline d'entraînement du modèle actif et la baseline courante. Si la dérive dépasse un seuil, forcer un re-entraînement anticipé.
**Méthode : Kolmogorov-Smirnov (KS test) ou Maximum Mean Discrepancy (MMD)**
Pour chaque feature :
```python
from scipy import stats
ks_stat, p_value = stats.ks_2samp(baseline_trained[feat], baseline_current[feat])
```
Si la fraction de features avec `p_value < 0.05` dépasse un seuil configurable (ex. 30%), déclencher un retrain et logguer un événement `DRIFT_DETECTED`.
### Bénéfices
- Retrain opportuniste plutôt que temporel fixe
- Détection proactive des changements de comportement réseau
- Réduction des faux positifs liés à la dérive
### Références
- Gama et al. (2014) — *A Survey on Concept Drift Adaptation*
- Rabanser et al. (2019) — *Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift*
### Implémentation suggérée
- Sauvegarder la distribution de la baseline d'entraînement dans le `.meta.json`
- Calculer le KS test au début de chaque cycle avant la décision de chargement
- Ajouter un paramètre `DRIFT_THRESHOLD` (défaut : 0.30)
- Logguer l'événement `DRIFT_DETECTED` avec les features déroutantes
---
## A2 — Seuil adaptatif par percentile
### Problème
`ANOMALY_THRESHOLD = -0.03` est un seuil **global et statique**. Ce seuil a une signification différente selon :
- Le volume de trafic (plus de trafic = distribution de scores plus resserrée)
- La contamination effective du cycle (jour calme vs attaque active)
- Les caractéristiques du modèle actif (entraîné sur 1 264 vs 1 725 sessions)
Un seuil fixe peut produire des **rafales de faux positifs** lors d'événements légitimes inhabituels (campagne marketing, crawler partenaire) ou rater des menaces réelles lors de trafic atypique.
### Approche proposée
Calculer dynamiquement le seuil à partir de la **distribution des scores du cycle courant** :
```python
scores = model.decision_function(X_test)
# Seuil = percentile P de la distribution des scores négatifs
adaptive_threshold = np.percentile(scores, ANOMALY_PERCENTILE)
# On prend le min avec le seuil statique pour éviter d'aller trop haut
threshold = min(adaptive_threshold, ANOMALY_THRESHOLD)
```
**Paramètre ajoutable** : `ANOMALY_PERCENTILE` (défaut : 5 → top 5% des scores les plus négatifs).
Cette approche est complémentaire au seuil statique (garde-fou) : elle s'adapte vers le bas mais ne remonte jamais au-dessus du seuil configuré.
### Bénéfices
- Stabilité du taux de faux positifs au fil du temps
- Auto-adaptation aux variations de volume
- Comportement plus prédictible en production
### Implémentation suggérée
- Ajouter `ANOMALY_PERCENTILE` (020, défaut 5) comme variable d'environnement
- Calculer le seuil adaptatif dans `run_semi_supervised_logic()`
- Logguer le seuil effectif utilisé dans `CYCLE_START` / `ANOMALY`
---
## A3 — Analyse multi-fenêtres temporelles
### Problème
La fenêtre 1h est un compromis. Elle manque :
- Les **attaques rapides** (burst de quelques minutes) : le signal est dilué
- Les **bots lents** (low-and-slow, 12 req/min sur 24h) : comportement normal sur 1h
### Approche proposée
Ajouter une deuxième vue ClickHouse agrégée sur **24h** et un troisième modèle sur cette fenêtre. Les scores des deux modèles peuvent être combinés :
```
score_final = w1 * score_1h + w2 * score_24h
```
Ou, plus simplement, un AND logique : une IP n'est flaggée que si elle est anomalie sur les **deux fenêtres**, réduisant drastiquement les faux positifs.
### Bénéfices
- Détection des bots low-and-slow (reconnaissance, scraping discret)
- Réduction des faux positifs par corrélation multi-temporelle
- Complémentarité avec le modèle 1h existant
### Considerations
- Nécessite une vue `view_ai_features_24h` dans ClickHouse
- Modèle 24h beaucoup plus stable (moins de bruit)
- Le volume de données à traiter augmente
### Références
- Stalmans & Irwin (2011) — *A Framework for Web Bot Detection Using Request Rate Monitoring*
- Stevanovic et al. (2013) — *An Efficient Flow-based Botnet Detection Using Supervised Machine Learning*
---
## A4 — Explainabilité par SHAP
### Problème
Le champ `reason` actuel est basique :
```
"[Complet] Score: -0.312 | Vel: 45.2 req/s | Fuzzing: 8.3 | Threat: CRITICAL"
```
Pour un opérateur de sécurité, il manque :
- **Quelles features** ont le plus contribué à ce score ?
- Est-ce principalement comportemental (velocity) ou fingerprint (JA4) ?
- Comment comparer deux anomalies de même score ?
### Approche proposée
Utiliser **TreeSHAP** (Lundberg & Lee, 2017) qui supporte nativement les forêts d'arbres :
```python
import shap
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test.iloc[[idx]])
top_features = sorted(zip(features, shap_values[0]), key=lambda x: abs(x[1]), reverse=True)[:5]
```
Enrichir le champ `reason` avec les 5 features les plus contributives et leur valeur SHAP.
### Bénéfices
- Triage des alertes facilité pour les analystes SOC
- Détection des features systématiquement sur-représentées (potentiel bug de feature engineering)
- Conformité avec les exigences de traçabilité des décisions IA
### Implémentation suggérée
- Ajouter `shap` aux requirements (compatible sklearn)
- Calculer SHAP uniquement pour les IP flaggées (pas sur tout le dataset)
- Stocker `shap_top5` comme JSON dans le log JSONL
- Option : `ENABLE_SHAP=true/false` pour contrôler la charge CPU
### Références
- Lundberg & Lee (2017) — *A Unified Approach to Interpreting Model Predictions*
---
## A5 — Déduplication avec TTL inter-cycles
### Problème
Avec un cycle de 5 min et une fenêtre 1h, la même IP malveillante est potentiellement **réinsérée 12 fois par heure** dans `ml_detected_anomalies`. Cela :
- Gonfle la table artificellement
- Complique les requêtes d'analyse (nécessite un DISTINCT)
- Fausse les métriques de comptage
Le mécanisme actuel de `drop_duplicates(subset=['src_ip'])` ne fonctionne qu'au sein d'un seul cycle, pas entre cycles.
### Approche proposée
Avant insertion, interroger ClickHouse pour filtrer les IPs déjà insérées récemment :
```python
# Récupérer les IPs déjà détectées dans les N dernières minutes
recent_ips = client.query_df(f"""
SELECT DISTINCT src_ip
FROM {DB}.ml_detected_anomalies
WHERE detected_at > now() - INTERVAL {DEDUP_TTL_MIN} MINUTE
""")
# Exclure ces IPs sauf si le score s'est dégradé significativement
new_anomalies = anomalies[~anomalies['src_ip'].isin(recent_ips['src_ip'])]
```
**Paramètre ajoutable** : `DEDUP_TTL_MIN` (défaut : 60 minutes).
**Variante** : ne re-insérer que si `new_score < existing_score - 0.05` (dégradation significative).
### Bénéfices
- Réduction du volume de la table de détection
- Requêtes d'analyse plus simples
- Gestion de la montée en charge (moins d'insertions)
### Implémentation suggérée
- Paramètre `DEDUP_TTL_MIN` (0 pour désactiver)
- La requête de déduplication est légère (index sur `detected_at`)
- Logguer le nb d'IP filtrées dans `CYCLE_END`
---
## A6 — Pondération par récurrence dans le score
### Problème
La récurrence est actuellement un champ **informatif seulement** : une IP détectée 50 fois a le même seuil de filtrage qu'une IP vue pour la première fois. Un bot persistant et connu ne reçoit pas de pénalité de score.
### Approche proposée
Ajuster le score de décision en fonction de la récurrence :
```python
# Score ajusté : plus une IP est récurrente, plus son score s'aggrave
recurrence_penalty = np.log1p(recurrence) * RECURRENCE_WEIGHT
adjusted_score = anomaly_score - recurrence_penalty
```
Avec `RECURRENCE_WEIGHT = 0.005` par défaut (configurable). Une IP vue 10 fois voit son score pénalisé de ~0.012, une IP vue 100 fois de ~0.023.
Cette approche simule un **Prior bayésien** : la probabilité qu'une IP soit malveillante augmente avec ses détections passées.
### Bénéfices
- Menaces persistantes classifiées plus sévèrement
- Réduction du bruit des anomalies éphémères
- Signal plus fort pour les blocages automatisés
### Implémentation suggérée
- Ajouter `RECURRENCE_WEIGHT` (défaut 0.005, 0 pour désactiver)
- Stocker `raw_score` et `adjusted_score` séparément dans les logs
---
## A7 — Validation de complétude des features
### Problème
Si une feature est absente de la vue (colonne manquante, erreur de schéma), elle est silencieusement remplacée par `0` via `fillna(0)`. Cela **dégrade la qualité du modèle sans avertissement** : une feature entièrement à zéro n'apporte aucune information discriminante et biaise les scores.
### Approche proposée
Au début de chaque cycle, après chargement du DataFrame :
```python
def validate_features(df: pd.DataFrame, features: list, name: str) -> list:
zero_features = [f for f in features if f in df.columns and df[f].std() == 0]
missing_features = [f for f in features if f not in df.columns]
if missing_features:
log_info(f"[{name}] ATTENTION: {len(missing_features)} features manquantes: {missing_features}")
if zero_features:
log_info(f"[{name}] ATTENTION: {len(zero_features)} features constantes (=0): {zero_features}")
# Retourner uniquement les features exploitables
valid = [f for f in features if f in df.columns and df[f].std() > 0]
return valid
```
Un événement `FEATURE_WARNING` serait loggué, et si plus de 20% des features sont invalides, le cycle peut être `SKIPPED`.
### Bénéfices
- Détection rapide des régressions de schéma ClickHouse
- Qualité de modèle assurée
- Facilite le debugging lors des évolutions de la vue
### Implémentation suggérée
- Paramètre `MIN_VALID_FEATURE_RATIO` (défaut 0.8)
- Comparaison avec les features du modèle chargé (détecte les dérives de schéma post-mise à jour)
---
## A8 — Clustering comportemental des anomalies
### Problème
Les anomalies sont analysées et insérées individuellement. Or, une campagne de botnet coordonnée peut impliquer des **dizaines d'IPs avec des profils similaires**. Cette information de **corrélation horizontale** est aujourd'hui invisible.
### Approche proposée
Après la détection, appliquer un **DBSCAN** sur les features des anomalies pour identifier des clusters d'attaque :
```python
from sklearn.cluster import DBSCAN
X_anomalies = anomalies[features].fillna(0)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_anomalies)
labels = DBSCAN(eps=0.5, min_samples=3).fit_predict(X_scaled)
anomalies['campaign_id'] = labels # -1 = isolé, 0+ = cluster
```
Les IPs d'un même cluster partagent un comportement similaire et peuvent faire partie d'une même infrastructure d'attaque.
### Bénéfices
- Identification des campagnes coordonnées (botnets distribués)
- Enrichissement de `reason` avec un identifiant de campagne
- Permet des blocages de plages d'IPs entières
### Implémentation suggérée
- DBSCAN uniquement si ≥ 5 anomalies dans le cycle (pas de coût si peu d'anomalies)
- Stocker `campaign_id` dans `ml_detected_anomalies`
- `eps` et `min_samples` configurables
### Références
- Ester et al. (1996) — *A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases*
---
## A9 — Métriques Prometheus / health check enrichi
### Problème
Le health check actuel est binaire (OK/DEGRADED). Cela ne permet pas :
- De monitorer la dérive du taux d'anomalies dans le temps
- D'alerter si aucun cycle ne s'est exécuté depuis X minutes
- De suivre l'âge du modèle en production
### Approche proposée
Exposer un endpoint `/metrics` au format **Prometheus text** sur le même port :
```
# HELP botdetector_cycle_duration_seconds Duration of last analysis cycle
# TYPE botdetector_cycle_duration_seconds gauge
botdetector_cycle_duration_seconds 12.4
# HELP botdetector_anomalies_total Total anomalies detected in last cycle
# TYPE botdetector_anomalies_total gauge
botdetector_anomalies_total{model="Complet"} 3
botdetector_anomalies_total{model="Applicatif"} 7
# HELP botdetector_model_age_hours Age of active model in hours
botdetector_model_age_hours{model="Applicatif"} 0.91
# HELP botdetector_human_baseline_size Nb of human samples used for training
botdetector_human_baseline_size{model="Applicatif"} 1725
```
Implémenté sans dépendance externe (format texte manuel ou lib légère `prometheus_client`).
### Bénéfices
- Intégration Grafana/Alertmanager
- Alertes sur dérive du taux d'anomalies (ex. : >50% d'une heure à l'autre)
- Monitoring de la fraîcheur du modèle
### Implémentation suggérée
- Ajouter `prometheus_client` ou générer le format texte manuellement
- Endpoint `/metrics` sur le même `HTTPServer` existant
- Métriques stockées dans un dict thread-safe mis à jour après chaque cycle
---
## A10 — Normalisation des scores entre modèles
### Problème
Les scores `decision_function` de l'IF ne sont **pas comparables entre modèles** entraînés sur des données différentes. Un score de -0.10 sur le modèle Complet et -0.10 sur le modèle Applicatif n'ont pas la même signification si les baselines et les features sont différentes.
La déduplication actuelle par `src_ip` prend le score le plus bas sans tenir compte de cette non-comparabilité.
### Approche proposée
Normaliser les scores par rapport à la distribution des scores négatifs du cycle courant :
```python
# Normalisation min-max sur le sous-ensemble des scores < 0
neg_scores = unknown_traffic['anomaly_score'][unknown_traffic['anomaly_score'] < 0]
if len(neg_scores) > 0:
score_min, score_max = neg_scores.min(), neg_scores.max()
unknown_traffic['normalized_score'] = (
(unknown_traffic['anomaly_score'] - score_min) / (score_max - score_min + 1e-9)
).clip(0, 1) * -1 # entre -1 et 0
```
Les niveaux de menace seraient alors calculés sur le score normalisé, rendant la comparaison entre modèles cohérente.
### Bénéfices
- Cohérence des niveaux CRITICAL/HIGH/MEDIUM entre modèles
- Déduplication plus juste
- Seuils de threat_level interprétables de façon constante
---
## Notes d'implémentation générales
- **Compatibilité** : toute amélioration doit rester rétrocompatible avec le schéma `ml_detected_anomalies` existant (ajout de colonnes optionnelles uniquement)
- **Lisibilité** : garder le code en sections délimitées par les bandeaux `═══` existants
- **Tests** : valider chaque changement par une exécution Docker sur la base de données réelle
- **Documentation** : mettre à jour `DOCUMENTATION.md` après chaque implémentation
- **Feature flags** : les nouvelles fonctionnalités comportementales devraient être activables via variable d'environnement pour un rollout progressif
---
# Nouvelles dimensions de features — Propositions B
> Propositions de features supplémentaires pour l'Isolation Forest, validées sur les données réelles de `mabase_prod`.
> Chaque proposition indique la force du signal observée en base, la source de données, la formule de calcul et les références scientifiques.
## Résumé des signaux
| # | Feature | Signal observé | Modèle | Impact estimé |
|---|---------|---------------|--------|--------------|
| B1 | JA3/JA4 diversity ratio | 809 JA3 pour 2 JA4 (IP connue bot) | Complet | 🔴 Élevé |
| B2 | SYN timing regularity | 386/3222 IPs (12%) avec variance=0 | Complet | 🔴 Élevé |
| B3 | TLS 1.2 exclusive ratio | 136/3259 IPs (4%) — jamais TLS 1.3 | Complet | 🔴 Élevé |
| B4 | HEAD method ratio | 67/3335 IPs (2%) à >50% HEAD | Les deux | 🟠 Moyen |
| B5 | Sec-Fetch absence rate | Signal L7 universel (correlated=0 aussi) | Les deux | 🟠 Moyen |
| B6 | Accept header entropy | Bots = Accept vide ou `*/*` constant | Les deux | 🟠 Moyen |
| B7 | TLS version entropy | TLS 1.3 = 97.3% du trafic légitime | Complet | 🟠 Moyen |
| B8 | HTTP/TLS protocol mismatch | HTTP/1.1 + TLS 1.3 = ratio anormal | Complet | 🟡 Utile |
| B9 | IP DF-bit variance | DF inconsistant = stack spoofé | Complet | 🟡 Utile |
| B10 | JA4 concentration intra-ASN | JA4 rare dans ASN = outil exotique | Complet | 🟡 Utile |
---
## B1 — JA3/JA4 Diversity Ratio (rotation de fingerprint TLS)
### Observation
```
185.177.72.60 → 1619 JA3 distincts / 2 JA4 → ratio 809.5
194.187.171.160 → 153 JA3 distincts / 2 JA4 → ratio 76.5
```
Le JA4 reste stable (il encode le type de client TLS + ALPN) mais le JA3 varie massivement. C'est la signature d'un **bot qui randomise les extensions TLS** pour contourner la détection par fingerprint.
### Feature proposée
```sql
-- Dans mv_agg_host_ip_ja4_1h
uniqState(ja3) AS uniq_ja3 -- à ajouter dans la table d'agrégation
```
```python
# Dans view_ai_features_1h
ja3_diversity_ratio = uniq_ja3 / greatest(uniq_ja4, 1)
```
### Signal en base
- Trafic humain : ratio typiquement 13 (même navigateur, légères variations)
- Bot avec rotation : ratio 17809 → signal extrêmement discriminant
- Disponible : `ja3` est présent dans `http_logs` avec 100% de valeurs non-vides pour correlated=1
### Modifications requises
1. Ajouter `uniqState(ja3) AS uniq_ja3` dans `mv_agg_host_ip_ja4_1h` et `agg_host_ip_ja4_1h`
2. Ajouter `uniqMerge(uniq_ja3) / greatest(uniq_ja4_merged, 1) AS ja3_diversity_ratio` dans `view_ai_features_1h`
3. Ajouter `ja3_diversity_ratio` à `feats_complet` dans `bot_detector.py`
### Références
- Siby et al. (2020) — *Encrypted DNS → Privacy? A Traffic Analysis Perspective* — méthodes de diversité de fingerprint
- Anderson & McGrew (2016) — *Machine Learning for Encrypted Malware Traffic Classification* — JA3 comme feature primaire
- Husák et al. (2022) — *TLS fingerprinting for bot detection* — rotation JA3 comme évasion signature
---
## B2 — SYN-to-ClientHello Timing Regularity
### Observation
```
88.202.237.59 : 45 connexions, avg=22ms, std=0.00ms → timing robotique parfait
92.184.144.129: 41 connexions, avg=10ms, std=0.00ms → idem
386/3222 IPs analysées (12%) ont une variance=0
```
Un humain présente une distribution aléatoire (Weibull ou log-normale) des temps de réponse réseau. Un bot utilisant un scheduler fixe ou une connexion locale a une variance proche de zéro.
### Feature proposée
```sql
-- Dans view_ai_features_1h (CTE)
varPopMerge(tcp_jitter_variance) AS syn_jitter_variance, -- déjà présent (tcp_jitter_variance)
-- Ajouter le coefficient de variation (normalisé)
```
```python
# cv = std / mean → 0 = robotique, >0.5 = humain
syn_timing_cv = sqrt(syn_jitter_variance) / greatest(avg_syn_ms, 1)
```
**Note** : `tcp_jitter_variance` est déjà dans le modèle mais c'est la variance brute. Le **coefficient de variation** (std/mean) normalise par le délai moyen et est plus discriminant pour différencier bots rapides (10ms) de bots lents (100ms).
### Modifications requises
1. Ajouter `avg(syn_to_clienthello_ms)` dans `mv_agg_host_ip_ja4_1h``avg_syn_ms`
2. Calculer `syn_timing_cv = sqrt(tcp_jitter_variance) / greatest(avg_syn_ms, 1)` dans `view_ai_features_1h`
3. Ajouter `syn_timing_cv` à `feats_complet`
### Références
- Zeber et al. (2020) — *The Measurement of Web Timing* — distribution log-normale pour humains
- Beugin et al. (2021) — *Robustness of Traffic Analysis Against Adversarial Timing* — variance comme discriminant
- Stevanovic & Pedersen (2015) — *Detecting Bots Using Multi-level Traffic Analysis* — timing régularité = signal bot L4
---
## B3 — TLS 1.2 Exclusive Ratio
### Observation
```
95.217.144.244 : 360/360 requêtes en TLS 1.2 (jamais TLS 1.3)
37.65.177.201 : 267/267 requêtes en TLS 1.2
136 IPs utilisent exclusivement TLS 1.2 sur 3259 analysées (4.2%)
```
TLS 1.3 représente 97.3% du trafic en 2026. Les navigateurs modernes n'utilisent TLS 1.2 que comme fallback exceptionnel. Une IP utilisant **exclusivement** TLS 1.2 utilise un client obsolète, une bibliothèque custom, ou un outil de scan.
### Feature proposée
```sql
-- Dans mv_agg_host_ip_ja4_1h
sum(IF(tls_version = '1.2', 1, 0)) AS tls12_count -- nouveau
-- tls_version déjà stockée via tls_alpn_raw → à distinguer ou ajouter
```
```python
# Dans view_ai_features_1h
tls12_ratio = tls12_count / greatest(hits, 1)
```
### Modifications requises
1. Ajouter `sum(IF(src.tls_version = '1.2', 1, 0)) AS tls12_count` dans `mv_agg_host_ip_ja4_1h`
2. Ajouter `tls12_count` dans `agg_host_ip_ja4_1h`
3. Calculer `tls12_count / hits AS tls12_ratio` dans `view_ai_features_1h`
### Références
- Kotzias et al. (2018) — *Coming of Age: A Longitudinal Study of TLS Deployment* — vieillissement des stacks
- Naylor et al. (2014) — *The Cost of the S in HTTPS* — adoption TLS 1.3 par navigateurs légitimes
- Cloudflare Radar 2024 — TLS 1.3 = 95%+ du trafic web mondial
---
## B4 — HEAD Method Ratio
### Observation
```
34.140.199.84 : 11/12 requêtes HEAD (91.7%) → Google Cloud uptime checker
67/3335 IPs ont >50% de requêtes HEAD
```
La méthode HEAD est utilisée pour vérifier la disponibilité d'une ressource sans télécharger son contenu. C'est la signature des :
- **Uptime checkers** (Pingdom, UptimeRobot, Google Cloud Health Check)
- **Scanners de vulnérabilités** (Nikto, Nuclei)
- **Bots de reconnaissance discrète**
### Feature proposée
```python
# head_ratio = déjà calculable depuis count_post (method breakdown)
# Ajouter dans mv_agg_host_ip_ja4_1h :
count_head = sum(IF(method = 'HEAD', 1, 0))
```
```python
head_ratio = count_head / greatest(hits, 1)
```
### Note : disponibilité dans les deux modèles
Contrairement aux features TCP, `head_ratio` est disponible pour `correlated=0` aussi — c'est une feature HTTP pure. À ajouter dans les deux listes `feats` et `feats_complet`.
### Références
- Barracuda Networks (2023) — *Bot Traffic Report* — HEAD requests pattern
- OWASP Automated Threat Handbook — OAT-011: Scraping, OAT-018: Credential Stuffing
---
## B5 — Sec-Fetch Absence Rate
### Observation
Les headers `Sec-Fetch-Site`, `Sec-Fetch-Mode`, `Sec-Fetch-Dest` sont injectés par les navigateurs modernes (Chrome 76+, Firefox 90+) **automatiquement** depuis 2019. Leur absence est un signal de :
- Client HTTP non-navigateur (curl, requests, Scrapy, headless Chrome sans headers complets)
- Vieux navigateur ou UA spoofé
- HTTP CONNECT proxy
### Feature proposée
```sql
-- Dans mv_agg_host_ip_ja4_1h
sum(IF(length(src.header_sec_fetch_site) = 0, 1, 0)) AS count_no_sec_fetch
```
```python
sec_fetch_absence_rate = count_no_sec_fetch / greatest(hits, 1)
```
### Combinaison avec `modern_browser_score`
`sec_fetch_absence_rate` + `modern_browser_score` forment une paire complémentaire :
- Bot avec UA Chrome forgé → `modern_browser_score` élevé mais `sec_fetch_absence_rate` = 1 → contradiction forte
### Modifications requises
1. `count_no_sec_fetch` dans le MV et la table
2. Calcul dans la vue
### Références
- West & Loshbough (2019) — *Fetch Metadata Request Headers* (W3C Spec)
- Invernizzi et al. (2016) — *CLOAK of Visibility* — inconsistance headers = bot
---
## B6 — Accept Header Entropy
### Observation
Les navigateurs légitimes envoient des headers `Accept` complexes et cohérents :
```
image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
```
Les bots envoient :
```
*/* (curl, wget, Scrapy)
(vide) (bots minimalistes)
text/html (outils basiques)
```
### Feature proposée
```python
# Diversité des valeurs Accept par IP (proxy de comportement navigateur)
accept_entropy = -sum(p * log2(p+1e-9) for p in accept_value_probs)
# Ou plus simplement : fraction de requêtes avec Accept générique/vide
generic_accept_ratio = count_generic_accept / hits
# où generic = longueur(Accept) < 10 ou Accept IN ('*/*', '')
```
```sql
sum(IF(length(src.header_accept) < 5, 1, 0)) AS count_generic_accept
```
### Références
- Nikiforakis et al. (2013) — *Cookieless Monster: Exploring the Ecosystem of Web-based Device Fingerprinting* — Accept comme composant stable
- Acar et al. (2014) — *The Web Never Forgets* — entropie des headers HTTP
---
## B7 — HTTP/TLS Protocol Version Mismatch
### Observation
```
HTTP/2.0 → 160855 requêtes (84%)
HTTP/1.1 → 26421 requêtes (14%)
TLS 1.3 → 177330 requêtes (97%)
```
HTTP/2 requiert TLS dans les navigateurs modernes. Combinaisons anormales :
- HTTP/1.1 + TLS 1.3 : légitime mais rare pour les vrais navigateurs (eux font HTTP/2 si TLS 1.3)
- HTTP/1.0 + TLS : extrêmement suspect (outil custom ou ancien bot)
- HTTP/2 + TLS 1.2 : possible mais déclinant
### Feature proposée
```python
# Fraction de requêtes avec HTTP/1.x malgré TLS 1.3 disponible
http1_tls13_ratio = count_http1_with_tls13 / greatest(hits, 1)
# http1_0_ratio = count_http10 / hits # signal fort
```
```sql
sum(IF(http_version = 'HTTP/1.0', 1, 0)) AS count_http10,
sum(IF(http_version LIKE 'HTTP/1%' AND tls_version = '1.3', 1, 0)) AS count_http1_tls13
```
---
## B8 — IP DF-Bit Consistency
### Observation
```
df=1 : 172490 paquets (92%)
df=0 : 15016 paquets (8%)
```
Le bit "Don't Fragment" est généralement constant pour une session TCP donnée. Une IP qui alterne DF=0 et DF=1 au sein d'une même session, ou entre sessions, peut indiquer :
- **Usurpation d'IP** (spoofed source packets dans un botnet)
- **Stack TCP custom** (bots implémentant leur propre TCP)
- **NAT traversal** avec réécriture de paquets
### Feature proposée
```python
df_variance = stddev(ip_meta_df) per IP # 0 = cohérent, >0 = mélangé
```
```sql
varPop(toFloat64(ip_meta_df)) AS ip_df_variance
```
Faible impact seul, mais utile en combinaison avec TTL variance pour le TCP fingerprinting multi-dimensional.
---
## Récapitulatif des modifications ClickHouse nécessaires
### Colonnes à ajouter dans `agg_host_ip_ja4_1h`
```sql
ALTER TABLE mabase_prod.agg_host_ip_ja4_1h
ADD COLUMN uniq_ja3 AggregateFunction(uniq, String),
ADD COLUMN avg_syn_ms SimpleAggregateFunction(avg, Float64),
ADD COLUMN tls12_count SimpleAggregateFunction(sum, UInt64),
ADD COLUMN count_head SimpleAggregateFunction(sum, UInt64),
ADD COLUMN count_no_sec_fetch SimpleAggregateFunction(sum, UInt64),
ADD COLUMN count_generic_accept SimpleAggregateFunction(sum, UInt64),
ADD COLUMN count_http10 SimpleAggregateFunction(sum, UInt64);
```
### Nouvelles features dans `view_ai_features_1h`
| Feature | Formule | Modèle |
|---------|---------|--------|
| `ja3_diversity_ratio` | `uniq_ja3 / greatest(uniq_ja4, 1)` | Complet |
| `syn_timing_cv` | `sqrt(tcp_jitter_variance) / greatest(avg_syn_ms, 1)` | Complet |
| `tls12_ratio` | `tls12_count / greatest(hits, 1)` | Complet |
| `head_ratio` | `count_head / greatest(hits, 1)` | Les deux |
| `sec_fetch_absence_rate` | `count_no_sec_fetch / greatest(hits, 1)` | Les deux |
| `generic_accept_ratio` | `count_generic_accept / greatest(hits, 1)` | Les deux |
| `http10_ratio` | `count_http10 / greatest(hits, 1)` | Les deux |
> ⚠️ Les colonnes ajoutées par ALTER ne sont pas rétro-alimentées dans les données historiques. Un backfill depuis `http_logs` sera nécessaire.
> ⚠️ La MV `mv_agg_host_ip_ja4_1h` doit être **recréée** (pas de ALTER sur une MV) pour inclure les nouveaux champs.