306 lines
10 KiB
Python
306 lines
10 KiB
Python
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 _matches_time(value: Optional[datetime], time_from: Optional[datetime], time_to: Optional[datetime]) -> bool:
|
|
if value is None:
|
|
return False
|
|
if time_from and value < time_from:
|
|
return False
|
|
if time_to and value > time_to:
|
|
return False
|
|
return True
|
|
|
|
|
|
@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),
|
|
):
|
|
stmt = (
|
|
select(BGPObservation)
|
|
.where(BGPObservation.source.in_(BGP_SOURCES))
|
|
.order_by(BGPObservation.observed_at.desc(), BGPObservation.id.desc())
|
|
)
|
|
if source:
|
|
stmt = stmt.where(BGPObservation.source == source)
|
|
|
|
result = await db.execute(stmt)
|
|
records = result.scalars().all()
|
|
dt_from = _parse_dt(time_from)
|
|
dt_to = _parse_dt(time_to)
|
|
|
|
filtered = []
|
|
for record in records:
|
|
if prefix and record.prefix != prefix:
|
|
continue
|
|
if origin_asn is not None and record.origin_asn != origin_asn:
|
|
continue
|
|
if peer_asn is not None and record.peer_asn != peer_asn:
|
|
continue
|
|
if collector and record.collector != collector:
|
|
continue
|
|
if event_type and record.event_type != event_type:
|
|
continue
|
|
if (dt_from or dt_to) and not _matches_time(record.observed_at, dt_from, dt_to):
|
|
continue
|
|
filtered.append(record)
|
|
|
|
offset = (page - 1) * page_size
|
|
return {
|
|
"total": len(filtered),
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"data": [record.to_dict() for record in filtered[offset : offset + page_size]],
|
|
}
|
|
|
|
|
|
@router.get("/events/summary")
|
|
async def get_bgp_event_summary(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(BGPObservation).where(BGPObservation.source.in_(BGP_SOURCES)))
|
|
records = result.scalars().all()
|
|
|
|
collectors = sorted({record.collector for record in records if record.collector})
|
|
prefixes = sorted({record.prefix for record in records if record.prefix})
|
|
by_type: dict[str, int] = {}
|
|
for record in records:
|
|
by_type[record.event_type] = by_type.get(record.event_type, 0) + 1
|
|
|
|
return {
|
|
"total": len(records),
|
|
"collector_count": len(collectors),
|
|
"prefix_count": len(prefixes),
|
|
"by_type": by_type,
|
|
}
|
|
|
|
|
|
@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("/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),
|
|
):
|
|
stmt = select(BGPAnomaly).order_by(BGPAnomaly.created_at.desc(), BGPAnomaly.id.desc())
|
|
if severity:
|
|
stmt = stmt.where(BGPAnomaly.severity == severity)
|
|
if anomaly_type:
|
|
stmt = stmt.where(BGPAnomaly.anomaly_type == anomaly_type)
|
|
if status:
|
|
stmt = stmt.where(BGPAnomaly.status == status)
|
|
if prefix:
|
|
stmt = stmt.where(BGPAnomaly.prefix == prefix)
|
|
if origin_asn is not None:
|
|
stmt = stmt.where(BGPAnomaly.origin_asn == origin_asn)
|
|
|
|
result = await db.execute(stmt)
|
|
records = result.scalars().all()
|
|
dt_from = _parse_dt(time_from)
|
|
dt_to = _parse_dt(time_to)
|
|
if dt_from or dt_to:
|
|
records = [record for record in records if _matches_time(record.created_at, dt_from, dt_to)]
|
|
|
|
offset = (page - 1) * page_size
|
|
return {
|
|
"total": len(records),
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"data": [record.to_dict() for record in records[offset : offset + page_size]],
|
|
}
|
|
|
|
|
|
@router.get("/anomalies/summary")
|
|
async def get_bgp_anomaly_summary(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
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()},
|
|
}
|
|
|
|
|
|
@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),
|
|
):
|
|
stmt = select(BGPIncident).order_by(BGPIncident.created_at.desc(), BGPIncident.id.desc())
|
|
if severity:
|
|
stmt = stmt.where(BGPIncident.severity == severity)
|
|
if incident_type:
|
|
stmt = stmt.where(BGPIncident.incident_type == incident_type)
|
|
if status:
|
|
stmt = stmt.where(BGPIncident.status == status)
|
|
|
|
result = await db.execute(stmt)
|
|
records = result.scalars().all()
|
|
offset = (page - 1) * page_size
|
|
return {
|
|
"total": len(records),
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"data": [record.to_dict() for record in records[offset : offset + page_size]],
|
|
}
|
|
|
|
|
|
@router.get("/incidents/summary")
|
|
async def get_bgp_incident_summary(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
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("/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()
|