feat: guarddog-nexus — webhook-based PyPI scanner with web UI
This commit is contained in:
41
guarddog_nexus/web/templates/base.html
Normal file
41
guarddog_nexus/web/templates/base.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>GuardDog Nexus</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<style>
|
||||
.flagged { color: var(--pico-color-red-400); font-weight: bold; }
|
||||
.clean { color: var(--pico-color-green-400); }
|
||||
.status-pending { color: var(--pico-color-yellow-400); }
|
||||
.status-scanning { color: var(--pico-color-blue-400); }
|
||||
.status-completed { color: var(--pico-color-green-400); }
|
||||
.status-failed { color: var(--pico-color-red-400); }
|
||||
.severity-WARNING { color: var(--pico-color-yellow-400); }
|
||||
.severity-ERROR { color: var(--pico-color-red-400); }
|
||||
.finding-card { margin-bottom: 0.5rem; padding: 0.5rem; border-left: 3px solid; }
|
||||
.finding-card.WARNING { border-left-color: var(--pico-color-yellow-400); }
|
||||
.finding-card.ERROR { border-left-color: var(--pico-color-red-400); }
|
||||
.finding-card.INFO { border-left-color: var(--pico-color-blue-400); }
|
||||
table { font-size: 0.9rem; }
|
||||
nav { margin-bottom: 1rem; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
||||
.stat-card { text-align: center; padding: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<nav>
|
||||
<ul><li><strong><a href="/">GuardDog Nexus</a></strong></li></ul>
|
||||
<ul>
|
||||
<li><a href="/">Dashboard</a></li>
|
||||
<li><a href="/scans">Scans</a></li>
|
||||
<li><a href="/packages">Packages</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
8
guarddog_nexus/web/templates/dashboard.html
Normal file
8
guarddog_nexus/web/templates/dashboard.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<div hx-get="/api/v1/scans/stats" hx-trigger="every 30s" hx-swap="innerHTML">
|
||||
{% include "dashboard_stats.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
56
guarddog_nexus/web/templates/dashboard_stats.html
Normal file
56
guarddog_nexus/web/templates/dashboard_stats.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<div class="stats-grid">
|
||||
<article class="stat-card">
|
||||
<h5>{{ total_scans }}</h5>
|
||||
<small>Total Scans</small>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<h5 class="flagged">{{ flagged_scans }}</h5>
|
||||
<small>Flagged</small>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<h5 class="flagged">{{ recent_flagged }}</h5>
|
||||
<small>Flagged (7 days)</small>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<h5>{{ total_findings }}</h5>
|
||||
<small>Total Findings</small>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<h2>Latest Scans</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>Version</th>
|
||||
<th>Ecosystem</th>
|
||||
<th>Status</th>
|
||||
<th>Findings</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in latest_scans %}
|
||||
<tr>
|
||||
<td><a href="/packages/{{ s.package_name }}/{{ s.package_version }}">{{ s.package_name }}</a></td>
|
||||
<td>{{ s.package_version }}</td>
|
||||
<td>{{ s.ecosystem }}</td>
|
||||
<td><span class="status-{{ s.status }}">{{ s.status }}</span></td>
|
||||
<td>{% if s.flagged %}<span class="flagged">{{ s.total_findings }}</span>{% else %}<span class="clean">{{ s.total_findings }}</span>{% endif %}</td>
|
||||
<td>{{ s.started_at.strftime('%Y-%m-%d %H:%M') if s.started_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if top_rules %}
|
||||
<h2>Top Rules Triggered</h2>
|
||||
<table>
|
||||
<thead><tr><th>Rule</th><th>Count</th></tr></thead>
|
||||
<tbody>
|
||||
{% for rule, cnt in top_rules %}
|
||||
<tr><td><code>{{ rule }}</code></td><td>{{ cnt }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
36
guarddog_nexus/web/templates/package_detail.html
Normal file
36
guarddog_nexus/web/templates/package_detail.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>{{ pkg_name }} <small>v{{ pkg_version }}</small></h1>
|
||||
|
||||
<h2>Scans ({{ scans|length }})</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Repo</th><th>Status</th><th>Findings</th><th>Time</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in scans %}
|
||||
<tr>
|
||||
<td><a href="/scans/{{ s.id }}">#{{ s.id }}</a></td>
|
||||
<td>{{ s.repository }}</td>
|
||||
<td><span class="status-{{ s.status }}">{{ s.status }}</span></td>
|
||||
<td>{% if s.flagged %}<span class="flagged">{{ s.total_findings }}</span>{% else %}<span class="clean">0</span>{% endif %}</td>
|
||||
<td>{{ s.started_at.strftime('%Y-%m-%d %H:%M') if s.started_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Findings ({{ findings|length }})</h2>
|
||||
{% if findings %}
|
||||
{% for f in findings|sort(attribute='severity', reverse=true) %}
|
||||
<article class="finding-card {{ f.severity }}">
|
||||
<strong class="severity-{{ f.severity }}">[{{ f.severity }}]</strong>
|
||||
<strong>{{ f.rule }}</strong>
|
||||
{% if f.location %}<small> @ {{ f.location }}</small>{% endif %}
|
||||
<p>{{ f.message }}</p>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="clean">No findings — package looks clean.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
48
guarddog_nexus/web/templates/packages_list.html
Normal file
48
guarddog_nexus/web/templates/packages_list.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Packages</h1>
|
||||
|
||||
<p>
|
||||
<a href="?flagged={% if flagged_filter == '1' %}0{% else %}1{% endif %}" role="button" class="outline">
|
||||
{% if flagged_filter == '1' %}Show all{% else %}Flagged only{% endif %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Version</th>
|
||||
<th>Ecosystem</th>
|
||||
<th>Repo</th>
|
||||
<th>Flagged</th>
|
||||
<th>Findings</th>
|
||||
<th>Last Scan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in packages %}
|
||||
<tr>
|
||||
<td><a href="/packages/{{ p.pkg_name }}/{{ p.pkg_ver }}">{{ p.pkg_name }}</a></td>
|
||||
<td>{{ p.pkg_ver }}</td>
|
||||
<td>{{ p.ecosystem }}</td>
|
||||
<td>{{ p.repository }}</td>
|
||||
<td>{% if p.is_flagged %}<span class="flagged">YES</span>{% else %}<span class="clean">No</span>{% endif %}</td>
|
||||
<td>{{ p.findings_sum }}</td>
|
||||
<td>{{ p.last_scan.strftime('%Y-%m-%d %H:%M') if p.last_scan }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% set total_pages = (total // per_page) + (1 if total % per_page else 0) %}
|
||||
{% if total_pages > 1 %}
|
||||
<nav>
|
||||
<ul>
|
||||
<li>{% if page > 1 %}<a href="?page={{ page - 1 }}&flagged={{ flagged_filter }}">Prev</a>{% else %}<span>Prev</span>{% endif %}</li>
|
||||
<li><small>Page {{ page }} of {{ total_pages }}</small></li>
|
||||
<li>{% if page < total_pages %}<a href="?page={{ page + 1 }}&flagged={{ flagged_filter }}">Next</a>{% else %}<span>Next</span>{% endif %}</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
30
guarddog_nexus/web/templates/scan_detail.html
Normal file
30
guarddog_nexus/web/templates/scan_detail.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Scan #{{ scan.id }}</h1>
|
||||
|
||||
<table>
|
||||
<tr><td><strong>Package</strong></td><td><a href="/packages/{{ scan.package_name }}/{{ scan.package_version }}">{{ scan.package_name }}</a></td></tr>
|
||||
<tr><td><strong>Version</strong></td><td>{{ scan.package_version }}</td></tr>
|
||||
<tr><td><strong>Ecosystem</strong></td><td>{{ scan.ecosystem }}</td></tr>
|
||||
<tr><td><strong>Repository</strong></td><td>{{ scan.repository }}</td></tr>
|
||||
<tr><td><strong>Status</strong></td><td><span class="status-{{ scan.status }}">{{ scan.status }}</span></td></tr>
|
||||
<tr><td><strong>SHA256</strong></td><td><code>{{ scan.sha256 or '-' }}</code></td></tr>
|
||||
<tr><td><strong>Started</strong></td><td>{{ scan.started_at.isoformat() if scan.started_at }}</td></tr>
|
||||
<tr><td><strong>Finished</strong></td><td>{{ scan.finished_at.isoformat() if scan.finished_at }}</td></tr>
|
||||
{% if scan.error_message %}<tr><td><strong>Error</strong></td><td><span class="flagged">{{ scan.error_message }}</span></td></tr>{% endif %}
|
||||
</table>
|
||||
|
||||
<h2>Findings ({{ scan.findings|length }})</h2>
|
||||
{% if scan.findings %}
|
||||
{% for f in scan.findings|sort(attribute='severity', reverse=true) %}
|
||||
<article class="finding-card {{ f.severity }}">
|
||||
<strong class="severity-{{ f.severity }}">[{{ f.severity }}]</strong>
|
||||
<strong>{{ f.rule }}</strong>
|
||||
{% if f.location %}<small> @ {{ f.location }}</small>{% endif %}
|
||||
<p>{{ f.message }}</p>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="clean">No findings — package looks clean.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
48
guarddog_nexus/web/templates/scans_list.html
Normal file
48
guarddog_nexus/web/templates/scans_list.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Scans</h1>
|
||||
|
||||
<p>
|
||||
<a href="?flagged={% if flagged_filter == '1' %}0{% else %}1{% endif %}" role="button" class="outline">
|
||||
{% if flagged_filter == '1' %}Show all{% else %}Flagged only{% endif %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Package</th>
|
||||
<th>Version</th>
|
||||
<th>Repo</th>
|
||||
<th>Status</th>
|
||||
<th>Findings</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in scans %}
|
||||
<tr>
|
||||
<td><a href="/scans/{{ s.id }}">#{{ s.id }}</a></td>
|
||||
<td>{{ s.package_name }}</td>
|
||||
<td>{{ s.package_version }}</td>
|
||||
<td>{{ s.repository }}</td>
|
||||
<td><span class="status-{{ s.status }}">{{ s.status }}</span></td>
|
||||
<td>{% if s.flagged %}<span class="flagged">{{ s.total_findings }}</span>{% else %}<span class="clean">0</span>{% endif %}</td>
|
||||
<td>{{ s.started_at.strftime('%Y-%m-%d %H:%M') if s.started_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% set total_pages = (total // per_page) + (1 if total % per_page else 0) %}
|
||||
{% if total_pages > 1 %}
|
||||
<nav>
|
||||
<ul>
|
||||
<li>{% if page > 1 %}<a href="?page={{ page - 1 }}&flagged={{ flagged_filter }}">Prev</a>{% else %}<span>Prev</span>{% endif %}</li>
|
||||
<li><small>Page {{ page }} of {{ total_pages }}</small></li>
|
||||
<li>{% if page < total_pages %}<a href="?page={{ page + 1 }}&flagged={{ flagged_filter }}">Next</a>{% else %}<span>Next</span>{% endif %}</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user