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:
1 parent
7b3eec2166
commit
ff3d41ea35
14 files changed
+96
-56
No files matched your search
+3
-2
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user