mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-24 04:14:18 +00:00
119 lines
4.8 KiB
TypeScript
119 lines
4.8 KiB
TypeScript
import { TriangleAlert } from "lucide-react";
|
|
import Link from "next/link";
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
|
|
import { issuesHref } from "@/lib/issues/scope";
|
|
import { VERDICT_TONE, VERDICTS, VerdictTone } from "@/lib/issues/verdicts";
|
|
import { cn } from "@/lib/utils";
|
|
import { Verdict } from "@/types/issues";
|
|
|
|
interface ExploitabilityChartProps {
|
|
counts: Record<Verdict, number>;
|
|
issues: number;
|
|
findings: number;
|
|
actionable: number;
|
|
/** Overview scope, carried into every drill-down so the counts still match. */
|
|
scope: Record<string, string>;
|
|
}
|
|
|
|
/** The verdicts below the headline, in the order the card lists them. */
|
|
const REST: Verdict[] = ["conditional", "unknown", "not_exploitable"];
|
|
|
|
/** The dot each verdict row wears. */
|
|
const TONE_DOT: Record<VerdictTone, string> = {
|
|
[VERDICT_TONE.fail]: "bg-bg-fail",
|
|
[VERDICT_TONE.warning]: "bg-bg-data-medium",
|
|
[VERDICT_TONE.pass]: "bg-bg-pass",
|
|
[VERDICT_TONE.neutral]: "bg-bg-data-muted",
|
|
};
|
|
|
|
/**
|
|
* The fold, sized for the ThreatScore row.
|
|
*
|
|
* The headline is exploitable + likely — the issues someone should look at
|
|
* tonight — on a red surface. Everything else is work, not an incident, so it
|
|
* sits as a quiet list underneath. Every number links into the issue list, so
|
|
* nothing here is a dead end.
|
|
*/
|
|
export const ExploitabilityChart = ({
|
|
counts,
|
|
issues,
|
|
findings,
|
|
actionable,
|
|
scope,
|
|
}: ExploitabilityChartProps) => {
|
|
return (
|
|
<Card
|
|
variant="base"
|
|
className="flex min-h-[372px] w-full min-w-0 flex-col lg:max-w-[312px]"
|
|
>
|
|
<CardHeader className="flex items-baseline justify-between gap-3">
|
|
<CardTitle>Exploitability</CardTitle>
|
|
<span className="text-text-neutral-tertiary text-xs">
|
|
{issues.toLocaleString()} {issues === 1 ? "issue" : "issues"}
|
|
</span>
|
|
</CardHeader>
|
|
|
|
<CardContent className="flex flex-1 flex-col gap-3">
|
|
<Link
|
|
href={issuesHref(scope, {
|
|
"filter[verdict__in]": "exploitable,likely",
|
|
})}
|
|
aria-label={`${actionable} exploitable or likely issues`}
|
|
className="border-bg-fail/35 bg-bg-fail/8 hover:bg-bg-fail/15 focus-visible:ring-ring/50 flex items-center gap-3 rounded-lg border px-4 py-3.5 transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
>
|
|
<TriangleAlert className="text-bg-fail size-5 shrink-0" />
|
|
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
<span className="text-text-neutral-primary text-sm font-medium">
|
|
Exploitable or likely
|
|
</span>
|
|
<span className="text-text-neutral-tertiary text-xs">
|
|
{counts.exploitable} exploitable · {counts.likely} likely
|
|
</span>
|
|
</span>
|
|
<span className="text-text-neutral-primary text-[28px] leading-8 font-light tracking-tight tabular-nums">
|
|
{actionable.toLocaleString()}
|
|
</span>
|
|
</Link>
|
|
|
|
<Card variant="inner" padding="none" className="gap-0 px-4 py-0.5">
|
|
<ul className="divide-border-neutral-secondary flex flex-col divide-y">
|
|
{REST.map((id) => {
|
|
const verdict = VERDICTS.find((item) => item.id === id);
|
|
return (
|
|
<li key={id}>
|
|
<Link
|
|
href={issuesHref(scope, { "filter[verdict]": id })}
|
|
aria-label={`${counts[id]} ${(verdict?.label ?? id).toLowerCase()} ${counts[id] === 1 ? "issue" : "issues"}`}
|
|
className="text-text-neutral-secondary hover:text-text-neutral-primary focus-visible:ring-ring/50 flex h-10 items-center gap-2.5 rounded-md text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
>
|
|
<span
|
|
aria-hidden
|
|
className={cn(
|
|
"size-2 shrink-0 rounded-full",
|
|
verdict && TONE_DOT[verdict.tone],
|
|
)}
|
|
/>
|
|
<span className="flex-1">{verdict?.label ?? id}</span>
|
|
<span className="text-text-neutral-primary font-semibold tabular-nums">
|
|
{counts[id].toLocaleString()}
|
|
</span>
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</Card>
|
|
|
|
{/* The fold. Without the findings count the issue count says nothing
|
|
about how much noise was removed, so the two are always shown
|
|
together. */}
|
|
<p className="border-border-neutral-secondary text-text-neutral-tertiary mt-auto border-t pt-3 text-xs">
|
|
{findings.toLocaleString()} failed findings folded into{" "}
|
|
{issues.toLocaleString()} {issues === 1 ? "issue" : "issues"}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|