feat(dashboard): browser signatures management UI

- Ajoute dict_browser_h2 dans /reflists (lecture seule via dict_browser_h2)
- Nouveaux endpoints API :
    GET  /api/browser-signatures/entries — liste browser_h2_signatures
         (fallback dict CSV si migration 06 non appliquée)
    POST /api/browser-signatures/entries — ajout fingerprint + reload dict
    DELETE /api/browser-signatures/entries — suppression + reload dict
- Page /browsers : 2 nouvelles sections
    'Base de signatures H2' — tableau des 10 fingerprints, form d'ajout,
    mode lecture seule automatique si migration 06 non appliquée
    'Règles de scoring browser_matcher.py' — tableau statique des 7 dimensions
    (poids, valeurs par famille, seuils de bypass)
- Integration : browser_h2.csv copié dans user_files au démarrage ClickHouse

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
toto
2026-04-10 14:46:07 +02:00
parent da1b579d4f
commit fde6864311
4 changed files with 386 additions and 1 deletions

View File

@ -1505,6 +1505,7 @@ _REFLIST_SORT = {
"bot_ip": {"prefix", "bot_name"},
"bot_ja4": {"ja4", "bot_name"},
"browser_ja4": {"ja4", "browser_family", "tls_library"},
"browser_h2": {"h2_fingerprint", "browser_family"},
"asn_reputation": {"src_asn", "label"},
"iplocate_asn": {"asn", "country_code", "name", "network"},
"anubis_ip_rules": {"prefix", "bot_name", "action", "category"},
@ -1515,6 +1516,7 @@ _REFLIST_SEARCH_COLS: dict[str, list[str]] = {
"bot_ip": ["prefix", "bot_name"],
"bot_ja4": ["ja4", "bot_name"],
"browser_ja4": ["ja4", "browser_family", "tls_library", "context"],
"browser_h2": ["h2_fingerprint", "browser_family"],
"asn_reputation": ["toString(src_asn)", "label"],
"iplocate_asn": ["network", "toString(asn)", "country_code", "name"],
"anubis_ip_rules": ["prefix", "bot_name", "action", "category"],
@ -1529,6 +1531,10 @@ _REFLIST_QUERIES: dict[str, str] = {
f"SELECT ja4, browser_family, tls_library, context "
f"FROM dictionary('{_DB}.dict_browser_ja4')"
),
"browser_h2": (
f"SELECT h2_fingerprint, browser_family "
f"FROM dictionary('{_DB}.dict_browser_h2') ORDER BY browser_family"
),
"asn_reputation": (
f"SELECT src_asn, label FROM dictionary('{_DB}.dict_asn_reputation')"
),
@ -1786,3 +1792,108 @@ async def browser_signatures() -> dict[str, Any]:
return result
# ---------------------------------------------------------------------------
# GET /api/browser-signatures/entries — liste des fingerprints H2 gérés
# POST /api/browser-signatures/entries — ajouter un fingerprint H2
# DELETE /api/browser-signatures/entries — supprimer un fingerprint H2
# ---------------------------------------------------------------------------
class BrowserH2Entry(BaseModel):
"""Nouveau fingerprint H2 à enregistrer dans browser_h2_signatures."""
h2_fingerprint: str
browser_family: str
confidence: float = 1.0
notes: str = ""
_VALID_BROWSER_FAMILIES = {"Chrome", "Firefox", "Safari", "Edge", "Other"}
@router.get("/browser-signatures/entries")
async def browser_sig_entries() -> dict[str, Any]:
"""Retourne le contenu de la table browser_h2_signatures.
Si la table n'existe pas encore (migration 06 non appliquée),
retourne les données du dictionnaire CSV (sans confidence/notes).
"""
# Essai prioritaire : table structurée (post-migration 06)
try:
rows = query(
f"SELECT h2_fingerprint, browser_family, confidence, notes "
f"FROM {_DB}.browser_h2_signatures "
f"ORDER BY browser_family, confidence DESC"
)
return {"entries": rows, "total": len(rows), "source": "table"}
except Exception:
pass
# Fallback : dictionnaire CSV (pré-migration 06)
try:
rows = query(
f"SELECT h2_fingerprint, browser_family, "
f"toFloat32(1.0) AS confidence, '' AS notes "
f"FROM dictionary('{_DB}.dict_browser_h2') "
f"ORDER BY browser_family"
)
return {"entries": rows, "total": len(rows), "source": "dict_csv", "readonly": True}
except Exception as exc:
logger.exception("browser_h2 entries fallback failed")
raise HTTPException(status_code=500, detail=str(exc))
@router.post("/browser-signatures/entries", status_code=201)
async def browser_sig_add(body: BrowserH2Entry) -> dict[str, Any]:
"""Ajoute un fingerprint H2 dans browser_h2_signatures et recharge le dictionnaire."""
if not body.h2_fingerprint.strip():
raise HTTPException(status_code=422, detail="h2_fingerprint ne peut pas être vide")
if body.browser_family not in _VALID_BROWSER_FAMILIES:
raise HTTPException(
status_code=422,
detail=f"browser_family doit être l'un de {_VALID_BROWSER_FAMILIES}",
)
if not 0.0 <= body.confidence <= 1.0:
raise HTTPException(status_code=422, detail="confidence doit être entre 0.0 et 1.0")
try:
execute(
f"INSERT INTO {_DB}.browser_h2_signatures "
"(h2_fingerprint, browser_family, confidence, notes) VALUES "
"({fp:String}, {fam:String}, {conf:Float32}, {notes:String})",
{
"fp": body.h2_fingerprint.strip(),
"fam": body.browser_family,
"conf": body.confidence,
"notes": body.notes,
},
)
# Force le rechargement du dictionnaire
try:
execute(f"SYSTEM RELOAD DICTIONARY {_DB}.dict_browser_h2")
except Exception:
logger.warning("dict_browser_h2 reload failed (migration 06 peut-être non appliquée)")
return {"status": "ok", "h2_fingerprint": body.h2_fingerprint.strip()}
except Exception as exc:
logger.exception("browser_h2_signatures insert failed")
raise HTTPException(status_code=500, detail=str(exc))
@router.delete("/browser-signatures/entries")
async def browser_sig_delete(fingerprint: str = Query(...)) -> dict[str, Any]:
"""Supprime un fingerprint H2 de browser_h2_signatures et recharge le dictionnaire."""
if not fingerprint.strip():
raise HTTPException(status_code=422, detail="fingerprint ne peut pas être vide")
try:
execute(
f"ALTER TABLE {_DB}.browser_h2_signatures DELETE "
"WHERE h2_fingerprint = {fp:String}",
{"fp": fingerprint.strip()},
)
try:
execute(f"SYSTEM RELOAD DICTIONARY {_DB}.dict_browser_h2")
except Exception:
logger.warning("dict_browser_h2 reload failed")
return {"status": "ok", "deleted": fingerprint.strip()}
except Exception as exc:
logger.exception("browser_h2_signatures delete failed")
raise HTTPException(status_code=500, detail=str(exc))