feat(dashboard): sélecteur de plage temporelle sur /campaigns
Avant : toutes les vues de campagnes étaient fixes à 7 jours.
Après : sélecteur 1j / 7j (défaut) / 14j / 30j / 90j en haut à droite.
- Ajout du paramètre ?days= (1–90, défaut 7) à :
GET /api/campaigns
GET /api/campaigns/graph
GET /api/campaigns/scatter
GET /api/campaigns/{cid}
- Le sélecteur recharge simultanément les 3 vues (cartes, scatter, graphe)
et le panneau de détail avec la même fenêtre temporelle
- Le compteur de campagnes indique la plage active : (4 campagnes — 30j)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -911,8 +911,9 @@ async def classifications() -> dict[str, Any]:
|
||||
# GET /api/campaigns — HDBSCAN bot campaign clusters
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/campaigns")
|
||||
async def campaigns() -> dict[str, Any]:
|
||||
async def campaigns(days: int = 7) -> dict[str, Any]:
|
||||
"""Campagnes de bots détectées par clustering HDBSCAN."""
|
||||
days = max(1, min(days, 90))
|
||||
try:
|
||||
rows = query(
|
||||
f"SELECT campaign_id, "
|
||||
@ -930,9 +931,10 @@ async def campaigns() -> dict[str, Any]:
|
||||
f"avg(post_ratio) AS avg_post_ratio "
|
||||
f"FROM {_DB}.ml_detected_anomalies "
|
||||
"WHERE campaign_id >= 0 "
|
||||
"AND detected_at >= now() - INTERVAL 7 DAY "
|
||||
"AND detected_at >= now() - INTERVAL {days:UInt16} DAY "
|
||||
"GROUP BY campaign_id "
|
||||
"ORDER BY members DESC LIMIT 50"
|
||||
"ORDER BY members DESC LIMIT 50",
|
||||
{"days": days},
|
||||
)
|
||||
return {"campaigns": rows}
|
||||
except Exception as exc:
|
||||
@ -944,8 +946,9 @@ async def campaigns() -> dict[str, Any]:
|
||||
# GET /api/campaigns/graph — Network graph data (shared JA4/ASN links)
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/campaigns/graph")
|
||||
async def campaigns_graph() -> dict[str, Any]:
|
||||
async def campaigns_graph(days: int = 7) -> dict[str, Any]:
|
||||
"""Données de graphe réseau : nœuds (IPs) et liens (JA4/ASN partagés)."""
|
||||
days = max(1, min(days, 90))
|
||||
try:
|
||||
# Nœuds : chaque IP avec ses attributs principaux
|
||||
nodes = query(
|
||||
@ -958,10 +961,11 @@ async def campaigns_graph() -> dict[str, Any]:
|
||||
f"min(anomaly_score) AS worst_score "
|
||||
f"FROM {_DB}.ml_detected_anomalies "
|
||||
"WHERE campaign_id >= 0 "
|
||||
"AND detected_at >= now() - INTERVAL 7 DAY "
|
||||
"AND detected_at >= now() - INTERVAL {days:UInt16} DAY "
|
||||
"GROUP BY src_ip, campaign_id "
|
||||
"ORDER BY campaign_id, worst_score ASC "
|
||||
"LIMIT 500"
|
||||
"LIMIT 500",
|
||||
{"days": days},
|
||||
)
|
||||
# Liens : IPs partageant le même JA4 dans la même campagne
|
||||
edges = query(
|
||||
@ -974,9 +978,10 @@ async def campaigns_graph() -> dict[str, Any]:
|
||||
"ON a.ja4 = b.ja4 AND a.campaign_id = b.campaign_id "
|
||||
"WHERE a.campaign_id >= 0 "
|
||||
"AND a.src_ip < b.src_ip "
|
||||
"AND a.detected_at >= now() - INTERVAL 7 DAY "
|
||||
"AND b.detected_at >= now() - INTERVAL 7 DAY "
|
||||
"LIMIT 2000"
|
||||
"AND a.detected_at >= now() - INTERVAL {days:UInt16} DAY "
|
||||
"AND b.detected_at >= now() - INTERVAL {days:UInt16} DAY "
|
||||
"LIMIT 2000",
|
||||
{"days": days},
|
||||
)
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
except Exception as exc:
|
||||
@ -988,8 +993,9 @@ async def campaigns_graph() -> dict[str, Any]:
|
||||
# GET /api/campaigns/scatter — Scatter plot data (score vs velocity per IP)
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/campaigns/scatter")
|
||||
async def campaigns_scatter() -> dict[str, Any]:
|
||||
async def campaigns_scatter(days: int = 7) -> dict[str, Any]:
|
||||
"""Données scatter plot : score vs vélocité par IP, coloré par campagne."""
|
||||
days = max(1, min(days, 90))
|
||||
try:
|
||||
rows = query(
|
||||
f"SELECT toString(src_ip) AS ip, "
|
||||
@ -1002,9 +1008,10 @@ async def campaigns_scatter() -> dict[str, Any]:
|
||||
f"any(threat_level) AS threat "
|
||||
f"FROM {_DB}.ml_detected_anomalies "
|
||||
"WHERE campaign_id >= 0 "
|
||||
"AND detected_at >= now() - INTERVAL 7 DAY "
|
||||
"AND detected_at >= now() - INTERVAL {days:UInt16} DAY "
|
||||
"GROUP BY src_ip, campaign_id "
|
||||
"ORDER BY score ASC LIMIT 500"
|
||||
"ORDER BY score ASC LIMIT 500",
|
||||
{"days": days},
|
||||
)
|
||||
return {"data": rows}
|
||||
except Exception as exc:
|
||||
@ -1016,8 +1023,9 @@ async def campaigns_scatter() -> dict[str, Any]:
|
||||
# GET /api/campaigns/{cid} — Campaign detail (member IPs + features)
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/campaigns/{cid}")
|
||||
async def campaign_detail(cid: int) -> dict[str, Any]:
|
||||
async def campaign_detail(cid: int, days: int = 7) -> dict[str, Any]:
|
||||
"""Détail d'une campagne : IPs membres, features comportementales, timeline."""
|
||||
days = max(1, min(days, 90))
|
||||
try:
|
||||
members = query(
|
||||
f"SELECT toString(src_ip) AS src_ip, ja4, host, "
|
||||
@ -1028,9 +1036,9 @@ async def campaign_detail(cid: int) -> dict[str, Any]:
|
||||
f"browser_family, bot_name, detected_at, reason "
|
||||
f"FROM {_DB}.ml_detected_anomalies "
|
||||
"WHERE campaign_id = {cid:Int32} "
|
||||
"AND detected_at >= now() - INTERVAL 7 DAY "
|
||||
"AND detected_at >= now() - INTERVAL {days:UInt16} DAY "
|
||||
"ORDER BY anomaly_score ASC LIMIT 200",
|
||||
{"cid": cid},
|
||||
{"cid": cid, "days": days},
|
||||
)
|
||||
# Profil agrégé de la campagne
|
||||
profile = query(
|
||||
@ -1049,8 +1057,8 @@ async def campaign_detail(cid: int) -> dict[str, Any]:
|
||||
f"min(detected_at) AS first_seen, max(detected_at) AS last_seen "
|
||||
f"FROM {_DB}.ml_detected_anomalies "
|
||||
"WHERE campaign_id = {cid:Int32} "
|
||||
"AND detected_at >= now() - INTERVAL 7 DAY",
|
||||
{"cid": cid},
|
||||
"AND detected_at >= now() - INTERVAL {days:UInt16} DAY",
|
||||
{"cid": cid, "days": days},
|
||||
)
|
||||
# Timeline horaire de la campagne
|
||||
timeline = query(
|
||||
@ -1058,9 +1066,9 @@ async def campaign_detail(cid: int) -> dict[str, Any]:
|
||||
f"count() AS detections, uniqExact(src_ip) AS active_ips "
|
||||
f"FROM {_DB}.ml_detected_anomalies "
|
||||
"WHERE campaign_id = {cid:Int32} "
|
||||
"AND detected_at >= now() - INTERVAL 7 DAY "
|
||||
"AND detected_at >= now() - INTERVAL {days:UInt16} DAY "
|
||||
"GROUP BY hour ORDER BY hour",
|
||||
{"cid": cid},
|
||||
{"cid": cid, "days": days},
|
||||
)
|
||||
return {
|
||||
"campaign_id": cid,
|
||||
|
||||
Reference in New Issue
Block a user