Files
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

150 lines
5.3 KiB
Go

package notify
import "testing"
func TestParseFilterMalformedFallsBackToMatchAll(t *testing.T) {
// Malformed JSON, empty input, fields of the wrong type — all must degrade to
// a zero-value Filter, i.e. "no filtering". This invariant is where "rather
// over-push than miss" lands: if this ever changes to erroring or partial
// parsing, one mistyped character silently drops every high-severity
// notification.
cases := []struct {
name string
raw string
}{
{"empty input", ""},
{"invalid JSON", `{not json`},
{"truncated JSON", `{"min_severity":`},
{"type mismatch", `{"min_severity": 123, "task_ids": "abc"}`},
{"top-level array", `[1,2,3]`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
f := ParseFilter([]byte(tc.raw))
if f.MinSeverity != "" || len(f.TaskIDs) != 0 || len(f.AssetIDs) != 0 {
t.Fatalf("malformed configuration should degrade to a zero-value Filter, got %+v", f)
}
// A zero-value Filter must match any event.
ev := Snapshot{Kind: EventFindingCreated, Severity: "low", VulnClass: "XSS"}
if !Match(f, ev) {
t.Fatal("a zero-value Filter should match all events")
}
})
}
}
func TestMatchSeverityThreshold(t *testing.T) {
ev := func(sev string) Snapshot {
return Snapshot{Kind: EventFindingCreated, Severity: sev}
}
cases := []struct {
min string
sev string
expect bool
}{
{"", "low", true},
{"", "critical", true},
{"high", "critical", true},
{"high", "high", true},
{"high", "medium", false},
{"high", "low", false},
{"critical", "high", false},
{"critical", "critical", true},
// Unknown severities have ordinal 0 and should be stopped by any non-empty
// threshold (when in doubt, do not push).
{"low", "", false},
{"low", "unknown", false},
{"", "", true},
}
for _, tc := range cases {
got := Match(Filter{MinSeverity: tc.min}, ev(tc.sev))
if got != tc.expect {
t.Errorf("min=%q sev=%q: want %v got %v", tc.min, tc.sev, tc.expect, got)
}
}
}
func TestMatchScopeRestrictions(t *testing.T) {
ev := Snapshot{
Kind: EventFindingCreated,
Severity: "high",
TaskID: 7,
AssetIDs: []int64{10, 20},
VulnClass: "SQL injection",
}
cases := []struct {
name string
filter Filter
expect bool
}{
{"empty scope = unrestricted", Filter{}, true},
{"task hit", Filter{TaskIDs: []int64{7}}, true},
{"task miss", Filter{TaskIDs: []int64{8}}, false},
{"task multi-select with hit", Filter{TaskIDs: []int64{8, 7}}, true},
{"assets intersect", Filter{AssetIDs: []int64{20, 99}}, true},
{"assets do not intersect", Filter{AssetIDs: []int64{99}}, false},
{"task and asset both hit", Filter{TaskIDs: []int64{7}, AssetIDs: []int64{10}}, true},
{"task hit but asset miss", Filter{TaskIDs: []int64{7}, AssetIDs: []int64{99}}, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Match(tc.filter, ev); got != tc.expect {
t.Errorf("want %v got %v", tc.expect, got)
}
})
}
}
func TestMatchVulnClassKeywords(t *testing.T) {
ev := func(class string) Snapshot {
return Snapshot{Kind: EventFindingCreated, Severity: "high", VulnClass: class}
}
cases := []struct {
name string
filter Filter
class string
expect bool
}{
{"empty include = accept all", Filter{}, "any class", true},
{"include hit", Filter{VulnClassInclude: []string{"SQL"}}, "SQL injection", true},
{"include miss", Filter{VulnClassInclude: []string{"command execution"}}, "SQL injection", false},
{"include multi-word any hit", Filter{VulnClassInclude: []string{"command execution", "SQL"}}, "SQL injection", true},
{"case insensitive", Filter{VulnClassInclude: []string{"sql"}}, "SQL injection", true},
{"exclude hit excludes", Filter{VulnClassExclude: []string{"information disclosure"}}, "information disclosure", false},
{"exclude miss passes", Filter{VulnClassExclude: []string{"information disclosure"}}, "SQL injection", true},
// Exclusion takes precedence over inclusion: an event hitting both should
// be knocked out.
{"exclusion takes precedence over inclusion", Filter{
VulnClassInclude: []string{"SQL"},
VulnClassExclude: []string{"injection"},
}, "SQL injection", false},
// Pure-whitespace keywords must be ignored, otherwise they degrade into
// "match any string containing a space".
{"whitespace keywords ignored", Filter{VulnClassInclude: []string{"", " "}}, "SQL injection", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Match(tc.filter, ev(tc.class)); got != tc.expect {
t.Errorf("want %v got %v", tc.expect, got)
}
})
}
}
func TestMatchStatusChangeRequiresOptIn(t *testing.T) {
ev := Snapshot{Kind: EventFindingStatusChanged, Severity: "critical", FromStatus: "pending", ToStatus: "fixed"}
// Default off: what most people mean by "push vulnerabilities" is discovering
// new vulnerabilities, not a running log of status changes.
if Match(Filter{MinSeverity: "low"}, ev) {
t.Fatal("status-change events should be skipped when not enabled")
}
if !Match(Filter{OnStatusChange: true}, ev) {
t.Fatal("status-change events should match once on_status_change is enabled")
}
// Creation events are not affected by on_status_change.
created := Snapshot{Kind: EventFindingCreated, Severity: "critical"}
if !Match(Filter{MinSeverity: "low"}, created) {
t.Fatal("creation events should not depend on on_status_change")
}
}