mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 09:06:36 +08:00
feat(waf): StatusRatio/StatusCount 支持 2xx/4xx/5xx 类写法
自动 IP 组表达式可按状态码类汇总占比与计数,兼容原有精确状态码。
This commit is contained in:
@@ -40,18 +40,102 @@ type ipGroupAutoRuleEnv struct {
|
||||
statusCounts map[int]int
|
||||
}
|
||||
|
||||
func (env ipGroupAutoRuleEnv) StatusCount(code int) int {
|
||||
func (env ipGroupAutoRuleEnv) StatusCount(code any) int {
|
||||
if env.statusCounts == nil {
|
||||
return 0
|
||||
}
|
||||
return env.statusCounts[code]
|
||||
return countStatusMatches(env.statusCounts, code)
|
||||
}
|
||||
|
||||
func (env ipGroupAutoRuleEnv) StatusRatio(code int) float64 {
|
||||
func (env ipGroupAutoRuleEnv) StatusRatio(code any) float64 {
|
||||
if env.RequestCount <= 0 || env.statusCounts == nil {
|
||||
return 0.0
|
||||
}
|
||||
return float64(env.statusCounts[code]) / float64(env.RequestCount)
|
||||
return float64(countStatusMatches(env.statusCounts, code)) / float64(env.RequestCount)
|
||||
}
|
||||
|
||||
// countStatusMatches sums status counts for an exact code or class token.
|
||||
// Accepted forms:
|
||||
// - int / int64 / float64: exact status code (e.g. 404)
|
||||
// - string digits: exact status code (e.g. "404")
|
||||
// - string class: "1xx".."5xx" (case-insensitive), matching that hundred range
|
||||
func countStatusMatches(statusCounts map[int]int, code any) int {
|
||||
if statusCounts == nil {
|
||||
return 0
|
||||
}
|
||||
switch v := code.(type) {
|
||||
case int:
|
||||
return statusCounts[v]
|
||||
case int8:
|
||||
return statusCounts[int(v)]
|
||||
case int16:
|
||||
return statusCounts[int(v)]
|
||||
case int32:
|
||||
return statusCounts[int(v)]
|
||||
case int64:
|
||||
return statusCounts[int(v)]
|
||||
case uint:
|
||||
return statusCounts[int(v)]
|
||||
case uint8:
|
||||
return statusCounts[int(v)]
|
||||
case uint16:
|
||||
return statusCounts[int(v)]
|
||||
case uint32:
|
||||
return statusCounts[int(v)]
|
||||
case uint64:
|
||||
return statusCounts[int(v)]
|
||||
case float32:
|
||||
if v != float32(int(v)) {
|
||||
return 0
|
||||
}
|
||||
return statusCounts[int(v)]
|
||||
case float64:
|
||||
if v != float64(int(v)) {
|
||||
return 0
|
||||
}
|
||||
return statusCounts[int(v)]
|
||||
case string:
|
||||
return countStatusMatchesString(statusCounts, v)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func countStatusMatchesString(statusCounts map[int]int, raw string) int {
|
||||
token := strings.TrimSpace(strings.ToLower(raw))
|
||||
if token == "" {
|
||||
return 0
|
||||
}
|
||||
if len(token) == 3 && token[1] == 'x' && token[2] == 'x' {
|
||||
classDigit := token[0]
|
||||
if classDigit < '1' || classDigit > '5' {
|
||||
return 0
|
||||
}
|
||||
base := int(classDigit-'0') * 100
|
||||
total := 0
|
||||
for code, count := range statusCounts {
|
||||
if code >= base && code < base+100 {
|
||||
total += count
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
// exact numeric string, e.g. "404"
|
||||
var code int
|
||||
for _, ch := range token {
|
||||
if ch < '0' || ch > '9' {
|
||||
return 0
|
||||
}
|
||||
code = code*10 + int(ch-'0')
|
||||
if code > 999 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if code < 100 || code > 599 {
|
||||
// still allow lookup for non-standard codes if present
|
||||
return statusCounts[code]
|
||||
}
|
||||
return statusCounts[code]
|
||||
}
|
||||
|
||||
type ipGroupAutoAccumulator struct {
|
||||
|
||||
@@ -209,3 +209,62 @@ func seedWAFAccessLogs(t *testing.T, ctx context.Context, loggedAt time.Time, re
|
||||
}
|
||||
require.NoError(t, model.InsertOpenFlareAccessLogsBatch(ctx, records))
|
||||
}
|
||||
|
||||
func TestCountStatusMatchesSupportsClassTokens(t *testing.T) {
|
||||
counts := map[int]int{
|
||||
200: 10,
|
||||
201: 5,
|
||||
404: 20,
|
||||
403: 10,
|
||||
500: 4,
|
||||
502: 1,
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
code any
|
||||
want int
|
||||
}{
|
||||
{name: "exact int", code: 404, want: 20},
|
||||
{name: "exact string", code: "403", want: 10},
|
||||
{name: "2xx class", code: "2xx", want: 15},
|
||||
{name: "4xx class upper", code: "4XX", want: 30},
|
||||
{name: "5xx class", code: "5xx", want: 5},
|
||||
{name: "unknown class", code: "9xx", want: 0},
|
||||
{name: "invalid token", code: "abc", want: 0},
|
||||
{name: "float exact", code: float64(200), want: 10},
|
||||
{name: "float non-int", code: 200.5, want: 0},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := countStatusMatches(counts, tc.code)
|
||||
if got != tc.want {
|
||||
t.Errorf("countStatusMatches(%v) = %d, want %d", tc.code, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusRatioClassTokenInExpr(t *testing.T) {
|
||||
cleanup := setupIPGroupSyncTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now().UTC()
|
||||
// 100 requests, 80 of which are 404 → 4xx ratio 0.8
|
||||
seedWAFAccessLogs(t, ctx, now, "203.0.113.40", "app.example.com", 100, 80)
|
||||
// mostly OK → should not match
|
||||
seedWAFAccessLogs(t, ctx, now, "203.0.113.41", "app.example.com", 100, 10)
|
||||
|
||||
result, err := TestIPGroupAutoConfig(ctx, IPGroupAutoTestInput{
|
||||
AutoConfig: json.RawMessage(`{
|
||||
"lookback_minutes": 60,
|
||||
"rules": [
|
||||
{"name":"高 4xx 占比","expr":"request_count >= 100 && StatusRatio(\"4xx\") >= 0.8"}
|
||||
]
|
||||
}`),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, result.MatchedCount)
|
||||
require.Len(t, result.MatchedIPs, 1)
|
||||
assert.Equal(t, "203.0.113.40", result.MatchedIPs[0])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user