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:
@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user