mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-09-19 09:32:29 +08:00
Merge pull request #780 from samwafgo/fix_owasp_update
fix:owasp set update
This commit is contained in:
@@ -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 专用)
|
||||
#
|
||||
# -- 示例 1:SamWaf 补充检测——检测 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 范围:991001–991099(SamWaf Layer 1 before-hooks,CRS 安全区)
|
||||
# ID 范围:991001–991099(SamWaf Layer 2 before-hooks,CRS 安全区)
|
||||
# =============================================================================
|
||||
|
||||
# 允许的 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-Type(CRS 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
@@ -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 1(SamWaf 出厂默认,全部先于 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/threshold,990001-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/threshold,990001-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 0(CRS 规则集):
|
||||
// 6. data/owasp/coreruleset/rules/*.conf
|
||||
// Layer 1(CRS 规则集):
|
||||
// 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()})
|
||||
}
|
||||
|
||||
// 步骤 3:Layer 1a — 00-samwaf-base.conf(数值型变量:paranoia / threshold,ID 990001-990009)
|
||||
samwafBase := filepath.Join(m.OverridesDir(), OverrideSamWafBaseFile)
|
||||
if _, err := os.Stat(samwafBase); err == nil {
|
||||
cfg = cfg.WithDirectivesFromFile(samwafBase)
|
||||
}
|
||||
|
||||
// 步骤 4:Layer 1b — samwaf/before/*.conf(字符串/列表型变量:allowed_methods 等,ID 991001-991099)
|
||||
// 必须在 Layer 2(05-user-vars.conf)之前,确保用户覆盖优先级更高。
|
||||
// 步骤 3:Layer 2 before — samwaf/before/*.conf(按字母序,00-samwaf-base.conf 先于 01-samwaf-init-vars.conf)
|
||||
// 包含数值变量(paranoia/threshold)和字符串/列表变量(allowed_methods 等)。
|
||||
cfg = loadDirConfs(cfg, m.samwafHooksDir("before"))
|
||||
|
||||
// 步骤 5:Layer 2 — 05-user-vars.conf(用户覆盖层,最后加载 → 同名变量用户值胜出)
|
||||
// 同时覆盖 Layer 1a 的数值变量和 Layer 1b 的字符串变量。
|
||||
// 步骤 4:Layer 3 — overrides/05-user-vars.conf(用户覆盖层,最后加载 → 同名变量用户值胜出)
|
||||
// 覆盖 Layer 2 before 中的数值变量和字符串变量。
|
||||
tuningConf := filepath.Join(m.OverridesDir(), OverrideTuningFile)
|
||||
if _, err := os.Stat(tuningConf); err == nil {
|
||||
cfg = cfg.WithDirectivesFromFile(tuningConf)
|
||||
}
|
||||
|
||||
// 步骤 6:Layer 0 — CRS 官方规则集(此时所有 tx.* 变量均已就绪)
|
||||
// 步骤 5:Layer 1 — CRS 官方规则集(此时所有 tx.* 变量均已就绪)
|
||||
rulesGlob := filepath.Join(root, "coreruleset", "rules", "*.conf")
|
||||
cfg = cfg.WithDirectivesFromFile(rulesGlob)
|
||||
|
||||
// 步骤 7:Layer 1 后置钩子(samwaf/after/*.conf,ID 991100-991199)—— CRS 规则执行后
|
||||
// 步骤 6:Layer 2 后置钩子(samwaf/after/*.conf,ID 991100-991199)—— CRS 规则执行后
|
||||
// 用途:补充检测规则、基于评分的自定义动作等。
|
||||
cfg = loadDirConfs(cfg, m.samwafHooksDir("after"))
|
||||
|
||||
// 步骤 8-9:Layer 2 后置文件(用户禁用 / 改写规则,须在 CRS 规则之后才能 RemoveById)
|
||||
// 跳过已在步骤 3-5 提前加载的 pre-rules 文件。
|
||||
// 步骤 7-8:Layer 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
@@ -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 1:SamWaf 产品基线(由 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 2(samwaf/)和 Layer 3(overrides/)的读写,内部持锁串行以避免并发写坏文件。
|
||||
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.conf(Layer 1):每次调用均重写,确保 SamWaf 产品默认值随二进制更新。
|
||||
// - 05-user-vars.conf(Layer 2):仅首次(registry 缺失时)写入;后续由用户操作写入。
|
||||
// - 兼容迁移:若发现旧版 00-tuning.conf,自动重命名为 05-user-vars.conf。
|
||||
// - samwaf/before/00-samwaf-base.conf(Layer 2):每次调用均重写,确保 SamWaf 产品默认值随二进制更新。
|
||||
// - overrides/05-user-vars.conf(Layer 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 范围 层 文件
|
||||
// 900000–989999 CRS coreruleset/rules/*.conf (禁止使用)
|
||||
// 990001–990009 Layer 1 overrides/00-samwaf-base.conf(paranoia/threshold)
|
||||
// 990100–990999 Layer 2 overrides/05-user-vars.conf(用户自定义 CRS 变量)
|
||||
// 991001–991099 Layer 1 samwaf/before/*.conf(CRS 变量初始化,如 allowed_methods)
|
||||
// 991100–991199 Layer 1 samwaf/after/*.conf(补充检测规则)
|
||||
// 950001–950006 Layer 2 overrides/05-user-vars.conf(调参:paranoia/threshold 覆盖)
|
||||
// 注:CRS 从 950010 开始,950001-950006 实测无冲突
|
||||
// ID 范围 层 文件
|
||||
// 900000–989999 CRS (Layer 1) coreruleset/rules/*.conf (禁止使用)
|
||||
// 990001–990009 Layer 2 samwaf/00-samwaf-base.conf(paranoia/threshold)
|
||||
// 990100–990999 Layer 3 overrides/05-user-vars.conf(用户自定义 CRS 变量)
|
||||
// 991001–991099 Layer 2 samwaf/before/*.conf(CRS 变量初始化,如 allowed_methods)
|
||||
// 991100–991199 Layer 2 samwaf/after/*.conf(补充检测规则)
|
||||
// 950001–950006 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.conf:SamWaf 基线 tx.* 数值变量。
|
||||
// writeSamWafBaseConfFile 生成 Layer 2 文件 samwaf/before/00-samwaf-base.conf:SamWaf 基线 tx.* 数值变量。
|
||||
//
|
||||
// 仅设置 paranoia level / anomaly threshold 等数值参数;
|
||||
// 不包含 SecRuleEngine(用户运营决策)和字符串/列表型变量(由 samwaf/before/*.conf 负责)。
|
||||
// 不包含 SecRuleEngine(用户运营决策)和字符串/列表型变量(由 samwaf/before/01-samwaf-init-vars.conf 负责)。
|
||||
// 使用 ID 段 990001–990009(CRS 安全区)。
|
||||
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)
|
||||
|
||||
@@ -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 {
|
||||
// ── 备份 + 替换 samwaf(Layer 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")
|
||||
|
||||
@@ -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 路径创建一个 ZIP,entries 是 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.conf(coreruleset 自身作为顶层)
|
||||
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
@@ -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 2(SamWaf 官方层,随 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.conf)→ overrides/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 只在变量未设置时才写默认值。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user