mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
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
150 lines
5.3 KiB
Go
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")
|
|
}
|
|
}
|