Read every setting through viper and rename the settings (closes #211)
check / check (push) Successful in 4m46s

Every setting of discriminator 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. Discriminator'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-09 06:20:26 +00:00
1 parent aede434f6a
commit d282a737a8
109 files changed
+2196 -2079

No files matched your search

+6 -6
View File
@@ -1,7 +1,7 @@
package discriminator
// The heuristic abuseipdb. It reads SWW_ABUSEIPDB_KEY,
// SWW_ABUSEIPDB_MIN_SCORE and SWW_REPUTATION_ACTION.
// The heuristic abuseipdb. It reads SWW_WAF_ABUSEIPDB_KEY,
// SWW_WAF_ABUSEIPDB_MIN_SCORE and SWW_WAF_REPUTATION_ACTION.
import (
"context"
@@ -16,12 +16,12 @@ import (
// abuseIPDB adds the tag abuseipdb and notes AbuseIPDB, as noteHit does, with
// the score, when its score of the client is a hit, and refuses the request
// when SWW_REPUTATION_ACTION is deny, as dnsbl does for a zone. While
// SWW_ABUSEIPDB_KEY is unset it does nothing. A client without a score is
// when SWW_WAF_REPUTATION_ACTION is deny, as dnsbl does for a zone. While
// SWW_WAF_ABUSEIPDB_KEY is unset it does nothing. A client without a score is
// checked in the background, by the request's address, if its history counts
// an offence, and the request does not wait for the answer. The score is then
// used for each address of the client. Its figures are the score and
// SWW_ABUSEIPDB_MIN_SCORE.
// SWW_WAF_ABUSEIPDB_MIN_SCORE.
type abuseIPDB struct{}
func (abuseIPDB) name() string { return "abuseipdb" }
@@ -43,7 +43,7 @@ func (abuseIPDB) check(ctx context.Context, rq *request, _ time.Time) *verdict {
rq.abuseIPDBHit = true
rq.addTag(config.AbuseIPDBTag)
rq.noteHit(bans.ReputationHit{Source: reputation.AbuseIPDBSource, Score: &score},
"scored by AbuseIPDB at or over SWW_ABUSEIPDB_MIN_SCORE")
"scored by AbuseIPDB at or over SWW_WAF_ABUSEIPDB_MIN_SCORE")
if rq.h.config.ReputationAction != deny {
return nil
+1 -1
View File
@@ -51,7 +51,7 @@ func (rq *request) answerHealth() {
// answerAdmin answers a request for one of discriminator's own endpoints. Each
// needs a token, sent as Authorization: Bearer <token>: the metrics
// SWW_METRICS_TOKEN, the others SWW_ADMIN_TOKEN. A request without it is
// SWW_WAF_METRICS_TOKEN, the others SWW_WAF_ADMIN_TOKEN. A request without it is
// refused with 401. An endpoint whose token is unset answers 404, as any other
// path does.
func (rq *request) answerAdmin() {
+4 -4
View File
@@ -17,8 +17,8 @@ import (
)
const (
adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name
// adminSecret is the SWW_ADMIN_TOKEN the tests set, and adminBearer
adminToken = "SWW_WAF_ADMIN_TOKEN" //nolint:gosec // the setting's name
// adminSecret is the SWW_WAF_ADMIN_TOKEN the tests set, and adminBearer
// how a request carries it.
adminSecret = "fedcba9876543210fedcba9876543210"
adminBearer = "Bearer " + adminSecret
@@ -337,12 +337,12 @@ func TestAdminEndpointsNeedTheTokenInObserveMode(t *testing.T) {
}
}
// adminEndpoint is a request to an endpoint SWW_ADMIN_TOKEN opens.
// adminEndpoint is a request to an endpoint SWW_WAF_ADMIN_TOKEN opens.
type adminEndpoint struct {
method, path, body string
}
// adminEndpoints returns a request to each endpoint SWW_ADMIN_TOKEN
// adminEndpoints returns a request to each endpoint SWW_WAF_ADMIN_TOKEN
// opens: listing the bans, banning otherClient for an hour, lifting the
// bans on otherClient, and asking about otherClient.
func adminEndpoints() []adminEndpoint {
+2 -2
View File
@@ -15,8 +15,8 @@ import (
)
const (
alertWebhookURL = "SWW_ALERT_WEBHOOK_URL"
alertMaxPerHour = "SWW_ALERT_MAX_PER_HOUR"
alertWebhookURL = "SWW_WAF_ALERT_WEBHOOK_URL"
alertMaxPerHour = "SWW_WAF_ALERT_MAX_PER_HOUR"
// alertInstance is the instance every alert of these tests gives.
alertInstance = "fsn1app1/blog"
)
+11 -11
View File
@@ -21,11 +21,11 @@ import (
// The anomaly thresholds: the prefix of a scope followed by the end of a
// count.
const (
anomalyClient = "SWW_ANOMALY_CLIENT_"
anomalyNet = "SWW_ANOMALY_NET_"
anomalyASN = "SWW_ANOMALY_ASN_"
anomalyTotal = "SWW_ANOMALY_TOTAL_"
anomalyWatch = "SWW_WATCH_"
anomalyClient = "SWW_WAF_ANOMALY_CLIENT_"
anomalyNet = "SWW_WAF_ANOMALY_NET_"
anomalyASN = "SWW_WAF_ANOMALY_ASN_"
anomalyTotal = "SWW_WAF_ANOMALY_TOTAL_"
anomalyWatch = "SWW_WAF_WATCH_"
requestsPerMinute = "REQUESTS_PER_MINUTE"
requestsPerHour = "REQUESTS_PER_HOUR"
@@ -35,9 +35,9 @@ const (
// The other anomaly settings.
const (
anomalyNetV4Prefix = "SWW_ANOMALY_NET_V4_PREFIX"
anomalyNetV6Prefix = "SWW_ANOMALY_NET_V6_PREFIX"
watchNets = "SWW_WATCH_NETS"
anomalyNetV4Prefix = "SWW_WAF_ANOMALY_NET_V4_PREFIX"
anomalyNetV6Prefix = "SWW_WAF_ANOMALY_NET_V6_PREFIX"
watchNets = "SWW_WAF_WATCH_NETS"
)
const (
@@ -164,9 +164,9 @@ func TestEveryRequestIsCountedWhateverIsDoneWithIt(t *testing.T) {
t.Parallel()
const (
allowed = "192.0.2.7" // in SWW_ALLOW_NETS
exempt = "192.0.2.10" // in SWW_RATE_LIMIT_EXEMPT_NETS
denied = "192.0.2.20" // in SWW_DENY_NETS
allowed = "192.0.2.7" // in SWW_WAF_ALLOW_NETS
exempt = "192.0.2.10" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
denied = "192.0.2.20" // in SWW_WAF_DENY_NETS
)
s, _, _, queue := startAppWithAlerts(t, readAndAnswer, map[string]string{
+4 -4
View File
@@ -23,10 +23,10 @@ import (
)
const (
// answerTimeLimit is the default of rproxy's SWRP_TIMEOUT_MS, the time
// answerTimeLimit is the default of rproxy's SWW_RPROXY_TIMEOUT_MS, the time
// discriminator has to answer.
answerTimeLimit = 2 * time.Second
// defaultLookupWait is the default of SWW_LOOKUP_TIMEOUT.
// defaultLookupWait is the default of SWW_WAF_LOOKUP_TIMEOUT.
defaultLookupWait = 800 * time.Millisecond
// The header limits SPEC.md gives.
cookieLimit = 50
@@ -178,7 +178,7 @@ func TestHeaderLimitsHoldWhateverTheCoreRuleSetSettingsSay(t *testing.T) {
for _, env := range []map[string]string{
{wafMode: off},
{wafMode: "detect"},
{wafMode: block, "SWW_WAF_EXEMPT_PATHS": "/"},
{wafMode: block, "SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS": "/"},
} {
t.Run(fmt.Sprint(env), func(t *testing.T) {
t.Parallel()
@@ -413,7 +413,7 @@ func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
action string
}{
{
name: "SWW_DENY_NETS", env: map[string]string{denyNets: client},
name: "SWW_WAF_DENY_NETS", env: map[string]string{denyNets: client},
prepare: leaveAsItIs, status: forbidden, action: requestlog.ActionClientDenied,
},
{
+6 -6
View File
@@ -1,7 +1,7 @@
package discriminator
// The heuristic active_ban, and the code that makes bans, which several
// heuristics share. active_ban reads SWW_MODE; the bans are the ban ledger's.
// heuristics share. active_ban reads SWW_WAF_MODE; the bans are the ban ledger's.
import (
"context"
@@ -15,7 +15,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// banResponse is a refusal answered with SWW_BAN_RESPONSE, and logged
// banResponse is a refusal answered with SWW_WAF_BAN_RESPONSE, and logged
// with action.
func (rq *request) banResponse(action string) *refusal {
return &refusal{status: rq.h.config.BanResponse, action: action}
@@ -77,7 +77,7 @@ func limitFigures(hit ratelimit.Hit) map[string]any {
// countedBytes returns the request's bytes, once it has ended, as the
// byte limits and the anomaly thresholds count them: the response's body
// bytes, the request's, or both, as SWW_BYTES_COUNT says.
// bytes, the request's, or both, as SWW_WAF_BYTES_COUNT says.
func (rq *request) countedBytes() int64 {
response, request := rq.out.bytes, rq.requestBytes
@@ -94,7 +94,7 @@ func (rq *request) countedBytes() int64 {
// banForLimit bans the client's netblock at now for a broken limit, the
// one hit names, notes the offence for the log line and counts the hit in
// the metrics. status is what the client was sent, or is sent:
// SWW_BAN_RESPONSE for a request over a rate limit, what the report says for
// SWW_WAF_BAN_RESPONSE for a request over a rate limit, what the report says for
// one whose bytes broke a byte limit, the refusal for one that broke the
// error burst. The ban's notes give the client's limit percentage for a
// rate limit or a byte limit; the error burst is not lowered. The ban sets
@@ -237,7 +237,7 @@ func (rq *request) banForCrowdSec(now time.Time, decision reputation.Decision) s
// wouldAlertBan reports whether the alert for a ban on netblock for cause
// made at now would be sent. In observe mode the ban the request would
// have made is worked out only then, at most once per
// SWW_ALERT_COOLDOWN and never with no webhook set: its notes count the
// SWW_WAF_ALERT_COOLDOWN and never with no webhook set: its notes count the
// netblock's requests, which can mean going through every client.
func (rq *request) wouldAlertBan(
netblock netip.Prefix, now time.Time, cause string,
@@ -303,7 +303,7 @@ func (rq *request) netblockRequests(netblock netip.Prefix) int64 {
}
// netblock is the netblock a ban on client covers: its IPv4 address,
// widened to SWW_BAN_SCOPE_V4_PREFIX, or the IPv6 group clientGroup
// widened to SWW_WAF_BAN_SCOPE_V4_PREFIX, or the IPv6 group clientGroup
// counts it in.
func (h *handler) netblock(client netip.Addr) netip.Prefix {
addr := client.Unmap()
+11 -11
View File
@@ -26,7 +26,7 @@ const (
userAgent = "ban-test/1.0"
// permanent is the log line's ban_expires for a permanent ban.
permanent = "permanent"
// banResponseClose is the SWW_BAN_RESPONSE that closes the connection.
// banResponseClose is the SWW_WAF_BAN_RESPONSE that closes the connection.
banResponseClose = "close"
)
@@ -91,19 +91,19 @@ func TestBanLengthsFollowTheSettings(t *testing.T) {
}
}
// A first ban lasts SWW_LIMIT_BAN_DURATION; one within
// SWW_LIMIT_BAN_REPEAT_WINDOW after it ended, three times as long.
// A first ban lasts SWW_WAF_LIMIT_BAN_DURATION; one within
// SWW_WAF_LIMIT_BAN_REPEAT_WINDOW after it ended, three times as long.
wantExpires(breakLimit(), 10*time.Minute)
clk.advance(10*time.Minute + time.Hour)
wantExpires(breakLimit(), 30*time.Minute)
// Later than that, SWW_LIMIT_BAN_DURATION again.
// Later than that, SWW_WAF_LIMIT_BAN_DURATION again.
clk.advance(30*time.Minute + time.Hour + time.Second)
wantExpires(breakLimit(), 10*time.Minute)
clk.advance(10 * time.Minute)
wantExpires(breakLimit(), 30*time.Minute)
// 90 minutes would be longer than SWW_MAX_BAN_DURATION: the ban is
// 90 minutes would be longer than SWW_WAF_MAX_BAN_DURATION: the ban is
// permanent.
clk.advance(30 * time.Minute)
@@ -150,8 +150,8 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
// In the IPv4 cases, client breaks the limit; these two are next to it.
const (
allowed = "203.0.113.60" // in SWW_ALLOW_NETS
exempt = "203.0.113.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
allowed = "203.0.113.60" // in SWW_WAF_ALLOW_NETS
exempt = "203.0.113.50" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
)
for _, tc := range []struct {
@@ -166,7 +166,7 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
nil, []string{otherClient, exempt},
},
{
"the IPv4 netblock SWW_BAN_SCOPE_V4_PREFIX sets",
"the IPv4 netblock SWW_WAF_BAN_SCOPE_V4_PREFIX sets",
map[string]string{banScopeV4Prefix: "24"}, client,
[]string{otherClient, exempt}, []string{"203.0.112.9", allowed},
},
@@ -175,7 +175,7 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
[]string{"2001:db8:5::ffff:1"}, []string{"2001:db8:5:1::1"},
},
{
"the IPv6 netblock SWW_IPV6_GROUP_PREFIX sets",
"the IPv6 netblock SWW_WAF_IPV6_GROUP_PREFIX sets",
map[string]string{ipv6GroupPrefix: "48"}, "2001:db8:7::1",
[]string{"2001:db8:7:ffff::1"}, []string{"2001:db8:8::1"},
},
@@ -249,10 +249,10 @@ func TestRefusalUnderABanNamesItsHeuristicNetblockAndEnd(t *testing.T) {
func TestBanResponseAnswersEveryRefusalButTheSizeLimits(t *testing.T) {
t.Parallel()
const denied = "192.0.2.50" // in SWW_DENY_NETS
const denied = "192.0.2.50" // in SWW_WAF_DENY_NETS
for _, tc := range []struct {
setting string // "" leaves SWW_BAN_RESPONSE at its default
setting string // "" leaves SWW_WAF_BAN_RESPONSE at its default
status int // 0 is the connection closed without an answer
}{
{"", http.StatusForbidden},
+18 -17
View File
@@ -17,9 +17,9 @@ type percentage struct {
}
// biasedThresholdsSet reports whether a biased threshold can lower a
// client's limits by its lookup: SWW_ASN_BYTES_PERCENT or
// SWW_COUNTRY_BYTES_PERCENT is not empty, SWW_UNKNOWN_LIMIT_PERCENT is
// below 100, or SWW_ASN_LIMIT_PERCENT_URL is set. The client's lookup is
// client's limits by its lookup: SWW_WAF_ASN_BYTES_PERCENT or
// SWW_WAF_COUNTRY_BYTES_PERCENT is not empty, SWW_WAF_UNKNOWN_LIMIT_PERCENT is
// below 100, or SWW_WAF_ASN_LIMIT_PERCENT_URL is set. The client's lookup is
// then needed before its request goes on.
func biasedThresholdsSet(cfg *config.Config) bool {
return len(cfg.ASNBytesPercent) > 0 || len(cfg.CountryBytesPercent) > 0 ||
@@ -31,15 +31,15 @@ func biasedThresholdsSet(cfg *config.Config) bool {
// as looked up, each "" when unknown, and the blocklists, DNSBL zones and
// AbuseIPDB that list it. Each is the lowest of those the settings give
// it, the first of them in the order below when several are lowest: the
// percentage SWW_TAG_LIMIT_PERCENT gives its asn= tag, the one the file
// SWW_ASN_LIMIT_PERCENT_URL names gives its AS number, the one
// SWW_TAG_LIMIT_PERCENT gives its country= tag, the lowest it gives its
// percentage SWW_WAF_TAG_LIMIT_PERCENT gives its asn= tag, the one the file
// SWW_WAF_ASN_LIMIT_PERCENT_URL names gives its AS number, the one
// SWW_WAF_TAG_LIMIT_PERCENT gives its country= tag, the lowest it gives its
// other tags, for a client without a country, unless its request skipped
// the wait for GeoJS, SWW_UNKNOWN_LIMIT_PERCENT, for a client a blocklist
// lists, the percentage of SWW_BLOCKLIST_ACTION while it is limit, and
// the wait for GeoJS, SWW_WAF_UNKNOWN_LIMIT_PERCENT, for a client a blocklist
// lists, the percentage of SWW_WAF_BLOCKLIST_ACTION while it is limit, and
// for a client a DNSBL zone's verdict lists, or whose AbuseIPDB score is
// a hit, the percentage of SWW_REPUTATION_ACTION while it is limit. For
// the byte limits, SWW_ASN_BYTES_PERCENT and SWW_COUNTRY_BYTES_PERCENT
// a hit, the percentage of SWW_WAF_REPUTATION_ACTION while it is limit. For
// the byte limits, SWW_WAF_ASN_BYTES_PERCENT and SWW_WAF_COUNTRY_BYTES_PERCENT
// take the place of the first three for an AS number or a country they
// list.
func (rq *request) limitPercentages() (percentage, percentage) {
@@ -48,25 +48,25 @@ func (rq *request) limitPercentages() (percentage, percentage) {
unknown := percentage{percent: whole}
if country == "" && !rq.skippedLookupWait {
unknown = percentage{cfg.UnknownLimitPercent, "SWW_UNKNOWN_LIMIT_PERCENT"}
unknown = percentage{cfg.UnknownLimitPercent, "SWW_WAF_UNKNOWN_LIMIT_PERCENT"}
}
fetched := percentage{percent: whole}
if percent, listed := rq.h.lists.ASNLimitPercent(asn); listed {
fetched = percentage{percent, "SWW_ASN_LIMIT_PERCENT_URL"}
fetched = percentage{percent, "SWW_WAF_ASN_LIMIT_PERCENT_URL"}
}
blocklisted := percentage{percent: whole}
if rq.blocklisted && cfg.BlocklistAction == "limit" {
blocklisted = percentage{cfg.BlocklistLimitPercent, "SWW_BLOCKLIST_ACTION"}
blocklisted = percentage{cfg.BlocklistLimitPercent, "SWW_WAF_BLOCKLIST_ACTION"}
}
reputationListed := percentage{percent: whole}
if (rq.dnsblListed || rq.abuseIPDBHit) && cfg.ReputationAction == "limit" {
reputationListed = percentage{cfg.ReputationLimitPercent, "SWW_REPUTATION_ACTION"}
reputationListed = percentage{cfg.ReputationLimitPercent, "SWW_WAF_REPUTATION_ACTION"}
}
const tagSetting = "SWW_TAG_LIMIT_PERCENT"
const tagSetting = "SWW_WAF_TAG_LIMIT_PERCENT"
asnRequests := lowest(given(cfg.TagLimitPercent, asnTag(asn), tagSetting), fetched)
countryRequests := given(cfg.TagLimitPercent, countryTag(country), tagSetting)
@@ -81,11 +81,12 @@ func (rq *request) limitPercentages() (percentage, percentage) {
asnBytes, countryBytes := asnRequests, countryRequests
if _, listed := cfg.ASNBytesPercent[asn]; listed {
asnBytes = given(cfg.ASNBytesPercent, asn, "SWW_ASN_BYTES_PERCENT")
asnBytes = given(cfg.ASNBytesPercent, asn, "SWW_WAF_ASN_BYTES_PERCENT")
}
if _, listed := cfg.CountryBytesPercent[country]; listed {
countryBytes = given(cfg.CountryBytesPercent, country, "SWW_COUNTRY_BYTES_PERCENT")
countryBytes = given(cfg.CountryBytesPercent, country,
"SWW_WAF_COUNTRY_BYTES_PERCENT")
}
return lowest(asnRequests, countryRequests, otherTags, unknown, blocklisted,
+7 -7
View File
@@ -22,10 +22,10 @@ import (
// The biased thresholds.
const (
tagLimitPercent = "SWW_TAG_LIMIT_PERCENT"
asnBytesPercent = "SWW_ASN_BYTES_PERCENT"
countryBytesPercent = "SWW_COUNTRY_BYTES_PERCENT"
unknownLimitPercent = "SWW_UNKNOWN_LIMIT_PERCENT"
tagLimitPercent = "SWW_WAF_TAG_LIMIT_PERCENT"
asnBytesPercent = "SWW_WAF_ASN_BYTES_PERCENT"
countryBytesPercent = "SWW_WAF_COUNTRY_BYTES_PERCENT"
unknownLimitPercent = "SWW_WAF_UNKNOWN_LIMIT_PERCENT"
)
const (
@@ -34,7 +34,7 @@ const (
asnDEHalf = asnDE + ":50"
asnDEQuarter = asnDE + ":25"
countryDEHalf = "de:50"
// The same, as SWW_TAG_LIMIT_PERCENT names fromDE's tags.
// The same, as SWW_WAF_TAG_LIMIT_PERCENT names fromDE's tags.
asnTagDEHalf = "asn=64496:50"
asnTagDEQuarter = "asn=64496:25"
countryTagDEHalf = "country=DE:50"
@@ -234,7 +234,7 @@ func TestLowestPercentageApplies(t *testing.T) {
noCountry, "25 from " + unknownLimitPercent,
},
{
// SWW_UNKNOWN_LIMIT_PERCENT is left at its default, 100.
// SWW_WAF_UNKNOWN_LIMIT_PERCENT is left at its default, 100.
"the AS number's, for a client without a country",
map[string]string{tagLimitPercent: "asn=64500:25"},
noCountry, "25 from " + tagLimitPercent,
@@ -497,7 +497,7 @@ func wantPercent(t *testing.T, what string, percent *int64, setting, want string
}
// percentText gives a limit percentage and the setting that gave it as
// text, such as "50 from SWW_TAG_LIMIT_PERCENT", or none when both are
// text, such as "50 from SWW_WAF_TAG_LIMIT_PERCENT", or none when both are
// left out.
func percentText(percent *int64, setting string) string {
switch {
+3 -3
View File
@@ -1,6 +1,6 @@
package discriminator
// The heuristic blocklists. It reads SWW_BLOCKLIST_ACTION, and the blocklists
// The heuristic blocklists. It reads SWW_WAF_BLOCKLIST_ACTION, and the blocklists
// through the reputation lists.
import (
@@ -11,7 +11,7 @@ import (
)
// blocklists notes the blocklists that list the client, as noteListed does,
// and refuses the request when SWW_BLOCKLIST_ACTION is deny. Being limit, it
// and refuses the request when SWW_WAF_BLOCKLIST_ACTION is deny. Being limit, it
// lowers the client's limits instead (see limitPercentages), and being log, it
// does nothing more. Its figure is the blocklists that list the client.
type blocklists struct{}
@@ -35,7 +35,7 @@ func (blocklists) check(_ context.Context, rq *request, _ time.Time) *verdict {
// refusedByBlocklists reports whether the heuristic blocklists refuses a
// client, given listedBy, the blocklists that list it: it does when one lists
// it and SWW_BLOCKLIST_ACTION is deny.
// it and SWW_WAF_BLOCKLIST_ACTION is deny.
func (rq *request) refusedByBlocklists(listedBy []string) bool {
return len(listedBy) > 0 && rq.h.config.BlocklistAction == deny
}
+1 -1
View File
@@ -1,6 +1,6 @@
package discriminator
// The heuristic byte_limits. It reads SWW_BYTES_COUNT, and the byte limits
// The heuristic byte_limits. It reads SWW_WAF_BYTES_COUNT, and the byte limits
// and biased thresholds through the limiter and limitPercentages.
import (
+8 -8
View File
@@ -19,13 +19,13 @@ import (
// The byte limit settings.
const (
bytesLimitPerMinute = "SWW_BYTES_LIMIT_PER_MINUTE"
bytesLimitPerHour = "SWW_BYTES_LIMIT_PER_HOUR"
bytesLimitPerDay = "SWW_BYTES_LIMIT_PER_DAY"
bytesCount = "SWW_BYTES_COUNT"
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"
)
// The values of SWW_BYTES_COUNT.
// The values of SWW_WAF_BYTES_COUNT.
const (
countResponse = "response"
countRequest = "request"
@@ -40,7 +40,7 @@ const (
// byteLimit is the byte limit these tests set, as a setting: a request
// with a body and its answer, 100 bytes, go over it.
byteLimit = "99"
// minuteBytes is limit_hit for SWW_BYTES_LIMIT_PER_MINUTE.
// minuteBytes is limit_hit for SWW_WAF_BYTES_LIMIT_PER_MINUTE.
minuteBytes = "minute_bytes"
)
@@ -227,8 +227,8 @@ func TestByteLimitsLeaveOutWhatTheRateLimitsLeaveOut(t *testing.T) {
t.Parallel()
const (
allowed = "192.0.2.7" // in SWW_ALLOW_NETS
exempt = "192.0.2.10" // in SWW_RATE_LIMIT_EXEMPT_NETS
allowed = "192.0.2.7" // in SWW_WAF_ALLOW_NETS
exempt = "192.0.2.10" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
)
s, _ := startWithAnswers(t, map[string]string{
+1 -1
View File
@@ -43,7 +43,7 @@ func scheme(r *http.Request, isQuestion bool) string {
// clientGroup is the client a request is counted toward: its IPv4
// address, or its IPv6 group, the netblock its IPv6 address is in of the
// length SWW_IPV6_GROUP_PREFIX sets, a /64 by default, since one abuser
// length SWW_WAF_IPV6_GROUP_PREFIX sets, a /64 by default, since one abuser
// usually holds a whole /64. An IPv4 address in IPv6 form counts as IPv4.
func (h *handler) clientGroup(addr netip.Addr) netip.Prefix {
addr = addr.Unmap()
+13 -12
View File
@@ -1,7 +1,7 @@
package discriminator
// The heuristic core_rule_set. It reads SWW_WAF_MODE, SWW_WAF_EXEMPT_PATHS
// and SWW_WAF_ANOMALY_THRESHOLD.
// The heuristic core_rule_set. It reads SWW_WAF_CORE_RULE_SET_MODE,
// SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS and SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD.
import (
"context"
@@ -14,13 +14,13 @@ import (
)
// coreRuleSet inspects the request with the Core Rule Set, unless
// SWW_WAF_MODE is off or SWW_WAF_EXEMPT_PATHS exempts its path, as
// pathExempt decides, and notes the rules it matched and its score in the
// log line, and the rules in the metrics. A score at or over
// SWW_WAF_ANOMALY_THRESHOLD is a match: it adds the tag exploitsignature,
// raises the waf_block alert, and in block mode refuses the request with 403,
// which is an offence its client's history counts. Its figures are the score
// and the threshold.
// SWW_WAF_CORE_RULE_SET_MODE is off or SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS
// exempts its path, as pathExempt decides, and notes the rules it matched and
// its score in the log line, and the rules in the metrics. A score at or over
// SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD is a match: it adds the tag
// exploitsignature, raises the waf_block alert, and in block mode refuses the
// request with 403, which is an offence its client's history counts. Its
// figures are the score and the threshold.
type coreRuleSet struct{}
func (coreRuleSet) name() string { return "core_rule_set" }
@@ -67,7 +67,7 @@ func (coreRuleSet) check(_ context.Context, rq *request, _ time.Time) *verdict {
}
// alertWAFBlock raises the waf_block alert for the request, which the Core
// Rule Set scored at result, at or over SWW_WAF_ANOMALY_THRESHOLD. Its
// Rule Set scored at result, at or over SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD. Its
// detail gives the rule ids, the score, the method and the path with the
// query, and, for a request that is not refused for it, the mode: detect,
// or observe in observe mode.
@@ -94,7 +94,8 @@ func (rq *request) alertWAFBlock(result waf.Result) {
ASN: rq.line.ASN,
ASName: rq.line.ASName,
Country: rq.line.Country,
Reason: "scored by the Core Rule Set at or over SWW_WAF_ANOMALY_THRESHOLD",
Detail: detail,
Reason: "scored by the Core Rule Set at or over " +
"SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD",
Detail: detail,
})
}
+13 -12
View File
@@ -14,13 +14,13 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// The Core Rule Set's settings the tests set, besides SWW_WAF_MODE, and
// The Core Rule Set's settings the tests set, besides SWW_WAF_CORE_RULE_SET_MODE, and
// its two modes that inspect requests.
const (
wafAnomalyThreshold = "SWW_WAF_ANOMALY_THRESHOLD"
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
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"
block = "block"
detect = "detect"
)
@@ -28,7 +28,7 @@ const (
// formData is the type of a form's body.
const formData = "application/x-www-form-urlencoded"
// sizeLimit is SWW_WAF_BODY_LIMIT in the tests that read bodies, 1K as a
// sizeLimit is SWW_WAF_CORE_RULE_SET_BODY_LIMIT in the tests that read bodies, 1K as a
// setting.
const (
sizeLimit = 1 << 10
@@ -37,7 +37,7 @@ const (
// sqlInjection asks for / with an SQL injection in its query, which only
// the Core Rule Set's rule 942100 matches, with a score of 5, the default
// SWW_WAF_ANOMALY_THRESHOLD.
// SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD.
const sqlInjection = "/?id=1'%20OR%20'1'='1"
// wantWAF checks the request log line's waf_rule_ids and waf_score, and
@@ -171,7 +171,7 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
) {
t.Parallel()
const allowed = "192.0.2.60" // in SWW_ALLOW_NETS
const allowed = "192.0.2.60" // in SWW_WAF_ALLOW_NETS
s, _, _ := startWithClock(t, "", map[string]string{
wafMode: block,
@@ -180,7 +180,7 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
rulesDir: writeRules(t, testRules),
})
// A client in SWW_ALLOW_NETS, and a path SWW_WAF_EXEMPT_PATHS
// A client in SWW_WAF_ALLOW_NETS, and a path SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS
// exempts, are not inspected.
line := s.request(allowed, sqlInjection, http.StatusOK, requestlog.ActionForward)
wantWAF(t, line, nil)
@@ -199,7 +199,7 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
requestlog.ActionBlockedByBlockRule)
wantWAF(t, line, nil)
// With SWW_WAF_MODE off, no request is.
// With SWW_WAF_CORE_RULE_SET_MODE off, no request is.
s, _, _ = startWithClock(t, "", map[string]string{wafMode: off})
line = s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
wantWAF(t, line, nil)
@@ -514,8 +514,9 @@ func TestCoreRuleSetMatchRaisesTheWAFBlockAlert(t *testing.T) {
Host: appHost,
Client: netip.MustParseAddr(client),
Netblock: netip.MustParsePrefix(client + "/32"),
Reason: "scored by the Core Rule Set at or over SWW_WAF_ANOMALY_THRESHOLD",
Detail: detail,
Reason: "scored by the Core Rule Set at or over " +
"SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD",
Detail: detail,
})
if queue.Suppressed() != 1 {
+3 -3
View File
@@ -1,6 +1,6 @@
package discriminator
// The heuristic countries. It reads SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
// The heuristic countries. It reads SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES.
import (
"context"
@@ -10,7 +10,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// countries refuses a request when SWW_EXCLUSIVELY_ALLOWED_COUNTRIES is set and
// countries refuses a request when SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES is set and
// does not hold the client's country as it was looked up. A client without a
// country, or whose country cannot be found, is refused. Its figures are the
// country and the setting that refused it.
@@ -29,7 +29,7 @@ func (countries) check(_ context.Context, rq *request, _ time.Time) *verdict {
return &verdict{
action: requestlog.ActionCountryDenied,
figures: map[string]any{
"country": country, "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES",
"country": country, "setting": "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES",
},
}
}
+6 -6
View File
@@ -38,7 +38,7 @@ func TestCountryLists(t *testing.T) {
[]string{fromKP, unplaced},
},
{
"with a country in SWW_TAG_DENY",
"with a country in SWW_WAF_TAG_DENY",
map[string]string{tagDeny: denyKP, allowedCountries: "de,fr"},
[]string{fromKP, unplaced},
},
@@ -100,11 +100,11 @@ func TestCountryRefusalNamesItsHeuristicCountryAndSetting(t *testing.T) {
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied)
wantHeuristic(t, line, "countries",
`{"country": "KP", "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
`{"country": "KP", "setting": "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
line = s.get(unplaced, http.StatusForbidden, requestlog.ActionCountryDenied)
wantHeuristic(t, line, "countries",
`{"country": "", "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
`{"country": "", "setting": "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
}
func TestCountryRefusalKeepsARequestFromTheApp(t *testing.T) {
@@ -262,11 +262,11 @@ func TestExclusiveListRefusesAPrivateAddressUnlessAllowed(t *testing.T) {
action string
}{
{
"not in SWW_ALLOW_NETS", "",
"not in SWW_WAF_ALLOW_NETS", "",
http.StatusForbidden, requestlog.ActionCountryDenied,
},
{
"in SWW_ALLOW_NETS", "10.0.0.7,fd00::/8",
"in SWW_WAF_ALLOW_NETS", "10.0.0.7,fd00::/8",
http.StatusOK, requestlog.ActionForward,
},
} {
@@ -296,7 +296,7 @@ func TestExclusiveListRefusesAPrivateAddressUnlessAllowed(t *testing.T) {
// startGeoJS starts a stand-in for GeoJS, which places fromDE and fromKP,
// each in an AS of its own, and no other address. It returns its URL, and
// what returns the addresses it has been asked about. A test that needs
// the stand-in to be asked or to answer sets SWW_LOOKUP_TIMEOUT to an
// the stand-in to be asked or to answer sets SWW_WAF_LOOKUP_TIMEOUT to an
// hour, whether or not a request waits for the answer: on the default, a
// hold-up of the test process can abandon the request to the stand-in, and
// leave the client unknown.
+1 -1
View File
@@ -1,6 +1,6 @@
package discriminator
// The heuristic crowdsec. It reads SWW_CROWDSEC_LAPI_URL, and the decisions
// The heuristic crowdsec. It reads SWW_WAF_CROWDSEC_LAPI_URL, and the decisions
// through the reputation lists.
import (
+2 -2
View File
@@ -18,8 +18,8 @@ import (
// test puts in the copy of its decision list, at decisionsURL, that it
// needs, as reputation.json would at start.
const (
crowdSecURL = "SWW_CROWDSEC_LAPI_URL"
crowdSecKey = "SWW_CROWDSEC_LAPI_KEY"
crowdSecURL = "SWW_WAF_CROWDSEC_LAPI_URL"
crowdSecKey = "SWW_WAF_CROWDSEC_LAPI_KEY"
lapi = "http://crowdsec.example:8080"
decisionsURL = lapi + "/v1/decisions"
bouncerKey = "crowdsec-key-0123456789abcdef"
+2 -2
View File
@@ -1,6 +1,6 @@
package discriminator
// The heuristic deny_nets. It reads SWW_DENY_NETS.
// The heuristic deny_nets. It reads SWW_WAF_DENY_NETS.
import (
"context"
@@ -9,7 +9,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// denyNets refuses a client in SWW_DENY_NETS. Its figure is the netblock of
// denyNets refuses a client in SWW_WAF_DENY_NETS. Its figure is the netblock of
// the setting the client is in.
type denyNets struct{}
+3 -3
View File
@@ -40,7 +40,7 @@ const (
// headers and body.
DispositionDeny = "deny"
// DispositionClose tells rproxy to close the visitor's connection
// without an answer, which SWW_BAN_RESPONSE asks for when it is close.
// without an answer, which SWW_WAF_BAN_RESPONSE asks for when it is close.
// discriminator closing the question's connection instead would read as
// discriminator being down, and rproxy would then let the request through.
DispositionClose = "close"
@@ -302,7 +302,7 @@ func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request {
}
// allow answers a question with DispositionAllow and, while
// SWW_ADD_LOOKUP_HEADERS is set, the client's AS number and country.
// SWW_WAF_ADD_LOOKUP_HEADERS is set, the client's AS number and country.
func (rq *request) allow() {
rq.allowed = true
@@ -328,7 +328,7 @@ func (rq *request) deny(r refusal) {
rq.refused = &r
if r.status == 0 {
// SWW_BAN_RESPONSE is close. Written past rq.out, since the log line's
// SWW_WAF_BAN_RESPONSE is close. Written past rq.out, since the log line's
// status stays 0: the visitor is sent nothing.
rq.out.Header().Set(DispositionHeader, DispositionClose)
rq.out.ResponseWriter.WriteHeader(http.StatusOK)
+3 -3
View File
@@ -39,7 +39,7 @@ const (
// reportBodyLimit is the longest body SPEC.md lets a report have.
reportBodyLimit = 1 << 20
// anomalyClientPerMinute is the anomaly threshold these tests set.
anomalyClientPerMinute = "SWW_ANOMALY_CLIENT_REQUESTS_PER_MINUTE"
anomalyClientPerMinute = "SWW_WAF_ANOMALY_CLIENT_REQUESTS_PER_MINUTE"
)
// The fields of a report, as "discriminator" in SPEC.md names them.
@@ -181,7 +181,7 @@ func (s *Discriminator) history() ratelimit.History {
}
// metrics returns what the metrics endpoint answers, for a server started with
// the token as SWW_METRICS_TOKEN.
// the token as SWW_WAF_METRICS_TOKEN.
func (s *Discriminator) metrics() string {
s.t.Helper()
@@ -1485,7 +1485,7 @@ func TestALargeBodyAnnouncedAndLongHeadersAreNoReasonToRefuse(t *testing.T) {
func TestLogLevelHoldsBackNoRequestLine(t *testing.T) {
t.Parallel()
s := startDiscriminator(t, "", map[string]string{"SWW_LOG_LEVEL": "error"})
s := startDiscriminator(t, "", map[string]string{"SWW_WAF_LOG_LEVEL": "error"})
// A refused question would write a process warning, held back at error.
req := s.question(siteA, client, "/")
+2 -2
View File
@@ -1,6 +1,6 @@
package discriminator
// The heuristic dnsbl. It reads SWW_REPUTATION_ACTION, and the DNSBL zones'
// The heuristic dnsbl. It reads SWW_WAF_REPUTATION_ACTION, and the DNSBL zones'
// verdicts through the DNSBL checker.
import (
@@ -14,7 +14,7 @@ import (
// dnsbl notes the DNSBL zones whose verdict lists the client, as noteListed
// does, adds the tag dnsbl=<zone> for each, the zone's key masked, and refuses
// the request when SWW_REPUTATION_ACTION is deny. Being
// the request when SWW_WAF_REPUTATION_ACTION is deny. Being
// limit, it lowers the client's limits instead (see limitPercentages), and
// being log, it does nothing more. A zone without a verdict on the client is
// asked about it in the background, and the request does not wait for the
+1 -1
View File
@@ -37,7 +37,7 @@ const (
)
// The lists of heuristics, each in the order its heuristics run. A client in
// SWW_ALLOW_NETS skips them all.
// SWW_WAF_ALLOW_NETS skips them all.
// beforeLookup are the heuristics that run before the client's AS number and
// country are looked up, so that a client they refuse is not looked up.
+2 -2
View File
@@ -12,7 +12,7 @@ import (
)
// The headers in which an answer of allow gives rproxy the client's AS number
// and country, for the app, while SWW_ADD_LOOKUP_HEADERS is set. Go writes
// and country, for the app, while SWW_WAF_ADD_LOOKUP_HEADERS is set. Go writes
// every header name in this form, as it sends it and as it receives it, so
// X-Client-ASN arrives as X-Client-Asn; header names are not case-sensitive.
const (
@@ -23,7 +23,7 @@ const (
// lookUp looks up the client's AS number and country, in the lookup
// database or through GeoJS, and notes them for the log line and as the
// request's asn= and country= tags, unless
// SWW_LOOKUP_SOURCE is off or the client is on a private, loopback or
// SWW_WAF_LOOKUP_SOURCE is off or the client is on a private, loopback or
// link-local address, which no lookup can place. The lookup database
// answers at once. With GeoJS, while a setting needs the answer, such as a
// country list or a biased threshold, a new client's request waits for it,
+1 -1
View File
@@ -20,7 +20,7 @@ import (
// and country.
type asnAndCountry struct{ asn, asName, country string }
// fileSource is the SWW_LOOKUP_SOURCE that looks clients up in the
// fileSource is the SWW_WAF_LOOKUP_SOURCE that looks clients up in the
// lookup database.
const fileSource = "file"
+8 -8
View File
@@ -17,9 +17,9 @@ import (
)
const (
metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name
metricsTopN = "SWW_METRICS_TOP_N"
// token is the SWW_METRICS_TOKEN the tests set, and bearer how a
metricsToken = "SWW_WAF_METRICS_TOKEN" //nolint:gosec // the setting's name
metricsTopN = "SWW_WAF_METRICS_TOP_N"
// token is the SWW_WAF_METRICS_TOKEN the tests set, and bearer how a
// request carries it.
token = "0123456789abcdef0123456789abcdef"
bearer = "Bearer " + token
@@ -113,7 +113,7 @@ func TestMetricsCountTheTraffic(t *testing.T) {
func TestMetricsCountLimitsAndBans(t *testing.T) {
t.Parallel()
const denied = "192.0.2.50" // in SWW_DENY_NETS
const denied = "192.0.2.50" // in SWW_WAF_DENY_NETS
s, clk, _ := startWithClock(t, "", map[string]string{
metricsToken: token,
@@ -124,7 +124,7 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
maxBanDuration: "2h",
})
// SWW_BAN_RESPONSE=close sends no status at all.
// SWW_WAF_BAN_RESPONSE=close sends no status at all.
s.get(denied, 0, requestlog.ActionClientDenied)
// A first broken limit bans for an hour.
@@ -145,7 +145,7 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
wantMetric(t, s.scrape(), `sww_active_bans{instance="app"}`, 0)
// A limit broken again right after would ban for three hours, longer
// than SWW_MAX_BAN_DURATION, so the ban is permanent.
// than SWW_WAF_MAX_BAN_DURATION, so the ban is permanent.
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, 0, requestlog.ActionClientIsRateLimited)
@@ -327,13 +327,13 @@ func TestMetricsCountGeoJSRequestsAndFailures(t *testing.T) {
tagDeny: denyKP,
})
// GeoJS fails, so the client has no country= tag, and SWW_TAG_DENY does
// GeoJS fails, so the client has no country= tag, and SWW_WAF_TAG_DENY does
// not refuse it.
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
req.Header.Set(forwardedFor, fromDE)
wantStatus(t, do(t, req), http.StatusOK)
// The client stops waiting for GeoJS once SWW_LOOKUP_TIMEOUT is up, so
// The client stops waiting for GeoJS once SWW_WAF_LOOKUP_TIMEOUT is up, so
// GeoJS's failure can come after its request has ended.
deadline := time.Now().Add(waitLimit)
metrics := scrape(t, server.Addr)
+3 -3
View File
@@ -12,7 +12,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// observe is the value of SWW_MODE for observe mode.
// observe is the value of SWW_WAF_MODE for observe mode.
const observe = "observe"
func TestBanActionSaysWhetherTheBanRunsOut(t *testing.T) {
@@ -118,12 +118,12 @@ func TestObserveModeForwardsWhatEnforceModeRefuses(t *testing.T) {
t.Parallel()
const (
denied = "192.0.2.50" // in SWW_DENY_NETS
denied = "192.0.2.50" // in SWW_WAF_DENY_NETS
banned = otherClient // under a ban read from bans.json
)
for _, tc := range []struct {
setting string // "" leaves SWW_MODE at its default
setting string // "" leaves SWW_WAF_MODE at its default
observe bool
}{
{"", false},
+17 -17
View File
@@ -34,15 +34,15 @@ const adminPrefix = "/_sww/"
const HealthPath = "/_sww/healthz"
// MetricsPath is where the metrics are, for a request that carries
// SWW_METRICS_TOKEN.
// SWW_WAF_METRICS_TOKEN.
const MetricsPath = "/_sww/metrics"
// BansPath is where an admin lists and adds bans, and, followed by / and
// a client's address, lifts them, with SWW_ADMIN_TOKEN.
// a client's address, lifts them, with SWW_WAF_ADMIN_TOKEN.
const BansPath = "/_sww/bans"
// ClientsPath is where an admin asks what discriminator knows of a client, by
// the client's address after it, with SWW_ADMIN_TOKEN.
// the client's address after it, with SWW_WAF_ADMIN_TOKEN.
const ClientsPath = "/_sww/clients/"
// Params are what New needs.
@@ -55,16 +55,16 @@ type Params struct {
// ProcessLog receives the process's own messages.
ProcessLog *slog.Logger
// GeoJSURL is where clients' AS numbers and countries are looked up
// while SWW_LOOKUP_SOURCE is geojs, normally lookup.URL.
// while SWW_WAF_LOOKUP_SOURCE is geojs, normally lookup.URL.
GeoJSURL string
// AbuseIPDBURL is where clients are checked with AbuseIPDB while
// SWW_ABUSEIPDB_KEY is set, normally reputation.AbuseIPDBURL.
// SWW_WAF_ABUSEIPDB_KEY is set, normally reputation.AbuseIPDBURL.
AbuseIPDBURL string
// LookupFile is the lookup database they are looked up in while
// SWW_LOOKUP_SOURCE is file, and nil otherwise.
// SWW_WAF_LOOKUP_SOURCE is file, and nil otherwise.
LookupFile *lookup.File
// CoreRuleSet is what NewCoreRuleSet returns: what inspects requests
// while SWW_WAF_MODE is not off, and nil while it is.
// while SWW_WAF_CORE_RULE_SET_MODE is not off, and nil while it is.
CoreRuleSet *waf.CoreRuleSet
// Now tells the time by which requests are counted for the rate
// limits, bans are made and run out, and GeoJS's answers are kept,
@@ -77,17 +77,17 @@ type Params struct {
// permanent, for each count over an anomaly threshold, for each request
// whose client a blocklist, the CrowdSec decision list, a DNSBL zone or
// AbuseIPDB lists, for each request the Core Rule Set scores at or over
// SWW_WAF_ANOMALY_THRESHOLD, and for GeoJS failing, a fetch of a list
// SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD, and for GeoJS failing, a fetch of a list
// failing, a query to a DNSBL zone or a check with AbuseIPDB failing,
// or the day's AbuseIPDB checks used up.
Alerts *alerts.Queue
}
// Server is the server discriminator runs, with the parts whose state the state
// files keep, the lookup database, nil unless SWW_LOOKUP_SOURCE is file, the
// files keep, the lookup database, nil unless SWW_WAF_LOOKUP_SOURCE is file, the
// lists fetched from URLs, which its Run fetches, the DNSBL zones' verdicts,
// AbuseIPDB's scores and checks spent, and the metrics. RouteWatch, nil while
// SWW_ROUTEWATCH_URL is unset, keeps its answers in memory only.
// SWW_WAF_ROUTEWATCH_URL is unset, keeps its answers in memory only.
type Server struct {
*http.Server
@@ -185,7 +185,7 @@ func newHandler(params Params) *handler {
// newReputation returns the lists fetched from URLs, the DNSBL zones'
// verdicts and AbuseIPDB's scores, as the settings in params name them,
// with none fetched, asked for or checked yet, and adds their metrics to
// m, AbuseIPDB's while SWW_ABUSEIPDB_KEY is set.
// m, AbuseIPDB's while SWW_WAF_ABUSEIPDB_KEY is set.
func newReputation(
params Params, m *metrics.Metrics,
) (*reputation.Lists, *reputation.DNSBL, *reputation.AbuseIPDB) {
@@ -218,11 +218,11 @@ func newReputation(
return lists, dnsbl, abuseIPDB
}
// NewCoreRuleSet returns the Core Rule Set at SWW_WAF_PARANOIA_LEVEL,
// without the rules SWW_WAF_DISABLED_RULES switches off, reading bodies
// up to SWW_WAF_BODY_LIMIT, with the exclusions in
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE, or nil while SWW_WAF_MODE is off. It
// reads at most QuestionBodyMaxBytes less one byte: a question's body of
// NewCoreRuleSet returns the Core Rule Set at SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL,
// without the rules SWW_WAF_CORE_RULE_SET_DISABLED_RULES switches off, reading bodies
// up to SWW_WAF_CORE_RULE_SET_BODY_LIMIT, with the exclusions in
// SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE, or nil while SWW_WAF_CORE_RULE_SET_MODE is
// off. It reads at most QuestionBodyMaxBytes less one byte: a question's body of
// QuestionBodyMaxBytes may be cut short, and the Core Rule Set does not read a
// JSON or XML body over its limit. An exclusions file that waf.New refuses is
// an error in every mode. While the mode is off, a set file is loaded with the
@@ -238,7 +238,7 @@ func NewCoreRuleSet(cfg *config.Config) (*waf.CoreRuleSet, error) {
ExclusionsFile: cfg.CoreRuleSetExclusionsFile,
})
if err != nil {
return nil, fmt.Errorf("SWW_CORE_RULE_SET_EXCLUSIONS_FILE: %w", err)
return nil, fmt.Errorf("SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE: %w", err)
}
if cfg.WAFMode == config.WAFModeOff {
+43 -50
View File
@@ -17,6 +17,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
"sneak.berlin/go/smallwebwaf/internal/discriminator"
"sneak.berlin/go/smallwebwaf/internal/lookup"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
@@ -55,35 +56,35 @@ const (
// The settings the tests set.
const (
mode = "SWW_MODE"
trustedProxies = "SWRP_TRUSTED_PROXIES"
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"
rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY"
rateLimitExemptPaths = "SWW_RATE_LIMIT_EXEMPT_PATHS"
lookupSource = "SWW_LOOKUP_SOURCE"
lookupDBPath = "SWW_LOOKUP_DB_PATH"
lookupTimeout = "SWW_LOOKUP_TIMEOUT"
addLookupHeaders = "SWW_ADD_LOOKUP_HEADERS"
tagDeny = "SWW_TAG_DENY"
// denyKP is the value of SWW_TAG_DENY that refuses fromKP's country.
mode = "SWW_WAF_MODE"
trustedProxies = "SWW_RPROXY_TRUSTED_PROXIES"
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"
rateLimitPerDay = "SWW_WAF_RATE_LIMIT_PER_DAY"
rateLimitExemptPaths = "SWW_WAF_RATE_LIMIT_EXEMPT_PATHS"
lookupSource = "SWW_WAF_LOOKUP_SOURCE"
lookupDBPath = "SWW_WAF_LOOKUP_DB_PATH"
lookupTimeout = "SWW_WAF_LOOKUP_TIMEOUT"
addLookupHeaders = "SWW_WAF_ADD_LOOKUP_HEADERS"
tagDeny = "SWW_WAF_TAG_DENY"
// denyKP is the value of SWW_WAF_TAG_DENY that refuses fromKP's country.
denyKP = "country=KP"
allowedCountries = "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"
banResponse = "SWW_BAN_RESPONSE"
limitBanDuration = "SWW_LIMIT_BAN_DURATION"
limitBanRepeatWindow = "SWW_LIMIT_BAN_REPEAT_WINDOW"
maxBanDuration = "SWW_MAX_BAN_DURATION"
maxBans = "SWW_MAX_BANS"
banScopeV4Prefix = "SWW_BAN_SCOPE_V4_PREFIX"
instanceName = "SWW_INSTANCE_NAME"
logRequestHeaders = "SWW_LOG_REQUEST_HEADERS"
attackBanDuration = "SWW_ATTACK_BAN_DURATION"
rulesDir = "SWW_RULES_DIR"
wafMode = "SWW_WAF_MODE"
allowedCountries = "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES"
banResponse = "SWW_WAF_BAN_RESPONSE"
limitBanDuration = "SWW_WAF_LIMIT_BAN_DURATION"
limitBanRepeatWindow = "SWW_WAF_LIMIT_BAN_REPEAT_WINDOW"
maxBanDuration = "SWW_WAF_MAX_BAN_DURATION"
maxBans = "SWW_WAF_MAX_BANS"
banScopeV4Prefix = "SWW_WAF_BAN_SCOPE_V4_PREFIX"
instanceName = "SWW_WAF_INSTANCE_NAME"
logRequestHeaders = "SWW_WAF_LOG_REQUEST_HEADERS"
attackBanDuration = "SWW_WAF_ATTACK_BAN_DURATION"
rulesDir = "SWW_WAF_RULES_DIR"
wafMode = "SWW_WAF_CORE_RULE_SET_MODE"
)
// off is the value that switches a setting off.
@@ -296,8 +297,8 @@ func startProxyWithGeoJS(
// startProxyWithClock is startProxyWithGeoJS with requests counted and
// bans made by the time now tells, and returns discriminator's server as well, whose
// Addr is where discriminator listens. Unless env sets SWW_RULES_DIR, it is an empty
// directory, of no rules, and unless it sets SWW_INSTANCE_NAME, that is app,
// Addr is where discriminator listens. Unless env sets SWW_WAF_RULES_DIR, it is an
// empty directory, of no rules, and unless it sets SWW_WAF_INSTANCE_NAME, that is app,
// the label instance of every metric.
func startProxyWithClock(
t *testing.T, appURL, geojsURL string, now func() time.Time,
@@ -347,25 +348,21 @@ func serve(t *testing.T, server *discriminator.Server) {
// startRProxy starts rproxy in front of the app at appURL, asking the discriminator
// at discriminatorAddr, with the settings in env on top of the defaults, and returns
// where it listens. What it writes is dropped. SWRP_TIMEOUT_MS is a minute unless
// env sets it, so that on a busy host SWRP_DEFAULT_DISPOSITION does not decide.
// where it listens. What it writes is dropped. SWW_RPROXY_TIMEOUT_MS is a minute unless
// env sets it, so that on a busy host SWW_RPROXY_DEFAULT_DISPOSITION does not decide.
func startRProxy(
t *testing.T, discriminatorAddr, appURL string, env map[string]string,
) string {
t.Helper()
settings := map[string]string{
"SWRP_WAF_BACKEND": "http://" + discriminatorAddr,
"SWRP_UPSTREAM_URL": appURL,
"SWRP_TIMEOUT_MS": "60000",
"SWW_RPROXY_WAF_BACKEND": "http://" + discriminatorAddr,
"SWW_RPROXY_UPSTREAM_URL": appURL,
"SWW_RPROXY_TIMEOUT_MS": "60000",
}
maps.Copy(settings, env)
cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
value, ok := settings[name]
return value, ok
})
cfg, err := config.RProxyFromEnvironment(configtest.Viper(settings))
if err != nil {
t.Fatalf("settings %v: %v", settings, err)
}
@@ -397,11 +394,11 @@ func startRProxy(
// settings in env make it. No alert is sent from the queue: they wait in it,
// for the test to look at.
// With no geojsURL, there is no stand-in for GeoJS to look clients up at, and
// SWW_LOOKUP_SOURCE is off unless env sets it. While it is file, the lookup
// database SWW_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB
// at abuseIPDBURL while env sets SWW_ABUSEIPDB_KEY. SWW_WAF_MODE is off unless
// env sets it, so that only the tests of the Core Rule Set have their requests
// inspected by it.
// SWW_WAF_LOOKUP_SOURCE is off unless env sets it. While it is file, the lookup
// database SWW_WAF_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB
// at abuseIPDBURL while env sets SWW_WAF_ABUSEIPDB_KEY.
// SWW_WAF_CORE_RULE_SET_MODE is off unless env sets it, so that only the tests of the
// Core Rule Set have their requests inspected by it.
func newDiscriminator(
t *testing.T, geojsURL string, now func() time.Time, env map[string]string,
) (*discriminator.Server, *output, *alerts.Queue) {
@@ -426,11 +423,7 @@ func newDiscriminatorWith(
maps.Copy(settings, env)
cfg, err := config.FromEnvironment(func(name string) (string, bool) {
value, ok := settings[name]
return value, ok
})
cfg, err := config.FromEnvironment(configtest.Viper(settings))
if err != nil {
t.Fatalf("settings %v: %v", settings, err)
}
+4 -4
View File
@@ -1,7 +1,7 @@
package discriminator
// The heuristic rate_limits. It reads SWW_RATE_LIMIT_EXEMPT_NETS,
// SWW_RATE_LIMIT_EXEMPT_PATHS and SWW_BAN_RESPONSE, and the rate limits and
// The heuristic rate_limits. It reads SWW_WAF_RATE_LIMIT_EXEMPT_NETS,
// SWW_WAF_RATE_LIMIT_EXEMPT_PATHS and SWW_WAF_BAN_RESPONSE, and the rate limits and
// biased thresholds through the limiter and limitPercentages.
import (
@@ -57,8 +57,8 @@ func (rq *request) overAWholeRateLimit(now time.Time) bool {
}
// countedByRateLimits reports whether the rate limits count the request: its
// client is not in SWW_RATE_LIMIT_EXEMPT_NETS, and its path is not exempt
// under SWW_RATE_LIMIT_EXEMPT_PATHS.
// client is not in SWW_WAF_RATE_LIMIT_EXEMPT_NETS, and its path is not exempt
// under SWW_WAF_RATE_LIMIT_EXEMPT_PATHS.
func (rq *request) countedByRateLimits() bool {
cfg := rq.h.config
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// minute is the window of SWW_RATE_LIMIT_PER_MINUTE, as a log line's
// minute is the window of SWW_WAF_RATE_LIMIT_PER_MINUTE, as a log line's
// limit_hit names it.
const minute = "minute"
@@ -86,7 +86,7 @@ func TestRateLimitRefusalNamesItsHeuristicWindowCountAndLimit(t *testing.T) {
func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
t.Parallel()
// With SWW_IPV6_GROUP_PREFIX at 48, the first two addresses, in two
// With SWW_WAF_IPV6_GROUP_PREFIX at 48, the first two addresses, in two
// /64s of one /48, are one client, and the second's request breaks the
// limit; the third, in the next /48, is another client.
const (
@@ -129,7 +129,7 @@ func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
t.Parallel()
const denied = "192.0.2.50" // in SWW_DENY_NETS
const denied = "192.0.2.50" // in SWW_WAF_DENY_NETS
geojsURL, _ := startGeoJS(t)
s, _, server := startWithClock(t, geojsURL, map[string]string{
@@ -163,7 +163,7 @@ func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
s.request(client, "/assets", http.StatusForbidden,
requestlog.ActionClientIsRateLimited)
// A ban, SWW_DENY_NETS and SWW_TAG_DENY still refuse a path under a
// A ban, SWW_WAF_DENY_NETS and SWW_WAF_TAG_DENY still refuse a path under a
// prefix.
s.request(client, "/assets/app.js", http.StatusForbidden,
requestlog.ActionTemporarilyBanned)
+4 -4
View File
@@ -1,7 +1,7 @@
package discriminator
// The heuristic refusal_limit. It reads SWW_ERROR_BURST_THRESHOLD and
// SWW_BAN_RESPONSE.
// The heuristic refusal_limit. It reads SWW_WAF_ERROR_BURST_THRESHOLD and
// SWW_WAF_BAN_RESPONSE.
import (
"context"
@@ -14,8 +14,8 @@ import (
// refusalLimit counts the request once it has been answered, if discriminator
// refused it after a rule file match, a trap path or a Core Rule Set match, and
// in observe mode if enforce mode would have: more than
// SWW_ERROR_BURST_THRESHOLD such refusals of the client within a minute break
// a limit, which bans the client. A client in SWW_ALLOW_NETS, which the
// SWW_WAF_ERROR_BURST_THRESHOLD such refusals of the client within a minute break
// a limit, which bans the client. A client in SWW_WAF_ALLOW_NETS, which the
// heuristics skip, is not counted, and nothing is while the threshold is off.
// Its figures are the window, the count and the limit.
type refusalLimit struct{}
+5 -5
View File
@@ -15,7 +15,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
const errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
const errorBurstThreshold = "SWW_WAF_ERROR_BURST_THRESHOLD"
// refused is a request the tests here send, which discriminator refuses after a rule
// file match.
@@ -49,7 +49,7 @@ func (r refused) send(s *sender, from string) logLine {
}
// startForErrorBurst is startWithClock with testRules, the metrics token and
// SWW_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
// SWW_WAF_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
func startForErrorBurst(
t *testing.T, threshold string, env map[string]string,
) (*sender, *clock, *discriminator.Server) {
@@ -289,15 +289,15 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
t.Parallel()
const (
allowed = "192.0.2.60" // in SWW_ALLOW_NETS
exempt = "192.0.2.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
allowed = "192.0.2.60" // in SWW_WAF_ALLOW_NETS
exempt = "192.0.2.50" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
)
s, _, _ := startForErrorBurst(t, "1", map[string]string{
allowNets: allowed, rateLimitExemptNets: exempt,
})
// A client in SWW_ALLOW_NETS is not refused, and so not counted.
// A client in SWW_WAF_ALLOW_NETS is not refused, and so not counted.
for range 3 {
s.request(allowed, blockedPath, http.StatusOK, requestlog.ActionForward)
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/bans"
)
// deny is the SWW_BLOCKLIST_ACTION and the SWW_REPUTATION_ACTION that
// deny is the SWW_WAF_BLOCKLIST_ACTION and the SWW_WAF_REPUTATION_ACTION that
// refuses the requests of a client a source lists.
const deny = "deny"
+16 -16
View File
@@ -23,12 +23,12 @@ import (
// The reputation settings.
const (
blocklistURLs = "SWW_BLOCKLIST_URLS"
blocklistAction = "SWW_BLOCKLIST_ACTION"
asnLimitPercentURL = "SWW_ASN_LIMIT_PERCENT_URL"
blocklistURLs = "SWW_WAF_BLOCKLIST_URLS"
blocklistAction = "SWW_WAF_BLOCKLIST_ACTION"
asnLimitPercentURL = "SWW_WAF_ASN_LIMIT_PERCENT_URL"
)
// The actions of SWW_BLOCKLIST_ACTION and SWW_REPUTATION_ACTION:
// The actions of SWW_WAF_BLOCKLIST_ACTION and SWW_WAF_REPUTATION_ACTION:
// limitHalf gives a listed client half of every limit, and limitQuarter a
// quarter.
const (
@@ -177,7 +177,7 @@ func TestBlocklistsComeAfterAllowedCountriesAndSkipAllowNets(t *testing.T) {
loadLists(t, server, map[string][]string{dropURL: {fromDE, fromKP}})
// fromKP's country refuses it before the list is looked at, and fromDE,
// in SWW_ALLOW_NETS, is not checked at all: neither is noted, nor
// in SWW_WAF_ALLOW_NETS, is not checked at all: neither is noted, nor
// alerted.
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied))
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward))
@@ -274,7 +274,7 @@ func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(t *testing.T)
},
{"none, for an AS number the file does not list", nil, asnKP + ":25", none, none},
{
"SWW_ASN_BYTES_PERCENT's in place of the file's for the byte limits",
"SWW_WAF_ASN_BYTES_PERCENT's in place of the file's for the byte limits",
map[string]string{asnBytesPercent: asnDE + ":100"}, asnDEQuarter, fromURL, none,
},
} {
@@ -362,9 +362,9 @@ func TestEachBlocklistThatListsAClientRaisesAnAlertOncePerCooldownAndIsCounted(
// The DNSBL settings.
const (
dnsblZones = "SWW_DNSBL_ZONES"
dnsblResolver = "SWW_DNSBL_RESOLVER"
reputationAction = "SWW_REPUTATION_ACTION"
dnsblZones = "SWW_WAF_DNSBL_ZONES"
dnsblResolver = "SWW_WAF_DNSBL_RESOLVER"
reputationAction = "SWW_WAF_REPUTATION_ACTION"
)
// The DNSBL zones these tests name, which are never asked about the
@@ -420,7 +420,7 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
for _, from := range []string{fromDE, fromKP} {
for i := range 3 {
line := s.get(from, tc.statuses[i], tc.actions[i])
// In the order SWW_DNSBL_ZONES names them.
// In the order SWW_WAF_DNSBL_ZONES names them.
wantReputation(t, line, listedBy[from]...)
wantPercent(t, "limit_percent", line.LimitPercent,
line.LimitPercentSetting, tc.percent)
@@ -466,7 +466,7 @@ func TestDNSBLZonesComeAfterTheBlocklistsAndSkipAllowNets(t *testing.T) {
loadVerdicts(server, map[string][]string{fromKP: {dnsblZone}, fromDE: {dnsblZone}})
// The blocklist refuses fromKP before its verdict is looked at, and
// fromDE, in SWW_ALLOW_NETS, is not checked at all: neither is noted
// fromDE, in SWW_WAF_ALLOW_NETS, is not checked at all: neither is noted
// for the zone, nor alerted, nor asked about.
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied),
dropURL)
@@ -699,7 +699,7 @@ func TestRequestFromAClientWithoutAVerdictHasTheZoneAskedAboutIt(t *testing.T) {
// The AbuseIPDB settings, and accountKey, the key the tests set.
const (
abuseIPDBKey = "SWW_ABUSEIPDB_KEY"
abuseIPDBKey = "SWW_WAF_ABUSEIPDB_KEY"
accountKey = "abuseipdb-key-0123456789abcdef"
)
@@ -890,7 +890,7 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *test
rateLimitPerMinute: fourAMinute, abuseIPDBKey: accountKey,
reputationAction: tc.action,
})
// At SWW_ABUSEIPDB_MIN_SCORE, 75 by default, and just under it.
// At SWW_WAF_ABUSEIPDB_MIN_SCORE, 75 by default, and just under it.
loadScores(server, map[string]int64{fromDE: 75, fromKP: 74})
for i := range 3 {
@@ -941,7 +941,7 @@ func TestAbuseIPDBHitRaisesAnAlertWithTheScoreOncePerCooldownAndIsCounted(
// The alert is made as a DNSBL zone's is, with the score besides.
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
if len(waiting) != 1 || waiting[0].Event != alerts.EventReputationHit ||
waiting[0].Reason != "scored by AbuseIPDB at or over SWW_ABUSEIPDB_MIN_SCORE" ||
waiting[0].Reason != "scored by AbuseIPDB at or over SWW_WAF_ABUSEIPDB_MIN_SCORE" ||
waiting[0].Detail["source"] != abuseipdb || waiting[0].Detail["score"] != int64(90) ||
queue.Suppressed() != 1 {
t.Errorf("alerts waiting %+v, %d held back, want AbuseIPDB's reputation_hit "+
@@ -1012,7 +1012,7 @@ func TestBanNotesNameEachReputationSourceThatListedTheClient(t *testing.T) {
abuseIPDBKey: accountKey, reputationAction: actionLog,
})
// Every source lists client, and none otherClient, whose score is
// under SWW_ABUSEIPDB_MIN_SCORE, 75 by default.
// under SWW_WAF_ABUSEIPDB_MIN_SCORE, 75 by default.
loadLists(t, server, map[string][]string{dropURL: {client}})
loadVerdicts(server, map[string][]string{client: {dnsblZone}, otherClient: nil})
loadScores(server, map[string]int64{client: score, otherClient: 74})
@@ -1061,7 +1061,7 @@ func verdictsFetched() time.Time {
}
// loadVerdicts puts into server's DNSBL, for each client listedBy names,
// a verdict of each zone SWW_DNSBL_ZONES names, fetched at
// a verdict of each zone SWW_WAF_DNSBL_ZONES names, fetched at
// verdictsFetched, as reputation.json would at start: one that lists the
// client from each zone listedBy gives for it, and one that does not from
// each other zone.
+7 -7
View File
@@ -44,7 +44,7 @@ type decision struct {
lookedUp bool
lookupAnswer lookup.Answer
// skippedLookupWait is true for a request that did not wait for GeoJS and
// found no answer kept. SWW_UNKNOWN_LIMIT_PERCENT does not apply to it,
// found no answer kept. SWW_WAF_UNKNOWN_LIMIT_PERCENT does not apply to it,
// so that not waiting never lowers its client's limits.
skippedLookupWait bool
// counted is true for a request the rate limits counted, whose bytes
@@ -170,7 +170,7 @@ func requestHeaders(r *http.Request, names []string) map[string]string {
// check is the one place where a request can be refused. It returns nil to let
// the request through. The heuristics of checkClient decide: a request they
// refuse is answered with SWW_BAN_RESPONSE, 403 for a block rule or the Core
// refuse is answered with SWW_WAF_BAN_RESPONSE, 403 for a block rule or the Core
// Rule Set, or 431 for the header limits. In observe mode nothing is refused,
// and the log line names what enforce mode would have done. ctx is the
// request's own context.
@@ -200,7 +200,7 @@ func (rq *request) check(ctx context.Context) *refusal {
// checkClient runs the heuristics of beforeLookup on the request, then looks
// up the client's AS number and country, then runs those of afterLookup. It
// returns the first verdict, or nil when none gives one. A client in
// SWW_ALLOW_NETS skips them all, and is not looked up. ctx is the request's
// SWW_WAF_ALLOW_NETS skips them all, and is not looked up. ctx is the request's
// own context.
func (rq *request) checkClient(ctx context.Context) *verdict {
if isInside(rq.client, rq.h.config.AllowNets) {
@@ -224,7 +224,7 @@ func (rq *request) checkClient(ctx context.Context) *verdict {
}
// pathExempt reports whether r is exempt under prefixes,
// SWW_RATE_LIMIT_EXEMPT_PATHS or SWW_WAF_EXEMPT_PATHS: whether its
// SWW_WAF_RATE_LIMIT_EXEMPT_PATHS or SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS: whether its
// path as sent, the path the app receives, not percent-decoded, starts
// with one of prefixes, so that /%61ssets/x is not under /assets/ for an
// app whose router matches the path as received. A request whose decoded
@@ -326,9 +326,9 @@ func (rq *request) addToHistory() {
// countAnomalies counts the request, which has ended, and its bytes, as
// countedBytes gives them, for the anomaly thresholds, whatever was done
// with it: a request refused, one from a client in SWW_ALLOW_NETS or
// SWW_RATE_LIMIT_EXEMPT_NETS, and one for a path in
// SWW_RATE_LIMIT_EXEMPT_PATHS are counted too. It is counted for its
// with it: a request refused, one from a client in SWW_WAF_ALLOW_NETS or
// SWW_WAF_RATE_LIMIT_EXEMPT_NETS, and one for a path in
// SWW_WAF_RATE_LIMIT_EXEMPT_PATHS are counted too. It is counted for its
// client's AS number when answerAtTheEnd gives one. With every anomaly
// threshold off, the default, it does nothing.
func (rq *request) countAnomalies() {
+2 -2
View File
@@ -19,7 +19,7 @@ import (
const (
// requestIDHeader carries the request's id.
requestIDHeader = "X-Request-ID"
// instance is the SWW_INSTANCE_NAME a test sets.
// instance is the SWW_WAF_INSTANCE_NAME a test sets.
instance = "fsn1app1/blog"
// ipv6Client is a client on IPv6, and ipv6Group the netblock the rate
// limits count it as.
@@ -196,7 +196,7 @@ func TestHasAuthorizationAndHasCookieEachComeFromTheirOwnHeader(t *testing.T) {
func TestTimingsAreInOrder(t *testing.T) {
t.Parallel()
const denied = "192.0.2.50" // in SWW_DENY_NETS
const denied = "192.0.2.50" // in SWW_WAF_DENY_NETS
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
// The pause sets the times apart; a hold-up of the test only lengthens
+5 -4
View File
@@ -1,6 +1,6 @@
package discriminator
// The heuristic routewatch_tags. It reads SWW_ROUTEWATCH_URL.
// The heuristic routewatch_tags. It reads SWW_WAF_ROUTEWATCH_URL.
import (
"context"
@@ -13,9 +13,10 @@ import (
// routeWatchTags adds to the request the tags asn=<number> and prefix=<netblock>
// from what routewatch answered about the client. It refuses nothing, and the
// request does not wait for routewatch. A request that gets no tags from it has
// the reason in the log line's routewatch_tags_missing. While SWW_ROUTEWATCH_URL
// is unset it does nothing. Answers are kept by client and asked for by the
// request's address, and a route without an origin AS gives only the prefix= tag.
// the reason in the log line's routewatch_tags_missing. While
// SWW_WAF_ROUTEWATCH_URL is unset it does nothing. Answers are kept by client and
// asked for by the request's address, and a route without an origin AS gives only
// the prefix= tag.
type routeWatchTags struct{}
func (routeWatchTags) name() string { return "routewatch_tags" }
@@ -14,10 +14,10 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// routeWatchURL is SWW_ROUTEWATCH_URL, and tagsMissing the log line's field
// routeWatchURL is SWW_WAF_ROUTEWATCH_URL, and tagsMissing the log line's field
// that says why a request got no tags from routewatch.
const (
routeWatchURL = "SWW_ROUTEWATCH_URL"
routeWatchURL = "SWW_WAF_ROUTEWATCH_URL"
tagsMissing = "routewatch_tags_missing"
fromV6 = "2001:db8:1::5"
prefixV6 = "2001:db8::/32"
@@ -137,7 +137,7 @@ func TestUnsetRouteWatchChangesNothing(t *testing.T) {
s, _, server := startWithClock(t, "", map[string]string{lookupSource: off})
if server.RouteWatch != nil {
t.Error("RouteWatch is set while SWW_ROUTEWATCH_URL is unset")
t.Error("RouteWatch is set while SWW_WAF_ROUTEWATCH_URL is unset")
}
line := s.get(fromDE, http.StatusOK, requestlog.ActionForward)
+1 -1
View File
@@ -1,7 +1,7 @@
package discriminator
// The heuristic rule_files. It reads the rules of the rule files, which
// SWW_RULES_DIR and SWW_RULES_ENABLED name.
// SWW_WAF_RULES_DIR and SWW_WAF_RULES_ENABLED name.
import (
"context"
+6 -6
View File
@@ -34,14 +34,14 @@ func TestEachRuleAction(t *testing.T) {
line := s.get(client, http.StatusOK, requestlog.ActionForward)
wantRuleIDs(t, line, "noted")
// A block rule refuses with 403, whatever SWW_BAN_RESPONSE is, and
// A block rule refuses with 403, whatever SWW_WAF_BAN_RESPONSE is, and
// bans no one.
line = s.request(client, "/blocked", http.StatusForbidden,
requestlog.ActionBlockedByBlockRule)
wantRuleIDs(t, line, "noted", "blocked")
s.get(client, http.StatusOK, requestlog.ActionForward)
// A ban rule refuses with SWW_BAN_RESPONSE, and bans the client for
// A ban rule refuses with SWW_WAF_BAN_RESPONSE, and bans the client for
// seven days, the default.
line = s.request(client, "/.env", http.StatusTooManyRequests,
requestlog.ActionTemporarilyBanned)
@@ -132,7 +132,7 @@ func TestNextClearSignOfAttackAfterABanBansPermanently(t *testing.T) {
attackBanDuration: "1h",
})
// The first probe bans for SWW_ATTACK_BAN_DURATION.
// The first probe bans for SWW_WAF_ATTACK_BAN_DURATION.
line := s.request(client, "/.env", http.StatusForbidden,
requestlog.ActionTemporarilyBanned)
if line.BanExpires != requestlog.FormatTime(clk.Now().Add(time.Hour)) {
@@ -155,8 +155,8 @@ func TestRulesComeAfterTheOtherChecks(t *testing.T) {
t.Parallel()
const (
allowed = "192.0.2.60" // in SWW_ALLOW_NETS
exempt = "192.0.2.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
allowed = "192.0.2.60" // in SWW_WAF_ALLOW_NETS
exempt = "192.0.2.50" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
)
s, _, server := startWithClock(t, "", map[string]string{
@@ -166,7 +166,7 @@ func TestRulesComeAfterTheOtherChecks(t *testing.T) {
rateLimitPerMinute: "1",
})
// A client in SWW_ALLOW_NETS is not checked.
// A client in SWW_WAF_ALLOW_NETS is not checked.
line := s.request(allowed, "/.env", http.StatusOK, requestlog.ActionForward)
wantRuleIDs(t, line)
+3 -3
View File
@@ -26,7 +26,7 @@ func TestAllowNetsSkipEveryCheck(t *testing.T) {
calls.Add(1)
})
geojsURL, asked := startGeoJS(t)
// fromKP is in SWW_ALLOW_NETS, and in SWW_DENY_NETS too, which
// fromKP is in SWW_WAF_ALLOW_NETS, and in SWW_WAF_DENY_NETS too, which
// comes after it.
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
trustedProxies: trustLocalhost,
@@ -36,7 +36,7 @@ func TestAllowNetsSkipEveryCheck(t *testing.T) {
rateLimitPerMinute: "1",
})
// Neither SWW_DENY_NETS, SWW_TAG_DENY nor the limit of one request a
// Neither SWW_WAF_DENY_NETS, SWW_WAF_TAG_DENY nor the limit of one request a
// minute refuses the client, and its country is not looked up.
wantAnswers(t, addr, out, []sentRequest{
{fromKP, http.StatusOK, requestlog.ActionForward},
@@ -147,7 +147,7 @@ func TestRateLimitExemptNetsAreNeitherCountedNorRefused(t *testing.T) {
// listedAddr's requests are neither refused nor counted, so the first
// request from unlistedAddr is within the limit of one a minute.
// SWW_TAG_DENY still refuses an exempt client.
// SWW_WAF_TAG_DENY still refuses an exempt client.
wantAnswers(t, addr, out, []sentRequest{
{listedAddr, http.StatusOK, requestlog.ActionForward},
{listedAddr, http.StatusOK, requestlog.ActionForward},
+2 -2
View File
@@ -1,6 +1,6 @@
package discriminator
// The heuristic tag_deny. It reads SWW_TAG_DENY.
// The heuristic tag_deny. It reads SWW_WAF_TAG_DENY.
import (
"context"
@@ -10,7 +10,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// tagDeny refuses a request that carries a tag in SWW_TAG_DENY. Its figure is
// tagDeny refuses a request that carries a tag in SWW_WAF_TAG_DENY. Its figure is
// the first such tag, in sorted order.
type tagDeny struct{}
+1 -1
View File
@@ -51,7 +51,7 @@ func TestTagDenyRefusesAMixedCaseZoneByItsLowerCaseTag(t *testing.T) {
const mixedCaseZone = "DNSBL.example"
// The zone's tag is in lower case, however SWW_DNSBL_ZONES writes it.
// The zone's tag is in lower case, however SWW_WAF_DNSBL_ZONES writes it.
s, server, _ := startWithLookups(t, map[string]string{
dnsblZones: mixedCaseZone, dnsblResolver: noResolver,
reputationAction: actionLog, tagDeny: "dnsbl=" + dnsblZone,
+2 -2
View File
@@ -1,6 +1,6 @@
package discriminator
// The heuristic tag_lists. It reads the address lists of SWW_TAG_LISTS through
// The heuristic tag_lists. It reads the address lists of SWW_WAF_TAG_LISTS through
// the reputation lists.
import (
@@ -8,7 +8,7 @@ import (
"time"
)
// tagLists adds to the request one tag for each list of SWW_TAG_LISTS that
// tagLists adds to the request one tag for each list of SWW_WAF_TAG_LISTS that
// holds the client, the tag being the list's name. It refuses nothing, and runs
// first so that every refusal's log line carries these tags.
type tagLists struct{}
+2 -2
View File
@@ -8,9 +8,9 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// tagLists is SWW_TAG_LISTS, and vpnList and torList lists it names.
// tagLists is SWW_WAF_TAG_LISTS, and vpnList and torList lists it names.
const (
tagLists = "SWW_TAG_LISTS"
tagLists = "SWW_WAF_TAG_LISTS"
vpnList = "https://lists.example/vpn.txt"
torList = "https://lists.example/torexit.txt"
)
+2 -2
View File
@@ -1,6 +1,6 @@
package discriminator
// The heuristic trap_paths. It reads SWW_TRAP_PATHS.
// The heuristic trap_paths. It reads SWW_WAF_TRAP_PATHS.
import (
"context"
@@ -11,7 +11,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/rules"
)
// trapPaths refuses a request for a path in SWW_TRAP_PATHS: its path as a
// trapPaths refuses a request for a path in SWW_WAF_TRAP_PATHS: its path as a
// path rule sees it, without the query, as sent or once percent-decoded, is one
// of them. Such a request is a clear sign of attack, as a ban rule's match is:
// it bans the client's netblock, or in observe mode raises the alert for the
+5 -5
View File
@@ -14,14 +14,14 @@ import (
// trapPaths is the setting's name, and trapPathList what the tests set it
// to.
const (
trapPaths = "SWW_TRAP_PATHS"
trapPaths = "SWW_WAF_TRAP_PATHS"
trapPathList = "/wp-login.php,/xmlrpc.php"
)
func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
t.Parallel()
const allowed = "192.0.2.60" // in SWW_ALLOW_NETS
const allowed = "192.0.2.60" // in SWW_WAF_ALLOW_NETS
// A block rule for the same path: the trap path comes first.
s, clk, server := startWithClock(t, "", map[string]string{
@@ -38,7 +38,7 @@ func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
s.request(otherClient, path, http.StatusOK, requestlog.ActionForward)
}
// A client in SWW_ALLOW_NETS is not checked.
// A client in SWW_WAF_ALLOW_NETS is not checked.
s.request(allowed, "/xmlrpc.php", http.StatusOK, requestlog.ActionForward)
// The query is not part of the path.
@@ -114,8 +114,8 @@ func TestTrapPathsNeedNoRuleFiles(t *testing.T) {
t.Parallel()
s, _, _ := startWithClock(t, "", map[string]string{
trapPaths: trapPathList,
"SWW_RULES_ENABLED": "false",
trapPaths: trapPathList,
"SWW_WAF_RULES_ENABLED": "false",
})
s.request(client, "/xmlrpc.php", http.StatusForbidden,