SWW_APPS: an app's Core Rule Set fixes apply only to the hostnames that serve it (closes #220)
check / check (push) Waiting to run

Each app's fixes are one .conf file and one entry in apps. They apply
only while Inspect has set the app's transaction variable, such as
tx.app_gitea. SWW_APPS names each hostname's apps. A hostname gets only
its apps' fixes; while SWW_APPS is unset, every hostname gets every
app's fixes.

A hostname in SWW_APPS that is not a DNS name, such as *.example.com,
stops the start. Unset, SWW_APPS is logged at start as null; set
empty, as "".

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 02:19:08 +00:00
1 parent f7e3190654
commit 7cd08a03bb
22 files changed
+1534 -828

No files matched your search

+25 -15
View File
@@ -13,6 +13,8 @@ import (
"slices"
"strings"
"testing"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
)
// giteaRequests returns the recorded ordinary gitea requests in testdata.
@@ -52,10 +54,10 @@ func TestOrdinaryGiteaRequestsPass(t *testing.T) {
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
crs := atDefaults(t)
crs := servingApps(t, map[string][]string{giteaHost: {gitea}})
for _, r := range giteaRequests(t) {
if got := inspect(t, crs, r); got.Score >= threshold {
if got := inspect(t, crs, withHost(r, giteaHost)); got.Score >= threshold {
t.Errorf("%s %q refused: %+v", r.target, r.headers[1:], got)
}
}
@@ -102,17 +104,25 @@ func recordedRequests(t *testing.T, name string) []recordedRequest {
}
// wantRecordedRequestsPass checks that none of the recorded ordinary
// requests in the file name in testdata is refused, at the defaults and
// while bodies are read. A request with neither a body nor a Content-Type
// header is checked only at the defaults: with neither, the set that reads
// bodies has nothing more to check. Each request runs as its own parallel
// subtest.
func wantRecordedRequestsPass(t *testing.T, name string) {
// requests in the file name in testdata is refused on the hostname where
// they were recorded, otherHost for WordPress and example.com for the other
// apps, while SWW_APPS lists only app for it. They are checked at the
// defaults and while bodies are read. With SWW_APPS unset, every app's fixes
// apply. These only switch checks off, so the requests pass then too. A
// request with neither a body nor a Content-Type header is checked only at
// the defaults: with neither, the set that reads bodies has nothing more to
// check. Each request runs as its own parallel subtest.
func wantRecordedRequestsPass(t *testing.T, name, app string) {
t.Helper()
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
defaults, bodies := atDefaults(t), readingBodies(t)
apps := map[string][]string{"example.com": {app}, otherHost: {app}}
defaults := servingApps(t, apps)
bodies := shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit,
Apps: apps,
})
for _, recorded := range recordedRequests(t, name) {
t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) {
@@ -146,35 +156,35 @@ func wantRecordedRequestsPass(t *testing.T, name string) {
func TestOrdinaryDrupalRequestsPass(t *testing.T) {
t.Parallel()
wantRecordedRequestsPass(t, "drupal-requests.txt")
wantRecordedRequestsPass(t, "drupal-requests.txt", "drupal")
}
func TestOrdinaryMastodonRequestsPass(t *testing.T) {
t.Parallel()
wantRecordedRequestsPass(t, "mastodon-requests.txt")
wantRecordedRequestsPass(t, "mastodon-requests.txt", "mastodon")
}
func TestOrdinaryMediaWikiRequestsPass(t *testing.T) {
t.Parallel()
wantRecordedRequestsPass(t, "mediawiki-requests.txt")
wantRecordedRequestsPass(t, "mediawiki-requests.txt", mediawiki)
}
func TestOrdinaryNextcloudRequestsPass(t *testing.T) {
t.Parallel()
wantRecordedRequestsPass(t, "nextcloud-requests.txt")
wantRecordedRequestsPass(t, "nextcloud-requests.txt", nextcloud)
}
func TestOrdinaryVaultwardenRequestsPass(t *testing.T) {
t.Parallel()
wantRecordedRequestsPass(t, "vaultwarden-requests.txt")
wantRecordedRequestsPass(t, "vaultwarden-requests.txt", "vaultwarden")
}
func TestOrdinaryWordPressRequestsPass(t *testing.T) {
t.Parallel()
wantRecordedRequestsPass(t, "wordpress-requests.txt")
wantRecordedRequestsPass(t, "wordpress-requests.txt", wordpress)
}