Merge pull request #911 from samwafgo/fix_ruleglobal

fix:rule bug
This commit is contained in:
samwafgo
2026-08-03 13:40:32 +08:00
committed by GitHub
11 changed files with 493 additions and 170 deletions
+2 -2
View File
@@ -44,7 +44,7 @@ rule R<唯一标识> "规则中文描述" salience <优先级数字> {
}
说明:
- R<唯一标识>:规则名,以大写 R 开头,只能由字母和数字组成(不含横线)。
- salience:优先级,数值越大越先命中。默认写 10;放行类建议写 100 使其优先。
- salience:优先级,数值越大越优先。站点规则和全局规则一起按 salience 仲裁,同优先级时按 拦截 > 放行 > 仅记录 取。默认写 10;放行类要压过全局拦截规则时写 100。
- when:条件,为 true 时命中。then:命中后的动作,必须写且只能写一个。
# 可用请求字段(MF 开头,代表当前请求)
@@ -103,7 +103,7 @@ rule R<uniqueId> "rule description" salience <priority> {
}
Notes:
- R<uniqueId>: rule name, starts with uppercase R, letters and digits only (no dash).
- salience: priority, higher wins first. Use 10 by default; use 100 for allow rules so they take precedence.
- salience: priority, higher wins. Site rules and global rules are arbitrated together by salience; on a tie the order is deny > allow > log. Use 10 by default; use 100 for allow rules that must override a global deny rule.
- when: condition; matches when true. then: exactly one action.
# Request fields (MF = current request)
+4
View File
@@ -16,4 +16,8 @@ type Rules struct {
IsPublicRule int `json:"is_public_rule"` //是否为公共规则
IsManualRule int `json:"is_manual_rule"` //是否为手工写规则 1:手工编写 0 :UI界面形式
RuleStatus int `json:"rule_status"` //规则是否开启 1,开启 0,关闭不生效 999 删除
//Salience 规则优先级,不落库:从 RuleContent 里解析出来供列表展示
//优先级本身写在规则文本(GRL)里,手工模式和界面模式都有,所以统一从文本解析,无需加数据库列
Salience int `gorm:"-" json:"salience"`
}
+12
View File
@@ -8,6 +8,7 @@ import (
"SamWaf/model"
"SamWaf/model/baseorm"
"SamWaf/model/request"
"SamWaf/utils"
"errors"
"time"
)
@@ -147,9 +148,19 @@ func (receiver *WafRuleService) GetListApi(wafRuleSearchReq request.WafRuleSearc
global.GWAF_LOCAL_DB.Model(&model.Rules{}).Where(whereField, whereValues...).Limit(wafRuleSearchReq.PageSize).Offset(wafRuleSearchReq.PageSize * (wafRuleSearchReq.PageIndex - 1)).Find(&rules)
global.GWAF_LOCAL_DB.Model(&model.Rules{}).Where(whereField, whereValues...).Count(&total)
fillRuleSalience(rules)
return rules, total, nil
}
// fillRuleSalience 给列表数据补上优先级
// 优先级只存在于规则文本(GRL)里,没有数据库列;手工模式和界面模式的规则文本都带 salience,
// 所以统一从文本解析,避免为了展示一个字段去加列 + 写迁移 + 回填存量数据
func fillRuleSalience(rules []model.Rules) {
for i := range rules {
rules[i].Salience = utils.ExtractRuleSalience(rules[i].RuleContent)
}
}
func (receiver *WafRuleService) GetListByHostCodeApi(wafRuleSearchReq request.WafRuleSearchReq) ([]model.Rules, int64, error) {
var total int64 = 0
var rules []model.Rules
@@ -159,6 +170,7 @@ func (receiver *WafRuleService) GetListByHostCodeApi(wafRuleSearchReq request.Wa
global.GWAF_LOCAL_DB.Where("host_code = ? and rule_status <> 999",
wafRuleSearchReq.HostCode).Model(&model.Rules{}).Count(&total)
fillRuleSalience(rules)
return rules, total, nil
}
+20
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
)
@@ -21,6 +22,8 @@ const (
// ruleSkipModules 允许被跳过的检测模块白名单
// 注意:跳过只对"排在自定义规则之后"的检测环节生效,具体取决于规则编排模式
// Allow 和 AllowAll 都同样参与站点/全局的跨作用域仲裁(按 salience 比较),
// 两者的区别只在于放行之后跳过多大范围的后续检测
var ruleSkipModules = []string{
"BOT", // 爬虫检测
"SQLI", // SQL注入
@@ -77,6 +80,8 @@ var (
ruleThenRegex = regexp.MustCompile(`\bthen\b`)
// 动作标记:RF.Allow(...) / RF.AllowAll() / RF.Deny() / RF.Log()
ruleActionRegex = regexp.MustCompile(`RF\s*\.\s*(AllowAll|Allow|Deny|Log)\s*\(([^)]*)\)`)
// 优先级:rule Rxxx "描述" salience 10 {
ruleSalienceRegex = regexp.MustCompile(`\bsalience\s+(\d+)`)
)
// BuildGrlSkeleton 生成规则文本的"骨架":把字符串字面量和注释里的内容全部替换成空格,
@@ -363,6 +368,21 @@ func CountRuleBlocks(ruleText string) int {
return len(splitRuleBlocks(BuildGrlSkeleton(ruleText)))
}
// ExtractRuleSalience 提取规则文本里的优先级(salience),用于列表展示
// 在骨架上匹配,避免把 MF.URL.Contains("salience 999") 这类字符串字面量里的内容当成真优先级。
// 一段文本含多条规则时取第一条;解析不出来返回 0。
func ExtractRuleSalience(ruleText string) int {
m := ruleSalienceRegex.FindStringSubmatch(BuildGrlSkeleton(ruleText))
if len(m) < 2 {
return 0
}
v, err := strconv.Atoi(m[1])
if err != nil {
return 0
}
return v
}
// ExtractRuleNamesFromText 提取规则文本中所有规则名(含 R 前缀)
func ExtractRuleNamesFromText(ruleText string) []string {
blocks := splitRuleBlocks(BuildGrlSkeleton(ruleText))
+69
View File
@@ -259,6 +259,75 @@ func TestBuildGrlSkeleton_KeepsLength(t *testing.T) {
}
}
// ---------- 优先级解析 ----------
func TestExtractRuleSalience(t *testing.T) {
cases := []struct {
name string
ruleText string
want int
}{
{
name: "正常解析",
ruleText: `rule Rabc123 "拦截" salience 90 {
when MF.URL == "/admin"
then RF.Deny();
}`,
want: 90,
},
{
name: "salience为0",
ruleText: `rule Rabc123 "拦截" salience 0 {
when MF.URL == "/admin"
then RF.Deny();
}`,
want: 0,
},
{
name: "字符串字面量里的假优先级不该被解析",
ruleText: `rule Rabc123 "拦截" salience 10 {
when MF.URL.Contains("salience 999") == true
then RF.Deny();
}`,
want: 10,
},
{
name: "注释里的假优先级不该被解析",
ruleText: `// salience 999
rule Rabc123 "拦截" salience 10 {
when MF.URL == "/admin"
then RF.Deny();
}`,
want: 10,
},
{
name: "解析不出来返回0",
ruleText: `这不是规则`,
want: 0,
},
{
name: "多条规则取第一条",
ruleText: `rule Rone "放行" salience 100 {
when MF.SRC_IP == "1.1.1.1"
then RF.Allow();
}
rule Rtwo "拦截" salience 10 {
when MF.URL == "/admin"
then RF.Deny();
}`,
want: 100,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := ExtractRuleSalience(tc.ruleText); got != tc.want {
t.Errorf("期望 %d 实际 %d", tc.want, got)
}
})
}
}
// ---------- 多条规则 ----------
func TestExtractRuleActions_MultipleRules(t *testing.T) {
+94 -76
View File
@@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"regexp"
"sort"
"strconv"
"github.com/hyperjumptech/grule-rule-engine/ast"
@@ -83,6 +84,8 @@ type ruleMatchResult struct {
Matched bool
Action utils.RuleActionInfo
Title string
// TopSalience 本侧命中规则里的最高优先级,用于和另一侧仲裁
TopSalience int
}
// matchRules 执行一组规则并解析出动作
@@ -114,9 +117,73 @@ func matchRules(ruleHelper *utils.RuleHelper, weblogbean *innerbean.WebLog, titl
out.Matched = true
out.Action = pickRuleAction(ruleHelper, ruleMatchs)
out.Title = titlePrefix + rulestr
// grule 的 FetchMatchingRules 已按 salience 降序返回,第一条即最高优先级
out.TopSalience = ruleMatchs[0].Salience
return out
}
// arbitrate 在站点规则和全局规则的命中结果之间裁决最终动作
//
// salience 只在单个 KnowledgeBase 内排序,而站点和全局是两个独立的规则引擎,
// 所以跨作用域的优先级必须在这里显式仲裁,否则用户在界面上填的"优先级"跨作用域就是空的。
//
// 1. 只有一侧命中 → 用那一侧
// 2. 两侧都命中 → salience 高的一侧胜出
// 3. salience 相同 → 按 拦截 > 放行 > 仅记录 取,保证偏安全,也和历史行为一致
// (老配置站点和全局默认都是 salience 10,此时全局拦截依旧压过站点放行)
//
// 两侧同为放行且同优先级时,跳过模块取并集、Title 拼接。
func arbitrate(local, globalR ruleMatchResult) ruleMatchResult {
if !local.Matched {
return globalR
}
if !globalR.Matched {
return local
}
if local.TopSalience > globalR.TopSalience {
return local
}
if globalR.TopSalience > local.TopSalience {
return globalR
}
// 同优先级:按动作强弱兜底
rank := map[string]int{
utils.RuleActionDeny: 3,
utils.RuleActionAllow: 2,
utils.RuleActionLog: 1,
}
if rank[local.Action.Action] > rank[globalR.Action.Action] {
return local
}
if rank[globalR.Action.Action] > rank[local.Action.Action] {
return globalR
}
// 动作也相同:合并两侧信息
merged := ruleMatchResult{
Matched: true,
Action: utils.RuleActionInfo{Action: local.Action.Action},
Title: local.Title + globalR.Title,
TopSalience: local.TopSalience,
}
if merged.Action.Action == utils.RuleActionAllow {
skipSet := make(map[string]bool)
for _, m := range local.Action.SkipModules {
skipSet[m] = true
}
for _, m := range globalR.Action.SkipModules {
skipSet[m] = true
}
for m := range skipSet {
merged.Action.SkipModules = append(merged.Action.SkipModules, m)
}
sort.Strings(merged.Action.SkipModules)
}
return merged
}
/*
*
检测rule
@@ -129,94 +196,45 @@ func (waf *WafEngine) CheckRule(r *http.Request, weblogbean *innerbean.WebLog, f
Content: "",
}
logTitle := ""
allowTitle := ""
skipSet := make(map[string]bool)
//规则判断 (局部)
localResult := ruleMatchResult{}
if hostTarget.Rule != nil {
localResult := matchRules(hostTarget.Rule, weblogbean, "")
if localResult.Matched {
switch localResult.Action.Action {
case utils.RuleActionDeny:
weblogbean.RISK_LEVEL = 1
result.IsBlock = true
result.Title = localResult.Title
result.Content = "您的访问被阻止触发规则"
return result
case utils.RuleActionAllow:
allowTitle = localResult.Title
for _, m := range localResult.Action.SkipModules {
skipSet[m] = true
}
//放行且跳过后续所有检测,全局规则也不用再看了
if localResult.Action.SkipAll() {
result.JumpGuardResult = true
result.IsRuleAllow = true
result.SkipModules = []string{utils.RuleSkipAll}
result.Title = allowTitle
return result
}
case utils.RuleActionLog:
weblogbean.RISK_LEVEL = 1
logTitle = localResult.Title
}
}
localResult = matchRules(hostTarget.Rule, weblogbean, "")
}
//规则判断 (全局网站)
//两侧都要跑完再仲裁:局部先命中就 return 的话,全局那条优先级更高的规则永远没机会生效
globalResult := ruleMatchResult{}
globalHost := waf.rt().HostTarget[global.GWAF_GLOBAL_HOST_NAME]
if globalHost != nil && globalHost.Host.GUARD_STATUS == 1 && globalHost.Rule != nil {
globalResult := matchRules(globalHost.Rule, weblogbean, "【全局】")
if globalResult.Matched {
switch globalResult.Action.Action {
case utils.RuleActionDeny:
//全局拦截优先级最高,可以覆盖局部的放行/仅记录
weblogbean.RISK_LEVEL = 1
result.IsBlock = true
result.Title = globalResult.Title
result.Content = "您的访问被阻止触发规则"
return result
case utils.RuleActionAllow:
if allowTitle == "" {
allowTitle = globalResult.Title
} else {
allowTitle = allowTitle + globalResult.Title
}
for _, m := range globalResult.Action.SkipModules {
skipSet[m] = true
}
if globalResult.Action.SkipAll() {
result.JumpGuardResult = true
result.IsRuleAllow = true
result.SkipModules = []string{utils.RuleSkipAll}
result.Title = allowTitle
return result
}
case utils.RuleActionLog:
weblogbean.RISK_LEVEL = 1
if logTitle == "" {
logTitle = globalResult.Title
} else {
logTitle = logTitle + globalResult.Title
}
}
}
globalResult = matchRules(globalHost.Rule, weblogbean, "【全局】")
}
//放行优先于仅记录
if allowTitle != "" {
result.IsRuleAllow = true
result.Title = allowTitle
for m := range skipSet {
result.SkipModules = append(result.SkipModules, m)
}
final := arbitrate(localResult, globalResult)
if !final.Matched {
return result
}
if logTitle != "" {
switch final.Action.Action {
case utils.RuleActionDeny:
weblogbean.RISK_LEVEL = 1
result.IsBlock = true
result.Title = final.Title
result.Content = "您的访问被阻止触发规则"
case utils.RuleActionAllow:
result.IsRuleAllow = true
result.Title = final.Title
if final.Action.SkipAll() {
//放行且跳过后续所有检测,直通后端
result.JumpGuardResult = true
result.SkipModules = []string{utils.RuleSkipAll}
} else {
result.SkipModules = final.Action.SkipModules
}
case utils.RuleActionLog:
weblogbean.RISK_LEVEL = 1
result.IsLogOnly = true
result.Title = logTitle
return result
result.Title = final.Title
}
return result
}
+238
View File
@@ -0,0 +1,238 @@
package wafenginecore
import (
"SamWaf/global"
"SamWaf/innerbean"
"SamWaf/model"
"SamWaf/model/wafenginmodel"
"testing"
)
// newTestEngineWithGlobalRule 造一个只挂了"全局网站"的引擎,全局规则用 globalRule 文本
// globalRule 传空串表示全局网站没有任何规则
func newTestEngineWithGlobalRule(t *testing.T, globalRule string) *WafEngine {
t.Helper()
waf := &WafEngine{}
waf.InitRouting()
if globalRule == "" {
return waf
}
globalHost := &wafenginmodel.HostSafe{
Host: model.Hosts{GUARD_STATUS: 1, GLOBAL_HOST: 1},
Rule: buildRuleHelper(t, globalRule),
}
waf.withWriteTable(func(nt *routingTable) {
nt.HostTarget[global.GWAF_GLOBAL_HOST_NAME] = globalHost
})
return waf
}
// newTestSiteHost 造一个挂了站点规则的 HostSafe,siteRule 传空串表示站点没有规则
func newTestSiteHost(t *testing.T, siteRule string) *wafenginmodel.HostSafe {
t.Helper()
h := &wafenginmodel.HostSafe{Host: model.Hosts{GUARD_STATUS: 1}}
if siteRule != "" {
h.Rule = buildRuleHelper(t, siteRule)
}
return h
}
// TestCheckRule_CrossScopeArbitration 站点规则与全局规则按 salience 统一仲裁
//
// 覆盖 issue #907:站点白名单 salience 90 必须能压过全局拦截 salience 10;
// 同时保证老配置(两侧都是默认 salience 10)行为不变——全局拦截依旧赢。
func TestCheckRule_CrossScopeArbitration(t *testing.T) {
const testIP = "1.2.3.4"
// 站点放行规则模板:命中测试 IP
siteAllow := func(salience string, action string) string {
return `
rule Rsite001 "站点白名单" salience ` + salience + ` {
when MF.SRC_IP == "` + testIP + `"
then ` + action + `
}`
}
// 全局拦截规则模板:命中测试 IP
globalRule := func(salience string, action string) string {
return `
rule Rglobal001 "全局屏蔽" salience ` + salience + ` {
when MF.SRC_IP == "` + testIP + `"
then ` + action + `
}`
}
cases := []struct {
name string
siteRule string
globalRule string
wantBlock bool
wantAllow bool
wantLogOnly bool
wantJump bool
}{
{
name: "#907场景_站点放行salience90压过全局拦截salience10",
siteRule: siteAllow("90", "RF.Allow();"),
globalRule: globalRule("10", "RF.Deny();"),
wantAllow: true,
},
{
name: "同优先级_全局拦截仍旧赢_兼容老配置",
siteRule: siteAllow("10", "RF.Allow();"),
globalRule: globalRule("10", "RF.Deny();"),
wantBlock: true,
},
{
name: "全局放行salience90压过站点拦截salience10",
siteRule: siteAllow("10", "RF.Deny();"),
globalRule: globalRule("90", "RF.Allow();"),
wantAllow: true,
},
{
name: "站点拦截salience90压过全局放行salience10",
siteRule: siteAllow("90", "RF.Deny();"),
globalRule: globalRule("10", "RF.Allow();"),
wantBlock: true,
},
{
name: "站点AllowAll压过全局拦截_并跳过后续全部检测",
siteRule: siteAllow("90", "RF.AllowAll();"),
globalRule: globalRule("10", "RF.Deny();"),
wantAllow: true,
wantJump: true,
},
{
name: "站点仅记录salience90压过全局拦截_不拦截",
siteRule: siteAllow("90", "RF.Log();"),
globalRule: globalRule("10", "RF.Deny();"),
wantLogOnly: true,
},
{
name: "站点无规则_全局拦截生效",
siteRule: "",
globalRule: globalRule("10", "RF.Deny();"),
wantBlock: true,
},
{
name: "全局无规则_站点放行生效",
siteRule: siteAllow("10", "RF.Allow();"),
wantAllow: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
waf := newTestEngineWithGlobalRule(t, tc.globalRule)
hostTarget := newTestSiteHost(t, tc.siteRule)
weblog := &innerbean.WebLog{SRC_IP: testIP, URL: "/download"}
result := waf.CheckRule(nil, weblog, nil, hostTarget, nil)
if result.IsBlock != tc.wantBlock {
t.Errorf("IsBlock 期望 %v 实际 %v (Title=%s)", tc.wantBlock, result.IsBlock, result.Title)
}
if result.IsRuleAllow != tc.wantAllow {
t.Errorf("IsRuleAllow 期望 %v 实际 %v (Title=%s)", tc.wantAllow, result.IsRuleAllow, result.Title)
}
if result.IsLogOnly != tc.wantLogOnly {
t.Errorf("IsLogOnly 期望 %v 实际 %v (Title=%s)", tc.wantLogOnly, result.IsLogOnly, result.Title)
}
if result.JumpGuardResult != tc.wantJump {
t.Errorf("JumpGuardResult 期望 %v 实际 %v", tc.wantJump, result.JumpGuardResult)
}
})
}
}
// TestCheckRule_NoMatch 两侧都不命中时不拦不放
func TestCheckRule_NoMatch(t *testing.T) {
waf := newTestEngineWithGlobalRule(t, `
rule Rglobal001 "全局屏蔽" salience 10 {
when MF.SRC_IP == "9.9.9.9"
then RF.Deny();
}`)
hostTarget := newTestSiteHost(t, `
rule Rsite001 "站点白名单" salience 90 {
when MF.SRC_IP == "1.2.3.4"
then RF.Allow();
}`)
result := waf.CheckRule(nil, &innerbean.WebLog{SRC_IP: "8.8.8.8"}, nil, hostTarget, nil)
if result.IsBlock || result.IsRuleAllow || result.IsLogOnly {
t.Fatalf("两侧都不命中时应无任何动作, 实际 %+v", result)
}
}
// TestCheckRule_AllowSkipModulesUnion 同优先级两侧都放行时,跳过模块取并集
func TestCheckRule_AllowSkipModulesUnion(t *testing.T) {
const testIP = "1.2.3.4"
waf := newTestEngineWithGlobalRule(t, `
rule Rglobal001 "全局放行" salience 50 {
when MF.SRC_IP == "`+testIP+`"
then RF.Allow("AI", "CC");
}`)
hostTarget := newTestSiteHost(t, `
rule Rsite001 "站点放行" salience 50 {
when MF.SRC_IP == "`+testIP+`"
then RF.Allow("CC", "XSS");
}`)
result := waf.CheckRule(nil, &innerbean.WebLog{SRC_IP: testIP}, nil, hostTarget, nil)
if !result.IsRuleAllow {
t.Fatalf("应放行, 实际 %+v", result)
}
got := map[string]bool{}
for _, m := range result.SkipModules {
got[m] = true
}
for _, want := range []string{"AI", "CC", "XSS"} {
if !got[want] {
t.Errorf("跳过模块应包含 %s, 实际 %v", want, result.SkipModules)
}
}
if len(result.SkipModules) != 3 {
t.Errorf("跳过模块应去重为3个, 实际 %v", result.SkipModules)
}
}
// TestShouldRecordWebLog abnormal 模式下"命中了但没拦"的请求必须留痕
func TestShouldRecordWebLog(t *testing.T) {
origin := global.GWAF_RUNTIME_RECORD_LOG_TYPE
defer func() { global.GWAF_RUNTIME_RECORD_LOG_TYPE = origin }()
cases := []struct {
name string
logType string
weblog *innerbean.WebLog
excludeURL string
want bool
}{
{"all模式_普通放行也记", "all",
&innerbean.WebLog{ACTION: "放行", URL: "/a"}, "", true},
{"all模式_排除前缀命中不记", "all",
&innerbean.WebLog{ACTION: "放行", URL: "/static/x.js"}, "/static", false},
{"abnormal模式_普通放行不记", "abnormal",
&innerbean.WebLog{ACTION: "放行", URL: "/a"}, "", false},
{"abnormal模式_拦截要记", "abnormal",
&innerbean.WebLog{ACTION: "阻止", URL: "/a"}, "", true},
{"abnormal模式_自定义规则放行要留痕", "abnormal",
&innerbean.WebLog{ACTION: "放行", URL: "/a", RULE: "自定义规则放行:站点白名单"}, "", true},
{"abnormal模式_自定义规则仅记录要留痕", "abnormal",
&innerbean.WebLog{ACTION: "放行", URL: "/a", RULE: "自定义规则记录:观察"}, "", true},
{"abnormal模式_站点仅记录模式要留痕", "abnormal",
&innerbean.WebLog{ACTION: "放行", URL: "/a", LogOnlyMode: 1}, "", true},
{"abnormal模式_命中规则但URL被排除仍不记", "abnormal",
&innerbean.WebLog{ACTION: "放行", URL: "/static/x.js", RULE: "自定义规则放行:x"}, "/static", false},
{"排除列表里的空行不该吞掉全部日志", "all",
&innerbean.WebLog{ACTION: "放行", URL: "/a"}, "\n \n", true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
global.GWAF_RUNTIME_RECORD_LOG_TYPE = tc.logType
if got := shouldRecordWebLog(tc.weblog, tc.excludeURL); got != tc.want {
t.Errorf("期望 %v 实际 %v", tc.want, got)
}
})
}
}
+2 -19
View File
@@ -613,25 +613,8 @@ func (waf *WafEngine) logStaticFileAccess(path, remoteAddr string, fileSize int6
weblog.RISK_LEVEL = 0 // 无风险
// 按照全局日志记录策略决定是否记录
if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "all" {
if hostsafe.Host.EXCLUDE_URL_LOG == "" {
global.GQEQUE_LOG_DB.Enqueue(weblog)
} else {
lines := strings.Split(hostsafe.Host.EXCLUDE_URL_LOG, "\n")
isRecordLog := true
// 检查每一行
for _, line := range lines {
if strings.HasPrefix(weblog.URL, line) {
isRecordLog = false
}
}
if isRecordLog {
global.GQEQUE_LOG_DB.Enqueue(weblog)
}
}
} else if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "abnormal" && weblog.ACTION != "放行" {
// 对于静态文件成功访问,ACTION是"放行",所以在abnormal模式下不会记录
// 这里保持逻辑一致性,虽然实际上不会执行
// abnormal 模式下静态文件成功访问(ACTION=放行)通常不记录,但若命中过自定义规则(RULE 非空)仍要留痕
if shouldRecordWebLog(weblog, hostsafe.Host.EXCLUDE_URL_LOG) {
global.GQEQUE_LOG_DB.Enqueue(weblog)
}
}
+2 -6
View File
@@ -282,12 +282,8 @@ func (waf *WafEngine) enqueueTamperLog(weblog *innerbean.WebLog, excludeURLLog s
weblog.TASK_FLAG = 1
// 防篡改替换属于异常事件,all / abnormal 两种模式都记录
if excludeURLLog != "" {
for _, line := range strings.Split(excludeURLLog, "\n") {
if line != "" && strings.HasPrefix(weblog.URL, line) {
return
}
}
if isURLLogExcluded(weblog.URL, excludeURLLog) {
return
}
global.GQEQUE_LOG_DB.Enqueue(weblog)
}
+4 -67
View File
@@ -1170,23 +1170,7 @@ func (waf *WafEngine) modifyResponse() func(*http.Response) error {
weblogfrist.TASK_FLAG = 1
// 记录日志
if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "all" {
if waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG == "" {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
} else {
lines := strings.Split(waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG, "\n")
isRecordLog := true
// 检查每一行
for _, line := range lines {
if strings.HasPrefix(weblogfrist.URL, line) {
isRecordLog = false
}
}
if isRecordLog {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
} else if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "abnormal" && weblogfrist.ACTION != "放行" {
if shouldRecordWebLog(weblogfrist, waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG) {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
@@ -1257,22 +1241,7 @@ func (waf *WafEngine) modifyResponse() func(*http.Response) error {
weblogfrist.BackendCheckCost = time.Now().UnixNano()/1e6 - backendCheckStart //响应数据处理时间
// 记录流式访问日志
if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "all" {
if waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG == "" {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
} else {
lines := strings.Split(waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG, "\n")
isRecordLog := true
for _, line := range lines {
if strings.HasPrefix(weblogfrist.URL, line) {
isRecordLog = false
}
}
if isRecordLog {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
} else if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "abnormal" && weblogfrist.ACTION != "放行" {
if shouldRecordWebLog(weblogfrist, waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG) {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
@@ -1534,23 +1503,7 @@ func (waf *WafEngine) modifyResponse() func(*http.Response) error {
weblogfrist.BackendCheckCost = time.Now().UnixNano()/1e6 - backendCheckStart
// 记录日志 - 根据配置决定是否记录
if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "all" {
if waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG == "" {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
} else {
lines := strings.Split(waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG, "\n")
isRecordLog := true
for _, line := range lines {
if strings.HasPrefix(weblogfrist.URL, line) {
isRecordLog = false
}
}
if isRecordLog {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
} else if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "abnormal" && weblogfrist.ACTION != "放行" {
// 自定义错误页也属于"异常"情况,需要记录
if shouldRecordWebLog(weblogfrist, waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG) {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
@@ -1566,23 +1519,7 @@ func (waf *WafEngine) modifyResponse() func(*http.Response) error {
weblogfrist.STATUS_CODE = resp.StatusCode
weblogfrist.TASK_FLAG = 1
weblogfrist.BackendCheckCost = time.Now().UnixNano()/1e6 - backendCheckStart //响应数据处理时间
if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "all" {
if waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG == "" {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
} else {
lines := strings.Split(waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG, "\n")
isRecordLog := true
// 检查每一行
for _, line := range lines {
if strings.HasPrefix(weblogfrist.URL, line) {
isRecordLog = false
}
}
if isRecordLog {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
} else if global.GWAF_RUNTIME_RECORD_LOG_TYPE == "abnormal" && weblogfrist.ACTION != "放行" {
if shouldRecordWebLog(weblogfrist, waf.rt().HostTarget[host].Host.EXCLUDE_URL_LOG) {
global.GQEQUE_LOG_DB.Enqueue(weblogfrist)
}
}
+46
View File
@@ -0,0 +1,46 @@
package wafenginecore
import (
"SamWaf/global"
"SamWaf/innerbean"
"strings"
)
// isURLLogExcluded 判断该 URL 是否命中站点配置的"排除记录"前缀列表
// excludeURLLog 为换行分隔的前缀列表;空行直接忽略(否则 HasPrefix(url,"") 恒为 true,
// 配置里多敲一个回车就会把整站日志全部吞掉)
func isURLLogExcluded(url string, excludeURLLog string) bool {
if excludeURLLog == "" {
return false
}
for _, line := range strings.Split(excludeURLLog, "\n") {
line = strings.TrimSpace(line)
if line != "" && strings.HasPrefix(url, line) {
return true
}
}
return false
}
// shouldRecordWebLog 判断一条走完正常流程的访问日志该不该落库
//
// 记录类型 all:除排除前缀外全记。
// 记录类型 abnormal:除了非放行的请求,"命中了规则但没拦"的请求也必须留痕 ——
// 自定义规则放行/仅记录、站点仅记录模式(LogOnlyMode)、AI 观察命中,这些请求最终
// ACTION 都会被 modifyResponse 覆写成"放行",若只看 ACTION 就会被整条丢弃,
// 导致白名单被谁用了、仅记录模式抓到了什么完全无法审计。
func shouldRecordWebLog(weblog *innerbean.WebLog, excludeURLLog string) bool {
if weblog == nil {
return false
}
if isURLLogExcluded(weblog.URL, excludeURLLog) {
return false
}
switch global.GWAF_RUNTIME_RECORD_LOG_TYPE {
case "all":
return true
case "abnormal":
return weblog.ACTION != "放行" || weblog.RULE != "" || weblog.LogOnlyMode == 1
}
return false
}