Rename the settings whose names do not say what they control (closes #234)
check / check (push) Waiting to run

Applies the setting renames of the naming audit under the SWW_WAF_ and
SWW_RPROXY_ prefixes, with sneak's names for the IP reputation cache
time and the two time limits, which now take milliseconds. The rproxy
log field default_disposition becomes waf_backend_no_answer, and
config.Config and config.FromEnvironment become config.WAF and
config.WAFFromEnvironment. Go fields, test names and prose follow. No
aliases; nothing reads the old names.

Model: opus-5-5
This commit is contained in:
clawbot committed 2026-11-12 09:48:09 +00:00
1 parent 3304beca46
commit 4d4ae61abf
100 files changed
+2261 -2019

No files matched your search

+32 -5
View File
@@ -16,6 +16,7 @@ import (
"os"
"strconv"
"strings"
"time"
"github.com/spf13/viper"
)
@@ -36,7 +37,9 @@ const (
var (
errNotSize = errors.New(
"is not a size such as 512K, 100M or 5G, or off")
errNotPositive = errors.New("must be more than zero, or off")
errNotPositive = errors.New("must be more than zero, or off")
errNotMilliseconds = errors.New(
"is not a whole number of milliseconds above zero, such as 2000")
errEmptyItem = errors.New("has an empty item in its list")
errNotNetblock = errors.New(
"is not a netblock such as 10.0.0.0/8, or an address")
@@ -62,10 +65,10 @@ func Command(v *viper.Viper) (string, error) {
}
}
// environment is where FromEnvironment reads the settings: it notes each
// 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.
// environment is where WAFFromEnvironment and RProxyFromEnvironment read the
// settings: it notes each 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 {
v *viper.Viper
settings []slog.Attr
@@ -162,6 +165,19 @@ func (e *environment) size(name, defaultValue string) int64 {
return size
}
// milliseconds reads a setting that is a whole number of milliseconds above
// zero.
func (e *environment) milliseconds(name, defaultValue string) time.Duration {
value := e.value(name, defaultValue)
duration, valid := parseMilliseconds(value)
if !valid {
e.check(name, fmt.Errorf("%q %w", value, errNotMilliseconds))
}
return duration
}
// parseSize reads a number of bytes with an optional K, M or G suffix, in
// powers of 1024 (1K is 1024 bytes), or off.
func parseSize(value string) (int64, error) {
@@ -198,6 +214,17 @@ func splitUnit(value string) (string, int64) {
}
}
// parseMilliseconds reads a whole number of milliseconds above zero, and
// reports whether it is one.
func parseMilliseconds(value string) (time.Duration, bool) {
n, err := strconv.ParseInt(value, 10, 64)
if err != nil || n <= 0 || n > math.MaxInt64/int64(time.Millisecond) {
return 0, false
}
return time.Duration(n) * time.Millisecond, true
}
// parseList splits a comma-separated list and trims the spaces around
// each item. An empty value is an empty list.
func parseList(value string) ([]string, error) {
+16 -38
View File
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"log/slog"
"math"
"net/netip"
"net/url"
"strconv"
@@ -25,13 +24,15 @@ type RProxy struct {
TrustedProxies []netip.Prefix
// 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 (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 (SWW_RPROXY_DEFAULT_DISPOSITION).
DefaultDisposition string
// WAFBackendTimeout is how long rproxy waits for waf's answer to a
// question, and to a report (SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS). It
// cannot be off.
WAFBackendTimeout time.Duration
// WAFBackendNoAnswerDisposition is what is done with a request when waf
// cannot be connected to, does not answer at all, or does not answer
// within WAFBackendTimeout: allow or deny
// (SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION).
WAFBackendNoAnswerDisposition string
// MaxRequestSize is the largest request body (SWW_RPROXY_MAX_REQUEST_SIZE), and
// MaxResponseSize the largest response body (SWW_RPROXY_MAX_RESPONSE_SIZE).
MaxRequestSize int64
@@ -62,9 +63,7 @@ var (
"such as http://127.0.0.1:8081")
errNeedsWAFBackend = errors.New(
"is required: it is waf's URL, such as http://waf:8080")
errNotDisposition = errors.New("is not allow or deny")
errNotMilliseconds = errors.New(
"is not a whole number of milliseconds above zero, such as 2000")
errNotDisposition = errors.New("is not allow or deny")
errNotMillisecondsOrOff = errors.New(
"is not a whole number of milliseconds above zero, such as 60000, or off",
)
@@ -89,9 +88,12 @@ func RProxyFromEnvironment(v *viper.Viper) (*RProxy, error) {
privateRanges,
),
WAFBackend: env.wafBackend("SWW_RPROXY_WAF_BACKEND"),
Timeout: env.milliseconds("SWW_RPROXY_TIMEOUT_MS", "2000"),
DefaultDisposition: env.disposition(
"SWW_RPROXY_DEFAULT_DISPOSITION",
WAFBackendTimeout: env.milliseconds(
"SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS",
"2000",
),
WAFBackendNoAnswerDisposition: env.disposition(
"SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION",
"allow",
),
MaxRequestSize: env.size("SWW_RPROXY_MAX_REQUEST_SIZE", "100M"),
@@ -173,19 +175,6 @@ func (e *environment) disposition(name, defaultValue string) string {
return value
}
// milliseconds reads a setting that is a whole number of milliseconds above
// zero.
func (e *environment) milliseconds(name, defaultValue string) time.Duration {
value := e.value(name, defaultValue)
duration, valid := parseMilliseconds(value)
if !valid {
e.check(name, fmt.Errorf("%q %w", value, errNotMilliseconds))
}
return duration
}
// millisecondsOrOff reads a setting that is a whole number of milliseconds
// above zero, or off, which is 0.
func (e *environment) millisecondsOrOff(
@@ -204,17 +193,6 @@ func (e *environment) millisecondsOrOff(
return duration
}
// parseMilliseconds reads a whole number of milliseconds above zero, and
// reports whether it is one.
func parseMilliseconds(value string) (time.Duration, bool) {
n, err := strconv.ParseInt(value, 10, 64)
if err != nil || n <= 0 || n > math.MaxInt64/int64(time.Millisecond) {
return 0, false
}
return time.Duration(n) * time.Millisecond, true
}
// parseUpstreamURL reads the app's URL: http or https, a host and an
// optional port from 1 to 65535, and nothing else, since the request's
// own path and query go to the app unchanged.
+37 -35
View File
@@ -15,20 +15,20 @@ import (
// rproxy's settings, by name, and their values in these tests.
const (
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"
notASize = "a lot"
wafBackend = "SWW_RPROXY_WAF_BACKEND"
wafBackendTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
rproxyNoAnswer = "SWW_RPROXY_WAF_BACKEND_NO_ANSWER_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"
rproxyNoAnswerAddr = ":8080"
rproxyNoAnswerApp = "http://127.0.0.1:8081"
notASize = "a lot"
)
// rproxyConfig reads rproxy's settings from env.
@@ -44,8 +44,8 @@ func TestRProxyDefaults(t *testing.T) {
t.Fatalf("settings: %v", err)
}
if cfg.ListenAddr != rproxyDefaultAddr ||
cfg.UpstreamURL.String() != rproxyDefaultApp ||
if cfg.ListenAddr != rproxyNoAnswerAddr ||
cfg.UpstreamURL.String() != rproxyNoAnswerApp ||
cfg.WAFBackend.String() != backendURL {
t.Errorf(
"addresses %q, %v, %v",
@@ -55,11 +55,12 @@ func TestRProxyDefaults(t *testing.T) {
)
}
if cfg.Timeout != 2*time.Second || cfg.DefaultDisposition != "allow" {
if cfg.WAFBackendTimeout != 2*time.Second ||
cfg.WAFBackendNoAnswerDisposition != "allow" {
t.Errorf(
"timeout %v, default disposition %q",
cfg.Timeout,
cfg.DefaultDisposition,
"waf backend timeout %v, no-answer disposition %q",
cfg.WAFBackendTimeout,
cfg.WAFBackendNoAnswerDisposition,
)
}
@@ -90,8 +91,8 @@ func TestRProxySettingsAreRead(t *testing.T) {
rproxyListen: ":9000",
rproxyUpstream: "http://app:3000",
rproxyTrusted: "192.0.2.0/24",
rproxyTimeout: "150",
rproxyDefault: "deny",
wafBackendTimeout: "150",
rproxyNoAnswer: "deny",
rproxyMaxRequest: "1K",
rproxyMaxResponse: off,
rproxyRequestTime: off,
@@ -107,8 +108,8 @@ func TestRProxySettingsAreRead(t *testing.T) {
t.Errorf("addresses %+v", cfg)
}
if cfg.Timeout != 150*time.Millisecond ||
cfg.DefaultDisposition != "deny" ||
if cfg.WAFBackendTimeout != 150*time.Millisecond ||
cfg.WAFBackendNoAnswerDisposition != "deny" ||
cfg.MaxRequestSize != 1024 ||
cfg.MaxResponseSize != 0 ||
cfg.MaxRequestTime != 0 ||
@@ -126,13 +127,13 @@ func TestRProxyInvalidSettingsAreRefused(t *testing.T) {
{wafBackend, ""},
{wafBackend, "waf:8080"},
{wafBackend, "http://waf:8080/path"},
{rproxyTimeout, "0"},
{rproxyTimeout, "-1"},
{rproxyTimeout, off},
{rproxyTimeout, "2s"},
{rproxyTimeout, "1.25"},
{rproxyTimeout, "99999999999999999"},
{rproxyDefault, "maybe"},
{wafBackendTimeout, "0"},
{wafBackendTimeout, "-1"},
{wafBackendTimeout, off},
{wafBackendTimeout, "2s"},
{wafBackendTimeout, "1.25"},
{wafBackendTimeout, "99999999999999999"},
{rproxyNoAnswer, "maybe"},
{rproxyListen, "8080"},
{rproxyUpstream, "app"},
{rproxyUpstream, "127.0.0.1:8081"},
@@ -219,11 +220,11 @@ func TestRProxyUnreadableSettingFileStopsTheStart(t *testing.T) {
for _, path := range []string{filepath.Join(dir, "missing"), dir} {
_, err := rproxyConfig(map[string]string{
wafBackend: backendURL,
rproxyTimeout + "_FILE": path,
wafBackend: backendURL,
wafBackendTimeout + "_FILE": path,
})
want := rproxyTimeout + "_FILE: cannot be read: "
want := wafBackendTimeout + "_FILE: cannot be read: "
if err == nil || !strings.HasPrefix(err.Error(), want) {
t.Errorf("%s: error %v, want one starting %s", path, err, want)
}
@@ -246,7 +247,8 @@ func TestRProxySettingsAreLoggedByName(t *testing.T) {
}
for _, name := range []string{
rproxyListen, rproxyUpstream, rproxyTrusted, wafBackend, rproxyTimeout, rproxyDefault,
rproxyListen, rproxyUpstream, rproxyTrusted, wafBackend, wafBackendTimeout,
rproxyNoAnswer,
rproxyMaxRequest, rproxyMaxResponse, rproxyRequestTime, rproxyAnswerTime,
} {
if !slices.Contains(names, name) {
+1 -1
View File
@@ -11,7 +11,7 @@ import (
func TestTokenSetEmptyInTheEnvironmentStopsTheStart(t *testing.T) {
t.Setenv(adminToken, "")
_, err := config.FromEnvironment(config.NewViper())
_, err := config.WAFFromEnvironment(config.NewViper())
want := adminToken + ": is shorter than 32 characters"
if err == nil || err.Error() != want {
+231 -198
View File
@@ -27,9 +27,8 @@ import (
"sneak.berlin/go/smallwebwaf/internal/remotelog"
)
// Config is the settings of `sww waf`. A time or size limit of
// zero is off.
type Config struct {
// WAF is the settings of `sww waf`. A time or size limit of zero is off.
type WAF struct {
// ListenAddr is where waf listens (SWW_WAF_LISTEN_ADDR).
ListenAddr string
// InstanceName is the name every log line and alert gives as instance,
@@ -70,25 +69,26 @@ type Config struct {
// BytesLimitPerMinute, BytesLimitPerHour and BytesLimitPerDay are the
// most bytes a client's requests may carry in a minute, an hour and a
// day (SWW_WAF_BYTES_LIMIT_PER_MINUTE, SWW_WAF_BYTES_LIMIT_PER_HOUR and
// SWW_WAF_BYTES_LIMIT_PER_DAY). BytesCount is which body bytes count
// toward them (SWW_WAF_BYTES_COUNT): response, request or both.
// SWW_WAF_BYTES_LIMIT_PER_DAY). BytesCounted is which body bytes count
// toward them and the byte anomaly thresholds (SWW_WAF_BYTES_COUNTED):
// response, request or both.
BytesLimitPerMinute int64
BytesLimitPerHour int64
BytesLimitPerDay int64
BytesCount string
// LookupSource is where each client's AS number and country are
// looked up (SWW_WAF_LOOKUP_SOURCE): geojs, file, or off for nowhere.
// LookupDBPath is the lookup database, the IPinfo Lite file looked up
// in while LookupSource is file (SWW_WAF_LOOKUP_DB_PATH), and "" for any
// other source. A request waits up to LookupTimeout for its client's
// first answer from GeoJS while a setting needs it
// (SWW_WAF_LOOKUP_TIMEOUT), which cannot be off. AddLookupHeaders is true
// when the app is passed the client's AS number and country in headers
// (SWW_WAF_ADD_LOOKUP_HEADERS).
LookupSource string
LookupDBPath string
LookupTimeout time.Duration
AddLookupHeaders bool
BytesCounted string
// LookupSource is where each client's AS number and country are looked
// up (SWW_WAF_LOOKUP_SOURCE): geojs, file, or off for nowhere.
// LookupDBPath is the lookup database, the IPinfo Lite file looked up in
// while LookupSource is file (SWW_WAF_LOOKUP_DB_PATH), and "" for any
// other source. A request waits up to ASNAndCountryLookupTimeout for its
// client's first answer from GeoJS while a setting needs it
// (SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS), which cannot be off.
// AddLookupHeaders is true when the app is passed the client's AS number
// and country in headers (SWW_WAF_ADD_LOOKUP_HEADERS).
LookupSource string
LookupDBPath string
ASNAndCountryLookupTimeout time.Duration
AddLookupHeaders bool
// ExclusivelyAllowedCountries, when not empty, are the only countries
// whose clients are let through (SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES),
// two-letter codes in capitals, as GeoJS gives them.
@@ -105,21 +105,22 @@ type Config struct {
// RouteWatchURL is where the routewatch instance is that adds the asn=
// and prefix= tags, "" for none (SWW_WAF_ROUTEWATCH_URL).
RouteWatchURL string
// The biased thresholds. ASNBytesPercent and CountryBytesPercent give
// those they list a percentage of the byte limits in place of the one
// TagLimitPercent gives their asn= or country= tag
// (SWW_WAF_ASN_BYTES_PERCENT and SWW_WAF_COUNTRY_BYTES_PERCENT). Each holds
// percentages from 0 to 100, by AS number, written as AS64496, or by
// country, a two-letter code in capitals, as the lookup gives them.
// UnknownLimitPercent is the percentage of every limit a client without
// a country gets (SWW_WAF_UNKNOWN_LIMIT_PERCENT). ASNLimitPercentURL is
// The biased thresholds. ASNBytesLimitPercent and
// CountryBytesLimitPercent give those they list a percentage of the byte
// limits in place of the one TagLimitPercent gives their asn= or
// country= tag (SWW_WAF_ASN_BYTES_LIMIT_PERCENT and
// SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT). Each holds percentages from 0 to
// 100, by AS number, written as AS64496, or by country, a two-letter
// code in capitals, as the lookup gives them. UnknownCountryLimitPercent
// is the percentage of every limit a client without a country gets
// (SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT). ASNLimitPercentURL is
// where a file of AS:percent lines is fetched from, whose percentages
// count like asn= tags in TagLimitPercent (SWW_WAF_ASN_LIMIT_PERCENT_URL),
// "" while it is unset.
ASNBytesPercent map[string]int64
CountryBytesPercent map[string]int64
UnknownLimitPercent int64
ASNLimitPercentURL string
ASNBytesLimitPercent map[string]int64
CountryBytesLimitPercent map[string]int64
UnknownCountryLimitPercent int64
ASNLimitPercentURL string
// BlocklistURLs are where the blocklists are fetched from
// (SWW_WAF_BLOCKLIST_URLS). Each list, and ASNLimitPercentURL's, is
// fetched again BlocklistRefresh after it was last fetched or tried
@@ -139,21 +140,23 @@ type Config struct {
// of AbuseIPDBMinScore or more is a hit (SWW_WAF_ABUSEIPDB_MIN_SCORE), and
// at most AbuseIPDBDailyBudget checks are made a day
// (SWW_WAF_ABUSEIPDB_DAILY_BUDGET).
// ReputationAction is what is done with a client a zone's verdict lists,
// or whose score is a hit (SWW_WAF_REPUTATION_ACTION): deny, limit or log;
// for limit, ReputationLimitPercent is the percentage of every limit it
// gets. A verdict or a score is used for ReputationCacheTTL after it was
// fetched (SWW_WAF_REPUTATION_CACHE_TTL), and a query or a check may take
// ReputationTimeout (SWW_WAF_REPUTATION_TIMEOUT). Neither can be off.
DNSBLZones []string
DNSBLResolver netip.AddrPort
AbuseIPDBKey string
AbuseIPDBMinScore int64
AbuseIPDBDailyBudget int
ReputationAction string
ReputationLimitPercent int64
ReputationCacheTTL time.Duration
ReputationTimeout time.Duration
// DNSBLAbuseIPDBAction is what is done with a client a zone's verdict
// lists, or whose score is a hit (SWW_WAF_DNSBL_ABUSEIPDB_ACTION): deny,
// limit or log; for limit, DNSBLAbuseIPDBLimitPercent is the percentage
// of every limit it gets. A verdict or a score is used for
// IPReputationCacheTTL after it was fetched
// (SWW_WAF_IP_REPUTATION_CACHE_TTL), and a query or a check may take
// DNSBLAbuseIPDBTimeout (SWW_WAF_DNSBL_ABUSEIPDB_TIMEOUT_MS). Neither
// can be off.
DNSBLZones []string
DNSBLResolver netip.AddrPort
AbuseIPDBKey string
AbuseIPDBMinScore int64
AbuseIPDBDailyBudget int
DNSBLAbuseIPDBAction string
DNSBLAbuseIPDBLimitPercent int64
IPReputationCacheTTL time.Duration
DNSBLAbuseIPDBTimeout time.Duration
// CrowdSecDecisionsURL is where the decision list of the CrowdSec
// engine whose local API SWW_WAF_CROWDSEC_LAPI_URL names is fetched from:
// that URL with v1/decisions added to its path, "" while it is unset and
@@ -171,11 +174,11 @@ type Config struct {
// (SWW_WAF_LIMIT_BAN_DURATION). A limit broken again within
// LimitBanRepeatWindow after the last ban ended bans for three times
// as long as that ban (SWW_WAF_LIMIT_BAN_REPEAT_WINDOW), and a ban that
// would be longer than MaxBanDuration is permanent instead
// (SWW_WAF_MAX_BAN_DURATION). None of them can be off.
LimitBanDuration time.Duration
LimitBanRepeatWindow time.Duration
MaxBanDuration time.Duration
// would be longer than LimitBanPermanentAbove is permanent instead
// (SWW_WAF_LIMIT_BAN_PERMANENT_ABOVE). None of them can be off.
LimitBanDuration time.Duration
LimitBanRepeatWindow time.Duration
LimitBanPermanentAbove time.Duration
// AttackBanDuration is the ban for a first clear sign of attack
// (SWW_WAF_ATTACK_BAN_DURATION). It cannot be off.
AttackBanDuration time.Duration
@@ -185,13 +188,13 @@ type Config struct {
// client that a ban covers (SWW_WAF_BAN_SCOPE_V4_PREFIX).
BanScopeV4Prefix int
// StateDir is the directory of the state files, an absolute path
// (SWW_WAF_STATE_DIR). bans.json is written StateWriteDelay after a ban
// is made (SWW_WAF_STATE_WRITE_DELAY), and every state and cache file
// every StateCounterInterval (SWW_WAF_STATE_COUNTER_INTERVAL). Neither
// (SWW_WAF_STATE_DIR). bans.json is written BansWriteDelay after a ban
// is made (SWW_WAF_BANS_WRITE_DELAY), and every state and cache file
// every StateWriteInterval (SWW_WAF_STATE_WRITE_INTERVAL). Neither
// can be off.
StateDir string
StateWriteDelay time.Duration
StateCounterInterval time.Duration
StateDir string
BansWriteDelay time.Duration
StateWriteInterval time.Duration
// CacheDir is the directory of the cache files, which keep answers
// from outside services, an absolute path (SWW_WAF_CACHE_DIR).
CacheDir string
@@ -207,32 +210,38 @@ type Config struct {
AdminToken string
// MetricsToken is the bearer token a scraper sends for the metrics
// (SWW_WAF_METRICS_TOKEN), "" while it is unset and the metrics are off.
// MetricsTopN is how many AS numbers and how many countries get series
// of their own in the metrics (SWW_WAF_METRICS_TOP_N).
MetricsToken string
MetricsTopN int
// RulesDir is the directory of the rule files (SWW_WAF_RULES_DIR), read
// unless RulesEnabled is false (SWW_WAF_RULES_ENABLED).
RulesDir string
RulesEnabled bool
// WAFMode is what the Core Rule Set does (SWW_WAF_CORE_RULE_SET_MODE): WAFModeOff,
// WAFModeDetect or WAFModeBlock. WAFParanoiaLevel is its paranoia
// level, from 1 to 4 (SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL), and
// WAFAnomalyThreshold the anomaly score at which a request is a match
// (SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD), 0 while it is off. WAFDisabledRules
// are the ids of its rules switched off (SWW_WAF_CORE_RULE_SET_DISABLED_RULES),
// WAFExemptPaths the path prefixes it does not inspect
// (SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS), and WAFBodyLimit the most of a request body
// it reads (SWW_WAF_CORE_RULE_SET_BODY_LIMIT), 0 while it is off and it reads none.
// MetricsBusiestCountriesAndASNs is how many AS numbers and how many
// countries get series of their own in the metrics
// (SWW_WAF_METRICS_BUSIEST_COUNTRIES_AND_ASNS).
MetricsToken string
MetricsBusiestCountriesAndASNs int
// RuleFilesDir is the directory of the rule files
// (SWW_WAF_RULE_FILES_DIR), read unless RuleFilesEnabled is false
// (SWW_WAF_RULE_FILES_ENABLED).
RuleFilesDir string
RuleFilesEnabled bool
// CoreRuleSetMode is what the Core Rule Set does
// (SWW_WAF_CORE_RULE_SET_MODE): CoreRuleSetModeOff,
// CoreRuleSetModeDetect or CoreRuleSetModeBlock.
// CoreRuleSetParanoiaLevel is its paranoia level, from 1 to 4
// (SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL), and CoreRuleSetScoreThreshold
// the anomaly score at which a request is a match
// (SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD), 0 while it is off.
// CoreRuleSetDisabledRules are the ids of its rules switched off
// (SWW_WAF_CORE_RULE_SET_DISABLED_RULES), CoreRuleSetExemptPaths the
// path prefixes it does not inspect (SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS),
// and CoreRuleSetBodyInspectionSize the most of a request body it reads
// (SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE), 0 while it is off and it
// reads none.
// CoreRuleSetExclusionsFile is the path of the file of its exclusions
// (SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE), "" while there is none.
WAFMode string
WAFParanoiaLevel int
WAFAnomalyThreshold int
WAFDisabledRules []int
WAFExemptPaths []string
WAFBodyLimit int64
CoreRuleSetExclusionsFile string
CoreRuleSetMode string
CoreRuleSetParanoiaLevel int
CoreRuleSetScoreThreshold int
CoreRuleSetDisabledRules []int
CoreRuleSetExemptPaths []string
CoreRuleSetBodyInspectionSize int64
CoreRuleSetExclusionsFile string
// Apps are the names of the apps each hostname serves, by hostname in
// lowercase (SWW_WAF_APPS): a request gets only its hostname's apps' fixes
// to the Core Rule Set. Apps is nil while SWW_WAF_APPS is unset, and every
@@ -241,25 +250,26 @@ type Config struct {
// TrapPaths are the paths a request for which is a clear sign of
// attack (SWW_WAF_TRAP_PATHS), each starting with / and without a ?.
TrapPaths []string
// ErrorBurstThreshold is the most requests of a client within a minute
// RefusalsLimitPerMinute is the most requests of a client within a minute
// that waf may refuse after a rule file or Core Rule Set match; one
// more breaks a limit (SWW_WAF_ERROR_BURST_THRESHOLD). 0 is off.
ErrorBurstThreshold int64
// more breaks a limit (SWW_WAF_REFUSALS_LIMIT_PER_MINUTE). 0 is off.
RefusalsLimitPerMinute int64
// LogRemoteURL is where every line on stdout is also sent
// (SWW_WAF_LOG_REMOTE_URL), nil while it is unset and nothing is sent.
// LogRemoteTLSCAs are the certificates a syslog+tls endpoint's
// certificate must chain to (SWW_WAF_LOG_REMOTE_TLS_CA_FILE), nil while
// it is unset and the host's own are used. LogRemoteBuffer is the most
// lines held while they wait to be sent (SWW_WAF_LOG_REMOTE_BUFFER).
// it is unset and the host's own are used. LogRemoteBufferLines is the
// most lines held while they wait to be sent
// (SWW_WAF_LOG_REMOTE_BUFFER_LINES).
// LogRemoteFacility is the number of the syslog facility
// (SWW_WAF_LOG_REMOTE_FACILITY), and LogRemoteAppName the APP-NAME
// (SWW_WAF_LOG_REMOTE_APP_NAME, by default InstanceName), of the records
// the lines are sent in.
LogRemoteURL *url.URL
LogRemoteTLSCAs *x509.CertPool
LogRemoteBuffer int
LogRemoteFacility int
LogRemoteAppName string
LogRemoteURL *url.URL
LogRemoteTLSCAs *x509.CertPool
LogRemoteBufferLines int
LogRemoteFacility int
LogRemoteAppName string
// AlertWebhookURL is where each alert is posted as JSON
// (SWW_WAF_ALERT_WEBHOOK_URL), nil while it is unset. AlertWebhookHeaders
// are sent with each (SWW_WAF_ALERT_WEBHOOK_HEADERS).
@@ -285,19 +295,19 @@ type Config struct {
// and bytes a minute and an hour per client (SWW_WAF_ANOMALY_CLIENT_*),
// per netblock around a client (SWW_WAF_ANOMALY_NET_*), per AS number
// (SWW_WAF_ANOMALY_ASN_*), for the whole service (SWW_WAF_ANOMALY_TOTAL_*)
// and per named netblock (SWW_WAF_WATCH_*), each 0 while it is off.
// AnomalyNetV4Prefix and AnomalyNetV6Prefix are the lengths of the
// netblock around a client (SWW_WAF_ANOMALY_NET_V4_PREFIX and
// SWW_WAF_ANOMALY_NET_V6_PREFIX), and WatchNets the named netblocks
// (SWW_WAF_WATCH_NETS).
// and per watched netblock (SWW_WAF_ANOMALY_WATCHED_NET_*), each 0 while
// it is off. AnomalyNetV4Prefix and AnomalyNetV6Prefix are the lengths
// of the netblock around a client (SWW_WAF_ANOMALY_NET_V4_PREFIX and
// SWW_WAF_ANOMALY_NET_V6_PREFIX), and AnomalyWatchedNets the watched
// netblocks (SWW_WAF_ANOMALY_WATCHED_NETS).
AnomalyClient anomaly.Thresholds
AnomalyNet anomaly.Thresholds
AnomalyASN anomaly.Thresholds
AnomalyTotal anomaly.Thresholds
AnomalyWatch anomaly.Thresholds
AnomalyWatchedNet anomaly.Thresholds
AnomalyNetV4Prefix int
AnomalyNetV6Prefix int
WatchNets []anomaly.NamedNetblock
AnomalyWatchedNets []anomaly.NamedNetblock
// settings are the values read, as given or by default, and the
// files they were read from, for the log line at start.
@@ -306,12 +316,12 @@ type Config struct {
// The values of SWW_WAF_CORE_RULE_SET_MODE.
const (
// WAFModeOff runs no request through the Core Rule Set.
WAFModeOff = off
// WAFModeDetect logs and alerts a match, and refuses nothing.
WAFModeDetect = "detect"
// WAFModeBlock refuses a match with 403.
WAFModeBlock = "block"
// CoreRuleSetModeOff runs no request through the Core Rule Set.
CoreRuleSetModeOff = off
// CoreRuleSetModeDetect logs and alerts a match, and refuses nothing.
CoreRuleSetModeDetect = "detect"
// CoreRuleSetModeBlock refuses a match with 403.
CoreRuleSetModeBlock = "block"
)
// fileSource is the SWW_WAF_LOOKUP_SOURCE that looks clients up in the
@@ -379,10 +389,10 @@ var (
"is not a name, = and a netblock, such as office=203.0.113.0/24")
errNotAbsolutePath = errors.New(
"is not an absolute path, such as /var/lib/sww")
errShortToken = errors.New("is shorter than 32 characters")
errNotMode = errors.New("is not enforce or observe")
errNotBytesCount = errors.New("is not response, request or both")
errNotPathPrefix = errors.New(
errShortToken = errors.New("is shorter than 32 characters")
errNotMode = errors.New("is not enforce or observe")
errNotBytesCounted = errors.New("is not response, request or both")
errNotPathPrefix = errors.New(
"is not a path prefix starting with /, such as /assets/")
errNotTrapPath = errors.New(
"is not a path starting with / and without a ?, such as /wp-login.php",
@@ -396,9 +406,11 @@ var (
errNotApp = errors.New(
"is not an app sww has fixes for; the apps are",
)
errNotWAFMode = errors.New("is not off, detect or block")
errNotParanoiaLevel = errors.New("is not a paranoia level, from 1 to 4")
errNotRuleID = errors.New(
errNotCoreRuleSetMode = errors.New("is not off, detect or block")
errNotParanoiaLevel = errors.New(
"is not a paranoia level, from 1 to 4",
)
errNotRuleID = errors.New(
"is not the id of a Core Rule Set rule, a whole number such as 942100",
)
errSetupRuleID = errors.New(
@@ -501,7 +513,7 @@ var (
)
)
// FromEnvironment reads the settings through v, normally the viper
// WAFFromEnvironment reads the settings through v, normally the viper
// NewViper returns, bound to the process's environment. 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 (see lookup). A
@@ -509,9 +521,9 @@ var (
// invalid is an error that names it.
//
//nolint:funlen // one line for each setting, a list that grows with them
func FromEnvironment(v *viper.Viper) (*Config, error) {
func WAFFromEnvironment(v *viper.Viper) (*WAF, error) {
env := &environment{v: v}
cfg := &Config{
cfg := &WAF{
ListenAddr: env.address("SWW_WAF_LISTEN_ADDR"),
InstanceName: env.instanceName(),
Observe: env.observe("SWW_WAF_MODE", "enforce"),
@@ -545,15 +557,15 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
),
BytesLimitPerHour: env.size("SWW_WAF_BYTES_LIMIT_PER_HOUR", "50G"),
BytesLimitPerDay: env.size("SWW_WAF_BYTES_LIMIT_PER_DAY", "100G"),
BytesCount: env.bytesCount("SWW_WAF_BYTES_COUNT", "both"),
BytesCounted: env.bytesCounted("SWW_WAF_BYTES_COUNTED", "both"),
LookupSource: env.lookupSource(
"SWW_WAF_LOOKUP_SOURCE",
"geojs",
),
LookupDBPath: env.value("SWW_WAF_LOOKUP_DB_PATH", ""),
LookupTimeout: env.durationNotOff(
"SWW_WAF_LOOKUP_TIMEOUT",
"800ms",
ASNAndCountryLookupTimeout: env.milliseconds(
"SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS",
"800",
),
AddLookupHeaders: env.boolean("SWW_WAF_ADD_LOOKUP_HEADERS", "false"),
ExclusivelyAllowedCountries: env.countries(
@@ -562,16 +574,16 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
TagDeny: env.tags("SWW_WAF_TAG_DENY"),
TagLimitPercent: env.tagPercents("SWW_WAF_TAG_LIMIT_PERCENT"),
RouteWatchURL: env.routeWatchURL("SWW_WAF_ROUTEWATCH_URL"),
ASNBytesPercent: env.percents(
"SWW_WAF_ASN_BYTES_PERCENT",
ASNBytesLimitPercent: env.percents(
"SWW_WAF_ASN_BYTES_LIMIT_PERCENT",
ParseASN,
),
CountryBytesPercent: env.percents(
"SWW_WAF_COUNTRY_BYTES_PERCENT",
CountryBytesLimitPercent: env.percents(
"SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT",
parseCountry,
),
UnknownLimitPercent: env.percent(
"SWW_WAF_UNKNOWN_LIMIT_PERCENT",
UnknownCountryLimitPercent: env.percent(
"SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT",
"100",
),
ASNLimitPercentURL: env.listURL("SWW_WAF_ASN_LIMIT_PERCENT_URL"),
@@ -585,13 +597,13 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
"SWW_WAF_ABUSEIPDB_DAILY_BUDGET",
"900",
),
ReputationCacheTTL: env.durationNotOff(
"SWW_WAF_REPUTATION_CACHE_TTL",
IPReputationCacheTTL: env.durationNotOff(
"SWW_WAF_IP_REPUTATION_CACHE_TTL",
"24h",
),
ReputationTimeout: env.durationNotOff(
"SWW_WAF_REPUTATION_TIMEOUT",
"2s",
DNSBLAbuseIPDBTimeout: env.milliseconds(
"SWW_WAF_DNSBL_ABUSEIPDB_TIMEOUT_MS",
"2000",
),
CrowdSecDecisionsURL: env.crowdSecDecisionsURL(
"SWW_WAF_CROWDSEC_LAPI_URL",
@@ -606,8 +618,8 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
"SWW_WAF_LIMIT_BAN_REPEAT_WINDOW",
"24h",
),
MaxBanDuration: env.durationNotOff(
"SWW_WAF_MAX_BAN_DURATION",
LimitBanPermanentAbove: env.durationNotOff(
"SWW_WAF_LIMIT_BAN_PERMANENT_ABOVE",
"7d",
),
AttackBanDuration: env.durationNotOff(
@@ -620,12 +632,12 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
"SWW_WAF_STATE_DIR",
"/var/lib/sww",
),
StateWriteDelay: env.durationNotOff(
"SWW_WAF_STATE_WRITE_DELAY",
BansWriteDelay: env.durationNotOff(
"SWW_WAF_BANS_WRITE_DELAY",
"10s",
),
StateCounterInterval: env.durationNotOff(
"SWW_WAF_STATE_COUNTER_INTERVAL",
StateWriteInterval: env.durationNotOff(
"SWW_WAF_STATE_WRITE_INTERVAL",
"15m",
),
CacheDir: env.absolutePath(
@@ -636,48 +648,62 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
"SWW_WAF_LOG_REQUEST_HEADERS",
"accept,accept-language,accept-encoding,content-type,origin,range",
),
LogLevel: env.logLevel("SWW_WAF_LOG_LEVEL", "info"),
LogLevel: env.logLevel(
"SWW_WAF_LOG_LEVEL",
"info",
),
AdminToken: env.token("SWW_WAF_ADMIN_TOKEN"),
MetricsToken: env.token("SWW_WAF_METRICS_TOKEN"),
MetricsTopN: env.numberNotOff("SWW_WAF_METRICS_TOP_N", "50"),
RulesDir: env.value("SWW_WAF_RULES_DIR", "/etc/sww/rules.d"),
RulesEnabled: env.boolean("SWW_WAF_RULES_ENABLED", "true"),
WAFMode: env.wafMode(
"SWW_WAF_CORE_RULE_SET_MODE",
WAFModeBlock,
MetricsBusiestCountriesAndASNs: env.numberNotOff(
"SWW_WAF_METRICS_BUSIEST_COUNTRIES_AND_ASNS",
"50",
),
WAFParanoiaLevel: env.paranoiaLevel(
RuleFilesDir: env.value(
"SWW_WAF_RULE_FILES_DIR",
"/etc/sww/rules.d",
),
RuleFilesEnabled: env.boolean(
"SWW_WAF_RULE_FILES_ENABLED",
"true",
),
CoreRuleSetMode: env.coreRuleSetMode(
"SWW_WAF_CORE_RULE_SET_MODE",
CoreRuleSetModeBlock,
),
CoreRuleSetParanoiaLevel: env.paranoiaLevel(
"SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL",
"1",
),
WAFAnomalyThreshold: env.numberOrOff(
"SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD",
CoreRuleSetScoreThreshold: env.numberOrOff(
"SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD",
"5",
),
WAFDisabledRules: env.ruleIDs("SWW_WAF_CORE_RULE_SET_DISABLED_RULES",
"920340,920420,920440,920640,930130,930140"),
WAFExemptPaths: env.pathPrefixes(
CoreRuleSetDisabledRules: env.ruleIDs(
"SWW_WAF_CORE_RULE_SET_DISABLED_RULES",
"920340,920420,920440,920640,930130,930140",
),
CoreRuleSetExemptPaths: env.pathPrefixes(
"SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS",
"",
),
WAFBodyLimit: env.wafBodyLimit(
"SWW_WAF_CORE_RULE_SET_BODY_LIMIT",
CoreRuleSetBodyInspectionSize: env.coreRuleSetBodyInspectionSize(
"SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE",
off,
),
CoreRuleSetExclusionsFile: env.filePath(
"SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE"),
Apps: env.apps("SWW_WAF_APPS"),
TrapPaths: env.trapPaths("SWW_WAF_TRAP_PATHS"),
ErrorBurstThreshold: env.count(
"SWW_WAF_ERROR_BURST_THRESHOLD",
RefusalsLimitPerMinute: env.count(
"SWW_WAF_REFUSALS_LIMIT_PER_MINUTE",
"30",
),
LogRemoteURL: env.logRemoteURL("SWW_WAF_LOG_REMOTE_URL"),
LogRemoteTLSCAs: env.certificates(
"SWW_WAF_LOG_REMOTE_TLS_CA_FILE",
),
LogRemoteBuffer: env.numberNotOff(
"SWW_WAF_LOG_REMOTE_BUFFER",
LogRemoteBufferLines: env.numberNotOff(
"SWW_WAF_LOG_REMOTE_BUFFER_LINES",
"10000",
),
LogRemoteFacility: env.facility(
@@ -700,11 +726,11 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
"SWW_WAF_ALERT_MAX_PER_HOUR",
"60",
),
AnomalyClient: env.thresholds("SWW_WAF_ANOMALY_CLIENT_"),
AnomalyNet: env.thresholds("SWW_WAF_ANOMALY_NET_"),
AnomalyASN: env.thresholds("SWW_WAF_ANOMALY_ASN_"),
AnomalyTotal: env.thresholds("SWW_WAF_ANOMALY_TOTAL_"),
AnomalyWatch: env.thresholds("SWW_WAF_WATCH_"),
AnomalyClient: env.thresholds("SWW_WAF_ANOMALY_CLIENT_"),
AnomalyNet: env.thresholds("SWW_WAF_ANOMALY_NET_"),
AnomalyASN: env.thresholds("SWW_WAF_ANOMALY_ASN_"),
AnomalyTotal: env.thresholds("SWW_WAF_ANOMALY_TOTAL_"),
AnomalyWatchedNet: env.thresholds("SWW_WAF_ANOMALY_WATCHED_NET_"),
AnomalyNetV4Prefix: env.v4Prefix(
"SWW_WAF_ANOMALY_NET_V4_PREFIX",
"24",
@@ -713,15 +739,17 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
"SWW_WAF_ANOMALY_NET_V6_PREFIX",
"48",
),
WatchNets: env.namedNetblocks("SWW_WAF_WATCH_NETS"),
AnomalyWatchedNets: env.namedNetblocks(
"SWW_WAF_ANOMALY_WATCHED_NETS",
),
}
cfg.LogRemoteAppName = env.appName("SWW_WAF_LOG_REMOTE_APP_NAME",
cfg.InstanceName, cfg.LogRemoteURL != nil)
cfg.BlocklistAction, cfg.BlocklistLimitPercent = env.action(
"SWW_WAF_BLOCKLIST_ACTION", "deny")
cfg.ReputationAction, cfg.ReputationLimitPercent = env.action(
"SWW_WAF_REPUTATION_ACTION", "limit:25")
cfg.DNSBLAbuseIPDBAction, cfg.DNSBLAbuseIPDBLimitPercent = env.action(
"SWW_WAF_DNSBL_ABUSEIPDB_ACTION", "limit:25")
env.checkInstanceNameForNtfy(cfg.InstanceName, cfg.AlertNtfyURL != nil)
env.checkLookupDBPath(cfg)
@@ -741,7 +769,7 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
}
// WAFListenAddr reads only SWW_WAF_LISTEN_ADDR, which may be given as a
// file, as FromEnvironment does. The health check in waf's container needs
// file, as WAFFromEnvironment does. The health check in waf's container needs
// no other setting, so it reads no other, nor a file that another names.
func WAFListenAddr(v *viper.Viper) (string, error) {
env := &environment{v: v}
@@ -751,18 +779,18 @@ func WAFListenAddr(v *viper.Viper) (string, error) {
}
// InstanceName reads only SWW_WAF_INSTANCE_NAME, which may be given as a
// file, as FromEnvironment does, so that the line saying a setting is
// file, as WAFFromEnvironment does, so that the line saying a setting is
// invalid carries it too. A file that cannot be read gives the default
// here, and FromEnvironment then stops the start over it.
// here, and WAFFromEnvironment then stops the start over it.
func InstanceName(v *viper.Viper) string {
env := &environment{v: v}
return env.instanceName()
}
// LogValue makes a Config log as each setting's name with the value it was
// LogValue makes a WAF log as each setting's name with the value it was
// given, or its default.
func (c *Config) LogValue() slog.Value {
func (c *WAF) LogValue() slog.Value {
return slog.GroupValue(c.settings...)
}
@@ -795,10 +823,14 @@ func (e *environment) duration(name, defaultValue string) time.Duration {
return duration
}
// wafBodyLimit reads the setting that is the most of a request body the
// Core Rule Set reads.
func (e *environment) wafBodyLimit(name, defaultValue string) int64 {
limit, err := parseWAFBodyLimit(e.value(name, defaultValue))
// coreRuleSetBodyInspectionSize reads the setting that is the most of a
// request body the Core Rule Set reads.
func (e *environment) coreRuleSetBodyInspectionSize(
name, defaultValue string,
) int64 {
limit, err := parseCoreRuleSetBodyInspectionSize(
e.value(name, defaultValue),
)
e.check(name, err)
return limit
@@ -812,12 +844,12 @@ func (e *environment) count(name, defaultValue string) int64 {
return count
}
// bytesCount reads the setting that is which body bytes count toward the
// byte limits: response, request or both.
func (e *environment) bytesCount(name, defaultValue string) string {
// bytesCounted reads the setting that is which body bytes count toward the
// byte limits and the byte anomaly thresholds: response, request or both.
func (e *environment) bytesCounted(name, defaultValue string) string {
value := e.value(name, defaultValue)
if value != "response" && value != "request" && value != "both" {
e.check(name, fmt.Errorf("%q %w", value, errNotBytesCount))
e.check(name, fmt.Errorf("%q %w", value, errNotBytesCounted))
}
return value
@@ -859,12 +891,13 @@ func (e *environment) trapPaths(name string) []string {
return paths
}
// wafMode reads the setting that is what the Core Rule Set does: off,
// coreRuleSetMode reads the setting that is what the Core Rule Set does: off,
// detect or block.
func (e *environment) wafMode(name, defaultValue string) string {
func (e *environment) coreRuleSetMode(name, defaultValue string) string {
mode := e.value(name, defaultValue)
if mode != WAFModeOff && mode != WAFModeDetect && mode != WAFModeBlock {
e.check(name, fmt.Errorf("%q %w", mode, errNotWAFMode))
if mode != CoreRuleSetModeOff && mode != CoreRuleSetModeDetect &&
mode != CoreRuleSetModeBlock {
e.check(name, fmt.Errorf("%q %w", mode, errNotCoreRuleSetMode))
}
return mode
@@ -1078,7 +1111,7 @@ func (e *environment) lookupSource(name, defaultValue string) string {
// checkLookupDBPath refuses SWW_WAF_LOOKUP_SOURCE=file without
// SWW_WAF_LOOKUP_DB_PATH, and SWW_WAF_LOOKUP_DB_PATH with any other source:
// one source at a time.
func (e *environment) checkLookupDBPath(cfg *Config) {
func (e *environment) checkLookupDBPath(cfg *WAF) {
switch {
case cfg.LookupSource == fileSource && cfg.LookupDBPath == "":
e.check("SWW_WAF_LOOKUP_SOURCE", fmt.Errorf(
@@ -1100,9 +1133,9 @@ func (e *environment) checkLookupDBPath(cfg *Config) {
// up: the country list, SWW_WAF_ADD_LOOKUP_HEADERS, the tag settings that name
// an asn= or country= tag, the biased thresholds,
// SWW_WAF_ASN_LIMIT_PERCENT_URL among them, of which
// SWW_WAF_UNKNOWN_LIMIT_PERCENT needs them only below 100, where it lowers a
// SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT needs them only below 100, where it lowers a
// limit, and the anomaly thresholds per AS number.
func (e *environment) checkCountriesAndLookups(cfg *Config) {
func (e *environment) checkCountriesAndLookups(cfg *WAF) {
for _, country := range cfg.ExclusivelyAllowedCountries {
if slices.Contains(cfg.TagDeny, CountryTagPrefix+country) {
e.check("SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES",
@@ -1129,9 +1162,9 @@ func (e *environment) checkCountriesAndLookups(cfg *Config) {
{"SWW_WAF_TAG_DENY", slices.ContainsFunc(cfg.TagDeny, onlyLookupGives)},
{"SWW_WAF_TAG_LIMIT_PERCENT", slices.ContainsFunc(
slices.Collect(maps.Keys(cfg.TagLimitPercent)), onlyLookupGives)},
{"SWW_WAF_ASN_BYTES_PERCENT", len(cfg.ASNBytesPercent) > 0},
{"SWW_WAF_COUNTRY_BYTES_PERCENT", len(cfg.CountryBytesPercent) > 0},
{"SWW_WAF_UNKNOWN_LIMIT_PERCENT", cfg.UnknownLimitPercent < 100},
{"SWW_WAF_ASN_BYTES_LIMIT_PERCENT", len(cfg.ASNBytesLimitPercent) > 0},
{"SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT", len(cfg.CountryBytesLimitPercent) > 0},
{"SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT", cfg.UnknownCountryLimitPercent < 100},
{"SWW_WAF_ASN_LIMIT_PERCENT_URL", cfg.ASNLimitPercentURL != ""},
{"SWW_WAF_ANOMALY_ASN_REQUESTS_PER_MINUTE", cfg.AnomalyASN.RequestsPerMinute > 0},
{"SWW_WAF_ANOMALY_ASN_REQUESTS_PER_HOUR", cfg.AnomalyASN.RequestsPerHour > 0},
@@ -1150,7 +1183,7 @@ func (e *environment) checkCountriesAndLookups(cfg *Config) {
// checkASNLimitPercentURL refuses SWW_WAF_ASN_LIMIT_PERCENT_URL naming a
// blocklist too: the file at a URL is fetched as one list or the other.
func (e *environment) checkASNLimitPercentURL(cfg *Config) {
func (e *environment) checkASNLimitPercentURL(cfg *WAF) {
if slices.Contains(cfg.BlocklistURLs, cfg.ASNLimitPercentURL) {
e.check("SWW_WAF_ASN_LIMIT_PERCENT_URL",
fmt.Errorf("%q %w", cfg.ASNLimitPercentURL, errInBlocklistURLs))
@@ -1160,7 +1193,7 @@ func (e *environment) checkASNLimitPercentURL(cfg *Config) {
// checkCrowdSec refuses SWW_WAF_CROWDSEC_LAPI_URL without
// SWW_WAF_CROWDSEC_LAPI_KEY, the key without the URL, and a decision list
// that is fetched as another list too.
func (e *environment) checkCrowdSec(cfg *Config) {
func (e *environment) checkCrowdSec(cfg *WAF) {
decisionsURL := cfg.CrowdSecDecisionsURL
switch {
@@ -1186,7 +1219,7 @@ func (e *environment) checkCrowdSec(cfg *Config) {
// checkTagLists refuses a list of SWW_WAF_TAG_LISTS whose URL is fetched as
// another list too: the file at a URL is read as one kind of list.
func (e *environment) checkTagLists(cfg *Config) {
func (e *environment) checkTagLists(cfg *WAF) {
for _, list := range cfg.TagLists {
if slices.Contains(cfg.BlocklistURLs, list.URL) ||
list.URL == cfg.ASNLimitPercentURL || list.URL == cfg.CrowdSecDecisionsURL {
@@ -1201,7 +1234,7 @@ func (e *environment) checkTagLists(cfg *Config) {
// checkTags refuses a tag of SWW_WAF_TAG_DENY or SWW_WAF_TAG_LIMIT_PERCENT that would
// never apply: crowdsec, whose request is refused before tags are acted on, and
// any tag these settings cannot give a request.
func (e *environment) checkTags(cfg *Config) {
func (e *environment) checkTags(cfg *WAF) {
for _, tag := range cfg.TagDeny {
e.check("SWW_WAF_TAG_DENY", checkTag(cfg, tag))
}
@@ -1212,7 +1245,7 @@ func (e *environment) checkTags(cfg *Config) {
}
// checkTag returns an error when tag would never apply, as checkTags says.
func checkTag(cfg *Config, tag string) error {
func checkTag(cfg *WAF, tag string) error {
if tag == CrowdSecTag {
return fmt.Errorf("%q %w", tag, errCrowdSecTag)
}
@@ -1225,17 +1258,17 @@ func checkTag(cfg *Config, tag string) error {
}
// requestCanGetTag reports whether anything cfg sets up can give a request tag.
func requestCanGetTag(cfg *Config, tag string) bool {
func requestCanGetTag(cfg *WAF, tag string) bool {
isListName := func(list TagList) bool { return list.Name == tag }
if isLookupTag(tag) || slices.ContainsFunc(cfg.TagLists, isListName) {
return true
}
if tag == ExploitSignatureTag {
coreRuleSetOn := cfg.WAFMode != WAFModeOff &&
cfg.WAFAnomalyThreshold != 0
coreRuleSetOn := cfg.CoreRuleSetMode != CoreRuleSetModeOff &&
cfg.CoreRuleSetScoreThreshold != 0
return cfg.RulesEnabled || coreRuleSetOn
return cfg.RuleFilesEnabled || coreRuleSetOn
}
if tag == AbuseIPDBTag {
@@ -1256,14 +1289,14 @@ func requestCanGetTag(cfg *Config, tag string) bool {
// TagsNeedLookup reports whether SWW_WAF_TAG_DENY or SWW_WAF_TAG_LIMIT_PERCENT
// names an asn= or country= tag.
func (c *Config) TagsNeedLookup() bool {
func (c *WAF) TagsNeedLookup() bool {
return slices.ContainsFunc(c.TagDeny, isLookupTag) ||
tagLimitNeedsLookups(c)
}
// tagLimitNeedsLookups reports whether SWW_WAF_TAG_LIMIT_PERCENT names an asn=
// or country= tag.
func tagLimitNeedsLookups(cfg *Config) bool {
func tagLimitNeedsLookups(cfg *WAF) bool {
for tag := range cfg.TagLimitPercent {
if isLookupTag(tag) {
return true
@@ -1639,10 +1672,10 @@ func durationOrDays(value string) (time.Duration, error) {
return time.Duration(n) * day, nil
}
// parseWAFBodyLimit reads the most of a request body the Core Rule Set
// parseCoreRuleSetBodyInspectionSize reads the most of a request body the Core Rule Set
// reads: a size as parseSize reads it, or off, but at most 64K, since waf
// gets no more of a body than that.
func parseWAFBodyLimit(value string) (int64, error) {
func parseCoreRuleSetBodyInspectionSize(value string) (int64, error) {
limit, err := parseSize(value)
if err != nil {
return 0, err
File diff suppressed because it is too large. Load diff