check / check (push) Failing after 1m14s
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_. Model: deepseek-v4-pro (first version); opus-5-5 (rework)
102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
package discriminator
|
|
|
|
// The heuristic core_rule_set. It reads SWW_WAF_CORE_RULE_SET_MODE,
|
|
// SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS and SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD.
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
|
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
)
|
|
|
|
// coreRuleSet inspects the request with the Core Rule Set, unless
|
|
// SWW_WAF_CORE_RULE_SET_MODE is off or SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS
|
|
// exempts its path, as pathExempt decides, and notes the rules it matched and
|
|
// its score in the log line, and the rules in the metrics. A score at or over
|
|
// SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD is a match: it adds the tag
|
|
// exploitsignature, raises the core_rule_set_match alert, and in block mode
|
|
// refuses the request with 403, which is an offence its client's history
|
|
// counts. Its figures are the score and the threshold.
|
|
type coreRuleSet struct{}
|
|
|
|
func (coreRuleSet) name() string { return "core_rule_set" }
|
|
|
|
func (coreRuleSet) check(_ context.Context, rq *request, _ time.Time) *verdict {
|
|
cfg := rq.h.config
|
|
if cfg.WAFMode == config.WAFModeOff || pathExempt(rq.in, cfg.WAFExemptPaths) {
|
|
return nil
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
// A question's body has been read into memory, so that reading it here
|
|
// cannot fail.
|
|
result, read, _ := rq.h.coreRuleSet.Inspect(rq.in, rq.client, rq.in.Body)
|
|
rq.requestBytes = int64(len(read))
|
|
|
|
rq.line.DurationCoreRuleSetMS = new(requestlog.Milliseconds(time.Since(start)))
|
|
rq.line.CoreRuleSetRuleIDs = result.RuleIDs
|
|
rq.line.CoreRuleSetScore = &result.Score
|
|
|
|
for _, id := range result.RuleIDs {
|
|
rq.h.metrics.CoreRuleSetMatched(cfg.WAFMode, id)
|
|
}
|
|
|
|
threshold := cfg.WAFAnomalyThreshold
|
|
if threshold == 0 || result.Score < threshold {
|
|
return nil
|
|
}
|
|
|
|
rq.addTag(config.ExploitSignatureTag)
|
|
rq.alertCoreRuleSetMatch(result)
|
|
|
|
if cfg.WAFMode == config.WAFModeDetect {
|
|
return nil
|
|
}
|
|
|
|
rq.blockedByCoreRuleSet = true
|
|
|
|
return &verdict{
|
|
action: requestlog.ActionBlockedByCoreRuleSet,
|
|
figures: map[string]any{figureScore: result.Score, "threshold": threshold},
|
|
}
|
|
}
|
|
|
|
// alertCoreRuleSetMatch raises the core_rule_set_match alert for the request,
|
|
// which the Core Rule Set scored at result, at or over
|
|
// SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD. Its detail gives the rule ids, the
|
|
// score, the method and the path with the query, and, for a request that is
|
|
// not refused for it, the mode: detect, or observe in observe mode.
|
|
func (rq *request) alertCoreRuleSetMatch(result coreruleset.Result) {
|
|
detail := map[string]any{
|
|
"rule_ids": result.RuleIDs,
|
|
"score": result.Score,
|
|
"method": rq.in.Method,
|
|
"path": rq.in.URL.RequestURI(),
|
|
}
|
|
|
|
switch {
|
|
case rq.h.config.WAFMode == config.WAFModeDetect:
|
|
detail["mode"] = config.WAFModeDetect
|
|
case rq.h.config.Observe:
|
|
detail["mode"] = "observe"
|
|
}
|
|
|
|
rq.h.alerts.Raise(alerts.Alert{
|
|
Event: alerts.EventCoreRuleSetMatch,
|
|
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: "scored by the Core Rule Set at or over " +
|
|
"SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD",
|
|
Detail: detail,
|
|
})
|
|
}
|