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
99 lines
3.6 KiB
Go
99 lines
3.6 KiB
Go
package proxy
|
|
|
|
import (
|
|
"context"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
|
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
|
)
|
|
|
|
// deny is the SWWAF_BLOCKLIST_ACTION and the SWWAF_REPUTATION_ACTION that
|
|
// refuses the requests of a client a source lists.
|
|
const deny = "deny"
|
|
|
|
// blocklistDenied notes the blocklists that list the client, as
|
|
// noteListed does, and reports whether SWWAF_BLOCKLIST_ACTION, being deny,
|
|
// refuses the request. Being limit, it lowers the client's limits instead
|
|
// (see limitPercentages), and being log, it does nothing more.
|
|
func (rq *request) blocklistDenied() bool {
|
|
listedBy := rq.h.lists.ListedBy(rq.client)
|
|
rq.blocklisted = len(listedBy) > 0
|
|
rq.noteListed(listedBy, "listed by a blocklist")
|
|
|
|
return rq.blocklisted && rq.h.config.BlocklistAction == deny
|
|
}
|
|
|
|
// dnsblDenied notes the DNSBL zones whose verdict lists the client, as
|
|
// noteListed does, and reports whether SWWAF_REPUTATION_ACTION, being
|
|
// deny, refuses the request. 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 answer. ctx is the
|
|
// request's own context.
|
|
func (rq *request) dnsblDenied(ctx context.Context) bool {
|
|
listedBy := rq.h.dnsbl.ListedBy(ctx, rq.client)
|
|
rq.dnsblListed = len(listedBy) > 0
|
|
rq.noteListed(listedBy, "listed by a DNSBL zone")
|
|
|
|
return rq.dnsblListed && rq.h.config.ReputationAction == deny
|
|
}
|
|
|
|
// abuseIPDBDenied notes AbuseIPDB, as noteHit does, with the score, when
|
|
// its score of the client is a hit, and reports whether
|
|
// SWWAF_REPUTATION_ACTION, being deny, refuses the request, as dnsblDenied
|
|
// does for a zone. While SWWAF_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. ctx is the request's own context.
|
|
func (rq *request) abuseIPDBDenied(ctx context.Context) bool {
|
|
if rq.h.config.AbuseIPDBKey == "" {
|
|
return false
|
|
}
|
|
|
|
client := clientGroup(rq.client)
|
|
held, _ := rq.h.limiter.Client(client)
|
|
offender := held.History.Offences != ratelimit.Offences{}
|
|
|
|
score, hit := rq.h.abuseIPDB.Hit(ctx, client, rq.client, offender)
|
|
if !hit {
|
|
return false
|
|
}
|
|
|
|
rq.abuseIPDBHit = true
|
|
rq.noteHit(reputation.AbuseIPDBSource, "scored by AbuseIPDB at or over "+
|
|
"SWWAF_ABUSEIPDB_MIN_SCORE", map[string]any{
|
|
"source": reputation.AbuseIPDBSource, "score": score,
|
|
})
|
|
|
|
return rq.h.config.ReputationAction == deny
|
|
}
|
|
|
|
// noteListed notes each of sources, the URLs of the blocklists or the
|
|
// DNSBL zones, their keys masked, that list the client, as noteHit does,
|
|
// with reason, and the source in the alert's detail.
|
|
func (rq *request) noteListed(sources []string, reason string) {
|
|
for _, source := range sources {
|
|
rq.noteHit(source, reason, map[string]any{"source": source})
|
|
}
|
|
}
|
|
|
|
// noteHit adds source, which lists the client, to the log line's
|
|
// reputation, counts it in the metrics, and raises a reputation_hit alert
|
|
// with reason and detail.
|
|
func (rq *request) noteHit(source, reason string, detail map[string]any) {
|
|
rq.line.Reputation = append(rq.line.Reputation, source)
|
|
rq.h.metrics.ReputationHit(source)
|
|
rq.h.alerts.Raise(alerts.Alert{
|
|
Event: alerts.EventReputationHit,
|
|
Client: rq.client,
|
|
Netblock: clientGroup(rq.client),
|
|
ASN: rq.line.ASN,
|
|
ASName: rq.line.ASName,
|
|
Country: rq.line.Country,
|
|
Reason: reason,
|
|
Detail: detail,
|
|
})
|
|
}
|