feat(findings): group findings and add deepen workflows

This commit is contained in:
pphua committed 2026-08-24 19:29:24 +08:00
1 parent 31dc9e7446
commit 27dc6d8bad
9 files changed
+2098 -502

No files matched your search

@@ -8,16 +8,19 @@ import { ArrowLeftIcon, ArrowUpRightIcon, ShieldAlertIcon } from "lucide-react";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
import { StatusBadge } from "@/components/status-badge";
import { CopyButton } from "@/components/copy-button";
import { Markdown } from "@/components/markdown";
import { FindingLineageView } from "./lineage";
import { statusMeta } from "@/lib/status";
@@ -63,6 +66,7 @@ function FieldRow({
function FindingDetailInner() {
const searchParams = useSearchParams();
const id = searchParams.get("id") ?? "";
const contextTaskId = searchParams.get("context_task") ?? "";
const [finding, setFinding] = React.useState<Finding | null>(null);
const [loaded, setLoaded] = React.useState(false);
const [tab, setTab] = React.useState("overview");
@@ -73,18 +77,18 @@ function FindingDetailInner() {
return;
}
api
.getFinding(id)
.getFinding(id, contextTaskId || undefined)
.then((f) => setFinding(f))
.catch(() => setFinding(null))
.finally(() => setLoaded(true));
}, [id]);
}, [contextTaskId, id]);
React.useEffect(() => {
load();
}, [load]);
const changeSeverity = React.useCallback(
async (next: Severity) => {
if (!finding || next === finding.severity) return;
if (!finding || finding.inherited || next === finding.severity) return;
const prev = finding.severity;
setFinding({ ...finding, severity: next });
try {
@@ -101,7 +105,7 @@ function FindingDetailInner() {
const changeStatus = React.useCallback(
async (next: FindingStatus) => {
if (!finding || next === finding.status) return;
if (!finding || finding.inherited || next === finding.status) return;
const prev = finding.status;
setFinding({ ...finding, status: next });
try {
@@ -160,6 +164,11 @@ function FindingDetailInner() {
<Separator orientation="vertical" className="mx-1 h-4" />
<StatusBadge domain="severity" value={finding.severity} dot />
<StatusBadge domain="finding" value={finding.status} dot />
{finding.inherited && finding.source_task_id && (
<Badge variant="outline">
来源任务 #{finding.source_task_id} · 只读
</Badge>
)}
</div>
<TabsList>
<TabsTrigger value="overview">概览</TabsTrigger>
@@ -200,8 +209,11 @@ function FindingDetailInner() {
</Card>
{/* 证据下方:详细报告(Markdown 渲染) */}
<Card>
<CardHeader>
<CardHeader className="flex-row items-center justify-between">
<CardTitle className="text-sm">详细报告</CardTitle>
{finding.report && (
<CopyButton text={finding.report} successMessage="已复制详细报告" />
)}
</CardHeader>
<CardContent>
{finding.report ? (
@@ -228,48 +240,60 @@ function FindingDetailInner() {
</code>
</FieldRow>
{/* 严重等级(可改) */}
{/* 严重等级 */}
<FieldRow label="严重等级">
<Select
value={finding.severity}
onValueChange={(v) => changeSeverity(v as Severity)}
>
<SelectTrigger
size="sm"
className="h-7 w-auto border-none px-1 shadow-none focus-visible:ring-0"
{finding.inherited ? (
<StatusBadge domain="severity" value={finding.severity} dot />
) : (
<Select
value={finding.severity}
onValueChange={(v) => changeSeverity(v as Severity)}
>
<StatusBadge domain="severity" value={finding.severity} dot />
</SelectTrigger>
<SelectContent position="popper" align="end">
{SEVERITIES.map((sv) => (
<SelectItem key={sv} value={sv}>
{statusMeta("severity", sv).label}
</SelectItem>
))}
</SelectContent>
</Select>
<SelectTrigger
size="sm"
className="h-7 w-auto border-none px-1 shadow-none focus-visible:ring-0"
>
<StatusBadge domain="severity" value={finding.severity} dot />
</SelectTrigger>
<SelectContent position="popper" align="end">
<SelectGroup>
{SEVERITIES.map((sv) => (
<SelectItem key={sv} value={sv}>
{statusMeta("severity", sv).label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)}
</FieldRow>
{/* 处理状态(可改) */}
{/* 处理状态 */}
<FieldRow label="处理状态">
<Select
value={finding.status}
onValueChange={(v) => changeStatus(v as FindingStatus)}
>
<SelectTrigger
size="sm"
className="h-7 w-auto border-none px-1 shadow-none focus-visible:ring-0"
{finding.inherited ? (
<StatusBadge domain="finding" value={finding.status} dot />
) : (
<Select
value={finding.status}
onValueChange={(v) => changeStatus(v as FindingStatus)}
>
<StatusBadge domain="finding" value={finding.status} dot />
</SelectTrigger>
<SelectContent position="popper" align="end">
{FINDING_STATUSES.map((st) => (
<SelectItem key={st} value={st}>
{statusMeta("finding", st).label}
</SelectItem>
))}
</SelectContent>
</Select>
<SelectTrigger
size="sm"
className="h-7 w-auto border-none px-1 shadow-none focus-visible:ring-0"
>
<StatusBadge domain="finding" value={finding.status} dot />
</SelectTrigger>
<SelectContent position="popper" align="end">
<SelectGroup>
{FINDING_STATUSES.map((st) => (
<SelectItem key={st} value={st}>
{statusMeta("finding", st).label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)}
</FieldRow>
{/* 漏洞类型 */}
File diff suppressed because it is too large. Load diff
@@ -5,10 +5,12 @@ import Link from "next/link";
import { ArrowUpRightIcon, ChevronRightIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { StatusBadge } from "@/components/status-badge";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
@@ -30,9 +32,11 @@ const FINDING_STATUSES: FindingStatus[] = [
function Row({
f,
contextTaskId,
onStatus,
}: {
f: Finding;
contextTaskId: string;
onStatus: (f: Finding, next: FindingStatus) => void;
}) {
const [open, setOpen] = React.useState(false);
@@ -51,10 +55,17 @@ function Row({
)}
/>
<StatusBadge domain="severity" value={f.severity} dot />
<div className="flex min-w-0 flex-col">
<span className="truncate font-medium">
{f.name || f.vulnclass || "未分类"}
</span>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate font-medium">
{f.name || f.vulnclass || "未分类"}
</span>
{f.inherited && f.source_task_id && (
<Badge variant="outline" className="shrink-0">
来源 #{f.source_task_id} · 只读
</Badge>
)}
</div>
<span className="truncate text-xs text-muted-foreground">
{f.summary}
</span>
@@ -78,7 +89,7 @@ function Row({
)}
</div>
)}
{f.finding_id ? (
{f.finding_id && !f.inherited ? (
<Select
value={f.status}
onValueChange={(v) => onStatus(f, v as FindingStatus)}
@@ -90,11 +101,13 @@ function Row({
<StatusBadge domain="finding" value={f.status} dot />
</SelectTrigger>
<SelectContent position="popper" align="end">
{FINDING_STATUSES.map((st) => (
<SelectItem key={st} value={st}>
{statusMeta("finding", st).label}
</SelectItem>
))}
<SelectGroup>
{FINDING_STATUSES.map((st) => (
<SelectItem key={st} value={st}>
{statusMeta("finding", st).label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
) : (
@@ -105,7 +118,11 @@ function Row({
</span>
{f.finding_id && (
<Link
href={`/function/findings/detail?id=${f.finding_id}`}
href={
f.inherited
? `/function/findings/detail?id=${f.finding_id}&context_task=${contextTaskId}`
: `/function/findings/detail?id=${f.finding_id}`
}
className="text-muted-foreground hover:text-primary inline-flex shrink-0 items-center gap-0.5 text-xs"
title="查看漏洞详情"
>
@@ -151,7 +168,7 @@ export function FindingsTab({ taskId }: { taskId: string }) {
const onStatus = React.useCallback(
async (f: Finding, next: FindingStatus) => {
if (!f.finding_id || next === f.status) return;
if (f.inherited || !f.finding_id || next === f.status) return;
const prev = f.status;
setFindings((cur) =>
cur.map((x) => (x.id === f.id ? { ...x, status: next } : x)),
@@ -170,7 +187,7 @@ export function FindingsTab({ taskId }: { taskId: string }) {
);
const items = findings
.filter((f) => f.task_id === taskId)
.filter((f) => f.task_id === taskId || f.inherited)
.sort((a, b) => {
const order = { critical: 0, high: 1, medium: 2, low: 3 };
return order[a.severity] - order[b.severity];
@@ -180,11 +197,11 @@ export function FindingsTab({ taskId }: { taskId: string }) {
<Card className="overflow-hidden py-0">
<CardContent className="px-0">
{items.map((f) => (
<Row key={f.id} f={f} onStatus={onStatus} />
<Row key={f.id} f={f} contextTaskId={taskId} onStatus={onStatus} />
))}
{items.length === 0 && (
<p className="px-4 py-8 text-center text-sm text-muted-foreground">
本任务暂无确认发现。
本任务及直接关联任务暂无确认发现。
</p>
)}
</CardContent>
@@ -1,11 +1,15 @@
"use client";
import * as React from "react";
import { FileTextIcon, CopyIcon, CheckIcon } from "lucide-react";
import { CheckIcon, CopyIcon, FileTextIcon } from "lucide-react";
import { toast } from "sonner";
import { Markdown } from "@/components/markdown";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { api } from "@/lib/api";
import { copyText } from "@/lib/utils";
export function ReportTab({ taskId }: { taskId: string }) {
const [report, setReport] = React.useState<string>("");
@@ -31,12 +35,39 @@ export function ReportTab({ taskId }: { taskId: string }) {
};
}, [taskId]);
function copy() {
async function copy() {
if (!report) return;
navigator.clipboard?.writeText(report);
setCopied(true);
toast.success("已复制 Markdown");
setTimeout(() => setCopied(false), 1500);
const ok = await copyText(report);
if (ok) {
setCopied(true);
toast.success("已复制 Markdown");
setTimeout(() => setCopied(false), 1500);
} else {
toast.error("复制失败,请手动选择文本复制");
}
}
let content: React.ReactNode;
if (loading) {
content = (
<div className="flex flex-col items-center justify-center gap-2 rounded-md border border-dashed py-16 text-muted-foreground text-sm">
<FileTextIcon className="size-8 opacity-40" />
加载中…
</div>
);
} else if (report) {
content = (
<div className="max-h-[60vh] overflow-auto rounded-md border bg-muted/20 p-4">
<Markdown text={report} />
</div>
);
} else {
content = (
<div className="flex flex-col items-center justify-center gap-2 rounded-md border border-dashed py-16 text-muted-foreground text-sm">
<FileTextIcon className="size-8 opacity-40" />
暂无报告
</div>
);
}
return (
@@ -53,23 +84,7 @@ export function ReportTab({ taskId }: { taskId: string }) {
)}
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex flex-col items-center justify-center gap-2 rounded-md border border-dashed py-16 text-sm text-muted-foreground">
<FileTextIcon className="size-8 opacity-40" />
加载中…
</div>
) : report ? (
<pre className="max-h-[60vh] overflow-auto rounded-md border bg-muted/40 p-4 font-mono text-xs whitespace-pre-wrap">
{report}
</pre>
) : (
<div className="flex flex-col items-center justify-center gap-2 rounded-md border border-dashed py-16 text-sm text-muted-foreground">
<FileTextIcon className="size-8 opacity-40" />
暂无报告
</div>
)}
</CardContent>
<CardContent>{content}</CardContent>
</Card>
);
}