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

29 KiB
Raw Blame History

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_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 :

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 :

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 :

# 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_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 :

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 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 :

# 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

-- 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 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

-- 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

  1. Ajouter avg(syn_to_clienthello_ms) dans mv_agg_host_ip_ja4_1havg_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

-- 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

  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

# 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é 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

# 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 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.