check / check (push) Failing after 3m27s
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_. Every place a name appeared changed: the code, the viper keys, tests, documents, examples, the README's settings table and the image files. Model: deepseek-v4-pro
123 lines
4.4 KiB
Go
123 lines
4.4 KiB
Go
package discriminator
|
|
|
|
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 SWW_WAF_BLOCKLIST_ACTION and the SWW_WAF_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 SWW_WAF_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 SWW_WAF_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
|
|
// SWW_WAF_REPUTATION_ACTION, being deny, refuses the request, as dnsblDenied
|
|
// 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. 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 SWW_WAF_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,
|
|
Host: rq.site,
|
|
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,
|
|
})
|
|
}
|