from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.security import get_current_user from app.db.session import get_db from app.models.bgp_anomaly import BGPAnomaly from app.models.bgp_incident import BGPIncident from app.models.bgp_observation import BGPObservation from app.models.user import User from app.services.bgp_collectors import build_bgp_collector_coverage router = APIRouter() BGP_SOURCES = ("ris_live_bgp", "bgpstream_bgp") def _parse_dt(value: Optional[str]) -> Optional[datetime]: if not value: return None return datetime.fromisoformat(value.replace("Z", "+00:00")) def _event_filters( *, prefix: Optional[str], origin_asn: Optional[int], peer_asn: Optional[int], collector: Optional[str], event_type: Optional[str], source: Optional[str], time_from: Optional[datetime], time_to: Optional[datetime], ): filters = [BGPObservation.source.in_(BGP_SOURCES)] if source: filters.append(BGPObservation.source == source) if prefix: filters.append(BGPObservation.prefix == prefix) if origin_asn is not None: filters.append(BGPObservation.origin_asn == origin_asn) if peer_asn is not None: filters.append(BGPObservation.peer_asn == peer_asn) if collector: filters.append(BGPObservation.collector == collector) if event_type: filters.append(BGPObservation.event_type == event_type) if time_from: filters.append(BGPObservation.observed_at >= time_from) if time_to: filters.append(BGPObservation.observed_at <= time_to) return filters def _anomaly_filters( *, severity: Optional[str], anomaly_type: Optional[str], status: Optional[str], prefix: Optional[str], origin_asn: Optional[int], time_from: Optional[datetime], time_to: Optional[datetime], ): filters = [] if severity: filters.append(BGPAnomaly.severity == severity) if anomaly_type: filters.append(BGPAnomaly.anomaly_type == anomaly_type) if status: filters.append(BGPAnomaly.status == status) if prefix: filters.append(BGPAnomaly.prefix == prefix) if origin_asn is not None: filters.append(BGPAnomaly.origin_asn == origin_asn) if time_from: filters.append(BGPAnomaly.created_at >= time_from) if time_to: filters.append(BGPAnomaly.created_at <= time_to) return filters def _incident_filters( *, severity: Optional[str], incident_type: Optional[str], status: Optional[str], ): filters = [] if severity: filters.append(BGPIncident.severity == severity) if incident_type: filters.append(BGPIncident.incident_type == incident_type) if status: filters.append(BGPIncident.status == status) return filters async def _build_event_summary_payload(db: AsyncSession) -> dict: base_filters = [BGPObservation.source.in_(BGP_SOURCES)] total_result = await db.execute( select(func.count(BGPObservation.id)).where(*base_filters) ) collectors_result = await db.execute( select(func.count(func.distinct(BGPObservation.collector))).where( *base_filters, BGPObservation.collector.isnot(None) ) ) prefixes_result = await db.execute( select(func.count(func.distinct(BGPObservation.prefix))).where( *base_filters, BGPObservation.prefix.isnot(None) ) ) type_result = await db.execute( select(BGPObservation.event_type, func.count(BGPObservation.id)) .where(*base_filters) .group_by(BGPObservation.event_type) ) return { "total": total_result.scalar() or 0, "collector_count": collectors_result.scalar() or 0, "prefix_count": prefixes_result.scalar() or 0, "by_type": {row[0]: row[1] for row in type_result.fetchall()}, } async def _build_anomaly_summary_payload(db: AsyncSession) -> dict: total_result = await db.execute(select(func.count(BGPAnomaly.id))) type_result = await db.execute( select(BGPAnomaly.anomaly_type, func.count(BGPAnomaly.id)) .group_by(BGPAnomaly.anomaly_type) .order_by(func.count(BGPAnomaly.id).desc()) ) severity_result = await db.execute( select(BGPAnomaly.severity, func.count(BGPAnomaly.id)) .group_by(BGPAnomaly.severity) .order_by(func.count(BGPAnomaly.id).desc()) ) status_result = await db.execute( select(BGPAnomaly.status, func.count(BGPAnomaly.id)) .group_by(BGPAnomaly.status) .order_by(func.count(BGPAnomaly.id).desc()) ) return { "total": total_result.scalar() or 0, "by_type": {row[0]: row[1] for row in type_result.fetchall()}, "by_severity": {row[0]: row[1] for row in severity_result.fetchall()}, "by_status": {row[0]: row[1] for row in status_result.fetchall()}, } async def _build_incident_summary_payload(db: AsyncSession) -> dict: total_result = await db.execute(select(func.count(BGPIncident.id))) type_result = await db.execute( select(BGPIncident.incident_type, func.count(BGPIncident.id)) .group_by(BGPIncident.incident_type) .order_by(func.count(BGPIncident.id).desc()) ) severity_result = await db.execute( select(BGPIncident.severity, func.count(BGPIncident.id)) .group_by(BGPIncident.severity) .order_by(func.count(BGPIncident.id).desc()) ) status_result = await db.execute( select(BGPIncident.status, func.count(BGPIncident.id)) .group_by(BGPIncident.status) .order_by(func.count(BGPIncident.id).desc()) ) return { "total": total_result.scalar() or 0, "by_type": {row[0]: row[1] for row in type_result.fetchall()}, "by_severity": {row[0]: row[1] for row in severity_result.fetchall()}, "by_status": {row[0]: row[1] for row in status_result.fetchall()}, } @router.get("/events") async def list_bgp_events( prefix: Optional[str] = Query(None), origin_asn: Optional[int] = Query(None), peer_asn: Optional[int] = Query(None), collector: Optional[str] = Query(None), event_type: Optional[str] = Query(None), source: Optional[str] = Query(None), time_from: Optional[str] = Query(None), time_to: Optional[str] = Query(None), page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): dt_from = _parse_dt(time_from) dt_to = _parse_dt(time_to) filters = _event_filters( prefix=prefix, origin_asn=origin_asn, peer_asn=peer_asn, collector=collector, event_type=event_type, source=source, time_from=dt_from, time_to=dt_to, ) offset = (page - 1) * page_size count_result = await db.execute( select(func.count(BGPObservation.id)).where(*filters) ) data_result = await db.execute( select(BGPObservation) .where(*filters) .order_by(BGPObservation.observed_at.desc(), BGPObservation.id.desc()) .offset(offset) .limit(page_size) ) records = data_result.scalars().all() return { "total": count_result.scalar() or 0, "page": page, "page_size": page_size, "data": [record.to_dict() for record in records], } @router.get("/events/summary") async def get_bgp_event_summary( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): return await _build_event_summary_payload(db) @router.get("/collectors") async def list_bgp_collectors( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): data = await build_bgp_collector_coverage(db, source_filter=BGP_SOURCES) return { "total": len(data), "data": data, } @router.get("/collectors/summary") async def get_bgp_collector_summary( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): collectors = await build_bgp_collector_coverage(db, source_filter=BGP_SOURCES) active_collectors = [item for item in collectors if item["observation_count"] > 0] return { "total": len(collectors), "active_collectors": len(active_collectors), "observed_prefixes": sum(item["prefix_count"] for item in active_collectors), "observed_origins": sum(item["origin_asn_count"] for item in active_collectors), "recent_24h_events": sum(item["recent_24h_observation_count"] for item in active_collectors), "recent_7d_events": sum(item["recent_7d_observation_count"] for item in active_collectors), } @router.get("/overview/summary") async def get_bgp_overview_summary( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): event_summary = await _build_event_summary_payload(db) anomaly_summary = await _build_anomaly_summary_payload(db) incident_summary = await _build_incident_summary_payload(db) collectors = await build_bgp_collector_coverage(db, source_filter=BGP_SOURCES) active_collectors = [item for item in collectors if item["observation_count"] > 0] return { "incidentSummary": incident_summary, "anomalySummary": anomaly_summary, "eventSummary": event_summary, "collectorSummary": { "total": len(collectors), "active_collectors": len(active_collectors), "observed_prefixes": sum(item["prefix_count"] for item in active_collectors), "observed_origins": sum(item["origin_asn_count"] for item in active_collectors), "recent_24h_events": sum(item["recent_24h_observation_count"] for item in active_collectors), "recent_7d_events": sum(item["recent_7d_observation_count"] for item in active_collectors), }, } @router.get("/events/{event_id}") async def get_bgp_event( event_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): record = await db.get(BGPObservation, event_id) if not record or record.source not in BGP_SOURCES: raise HTTPException(status_code=404, detail="BGP event not found") return record.to_dict() @router.get("/anomalies") async def list_bgp_anomalies( severity: Optional[str] = Query(None), anomaly_type: Optional[str] = Query(None), status: Optional[str] = Query(None), prefix: Optional[str] = Query(None), origin_asn: Optional[int] = Query(None), time_from: Optional[str] = Query(None), time_to: Optional[str] = Query(None), page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): dt_from = _parse_dt(time_from) dt_to = _parse_dt(time_to) filters = _anomaly_filters( severity=severity, anomaly_type=anomaly_type, status=status, prefix=prefix, origin_asn=origin_asn, time_from=dt_from, time_to=dt_to, ) offset = (page - 1) * page_size total_result = await db.execute( select(func.count(BGPAnomaly.id)).where(*filters) ) data_result = await db.execute( select(BGPAnomaly) .where(*filters) .order_by(BGPAnomaly.created_at.desc(), BGPAnomaly.id.desc()) .offset(offset) .limit(page_size) ) records = data_result.scalars().all() return { "total": total_result.scalar() or 0, "page": page, "page_size": page_size, "data": [record.to_dict() for record in records], } @router.get("/anomalies/summary") async def get_bgp_anomaly_summary( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): return await _build_anomaly_summary_payload(db) @router.get("/anomalies/{anomaly_id}") async def get_bgp_anomaly( anomaly_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): record = await db.get(BGPAnomaly, anomaly_id) if not record: raise HTTPException(status_code=404, detail="BGP anomaly not found") return record.to_dict() @router.get("/incidents") async def list_bgp_incidents( severity: Optional[str] = Query(None), incident_type: Optional[str] = Query(None), status: Optional[str] = Query(None), page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): filters = _incident_filters( severity=severity, incident_type=incident_type, status=status, ) offset = (page - 1) * page_size total_result = await db.execute( select(func.count(BGPIncident.id)).where(*filters) ) data_result = await db.execute( select(BGPIncident) .where(*filters) .order_by(BGPIncident.created_at.desc(), BGPIncident.id.desc()) .offset(offset) .limit(page_size) ) records = data_result.scalars().all() return { "total": total_result.scalar() or 0, "page": page, "page_size": page_size, "data": [record.to_dict() for record in records], } @router.get("/incidents/summary") async def get_bgp_incident_summary( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): return await _build_incident_summary_payload(db) @router.get("/incidents/{incident_id}") async def get_bgp_incident( incident_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): record = await db.get(BGPIncident, incident_id) if not record: raise HTTPException(status_code=404, detail="BGP incident not found") return record.to_dict()