mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-28 01:03:50 +08:00
refactor(graph_overview): 精简态势概览字段
- 删除 cold_index 字段及其 expand_index 工具(含两处注册),移除 仅服务它的 coldDigestOverview 分桶逻辑(改用 activeDigestBodies) - 删除 covered_members 字段(悬空血缘解析表) - 删除 coverage.scope 字段(范围根资产 + company 富化) - finding_list 每条仅保留 id/summary/from_intent,去掉 evidence/assets, 并清理随之无用的 findingMeta 预计算与 assetValue 辅助函数
This commit is contained in:
1 parent
bf7f414477
commit
06a43f37ff
3 files changed
+14
-319
No files matched your search
+9
-154
@@ -419,8 +419,7 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
||||
// (below), not the flat recent_* lists. `covered` maps member id → its digest id.
|
||||
// §6 render-time revival check: a covered member that has become hot again (a new
|
||||
// intent derived from it) must reappear this round — so `hidden` folds a member out
|
||||
// only when it is covered AND still cold. covered_members (built from hidden) lets a
|
||||
// parents/yields id pointing into a live fold stay resolvable (§6.5 dangling lineage).
|
||||
// only when it is covered AND still cold.
|
||||
covered, _ := t.ts.CoveredMembers()
|
||||
// Hot set at render time serves two §6 needs: (1) a covered member that revived
|
||||
// (now hot) must reappear this round; (2) the 60-cap must never truncate hot
|
||||
@@ -466,58 +465,16 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 每条只留 {id, summary, from_intent?}:from_intent 是产生本漏洞的意图。
|
||||
// evidence/assets/vulnclass/severity/state 等仍可用 list_findings / node_detail(id) 取。
|
||||
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
|
||||
@@ -568,23 +525,9 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
||||
compactIntents(doneHot, parentsOf, yieldsOf),
|
||||
compactIntents(doneCold[:keepColdDone], parentsOf, yieldsOf)...)
|
||||
out["recent_facts"] = append(recentFactsHot, recentFactsCold[:keepColdFacts]...) // {id, summary, from_intent, confidence?};详情用 node_detail(id)
|
||||
// cold-digest §6.1/§6.2: the folded cold region + the per-asset directory that
|
||||
// collapses independent directions, plus the dangling-lineage resolver map.
|
||||
if cds, cidx := t.coldDigestOverview(); len(cds) > 0 {
|
||||
// cold-digest §6.1: the folded cold region as flat digest bodies.
|
||||
if cds := activeDigestBodies(t.ts); len(cds) > 0 {
|
||||
out["cold_digests"] = cds // [{id, body, member_count}] —— 直接读 body (§6.1)
|
||||
out["cold_index"] = cidx // [{asset, asset_id, digest_ids}] —— 按资产收敛方向 (§6.2)
|
||||
}
|
||||
if len(covered) > 0 {
|
||||
cm := make(map[string]int64, len(covered))
|
||||
for member, dig := range covered {
|
||||
if hotAtRender[member] {
|
||||
continue // revived → shown live this round, not a dangling folded id
|
||||
}
|
||||
cm[strconv.FormatInt(member, 10)] = dig
|
||||
}
|
||||
if len(cm) > 0 {
|
||||
out["covered_members"] = cm // 悬空血缘 id → 它属于哪个 digest;用 expand_digest 展开 (§6.5)
|
||||
}
|
||||
}
|
||||
// the original task (root) so the planner always has it, not just the
|
||||
// decomposed goals.
|
||||
@@ -597,9 +540,9 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
||||
out["related_tasks"] = t.relatedTaskOverviews()
|
||||
// coverage:粗略的资产测试覆盖度参考——范围(task_scope)内的资产里,被 fact 碰过的
|
||||
// 占比 + by_type(按类型的 总数/已测)。要看未测的具体资产由 agent 按需调 list_untested_assets 自行判断。仅任务上下文有。
|
||||
// 资产覆盖度功能关闭时(coverageDisabled):只保留 scope/hosts(范围边界与目标主机的
|
||||
// 感知信息,company 关联经由 scope 在此浮现),丢弃 denominator/tested/pct/by_type/note
|
||||
// 等覆盖度度量,避免污染上下文、也不诱导已隐藏的 add_task_scope/list_untested_assets。
|
||||
// 资产覆盖度功能关闭时(coverageDisabled):只保留 host_count(目标主机数的感知信息),
|
||||
// 丢弃 denominator/tested/pct/by_type/note 等覆盖度度量,避免污染上下文、也不诱导
|
||||
// 已隐藏的 add_task_scope/list_untested_assets。
|
||||
if t.as != nil && t.ts != nil && t.taskID > 0 {
|
||||
{
|
||||
m := map[string]any{}
|
||||
@@ -617,65 +560,6 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
||||
}
|
||||
}
|
||||
}
|
||||
// scope:当前测试范围的根资产(task_scope 原始行),让 agent 知道这个任务到底
|
||||
// 圈定了哪些目标(不是全部测试资产,而是范围边界本身)。覆盖度开关无关,始终提供。
|
||||
if rows, err := t.as.ListTaskScopeWithSources(t.taskID); err == nil && len(rows) > 0 {
|
||||
scope := make([]map[string]any, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
e := map[string]any{"kind": r.Kind, "source": r.Source, "task_id": r.TaskID}
|
||||
if r.TaskID != t.taskID {
|
||||
inheritedMap(e, r.TaskID)
|
||||
}
|
||||
switch {
|
||||
case r.Domain != "":
|
||||
e["value"] = r.Domain
|
||||
case r.Net != "":
|
||||
e["value"] = r.Net
|
||||
case r.Value != "":
|
||||
e["value"] = r.Value
|
||||
case r.CompanyID != nil:
|
||||
e["company_id"] = *r.CompanyID
|
||||
if t.cs != nil {
|
||||
if company, err := t.cs.GetCompany(*r.CompanyID); err == nil && company != nil {
|
||||
e["company_name"] = company.Name
|
||||
}
|
||||
if rules, err := t.cs.GetScope(*r.CompanyID); err == nil {
|
||||
keywords := make([]string, 0)
|
||||
companyScope := make([]map[string]any, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
value := rule.Raw
|
||||
if value == "" {
|
||||
switch rule.Kind {
|
||||
case "domain":
|
||||
value = rule.Domain
|
||||
case "ip", "cidr":
|
||||
value = rule.Net
|
||||
default:
|
||||
value = rule.Value
|
||||
}
|
||||
}
|
||||
entry := map[string]any{"kind": rule.Kind, "value": value}
|
||||
if rule.Reason != "" {
|
||||
entry["reason"] = rule.Reason
|
||||
}
|
||||
companyScope = append(companyScope, entry)
|
||||
if rule.Kind == "keyword" && rule.Raw != "" {
|
||||
keywords = append(keywords, rule.Raw)
|
||||
}
|
||||
}
|
||||
if len(companyScope) > 0 {
|
||||
e["company_scope"] = companyScope
|
||||
}
|
||||
if len(keywords) > 0 {
|
||||
e["company_keywords"] = keywords
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scope = append(scope, e)
|
||||
}
|
||||
m["scope"] = scope
|
||||
}
|
||||
if hosts, err := t.as.HostsByTaskWithSources(t.taskID); err == nil {
|
||||
// 只给主机总数,不再把 host 列表平铺进 graph_overview(大范围任务里那是每轮
|
||||
// 都重复携带的大量字符串,对规划决策价值有限);具体主机按需 list_assets 查。
|
||||
@@ -1014,35 +898,6 @@ 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
|
||||
@@ -2196,7 +2051,7 @@ func (t *ToolSet) PlannerTools() []actool.CoreTool {
|
||||
return []actool.CoreTool{
|
||||
t.graphOverview(), t.listFindings(), t.listFacts(), t.nodeDetail(),
|
||||
// cold-digest §6.1: restore folded cold nodes (digest body → members → detail).
|
||||
t.expandDigest(), t.expandIndex(),
|
||||
t.expandDigest(),
|
||||
t.getWorkerOutput(), t.getWorkerTrace(), t.searchAllWorkerTraces(), t.listGoals(), t.addIntent(), t.proveGoal(), t.goalMet(),
|
||||
t.killWorkTool(), t.steerWorkTool(),
|
||||
// report_finding:规划态势研判时若自身已确证漏洞,可直接登记(与 worker 同工具)。
|
||||
|
||||
+4
-164
@@ -2,122 +2,19 @@ package agent
|
||||
|
||||
// cold-digest §6: graph_overview folding + the restore tools.
|
||||
//
|
||||
// coldDigestOverview — builds the folded cold region for graph_overview:
|
||||
// cold_digests (flat {id, body, member_count}) and cold_index (§6.2, the
|
||||
// per-asset directory that collapses independent directions).
|
||||
// expand_digest(id) — level-1 restore: a digest's member compact list.
|
||||
// expand_index(asset_id) — level-0 restore: the digests under one asset.
|
||||
// activeDigestBodies — builds the folded cold region for graph_overview:
|
||||
// cold_digests (flat {id, body, member_count}).
|
||||
// expand_digest(id) — level-1 restore: a digest's member compact list.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
actool "github.com/Autumn-27/norma/tool"
|
||||
)
|
||||
|
||||
// coldDigestOverview returns the folded cold region for graph_overview: the flat
|
||||
// digest bodies and the asset-grouped index (§6.1/§6.2). covered is unused here
|
||||
// (kept for symmetry with the caller's coverage computation).
|
||||
func (t *ToolSet) coldDigestOverview() (digests []map[string]any, index []map[string]any) {
|
||||
ads, err := t.ts.ActiveDigests()
|
||||
if err != nil || len(ads) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
memByDigest := map[int64][]int64{}
|
||||
var allMembers []int64
|
||||
for _, d := range ads {
|
||||
ms, _ := t.ts.DigestMembers(d.ID)
|
||||
memByDigest[d.ID] = ms
|
||||
allMembers = append(allMembers, ms...)
|
||||
}
|
||||
assetsByNode, _ := t.ts.NodeAssets(allMembers)
|
||||
|
||||
digests = make([]map[string]any, 0, len(ads))
|
||||
for _, d := range ads {
|
||||
var p struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
_ = json.Unmarshal(d.Payload, &p)
|
||||
digests = append(digests, map[string]any{
|
||||
"id": d.ID,
|
||||
"body": p.Body,
|
||||
"member_count": len(memByDigest[d.ID]),
|
||||
})
|
||||
}
|
||||
|
||||
// index (§6.2): asset → digests. Each digest lands in EXACTLY ONE bucket — its
|
||||
// representative asset = the asset anchored on the most of its members (mode;
|
||||
// tie-break lowest id). This is what makes the index converge: bucketing a digest
|
||||
// into every asset its members touch would duplicate it across dozens of buckets
|
||||
// and blow up the top-level count instead of shrinking it (asset_ids are fine-
|
||||
// grained — a real task has ~144 of them). A digest whose members anchor no asset
|
||||
// falls into the 0 bucket ("(未锚定资产)").
|
||||
byAsset := map[int64]map[int64]bool{} // asset id → set of digest ids
|
||||
assetSet := map[int64]bool{}
|
||||
for dID, ms := range memByDigest {
|
||||
counts := map[int64]int{}
|
||||
for _, m := range ms {
|
||||
for _, a := range assetsByNode[m] {
|
||||
counts[a]++
|
||||
}
|
||||
}
|
||||
rep, best := int64(0), 0
|
||||
for a, c := range counts {
|
||||
if c > best || (c == best && (rep == 0 || a < rep)) {
|
||||
rep, best = a, c
|
||||
}
|
||||
}
|
||||
if byAsset[rep] == nil {
|
||||
byAsset[rep] = map[int64]bool{}
|
||||
}
|
||||
byAsset[rep][dID] = true
|
||||
if rep != 0 {
|
||||
assetSet[rep] = true
|
||||
}
|
||||
}
|
||||
labels := map[int64]string{}
|
||||
if t.as != nil && len(assetSet) > 0 {
|
||||
ids := make([]int64, 0, len(assetSet))
|
||||
for a := range assetSet {
|
||||
ids = append(ids, a)
|
||||
}
|
||||
if assets, err := t.as.GetByIDs(ids); err == nil {
|
||||
for _, a := range assets {
|
||||
if v := assetValue(a); v != "" {
|
||||
labels[a.ID] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
index = make([]map[string]any, 0, len(byAsset))
|
||||
for a, dset := range byAsset {
|
||||
dids := make([]int64, 0, len(dset))
|
||||
for d := range dset {
|
||||
dids = append(dids, d)
|
||||
}
|
||||
sort.Slice(dids, func(i, j int) bool { return dids[i] < dids[j] })
|
||||
entry := map[string]any{"digest_ids": dids}
|
||||
if a == 0 {
|
||||
entry["asset"] = "(未锚定资产)"
|
||||
} else {
|
||||
entry["asset_id"] = a
|
||||
if l := labels[a]; l != "" {
|
||||
entry["asset"] = l
|
||||
} else {
|
||||
entry["asset"] = fmt.Sprintf("#%d", a)
|
||||
}
|
||||
}
|
||||
index = append(index, entry)
|
||||
}
|
||||
sort.Slice(index, func(i, j int) bool {
|
||||
return fmt.Sprint(index[i]["asset"]) < fmt.Sprint(index[j]["asset"])
|
||||
})
|
||||
return digests, index
|
||||
}
|
||||
|
||||
// digestMemberEntry builds the compact per-member view expand_digest returns —
|
||||
// same shape as recent_facts / recent_done_intents (§6.1 middle level). store is
|
||||
// the digest's OWNING store (the current task, or a read-only source task §2).
|
||||
@@ -200,7 +97,7 @@ func (t *ToolSet) expandDigest() actool.CoreTool {
|
||||
map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"id": map[string]any{"type": "integer", "description": "digest 节点 id(来自概览 cold_digests / cold_index / covered_members)"},
|
||||
"id": map[string]any{"type": "integer", "description": "digest 节点 id(来自概览 cold_digests)"},
|
||||
},
|
||||
"required": []any{"id"},
|
||||
},
|
||||
@@ -240,60 +137,3 @@ func (t *ToolSet) expandDigest() actool.CoreTool {
|
||||
return jsonResult(out)
|
||||
})
|
||||
}
|
||||
|
||||
// expandIndex returns the active digests under one asset (§6.2 level-0), each
|
||||
// with its body + member count — so the planner can drill an asset directory down
|
||||
// to its directions without reading every digest globally.
|
||||
func (t *ToolSet) expandIndex() actool.CoreTool {
|
||||
return t.writeExpTool("expand_index",
|
||||
"展开冷区某个资产条目(来自概览 cold_index):返回该资产名下的 cold digest 列表(id/body/member_count)。再往下看某个 digest 的成员用 expand_digest(id)。",
|
||||
map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"asset_id": map[string]any{"type": "integer", "description": "资产 id(来自概览 cold_index 的 asset_id;传 0 或省略取未锚定资产桶)"},
|
||||
},
|
||||
},
|
||||
func(ctx context.Context, raw json.RawMessage) (actool.Result, error) {
|
||||
var in struct {
|
||||
AssetID int64 `json:"asset_id"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
ads, _ := t.ts.ActiveDigests()
|
||||
out := make([]map[string]any, 0)
|
||||
for _, d := range ads {
|
||||
members, _ := t.ts.DigestMembers(d.ID)
|
||||
assetsByNode, _ := t.ts.NodeAssets(members)
|
||||
match := in.AssetID == 0
|
||||
for _, m := range members {
|
||||
for _, a := range assetsByNode[m] {
|
||||
if a == in.AssetID {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
if match {
|
||||
break
|
||||
}
|
||||
}
|
||||
// asset_id==0 means "unanchored bucket": include only digests with no asset.
|
||||
if in.AssetID == 0 {
|
||||
anchored := false
|
||||
for _, m := range members {
|
||||
if len(assetsByNode[m]) > 0 {
|
||||
anchored = true
|
||||
break
|
||||
}
|
||||
}
|
||||
match = !anchored
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
var p struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
_ = json.Unmarshal(d.Payload, &p)
|
||||
out = append(out, map[string]any{"id": d.ID, "body": p.Body, "member_count": len(members)})
|
||||
}
|
||||
return jsonResult(map[string]any{"asset_id": in.AssetID, "digests": out})
|
||||
})
|
||||
}
|
||||
@@ -664,7 +664,7 @@ func (t *ToolSet) WorkerTools() []actool.CoreTool {
|
||||
func (t *ToolSet) MainAgentTools() []actool.CoreTool {
|
||||
return []actool.CoreTool{
|
||||
t.graphOverview(), t.listFindings(), t.listFacts(), t.nodeDetail(),
|
||||
t.expandDigest(), t.expandIndex(), // cold-digest §6.1
|
||||
t.expandDigest(), // cold-digest §6.1
|
||||
t.getWorkerOutput(), t.getWorkerTrace(), t.searchAllWorkerTraces(), t.addHint(), t.addIntent(),
|
||||
// steer_work:人可对某条正在运行的意图(work)实时注入纠偏指令(不打断、不丢进展)。
|
||||
t.steerWorkTool(),
|
||||
|
||||
Reference in new issue
Block a user