Alerts to a JSON webhook, with a cooldown and an hourly summary (closes #26)
check / check (push) Waiting to run
check / check (push) Waiting to run
SWWAF_ALERT_WEBHOOK_URL gets one JSON POST per alert, in SPEC.md's schema, with SWWAF_ALERT_WEBHOOK_HEADERS: ban and permanent_ban, with the ban's notes, in observe mode too, marked mode observe; source_failure for GeoJS; file_error for a rule or state file with an error. SWWAF_ALERT_EVENTS chooses; SWWAF_ALERT_COOLDOWN holds back repeats by netblock, file or source; past SWWAF_ALERT_MAX_PER_HOUR the hour ends in one summary. A bounded queue, retried with backoff, holds up no request; a 4xx other than 408 and 429 gives the alert up. alerts.json keeps the queue, the cooldowns and the hour. Nothing shows the URL's path or query. Judgement call: the summary's event is summary, which SPEC.md omits. Judgement call: an admin's ban raises no alert. Model: opus-5-5
This commit is contained in:
@@ -42,7 +42,7 @@ func TestSnapshotListsEveryBanByNetblock(t *testing.T) {
|
||||
high := netip.MustParsePrefix("203.0.113.10/32")
|
||||
low := netip.MustParsePrefix("203.0.113.9/32")
|
||||
|
||||
first := ledger.BanForLimit(v6, midnight(), bans.Notes{})
|
||||
first, _ := ledger.BanForLimit(v6, midnight(), bans.Notes{})
|
||||
ledger.BanForLimit(high, midnight(), bans.Notes{})
|
||||
ledger.BanForLimit(low, midnight(), bans.Notes{})
|
||||
ledger.BanForLimit(v6, first.Expires, bans.Notes{})
|
||||
@@ -68,7 +68,7 @@ func TestLoadedBansCarryOn(t *testing.T) {
|
||||
|
||||
before := bans.New(defaultRules())
|
||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||
ban := before.BanForLimit(netblock, midnight(), bans.Notes{Limit: 1})
|
||||
ban, _ := before.BanForLimit(netblock, midnight(), bans.Notes{Limit: 1})
|
||||
|
||||
// Loaded into a new ledger, as across a restart, the ban still refuses
|
||||
// while it lasts, and once it has ended a broken limit bans for three
|
||||
@@ -76,12 +76,12 @@ func TestLoadedBansCarryOn(t *testing.T) {
|
||||
after := bans.New(defaultRules())
|
||||
after.Load(before.Snapshot())
|
||||
|
||||
_, banned := after.Check(netblock.Addr(), ban.Expires.Add(-time.Second))
|
||||
_, banned, _ := after.Check(netblock.Addr(), ban.Expires.Add(-time.Second))
|
||||
if !banned {
|
||||
t.Error("the loaded ban does not refuse")
|
||||
}
|
||||
|
||||
again := after.BanForLimit(netblock, ban.Expires, bans.Notes{})
|
||||
again, _ := after.BanForLimit(netblock, ban.Expires, bans.Notes{})
|
||||
if again.Expires.Sub(again.Start) != 3*time.Hour ||
|
||||
again.Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
||||
t.Errorf("the next ban lasts %s with earlier bans %+v, want 3h and 1 for a limit",
|
||||
@@ -111,7 +111,7 @@ func TestLoadedBanRefusesEveryClientInItsNetblock(t *testing.T) {
|
||||
"198.51.100.7": true,
|
||||
"198.51.100.8": false,
|
||||
} {
|
||||
_, banned := ledger.Check(netip.MustParseAddr(client), midnight())
|
||||
_, banned, _ := ledger.Check(netip.MustParseAddr(client), midnight())
|
||||
if banned != want {
|
||||
t.Errorf("%s is refused: %t, want %t", client, banned, want)
|
||||
}
|
||||
@@ -150,19 +150,19 @@ func TestPermanentBanStartedBeforeAnEndedOneRefuses(t *testing.T) {
|
||||
now := midnight().Add(2 * time.Hour)
|
||||
client := netip.MustParseAddr("203.0.113.9")
|
||||
|
||||
ban, banned := ledger.Find(client, now)
|
||||
ban, banned, _ := ledger.Find(client, now)
|
||||
if !banned || !ban.Permanent() {
|
||||
t.Errorf("find gives %+v and %t, want the permanent ban", ban, banned)
|
||||
}
|
||||
|
||||
ban, banned = ledger.Check(client, now)
|
||||
ban, banned, _ = ledger.Check(client, now)
|
||||
if !banned || !ban.Permanent() {
|
||||
t.Errorf("the client is refused: %t, under %+v, want under the permanent ban",
|
||||
banned, ban)
|
||||
}
|
||||
|
||||
// A limit broken now makes no shorter ban over the permanent one.
|
||||
ban = ledger.BanForLimit(netblock, now, bans.Notes{})
|
||||
ban, _ = ledger.BanForLimit(netblock, now, bans.Notes{})
|
||||
if !ban.Permanent() || len(ledger.Bans(netblock)) != 2 {
|
||||
t.Errorf("a broken limit returned %+v and left the netblock %d bans, "+
|
||||
"want the permanent ban and 2", ban, len(ledger.Bans(netblock)))
|
||||
@@ -194,7 +194,7 @@ func TestNextBanWorkedOutFromTheBanThatEndedLast(t *testing.T) {
|
||||
// Once both have ended, a limit broken within the repeat window bans
|
||||
// for three times the 9 hours, and the notes count the two bans
|
||||
// before the 9-hour one and it, for a limit, and the admin's.
|
||||
ban := ledger.BanForLimit(netblock, nineHours.Expires.Add(time.Hour), bans.Notes{})
|
||||
ban, _ := ledger.BanForLimit(netblock, nineHours.Expires.Add(time.Hour), bans.Notes{})
|
||||
if ban.Expires.Sub(ban.Start) != 27*time.Hour ||
|
||||
ban.Notes.EarlierBans != (bans.EarlierBans{Limit: 3, Admin: 1}) {
|
||||
t.Errorf("the next ban lasts %s with earlier bans %+v, "+
|
||||
@@ -255,15 +255,15 @@ func TestLoadReplacesTheBansHeld(t *testing.T) {
|
||||
// bans.json is taken in, that ban is lifted.
|
||||
ledger.Load([]bans.Ban{kept})
|
||||
|
||||
_, banned := ledger.Check(netip.MustParseAddr("203.0.113.9"), midnight())
|
||||
_, banned, _ := ledger.Check(netip.MustParseAddr("203.0.113.9"), midnight())
|
||||
if banned {
|
||||
t.Error("a ban left out of the second load still refuses")
|
||||
}
|
||||
|
||||
// The ledger holds one ban, so it makes two more without dropping any.
|
||||
first := ledger.BanForLimit(netip.MustParsePrefix("198.51.100.7/32"), midnight(),
|
||||
first, _ := ledger.BanForLimit(netip.MustParsePrefix("198.51.100.7/32"), midnight(),
|
||||
bans.Notes{})
|
||||
second := ledger.BanForLimit(netip.MustParsePrefix("198.51.100.8/32"), midnight(),
|
||||
second, _ := ledger.BanForLimit(netip.MustParsePrefix("198.51.100.8/32"), midnight(),
|
||||
bans.Notes{})
|
||||
|
||||
want := []bans.Ban{first, second, kept}
|
||||
|
||||
Reference in New Issue
Block a user