feat: 新增发现详情页(多 Tab + 状态区可改严重度/处置状态)

详情页 /function/findings/detail?id=<finding_id>:
- 概览 Tab:左栏摘要 + 证据/PoC,右栏状态区(严重等级、漏洞类型、涉及资产、处理状态、所属任务、发现时间)
- 严重等级与处理状态用下拉,选择后直接调后端接口即时修正
- 另有「证据/PoC」「元数据」两个 Tab
- 发现列表页漏洞名称改为链接进入详情

后端:
- GET  /api/exploration/findings/{id}  取单条(含资产解析)
- PATCH /api/exploration/findings/{id} 扩展为部分更新 {status?, severity?}
- db.GetFinding / db.SetFindingSeverity(同步探索节点 payload,保持任务内视图一致)
- db.ValidSeverity 白名单;api 增加 getFinding / setFindingSeverity
- mock 支持单条 GET / PATCH,并回填 finding_id 供 demo 链接

db 测试补充 GetFinding / SetFindingSeverity 往返断言;next build 通过。
This commit is contained in:
Autumn-27 committed 2026-08-18 11:43:50 -04:00
1 parent c69534e09b
commit 64d9c66f6a
7 files changed
+552 -18

No files matched your search

+5 -2
View File
@@ -6,7 +6,7 @@
import { MOCK } from "@/lib/mock/enabled";
import { mockHandle } from "@/lib/mock/handler";
import type {
Task, Stats, Asset, AssetNode, Edge, Company, TaskNode, Finding, FindingStatus,
Task, Stats, Asset, AssetNode, Edge, Company, TaskNode, Finding, Severity, FindingStatus,
FindingsPage, FindingStats, FindingQuery, Activity,
Audit, TrafficResp, TrafficDetail, TrafficHost, Settings, LLMProfile, Agent, AgentDetail, PromptVar,
PromptVersion, MCPServer, MCPTool, SkillItem, TokenUsage, TokenTotal, DailyTokenBucket,
@@ -245,8 +245,11 @@ export const api = {
return get<FindingsPage>(`/exploration/findings?${p.toString()}`);
},
findingStats: () => get<FindingStats>("/exploration/findings/stats"),
getFinding: (id: string) => get<Finding>(`/exploration/findings/${id}`),
setFindingStatus: (id: string, status: FindingStatus) =>
patch<{ id: string; status: FindingStatus }>(`/exploration/findings/${id}`, { status }),
patch<Finding>(`/exploration/findings/${id}`, { status }),
setFindingSeverity: (id: string, severity: Severity) =>
patch<Finding>(`/exploration/findings/${id}`, { severity }),
intents: (task?: string) => get<TaskNode[]>(`/exploration/intents${tq(task)}`).then(arr),
tokenStats: (task?: string) =>
get<{ workers: TokenUsage[]; total: TokenTotal }>(`/exploration/tokens${tq(task)}`).then((r) => ({
+21 -3
View File
@@ -85,10 +85,28 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
vulnclasses,
};
}
// 单条 finding:GET 详情 / PATCH 改状态或严重度(demo 直接改内存对象)。
if (
seg[0] === "exploration" &&
seg[1] === "findings" &&
seg.length === 3 &&
seg[2] !== "stats"
) {
const f = D.findings.find((x) => x.id === seg[2]);
if (!f) return {};
if (m === "PATCH") {
if (typeof b.status === "string") f.status = b.status as typeof f.status;
if (typeof b.severity === "string")
f.severity = b.severity as typeof f.severity;
}
return { ...f, finding_id: f.id };
}
if (path === "/exploration/findings") {
if (task) return D.findings.filter((f) => f.task_id === task);
// finding_id=id:真后端用独立表行 id 作为状态/详情句柄,mock 里用自身 id 顶上。
const withFid = (f: (typeof D.findings)[number]) => ({ ...f, finding_id: f.id });
if (task) return D.findings.filter((f) => f.task_id === task).map(withFid);
// 全局:带 page/limit → 分页对象;否则裸数组(dashboard)。
if (!q.has("page") && !q.has("limit")) return D.findings;
if (!q.has("page") && !q.has("limit")) return D.findings.map(withFid);
const sev = { critical: 4, high: 3, medium: 2, low: 1 } as const;
let list = D.findings.slice();
const fSev = q.get("severity");
@@ -105,7 +123,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const page = Number(q.get("page") ?? 1);
const pageSize = Number(q.get("limit") ?? 20);
return {
items: list.slice((page - 1) * pageSize, page * pageSize),
items: list.slice((page - 1) * pageSize, page * pageSize).map(withFid),
total: list.length,
page,
page_size: pageSize,