# Alerts Each alert goes to each destination you name. `SWWAF_ALERT_WEBHOOK_URL` gets it as one JSON object. `SWWAF_ALERT_SLACK_WEBHOOK_URL` and `SWWAF_ALERT_NTFY_URL` get a message made from it. The settings are under "Alert destinations" in [`settings.md`](settings.md). An alert is one of these events, and is sent when its event is in `SWWAF_ALERT_EVENTS`: - `ban`: a ban `smallwebwaf` made. - `permanent_ban`: a permanent ban it made, or an existing ban made permanent by a request. - `waf_block`: the Core Rule Set scored a request at or over the threshold. In `block` mode the request was refused. In `detect` mode it was not, and the alert's `detail` gives `mode`. - `anomaly`: a count over an anomaly threshold. - `reputation_hit`: a request from a client on a blocklist, the CrowdSec decision list or a DNS blocklist, or with an AbuseIPDB score that is a hit. One alert per source. - `source_failure`: one of: - GeoJS failing; - a list that cannot be fetched; - a DNS blocklist or AbuseIPDB failing or refusing a query; - the day's AbuseIPDB checks used up. - `file_error`: one of: - a rule file or state file edit with an error; - a state file that cannot be written; - a replacement lookup database that cannot be read. - `summary`: the hourly summary described under "Repeats and the hourly cap" below. The bans you make raise no alert. In `observe` mode, a request that would have made a ban still raises the ban's alert, with `mode` set to `observe` in its `detail`. ## The JSON object This is the alert for a ban for a broken rate limit, shown indented; it is sent on one line: ```json { "instance": "fsn1app1/gitea", "time": "2026-10-06T12:00:00.123461Z", "event": "ban", "client": "203.0.113.9", "netblock": "203.0.113.9/32", "asn": "", "as_name": "", "country": "", "reason": "requests per minute over the limit of 1000", "detail": { "ban_expires": "2026-10-06T13:00:00.123Z", "cause": "limit", "notes": { "asn": "", "as_name": "", "country": "", "kind": "requests", "limit": 1000, "window": "minute", "count": 1001, "request": { "time": "2026-10-06T12:00:00.123456789Z", "method": "GET", "host": "app.example", "path": "/owner/repo/commits/branch/main?page=812", "status": 403, "user_agent": "scraper/1.0" }, "requests": 5210, "refused": 0, "earlier_bans": { "limit": 0, "attack": 0, "admin": 0, "crowdsec": 0 } } }, "suppressed_repeats": 0 } ``` - `instance` is `SWWAF_INSTANCE_NAME`. `time` is when the alert was raised. `event` is one of the events above. - `client` is the address whose request raised the alert. `netblock` is the ban's netblock, the netblock an anomaly counted, or the client's group. Both are empty for `source_failure` and `file_error`. `asn`, `as_name` and `country` are the client's as known when the alert was raised. They are empty when GeoJS had not answered yet. - `reason` is a short sentence, such as `requests per minute over the limit of 1000` or `listed by a blocklist`. - `detail` is what is particular to the event: - for a ban, its `cause`, `ban_expires` and `notes` (see [`bans.md`](bans.md) for the notes); - for an anomaly, the `scope`, `window`, `kind`, `count` and `threshold`; - for a reputation hit, the `source` and, for AbuseIPDB, the `score`; - for a Core Rule Set match, the `rule_ids`, `score`, `method` and `path`; - for a source failure, the `source` and `error`; - for a file error, the `file` and `error`. - `suppressed_repeats` is how many repeats the cooldown held back before this alert. ## Slack and ntfy Slack and ntfy get a message with a title and a text. The title is the instance and the event, such as `fsn1app1/gitea: ban`. The text is the `reason`, then one line for each of these that the alert has: - the client, the netblock and the country; - the file, the source, the error and the mode from `detail`; - the suppressed repeats, if any. Slack gets the title in bold and the text below it, with `&`, `<` and `>` escaped. ntfy gets the text as the message, with these headers: - `Title`: the title; - `Authorization`: the token, as `Bearer `, while `SWWAF_ALERT_NTFY_TOKEN` is set; - `Priority` and `Tags`: by event, as listed below. The priority and tag by event: - `ban`: priority `default`, tag `no_entry`. - `permanent_ban`: priority `high`, tag `no_entry`. - `waf_block`: priority `default`, tag `shield`. - `anomaly`: priority `high`, tag `chart_with_upwards_trend`. - `reputation_hit`: priority `low`, tag `label`. - `source_failure` and `file_error`: priority `high`, tag `warning`. - `summary`: priority `default`, tag `bar_chart`. ## Repeats and the hourly cap A repeat is an alert for the same event on the same netblock, source, file or thing counted, less than `SWWAF_ALERT_COOLDOWN` after the last. It is held back and counted, and the next alert sent gives the count. Past `SWWAF_ALERT_MAX_PER_HOUR` alerts in an hour, the rest are held back and counted by event. Once the hour ends, one alert with the event `summary` gives those counts. It also counts the repeats that a cooldown held back and that no later alert reported. ## Sending Each destination has a queue of at most 1000 alerts, sent one at a time, so a slow destination holds up nothing. A `2xx` answer takes an alert. A `4xx` other than `408` and `429` refuses it, and it is dropped. Anything else is a failure: the alert is sent again a second later, and twice as long after each further failure, up to a minute. `alerts.json` keeps the queues and cooldowns across a restart (see [`state-files.md`](state-files.md)).