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 @@ + + + +
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 +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_*)
+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.
+ 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
+La valeur du frame WINDOW_UPDATE est l'empreinte H2 la plus fiable car + chaque navigateur utilise une valeur distincte et constante :
+Source : view_ai_features_1h.h2_window_*
+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 :
+Source : view_ai_features_1h.tls_h2_family_mismatch
+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.
+| IP | +JA4 famille | +H2 famille | +WU value | +Pseudo-order | +Hits | +Cohérence | +
|---|---|---|---|---|---|---|
| Chargement… | ||||||
L'ordre des pseudo-headers (:method, :authority,
+ :scheme, :path) est spécifique à chaque navigateur :
Un ordre non répertorié indique un outil ou une version rare.
+Source : view_ai_features_1h (h2_order_chromesafari, h2_order_firefox)
+m,a,s,pm,p,s,a