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:
@@ -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":
|
||||
|
||||
60
internal/waf/engine_plugins_test.go
Normal file
60
internal/waf/engine_plugins_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||
)
|
||||
|
||||
// mustWrite legt eine Datei (inkl. Verzeichnis) an.
|
||||
func mustWrite(t *testing.T, p, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDirectives_CRSPluginIncludeOrder(t *testing.T) {
|
||||
crs := t.TempDir()
|
||||
mustWrite(t, filepath.Join(crs, "crs-setup.conf"), "# setup\n")
|
||||
mustWrite(t, filepath.Join(crs, "rules", "REQUEST-942.conf"), "# rules\n")
|
||||
mustWrite(t, filepath.Join(crs, "plugins", "nextcloud-rule-exclusions-config.conf"), "# nc config\n")
|
||||
mustWrite(t, filepath.Join(crs, "plugins", "nextcloud-rule-exclusions-before.conf"), "# nc before\n")
|
||||
|
||||
cfg := models.WafConfig{Mode: "blocking", ParanoiaLevel: 1, CRSPlugins: []string{"nextcloud", "unknown-x"}}
|
||||
out := buildDirectives(cfg, crs)
|
||||
|
||||
iSetup := strings.Index(out, "crs-setup.conf")
|
||||
iCfg := strings.Index(out, "nextcloud-rule-exclusions-config.conf")
|
||||
iBefore := strings.Index(out, "nextcloud-rule-exclusions-before.conf")
|
||||
iRules := strings.Index(out, filepath.Join("rules", "*.conf"))
|
||||
if iSetup < 0 || iCfg < 0 || iBefore < 0 || iRules < 0 {
|
||||
t.Fatalf("erwartete Includes fehlen:\n%s", out)
|
||||
}
|
||||
// config + before MÜSSEN vor den CRS-Rules stehen (Plugin-Spec).
|
||||
if iSetup >= iCfg || iCfg >= iBefore || iBefore >= iRules {
|
||||
t.Errorf("falsche Include-Reihenfolge (setup=%d cfg=%d before=%d rules=%d):\n%s", iSetup, iCfg, iBefore, iRules, out)
|
||||
}
|
||||
// Unbekanntes Plugin darf NICHT inkludiert werden (Whitelist).
|
||||
if strings.Contains(out, "unknown-x") {
|
||||
t.Errorf("unbekanntes Plugin wurde inkludiert:\n%s", out)
|
||||
}
|
||||
// Nicht existente after.conf → keine Include-Zeile.
|
||||
if strings.Contains(out, "nextcloud-rule-exclusions-after.conf") {
|
||||
t.Errorf("nicht existente after.conf wurde inkludiert:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCRSPlugins(t *testing.T) {
|
||||
got := resolveCRSPlugins([]string{"nextcloud", " wordpress ", "bogus", ""})
|
||||
want := "nextcloud-rule-exclusions,wordpress-rule-exclusions"
|
||||
if strings.Join(got, ",") != want {
|
||||
t.Errorf("resolveCRSPlugins=%v want %q", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user