maj cumulative
This commit is contained in:
@ -19,7 +19,8 @@ async def get_detections(
|
||||
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)")
|
||||
sort_order: str = Query("DESC", description="Ordre (ASC/DESC)"),
|
||||
group_by_ip: bool = Query(False, description="Grouper par IP (first_seen/last_seen agrégés)")
|
||||
):
|
||||
"""
|
||||
Récupère la liste des détections avec pagination et filtres
|
||||
@ -47,7 +48,7 @@ async def get_detections(
|
||||
|
||||
if search:
|
||||
where_clauses.append(
|
||||
"(src_ip ILIKE %(search)s OR ja4 ILIKE %(search)s OR host ILIKE %(search)s)"
|
||||
"(ilike(toString(src_ip), %(search)s) OR ilike(ja4, %(search)s) OR ilike(host, %(search)s))"
|
||||
)
|
||||
params["search"] = f"%{search}%"
|
||||
|
||||
@ -66,6 +67,124 @@ async def get_detections(
|
||||
# Requête principale
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
sort_order = "DESC" if sort_order.upper() == "DESC" else "ASC"
|
||||
|
||||
# ── Mode groupé par IP (first_seen / last_seen depuis la DB) ────────────
|
||||
if group_by_ip:
|
||||
valid_sort_grouped = ["anomaly_score", "hits", "hit_velocity", "first_seen", "last_seen", "src_ip", "detected_at"]
|
||||
grouped_sort = sort_by if sort_by in valid_sort_grouped else "last_seen"
|
||||
# detected_at → last_seen (max(detected_at) dans le GROUP BY)
|
||||
if grouped_sort == "detected_at":
|
||||
grouped_sort = "last_seen"
|
||||
# In outer query, min_score is exposed as anomaly_score — keep the alias
|
||||
outer_sort = "min_score" if grouped_sort == "anomaly_score" else grouped_sort
|
||||
|
||||
# Count distinct IPs
|
||||
count_ip_query = f"""
|
||||
SELECT uniq(src_ip)
|
||||
FROM ml_detected_anomalies
|
||||
WHERE {where_clause}
|
||||
"""
|
||||
cr = db.query(count_ip_query, params)
|
||||
total = cr.result_rows[0][0] if cr.result_rows else 0
|
||||
|
||||
grouped_query = f"""
|
||||
SELECT
|
||||
ip_data.src_ip,
|
||||
ip_data.first_seen,
|
||||
ip_data.last_seen,
|
||||
ip_data.detection_count,
|
||||
ip_data.unique_ja4s,
|
||||
ip_data.unique_hosts,
|
||||
ip_data.min_score AS anomaly_score,
|
||||
ip_data.threat_level,
|
||||
ip_data.model_name,
|
||||
ip_data.country_code,
|
||||
ip_data.asn_number,
|
||||
ip_data.asn_org,
|
||||
ip_data.hit_velocity,
|
||||
ip_data.hits,
|
||||
ip_data.asn_label,
|
||||
ar.label AS asn_rep_label
|
||||
FROM (
|
||||
SELECT
|
||||
src_ip,
|
||||
min(detected_at) AS first_seen,
|
||||
max(detected_at) AS last_seen,
|
||||
count() AS detection_count,
|
||||
groupUniqArray(5)(ja4) AS unique_ja4s,
|
||||
groupUniqArray(5)(host) AS unique_hosts,
|
||||
min(anomaly_score) AS min_score,
|
||||
argMin(threat_level, anomaly_score) AS threat_level,
|
||||
argMin(model_name, anomaly_score) AS model_name,
|
||||
any(country_code) AS country_code,
|
||||
any(asn_number) AS asn_number,
|
||||
any(asn_org) AS asn_org,
|
||||
max(hit_velocity) AS hit_velocity,
|
||||
sum(hits) AS hits,
|
||||
any(asn_label) AS asn_label
|
||||
FROM ml_detected_anomalies
|
||||
WHERE {where_clause}
|
||||
GROUP BY src_ip
|
||||
) ip_data
|
||||
LEFT JOIN mabase_prod.asn_reputation ar
|
||||
ON ar.src_asn = toUInt32OrZero(ip_data.asn_number)
|
||||
ORDER BY {outer_sort} {sort_order}
|
||||
LIMIT %(limit)s OFFSET %(offset)s
|
||||
"""
|
||||
params["limit"] = page_size
|
||||
params["offset"] = offset
|
||||
gresult = db.query(grouped_query, params)
|
||||
|
||||
def _label_to_score(label: str) -> float | None:
|
||||
if not label: return None
|
||||
mapping = {'human': 0.9, 'bot': 0.05, 'proxy': 0.25, 'vpn': 0.3,
|
||||
'tor': 0.1, 'datacenter': 0.4, 'scanner': 0.05, 'malicious': 0.05}
|
||||
return mapping.get(label.lower(), 0.5)
|
||||
|
||||
detections = []
|
||||
for row in gresult.result_rows:
|
||||
# row: src_ip, first_seen, last_seen, detection_count, unique_ja4s, unique_hosts,
|
||||
# anomaly_score, threat_level, model_name, country_code, asn_number, asn_org,
|
||||
# hit_velocity, hits, asn_label, asn_rep_label
|
||||
ja4s = list(row[4]) if row[4] else []
|
||||
hosts = list(row[5]) if row[5] else []
|
||||
detections.append(Detection(
|
||||
detected_at=row[1],
|
||||
src_ip=str(row[0]),
|
||||
ja4=ja4s[0] if ja4s else "",
|
||||
host=hosts[0] if hosts else "",
|
||||
bot_name="",
|
||||
anomaly_score=float(row[6]) if row[6] else 0.0,
|
||||
threat_level=row[7] or "LOW",
|
||||
model_name=row[8] or "",
|
||||
recurrence=int(row[3] or 0),
|
||||
asn_number=str(row[10]) if row[10] else "",
|
||||
asn_org=row[11] or "",
|
||||
asn_detail="",
|
||||
asn_domain="",
|
||||
country_code=row[9] or "",
|
||||
asn_label=row[14] or "",
|
||||
hits=int(row[13] or 0),
|
||||
hit_velocity=float(row[12]) if row[12] else 0.0,
|
||||
fuzzing_index=0.0,
|
||||
post_ratio=0.0,
|
||||
reason="",
|
||||
asn_rep_label=row[15] or "",
|
||||
asn_score=_label_to_score(row[15] or ""),
|
||||
first_seen=row[1],
|
||||
last_seen=row[2],
|
||||
unique_ja4s=ja4s,
|
||||
unique_hosts=hosts,
|
||||
))
|
||||
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
return DetectionsListResponse(
|
||||
items=detections, total=total, page=page,
|
||||
page_size=page_size, total_pages=total_pages
|
||||
)
|
||||
|
||||
# ── Mode individuel (comportement original) ──────────────────────────────
|
||||
# Validation du tri
|
||||
valid_sort_columns = [
|
||||
"detected_at", "src_ip", "threat_level", "anomaly_score",
|
||||
@ -74,8 +193,6 @@ async def get_detections(
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user