check / check (push) Waiting to run
The package passes nothing to an app since rproxy took that over, so it is named after the command it serves. Its constructor is New, like rproxy's. The rproxy tests call their stand-in for servewaf standIn, since a local servewaf would hide the package. No change of behaviour. Model: opus-5-5
89 lines
2.6 KiB
Go
89 lines
2.6 KiB
Go
package servewaf
|
|
|
|
import (
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
)
|
|
|
|
// checkCoreRuleSet inspects the request with the Core Rule Set, unless
|
|
// SWWAF_WAF_MODE is off or SWWAF_WAF_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
|
|
// SWWAF_WAF_ANOMALY_THRESHOLD is a match: it raises the waf_block alert,
|
|
// and in block mode refuses the request, which is an offence its client's
|
|
// history counts, and so returns ActionWAFBlocked. It returns "" for a
|
|
// request it does not refuse.
|
|
func (rq *request) checkCoreRuleSet() string {
|
|
cfg := rq.h.config
|
|
if cfg.WAFMode == config.WAFModeOff || pathExempt(rq.in, cfg.WAFExemptPaths) {
|
|
return ""
|
|
}
|
|
|
|
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.DurationWAF = new(requestlog.Milliseconds(time.Since(start)))
|
|
rq.line.WAFRuleIDs = result.RuleIDs
|
|
rq.line.WAFScore = &result.Score
|
|
|
|
for _, id := range result.RuleIDs {
|
|
rq.h.metrics.WAFMatched(cfg.WAFMode, id)
|
|
}
|
|
|
|
threshold := cfg.WAFAnomalyThreshold
|
|
if threshold == 0 || result.Score < threshold {
|
|
return ""
|
|
}
|
|
|
|
rq.alertWAFBlock(result)
|
|
|
|
if cfg.WAFMode == config.WAFModeDetect {
|
|
return ""
|
|
}
|
|
|
|
rq.wafBlocked = true
|
|
|
|
return requestlog.ActionWAFBlocked
|
|
}
|
|
|
|
// alertWAFBlock raises the waf_block alert for the request, which the Core
|
|
// Rule Set scored at result, at or over SWWAF_WAF_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) alertWAFBlock(result waf.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.EventWAFBlock,
|
|
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 SWWAF_WAF_ANOMALY_THRESHOLD",
|
|
Detail: detail,
|
|
})
|
|
}
|