Ban notes name the reputation sources that listed the client (closes #109)
check / check (push) Waiting to run

A ban's notes, in bans.json and in its alert, gain `reputation`: each
blocklist, DNSBL zone or AbuseIPDB that listed the client when the ban
was made, as its `source`, named and ordered as in the request log's
`reputation`, with AbuseIPDB's `score`. It is left out when none did.
README.md shows it in a bans.json example.

Notes now hold a list, so bans can no longer be compared with ==: the
tests compare them with reflect.DeepEqual.

Judgement call: the score is a pointer, so a score of 0, a hit while
SWWAF_ABUSEIPDB_MIN_SCORE is 0, is still written.

Model: opus-5-5
This commit is contained in:
2026-10-08 00:40:10 +00:00
parent a6634454cd
commit e3e0758465
16 changed files with 243 additions and 68 deletions
+75 -16
View File
@@ -163,15 +163,16 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
and the requests or bytes counted in it, the client's percentage of that kind
of limit and the setting that gave it when a biased threshold lowered the
limit, the request that broke it, the client's AS number, AS name and country
once they are looked up, the netblock's requests since it was first seen, how
many of them the ban has refused, and how many bans the netblock had before,
for a broken limit, for a clear sign of attack and by an admin. At most
`SWWAF_MAX_BANS` bans `smallwebwaf` made are kept, past, active and permanent;
past that, the earliest such ban of the netblock that has gone longest without
a request is dropped first. The bans whose cause is `admin`, those you make or
keep, are kept besides, and never dropped. `bans.json` shows the bans and
their notes, a restart lifts none, and you make, keep or lift a ban by editing
it (see "State files" below).
once they are looked up, the blocklists, DNSBL zones and AbuseIPDB, with its
score, that listed the client when the ban was made, the netblock's requests
since it was first seen, how many of them the ban has refused, and how many
bans the netblock had before, for a broken limit, for a clear sign of attack
and by an admin. At most `SWWAF_MAX_BANS` bans `smallwebwaf` made are kept,
past, active and permanent; past that, the earliest such ban of the netblock
that has gone longest without a request is dropped first. The bans whose cause
is `admin`, those you make or keep, are kept besides, and never dropped.
`bans.json` shows the bans and their notes, a restart lifts none, and you
make, keep or lift a ban by editing it (see "State files" below).
- Checks each request against the rules of the rule files (see "Rule files"
below) after the rate limits, and before its body is read. A `log` rule that
matches is noted in the log line; a `block` rule refuses the request with
@@ -1074,7 +1075,11 @@ with times in UTC.
ban for a broken limit is `requests` or `bytes`, what the limit is on. For a
limit a biased threshold lowered, the reason and the notes' `limit` give the
lowered limit, and the notes' `limit_percent` and `limit_percent_setting` the
client's percentage of that kind of limit and the setting that gave it.
client's percentage of that kind of limit and the setting that gave it. The
notes' `reputation` gives each blocklist, DNSBL zone or AbuseIPDB that listed
the client when the ban was made, as its `source`, named and ordered as in the
request log's `reputation`, with AbuseIPDB's `score` of the client. It is left
out when none did, and the example below shows it.
- `clients.json`: each client's two buckets of requests in the minute, the hour
and the day, its two buckets of bytes in each, `minute_bytes`, `hour_bytes`
and `day_bytes`, and its history: when it was first and last seen, its AS
@@ -1121,6 +1126,60 @@ with times in UTC.
Slack and ntfy too, stops the start: put the list under `"webhook"`, or remove
the file.
This `bans.json` holds a ban for a broken rate limit on a client that a DNSBL
zone lists and AbuseIPDB scores at 100, whose limits `SWWAF_REPUTATION_ACTION`,
at its default of `limit:25`, lowered to a quarter:
```json
{
"version": 1,
"bans": [
{
"netblock": "203.0.113.9/32",
"start": "2026-10-06T12:00:41.5Z",
"expires": "2026-10-06T13:00:41.5Z",
"cause": "limit",
"reason": "requests per minute over the limit of 250",
"notes": {
"asn": "AS64496",
"as_name": "Example Net",
"country": "DE",
"kind": "requests",
"limit": 250,
"window": "minute",
"count": 251,
"limit_percent": 25,
"limit_percent_setting": "SWWAF_REPUTATION_ACTION",
"reputation": [
{
"source": "dnsbl.dronebl.org"
},
{
"source": "abuseipdb",
"score": 100
}
],
"request": {
"time": "2026-10-06T12:00:41.5Z",
"method": "GET",
"host": "app.example",
"path": "/owner/repo/commits/branch/main?page=812",
"status": 403,
"user_agent": "scraper/1.0"
},
"requests": 512,
"refused": 0,
"earlier_bans": {
"limit": 0,
"attack": 0,
"admin": 0
}
}
}
]
}
```
`bans.json` is written `SWWAF_STATE_WRITE_DELAY` after a ban is made, lifted
through `DELETE /_smallwebwaf/bans/<client>`, or made permanent, with every such
change in between, and every file every `SWWAF_STATE_COUNTER_INTERVAL` and when
@@ -1759,8 +1818,8 @@ fetched before then stays in use, and the failure is counted, logged and raised
as a `source_failure` alert; a fetch cut off as `smallwebwaf` stops is not a
failure. The last good copy of each list is kept whole, comment lines included,
in `reputation.json` (see "State files" above), so that a restart keeps it in
use too. Each list is named by its URL, in the request log, the alerts and the
metrics, so keep a secret out of it.
use too. Each list is named by its URL, in the request log, the alerts, the
notes of bans and the metrics, so keep a secret out of it.
A client in `SWWAF_ALLOW_NETS` is not checked. Any other is checked by its own
address after the country lists, and `SWWAF_BLOCKLIST_ACTION` says what is done
@@ -1829,10 +1888,10 @@ it, such as `<key>.xbl.dq.spamhaus.net`. The key of a zone under
`dq.spamhaus.net` is its first label, and `smallwebwaf` shows `********` in its
place wherever it names the zone, as `********.xbl.dq.spamhaus.net`: in the
settings logged at start, an error that stops the start, its own messages, the
request log, the alerts and the metrics. Only `reputation.json` keeps the zone
with its key. A key in the name of any other zone is shown as given. Several
zones refuse queries that come through a public resolver; `SWWAF_DNSBL_RESOLVER`
names another resolver to ask through.
request log, the alerts, the notes of bans and the metrics. Only
`reputation.json` keeps the zone with its key. A key in the name of any other
zone is shown as given. Several zones refuse queries that come through a public
resolver; `SWWAF_DNSBL_RESOLVER` names another resolver to ask through.
## AbuseIPDB