Rule files, and bans for a clear sign of attack (closes #24)
check / check (push) Successful in 3m13s

Every *.rules file in SWWAF_RULES_DIR is read at start and on each
change, and each request is checked against the rules after the rate
limits: log notes a match, block refuses with 403, ban refuses and bans
the netblock for SWWAF_ATTACK_BAN_DURATION, made permanent by its next
request or clear sign of attack. path, query and uri are matched as the
request line sent them. bans.json gains each ban's cause, and ban notes
count earlier bans by cause. The image ships 00-default.rules.

Judgement call: a header sent twice is matched with its values joined
by ", ".
Judgement call: SWWAF_MAX_BAN_DURATION does not cap a ban for an attack.
Not in this unit: offences for rule matches, with the error burst.

Model: opus-5-5
This commit is contained in:
2026-10-06 14:33:26 +00:00
parent 6ec52e5b87
commit 7f472c40e2
27 changed files with 2164 additions and 263 deletions
+15 -12
View File
@@ -110,20 +110,23 @@ func (h *handler) newRequest(w http.ResponseWriter, r *http.Request) *request {
// check is the one place where a request can be refused once its client
// is known, before its body is read or anything reaches the app. It
// returns nil to let the request through. The checks of checkClient come
// first, answered with SWWAF_BAN_RESPONSE, and then the size limit, so
// that a request the rate limits count is counted even when it is
// refused for its size. In observe mode a request checkClient refuses
// goes on to the size limit like any other. ctx is the request's own
// context.
// first, answered with SWWAF_BAN_RESPONSE, or 403 for a block rule, and
// then the size limit, so that a request the rate limits count is counted
// even when it is refused for its size. In observe mode a request
// checkClient refuses goes on to the size limit like any other. ctx is
// the request's own context.
func (rq *request) check(ctx context.Context) *refusal {
action := rq.checkClient(ctx)
if action != "" {
if !rq.h.config.Observe {
return rq.banResponse(action)
}
switch {
case action == "":
case rq.h.config.Observe:
// The log line names what enforce mode would have done.
rq.line.WouldAction = action
case action == requestlog.ActionRuleBlocked:
return &refusal{status: http.StatusForbidden, action: action}
default:
return rq.banResponse(action)
}
maxBytes := rq.h.config.RequestMaxBytes
@@ -145,8 +148,8 @@ func (rq *request) check(ctx context.Context) *refusal {
// client either refuses is not looked up, and then the country lists; a
// request any of them refuses is not counted for the rate limits. Then
// come the rate limits, unless the client is in
// SWWAF_RATE_LIMIT_EXEMPT_NETS, so that every other request is counted.
// ctx is the request's own context.
// SWWAF_RATE_LIMIT_EXEMPT_NETS, so that every other request is counted,
// and last the rule files. ctx is the request's own context.
func (rq *request) checkClient(ctx context.Context) string {
cfg := rq.h.config
if isInside(rq.client, cfg.AllowNets) {
@@ -171,7 +174,7 @@ func (rq *request) checkClient(ctx context.Context) string {
return requestlog.ActionRateLimited
}
return ""
return rq.checkRules(now)
}
// forward passes the request to the app and the app's answer back. ctx