"""REST API for packages (distinct packages across scans).""" from fastapi import APIRouter, Depends, Query from sqlalchemy import func, select 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"]) @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), 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) total_q = select(func.count()).select_from(subq.subquery()) total = await session.scalar(total_q) rows = ( await session.execute( subq.order_by(func.max(Scan.started_at).desc()).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("/{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, }