package servewaf // The heuristic core_rule_set. It reads SWWAF_WAF_MODE, SWWAF_WAF_EXEMPT_PATHS // and SWWAF_WAF_ANOMALY_THRESHOLD. import ( "context" "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" ) // coreRuleSet 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 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.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 nil } rq.alertWAFBlock(result) if cfg.WAFMode == config.WAFModeDetect { return nil } rq.wafBlocked = true return &verdict{ action: requestlog.ActionWAFBlocked, figures: map[string]any{figureScore: result.Score, "threshold": threshold}, } } // 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, }) }