feat: 新增发现详情页(多 Tab + 状态区可改严重度/处置状态)

详情页 /function/findings/detail?id=<finding_id>:
- 概览 Tab:左栏摘要 + 证据/PoC,右栏状态区(严重等级、漏洞类型、涉及资产、处理状态、所属任务、发现时间)
- 严重等级与处理状态用下拉,选择后直接调后端接口即时修正
- 另有「证据/PoC」「元数据」两个 Tab
- 发现列表页漏洞名称改为链接进入详情

后端:
- GET  /api/exploration/findings/{id}  取单条(含资产解析)
- PATCH /api/exploration/findings/{id} 扩展为部分更新 {status?, severity?}
- db.GetFinding / db.SetFindingSeverity(同步探索节点 payload,保持任务内视图一致)
- db.ValidSeverity 白名单;api 增加 getFinding / setFindingSeverity
- mock 支持单条 GET / PATCH,并回填 finding_id 供 demo 链接

db 测试补充 GetFinding / SetFindingSeverity 往返断言;next build 通过。
This commit is contained in:
Autumn-27 committed 2026-08-18 11:43:50 -04:00
1 parent c69534e09b
commit 64d9c66f6a
7 files changed
+552 -18

No files matched your search

+65 -10
View File
@@ -607,7 +607,8 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("GET /api/exploration/frontier", s.frontier)
mux.HandleFunc("GET /api/exploration/findings", s.findings)
mux.HandleFunc("GET /api/exploration/findings/stats", s.findingStats)
mux.HandleFunc("PATCH /api/exploration/findings/{id}", s.setFindingStatus)
mux.HandleFunc("GET /api/exploration/findings/{id}", s.getFinding)
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)
mux.HandleFunc("GET /api/exploration/activity", s.activity)
@@ -1464,35 +1465,89 @@ func (s *Server) findingStats(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, st)
}
// setFindingStatus updates one finding's triage state (pending / false_positive /
// ignored / resolved). The id is the standalone findings-table id (DTO finding_id).
func (s *Server) setFindingStatus(w http.ResponseWriter, r *http.Request) {
// getFinding returns one finding by its standalone-table id (DTO finding_id),
// with anchored assets resolved for display.
func (s *Server) getFinding(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
}
assets := s.resolveAssetIDs(f.AssetIDs)
writeJSON(w, 200, findingFromDB(f, assets))
}
// 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
// sync. Returns the updated finding DTO.
func (s *Server) patchFinding(w http.ResponseWriter, r *http.Request) {
id := int64(atoiDefault(r.PathValue("id"), 0))
if id <= 0 {
writeErr(w, 400, "bad finding id")
return
}
var body struct {
Status string `json:"status"`
Status *string `json:"status"`
Severity *string `json:"severity"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeErr(w, 400, "bad json: "+err.Error())
return
}
if !db.ValidFindingStatus(body.Status) {
writeErr(w, 400, "bad status: "+body.Status)
if body.Status == nil && body.Severity == nil {
writeErr(w, 400, "nothing to update: provide status and/or severity")
return
}
n, err := s.m.pg.SetFindingStatus(id, body.Status)
if body.Status != nil {
if !db.ValidFindingStatus(*body.Status) {
writeErr(w, 400, "bad status: "+*body.Status)
return
}
n, err := s.m.pg.SetFindingStatus(id, *body.Status)
if err != nil {
writeErr(w, 500, err.Error())
return
}
if n == 0 {
writeErr(w, 404, "finding not found")
return
}
}
if body.Severity != nil {
if !db.ValidSeverity(*body.Severity) {
writeErr(w, 400, "bad severity: "+*body.Severity)
return
}
n, err := s.m.pg.SetFindingSeverity(id, *body.Severity)
if err != nil {
writeErr(w, 500, err.Error())
return
}
if n == 0 {
writeErr(w, 404, "finding not found")
return
}
}
f, err := s.m.pg.GetFinding(id)
if err != nil {
writeErr(w, 500, err.Error())
return
}
if n == 0 {
if f == nil {
writeErr(w, 404, "finding not found")
return
}
writeJSON(w, 200, map[string]any{"id": i64s(id), "status": body.Status})
writeJSON(w, 200, findingFromDB(f, s.resolveAssetIDs(f.AssetIDs)))
}
func (s *Server) intents(w http.ResponseWriter, r *http.Request) {