"""REST API for packages (distinct packages across scans).""" import csv import io from fastapi import APIRouter, Depends, Query, Response from sqlalchemy import func, select, text from sqlalchemy.ext.asyncio import AsyncSession from guarddog_nexus.database import get_session from guarddog_nexus.models import Finding, Scan router = APIRouter(prefix="/api/v1/packages", tags=["packages"]) VALID_SORT_FIELDS = { "name": Scan.package_name, "version": Scan.package_version, "last_scanned_at": Scan.started_at, "total_findings": Scan.total_findings, "flagged": Scan.flagged, } @router.get("") async def list_packages( limit: int = Query(50, le=200), offset: int = Query(0, ge=0), ecosystem: str | None = Query(None), flagged: bool | None = Query(None), search: str | None = Query(None), repository: str | None = Query(None), sort_by: str = Query("last_scanned_at"), sort_dir: str = Query("desc"), session: AsyncSession = Depends(get_session), ): subq = select( Scan.package_name, Scan.package_version, Scan.ecosystem, Scan.repository, func.max(Scan.started_at).label("last_scanned_at"), func.max(Scan.flagged).label("is_flagged"), func.sum(Scan.total_findings).label("total_findings"), func.max(Scan.id).label("latest_scan_id"), ).group_by(Scan.package_name, Scan.package_version) if ecosystem: subq = subq.where(Scan.ecosystem == ecosystem) if flagged is not None: subq = subq.having(func.max(Scan.flagged) == flagged) if repository: subq = subq.where(Scan.repository == repository) if search: pattern = f"%{search}%" subq = subq.where( Scan.package_name.ilike(pattern) | Scan.package_version.ilike(pattern) ) sort_field = VALID_SORT_FIELDS.get(sort_by, Scan.started_at) sort_dir = "asc" if sort_dir.lower() == "asc" else "desc" sort_col = func.max(sort_field) subq = subq.order_by( sort_col.desc() if sort_dir == "desc" else sort_col.asc() ) total_q = select(func.count()).select_from(subq.subquery()) total = await session.scalar(total_q) rows = ( await session.execute(subq.offset(offset).limit(limit)) ).all() return { "total": total, "limit": limit, "offset": offset, "packages": [ { "name": r.package_name, "version": r.package_version, "ecosystem": r.ecosystem, "repository": r.repository, "last_scanned_at": r.last_scanned_at.isoformat() if r.last_scanned_at else None, "flagged": bool(r.is_flagged), "total_findings": r.total_findings, "latest_scan_id": r.latest_scan_id, } for r in rows ], } @router.get("/export") async def export_packages_csv( flagged: bool | None = Query(None), search: str | None = Query(None), session: AsyncSession = Depends(get_session), ): subq = select( Scan.package_name, Scan.package_version, Scan.ecosystem, Scan.repository, func.max(Scan.started_at).label("last_scanned_at"), func.max(Scan.flagged).label("is_flagged"), func.sum(Scan.total_findings).label("total_findings"), ).group_by(Scan.package_name, Scan.package_version) if flagged is not None: subq = subq.having(func.max(Scan.flagged) == flagged) if search: pattern = f"%{search}%" subq = subq.where( Scan.package_name.ilike(pattern) | Scan.package_version.ilike(pattern) ) subq = subq.order_by(func.max(Scan.started_at).desc()) rows = (await session.execute(subq)).all() output = io.StringIO() writer = csv.writer(output) writer.writerow([ "name", "version", "ecosystem", "repository", "last_scanned_at", "flagged", "total_findings" ]) for r in rows: writer.writerow([ r.package_name, r.package_version, r.ecosystem, r.repository, r.last_scanned_at.isoformat() if r.last_scanned_at else "", bool(r.is_flagged), r.total_findings, ]) return Response( content=output.getvalue(), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=packages_export.csv"}, ) @router.get("/{name}/{version}") async def get_package( name: str, version: str, session: AsyncSession = Depends(get_session), ): scans = ( ( await session.execute( select(Scan) .where(Scan.package_name == name, Scan.package_version == version) .order_by(Scan.started_at.desc()) ) ) .scalars() .all() ) if not scans: return {"detail": "Not found"} all_findings: list[dict] = [] for s in scans: findings = ( (await session.execute(select(Finding).where(Finding.scan_id == s.id))).scalars().all() ) for f in findings: all_findings.append({"id": f.id, **f.data}) return { "name": scans[0].package_name, "version": scans[0].package_version, "ecosystem": scans[0].ecosystem, "repository": scans[0].repository, "flagged": any(s.flagged for s in scans), "scans": [ { "id": s.id, "status": s.status, "total_findings": s.total_findings, "flagged": s.flagged, "started_at": s.started_at.isoformat() if s.started_at else None, } for s in scans ], "findings": all_findings, }