check / check (push) Waiting to run
SWWAF_CROWDSEC_LAPI_URL and SWWAF_CROWDSEC_LAPI_KEY name an engine whose decision list, <url>/v1/decisions, is fetched every minute with the key in X-Api-Key, following no redirect, and kept as a blocklist is: used while a fetch fails, and across restarts through reputation.json. Ban decisions on an Ip or a Range end at the fetch time plus their duration. A listed client's request is refused and bans its netblock with the cause crowdsec until the decision ends; bans.json, ban notes and metrics take the cause. Judgement call: fetched every minute, not a setting. Judgement call: a crowdsec ban never lengthens a limit ban. Judgement call: a lifted crowdsec ban is remade while its decision lasts. Model: opus-5-5
122 lines
4.3 KiB
Go
122 lines
4.3 KiB
Go
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
|
"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
|
|
}
|
|
|
|
// crowdSecBanned reports whether a decision of the CrowdSec decision list
|
|
// on the client is in force at now. If one is, it notes the list, as
|
|
// noteHit does, and bans the client until that decision ends.
|
|
func (rq *request) crowdSecBanned(now time.Time) bool {
|
|
decision, listed := rq.h.lists.CrowdSecDecision(rq.client, now)
|
|
if !listed {
|
|
return false
|
|
}
|
|
|
|
rq.noteHit(bans.ReputationHit{Source: rq.h.config.CrowdSecDecisionsURL},
|
|
"listed by the CrowdSec decision list")
|
|
rq.banForCrowdSec(now, decision)
|
|
|
|
return true
|
|
}
|
|
|
|
// 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 := rq.h.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(bans.ReputationHit{Source: reputation.AbuseIPDBSource, Score: &score},
|
|
"scored by AbuseIPDB at or over SWWAF_ABUSEIPDB_MIN_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.
|
|
func (rq *request) noteListed(sources []string, reason string) {
|
|
for _, source := range sources {
|
|
rq.noteHit(bans.ReputationHit{Source: source}, reason)
|
|
}
|
|
}
|
|
|
|
// noteHit adds hit's source, which lists the client, to the log line's
|
|
// reputation, and hit to the notes of a ban the request makes, counts the
|
|
// source in the metrics, and raises a reputation_hit alert with reason,
|
|
// whose detail gives hit's source and score.
|
|
func (rq *request) noteHit(hit bans.ReputationHit, reason string) {
|
|
detail := map[string]any{"source": hit.Source}
|
|
if hit.Score != nil {
|
|
detail["score"] = *hit.Score
|
|
}
|
|
|
|
rq.line.Reputation = append(rq.line.Reputation, hit.Source)
|
|
rq.reputation = append(rq.reputation, hit)
|
|
rq.h.metrics.ReputationHit(hit.Source)
|
|
rq.h.alerts.Raise(alerts.Alert{
|
|
Event: alerts.EventReputationHit,
|
|
Client: rq.client,
|
|
Netblock: rq.h.clientGroup(rq.client),
|
|
ASN: rq.line.ASN,
|
|
ASName: rq.line.ASName,
|
|
Country: rq.line.Country,
|
|
Reason: reason,
|
|
Detail: detail,
|
|
})
|
|
}
|