Read every setting through viper and rename the settings (closes #211)
check / check (push) Waiting to run

Every setting of waf and rproxy is read through viper, in place of the
os.LookupEnv function Main passed down. NewViper turns AutomaticEnv and
AllowEmptyEnv on, so a setting that is set but empty reads as set. A
setting may also be given as a file, the _FILE form, which rproxy's
settings now have too; rproxy still writes nothing to disk.

The settings are renamed: waf's take the prefix SWW_WAF_, rproxy's take
SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core Rule Set's
settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_.

Model: deepseek-v4-pro (first version); opus-5-5 (rework)
This commit is contained in:
sneak committed 2026-11-12 02:34:16 +00:00
1 parent 4b62bc84ce
commit 24d1f3cde3
128 files changed
+2435 -2252

No files matched your search

+28 -15
View File
@@ -1,10 +1,9 @@
// Package config reads sww's settings, and is the one place they are
// read. Every setting is an environment variable. The settings of `sww
// waf` start with SWW_, every one of them has a default, and most can be
// given as a file instead. Those of `sww rproxy` start with SWRP_. Each
// of them but SWRP_WAF_BACKEND has a default, and none can be given as a file,
// since rproxy never touches disk. SWW_COMMAND chooses which of the two
// the image runs.
// waf` start with SWW_WAF_, every one of them has a default, and most can be
// given as a file instead. Those of `sww rproxy` start with SWW_RPROXY_. Each
// of them but SWW_RPROXY_WAF_BACKEND has a default, and each can be given as a file
// too. SWW_COMMAND chooses which of the two the image runs.
package config
import (
@@ -17,6 +16,8 @@ import (
"os"
"strconv"
"strings"
"github.com/spf13/viper"
)
// off is the value that switches a timeout, a size limit or a rate limit
@@ -27,8 +28,8 @@ const (
kibibyte = 1 << 10
mebibyte = 1 << 20
gibibyte = 1 << 30
// defaultListenAddr is the default of SWW_LISTEN_ADDR and
// SWRP_LISTEN_ADDR.
// defaultListenAddr is the default of SWW_WAF_LISTEN_ADDR and
// SWW_RPROXY_LISTEN_ADDR.
defaultListenAddr = ":8080"
)
@@ -48,8 +49,8 @@ var (
// Command reads only SWW_COMMAND, the command the image runs and its
// health check checks: rproxy when it is unset or empty, or waf. It cannot
// be given as a file. Any other value is an error that names the setting.
func Command(lookupEnv func(string) (string, bool)) (string, error) {
command, _ := lookupEnv("SWW_COMMAND")
func Command(v *viper.Viper) (string, error) {
command, _ := readSetting(v, "SWW_COMMAND")
switch command {
case "", "rproxy":
@@ -62,11 +63,23 @@ func Command(lookupEnv func(string) (string, bool)) (string, error) {
}
// environment is where FromEnvironment reads the settings: it notes each
// value for the log, and keeps the first error.
// value for the log, and keeps the first error. It reads every setting
// through v, the viper NewViper returns, and honours each setting's _FILE
// form too.
type environment struct {
lookupEnv func(string) (string, bool)
settings []slog.Attr
err error
v *viper.Viper
settings []slog.Attr
err error
}
// readSetting returns a setting's value and whether it is set, reading it
// through v.
func readSetting(v *viper.Viper, name string) (string, bool) {
if !v.IsSet(name) {
return "", false
}
return v.GetString(name), true
}
// value returns a setting's value, or its default when it is not set,
@@ -87,10 +100,10 @@ func (e *environment) value(name, defaultValue string) string {
// names, less one newline at their end. It notes that file's path for the
// log. Both variables set, or a file that cannot be read, is an error.
func (e *environment) lookup(name string) (string, bool) {
value, set := e.lookupEnv(name)
value, set := readSetting(e.v, name)
fileName := name + "_FILE"
path, inFile := e.lookupEnv(fileName)
path, inFile := readSetting(e.v, fileName)
if !inFile {
return value, set
}
+16
View File
@@ -0,0 +1,16 @@
// Package configtest gives the tests of the packages that read settings the
// settings they choose.
package configtest
import "github.com/spf13/viper"
// Viper returns a viper holding each of settings, as the viper config.NewViper
// returns holds a process's environment.
func Viper(settings map[string]string) *viper.Viper {
v := viper.New()
for name, value := range settings {
v.Set(name, value)
}
return v
}
+37 -51
View File
@@ -8,38 +8,39 @@ import (
"net/netip"
"net/url"
"strconv"
"strings"
"time"
"github.com/spf13/viper"
)
// RProxy is the settings of `sww rproxy`. A size or time limit of zero
// is off.
type RProxy struct {
// ListenAddr is where rproxy listens (SWRP_LISTEN_ADDR).
// ListenAddr is where rproxy listens (SWW_RPROXY_LISTEN_ADDR).
ListenAddr string
// UpstreamURL is the app (SWRP_UPSTREAM_URL).
// UpstreamURL is the app (SWW_RPROXY_UPSTREAM_URL).
UpstreamURL *url.URL
// TrustedProxies are the netblocks whose X-Forwarded-For is believed
// (SWRP_TRUSTED_PROXIES).
// (SWW_RPROXY_TRUSTED_PROXIES).
TrustedProxies []netip.Prefix
// WAFBackend is waf's URL (SWRP_WAF_BACKEND).
// WAFBackend is waf's URL (SWW_RPROXY_WAF_BACKEND).
WAFBackend *url.URL
// Timeout is how long rproxy waits for waf's answer to a question,
// and to a report (SWRP_TIMEOUT_MS). It cannot be off.
// and to a report (SWW_RPROXY_TIMEOUT_MS). It cannot be off.
Timeout time.Duration
// DefaultDisposition is what is done with a request when waf cannot be
// connected to, does not answer at all, or does not answer within Timeout:
// allow or deny (SWRP_DEFAULT_DISPOSITION).
// allow or deny (SWW_RPROXY_DEFAULT_DISPOSITION).
DefaultDisposition string
// MaxRequestSize is the largest request body (SWRP_MAX_REQUEST_SIZE), and
// MaxResponseSize the largest response body (SWRP_MAX_RESPONSE_SIZE).
// MaxRequestSize is the largest request body (SWW_RPROXY_MAX_REQUEST_SIZE), and
// MaxResponseSize the largest response body (SWW_RPROXY_MAX_RESPONSE_SIZE).
MaxRequestSize int64
MaxResponseSize int64
// MaxRequestTime bounds sending the whole request: the visitor's request
// line and headers, and then, from their end, its body on its way to the
// app (SWRP_MAX_REQUEST_TIME_MS). MaxResponseTime bounds the answer, from
// app (SWW_RPROXY_MAX_REQUEST_TIME_MS). MaxResponseTime bounds the answer, from
// the end of the request to its last byte reaching the visitor
// (SWRP_MAX_RESPONSE_TIME_MS).
// (SWW_RPROXY_MAX_RESPONSE_TIME_MS).
MaxRequestTime time.Duration
MaxResponseTime time.Duration
@@ -48,7 +49,7 @@ type RProxy struct {
settings []slog.Attr
}
// defaultUpstreamURL is the default of SWRP_UPSTREAM_URL.
// defaultUpstreamURL is the default of SWW_RPROXY_UPSTREAM_URL.
const defaultUpstreamURL = "http://127.0.0.1:8081"
// privateRanges are the private address ranges, the default trusted
@@ -69,39 +70,38 @@ var (
)
)
// RProxyFromEnvironment reads rproxy's settings with lookupEnv, normally
// os.LookupEnv. A setting that is not set takes its default, and
// SWRP_WAF_BACKEND, which has none, must be set. A setting that is set but
// invalid is an error that names it. Since rproxy never touches disk, its
// settings have no _FILE form.
func RProxyFromEnvironment(
lookupEnv func(string) (string, bool),
) (*RProxy, error) {
env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
// RProxyFromEnvironment reads rproxy's settings through v, normally the viper
// NewViper returns, bound to the process's environment. A setting that is not
// set takes its default, and SWW_RPROXY_WAF_BACKEND, which has none, must be
// set. A setting may instead be given as a file: the variable named by the
// setting's name with _FILE added names the file, which is read now. A setting
// that is set but invalid is an error that names it.
func RProxyFromEnvironment(v *viper.Viper) (*RProxy, error) {
env := &environment{v: v}
cfg := &RProxy{
ListenAddr: env.address("SWRP_LISTEN_ADDR"),
ListenAddr: env.address("SWW_RPROXY_LISTEN_ADDR"),
UpstreamURL: env.appURL(
"SWRP_UPSTREAM_URL",
"SWW_RPROXY_UPSTREAM_URL",
defaultUpstreamURL,
),
TrustedProxies: env.netblocks(
"SWRP_TRUSTED_PROXIES",
"SWW_RPROXY_TRUSTED_PROXIES",
privateRanges,
),
WAFBackend: env.wafBackend("SWRP_WAF_BACKEND"),
Timeout: env.milliseconds("SWRP_TIMEOUT_MS", "2000"),
WAFBackend: env.wafBackend("SWW_RPROXY_WAF_BACKEND"),
Timeout: env.milliseconds("SWW_RPROXY_TIMEOUT_MS", "2000"),
DefaultDisposition: env.disposition(
"SWRP_DEFAULT_DISPOSITION",
"SWW_RPROXY_DEFAULT_DISPOSITION",
"allow",
),
MaxRequestSize: env.size("SWRP_MAX_REQUEST_SIZE", "100M"),
MaxResponseSize: env.size("SWRP_MAX_RESPONSE_SIZE", "1G"),
MaxRequestSize: env.size("SWW_RPROXY_MAX_REQUEST_SIZE", "100M"),
MaxResponseSize: env.size("SWW_RPROXY_MAX_RESPONSE_SIZE", "1G"),
MaxRequestTime: env.millisecondsOrOff(
"SWRP_MAX_REQUEST_TIME_MS",
"SWW_RPROXY_MAX_REQUEST_TIME_MS",
"60000",
),
MaxResponseTime: env.millisecondsOrOff(
"SWRP_MAX_RESPONSE_TIME_MS",
"SWW_RPROXY_MAX_RESPONSE_TIME_MS",
"1800000",
),
}
@@ -115,15 +115,15 @@ func RProxyFromEnvironment(
return cfg, nil
}
// RProxyListenAddrAndUpstreamURL reads only SWRP_LISTEN_ADDR and
// SWRP_UPSTREAM_URL. The health check in rproxy's container needs no other
// RProxyListenAddrAndUpstreamURL reads only SWW_RPROXY_LISTEN_ADDR and
// SWW_RPROXY_UPSTREAM_URL. The health check in rproxy's container needs no other
// setting, so it reads no other.
func RProxyListenAddrAndUpstreamURL(
lookupEnv func(string) (string, bool),
v *viper.Viper,
) (string, *url.URL, error) {
env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
listenAddr := env.address("SWRP_LISTEN_ADDR")
upstreamURL := env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL)
env := &environment{v: v}
listenAddr := env.address("SWW_RPROXY_LISTEN_ADDR")
upstreamURL := env.appURL("SWW_RPROXY_UPSTREAM_URL", defaultUpstreamURL)
if env.err != nil {
return "", nil, env.err
@@ -132,20 +132,6 @@ func RProxyListenAddrAndUpstreamURL(
return listenAddr, upstreamURL, nil
}
// withoutFileForms returns lookupEnv finding no setting named with _FILE, since
// rproxy never touches disk.
func withoutFileForms(
lookupEnv func(string) (string, bool),
) func(string) (string, bool) {
return func(name string) (string, bool) {
if strings.HasSuffix(name, "_FILE") {
return "", false
}
return lookupEnv(name)
}
}
// LogValue makes an RProxy log as each setting's name with the value it was
// given, or its default.
func (c *RProxy) LogValue() slog.Value {
+36 -30
View File
@@ -10,20 +10,21 @@ import (
"time"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
)
// rproxy's settings, by name, and their values in these tests.
const (
wafBackend = "SWRP_WAF_BACKEND"
rproxyTimeout = "SWRP_TIMEOUT_MS"
rproxyDefault = "SWRP_DEFAULT_DISPOSITION"
rproxyListen = "SWRP_LISTEN_ADDR"
rproxyUpstream = "SWRP_UPSTREAM_URL"
rproxyTrusted = "SWRP_TRUSTED_PROXIES"
rproxyMaxRequest = "SWRP_MAX_REQUEST_SIZE"
rproxyMaxResponse = "SWRP_MAX_RESPONSE_SIZE"
rproxyRequestTime = "SWRP_MAX_REQUEST_TIME_MS"
rproxyAnswerTime = "SWRP_MAX_RESPONSE_TIME_MS"
wafBackend = "SWW_RPROXY_WAF_BACKEND"
rproxyTimeout = "SWW_RPROXY_TIMEOUT_MS"
rproxyDefault = "SWW_RPROXY_DEFAULT_DISPOSITION"
rproxyListen = "SWW_RPROXY_LISTEN_ADDR"
rproxyUpstream = "SWW_RPROXY_UPSTREAM_URL"
rproxyTrusted = "SWW_RPROXY_TRUSTED_PROXIES"
rproxyMaxRequest = "SWW_RPROXY_MAX_REQUEST_SIZE"
rproxyMaxResponse = "SWW_RPROXY_MAX_RESPONSE_SIZE"
rproxyRequestTime = "SWW_RPROXY_MAX_REQUEST_TIME_MS"
rproxyAnswerTime = "SWW_RPROXY_MAX_RESPONSE_TIME_MS"
backendURL = "http://waf:8080"
rproxyDefaultAddr = ":8080"
rproxyDefaultApp = "http://127.0.0.1:8081"
@@ -32,11 +33,7 @@ const (
// rproxyConfig reads rproxy's settings from env.
func rproxyConfig(env map[string]string) (*config.RProxy, error) {
return config.RProxyFromEnvironment(func(name string) (string, bool) {
value, ok := env[name]
return value, ok
})
return config.RProxyFromEnvironment(configtest.Viper(env))
}
func TestRProxyDefaults(t *testing.T) {
@@ -195,32 +192,41 @@ func TestRProxyNeedsItsBackend(t *testing.T) {
}
}
func TestRProxyReadsNoFile(t *testing.T) {
func TestRProxySettingReadFromFile(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "backend")
err := os.WriteFile(path, []byte("http://from-a-file:8080\n"), 0o600)
err := os.WriteFile(path, []byte(backendURL+"\n"), 0o600)
if err != nil {
t.Fatalf("write: %v", err)
}
_, err = rproxyConfig(map[string]string{wafBackend + "_FILE": path})
if err == nil ||
!strings.HasPrefix(err.Error(), wafBackend+": is required") {
t.Errorf("error %v, want %s still required", err, wafBackend)
}
cfg, err := rproxyConfig(map[string]string{
wafBackend: backendURL,
rproxyTimeout + "_FILE": "/nonexistent/timeout",
})
cfg, err := rproxyConfig(map[string]string{wafBackend + "_FILE": path})
if err != nil {
t.Fatalf("settings: %v, with the file left unread", err)
t.Fatalf("settings: %v", err)
}
if cfg.Timeout != 2*time.Second {
t.Errorf("timeout %v, want the default", cfg.Timeout)
if cfg.WAFBackend.String() != backendURL {
t.Errorf("backend %v, want %s", cfg.WAFBackend, backendURL)
}
}
func TestRProxyUnreadableSettingFileStopsTheStart(t *testing.T) {
t.Parallel()
dir := t.TempDir()
for _, path := range []string{filepath.Join(dir, "missing"), dir} {
_, err := rproxyConfig(map[string]string{
wafBackend: backendURL,
rproxyTimeout + "_FILE": path,
})
want := rproxyTimeout + "_FILE: cannot be read: "
if err == nil || !strings.HasPrefix(err.Error(), want) {
t.Errorf("%s: error %v, want one starting %s", path, err, want)
}
}
}
+16
View File
@@ -0,0 +1,16 @@
package config
import "github.com/spf13/viper"
// NewViper returns the viper that reads the settings: each setting's
// environment variable, bound to its own name. Every setting of waf
// and rproxy is read through it, and none is read with os.Getenv or
// os.LookupEnv outside it. An environment variable that is set but empty is
// set, as one that holds a value is, which viper does not take for granted.
func NewViper() *viper.Viper {
v := viper.New()
v.AutomaticEnv()
v.AllowEmptyEnv(true)
return v
}
+34
View File
@@ -0,0 +1,34 @@
package config_test
import (
"testing"
"sneak.berlin/go/smallwebwaf/internal/config"
)
// These tests set real environment variables, so none runs in parallel.
func TestTokenSetEmptyInTheEnvironmentStopsTheStart(t *testing.T) {
t.Setenv(adminToken, "")
_, err := config.FromEnvironment(config.NewViper())
want := adminToken + ": is shorter than 32 characters"
if err == nil || err.Error() != want {
t.Fatalf("error %v, want %s", err, want)
}
}
func TestListSetEmptyInTheEnvironmentReplacesItsDefault(t *testing.T) {
t.Setenv(wafBackend, backendURL)
t.Setenv(rproxyTrusted, "")
cfg, err := config.RProxyFromEnvironment(config.NewViper())
if err != nil {
t.Fatalf("settings: %v", err)
}
if len(cfg.TrustedProxies) != 0 {
t.Errorf("trusted proxies %v, want none", cfg.TrustedProxies)
}
}
+312 -268
View File
File diff suppressed because it is too large. Load diff
+154 -153
View File
@@ -17,108 +17,111 @@ import (
"testing"
"time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/anomaly"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
)
// The settings, by name.
const (
listenAddr = "SWW_LISTEN_ADDR"
mode = "SWW_MODE"
ipv6GroupPrefix = "SWW_IPV6_GROUP_PREFIX"
maxTrackedClients = "SWW_MAX_TRACKED_CLIENTS"
allowNets = "SWW_ALLOW_NETS"
rateLimitExemptNets = "SWW_RATE_LIMIT_EXEMPT_NETS"
denyNets = "SWW_DENY_NETS"
rateLimitPerMinute = "SWW_RATE_LIMIT_PER_MINUTE"
rateLimitPerHour = "SWW_RATE_LIMIT_PER_HOUR"
rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY"
rateLimitExemptPaths = "SWW_RATE_LIMIT_EXEMPT_PATHS"
bytesLimitPerMinute = "SWW_BYTES_LIMIT_PER_MINUTE"
bytesLimitPerHour = "SWW_BYTES_LIMIT_PER_HOUR"
bytesLimitPerDay = "SWW_BYTES_LIMIT_PER_DAY"
bytesCount = "SWW_BYTES_COUNT"
lookupSource = "SWW_LOOKUP_SOURCE"
lookupDBPath = "SWW_LOOKUP_DB_PATH"
lookupTimeout = "SWW_LOOKUP_TIMEOUT"
addLookupHeaders = "SWW_ADD_LOOKUP_HEADERS"
allowedCountries = "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"
tagLists = "SWW_TAG_LISTS"
tagDeny = "SWW_TAG_DENY"
tagLimitPercent = "SWW_TAG_LIMIT_PERCENT"
asnBytesPercent = "SWW_ASN_BYTES_PERCENT"
countryBytesPercent = "SWW_COUNTRY_BYTES_PERCENT"
unknownLimitPercent = "SWW_UNKNOWN_LIMIT_PERCENT"
asnLimitPercentURL = "SWW_ASN_LIMIT_PERCENT_URL"
blocklistURLs = "SWW_BLOCKLIST_URLS"
blocklistRefresh = "SWW_BLOCKLIST_REFRESH"
blocklistAction = "SWW_BLOCKLIST_ACTION"
dnsblZones = "SWW_DNSBL_ZONES"
dnsblResolver = "SWW_DNSBL_RESOLVER"
abuseIPDBKey = "SWW_ABUSEIPDB_KEY"
abuseIPDBMinScore = "SWW_ABUSEIPDB_MIN_SCORE"
abuseIPDBDailyBudget = "SWW_ABUSEIPDB_DAILY_BUDGET"
reputationAction = "SWW_REPUTATION_ACTION"
reputationCacheTTL = "SWW_REPUTATION_CACHE_TTL"
reputationTimeout = "SWW_REPUTATION_TIMEOUT"
routeWatchURL = "SWW_ROUTEWATCH_URL"
crowdSecURL = "SWW_CROWDSEC_LAPI_URL"
crowdSecKey = "SWW_CROWDSEC_LAPI_KEY"
banResponse = "SWW_BAN_RESPONSE"
limitBanDuration = "SWW_LIMIT_BAN_DURATION"
limitBanRepeatWindow = "SWW_LIMIT_BAN_REPEAT_WINDOW"
maxBanDuration = "SWW_MAX_BAN_DURATION"
attackBanDuration = "SWW_ATTACK_BAN_DURATION"
maxBans = "SWW_MAX_BANS"
banScopeV4Prefix = "SWW_BAN_SCOPE_V4_PREFIX"
stateDir = "SWW_STATE_DIR"
stateWriteDelay = "SWW_STATE_WRITE_DELAY"
stateCounterInterval = "SWW_STATE_COUNTER_INTERVAL"
cacheDir = "SWW_CACHE_DIR"
adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name
metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name
metricsTopN = "SWW_METRICS_TOP_N"
instanceName = "SWW_INSTANCE_NAME"
logRequestHeaders = "SWW_LOG_REQUEST_HEADERS"
logLevel = "SWW_LOG_LEVEL"
rulesDir = "SWW_RULES_DIR"
rulesEnabled = "SWW_RULES_ENABLED"
wafMode = "SWW_WAF_MODE"
wafParanoiaLevel = "SWW_WAF_PARANOIA_LEVEL"
wafAnomalyThreshold = "SWW_WAF_ANOMALY_THRESHOLD"
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
exclusionsFile = "SWW_CORE_RULE_SET_EXCLUSIONS_FILE"
apps = "SWW_APPS"
trapPaths = "SWW_TRAP_PATHS"
errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
logRemoteURL = "SWW_LOG_REMOTE_URL"
logRemoteTLSCAFile = "SWW_LOG_REMOTE_TLS_CA_FILE"
logRemoteBuffer = "SWW_LOG_REMOTE_BUFFER"
logRemoteFacility = "SWW_LOG_REMOTE_FACILITY"
logRemoteAppName = "SWW_LOG_REMOTE_APP_NAME"
alertWebhookURL = "SWW_ALERT_WEBHOOK_URL"
alertWebhookHeaders = "SWW_ALERT_WEBHOOK_HEADERS"
alertSlackWebhookURL = "SWW_ALERT_SLACK_WEBHOOK_URL"
alertNtfyURL = "SWW_ALERT_NTFY_URL"
alertNtfyToken = "SWW_ALERT_NTFY_TOKEN" //nolint:gosec // the setting's name
alertEvents = "SWW_ALERT_EVENTS"
alertCooldown = "SWW_ALERT_COOLDOWN"
alertMaxPerHour = "SWW_ALERT_MAX_PER_HOUR"
anomalyNetV4Prefix = "SWW_ANOMALY_NET_V4_PREFIX"
anomalyNetV6Prefix = "SWW_ANOMALY_NET_V6_PREFIX"
watchNets = "SWW_WATCH_NETS"
listenAddr = "SWW_WAF_LISTEN_ADDR"
mode = "SWW_WAF_MODE"
ipv6GroupPrefix = "SWW_WAF_IPV6_GROUP_PREFIX"
maxTrackedClients = "SWW_WAF_MAX_TRACKED_CLIENTS"
allowNets = "SWW_WAF_ALLOW_NETS"
rateLimitExemptNets = "SWW_WAF_RATE_LIMIT_EXEMPT_NETS"
denyNets = "SWW_WAF_DENY_NETS"
rateLimitPerMinute = "SWW_WAF_RATE_LIMIT_PER_MINUTE"
rateLimitPerHour = "SWW_WAF_RATE_LIMIT_PER_HOUR"
rateLimitPerDay = "SWW_WAF_RATE_LIMIT_PER_DAY"
rateLimitExemptPaths = "SWW_WAF_RATE_LIMIT_EXEMPT_PATHS"
bytesLimitPerMinute = "SWW_WAF_BYTES_LIMIT_PER_MINUTE"
bytesLimitPerHour = "SWW_WAF_BYTES_LIMIT_PER_HOUR"
bytesLimitPerDay = "SWW_WAF_BYTES_LIMIT_PER_DAY"
bytesCount = "SWW_WAF_BYTES_COUNT"
lookupSource = "SWW_WAF_LOOKUP_SOURCE"
lookupDBPath = "SWW_WAF_LOOKUP_DB_PATH"
lookupTimeout = "SWW_WAF_LOOKUP_TIMEOUT"
addLookupHeaders = "SWW_WAF_ADD_LOOKUP_HEADERS"
allowedCountries = "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES"
tagLists = "SWW_WAF_TAG_LISTS"
tagDeny = "SWW_WAF_TAG_DENY"
tagLimitPercent = "SWW_WAF_TAG_LIMIT_PERCENT"
asnBytesPercent = "SWW_WAF_ASN_BYTES_PERCENT"
countryBytesPercent = "SWW_WAF_COUNTRY_BYTES_PERCENT"
unknownLimitPercent = "SWW_WAF_UNKNOWN_LIMIT_PERCENT"
asnLimitPercentURL = "SWW_WAF_ASN_LIMIT_PERCENT_URL"
blocklistURLs = "SWW_WAF_BLOCKLIST_URLS"
blocklistRefresh = "SWW_WAF_BLOCKLIST_REFRESH"
blocklistAction = "SWW_WAF_BLOCKLIST_ACTION"
dnsblZones = "SWW_WAF_DNSBL_ZONES"
dnsblResolver = "SWW_WAF_DNSBL_RESOLVER"
abuseIPDBKey = "SWW_WAF_ABUSEIPDB_KEY"
abuseIPDBMinScore = "SWW_WAF_ABUSEIPDB_MIN_SCORE"
abuseIPDBDailyBudget = "SWW_WAF_ABUSEIPDB_DAILY_BUDGET"
reputationAction = "SWW_WAF_REPUTATION_ACTION"
reputationCacheTTL = "SWW_WAF_REPUTATION_CACHE_TTL"
reputationTimeout = "SWW_WAF_REPUTATION_TIMEOUT"
routeWatchURL = "SWW_WAF_ROUTEWATCH_URL"
crowdSecURL = "SWW_WAF_CROWDSEC_LAPI_URL"
crowdSecKey = "SWW_WAF_CROWDSEC_LAPI_KEY"
banResponse = "SWW_WAF_BAN_RESPONSE"
limitBanDuration = "SWW_WAF_LIMIT_BAN_DURATION"
limitBanRepeatWindow = "SWW_WAF_LIMIT_BAN_REPEAT_WINDOW"
maxBanDuration = "SWW_WAF_MAX_BAN_DURATION"
attackBanDuration = "SWW_WAF_ATTACK_BAN_DURATION"
maxBans = "SWW_WAF_MAX_BANS"
banScopeV4Prefix = "SWW_WAF_BAN_SCOPE_V4_PREFIX"
stateDir = "SWW_WAF_STATE_DIR"
stateWriteDelay = "SWW_WAF_STATE_WRITE_DELAY"
stateCounterInterval = "SWW_WAF_STATE_COUNTER_INTERVAL"
cacheDir = "SWW_WAF_CACHE_DIR"
adminToken = "SWW_WAF_ADMIN_TOKEN" //nolint:gosec // the setting's name
metricsToken = "SWW_WAF_METRICS_TOKEN" //nolint:gosec // the setting's name
metricsTopN = "SWW_WAF_METRICS_TOP_N"
instanceName = "SWW_WAF_INSTANCE_NAME"
logRequestHeaders = "SWW_WAF_LOG_REQUEST_HEADERS"
logLevel = "SWW_WAF_LOG_LEVEL"
rulesDir = "SWW_WAF_RULES_DIR"
rulesEnabled = "SWW_WAF_RULES_ENABLED"
wafMode = "SWW_WAF_CORE_RULE_SET_MODE"
wafParanoiaLevel = "SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL"
wafAnomalyThreshold = "SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD"
wafDisabledRules = "SWW_WAF_CORE_RULE_SET_DISABLED_RULES"
wafExemptPaths = "SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS"
wafBodyLimit = "SWW_WAF_CORE_RULE_SET_BODY_LIMIT"
exclusionsFile = "SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE"
apps = "SWW_WAF_APPS"
trapPaths = "SWW_WAF_TRAP_PATHS"
errorBurstThreshold = "SWW_WAF_ERROR_BURST_THRESHOLD"
logRemoteURL = "SWW_WAF_LOG_REMOTE_URL"
logRemoteTLSCAFile = "SWW_WAF_LOG_REMOTE_TLS_CA_FILE"
logRemoteBuffer = "SWW_WAF_LOG_REMOTE_BUFFER"
logRemoteFacility = "SWW_WAF_LOG_REMOTE_FACILITY"
logRemoteAppName = "SWW_WAF_LOG_REMOTE_APP_NAME"
alertWebhookURL = "SWW_WAF_ALERT_WEBHOOK_URL"
alertWebhookHeaders = "SWW_WAF_ALERT_WEBHOOK_HEADERS"
alertSlackWebhookURL = "SWW_WAF_ALERT_SLACK_WEBHOOK_URL"
alertNtfyURL = "SWW_WAF_ALERT_NTFY_URL"
alertNtfyToken = "SWW_WAF_ALERT_NTFY_TOKEN" //nolint:gosec // the setting's name
alertEvents = "SWW_WAF_ALERT_EVENTS"
alertCooldown = "SWW_WAF_ALERT_COOLDOWN"
alertMaxPerHour = "SWW_WAF_ALERT_MAX_PER_HOUR"
anomalyNetV4Prefix = "SWW_WAF_ANOMALY_NET_V4_PREFIX"
anomalyNetV6Prefix = "SWW_WAF_ANOMALY_NET_V6_PREFIX"
watchNets = "SWW_WAF_WATCH_NETS"
)
// The anomaly thresholds: each of the prefixes below, which name a scope,
// followed by each of the four ends.
const (
anomalyClient = "SWW_ANOMALY_CLIENT_"
anomalyNet = "SWW_ANOMALY_NET_"
anomalyASN = "SWW_ANOMALY_ASN_"
anomalyTotal = "SWW_ANOMALY_TOTAL_"
watch = "SWW_WATCH_"
anomalyClient = "SWW_WAF_ANOMALY_CLIENT_"
anomalyNet = "SWW_WAF_ANOMALY_NET_"
anomalyASN = "SWW_WAF_ANOMALY_ASN_"
anomalyTotal = "SWW_WAF_ANOMALY_TOTAL_"
watch = "SWW_WAF_WATCH_"
requestsPerMinute = "REQUESTS_PER_MINUTE"
requestsPerHour = "REQUESTS_PER_HOUR"
@@ -170,21 +173,21 @@ func loggedAnomalyDefaults() map[string]string {
return logged
}
// defaultAlertEvents is the default of SWW_ALERT_EVENTS, and
// defaultAlertCooldown that of SWW_ALERT_COOLDOWN.
// defaultAlertEvents is the default of SWW_WAF_ALERT_EVENTS, and
// defaultAlertCooldown that of SWW_WAF_ALERT_COOLDOWN.
const (
defaultAlertEvents = "ban,permanent_ban,core_rule_set_match,anomaly,reputation_hit," +
"source_failure,file_error"
defaultAlertCooldown = "15m"
)
// defaultReputationCacheTTL is the default of SWW_REPUTATION_CACHE_TTL.
// defaultReputationCacheTTL is the default of SWW_WAF_REPUTATION_CACHE_TTL.
const defaultReputationCacheTTL = "24h"
// defaultWAFDisabledRules is the default of SWW_WAF_DISABLED_RULES.
// defaultWAFDisabledRules is the default of SWW_WAF_CORE_RULE_SET_DISABLED_RULES.
const defaultWAFDisabledRules = "920340,920420,920440,920640,930130,930140"
// defaultLogRequestHeaders is the default of SWW_LOG_REQUEST_HEADERS.
// defaultLogRequestHeaders is the default of SWW_WAF_LOG_REQUEST_HEADERS.
const defaultLogRequestHeaders = "accept,accept-language,accept-encoding," +
"content-type,origin,range"
@@ -209,8 +212,8 @@ const (
otherToken = "fedcba9876543210fedcba9876543210"
)
// instance is an SWW_INSTANCE_NAME that is a valid app name too, and
// remoteURL an SWW_LOG_REMOTE_URL, for the tests that send the lines.
// instance is an SWW_WAF_INSTANCE_NAME that is a valid app name too, and
// remoteURL an SWW_WAF_LOG_REMOTE_URL, for the tests that send the lines.
const (
instance = "fsn1app1/blog"
remoteURL = "syslog+udp://192.0.2.1:514"
@@ -225,7 +228,7 @@ const enabled = "true"
// disabled is false, as a setting's value.
const disabled = "false"
// defaultLookupSource is the default of SWW_LOOKUP_SOURCE, and
// defaultLookupSource is the default of SWW_WAF_LOOKUP_SOURCE, and
// fileSource the source that is the lookup database.
const (
defaultLookupSource = "geojs"
@@ -235,18 +238,16 @@ const (
// environment is a set of environment variables, for FromEnvironment.
type environment map[string]string
// lookupEnv reads one of the variables, as os.LookupEnv does.
func (e environment) lookupEnv(name string) (string, bool) {
value, ok := e[name]
return value, ok
// viper returns a viper holding each of the variables.
func (e environment) viper() *viper.Viper {
return configtest.Viper(e)
}
// fromEnvironment reads the settings from env, which must be valid.
func fromEnvironment(t *testing.T, env environment) *config.Config {
t.Helper()
cfg, err := config.FromEnvironment(env.lookupEnv)
cfg, err := config.FromEnvironment(env.viper())
if err != nil {
t.Fatalf("settings %v: %v", env, err)
}
@@ -512,7 +513,7 @@ func TestPathPrefixNotStartingWithSlashStopsTheStart(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(
environment{rateLimitExemptPaths: "/favicon.ico,assets/"}.lookupEnv)
environment{rateLimitExemptPaths: "/favicon.ico,assets/"}.viper())
want := rateLimitExemptPaths + `: "assets/" is not a path prefix ` +
`starting with /, such as /assets/`
@@ -574,7 +575,7 @@ func TestInvalidTrapPathOrErrorBurstThresholdStopsTheStart(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(
environment{tc.name: tc.value}.lookupEnv,
environment{tc.name: tc.value}.viper(),
)
want := tc.name + ": " + tc.want
@@ -640,7 +641,7 @@ func TestCoreRuleSetSettings(t *testing.T) {
}
}
// gitea is gitea's name in SWW_APPS.
// gitea is gitea's name in SWW_WAF_APPS.
const gitea = "gitea"
func TestApps(t *testing.T) {
@@ -670,7 +671,7 @@ func TestApps(t *testing.T) {
}
}
// Unset and set empty, SWW_APPS has opposite effects, so it logs differently.
// Unset and set empty, SWW_WAF_APPS has opposite effects, so it logs differently.
func TestUnsetAndEmptyAppsAreLoggedDifferently(t *testing.T) {
t.Parallel()
@@ -731,7 +732,7 @@ func TestInvalidAppsStopsTheStart(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(
environment{apps: tc.value}.lookupEnv,
environment{apps: tc.value}.viper(),
)
want := apps + ": " + tc.want
@@ -807,7 +808,7 @@ func TestInvalidCoreRuleSetSettingStopsTheStart(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(
environment{tc.name: tc.value}.lookupEnv,
environment{tc.name: tc.value}.viper(),
)
want := tc.name + ": " + tc.want
@@ -958,7 +959,7 @@ func TestInvalidRemoteLogSettingStopsTheStart(t *testing.T) {
{logRemoteAppName, "gitéa"}, {logRemoteAppName, strings.Repeat("a", 49)},
} {
_, err := config.FromEnvironment(
environment{tc.name: tc.value}.lookupEnv,
environment{tc.name: tc.value}.viper(),
)
if err == nil || !strings.HasPrefix(err.Error(), tc.name+": ") {
t.Errorf(
@@ -982,7 +983,7 @@ func TestRemoteLogCAFileWithoutCertificateStopsTheStart(t *testing.T) {
}
_, err = config.FromEnvironment(
environment{logRemoteTLSCAFile: caFile}.lookupEnv,
environment{logRemoteTLSCAFile: caFile}.viper(),
)
want := logRemoteTLSCAFile + `: "` + caFile + `" holds no PEM certificate`
@@ -1000,7 +1001,7 @@ func TestInstanceNameNotAnAppNameStopsTheStartOnlyWhileSending(
sending := environment{logRemoteURL: remoteURL, instanceName: spaced}
_, err := config.FromEnvironment(sending.lookupEnv)
_, err := config.FromEnvironment(sending.viper())
want := logRemoteAppName + `: is unset, and ` + instanceName +
` "fsn1 app1", its default, is not 1 to 48 printable ASCII characters ` +
@@ -1034,7 +1035,7 @@ func TestAppNameSetStopsTheStartWhileSending(t *testing.T) {
logRemoteURL: remoteURL,
instanceName: instance,
logRemoteAppName: "my app",
}.lookupEnv)
}.viper())
want := logRemoteAppName + `: "my app" is not 1 to 48 printable ASCII ` +
`characters without a space, such as blog`
@@ -1196,7 +1197,7 @@ func TestWebhookHeadersAreLoggedMaskedAndNeverShown(t *testing.T) {
// An item that is not a header is named by its place, not shown.
_, err := config.FromEnvironment(environment{
alertWebhookHeaders: "X-Team:ops," + secret,
}.lookupEnv)
}.viper())
want := alertWebhookHeaders + ": item 2 is not a header name followed by : " +
"and the header's value, such as Authorization:Bearer <token>"
@@ -1235,7 +1236,7 @@ func TestWebhookURLIsLoggedWithoutItsPathOrQueryAndNeverShown(t *testing.T) {
"https://hooks.example/services/%zz" + secret,
} {
_, err := config.FromEnvironment(
environment{alertWebhookURL: value}.lookupEnv,
environment{alertWebhookURL: value}.viper(),
)
want := alertWebhookURL + ": is not an http or https URL without a user or " +
@@ -1290,7 +1291,7 @@ func TestNtfyTokenWithAControlCharacterStopsTheStartWithoutShowingIt(
"set": {alertNtfyToken: token + "\r"},
"in a file": {alertNtfyToken + "_FILE": writeFile(t, token+"\r\n")},
} {
_, err := config.FromEnvironment(env.lookupEnv)
_, err := config.FromEnvironment(env.viper())
want := alertNtfyToken + ": holds a control character, such as the " +
"carriage return of a Windows line end"
@@ -1309,7 +1310,7 @@ func TestInstanceNameWithAControlCharacterStopsTheStartOnlyWithNtfySet(
_, err := config.FromEnvironment(environment{
instanceName: name, alertNtfyURL: "https://ntfy.example/sww-alerts",
}.lookupEnv)
}.viper())
want := instanceName + `: "fsn1app1\r" holds a control character, such as the ` +
`carriage return of a Windows line end, and is sent to ntfy in a header ` +
@@ -1338,7 +1339,7 @@ func TestInstanceNameNotUTF8StopsTheStart(t *testing.T) {
"set": {instanceName: latin1},
"in a file": {instanceName + "_FILE": writeFile(t, latin1+"\n")},
} {
_, err := config.FromEnvironment(env.lookupEnv)
_, err := config.FromEnvironment(env.viper())
want := instanceName + `: "caf\xe9" is not valid UTF-8`
if err == nil || err.Error() != want {
@@ -1353,10 +1354,10 @@ func TestCodeInAllowedCountriesAndTagDenyStopsTheStart(t *testing.T) {
_, err := config.FromEnvironment(environment{
tagDeny: "country=cn,country=ru",
allowedCountries: "de,RU",
}.lookupEnv)
}.viper())
if err == nil {
t.Fatal(
"ru in SWW_EXCLUSIVELY_ALLOWED_COUNTRIES and SWW_TAG_DENY was accepted",
"ru in SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES and SWW_WAF_TAG_DENY was accepted",
)
}
@@ -1420,7 +1421,7 @@ func TestLookupDBPathGoesWithTheFileSourceAlone(t *testing.T) {
lookupDBPath + ": is set while " + lookupSource + " is off; only file reads it",
},
} {
_, err := config.FromEnvironment(tc.env.lookupEnv)
_, err := config.FromEnvironment(tc.env.viper())
if err == nil || err.Error() != tc.want {
t.Errorf("settings %v: error %v, want %s", tc.env, err, tc.want)
}
@@ -1449,7 +1450,7 @@ func TestSettingNeedingLookupsStopsTheStartWhileTheyAreOff(t *testing.T) {
_, err := config.FromEnvironment(environment{
lookupSource: off, name: value,
}.lookupEnv)
}.viper())
if err == nil || !strings.HasPrefix(err.Error(), name+": ") ||
!strings.Contains(err.Error(), lookupSource+" is off") {
t.Errorf(
@@ -1463,7 +1464,7 @@ func TestSettingNeedingLookupsStopsTheStartWhileTheyAreOff(t *testing.T) {
}
// Set empty, the lists need nothing looked up, and nor does
// SWW_UNKNOWN_LIMIT_PERCENT at 100, which lowers no limit, a tag the
// SWW_WAF_UNKNOWN_LIMIT_PERCENT at 100, which lowers no limit, a tag the
// lookup does not give, an anomaly threshold per AS number that is off,
// any other anomaly threshold, or a blocklist.
env := environment{
@@ -1622,7 +1623,7 @@ func TestInvalidAnomalySettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(
environment{tc.name: tc.value}.lookupEnv,
environment{tc.name: tc.value}.viper(),
)
want := tc.name + ": " + tc.want
@@ -1731,7 +1732,7 @@ func TestRouteWatchURLAndPrefixTags(t *testing.T) {
{environment{lookupSource: off, routeWatchURL: rw, tagDeny: "country=DE"}, tagDeny},
{environment{lookupSource: off, tagDeny: "asn=64496"}, tagDeny},
} {
_, err := config.FromEnvironment(stopped.env.lookupEnv)
_, err := config.FromEnvironment(stopped.env.viper())
if err == nil || !strings.HasPrefix(err.Error(), stopped.name+": ") {
t.Errorf(
"%v gave the error %v, want one naming %s",
@@ -1743,7 +1744,7 @@ func TestRouteWatchURLAndPrefixTags(t *testing.T) {
}
badURL := environment{routeWatchURL: "ftp://routewatch.example"}
_, err := config.FromEnvironment(badURL.lookupEnv)
_, err := config.FromEnvironment(badURL.viper())
want := routeWatchURL + `: "ftp://routewatch.example" is not an http or https URL ` +
"without a user or a fragment, such as https://routewatch.example"
@@ -1798,7 +1799,7 @@ func TestTagThatNoRequestCanGetIsToldApartFromOneItCan(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(tc.env.lookupEnv)
_, err := config.FromEnvironment(tc.env.viper())
switch {
case tc.want == "" && err != nil:
@@ -1837,7 +1838,7 @@ func TestErrorOfATagSettingLeavesOutAZoneKey(t *testing.T) {
{tagLimitPercent, tag + ":50,,", emptyItem},
} {
_, err := config.FromEnvironment(
environment{tc.name: tc.value}.lookupEnv,
environment{tc.name: tc.value}.viper(),
)
want := tc.name + ": " + tc.want
@@ -1866,7 +1867,7 @@ func TestTagListURLFetchedAsAnotherListStopsTheStart(t *testing.T) {
{"the file of AS:percent lines", asnURL, environment{asnLimitPercentURL: asnURL}},
{
"the CrowdSec decision list", lapi + "/v1/decisions",
environment{"SWW_CROWDSEC_LAPI_URL": lapi, "SWW_CROWDSEC_LAPI_KEY": "key"},
environment{"SWW_WAF_CROWDSEC_LAPI_URL": lapi, "SWW_WAF_CROWDSEC_LAPI_KEY": "key"},
},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -1874,10 +1875,10 @@ func TestTagListURLFetchedAsAnotherListStopsTheStart(t *testing.T) {
tc.env[tagLists] = "dup=" + tc.listURL
want := tagLists + `: "` + tc.listURL + `" is in SWW_BLOCKLIST_URLS, is ` +
"SWW_ASN_LIMIT_PERCENT_URL or is the CrowdSec decision list too"
want := tagLists + `: "` + tc.listURL + `" is in SWW_WAF_BLOCKLIST_URLS, is ` +
"SWW_WAF_ASN_LIMIT_PERCENT_URL or is the CrowdSec decision list too"
_, err := config.FromEnvironment(tc.env.lookupEnv)
_, err := config.FromEnvironment(tc.env.viper())
if err == nil || err.Error() != want {
t.Errorf("error %v, want %s", err, want)
}
@@ -1955,7 +1956,7 @@ func TestInvalidBiasedThresholdStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(
environment{tc.name: tc.value}.lookupEnv,
environment{tc.name: tc.value}.viper(),
)
want := tc.name + ": " + tc.want
@@ -1974,7 +1975,7 @@ const (
asnURL = "https://lists.example/asn.txt"
)
// The actions of SWW_BLOCKLIST_ACTION, as Config gives them.
// The actions of SWW_WAF_BLOCKLIST_ACTION, as Config gives them.
const (
actionDeny = "deny"
actionLimit = "limit"
@@ -2071,7 +2072,7 @@ func TestInvalidReputationSettingStopsTheStartSayingWhatIsWrong(
t.Parallel()
_, err := config.FromEnvironment(
environment{tc.name: tc.value}.lookupEnv,
environment{tc.name: tc.value}.viper(),
)
want := tc.name + ": " + tc.want
@@ -2087,9 +2088,9 @@ func TestASNLimitPercentURLThatIsABlocklistStopsTheStart(t *testing.T) {
_, err := config.FromEnvironment(environment{
blocklistURLs: dropURL + "," + asnURL, asnLimitPercentURL: asnURL,
}.lookupEnv)
}.viper())
want := asnLimitPercentURL + `: "` + asnURL + `" is in SWW_BLOCKLIST_URLS too`
want := asnLimitPercentURL + `: "` + asnURL + `" is in SWW_WAF_BLOCKLIST_URLS too`
if err == nil || err.Error() != want {
t.Errorf("error %v, want %s", err, want)
}
@@ -2240,7 +2241,7 @@ func TestInvalidDNSBLSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(
environment{tc.name: tc.value}.lookupEnv,
environment{tc.name: tc.value}.viper(),
)
want := tc.name + ": " + tc.want
@@ -2319,7 +2320,7 @@ func TestDNSBLZoneKeyIsLoggedMaskedAndNeverShown(t *testing.T) {
{spamhaus + ",,", "has an empty item in its list"},
} {
_, err := config.FromEnvironment(
environment{dnsblZones: tc.value}.lookupEnv,
environment{dnsblZones: tc.value}.viper(),
)
want := dnsblZones + ": " + tc.want
@@ -2391,7 +2392,7 @@ func TestInvalidAbuseIPDBSettingStopsTheStartSayingWhatIsWrong(
t.Parallel()
_, err := config.FromEnvironment(
environment{tc.name: tc.value}.lookupEnv,
environment{tc.name: tc.value}.viper(),
)
want := tc.name + ": " + tc.want
@@ -2463,7 +2464,7 @@ func TestInvalidCrowdSecSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
notLAPIURL = " is not an http or https URL without a user or a fragment, " +
"such as http://172.17.0.1:8080"
anotherList = `gives the decision list "` + lapi + `/v1/decisions", which is in ` +
`SWW_BLOCKLIST_URLS or is SWW_ASN_LIMIT_PERCENT_URL too`
`SWW_WAF_BLOCKLIST_URLS or is SWW_WAF_ASN_LIMIT_PERCENT_URL too`
)
for _, tc := range []struct {
@@ -2520,7 +2521,7 @@ func TestInvalidCrowdSecSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(tc.env.lookupEnv)
_, err := config.FromEnvironment(tc.env.viper())
if err == nil || err.Error() != tc.want {
t.Errorf("error %v, want %s", err, tc.want)
}
@@ -2714,14 +2715,14 @@ func TestInvalidLookupTagOrLogValueStopsTheStart(t *testing.T) {
// wantExploitTagStopsTheStartWithRulesAndCoreRuleSetOff checks that with rule
// files off, and either setting turning off the Core Rule Set, the tag
// exploitsignature in SWW_TAG_DENY stops the start.
// exploitsignature in SWW_WAF_TAG_DENY stops the start.
func wantExploitTagStopsTheStartWithRulesAndCoreRuleSetOff(t *testing.T) {
t.Helper()
for _, name := range []string{wafMode, wafAnomalyThreshold} {
_, err := config.FromEnvironment(environment{
tagDeny: exploitTag, rulesEnabled: disabled, name: off,
}.lookupEnv)
}.viper())
if err == nil || !strings.HasPrefix(err.Error(), tagDeny+": ") {
t.Errorf(
"%s=off with rule files off gave the error %v, want one naming %s",
@@ -2769,7 +2770,7 @@ func wantStartStopped(t *testing.T, invalid []struct{ name, value string }) {
t.Parallel()
_, err := config.FromEnvironment(
environment{tc.name: tc.value}.lookupEnv,
environment{tc.name: tc.value}.viper(),
)
if err == nil {
t.Fatalf("%s=%q was accepted", tc.name, tc.value)
@@ -2796,7 +2797,7 @@ func TestHostOrTransferEncodingStopsTheStart(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(
environment{logRequestHeaders: value}.lookupEnv,
environment{logRequestHeaders: value}.viper(),
)
if err == nil || err.Error() != logRequestHeaders+": "+want {
t.Errorf(
@@ -2820,7 +2821,7 @@ func TestShortTokenStopsTheStartWithoutShowingIt(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(
environment{name: value}.lookupEnv,
environment{name: value}.viper(),
)
want := name + ": is shorter than 32 characters"
@@ -2887,7 +2888,7 @@ func TestSettingFromFileIsCheckedAsTheSettingItself(t *testing.T) {
_, err := config.FromEnvironment(environment{
bytesLimitPerMinute + "_FILE": writeFile(t, "lots\n"),
}.lookupEnv)
}.viper())
want := bytesLimitPerMinute + `: "lots" is not a size such as 512K, 100M or 5G, or off`
if err == nil || err.Error() != want {
@@ -2898,7 +2899,7 @@ func TestSettingFromFileIsCheckedAsTheSettingItself(t *testing.T) {
logRemoteURL: remoteURL,
instanceName: instance,
logRemoteAppName + "_FILE": writeFile(t, "my app\n"),
}.lookupEnv)
}.viper())
want = logRemoteAppName + `: "my app" is not 1 to 48 printable ASCII ` +
`characters without a space, such as blog`
@@ -2913,7 +2914,7 @@ func TestSettingAndItsFileBothSetStopsTheStart(t *testing.T) {
_, err := config.FromEnvironment(environment{
metricsToken: token,
metricsToken + "_FILE": writeFile(t, token),
}.lookupEnv)
}.viper())
want := metricsToken + ": is set, and so is " + metricsToken +
"_FILE; set only one of them"
@@ -2929,7 +2930,7 @@ func TestUnreadableSettingFileStopsTheStart(t *testing.T) {
for _, path := range []string{filepath.Join(dir, "missing"), dir} {
_, err := config.FromEnvironment(
environment{metricsToken + "_FILE": path}.lookupEnv,
environment{metricsToken + "_FILE": path}.viper(),
)
want := metricsToken + "_FILE: cannot be read: "