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>
29 KiB
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) | 🔴 Élevé | Moyenne | ⭐⭐⭐ |
| A2 | Seuil adaptatif par percentile | 🔴 Élevé | Faible | ⭐⭐⭐ |
| A3 | Analyse multi-fenêtres temporelles | 🔴 Élevé | Élevée | ⭐⭐ |
| A4 | Explainabilité par SHAP | 🟠 Moyen | Moyenne | ⭐⭐⭐ |
| A5 | Déduplication avec TTL inter-cycles | 🟠 Moyen | Faible | ⭐⭐⭐ |
| A6 | Pondération par récurrence dans le score | 🟠 Moyen | Faible | ⭐⭐ |
| A7 | Validation de complétude des features | 🟠 Moyen | Faible | ⭐⭐⭐ |
| A8 | Clustering comportemental des anomalies | 🟡 Utile | Moyenne | ⭐⭐ |
| A9 | Métriques Prometheus / health check enrichi | 🟡 Utile | Faible | ⭐⭐ |
| 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 :
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_DETECTEDavec 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 :
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_24hdans 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 :
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
shapaux requirements (compatible sklearn) - Calculer SHAP uniquement pour les IP flaggées (pas sur tout le dataset)
- Stocker
shap_top5comme JSON dans le log JSONL - Option :
ENABLE_SHAP=true/falsepour 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 :
# 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 :
# 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_scoreetadjusted_scoresé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 :
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 :
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
reasonavec 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_iddansml_detected_anomalies epsetmin_samplesconfigurables
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_clientou générer le format texte manuellement - Endpoint
/metricssur le mêmeHTTPServerexistant - 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 :
# 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_anomaliesexistant (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.mdaprè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
-- Dans mv_agg_host_ip_ja4_1h
uniqState(ja3) AS uniq_ja3 -- à ajouter dans la table d'agrégation
# 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 :
ja3est présent danshttp_logsavec 100% de valeurs non-vides pour correlated=1
Modifications requises
- Ajouter
uniqState(ja3) AS uniq_ja3dansmv_agg_host_ip_ja4_1hetagg_host_ip_ja4_1h - Ajouter
uniqMerge(uniq_ja3) / greatest(uniq_ja4_merged, 1) AS ja3_diversity_ratiodansview_ai_features_1h - Ajouter
ja3_diversity_ratioàfeats_completdansbot_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
-- 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é)
# 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
- Ajouter
avg(syn_to_clienthello_ms)dansmv_agg_host_ip_ja4_1h→avg_syn_ms - Calculer
syn_timing_cv = sqrt(tcp_jitter_variance) / greatest(avg_syn_ms, 1)dansview_ai_features_1h - 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
-- 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
# Dans view_ai_features_1h
tls12_ratio = tls12_count / greatest(hits, 1)
Modifications requises
- Ajouter
sum(IF(src.tls_version = '1.2', 1, 0)) AS tls12_countdansmv_agg_host_ip_ja4_1h - Ajouter
tls12_countdansagg_host_ip_ja4_1h - Calculer
tls12_count / hits AS tls12_ratiodansview_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
# 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))
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
-- Dans mv_agg_host_ip_ja4_1h
sum(IF(length(src.header_sec_fetch_site) = 0, 1, 0)) AS count_no_sec_fetch
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é maissec_fetch_absence_rate= 1 → contradiction forte
Modifications requises
count_no_sec_fetchdans le MV et la table- 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
# 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 ('*/*', '')
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
# 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
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
df_variance = stddev(ip_meta_df) per IP # 0 = cohérent, >0 = mélangé
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
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_logssera nécessaire. ⚠️ La MVmv_agg_host_ip_ja4_1hdoit être recréée (pas de ALTER sur une MV) pour inclure les nouveaux champs.