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
+41
-1
@@ -608,6 +608,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /api/exploration/findings", s.findings)
|
||||
mux.HandleFunc("GET /api/exploration/findings/stats", s.findingStats)
|
||||
mux.HandleFunc("GET /api/exploration/findings/{id}", s.getFinding)
|
||||
mux.HandleFunc("GET /api/exploration/findings/{id}/lineage", s.findingLineage)
|
||||
mux.HandleFunc("PATCH /api/exploration/findings/{id}", s.patchFinding)
|
||||
mux.HandleFunc("GET /api/exploration/intents", s.intents)
|
||||
mux.HandleFunc("GET /api/exploration/graph", s.explorationGraph)
|
||||
@@ -1377,7 +1378,9 @@ func (s *Server) findings(w http.ResponseWriter, r *http.Request) {
|
||||
Severity: normFilter(q.Get("severity")),
|
||||
Status: normFilter(q.Get("status")),
|
||||
VulnClass: normFilter(q.Get("vulnclass")),
|
||||
Sort: q.Get("sort"),
|
||||
// task_id(独立于会切到「按任务节点」分支的 task 参数):全局表按任务筛选。
|
||||
TaskID: normFilter(q.Get("task_id")),
|
||||
Sort: q.Get("sort"),
|
||||
}
|
||||
fs, total, err := s.m.pg.ListFindingsPage(filter, page, limit)
|
||||
if err != nil {
|
||||
@@ -1486,6 +1489,43 @@ func (s *Server) getFinding(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, 200, findingFromDB(f, assets))
|
||||
}
|
||||
|
||||
// findingLineage returns the exploration sub-DAG from the task root to this
|
||||
// finding's node — the finding node + all its ancestors + edges among them — so
|
||||
// the detail page can show "how this finding was reached". Empty {nodes,edges}
|
||||
// when the finding has no node/task (e.g. the originating task was deleted).
|
||||
func (s *Server) findingLineage(w http.ResponseWriter, r *http.Request) {
|
||||
id := int64(atoiDefault(r.PathValue("id"), 0))
|
||||
if id <= 0 {
|
||||
writeErr(w, 400, "bad finding id")
|
||||
return
|
||||
}
|
||||
f, err := s.m.pg.GetFinding(id)
|
||||
if err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
if f == nil {
|
||||
writeErr(w, 404, "finding not found")
|
||||
return
|
||||
}
|
||||
empty := map[string]any{"nodes": []any{}, "edges": []any{}}
|
||||
if f.NodeID == nil || f.TaskID == nil {
|
||||
writeJSON(w, 200, empty)
|
||||
return
|
||||
}
|
||||
t := s.m.ResolveTask(i64s(*f.TaskID))
|
||||
if t == nil {
|
||||
writeJSON(w, 200, empty)
|
||||
return
|
||||
}
|
||||
nodes, edges, err := t.Store.FindingLineage(*f.NodeID)
|
||||
if err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, map[string]any{"nodes": taskNodeDTOs(nodes), "edges": edgeDTOs(edges)})
|
||||
}
|
||||
|
||||
// patchFinding partially updates a finding: any subset of {status, severity}.
|
||||
// The id is the standalone findings-table id (DTO finding_id). Severity edits are
|
||||
// mirrored onto the originating exploration node so the per-task view stays in
|
||||
|
||||
Reference in new issue
Block a user