From 81dd44c8fcd8e93df85d7b0cf6dc149fac659f16 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 2 Jun 2026 08:31:39 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E7=8A=B6=E6=80=81=E7=A0=81=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guide/waf-ip-group-expr.md | 7 +++ openflare_server/service/waf_ip_group.go | 23 ++++++++- openflare_server/service/waf_test.go | 47 +++++++++++++++++++ .../waf/components/ip-groups-page.tsx | 2 +- 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/docs/guide/waf-ip-group-expr.md b/docs/guide/waf-ip-group-expr.md index dc6c9c59..3bd20050 100644 --- a/docs/guide/waf-ip-group-expr.md +++ b/docs/guide/waf-ip-group-expr.md @@ -57,6 +57,13 @@ Host 是否为“通过 IP 访问”按请求日志中的 `Host` 字段判断: 比例字段都是 `0` 到 `1` 之间的小数。80% 应写成 `0.8`,50% 应写成 `0.5`。 +### 自定义状态码匹配方法 + +如果内置的 `status_404_count` 和 `status_404_ratio` 不能满足您的需求,您可以使用以下内置方法来匹配任意状态码的请求数与占比: + +* **`StatusCount(code)`**: 获取当前 IP 在回看窗口内返回指定状态码的请求数(如 `StatusCount(403) > 10`) +* **`StatusRatio(code)`**: 获取当前 IP 在回看窗口内返回指定状态码的请求数占该 IP 总请求数的比例(如 `StatusRatio(502) >= 0.5`) + ## Expr 常用写法 自动 IP 组使用 Expr 语法,当前表达式必须返回布尔值。 diff --git a/openflare_server/service/waf_ip_group.go b/openflare_server/service/waf_ip_group.go index 53f7ce47..bc9448c2 100644 --- a/openflare_server/service/waf_ip_group.go +++ b/openflare_server/service/waf_ip_group.go @@ -69,6 +69,21 @@ type wafIPGroupAutoRuleEnv struct { ClientErrorCount int `expr:"client_error_count"` ServerErrorCount int `expr:"server_error_count"` LastSeenUnix int64 `expr:"last_seen_unix"` + statusCounts map[int]int +} + +func (env wafIPGroupAutoRuleEnv) StatusCount(code int) int { + if env.statusCounts == nil { + return 0 + } + return env.statusCounts[code] +} + +func (env wafIPGroupAutoRuleEnv) StatusRatio(code int) float64 { + if env.RequestCount <= 0 || env.statusCounts == nil { + return 0.0 + } + return float64(env.statusCounts[code]) / float64(env.RequestCount) } type wafIPGroupAutoAccumulator struct { @@ -79,6 +94,7 @@ type wafIPGroupAutoAccumulator struct { clientErrorCount int serverErrorCount int lastSeen time.Time + statusCounts map[int]int } type WAFIPGroupInput struct { @@ -755,10 +771,14 @@ func evaluateParsedWAFIPGroupAutoConfig(config wafIPGroupAutoConfig, now time.Ti } acc := accumulators[ip] if acc == nil { - acc = &wafIPGroupAutoAccumulator{ip: ip} + acc = &wafIPGroupAutoAccumulator{ + ip: ip, + statusCounts: make(map[int]int), + } accumulators[ip] = acc } acc.requestCount++ + acc.statusCounts[item.StatusCode]++ if item.StatusCode == http.StatusNotFound { acc.status404Count++ } @@ -800,6 +820,7 @@ func (acc *wafIPGroupAutoAccumulator) toExprEnv() wafIPGroupAutoRuleEnv { IPHostCount: acc.ipHostCount, ClientErrorCount: acc.clientErrorCount, ServerErrorCount: acc.serverErrorCount, + statusCounts: acc.statusCounts, } if acc.requestCount > 0 { env.Status404Ratio = float64(acc.status404Count) / float64(acc.requestCount) diff --git a/openflare_server/service/waf_test.go b/openflare_server/service/waf_test.go index c9b2a88e..b27e64d2 100644 --- a/openflare_server/service/waf_test.go +++ b/openflare_server/service/waf_test.go @@ -464,3 +464,50 @@ func seedWAFNodeAccessLogs(t *testing.T, loggedAt time.Time, remoteAddr string, } } } + +func TestSyncWAFIPGroupAutomaticCustomStatusRules(t *testing.T) { + setupServiceTestDB(t) + + now := time.Now().UTC() + // Seed 10 requests from 203.0.113.50, where 3 return 403, 7 return 200 + seedWAFNodeAccessLogsWithStatus(t, now, "203.0.113.50", "app.example.com", 7, http.StatusOK) + seedWAFNodeAccessLogsWithStatus(t, now, "203.0.113.50", "app.example.com", 3, http.StatusForbidden) + + group, err := CreateWAFIPGroup(WAFIPGroupInput{ + Name: "custom status code blacklist", + Type: WAFIPGroupTypeAutomatic, + Enabled: true, + AutoConfig: json.RawMessage(`{ + "lookback_minutes": 60, + "rules": [ + {"name":"高频 403 探测","expr":"StatusCount(403) >= 3 && StatusRatio(403) >= 0.3"} + ] + }`), + }) + if err != nil { + t.Fatalf("CreateWAFIPGroup failed: %v", err) + } + result, err := SyncWAFIPGroup(group.ID) + if err != nil { + t.Fatalf("SyncWAFIPGroup failed: %v", err) + } + if result.IPCount != 1 || result.Group.IPList[0] != "203.0.113.50" { + t.Fatalf("expected 203.0.113.50 to be matched, got %#v", result) + } +} + +func seedWAFNodeAccessLogsWithStatus(t *testing.T, loggedAt time.Time, remoteAddr string, host string, count int, statusCode int) { + t.Helper() + for i := 0; i < count; i++ { + if err := model.DB.Create(&model.NodeAccessLog{ + NodeID: "node-waf-auto", + LoggedAt: loggedAt.Add(-time.Duration(i%30) * time.Second), + RemoteAddr: remoteAddr, + Host: host, + Path: "/probe", + StatusCode: statusCode, + }).Error; err != nil { + t.Fatalf("failed to seed access log: %v", err) + } + } +} diff --git a/openflare_server/web/features/waf/components/ip-groups-page.tsx b/openflare_server/web/features/waf/components/ip-groups-page.tsx index f7db55a0..ac74cd8d 100644 --- a/openflare_server/web/features/waf/components/ip-groups-page.tsx +++ b/openflare_server/web/features/waf/components/ip-groups-page.tsx @@ -587,7 +587,7 @@ export function WAFIPGroupsPage() {