详情页 /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 通过。
128 lines
3.9 KiB
Go
128 lines
3.9 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|