feat: 漏洞新增 severity=critical(严重) 与 name(漏洞名称)字段

severity 严重级:
- report_finding 工具 severity 说明改为 critical|high|medium|low
- DB 排序 CASE、report 报告 sevRank、FindingStats 增加 critical
- 前端 Severity 类型、status.ts 新增 rose 色调(实心高强调,视觉高于高危)、
  发现页严重度筛选/统计卡、任务发现 Tab 排序、dashboard 汇总均加入严重

name 漏洞名称:
- findings 表加 name 列(schema + ALTER TABLE ADD COLUMN IF NOT EXISTS 补旧库)
- report_finding 工具新增 name 入参(非必填),写入 node payload 与独立 findings 行
- DBFinding/AddFinding/AddStandaloneFinding、FindingDTO、findingPayload、report 全链路透传
- 发现列表页与任务发现 Tab 展示 name,为空时回退 vulnclass;vulnclass 移到展开详情「类型」
- report 报告标题优先 name,回退 vulnclass

无 CHECK 约束,旧库仅需补 name 列;critical/name 均向后兼容。
db 测试补充 critical 排序/统计与 name 往返断言。
This commit is contained in:
Autumn-27 committed 2026-08-18 11:21:08 -04:00
1 parent 7b3eec2166
commit ff3d41ea35
14 files changed
+96 -56

No files matched your search

+3 -2
View File
@@ -632,6 +632,7 @@ FROM activity WHERE exploration_id=$1 AND id>$2 ORDER BY id LIMIT $3`, s.expID,
// - Worker != "" → filter by worker name (Main = "mainagent", Plan = "planner").
// Goal Agent 的第 0 轮拆解也以 worker="planner" 落库,故 Plan 会话完整覆盖 Goal+Planner。
// - NodeID != nil → a Worker session, filtered by node_id (= intent id).
//
// A zero value (both empty) matches the whole task (no session filter).
type ActivitySessionFilter struct {
Worker string
@@ -885,6 +886,6 @@ FROM activity WHERE exploration_id=$1 AND kind<>'thinking' AND id IN (`+strings.
// AddStandaloneFinding writes a finding to the standalone findings table, which
// persists across task deletion (task_id / node_id become NULL when the task or
// exploration node is deleted). taskID and nodeID may be 0 (stored as NULL).
func (s *ExplorationStore) AddStandaloneFinding(taskID, nodeID int64, vulnclass, severity, summary, evidence, worker string, assetIDs []int64) (int64, error) {
return s.db.AddFinding(taskID, nodeID, vulnclass, severity, summary, evidence, worker, assetIDs)
func (s *ExplorationStore) AddStandaloneFinding(taskID, nodeID int64, vulnclass, name, severity, summary, evidence, worker string, assetIDs []int64) (int64, error) {
return s.db.AddFinding(taskID, nodeID, vulnclass, name, severity, summary, evidence, worker, assetIDs)
}
+14 -10
View File
@@ -13,6 +13,7 @@ type DBFinding struct {
TaskID *int64
NodeID *int64
VulnClass string
Name string // 漏洞名称(可读标题);为空时前端回退展示 VulnClass
Severity string
Summary string
Evidence string
@@ -46,8 +47,9 @@ func ValidFindingStatus(s string) bool {
}
// 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) {
// nodeID may be 0 (stored as NULL). name may be "" (frontend falls back to
// vulnclass). Returns the new finding id.
func (d *DB) AddFinding(taskID, nodeID int64, vulnclass, name, severity, summary, evidence, worker string, assetIDs []int64) (int64, error) {
aidsJSON, _ := json.Marshal(assetIDs)
if assetIDs == nil {
aidsJSON = []byte("[]")
@@ -61,16 +63,16 @@ func (d *DB) AddFinding(taskID, nodeID int64, vulnclass, severity, summary, evid
}
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),
`INSERT INTO findings (task_id, node_id, vulnclass, name, severity, summary, evidence, worker, asset_ids)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`,
tid, nid, vulnclass, name, severity, summary, evidence, worker, string(aidsJSON),
).Scan(&id)
return id, err
}
// findingSelectCols is the column list (with task_description join) every finding
// list query selects, so scanFinding stays in sync across callers.
const findingSelectCols = `f.id, f.task_id, f.node_id, f.vulnclass, f.severity, f.summary,
const findingSelectCols = `f.id, f.task_id, f.node_id, f.vulnclass, COALESCE(f.name, ''), f.severity, f.summary,
f.evidence, f.worker, f.asset_ids, COALESCE(f.status, 'pending'), f.created_at,
COALESCE(t.description, '') AS task_description`
@@ -84,7 +86,7 @@ func scanFindings(rows interface {
for rows.Next() {
f := &DBFinding{}
var aidsJSON string
if err := rows.Scan(&f.ID, &f.TaskID, &f.NodeID, &f.VulnClass, &f.Severity,
if err := rows.Scan(&f.ID, &f.TaskID, &f.NodeID, &f.VulnClass, &f.Name, &f.Severity,
&f.Summary, &f.Evidence, &f.Worker, &aidsJSON, &f.Status, &f.CreatedAt, &f.TaskDescription); err != nil {
return nil, err
}
@@ -162,8 +164,8 @@ func (d *DB) ListFindingsPage(f FindingFilter, page, pageSize int) ([]*DBFinding
order := "f.created_at DESC"
if f.Sort == "severity" {
// high > medium > low > 其它, then newest first.
order = `CASE f.severity WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'low' THEN 1 ELSE 0 END DESC, f.created_at DESC`
// critical > high > medium > low > 其它, then newest first.
order = `CASE f.severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'low' THEN 1 ELSE 0 END DESC, f.created_at DESC`
}
pageArgs := append(append([]any{}, args...), pageSize, (page-1)*pageSize)
q := fmt.Sprintf(`
@@ -187,6 +189,7 @@ func (d *DB) ListFindingsPage(f FindingFilter, page, pageSize int) ([]*DBFinding
type FindingStats struct {
Total int `json:"total"`
Pending int `json:"pending"`
Critical int `json:"critical"`
High int `json:"high"`
Medium int `json:"medium"`
Low int `json:"low"`
@@ -200,10 +203,11 @@ func (d *DB) FindingStats() (*FindingStats, error) {
err := d.QueryRow(`SELECT
COUNT(*),
COUNT(*) FILTER (WHERE status = 'pending'),
COUNT(*) FILTER (WHERE severity = 'critical'),
COUNT(*) FILTER (WHERE severity = 'high'),
COUNT(*) FILTER (WHERE severity = 'medium'),
COUNT(*) FILTER (WHERE severity = 'low')
FROM findings`).Scan(&st.Total, &st.Pending, &st.High, &st.Medium, &st.Low)
FROM findings`).Scan(&st.Total, &st.Pending, &st.Critical, &st.High, &st.Medium, &st.Low)
if err != nil {
return nil, err
}
+24 -20
View File
@@ -21,19 +21,21 @@ func TestFindingsPageAndStats(t *testing.T) {
cleanup()
defer cleanup()
// Seed 5 findings under the marker vulnclass: 3 high, 2 low; 2 pending, 3 resolved.
// Seed 6 findings under the marker vulnclass: 1 critical, 3 high, 2 low; 2 pending.
// The critical row carries a name to verify round-trip.
seed := []struct {
sev, status string
sev, status, name string
}{
{"high", "pending"},
{"high", "resolved"},
{"high", "resolved"},
{"low", "pending"},
{"low", "resolved"},
{"critical", "pending", "严重漏洞标题"},
{"high", "resolved", ""},
{"high", "resolved", ""},
{"high", "pending", ""},
{"low", "resolved", ""},
{"low", "resolved", ""},
}
var ids []int64
for i, s := range seed {
id, err := d.AddFinding(0, 0, vc, s.sev, "summary", "poc", "tester", nil)
id, err := d.AddFinding(0, 0, vc, s.name, s.sev, "summary", "poc", "tester", nil)
if err != nil {
t.Fatalf("AddFinding[%d]: %v", i, err)
}
@@ -43,27 +45,26 @@ func TestFindingsPageAndStats(t *testing.T) {
ids = append(ids, id)
}
// Filter by our vulnclass → exactly the 5 seeded rows, paged 2 per page.
// Filter by our vulnclass → exactly the 6 seeded rows, paged 2 per page.
p1, total, err := d.ListFindingsPage(FindingFilter{VulnClass: vc, Sort: "severity"}, 1, 2)
if err != nil {
t.Fatal(err)
}
if total != 5 {
t.Fatalf("total: want 5, got %d", total)
if total != 6 {
t.Fatalf("total: want 6, got %d", total)
}
if len(p1) != 2 {
t.Fatalf("page1 size: want 2, got %d", len(p1))
}
// severity sort → highs first
if p1[0].Severity != "high" || p1[1].Severity != "high" {
t.Fatalf("severity sort broken: %q, %q", p1[0].Severity, p1[1].Severity)
// severity sort → critical first (with its name round-tripped), then high.
if p1[0].Severity != "critical" {
t.Fatalf("severity sort: want critical first, got %q", p1[0].Severity)
}
p3, _, err := d.ListFindingsPage(FindingFilter{VulnClass: vc, Sort: "severity"}, 3, 2)
if err != nil {
t.Fatal(err)
if p1[0].Name != "严重漏洞标题" {
t.Fatalf("name round-trip: want 严重漏洞标题, got %q", p1[0].Name)
}
if len(p3) != 1 {
t.Fatalf("page3 size: want 1 (5 rows / 2), got %d", len(p3))
if p1[1].Severity != "high" {
t.Fatalf("severity sort: want high second, got %q", p1[1].Severity)
}
// Combined filter: vulnclass + status=pending → 2 rows.
@@ -90,7 +91,10 @@ func TestFindingsPageAndStats(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if st.Total < 5 || st.High < 3 || st.Low < 2 || st.Pending < 2 {
if st.Critical < 1 {
t.Fatalf("stats critical undercount: %+v", st)
}
if st.Total < 6 || st.High < 3 || st.Low < 2 || st.Pending < 2 {
t.Fatalf("stats undercount: %+v", st)
}
if !slices.Contains(st.VulnClasses, vc) {
+4
View File
@@ -540,6 +540,9 @@ CREATE TABLE IF NOT EXISTS findings (
task_id BIGINT REFERENCES tasks(id) ON DELETE SET NULL,
node_id BIGINT REFERENCES exploration_nodes(id) ON DELETE SET NULL,
vulnclass TEXT NOT NULL DEFAULT '',
-- 漏洞名称(可读标题);为空时前端回退展示 vulnclass。severity 取值:
-- critical 严重 / high 高 / medium 中 / low 低(不加 CHECK,与 status 一致由 server 白名单校验)。
name TEXT NOT NULL DEFAULT '',
severity TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL DEFAULT '',
evidence TEXT NOT NULL DEFAULT '',
@@ -552,6 +555,7 @@ CREATE TABLE IF NOT EXISTS findings (
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE findings ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'pending';
ALTER TABLE findings ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS idx_findings_task ON findings(task_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_findings_time ON findings(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_findings_status ON findings(status, created_at DESC);