Bans an admin makes or lifts: the admin cause, a reason, lifted bans kept (closes #86)
check / check (push) Successful in 3m4s
check / check (push) Successful in 3m4s
A bans.json entry without a cause gets the cause admin, written back so. Bans whose cause is admin are never dropped and do not count toward SWWAF_MAX_BANS, so setting a ban's cause to admin keeps it. Bans smallwebwaf makes get a reason: the limit broken or the rule matched. A lifted ban refuses nothing, is kept, and makes no later ban longer. smallwebwaf_bans_made_total counts admin bans an edit adds while running; earlier_bans counts admin in place of without_cause. Judgement call: lifted lifts at once, whatever time it gives. Judgement call: a lifted ban still counts in earlier_bans. Known gap: a ban dropped from behind an admin's ban on its netblock leaves that netblock's later earlier_bans. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
package bans_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||
)
|
||||
|
||||
func TestBanWithoutACauseIsAnAdmins(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
netblock := netip.MustParsePrefix("203.0.113.0/24")
|
||||
ledger := bans.New(defaultRules())
|
||||
ledger.Load([]bans.Ban{{Netblock: netblock, Start: midnight()}})
|
||||
|
||||
if got := ledger.Bans(netblock)[0].Cause; got != bans.CauseAdmin {
|
||||
t.Errorf("the ban's cause is %q, want admin", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminsBansAreNeverDroppedAndDoNotCountTowardMaxBans(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rules := defaultRules()
|
||||
rules.MaxBans = 1
|
||||
ledger := bans.New(rules)
|
||||
adminsOnly := netip.MustParsePrefix("198.51.100.0/24")
|
||||
both := netip.MustParsePrefix("203.0.113.1/32")
|
||||
second := netip.MustParsePrefix("203.0.113.2/32")
|
||||
third := netip.MustParsePrefix("203.0.113.3/32")
|
||||
|
||||
// Seen longest ago, a netblock with two of an admin's bans alone, and
|
||||
// then one with an admin's ban before a ban smallwebwaf made: the one
|
||||
// ban counted toward MaxBans.
|
||||
ledger.Load([]bans.Ban{
|
||||
{Netblock: adminsOnly, Start: midnight().Add(-3 * time.Hour), Cause: bans.CauseAdmin},
|
||||
{Netblock: adminsOnly, Start: midnight().Add(-2 * time.Hour), Cause: bans.CauseAdmin},
|
||||
{Netblock: both, Start: midnight().Add(-time.Hour), Cause: bans.CauseAdmin},
|
||||
{
|
||||
Netblock: both,
|
||||
Start: midnight(),
|
||||
Expires: midnight().Add(time.Hour),
|
||||
Cause: bans.CauseLimit,
|
||||
},
|
||||
})
|
||||
wantBans(t, ledger, map[netip.Prefix]int{adminsOnly: 2, both: 2})
|
||||
|
||||
// A new ban drops the ban smallwebwaf made, and only that one.
|
||||
ledger.BanForLimit(second, midnight(), bans.Notes{})
|
||||
wantBans(t, ledger, map[netip.Prefix]int{adminsOnly: 2, both: 1, second: 1})
|
||||
|
||||
if ledger.Bans(both)[0].Cause != bans.CauseAdmin {
|
||||
t.Errorf("%s kept %+v, want the admin's ban", both, ledger.Bans(both))
|
||||
}
|
||||
|
||||
// And the next drops that one.
|
||||
ledger.BanForLimit(third, midnight(), bans.Notes{})
|
||||
wantBans(t, ledger, map[netip.Prefix]int{adminsOnly: 2, both: 1, second: 0, third: 1})
|
||||
}
|
||||
|
||||
func TestReasonOfTheBansSmallwebwafMakes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ledger := bans.New(defaultRules())
|
||||
|
||||
limit := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.1/32"), midnight(),
|
||||
bans.Notes{Limit: 1000, Window: "minute"})
|
||||
attack := ledger.BanForAttack(netip.MustParsePrefix("203.0.113.2/32"), midnight(),
|
||||
bans.Notes{RuleID: "git-dir", Target: "path"})
|
||||
|
||||
for _, tc := range []struct{ got, want string }{
|
||||
{limit.Reason, "requests per minute over the limit of 1000"},
|
||||
{attack.Reason, "matched the rule git-dir"},
|
||||
} {
|
||||
if tc.got != tc.want {
|
||||
t.Errorf("the reason is %q, want %q", tc.got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiftedBanForALimitRefusesNothingAndMakesNoBanLonger(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// An hour's ban lifted ten minutes after it started.
|
||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||
lifted := bans.Ban{
|
||||
Netblock: netblock,
|
||||
Start: midnight(),
|
||||
Expires: midnight().Add(time.Hour),
|
||||
Cause: bans.CauseLimit,
|
||||
Lifted: midnight().Add(10 * time.Minute),
|
||||
}
|
||||
|
||||
ledger := bans.New(defaultRules())
|
||||
ledger.Load([]bans.Ban{lifted})
|
||||
|
||||
// While it would still last, it refuses nothing, and a limit broken
|
||||
// bans for an hour, as a first broken limit does; the lifted ban is
|
||||
// kept, and counted among the earlier bans.
|
||||
now := midnight().Add(30 * time.Minute)
|
||||
|
||||
_, banned := ledger.Check(netblock.Addr(), now)
|
||||
if banned {
|
||||
t.Error("the lifted ban refuses")
|
||||
}
|
||||
|
||||
ban := ledger.BanForLimit(netblock, now, bans.Notes{})
|
||||
if ban.Expires.Sub(ban.Start) != time.Hour ||
|
||||
ban.Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
||||
t.Errorf("the next ban lasts %s with earlier bans %+v, want 1h and 1 for a limit",
|
||||
ban.Expires.Sub(ban.Start), ban.Notes.EarlierBans)
|
||||
}
|
||||
|
||||
held := ledger.Bans(netblock)
|
||||
if len(held) != 2 || held[0] != lifted {
|
||||
t.Errorf("the ledger holds %+v, want the lifted ban and the new one", held)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiftedBanForAnAttackRefusesNothingAndMakesNoBanLonger(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// A permanent ban for a clear sign of attack, lifted.
|
||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||
ledger := bans.New(defaultRules())
|
||||
ledger.Load([]bans.Ban{{
|
||||
Netblock: netblock,
|
||||
Start: midnight(),
|
||||
Cause: bans.CauseAttack,
|
||||
Lifted: midnight().Add(time.Hour),
|
||||
}})
|
||||
|
||||
now := midnight().Add(2 * time.Hour)
|
||||
|
||||
_, banned := ledger.Find(netblock.Addr(), now)
|
||||
if banned {
|
||||
t.Error("the lifted ban refuses")
|
||||
}
|
||||
|
||||
active, permanent := ledger.Count(now)
|
||||
if active != 0 || permanent != 0 {
|
||||
t.Errorf("%d bans are active and %d permanent, want none", active, permanent)
|
||||
}
|
||||
|
||||
// The next clear sign of attack bans for seven days, as a first does.
|
||||
ban := ledger.BanForAttack(netblock, now, bans.Notes{})
|
||||
if ban.Expires.Sub(ban.Start) != 7*day {
|
||||
t.Errorf("the next ban for an attack ends at %s, want seven days on", ban.Expires)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEditCountsTheBansAnAdminMade(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ledger := bans.New(defaultRules())
|
||||
made := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.1/32"), midnight(),
|
||||
bans.Notes{})
|
||||
atStart := bans.Ban{
|
||||
Netblock: netip.MustParsePrefix("203.0.113.2/32"),
|
||||
Start: midnight(),
|
||||
}
|
||||
|
||||
// The bans read at the start were made before it.
|
||||
ledger.Load([]bans.Ban{made, atStart})
|
||||
|
||||
if got := ledger.Made(bans.CauseAdmin); got != 0 {
|
||||
t.Fatalf("%d bans made by an admin after the start's, want none", got)
|
||||
}
|
||||
|
||||
// The admin keeps the ban smallwebwaf made, keeps the one read at the
|
||||
// start, and adds one without a cause: that one alone is made.
|
||||
kept := made
|
||||
kept.Cause = bans.CauseAdmin
|
||||
added := bans.Ban{
|
||||
Netblock: netip.MustParsePrefix("203.0.113.3/32"),
|
||||
Start: midnight(),
|
||||
}
|
||||
ledger.LoadEdit([]bans.Ban{kept, atStart, added})
|
||||
|
||||
if ledger.Made(bans.CauseAdmin) != 1 || ledger.Made(bans.CauseLimit) != 1 {
|
||||
t.Errorf("%d bans made by an admin and %d for a limit, want 1 of each",
|
||||
ledger.Made(bans.CauseAdmin), ledger.Made(bans.CauseLimit))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user