fix(dashboard): campaigns scatter chart — show campaigns not IPs

- API /api/campaigns/scatter: aggregate by campaign_id instead of per-IP
  Returns avg_score, avg_velocity, unique_ips, ja4_list, asn_list, country_list
- Template: one bubble per campaign, sized by IP count
- Tooltip: campaign-level info (IPs, score, velocity, ASNs, pays, JA4s)
- Click navigates to campaign detail (not IP detail)
- Updated doc panel text

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
toto
2026-04-10 15:09:02 +02:00
parent fb73c60e7d
commit 261205028d
2 changed files with 50 additions and 42 deletions

View File

@ -990,27 +990,30 @@ async def campaigns_graph(days: int = 7) -> dict[str, Any]:
# ---------------------------------------------------------------------------
# GET /api/campaigns/scatter — Scatter plot data (score vs velocity per IP)
# GET /api/campaigns/scatter — Scatter plot : une bulle par campagne
# ---------------------------------------------------------------------------
@router.get("/campaigns/scatter")
async def campaigns_scatter(days: int = 7) -> dict[str, Any]:
"""Données scatter plot : score vs vélocité par IP, coloré par campagne."""
"""Données scatter plot : score vs vélocité agrégés par campagne."""
days = max(1, min(days, 90))
try:
rows = query(
f"SELECT toString(src_ip) AS ip, "
f"campaign_id, "
f"min(anomaly_score) AS score, "
f"avg(hit_velocity) AS velocity, "
f"SELECT campaign_id, "
f"avg(anomaly_score) AS avg_score, "
f"avg(hit_velocity) AS avg_velocity, "
f"sum(hits) AS total_hits, "
f"any(ja4) AS ja4, "
f"any(asn_org) AS asn_org, "
f"any(threat_level) AS threat "
f"uniqExact(src_ip) AS unique_ips, "
f"groupUniqArray(5)(ja4) AS ja4_list, "
f"groupUniqArray(3)(asn_org) AS asn_list, "
f"groupUniqArray(3)(country_code) AS country_list, "
f"any(threat_level) AS threat, "
f"min(detected_at) AS first_seen, "
f"max(detected_at) AS last_seen "
f"FROM {_DB}.ml_detected_anomalies "
"WHERE campaign_id >= 0 "
"AND detected_at >= now() - INTERVAL {days:UInt16} DAY "
"GROUP BY src_ip, campaign_id "
"ORDER BY score ASC LIMIT 500",
"GROUP BY campaign_id "
"ORDER BY avg_score ASC LIMIT 100",
{"days": days},
)
return {"data": rows}