Files
guarddog-nexus/guarddog_nexus/api/packages.py

113 lines
3.3 KiB
Python

"""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,
}