- findings 表新增 status 列(默认 pending),schema.sql 同步 ALTER TABLE 补旧库
- 新增 PATCH /api/exploration/findings/{id} 更新处置状态,server 侧白名单校验
- FindingDTO 增加 status/finding_id/assets(后端预渲染资产 label)
- 任务内发现节点通过 node_id 关联独立 findings 行,复用同一处置状态
- 发现列表页:漏洞名称(类型主 + 摘要小字)、新增资产列与状态列(可下拉修改)、状态筛选与待处理统计
- 任务详情发现 Tab 同步展示/编辑处置状态与绑定资产
137 lines
3.8 KiB
Go
137 lines
3.8 KiB
Go
package db
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
// DBFinding is a row in the standalone findings table (persists across task deletion).
|
|
type DBFinding struct {
|
|
ID int64
|
|
TaskID *int64
|
|
NodeID *int64
|
|
VulnClass string
|
|
Severity string
|
|
Summary string
|
|
Evidence string
|
|
Worker string
|
|
AssetIDs []int64
|
|
Status string
|
|
CreatedAt time.Time
|
|
TaskDescription string // populated via LEFT JOIN on tasks
|
|
}
|
|
|
|
// Finding triage states (findings.status).
|
|
const (
|
|
FindingPending = "pending" // 待处理
|
|
FindingFalsePositive = "false_positive" // 误报
|
|
FindingIgnored = "ignored" // 忽略
|
|
FindingResolved = "resolved" // 已处理
|
|
)
|
|
|
|
// ValidFindingStatus reports whether s is a known triage state.
|
|
func ValidFindingStatus(s string) bool {
|
|
switch s {
|
|
case FindingPending, FindingFalsePositive, FindingIgnored, FindingResolved:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// AddFinding inserts a finding into the standalone findings table. taskID and
|
|
// nodeID may be 0 (stored as NULL). Returns the new finding id.
|
|
func (d *DB) AddFinding(taskID, nodeID int64, vulnclass, severity, summary, evidence, worker string, assetIDs []int64) (int64, error) {
|
|
aidsJSON, _ := json.Marshal(assetIDs)
|
|
if assetIDs == nil {
|
|
aidsJSON = []byte("[]")
|
|
}
|
|
var tid, nid *int64
|
|
if taskID > 0 {
|
|
tid = &taskID
|
|
}
|
|
if nodeID > 0 {
|
|
nid = &nodeID
|
|
}
|
|
var id int64
|
|
err := d.QueryRow(
|
|
`INSERT INTO findings (task_id, node_id, vulnclass, severity, summary, evidence, worker, asset_ids)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
|
|
tid, nid, vulnclass, severity, summary, evidence, worker, string(aidsJSON),
|
|
).Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
// ListFindings returns all findings (newest first), joined with task description.
|
|
func (d *DB) ListFindings(limit int) ([]*DBFinding, error) {
|
|
if limit <= 0 {
|
|
limit = 500
|
|
}
|
|
rows, err := d.Query(`
|
|
SELECT f.id, f.task_id, f.node_id, f.vulnclass, f.severity, f.summary,
|
|
f.evidence, f.worker, f.asset_ids, COALESCE(f.status, 'pending'), f.created_at,
|
|
COALESCE(t.description, '') AS task_description
|
|
FROM findings f
|
|
LEFT JOIN tasks t ON f.task_id = t.id
|
|
ORDER BY f.created_at DESC
|
|
LIMIT $1`, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []*DBFinding
|
|
for rows.Next() {
|
|
f := &DBFinding{}
|
|
var aidsJSON string
|
|
if err := rows.Scan(&f.ID, &f.TaskID, &f.NodeID, &f.VulnClass, &f.Severity,
|
|
&f.Summary, &f.Evidence, &f.Worker, &aidsJSON, &f.Status, &f.CreatedAt, &f.TaskDescription); err != nil {
|
|
return nil, err
|
|
}
|
|
_ = json.Unmarshal([]byte(aidsJSON), &f.AssetIDs)
|
|
out = append(out, f)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// 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)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.RowsAffected()
|
|
}
|
|
|
|
// FindingStatusByNodeID maps a task's finding node ids to their standalone-row
|
|
// id and triage state, so the per-task view (which reads exploration nodes) can
|
|
// show and edit the same status as the global 发现 page.
|
|
func (d *DB) FindingStatusByNodeID(taskID int64) (map[int64]struct {
|
|
ID int64
|
|
Status string
|
|
}, error) {
|
|
out := map[int64]struct {
|
|
ID int64
|
|
Status string
|
|
}{}
|
|
if taskID <= 0 {
|
|
return out, nil
|
|
}
|
|
rows, err := d.Query(`SELECT node_id, id, COALESCE(status,'pending') FROM findings
|
|
WHERE task_id=$1 AND node_id IS NOT NULL`, taskID)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var nid, id int64
|
|
var st string
|
|
if err := rows.Scan(&nid, &id, &st); err != nil {
|
|
return out, err
|
|
}
|
|
out[nid] = struct {
|
|
ID int64
|
|
Status string
|
|
}{ID: id, Status: st}
|
|
}
|
|
return out, rows.Err()
|
|
}
|