feat: 漏洞详情页删除证据/元数据 Tab,新增「链路图」
链路图展示该漏洞所属任务探索图里,从初始节点(任务根)追溯到本漏洞节点的
那条链路:漏洞节点 + 其全部祖先(沿边反向可达)+ 之间的关系边。
- db.ExplorationStore.FindingLineage(nodeID):递归 CTE 取祖先子图(节点+边),
下游(如 finding→proves→goal)与无关节点不纳入
- GET /api/exploration/findings/{id}/lineage:解析 finding 的 node_id/task_id →
返回 {nodes,edges};无节点/任务已删 → 空图
- 前端 api.findingLineage;详情页 Tab 改为「概览 / 链路图」
- lineage.tsx:按深度拓扑排序,自根到漏洞纵向展示节点卡片 + 入边关系标注,
漏洞节点高亮
- db 测试补 FindingLineage 断言(含祖先/排除下游 goal/排除无关意图)
go build / db / server 测试通过。
This commit is contained in:
1 parent
33714a5ff0
commit
b741ee176c
6 files changed
+356
-37
No files matched your search
@@ -372,6 +372,66 @@ func (s *ExplorationStore) Edges(limit int) ([]Edge, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// FindingLineage returns the sub-DAG that leads FROM the exploration root TO the
|
||||
// given node: the node itself plus all its ancestors (nodes reverse-reachable by
|
||||
// following edges backward), and every edge whose both endpoints are in that set.
|
||||
// Used to render "how this finding was reached" — origin → … → finding. Returns
|
||||
// empty (not error) for a nonexistent node.
|
||||
func (s *ExplorationStore) FindingLineage(nodeID int64) ([]*Node, []Edge, error) {
|
||||
// anc = the node + everything that can reach it via edges (walk src<-dst).
|
||||
nodeRows, err := s.db.Query(`
|
||||
WITH RECURSIVE anc(id) AS (
|
||||
SELECT $2::bigint
|
||||
UNION
|
||||
SELECT e.src_id
|
||||
FROM exploration_edges e
|
||||
JOIN anc ON e.dst_id = anc.id
|
||||
WHERE e.exploration_id = $1
|
||||
)
|
||||
SELECT `+nodeCols+`
|
||||
FROM exploration_nodes
|
||||
WHERE exploration_id = $1 AND id IN (SELECT id FROM anc)
|
||||
ORDER BY id`, s.expID, nodeID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
nodes, err := scanNodes(nodeRows)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
// edges among the ancestor set only (the lineage's internal relations).
|
||||
edgeRows, err := s.db.Query(`
|
||||
WITH RECURSIVE anc(id) AS (
|
||||
SELECT $2::bigint
|
||||
UNION
|
||||
SELECT e.src_id
|
||||
FROM exploration_edges e
|
||||
JOIN anc ON e.dst_id = anc.id
|
||||
WHERE e.exploration_id = $1
|
||||
)
|
||||
SELECT src_id, rel, dst_id
|
||||
FROM exploration_edges
|
||||
WHERE exploration_id = $1
|
||||
AND src_id IN (SELECT id FROM anc)
|
||||
AND dst_id IN (SELECT id FROM anc)`, s.expID, nodeID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer edgeRows.Close()
|
||||
var edges []Edge
|
||||
for edgeRows.Next() {
|
||||
var e Edge
|
||||
if err := edgeRows.Scan(&e.From, &e.Rel, &e.To); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
edges = append(edges, e)
|
||||
}
|
||||
return nodes, edges, edgeRows.Err()
|
||||
}
|
||||
|
||||
// FindingIntents maps each finding id to the intent that produced it (the
|
||||
// intent --yields--> finding edge; report_finding links it). Findings with no
|
||||
// producing intent are absent. Precise (JOIN, no edge-scan limit).
|
||||
|
||||
@@ -63,6 +63,27 @@ func TestExplorationFlow(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// lineage: ancestors of the finding traced backward — here {i2, find} joined by
|
||||
// the yields edge. The proves→goal edge is DOWNSTREAM (goal must be excluded),
|
||||
// and the unrelated intent i1 is not on any path to the finding (excluded too).
|
||||
lnNodes, lnEdges, err := es.FindingLineage(find)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := map[int64]bool{}
|
||||
for _, n := range lnNodes {
|
||||
got[n.ID] = true
|
||||
}
|
||||
if len(lnNodes) != 2 || !got[find] || !got[i2] {
|
||||
t.Fatalf("lineage nodes: want {i2,find}, got %+v", lnNodes)
|
||||
}
|
||||
if got[goal] {
|
||||
t.Fatalf("lineage must exclude the proved goal (it is downstream of the finding)")
|
||||
}
|
||||
if len(lnEdges) != 1 || lnEdges[0].From != i2 || lnEdges[0].To != find || lnEdges[0].Rel != "yields" {
|
||||
t.Fatalf("lineage edges: want i2-yields->find, got %+v", lnEdges)
|
||||
}
|
||||
|
||||
// activity poll by id cursor
|
||||
id1, err := es.AppendActivity(Activity{Worker: "worker-1", Kind: "tool_use", Tool: "Bash", Summary: "ran curl", Detail: "full output"})
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user