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>
757 lines
29 KiB
Markdown
757 lines
29 KiB
Markdown
# 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` (0–20, 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, 1–2 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 `ja4_processing`.
|
||
> 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 1–3 (même navigateur, légères variations)
|
||
- Bot avec rotation : ratio 17–809 → 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 ja4_processing.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.
|
||
|