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
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. Listing forgejo, a fork of gitea, brings gitea's fixes too. 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:
1 parent
b6aa2cbc4b
commit
ed54adbc98
24 files changed
+1665
-874
No files matched your search
@@ -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,24 +156,32 @@ 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")
|
||||
}
|
||||
|
||||
// TestOrdinaryForgejoRequestsPass checks Forgejo's recorded requests at the
|
||||
// defaults only. While bodies are read, the Core Rule Set still refuses the
|
||||
// file and branch names some of them carry in a form or JSON body, as it does
|
||||
// gitea's (TestGiteaBodiesTheCoreRuleSetRefuses).
|
||||
// defaults only: with SWW_APPS unset, and on example.com, where they were
|
||||
// recorded, while SWW_APPS lists only forgejo for it. While bodies are read,
|
||||
// the Core Rule Set still refuses the file and branch names some of them
|
||||
// carry in a form or JSON body, as it does gitea's
|
||||
// (TestGiteaBodiesTheCoreRuleSetRefuses).
|
||||
func TestOrdinaryForgejoRequestsPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := atDefaults(t)
|
||||
requests := recordedRequests(t, "forgejo-requests.txt")
|
||||
forgejoOnly := map[string][]string{"example.com": {forgejo}}
|
||||
|
||||
for _, recorded := range recordedRequests(t, "forgejo-requests.txt") {
|
||||
r := request{recorded.Method, recorded.Target, recorded.Headers}
|
||||
if got, _ := inspectBody(t, crs, r, recorded.Body); got.Score >= threshold {
|
||||
t.Errorf("%s %s refused: %+v", r.method, r.target, got)
|
||||
for apps, crs := range map[string]*coreruleset.CoreRuleSet{
|
||||
"SWW_APPS unset": atDefaults(t),
|
||||
"SWW_APPS=example.com:forgejo": servingApps(t, forgejoOnly),
|
||||
} {
|
||||
for _, recorded := range requests {
|
||||
r := request{recorded.Method, recorded.Target, recorded.Headers}
|
||||
if got, _ := inspectBody(t, crs, r, recorded.Body); got.Score >= threshold {
|
||||
t.Errorf("%s: %s %s refused: %+v", apps, r.method, r.target, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,29 +189,29 @@ func TestOrdinaryForgejoRequestsPass(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user