check / check (push) Waiting to run
Coraza v3.8.1 runs the Core Rule Set 4.25.0 (coraza-coreruleset v4.25.0) after the rule files, with the six changes and the default SWWAF_WAF_DISABLED_RULES that SPEC.md gives; no body, no response. SWWAF_WAF_MODE, SWWAF_WAF_PARANOIA_LEVEL, SWWAF_WAF_ANOMALY_THRESHOLD and SWWAF_WAF_EXEMPT_PATHS as specified. In block mode a match is refused with 403, an offence counted toward the error burst; in detect mode it is let through. Both log waf_rule_ids, waf_score and duration_waf, raise waf_block, and count smallwebwaf_waf_matches_total. Judgement call: waf_block is raised in block mode too. Deviation: no engine-error path; with no body read, Coraza cannot fail. Model: opus-5-5
83 lines
2.4 KiB
Go
83 lines
2.4 KiB
Go
package proxy
|
|
|
|
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.URL, cfg.WAFExemptPaths) {
|
|
return ""
|
|
}
|
|
|
|
start := time.Now()
|
|
result := rq.h.coreRuleSet.Inspect(rq.in, rq.client)
|
|
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,
|
|
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,
|
|
})
|
|
}
|