feat(overview): graph_overview 全量输出 finding_list

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 表,覆盖新旧漏洞)。
This commit is contained in:
Autumn-27 committed 2026-09-09 09:24:13 -04:00
1 parent f270ca66bc
commit d728e509dc
3 files changed
+94 -1

No files matched your search

+1 -1
View File
@@ -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 的,按【尚未探明】处理,若在范围内且无其它意图覆盖,默认派一条复核意图去证实或推翻(**同一否定方向至多复核一次**;复核后仍为否定、且证据合理,就尊重该结论、不再派)。
+86
View File
@@ -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
+7
View File
@@ -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.