diff --git a/services/dashboard/backend/routes/api.py b/services/dashboard/backend/routes/api.py index 611b879..826eebc 100644 --- a/services/dashboard/backend/routes/api.py +++ b/services/dashboard/backend/routes/api.py @@ -803,3 +803,187 @@ async def classifications() -> dict[str, Any]: except Exception as exc: logger.exception("classifications query failed") return {"data": []} + + +# --------------------------------------------------------------------------- +# GET /api/campaigns — HDBSCAN bot campaign clusters +# --------------------------------------------------------------------------- +@router.get("/campaigns") +async def campaigns() -> dict[str, Any]: + """Campagnes de bots détectées par clustering HDBSCAN.""" + try: + rows = query( + f"SELECT campaign_id, " + f"count() AS members, " + f"min(detected_at) AS first_seen, max(detected_at) AS last_seen, " + f"avg(anomaly_score) AS avg_score, " + f"max(anomaly_score) AS max_score, " + f"uniqExact(src_ip) AS unique_ips, " + f"groupUniqArray(10)(ja4) AS ja4_list, " + f"groupUniqArray(5)(asn_org) AS asn_list, " + f"groupUniqArray(5)(country_code) AS countries " + f"FROM {_DB}.ml_detected_anomalies " + "WHERE campaign_id != '' AND campaign_id != '0' " + "AND detected_at >= now() - INTERVAL 7 DAY " + "GROUP BY campaign_id " + "ORDER BY members DESC LIMIT 50" + ) + return {"campaigns": rows} + except Exception as exc: + logger.exception("campaigns query failed") + return {"campaigns": []} + + +# --------------------------------------------------------------------------- +# GET /api/brute-force — Form brute-force detection +# --------------------------------------------------------------------------- +@router.get("/brute-force") +async def brute_force() -> dict[str, Any]: + """Détection de brute-force / credential stuffing via view_form_bruteforce_detected.""" + try: + rows = query( + f"SELECT toString(src_ip) AS src_ip, host, " + f"post_count, distinct_paths, first_seen, last_seen " + f"FROM {_DB}.view_form_bruteforce_detected " + "ORDER BY post_count DESC LIMIT 100" + ) + return {"data": rows} + except Exception as exc: + logger.exception("brute-force query failed") + return {"data": []} + + +# --------------------------------------------------------------------------- +# GET /api/ja4-rotation — JA4 fingerprint rotation detection +# --------------------------------------------------------------------------- +@router.get("/ja4-rotation") +async def ja4_rotation() -> dict[str, Any]: + """IPs présentant une rotation de fingerprints JA4 (évasion potentielle).""" + try: + rows = query( + f"SELECT toString(src_ip) AS src_ip, host, " + f"distinct_ja4, ja4_list, total_hits, window_start " + f"FROM {_DB}.view_host_ip_ja4_rotation " + "ORDER BY distinct_ja4 DESC LIMIT 100" + ) + return {"data": rows} + except Exception as exc: + logger.exception("ja4-rotation query failed") + return {"data": []} + + +# --------------------------------------------------------------------------- +# GET /api/recurrence — Persistent threat IPs +# --------------------------------------------------------------------------- +@router.get("/recurrence") +async def recurrence() -> dict[str, Any]: + """IPs récurrentes détectées sur plusieurs fenêtres temporelles.""" + try: + rows = query( + f"SELECT toString(src_ip) AS src_ip, " + f"recurrence, worst_score, worst_threat, " + f"first_seen, last_seen, " + f"top_ja4, top_host " + f"FROM {_DB}.view_ip_recurrence " + "ORDER BY recurrence DESC, worst_score DESC LIMIT 100" + ) + return {"data": rows} + except Exception as exc: + logger.exception("recurrence query failed") + return {"data": []} + + +# --------------------------------------------------------------------------- +# GET /api/cascade/{ip} — Resource cascade for headless detection +# --------------------------------------------------------------------------- +@router.get("/cascade/{ip}") +async def cascade(ip: str) -> dict[str, Any]: + """Cascade de ressources (détection navigateurs headless) pour une IP.""" + clean_ip = ip.replace("::ffff:", "") + try: + rows = query( + f"SELECT toString(src_ip) AS src_ip, host, " + f"page_count, avg_sub_delay_ms, stddev_sub_delay_ms, " + f"max_sub_resources, window_start " + f"FROM {_DB}.view_resource_cascade_1h " + "WHERE src_ip = toIPv6({ip:String}) " + "ORDER BY window_start DESC LIMIT 50", + {"ip": clean_ip}, + ) + return {"data": rows} + except Exception as exc: + logger.exception("cascade query failed for %s", ip) + return {"data": []} + + +# --------------------------------------------------------------------------- +# GET /api/alerts — Live alert feed (recent HIGH/CRITICAL) +# --------------------------------------------------------------------------- +@router.get("/alerts") +async def alerts( + limit: int = Query(20, ge=1, le=100), +) -> dict[str, Any]: + """Flux d'alertes en temps réel (CRITICAL, HIGH, KNOWN_BOT).""" + try: + rows = query( + f"SELECT detected_at, toString(src_ip) AS src_ip, " + f"anomaly_score, threat_level, ja4, host, " + f"asn_org, country_code, bot_name, campaign_id, " + f"hits, hit_velocity, reason " + f"FROM {_DB}.ml_detected_anomalies " + "WHERE detected_at >= now() - INTERVAL 1 DAY " + "ORDER BY detected_at DESC " + f"LIMIT {{lim:UInt32}}", + {"lim": limit}, + ) + return {"alerts": rows} + except Exception as exc: + logger.exception("alerts query failed") + return {"alerts": []} + + +# --------------------------------------------------------------------------- +# GET /api/timeline-detail — Hourly threat-level breakdown +# --------------------------------------------------------------------------- +@router.get("/timeline-detail") +async def timeline_detail() -> dict[str, Any]: + """Timeline horaire avec ventilation par threat level.""" + try: + rows = query( + f"SELECT toStartOfHour(detected_at) AS hour, " + f"threat_level, count() AS cnt " + f"FROM {_DB}.ml_detected_anomalies " + "WHERE detected_at >= now() - INTERVAL 1 DAY " + "GROUP BY hour, threat_level " + "ORDER BY hour" + ) + # Pivot: group by hour + hours: dict[str, dict] = {} + for r in rows: + h = str(r["hour"]) + if h not in hours: + hours[h] = {"hour": h} + hours[h][r["threat_level"]] = r["cnt"] + return {"timeline": list(hours.values())} + except Exception as exc: + logger.exception("timeline-detail query failed") + return {"timeline": []} + + +# --------------------------------------------------------------------------- +# GET /api/ua-rotation — User-Agent rotation detection +# --------------------------------------------------------------------------- +@router.get("/ua-rotation") +async def ua_rotation() -> dict[str, Any]: + """IPs avec rotation de User-Agent (évasion potentielle).""" + try: + rows = query( + f"SELECT toString(src_ip) AS src_ip, ja4, " + f"distinct_ua_count, ua_samples, total_requests, window_start " + f"FROM {_DB}.view_dashboard_user_agents " + "ORDER BY distinct_ua_count DESC LIMIT 100" + ) + return {"data": rows} + except Exception as exc: + logger.exception("ua-rotation query failed") + return {"data": []} diff --git a/services/dashboard/backend/templates/base.html b/services/dashboard/backend/templates/base.html index c18c893..4f0082e 100644 --- a/services/dashboard/backend/templates/base.html +++ b/services/dashboard/backend/templates/base.html @@ -16,28 +16,30 @@ extend: { colors: { brand: { 50:'#eef2ff',100:'#e0e7ff',500:'#6366f1',600:'#4f46e5',700:'#4338ca',900:'#312e81' }, + surface: { 800:'#1e293b', 900:'#0f172a', 950:'#020617' }, }, - fontFamily: { - sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'], - }, + fontFamily: { sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'] }, } } } {% block head %}{% endblock %} -
- -