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:
@@ -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,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""REST API for 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
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -78,6 +81,52 @@ async def list_scans(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/export")
|
||||
async def export_scans_csv(
|
||||
flagged: bool | None = Query(None),
|
||||
search: str | None = Query(None),
|
||||
status: str | None = Query(None),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
q = select(Scan)
|
||||
|
||||
if flagged is not None:
|
||||
q = q.where(Scan.flagged == flagged)
|
||||
if status:
|
||||
q = q.where(Scan.status == status)
|
||||
if search:
|
||||
pattern = f"%{search}%"
|
||||
q = q.where(
|
||||
Scan.package_name.ilike(pattern) | Scan.package_version.ilike(pattern)
|
||||
)
|
||||
|
||||
q = q.order_by(Scan.started_at.desc())
|
||||
scans = (await session.execute(q)).scalars().all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow([
|
||||
"id", "package_name", "package_version", "ecosystem", "repository",
|
||||
"status", "total_findings", "flagged", "started_at", "finished_at",
|
||||
"error_message", "sha256"
|
||||
])
|
||||
for s in scans:
|
||||
writer.writerow([
|
||||
s.id, s.package_name, s.package_version, s.ecosystem, s.repository,
|
||||
s.status, s.total_findings, s.flagged,
|
||||
s.started_at.isoformat() if s.started_at else "",
|
||||
s.finished_at.isoformat() if s.finished_at else "",
|
||||
s.error_message or "",
|
||||
s.sha256 or "",
|
||||
])
|
||||
|
||||
return Response(
|
||||
content=output.getvalue(),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": "attachment; filename=scans_export.csv"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def scan_stats(session: AsyncSession = Depends(get_session)):
|
||||
total_scans = await session.scalar(select(func.count(Scan.id)))
|
||||
|
||||
Reference in New Issue
Block a user