|
|
@@ -7,9 +7,6 @@
|
|
|
|
// the image runs.
|
|
|
|
// the image runs.
|
|
|
|
package config
|
|
|
|
package config
|
|
|
|
|
|
|
|
|
|
|
|
// FIXME: the rproxy config is in rproxy.go, the servewaf is in config.go -
|
|
|
|
|
|
|
|
// wrong and inconsistent.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
import (
|
|
|
|
"crypto/x509"
|
|
|
|
"crypto/x509"
|
|
|
|
"errors"
|
|
|
|
"errors"
|
|
|
@@ -366,16 +363,13 @@ var (
|
|
|
|
"is not a header name such as accept-language")
|
|
|
|
"is not a header name such as accept-language")
|
|
|
|
errHeaderTakenOut = errors.New(
|
|
|
|
errHeaderTakenOut = errors.New(
|
|
|
|
"is taken out of every request by Go's HTTP server, so it can never " +
|
|
|
|
"is taken out of every request by Go's HTTP server, so it can never " +
|
|
|
|
"be logged",
|
|
|
|
"be logged")
|
|
|
|
)
|
|
|
|
|
|
|
|
errOnBothLists = errors.New("is in SWWAF_DENIED_COUNTRIES too")
|
|
|
|
errOnBothLists = errors.New("is in SWWAF_DENIED_COUNTRIES too")
|
|
|
|
errNotLookupSource = errors.New("is not geojs, file or off")
|
|
|
|
errNotLookupSource = errors.New("is not geojs, file or off")
|
|
|
|
errNeedsLookups = errors.New("it needs each client looked up")
|
|
|
|
errNeedsLookups = errors.New("it needs each client looked up")
|
|
|
|
errNeedsDBPath = errors.New(
|
|
|
|
errNeedsDBPath = errors.New("it names the file to look clients up in")
|
|
|
|
"it names the file to look clients up in",
|
|
|
|
errDBPathUnused = errors.New("only file reads it")
|
|
|
|
)
|
|
|
|
errOver64K = errors.New(
|
|
|
|
errDBPathUnused = errors.New("only file reads it")
|
|
|
|
|
|
|
|
errOver64K = errors.New(
|
|
|
|
|
|
|
|
"is more than 64K, the most servewaf gets of a body")
|
|
|
|
"is more than 64K, the most servewaf gets of a body")
|
|
|
|
errNotCommand = errors.New("is not rproxy or servewaf")
|
|
|
|
errNotCommand = errors.New("is not rproxy or servewaf")
|
|
|
|
errNotDurationAboveZero = errors.New(
|
|
|
|
errNotDurationAboveZero = errors.New(
|
|
|
@@ -400,29 +394,22 @@ var (
|
|
|
|
errNotPathPrefix = errors.New(
|
|
|
|
errNotPathPrefix = errors.New(
|
|
|
|
"is not a path prefix starting with /, such as /assets/")
|
|
|
|
"is not a path prefix starting with /, such as /assets/")
|
|
|
|
errNotTrapPath = errors.New(
|
|
|
|
errNotTrapPath = errors.New(
|
|
|
|
"is not a path starting with / and without a ?, such as /wp-login.php",
|
|
|
|
"is not a path starting with / and without a ?, such as /wp-login.php")
|
|
|
|
)
|
|
|
|
|
|
|
|
errNotWAFMode = errors.New("is not off, detect or block")
|
|
|
|
errNotWAFMode = errors.New("is not off, detect or block")
|
|
|
|
errNotParanoiaLevel = errors.New("is not a paranoia level, from 1 to 4")
|
|
|
|
errNotParanoiaLevel = errors.New("is not a paranoia level, from 1 to 4")
|
|
|
|
errNotRuleID = errors.New(
|
|
|
|
errNotRuleID = errors.New(
|
|
|
|
"is not the id of a Core Rule Set rule, a whole number such as 942100",
|
|
|
|
"is not the id of a Core Rule Set rule, a whole number such as 942100")
|
|
|
|
)
|
|
|
|
|
|
|
|
errSetupRuleID = errors.New(
|
|
|
|
errSetupRuleID = errors.New(
|
|
|
|
"is from 900000 to 900999, the ids of the rules that set the Core Rule Set " +
|
|
|
|
"is from 900000 to 900999, the ids of the rules that set the Core Rule Set " +
|
|
|
|
"up and of smallwebwaf's own, which cannot be switched off",
|
|
|
|
"up and of smallwebwaf's own, which cannot be switched off")
|
|
|
|
)
|
|
|
|
|
|
|
|
errNotBoolean = errors.New("is not true or false")
|
|
|
|
errNotBoolean = errors.New("is not true or false")
|
|
|
|
errNotLogRemoteURL = errors.New(
|
|
|
|
errNotLogRemoteURL = errors.New(
|
|
|
|
"is not syslog+udp, syslog+tcp or syslog+tls with a host and a port, " +
|
|
|
|
"is not syslog+udp, syslog+tcp or syslog+tls with a host and a port, " +
|
|
|
|
"and nothing more, such as syslog+tls://logs.example:6514",
|
|
|
|
"and nothing more, such as syslog+tls://logs.example:6514")
|
|
|
|
)
|
|
|
|
|
|
|
|
errNoCertificate = errors.New("holds no PEM certificate")
|
|
|
|
errNoCertificate = errors.New("holds no PEM certificate")
|
|
|
|
errNotFacility = errors.New(
|
|
|
|
errNotFacility = errors.New("is not a syslog facility such as local0 or daemon")
|
|
|
|
"is not a syslog facility such as local0 or daemon",
|
|
|
|
errNotAppName = errors.New(
|
|
|
|
)
|
|
|
|
"is not 1 to 48 printable ASCII characters without a space, such as gitea")
|
|
|
|
errNotAppName = errors.New(
|
|
|
|
|
|
|
|
"is not 1 to 48 printable ASCII characters without a space, such as gitea",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
errSetTwice = errors.New("set only one of them")
|
|
|
|
errSetTwice = errors.New("set only one of them")
|
|
|
|
errNotWebhookURL = errors.New(
|
|
|
|
errNotWebhookURL = errors.New(
|
|
|
|
"is not an http or https URL without a user or a fragment, " +
|
|
|
|
"is not an http or https URL without a user or a fragment, " +
|
|
|
@@ -431,50 +418,35 @@ var (
|
|
|
|
"is not a header name followed by : and the header's value, " +
|
|
|
|
"is not a header name followed by : and the header's value, " +
|
|
|
|
"such as Authorization:Bearer <token>")
|
|
|
|
"such as Authorization:Bearer <token>")
|
|
|
|
errControlCharacter = errors.New(
|
|
|
|
errControlCharacter = errors.New(
|
|
|
|
"holds a control character, such as the carriage return of a Windows line end",
|
|
|
|
"holds a control character, such as the carriage return of a Windows line end")
|
|
|
|
)
|
|
|
|
|
|
|
|
errNotAlertEvent = errors.New(
|
|
|
|
errNotAlertEvent = errors.New(
|
|
|
|
"is not ban, permanent_ban, waf_block, anomaly, reputation_hit, " +
|
|
|
|
"is not ban, permanent_ban, waf_block, anomaly, reputation_hit, " +
|
|
|
|
"source_failure or file_error")
|
|
|
|
"source_failure or file_error")
|
|
|
|
errNotNumberOrOff = errors.New(
|
|
|
|
errNotNumberOrOff = errors.New("is not a whole number above zero, such as 60, or off")
|
|
|
|
"is not a whole number above zero, such as 60, or off",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
errNotUTF8 = errors.New("is not valid UTF-8")
|
|
|
|
errNotUTF8 = errors.New("is not valid UTF-8")
|
|
|
|
errNotASN = errors.New("is not an AS number such as AS64496")
|
|
|
|
errNotASN = errors.New("is not an AS number such as AS64496")
|
|
|
|
errNotPercentItem = errors.New(
|
|
|
|
errNotPercentItem = errors.New(
|
|
|
|
"is not a code, : and a percentage, such as AS64496:50 or cn:25")
|
|
|
|
"is not a code, : and a percentage, such as AS64496:50 or cn:25")
|
|
|
|
errNotPercent = errors.New(
|
|
|
|
errNotPercent = errors.New("is not a percentage, a whole number from 0 to 100")
|
|
|
|
"is not a percentage, a whole number from 0 to 100",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
errListedTwice = errors.New("is listed twice")
|
|
|
|
errListedTwice = errors.New("is listed twice")
|
|
|
|
errNotListURL = errors.New(
|
|
|
|
errNotListURL = errors.New(
|
|
|
|
"is not an http or https URL without a user or a fragment, " +
|
|
|
|
"is not an http or https URL without a user or a fragment, " +
|
|
|
|
"such as https://www.spamhaus.org/drop/drop.txt")
|
|
|
|
"such as https://www.spamhaus.org/drop/drop.txt")
|
|
|
|
errInBlocklistURLs = errors.New("is in SWWAF_BLOCKLIST_URLS too")
|
|
|
|
errInBlocklistURLs = errors.New("is in SWWAF_BLOCKLIST_URLS too")
|
|
|
|
errNotAnHourOrMore = errors.New(
|
|
|
|
errNotAnHourOrMore = errors.New("is not a duration of 1h or more, such as 24h")
|
|
|
|
"is not a duration of 1h or more, such as 24h",
|
|
|
|
errNotAction = errors.New(
|
|
|
|
)
|
|
|
|
|
|
|
|
errNotAction = errors.New(
|
|
|
|
|
|
|
|
"is not deny, limit:<percent> such as limit:25, or log")
|
|
|
|
"is not deny, limit:<percent> such as limit:25, or log")
|
|
|
|
errNotZone = errors.New(
|
|
|
|
errNotZone = errors.New("is not a DNS zone such as dnsbl.dronebl.org")
|
|
|
|
"is not a DNS zone such as dnsbl.dronebl.org",
|
|
|
|
errZoneTooLong = errors.New("is longer than 189 characters, too long for the " +
|
|
|
|
)
|
|
|
|
"names IPv6 clients are asked about by")
|
|
|
|
errZoneTooLong = errors.New(
|
|
|
|
errNotResolver = errors.New("is not an IP address with an optional port, " +
|
|
|
|
"is longer than 189 characters, too long for the " +
|
|
|
|
"such as 192.0.2.53 or [2001:db8::53]:5353")
|
|
|
|
"names IPv6 clients are asked about by",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
errNotResolver = errors.New(
|
|
|
|
|
|
|
|
"is not an IP address with an optional port, " +
|
|
|
|
|
|
|
|
"such as 192.0.2.53 or [2001:db8::53]:5353",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
errNotLAPIURL = errors.New(
|
|
|
|
errNotLAPIURL = errors.New(
|
|
|
|
"is not an http or https URL without a user or a fragment, " +
|
|
|
|
"is not an http or https URL without a user or a fragment, " +
|
|
|
|
"such as http://172.17.0.1:8080")
|
|
|
|
"such as http://172.17.0.1:8080")
|
|
|
|
errNeedsLAPIKey = errors.New("the engine answers no request without it")
|
|
|
|
errNeedsLAPIKey = errors.New("the engine answers no request without it")
|
|
|
|
errLAPIKeyUnused = errors.New(
|
|
|
|
errLAPIKeyUnused = errors.New("it is sent only to the engine at that URL")
|
|
|
|
"it is sent only to the engine at that URL",
|
|
|
|
errAnotherList = errors.New(
|
|
|
|
)
|
|
|
|
|
|
|
|
errAnotherList = errors.New(
|
|
|
|
|
|
|
|
"is in SWWAF_BLOCKLIST_URLS or is SWWAF_ASN_LIMIT_PERCENT_URL too")
|
|
|
|
"is in SWWAF_BLOCKLIST_URLS or is SWWAF_ASN_LIMIT_PERCENT_URL too")
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
@@ -485,208 +457,97 @@ var (
|
|
|
|
// setting that is set but invalid is an error that names it.
|
|
|
|
// setting that is set but invalid is an error that names it.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//nolint:funlen // one line for each setting, a list that grows with them
|
|
|
|
//nolint:funlen // one line for each setting, a list that grows with them
|
|
|
|
func FromEnvironment(
|
|
|
|
func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
|
|
|
|
lookupEnv func(string) (string, bool),
|
|
|
|
|
|
|
|
) (*Config, error) {
|
|
|
|
|
|
|
|
env := &environment{lookupEnv: lookupEnv}
|
|
|
|
env := &environment{lookupEnv: lookupEnv}
|
|
|
|
cfg := &Config{
|
|
|
|
cfg := &Config{
|
|
|
|
ListenAddr: env.address("SWWAF_LISTEN_ADDR"),
|
|
|
|
ListenAddr: env.address("SWWAF_LISTEN_ADDR"),
|
|
|
|
InstanceName: env.instanceName(),
|
|
|
|
InstanceName: env.instanceName(),
|
|
|
|
Observe: env.observe("SWWAF_MODE", "enforce"),
|
|
|
|
Observe: env.observe("SWWAF_MODE", "enforce"),
|
|
|
|
IPv6GroupPrefix: env.ipv6GroupPrefix(
|
|
|
|
IPv6GroupPrefix: env.ipv6GroupPrefix("SWWAF_IPV6_GROUP_PREFIX", "64"),
|
|
|
|
"SWWAF_IPV6_GROUP_PREFIX",
|
|
|
|
MaxTrackedClients: env.numberNotOff("SWWAF_MAX_TRACKED_CLIENTS", "20000"),
|
|
|
|
"64",
|
|
|
|
AllowNets: env.netblocks("SWWAF_ALLOW_NETS", ""),
|
|
|
|
),
|
|
|
|
RateLimitExemptNets: env.netblocks("SWWAF_RATE_LIMIT_EXEMPT_NETS", ""),
|
|
|
|
MaxTrackedClients: env.numberNotOff(
|
|
|
|
DenyNets: env.netblocks("SWWAF_DENY_NETS", ""),
|
|
|
|
"SWWAF_MAX_TRACKED_CLIENTS",
|
|
|
|
RateLimitPerMinute: env.count("SWWAF_RATE_LIMIT_PER_MINUTE", "120"),
|
|
|
|
"20000",
|
|
|
|
RateLimitPerHour: env.count("SWWAF_RATE_LIMIT_PER_HOUR", "1500"),
|
|
|
|
),
|
|
|
|
RateLimitPerDay: env.count("SWWAF_RATE_LIMIT_PER_DAY", "6000"),
|
|
|
|
AllowNets: env.netblocks("SWWAF_ALLOW_NETS", ""),
|
|
|
|
RateLimitExemptPaths: env.pathPrefixes("SWWAF_RATE_LIMIT_EXEMPT_PATHS", ""),
|
|
|
|
RateLimitExemptNets: env.netblocks(
|
|
|
|
BytesLimitPerMinute: env.size("SWWAF_BYTES_LIMIT_PER_MINUTE", "off"),
|
|
|
|
"SWWAF_RATE_LIMIT_EXEMPT_NETS",
|
|
|
|
BytesLimitPerHour: env.size("SWWAF_BYTES_LIMIT_PER_HOUR", "50G"),
|
|
|
|
"",
|
|
|
|
BytesLimitPerDay: env.size("SWWAF_BYTES_LIMIT_PER_DAY", "100G"),
|
|
|
|
),
|
|
|
|
BytesCount: env.bytesCount("SWWAF_BYTES_COUNT", "both"),
|
|
|
|
DenyNets: env.netblocks("SWWAF_DENY_NETS", ""),
|
|
|
|
LookupSource: env.lookupSource("SWWAF_LOOKUP_SOURCE", "geojs"),
|
|
|
|
RateLimitPerMinute: env.count(
|
|
|
|
LookupDBPath: env.value("SWWAF_LOOKUP_DB_PATH", ""),
|
|
|
|
"SWWAF_RATE_LIMIT_PER_MINUTE",
|
|
|
|
LookupTimeout: env.durationNotOff("SWWAF_LOOKUP_TIMEOUT", "800ms"),
|
|
|
|
"120",
|
|
|
|
AddLookupHeaders: env.boolean("SWWAF_ADD_LOOKUP_HEADERS", "false"),
|
|
|
|
),
|
|
|
|
DeniedCountries: env.countries("SWWAF_DENIED_COUNTRIES", ""),
|
|
|
|
RateLimitPerHour: env.count("SWWAF_RATE_LIMIT_PER_HOUR", "1500"),
|
|
|
|
|
|
|
|
RateLimitPerDay: env.count("SWWAF_RATE_LIMIT_PER_DAY", "6000"),
|
|
|
|
|
|
|
|
RateLimitExemptPaths: env.pathPrefixes(
|
|
|
|
|
|
|
|
"SWWAF_RATE_LIMIT_EXEMPT_PATHS",
|
|
|
|
|
|
|
|
"",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
BytesLimitPerMinute: env.size(
|
|
|
|
|
|
|
|
"SWWAF_BYTES_LIMIT_PER_MINUTE",
|
|
|
|
|
|
|
|
"off",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
BytesLimitPerHour: env.size("SWWAF_BYTES_LIMIT_PER_HOUR", "50G"),
|
|
|
|
|
|
|
|
BytesLimitPerDay: env.size("SWWAF_BYTES_LIMIT_PER_DAY", "100G"),
|
|
|
|
|
|
|
|
BytesCount: env.bytesCount("SWWAF_BYTES_COUNT", "both"),
|
|
|
|
|
|
|
|
LookupSource: env.lookupSource(
|
|
|
|
|
|
|
|
"SWWAF_LOOKUP_SOURCE",
|
|
|
|
|
|
|
|
"geojs",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
LookupDBPath: env.value("SWWAF_LOOKUP_DB_PATH", ""),
|
|
|
|
|
|
|
|
LookupTimeout: env.durationNotOff(
|
|
|
|
|
|
|
|
"SWWAF_LOOKUP_TIMEOUT",
|
|
|
|
|
|
|
|
"800ms",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
AddLookupHeaders: env.boolean(
|
|
|
|
|
|
|
|
"SWWAF_ADD_LOOKUP_HEADERS",
|
|
|
|
|
|
|
|
"false",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
DeniedCountries: env.countries("SWWAF_DENIED_COUNTRIES", ""),
|
|
|
|
|
|
|
|
ExclusivelyAllowedCountries: env.countries(
|
|
|
|
ExclusivelyAllowedCountries: env.countries(
|
|
|
|
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
|
|
|
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
|
|
|
ASNLimitPercent: env.percents(
|
|
|
|
ASNLimitPercent: env.percents("SWWAF_ASN_LIMIT_PERCENT", ParseASN),
|
|
|
|
"SWWAF_ASN_LIMIT_PERCENT",
|
|
|
|
CountryLimitPercent: env.percents("SWWAF_COUNTRY_LIMIT_PERCENT", parseCountry),
|
|
|
|
ParseASN,
|
|
|
|
ASNBytesPercent: env.percents("SWWAF_ASN_BYTES_PERCENT", ParseASN),
|
|
|
|
),
|
|
|
|
CountryBytesPercent: env.percents("SWWAF_COUNTRY_BYTES_PERCENT", parseCountry),
|
|
|
|
CountryLimitPercent: env.percents(
|
|
|
|
UnknownLimitPercent: env.percent("SWWAF_UNKNOWN_LIMIT_PERCENT", "100"),
|
|
|
|
"SWWAF_COUNTRY_LIMIT_PERCENT",
|
|
|
|
ASNLimitPercentURL: env.listURL("SWWAF_ASN_LIMIT_PERCENT_URL"),
|
|
|
|
parseCountry,
|
|
|
|
BlocklistURLs: env.listURLs("SWWAF_BLOCKLIST_URLS"),
|
|
|
|
),
|
|
|
|
BlocklistRefresh: env.refresh("SWWAF_BLOCKLIST_REFRESH", "24h"),
|
|
|
|
ASNBytesPercent: env.percents(
|
|
|
|
DNSBLZones: env.zones("SWWAF_DNSBL_ZONES"),
|
|
|
|
"SWWAF_ASN_BYTES_PERCENT",
|
|
|
|
DNSBLResolver: env.resolver("SWWAF_DNSBL_RESOLVER"),
|
|
|
|
ParseASN,
|
|
|
|
AbuseIPDBKey: env.secret("SWWAF_ABUSEIPDB_KEY"),
|
|
|
|
),
|
|
|
|
AbuseIPDBMinScore: env.percent("SWWAF_ABUSEIPDB_MIN_SCORE", "75"),
|
|
|
|
CountryBytesPercent: env.percents(
|
|
|
|
AbuseIPDBDailyBudget: env.numberNotOff("SWWAF_ABUSEIPDB_DAILY_BUDGET", "900"),
|
|
|
|
"SWWAF_COUNTRY_BYTES_PERCENT",
|
|
|
|
ReputationCacheTTL: env.durationNotOff("SWWAF_REPUTATION_CACHE_TTL", "24h"),
|
|
|
|
parseCountry,
|
|
|
|
ReputationTimeout: env.durationNotOff("SWWAF_REPUTATION_TIMEOUT", "2s"),
|
|
|
|
),
|
|
|
|
CrowdSecDecisionsURL: env.crowdSecDecisionsURL("SWWAF_CROWDSEC_LAPI_URL"),
|
|
|
|
UnknownLimitPercent: env.percent(
|
|
|
|
CrowdSecKey: env.secret("SWWAF_CROWDSEC_LAPI_KEY"),
|
|
|
|
"SWWAF_UNKNOWN_LIMIT_PERCENT",
|
|
|
|
BanResponse: env.banResponse("SWWAF_BAN_RESPONSE", "403"),
|
|
|
|
"100",
|
|
|
|
LimitBanDuration: env.durationNotOff("SWWAF_LIMIT_BAN_DURATION", "1h"),
|
|
|
|
),
|
|
|
|
LimitBanRepeatWindow: env.durationNotOff("SWWAF_LIMIT_BAN_REPEAT_WINDOW", "24h"),
|
|
|
|
ASNLimitPercentURL: env.listURL("SWWAF_ASN_LIMIT_PERCENT_URL"),
|
|
|
|
MaxBanDuration: env.durationNotOff("SWWAF_MAX_BAN_DURATION", "7d"),
|
|
|
|
BlocklistURLs: env.listURLs("SWWAF_BLOCKLIST_URLS"),
|
|
|
|
AttackBanDuration: env.durationNotOff("SWWAF_ATTACK_BAN_DURATION", "7d"),
|
|
|
|
BlocklistRefresh: env.refresh("SWWAF_BLOCKLIST_REFRESH", "24h"),
|
|
|
|
MaxBans: env.numberNotOff("SWWAF_MAX_BANS", "5000"),
|
|
|
|
DNSBLZones: env.zones("SWWAF_DNSBL_ZONES"),
|
|
|
|
BanScopeV4Prefix: env.v4Prefix("SWWAF_BAN_SCOPE_V4_PREFIX", "32"),
|
|
|
|
DNSBLResolver: env.resolver("SWWAF_DNSBL_RESOLVER"),
|
|
|
|
StateDir: env.absolutePath("SWWAF_STATE_DIR", "/var/lib/smallwebwaf"),
|
|
|
|
AbuseIPDBKey: env.secret("SWWAF_ABUSEIPDB_KEY"),
|
|
|
|
StateWriteDelay: env.durationNotOff("SWWAF_STATE_WRITE_DELAY", "10s"),
|
|
|
|
AbuseIPDBMinScore: env.percent("SWWAF_ABUSEIPDB_MIN_SCORE", "75"),
|
|
|
|
StateCounterInterval: env.durationNotOff("SWWAF_STATE_COUNTER_INTERVAL", "15m"),
|
|
|
|
AbuseIPDBDailyBudget: env.numberNotOff(
|
|
|
|
LogRequestHeaders: env.headerNames("SWWAF_LOG_REQUEST_HEADERS",
|
|
|
|
"SWWAF_ABUSEIPDB_DAILY_BUDGET",
|
|
|
|
"accept,accept-language,accept-encoding,content-type,origin,range"),
|
|
|
|
"900",
|
|
|
|
LogLevel: env.logLevel("SWWAF_LOG_LEVEL", "info"),
|
|
|
|
),
|
|
|
|
AdminToken: env.token("SWWAF_ADMIN_TOKEN"),
|
|
|
|
ReputationCacheTTL: env.durationNotOff(
|
|
|
|
MetricsToken: env.token("SWWAF_METRICS_TOKEN"),
|
|
|
|
"SWWAF_REPUTATION_CACHE_TTL",
|
|
|
|
MetricsTopN: env.numberNotOff("SWWAF_METRICS_TOP_N", "50"),
|
|
|
|
"24h",
|
|
|
|
RulesDir: env.value("SWWAF_RULES_DIR", "/etc/smallwebwaf/rules.d"),
|
|
|
|
),
|
|
|
|
RulesEnabled: env.boolean("SWWAF_RULES_ENABLED", "true"),
|
|
|
|
ReputationTimeout: env.durationNotOff(
|
|
|
|
WAFMode: env.wafMode("SWWAF_WAF_MODE", WAFModeBlock),
|
|
|
|
"SWWAF_REPUTATION_TIMEOUT",
|
|
|
|
WAFParanoiaLevel: env.paranoiaLevel("SWWAF_WAF_PARANOIA_LEVEL", "1"),
|
|
|
|
"2s",
|
|
|
|
WAFAnomalyThreshold: env.numberOrOff("SWWAF_WAF_ANOMALY_THRESHOLD", "5"),
|
|
|
|
),
|
|
|
|
|
|
|
|
CrowdSecDecisionsURL: env.crowdSecDecisionsURL(
|
|
|
|
|
|
|
|
"SWWAF_CROWDSEC_LAPI_URL",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
CrowdSecKey: env.secret("SWWAF_CROWDSEC_LAPI_KEY"),
|
|
|
|
|
|
|
|
BanResponse: env.banResponse("SWWAF_BAN_RESPONSE", "403"),
|
|
|
|
|
|
|
|
LimitBanDuration: env.durationNotOff(
|
|
|
|
|
|
|
|
"SWWAF_LIMIT_BAN_DURATION",
|
|
|
|
|
|
|
|
"1h",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
LimitBanRepeatWindow: env.durationNotOff(
|
|
|
|
|
|
|
|
"SWWAF_LIMIT_BAN_REPEAT_WINDOW",
|
|
|
|
|
|
|
|
"24h",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
MaxBanDuration: env.durationNotOff(
|
|
|
|
|
|
|
|
"SWWAF_MAX_BAN_DURATION",
|
|
|
|
|
|
|
|
"7d",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
AttackBanDuration: env.durationNotOff(
|
|
|
|
|
|
|
|
"SWWAF_ATTACK_BAN_DURATION",
|
|
|
|
|
|
|
|
"7d",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
MaxBans: env.numberNotOff("SWWAF_MAX_BANS", "5000"),
|
|
|
|
|
|
|
|
BanScopeV4Prefix: env.v4Prefix(
|
|
|
|
|
|
|
|
"SWWAF_BAN_SCOPE_V4_PREFIX",
|
|
|
|
|
|
|
|
"32",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
StateDir: env.absolutePath(
|
|
|
|
|
|
|
|
"SWWAF_STATE_DIR",
|
|
|
|
|
|
|
|
"/var/lib/smallwebwaf",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
StateWriteDelay: env.durationNotOff(
|
|
|
|
|
|
|
|
"SWWAF_STATE_WRITE_DELAY",
|
|
|
|
|
|
|
|
"10s",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
StateCounterInterval: env.durationNotOff(
|
|
|
|
|
|
|
|
"SWWAF_STATE_COUNTER_INTERVAL",
|
|
|
|
|
|
|
|
"15m",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
LogRequestHeaders: env.headerNames(
|
|
|
|
|
|
|
|
"SWWAF_LOG_REQUEST_HEADERS",
|
|
|
|
|
|
|
|
"accept,accept-language,accept-encoding,content-type,origin,range",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
LogLevel: env.logLevel("SWWAF_LOG_LEVEL", "info"),
|
|
|
|
|
|
|
|
AdminToken: env.token("SWWAF_ADMIN_TOKEN"),
|
|
|
|
|
|
|
|
MetricsToken: env.token("SWWAF_METRICS_TOKEN"),
|
|
|
|
|
|
|
|
MetricsTopN: env.numberNotOff("SWWAF_METRICS_TOP_N", "50"),
|
|
|
|
|
|
|
|
RulesDir: env.value(
|
|
|
|
|
|
|
|
"SWWAF_RULES_DIR",
|
|
|
|
|
|
|
|
"/etc/smallwebwaf/rules.d",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
RulesEnabled: env.boolean("SWWAF_RULES_ENABLED", "true"),
|
|
|
|
|
|
|
|
WAFMode: env.wafMode("SWWAF_WAF_MODE", WAFModeBlock),
|
|
|
|
|
|
|
|
WAFParanoiaLevel: env.paranoiaLevel(
|
|
|
|
|
|
|
|
"SWWAF_WAF_PARANOIA_LEVEL",
|
|
|
|
|
|
|
|
"1",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
WAFAnomalyThreshold: env.numberOrOff(
|
|
|
|
|
|
|
|
"SWWAF_WAF_ANOMALY_THRESHOLD",
|
|
|
|
|
|
|
|
"5",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
WAFDisabledRules: env.ruleIDs("SWWAF_WAF_DISABLED_RULES",
|
|
|
|
WAFDisabledRules: env.ruleIDs("SWWAF_WAF_DISABLED_RULES",
|
|
|
|
"920340,920420,920440,920640,930130,930140"),
|
|
|
|
"920340,920420,920440,920640,930130,930140"),
|
|
|
|
WAFExemptPaths: env.pathPrefixes("SWWAF_WAF_EXEMPT_PATHS", ""),
|
|
|
|
WAFExemptPaths: env.pathPrefixes("SWWAF_WAF_EXEMPT_PATHS", ""),
|
|
|
|
WAFBodyLimit: env.wafBodyLimit("SWWAF_WAF_BODY_LIMIT", off),
|
|
|
|
WAFBodyLimit: env.wafBodyLimit("SWWAF_WAF_BODY_LIMIT", off),
|
|
|
|
TrapPaths: env.trapPaths("SWWAF_TRAP_PATHS"),
|
|
|
|
TrapPaths: env.trapPaths("SWWAF_TRAP_PATHS"),
|
|
|
|
ErrorBurstThreshold: env.count("SWWAF_ERROR_BURST_THRESHOLD", "30"),
|
|
|
|
ErrorBurstThreshold: env.count("SWWAF_ERROR_BURST_THRESHOLD", "30"),
|
|
|
|
LogRemoteURL: env.logRemoteURL("SWWAF_LOG_REMOTE_URL"),
|
|
|
|
LogRemoteURL: env.logRemoteURL("SWWAF_LOG_REMOTE_URL"),
|
|
|
|
LogRemoteTLSCAs: env.certificates(
|
|
|
|
LogRemoteTLSCAs: env.certificates("SWWAF_LOG_REMOTE_TLS_CA_FILE"),
|
|
|
|
"SWWAF_LOG_REMOTE_TLS_CA_FILE",
|
|
|
|
LogRemoteBuffer: env.numberNotOff("SWWAF_LOG_REMOTE_BUFFER", "10000"),
|
|
|
|
),
|
|
|
|
LogRemoteFacility: env.facility("SWWAF_LOG_REMOTE_FACILITY", "local0"),
|
|
|
|
LogRemoteBuffer: env.numberNotOff(
|
|
|
|
AlertWebhookURL: env.webhookURL("SWWAF_ALERT_WEBHOOK_URL"),
|
|
|
|
"SWWAF_LOG_REMOTE_BUFFER",
|
|
|
|
AlertWebhookHeaders: env.webhookHeaders("SWWAF_ALERT_WEBHOOK_HEADERS"),
|
|
|
|
"10000",
|
|
|
|
AlertSlackWebhookURL: env.webhookURL("SWWAF_ALERT_SLACK_WEBHOOK_URL"),
|
|
|
|
),
|
|
|
|
AlertNtfyURL: env.webhookURL("SWWAF_ALERT_NTFY_URL"),
|
|
|
|
LogRemoteFacility: env.facility(
|
|
|
|
AlertNtfyToken: env.secret("SWWAF_ALERT_NTFY_TOKEN"),
|
|
|
|
"SWWAF_LOG_REMOTE_FACILITY",
|
|
|
|
|
|
|
|
"local0",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
AlertWebhookURL: env.webhookURL("SWWAF_ALERT_WEBHOOK_URL"),
|
|
|
|
|
|
|
|
AlertWebhookHeaders: env.webhookHeaders(
|
|
|
|
|
|
|
|
"SWWAF_ALERT_WEBHOOK_HEADERS",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
AlertSlackWebhookURL: env.webhookURL(
|
|
|
|
|
|
|
|
"SWWAF_ALERT_SLACK_WEBHOOK_URL",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
AlertNtfyURL: env.webhookURL("SWWAF_ALERT_NTFY_URL"),
|
|
|
|
|
|
|
|
AlertNtfyToken: env.secret("SWWAF_ALERT_NTFY_TOKEN"),
|
|
|
|
|
|
|
|
AlertEvents: env.alertEvents("SWWAF_ALERT_EVENTS",
|
|
|
|
AlertEvents: env.alertEvents("SWWAF_ALERT_EVENTS",
|
|
|
|
strings.Join(alerts.Events(), ",")),
|
|
|
|
strings.Join(alerts.Events(), ",")),
|
|
|
|
AlertCooldown: env.duration("SWWAF_ALERT_COOLDOWN", "15m"),
|
|
|
|
AlertCooldown: env.duration("SWWAF_ALERT_COOLDOWN", "15m"),
|
|
|
|
AlertMaxPerHour: env.numberOrOff(
|
|
|
|
AlertMaxPerHour: env.numberOrOff("SWWAF_ALERT_MAX_PER_HOUR", "60"),
|
|
|
|
"SWWAF_ALERT_MAX_PER_HOUR",
|
|
|
|
AnomalyClient: env.thresholds("SWWAF_ANOMALY_CLIENT_"),
|
|
|
|
"60",
|
|
|
|
AnomalyNet: env.thresholds("SWWAF_ANOMALY_NET_"),
|
|
|
|
),
|
|
|
|
AnomalyASN: env.thresholds("SWWAF_ANOMALY_ASN_"),
|
|
|
|
AnomalyClient: env.thresholds("SWWAF_ANOMALY_CLIENT_"),
|
|
|
|
AnomalyTotal: env.thresholds("SWWAF_ANOMALY_TOTAL_"),
|
|
|
|
AnomalyNet: env.thresholds("SWWAF_ANOMALY_NET_"),
|
|
|
|
AnomalyWatch: env.thresholds("SWWAF_WATCH_"),
|
|
|
|
AnomalyASN: env.thresholds("SWWAF_ANOMALY_ASN_"),
|
|
|
|
AnomalyNetV4Prefix: env.v4Prefix("SWWAF_ANOMALY_NET_V4_PREFIX", "24"),
|
|
|
|
AnomalyTotal: env.thresholds("SWWAF_ANOMALY_TOTAL_"),
|
|
|
|
AnomalyNetV6Prefix: env.v6Prefix("SWWAF_ANOMALY_NET_V6_PREFIX", "48"),
|
|
|
|
AnomalyWatch: env.thresholds("SWWAF_WATCH_"),
|
|
|
|
WatchNets: env.namedNetblocks("SWWAF_WATCH_NETS"),
|
|
|
|
AnomalyNetV4Prefix: env.v4Prefix(
|
|
|
|
|
|
|
|
"SWWAF_ANOMALY_NET_V4_PREFIX",
|
|
|
|
|
|
|
|
"24",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
AnomalyNetV6Prefix: env.v6Prefix(
|
|
|
|
|
|
|
|
"SWWAF_ANOMALY_NET_V6_PREFIX",
|
|
|
|
|
|
|
|
"48",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
WatchNets: env.namedNetblocks("SWWAF_WATCH_NETS"),
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cfg.LogRemoteAppName = env.appName("SWWAF_LOG_REMOTE_APP_NAME",
|
|
|
|
cfg.LogRemoteAppName = env.appName("SWWAF_LOG_REMOTE_APP_NAME",
|
|
|
@@ -723,20 +584,14 @@ func Command(lookupEnv func(string) (string, bool)) (string, error) {
|
|
|
|
case "servewaf":
|
|
|
|
case "servewaf":
|
|
|
|
return command, nil
|
|
|
|
return command, nil
|
|
|
|
default:
|
|
|
|
default:
|
|
|
|
return "", fmt.Errorf(
|
|
|
|
return "", fmt.Errorf("SMALLWEBWAF_COMMAND: %q %w", command, errNotCommand)
|
|
|
|
"SMALLWEBWAF_COMMAND: %q %w",
|
|
|
|
|
|
|
|
command,
|
|
|
|
|
|
|
|
errNotCommand,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ServeWAFListenAddr reads only SWWAF_LISTEN_ADDR, which may be given as a
|
|
|
|
// ServeWAFListenAddr reads only SWWAF_LISTEN_ADDR, which may be given as a
|
|
|
|
// file, as FromEnvironment does. The health check in servewaf's container needs
|
|
|
|
// file, as FromEnvironment does. The health check in servewaf's container needs
|
|
|
|
// no other setting, so it reads no other, nor a file that another names.
|
|
|
|
// no other setting, so it reads no other, nor a file that another names.
|
|
|
|
func ServeWAFListenAddr(
|
|
|
|
func ServeWAFListenAddr(lookupEnv func(string) (string, bool)) (string, error) {
|
|
|
|
lookupEnv func(string) (string, bool),
|
|
|
|
|
|
|
|
) (string, error) {
|
|
|
|
|
|
|
|
env := &environment{lookupEnv: lookupEnv}
|
|
|
|
env := &environment{lookupEnv: lookupEnv}
|
|
|
|
listenAddr := env.address("SWWAF_LISTEN_ADDR")
|
|
|
|
listenAddr := env.address("SWWAF_LISTEN_ADDR")
|
|
|
|
|
|
|
|
|
|
|
@@ -798,19 +653,14 @@ func (e *environment) lookup(name string) (string, bool) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if set {
|
|
|
|
if set {
|
|
|
|
e.check(
|
|
|
|
e.check(name, fmt.Errorf("is set, and so is %s; %w", fileName, errSetTwice))
|
|
|
|
name,
|
|
|
|
|
|
|
|
fmt.Errorf("is set, and so is %s; %w", fileName, errSetTwice),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return value, set
|
|
|
|
return value, set
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
e.settings = append(e.settings, slog.String(fileName, path))
|
|
|
|
e.settings = append(e.settings, slog.String(fileName, path))
|
|
|
|
|
|
|
|
|
|
|
|
contents, err := os.ReadFile(
|
|
|
|
contents, err := os.ReadFile(path) //nolint:gosec // a file the admin names
|
|
|
|
path,
|
|
|
|
|
|
|
|
) //nolint:gosec // a file the admin names
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
e.check(fileName, fmt.Errorf("cannot be read: %w", err))
|
|
|
|
e.check(fileName, fmt.Errorf("cannot be read: %w", err))
|
|
|
|
|
|
|
|
|
|
|
@@ -1061,10 +911,7 @@ func (e *environment) zones(name string) []string {
|
|
|
|
logged[i] = MaskZoneKey(zone)
|
|
|
|
logged[i] = MaskZoneKey(zone)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
e.settings = append(
|
|
|
|
e.settings = append(e.settings, slog.String(name, strings.Join(logged, ",")))
|
|
|
|
e.settings,
|
|
|
|
|
|
|
|
slog.String(name, strings.Join(logged, ",")),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return zones
|
|
|
|
return zones
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -1116,15 +963,10 @@ func (e *environment) checkLookupDBPath(cfg *Config) {
|
|
|
|
switch {
|
|
|
|
switch {
|
|
|
|
case cfg.LookupSource == fileSource && cfg.LookupDBPath == "":
|
|
|
|
case cfg.LookupSource == fileSource && cfg.LookupDBPath == "":
|
|
|
|
e.check("SWWAF_LOOKUP_SOURCE", fmt.Errorf(
|
|
|
|
e.check("SWWAF_LOOKUP_SOURCE", fmt.Errorf(
|
|
|
|
"is file while SWWAF_LOOKUP_DB_PATH is unset; %w",
|
|
|
|
"is file while SWWAF_LOOKUP_DB_PATH is unset; %w", errNeedsDBPath))
|
|
|
|
errNeedsDBPath,
|
|
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
case cfg.LookupSource != fileSource && cfg.LookupDBPath != "":
|
|
|
|
case cfg.LookupSource != fileSource && cfg.LookupDBPath != "":
|
|
|
|
e.check("SWWAF_LOOKUP_DB_PATH", fmt.Errorf(
|
|
|
|
e.check("SWWAF_LOOKUP_DB_PATH", fmt.Errorf(
|
|
|
|
"is set while SWWAF_LOOKUP_SOURCE is %s; %w",
|
|
|
|
"is set while SWWAF_LOOKUP_SOURCE is %s; %w", cfg.LookupSource, errDBPathUnused))
|
|
|
|
cfg.LookupSource,
|
|
|
|
|
|
|
|
errDBPathUnused,
|
|
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -1165,11 +1007,8 @@ func (e *environment) checkCountriesAndLookups(cfg *Config) {
|
|
|
|
{"SWWAF_ANOMALY_ASN_BYTES_PER_HOUR", cfg.AnomalyASN.BytesPerHour > 0},
|
|
|
|
{"SWWAF_ANOMALY_ASN_BYTES_PER_HOUR", cfg.AnomalyASN.BytesPerHour > 0},
|
|
|
|
} {
|
|
|
|
} {
|
|
|
|
if setting.set {
|
|
|
|
if setting.set {
|
|
|
|
e.check(
|
|
|
|
e.check(setting.name, fmt.Errorf("is set while SWWAF_LOOKUP_SOURCE is off; %w",
|
|
|
|
setting.name,
|
|
|
|
errNeedsLookups))
|
|
|
|
fmt.Errorf("is set while SWWAF_LOOKUP_SOURCE is off; %w",
|
|
|
|
|
|
|
|
errNeedsLookups),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -1192,21 +1031,14 @@ func (e *environment) checkCrowdSec(cfg *Config) {
|
|
|
|
switch {
|
|
|
|
switch {
|
|
|
|
case decisionsURL != "" && cfg.CrowdSecKey == "":
|
|
|
|
case decisionsURL != "" && cfg.CrowdSecKey == "":
|
|
|
|
e.check("SWWAF_CROWDSEC_LAPI_URL", fmt.Errorf(
|
|
|
|
e.check("SWWAF_CROWDSEC_LAPI_URL", fmt.Errorf(
|
|
|
|
"is set while SWWAF_CROWDSEC_LAPI_KEY is unset; %w",
|
|
|
|
"is set while SWWAF_CROWDSEC_LAPI_KEY is unset; %w", errNeedsLAPIKey))
|
|
|
|
errNeedsLAPIKey,
|
|
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
case decisionsURL == "" && cfg.CrowdSecKey != "":
|
|
|
|
case decisionsURL == "" && cfg.CrowdSecKey != "":
|
|
|
|
e.check("SWWAF_CROWDSEC_LAPI_KEY", fmt.Errorf(
|
|
|
|
e.check("SWWAF_CROWDSEC_LAPI_KEY", fmt.Errorf(
|
|
|
|
"is set while SWWAF_CROWDSEC_LAPI_URL is unset; %w",
|
|
|
|
"is set while SWWAF_CROWDSEC_LAPI_URL is unset; %w", errLAPIKeyUnused))
|
|
|
|
errLAPIKeyUnused,
|
|
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
case decisionsURL != "" && (slices.Contains(cfg.BlocklistURLs, decisionsURL) ||
|
|
|
|
case decisionsURL != "" && (slices.Contains(cfg.BlocklistURLs, decisionsURL) ||
|
|
|
|
decisionsURL == cfg.ASNLimitPercentURL):
|
|
|
|
decisionsURL == cfg.ASNLimitPercentURL):
|
|
|
|
e.check(
|
|
|
|
e.check("SWWAF_CROWDSEC_LAPI_URL", fmt.Errorf("gives the decision list %q, which %w",
|
|
|
|
"SWWAF_CROWDSEC_LAPI_URL",
|
|
|
|
decisionsURL, errAnotherList))
|
|
|
|
fmt.Errorf("gives the decision list %q, which %w",
|
|
|
|
|
|
|
|
decisionsURL, errAnotherList),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -1221,9 +1053,7 @@ func (e *environment) headerNames(name, defaultValue string) []string {
|
|
|
|
|
|
|
|
|
|
|
|
// durationNotOff reads a setting that is a duration and, unlike a
|
|
|
|
// durationNotOff reads a setting that is a duration and, unlike a
|
|
|
|
// timeout, cannot be off.
|
|
|
|
// timeout, cannot be off.
|
|
|
|
func (e *environment) durationNotOff(
|
|
|
|
func (e *environment) durationNotOff(name, defaultValue string) time.Duration {
|
|
|
|
name, defaultValue string,
|
|
|
|
|
|
|
|
) time.Duration {
|
|
|
|
|
|
|
|
duration, err := ParseDurationNotOff(e.value(name, defaultValue))
|
|
|
|
duration, err := ParseDurationNotOff(e.value(name, defaultValue))
|
|
|
|
e.check(name, err)
|
|
|
|
e.check(name, err)
|
|
|
|
|
|
|
|
|
|
|
@@ -1403,10 +1233,7 @@ func (e *environment) instanceName() string {
|
|
|
|
|
|
|
|
|
|
|
|
value := e.value("SWWAF_INSTANCE_NAME", hostname)
|
|
|
|
value := e.value("SWWAF_INSTANCE_NAME", hostname)
|
|
|
|
if !utf8.ValidString(value) {
|
|
|
|
if !utf8.ValidString(value) {
|
|
|
|
e.check(
|
|
|
|
e.check("SWWAF_INSTANCE_NAME", fmt.Errorf("%q %w", value, errNotUTF8))
|
|
|
|
"SWWAF_INSTANCE_NAME",
|
|
|
|
|
|
|
|
fmt.Errorf("%q %w", value, errNotUTF8),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return value
|
|
|
|
return value
|
|
|
@@ -1415,10 +1242,7 @@ func (e *environment) instanceName() string {
|
|
|
|
// appName reads the setting that is the APP-NAME of the records the log
|
|
|
|
// appName reads the setting that is the APP-NAME of the records the log
|
|
|
|
// lines are sent in, by default the instance name. Its value is checked
|
|
|
|
// lines are sent in, by default the instance name. Its value is checked
|
|
|
|
// when it is set, and, while lines are sent, when it is the instance name.
|
|
|
|
// when it is set, and, while lines are sent, when it is the instance name.
|
|
|
|
func (e *environment) appName(
|
|
|
|
func (e *environment) appName(name, instanceName string, sending bool) string {
|
|
|
|
name, instanceName string,
|
|
|
|
|
|
|
|
sending bool,
|
|
|
|
|
|
|
|
) string {
|
|
|
|
|
|
|
|
value, set := e.lookup(name)
|
|
|
|
value, set := e.lookup(name)
|
|
|
|
if !set {
|
|
|
|
if !set {
|
|
|
|
value = instanceName
|
|
|
|
value = instanceName
|
|
|
@@ -1431,14 +1255,8 @@ func (e *environment) appName(
|
|
|
|
case set:
|
|
|
|
case set:
|
|
|
|
e.check(name, fmt.Errorf("%q %w", value, errNotAppName))
|
|
|
|
e.check(name, fmt.Errorf("%q %w", value, errNotAppName))
|
|
|
|
case sending:
|
|
|
|
case sending:
|
|
|
|
e.check(
|
|
|
|
e.check(name, fmt.Errorf("is unset, and SWWAF_INSTANCE_NAME %q, its default, %w",
|
|
|
|
name,
|
|
|
|
value, errNotAppName))
|
|
|
|
fmt.Errorf(
|
|
|
|
|
|
|
|
"is unset, and SWWAF_INSTANCE_NAME %q, its default, %w",
|
|
|
|
|
|
|
|
value,
|
|
|
|
|
|
|
|
errNotAppName,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return value
|
|
|
|
return value
|
|
|
@@ -1447,16 +1265,11 @@ func (e *environment) appName(
|
|
|
|
// checkInstanceNameForNtfy refuses an instance name that holds a control
|
|
|
|
// checkInstanceNameForNtfy refuses an instance name that holds a control
|
|
|
|
// character while ntfySet, SWWAF_ALERT_NTFY_URL being set: ntfy is sent
|
|
|
|
// character while ntfySet, SWWAF_ALERT_NTFY_URL being set: ntfy is sent
|
|
|
|
// the instance name in a header, which cannot hold one.
|
|
|
|
// the instance name in a header, which cannot hold one.
|
|
|
|
func (e *environment) checkInstanceNameForNtfy(
|
|
|
|
func (e *environment) checkInstanceNameForNtfy(instanceName string, ntfySet bool) {
|
|
|
|
instanceName string,
|
|
|
|
|
|
|
|
ntfySet bool,
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
if ntfySet && strings.ContainsFunc(instanceName, unicode.IsControl) {
|
|
|
|
if ntfySet && strings.ContainsFunc(instanceName, unicode.IsControl) {
|
|
|
|
e.check("SWWAF_INSTANCE_NAME", fmt.Errorf(
|
|
|
|
e.check("SWWAF_INSTANCE_NAME", fmt.Errorf(
|
|
|
|
"%q %w, and is sent to ntfy in a header while SWWAF_ALERT_NTFY_URL is set",
|
|
|
|
"%q %w, and is sent to ntfy in a header while SWWAF_ALERT_NTFY_URL is set",
|
|
|
|
instanceName,
|
|
|
|
instanceName, errControlCharacter))
|
|
|
|
errControlCharacter,
|
|
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -1776,10 +1589,7 @@ func parseNamedNetblocks(value string) ([]anomaly.NamedNetblock, error) {
|
|
|
|
return nil, fmt.Errorf("%q %w", name, errListedTwice)
|
|
|
|
return nil, fmt.Errorf("%q %w", name, errListedTwice)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
named = append(
|
|
|
|
named = append(named, anomaly.NamedNetblock{Name: name, Netblock: netblock})
|
|
|
|
named,
|
|
|
|
|
|
|
|
anomaly.NamedNetblock{Name: name, Netblock: netblock},
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return named, nil
|
|
|
|
return named, nil
|
|
|
@@ -2018,11 +1828,8 @@ func parseHeaderNames(value string) ([]string, error) {
|
|
|
|
header := strings.ToLower(item)
|
|
|
|
header := strings.ToLower(item)
|
|
|
|
switch header {
|
|
|
|
switch header {
|
|
|
|
case "host":
|
|
|
|
case "host":
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
return nil, fmt.Errorf("%q %w; the request's host is the field host",
|
|
|
|
"%q %w; the request's host is the field host",
|
|
|
|
item, errHeaderTakenOut)
|
|
|
|
item,
|
|
|
|
|
|
|
|
errHeaderTakenOut,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
case "transfer-encoding":
|
|
|
|
case "transfer-encoding":
|
|
|
|
return nil, fmt.Errorf("%q %w", item, errHeaderTakenOut)
|
|
|
|
return nil, fmt.Errorf("%q %w", item, errHeaderTakenOut)
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -2059,11 +1866,9 @@ func parseUpstreamURL(value string) (*url.URL, error) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onlySchemeAndHost := (upstream.Scheme == "http" || upstream.Scheme == "https") &&
|
|
|
|
onlySchemeAndHost := (upstream.Scheme == "http" || upstream.Scheme == "https") &&
|
|
|
|
upstream.Hostname() != "" && upstream.User == nil &&
|
|
|
|
upstream.Hostname() != "" && upstream.User == nil && upstream.Opaque == "" &&
|
|
|
|
upstream.Opaque == "" &&
|
|
|
|
|
|
|
|
(upstream.Path == "" || upstream.Path == "/") &&
|
|
|
|
(upstream.Path == "" || upstream.Path == "/") &&
|
|
|
|
upstream.RawQuery == "" &&
|
|
|
|
upstream.RawQuery == "" && upstream.Fragment == ""
|
|
|
|
upstream.Fragment == ""
|
|
|
|
|
|
|
|
if !onlySchemeAndHost {
|
|
|
|
if !onlySchemeAndHost {
|
|
|
|
return nil, fmt.Errorf("%q %w", value, errNotUpstreamURL)
|
|
|
|
return nil, fmt.Errorf("%q %w", value, errNotUpstreamURL)
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -2087,11 +1892,7 @@ func parseLogRemoteURL(value string) (*url.URL, error) {
|
|
|
|
return nil, fmt.Errorf("%q %w", value, errNotLogRemoteURL)
|
|
|
|
return nil, fmt.Errorf("%q %w", value, errNotLogRemoteURL)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
schemes := []string{
|
|
|
|
schemes := []string{remotelog.SchemeUDP, remotelog.SchemeTCP, remotelog.SchemeTLS}
|
|
|
|
remotelog.SchemeUDP,
|
|
|
|
|
|
|
|
remotelog.SchemeTCP,
|
|
|
|
|
|
|
|
remotelog.SchemeTLS,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
port, err := strconv.ParseUint(remote.Port(), 10, 16)
|
|
|
|
port, err := strconv.ParseUint(remote.Port(), 10, 16)
|
|
|
|
|
|
|
|
|
|
|
|
onlySchemeHostAndPort := slices.Contains(schemes, remote.Scheme) &&
|
|
|
|
onlySchemeHostAndPort := slices.Contains(schemes, remote.Scheme) &&
|
|
|
@@ -2154,9 +1955,7 @@ func isHTTPURL(u *url.URL) bool {
|
|
|
|
|
|
|
|
|
|
|
|
return (u.Scheme == "http" || u.Scheme == "https") && u.Hostname() != "" &&
|
|
|
|
return (u.Scheme == "http" || u.Scheme == "https") && u.Hostname() != "" &&
|
|
|
|
(u.Port() == "" || (err == nil && port != 0)) &&
|
|
|
|
(u.Port() == "" || (err == nil && port != 0)) &&
|
|
|
|
u.User == nil &&
|
|
|
|
u.User == nil && u.Opaque == "" && u.Fragment == ""
|
|
|
|
u.Opaque == "" &&
|
|
|
|
|
|
|
|
u.Fragment == ""
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// isListURL reports whether value is a URL a list can be fetched from, as
|
|
|
|
// isListURL reports whether value is a URL a list can be fetched from, as
|
|
|
@@ -2223,12 +2022,9 @@ func parseZones(value string) ([]string, error) {
|
|
|
|
|
|
|
|
|
|
|
|
for i, zone := range zones {
|
|
|
|
for i, zone := range zones {
|
|
|
|
shown := MaskZoneKey(zone)
|
|
|
|
shown := MaskZoneKey(zone)
|
|
|
|
listedBefore := slices.ContainsFunc(
|
|
|
|
listedBefore := slices.ContainsFunc(zones[:i], func(earlier string) bool {
|
|
|
|
zones[:i],
|
|
|
|
return strings.EqualFold(MaskZoneKey(earlier), shown)
|
|
|
|
func(earlier string) bool {
|
|
|
|
})
|
|
|
|
return strings.EqualFold(MaskZoneKey(earlier), shown)
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch {
|
|
|
|
switch {
|
|
|
|
case len(zone) > maxZoneLength:
|
|
|
|
case len(zone) > maxZoneLength:
|
|
|
@@ -2315,13 +2111,8 @@ func parseWebhookHeaders(value string) (http.Header, string, error) {
|
|
|
|
name, headerValue, found := strings.Cut(item, ":")
|
|
|
|
name, headerValue, found := strings.Cut(item, ":")
|
|
|
|
name = strings.TrimSpace(name)
|
|
|
|
name = strings.TrimSpace(name)
|
|
|
|
|
|
|
|
|
|
|
|
if !found || !IsHeaderName(name) ||
|
|
|
|
if !found || !IsHeaderName(name) || strings.ContainsAny(headerValue, "\r\n\x00") {
|
|
|
|
strings.ContainsAny(headerValue, "\r\n\x00") {
|
|
|
|
return nil, "", fmt.Errorf("item %d %w", i+1, errNotWebhookHeader)
|
|
|
|
return nil, "", fmt.Errorf(
|
|
|
|
|
|
|
|
"item %d %w",
|
|
|
|
|
|
|
|
i+1,
|
|
|
|
|
|
|
|
errNotWebhookHeader,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
headers.Add(name, strings.TrimSpace(headerValue))
|
|
|
|
headers.Add(name, strings.TrimSpace(headerValue))
|
|
|
|