Rule files, and bans for a clear sign of attack (closes #24)
check / check (push) Waiting to run
check / check (push) Waiting to run
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:
@@ -63,7 +63,11 @@ const permanentBansJSON = `{
|
||||
},
|
||||
"requests": 1500,
|
||||
"refused": 3,
|
||||
"earlier_bans": 5
|
||||
"earlier_bans": {
|
||||
"limit": 3,
|
||||
"attack": 1,
|
||||
"without_cause": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -263,6 +267,17 @@ func TestEntryWithoutAFieldItNeedsStopsTheStart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanWithACauseSmallwebwafDoesNotGiveStopsTheStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wantRefused(t, bansJSON, `{"version": 1, "bans": [`+
|
||||
`{"netblock": "203.0.113.9/32", "start": "2026-10-06T00:00:00Z", `+
|
||||
`"expires": null, "cause": "attack"}, `+
|
||||
`{"netblock": "203.0.113.10/32", "start": "2026-10-06T00:00:00Z", `+
|
||||
`"expires": null, "cause": "atack"}]}`,
|
||||
`: entry 2's cause "atack" is not limit or attack`)
|
||||
}
|
||||
|
||||
func TestUnknownVersionStopsTheStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -881,6 +896,7 @@ func newParams(dir string) state.Params {
|
||||
LimitBanDuration: time.Hour,
|
||||
LimitBanRepeatWindow: 24 * time.Hour,
|
||||
MaxBanDuration: 7 * 24 * time.Hour,
|
||||
AttackBanDuration: 7 * 24 * time.Hour,
|
||||
MaxBans: 5000,
|
||||
}),
|
||||
Limiter: ratelimit.New(ratelimit.Limits{}),
|
||||
@@ -893,14 +909,17 @@ func newParams(dir string) state.Params {
|
||||
}
|
||||
}
|
||||
|
||||
// fill puts a ban that ends and one that does not, clients with counts
|
||||
// and histories, and GeoJS answers into the parts of params.
|
||||
// fill puts a permanent ban without a cause, as an admin adds one, a ban
|
||||
// for a broken limit and one for a clear sign of attack, clients with
|
||||
// counts and histories, and GeoJS answers into the parts of params.
|
||||
func fill(params state.Params) {
|
||||
now := midnight()
|
||||
client := netip.MustParsePrefix("203.0.113.9/32")
|
||||
|
||||
params.Ledger.Load([]bans.Ban{permanentBan()})
|
||||
params.Ledger.BanForLimit(client, now, bans.Notes{Country: "DE", Limit: 1})
|
||||
params.Ledger.BanForAttack(netip.MustParsePrefix("192.0.2.1/32"), now,
|
||||
bans.Notes{RuleID: "env-file", Target: "path"})
|
||||
|
||||
for _, c := range []string{"2001:db8::/64", "203.0.113.9/32", "192.0.2.1/32"} {
|
||||
params.Limiter.Count(netip.MustParsePrefix(c), now)
|
||||
@@ -939,7 +958,7 @@ func permanentBan() bans.Ban {
|
||||
},
|
||||
Requests: 1500,
|
||||
Refused: 3,
|
||||
EarlierBans: 5,
|
||||
EarlierBans: bans.EarlierBans{Limit: 3, Attack: 1, WithoutCause: 1},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user