feat: guarddog-nexus — webhook-based PyPI scanner with web UI

This commit is contained in:
Marker689
2026-05-09 04:48:10 +03:00
parent bdcc82807d
commit 4ce99d3c85
32 changed files with 1865 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
"""Web UI routes — Jinja2 + htmx pages."""
import datetime
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
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(tags=["web"])
TEMPLATES: dict[str, str] = {}
def _render(name: str, **context) -> HTMLResponse:
from jinja2 import Environment, PackageLoader, select_autoescape
env = Environment(
loader=PackageLoader("guarddog_nexus", "web/templates"),
autoescape=select_autoescape(),
)
template = env.get_template(name)
return HTMLResponse(template.render(**context))
@router.get("/", response_class=HTMLResponse)
async def dashboard(request: Request, session: AsyncSession = Depends(get_session)):
total_scans = await session.scalar(select(func.count(Scan.id)))
flagged_scans = await session.scalar(
select(func.count(Scan.id)).where(Scan.flagged == True)
)
recent_flagged = await session.scalar(
select(func.count(Scan.id)).where(
Scan.flagged == True,
Scan.started_at >= func.datetime("now", "-7 days"),
)
)
total_findings = await session.scalar(select(func.count(Finding.id)))
latest_scans = (
(await session.execute(
select(Scan).order_by(Scan.started_at.desc()).limit(10)
))
.scalars()
.all()
)
top_rules = (
await session.execute(
select(Finding.rule, func.count(Finding.id).label("cnt"))
.group_by(Finding.rule)
.order_by(func.count(Finding.id).desc())
.limit(10)
)
).all()
return _render(
"dashboard.html",
total_scans=total_scans,
flagged_scans=flagged_scans,
recent_flagged=recent_flagged,
total_findings=total_findings,
latest_scans=latest_scans,
top_rules=[(r.rule, r.cnt) for r in top_rules],
now=datetime.datetime.now(datetime.timezone.utc),
request=request,
)
@router.get("/scans", response_class=HTMLResponse)
async def scans_list(
request: Request,
page: int = 1,
flagged: str = "",
session: AsyncSession = Depends(get_session),
):
per_page = 50
offset = (page - 1) * per_page
q = select(Scan)
if flagged == "1":
q = q.where(Scan.flagged == True)
q = q.order_by(Scan.started_at.desc()).offset(offset).limit(per_page)
scans = (await session.execute(q)).scalars().all()
total = await session.scalar(select(func.count(Scan.id)))
return _render(
"scans_list.html",
scans=scans,
page=page,
per_page=per_page,
total=total,
flagged_filter=flagged,
request=request,
)
@router.get("/scans/{scan_id}", response_class=HTMLResponse)
async def scan_detail(scan_id: int, request: Request, session: AsyncSession = Depends(get_session)):
from sqlalchemy.orm import selectinload
scan = await session.scalar(
select(Scan).where(Scan.id == scan_id).options(selectinload(Scan.findings))
)
if not scan:
return HTMLResponse("<h1>Not found</h1>", status_code=404)
return _render("scan_detail.html", scan=scan, request=request)
@router.get("/packages", response_class=HTMLResponse)
async def packages_list(
request: Request,
page: int = 1,
flagged: str = "",
session: AsyncSession = Depends(get_session),
):
per_page = 50
offset = (page - 1) * per_page
subq = select(
Scan.package_name.label("pkg_name"),
Scan.package_version.label("pkg_ver"),
Scan.ecosystem,
Scan.repository,
func.max(Scan.started_at).label("last_scan"),
func.max(Scan.flagged).label("is_flagged"),
func.sum(Scan.total_findings).label("findings_sum"),
func.max(Scan.id).label("sid"),
).group_by(Scan.package_name, Scan.package_version)
if flagged == "1":
subq = subq.having(func.max(Scan.flagged) == True)
subq = subq.subquery()
total = await session.scalar(select(func.count()).select_from(subq))
rows = (
await session.execute(
select(subq)
.order_by(subq.c.last_scan.desc())
.offset(offset)
.limit(per_page)
)
).all()
return _render(
"packages_list.html",
packages=rows,
page=page,
per_page=per_page,
total=total,
flagged_filter=flagged,
request=request,
)
@router.get("/packages/{name}/{version}", response_class=HTMLResponse)
async def package_detail(
name: str,
version: str,
request: Request,
session: AsyncSession = Depends(get_session),
):
from sqlalchemy.orm import selectinload
scans = (
await session.execute(
select(Scan)
.where(Scan.package_name == name, Scan.package_version == version)
.options(selectinload(Scan.findings))
.order_by(Scan.started_at.desc())
)
).scalars().all()
if not scans:
return HTMLResponse("<h1>Not found</h1>", status_code=404)
all_findings = []
for s in scans:
all_findings.extend(s.findings)
return _render(
"package_detail.html",
pkg_name=name,
pkg_version=version,
scans=scans,
findings=all_findings,
request=request,
)