Files
securelens-backend/app/routers/history.py
2026-04-07 18:13:43 +05:30

227 lines
6.6 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.middleware.auth import get_current_user
from app.models.scan import ScanResult
from app.models.user import User
from app.schemas.scan import (
Issue,
LayerStatus,
ScanHistoryItem,
ScanHistoryResponse,
ScanResponse,
DashboardTrendsResponse,
ChatRequest,
ChatResponse,
ThreatNarrativeResponse,
ScanDiffResponse,
)
from app.services.ai import chat_with_scan_context, generate_threat_narrative
router = APIRouter(prefix="/scans", tags=["history"])
@router.get("", response_model=ScanHistoryResponse)
async def list_scans(
page: int = Query(1, ge=1),
per_page: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
offset = (page - 1) * per_page
count_result = await db.execute(
select(func.count()).select_from(ScanResult).where(ScanResult.user_id == current_user.id)
)
total = count_result.scalar_one()
result = await db.execute(
select(ScanResult)
.where(ScanResult.user_id == current_user.id)
.order_by(ScanResult.created_at.desc())
.offset(offset)
.limit(per_page)
)
scans = result.scalars().all()
return ScanHistoryResponse(
scans=[ScanHistoryItem.model_validate(s) for s in scans],
total=total,
page=page,
per_page=per_page,
)
@router.get("/trends", response_model=DashboardTrendsResponse)
async def get_trends(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
count_result = await db.execute(
select(func.count()).select_from(ScanResult).where(ScanResult.user_id == current_user.id)
)
total_scans = count_result.scalar_one()
avg_result = await db.execute(
select(func.avg(ScanResult.security_score)).where(ScanResult.user_id == current_user.id)
)
avg_score = avg_result.scalar_one() or 0.0
recent_result = await db.execute(
select(ScanResult)
.where(ScanResult.user_id == current_user.id)
.order_by(ScanResult.created_at.desc())
.limit(5)
)
recent_scans = recent_result.scalars().all()
return DashboardTrendsResponse(
total_scans=total_scans,
average_score=float(avg_score),
recent_scans=[ScanHistoryItem.model_validate(s) for s in recent_scans]
)
@router.get("/{scan_id}", response_model=ScanResponse)
async def get_scan(
scan_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(ScanResult).where(
ScanResult.id == scan_id,
ScanResult.user_id == current_user.id,
)
)
scan = result.scalar_one_or_none()
if scan is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scan not found")
return ScanResponse(
id=scan.id,
url=scan.url,
security_score=scan.security_score,
layers={k: LayerStatus(**v) for k, v in scan.layers.items()},
issues=[Issue(**i) for i in scan.issues],
created_at=scan.created_at,
)
@router.delete("/{scan_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_scan(
scan_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(ScanResult).where(
ScanResult.id == scan_id,
ScanResult.user_id == current_user.id,
)
)
scan = result.scalar_one_or_none()
if scan is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scan not found")
await db.delete(scan)
@router.post("/{scan_id}/chat", response_model=ChatResponse)
async def chat_about_scan(
scan_id: str,
data: ChatRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(ScanResult).where(
ScanResult.id == scan_id,
ScanResult.user_id == current_user.id,
)
)
scan = result.scalar_one_or_none()
if not scan:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scan not found")
context_data = {
"url": scan.url,
"score": scan.security_score,
"layers": scan.layers,
"issues": scan.issues,
}
reply = await chat_with_scan_context(scan_id, context_data, data.message)
return ChatResponse(reply=reply)
@router.get("/{scan_id}/threat-narrative", response_model=ThreatNarrativeResponse)
async def get_threat_narrative(
scan_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(ScanResult).where(
ScanResult.id == scan_id,
ScanResult.user_id == current_user.id,
)
)
scan = result.scalar_one_or_none()
if not scan:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scan not found")
context_data = {
"url": scan.url,
"score": scan.security_score,
"layers": scan.layers,
"issues": scan.issues,
}
narrative = await generate_threat_narrative(context_data)
return ThreatNarrativeResponse(narrative=narrative)
@router.get("/{old_id}/diff/{new_id}", response_model=ScanDiffResponse)
async def diff_scans(
old_id: str,
new_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(ScanResult).where(
ScanResult.id.in_([old_id, new_id]),
ScanResult.user_id == current_user.id
)
)
scans = result.scalars().all()
if len(scans) != 2:
raise HTTPException(status_code=404, detail="One or both scans not found, or access denied.")
s_old = scans[0] if scans[0].id == old_id else scans[1]
s_new = scans[1] if scans[1].id == new_id else scans[0]
# Convert to set-like structures using issue names
old_map = {i.get("issue"): i for i in s_old.issues}
new_map = {i.get("issue"): i for i in s_new.issues}
resolved = [v for k, v in old_map.items() if k not in new_map]
new_issues = [v for k, v in new_map.items() if k not in old_map]
persisting = [v for k, v in new_map.items() if k in old_map]
return ScanDiffResponse(
resolved_issues=resolved,
new_issues=new_issues,
persisting_issues=persisting,
score_change=s_new.security_score - s_old.security_score
)