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:
1 parent
c69534e09b
commit
64d9c66f6a
7 files changed
+552
-18
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user