From 9c308747bd744bd7b095683a0838cbb77898f5f3 Mon Sep 17 00:00:00 2001 From: toto Date: Fri, 10 Apr 2026 14:02:39 +0200 Subject: [PATCH] feat(dashboard): page Browser Signature Detection (/browsers) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouvelle page dédiée à l'analyse passive des signatures navigateur (§4) : API — GET /api/browsers : Requête view_ai_features_1h pour : - Compteurs globaux (total, sessions_with_h2, matched, mismatch %) - Distribution h2_dict_family (Chrome/Firefox/Safari/Edge) - Répartition des signaux WINDOW_UPDATE (chrome/firefox/safari/absent/autre) - Mismatch TLS↔H2 par famille JA4 (total + count + %) - Top 20 sessions suspectes (tls_h2_family_mismatch=1, triées par hits) Page /browsers : - 6 KPI header (sessions, avec H2, famille connue, taux match, mismatch, % mismatch) - Doc banner expliquant browser_matcher §4 et le mode DUAL_MODE - Donut : familles H2 (dict_browser_h2 lookup) - Bar horizontal : WINDOW_UPDATE signals par famille - Bar groupé + ligne : mismatch TLS↔H2 par famille JA4 (count + %) - Table : top 20 imposteurs potentiels avec IP cliquable, pseudo-order, cohérence - Mini-KPIs : ordres pseudo-headers Chrome/Safari, Firefox, inconnu, PRIORITY frames - Lien nav 'Navigateurs' dans le groupe Surveillance de base.html Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- services/dashboard/backend/routes/api.py | 103 +++++ services/dashboard/backend/routes/pages.py | 5 + .../dashboard/backend/templates/base.html | 4 + .../dashboard/backend/templates/browsers.html | 381 ++++++++++++++++++ 4 files changed, 493 insertions(+) create mode 100644 services/dashboard/backend/templates/browsers.html diff --git a/services/dashboard/backend/routes/api.py b/services/dashboard/backend/routes/api.py index b6508b2..514c89a 100644 --- a/services/dashboard/backend/routes/api.py +++ b/services/dashboard/backend/routes/api.py @@ -1683,3 +1683,106 @@ async def health_metrics() -> dict[str, Any]: "avg_anomaly_rate": round(avg_anomaly, 4), "avg_latency_ms": round(avg_latency), } + + +@router.get("/browsers") +async def browsers() -> dict[str, Any]: + """Analyse des signatures navigateur passives (browser_matcher, §4). + + Exploite les colonnes H2 de view_ai_features_1h : + h2_dict_family, h2_window_{chrome,firefox,safari,absent}, + tls_h2_family_mismatch, h2_order_*, h2_settings_known. + """ + result: dict[str, Any] = { + "stats": {}, + "h2_families": [], + "h2_window_signals": [], + "mismatch_by_family": [], + "top_mismatches": [], + } + + # ── Compteurs globaux ────────────────────────────────────────────────── + try: + stats = query( + f"SELECT " + f" count() AS total_sessions, " + f" countIf(h2_settings_known > 0) AS sessions_with_h2, " + f" countIf(h2_dict_family != '') AS sessions_matched, " + f" countIf(tls_h2_family_mismatch > 0) AS sessions_mismatch, " + f" round(100.0 * countIf(h2_dict_family != '') / greatest(countIf(h2_settings_known > 0), 1), 1) AS match_rate, " + f" round(100.0 * countIf(tls_h2_family_mismatch > 0) / greatest(countIf(h2_settings_known > 0), 1), 1) AS mismatch_rate " + f"FROM {_DB}.view_ai_features_1h" + ) + if stats: + result["stats"] = stats[0] + except Exception: + logger.debug("view_ai_features_1h unavailable for /api/browsers stats") + + # ── Distribution des familles H2 (dict_browser_h2) ──────────────────── + try: + families = query( + f"SELECT h2_dict_family AS family, count() AS sessions " + f"FROM {_DB}.view_ai_features_1h " + f"WHERE h2_dict_family != '' " + f"GROUP BY family ORDER BY sessions DESC" + ) + result["h2_families"] = families + except Exception: + pass + + # ── Répartition des signaux WINDOW_UPDATE H2 ────────────────────────── + try: + wu_rows = query( + f"SELECT " + f" 'Chrome (WU≈15663105)' AS signal, countIf(h2_window_chrome > 0) AS sessions FROM {_DB}.view_ai_features_1h " + f"UNION ALL " + f"SELECT 'Firefox (WU≈12517377)', countIf(h2_window_firefox > 0) FROM {_DB}.view_ai_features_1h " + f"UNION ALL " + f"SELECT 'Safari (WU≈10485760)', countIf(h2_window_safari > 0) FROM {_DB}.view_ai_features_1h " + f"UNION ALL " + f"SELECT 'Absent (outil/curl)', countIf(h2_window_absent > 0) FROM {_DB}.view_ai_features_1h " + f"UNION ALL " + f"SELECT 'Autre / go net/http', countIf(h2_settings_known > 0 AND h2_window_chrome = 0 AND h2_window_firefox = 0 AND h2_window_safari = 0 AND h2_window_absent = 0) FROM {_DB}.view_ai_features_1h" + ) + result["h2_window_signals"] = wu_rows + except Exception: + pass + + # ── Mismatch TLS↔H2 par famille JA4 ────────────────────────────────── + try: + mismatches = query( + f"SELECT browser_family AS ja4_family, " + f" count() AS total, " + f" countIf(tls_h2_family_mismatch > 0) AS mismatches, " + f" round(100.0 * countIf(tls_h2_family_mismatch > 0) / count(), 1) AS mismatch_pct " + f"FROM {_DB}.view_ai_features_1h " + f"WHERE browser_family != '' " + f"GROUP BY ja4_family ORDER BY mismatches DESC" + ) + result["mismatch_by_family"] = mismatches + except Exception: + pass + + # ── Top 20 sessions suspectes (mismatch TLS↔H2 confirmé) ───────────── + try: + suspects = query( + f"SELECT " + f" replaceRegexpOne(toString(src_ip), '^::ffff:', '') AS ip, " + f" ja4, " + f" browser_family AS ja4_family, " + f" h2_dict_family, " + f" h2_window_update_value AS wu_value, " + f" hits, " + f" h2_pseudo_ord_raw AS pseudo_order, " + f" fingerprint_coherence_score AS coherence " + f"FROM {_DB}.view_ai_features_1h " + f"WHERE tls_h2_family_mismatch > 0 " + f"ORDER BY hits DESC " + f"LIMIT 20" + ) + result["top_mismatches"] = suspects + except Exception: + pass + + return result + diff --git a/services/dashboard/backend/routes/pages.py b/services/dashboard/backend/routes/pages.py index d151745..557df68 100644 --- a/services/dashboard/backend/routes/pages.py +++ b/services/dashboard/backend/routes/pages.py @@ -91,3 +91,8 @@ async def fleet_page(request: Request): @router.get("/health") async def health_page(request: Request): return templates.TemplateResponse("health.html", _ctx(request, "health")) + + +@router.get("/browsers") +async def browsers_page(request: Request): + return templates.TemplateResponse("browsers.html", _ctx(request, "browsers")) diff --git a/services/dashboard/backend/templates/base.html b/services/dashboard/backend/templates/base.html index 98174c7..853b6b6 100644 --- a/services/dashboard/backend/templates/base.html +++ b/services/dashboard/backend/templates/base.html @@ -159,6 +159,10 @@ Flottes + + + Navigateurs + diff --git a/services/dashboard/backend/templates/browsers.html b/services/dashboard/backend/templates/browsers.html new file mode 100644 index 0000000..8c3b01e --- /dev/null +++ b/services/dashboard/backend/templates/browsers.html @@ -0,0 +1,381 @@ +{% extends "base.html" %} +{% block title %}JA4 SOC — Browser Signatures{% endblock %} +{% block page_title %} + Browser Signature Detection +
+

Détection passive des navigateurs (§4)

+

Analyse croisée du fingerprint HTTP/2 (SETTINGS, WINDOW_UPDATE, pseudo-headers) + et du JA4 TLS pour identifier les vrais navigateurs et détecter les imposteurs.

+

TLS↔H2 mismatch : sessions où le JA4 identifie une famille + (ex: Chromium) mais les SETTINGS H2 appartiennent à une autre (ex: Firefox). + Signal fort d'un outil qui émule un TLS de navigateur sans répliquer le H2.

+

Source : view_ai_features_1h (dict_browser_h2, h2_window_*)

+
+{% endblock %} +{% block content %} +
+ + +
+
+
+
Sessions (24h)
+
+
+
+
Avec données H2
+
+
+
+
Famille H2 connue
+
+
+
+
Taux match H2
+
+
+
+
Mismatch TLS↔H2
+
+
+
+
% Mismatch / H2
+
+
+ + +
+ Browser Signature Detection §4 — Le moteur + browser_matcher score chaque session sur 7 dimensions : + H2 SETTINGS (0.30), WINDOW_UPDATE (0.15), + pseudo-headers (0.15), H2 PRIORITY (0.10), HTTP headers (0.15), + structure TLS (0.10), dictionnaire JA4 (0.05). + Un mismatch TLS↔H2 est détecté quand le JA4 (couche TLS) + identifie Chrome mais le WINDOW_UPDATE H2 est celui de Firefox (ou vice-versa) — signature + d'un outil qui copie le TLS sans répliquer fidèlement le H2. + En mode DUAL_MODE (défaut), les décisions sont journalisées sans modifier le bypass — + activer BROWSER_MATCHER_REPLACE=true pour basculer. +
+ + +
+ + +
+
+ + Familles détectées — dict H2 +
+

Familles H2 (dict_browser_h2)

+

Correspondance du fingerprint SETTINGS H2 complet (format Akamai) + avec le dictionnaire dict_browser_h2. Une correspondance + exacte identifie Chrome, Firefox, Safari ou Edge avec un haut degré + de certitude.

+

Source : view_ai_features_1h.h2_dict_family

+
+
+
+
+
+
+
+ + +
+
+ + Signal WINDOW_UPDATE H2 +
+

Valeur WINDOW_UPDATE par famille

+

La valeur du frame WINDOW_UPDATE est l'empreinte H2 la plus fiable car + chaque navigateur utilise une valeur distincte et constante :

+
    +
  • Chrome : 15 663 105
  • +
  • Firefox : 12 517 377
  • +
  • Safari : 10 485 760
  • +
  • curl/httpx : absent (0)
  • +
+

Source : view_ai_features_1h.h2_window_*

+
+
+
+
+
+
+
+ +
+ + +
+ + +
+
+ + Mismatch TLS↔H2 par famille JA4 +
+

Incohérences cross-layer

+

Proportion de sessions dont la famille JA4 (TLS) contredit la famille H2. + Un taux élevé pour "Chromium" indique des outils qui imitent Chrome TLS + sans reproduire le comportement H2.

+

Exemples de mismatch :

+
    +
  • JA4=Chromium + WU=12517377 → Firefox H2
  • +
  • JA4=Firefox + WU=15663105 → Chrome H2
  • +
  • JA4=Navigateur + WU absent → outil sans H2
  • +
+

Source : view_ai_features_1h.tls_h2_family_mismatch

+
+
+
+
+
+
+
+ + +
+
+ + Sessions suspectes (mismatch confirmé) +
+

Imposteurs potentiels

+

Sessions dont le JA4 (TLS) et les SETTINGS H2 identifient des familles + différentes — signal fort d'un outil qui émule le TLS d'un navigateur + mais trahit son origine via le H2.

+

Action : Investiguer l'IP, vérifier le JA4 dans la page + Détections, ou ajouter à la liste de blocage.

+
+
+
+
+ + + + + + + + + + + + + + + +
IPJA4 familleH2 familleWU valuePseudo-orderHitsCohérence
Chargement…
+
+
+ +
+ + +
+
+ + Distribution des ordres pseudo-headers H2 +
+

Ordre des pseudo-headers HTTP/2

+

L'ordre des pseudo-headers (:method, :authority, + :scheme, :path) est spécifique à chaque navigateur :

+
    +
  • m,a,s,p — Chrome et Safari
  • +
  • m,p,s,a — Firefox
  • +
+

Un ordre non répertorié indique un outil ou une version rare.

+

Source : view_ai_features_1h (h2_order_chromesafari, h2_order_firefox)

+
+
+
+
+
+ +
+
Chrome / Safari — m,a,s,p
+
+
sessions
+
+
+
Firefox — m,p,s,a
+
+
sessions
+
+
+
H2 présent — ordre non répertorié
+
+
sessions (outil probable)
+
+
+
H2 PRIORITY frames présents
+
+
sessions (Firefox ancien)
+
+
+
+
+ +
+ + +{% endblock %}