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