From 97b89d3fdfcb58e93fa1144122402c4a9539506c Mon Sep 17 00:00:00 2001 From: Marker689 Date: Sun, 10 May 2026 03:16:58 +0300 Subject: [PATCH] =?UTF-8?q?ui:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80=D1=82=20?= =?UTF-8?q?=D0=B2=20CSV=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BA=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B9=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 включает все поля с правильным форматированием --- guarddog_nexus/api/packages.py | 53 ++++++++++++++++++- guarddog_nexus/api/scans.py | 51 +++++++++++++++++- .../web/templates/packages_list.html | 1 + guarddog_nexus/web/templates/scans_list.html | 1 + 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/guarddog_nexus/api/packages.py b/guarddog_nexus/api/packages.py index 11ae274..1cff4dc 100644 --- a/guarddog_nexus/api/packages.py +++ b/guarddog_nexus/api/packages.py @@ -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, diff --git a/guarddog_nexus/api/scans.py b/guarddog_nexus/api/scans.py index 4c45b83..a2816d2 100644 --- a/guarddog_nexus/api/scans.py +++ b/guarddog_nexus/api/scans.py @@ -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))) diff --git a/guarddog_nexus/web/templates/packages_list.html b/guarddog_nexus/web/templates/packages_list.html index c00e921..ff88131 100644 --- a/guarddog_nexus/web/templates/packages_list.html +++ b/guarddog_nexus/web/templates/packages_list.html @@ -15,6 +15,7 @@ {% if flagged_filter == '1' %}Show all{% else %}Flagged only{% endif %} + Export CSV
diff --git a/guarddog_nexus/web/templates/scans_list.html b/guarddog_nexus/web/templates/scans_list.html index c06fa1e..7da28f3 100644 --- a/guarddog_nexus/web/templates/scans_list.html +++ b/guarddog_nexus/web/templates/scans_list.html @@ -22,6 +22,7 @@ {% if flagged_filter == '1' %}Show all{% else %}Flagged only{% endif %} + Export CSV