Cybersecurity-Projects/PROJECTS/advanced/ai-threat-detection/backend/app/services/stats_service.py

100 lines
3.1 KiB
Python

"""
©AngelaMos | 2026
stats_service.py
Threat statistics aggregation service computing time-
windowed metrics from stored events
get_stats accepts a time_range string (1h, 6h, 24h, 7d,
30d) mapped to timedeltas via _RANGE_MAP, queries threat
events since the cutoff, and returns a StatsResponse with
total count, severity breakdown (HIGH/MEDIUM/LOW counts
via GROUP BY), top 10 source IPs, and top 10 attacked
paths ordered by frequency
Connects to:
models/threat_event - ThreatEvent queries
schemas/stats - StatsResponse, SeverityBreakdown,
IPStatEntry, PathStatEntry
api/stats - called from GET /stats endpoint
"""
from datetime import datetime, timedelta, UTC
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.threat_event import ThreatEvent
from app.schemas.stats import (
IPStatEntry,
PathStatEntry,
SeverityBreakdown,
StatsResponse,
)
_RANGE_MAP: dict[str, timedelta] = {
"1h": timedelta(hours=1),
"6h": timedelta(hours=6),
"24h": timedelta(hours=24),
"7d": timedelta(days=7),
"30d": timedelta(days=30),
}
async def get_stats(
session: AsyncSession,
time_range: str = "24h",
) -> StatsResponse:
"""
Compute aggregate threat statistics for a given time window.
"""
delta = _RANGE_MAP.get(time_range, timedelta(hours=24))
cutoff = datetime.now(UTC) - delta
base = select(ThreatEvent).where(ThreatEvent.created_at
>= cutoff) # type: ignore[arg-type]
total_q = select(func.count()).select_from(base.subquery())
total = (await session.execute(total_q)).scalar_one()
sev_q = (
select(ThreatEvent.severity,
func.count()) # type: ignore[call-overload]
.where(ThreatEvent.created_at >= cutoff).group_by(
ThreatEvent.severity))
sev_rows = (await session.execute(sev_q)).all()
sev_map = {row[0]: row[1] for row in sev_rows}
threats_detected = total
ip_q = (
select(ThreatEvent.source_ip,
func.count().label("cnt")) # type: ignore[call-overload]
.where(ThreatEvent.created_at >= cutoff).group_by(
ThreatEvent.source_ip).order_by(func.count().desc()).limit(10))
ip_rows = (await session.execute(ip_q)).all()
path_q = (
select(ThreatEvent.request_path,
func.count().label("cnt")) # type: ignore[call-overload]
.where(ThreatEvent.created_at >= cutoff).group_by(
ThreatEvent.request_path).order_by(func.count().desc()).limit(10))
path_rows = (await session.execute(path_q)).all()
return StatsResponse(
time_range=time_range,
threats_stored=total,
threats_detected=threats_detected,
severity_breakdown=SeverityBreakdown(
high=sev_map.get("HIGH", 0),
medium=sev_map.get("MEDIUM", 0),
low=sev_map.get("LOW", 0),
),
top_source_ips=[
IPStatEntry(source_ip=row[0], count=row[1]) for row in ip_rows
],
top_attacked_paths=[
PathStatEntry(path=row[0], count=row[1]) for row in path_rows
],
)