feat: Added vuln filter
Chart Release / chart (push) Successful in 25s
Server Deploy / deploy (push) Successful in 2m29s

This commit is contained in:
2026-08-07 11:58:42 +01:00
parent 82bcc5776f
commit e28238191d
5 changed files with 83 additions and 7 deletions
+46 -5
View File
@@ -26,17 +26,33 @@ import { groupByPackage } from "@/lib/vulnPackages";
const STATES: FindingState[] = ["open", "accepted", "fixed"];
/*
* The fix filter. "Unfixable" is not a synonym for "ignorable": those findings
* are the ones whose action is to remove the package, disable the service or
* move off an end-of-life release, and they are invisible in a list sorted for
* patching. Splitting them is what lets the patchable list be worked top to
* bottom without them quietly disappearing.
*/
const FIX_FILTERS: { key: string; label: string; hasFix: boolean | undefined }[] = [
{ key: "all", label: "All", hasFix: undefined },
{ key: "fixable", label: "Fix available", hasFix: true },
{ key: "nofix", label: "No fix", hasFix: false },
];
export default function VulnerabilitiesPage() {
const { isAdmin } = useAuth();
const qc = useQueryClient();
const [state, setState] = useState<FindingState>("open");
const [severity, setSeverity] = useState<Severity | "">("");
const [fixFilter, setFixFilter] = useState("all");
const [accepting, setAccepting] = useState<VulnFinding | null>(null);
const hasFix = FIX_FILTERS.find((f) => f.key === fixFilter)?.hasFix;
const groups = useQuery({
queryKey: ["vulnerabilities", state, severity],
queryFn: () => vulnerabilities.list({ state, severity: severity || undefined }),
queryKey: ["vulnerabilities", state, severity, fixFilter],
queryFn: () => vulnerabilities.list({ state, severity: severity || undefined, hasFix }),
});
const summary = useQuery({
@@ -128,7 +144,7 @@ export default function VulnerabilitiesPage() {
))}
</div>
<div className="mb-4 flex gap-2">
<div className="mb-4 flex flex-wrap items-center gap-2">
{STATES.map((s) => (
<button
key={s}
@@ -142,6 +158,22 @@ export default function VulnerabilitiesPage() {
{s}
</button>
))}
<span aria-hidden className="mx-1 h-5 w-px bg-border" />
{FIX_FILTERS.map((f) => (
<button
key={f.key}
onClick={() => {
setFixFilter(f.key);
paged.reset();
}}
aria-pressed={fixFilter === f.key}
className={`rounded-lg border px-3 py-1.5 text-sm transition-colors ${fixFilter === f.key ? "border-accent text-accent" : "border-border text-text-secondary hover:text-text-primary"}`}
>
{f.label}
</button>
))}
</div>
{groups.error && <div className="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{(groups.error as Error).message}</div>}
@@ -161,9 +193,18 @@ export default function VulnerabilitiesPage() {
) : (
<div className="px-6 py-14 text-center">
<p className="text-[15px] font-semibold text-text-primary">
No {state} findings{severity ? ` at ${severity} severity` : ""}.
No {state} findings{severity ? ` at ${severity} severity` : ""}
{hasFix === true ? " with a fix available" : hasFix === false ? " without a vendor fix" : ""}.
</p>
{/* Named explicitly, because "no findings" under a filter
the reader has forgotten setting reads as a clean
fleet — the one claim this page must never make by
accident. */}
<p className="mx-auto mt-2 max-w-[52ch] text-sm text-text-secondary">
{hasFix !== undefined
? "This is a filtered view. Switch to All to see every finding in this state."
: "Servers report their packages hourly. A server whose distribution has no advisory feed is reported as unsupported on its own page rather than counted as clean here."}
</p>
<p className="mx-auto mt-2 max-w-[52ch] text-sm text-text-secondary">Servers report their packages hourly. A server whose distribution has no advisory feed is reported as unsupported on its own page rather than counted as clean here.</p>
</div>
)}
</Card>
+4 -1
View File
@@ -996,11 +996,14 @@ export interface VulnAlertRuleInput {
}
export const vulnerabilities = {
list(params?: { severity?: string; state?: string; server?: string; tags?: Record<string, string> }): Promise<VulnGroup[]> {
list(params?: { severity?: string; state?: string; server?: string; hasFix?: boolean; tags?: Record<string, string> }): Promise<VulnGroup[]> {
const q = new URLSearchParams();
if (params?.severity) q.set("severity", params.severity);
if (params?.state) q.set("state", params.state);
if (params?.server) q.set("server", params.server);
// Explicitly undefined-checked: `false` is a real selection here (the
// unfixable set), so a truthiness test would silently drop it.
if (params?.hasFix !== undefined) q.set("has_fix", String(params.hasFix));
for (const [k, v] of Object.entries(params?.tags ?? {})) q.append("tag", `${k}:${v}`);
const qs = q.toString();
return request<VulnGroup[]>(`/vulnerabilities${qs ? `?${qs}` : ""}`);
File diff suppressed because one or more lines are too long