Initial commit: Bot Detector Dashboard for SOC Incident Response
🛡️ Dashboard complet pour l'analyse et la classification des menaces Fonctionnalités principales: - Visualisation des détections en temps réel (24h) - Investigation multi-entités (IP, JA4, ASN, Host, User-Agent) - Analyse de corrélation pour classification SOC - Clustering automatique par subnet/JA4/UA - Export des classifications pour ML Composants: - Backend: FastAPI (Python) + ClickHouse - Frontend: React + TypeScript + TailwindCSS - 6 routes API: metrics, detections, variability, attributes, analysis, entities - 7 types d'entités investigables Documentation ajoutée: - NAVIGATION_GRAPH.md: Graph complet de navigation - SOC_OPTIMIZATION_PROPOSAL.md: Proposition d'optimisation pour SOC • Réduction de 7 à 2 clics pour classification • Nouvelle vue /incidents clusterisée • Panel latéral d'investigation • Quick Search (Cmd+K) • Timeline interactive • Graph de corrélations Sécurité: - .gitignore configuré (exclut .env, secrets, node_modules) - Credentials dans .env (à ne pas committer) ⚠️ Audit sécurité réalisé - Voir recommandations dans SOC_OPTIMIZATION_PROPOSAL.md Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
294
backend/routes/detections.py
Normal file
294
backend/routes/detections.py
Normal file
@ -0,0 +1,294 @@
|
||||
"""
|
||||
Endpoints pour la liste des détections
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from typing import Optional, List
|
||||
from ..database import db
|
||||
from ..models import DetectionsListResponse, Detection
|
||||
|
||||
router = APIRouter(prefix="/api/detections", tags=["detections"])
|
||||
|
||||
|
||||
@router.get("", response_model=DetectionsListResponse)
|
||||
async def get_detections(
|
||||
page: int = Query(1, ge=1, description="Numéro de page"),
|
||||
page_size: int = Query(25, ge=1, le=100, description="Nombre de lignes par page"),
|
||||
threat_level: Optional[str] = Query(None, description="Filtrer par niveau de menace"),
|
||||
model_name: Optional[str] = Query(None, description="Filtrer par modèle"),
|
||||
country_code: Optional[str] = Query(None, description="Filtrer par pays"),
|
||||
asn_number: Optional[str] = Query(None, description="Filtrer par ASN"),
|
||||
search: Optional[str] = Query(None, description="Recherche texte (IP, JA4, Host)"),
|
||||
sort_by: str = Query("detected_at", description="Trier par"),
|
||||
sort_order: str = Query("DESC", description="Ordre (ASC/DESC)")
|
||||
):
|
||||
"""
|
||||
Récupère la liste des détections avec pagination et filtres
|
||||
"""
|
||||
try:
|
||||
# Construction de la requête
|
||||
where_clauses = ["detected_at >= now() - INTERVAL 24 HOUR"]
|
||||
params = {}
|
||||
|
||||
if threat_level:
|
||||
where_clauses.append("threat_level = %(threat_level)s")
|
||||
params["threat_level"] = threat_level
|
||||
|
||||
if model_name:
|
||||
where_clauses.append("model_name = %(model_name)s")
|
||||
params["model_name"] = model_name
|
||||
|
||||
if country_code:
|
||||
where_clauses.append("country_code = %(country_code)s")
|
||||
params["country_code"] = country_code.upper()
|
||||
|
||||
if asn_number:
|
||||
where_clauses.append("asn_number = %(asn_number)s")
|
||||
params["asn_number"] = asn_number
|
||||
|
||||
if search:
|
||||
where_clauses.append(
|
||||
"(src_ip ILIKE %(search)s OR ja4 ILIKE %(search)s OR host ILIKE %(search)s)"
|
||||
)
|
||||
params["search"] = f"%{search}%"
|
||||
|
||||
where_clause = " AND ".join(where_clauses)
|
||||
|
||||
# Requête de comptage
|
||||
count_query = f"""
|
||||
SELECT count()
|
||||
FROM ml_detected_anomalies
|
||||
WHERE {where_clause}
|
||||
"""
|
||||
|
||||
count_result = db.query(count_query, params)
|
||||
total = count_result.result_rows[0][0] if count_result.result_rows else 0
|
||||
|
||||
# Requête principale
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# Validation du tri
|
||||
valid_sort_columns = [
|
||||
"detected_at", "src_ip", "threat_level", "anomaly_score",
|
||||
"asn_number", "country_code", "hits", "hit_velocity"
|
||||
]
|
||||
if sort_by not in valid_sort_columns:
|
||||
sort_by = "detected_at"
|
||||
|
||||
sort_order = "DESC" if sort_order.upper() == "DESC" else "ASC"
|
||||
|
||||
main_query = f"""
|
||||
SELECT
|
||||
detected_at,
|
||||
src_ip,
|
||||
ja4,
|
||||
host,
|
||||
bot_name,
|
||||
anomaly_score,
|
||||
threat_level,
|
||||
model_name,
|
||||
recurrence,
|
||||
asn_number,
|
||||
asn_org,
|
||||
asn_detail,
|
||||
asn_domain,
|
||||
country_code,
|
||||
asn_label,
|
||||
hits,
|
||||
hit_velocity,
|
||||
fuzzing_index,
|
||||
post_ratio,
|
||||
reason
|
||||
FROM ml_detected_anomalies
|
||||
WHERE {where_clause}
|
||||
ORDER BY {sort_by} {sort_order}
|
||||
LIMIT %(limit)s OFFSET %(offset)s
|
||||
"""
|
||||
|
||||
params["limit"] = page_size
|
||||
params["offset"] = offset
|
||||
|
||||
result = db.query(main_query, params)
|
||||
|
||||
detections = [
|
||||
Detection(
|
||||
detected_at=row[0],
|
||||
src_ip=str(row[1]),
|
||||
ja4=row[2] or "",
|
||||
host=row[3] or "",
|
||||
bot_name=row[4] or "",
|
||||
anomaly_score=float(row[5]) if row[5] else 0.0,
|
||||
threat_level=row[6] or "LOW",
|
||||
model_name=row[7] or "",
|
||||
recurrence=row[8] or 0,
|
||||
asn_number=str(row[9]) if row[9] else "",
|
||||
asn_org=row[10] or "",
|
||||
asn_detail=row[11] or "",
|
||||
asn_domain=row[12] or "",
|
||||
country_code=row[13] or "",
|
||||
asn_label=row[14] or "",
|
||||
hits=row[15] or 0,
|
||||
hit_velocity=float(row[16]) if row[16] else 0.0,
|
||||
fuzzing_index=float(row[17]) if row[17] else 0.0,
|
||||
post_ratio=float(row[18]) if row[18] else 0.0,
|
||||
reason=row[19] or ""
|
||||
)
|
||||
for row in result.result_rows
|
||||
]
|
||||
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
|
||||
return DetectionsListResponse(
|
||||
items=detections,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=total_pages
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Erreur lors de la récupération des détections: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{detection_id}")
|
||||
async def get_detection_details(detection_id: str):
|
||||
"""
|
||||
Récupère les détails d'une détection spécifique
|
||||
detection_id peut être une IP ou un identifiant
|
||||
"""
|
||||
try:
|
||||
query = """
|
||||
SELECT
|
||||
detected_at,
|
||||
src_ip,
|
||||
ja4,
|
||||
host,
|
||||
bot_name,
|
||||
anomaly_score,
|
||||
threat_level,
|
||||
model_name,
|
||||
recurrence,
|
||||
asn_number,
|
||||
asn_org,
|
||||
asn_detail,
|
||||
asn_domain,
|
||||
country_code,
|
||||
asn_label,
|
||||
hits,
|
||||
hit_velocity,
|
||||
fuzzing_index,
|
||||
post_ratio,
|
||||
port_exhaustion_ratio,
|
||||
orphan_ratio,
|
||||
tcp_jitter_variance,
|
||||
tcp_shared_count,
|
||||
true_window_size,
|
||||
window_mss_ratio,
|
||||
alpn_http_mismatch,
|
||||
is_alpn_missing,
|
||||
sni_host_mismatch,
|
||||
header_count,
|
||||
has_accept_language,
|
||||
has_cookie,
|
||||
has_referer,
|
||||
modern_browser_score,
|
||||
ua_ch_mismatch,
|
||||
header_order_shared_count,
|
||||
ip_id_zero_ratio,
|
||||
request_size_variance,
|
||||
multiplexing_efficiency,
|
||||
mss_mobile_mismatch,
|
||||
correlated,
|
||||
reason,
|
||||
asset_ratio,
|
||||
direct_access_ratio,
|
||||
is_ua_rotating,
|
||||
distinct_ja4_count,
|
||||
src_port_density,
|
||||
ja4_asn_concentration,
|
||||
ja4_country_concentration,
|
||||
is_rare_ja4
|
||||
FROM ml_detected_anomalies
|
||||
WHERE src_ip = %(ip)s
|
||||
ORDER BY detected_at DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
result = db.query(query, {"ip": detection_id})
|
||||
|
||||
if not result.result_rows:
|
||||
raise HTTPException(status_code=404, detail="Détection non trouvée")
|
||||
|
||||
row = result.result_rows[0]
|
||||
|
||||
return {
|
||||
"detected_at": row[0],
|
||||
"src_ip": str(row[1]),
|
||||
"ja4": row[2] or "",
|
||||
"host": row[3] or "",
|
||||
"bot_name": row[4] or "",
|
||||
"anomaly_score": float(row[5]) if row[5] else 0.0,
|
||||
"threat_level": row[6] or "LOW",
|
||||
"model_name": row[7] or "",
|
||||
"recurrence": row[8] or 0,
|
||||
"asn": {
|
||||
"number": str(row[9]) if row[9] else "",
|
||||
"org": row[10] or "",
|
||||
"detail": row[11] or "",
|
||||
"domain": row[12] or "",
|
||||
"label": row[14] or ""
|
||||
},
|
||||
"country": {
|
||||
"code": row[13] or "",
|
||||
},
|
||||
"metrics": {
|
||||
"hits": row[15] or 0,
|
||||
"hit_velocity": float(row[16]) if row[16] else 0.0,
|
||||
"fuzzing_index": float(row[17]) if row[17] else 0.0,
|
||||
"post_ratio": float(row[18]) if row[18] else 0.0,
|
||||
"port_exhaustion_ratio": float(row[19]) if row[19] else 0.0,
|
||||
"orphan_ratio": float(row[20]) if row[20] else 0.0,
|
||||
},
|
||||
"tcp": {
|
||||
"jitter_variance": float(row[21]) if row[21] else 0.0,
|
||||
"shared_count": row[22] or 0,
|
||||
"true_window_size": row[23] or 0,
|
||||
"window_mss_ratio": float(row[24]) if row[24] else 0.0,
|
||||
},
|
||||
"tls": {
|
||||
"alpn_http_mismatch": bool(row[25]) if row[25] is not None else False,
|
||||
"is_alpn_missing": bool(row[26]) if row[26] is not None else False,
|
||||
"sni_host_mismatch": bool(row[27]) if row[27] is not None else False,
|
||||
},
|
||||
"headers": {
|
||||
"count": row[28] or 0,
|
||||
"has_accept_language": bool(row[29]) if row[29] is not None else False,
|
||||
"has_cookie": bool(row[30]) if row[30] is not None else False,
|
||||
"has_referer": bool(row[31]) if row[31] is not None else False,
|
||||
"modern_browser_score": row[32] or 0,
|
||||
"ua_ch_mismatch": bool(row[33]) if row[33] is not None else False,
|
||||
"header_order_shared_count": row[34] or 0,
|
||||
},
|
||||
"behavior": {
|
||||
"ip_id_zero_ratio": float(row[35]) if row[35] else 0.0,
|
||||
"request_size_variance": float(row[36]) if row[36] else 0.0,
|
||||
"multiplexing_efficiency": float(row[37]) if row[37] else 0.0,
|
||||
"mss_mobile_mismatch": bool(row[38]) if row[38] is not None else False,
|
||||
"correlated": bool(row[39]) if row[39] is not None else False,
|
||||
},
|
||||
"advanced": {
|
||||
"asset_ratio": float(row[41]) if row[41] else 0.0,
|
||||
"direct_access_ratio": float(row[42]) if row[42] else 0.0,
|
||||
"is_ua_rotating": bool(row[43]) if row[43] is not None else False,
|
||||
"distinct_ja4_count": row[44] or 0,
|
||||
"src_port_density": float(row[45]) if row[45] else 0.0,
|
||||
"ja4_asn_concentration": float(row[46]) if row[46] else 0.0,
|
||||
"ja4_country_concentration": float(row[47]) if row[47] else 0.0,
|
||||
"is_rare_ja4": bool(row[48]) if row[48] is not None else False,
|
||||
},
|
||||
"reason": row[40] or ""
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}")
|
||||
Reference in New Issue
Block a user