mirror of
https://github.com/Rarebuffalo/securelens-backend.git
synced 2026-06-19 07:00:30 +00:00
227 lines
6.6 KiB
Python
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
|
|
)
|