ui: добавить экспорт в CSV для сканирований и пакетов

- api/scans.py: добавить /api/v1/scans/export endpoint с фильтрами
- api/packages.py: добавить /api/v1/packages/export endpoint с фильтрами
- scans_list.html: добавить кнопку Export CSV в filter bar
- packages_list.html: добавить кнопку Export CSV в filter bar
- CSV включает все поля с правильным форматированием
This commit is contained in:
Marker689
2026-05-10 03:16:58 +03:00
parent d5df1d75b8
commit 97b89d3fdf
4 changed files with 104 additions and 2 deletions

View File

@@ -1,6 +1,9 @@
"""REST API for packages (distinct packages across scans)."""
from fastapi import APIRouter, Depends, Query
import csv
import io
from fastapi import APIRouter, Depends, Query, Response
from sqlalchemy import func, select, text
from sqlalchemy.ext.asyncio import AsyncSession
@@ -87,6 +90,54 @@ async def list_packages(
}
@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,