Files
smallwebwaf/docs/alerts.md
T
sneak d712bb1496
check / check (push) Waiting to run
SPEC: requirements and reasons only, reference material in docs/
docs/settings.md is now the one settings reference; the spec keeps the
conventions, the off list and each group's requirements and reasons, and
links to it for the per-setting entries. The endpoint list, the ban
notes, the state file contents, the rule file format and the alert
object likewise live in docs/ and are linked from the spec. The package
list from the README's Design section joins "Architecture". The nine
docs files are byte-identical to those on the README branch. Package
comments that named "Alert webhook schema", "Request log" and "Metrics
endpoint" for their field lists now name the docs files.

Model: fable-5-1
2026-10-22 21:08:26 +00:00

5.9 KiB

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. 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:

{
    "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 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 <token>, 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).