check / check (push) In progress
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,
|
|
})
|
|
}
|