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; source_failure for GeoJS; file_error for a rule or state file edit that does not parse and a failed state write. SWWAF_ALERT_EVENTS chooses, SWWAF_ALERT_COOLDOWN holds back repeats, and past SWWAF_ALERT_MAX_PER_HOUR the hour ends in one summary. A bounded queue, retried with backoff, holds up no request; alerts.json keeps it, the cooldowns and the hour. The ledger now reports whether it made a ban, or made one permanent. Judgement call: the summary's event is summary, which SPEC.md omits. Judgement call: admin bans and observe mode raise no alert. Model: opus-5-5
This commit is contained in:
+47
-6
@@ -4,6 +4,7 @@ import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||
@@ -16,14 +17,25 @@ func (rq *request) banResponse(action string) *refusal {
|
||||
}
|
||||
|
||||
// banned reports whether a ban on a netblock the client is in covers the
|
||||
// request at now, and notes for the log line when that ban ends.
|
||||
// request at now, and notes for the log line when that ban ends. A
|
||||
// request that makes the ban permanent raises the alert for it.
|
||||
func (rq *request) banned(now time.Time) bool {
|
||||
check := rq.h.ledger.Check
|
||||
var (
|
||||
ban bans.Ban
|
||||
banned bool
|
||||
madePermanent bool
|
||||
)
|
||||
|
||||
if rq.h.config.Observe {
|
||||
check = rq.h.ledger.Find // in observe mode the ban refuses nothing
|
||||
ban, banned = rq.h.ledger.Find(rq.client, now) // the ban refuses nothing
|
||||
} else {
|
||||
ban, banned, madePermanent = rq.h.ledger.Check(rq.client, now)
|
||||
}
|
||||
|
||||
if madePermanent {
|
||||
rq.alertBan(ban)
|
||||
}
|
||||
|
||||
ban, banned := check(rq.client, now)
|
||||
if banned {
|
||||
rq.line.BanExpires = banExpires(ban)
|
||||
}
|
||||
@@ -54,7 +66,7 @@ func (rq *request) limitBroken(now time.Time) bool {
|
||||
}
|
||||
|
||||
netblock := rq.h.netblock(rq.client)
|
||||
ban := rq.h.ledger.BanForLimit(netblock, now, bans.Notes{
|
||||
ban, made := rq.h.ledger.BanForLimit(netblock, now, bans.Notes{
|
||||
Country: rq.line.Country,
|
||||
Limit: hit.Limit,
|
||||
Window: hit.Window,
|
||||
@@ -65,6 +77,10 @@ func (rq *request) limitBroken(now time.Time) bool {
|
||||
rq.h.limiter.Reset(group)
|
||||
rq.line.BanExpires = banExpires(ban)
|
||||
|
||||
if made {
|
||||
rq.alertBan(ban)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -72,7 +88,7 @@ func (rq *request) limitBroken(now time.Time) bool {
|
||||
// attack, the match of rule, a ban rule.
|
||||
func (rq *request) banForAttack(now time.Time, rule rules.Rule) {
|
||||
netblock := rq.h.netblock(rq.client)
|
||||
ban := rq.h.ledger.BanForAttack(netblock, now, bans.Notes{
|
||||
ban, made := rq.h.ledger.BanForAttack(netblock, now, bans.Notes{
|
||||
Country: rq.line.Country,
|
||||
RuleID: rule.ID,
|
||||
Target: rule.Target,
|
||||
@@ -80,6 +96,31 @@ func (rq *request) banForAttack(now time.Time, rule rules.Rule) {
|
||||
Requests: rq.netblockRequests(netblock),
|
||||
})
|
||||
rq.line.BanExpires = banExpires(ban)
|
||||
|
||||
if made {
|
||||
rq.alertBan(ban)
|
||||
}
|
||||
}
|
||||
|
||||
// alertBan raises the alert for ban, which the request made, or made
|
||||
// permanent: permanent_ban for a permanent ban, ban for another. Its
|
||||
// detail gives the ban's cause, when it ends, and its notes.
|
||||
func (rq *request) alertBan(ban bans.Ban) {
|
||||
event := alerts.EventBan
|
||||
if ban.Permanent() {
|
||||
event = alerts.EventPermanentBan
|
||||
}
|
||||
|
||||
rq.h.alerts.Raise(alerts.Alert{
|
||||
Event: event,
|
||||
Client: rq.client,
|
||||
Netblock: ban.Netblock,
|
||||
Country: ban.Notes.Country,
|
||||
Reason: ban.Reason,
|
||||
Detail: map[string]any{
|
||||
"cause": ban.Cause, "ban_expires": banExpires(ban), "notes": ban.Notes,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// noted is the request, refused at now with SWWAF_BAN_RESPONSE, as the
|
||||
|
||||
Reference in New Issue
Block a user