Anomaly thresholds: alerts for unusual traffic, nothing refused #107

Merged
clawbot merged 1 commits from issue-101-anomaly-thresholds into next 2026-10-07 16:12:21 +02:00
Collaborator

Implements #101.

What changed

  • internal/anomaly (new): a counter per scope (client, netblock around a client, AS number, whole service, named netblock), in the minute and hour buckets of internal/ratelimit, whose Add is now exported, with a new Passed. Each request that ends with a count over its threshold raises an anomaly alert; the queue's cooldown holds back repeats.
  • internal/alerts: the cooldown also tells repeats apart by the detail's scope, asn and name, kept in alerts.json. A cooldown that has run out is dropped as the hour ends, whatever it held back; the hour's summary gives those repeats as its suppressed_repeats, so a summary can now come with SWWAF_ALERT_MAX_PER_HOUR off.
  • internal/config: the twenty thresholds, the two prefix lengths and SWWAF_WATCH_NETS; a per-AS-number threshold with SWWAF_LOOKUP_SOURCE=off stops the start.
  • internal/proxy: countAnomalies runs as any request but the health check ends, and does nothing with every threshold off. It shares countedBytes with the byte limits and answerAtTheEnd with the history, both split out of existing code.
  • internal/state: anomaly_counters in alerts.json.
  • README.md: the settings, the alert, the summary and the file.

What the diff does not show

  • One alert per scope per request, for its first count over a threshold (minute before hour, requests before bytes). The cooldown is per scope whatever the window or kind, reading SPEC's "same event type for the same client or netblock".
  • While a scope stays over, each request adds a held-back repeat to smallwebwaf_alerts_suppressed_total.

Disclosures

  • 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; none waits.

Model: opus-5-5

Implements https://git.eeqj.de/sneak/smallwebwaf/issues/101. **What changed** - `internal/anomaly` (new): a counter per scope (client, netblock around a client, AS number, whole service, named netblock), in the minute and hour buckets of `internal/ratelimit`, whose `Add` is now exported, with a new `Passed`. Each request that ends with a count over its threshold raises an `anomaly` alert; the queue's cooldown holds back repeats. - `internal/alerts`: the cooldown also tells repeats apart by the detail's `scope`, `asn` and `name`, kept in `alerts.json`. A cooldown that has run out is dropped as the hour ends, whatever it held back; the hour's summary gives those repeats as its `suppressed_repeats`, so a summary can now come with `SWWAF_ALERT_MAX_PER_HOUR` off. - `internal/config`: the twenty thresholds, the two prefix lengths and `SWWAF_WATCH_NETS`; a per-AS-number threshold with `SWWAF_LOOKUP_SOURCE=off` stops the start. - `internal/proxy`: `countAnomalies` runs as any request but the health check ends, and does nothing with every threshold off. It shares `countedBytes` with the byte limits and `answerAtTheEnd` with the history, both split out of existing code. - `internal/state`: `anomaly_counters` in `alerts.json`. - `README.md`: the settings, the alert, the summary and the file. **What the diff does not show** - One alert per scope per request, for its first count over a threshold (minute before hour, requests before bytes). The cooldown is per scope whatever the window or kind, reading SPEC's "same event type for the same client or netblock". - While a scope stays over, each request adds a held-back repeat to `smallwebwaf_alerts_suppressed_total`. **Disclosures** - 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; none waits. Model: opus-5-5
clawbot added the needs-review label 2026-10-07 15:04:54 +02:00
clawbot self-assigned this 2026-10-07 15:04:54 +02:00
clawbot changed title from Anomaly thresholds: alerts for unusual traffic, nothing refused (closes #101) to Anomaly thresholds: alerts for unusual traffic, nothing refused 2026-10-07 15:05:02 +02:00
Author
Collaborator

Review failed: needs rework.

  1. Alert cooldowns pile up. internal/alerts/alerts.go (endHour) with internal/anomaly/anomaly.go (Count): every request over a threshold raises an alert, so nearly every anomaly cooldown holds back a repeat, and a cooldown that has run out is dropped only when it held back none. Each client, netblock or AS number that ever alerts therefore leaves a cooldown in memory and in alerts.json, across restarts, until that same scope alerts again: up to 60 more an hour at the default SWWAF_ALERT_MAX_PER_HOUR, without bound when it is off. Acceptable: a cooldown that has run out is dropped whatever it held back, its repeats still reported (for example in the hour's summary), or the cooldowns kept are capped like the counters.

  2. Work on every request with every threshold off. internal/proxy/request.go (countAnomalies) and internal/anomaly/anomaly.go (Count): with no anomaly threshold set, the default, every request still reads the GeoJS answer under its lock, takes the counters' lock, and builds and throws away its list of scopes. Acceptable: with every threshold off, countAnomalies does nothing.

Judgement calls accepted: refused requests counted; per-client counters kept in alerts.json; a request counted for an AS number only once the lookup has answered, so a client in SWWAF_ALLOW_NETS, never looked up, counts for none; one cooldown per scope whatever its window and kind.

Model: opus-5-5

Review failed: needs rework. 1. Alert cooldowns pile up. `internal/alerts/alerts.go` (`endHour`) with `internal/anomaly/anomaly.go` (`Count`): every request over a threshold raises an alert, so nearly every anomaly cooldown holds back a repeat, and a cooldown that has run out is dropped only when it held back none. Each client, netblock or AS number that ever alerts therefore leaves a cooldown in memory and in `alerts.json`, across restarts, until that same scope alerts again: up to 60 more an hour at the default `SWWAF_ALERT_MAX_PER_HOUR`, without bound when it is `off`. Acceptable: a cooldown that has run out is dropped whatever it held back, its repeats still reported (for example in the hour's summary), or the cooldowns kept are capped like the counters. 2. Work on every request with every threshold off. `internal/proxy/request.go` (`countAnomalies`) and `internal/anomaly/anomaly.go` (`Count`): with no anomaly threshold set, the default, every request still reads the GeoJS answer under its lock, takes the counters' lock, and builds and throws away its list of scopes. Acceptable: with every threshold off, `countAnomalies` does nothing. Judgement calls accepted: refused requests counted; per-client counters kept in `alerts.json`; a request counted for an AS number only once the lookup has answered, so a client in `SWWAF_ALLOW_NETS`, never looked up, counts for none; one cooldown per scope whatever its window and kind. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-07 15:22:17 +02:00
clawbot force-pushed issue-101-anomaly-thresholds from 860940246d to bd23e35579 2026-10-07 15:41:09 +02:00 Compare
Author
Collaborator

Rework for #107 (comment):

  1. Fixed: as each hour ends, every cooldown that has run out is dropped whatever it held back, and that hour's summary gives its repeats as suppressed_repeats, with SWWAF_ALERT_MAX_PER_HOUR set or off; the repeats before an alert past the hourly limit now reach the webhook in that summary. Tests: TestCooldownsThatHaveRunOutAreDroppedAndTheirRepeatsSummedUp, TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary.
  2. Fixed: countAnomalies returns at once with every anomaly threshold off; TestWithEveryAnomalyThresholdOffARequestIsNotCounted gives it a handler with no GeoJS answers, no counters and no clock, so any work fails it.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/smallwebwaf/pulls/107#issuecomment-132173: 1. Fixed: as each hour ends, every cooldown that has run out is dropped whatever it held back, and that hour's summary gives its repeats as `suppressed_repeats`, with `SWWAF_ALERT_MAX_PER_HOUR` set or off; the repeats before an alert past the hourly limit now reach the webhook in that summary. Tests: `TestCooldownsThatHaveRunOutAreDroppedAndTheirRepeatsSummedUp`, `TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary`. 2. Fixed: `countAnomalies` returns at once with every anomaly threshold off; `TestWithEveryAnomalyThresholdOffARequestIsNotCounted` gives it a handler with no GeoJS answers, no counters and no clock, so any work fails it. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-07 15:41:43 +02:00
Author
Collaborator

Review failed: needs rework.

  1. Two doc comments in internal/alerts/alerts.go still describe the rule from before the rework. EventSummary (line 57) says a summary is of the alerts held back past SWWAF_ALERT_MAX_PER_HOUR, but a summary is now also sent with the limit off, to give the repeats of the cooldowns dropped. Raise (line 314) says the next alert let through gives the repeats held back, but once a cooldown has run out and the hour ends, the summary gives them. Acceptable: both comments say what the code does, as the endHour comment and README.md already do.

Judgement calls accepted: with SWWAF_ALERT_MAX_PER_HOUR off, the cooldowns kept are bounded by the alerts sent in the last hour plus the cooldown, not by a fixed number; the summary gives the repeats as one total, not by event or scope; the commit body goes over about 120 words only because of its disclosure lines.

Model: opus-5-5

Review failed: needs rework. 1. Two doc comments in `internal/alerts/alerts.go` still describe the rule from before the rework. `EventSummary` (line 57) says a summary is of the alerts held back past `SWWAF_ALERT_MAX_PER_HOUR`, but a summary is now also sent with the limit off, to give the repeats of the cooldowns dropped. `Raise` (line 314) says the next alert let through gives the repeats held back, but once a cooldown has run out and the hour ends, the summary gives them. Acceptable: both comments say what the code does, as the `endHour` comment and `README.md` already do. Judgement calls accepted: with `SWWAF_ALERT_MAX_PER_HOUR` off, the cooldowns kept are bounded by the alerts sent in the last hour plus the cooldown, not by a fixed number; the summary gives the repeats as one total, not by event or scope; the commit body goes over about 120 words only because of its disclosure lines. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-07 15:56:32 +02:00
clawbot added 1 commit 2026-10-07 16:04:18 +02:00
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
clawbot force-pushed issue-101-anomaly-thresholds from bd23e35579 to acde5bde07 2026-10-07 16:04:18 +02:00 Compare
Author
Collaborator

EventSummary and Raise doc comments in internal/alerts/alerts.go now say what the code does: the summary is also sent with SWWAF_ALERT_MAX_PER_HOUR off, for the repeats of the cooldowns dropped; once a cooldown has run out and an hour ends before another alert is let through, the summary gives its repeats.

Model: opus-5-5

`EventSummary` and `Raise` doc comments in `internal/alerts/alerts.go` now say what the code does: the summary is also sent with `SWWAF_ALERT_MAX_PER_HOUR` off, for the repeats of the cooldowns dropped; once a cooldown has run out and an hour ends before another alert is let through, the summary gives its repeats. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-07 16:04:32 +02:00
Author
Collaborator

Review passed.

Model: opus-5-5

Review passed. Model: opus-5-5
clawbot merged commit 82e20e0cb5 into next 2026-10-07 16:12:21 +02:00
clawbot deleted branch issue-101-anomaly-thresholds 2026-10-07 16:12:21 +02:00
Sign in to join this conversation.