Rule files, and bans for a clear sign of attack (closes #24)
check / check (push) Waiting to run

Every *.rules file in SWWAF_RULES_DIR is read at start and on each
change. Each request is checked against the rules after the rate
limits: log notes a match, block refuses with 403, ban refuses and bans
the netblock for SWWAF_ATTACK_BAN_DURATION, made permanent by its next
request or attack. path, query and uri are matched as the request line
sent them; header:Host and header:Transfer-Encoding are refused. Bans
gain a cause. The image ships 00-default.rules.

Judgement call: a header sent twice is matched with its values joined
by ", ".
Judgement call: SWWAF_MAX_BAN_DURATION does not cap a ban for an attack.
Not in this unit: offences for rule matches, with the error burst.

Model: opus-5-5
This commit is contained in:
2026-10-06 17:04:23 +00:00
parent 74bdc6a449
commit 2c01d7f98f
26 changed files with 2239 additions and 277 deletions
+17 -3
View File
@@ -17,6 +17,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/proxy"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rules"
)
const (
@@ -74,6 +75,8 @@ const (
banScopeV4Prefix = "SWWAF_BAN_SCOPE_V4_PREFIX"
instanceName = "SWWAF_INSTANCE_NAME"
logRequestHeaders = "SWWAF_LOG_REQUEST_HEADERS"
attackBanDuration = "SWWAF_ATTACK_BAN_DURATION"
rulesDir = "SWWAF_RULES_DIR"
)
// output collects what smallwebwaf writes on stdout.
@@ -212,14 +215,15 @@ func startProxyWithGeoJS(
}
// startProxyWithClock is startProxyWithGeoJS with requests counted and
// bans made by the time now tells, and returns the server as well.
// bans made by the time now tells, and returns the server as well. Unless
// env sets SWWAF_RULES_DIR, it is an empty directory, of no rules.
func startProxyWithClock(
t *testing.T, appURL, geojsURL string, now func() time.Time,
env map[string]string,
) (string, *output, *proxy.Server) {
t.Helper()
settings := map[string]string{"SWWAF_UPSTREAM_URL": appURL}
settings := map[string]string{"SWWAF_UPSTREAM_URL": appURL, rulesDir: t.TempDir()}
maps.Copy(settings, env)
cfg, err := config.FromEnvironment(func(name string) (string, bool) {
@@ -232,12 +236,22 @@ func startProxyWithClock(
}
out := &output{}
processLog := requestlog.NewProcessLogger(out)
ruleFiles, err := rules.Load(rules.Params{
Dir: cfg.RulesDir, Enabled: cfg.RulesEnabled, ProcessLog: processLog,
})
if err != nil {
t.Fatalf("rule files: %v", err)
}
server := proxy.New(proxy.Params{
Config: cfg,
RequestLog: out,
ProcessLog: requestlog.NewProcessLogger(out),
ProcessLog: processLog,
GeoJSURL: geojsURL,
Now: now,
Rules: ruleFiles,
})
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")