feat: Added vuln filter
This commit is contained in:
@@ -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
@@ -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
Reference in New Issue
Block a user