diff --git a/services/dashboard/backend/routes/api.py b/services/dashboard/backend/routes/api.py index b928ca9..b6508b2 100644 --- a/services/dashboard/backend/routes/api.py +++ b/services/dashboard/backend/routes/api.py @@ -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, diff --git a/services/dashboard/backend/templates/campaigns.html b/services/dashboard/backend/templates/campaigns.html index 9a49a6a..5b46f93 100644 --- a/services/dashboard/backend/templates/campaigns.html +++ b/services/dashboard/backend/templates/campaigns.html @@ -40,6 +40,14 @@ Campagnes de bots