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 往返断言。
104 lines
3.1 KiB
Go
104 lines
3.1 KiB
Go
package db
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
// TestFindingsPageAndStats exercises ListFindingsPage (filter/sort/paging) and
|
|
// FindingStats against the live dev PG. It tags its rows with a unique vulnclass
|
|
// so assertions are isolated from any pre-existing data, and cleans up after.
|
|
func TestFindingsPageAndStats(t *testing.T) {
|
|
d, err := Open(testDSN(t))
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) — skipping", err)
|
|
}
|
|
defer d.Close()
|
|
|
|
const vc = "__test_vc_pagination__"
|
|
// clean any leftovers from a prior aborted run, and clean up on exit
|
|
cleanup := func() { _, _ = d.Exec(`DELETE FROM findings WHERE vulnclass=$1`, vc) }
|
|
cleanup()
|
|
defer cleanup()
|
|
|
|
// 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, name string
|
|
}{
|
|
{"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.name, s.sev, "summary", "poc", "tester", nil)
|
|
if err != nil {
|
|
t.Fatalf("AddFinding[%d]: %v", i, err)
|
|
}
|
|
if _, err := d.SetFindingStatus(id, s.status); err != nil {
|
|
t.Fatalf("SetFindingStatus[%d]: %v", i, err)
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
|
|
// 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 != 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 → 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)
|
|
}
|
|
if p1[0].Name != "严重漏洞标题" {
|
|
t.Fatalf("name round-trip: want 严重漏洞标题, got %q", p1[0].Name)
|
|
}
|
|
if p1[1].Severity != "high" {
|
|
t.Fatalf("severity sort: want high second, got %q", p1[1].Severity)
|
|
}
|
|
|
|
// Combined filter: vulnclass + status=pending → 2 rows.
|
|
pend, total, err := d.ListFindingsPage(FindingFilter{VulnClass: vc, Status: FindingPending}, 1, 50)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if total != 2 || len(pend) != 2 {
|
|
t.Fatalf("pending filter: want 2/2, got %d/%d", total, len(pend))
|
|
}
|
|
|
|
// Combined filter: vulnclass + severity=high → 3 rows.
|
|
_, total, err = d.ListFindingsPage(FindingFilter{VulnClass: vc, Severity: "high"}, 1, 50)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if total != 3 {
|
|
t.Fatalf("high filter: want 3, got %d", total)
|
|
}
|
|
|
|
// Stats: whole-table, so assert our contribution is reflected (>=) and the
|
|
// marker vulnclass is present.
|
|
st, err := d.FindingStats()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
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) {
|
|
t.Fatalf("stats vulnclasses missing %q", vc)
|
|
}
|
|
}
|