Split monolithic thesis into separate chapter markdown files under docs/thesis/. Remove fabricated bibliography entries, correct inflated claims, add GNN/Transformers section, and rename MetaLearner to Fusion LR. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
33 KiB
5. Techniques comportementales avancées
Introduction
Cette section présente huit techniques originales développées pour adresser les angles morts des systèmes de détection existants. Ces angles morts ont été identifiés par analyse systématique des faux négatifs observés en production : sessions mal classifiées comme humaines malgré un comportement automatisé, ou sessions légitimes pénalisées par des features trop sensibles au contexte réseau.
Les huit techniques couvrent l'intégralité de la chaîne de détection : analyse des séquences de navigation (§5.1), détection de flottes distribuées (§5.2), fingerprinting temporel (§5.3), arbre de dépendances de ressources (§5.4), dérive intra-session de fingerprint (§5.5), analyse DNS passive (§5.6, non implémentée), invariant de compression (§5.7, non implémentée), et session multi-domaine (§5.8).
Récapitulatif des statuts :
| Section | Technique | Statut |
|---|---|---|
| 5.1 | Path Sequence Entropy | [impl.] |
| 5.2 | Bipartite Bot Fleet Detection | [impl.] |
| 5.3 | Request Cadence Fingerprint | [impl.] |
| 5.4 | Resource Dependency Tree | [impl.] |
| 5.5 | Intra-Session JA4 Drift | [impl.] |
| 5.6 | DNS Shadow Analysis | [todo] |
| 5.7 | Compression Ratio Invariant | [todo] |
| 5.8 | Cross-Domain Session Linking | [impl.] |
5.1 Entropie de séquence de chemins (Path Sequence Entropy) [impl.]
Constat et motivation
La feature path_diversity_ratio mesure la diversité des chemins parcourus (nombre de chemins distincts / nombre total de requêtes), mais non leur ordre. Cette distinction est fondamentale : un crawler sophistiqué peut randomiser l'ordre de visite des pages pour augmenter la diversité apparente, sans reproduire les transitions naturelles d'un comportement humain.
La navigation humaine suit des patterns séquentiels prévisibles déterminés par l'architecture du site : page d'accueil → catégorie → produit → panier d'achat. Ces transitions ont une structure probabiliste stable que les crawlers systématiques ne reproduisent pas, même avec randomisation.
Modèle mathématique : chaîne de Markov du premier ordre
Une chaîne de Markov du premier ordre modélise un système dans lequel l'état suivant dépend uniquement de l'état courant (propriété d'absence de mémoire, aussi appelée propriété markovienne). Pour les séquences de chemins, chaque chemin est un état, et la probabilité de transition est :
P(chemin_j | chemin_i) = count(i → j) / count(i)
où count(i → j) est le nombre de fois que le chemin i est immédiatement suivi du chemin j, et count(i) est le nombre total de départs depuis i.
L'entropie de la matrice de transition mesure l'imprévisibilité des transitions :
H_transition = -Σ_{i,j} P(p_i → p_j) × log₂(P(p_i → p_j))
Interprétation :
- Entropie élevée : transitions imprévisibles — caractéristique d'une navigation humaine organique avec des chemins variés
- Entropie faible : transitions prévisibles — caractéristique d'un crawler systématique (lexicographique, profondeur d'abord)
- Entropie nulle : transition unique constante — bot répétant exactement le même chemin
Résistance à la rotation de diversité
Un bot qui randomise aléatoirement l'ordre de ses chemins augmente path_diversity_ratio mais produit une matrice de transition quasi-uniforme (entropie maximale). Or, la navigation humaine réelle possède des transitions structurées : les transitions produit → panier sont fréquentes, les transitions panier → page d'accueil sont rares. L'entropie maximale (uniforme) est donc elle-même un signal anormal, distinguable de l'entropie modérée mais structurée de la navigation humaine.
La combinaison (path_diversity_ratio, path_transition_entropy) forme un espace bidimensionnel où les clusters bots et humains sont mieux séparés qu'avec une feature seule.
Implémentation
Table ClickHouse : agg_path_sequences_1h
- Colonnes :
(src_ip, ja4, session_id, groupArray(path) AS path_sequence) - Agrégation par session sur fenêtre glissante d'une heure
Vue : view_thesis_features_1h
- Colonne :
path_transition_entropy
SQL de calcul :
-- Normalisation des préfixes à profondeur 2
-- ex. /shop/product/123 → /shop/product
WITH normalized_paths AS (
SELECT
session_id,
arrayMap(p -> arrayStringConcat(
arraySlice(splitByChar('/', p), 1, 3), '/'), path_sequence
) AS norm_seq
FROM agg_path_sequences_1h
),
-- Calcul des transitions
transitions AS (
SELECT
session_id,
arrayZip(
arraySlice(norm_seq, 1, length(norm_seq) - 1),
arraySlice(norm_seq, 2)
) AS transition_pairs
FROM normalized_paths
)
-- L'entropie finale est calculée via UDF Python (scipy.stats.entropy)
-- ou approximée par la formule SQL suivante :
SELECT
session_id,
-sum(p * log2(p)) AS path_transition_entropy
FROM (
SELECT
session_id,
count() / sum(count()) OVER (PARTITION BY session_id) AS p
FROM transitions
ARRAY JOIN transition_pairs AS tp
GROUP BY session_id, tp
)
GROUP BY session_id;
La feature path_transition_entropy est intégrée dans le vecteur feature de bot_detector (famille F7 — Comportement navigation).
5.2 Graphe de co-occurrence JA4×ASN (Bipartite Bot Fleet Detection) [impl.]
Constat et motivation
La feature ja4_asn_concentration mesure la concentration d'une paire (JA4, ASN) donnée — une valeur élevée signale un usage anormal d'une combinaison spécifique. Cependant, les botnets sophistiqués utilisent des dizaines de JA4 et d'ASN en rotation : chaque paire (JA4, ASN) individuelle est statistiquement anodine, mais le pattern global de co-occurrence révèle la coordination.
Un botnet distribuant ses requêtes sur 20 ASN différents, avec 5 JA4 distincts par ASN, produira 100 paires (JA4, ASN) différentes, chacune avec une concentration faible — indétectable par ja4_asn_concentration seul. Pourtant, ces 5 JA4 co-apparaissent systématiquement dans les mêmes ASN, formant une signature de flotte détectable par analyse de graphe.
Modèle mathématique : graphe bipartite et détection de communautés
Un graphe bipartite G = (U ∪ V, E) est un graphe dont les sommets se répartissent en deux ensembles disjoints (ici : JA4 fingerprints U et ASNs V) où les arêtes ne connectent que des sommets d'ensembles différents. Une arête (u, v) ∈ E existe si ≥ N IPs utilisent le JA4 u depuis l'ASN v dans la fenêtre temporelle considérée.
Projection sur l'espace JA4 : le graphe projeté G_JA4 = (U, E') est défini par (u₁, u₂) ∈ E' si ∃ v ∈ V tel que (u₁, v) ∈ E et (u₂, v) ∈ E. Deux JA4 partagent une arête si et seulement si ils co-apparaissent dans le même ASN.
Détection de communautés : une communauté est un sous-ensemble de sommets plus densément connectés entre eux qu'avec le reste du graphe. Dans le graphe projeté G_JA4, une communauté dense de JA4 indique un botnet utilisant plusieurs empreintes JA4 mais se coordonnant via une infrastructure ASN partagée.
L'implémentation utilise Louvain (Blondel et al., 2008) via la bibliothèque python-louvain pour la détection de communautés sur le graphe projeté pondéré. L'algorithme de Louvain optimise la modularité du graphe par agrégation hiérarchique itérative, identifiant les regroupements de JA4 partageant un profil de co-occurrence ASN similaire. En l'absence de python-louvain, un fallback vers les composantes connexes de NetworkX est utilisé.
Formule du score de flotte :
fleet_score = community_size × edge_density / log2(n_asn + 2)
où :
community_size: nombre de JA4 dans la communauté détectéeedge_density: densité du sous-graphe induit (arêtes observées / arêtes possibles)log(nb_ASN): terme de normalisation pour pénaliser les grandes flottes trop dispersées géographiquement (signal d'infrastructure légitime CDN)
Une valeur de fleet_score élevée (> seuil empirique déterminé en production) déclenche un malus sur le score d'anomalie de toutes les IPs des communautés concernées.
Implémentation
Module : fleet.py dans bot_detector
import networkx as nx
from networkx.algorithms import bipartite
def build_fleet_graph(df: pd.DataFrame, min_ips: int = 3):
"""Construit le graphe bipartite JA4 × ASN."""
G = nx.Graph()
edge_weights = (
df.groupby(['ja4', 'asn_number'])['src_ip'].nunique()
.reset_index(name='n_ips')
)
edge_weights = edge_weights[edge_weights['n_ips'] >= min_ips]
for _, row in edge_weights.iterrows():
G.add_edge(f"ja4:{row['ja4']}", f"asn:{row['asn_number']}",
weight=int(row['n_ips']))
return G, ja4_nodes, asn_nodes
def detect_fleet_communities(df: pd.DataFrame) -> dict:
"""Analyse le graphe et retourne {src_ip: fleet_score}."""
G, ja4_nodes, asn_nodes = build_fleet_graph(df)
# Projection bipartite : graphe des JA4 partageant des ASN
G_ja4 = bipartite.weighted_projected_graph(G, ja4_nodes)
# Détection de communautés (Louvain ou composantes connexes en fallback)
try:
from community import best_partition as louvain_partition
partition = louvain_partition(G_ja4, weight='weight', random_state=42)
except ImportError:
communities = {i: set(c) for i, c in enumerate(nx.connected_components(G_ja4))}
# fleet_score = taille × densité / log2(n_asn + 2)
for cid, members in communities.items():
score = len(members) * density / max(np.log2(n_asn + 2), 0.1)
for ja4_node in members:
fleet_scores[ja4_node.replace('ja4:', '')] = score
return ip_scores
matrix = np.zeros((len(ja4_nodes), len(asn_nodes)))
for i, ja4 in enumerate(ja4_nodes):
for j, asn in enumerate(asn_nodes):
if G.has_edge(ja4, asn):
matrix[i, j] = G[ja4][asn]['weight']
return ja4_nodes, matrix
def detect_fleets(sessions: pd.DataFrame) -> pd.DataFrame:
G = build_bipartite_graph(sessions)
ja4_nodes, matrix = project_ja4(G)
if len(ja4_nodes) < 5:
return pd.DataFrame()
clusterer = HDBSCAN(min_cluster_size=3, metric='jaccard')
labels = clusterer.fit_predict(matrix > 0)
# Calcul fleet_score par cluster...
Sortie : les résultats de la détection de flotte sont intégrés directement dans le DataFrame du cycle courant via deux colonnes additionnelles :
fleet_score: score de la communauté à laquelle appartient l'IP (0 si aucune flotte détectée)fleet_campaign_flag: 1 sifleet_score ≥ FLEET_SCORE_THRESHOLD(défaut : 2.0)
Les IPs appartenant à des communautés avec fleet_score élevé reçoivent un malus de score d'anomalie dans le pipeline final (ajout d'une constante au score brut EIF avant application de la fusion LR).
5.3 Fingerprinting par timing inter-requêtes (Request Cadence Fingerprint) [impl.]
Constat et motivation
Les features temporelles existantes — hit_velocity (vitesse moyenne de requêtes) et temporal_entropy (distribution horaire) — capturent la fréquence et la régularité macro-temporelle des requêtes. Elles manquent cependant le rythme microscopique : la distribution des intervalles entre requêtes consécutives (inter-request intervals, Δt) contient un signal discriminant riche.
Les humains produisent des intervalles irréguliers avec des rafales de chargement de page suivies de pauses de lecture. Les bots produisent des intervalles réguliers (basés sur sleep(N)) ou des intervalles exponentiels (backoff). Ces différences de structure statistique sont exploitées via quatre signaux complémentaires.
Signal 1 : Coefficient de variation (CV)
Le coefficient de variation est le rapport entre l'écart-type et la moyenne de la distribution des intervalles :
CV(Δt) = σ(Δt) / μ(Δt)
Valeurs caractéristiques :
- Navigation humaine : CV ≈ 1,5–3,0 (haute variabilité naturelle due aux temps de lecture variables)
- Bot régulier (sleep fixe N ms) : CV ≈ 0,01–0,3 (faible variabilité, intervalles quasi-constants)
- Bot avec jitter gaussien : CV ≈ 0,3–0,7 (variabilité modérée mais inférieure à l'humain)
Signal 2 : Autocorrélation de décalage 1 (Lag-1)
L'autocorrélation de décalage 1 mesure la corrélation entre des intervalles consécutifs :
ρ₁ = corr(Δt_n, Δt_{n+1}) = Cov(Δt_n, Δt_{n+1}) / (σ_n × σ_{n+1})
Valeurs caractéristiques :
- Navigation humaine : ρ₁ ≈ 0 (intervalles indépendants — la durée de lecture d'une page n'est pas corrélée à celle de la suivante)
- Bot avec jitter proportionnel : ρ₁ ≈ 0,8+ (corrélé — le jitter est généré proportionnellement à l'intervalle de base, créant une dépendance entre intervalles consécutifs)
Ce signal distingue spécifiquement les bots utilisant un jitter proportionnel (ex. sleep(N + random() × N)) des bots avec jitter additif indépendant.
Signal 3 : Ratio rafale/pause
Proportion des intervalles Δt classifiés comme :
- Rafale : Δt < 100 ms (chargement parallèle de ressources dans le même rendu de page)
- Pause : Δt > 5 000 ms (temps de lecture d'une page)
- Intermédiaire : 100 ms ≤ Δt ≤ 5 000 ms (navigation entre pages)
Patterns caractéristiques :
- Navigateur réel : alternance rafale (chargement de page) → pause (lecture) → rafale, avec ratio rafale/pause ≈ 0,3–0,6
- Scraper séquentiel : exclusivement intermédiaire (requêtes espacées uniformément, ni rafale ni pause)
- Playwright/Puppeteer : rafales très courtes (ordre de grandeur < 50 ms, à valider empiriquement) mais pauses quasi-absentes
Signal 4 : Déviation par rapport à la loi de Benford
La loi de Benford (aussi appelée loi du premier chiffre, Benford, 1938) stipule que dans de nombreux jeux de données naturels, la probabilité que le premier chiffre significatif d'un nombre soit d vaut :
P(premier chiffre = d) = log₁₀(1 + 1/d)
Table de la loi de Benford :
| Chiffre d | Probabilité attendue |
|---|---|
| 1 | 30,1 % |
| 2 | 17,6 % |
| 3 | 12,5 % |
| 4 | 9,7 % |
| 5 | 7,9 % |
| 6 | 6,7 % |
| 7 | 5,8 % |
| 8 | 5,1 % |
| 9 | 4,6 % |
Les jeux de données naturels (durées de visite, revenus fiscaux, séquences physiques mesurées) suivent cette distribution ; les jeux de données artificiels (timers sleep(1000ms)) ne la suivent pas — ils concentrent les premiers chiffres sur des valeurs spécifiques.
Par exemple, un bot utilisant sleep(1000ms) génère exclusivement des intervalles Δt autour de 1000 ms, avec premier chiffre 1 en quasi-totalité. Un bot utilisant sleep(2500ms + jitter±200ms) surreprésente les chiffres 2 et 3.
Métrique de déviation :
D_benford = Σ_{d=1}^{9} |P_observée(d) - P_benford(d)|
= mean absolute deviation
Valeurs caractéristiques :
- Navigation humaine : D_benford ≈ 0,02–0,08 (faible déviation)
- Bot sleep(N) : D_benford ≈ 0,30–0,80 (forte concentration sur un chiffre)
Caveat : l'applicabilité de la loi de Benford aux intervalles inter-requêtes est une hypothèse heuristique, non démontrée mathématiquement pour ce cas d'usage. Les données d'intervalles (quelques ms à quelques secondes) ne couvrent pas nécessairement les ordres de grandeur requis par la loi. Les seuils ci-dessus sont empiriques et nécessitent une validation sur les données de production.
Implémentation
Table ClickHouse : agg_request_timing_1h
- Colonnes :
(src_ip, ja4, session_id, groupArray(timestamp_ns) AS ts_array)
Vue : view_thesis_features_1h
- Colonnes :
cadence_cv,lag1_autocorrelation,burst_ratio,benford_deviation,pause_ratio
SQL :
WITH deltas AS (
SELECT
session_id,
arrayMap(
(t1, t2) -> (t2 - t1) / 1e6, -- nanoseconds → milliseconds
arraySlice(ts_array, 1, length(ts_array) - 1),
arraySlice(ts_array, 2)
) AS delta_ms
FROM agg_request_timing_1h
WHERE length(ts_array) >= 3
),
stats AS (
SELECT
session_id,
arrayReduce('avg', delta_ms) AS mu,
arrayReduce('stddevPop', delta_ms) AS sigma,
countIf(d -> d < 100, delta_ms) AS burst_count,
countIf(d -> d > 5000, delta_ms) AS pause_count,
length(delta_ms) AS n
FROM deltas
ARRAY JOIN delta_ms AS d
GROUP BY session_id
)
SELECT
session_id,
sigma / nullIf(mu, 0) AS cadence_cv,
burst_count / n AS burst_ratio,
pause_count / n AS pause_ratio
-- lag1_autocorrelation et benford_deviation calculés en Python post-processing
FROM stats;
5.4 Détection de navigation synthétique par arbre de dépendances (Resource Dependency Tree) [impl.]
Constat et motivation
La feature asset_ratio détecte les bots qui ne chargent pas les ressources statiques (CSS, JS, images). Les frameworks de scraping modernes — Playwright, Puppeteer, Selenium — chargent toutes les ressources pour paraître identiques à un navigateur réel. Le signal n'est plus la présence du chargement de ressources, mais son ordre temporel.
Un navigateur réel parse le HTML, construit le Document Object Model (DOM), puis déclenche le chargement des ressources selon leur type et priorité. Ce processus crée des cascades waterfall caractéristiques. Playwright simule ce processus mais avec des différences temporelles mesurables.
Concept : cascade de dépendances de ressources
Le processus de chargement d'une page par un vrai navigateur suit cette séquence déterministe :
- Requête HTML : t = 0 ms — le navigateur demande le document HTML
- Parsing HTML : t = 0–50 ms — construction du DOM, identification des ressources
- CSS bloquant : t = 50–100 ms — les feuilles de style CSS référencées dans
<link rel="stylesheet">sont chargées en priorité absolue (bloquant le rendu) - Construction CSSOM : t = 100–150 ms — le CSS Object Model est construit à partir des CSS téléchargés
- JS différé et images : t = 150–300 ms — après la construction du CSSOM, les scripts différés (
defer,async) et les images sont chargés en parallèle
Cette structure produit deux signaux mesurables :
Signal 1 : root_to_first_asset_delay
Le délai entre la requête HTML et la première requête de ressource (CSS ou JS) est caractéristique :
- Navigateur réel : 50–200 ms (temps de parsing HTML réel)
- Playwright headless : délai attendu faible (< 50 ms, hypothèse nécessitant validation empirique sur notre infrastructure)
- Scraper avec chargement d'assets : 0 ms (requêtes séquentielles immédiates)
Signal 2 : asset_load_stddev
L'écart-type des timestamps de chargement au sein d'une cascade (batch de ressources déclenchées en parallèle) :
- Navigateur réel : faible dans le batch (chargement effectivement parallèle sur HTTP/2), mais les batches successifs sont séparés par des gaps caractéristiques
- Playwright : quasi-nul (toutes les ressources déclenchées simultanément par le moteur JS simulé)
- Scraper séquentiel : élevé (les ressources sont chargées une par une)
Implémentation
Tables ClickHouse :
CREATE TABLE agg_resource_cascade_1h
(
session_id String,
src_ip IPv4,
resource_type Enum('html', 'css', 'js', 'image', 'font', 'other'),
timestamp_ns UInt64,
path String
)
ENGINE = MergeTree()
ORDER BY (session_id, timestamp_ns)
TTL toDateTime(timestamp_ns / 1e9) + INTERVAL 2 HOUR;
Vue : view_resource_cascade_1h
Fichier SQL : 12_thesis_features.sql
Colonnes : root_to_first_asset_delay (ms), asset_load_stddev (ms)
Identification des ressources : double-source pour robustesse :
- En-tête Accept :
Accept: text/css→ CSS ;Accept: */*avecSec-Fetch-Dest: script→ JS - Extension de chemin : regex
\.(css|js|png|jpg|webp|woff2|svg)$
WITH html_times AS (
SELECT session_id, min(timestamp_ns) AS html_ts
FROM agg_resource_cascade_1h
WHERE resource_type = 'html'
GROUP BY session_id
),
first_asset AS (
SELECT session_id, min(timestamp_ns) AS first_asset_ts
FROM agg_resource_cascade_1h
WHERE resource_type IN ('css', 'js')
GROUP BY session_id
),
asset_stats AS (
SELECT session_id,
stddevPop(timestamp_ns / 1e6) AS asset_load_stddev_ms
FROM agg_resource_cascade_1h
WHERE resource_type != 'html'
GROUP BY session_id
)
SELECT
h.session_id,
(f.first_asset_ts - h.html_ts) / 1e6 AS root_to_first_asset_delay_ms,
a.asset_load_stddev_ms
FROM html_times h
LEFT JOIN first_asset f USING (session_id)
LEFT JOIN asset_stats a USING (session_id);
5.5 Analyse de dérive de fingerprint TLS intra-session (Intra-Session JA4 Drift) [impl.]
Constat et motivation
La feature distinct_ja4_count mesure la diversité globale des JA4 par IP sur une fenêtre temporelle. Elle détecte les bots qui changent fréquemment de JA4. Cependant, un attaquant sophistiqué peut maintenir un JA4 stable pendant la majeure partie de la session et ne le changer qu'à la transition de phase d'attaque — une stratégie de discrétion qui contourne distinct_ja4_count.
Cette technique analyse la dynamique temporelle de la dérive JA4 plutôt que sa magnitude globale.
Technique : analyse de dérive temporelle
- Segmentation : la session est découpée en fenêtres de 10 minutes (W₁, W₂, …, W_k)
- JA4 dominant par segment : pour chaque fenêtre, le JA4 le plus fréquent (mode) est retenu comme JA4 représentatif du segment
- Comptage des transitions : une transition est comptée quand le JA4 dominant change entre deux segments consécutifs
- Calcul du ratio :
drift_ratio = nb_transitions / (nb_segments - 1) ∈ [0, 1]
Valeurs caractéristiques :
- Navigation humaine : drift_ratio = 0 (même navigateur = même JA4 tout au long de la session)
- Bot en rotation simple : drift_ratio ≈ 1 (JA4 changeant à chaque segment)
- Bot APT multi-phase : drift_ratio faible (0,1–0,3) avec une transition unique corrélée à un changement comportemental
Détection APT (Advanced Persistent Threat)
La combinaison drift_ratio + post_ratio + changement de path_prefix permet de détecter les transitions reconnaissance → exploitation :
- Phase 1 (reconnaissance) : JA4_A, requêtes GET, chemins
/,/robots.txt,/sitemap.xml - Transition : changement de JA4 (JA4_A → JA4_B)
- Phase 2 (exploitation) : JA4_B, requêtes POST, chemins
/admin/login,/wp-json/wp/v2/users
Ce pattern — faible drift_ratio global avec une transition ponctuelle coïncidant avec un changement de post_ratio et de path_prefix dominant — est un indicateur fort d'activité APT coordonnée.
Implémentation
Vue ClickHouse : view_thesis_features_1h
- Colonne :
ja4_drift_ratio(Float32)
SQL :
WITH segmented AS (
SELECT
src_ip,
ja4_fingerprint,
-- Segment de 10 minutes
toStartOfInterval(event_time, INTERVAL 10 MINUTE) AS segment_ts,
count() AS hits_in_segment
FROM ja4_processing.sessions
WHERE event_time >= now() - INTERVAL 1 HOUR
GROUP BY src_ip, ja4_fingerprint, segment_ts
),
dominant_ja4 AS (
SELECT
src_ip,
segment_ts,
argMax(ja4_fingerprint, hits_in_segment) AS dominant_ja4
FROM segmented
GROUP BY src_ip, segment_ts
),
ordered AS (
SELECT
src_ip,
segment_ts,
dominant_ja4,
lagInFrame(dominant_ja4) OVER (
PARTITION BY src_ip
ORDER BY segment_ts
) AS prev_ja4,
count() OVER (PARTITION BY src_ip) AS nb_segments
FROM dominant_ja4
)
SELECT
src_ip,
sum(dominant_ja4 != prev_ja4 AND prev_ja4 IS NOT NULL) /
nullIf(max(nb_segments) - 1, 0) AS ja4_drift_ratio
FROM ordered
GROUP BY src_ip
HAVING max(nb_segments) >= 3; -- Minimum 3 segments (30+ minutes)
La condition HAVING max(nb_segments) >= 3 exclut les sessions trop courtes pour un calcul fiable de drift_ratio (voir §6.5 pour les limites). Les sessions exclues reçoivent la valeur NULL dans le vecteur feature, traitée par imputation avec la médiane de la population.
5.6 Corrélation DNS passive × flux HTTP (DNS Shadow Analysis) [todo]
Constat et motivation
L'architecture actuelle ne capture pas les requêtes DNS. Or, chaque première visite d'un navigateur vers un domaine est précédée d'une résolution DNS — le client interroge un résolveur récursif pour obtenir l'adresse IP du serveur. Les bots qui ciblent directement des IPs, utilisent un fichier /etc/hosts personnalisé, ou recourent au DNS-over-HTTPS (DoH) ne génèrent aucune requête DNS observable sur le réseau local.
Cette asymétrie constitue un signal exploitable : un flux HTTP sans résolution DNS préalable observable est suspect.
Technique : ratio DNS shadow
Capture DNS passive via ja4ebpf étendu au port UDP/53, puis corrélation avec les flux HTTP :
dns_shadow_ratio = requêtes HTTP vers hôte X / résolutions DNS de l'hôte X observées
Valeurs caractéristiques :
- Navigateur réel : ratio ≈ 1 (une résolution DNS → de multiples requêtes HTTP via connexions Keep-Alive réutilisées)
- Bot utilisant /etc/hosts ou DoH : ratio → ∞ (requêtes HTTP sans résolution DNS observable)
- CDN/proxy : ratio variable mais cohérent dans le temps
DNS-over-HTTPS (DoH)
DNS-over-HTTPS (RFC 8484) est un protocole qui envoie les requêtes DNS encapsulées dans des connexions HTTPS plutôt qu'en clair sur UDP/53. Ceci prévient l'observation passive des résolutions DNS. Fournisseurs courants : Cloudflare 1.1.1.1, Google 8.8.8.8.
Les bots utilisant DoH contournent l'analyse DNS shadow. Cependant, les connexions HTTPS vers 1.1.1.1 ou 8.8.8.8 depuis le réseau serveur peuvent elles-mêmes être corrélées comme indicateur d'utilisation de DoH par le client (si observable au niveau réseau) — cette corrélation indirecte est une atténuation partielle.
Extension JA4D
Le fingerprinting JA4D/JA4D6 des requêtes DHCP/DHCPv6 pourrait compléter l'analyse en identifiant les dispositifs derrière NAT, ajoutant une couche d'identification au-delà de l'adresse IP. Cette extension est envisagée comme travail futur conjoint à DNS Shadow Analysis.
[todo] Non implémenté : nécessite l'extension de ja4ebpf pour la capture UDP/53. Travail futur priorité 1 (voir §6.6).
5.7 Détection par invariant de ratio de compression (Compression Ratio Invariant) [todo]
Constat et motivation
La feature missing_accept_enc_ratio détecte l'absence de l'en-tête Accept-Encoding dans les requêtes HTTP. Certains bots incluent cet en-tête (Accept-Encoding: gzip, deflate, br) sans pour autant traiter effectivement la compression dans les réponses reçues. L'en-tête est présent pour paraître légitime, mais le bot traite les données brutes sans décompresser.
Technique : invariant de ratio de compression côté serveur
Instrumentation Apache (module custom) :
- Le serveur compare la taille de la réponse compressée envoyée avec la taille
Content-Lengthnon compressée - Le ratio de compression effectif par session est calculé :
ratio = taille_compressée / taille_non_compressée - Ce ratio est corrélé avec les requêtes suivantes du client : un client qui décompresse correctement peut traiter des réponses de taille variable sans délai anormal ; un bot qui ne décompresse pas lit des données plus petites (la réponse compressée) mais échoue à les interpréter correctement
Signal subtil : timing différentiel Brotli vs gzip
Brotli (RFC 7932) est un algorithme de compression développé par Google, offrant 20 à 26 % de meilleure compression que gzip sur du contenu web, au prix d'un coût computationnel de décompression plus élevé. Il est activé via Accept-Encoding: br.
gzip (RFC 1952) est le format DEFLATE-based largement supporté depuis HTTP/1.1, avec une décompression rapide et faible consommation CPU.
Un client réel est légèrement plus lent à traiter une réponse Brotli (décompression coûteuse) qu'une réponse gzip de même contenu. Ce délai différentiel est mesurable en quelques dizaines de millisecondes sur les connexions à faible latence. Un bot qui n'effectue pas de décompression répond à vitesse constante indépendamment de l'encodage — l'absence de différentiel est le signal.
[todo] Non implémenté : nécessite une instrumentation Apache pour le suivi de compression. Interférence avec la compression CDN pour les sessions has_xff=1. Travail futur priorité 2 (voir §6.6).
5.8 Empreinte comportementale de session multi-domaine (Cross-Domain Session Linking) [impl.]
Constat et motivation
Dans un environnement multi-hôtes (plusieurs Apache VirtualHosts sur le même serveur), les features sont calculées indépendamment par triplet (src_ip, ja4, host). Un attaquant scannant plusieurs domaines depuis la même IP présente un pattern inter-domaine caractéristique invisible dans l'analyse mono-domaine.
Cette technique fusionne les informations de sessions multi-domaines pour détecter les comportements de scan d'infrastructure systématique.
Métrique 1 : host_diversity
host_diversity = uniqExact(host) OVER (PARTITION BY src_ip)
Valeurs caractéristiques :
- Navigation humaine : 1–3 hôtes (un utilisateur visite rarement plus de 3 domaines hébergés sur la même infrastructure)
- Scanner d'infrastructure : 10+ hôtes (balayage systématique de tous les VirtualHosts)
Métrique 2 : host_sweep_speed
host_sweep_speed = uniqExact(host) / session_duration_seconds
Un scanner parcourt rapidement de nombreux hôtes différents. Un utilisateur humain reste longtemps sur un même hôte. La dimension temporelle distingue un utilisateur occasionnel multi-domaine d'un scanner actif.
Métrique 3 : host_coverage_uniformity
host_coverage_uniformity = 1 − stddev(hits_per_host) / avg(hits_per_host)
Un scanner distribue ses requêtes uniformément entre les hôtes (chaque hôte reçoit approximativement le même nombre de requêtes de reconnaissance). Un utilisateur humain se concentre sur 1–2 hôtes avec une distribution très inégale.
Valeurs caractéristiques :
- Scanner : host_coverage_uniformity ≈ 0,8–1,0 (distribution uniforme)
- Utilisateur humain : host_coverage_uniformity ≈ 0,1–0,4 (très inégal)
Métrique 4 : cross_domain_path_similarity (coefficient de Jaccard)
Le coefficient de Jaccard mesure la similarité entre deux ensembles :
Jaccard(A, B) = |A ∩ B| / |A ∪ B|
où :
- |A ∩ B| = nombre de chemins apparaissant sur les deux hôtes A et B
- |A ∪ B| = nombre total de chemins distincts sur les deux hôtes réunis
- La valeur est dans [0, 1] : 0 = aucun chemin commun, 1 = ensembles identiques
Un scanner utilisant une wordlist commune (/admin, /wp-login.php, /.env, /phpinfo.php, /.git/config) sur tous les hôtes produit un Jaccard élevé (0,7–1,0). Une navigation humaine sur des sites distincts produit un Jaccard proche de 0.
Implémentation SQL :
WITH paths_by_host AS (
SELECT
src_ip,
host,
groupArray(DISTINCT path) AS paths
FROM ja4_processing.sessions
WHERE event_time >= now() - INTERVAL 1 HOUR
GROUP BY src_ip, host
),
host_pairs AS (
SELECT
a.src_ip,
a.host AS host_a,
b.host AS host_b,
arrayIntersect(a.paths, b.paths) AS common_paths,
arrayDistinct(arrayConcat(a.paths, b.paths)) AS union_paths
FROM paths_by_host a
JOIN paths_by_host b
ON a.src_ip = b.src_ip AND a.host < b.host
)
SELECT
src_ip,
avg(length(common_paths) / nullIf(length(union_paths), 0))
AS cross_domain_path_similarity
FROM host_pairs
GROUP BY src_ip;
Table récapitulative des features Cross-Domain :
| Feature | Type | Description | Statut |
|---|---|---|---|
host_diversity |
UInt16 | Nombre d'hôtes distincts par IP | [impl.] |
host_sweep_speed |
Float32 | Hôtes distincts / durée session (hôtes/s) | [impl.] |
host_coverage_uniformity |
Float32 | 1 − stddev/avg des hits par hôte | [impl.] |
cross_domain_path_similarity |
Float32 | Jaccard moyen entre paires d'hôtes | [impl.] |
Ces quatre features sont intégrées dans le vecteur feature du Modèle Complet (famille F8 — Multi-domaine).