mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-11 05:03:50 +08:00
feat(findings): consolidate duplicate reports into expandable cases
This commit is contained in:
1 parent
a91b27feb7
commit
24d4726aee
29 files changed
+3051
-63
No files matched your search
+45
-2
@@ -37,6 +37,10 @@ import type {
|
||||
ExplorationNodeQuery,
|
||||
Finding,
|
||||
FindingAssetTree,
|
||||
FindingCaseDetail,
|
||||
FindingCasePage,
|
||||
FindingCaseReviewRun,
|
||||
FindingCaseSuggestion,
|
||||
FindingDeepenResponse,
|
||||
FindingGroupsPage,
|
||||
FindingQuery,
|
||||
@@ -555,6 +559,42 @@ export const api = {
|
||||
// 节点带子树聚合计数。不分页——树是导航结构,一次取完。
|
||||
findingAssetTree: (q: Omit<FindingQuery, "page" | "pageSize">) =>
|
||||
get<FindingAssetTree>(`/exploration/findings/asset-tree?${findingFilterParams(q).toString()}`),
|
||||
|
||||
findingCases: (q: FindingQuery) => {
|
||||
const p = findingFilterParams(q);
|
||||
p.set("page", String(q.page));
|
||||
p.set("limit", String(q.pageSize));
|
||||
return get<FindingCasePage>(`/exploration/finding-cases?${p}`);
|
||||
},
|
||||
getFindingCase: (id: string, contextTask?: string) =>
|
||||
get<FindingCaseDetail>(
|
||||
`/exploration/finding-cases/${id}${contextTask ? `?context_task=${encodeURIComponent(contextTask)}` : ""}`,
|
||||
),
|
||||
findingCaseMembers: (id: string, page = 1, contextTask?: string) =>
|
||||
get<FindingsPage>(
|
||||
`/exploration/finding-cases/${id}/members?page=${page}&limit=20${contextTask ? `&context_task=${encodeURIComponent(contextTask)}` : ""}`,
|
||||
),
|
||||
removeFindingCaseMember: (id: string, fid: string, reason: string) =>
|
||||
http<{ ok: boolean }>(`/exploration/finding-cases/${id}/members/${fid}`, {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ reason }),
|
||||
}),
|
||||
findingCaseSuggestions: (task?: string) =>
|
||||
get<FindingCaseSuggestion[]>(
|
||||
`/exploration/finding-case-suggestions${task ? `?task_id=${encodeURIComponent(task)}` : ""}`,
|
||||
),
|
||||
resolveFindingCaseSuggestion: (id: string, accept: boolean) =>
|
||||
post<{ case_id: string }>(`/exploration/finding-case-suggestions/${id}`, { accept }),
|
||||
reviewFindingCases: (ids: string[]) =>
|
||||
post<{ runs: { conversation_id: number; task_id: string }[] }>("/exploration/finding-case-review", {
|
||||
finding_ids: ids,
|
||||
}),
|
||||
regenerateFindingCase: (id: string, contextTask?: string) =>
|
||||
post<{ runs: { conversation_id: number; task_id: string }[] }>(
|
||||
`/exploration/finding-case-review${contextTask ? `?context_task=${encodeURIComponent(contextTask)}` : ""}`,
|
||||
{ case_id: id },
|
||||
),
|
||||
findingCaseReviewRuns: () => get<FindingCaseReviewRun[]>("/exploration/finding-case-review"),
|
||||
findingStats: () => get<FindingStats>("/exploration/findings/stats"),
|
||||
// exportFindings 触发发现页导出并下载文件。scope=selected 时传 ids(finding_id 列表);
|
||||
// scope=filtered 时传当前筛选(沿用 FindingQuery 的筛选字段);scope=all 忽略筛选。
|
||||
@@ -563,8 +603,12 @@ export const api = {
|
||||
scope: "filtered" | "all" | "selected";
|
||||
filters?: Omit<FindingQuery, "page" | "pageSize">;
|
||||
ids?: string[];
|
||||
includeOriginals?: boolean;
|
||||
consolidated?: boolean;
|
||||
}) => {
|
||||
const p = new URLSearchParams({ format: opts.format, scope: opts.scope });
|
||||
p.set("mode", opts.consolidated === false ? "raw" : "consolidated");
|
||||
p.set("include_originals", String(opts.includeOriginals ?? false));
|
||||
if (opts.scope === "selected") {
|
||||
p.set("ids", (opts.ids ?? []).join(","));
|
||||
} else if (opts.scope === "filtered" && opts.filters) {
|
||||
@@ -825,8 +869,7 @@ export const api = {
|
||||
// 渠道是多实例资源(同一类型可配多个机器人、各有过滤规则),因此独立成组,
|
||||
// 不塞进扁平的 settings 键值里。
|
||||
notifyMeta: () => get<NotificationMeta>(`/notify/meta`),
|
||||
notifyChannels: () =>
|
||||
get<{ channels: NotificationChannel[] }>(`/notify/channels`).then((r) => arr(r.channels)),
|
||||
notifyChannels: () => get<{ channels: NotificationChannel[] }>(`/notify/channels`).then((r) => arr(r.channels)),
|
||||
notifyCreateChannel: (payload: {
|
||||
name: string;
|
||||
kind: string;
|
||||
|
||||
@@ -1843,6 +1843,35 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
return { deleted: 1, assets_deleted: assetsDeleted };
|
||||
}
|
||||
|
||||
// Demo mode keeps raw observations as independent findings, without executing a model.
|
||||
if (path === "/exploration/finding-case-suggestions" || (path === "/exploration/finding-case-review" && m === "GET"))
|
||||
return [];
|
||||
if (path === "/exploration/finding-case-review" && m === "POST")
|
||||
throw new Error("演示模式不执行 Reporter 整理,请在已配置模型的实例中使用");
|
||||
if (path === "/exploration/finding-cases" && m === "GET") {
|
||||
const list = mockApplyAssetScope(mockFilterFindings(q), q.get("asset_scope"));
|
||||
const ranks = { critical: 4, high: 3, medium: 2, low: 1 } as const;
|
||||
list.sort(
|
||||
(a, b) =>
|
||||
(q.get("sort") === "severity" ? ranks[b.severity] - ranks[a.severity] : 0) || +new Date(b.ts) - +new Date(a.ts),
|
||||
);
|
||||
const page = Math.max(1, Number(q.get("page")) || 1);
|
||||
const size = Math.min(100, Math.max(1, Number(q.get("limit")) || 20));
|
||||
const scoped = mockFindings.filter((f) => !q.get("task_id") || f.task_id === q.get("task_id"));
|
||||
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
||||
for (const f of scoped) counts[f.severity]++;
|
||||
return {
|
||||
items: list.slice((page - 1) * size, page * size).map((f) => ({
|
||||
finding: { ...f, report: undefined, finding_id: f.id, traffic_count: mockTrafficCount(f.id) },
|
||||
matched_ids: [],
|
||||
})),
|
||||
total: list.length,
|
||||
page,
|
||||
page_size: size,
|
||||
stats: { total: scoped.length, reports: scoped.length, unassessed: 0, ...counts },
|
||||
};
|
||||
}
|
||||
|
||||
// ── exploration ──
|
||||
if (path === "/exploration/frontier") return D.frontier;
|
||||
if (path === "/exploration/findings/stats") {
|
||||
|
||||
@@ -5,6 +5,7 @@ export type TaskStatus = "created" | "queued" | "running" | "paused" | "done" |
|
||||
export type EngineMode = "exploring" | "paused" | "stalled" | "idle";
|
||||
|
||||
export interface Task {
|
||||
distinct_findings?: FindingDistinctStats;
|
||||
id: string;
|
||||
name?: string; // 可选任务名称;空/缺省=未命名,展示时回退到描述
|
||||
category_id?: number;
|
||||
@@ -489,6 +490,7 @@ export interface FindingAsset {
|
||||
}
|
||||
|
||||
export interface Finding {
|
||||
case_id?: string;
|
||||
traffic_count?: number;
|
||||
evidence_version?: number;
|
||||
report_evidence_version?: number;
|
||||
@@ -550,6 +552,7 @@ export interface FindingDeepenResponse {
|
||||
|
||||
// FindingStats 是发现全表聚合(统计卡 + 漏洞类型下拉),服务端计算,不受分页影响。
|
||||
export interface FindingStats {
|
||||
distinct?: FindingDistinctStats;
|
||||
total: number;
|
||||
pending: number;
|
||||
critical: number;
|
||||
@@ -1610,3 +1613,70 @@ export interface InterceptExecution {
|
||||
seq: number;
|
||||
items: Activity[];
|
||||
}
|
||||
|
||||
export interface FindingDistinctStats {
|
||||
total: number;
|
||||
reports: number;
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
unassessed: number;
|
||||
}
|
||||
export interface FindingCase {
|
||||
id: string;
|
||||
task_id: string | null;
|
||||
title: string;
|
||||
reason: string;
|
||||
severity: Severity | "";
|
||||
severity_reason: string;
|
||||
report?: string;
|
||||
version: number;
|
||||
report_version: number;
|
||||
active: boolean;
|
||||
count: number;
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
created_at: string;
|
||||
}
|
||||
export interface FindingCaseListRow {
|
||||
case?: FindingCase | null;
|
||||
finding?: Finding;
|
||||
matched_ids: number[];
|
||||
}
|
||||
export interface FindingCasePage {
|
||||
items: FindingCaseListRow[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
stats: FindingDistinctStats;
|
||||
}
|
||||
export interface FindingCaseEvent {
|
||||
id: string;
|
||||
action: string;
|
||||
actor: string;
|
||||
reason: string;
|
||||
finding_ids: number[];
|
||||
created_at: string;
|
||||
}
|
||||
export interface FindingCaseDetail {
|
||||
case: FindingCase;
|
||||
events: FindingCaseEvent[];
|
||||
}
|
||||
export interface FindingCaseSuggestion {
|
||||
id: string;
|
||||
task_id: string;
|
||||
left_id: string;
|
||||
right_id: string;
|
||||
title: string;
|
||||
reason: string;
|
||||
state: string;
|
||||
}
|
||||
export interface FindingCaseReviewRun {
|
||||
conversation_id: number;
|
||||
task_id: string;
|
||||
state: "queued" | "running" | "done" | "failed";
|
||||
error: string;
|
||||
}
|
||||
Reference in new issue
Block a user