AbuseIPDB scores for clients that committed an offence, within a daily budget (closes #105)
check / check (push) Waiting to run

With SWWAF_ABUSEIPDB_KEY set, a client whose history counts an offence
(a broken limit, a ban rule's match or a block rule's refusal, counted
by kind) is checked in the background, at most
SWWAF_ABUSEIPDB_DAILY_BUDGET checks a day, the count kept in
reputation.json. A client, an IPv4 address or an IPv6 /64, is checked by
the address it sent from, and its score serves all its addresses. A
score at or over SWWAF_ABUSEIPDB_MIN_SCORE is a hit for
SWWAF_REPUTATION_ACTION, logged as abuseipdb and alerted with its score.
A failure or the used-up budget gives no score and raises
source_failure. The key goes only in the Key header.

Judgement call: the budget's day is UTC; AbuseIPDB documents no reset time.
Judgement call: each check sent spends budget; a minute's pause after a failure.

Model: opus-5-5
This commit is contained in:
2026-10-07 20:22:37 +00:00
parent 0b0f207423
commit 2672fc3b34
23 changed files with 2040 additions and 309 deletions
+21 -10
View File
@@ -155,14 +155,22 @@ type Config struct {
// DNSBLZones are the DNSBL zones clients are asked about
// (SWWAF_DNSBL_ZONES), through DNSBLResolver (SWWAF_DNSBL_RESOLVER), or
// the host's resolver while that is the zero AddrPort.
// ReputationAction is what is done with a client a zone's verdict lists
// (SWWAF_REPUTATION_ACTION): deny, limit or log; for limit,
// ReputationLimitPercent is the percentage of every limit it gets. A
// verdict is used for ReputationCacheTTL after it was fetched
// (SWWAF_REPUTATION_CACHE_TTL), and a query may take ReputationTimeout
// (SWWAF_REPUTATION_TIMEOUT). Neither can be off.
// AbuseIPDBKey is the key of the AbuseIPDB account clients are checked
// with (SWWAF_ABUSEIPDB_KEY), "" while it is unset and none is. A score
// of AbuseIPDBMinScore or more is a hit (SWWAF_ABUSEIPDB_MIN_SCORE), and
// at most AbuseIPDBDailyBudget checks are made a day
// (SWWAF_ABUSEIPDB_DAILY_BUDGET).
// ReputationAction is what is done with a client a zone's verdict lists,
// or whose score is a hit (SWWAF_REPUTATION_ACTION): deny, limit or log;
// for limit, ReputationLimitPercent is the percentage of every limit it
// gets. A verdict or a score is used for ReputationCacheTTL after it was
// fetched (SWWAF_REPUTATION_CACHE_TTL), and a query or a check may take
// ReputationTimeout (SWWAF_REPUTATION_TIMEOUT). Neither can be off.
DNSBLZones []string
DNSBLResolver netip.AddrPort
AbuseIPDBKey string
AbuseIPDBMinScore int64
AbuseIPDBDailyBudget int
ReputationAction string
ReputationLimitPercent int64
ReputationCacheTTL time.Duration
@@ -440,6 +448,9 @@ func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
BlocklistRefresh: env.refresh("SWWAF_BLOCKLIST_REFRESH", "24h"),
DNSBLZones: env.zones("SWWAF_DNSBL_ZONES"),
DNSBLResolver: env.resolver("SWWAF_DNSBL_RESOLVER"),
AbuseIPDBKey: env.secret("SWWAF_ABUSEIPDB_KEY"),
AbuseIPDBMinScore: env.percent("SWWAF_ABUSEIPDB_MIN_SCORE", "75"),
AbuseIPDBDailyBudget: env.numberNotOff("SWWAF_ABUSEIPDB_DAILY_BUDGET", "900"),
ReputationCacheTTL: env.durationNotOff("SWWAF_REPUTATION_CACHE_TTL", "24h"),
ReputationTimeout: env.durationNotOff("SWWAF_REPUTATION_TIMEOUT", "2s"),
BanResponse: env.banResponse("SWWAF_BAN_RESPONSE", "403"),
@@ -1110,10 +1121,10 @@ func (e *environment) webhookHeaders(name string) http.Header {
}
// secret reads a setting that is a secret another service gave, such as
// an ntfy token, "" while it is unset. It is sent in a header, which
// cannot hold a control character, so one in it is an error. The log
// shows ******** in place of a value that is not empty, and an error
// shows none of it.
// an ntfy token or an AbuseIPDB key, "" while it is unset. It is sent in
// a header, which cannot hold a control character, so one in it is an
// error. The log shows ******** in place of a value that is not empty, and
// an error shows none of it.
func (e *environment) secret(name string) string {
value, _ := e.lookup(name)