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, }) }