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

+63
View File
@@ -1,6 +1,7 @@
package db
import (
"database/sql"
"encoding/json"
"fmt"
"strings"
@@ -46,6 +47,23 @@ func ValidFindingStatus(s string) bool {
return false
}
// Finding severity levels (findings.severity).
const (
SeverityCritical = "critical" // 严重
SeverityHigh = "high" // 高
SeverityMedium = "medium" // 中
SeverityLow = "low" // 低
)
// ValidSeverity reports whether s is a known severity level.
func ValidSeverity(s string) bool {
switch s {
case SeverityCritical, SeverityHigh, SeverityMedium, SeverityLow:
return true
}
return false
}
// AddFinding inserts a finding into the standalone findings table. taskID and
// nodeID may be 0 (stored as NULL). name may be "" (frontend falls back to
// vulnclass). Returns the new finding id.
@@ -226,6 +244,27 @@ func (d *DB) FindingStats() (*FindingStats, error) {
return st, rows.Err()
}
// GetFinding returns a single finding row (with task_description joined), or nil
// when no row has that id.
func (d *DB) GetFinding(id int64) (*DBFinding, error) {
rows, err := d.Query(`SELECT `+findingSelectCols+`
FROM findings f
LEFT JOIN tasks t ON f.task_id = t.id
WHERE f.id = $1`, id)
if err != nil {
return nil, err
}
defer rows.Close()
out, err := scanFindings(rows)
if err != nil {
return nil, err
}
if len(out) == 0 {
return nil, nil
}
return out[0], nil
}
// SetFindingStatus updates one finding's triage state. Returns rows affected.
func (d *DB) SetFindingStatus(id int64, status string) (int64, error) {
res, err := d.Exec(`UPDATE findings SET status=$1 WHERE id=$2`, status, id)
@@ -235,6 +274,30 @@ func (d *DB) SetFindingStatus(id int64, status string) (int64, error) {
return res.RowsAffected()
}
// SetFindingSeverity updates one finding's severity in the standalone table AND
// keeps the originating exploration node's payload in sync (the per-task view
// reads severity from the node payload, not this table). Returns rows affected
// (0 when no finding has that id). The node sync is best-effort.
func (d *DB) SetFindingSeverity(id int64, severity string) (int64, error) {
var nodeID *int64
err := d.QueryRow(`UPDATE findings SET severity=$1 WHERE id=$2 RETURNING node_id`, severity, id).Scan(&nodeID)
if err == sql.ErrNoRows {
return 0, nil
}
if err != nil {
return 0, err
}
if nodeID != nil {
// jsonb_set the {severity} key so the finding node (read by the per-task
// 发现 Tab) shows the same level. Ignore errors — the standalone row is the
// source of truth for the global 发现 page.
_, _ = d.Exec(`UPDATE exploration_nodes
SET payload = jsonb_set(payload, '{severity}', to_jsonb($1::text))
WHERE id = $2`, severity, *nodeID)
}
return 1, nil
}
// FindingMeta is the standalone-row data (id, triage state, anchored assets) the
// per-task view grafts onto its exploration-node findings.
type FindingMeta struct {
+24
View File
@@ -100,4 +100,28 @@ func TestFindingsPageAndStats(t *testing.T) {
if !slices.Contains(st.VulnClasses, vc) {
t.Fatalf("stats vulnclasses missing %q", vc)
}
// GetFinding: single-row fetch round-trips id/name/severity.
one, err := d.GetFinding(ids[0])
if err != nil {
t.Fatal(err)
}
if one == nil || one.ID != ids[0] || one.Severity != "critical" || one.Name != "严重漏洞标题" {
t.Fatalf("GetFinding mismatch: %+v", one)
}
if miss, err := d.GetFinding(-1); err != nil || miss != nil {
t.Fatalf("GetFinding(-1): want nil,nil got %+v,%v", miss, err)
}
// SetFindingSeverity: standalone row updates; 0 rows for unknown id.
if n, err := d.SetFindingSeverity(ids[0], "high"); err != nil || n != 1 {
t.Fatalf("SetFindingSeverity: want 1,nil got %d,%v", n, err)
}
one, _ = d.GetFinding(ids[0])
if one.Severity != "high" {
t.Fatalf("severity not updated: %q", one.Severity)
}
if n, err := d.SetFindingSeverity(-1, "low"); err != nil || n != 0 {
t.Fatalf("SetFindingSeverity(-1): want 0,nil got %d,%v", n, err)
}
}