Merge pull request #780 from samwafgo/fix_owasp_update

fix:owasp set update
This commit is contained in:
samwafgo
2026-04-24 10:12:14 +08:00
committed by GitHub
8 changed files with 613 additions and 128 deletions
@@ -1,5 +1,5 @@
# =============================================================================
# SamWaf Layer 1 — After-CRS 钩子示例
# SamWaf Layer 2 — After-CRS 钩子示例
# 文件位置:data/owasp/samwaf/after/
# =============================================================================
#
@@ -13,13 +13,13 @@
# 【加载顺序(完整)】
# coraza.conf
# → coreruleset/crs-setup.conf
# → overrides/00-samwaf-base.conf (Layer 1 数值变量)
# → overrides/05-user-vars.conf (Layer 2 用户变量)
# → samwaf/before/*.conf (Layer 1 前置,见 before/ 目录)
# → coreruleset/rules/*.conf (CRS 官方规则)
# → samwaf/00-samwaf-base.conf (Layer 2 数值变量)
# → samwaf/before/*.conf (Layer 2 前置,见 before/ 目录)
# → overrides/05-user-vars.conf (Layer 3 用户变量)
# → coreruleset/rules/*.conf (CRS 官方规则Layer 1)
# → samwaf/after/*.conf ← 本目录,此处
# → overrides/10-disabled-rules.conf (用户禁用)
# → overrides/20-custom-rules.conf (用户改写)
# → overrides/10-disabled-rules.conf (Layer 3 用户禁用)
# → overrides/20-custom-rules.conf (Layer 3 用户改写)
#
# 【注意事项】
# 1. 本目录由 SamWaf 开发团队管理,随 SamWaf 版本发布更新(非用户层)。
@@ -29,7 +29,7 @@
#
# 【写法示例】
#
# ID 使用 991100-991199 段(Layer 1 after-hooks 专用)
# ID 使用 991100-991199 段(Layer 2 after-hooks 专用)
#
# -- 示例 1SamWaf 补充检测——检测 Log4Shell(额外保险层)--
#
@@ -43,7 +43,7 @@
# auditlog,\
# msg:'SamWaf: Log4Shell attempt',\
# severity:'CRITICAL',\
# tag:'SamWaf_layer1',\
# tag:'SamWaf_layer2',\
# tag:'attack-injection'"
#
#
@@ -56,6 +56,6 @@
# log,\
# noauditlog,\
# msg:'SamWaf: elevated anomaly score %{tx.anomaly_score}',\
# tag:'SamWaf_layer1'"
# tag:'SamWaf_layer2'"
#
# =============================================================================
@@ -1,5 +1,5 @@
# =============================================================================
# SamWaf Layer 1 — Before-CRS 钩子示例
# SamWaf Layer 2 — Before-CRS 钩子示例
# 文件位置:data/owasp/samwaf/before/
# =============================================================================
#
@@ -13,13 +13,12 @@
# 【加载顺序(完整)】
# coraza.conf
# → coreruleset/crs-setup.conf
# → overrides/00-samwaf-base.conf (Layer 1 数值变量,由 samwaf_base.json 驱动)
# → overrides/05-user-vars.conf (Layer 2 用户变量,优先级最高)
# → samwaf/before/*.conf ← 本目录,此处
# → coreruleset/rules/*.conf (CRS 官方规则)
# → samwaf/after/*.conf (Layer 1 后置,见 after/ 目录)
# → overrides/10-disabled-rules.conf (用户禁用)
# → overrides/20-custom-rules.conf (用户改写)
# → samwaf/before/*.conf ← 本目录,此处(00-samwaf-base.conf 数值变量按字母序最先加载)
# → overrides/05-user-vars.conf (Layer 3 用户变量,优先级最高)
# → coreruleset/rules/*.conf (CRS 官方规则,Layer 1)
# → samwaf/after/*.conf (Layer 2 后置,见 after/ 目录)
# → overrides/10-disabled-rules.conf (Layer 3 用户禁用)
# → overrides/20-custom-rules.conf (Layer 3 用户改写)
#
# 【注意事项】
# 1. 本目录由 SamWaf 开发团队管理,随 SamWaf 版本发布更新(非用户层)。
@@ -1,5 +1,5 @@
# =============================================================================
# SamWaf Layer 1 — CRS 字符串/列表型变量初始化
# SamWaf Layer 2 — CRS 字符串/列表型变量初始化
# 文件:data/owasp/samwaf/before/01-samwaf-init-vars.conf
# 由 SamWaf 随版本发布,本文件会在 SamWaf 升级时被覆盖。
# 用户如需覆盖,请通过 后台 → OWASP 规则管理 → 全局调参 → 自定义 CRS 变量 操作,
@@ -9,14 +9,14 @@
# 本文件采用"宽松默认值"策略,覆盖大多数现代 Web / REST API 场景。
# 若业务需要更严格的限制,可通过后台覆盖对应变量,无需修改本文件。
#
# 加载顺序:
# 00-samwaf-base.conf (990001-990009) ← 数值型变量(paranoia/threshold
# 本文件 (991001-991099) ← 字符串/列表型变量(methods/content-type 等)
# overrides/05-user-vars.conf ← 用户覆盖层(最终胜出)
# 加载顺序samwaf/before/ 目录按字母序统一加载)
# samwaf/before/00-samwaf-base.conf (990001-990009) ← 数值型变量(paranoia/threshold
# 本文件 (991001-991099) ← 字符串/列表型变量(methods/content-type 等)
# overrides/05-user-vars.conf ← 用户覆盖层(最终胜出Layer 3
# coreruleset/rules/REQUEST-901-INITIALIZATION.conf rule:901160
# → 检测"变量未设置"时才写默认值,本文件已提前 setvar,901160 会跳过。
#
# ID 范围:991001991099SamWaf Layer 1 before-hooksCRS 安全区)
# ID 范围:991001991099SamWaf Layer 2 before-hooksCRS 安全区)
# =============================================================================
# 允许的 HTTP 方法(CRS rule 911100 依赖此变量)
@@ -27,7 +27,7 @@ SecAction \
pass,\
nolog,\
t:none,\
tag:'SamWaf_layer1',\
tag:'SamWaf_layer2',\
setvar:'tx.allowed_methods=GET HEAD POST OPTIONS PATCH PUT DELETE'"
# 允许的请求体 Content-TypeCRS rule 920420 依赖此变量)
@@ -38,7 +38,7 @@ SecAction \
pass,\
nolog,\
t:none,\
tag:'SamWaf_layer1',\
tag:'SamWaf_layer2',\
setvar:'tx.allowed_request_content_type=|application/x-www-form-urlencoded| |multipart/form-data| |multipart/related| |text/xml| |application/xml| |application/soap+xml| |application/json| |application/cloudevents+json| |application/cloudevents-batch+json| |application/ld+json| |application/x-json| |text/plain| |application/octet-stream|'"
# 允许的 Content-Type 字符集(CRS rule 920480 依赖此变量)
@@ -49,7 +49,7 @@ SecAction \
pass,\
nolog,\
t:none,\
tag:'SamWaf_layer1',\
tag:'SamWaf_layer2',\
setvar:'tx.allowed_request_content_type_charset=utf-8|utf-16|iso-8859-1|iso-8859-15|windows-1252|us-ascii|gbk|gb2312|big5'"
# 允许的 HTTP 版本(CRS rule 920430 依赖此变量)
@@ -59,7 +59,7 @@ SecAction \
pass,\
nolog,\
t:none,\
tag:'SamWaf_layer1',\
tag:'SamWaf_layer2',\
setvar:'tx.allowed_http_versions=HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/2.0 HTTP/3 HTTP/3.0'"
# 单请求最大参数数量(CRS rule 920271 依赖此变量)
@@ -70,7 +70,7 @@ SecAction \
pass,\
nolog,\
t:none,\
tag:'SamWaf_layer1',\
tag:'SamWaf_layer2',\
setvar:'tx.max_num_args=1000'"
# 单参数值最大长度(字节,CRS rule 920271 依赖此变量)
@@ -81,7 +81,7 @@ SecAction \
pass,\
nolog,\
t:none,\
tag:'SamWaf_layer1',\
tag:'SamWaf_layer2',\
setvar:'tx.arg_length=2000'"
# 参数总长度上限(字节,CRS rule 920271 依赖此变量)
@@ -92,5 +92,5 @@ SecAction \
pass,\
nolog,\
t:none,\
tag:'SamWaf_layer1',\
tag:'SamWaf_layer2',\
setvar:'tx.total_arg_length=1048576'"
+33 -34
View File
@@ -31,7 +31,7 @@ type OwaspManager struct {
func NewOwaspManager(currentDir string) *OwaspManager {
m := &OwaspManager{dir: currentDir}
m.active.Store(true)
m.overrides = NewOverrideStore(m.OverridesDir())
m.overrides = NewOverrideStore(m.OverridesDir(), m.samwafDir())
// 首次启动:若 overrides 目录不存在,生成默认 tuning 与 registry
if err := m.overrides.EnsureDirAndDefaults(); err != nil {
zlog.Error("EnsureDirAndDefaults overrides failed", map[string]interface{}{"error": err.Error()})
@@ -52,7 +52,7 @@ func (m *OwaspManager) Overrides() *OverrideStore {
return m.overrides
}
// ApplyBaseConfig 写入 Layer 1 基线配置(samwaf_base.json + 00-samwaf-base.conf)并触发热重载。
// ApplyBaseConfig 写入 Layer 2 基线配置(samwaf/samwaf_base.json + samwaf/00-samwaf-base.conf)并触发热重载。
func (m *OwaspManager) ApplyBaseConfig(cfg BaseConfig) error {
if m.overrides == nil {
return fmt.Errorf("override store not initialized")
@@ -151,15 +151,20 @@ func (m *OwaspManager) OwaspRoot() string {
return filepath.Join(m.dir, "data", "owasp")
}
// OverridesDir 返回 overrides/ 目录(用户改动层)。
// OverridesDir 返回 overrides/ 目录(Layer 3 用户自定义层)。
func (m *OwaspManager) OverridesDir() string {
return filepath.Join(m.OwaspRoot(), "overrides")
}
// samwafDir 返回 samwaf/ 根目录(Layer 2 SamWaf 官方层)。
func (m *OwaspManager) samwafDir() string {
return filepath.Join(m.OwaspRoot(), "samwaf")
}
// samwafHooksDir 返回 samwaf 钩子子目录(before 或 after)的路径。
// 仅在目录存在时才有效;不存在时调用方跳过即可,不视为错误。
func (m *OwaspManager) samwafHooksDir(sub string) string {
return filepath.Join(m.OwaspRoot(), "samwaf", sub)
return filepath.Join(m.samwafDir(), sub)
}
// loadDirConfs 将指定目录下所有 .conf 文件(按字典序)依次追加到 cfg。
@@ -186,27 +191,28 @@ func loadDirConfs(cfg coraza.WAFConfig, dir string) coraza.WAFConfig {
//
// 完整加载顺序(优先级从低到高,后加载者覆盖同名变量):
//
// Layer 1SamWaf 出厂默认,全部先于 Layer 2 加载):
// Layer 2 前置SamWaf 官方,全部先于 Layer 3 加载):
// 1. data/owasp/coraza.conf
// 2. data/owasp/coreruleset/crs-setup.conf
// 3. data/owasp/overrides/00-samwaf-base.conf ← 数值变量(paranoia/threshold990001-990009
// 4. data/owasp/samwaf/before/*.conf ← 字符串变量(allowed_methods 等991001-991099
// 3. data/owasp/samwaf/before/*.conf ← 全部 SamWaf 变量(字母序,00-samwaf-base.conf 先于 01-samwaf-init-vars.conf
// 含:00-samwaf-base.conf(数值变量 paranoia/threshold990001-990009
// 01-samwaf-init-vars.conf(字符串变量 allowed_methods 等,991001-991099
//
// Layer 2(用户设置,最后加载 → 优先级最高):
// 5. data/owasp/overrides/05-user-vars.conf ← 用户所有覆盖(950001+ / 990100+
// Layer 3 前置(用户设置,最后加载 → 优先级最高):
// 4. data/owasp/overrides/05-user-vars.conf ← 用户所有覆盖(950001+ / 990100+
//
// ↑ 步骤 3-5 必须在 CRS rules 之前:CRS rule 901160 只在变量未设置时才写默认值。
// ↑ Layer 2(步骤 5)加载最晚 → 同名变量用户值胜出,正确覆盖 Layer 1
// ↑ 步骤 3-4 必须在 CRS rules 之前:CRS rule 901160 只在变量未设置时才写默认值。
// ↑ Layer 3(步骤 4)加载最晚 → 同名变量用户值胜出,正确覆盖 Layer 2
//
// Layer 0CRS 规则集):
// 6. data/owasp/coreruleset/rules/*.conf
// Layer 1CRS 规则集):
// 5. data/owasp/coreruleset/rules/*.conf
//
// Layer 1 后置(CRS 执行后补充):
// 7. data/owasp/samwaf/after/*.conf ← 补充检测(991100-991199
// Layer 2 后置(SamWaf 官方补充,CRS 执行后):
// 6. data/owasp/samwaf/after/*.conf ← 补充检测(991100-991199
//
// Layer 2 后置(用户禁用 / 改写,须在 CRS 之后):
// 8. data/owasp/overrides/10-disabled-rules.conf
// 9. data/owasp/overrides/20-custom-rules.conf
// Layer 3 后置(用户禁用 / 改写,须在 CRS 之后):
// 7. data/owasp/overrides/10-disabled-rules.conf
// 8. data/owasp/overrides/20-custom-rules.conf
//
// 构建失败时旧实例保留,返回错误由调用方决定如何处理。
func (m *OwaspManager) Reload() error {
@@ -229,36 +235,29 @@ func (m *OwaspManager) Reload() error {
zlog.Error("crs-setup.conf missing, skipping", map[string]interface{}{"path": setupConf, "err": err.Error()})
}
// 步骤 3Layer 1a — 00-samwaf-base.conf(数值型变量:paranoia / thresholdID 990001-990009
samwafBase := filepath.Join(m.OverridesDir(), OverrideSamWafBaseFile)
if _, err := os.Stat(samwafBase); err == nil {
cfg = cfg.WithDirectivesFromFile(samwafBase)
}
// 步骤 4Layer 1b — samwaf/before/*.conf(字符串/列表型变量:allowed_methods 等,ID 991001-991099
// 必须在 Layer 205-user-vars.conf)之前,确保用户覆盖优先级更高。
// 步骤 3Layer 2 before — samwaf/before/*.conf(按字母序,00-samwaf-base.conf 先于 01-samwaf-init-vars.conf
// 包含数值变量(paranoia/threshold)和字符串/列表变量(allowed_methods 等)。
cfg = loadDirConfs(cfg, m.samwafHooksDir("before"))
// 步骤 5Layer 2 — 05-user-vars.conf(用户覆盖层,最后加载 → 同名变量用户值胜出)
// 同时覆盖 Layer 1a 的数值变量和 Layer 1b 的字符串变量。
// 步骤 4Layer 3overrides/05-user-vars.conf(用户覆盖层,最后加载 → 同名变量用户值胜出)
// 覆盖 Layer 2 before 中的数值变量和字符串变量。
tuningConf := filepath.Join(m.OverridesDir(), OverrideTuningFile)
if _, err := os.Stat(tuningConf); err == nil {
cfg = cfg.WithDirectivesFromFile(tuningConf)
}
// 步骤 6Layer 0 — CRS 官方规则集(此时所有 tx.* 变量均已就绪)
// 步骤 5Layer 1 — CRS 官方规则集(此时所有 tx.* 变量均已就绪)
rulesGlob := filepath.Join(root, "coreruleset", "rules", "*.conf")
cfg = cfg.WithDirectivesFromFile(rulesGlob)
// 步骤 7Layer 1 后置钩子(samwaf/after/*.confID 991100-991199)—— CRS 规则执行后
// 步骤 6Layer 2 后置钩子(samwaf/after/*.confID 991100-991199)—— CRS 规则执行后
// 用途:补充检测规则、基于评分的自定义动作等。
cfg = loadDirConfs(cfg, m.samwafHooksDir("after"))
// 步骤 8-9Layer 2 后置文件(用户禁用 / 改写规则,须在 CRS 规则之后才能 RemoveById
// 跳过已在步骤 3-5 提前加载的 pre-rules 文件。
// 步骤 7-8Layer 3 后置文件(用户禁用 / 改写规则,须在 CRS 规则之后才能 RemoveById
// 跳过已在步骤 4 提前加载的 pre-rules 文件。
preRules := map[string]bool{
OverrideSamWafBaseFile: true,
OverrideTuningFile: true,
OverrideTuningFile: true,
}
if overrides, err := listOverrideConfs(m.OverridesDir()); err == nil && len(overrides) > 0 {
for _, p := range overrides {
+88 -51
View File
@@ -96,23 +96,29 @@ func (s *OverrideStore) LoadAuditLog() ([]AuditLogEntry, error) {
//
// 三层策略叠加(加载顺序):
//
// Layer 0: coreruleset/rules/*.conf ← CRS 上游,在线升级时覆盖
// Layer 1: overrides/00-samwaf-base.conf ← SamWaf 产品基线,由 samwaf_base.json 驱动
// Layer 2: overrides/05-user-vars.conf ← 用户 tx.* 变量,优先于 Layer 1(永不被升级覆盖)
// overrides/10-disabled-rules.conf
// overrides/20-custom-rules.conf
// Layer 1 (基线): coreruleset/rules/*.conf ← CRS 上游,在线升级时整体替换
// Layer 2 (SamWaf 官方): samwaf/00-samwaf-base.conf ← SamWaf 数值变量,由 samwaf_base.json 驱动
// samwaf/before/*.conf ← SamWaf 字符串/列表变量
// samwaf/after/*.conf ← SamWaf 补充检测规则
// Layer 3 (用户): overrides/05-user-vars.conf ← 用户 tx.* 变量,优先于 Layer 2(永不被升级覆盖)
// overrides/10-disabled-rules.conf
// overrides/20-custom-rules.conf
//
// 重要:Layer 1 和 Layer 2 均须在 rules/*.conf 之前加载,原因:
// 重要:Layer 2 和 Layer 3 均须在 rules/*.conf 之前加载,原因:
// CRS REQUEST-901-INITIALIZATION.conf 的 rule 901160 检查"变量未设置时才写默认值",
// 必须在它之前 setvar,否则 tx.allowed_methods 等设置永远无效。
const (
OverrideSamWafBaseFile = "00-samwaf-base.conf" // Layer 1SamWaf 产品基线(由 samwaf_base.json 生成
OverrideBaseConfigFile = "samwaf_base.json" // Layer 1 持久化:存储 SamWaf 基线参数
OverrideTuningFile = "05-user-vars.conf" // Layer 2:用户 tx.* 变量覆盖层(永不被升级覆盖
OverrideDisabledFile = "10-disabled-rules.conf" // Layer 2:用户按 ID 禁用的规则
OverrideCustomFile = "20-custom-rules.conf" // Layer 2:用户覆盖/自定义的规则
OverrideRegistryFile = "override_registry.json" // 元数据:所有用户改动的单一事实来源
OverrideRegistryShema = 1
// Layer 2 — SamWaf 官方层(文件在 samwaf/ 下,随 SamWaf 版本更新
SamWafBaseConfFile = "00-samwaf-base.conf" // 由 samwaf_base.json 生成的数值变量 conf
SamWafBaseJSONFile = "samwaf_base.json" // Layer 2 配置持久化源(paranoia/threshold 等
// Layer 3 — 用户自定义层(文件在 overrides/ 下,永不被升级覆盖)
OverrideTuningFile = "05-user-vars.conf" // 用户 tx.* 变量覆盖层
OverrideDisabledFile = "10-disabled-rules.conf" // 用户按 ID 禁用的规则
OverrideCustomFile = "20-custom-rules.conf" // 用户覆盖/自定义的规则
OverrideRegistryFile = "override_registry.json" // 元数据:所有用户改动的单一事实来源
OverrideRegistryShema = 1
)
// OverrideAction 用户对某条规则的动作。
@@ -196,12 +202,12 @@ func (b BaseConfig) toTuningConfig() TuningConfig {
}
}
// GetBaseConfig 读取 Layer 1 基线配置(来自 samwaf_base.json)。
// GetBaseConfig 读取 Layer 2 基线配置(来自 samwaf/samwaf_base.json)。
// 文件不存在时返回 DefaultBaseConfig()(不视为错误)。
func (s *OverrideStore) GetBaseConfig() (BaseConfig, error) {
s.mu.Lock()
defer s.mu.Unlock()
return loadBaseConfigLocked(filepath.Join(s.dir, OverrideBaseConfigFile))
return loadBaseConfigLocked(filepath.Join(s.samwafDir, SamWafBaseJSONFile))
}
func loadBaseConfigLocked(path string) (BaseConfig, error) {
@@ -219,7 +225,7 @@ func loadBaseConfigLocked(path string) (BaseConfig, error) {
return cfg, nil
}
// SetBaseConfig 持久化 Layer 1 配置并重新生成 00-samwaf-base.conf。
// SetBaseConfig 持久化 Layer 2 配置并重新生成 samwaf/before/00-samwaf-base.conf。
// 调用方(ApplyBaseConfig)负责触发 Reload()。
func (s *OverrideStore) SetBaseConfig(cfg BaseConfig) error {
s.mu.Lock()
@@ -229,11 +235,11 @@ func (s *OverrideStore) SetBaseConfig(cfg BaseConfig) error {
if err != nil {
return err
}
if err := atomicWriteFile(filepath.Join(s.dir, OverrideBaseConfigFile), data); err != nil {
return fmt.Errorf("write %s: %w", OverrideBaseConfigFile, err)
if err := atomicWriteFile(filepath.Join(s.samwafDir, SamWafBaseJSONFile), data); err != nil {
return fmt.Errorf("write %s: %w", SamWafBaseJSONFile, err)
}
if err := writeSamWafBaseConfFile(filepath.Join(s.dir, OverrideSamWafBaseFile), cfg.toTuningConfig()); err != nil {
return fmt.Errorf("write %s: %w", OverrideSamWafBaseFile, err)
if err := writeSamWafBaseConfFile(filepath.Join(s.samwafDir, "before", SamWafBaseConfFile), cfg.toTuningConfig()); err != nil {
return fmt.Errorf("write %s: %w", SamWafBaseConfFile, err)
}
s.AppendAuditLog(AuditLogEntry{
Action: AuditTuning,
@@ -253,15 +259,17 @@ type OverrideRegistry struct {
DeletedFiles []string `json:"deleted_files,omitempty"`
}
// OverrideStore 封装 overrides 目录的读写,内部持锁串行以避免并发写坏文件。
// OverrideStore 封装 Layer 2samwaf/)和 Layer 3overrides/的读写,内部持锁串行以避免并发写坏文件。
type OverrideStore struct {
dir string // overrides/ 目录绝对路径
mu sync.Mutex // 写锁
dir string // overrides/ 目录绝对路径Layer 3 用户层)
samwafDir string // samwaf/ 目录绝对路径(Layer 2 SamWaf 官方层)
mu sync.Mutex // 写锁
}
// NewOverrideStore 创建一个 OverrideStore,不会立即触达磁盘。
func NewOverrideStore(dir string) *OverrideStore {
return &OverrideStore{dir: dir}
// overridesDir 是 Layer 3 用户层目录(overrides/),samwafDir 是 Layer 2 SamWaf 官方层目录(samwaf/)。
func NewOverrideStore(overridesDir, samwafDir string) *OverrideStore {
return &OverrideStore{dir: overridesDir, samwafDir: samwafDir}
}
// Dir 返回 overrides 目录路径。
@@ -269,12 +277,14 @@ func (s *OverrideStore) Dir() string {
return s.dir
}
// EnsureDirAndDefaults 确保 overrides 目录存在并写入各层默认文件。
// EnsureDirAndDefaults 确保各层目录存在并写入各层默认文件。
//
// 行为:
// - 00-samwaf-base.confLayer 1):每次调用均重写,确保 SamWaf 产品默认值随二进制更新。
// - 05-user-vars.confLayer 2):仅首次(registry 缺失时)写入;后续由用户操作写入。
// - 兼容迁移:若发现旧版 00-tuning.conf,自动重命名为 05-user-vars.conf
// - samwaf/before/00-samwaf-base.confLayer 2):每次调用均重写,确保 SamWaf 产品默认值随二进制更新。
// - overrides/05-user-vars.confLayer 3):仅首次(registry 缺失时)写入;后续由用户操作写入。
// - 兼容迁移:若发现旧版 overrides/samwaf_base.json,自动移到 samwaf/samwaf_base.json。
// - 兼容迁移:若发现旧版 samwaf/00-samwaf-base.conf,删除(会在 samwaf/before/ 重新生成)。
// - 兼容迁移:若发现旧版 overrides/00-tuning.conf,自动重命名为 overrides/05-user-vars.conf。
func (s *OverrideStore) EnsureDirAndDefaults() error {
s.mu.Lock()
defer s.mu.Unlock()
@@ -282,20 +292,47 @@ func (s *OverrideStore) EnsureDirAndDefaults() error {
if err := os.MkdirAll(s.dir, 0755); err != nil {
return fmt.Errorf("mkdir overrides: %w", err)
}
if err := os.MkdirAll(s.samwafDir, 0755); err != nil {
return fmt.Errorf("mkdir samwaf: %w", err)
}
if err := os.MkdirAll(filepath.Join(s.samwafDir, "before"), 0755); err != nil {
return fmt.Errorf("mkdir samwaf/before: %w", err)
}
// Layer 1:从 samwaf_base.json 读取配置(不存在时用出厂默认值),生成 00-samwaf-base.conf
// 兼容迁移:旧版将 samwaf_base.json 放在 overrides/,新版移到 samwaf/
oldBaseJSON := filepath.Join(s.dir, SamWafBaseJSONFile)
newBaseJSON := filepath.Join(s.samwafDir, SamWafBaseJSONFile)
if _, err := os.Stat(oldBaseJSON); err == nil {
if _, err2 := os.Stat(newBaseJSON); os.IsNotExist(err2) {
_ = os.Rename(oldBaseJSON, newBaseJSON)
} else {
_ = os.Remove(oldBaseJSON)
}
}
// 旧版 overrides/00-samwaf-base.conf 不再在此目录生成,清理占位。
oldOverridesConf := filepath.Join(s.dir, SamWafBaseConfFile)
if _, err := os.Stat(oldOverridesConf); err == nil {
_ = os.Remove(oldOverridesConf)
}
// 旧版 samwaf/00-samwaf-base.conf(中间迁移路径),现改为 samwaf/before/,删除旧文件。
oldSamwafConf := filepath.Join(s.samwafDir, SamWafBaseConfFile)
if _, err := os.Stat(oldSamwafConf); err == nil {
_ = os.Remove(oldSamwafConf)
}
// Layer 2:从 samwaf/samwaf_base.json 读取配置(不存在时用出厂默认值),生成 samwaf/before/00-samwaf-base.conf。
// samwaf_base.json 可由 API 修改,因此每次启动只重新生成 .conf 文件而不覆盖 .json。
baseJSONPath := filepath.Join(s.dir, OverrideBaseConfigFile)
baseJSONPath := newBaseJSON
baseCfg, _ := loadBaseConfigLocked(baseJSONPath)
// 首次运行:写入出厂默认值到 samwaf_base.json
// 首次运行:写入出厂默认值到 samwaf/samwaf_base.json
if _, err := os.Stat(baseJSONPath); os.IsNotExist(err) {
if data, err2 := json.MarshalIndent(baseCfg, "", " "); err2 == nil {
_ = atomicWriteFile(baseJSONPath, data)
}
}
basePath := filepath.Join(s.dir, OverrideSamWafBaseFile)
basePath := filepath.Join(s.samwafDir, "before", SamWafBaseConfFile)
if err := writeSamWafBaseConfFile(basePath, baseCfg.toTuningConfig()); err != nil {
return fmt.Errorf("write %s: %w", OverrideSamWafBaseFile, err)
return fmt.Errorf("write %s: %w", SamWafBaseConfFile, err)
}
// 兼容迁移:旧版使用 00-tuning.conf,自动重命名为 05-user-vars.conf。
@@ -711,30 +748,30 @@ func rewriteCustomConfLocked(dir string, reg *OverrideRegistry) error {
// =============================================================================
// SamWaf SecAction/SecRule ID 分配表(全局,禁止跨层冲突)
//
// ID 范围 层 文件
// 900000989999 CRS coreruleset/rules/*.conf (禁止使用)
// 990001990009 Layer 1 overrides/00-samwaf-base.confparanoia/threshold
// 990100990999 Layer 2 overrides/05-user-vars.conf(用户自定义 CRS 变量)
// 991001991099 Layer 1 samwaf/before/*.confCRS 变量初始化,如 allowed_methods
// 991100991199 Layer 1 samwaf/after/*.conf(补充检测规则)
// 950001950006 Layer 2 overrides/05-user-vars.conf(调参:paranoia/threshold 覆盖)
// 注:CRS 从 950010 开始,950001-950006 实测无冲突
// ID 范围 层 文件
// 900000989999 CRS (Layer 1) coreruleset/rules/*.conf (禁止使用)
// 990001990009 Layer 2 samwaf/00-samwaf-base.confparanoia/threshold
// 990100990999 Layer 3 overrides/05-user-vars.conf(用户自定义 CRS 变量)
// 991001991099 Layer 2 samwaf/before/*.confCRS 变量初始化,如 allowed_methods
// 991100991199 Layer 2 samwaf/after/*.conf(补充检测规则)
// 950001950006 Layer 3 overrides/05-user-vars.conf(调参:paranoia/threshold 覆盖)
// 注:CRS 从 950010 开始,950001-950006 实测无冲突
//
// 验证结论(2026-04-21):
// CRS 4.x 实际 rule ID 范围 901001~980170,没有落在 990xxx / 991xxx / 950001-950006 段。
// =============================================================================
// writeSamWafBaseConfFile 生成 Layer 1 文件 00-samwaf-base.confSamWaf 基线 tx.* 数值变量。
// writeSamWafBaseConfFile 生成 Layer 2 文件 samwaf/before/00-samwaf-base.confSamWaf 基线 tx.* 数值变量。
//
// 仅设置 paranoia level / anomaly threshold 等数值参数;
// 不包含 SecRuleEngine(用户运营决策)和字符串/列表型变量(由 samwaf/before/*.conf 负责)。
// 不包含 SecRuleEngine(用户运营决策)和字符串/列表型变量(由 samwaf/before/01-samwaf-init-vars.conf 负责)。
// 使用 ID 段 990001990009CRS 安全区)。
func writeSamWafBaseConfFile(path string, t TuningConfig) error {
var sb strings.Builder
sb.WriteString("# Layer 1: SamWaf product defaults (numeric). Managed by SamWaf, updated with SamWaf releases.\n")
sb.WriteString("# Layer 2: SamWaf product defaults (numeric). Managed by SamWaf, updated with SamWaf releases.\n")
sb.WriteString("# DO NOT EDIT MANUALLY. To override, use 后台 → OWASP 规则管理 → 全局调参。\n")
sb.WriteString("# User settings in 05-user-vars.conf take precedence (loaded after this file).\n")
sb.WriteString("# ID range: 990001-990009 (SamWaf Layer 1 base, safe above CRS max 980170)\n\n")
sb.WriteString("# User settings in overrides/05-user-vars.conf take precedence (loaded after this file).\n")
sb.WriteString("# ID range: 990001-990009 (SamWaf Layer 2 base, safe above CRS max 980170)\n\n")
writeSetvar := func(id int, desc, name string, value int) {
if value <= 0 {
@@ -760,17 +797,17 @@ func writeSamWafBaseConfFile(path string, t TuningConfig) error {
return atomicWriteFile(path, []byte(sb.String()))
}
// writeTuningConfFile 根据 TuningConfig 生成 Layer 2 文件(05-user-vars.conf)。
// writeTuningConfFile 根据 TuningConfig 生成 Layer 3 文件(overrides/05-user-vars.conf)。
//
// 包含用户设置的全部内容:SecRuleEngine、tx.* 数值调参变量、用户自定义字符串变量。
// 加载顺序在 00-samwaf-base.conf 和 samwaf/before/*.conf 之后,同名变量以本文件为准。
// 加载顺序在 samwaf/00-samwaf-base.conf 和 samwaf/before/*.conf 之后,同名变量以本文件为准。
// ID 段:调参变量 950001-950006;用户自定义 CRS 变量 990100-990999(上限约 900 条)。
// 注:CRS 实际从 950010 开始,950001-950006 无冲突(已验证)。
func writeTuningConfFile(path string, t TuningConfig) error {
var sb strings.Builder
sb.WriteString("# Layer 2: User variable overrides. Managed by SamWaf via API. NEVER overwritten by any upgrade.\n")
sb.WriteString("# Layer 3: User variable overrides. Managed by SamWaf via API. NEVER overwritten by any upgrade.\n")
sb.WriteString("# Use 后台 → OWASP 规则管理 → 全局调参 来修改这里的内容。\n")
sb.WriteString("# Values here take precedence over 00-samwaf-base.conf (SamWaf defaults).\n\n")
sb.WriteString("# Values here take precedence over samwaf/00-samwaf-base.conf (SamWaf defaults).\n\n")
// RuleEngine: DetectionOnly/On/Off 通过 SecRuleEngine 指令直接设定
engine := strings.TrimSpace(t.RuleEngine)
+6 -1
View File
@@ -113,6 +113,10 @@ func ApplyUpgrade(m *OwaspManager) error {
owaspRoot := m.OwaspRoot()
if _, err := os.Stat(filepath.Join(owaspRoot, "lock.txt")); err == nil {
return fmt.Errorf("检测到 lock.txt,本地规则已被用户锁定,跳过升级")
}
info, err := CheckUpgrade(m)
if err != nil {
return err
@@ -194,7 +198,8 @@ func ApplyUpgrade(m *OwaspManager) error {
// ── 备份 + 替换 samwafLayer 1 SamWaf 钩子,ZIP 含 samwaf/ 时才执行)──
// samwaf/ 由 SamWaf 开发团队维护,随版本发布整体替换;
// 用户自定义内容应放在 overrides/,不受此步影响。
samwafInExtract := filepath.Join(extractDir, "samwaf")
// 用 filepath.Dir(srcCore) 而非 extractDir,兼容 zip 含顶层包装目录的情况。
samwafInExtract := filepath.Join(filepath.Dir(srcCore), "samwaf")
if st, statErr := os.Stat(samwafInExtract); statErr == nil && st.IsDir() {
samwafDir := filepath.Join(owaspRoot, "samwaf")
samwafBak := filepath.Join(owaspRoot, "samwaf.bak")
+439
View File
@@ -0,0 +1,439 @@
package wafowasp
import (
"archive/zip"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"SamWaf/common/zlog"
)
func TestMain(m *testing.M) {
zlog.InitZLog(false, "console")
os.Exit(m.Run())
}
// ---- 辅助 ----
// buildZip 在 dst 路径创建一个 ZIPentries 是 zip内路径→文件内容的映射。
// 以 "/" 结尾的 key 视为目录条目。
func buildZip(t *testing.T, dst string, entries map[string]string) {
t.Helper()
f, err := os.Create(dst)
if err != nil {
t.Fatalf("buildZip create: %v", err)
}
w := zip.NewWriter(f)
for name, content := range entries {
if strings.HasSuffix(name, "/") {
_, _ = w.Create(name)
continue
}
fw, err := w.Create(name)
if err != nil {
t.Fatalf("buildZip entry %s: %v", name, err)
}
if _, err := fw.Write([]byte(content)); err != nil {
t.Fatalf("buildZip write %s: %v", name, err)
}
}
if err := w.Close(); err != nil {
t.Fatalf("buildZip close: %v", err)
}
_ = f.Close()
}
// minimalOwaspRoot 初始化一个能让 Reload() 成功的最小 owaspRoot 目录。
// 返回 owaspRoot 路径。
func minimalOwaspRoot(t *testing.T, baseDir string) string {
t.Helper()
owaspRoot := filepath.Join(baseDir, "data", "owasp")
rulesDir := filepath.Join(owaspRoot, "coreruleset", "rules")
if err := os.MkdirAll(rulesDir, 0755); err != nil {
t.Fatal(err)
}
// coraza.conf 最小有效内容
must(t, os.WriteFile(filepath.Join(owaspRoot, "coraza.conf"),
[]byte("SecRuleEngine On\n"), 0644))
// crs-setup.conf 空文件即可
must(t, os.WriteFile(filepath.Join(owaspRoot, "coreruleset", "crs-setup.conf"),
[]byte(""), 0644))
// 一条最简单的规则
must(t, os.WriteFile(filepath.Join(rulesDir, "old-rule.conf"),
[]byte("SecAction \"id:900001,phase:1,pass,nolog\"\n"), 0644))
// 初始版本
must(t, os.WriteFile(filepath.Join(owaspRoot, "version"),
[]byte("1.0.20260101"), 0644))
return owaspRoot
}
func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
// ---- resolveCorerulesetInExtract ----
func TestResolveCorerulesetInExtract_Flat(t *testing.T) {
dir := t.TempDir()
must(t, os.MkdirAll(filepath.Join(dir, "coreruleset", "rules"), 0755))
got, err := resolveCorerulesetInExtract(dir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != filepath.Join(dir, "coreruleset") {
t.Errorf("got %s, want %s", got, filepath.Join(dir, "coreruleset"))
}
}
func TestResolveCorerulesetInExtract_Nested(t *testing.T) {
// ZIP 含顶层包装目录(如 owasp-1.0.20260422/coreruleset/
dir := t.TempDir()
must(t, os.MkdirAll(filepath.Join(dir, "owasp-1.0.20260422", "coreruleset", "rules"), 0755))
got, err := resolveCorerulesetInExtract(dir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := filepath.Join(dir, "owasp-1.0.20260422", "coreruleset")
if got != want {
t.Errorf("got %s, want %s", got, want)
}
}
func TestResolveCorerulesetInExtract_RulesAtRoot(t *testing.T) {
// ZIP 顶层就是 rules/ + crs-setup.confcoreruleset 自身作为顶层)
dir := t.TempDir()
must(t, os.MkdirAll(filepath.Join(dir, "rules"), 0755))
must(t, os.WriteFile(filepath.Join(dir, "crs-setup.conf"), []byte(""), 0644))
got, err := resolveCorerulesetInExtract(dir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != dir {
t.Errorf("got %s, want %s (dir itself)", got, dir)
}
}
func TestResolveCorerulesetInExtract_NotFound(t *testing.T) {
dir := t.TempDir()
// 空目录,找不到 coreruleset
_, err := resolveCorerulesetInExtract(dir)
if err == nil {
t.Error("expected error, got nil")
}
}
// ---- removeRulesByID ----
func TestRemoveRulesByID_RemovesMatchedBlock(t *testing.T) {
content := "SecRule ARGS \"@contains foo\" \\\n \"id:941100,phase:2,deny\"\nSecRule ARGS \"@contains bar\" \\\n \"id:941200,phase:2,deny\"\n"
ids := map[int]struct{}{941100: {}}
got, changed := removeRulesByID(content, ids)
if !changed {
t.Error("changed should be true")
}
if strings.Contains(got, "941100") {
t.Error("id 941100 should have been stripped")
}
if !strings.Contains(got, "941200") {
t.Error("id 941200 should remain")
}
}
func TestRemoveRulesByID_NoMatchLeaveContentUnchanged(t *testing.T) {
content := "SecRule ARGS \"@contains foo\" \\\n \"id:941100,phase:2,deny\"\n"
ids := map[int]struct{}{999999: {}}
_, changed := removeRulesByID(content, ids)
if changed {
t.Error("changed should be false when no ID matches")
}
}
func TestRemoveRulesByID_EmptyIDs(t *testing.T) {
content := "SecRule ARGS \"@contains foo\" \\\n \"id:941100,phase:2,deny\"\n"
_, changed := removeRulesByID(content, map[int]struct{}{})
if changed {
t.Error("changed should be false for empty id set")
}
}
// ---- lock.txt 检测 ----
func TestApplyUpgrade_LockFileBlocks(t *testing.T) {
baseDir := t.TempDir()
m := &OwaspManager{dir: baseDir}
owaspRoot := m.OwaspRoot()
must(t, os.MkdirAll(owaspRoot, 0755))
must(t, os.WriteFile(filepath.Join(owaspRoot, "lock.txt"), []byte("locked"), 0644))
ConfigureUpgrader(UpgradeConfig{UpdateVersionURL: "http://127.0.0.1:19999/"})
err := ApplyUpgrade(m)
if err == nil {
t.Fatal("expected error due to lock.txt, got nil")
}
if !strings.Contains(err.Error(), "lock.txt") {
t.Errorf("error should mention lock.txt, got: %v", err)
}
}
// ---- 端到端升级(平铺 ZIP 结构)----
func TestApplyUpgrade_EndToEnd_FlatZip(t *testing.T) {
baseDir := t.TempDir()
owaspRoot := minimalOwaspRoot(t, baseDir)
// 构建升级 ZIP(平铺,无包装目录)
zipPath := filepath.Join(t.TempDir(), "owasp-1.0.20260422.zip")
buildZip(t, zipPath, map[string]string{
"coreruleset/crs-setup.conf": "",
"coreruleset/rules/new-rule.conf": "SecAction \"id:900002,phase:1,pass,nolog\"\n",
"samwaf/before/01-init.conf": "# before hook\n",
"samwaf/after/99-custom.conf": "# after hook\n",
})
zipBytes, err := os.ReadFile(zipPath)
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/owasp-ruleset/latest.json":
fmt.Fprintf(w, `{"version":"1.0.20260422","url":"http://%s/owasp-ruleset/owasp-1.0.20260422.zip","sha256":"","changelog":"test"}`, r.Host)
case "/owasp-ruleset/owasp-1.0.20260422.zip":
w.Header().Set("Content-Type", "application/zip")
_, _ = w.Write(zipBytes)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
m := &OwaspManager{dir: baseDir}
m.active.Store(true)
m.overrides = NewOverrideStore(m.OverridesDir(), filepath.Join(m.OwaspRoot(), "samwaf"))
ConfigureUpgrader(UpgradeConfig{UpdateVersionURL: srv.URL + "/"})
err = ApplyUpgrade(m)
// Reload 可能因为简化规则失败,但文件操作应已完成(失败则回滚)
if err != nil && !strings.Contains(err.Error(), "Reload") {
t.Fatalf("ApplyUpgrade returned unexpected error: %v", err)
}
if err == nil {
// 升级成功:校验文件已到位
assertFileExists(t, owaspRoot, "coreruleset/rules/new-rule.conf")
assertFileNotExists(t, owaspRoot, "coreruleset/rules/old-rule.conf")
assertVersion(t, owaspRoot, "1.0.20260422")
assertFileExists(t, owaspRoot, "samwaf/before/01-init.conf")
assertFileExists(t, owaspRoot, "samwaf/after/99-custom.conf")
} else {
// Reload 失败回滚:旧 coreruleset 应恢复
assertFileExists(t, owaspRoot, "coreruleset/rules/old-rule.conf")
}
}
// TestApplyUpgrade_EndToEnd_NestedZip 验证 ZIP 含顶层包装目录时
// samwaf 路径能被正确识别(即本次 bug 修复的覆盖场景)。
func TestApplyUpgrade_EndToEnd_NestedZip(t *testing.T) {
baseDir := t.TempDir()
owaspRoot := minimalOwaspRoot(t, baseDir)
// 构建升级 ZIP(含包装目录 owasp-1.0.20260422/
zipPath := filepath.Join(t.TempDir(), "owasp-1.0.20260422.zip")
buildZip(t, zipPath, map[string]string{
"owasp-1.0.20260422/coreruleset/crs-setup.conf": "",
"owasp-1.0.20260422/coreruleset/rules/new-rule.conf": "SecAction \"id:900002,phase:1,pass,nolog\"\n",
"owasp-1.0.20260422/samwaf/before/01-init.conf": "# before hook\n",
"owasp-1.0.20260422/samwaf/after/99-custom.conf": "# after hook\n",
})
zipBytes, err := os.ReadFile(zipPath)
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/owasp-ruleset/latest.json":
fmt.Fprintf(w, `{"version":"1.0.20260422","url":"http://%s/owasp-ruleset/owasp-1.0.20260422.zip","sha256":"","changelog":"test"}`, r.Host)
case "/owasp-ruleset/owasp-1.0.20260422.zip":
w.Header().Set("Content-Type", "application/zip")
_, _ = w.Write(zipBytes)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
m := &OwaspManager{dir: baseDir}
m.active.Store(true)
m.overrides = NewOverrideStore(m.OverridesDir(), filepath.Join(m.OwaspRoot(), "samwaf"))
ConfigureUpgrader(UpgradeConfig{UpdateVersionURL: srv.URL + "/"})
err = ApplyUpgrade(m)
if err != nil && !strings.Contains(err.Error(), "Reload") {
t.Fatalf("ApplyUpgrade returned unexpected error: %v", err)
}
if err == nil {
// coreruleset 和 samwaf 都应该从包装目录内正确提取
assertFileExists(t, owaspRoot, "coreruleset/rules/new-rule.conf")
assertFileExists(t, owaspRoot, "samwaf/before/01-init.conf")
assertFileExists(t, owaspRoot, "samwaf/after/99-custom.conf")
assertVersion(t, owaspRoot, "1.0.20260422")
} else {
// 回滚:旧 coreruleset 应恢复
assertFileExists(t, owaspRoot, "coreruleset/rules/old-rule.conf")
}
}
// TestApplyUpgrade_RulesNotEmptyAfterUpgrade 专门验证升级后 rules/ 目录不为空。
func TestApplyUpgrade_RulesNotEmptyAfterUpgrade(t *testing.T) {
baseDir := t.TempDir()
minimalOwaspRoot(t, baseDir)
// ZIP 包含多条规则文件
zipPath := filepath.Join(t.TempDir(), "owasp-1.0.20260422.zip")
buildZip(t, zipPath, map[string]string{
"coreruleset/crs-setup.conf": "",
"coreruleset/rules/REQUEST-901.conf": "SecAction \"id:900001,phase:1,pass,nolog\"\n",
"coreruleset/rules/REQUEST-941-XSS.conf": "SecAction \"id:900002,phase:2,pass,nolog\"\n",
"coreruleset/rules/RESPONSE-950.conf": "SecAction \"id:900003,phase:4,pass,nolog\"\n",
})
zipBytes, err := os.ReadFile(zipPath)
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/owasp-ruleset/latest.json":
fmt.Fprintf(w, `{"version":"1.0.20260422","url":"http://%s/owasp-ruleset/owasp-1.0.20260422.zip","sha256":"","changelog":""}`, r.Host)
case "/owasp-ruleset/owasp-1.0.20260422.zip":
_, _ = w.Write(zipBytes)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
m := &OwaspManager{dir: baseDir}
m.active.Store(true)
m.overrides = NewOverrideStore(m.OverridesDir(), filepath.Join(m.OwaspRoot(), "samwaf"))
ConfigureUpgrader(UpgradeConfig{UpdateVersionURL: srv.URL + "/"})
err = ApplyUpgrade(m)
owaspRoot := m.OwaspRoot()
if err == nil {
// 升级成功:rules/ 必须有文件
entries, readErr := os.ReadDir(filepath.Join(owaspRoot, "coreruleset", "rules"))
if readErr != nil {
t.Fatalf("coreruleset/rules not accessible: %v", readErr)
}
if len(entries) == 0 {
t.Error("coreruleset/rules is empty after upgrade — this is the reported bug")
}
for _, e := range entries {
info, _ := e.Info()
if info.Size() == 0 {
t.Errorf("rules/%s is a zero-byte file after upgrade", e.Name())
}
}
} else if !strings.Contains(err.Error(), "Reload") {
t.Fatalf("unexpected error: %v", err)
}
}
// TestApplyUpgrade_RollbackOnReloadFailure 验证 Reload 失败时备份被恢复。
func TestApplyUpgrade_RollbackOnReloadFailure(t *testing.T) {
baseDir := t.TempDir()
owaspRoot := minimalOwaspRoot(t, baseDir)
// ZIP 里放一条会让 Coraza 解析失败的无效指令
zipPath := filepath.Join(t.TempDir(), "owasp-1.0.20260422.zip")
buildZip(t, zipPath, map[string]string{
"coreruleset/crs-setup.conf": "",
"coreruleset/rules/bad.conf": "THIS IS NOT VALID CORAZA SYNTAX !!!\n",
})
zipBytes, err := os.ReadFile(zipPath)
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/owasp-ruleset/latest.json":
fmt.Fprintf(w, `{"version":"1.0.20260422","url":"http://%s/owasp-ruleset/owasp-1.0.20260422.zip","sha256":"","changelog":""}`, r.Host)
case "/owasp-ruleset/owasp-1.0.20260422.zip":
_, _ = w.Write(zipBytes)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
m := &OwaspManager{dir: baseDir}
m.active.Store(true)
m.overrides = NewOverrideStore(m.OverridesDir(), filepath.Join(m.OwaspRoot(), "samwaf"))
ConfigureUpgrader(UpgradeConfig{UpdateVersionURL: srv.URL + "/"})
err = ApplyUpgrade(m)
if err == nil {
t.Skip("Coraza accepted invalid rule; rollback test not applicable")
return
}
if !strings.Contains(err.Error(), "Reload") {
t.Fatalf("expected Reload error, got: %v", err)
}
// 回滚:旧 coreruleset 应恢复,rules/ 有旧文件
assertFileExists(t, owaspRoot, "coreruleset/rules/old-rule.conf")
// 版本文件应已写入(在 Reload 之前写),但回滚不管 version;此处仅验证文件系统
}
// ---- assert helpers ----
func assertFileExists(t *testing.T, root, rel string) {
t.Helper()
p := filepath.Join(root, filepath.FromSlash(rel))
if _, err := os.Stat(p); err != nil {
t.Errorf("expected file %s to exist: %v", rel, err)
}
}
func assertFileNotExists(t *testing.T, root, rel string) {
t.Helper()
p := filepath.Join(root, filepath.FromSlash(rel))
if _, err := os.Stat(p); err == nil {
t.Errorf("expected file %s to be absent, but it exists", rel)
}
}
func assertVersion(t *testing.T, owaspRoot, want string) {
t.Helper()
data, err := os.ReadFile(filepath.Join(owaspRoot, "version"))
if err != nil {
t.Errorf("version file not readable: %v", err)
return
}
if got := strings.TrimSpace(string(data)); got != want {
t.Errorf("version = %q, want %q", got, want)
}
}
+17 -11
View File
@@ -22,13 +22,16 @@ const usageDocMD = `# OWASP 规则在线管理使用指南
三层策略叠加,加载顺序如下:
Layer 0 (CRS 上游,在线升级时整体替换)
Layer 1(基线,CRS 上游,在线升级时整体替换
data/owasp/coreruleset/rules/*.conf
Layer 1 (SamWaf 产品默认层,随 SamWaf 版本更新)
data/owasp/overrides/00-samwaf-base.conf
Layer 2SamWaf 官方层,随 SamWaf 版本更新
data/owasp/samwaf/samwaf_base.json ← Layer 2 配置持久化源
data/owasp/samwaf/before/00-samwaf-base.conf ← 数值变量(paranoia/threshold,由 samwaf_base.json 生成)
data/owasp/samwaf/before/*.conf ← 字符串/列表变量
data/owasp/samwaf/after/*.conf ← 补充检测规则
Layer 2 (用户自定义层,永不被任何升级覆盖)
Layer 3用户自定义层,永不被任何升级覆盖
data/owasp/overrides/05-user-vars.conf
data/owasp/overrides/10-disabled-rules.conf
data/owasp/overrides/20-custom-rules.conf
@@ -37,14 +40,17 @@ const usageDocMD = `# OWASP 规则在线管理使用指南
| 路径 | 层 | 作用 |
| --- | --- | --- |
| data/owasp/coraza.conf | — | Coraza 基础配置(请勿直接改) |
| data/owasp/coreruleset/ | 0 | 官方规则基线(CRS 在线升级时整体替换) |
| overrides/00-samwaf-base.conf | **1** | SamWaf 产品默认 tx.* 变量(随 SamWaf 二进制更新 |
| overrides/05-user-vars.conf | **2** | 用户覆盖的 tx.* 变量(优先于 Layer 1 |
| overrides/10-disabled-rules.conf | **2** | 按 ID 禁用的规则(SecRuleRemoveById |
| overrides/20-custom-rules.conf | **2** | 用户改写后的规则 |
| overrides/override_registry.json | **2** | 元数据:记录哪些 ID 被改动 |
| data/owasp/coreruleset/ | **1** | 官方规则基线(CRS 在线升级时整体替换) |
| samwaf/samwaf_base.json | **2** | Layer 2 配置源(由后台 API 写入 |
| samwaf/before/00-samwaf-base.conf | **2** | SamWaf 产品默认 tx.* 数值变量(由 samwaf_base.json 生成 |
| samwaf/before/*.conf | **2** | SamWaf 字符串/列表变量(allowed_methods 等 |
| samwaf/after/*.conf | **2** | SamWaf 补充检测规则 |
| overrides/05-user-vars.conf | **3** | 用户覆盖的 tx.* 变量(优先于 Layer 2 |
| overrides/10-disabled-rules.conf | **3** | 按 ID 禁用的规则(SecRuleRemoveById |
| overrides/20-custom-rules.conf | **3** | 用户改写后的规则 |
| overrides/override_registry.json | **3** | 元数据:记录哪些 ID 被改动 |
加载顺序:coraza.conf → crs-setup.conf → 00-samwaf-base.conf → 05-user-vars.conf → rules/*.conf → 10-disabled-rules.conf → 20-custom-rules.conf
加载顺序:coraza.conf → crs-setup.conf → samwaf/before/*.conf(含 00-samwaf-base.confoverrides/05-user-vars.conf → coreruleset/rules/*.conf → samwaf/after/*.conf → overrides/10-disabled-rules.conf → overrides/20-custom-rules.conf
tx.* 变量必须在 rules/*.conf 之前设置:CRS rule 901160 只在变量未设置时才写默认值。