From d728e509dcc10cf5792cc4fccead24dd438c2354 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:24:13 -0400 Subject: [PATCH] =?UTF-8?q?feat(overview):=20graph=5Foverview=20=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E8=BE=93=E5=87=BA=20finding=5Flist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findings 是任务最高价值产物且单任务通常不多,改为在 graph_overview 里全量带出 finding_list(不像 facts 只给最近窗口),planner/worker 每轮 即可一眼看全所有确认漏洞,无需再调 list_findings。 每条精简为 {id, summary, evidence?, from_intent?, assets?}: - evidence 取 finding payload 的 evidence.poc - from_intent 为产生该漏洞的意图(yields 边) - assets 直接给受影响资产的可读内容(url/域名/ip:port 等,不再是裸 id), 经 FindingMetaByNodeID 反查锚定资产、批量 GetByIDs 解析 - vulnclass/severity/state 等仍走 list_findings / node_detail 新增 AssetStore.FindingMetaByNodeID 薄封装,让只持有 AssetStore 的 ToolSet 能拿到 node_id→资产映射(读 anchors 表,覆盖新旧漏洞)。 --- agent/planner.go | 2 +- agent/tools.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ db/findings.go | 7 ++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/agent/planner.go b/agent/planner.go index 91b6d1c..1d89348 100644 --- a/agent/planner.go +++ b/agent/planner.go @@ -279,7 +279,7 @@ const plannerDefaultTmpl = `你是一个 ARTEX 平台授权渗透测试系统的 **每次唤醒的决策流程**: -1. **完整态势已附在本提示下方**(就是 graph_overview 的返回,无需再调它):task(原始标题+目标/根节点)、资产计数、goals+状态、open/running/recent_done 意图、sites_without_endpoints(无端点的站点,提示可能待探的方向)、findings(确认漏洞数)、facts(探索事实数,与漏洞是两类)、recent_facts({id,summary,confidence?})。 +1. **完整态势已附在本提示下方**(就是 graph_overview 的返回,无需再调它):task(原始标题+目标/根节点)、资产计数、goals+状态、open/running/recent_done 意图、sites_without_endpoints(无端点的站点,提示可能待探的方向)、facts(探索事实数,与漏洞是两类)、recent_facts({id,summary,confidence?})。 - **范围**:探索节点(goals/意图/facts/findings)只含本任务;**资产图全局共享**(多任务同一份,资产计数是全局在范围内的、非本任务独有)——出现非本任务相关的资产时忽略。 - **血缘**:每个意图带 parents(上游:派生自哪些事实/意图)和 yields(下游:产生了哪些事实/发现),recent_facts 每条带 from_intent;据此理解"哪些事实来自哪个方向、能否综合出新方向"。 - **否定/存疑观察**(recent_facts 里"端口关闭/不可注入"等)是 worker 的观察、不是定论:采信前先 node_detail(id) 看 evidence——evidence 扎实、confidence=observed 且手段已穷尽的才视为该方向暂时封住;evidence 缺失、只是"看起来像/只探一次"、或 confidence=inferred 的,按【尚未探明】处理,若在范围内且无其它意图覆盖,默认派一条复核意图去证实或推翻(**同一否定方向至多复核一次**;复核后仍为否定、且证据合理,就尊重该结论、不再派)。 diff --git a/agent/tools.go b/agent/tools.go index 2f1f0e5..fb0eec2 100644 --- a/agent/tools.go +++ b/agent/tools.go @@ -427,6 +427,63 @@ func (t *ToolSet) graphOverviewData() map[string]any { factNodes, _ := t.ts.ListByKind(db.KindFact, 1000) // newest first out["findings"] = len(vulnNodes) // 确认漏洞数(目标判定看它) out["facts"] = len(factNodes) // 探索事实/结论数(含否定结论) + // findings 是任务里最高价值的产物、单任务通常也不多 → 直接全量带进概览(不像 facts 那样 + // 只给最近窗口),让 planner 每轮判目标时一眼看全所有确认漏洞,无需再调 list_findings。 + // 每条只留 {id, summary, evidence?, from_intent?, assets?}:evidence 是 report_finding 的 + // PoC 文本(payload.evidence.poc);from_intent 是产生本漏洞的意图;assets 直接给受影响资产 + // 的可读内容(url/域名/ip:port 等,不再是裸 id)——锚定关系存于 exploration_anchors、经 findings + // 表回填。vulnclass/severity/state 等仍可用 list_findings / node_detail(id) 取。 + var findingMeta map[int64]db.FindingMeta // node_id -> 锚定资产等;仅任务上下文可查 + assetByID := map[int64]*db.Asset{} + if t.as != nil && t.taskID > 0 { + findingMeta, _ = t.as.FindingMetaByNodeID(t.taskID) + idSet := map[int64]struct{}{} + for _, meta := range findingMeta { + for _, aid := range meta.AssetIDs { + idSet[aid] = struct{}{} + } + } + if len(idSet) > 0 { + ids := make([]int64, 0, len(idSet)) + for aid := range idSet { + ids = append(ids, aid) + } + if assets, err := t.as.GetByIDs(ids); err == nil { + for _, a := range assets { + assetByID[a.ID] = a + } + } + } + } + findingList := make([]map[string]any, 0, len(vulnNodes)) + for _, n := range vulnNodes { + var fp map[string]any + _ = json.Unmarshal(n.Payload, &fp) + m := map[string]any{"id": n.ID, "summary": fp["summary"]} + if ev, ok := fp["evidence"].(map[string]any); ok { + if poc, ok := ev["poc"].(string); ok && poc != "" { + m["evidence"] = poc + } + } + if from := factFrom[n.ID]; from > 0 { + m["from_intent"] = from // 本漏洞由哪个意图产生 + } + if meta, ok := findingMeta[n.ID]; ok && len(meta.AssetIDs) > 0 { + assets := make([]string, 0, len(meta.AssetIDs)) + for _, aid := range meta.AssetIDs { + if a := assetByID[aid]; a != nil { + if v := assetValue(a); v != "" { + assets = append(assets, v) + continue + } + } + assets = append(assets, fmt.Sprintf("#%d", aid)) // 资产已删/查不到 → 退回 id 标记,别丢信息 + } + m["assets"] = assets // 受影响资产的可读内容 + } + findingList = append(findingList, m) + } + out["finding_list"] = findingList recentFacts := make([]map[string]any, 0, 20) for _, n := range factNodes { if len(recentFacts) >= 20 { @@ -853,6 +910,35 @@ func compactNode(n *db.Node) map[string]any { return m } +// assetValue distills an asset to its most identifying human-readable string +// (url / domain / ip[:port] / app / service name) so finding_list can show the +// affected asset's content inline instead of a bare id. Empty when nothing +// identifying is set (caller falls back to #id). +func assetValue(a *db.Asset) string { + switch { + case a.URL != "": + if a.Method != "" { + return a.Method + " " + a.URL // 接口:带上 HTTP 方法 + } + return a.URL + case a.Domain != "": + if a.Port != nil { + return fmt.Sprintf("%s:%d", a.Domain, *a.Port) + } + return a.Domain + case a.IP != "": + if a.Port != nil { + return fmt.Sprintf("%s:%d", a.IP, *a.Port) + } + return a.IP + case a.AppName != "": + return a.AppName + case a.ServiceName != "": + return a.ServiceName + } + return "" +} + // compactFinding is compactNode plus the vuln-specific vulnclass/severity. func compactFinding(n *db.Node) map[string]any { var p map[string]any diff --git a/db/findings.go b/db/findings.go index 18c9a16..7b1c4fe 100644 --- a/db/findings.go +++ b/db/findings.go @@ -742,6 +742,13 @@ type FindingMeta struct { AssetIDs []int64 } +// FindingMetaByNodeID maps a task's finding node ids to their standalone-row +// metadata (status + anchored asset ids) via the asset store, so callers holding +// only an AssetStore (e.g. the agent ToolSet) can reach it without a raw *DB. +func (a *AssetStore) FindingMetaByNodeID(taskID int64) (map[int64]FindingMeta, error) { + return a.db.FindingMetaByNodeID(taskID) +} + // FindingMetaByNodeID maps a task's finding node ids to their standalone-row // metadata, so the per-task view (which reads exploration nodes) can show and // edit the same status — and the same anchored assets — as the global 发现 page.