mirror of
https://github.com/Rarebuffalo/securelens-backend.git
synced 2026-06-19 07:00:30 +00:00
updated the architecture
This commit is contained in:
226
app/routers/history.py
Normal file
226
app/routers/history.py
Normal file
@@ -0,0 +1,226 @@
|
||||
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
|
||||
)
|
||||
Reference in New Issue
Block a user