- db: DeleteFinding(id) 删 findings 行 + 来源探索节点(kind='finding'),
级联清理 edges/node_assets、置空 activity 引用;返回受影响行数。
- server: DELETE /api/exploration/findings/{id}。
- web: api.deleteFinding;三处均可删且带 AlertDialog 二次确认——
发现列表页独立「操作」列(每行垃圾桶按钮)、漏洞详情页头部删除按钮、
任务详情·发现 Tab 展开区删除按钮。两处列表同源,删一次全部消失。
- 测试 TestDeleteFinding:findings 行 + 来源节点均被删,重复删返回(0,nil)。
go build/vet、tsc 通过。
186 lines
5.8 KiB
Go
186 lines
5.8 KiB
Go
package db
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
// TestDeleteFinding verifies删除漏洞 removes both the findings row and its
|
|
// originating exploration node (kind='finding').
|
|
func TestDeleteFinding(t *testing.T) {
|
|
d, err := Open(testDSN(t))
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) — skipping", err)
|
|
}
|
|
defer d.Close()
|
|
|
|
tk, err := d.CreateTask("删除漏洞测试", "目标", nil, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer d.DeleteTask(tk.ID)
|
|
|
|
// seed a finding node in the task's exploration graph, then a findings row on it
|
|
es := d.Exploration(tk.ExplorationID)
|
|
nodeID, err := es.AddNode(KindFinding, map[string]any{"summary": "x", "severity": "high"}, 5, "confirmed", "worker", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fid, err := d.AddFinding(tk.ID, nodeID, "XSS", "反射型 XSS", "high", "summary", "poc", "worker", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
n, err := d.DeleteFinding(fid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("DeleteFinding rows: want 1, got %d", n)
|
|
}
|
|
if f, _ := d.GetFinding(fid); f != nil {
|
|
t.Fatalf("finding row should be gone, got %+v", f)
|
|
}
|
|
var cnt int
|
|
d.QueryRow(`SELECT count(*) FROM exploration_nodes WHERE id=$1`, nodeID).Scan(&cnt)
|
|
if cnt != 0 {
|
|
t.Fatalf("originating finding node should be deleted, still %d", cnt)
|
|
}
|
|
|
|
// deleting a non-existent finding is a no-op (0 rows), not an error
|
|
if n, err := d.DeleteFinding(fid); err != nil || n != 0 {
|
|
t.Fatalf("re-delete: want (0,nil), got (%d,%v)", n, err)
|
|
}
|
|
}
|
|
|
|
// 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 one.Report != "" {
|
|
t.Fatalf("new finding report should be empty, got %q", one.Report)
|
|
}
|
|
// report column round-trips through GetFinding.
|
|
if _, err := d.Exec(`UPDATE findings SET report=$1 WHERE id=$2`, "# 报告\n正文", ids[0]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if one, _ = d.GetFinding(ids[0]); one.Report != "# 报告\n正文" {
|
|
t.Fatalf("report not read back: %q", one.Report)
|
|
}
|
|
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)
|
|
}
|
|
}
|