Compare commits

..
1 Commits
Author SHA1 Message Date
clawbot bd23e35579 Anomaly thresholds: alerts for unusual traffic, nothing refused (closes #101)
check / check (push) Waiting to run
SWWAF_ANOMALY_CLIENT_*, _NET_*, _ASN_*, _TOTAL_* and SWWAF_WATCH_* with
SWWAF_WATCH_NETS: requests and bytes per minute and per hour, each off by
default; with all off, nothing is counted. Otherwise every request but the
health check is counted, allow-listed and exempt ones included; a count
over its threshold raises an anomaly alert, with a cooldown per scope. At
most 20,000 counters, kept in alerts.json. A per-AS-number threshold with
lookups off, or a malformed SWWAF_WATCH_NETS, stops the start. A cooldown
that has run out is dropped as the hour ends, whatever it held back; the
hour's summary gives its repeats.

Judgement call: refused requests are counted too.
Judgement call: per-client counters are kept in alerts.json, which SPEC.md does not list.
Judgement call: a request counts for an AS number only if the lookup answered before it ended.

Model: opus-5-5
2026-10-07 13:41:06 +00:00
+11 -13
View File
@@ -54,11 +54,8 @@ const (
// runs that does not parse, a replacement of the lookup database that // runs that does not parse, a replacement of the lookup database that
// cannot be read, or a state file that cannot be written. // cannot be read, or a state file that cannot be written.
EventFileError = "file_error" EventFileError = "file_error"
// EventSummary is the summary sent as an hour ends: of the alerts held // EventSummary is the summary of the alerts an hour held back past
// back in it past SWWAF_ALERT_MAX_PER_HOUR, and of the repeats held // SWWAF_ALERT_MAX_PER_HOUR. SWWAF_ALERT_EVENTS does not name it.
// back by the cooldowns dropped as it ends, which no alert let through
// has given. It is sent with SWWAF_ALERT_MAX_PER_HOUR off too, for
// those repeats. SWWAF_ALERT_EVENTS does not name it.
EventSummary = "summary" EventSummary = "summary"
) )
@@ -314,14 +311,15 @@ func New(params Params) *Queue {
// unless no destination is set or SWWAF_ALERT_EVENTS leaves its event // unless no destination is set or SWWAF_ALERT_EVENTS leaves its event
// out. It gives alert the instance and the time. An alert that repeats // out. It gives alert the instance and the time. An alert that repeats
// the last one let through less than Cooldown before is held back and // the last one let through less than Cooldown before is held back and
// counted. The next one let through gives that count, unless an hour of // counted, and the next one let through gives that count. Past MaxPerHour
// the clock ends first after the cooldown has run out: the cooldown is // alerts let through in the hour under way, by the clock, an alert is
// then dropped, and that hour's summary gives the count. Past MaxPerHour // held back for that hour's summary instead, which is sent once the hour
// alerts let through in the hour under way, an alert is held back for // has ended; it starts no cooldown, and the repeats held back before it
// that hour's summary instead, which is sent once the hour has ended; it // are given by that summary, as their cooldown, which has run out, is
// starts no cooldown. Raise never waits: an alert let through joins the // dropped when the hour ends. Raise never waits: an alert
// queue of each destination, from which Run sends it, and with queueSize // let through joins the queue of each destination, from which Run sends
// alerts waiting for a destination, the oldest is dropped. // it, and with queueSize alerts waiting for a destination, the oldest is
// dropped.
func (q *Queue) Raise(alert Alert) { func (q *Queue) Raise(alert Alert) {
if len(q.destinations) == 0 || !slices.Contains(q.params.Events, alert.Event) { if len(q.destinations) == 0 || !slices.Contains(q.params.Events, alert.Event) {
return return