"""Scanning and Asset Checkup API endpoints.""" from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from typing import List from datetime import datetime from pydantic import BaseModel from app.db.session import get_db from app.models.models import Asset, Finding, Tenant from app.core.security import require_it_or_above from app.services.scanner import run_comprehensive_scan router = APIRouter(prefix="/scanning", tags=["scanning"]) class ScanRequest(BaseModel): asset_ids: List[str] = [] scan_all_assets: bool = False class ScanResult(BaseModel): status: str assets_scanned: int findings_created: int scan_started_at: datetime @router.post("/start-scan") async def start_scan( tenant_id: str = Query(...), request: ScanRequest = ..., background_tasks: BackgroundTasks = ..., payload: dict = Depends(require_it_or_above), db: AsyncSession = Depends(get_db), ): """Start comprehensive security scan on tenant assets.""" if payload.get("role") != "trustos_admin" and payload.get("tenant_id") != tenant_id: raise HTTPException(status_code=403, detail="Access denied") # Get assets to scan if request.scan_all_assets: result = await db.execute(select(Asset).where(Asset.tenant_id == tenant_id)) assets = result.scalars().all() else: result = await db.execute( select(Asset).where( Asset.tenant_id == tenant_id, Asset.id.in_(request.asset_ids) if request.asset_ids else True ) ) assets = result.scalars().all() if not assets: raise HTTPException(status_code=400, detail="No assets found to scan") # Queue background scans for asset in assets: background_tasks.add_task( run_comprehensive_scan, tenant_id, asset.id, asset.value, asset.asset_type.value if hasattr(asset.asset_type, 'value') else asset.asset_type, ) return ScanResult( status="scan_started", assets_scanned=len(assets), findings_created=0, scan_started_at=datetime.utcnow(), ) @router.get("/status") async def scan_status( tenant_id: str = Query(...), payload: dict = Depends(require_it_or_above), db: AsyncSession = Depends(get_db), ): """Get latest scan status and statistics.""" if payload.get("role") != "trustos_admin" and payload.get("tenant_id") != tenant_id: raise HTTPException(status_code=403, detail="Access denied") # Get all findings from automated scanner in last 24 hours from datetime import timedelta since = datetime.utcnow() - timedelta(days=1) result = await db.execute( select(Finding) .where( Finding.tenant_id == tenant_id, Finding.source == "automated_scanner", Finding.created_at >= since, ) .order_by(desc(Finding.created_at)) ) recent_findings = result.scalars().all() # Count by severity critical = sum(1 for f in recent_findings if f.severity.value == "critical") high = sum(1 for f in recent_findings if f.severity.value == "high") medium = sum(1 for f in recent_findings if f.severity.value == "medium") return { "status": "scan_complete", "last_scan": recent_findings[0].created_at if recent_findings else None, "findings_found": len(recent_findings), "critical_count": critical, "high_count": high, "medium_count": medium, "completion_percentage": min(100, 70 + (len(recent_findings) * 2)), # Progress metric } @router.get("/recent-findings") async def get_recent_findings( tenant_id: str = Query(...), limit: int = 20, payload: dict = Depends(require_it_or_above), db: AsyncSession = Depends(get_db), ): """Get findings from recent scans.""" if payload.get("role") != "trustos_admin" and payload.get("tenant_id") != tenant_id: raise HTTPException(status_code=403, detail="Access denied") result = await db.execute( select(Finding) .where( Finding.tenant_id == tenant_id, Finding.source == "automated_scanner", ) .order_by(desc(Finding.created_at)) .limit(limit) ) findings = result.scalars().all() return [ { "id": f.id, "title": f.title, "severity": f.severity.value, "category": f.category.value, "affected_component": f.affected_component, "found_at": f.created_at, "status": f.status.value if f.status else "open", } for f in findings ] @router.get("/asset-health") async def get_asset_health( tenant_id: str = Query(...), payload: dict = Depends(require_it_or_above), db: AsyncSession = Depends(get_db), ): """Get health score for each asset based on findings.""" if payload.get("role") != "trustos_admin" and payload.get("tenant_id") != tenant_id: raise HTTPException(status_code=403, detail="Access denied") # Get all assets result = await db.execute(select(Asset).where(Asset.tenant_id == tenant_id)) assets = result.scalars().all() asset_health = [] for asset in assets: # Count findings for this asset finding_result = await db.execute( select(Finding).where( Finding.tenant_id == tenant_id, Finding.affected_component == asset.value, ) ) findings = finding_result.scalars().all() critical = sum(1 for f in findings if f.severity.value == "critical") health_score = max(0, 100 - (critical * 20 + len(findings) * 2)) asset_health.append({ "asset_id": asset.id, "asset_name": asset.name, "asset_value": asset.value, "asset_type": asset.asset_type.value if hasattr(asset.asset_type, 'value') else asset.asset_type, "findings_count": len(findings), "critical_count": critical, "health_score": health_score, "risk_level": "critical" if health_score < 30 else "high" if health_score < 60 else "medium" if health_score < 80 else "low", }) return asset_health