feat(waf): CRS-App-Exclusion-Plugins (Nextcloud/WordPress/Drupal) pro Domain — v1.3.12

Statt manueller SecRuleRemoveById-IDs kann man pro Domain offizielle OWASP-CRS-
Exclusion-Plugins aktivieren — pfad-genaue, upstream-gepflegte App-Ausnahmen.

- Migration 0046: waf_configs.crs_plugins text[].
- Engine (engine.go): je gewähltem Plugin werden config/before VOR den CRS-Rules
  und after DANACH inkludiert (exakt nach OWASP-CRS-Plugin-Spec); nur die für
  DIESE Domain gewählten, nur wenn die Datei existiert. Whitelist KnownCRSPlugins.
- Handler: crs_plugins im Upsert-Body + Whitelist-Validierung (Include-Pfad-
  Injection-Schutz).
- Packaging (postinst): lädt die Plugins (coreruleset/<name>-plugin) nach
  <crs>/plugins/ — self-healing auf jedem configure, nur fehlende.
- UI: Multi-Select „App-Profile (CRS-Plugins)" im WAF-Config-Drawer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-08-03 10:28:03 +02:00
parent 088910ee19
commit 37f729381d
11 changed files with 220 additions and 19 deletions

View File

@@ -48,12 +48,23 @@ func buildDirectives(cfg models.WafConfig, crsDir string) string {
pl = 1
}
fmt.Fprintf(&sb, "SecAction \"id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=%d\"\n", pl)
setupConf := filepath.Join(crsDir, "crs-setup.conf")
if _, err := os.Stat(setupConf); err == nil {
fmt.Fprintf(&sb, "Include %s\n", setupConf)
includeIfExists(&sb, filepath.Join(crsDir, "crs-setup.conf"))
// CRS-App-Exclusion-Plugins: config + before laufen VOR den CRS-Rules
// (setzen Enable-Vars + pfad-scoped ctl:ruleRemoveById), after DANACH —
// exakt nach OWASP-CRS-Plugin-Spec. Es werden NUR die für DIESE Domain
// gewählten Plugins inkludiert (per-Domain, nicht global).
plugins := resolveCRSPlugins(cfg.CRSPlugins)
for _, prefix := range plugins {
includeIfExists(&sb, filepath.Join(crsDir, "plugins", prefix+"-config.conf"))
includeIfExists(&sb, filepath.Join(crsDir, "plugins", prefix+"-before.conf"))
}
// rules/*.conf ist ein Glob (kein Stat) — crsAvailable() hat oben bereits
// bestätigt, dass mind. eine .conf existiert.
fmt.Fprintf(&sb, "Include %s\n", filepath.Join(crsDir, "rules", "*.conf"))
for _, prefix := range plugins {
includeIfExists(&sb, filepath.Join(crsDir, "plugins", prefix+"-after.conf"))
}
rulesGlob := filepath.Join(crsDir, "rules", "*.conf")
fmt.Fprintf(&sb, "Include %s\n", rulesGlob)
}
// Rule exclusions (applied after CRS load so they override CRS).
@@ -78,6 +89,35 @@ func buildDirectives(cfg models.WafConfig, crsDir string) string {
return sb.String()
}
// KnownCRSPlugins mappt den kurzen Plugin-Namen (gespeichert in
// waf_configs.crs_plugins, im UI gewählt) auf sein Datei-Prefix in
// <crsDir>/plugins/. Nur diese werden paketiert (postinst) und akzeptiert.
var KnownCRSPlugins = map[string]string{
"nextcloud": "nextcloud-rule-exclusions",
"wordpress": "wordpress-rule-exclusions",
"drupal": "drupal-rule-exclusions",
}
// resolveCRSPlugins mappt gewählte Plugin-Namen auf ihre Datei-Prefixe und
// filtert unbekannte/leere raus — defensiv, nie ungültige Includes rendern.
func resolveCRSPlugins(names []string) []string {
out := make([]string, 0, len(names))
for _, n := range names {
if prefix, ok := KnownCRSPlugins[strings.TrimSpace(n)]; ok {
out = append(out, prefix)
}
}
return out
}
// includeIfExists rendert eine Include-Zeile nur, wenn die Datei existiert — so
// bricht ein gewähltes-aber-nicht-installiertes Plugin die Config nicht.
func includeIfExists(sb *strings.Builder, path string) {
if _, err := os.Stat(path); err == nil {
fmt.Fprintf(sb, "Include %s\n", path)
}
}
func ruleEngineMode(mode string) string {
switch mode {
case "blocking":