feat(dashboard): page Browser Signature Detection (/browsers)

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>
This commit is contained in:
toto
2026-04-10 14:02:39 +02:00
parent e52cdcc01f
commit 9c308747bd
4 changed files with 493 additions and 0 deletions

View File

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