check / check (push) Waiting to run
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
150 lines
5.9 KiB
Markdown
150 lines
5.9 KiB
Markdown
# 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 <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`](state-files.md)).
|