Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8f417b00b |
No files matched your search
+2
-16
@@ -124,20 +124,6 @@ RUN git checkout --quiet --detach b4b2c785308b1ce785b6155c7fe5f16879080193 \
|
|||||||
# ubuntu 26.04, 2026-09-27
|
# ubuntu 26.04, 2026-09-27
|
||||||
FROM ubuntu@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7
|
FROM ubuntu@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7
|
||||||
|
|
||||||
# The image's own labels, in place of the ones it would inherit from
|
|
||||||
# Ubuntu's. script/docker and script/cibuild pass the build arguments;
|
|
||||||
# a build that passes none labels the image unknown rather than leave
|
|
||||||
# Ubuntu's values.
|
|
||||||
ARG VERSION=unknown
|
|
||||||
ARG CREATED=unknown
|
|
||||||
ARG REVISION=unknown
|
|
||||||
LABEL org.opencontainers.image.title="smallwebwaf" \
|
|
||||||
org.opencontainers.image.description="A web application firewall for self-hosted web apps: sww rproxy in front of an app, and sww discriminator, which decides." \
|
|
||||||
org.opencontainers.image.version="${VERSION}" \
|
|
||||||
org.opencontainers.image.created="${CREATED}" \
|
|
||||||
org.opencontainers.image.revision="${REVISION}" \
|
|
||||||
org.opencontainers.image.source="https://git.eeqj.de/sneak/smallwebwaf"
|
|
||||||
|
|
||||||
# runit's install creates its _runit-log user with minsysusers, which
|
# runit's install creates its _runit-log user with minsysusers, which
|
||||||
# reads this file in place of runit's /usr/lib/sysusers.d/runit.conf.
|
# reads this file in place of runit's /usr/lib/sysusers.d/runit.conf.
|
||||||
# runit's line leaves out the shell, and minsysusers prints a Perl
|
# runit's line leaves out the shell, and minsysusers prints a Perl
|
||||||
@@ -189,12 +175,12 @@ RUN groupadd --system --gid 65532 sww \
|
|||||||
--gid sww --no-create-home --shell /usr/sbin/nologin \
|
--gid sww --no-create-home --shell /usr/sbin/nologin \
|
||||||
sww
|
sww
|
||||||
|
|
||||||
# The state files' directory, SWW_STATE_DIR by default, where a volume
|
# The state files' directory, SWWAF_STATE_DIR by default, where a volume
|
||||||
# is mounted to keep them across deploys. The run script gives it to the
|
# is mounted to keep them across deploys. The run script gives it to the
|
||||||
# sww user at each start of discriminator.
|
# sww user at each start of discriminator.
|
||||||
RUN mkdir /var/lib/sww
|
RUN mkdir /var/lib/sww
|
||||||
|
|
||||||
# The default rule file, in SWW_RULES_DIR by default, where a Dockerfile
|
# The default rule file, in SWWAF_RULES_DIR by default, where a Dockerfile
|
||||||
# built FROM this image can copy rule files of its own beside it.
|
# built FROM this image can copy rule files of its own beside it.
|
||||||
COPY share/rules.d/00-default.rules /etc/sww/rules.d/00-default.rules
|
COPY share/rules.d/00-default.rules /etc/sww/rules.d/00-default.rules
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ requests of scrapers, scanners and other abusers before they reach an app.
|
|||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
A web application firewall reads each request on its way to a web app and
|
A web application firewall reads each request on its way to a web app and
|
||||||
refuses the ones that look like abuse. `smallwebwaf` is one program, `sww`,
|
refuses the ones that look like abuse. `smallwebwaf` is one binary, shipped in
|
||||||
shipped in one image, with two commands that do this together:
|
one image, with two commands that do this together:
|
||||||
|
|
||||||
- `sww rproxy` is a small reverse proxy in front of one app. traefik routes to
|
- `sww rproxy` is a small reverse proxy in front of one app. traefik routes to
|
||||||
it. It asks `discriminator` about each request, passes the allowed ones to the
|
it. It asks `discriminator` about each request, passes the allowed ones to the
|
||||||
@@ -171,11 +171,11 @@ does.
|
|||||||
`https://<site>/_sww/healthz` answers `ok`. `discriminator`'s log lines about
|
`https://<site>/_sww/healthz` answers `ok`. `discriminator`'s log lines about
|
||||||
the request have `"action":"forward"`.
|
the request have `"action":"forward"`.
|
||||||
- To see a refusal without banning yourself, run `discriminator` with
|
- To see a refusal without banning yourself, run `discriminator` with
|
||||||
`SWW_MODE=observe` and ask for `/.env`. The request reaches the app.
|
`SWWAF_MODE=observe` and ask for `/.env`. The request reaches the app.
|
||||||
`discriminator`'s log lines have `"would_action":"banned"` and
|
`discriminator`'s log lines have `"would_action":"banned"` and
|
||||||
`"rule_ids":["env-file"]`. A ban you get in `enforce` mode is lifted as
|
`"rule_ids":["env-file"]`. A ban you get in `enforce` mode is lifted as
|
||||||
"Clients and bans" below describes.
|
"Clients and bans" below describes.
|
||||||
- With `SWW_METRICS_TOKEN` set, this answers with the metrics, from a host on
|
- With `SWWAF_METRICS_TOKEN` set, this answers with the metrics, from a host on
|
||||||
`discriminator`'s network:
|
`discriminator`'s network:
|
||||||
`curl -H "Authorization: Bearer $TOKEN" http://discriminator:8080/_sww/metrics`.
|
`curl -H "Authorization: Bearer $TOKEN" http://discriminator:8080/_sww/metrics`.
|
||||||
- `make example-app` builds the image and an app on it, runs them, and checks
|
- `make example-app` builds the image and an app on it, runs them, and checks
|
||||||
@@ -185,17 +185,16 @@ does.
|
|||||||
|
|
||||||
Every setting is an environment variable, read once at start; changing one means
|
Every setting is an environment variable, read once at start; changing one means
|
||||||
restarting the container. `rproxy` reads only the `SWRP_` settings, and
|
restarting the container. `rproxy` reads only the `SWRP_` settings, and
|
||||||
`discriminator` only the `SWW_` ones. Every setting but `SWRP_WAF_BACKEND` has a
|
`discriminator` only the `SWWAF_` ones. Every setting but `SWRP_WAF_BACKEND` has
|
||||||
default, chosen for a service on the open internet. A setting that needs an
|
a default, chosen for a service on the open internet. A setting that needs an
|
||||||
account, a key, a token or an alert destination stays off until you give one.
|
account, a key, a token or an alert destination stays off until you give one.
|
||||||
|
|
||||||
- Any `SWW_` setting can be given as a file instead, for secrets and long lists:
|
- Any `SWWAF_` setting can be given as a file instead, for secrets and long
|
||||||
`SWW_ADMIN_TOKEN_FILE` names the file that holds `SWW_ADMIN_TOKEN`. The file's
|
lists: `SWWAF_ADMIN_TOKEN_FILE` names the file that holds `SWWAF_ADMIN_TOKEN`.
|
||||||
contents are the value, less one newline at the end. Setting both forms stops
|
The file's contents are the value, less one newline at the end. Setting both
|
||||||
the start. `SWW_LOG_REMOTE_TLS_CA_FILE` and
|
forms stops the start. `SWWAF_LOG_REMOTE_TLS_CA_FILE` already names a file, so
|
||||||
`SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have no
|
it has no `_FILE` form, and no `SWRP_` setting has one, since `rproxy` never
|
||||||
`_FILE` form. `SWW_COMMAND` cannot be given as a file. No `SWRP_` setting has
|
touches disk.
|
||||||
one, since `rproxy` never touches disk.
|
|
||||||
- A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`.
|
- A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`.
|
||||||
- A size is a number of bytes with an optional `K`, `M` or `G`, in powers of
|
- A size is a number of bytes with an optional `K`, `M` or `G`, in powers of
|
||||||
1024: `512K`, `100M`, `5G`.
|
1024: `512K`, `100M`, `5G`.
|
||||||
@@ -215,122 +214,121 @@ account, a key, a token or an alert destination stays off until you give one.
|
|||||||
|
|
||||||
Every setting the code reads, with its default:
|
Every setting the code reads, with its default:
|
||||||
|
|
||||||
| Variable | Default | What it does |
|
| Variable | Default | What it does |
|
||||||
| ---------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `discriminator`. |
|
| `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `discriminator`. |
|
||||||
| `SWRP_WAF_BACKEND` | required | `discriminator`'s URL, such as `http://discriminator:8080`. |
|
| `SWRP_WAF_BACKEND` | required | `discriminator`'s URL, such as `http://discriminator:8080`. |
|
||||||
| `SWRP_UPSTREAM_URL` | `http://127.0.0.1:8081` | The app's URL: a scheme, a host and an optional port, nothing more. |
|
| `SWRP_UPSTREAM_URL` | `http://127.0.0.1:8081` | The app's URL: a scheme, a host and an optional port, nothing more. |
|
||||||
| `SWRP_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. |
|
| `SWRP_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. |
|
||||||
| `SWRP_TRUSTED_PROXIES` | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | The netblocks whose `X-Forwarded-For` `rproxy` believes; set but empty, it believes none. |
|
| `SWRP_TRUSTED_PROXIES` | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | The netblocks whose `X-Forwarded-For` `rproxy` believes; set but empty, it believes none. |
|
||||||
| `SWRP_TIMEOUT_MS` | `2000` | How long `rproxy` waits for `discriminator`'s answer to a question and to a report, in milliseconds. |
|
| `SWRP_TIMEOUT_MS` | `2000` | How long `rproxy` waits for `discriminator`'s answer to a question and to a report, in milliseconds. |
|
||||||
| `SWRP_DEFAULT_DISPOSITION` | `allow` | What `rproxy` does with a request when `discriminator` cannot be reached or does not answer in time: `allow` passes it to the app, `deny` answers `503`. |
|
| `SWRP_DEFAULT_DISPOSITION` | `allow` | What `rproxy` does with a request when `discriminator` cannot be reached or does not answer in time: `allow` passes it to the app, `deny` answers `503`. |
|
||||||
| `SWRP_MAX_REQUEST_SIZE` | `100M` | The largest request body, a size or `off`; a larger one is answered `413`. |
|
| `SWRP_MAX_REQUEST_SIZE` | `100M` | The largest request body, a size or `off`; a larger one is answered `413`. |
|
||||||
| `SWRP_MAX_RESPONSE_SIZE` | `1G` | The largest response body, a size or `off`; a larger one is answered `502`, or cut off once it has started. |
|
| `SWRP_MAX_RESPONSE_SIZE` | `1G` | The largest response body, a size or `off`; a larger one is answered `502`, or cut off once it has started. |
|
||||||
| `SWRP_MAX_REQUEST_TIME_MS` | `60000` | How long the visitor may take to send its request line and headers, and then the whole request to reach the app, in milliseconds or `off`. |
|
| `SWRP_MAX_REQUEST_TIME_MS` | `60000` | How long the visitor may take to send its request line and headers, and then the whole request to reach the app, in milliseconds or `off`. |
|
||||||
| `SWRP_MAX_RESPONSE_TIME_MS` | `1800000` | How long the answer may take, from the end of the request to its last byte reaching the visitor, in milliseconds or `off`. |
|
| `SWRP_MAX_RESPONSE_TIME_MS` | `1800000` | How long the answer may take, from the end of the request to its last byte reaching the visitor, in milliseconds or `off`. |
|
||||||
| `SWW_LISTEN_ADDR` | `:8080` | Where `discriminator` listens; leave the host part empty. |
|
| `SWWAF_LISTEN_ADDR` | `:8080` | Where `discriminator` listens; leave the host part empty. |
|
||||||
| `SWW_INSTANCE_NAME` | the container's host name | The name in every line `discriminator` logs, and in every alert and metric, such as `fsn1/discriminator`. |
|
| `SWWAF_INSTANCE_NAME` | the container's host name | The name in every line `discriminator` logs, and in every alert and metric, such as `fsn1/discriminator`. |
|
||||||
| `SWW_MODE` | `enforce` | `observe` refuses nothing and makes no ban; each log line says what `enforce` would have done. |
|
| `SWWAF_MODE` | `enforce` | `observe` refuses nothing and makes no ban; each log line says what `enforce` would have done. |
|
||||||
| `SWW_IPV6_GROUP_PREFIX` | `64` | The prefix length of the IPv6 netblock that counts as one client, from 32 to 128. |
|
| `SWWAF_IPV6_GROUP_PREFIX` | `64` | The prefix length of the IPv6 netblock that counts as one client, from 32 to 128. |
|
||||||
| `SWW_MAX_TRACKED_CLIENTS` | `20000` | The most clients kept, in memory and in `clients.json`; the least recently seen is dropped first. |
|
| `SWWAF_MAX_TRACKED_CLIENTS` | `20000` | The most clients kept, in memory and in `clients.json`; the least recently seen is dropped first. |
|
||||||
| `SWW_MAX_BANS` | `5000` | The most bans `discriminator` keeps of its own; bans an admin made do not count. |
|
| `SWWAF_MAX_BANS` | `5000` | The most bans `discriminator` keeps of its own; bans an admin made do not count. |
|
||||||
| `SWW_ALLOW_NETS` | empty | Netblocks whose clients skip every check, such as your monitoring. |
|
| `SWWAF_ALLOW_NETS` | empty | Netblocks whose clients skip every check, such as your monitoring. |
|
||||||
| `SWW_RATE_LIMIT_EXEMPT_NETS` | empty | Netblocks whose clients the request and byte limits neither count nor refuse. |
|
| `SWWAF_RATE_LIMIT_EXEMPT_NETS` | empty | Netblocks whose clients the request and byte limits neither count nor refuse. |
|
||||||
| `SWW_DENY_NETS` | empty | Netblocks whose clients are refused. |
|
| `SWWAF_DENY_NETS` | empty | Netblocks whose clients are refused. |
|
||||||
| `SWW_RATE_LIMIT_PER_MINUTE` | `120` | The most requests a client may make in a minute, a count or `off`; one more bans it. |
|
| `SWWAF_RATE_LIMIT_PER_MINUTE` | `120` | The most requests a client may make in a minute, a count or `off`; one more bans it. |
|
||||||
| `SWW_RATE_LIMIT_PER_HOUR` | `1500` | The most requests a client may make in an hour, a count or `off`; one more bans it. |
|
| `SWWAF_RATE_LIMIT_PER_HOUR` | `1500` | The most requests a client may make in an hour, a count or `off`; one more bans it. |
|
||||||
| `SWW_RATE_LIMIT_PER_DAY` | `6000` | The most requests a client may make in a day, a count or `off`; one more bans it. |
|
| `SWWAF_RATE_LIMIT_PER_DAY` | `6000` | The most requests a client may make in a day, a count or `off`; one more bans it. |
|
||||||
| `SWW_RATE_LIMIT_EXEMPT_PATHS` | empty | Path prefixes whose requests the request and byte limits neither count nor refuse, such as `/assets/`. |
|
| `SWWAF_RATE_LIMIT_EXEMPT_PATHS` | empty | Path prefixes whose requests the request and byte limits neither count nor refuse, such as `/assets/`. |
|
||||||
| `SWW_BYTES_LIMIT_PER_MINUTE` | `off` | The most bytes a client's requests may carry in a minute, a size or `off`; the request that takes a client past any byte limit bans it. |
|
| `SWWAF_BYTES_LIMIT_PER_MINUTE` | `off` | The most bytes a client's requests may carry in a minute, a size or `off`; the request that takes a client past any byte limit bans it. |
|
||||||
| `SWW_BYTES_LIMIT_PER_HOUR` | `50G` | The most bytes a client's requests may carry in an hour, a size or `off`. |
|
| `SWWAF_BYTES_LIMIT_PER_HOUR` | `50G` | The most bytes a client's requests may carry in an hour, a size or `off`. |
|
||||||
| `SWW_BYTES_LIMIT_PER_DAY` | `100G` | The most bytes a client's requests may carry in a day, a size or `off`. |
|
| `SWWAF_BYTES_LIMIT_PER_DAY` | `100G` | The most bytes a client's requests may carry in a day, a size or `off`. |
|
||||||
| `SWW_BYTES_COUNT` | `both` | Which body bytes the byte limits count: `response`, `request` or `both`. |
|
| `SWWAF_BYTES_COUNT` | `both` | Which body bytes the byte limits count: `response`, `request` or `both`. |
|
||||||
| `SWW_ERROR_BURST_THRESHOLD` | `30` | The most refusals by a rule file, a trap path or the Core Rule Set a client may get in a minute, a count or `off`; one more bans it. |
|
| `SWWAF_ERROR_BURST_THRESHOLD` | `30` | The most refusals by a rule file, a trap path or the Core Rule Set a client may get in a minute, a count or `off`; one more bans it. |
|
||||||
| `SWW_BAN_RESPONSE` | `403` | How a refused client is answered: `403`, `429` or `close`, which drops the connection. |
|
| `SWWAF_BAN_RESPONSE` | `403` | How a refused client is answered: `403`, `429` or `close`, which drops the connection. |
|
||||||
| `SWW_LIMIT_BAN_DURATION` | `1h` | The ban for a first broken limit. |
|
| `SWWAF_LIMIT_BAN_DURATION` | `1h` | The ban for a first broken limit. |
|
||||||
| `SWW_LIMIT_BAN_REPEAT_WINDOW` | `24h` | A limit broken again within this time after the last such ban ended gets a ban three times as long. |
|
| `SWWAF_LIMIT_BAN_REPEAT_WINDOW` | `24h` | A limit broken again within this time after the last such ban ended gets a ban three times as long. |
|
||||||
| `SWW_MAX_BAN_DURATION` | `7d` | A ban for a broken limit that would be longer than this is permanent instead. |
|
| `SWWAF_MAX_BAN_DURATION` | `7d` | A ban for a broken limit that would be longer than this is permanent instead. |
|
||||||
| `SWW_ATTACK_BAN_DURATION` | `7d` | The ban for a first clear sign of attack: a `ban` rule or a trap path. |
|
| `SWWAF_ATTACK_BAN_DURATION` | `7d` | The ban for a first clear sign of attack: a `ban` rule or a trap path. |
|
||||||
| `SWW_BAN_SCOPE_V4_PREFIX` | `32` | The prefix length of the netblock a ban on an IPv4 client covers, from 0 to 32. |
|
| `SWWAF_BAN_SCOPE_V4_PREFIX` | `32` | The prefix length of the netblock a ban on an IPv4 client covers, from 0 to 32. |
|
||||||
| `SWW_LOOKUP_SOURCE` | `geojs` | Where each client's AS number and country come from: `geojs`, a web service, `file`, or `off`. |
|
| `SWWAF_LOOKUP_SOURCE` | `geojs` | Where each client's AS number and country come from: `geojs`, a web service, `file`, or `off`. |
|
||||||
| `SWW_LOOKUP_DB_PATH` | empty | The IPinfo Lite database file, in its `.mmdb` form, read while the source is `file`. |
|
| `SWWAF_LOOKUP_DB_PATH` | empty | The IPinfo Lite database file, in its `.mmdb` form, read while the source is `file`. |
|
||||||
| `SWW_LOOKUP_TIMEOUT` | `800ms` | How long a request waits for its client's first GeoJS answer when a setting needs it, and how long a request to GeoJS may take. |
|
| `SWWAF_LOOKUP_TIMEOUT` | `800ms` | How long a request waits for its client's first GeoJS answer when a setting needs it, and how long a request to GeoJS may take. |
|
||||||
| `SWW_ADD_LOOKUP_HEADERS` | `false` | `true` passes the app the client's AS number and country in `X-Client-ASN` and `X-Client-Country`. |
|
| `SWWAF_ADD_LOOKUP_HEADERS` | `false` | `true` passes the app the client's AS number and country in `X-Client-ASN` and `X-Client-Country`. |
|
||||||
| `SWW_DENIED_COUNTRIES` | empty | Countries whose clients are refused, such as `cn,ru`. |
|
| `SWWAF_DENIED_COUNTRIES` | empty | Countries whose clients are refused, such as `cn,ru`. |
|
||||||
| `SWW_EXCLUSIVELY_ALLOWED_COUNTRIES` | empty | When set, the only countries whose clients get through; a client the lookup cannot place is refused too. |
|
| `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` | empty | When set, the only countries whose clients get through; a client the lookup cannot place is refused too. |
|
||||||
| `SWW_ASN_LIMIT_PERCENT` | empty | AS numbers whose clients get a percentage of every request and byte limit, such as `AS14061:50,AS16276:25`. |
|
| `SWWAF_ASN_LIMIT_PERCENT` | empty | AS numbers whose clients get a percentage of every request and byte limit, such as `AS14061:50,AS16276:25`. |
|
||||||
| `SWW_COUNTRY_LIMIT_PERCENT` | empty | The same by country, such as `cn:25`. |
|
| `SWWAF_COUNTRY_LIMIT_PERCENT` | empty | The same by country, such as `cn:25`. |
|
||||||
| `SWW_ASN_BYTES_PERCENT` | empty | A percentage of the byte limits alone for the AS numbers listed, in place of their `SWW_ASN_LIMIT_PERCENT`. |
|
| `SWWAF_ASN_BYTES_PERCENT` | empty | A percentage of the byte limits alone for the AS numbers listed, in place of their `SWWAF_ASN_LIMIT_PERCENT`. |
|
||||||
| `SWW_COUNTRY_BYTES_PERCENT` | empty | A percentage of the byte limits alone for the countries listed, in place of their `SWW_COUNTRY_LIMIT_PERCENT`. |
|
| `SWWAF_COUNTRY_BYTES_PERCENT` | empty | A percentage of the byte limits alone for the countries listed, in place of their `SWWAF_COUNTRY_LIMIT_PERCENT`. |
|
||||||
| `SWW_UNKNOWN_LIMIT_PERCENT` | `100` | The percentage of every limit a client without a country gets. |
|
| `SWWAF_UNKNOWN_LIMIT_PERCENT` | `100` | The percentage of every limit a client without a country gets. |
|
||||||
| `SWW_ASN_LIMIT_PERCENT_URL` | unset | The URL of a text file of `AS<number>:<percent>` lines, fetched like a blocklist; where it and `SWW_ASN_LIMIT_PERCENT` both give a percentage, the lower applies. |
|
| `SWWAF_ASN_LIMIT_PERCENT_URL` | unset | The URL of a text file of `AS<number>:<percent>` lines, fetched like a blocklist; where it and `SWWAF_ASN_LIMIT_PERCENT` both give a percentage, the lower applies. |
|
||||||
| `SWW_RULES_DIR` | `/etc/sww/rules.d` | The directory of the rule files, read at start and again when a file there changes. |
|
| `SWWAF_RULES_DIR` | `/etc/sww/rules.d` | The directory of the rule files, read at start and again when a file there changes. |
|
||||||
| `SWW_RULES_ENABLED` | `true` | `false` reads no rule file. |
|
| `SWWAF_RULES_ENABLED` | `true` | `false` reads no rule file. |
|
||||||
| `SWW_TRAP_PATHS` | empty | Paths only scanners ask for, such as `/wp-login.php,/xmlrpc.php`; a request for one bans the client. |
|
| `SWWAF_TRAP_PATHS` | empty | Paths only scanners ask for, such as `/wp-login.php,/xmlrpc.php`; a request for one bans the client. |
|
||||||
| `SWW_WAF_MODE` | `block` | What the Core Rule Set does with a match: `block` refuses it with `403`, `detect` only logs and alerts it, `off` runs no rule. |
|
| `SWWAF_WAF_MODE` | `block` | What the Core Rule Set does with a match: `block` refuses it with `403`, `detect` only logs and alerts it, `off` runs no rule. |
|
||||||
| `SWW_WAF_PARANOIA_LEVEL` | `1` | The Core Rule Set's paranoia level, from 1 to 4; each level up finds more attacks and takes more ordinary requests for attacks. |
|
| `SWWAF_WAF_PARANOIA_LEVEL` | `1` | The Core Rule Set's paranoia level, from 1 to 4; each level up finds more attacks and takes more ordinary requests for attacks. |
|
||||||
| `SWW_WAF_ANOMALY_THRESHOLD` | `5` | The Core Rule Set score at which a request is a match, a whole number or `off`; one critical rule scores 5. |
|
| `SWWAF_WAF_ANOMALY_THRESHOLD` | `5` | The Core Rule Set score at which a request is a match, a whole number or `off`; one critical rule scores 5. |
|
||||||
| `SWW_WAF_DISABLED_RULES` | `920340,920420,920440,920640,930130,930140` | Core Rule Set rule ids switched off; a list you give replaces the default, so repeat these six to keep them off. |
|
| `SWWAF_WAF_DISABLED_RULES` | `920340,920420,920440,920640,930130,930140` | Core Rule Set rule ids switched off; a list you give replaces the default, so repeat these six to keep them off. |
|
||||||
| `SWW_WAF_EXEMPT_PATHS` | empty | Path prefixes the Core Rule Set does not inspect. |
|
| `SWWAF_WAF_EXEMPT_PATHS` | empty | Path prefixes the Core Rule Set does not inspect. |
|
||||||
| `SWW_WAF_BODY_LIMIT` | `off` | How much of a request body the Core Rule Set reads, a size up to `64K`; `off` reads none. |
|
| `SWWAF_WAF_BODY_LIMIT` | `off` | How much of a request body the Core Rule Set reads, a size up to `64K`; `off` reads none. |
|
||||||
| `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` | unset | A file of Core Rule Set exclusions, as Coraza directives, that switch rules off for some fields or paths only. |
|
| `SWWAF_BLOCKLIST_URLS` | empty | Blocklists to fetch, text files of addresses and netblocks one to a line, such as `https://www.spamhaus.org/drop/drop.txt`. |
|
||||||
| `SWW_BLOCKLIST_URLS` | empty | Blocklists to fetch, text files of addresses and netblocks one to a line, such as `https://www.spamhaus.org/drop/drop.txt`. |
|
| `SWWAF_BLOCKLIST_REFRESH` | `24h` | How long after a list was last tried it is fetched again, `1h` or more. |
|
||||||
| `SWW_BLOCKLIST_REFRESH` | `24h` | How long after a list was last tried it is fetched again, `1h` or more. |
|
| `SWWAF_BLOCKLIST_ACTION` | `deny` | What a client on a blocklist gets: `deny`, `limit:<percent>` of every limit, or `log`. |
|
||||||
| `SWW_BLOCKLIST_ACTION` | `deny` | What a client on a blocklist gets: `deny`, `limit:<percent>` of every limit, or `log`. |
|
| `SWWAF_DNSBL_ZONES` | empty | DNS blocklists asked about each new client in the background, such as `dnsbl.dronebl.org`. |
|
||||||
| `SWW_DNSBL_ZONES` | empty | DNS blocklists asked about each new client in the background, such as `dnsbl.dronebl.org`. |
|
| `SWWAF_DNSBL_RESOLVER` | unset | The resolver the zones are asked through, an IP address with an optional port; unset, the host's resolver is used. |
|
||||||
| `SWW_DNSBL_RESOLVER` | unset | The resolver the zones are asked through, an IP address with an optional port; unset, the host's resolver is used. |
|
| `SWWAF_ABUSEIPDB_KEY` | unset | The key of your AbuseIPDB account; set, a client that has committed an offence is checked in the background. |
|
||||||
| `SWW_ABUSEIPDB_KEY` | unset | The key of your AbuseIPDB account; set, a client that has committed an offence is checked in the background. |
|
| `SWWAF_ABUSEIPDB_MIN_SCORE` | `75` | The least AbuseIPDB abuse confidence score, from 0 to 100, that is a hit. |
|
||||||
| `SWW_ABUSEIPDB_MIN_SCORE` | `75` | The least AbuseIPDB abuse confidence score, from 0 to 100, that is a hit. |
|
| `SWWAF_ABUSEIPDB_DAILY_BUDGET` | `900` | The most AbuseIPDB checks made in a day. |
|
||||||
| `SWW_ABUSEIPDB_DAILY_BUDGET` | `900` | The most AbuseIPDB checks made in a day. |
|
| `SWWAF_CROWDSEC_LAPI_URL` | unset | The local API of a CrowdSec engine you run, such as `http://172.17.0.1:8080`; its decision list is fetched every minute, and a listed client is banned. |
|
||||||
| `SWW_CROWDSEC_LAPI_URL` | unset | The local API of a CrowdSec engine you run, such as `http://172.17.0.1:8080`; its decision list is fetched every minute, and a listed client is banned. |
|
| `SWWAF_CROWDSEC_LAPI_KEY` | unset | The key the engine gave for `smallwebwaf`, as `cscli bouncers add smallwebwaf` prints it; set with the URL, never alone. |
|
||||||
| `SWW_CROWDSEC_LAPI_KEY` | unset | The key the engine gave for `smallwebwaf`, as `cscli bouncers add smallwebwaf` prints it; set with the URL, never alone. |
|
| `SWWAF_REPUTATION_ACTION` | `limit:25` | What a client listed by a DNS blocklist, or scored a hit by AbuseIPDB, gets: `deny`, `limit:<percent>` or `log`. |
|
||||||
| `SWW_REPUTATION_ACTION` | `limit:25` | What a client listed by a DNS blocklist, or scored a hit by AbuseIPDB, gets: `deny`, `limit:<percent>` or `log`. |
|
| `SWWAF_REPUTATION_CACHE_TTL` | `24h` | How long a DNS blocklist verdict or an AbuseIPDB score is used. |
|
||||||
| `SWW_REPUTATION_CACHE_TTL` | `24h` | How long a DNS blocklist verdict or an AbuseIPDB score is used. |
|
| `SWWAF_REPUTATION_TIMEOUT` | `2s` | How long a DNS blocklist query or an AbuseIPDB check may take. |
|
||||||
| `SWW_REPUTATION_TIMEOUT` | `2s` | How long a DNS blocklist query or an AbuseIPDB check may take. |
|
| `SWWAF_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. |
|
||||||
| `SWW_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. |
|
| `SWWAF_STATE_WRITE_DELAY` | `10s` | How long `bans.json` waits to be written after a ban is made, lifted or made permanent. |
|
||||||
| `SWW_STATE_WRITE_DELAY` | `10s` | How long `bans.json` waits to be written after a ban is made, lifted or made permanent. |
|
| `SWWAF_STATE_COUNTER_INTERVAL` | `15m` | How often every state file is written. |
|
||||||
| `SWW_STATE_COUNTER_INTERVAL` | `15m` | How often every state file is written. |
|
| `SWWAF_LOG_LEVEL` | `info` | The least severe of `discriminator`'s own messages that are written: `debug`, `info`, `warn` or `error`; request lines are always written. |
|
||||||
| `SWW_LOG_LEVEL` | `info` | The least severe of `discriminator`'s own messages that are written: `debug`, `info`, `warn` or `error`; request lines are always written. |
|
| `SWWAF_LOG_REQUEST_HEADERS` | `accept,accept-language,accept-encoding,content-type,origin,range` | The request headers whose values the request log records; `Authorization` and `Cookie` are never recorded. |
|
||||||
| `SWW_LOG_REQUEST_HEADERS` | `accept,accept-language,accept-encoding,content-type,origin,range` | The request headers whose values the request log records; `Authorization` and `Cookie` are never recorded. |
|
| `SWWAF_LOG_REMOTE_URL` | unset | A syslog server that also gets every line `discriminator` writes: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://` with a host and a port. |
|
||||||
| `SWW_LOG_REMOTE_URL` | unset | A syslog server that also gets every line `discriminator` writes: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://` with a host and a port. |
|
| `SWWAF_LOG_REMOTE_TLS_CA_FILE` | unset | A file of PEM certificates a `syslog+tls` server's certificate must chain to, instead of the host's own. |
|
||||||
| `SWW_LOG_REMOTE_TLS_CA_FILE` | unset | A file of PEM certificates a `syslog+tls` server's certificate must chain to, instead of the host's own. |
|
| `SWWAF_LOG_REMOTE_BUFFER` | `10000` | The most lines held while they wait to be sent; past it, the oldest is dropped. |
|
||||||
| `SWW_LOG_REMOTE_BUFFER` | `10000` | The most lines held while they wait to be sent; past it, the oldest is dropped. |
|
| `SWWAF_LOG_REMOTE_FACILITY` | `local0` | The syslog facility of each record sent. |
|
||||||
| `SWW_LOG_REMOTE_FACILITY` | `local0` | The syslog facility of each record sent. |
|
| `SWWAF_LOG_REMOTE_APP_NAME` | `SWWAF_INSTANCE_NAME` | The app name in each syslog record, 1 to 48 printable ASCII characters without a space. |
|
||||||
| `SWW_LOG_REMOTE_APP_NAME` | `SWW_INSTANCE_NAME` | The app name in each syslog record, 1 to 48 printable ASCII characters without a space. |
|
| `SWWAF_ADMIN_TOKEN` | unset | The bearer token for the ban and client endpoints, at least 32 characters; unset, they answer `404`. |
|
||||||
| `SWW_ADMIN_TOKEN` | unset | The bearer token for the ban and client endpoints, at least 32 characters; unset, they answer `404`. |
|
| `SWWAF_METRICS_TOKEN` | unset | The bearer token for `/_sww/metrics`, at least 32 characters; unset, the metrics are off. |
|
||||||
| `SWW_METRICS_TOKEN` | unset | The bearer token for `/_sww/metrics`, at least 32 characters; unset, the metrics are off. |
|
| `SWWAF_METRICS_TOP_N` | `50` | How many AS numbers and how many countries get metric series of their own; the rest are summed as `other`. |
|
||||||
| `SWW_METRICS_TOP_N` | `50` | How many AS numbers and how many countries get metric series of their own; the rest are summed as `other`. |
|
| `SWWAF_ALERT_WEBHOOK_URL` | unset | A webhook each alert is posted to as a JSON object. |
|
||||||
| `SWW_ALERT_WEBHOOK_URL` | unset | A webhook each alert is posted to as a JSON object. |
|
| `SWWAF_ALERT_WEBHOOK_HEADERS` | empty | Headers sent with each post, a list of `Name:value`, such as `Authorization:Bearer <token>`. |
|
||||||
| `SWW_ALERT_WEBHOOK_HEADERS` | empty | Headers sent with each post, a list of `Name:value`, such as `Authorization:Bearer <token>`. |
|
| `SWWAF_ALERT_SLACK_WEBHOOK_URL` | unset | A Slack incoming webhook each alert is posted to as a message. |
|
||||||
| `SWW_ALERT_SLACK_WEBHOOK_URL` | unset | A Slack incoming webhook each alert is posted to as a message. |
|
| `SWWAF_ALERT_NTFY_URL` | unset | An ntfy topic each alert is published to, as its full URL, such as `https://ntfy.sh/my-alerts`. |
|
||||||
| `SWW_ALERT_NTFY_URL` | unset | An ntfy topic each alert is published to, as its full URL, such as `https://ntfy.sh/my-alerts`. |
|
| `SWWAF_ALERT_NTFY_TOKEN` | unset | An ntfy access token sent with each alert. |
|
||||||
| `SWW_ALERT_NTFY_TOKEN` | unset | An ntfy access token sent with each alert. |
|
| `SWWAF_ALERT_EVENTS` | `ban,permanent_ban,waf_block,anomaly,reputation_hit,source_failure,file_error` | The events alerts are sent for; the hourly `summary` is sent whatever it says. |
|
||||||
| `SWW_ALERT_EVENTS` | `ban,permanent_ban,waf_block,anomaly,reputation_hit,source_failure,file_error` | The events alerts are sent for; the hourly `summary` is sent whatever it says. |
|
| `SWWAF_ALERT_COOLDOWN` | `15m` | How long after an alert its repeats are held back and counted, a duration or `off`. |
|
||||||
| `SWW_ALERT_COOLDOWN` | `15m` | How long after an alert its repeats are held back and counted, a duration or `off`. |
|
| `SWWAF_ALERT_MAX_PER_HOUR` | `60` | The most alerts sent in an hour, a number or `off`; the rest are counted in the hour's `summary`. |
|
||||||
| `SWW_ALERT_MAX_PER_HOUR` | `60` | The most alerts sent in an hour, a number or `off`; the rest are counted in the hour's `summary`. |
|
| `SWWAF_ANOMALY_CLIENT_REQUESTS_PER_MINUTE` | `off` | Alert when one client makes more requests than this in a minute, a count. |
|
||||||
| `SWW_ANOMALY_CLIENT_REQUESTS_PER_MINUTE` | `off` | Alert when one client makes more requests than this in a minute, a count. |
|
| `SWWAF_ANOMALY_CLIENT_REQUESTS_PER_HOUR` | `off` | Alert when one client makes more requests than this in an hour, a count. |
|
||||||
| `SWW_ANOMALY_CLIENT_REQUESTS_PER_HOUR` | `off` | Alert when one client makes more requests than this in an hour, a count. |
|
| `SWWAF_ANOMALY_CLIENT_BYTES_PER_MINUTE` | `off` | Alert when one client's requests carry more bytes than this in a minute, a size. |
|
||||||
| `SWW_ANOMALY_CLIENT_BYTES_PER_MINUTE` | `off` | Alert when one client's requests carry more bytes than this in a minute, a size. |
|
| `SWWAF_ANOMALY_CLIENT_BYTES_PER_HOUR` | `off` | Alert when one client's requests carry more bytes than this in an hour, a size. |
|
||||||
| `SWW_ANOMALY_CLIENT_BYTES_PER_HOUR` | `off` | Alert when one client's requests carry more bytes than this in an hour, a size. |
|
| `SWWAF_ANOMALY_NET_REQUESTS_PER_MINUTE` | `off` | Alert when the netblock around a client makes more requests than this in a minute, a count. |
|
||||||
| `SWW_ANOMALY_NET_REQUESTS_PER_MINUTE` | `off` | Alert when the netblock around a client makes more requests than this in a minute, a count. |
|
| `SWWAF_ANOMALY_NET_REQUESTS_PER_HOUR` | `off` | Alert when the netblock around a client makes more requests than this in an hour, a count. |
|
||||||
| `SWW_ANOMALY_NET_REQUESTS_PER_HOUR` | `off` | Alert when the netblock around a client makes more requests than this in an hour, a count. |
|
| `SWWAF_ANOMALY_NET_BYTES_PER_MINUTE` | `off` | Alert when the netblock around a client carries more bytes than this in a minute, a size. |
|
||||||
| `SWW_ANOMALY_NET_BYTES_PER_MINUTE` | `off` | Alert when the netblock around a client carries more bytes than this in a minute, a size. |
|
| `SWWAF_ANOMALY_NET_BYTES_PER_HOUR` | `off` | Alert when the netblock around a client carries more bytes than this in an hour, a size. |
|
||||||
| `SWW_ANOMALY_NET_BYTES_PER_HOUR` | `off` | Alert when the netblock around a client carries more bytes than this in an hour, a size. |
|
| `SWWAF_ANOMALY_NET_V4_PREFIX` | `24` | The prefix length of the netblock around an IPv4 client that the `SWWAF_ANOMALY_NET_` thresholds count, from 0 to 32. |
|
||||||
| `SWW_ANOMALY_NET_V4_PREFIX` | `24` | The prefix length of the netblock around an IPv4 client that the `SWW_ANOMALY_NET_` thresholds count, from 0 to 32. |
|
| `SWWAF_ANOMALY_NET_V6_PREFIX` | `48` | The prefix length of the netblock around an IPv6 client that the `SWWAF_ANOMALY_NET_` thresholds count, from 0 to 128. |
|
||||||
| `SWW_ANOMALY_NET_V6_PREFIX` | `48` | The prefix length of the netblock around an IPv6 client that the `SWW_ANOMALY_NET_` thresholds count, from 0 to 128. |
|
| `SWWAF_ANOMALY_ASN_REQUESTS_PER_MINUTE` | `off` | Alert when one AS number makes more requests than this in a minute, a count. |
|
||||||
| `SWW_ANOMALY_ASN_REQUESTS_PER_MINUTE` | `off` | Alert when one AS number makes more requests than this in a minute, a count. |
|
| `SWWAF_ANOMALY_ASN_REQUESTS_PER_HOUR` | `off` | Alert when one AS number makes more requests than this in an hour, a count. |
|
||||||
| `SWW_ANOMALY_ASN_REQUESTS_PER_HOUR` | `off` | Alert when one AS number makes more requests than this in an hour, a count. |
|
| `SWWAF_ANOMALY_ASN_BYTES_PER_MINUTE` | `off` | Alert when one AS number carries more bytes than this in a minute, a size. |
|
||||||
| `SWW_ANOMALY_ASN_BYTES_PER_MINUTE` | `off` | Alert when one AS number carries more bytes than this in a minute, a size. |
|
| `SWWAF_ANOMALY_ASN_BYTES_PER_HOUR` | `off` | Alert when one AS number carries more bytes than this in an hour, a size. |
|
||||||
| `SWW_ANOMALY_ASN_BYTES_PER_HOUR` | `off` | Alert when one AS number carries more bytes than this in an hour, a size. |
|
| `SWWAF_ANOMALY_TOTAL_REQUESTS_PER_MINUTE` | `off` | Alert when the whole service gets more requests than this in a minute, a count. |
|
||||||
| `SWW_ANOMALY_TOTAL_REQUESTS_PER_MINUTE` | `off` | Alert when the whole service gets more requests than this in a minute, a count. |
|
| `SWWAF_ANOMALY_TOTAL_REQUESTS_PER_HOUR` | `off` | Alert when the whole service gets more requests than this in an hour, a count. |
|
||||||
| `SWW_ANOMALY_TOTAL_REQUESTS_PER_HOUR` | `off` | Alert when the whole service gets more requests than this in an hour, a count. |
|
| `SWWAF_ANOMALY_TOTAL_BYTES_PER_MINUTE` | `off` | Alert when the whole service carries more bytes than this in a minute, a size. |
|
||||||
| `SWW_ANOMALY_TOTAL_BYTES_PER_MINUTE` | `off` | Alert when the whole service carries more bytes than this in a minute, a size. |
|
| `SWWAF_ANOMALY_TOTAL_BYTES_PER_HOUR` | `off` | Alert when the whole service carries more bytes than this in an hour, a size. |
|
||||||
| `SWW_ANOMALY_TOTAL_BYTES_PER_HOUR` | `off` | Alert when the whole service carries more bytes than this in an hour, a size. |
|
| `SWWAF_WATCH_NETS` | empty | Named netblocks the `SWWAF_WATCH_` thresholds count each as a whole, such as `office=203.0.113.0/24,scraper-x=198.51.100.0/22`. |
|
||||||
| `SWW_WATCH_NETS` | empty | Named netblocks the `SWW_WATCH_` thresholds count each as a whole, such as `office=203.0.113.0/24,scraper-x=198.51.100.0/22`. |
|
| `SWWAF_WATCH_REQUESTS_PER_MINUTE` | `off` | Alert when a watched netblock makes more requests than this in a minute, a count. |
|
||||||
| `SWW_WATCH_REQUESTS_PER_MINUTE` | `off` | Alert when a watched netblock makes more requests than this in a minute, a count. |
|
| `SWWAF_WATCH_REQUESTS_PER_HOUR` | `off` | Alert when a watched netblock makes more requests than this in an hour, a count. |
|
||||||
| `SWW_WATCH_REQUESTS_PER_HOUR` | `off` | Alert when a watched netblock makes more requests than this in an hour, a count. |
|
| `SWWAF_WATCH_BYTES_PER_MINUTE` | `off` | Alert when a watched netblock carries more bytes than this in a minute, a size. |
|
||||||
| `SWW_WATCH_BYTES_PER_MINUTE` | `off` | Alert when a watched netblock carries more bytes than this in a minute, a size. |
|
| `SWWAF_WATCH_BYTES_PER_HOUR` | `off` | Alert when a watched netblock carries more bytes than this in an hour, a size. |
|
||||||
| `SWW_WATCH_BYTES_PER_HOUR` | `off` | Alert when a watched netblock carries more bytes than this in an hour, a size. |
|
|
||||||
|
|
||||||
[`docs/settings.md`](docs/settings.md) gives the values each setting takes in
|
[`docs/settings.md`](docs/settings.md) gives the values each setting takes in
|
||||||
full, and the limits that are fixed rather than settings.
|
full, and the limits that are fixed rather than settings.
|
||||||
@@ -403,23 +401,23 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
SWW_COMMAND: discriminator
|
SWW_COMMAND: discriminator
|
||||||
# Monitoring skips every check.
|
# Monitoring skips every check.
|
||||||
SWW_ALLOW_NETS: 192.0.2.10
|
SWWAF_ALLOW_NETS: 192.0.2.10
|
||||||
# The office and the automation host are never rate limited.
|
# The office and the automation host are never rate limited.
|
||||||
SWW_RATE_LIMIT_EXEMPT_NETS: 203.0.113.0/24,198.51.100.7
|
SWWAF_RATE_LIMIT_EXEMPT_NETS: 203.0.113.0/24,198.51.100.7
|
||||||
# A netblock that sends nothing wanted.
|
# A netblock that sends nothing wanted.
|
||||||
SWW_DENY_NETS: 198.51.100.128/25
|
SWWAF_DENY_NETS: 198.51.100.128/25
|
||||||
# Tighter limits for everyone else: a small static site.
|
# Tighter limits for everyone else: a small static site.
|
||||||
SWW_RATE_LIMIT_PER_MINUTE: "60"
|
SWWAF_RATE_LIMIT_PER_MINUTE: "60"
|
||||||
SWW_RATE_LIMIT_PER_HOUR: "600"
|
SWWAF_RATE_LIMIT_PER_HOUR: "600"
|
||||||
SWW_RATE_LIMIT_PER_DAY: "3000"
|
SWWAF_RATE_LIMIT_PER_DAY: "3000"
|
||||||
# Hosting networks and one country get a share of those limits.
|
# Hosting networks and one country get a share of those limits.
|
||||||
SWW_ASN_LIMIT_PERCENT: AS14061:50,AS16276:25
|
SWWAF_ASN_LIMIT_PERCENT: AS14061:50,AS16276:25
|
||||||
SWW_COUNTRY_LIMIT_PERCENT: cn:25
|
SWWAF_COUNTRY_LIMIT_PERCENT: cn:25
|
||||||
# Paths only scanners ask for; one request bans the client.
|
# Paths only scanners ask for; one request bans the client.
|
||||||
SWW_TRAP_PATHS: /wp-login.php,/xmlrpc.php
|
SWWAF_TRAP_PATHS: /wp-login.php,/xmlrpc.php
|
||||||
# Answer a refused client with 429 rather than 403.
|
# Answer a refused client with 429 rather than 403.
|
||||||
SWW_BAN_RESPONSE: "429"
|
SWWAF_BAN_RESPONSE: "429"
|
||||||
SWW_LIMIT_BAN_DURATION: 30m
|
SWWAF_LIMIT_BAN_DURATION: 30m
|
||||||
volumes:
|
volumes:
|
||||||
- discriminator-state:/var/lib/sww
|
- discriminator-state:/var/lib/sww
|
||||||
|
|
||||||
@@ -455,16 +453,16 @@ services:
|
|||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWW_COMMAND: discriminator
|
SWW_COMMAND: discriminator
|
||||||
SWW_INSTANCE_NAME: fsn1/discriminator
|
SWWAF_INSTANCE_NAME: fsn1/discriminator
|
||||||
# Refuse nothing yet; log what enforce mode would have done.
|
# Refuse nothing yet; log what enforce mode would have done.
|
||||||
SWW_MODE: observe
|
SWWAF_MODE: observe
|
||||||
# Look addresses up in a local IPinfo Lite database, never GeoJS.
|
# Look addresses up in a local IPinfo Lite database, never GeoJS.
|
||||||
SWW_LOOKUP_SOURCE: file
|
SWWAF_LOOKUP_SOURCE: file
|
||||||
SWW_LOOKUP_DB_PATH: /var/lib/ipinfo/ipinfo_lite.mmdb
|
SWWAF_LOOKUP_DB_PATH: /var/lib/ipinfo/ipinfo_lite.mmdb
|
||||||
# Alerts to an ntfy topic, with its token read from a file.
|
# Alerts to an ntfy topic, with its token read from a file.
|
||||||
SWW_ALERT_NTFY_URL: https://ntfy.example/smallwebwaf
|
SWWAF_ALERT_NTFY_URL: https://ntfy.example/smallwebwaf
|
||||||
SWW_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy
|
SWWAF_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy
|
||||||
SWW_ALERT_EVENTS: ban,permanent_ban,waf_block,source_failure,file_error
|
SWWAF_ALERT_EVENTS: ban,permanent_ban,waf_block,source_failure,file_error
|
||||||
volumes:
|
volumes:
|
||||||
- discriminator-state:/var/lib/sww
|
- discriminator-state:/var/lib/sww
|
||||||
- /srv/ipinfo:/var/lib/ipinfo:ro
|
- /srv/ipinfo:/var/lib/ipinfo:ro
|
||||||
@@ -506,7 +504,7 @@ of the request reaches the app.
|
|||||||
|
|
||||||
`discriminator` takes the checks:
|
`discriminator` takes the checks:
|
||||||
|
|
||||||
3. The static lists, `SWW_ALLOW_NETS` and `SWW_DENY_NETS`.
|
3. The static lists, `SWWAF_ALLOW_NETS` and `SWWAF_DENY_NETS`.
|
||||||
4. The bans.
|
4. The bans.
|
||||||
5. The lookup of the client's AS number and country.
|
5. The lookup of the client's AS number and country.
|
||||||
6. The country lists.
|
6. The country lists.
|
||||||
@@ -527,20 +525,20 @@ of the request reaches the app.
|
|||||||
and the bytes each way. For an allowed request, `discriminator` counts the
|
and the bytes each way. For an allowed request, `discriminator` counts the
|
||||||
bytes toward the byte limits and adds the request to the client's history.
|
bytes toward the byte limits and adds the request to the client's history.
|
||||||
|
|
||||||
A refused visitor is answered with `SWW_BAN_RESPONSE`. A `block` rule or a Core
|
A refused visitor is answered with `SWWAF_BAN_RESPONSE`. A `block` rule or a
|
||||||
Rule Set match answers `403` and bans nobody, since the Core Rule Set has false
|
Core Rule Set match answers `403` and bans nobody, since the Core Rule Set has
|
||||||
positives; such refusals count toward `SWW_ERROR_BURST_THRESHOLD` instead. A
|
false positives; such refusals count toward `SWWAF_ERROR_BURST_THRESHOLD`
|
||||||
request over the header limits of step 12 is answered `431`, and bans nobody
|
instead. A request over the header limits of step 12 is answered `431`, and bans
|
||||||
either. `rproxy` itself answers `405` to a method it does not pass on, and the
|
nobody either. `rproxy` itself answers `405` to a method it does not pass on,
|
||||||
status for a size or time limit, such as `413` for a body too large.
|
and the status for a size or time limit, such as `413` for a body too large.
|
||||||
|
|
||||||
### Clients and bans
|
### Clients and bans
|
||||||
|
|
||||||
- A client is one IPv4 address or one IPv6 netblock of `SWW_IPV6_GROUP_PREFIX`,
|
- A client is one IPv4 address or one IPv6 netblock of
|
||||||
since one abuser usually controls a whole /64.
|
`SWWAF_IPV6_GROUP_PREFIX`, since one abuser usually controls a whole /64.
|
||||||
- Each limit is counted over a sliding window with a few integers per client.
|
- Each limit is counted over a sliding window with a few integers per client.
|
||||||
The table of clients and the table of bans each have a bound
|
The table of clients and the table of bans each have a bound
|
||||||
(`SWW_MAX_TRACKED_CLIENTS`, `SWW_MAX_BANS`), so memory is bounded too.
|
(`SWWAF_MAX_TRACKED_CLIENTS`, `SWWAF_MAX_BANS`), so memory is bounded too.
|
||||||
- A ban refuses every request from its netblock until it ends. Requests refused
|
- A ban refuses every request from its netblock until it ends. Requests refused
|
||||||
under a ban count toward no limit. A permanent ban ends only when an admin
|
under a ban count toward no limit. A permanent ban ends only when an admin
|
||||||
lifts it.
|
lifts it.
|
||||||
@@ -552,16 +550,13 @@ status for a size or time limit, such as `413` for a body too large.
|
|||||||
### Attack detection
|
### Attack detection
|
||||||
|
|
||||||
- The Core Rule Set reads the method, the URL with its query string, and the
|
- The Core Rule Set reads the method, the URL with its query string, and the
|
||||||
headers of every request, and no body unless `SWW_WAF_BODY_LIMIT` is set. In
|
headers of every request, and no body unless `SWWAF_WAF_BODY_LIMIT` is set. In
|
||||||
front of a code forge, bodies carry what people write and publish, which the
|
front of a code forge, bodies carry what people write and publish, which the
|
||||||
rules take for attacks.
|
rules take for attacks.
|
||||||
- `smallwebwaf` changes the Core Rule Set in eight ways so that it lets ordinary
|
- `smallwebwaf` changes the Core Rule Set in six ways so that it lets ordinary
|
||||||
requests to a code forge through, such as git over HTTP and sign-in through
|
requests to a code forge through, such as git over HTTP and sign-in through
|
||||||
Git Credential Manager. On gitea's own paths, its searches, file names and
|
Git Credential Manager. No setting undoes them; "Attack detection" in
|
||||||
branch names get through too. No setting undoes the changes; "Attack
|
[`SPEC.md`](SPEC.md) lists them.
|
||||||
detection" in [`SPEC.md`](SPEC.md) lists them.
|
|
||||||
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
|
|
||||||
fields or paths.
|
|
||||||
- Rule files are plain text, one rule to a line, edited by hand and read again
|
- Rule files are plain text, one rule to a line, edited by hand and read again
|
||||||
while `discriminator` runs. The image ships one file of probes no real visitor
|
while `discriminator` runs. The image ships one file of probes no real visitor
|
||||||
sends (see [`docs/rules.md`](docs/rules.md)).
|
sends (see [`docs/rules.md`](docs/rules.md)).
|
||||||
@@ -569,7 +564,7 @@ status for a size or time limit, such as `413` for a body too large.
|
|||||||
### State files
|
### State files
|
||||||
|
|
||||||
`discriminator` keeps everything in memory, and the five JSON files in
|
`discriminator` keeps everything in memory, and the five JSON files in
|
||||||
`SWW_STATE_DIR` hold a copy: `bans.json`, `clients.json`, `lookups.json`,
|
`SWWAF_STATE_DIR` hold a copy: `bans.json`, `clients.json`, `lookups.json`,
|
||||||
`reputation.json` and `alerts.json`. Each is written whole, to a temporary file
|
`reputation.json` and `alerts.json`. Each is written whole, to a temporary file
|
||||||
renamed over the real one, so a crash leaves a complete file. `discriminator`
|
renamed over the real one, so a crash leaves a complete file. `discriminator`
|
||||||
notices a file edited on disk and takes the edit in; an edit that does not parse
|
notices a file edited on disk and takes the edit in; an edit that does not parse
|
||||||
@@ -582,10 +577,10 @@ is set aside as `<name>.bad`, and never stops the running process (see
|
|||||||
come from Ubuntu's archive, and nixpkgs is fixed at one release. An app's
|
come from Ubuntu's archive, and nixpkgs is fixed at one release. An app's
|
||||||
`Dockerfile` installs a package with `nix-env -iA nixpkgs.<name>` and gets the
|
`Dockerfile` installs a package with `nix-env -iA nixpkgs.<name>` and gets the
|
||||||
same package each time it is built (see [`docs/image.md`](docs/image.md)).
|
same package each time it is built (see [`docs/image.md`](docs/image.md)).
|
||||||
- runit, a service supervisor, runs `sww` as the `sww` user (uid and gid 65532)
|
- runit, a service supervisor, runs `smallwebwaf` as the `sww` user (uid and
|
||||||
and starts it again when it exits. The `run` script of `discriminator` gives
|
gid 65532) and starts it again when it exits. The `run` script of
|
||||||
the state directory to the `sww` user at each start, so a volume needs no
|
`discriminator` gives the state directory to the `sww` user at each start, so
|
||||||
change of owner.
|
a volume needs no change of owner.
|
||||||
- `rproxy` and `discriminator` use no token between them. `discriminator`'s own
|
- `rproxy` and `discriminator` use no token between them. `discriminator`'s own
|
||||||
endpoints, under `/_sww/`, are for your admin tools and your metrics
|
endpoints, under `/_sww/`, are for your admin tools and your metrics
|
||||||
collector, at its own address on the private network; nothing routes the
|
collector, at its own address on the private network; nothing routes the
|
||||||
|
|||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
// Command sww is a web application firewall. `sww rproxy` is the reverse
|
// Command sww is a web application firewall. `sww rproxy` is
|
||||||
// proxy in front of one app, which asks discriminator about each request.
|
// the reverse proxy in front of one app, which asks discriminator about each request.
|
||||||
// `sww discriminator` decides, for many sites, and keeps what lasts. `sww
|
// `sww discriminator` decides, for many sites, and keeps what lasts.
|
||||||
// healthcheck` is the container's health check. Everything it does is in
|
// `sww healthcheck` is the container's health check. Everything it does
|
||||||
// internal/sww.
|
// is in internal/sww.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -6,12 +6,8 @@
|
|||||||
# A real app names the smallwebwaf image by digest. This one takes the
|
# A real app names the smallwebwaf image by digest. This one takes the
|
||||||
# image script/example-app has just built, or else the one `make docker`
|
# image script/example-app has just built, or else the one `make docker`
|
||||||
# builds.
|
# builds.
|
||||||
ARG SWW_IMAGE=smallwebwaf
|
ARG SMALLWEBWAF_IMAGE=smallwebwaf
|
||||||
FROM ${SWW_IMAGE}
|
FROM ${SMALLWEBWAF_IMAGE}
|
||||||
|
|
||||||
# Its own title and description, in place of the smallwebwaf image's.
|
|
||||||
LABEL org.opencontainers.image.title="smallwebwaf-example-app" \
|
|
||||||
org.opencontainers.image.description="An example app built on the smallwebwaf image."
|
|
||||||
|
|
||||||
# Packages the app needs, from the nixpkgs in the image.
|
# Packages the app needs, from the nixpkgs in the image.
|
||||||
RUN nix-env -iA nixpkgs.busybox
|
RUN nix-env -iA nixpkgs.busybox
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ The endpoints under `/_sww/`, what each takes and what each answers.
|
|||||||
"Admin endpoints" in [`SPEC.md`](../SPEC.md) says how their tokens work.
|
"Admin endpoints" in [`SPEC.md`](../SPEC.md) says how their tokens work.
|
||||||
|
|
||||||
- `GET /_sww/healthz`: `200` and `ok`, to anyone.
|
- `GET /_sww/healthz`: `200` and `ok`, to anyone.
|
||||||
- `GET /_sww/metrics`: the metrics, for `SWW_METRICS_TOKEN` (see
|
- `GET /_sww/metrics`: the metrics, for `SWWAF_METRICS_TOKEN` (see
|
||||||
[`metrics.md`](metrics.md)).
|
[`metrics.md`](metrics.md)).
|
||||||
- `GET /_sww/bans`: every ban held, under `bans`, each as `bans.json` lists it.
|
- `GET /_sww/bans`: every ban held, under `bans`, each as `bans.json` lists it.
|
||||||
Needs `SWW_ADMIN_TOKEN`, as do the three below.
|
Needs `SWWAF_ADMIN_TOKEN`, as do the three below.
|
||||||
- `POST /_sww/bans`: bans a netblock. The body is a JSON object of `netblock`,
|
- `POST /_sww/bans`: bans a netblock. The body is a JSON object of `netblock`,
|
||||||
`duration` and, if you like, `reason`. `netblock` is a netblock such as
|
`duration` and, if you like, `reason`. `netblock` is a netblock such as
|
||||||
`203.0.113.0/24`, or a client's address, which bans its IPv4 netblock
|
`203.0.113.0/24`, or a client's address, which bans its IPv4 netblock
|
||||||
(`SWW_BAN_SCOPE_V4_PREFIX`) or its IPv6 group. `duration` is a duration such
|
(`SWWAF_BAN_SCOPE_V4_PREFIX`) or its IPv6 group. `duration` is a duration such
|
||||||
as `1h` or `7d`, or `permanent`. A bad body is answered `400`, saying what is
|
as `1h` or `7d`, or `permanent`. A bad body is answered `400`, saying what is
|
||||||
wrong:
|
wrong:
|
||||||
- one that is not such an object, or has another field;
|
- one that is not such an object, or has another field;
|
||||||
@@ -38,7 +38,7 @@ in [`SPEC.md`](../SPEC.md)).
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
With `SWW_ADMIN_TOKEN` set, the token in `$TOKEN`, and `discriminator` reached
|
With `SWWAF_ADMIN_TOKEN` set, the token in `$TOKEN`, and `discriminator` reached
|
||||||
as `discriminator:8080` on its network:
|
as `discriminator:8080` on its network:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
+3
-3
@@ -77,7 +77,7 @@ on one line:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `instance`: `SWW_INSTANCE_NAME`. `time`: when the alert was raised. `event`:
|
- `instance`: `SWWAF_INSTANCE_NAME`. `time`: when the alert was raised. `event`:
|
||||||
one of the events above.
|
one of the events above.
|
||||||
- `host`: the site of the request that raised the alert, since one
|
- `host`: the site of the request that raised the alert, since one
|
||||||
`discriminator` serves many sites. It is left out of `source_failure`,
|
`discriminator` serves many sites. It is left out of `source_failure`,
|
||||||
@@ -122,8 +122,8 @@ Slack gets the title in bold and the text below it, with `&`, `<` and `>`
|
|||||||
escaped. ntfy gets the text as the message, with these headers:
|
escaped. ntfy gets the text as the message, with these headers:
|
||||||
|
|
||||||
- `Title`: the title;
|
- `Title`: the title;
|
||||||
- `Authorization`: the token, as `Bearer <token>`, while `SWW_ALERT_NTFY_TOKEN`
|
- `Authorization`: the token, as `Bearer <token>`, while
|
||||||
is set;
|
`SWWAF_ALERT_NTFY_TOKEN` is set;
|
||||||
- `Priority` and `Tags`: by event, as listed below.
|
- `Priority` and `Tags`: by event, as listed below.
|
||||||
|
|
||||||
The priority and tag by event:
|
The priority and tag by event:
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ keeps the `instance` label as `exported_instance` unless the scrape sets
|
|||||||
|
|
||||||
## Countries and AS numbers
|
## Countries and AS numbers
|
||||||
|
|
||||||
For the `SWW_METRICS_TOP_N` busiest of each, the rest as `other`:
|
For the `SWWAF_METRICS_TOP_N` busiest of each, the rest as `other`:
|
||||||
|
|
||||||
- `sww_country_requests_total`, `sww_country_request_bytes_total`,
|
- `sww_country_requests_total`, `sww_country_request_bytes_total`,
|
||||||
`sww_country_response_bytes_total` and `sww_country_list_refusals_total`, by
|
`sww_country_response_bytes_total` and `sww_country_list_refusals_total`, by
|
||||||
|
|||||||
+5
-5
@@ -44,7 +44,7 @@ Every one of these lines has:
|
|||||||
- `time`: when `discriminator` had read the question's body, RFC 3339 with
|
- `time`: when `discriminator` had read the question's body, RFC 3339 with
|
||||||
milliseconds, in UTC. The `report` line gives the same time as the `decision`
|
milliseconds, in UTC. The `report` line gives the same time as the `decision`
|
||||||
line.
|
line.
|
||||||
- `instance`: `SWW_INSTANCE_NAME`.
|
- `instance`: `SWWAF_INSTANCE_NAME`.
|
||||||
- `client_ip`: the visitor's address, as `rproxy` worked it out.
|
- `client_ip`: the visitor's address, as `rproxy` worked it out.
|
||||||
- `method`, `path` and `query`: the visitor's request.
|
- `method`, `path` and `query`: the visitor's request.
|
||||||
- `scheme`: how the visitor reached `rproxy` or the trusted proxy in front of
|
- `scheme`: how the visitor reached `rproxy` or the trusted proxy in front of
|
||||||
@@ -82,8 +82,8 @@ Every one of these lines has:
|
|||||||
- GeoJS had not answered by the time `discriminator` answered.
|
- GeoJS had not answered by the time `discriminator` answered.
|
||||||
- `content_type`: the visitor's `Content-Type`.
|
- `content_type`: the visitor's `Content-Type`.
|
||||||
- `content_length`: the body length the visitor announced.
|
- `content_length`: the body length the visitor announced.
|
||||||
- `request_headers`: the headers listed in `SWW_LOG_REQUEST_HEADERS`, by name in
|
- `request_headers`: the headers listed in `SWWAF_LOG_REQUEST_HEADERS`, by name
|
||||||
lower case.
|
in lower case.
|
||||||
- `has_authorization` and `has_cookie`: true when the request had those headers.
|
- `has_authorization` and `has_cookie`: true when the request had those headers.
|
||||||
Their values are never logged.
|
Their values are never logged.
|
||||||
|
|
||||||
@@ -96,8 +96,8 @@ Every one of these lines has:
|
|||||||
|
|
||||||
- `action`: what was done:
|
- `action`: what was done:
|
||||||
- `forward`: answered `allow`, so that `rproxy` passes it to the app;
|
- `forward`: answered `allow`, so that `rproxy` passes it to the app;
|
||||||
- `denied`: refused for `SWW_DENY_NETS`, or for a reputation source with the
|
- `denied`: refused for `SWWAF_DENY_NETS`, or for a reputation source with
|
||||||
action `deny`;
|
the action `deny`;
|
||||||
- `banned`: refused under a ban, or for a `ban` rule, a trap path or a
|
- `banned`: refused under a ban, or for a `ban` rule, a trap path or a
|
||||||
CrowdSec decision, each of which bans the client;
|
CrowdSec decision, each of which bans the client;
|
||||||
- `country_denied`, `rate_limited`, `rule_blocked` and `waf_blocked`:
|
- `country_denied`, `rate_limited`, `rule_blocked` and `waf_blocked`:
|
||||||
|
|||||||
+146
-153
@@ -4,9 +4,9 @@ Every setting, its default and the values it takes, in the groups of "Settings"
|
|||||||
in [`SPEC.md`](../SPEC.md). That section gives the conventions, the settings
|
in [`SPEC.md`](../SPEC.md). That section gives the conventions, the settings
|
||||||
that take `off`, the `_FILE` form, and what each group does and why.
|
that take `off`, the `_FILE` form, and what each group does and why.
|
||||||
|
|
||||||
The settings that start with `SWW_` are `discriminator`'s. Those that start with
|
The settings that start with `SWWAF_` are `discriminator`'s. Those that start
|
||||||
`SWRP_` are `rproxy`'s, and are under "`rproxy`" below. `SWW_COMMAND` chooses
|
with `SWRP_` are `rproxy`'s, and are under "`rproxy`" below. `SWW_COMMAND`
|
||||||
between the two (see "The image's command" below).
|
chooses between the two (see "The image's command" below).
|
||||||
|
|
||||||
## Forms of values
|
## Forms of values
|
||||||
|
|
||||||
@@ -28,250 +28,243 @@ between the two (see "The image's command" below).
|
|||||||
|
|
||||||
## Core
|
## Core
|
||||||
|
|
||||||
- `SWW_LISTEN_ADDR` (default `:8080`): where `discriminator` listens: `:` and a
|
- `SWWAF_LISTEN_ADDR` (default `:8080`): where `discriminator` listens: `:` and
|
||||||
port, such as `:9000`. Leave the host out (see "Ports" in
|
a port, such as `:9000`. Leave the host out (see "Ports" in
|
||||||
[`SPEC.md`](../SPEC.md)).
|
[`SPEC.md`](../SPEC.md)).
|
||||||
- `SWW_INSTANCE_NAME` (default: the container's host name): the `instance` name
|
- `SWWAF_INSTANCE_NAME` (default: the container's host name): the `instance`
|
||||||
in each line `discriminator` logs, and in each alert and metric, such as
|
name in each line `discriminator` logs, and in each alert and metric, such as
|
||||||
`fsn1/discriminator`. Valid UTF-8. No control character while
|
`fsn1/discriminator`. Valid UTF-8. No control character while
|
||||||
`SWW_ALERT_NTFY_URL` is set. 1 to 48 printable ASCII characters without a
|
`SWWAF_ALERT_NTFY_URL` is set. 1 to 48 printable ASCII characters without a
|
||||||
space while `SWW_LOG_REMOTE_URL` is set and `SWW_LOG_REMOTE_APP_NAME` is not.
|
space while `SWWAF_LOG_REMOTE_URL` is set and `SWWAF_LOG_REMOTE_APP_NAME` is
|
||||||
- `SWW_MODE` (default `enforce`): `enforce` or `observe`.
|
not.
|
||||||
- `SWW_IPV6_GROUP_PREFIX` (default `64`): the prefix length of an IPv6 client's
|
- `SWWAF_MODE` (default `enforce`): `enforce` or `observe`.
|
||||||
group, from 32 to 128.
|
- `SWWAF_IPV6_GROUP_PREFIX` (default `64`): the prefix length of an IPv6
|
||||||
- `SWW_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a whole
|
client's group, from 32 to 128.
|
||||||
|
- `SWWAF_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a whole
|
||||||
number above zero.
|
number above zero.
|
||||||
- `SWW_MAX_BANS` (default `5000`): how many of its own bans `discriminator`
|
- `SWWAF_MAX_BANS` (default `5000`): how many of its own bans `discriminator`
|
||||||
keeps, a whole number above zero.
|
keeps, a whole number above zero.
|
||||||
|
|
||||||
## Tokens
|
## Tokens
|
||||||
|
|
||||||
- `SWW_ADMIN_TOKEN` (default unset): the token for the ban endpoints and
|
- `SWWAF_ADMIN_TOKEN` (default unset): the token for the ban endpoints and
|
||||||
`/_sww/clients/<ip>`. At least 32 characters.
|
`/_sww/clients/<ip>`. At least 32 characters.
|
||||||
- `SWW_METRICS_TOKEN` (default unset): the token for `/_sww/metrics`. At least
|
- `SWWAF_METRICS_TOKEN` (default unset): the token for `/_sww/metrics`. At least
|
||||||
32 characters.
|
32 characters.
|
||||||
|
|
||||||
## State files
|
## State files
|
||||||
|
|
||||||
- `SWW_STATE_DIR` (default `/var/lib/sww`): the directory of the state files, an
|
- `SWWAF_STATE_DIR` (default `/var/lib/sww`): the directory of the state files,
|
||||||
absolute path that exists and can be written.
|
an absolute path that exists and can be written.
|
||||||
- `SWW_STATE_WRITE_DELAY` (default `10s`): how long `bans.json` waits to be
|
- `SWWAF_STATE_WRITE_DELAY` (default `10s`): how long `bans.json` waits to be
|
||||||
written after a ban is made, lifted or made permanent. A duration.
|
written after a ban is made, lifted or made permanent. A duration.
|
||||||
- `SWW_STATE_COUNTER_INTERVAL` (default `15m`): how often every state file is
|
- `SWWAF_STATE_COUNTER_INTERVAL` (default `15m`): how often every state file is
|
||||||
written. A duration.
|
written. A duration.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
- `SWW_LOG_LEVEL` (default `info`): the least severe of `discriminator`'s own
|
- `SWWAF_LOG_LEVEL` (default `info`): the least severe of `discriminator`'s own
|
||||||
messages that are written: `debug`, `info`, `warn` or `error`.
|
messages that are written: `debug`, `info`, `warn` or `error`.
|
||||||
- `SWW_LOG_REQUEST_HEADERS` (default
|
- `SWWAF_LOG_REQUEST_HEADERS` (default
|
||||||
`accept,accept-language,accept-encoding,content-type,origin,range`): the
|
`accept,accept-language,accept-encoding,content-type,origin,range`): the
|
||||||
request headers recorded in the log line, in either case. `Host` and
|
request headers recorded in the log line, in either case. `Host` and
|
||||||
`Transfer-Encoding` are refused; the host is the field `host`.
|
`Transfer-Encoding` are refused; the host is the field `host`.
|
||||||
- `SWW_LOG_REMOTE_URL` (default unset): a syslog server that also gets every
|
- `SWWAF_LOG_REMOTE_URL` (default unset): a syslog server that also gets every
|
||||||
line written to stdout: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://`,
|
line written to stdout: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://`,
|
||||||
each with a host and a port and nothing more.
|
each with a host and a port and nothing more.
|
||||||
- `SWW_LOG_REMOTE_TLS_CA_FILE` (default unset): a file of PEM certificates that
|
- `SWWAF_LOG_REMOTE_TLS_CA_FILE` (default unset): a file of PEM certificates
|
||||||
a `syslog+tls` server's certificate must chain to, instead of the host's own.
|
that a `syslog+tls` server's certificate must chain to, instead of the host's
|
||||||
A file that cannot be read, or holds no certificate, is refused.
|
own. A file that cannot be read, or holds no certificate, is refused.
|
||||||
- `SWW_LOG_REMOTE_BUFFER` (default `10000`): the most lines held while they wait
|
- `SWWAF_LOG_REMOTE_BUFFER` (default `10000`): the most lines held while they
|
||||||
to be sent, a whole number above zero.
|
wait to be sent, a whole number above zero.
|
||||||
- `SWW_LOG_REMOTE_FACILITY` (default `local0`): the syslog facility: `kern`,
|
- `SWWAF_LOG_REMOTE_FACILITY` (default `local0`): the syslog facility: `kern`,
|
||||||
`user`, `mail`, `daemon`, `auth`, `syslog`, `lpr`, `news`, `uucp`, `cron`,
|
`user`, `mail`, `daemon`, `auth`, `syslog`, `lpr`, `news`, `uucp`, `cron`,
|
||||||
`authpriv`, `ftp`, or `local0` to `local7`.
|
`authpriv`, `ftp`, or `local0` to `local7`.
|
||||||
- `SWW_LOG_REMOTE_APP_NAME` (default: `SWW_INSTANCE_NAME`): the app name in each
|
- `SWWAF_LOG_REMOTE_APP_NAME` (default: `SWWAF_INSTANCE_NAME`): the app name in
|
||||||
syslog record, 1 to 48 printable ASCII characters without a space.
|
each syslog record, 1 to 48 printable ASCII characters without a space.
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
||||||
- `SWW_METRICS_TOP_N` (default `50`): how many AS numbers and how many countries
|
- `SWWAF_METRICS_TOP_N` (default `50`): how many AS numbers and how many
|
||||||
get series of their own. A whole number above zero.
|
countries get series of their own. A whole number above zero.
|
||||||
|
|
||||||
## Static lists
|
## Static lists
|
||||||
|
|
||||||
- `SWW_ALLOW_NETS` (default empty): netblocks you trust, such as your monitoring
|
- `SWWAF_ALLOW_NETS` (default empty): netblocks you trust, such as your
|
||||||
(see step 3 in [`SPEC.md`](../SPEC.md)).
|
monitoring (see step 3 in [`SPEC.md`](../SPEC.md)).
|
||||||
- `SWW_RATE_LIMIT_EXEMPT_NETS` (default empty): netblocks whose clients are
|
- `SWWAF_RATE_LIMIT_EXEMPT_NETS` (default empty): netblocks whose clients are
|
||||||
exempt from rate limiting and the byte limits.
|
exempt from rate limiting and the byte limits.
|
||||||
- `SWW_DENY_NETS` (default empty): netblocks whose clients are refused (see step
|
- `SWWAF_DENY_NETS` (default empty): netblocks whose clients are refused (see
|
||||||
3 in [`SPEC.md`](../SPEC.md)).
|
step 3 in [`SPEC.md`](../SPEC.md)).
|
||||||
|
|
||||||
## Request rate limits, per client
|
## Request rate limits, per client
|
||||||
|
|
||||||
- `SWW_RATE_LIMIT_PER_MINUTE` (default `120`, 2 a second): the most requests a
|
- `SWWAF_RATE_LIMIT_PER_MINUTE` (default `120`, 2 a second): the most requests a
|
||||||
client may make in a minute. A count or `off`. The default leaves room for a
|
client may make in a minute. A count or `off`. The default leaves room for a
|
||||||
person opening two pages at once with an empty cache, up to 60 files each.
|
person opening two pages at once with an empty cache, up to 60 files each.
|
||||||
- `SWW_RATE_LIMIT_PER_HOUR` (default `1500`): the most requests a client may
|
- `SWWAF_RATE_LIMIT_PER_HOUR` (default `1500`): the most requests a client may
|
||||||
make in an hour. A count or `off`. It is the main limit against scrapers. A
|
make in an hour. A count or `off`. It is the main limit against scrapers. A
|
||||||
person reading a site for an hour, a page every 20 seconds, makes about 900. A
|
person reading a site for an hour, a page every 20 seconds, makes about 900. A
|
||||||
scraper making one request every 2 seconds makes 1,800 an hour, more than the
|
scraper making one request every 2 seconds makes 1,800 an hour, more than the
|
||||||
limit.
|
limit.
|
||||||
- `SWW_RATE_LIMIT_PER_DAY` (default `6000`): the most requests a client may make
|
- `SWWAF_RATE_LIMIT_PER_DAY` (default `6000`): the most requests a client may
|
||||||
in a day. A count or `off`. The default is four hours at the hour limit. A
|
make in a day. A count or `off`. The default is four hours at the hour limit.
|
||||||
scraper making one request every 10 seconds makes 8,640 a day, more than the
|
A scraper making one request every 10 seconds makes 8,640 a day, more than the
|
||||||
limit.
|
limit.
|
||||||
- A CI runner asks its code forge for work all day and can pass the limits. Put
|
- A CI runner asks its code forge for work all day and can pass the limits. Put
|
||||||
the runners' addresses in `SWW_RATE_LIMIT_EXEMPT_NETS`. Do the same for a
|
the runners' addresses in `SWWAF_RATE_LIMIT_EXEMPT_NETS`. Do the same for a
|
||||||
network where many people share one address, such as an office.
|
network where many people share one address, such as an office.
|
||||||
- Counting cannot catch a client that stays under all three limits (see "Request
|
- Counting cannot catch a client that stays under all three limits (see "Request
|
||||||
rate limits, per client" in [`SPEC.md`](../SPEC.md)).
|
rate limits, per client" in [`SPEC.md`](../SPEC.md)).
|
||||||
- `SWW_RATE_LIMIT_EXEMPT_PATHS` (default empty): path prefixes exempt from rate
|
- `SWWAF_RATE_LIMIT_EXEMPT_PATHS` (default empty): path prefixes exempt from
|
||||||
limiting and the byte limits, such as `/assets/`.
|
rate limiting and the byte limits, such as `/assets/`.
|
||||||
|
|
||||||
## Byte limits, per client
|
## Byte limits, per client
|
||||||
|
|
||||||
- `SWW_BYTES_LIMIT_PER_MINUTE` (default `off`), `SWW_BYTES_LIMIT_PER_HOUR`
|
- `SWWAF_BYTES_LIMIT_PER_MINUTE` (default `off`), `SWWAF_BYTES_LIMIT_PER_HOUR`
|
||||||
(default `50G`) and `SWW_BYTES_LIMIT_PER_DAY` (default `100G`): the most bytes
|
(default `50G`) and `SWWAF_BYTES_LIMIT_PER_DAY` (default `100G`): the most
|
||||||
counted for a client in each window. A size or `off`.
|
bytes counted for a client in each window. A size or `off`.
|
||||||
- `SWW_BYTES_COUNT` (default `both`): which body bytes the byte limits count:
|
- `SWWAF_BYTES_COUNT` (default `both`): which body bytes the byte limits count:
|
||||||
`response`, `request` or `both`.
|
`response`, `request` or `both`.
|
||||||
|
|
||||||
## Lookup of AS number and country
|
## Lookup of AS number and country
|
||||||
|
|
||||||
- `SWW_LOOKUP_SOURCE` (default `geojs`): `geojs`, `file` or `off`. `off` stops
|
- `SWWAF_LOOKUP_SOURCE` (default `geojs`): `geojs`, `file` or `off`. `off` stops
|
||||||
the start while a setting that needs a lookup is set:
|
the start while a setting that needs a lookup is set:
|
||||||
- the two country lists;
|
- the two country lists;
|
||||||
- `SWW_ADD_LOOKUP_HEADERS`;
|
- `SWWAF_ADD_LOOKUP_HEADERS`;
|
||||||
- the four biased threshold lists and `SWW_ASN_LIMIT_PERCENT_URL`;
|
- the four biased threshold lists and `SWWAF_ASN_LIMIT_PERCENT_URL`;
|
||||||
- `SWW_UNKNOWN_LIMIT_PERCENT` below `100`;
|
- `SWWAF_UNKNOWN_LIMIT_PERCENT` below `100`;
|
||||||
- an anomaly threshold per AS number.
|
- an anomaly threshold per AS number.
|
||||||
- `SWW_LOOKUP_DB_PATH` (default empty): the IPinfo Lite database file, in its
|
- `SWWAF_LOOKUP_DB_PATH` (default empty): the IPinfo Lite database file, in its
|
||||||
`.mmdb` form, for `SWW_LOOKUP_SOURCE=file`.
|
`.mmdb` form, for `SWWAF_LOOKUP_SOURCE=file`.
|
||||||
- `SWW_LOOKUP_TIMEOUT` (default `800ms`): how long a request waits for its
|
- `SWWAF_LOOKUP_TIMEOUT` (default `800ms`): how long a request waits for its
|
||||||
client's first answer from GeoJS, and how long a request to GeoJS may take. A
|
client's first answer from GeoJS, and how long a request to GeoJS may take. A
|
||||||
duration; never `off`.
|
duration; never `off`.
|
||||||
- `SWW_ADD_LOOKUP_HEADERS` (default `false`): `true` has `rproxy` pass the app
|
- `SWWAF_ADD_LOOKUP_HEADERS` (default `false`): `true` has `rproxy` pass the app
|
||||||
the client's AS number in `X-Client-ASN` and its country in
|
the client's AS number in `X-Client-ASN` and its country in
|
||||||
`X-Client-Country`.
|
`X-Client-Country`.
|
||||||
|
|
||||||
## Country lists
|
## Country lists
|
||||||
|
|
||||||
- `SWW_DENIED_COUNTRIES` (default empty): countries whose clients are refused,
|
- `SWWAF_DENIED_COUNTRIES` (default empty): countries whose clients are refused,
|
||||||
such as `cn,ru,kp`.
|
such as `cn,ru,kp`.
|
||||||
- `SWW_EXCLUSIVELY_ALLOWED_COUNTRIES` (default empty): when set, the only
|
- `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` (default empty): when set, the only
|
||||||
countries whose clients get through, such as `us,de`. A country on both lists
|
countries whose clients get through, such as `us,de`. A country on both lists
|
||||||
stops the start.
|
stops the start.
|
||||||
|
|
||||||
## Biased thresholds
|
## Biased thresholds
|
||||||
|
|
||||||
- `SWW_ASN_LIMIT_PERCENT` (default empty): a list of AS numbers, each with `:`
|
- `SWWAF_ASN_LIMIT_PERCENT` (default empty): a list of AS numbers, each with `:`
|
||||||
and a percentage, such as `AS14061:50,AS16276:50,AS45102:25`. An AS number
|
and a percentage, such as `AS14061:50,AS16276:50,AS45102:25`. An AS number
|
||||||
listed twice is refused.
|
listed twice is refused.
|
||||||
- `SWW_COUNTRY_LIMIT_PERCENT` (default empty): the same by country, such as
|
- `SWWAF_COUNTRY_LIMIT_PERCENT` (default empty): the same by country, such as
|
||||||
`cn:25,ru:50`.
|
`cn:25,ru:50`.
|
||||||
- `SWW_ASN_BYTES_PERCENT` and `SWW_COUNTRY_BYTES_PERCENT` (default empty): the
|
- `SWWAF_ASN_BYTES_PERCENT` and `SWWAF_COUNTRY_BYTES_PERCENT` (default empty):
|
||||||
same, for the byte limits alone.
|
the same, for the byte limits alone.
|
||||||
- `SWW_UNKNOWN_LIMIT_PERCENT` (default `100`): the percentage for a client the
|
- `SWWAF_UNKNOWN_LIMIT_PERCENT` (default `100`): the percentage for a client the
|
||||||
lookup cannot place.
|
lookup cannot place.
|
||||||
- `SWW_ASN_LIMIT_PERCENT_URL` (default unset): the `http` or `https` URL of a
|
- `SWWAF_ASN_LIMIT_PERCENT_URL` (default unset): the `http` or `https` URL of a
|
||||||
text file with one AS number and percentage on each line, such as
|
text file with one AS number and percentage on each line, such as
|
||||||
`AS64496:50`. A URL that is also in `SWW_BLOCKLIST_URLS` is refused.
|
`AS64496:50`. A URL that is also in `SWWAF_BLOCKLIST_URLS` is refused.
|
||||||
|
|
||||||
## Attack detection
|
## Attack detection
|
||||||
|
|
||||||
- `SWW_RULES_DIR` (default `/etc/sww/rules.d`): the directory of the rule files.
|
- `SWWAF_RULES_DIR` (default `/etc/sww/rules.d`): the directory of the rule
|
||||||
- `SWW_RULES_ENABLED` (default `true`): `false` reads no rule file.
|
files.
|
||||||
- `SWW_TRAP_PATHS` (default empty): paths only scanners ask for, such as
|
- `SWWAF_RULES_ENABLED` (default `true`): `false` reads no rule file.
|
||||||
|
- `SWWAF_TRAP_PATHS` (default empty): paths only scanners ask for, such as
|
||||||
`/wp-login.php,/xmlrpc.php`. Each starts with `/` and holds no `?`.
|
`/wp-login.php,/xmlrpc.php`. Each starts with `/` and holds no `?`.
|
||||||
- `SWW_WAF_MODE` (default `block`): `off`, `detect` or `block`.
|
- `SWWAF_WAF_MODE` (default `block`): `off`, `detect` or `block`.
|
||||||
- `SWW_WAF_PARANOIA_LEVEL` (default `1`): the Core Rule Set's paranoia level,
|
- `SWWAF_WAF_PARANOIA_LEVEL` (default `1`): the Core Rule Set's paranoia level,
|
||||||
from 1 to 4.
|
from 1 to 4.
|
||||||
- `SWW_WAF_ANOMALY_THRESHOLD` (default `5`): the score at which a request is a
|
- `SWWAF_WAF_ANOMALY_THRESHOLD` (default `5`): the score at which a request is a
|
||||||
match. A whole number above zero, or `off`.
|
match. A whole number above zero, or `off`.
|
||||||
- `SWW_WAF_DISABLED_RULES` (default
|
- `SWWAF_WAF_DISABLED_RULES` (default
|
||||||
`920340,920420,920440,920640,930130,930140`): the ids of Core Rule Set rules
|
`920340,920420,920440,920640,930130,930140`): the ids of Core Rule Set rules
|
||||||
to switch off. Each a whole number above zero. An id from 900000 to 900999 is
|
to switch off. Each a whole number above zero. An id from 900000 to 900999 is
|
||||||
refused: those are the rules that set the Core Rule Set up, and
|
refused: those are the rules that set the Core Rule Set up, and
|
||||||
`smallwebwaf`'s own.
|
`smallwebwaf`'s own.
|
||||||
- `SWW_WAF_EXEMPT_PATHS` (default empty): path prefixes the Core Rule Set does
|
- `SWWAF_WAF_EXEMPT_PATHS` (default empty): path prefixes the Core Rule Set does
|
||||||
not inspect.
|
not inspect.
|
||||||
- `SWW_WAF_BODY_LIMIT` (default `off`): `off`, or how much of a request body the
|
- `SWWAF_WAF_BODY_LIMIT` (default `off`): `off`, or how much of a request body
|
||||||
Core Rule Set reads, a size of at most `64K`. A question carries no more of a
|
the Core Rule Set reads, a size of at most `64K`. A question carries no more
|
||||||
body than 64 KiB, and the Core Rule Set reads at most 64 KiB less one byte of
|
of a body than 64 KiB, and the Core Rule Set reads at most 64 KiB less one
|
||||||
it, whatever the value (see "The answer" in [`SPEC.md`](../SPEC.md)).
|
byte of it, whatever the value (see "The answer" in [`SPEC.md`](../SPEC.md)).
|
||||||
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` (default unset): a file of Core Rule Set
|
- `SWWAF_ERROR_BURST_THRESHOLD` (default `30`): the most refusals of one client
|
||||||
exclusions, written as Coraza directives. It is loaded before the Core Rule
|
in a minute (see "Attack detection" in `SPEC.md`). A count or `off`.
|
||||||
Set's rules, so it holds only `SecAction` and `SecRule` lines that exclude
|
|
||||||
with `ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. A file that cannot be
|
|
||||||
read or does not parse stops the start, whatever `SWW_WAF_MODE` is. So does a
|
|
||||||
file that names a number from 900000 to 900999 outside a comment, alone or in
|
|
||||||
a range. So does a directive that removes or changes rules as the file loads,
|
|
||||||
such as `SecRuleRemoveById`, since none of the Core Rule Set's rules is loaded
|
|
||||||
yet. It has no `_FILE` form.
|
|
||||||
- `SWW_ERROR_BURST_THRESHOLD` (default `30`): the most refusals of one client in
|
|
||||||
a minute (see "Attack detection" in `SPEC.md`). A count or `off`.
|
|
||||||
|
|
||||||
## Ban lengths, response and scope
|
## Ban lengths, response and scope
|
||||||
|
|
||||||
- `SWW_BAN_RESPONSE` (default `403`): `403`, `429` or `close`.
|
- `SWWAF_BAN_RESPONSE` (default `403`): `403`, `429` or `close`.
|
||||||
- `SWW_LIMIT_BAN_DURATION` (default `1h`): the ban for a first broken limit. A
|
- `SWWAF_LIMIT_BAN_DURATION` (default `1h`): the ban for a first broken limit. A
|
||||||
duration.
|
duration.
|
||||||
- `SWW_LIMIT_BAN_REPEAT_WINDOW` (default `24h`): the time after a ban ends
|
- `SWWAF_LIMIT_BAN_REPEAT_WINDOW` (default `24h`): the time after a ban ends
|
||||||
within which breaking a limit again lengthens the next ban. A duration.
|
within which breaking a limit again lengthens the next ban. A duration.
|
||||||
- `SWW_MAX_BAN_DURATION` (default `7d`): the longest ban for a broken limit; one
|
- `SWWAF_MAX_BAN_DURATION` (default `7d`): the longest ban for a broken limit;
|
||||||
that would be longer is permanent. A duration.
|
one that would be longer is permanent. A duration.
|
||||||
- `SWW_ATTACK_BAN_DURATION` (default `7d`): the ban for a first clear sign of
|
- `SWWAF_ATTACK_BAN_DURATION` (default `7d`): the ban for a first clear sign of
|
||||||
attack. A duration.
|
attack. A duration.
|
||||||
- `SWW_BAN_SCOPE_V4_PREFIX` (default `32`): how much a ban on an IPv4 client
|
- `SWWAF_BAN_SCOPE_V4_PREFIX` (default `32`): how much a ban on an IPv4 client
|
||||||
covers: the prefix length of its netblock, from 0 to 32.
|
covers: the prefix length of its netblock, from 0 to 32.
|
||||||
|
|
||||||
## Reputation
|
## Reputation
|
||||||
|
|
||||||
- `SWW_BLOCKLIST_URLS` (default empty): blocklists to fetch, such as
|
- `SWWAF_BLOCKLIST_URLS` (default empty): blocklists to fetch, such as
|
||||||
`https://www.spamhaus.org/drop/drop.txt`. Each an `http` or `https` URL
|
`https://www.spamhaus.org/drop/drop.txt`. Each an `http` or `https` URL
|
||||||
without a user or a fragment. A URL listed twice is refused.
|
without a user or a fragment. A URL listed twice is refused.
|
||||||
- `SWW_BLOCKLIST_REFRESH` (default `24h`): how long a list waits after its last
|
- `SWWAF_BLOCKLIST_REFRESH` (default `24h`): how long a list waits after its
|
||||||
try before it is fetched again. A duration of `1h` or more.
|
last try before it is fetched again. A duration of `1h` or more.
|
||||||
- `SWW_BLOCKLIST_ACTION` (default `deny`): `deny`, `limit:<percent>` or `log`.
|
- `SWWAF_BLOCKLIST_ACTION` (default `deny`): `deny`, `limit:<percent>` or `log`.
|
||||||
- `SWW_DNSBL_ZONES` (default empty): the DNS blocklists to ask, such as
|
- `SWWAF_DNSBL_ZONES` (default empty): the DNS blocklists to ask, such as
|
||||||
`dnsbl.dronebl.org`. Each a DNS name of at most 189 characters. A Spamhaus
|
`dnsbl.dronebl.org`. Each a DNS name of at most 189 characters. A Spamhaus
|
||||||
zone carries its key, as in `<key>.xbl.dq.spamhaus.net`. A zone listed twice
|
zone carries its key, as in `<key>.xbl.dq.spamhaus.net`. A zone listed twice
|
||||||
is refused, whatever its case or key.
|
is refused, whatever its case or key.
|
||||||
- `SWW_DNSBL_RESOLVER` (default unset): the resolver to ask the zones through,
|
- `SWWAF_DNSBL_RESOLVER` (default unset): the resolver to ask the zones through,
|
||||||
such as `192.0.2.53` or `[2001:db8::53]:5353`. An IP address with an optional
|
such as `192.0.2.53` or `[2001:db8::53]:5353`. An IP address with an optional
|
||||||
port, 53 when none is given. Unset, the host's resolver is used.
|
port, 53 when none is given. Unset, the host's resolver is used.
|
||||||
- `SWW_ABUSEIPDB_KEY` (default unset): the key of your AbuseIPDB account. A
|
- `SWWAF_ABUSEIPDB_KEY` (default unset): the key of your AbuseIPDB account. A
|
||||||
control character in it is refused.
|
control character in it is refused.
|
||||||
- `SWW_ABUSEIPDB_MIN_SCORE` (default `75`): the least abuse confidence score,
|
- `SWWAF_ABUSEIPDB_MIN_SCORE` (default `75`): the least abuse confidence score,
|
||||||
from 0 to 100, that is a hit.
|
from 0 to 100, that is a hit.
|
||||||
- `SWW_ABUSEIPDB_DAILY_BUDGET` (default `900`): the most checks made in a day, a
|
- `SWWAF_ABUSEIPDB_DAILY_BUDGET` (default `900`): the most checks made in a day,
|
||||||
whole number above zero. AbuseIPDB's free accounts may make 1,000.
|
a whole number above zero. AbuseIPDB's free accounts may make 1,000.
|
||||||
- `SWW_CROWDSEC_LAPI_URL` (default unset): the local API of a CrowdSec engine
|
- `SWWAF_CROWDSEC_LAPI_URL` (default unset): the local API of a CrowdSec engine
|
||||||
you run, such as `http://172.17.0.1:8080`. An `http` or `https` URL without a
|
you run, such as `http://172.17.0.1:8080`. An `http` or `https` URL without a
|
||||||
user or a fragment; its decision list is at `v1/decisions` under it. Set
|
user or a fragment; its decision list is at `v1/decisions` under it. Set
|
||||||
without the key, it is refused. So is a decision list URL that is also in
|
without the key, it is refused. So is a decision list URL that is also in
|
||||||
`SWW_BLOCKLIST_URLS`, or that is `SWW_ASN_LIMIT_PERCENT_URL`.
|
`SWWAF_BLOCKLIST_URLS`, or that is `SWWAF_ASN_LIMIT_PERCENT_URL`.
|
||||||
- `SWW_CROWDSEC_LAPI_KEY` (default unset): the key the engine gave for
|
- `SWWAF_CROWDSEC_LAPI_KEY` (default unset): the key the engine gave for
|
||||||
`smallwebwaf`, as `cscli bouncers add smallwebwaf` prints it. Set without the
|
`smallwebwaf`, as `cscli bouncers add smallwebwaf` prints it. Set without the
|
||||||
URL, it is refused.
|
URL, it is refused.
|
||||||
- `SWW_REPUTATION_ACTION` (default `limit:25`): `deny`, `limit:<percent>` or
|
- `SWWAF_REPUTATION_ACTION` (default `limit:25`): `deny`, `limit:<percent>` or
|
||||||
`log`, for DNS blocklist and AbuseIPDB hits.
|
`log`, for DNS blocklist and AbuseIPDB hits.
|
||||||
- `SWW_REPUTATION_CACHE_TTL` (default `24h`): how long a zone's verdict or
|
- `SWWAF_REPUTATION_CACHE_TTL` (default `24h`): how long a zone's verdict or
|
||||||
AbuseIPDB's score is used. A duration.
|
AbuseIPDB's score is used. A duration.
|
||||||
- `SWW_REPUTATION_TIMEOUT` (default `2s`): how long a query to a zone or a check
|
- `SWWAF_REPUTATION_TIMEOUT` (default `2s`): how long a query to a zone or a
|
||||||
with AbuseIPDB may take. A duration.
|
check with AbuseIPDB may take. A duration.
|
||||||
|
|
||||||
## Alerting
|
## Alerting
|
||||||
|
|
||||||
- `SWW_ALERT_WEBHOOK_URL` (default unset): the webhook that receives each alert
|
- `SWWAF_ALERT_WEBHOOK_URL` (default unset): the webhook that receives each
|
||||||
as a JSON object. An `http` or `https` URL without a user or a fragment.
|
alert as a JSON object. An `http` or `https` URL without a user or a fragment.
|
||||||
- `SWW_ALERT_WEBHOOK_HEADERS` (default empty): headers sent with each post, such
|
- `SWWAF_ALERT_WEBHOOK_HEADERS` (default empty): headers sent with each post,
|
||||||
as `Authorization:Bearer 0123456789abcdef`. A list of name, `:` and value. A
|
such as `Authorization:Bearer 0123456789abcdef`. A list of name, `:` and
|
||||||
value cannot hold a comma.
|
value. A value cannot hold a comma.
|
||||||
- `SWW_ALERT_SLACK_WEBHOOK_URL` (default unset): the Slack incoming webhook that
|
- `SWWAF_ALERT_SLACK_WEBHOOK_URL` (default unset): the Slack incoming webhook
|
||||||
receives each alert as a message, checked as the webhook URL is.
|
that receives each alert as a message, checked as the webhook URL is.
|
||||||
- `SWW_ALERT_NTFY_URL` (default unset): the ntfy topic that receives each alert,
|
- `SWWAF_ALERT_NTFY_URL` (default unset): the ntfy topic that receives each
|
||||||
as the topic's full URL, such as `https://ntfy.sh/my-alerts`, checked as the
|
alert, as the topic's full URL, such as `https://ntfy.sh/my-alerts`, checked
|
||||||
webhook URL is.
|
as the webhook URL is.
|
||||||
- `SWW_ALERT_NTFY_TOKEN` (default unset): an ntfy access token. A control
|
- `SWWAF_ALERT_NTFY_TOKEN` (default unset): an ntfy access token. A control
|
||||||
character in it is refused.
|
character in it is refused.
|
||||||
- `SWW_ALERT_EVENTS` (default
|
- `SWWAF_ALERT_EVENTS` (default
|
||||||
`ban,permanent_ban,waf_block,anomaly,reputation_hit,source_failure,file_error`):
|
`ban,permanent_ban,waf_block,anomaly,reputation_hit,source_failure,file_error`):
|
||||||
the event types alerts are sent for, from those in [`alerts.md`](alerts.md)
|
the event types alerts are sent for, from those in [`alerts.md`](alerts.md)
|
||||||
but `summary`. Any other name is refused.
|
but `summary`. Any other name is refused.
|
||||||
- `SWW_ALERT_COOLDOWN` (default `15m`): how long after an alert is sent its
|
- `SWWAF_ALERT_COOLDOWN` (default `15m`): how long after an alert is sent its
|
||||||
repeats are held back. A duration or `off`.
|
repeats are held back. A duration or `off`.
|
||||||
- `SWW_ALERT_MAX_PER_HOUR` (default `60`): the most alerts sent in an hour. A
|
- `SWWAF_ALERT_MAX_PER_HOUR` (default `60`): the most alerts sent in an hour. A
|
||||||
whole number above zero, or `off`.
|
whole number above zero, or `off`.
|
||||||
|
|
||||||
## Anomaly thresholds
|
## Anomaly thresholds
|
||||||
@@ -280,25 +273,25 @@ Each scope has four thresholds: the most requests and the most bytes counted in
|
|||||||
a minute and in an hour. Requests are counts, bytes are sizes. Each is `off` by
|
a minute and in an hour. Requests are counts, bytes are sizes. Each is `off` by
|
||||||
default. The four thresholds per client are:
|
default. The four thresholds per client are:
|
||||||
|
|
||||||
- `SWW_ANOMALY_CLIENT_REQUESTS_PER_MINUTE`;
|
- `SWWAF_ANOMALY_CLIENT_REQUESTS_PER_MINUTE`;
|
||||||
- `SWW_ANOMALY_CLIENT_REQUESTS_PER_HOUR`;
|
- `SWWAF_ANOMALY_CLIENT_REQUESTS_PER_HOUR`;
|
||||||
- `SWW_ANOMALY_CLIENT_BYTES_PER_MINUTE`;
|
- `SWWAF_ANOMALY_CLIENT_BYTES_PER_MINUTE`;
|
||||||
- `SWW_ANOMALY_CLIENT_BYTES_PER_HOUR`.
|
- `SWWAF_ANOMALY_CLIENT_BYTES_PER_HOUR`.
|
||||||
|
|
||||||
The other scopes' names have another prefix before the same four endings, such
|
The other scopes' names have another prefix before the same four endings, such
|
||||||
as `REQUESTS_PER_MINUTE`:
|
as `REQUESTS_PER_MINUTE`:
|
||||||
|
|
||||||
- `SWW_ANOMALY_NET_...`: per netblock around a client. Two settings give the
|
- `SWWAF_ANOMALY_NET_...`: per netblock around a client. Two settings give the
|
||||||
netblock's length:
|
netblock's length:
|
||||||
- `SWW_ANOMALY_NET_V4_PREFIX` (default `24`, from 0 to 32) for an IPv4
|
- `SWWAF_ANOMALY_NET_V4_PREFIX` (default `24`, from 0 to 32) for an IPv4
|
||||||
client;
|
client;
|
||||||
- `SWW_ANOMALY_NET_V6_PREFIX` (default `48`, from 0 to 128) for an IPv6
|
- `SWWAF_ANOMALY_NET_V6_PREFIX` (default `48`, from 0 to 128) for an IPv6
|
||||||
client.
|
client.
|
||||||
- `SWW_ANOMALY_ASN_...`: per AS number.
|
- `SWWAF_ANOMALY_ASN_...`: per AS number.
|
||||||
- `SWW_ANOMALY_TOTAL_...`: the whole service.
|
- `SWWAF_ANOMALY_TOTAL_...`: the whole service.
|
||||||
- `SWW_WATCH_...`: each netblock in `SWW_WATCH_NETS`, as a whole.
|
- `SWWAF_WATCH_...`: each netblock in `SWWAF_WATCH_NETS`, as a whole.
|
||||||
`SWW_WATCH_NETS` (default empty) is a list of name, `=` and netblock, such as
|
`SWWAF_WATCH_NETS` (default empty) is a list of name, `=` and netblock, such
|
||||||
`office=203.0.113.0/24,scraper-x=198.51.100.0/22`. An item without `=` or a
|
as `office=203.0.113.0/24,scraper-x=198.51.100.0/22`. An item without `=` or a
|
||||||
name, or with a bad netblock, is refused. So is a name used twice.
|
name, or with a bad netblock, is refused. So is a name used twice.
|
||||||
|
|
||||||
## Settings given as files
|
## Settings given as files
|
||||||
@@ -319,7 +312,7 @@ Then mount its directory into the container read-only, and name the file:
|
|||||||
docker run \
|
docker run \
|
||||||
-v /srv/app/tokens:/etc/sww/tokens:ro \
|
-v /srv/app/tokens:/etc/sww/tokens:ro \
|
||||||
-e SWW_COMMAND=discriminator \
|
-e SWW_COMMAND=discriminator \
|
||||||
-e SWW_METRICS_TOKEN_FILE=/etc/sww/tokens/metrics \
|
-e SWWAF_METRICS_TOKEN_FILE=/etc/sww/tokens/metrics \
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -343,10 +336,10 @@ A few limits are fixed rather than settings:
|
|||||||
|
|
||||||
## `discriminator`'s answer and `rproxy`'s wait
|
## `discriminator`'s answer and `rproxy`'s wait
|
||||||
|
|
||||||
`SWW_LOOKUP_TIMEOUT` is the longest a question waits on an outside source. Keep
|
`SWWAF_LOOKUP_TIMEOUT` is the longest a question waits on an outside source.
|
||||||
it well under `rproxy`'s `SWRP_TIMEOUT_MS`, as the default is. No question waits
|
Keep it well under `rproxy`'s `SWRP_TIMEOUT_MS`, as the default is. No question
|
||||||
for `SWW_REPUTATION_TIMEOUT`, since the DNS blocklists and AbuseIPDB are asked
|
waits for `SWWAF_REPUTATION_TIMEOUT`, since the DNS blocklists and AbuseIPDB are
|
||||||
in the background (see "Answering in time" under "`discriminator`" in
|
asked in the background (see "Answering in time" under "`discriminator`" in
|
||||||
[`SPEC.md`](../SPEC.md)).
|
[`SPEC.md`](../SPEC.md)).
|
||||||
|
|
||||||
## `rproxy`
|
## `rproxy`
|
||||||
@@ -399,14 +392,14 @@ check checks: `rproxy` or `discriminator` (see "Deployment" in
|
|||||||
|
|
||||||
Nothing reads these names any more:
|
Nothing reads these names any more:
|
||||||
|
|
||||||
- `SWW_UPSTREAM_URL` is `SWRP_UPSTREAM_URL`.
|
- `SWWAF_UPSTREAM_URL` is `SWRP_UPSTREAM_URL`.
|
||||||
- `SWW_TRUSTED_PROXIES` is `SWRP_TRUSTED_PROXIES`.
|
- `SWWAF_TRUSTED_PROXIES` is `SWRP_TRUSTED_PROXIES`.
|
||||||
- `SWW_REQUEST_MAX_BYTES` is `SWRP_MAX_REQUEST_SIZE`.
|
- `SWWAF_REQUEST_MAX_BYTES` is `SWRP_MAX_REQUEST_SIZE`.
|
||||||
- `SWW_RESPONSE_MAX_BYTES` is `SWRP_MAX_RESPONSE_SIZE`.
|
- `SWWAF_RESPONSE_MAX_BYTES` is `SWRP_MAX_RESPONSE_SIZE`.
|
||||||
- `SWW_CLIENT_REQUEST_TIMEOUT` and `SWW_UPSTREAM_REQUEST_TIMEOUT` are
|
- `SWWAF_CLIENT_REQUEST_TIMEOUT` and `SWWAF_UPSTREAM_REQUEST_TIMEOUT` are
|
||||||
`SWRP_MAX_REQUEST_TIME_MS`, in milliseconds.
|
`SWRP_MAX_REQUEST_TIME_MS`, in milliseconds.
|
||||||
- `SWW_CLIENT_RESPONSE_TIMEOUT` and `SWW_UPSTREAM_RESPONSE_TIMEOUT` are
|
- `SWWAF_CLIENT_RESPONSE_TIMEOUT` and `SWWAF_UPSTREAM_RESPONSE_TIMEOUT` are
|
||||||
`SWRP_MAX_RESPONSE_TIME_MS`, in milliseconds.
|
`SWRP_MAX_RESPONSE_TIME_MS`, in milliseconds.
|
||||||
- `SWW_CLIENT_IDLE_TIMEOUT` and `SWW_CLIENT_REQUEST_HEADER_MAX_BYTES` have no
|
- `SWWAF_CLIENT_IDLE_TIMEOUT` and `SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES` have
|
||||||
setting. `rproxy` closes a kept-open connection idle for 120 seconds, and
|
no setting. `rproxy` closes a kept-open connection idle for 120 seconds, and
|
||||||
takes a request line and headers of at most 32 KiB.
|
takes a request line and headers of at most 32 KiB.
|
||||||
+7
-7
@@ -1,13 +1,13 @@
|
|||||||
# State files
|
# State files
|
||||||
|
|
||||||
The five JSON files in `SWW_STATE_DIR`, and what each holds. "Persistent state"
|
The five JSON files in `SWWAF_STATE_DIR`, and what each holds. "Persistent
|
||||||
in [`SPEC.md`](../SPEC.md) says how they are written, read and edited. Each has
|
state" in [`SPEC.md`](../SPEC.md) says how they are written, read and edited.
|
||||||
a top-level `version` of 1.
|
Each has a top-level `version` of 1.
|
||||||
|
|
||||||
## `bans.json`
|
## `bans.json`
|
||||||
|
|
||||||
Active and past bans: every ban an admin made, and up to `SWW_MAX_BANS` of those
|
Active and past bans: every ban an admin made, and up to `SWWAF_MAX_BANS` of
|
||||||
`discriminator` made. Per entry:
|
those `discriminator` made. Per entry:
|
||||||
|
|
||||||
- `netblock`, `start` and `expires` (`null` for a permanent ban);
|
- `netblock`, `start` and `expires` (`null` for a permanent ban);
|
||||||
- `cause`: `attack`, `limit`, `admin` or `crowdsec`;
|
- `cause`: `attack`, `limit`, `admin` or `crowdsec`;
|
||||||
@@ -93,10 +93,10 @@ GeoJS answers, one per line:
|
|||||||
## Sizes
|
## Sizes
|
||||||
|
|
||||||
- `clients.json`: about 1 KiB per client, about 20 MiB at the default
|
- `clients.json`: about 1 KiB per client, about 20 MiB at the default
|
||||||
`SWW_MAX_TRACKED_CLIENTS`. Written every 15 minutes, that is under 2 GiB of
|
`SWWAF_MAX_TRACKED_CLIENTS`. Written every 15 minutes, that is under 2 GiB of
|
||||||
disk writes a day.
|
disk writes a day.
|
||||||
- `bans.json`: about 1.2 KiB per ban, at most about 2.5 KiB, so about 6 MiB at
|
- `bans.json`: about 1.2 KiB per ban, at most about 2.5 KiB, so about 6 MiB at
|
||||||
the default `SWW_MAX_BANS`. Written every 15 minutes, a full file comes to
|
the default `SWWAF_MAX_BANS`. Written every 15 minutes, a full file comes to
|
||||||
about 600 MiB of writes a day, and a hundred new bans a day add about 600 MiB
|
about 600 MiB of writes a day, and a hundred new bans a day add about 600 MiB
|
||||||
more.
|
more.
|
||||||
- `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when
|
- `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when
|
||||||
|
|||||||
+30
-30
@@ -1,12 +1,12 @@
|
|||||||
// Package alerts sends alerts on bans, on traffic over an anomaly
|
// Package alerts sends alerts on bans, on traffic over an anomaly
|
||||||
// threshold, on a source that fails and on a file with an error to each
|
// threshold, on a source that fails and on a file with an error to each
|
||||||
// destination set: to the webhook
|
// destination set: to the webhook
|
||||||
// SWW_ALERT_WEBHOOK_URL names, each as one JSON object, as "The JSON
|
// SWWAF_ALERT_WEBHOOK_URL names, each as one JSON object, as "The JSON
|
||||||
// object" in docs/alerts.md describes, to the Slack incoming
|
// object" in docs/alerts.md describes, to the Slack incoming
|
||||||
// webhook SWW_ALERT_SLACK_WEBHOOK_URL names, as a message, and to the
|
// webhook SWWAF_ALERT_SLACK_WEBHOOK_URL names, as a message, and to the
|
||||||
// ntfy topic SWW_ALERT_NTFY_URL names. A repeat within
|
// ntfy topic SWWAF_ALERT_NTFY_URL names. A repeat within
|
||||||
// SWW_ALERT_COOLDOWN is held back, and so is an alert past
|
// SWWAF_ALERT_COOLDOWN is held back, and so is an alert past
|
||||||
// SWW_ALERT_MAX_PER_HOUR, for the hour's summary. The others wait in a
|
// SWWAF_ALERT_MAX_PER_HOUR, for the hour's summary. The others wait in a
|
||||||
// bounded queue of each destination's own, so that a destination that is
|
// bounded queue of each destination's own, so that a destination that is
|
||||||
// slow or unreachable holds up neither the others nor any request. The
|
// slow or unreachable holds up neither the others nor any request. The
|
||||||
// state is written to alerts.json and read from it by the state package.
|
// state is written to alerts.json and read from it by the state package.
|
||||||
@@ -34,7 +34,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The events an alert is for, as SWW_ALERT_EVENTS names them.
|
// The events an alert is for, as SWWAF_ALERT_EVENTS names them.
|
||||||
const (
|
const (
|
||||||
// EventBan is a ban sww made.
|
// EventBan is a ban sww made.
|
||||||
EventBan = "ban"
|
EventBan = "ban"
|
||||||
@@ -45,7 +45,7 @@ const (
|
|||||||
// threshold.
|
// threshold.
|
||||||
EventAnomaly = "anomaly"
|
EventAnomaly = "anomaly"
|
||||||
// EventWAFBlock is a request the Core Rule Set scored at or over
|
// EventWAFBlock is a request the Core Rule Set scored at or over
|
||||||
// SWW_WAF_ANOMALY_THRESHOLD, refused in block mode, let through in
|
// SWWAF_WAF_ANOMALY_THRESHOLD, refused in block mode, let through in
|
||||||
// detect mode.
|
// detect mode.
|
||||||
EventWAFBlock = "waf_block"
|
EventWAFBlock = "waf_block"
|
||||||
// EventReputationHit is a request whose client a blocklist, the
|
// EventReputationHit is a request whose client a blocklist, the
|
||||||
@@ -61,14 +61,14 @@ const (
|
|||||||
// cannot be read, or a state file that cannot be written.
|
// cannot be read, or a state file that cannot be written.
|
||||||
EventFileError = "file_error"
|
EventFileError = "file_error"
|
||||||
// EventSummary is the summary sent as an hour ends: of the alerts held
|
// EventSummary is the summary sent as an hour ends: of the alerts held
|
||||||
// back in it past SWW_ALERT_MAX_PER_HOUR, and of the repeats held
|
// back in it past SWWAF_ALERT_MAX_PER_HOUR, and of the repeats held
|
||||||
// back by the cooldowns dropped as it ends, which no alert let through
|
// back by the cooldowns dropped as it ends, which no alert let through
|
||||||
// has given. It is sent with SWW_ALERT_MAX_PER_HOUR off too, for
|
// has given. It is sent with SWWAF_ALERT_MAX_PER_HOUR off too, for
|
||||||
// those repeats. SWW_ALERT_EVENTS does not name it.
|
// those repeats. SWWAF_ALERT_EVENTS does not name it.
|
||||||
EventSummary = "summary"
|
EventSummary = "summary"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Events returns every event SWW_ALERT_EVENTS can name, which is its
|
// Events returns every event SWWAF_ALERT_EVENTS can name, which is its
|
||||||
// default.
|
// default.
|
||||||
func Events() []string {
|
func Events() []string {
|
||||||
return []string{
|
return []string{
|
||||||
@@ -80,12 +80,12 @@ func Events() []string {
|
|||||||
// The destinations alerts are sent to, as the metrics and alerts.json
|
// The destinations alerts are sent to, as the metrics and alerts.json
|
||||||
// name them.
|
// name them.
|
||||||
const (
|
const (
|
||||||
// DestinationWebhook is the webhook SWW_ALERT_WEBHOOK_URL names.
|
// DestinationWebhook is the webhook SWWAF_ALERT_WEBHOOK_URL names.
|
||||||
DestinationWebhook = "webhook"
|
DestinationWebhook = "webhook"
|
||||||
// DestinationSlack is the Slack incoming webhook
|
// DestinationSlack is the Slack incoming webhook
|
||||||
// SWW_ALERT_SLACK_WEBHOOK_URL names.
|
// SWWAF_ALERT_SLACK_WEBHOOK_URL names.
|
||||||
DestinationSlack = "slack"
|
DestinationSlack = "slack"
|
||||||
// DestinationNtfy is the ntfy topic SWW_ALERT_NTFY_URL names.
|
// DestinationNtfy is the ntfy topic SWWAF_ALERT_NTFY_URL names.
|
||||||
DestinationNtfy = "ntfy"
|
DestinationNtfy = "ntfy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,26 +121,26 @@ var (
|
|||||||
// NtfyURL set, no alert is sent.
|
// NtfyURL set, no alert is sent.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
// WebhookURL is where each alert is posted as JSON
|
// WebhookURL is where each alert is posted as JSON
|
||||||
// (SWW_ALERT_WEBHOOK_URL), nil while it is unset. WebhookHeaders
|
// (SWWAF_ALERT_WEBHOOK_URL), nil while it is unset. WebhookHeaders
|
||||||
// are sent with each (SWW_ALERT_WEBHOOK_HEADERS).
|
// are sent with each (SWWAF_ALERT_WEBHOOK_HEADERS).
|
||||||
WebhookURL *url.URL
|
WebhookURL *url.URL
|
||||||
WebhookHeaders http.Header
|
WebhookHeaders http.Header
|
||||||
// SlackURL is the Slack incoming webhook each alert is posted to as a
|
// SlackURL is the Slack incoming webhook each alert is posted to as a
|
||||||
// message (SWW_ALERT_SLACK_WEBHOOK_URL), nil while it is unset.
|
// message (SWWAF_ALERT_SLACK_WEBHOOK_URL), nil while it is unset.
|
||||||
SlackURL *url.URL
|
SlackURL *url.URL
|
||||||
// NtfyURL is the ntfy topic each alert is published to
|
// NtfyURL is the ntfy topic each alert is published to
|
||||||
// (SWW_ALERT_NTFY_URL), nil while it is unset. NtfyToken, unless
|
// (SWWAF_ALERT_NTFY_URL), nil while it is unset. NtfyToken, unless
|
||||||
// empty, is sent with each as a bearer token (SWW_ALERT_NTFY_TOKEN).
|
// empty, is sent with each as a bearer token (SWWAF_ALERT_NTFY_TOKEN).
|
||||||
NtfyURL *url.URL
|
NtfyURL *url.URL
|
||||||
NtfyToken string
|
NtfyToken string
|
||||||
// Events are the events alerts are sent for (SWW_ALERT_EVENTS).
|
// Events are the events alerts are sent for (SWWAF_ALERT_EVENTS).
|
||||||
Events []string
|
Events []string
|
||||||
// Cooldown is how long a repeat of an alert is held back
|
// Cooldown is how long a repeat of an alert is held back
|
||||||
// (SWW_ALERT_COOLDOWN), 0 for no time. MaxPerHour is the most alerts
|
// (SWWAF_ALERT_COOLDOWN), 0 for no time. MaxPerHour is the most alerts
|
||||||
// sent in an hour (SWW_ALERT_MAX_PER_HOUR), 0 for no limit.
|
// sent in an hour (SWWAF_ALERT_MAX_PER_HOUR), 0 for no limit.
|
||||||
Cooldown time.Duration
|
Cooldown time.Duration
|
||||||
MaxPerHour int
|
MaxPerHour int
|
||||||
// Instance is SWW_INSTANCE_NAME, which every alert gives.
|
// Instance is SWWAF_INSTANCE_NAME, which every alert gives.
|
||||||
Instance string
|
Instance string
|
||||||
// Now tells the time of an alert, normally time.Now in UTC.
|
// Now tells the time of an alert, normally time.Now in UTC.
|
||||||
Now func() time.Time
|
Now func() time.Time
|
||||||
@@ -301,17 +301,17 @@ func New(params Params) *Queue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if params.WebhookURL != nil {
|
if params.WebhookURL != nil {
|
||||||
q.addDestination(DestinationWebhook, "SWW_ALERT_WEBHOOK_URL",
|
q.addDestination(DestinationWebhook, "SWWAF_ALERT_WEBHOOK_URL",
|
||||||
params.WebhookURL, q.webhookMessage)
|
params.WebhookURL, q.webhookMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.SlackURL != nil {
|
if params.SlackURL != nil {
|
||||||
q.addDestination(DestinationSlack, "SWW_ALERT_SLACK_WEBHOOK_URL",
|
q.addDestination(DestinationSlack, "SWWAF_ALERT_SLACK_WEBHOOK_URL",
|
||||||
params.SlackURL, slackMessage)
|
params.SlackURL, slackMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.NtfyURL != nil {
|
if params.NtfyURL != nil {
|
||||||
q.addDestination(DestinationNtfy, "SWW_ALERT_NTFY_URL",
|
q.addDestination(DestinationNtfy, "SWWAF_ALERT_NTFY_URL",
|
||||||
params.NtfyURL, q.ntfyMessage)
|
params.NtfyURL, q.ntfyMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ func New(params Params) *Queue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Raise sends alert, which names its event and what is particular to it,
|
// Raise sends alert, which names its event and what is particular to it,
|
||||||
// unless no destination is set or SWW_ALERT_EVENTS leaves its event
|
// unless no destination is set or SWWAF_ALERT_EVENTS leaves its event
|
||||||
// out. It gives alert the instance and the time. An alert that repeats
|
// out. It gives alert the instance and the time. An alert that repeats
|
||||||
// the last one let through less than Cooldown before is held back and
|
// the last one let through less than Cooldown before is held back and
|
||||||
// counted. The next one let through gives that count, unless an hour of
|
// counted. The next one let through gives that count, unless an hour of
|
||||||
@@ -363,7 +363,7 @@ func (q *Queue) Raise(alert Alert) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WouldSend reports whether Raise would let an alert for event on
|
// WouldSend reports whether Raise would let an alert for event on
|
||||||
// netblock through now: a destination is set, SWW_ALERT_EVENTS chooses
|
// netblock through now: a destination is set, SWWAF_ALERT_EVENTS chooses
|
||||||
// event, no alert for event on netblock was let through less than
|
// event, no alert for event on netblock was let through less than
|
||||||
// Cooldown before, and fewer than MaxPerHour alerts have been let through
|
// Cooldown before, and fewer than MaxPerHour alerts have been let through
|
||||||
// in the hour under way. Unlike Raise, it counts nothing.
|
// in the hour under way. Unlike Raise, it counts nothing.
|
||||||
@@ -606,13 +606,13 @@ func (q *Queue) endHour(now time.Time) {
|
|||||||
|
|
||||||
if heldBack > 0 {
|
if heldBack > 0 {
|
||||||
reasons = append(reasons, fmt.Sprintf("%d alerts held back in the hour from %s, "+
|
reasons = append(reasons, fmt.Sprintf("%d alerts held back in the hour from %s, "+
|
||||||
"past the %d an hour SWW_ALERT_MAX_PER_HOUR allows", heldBack,
|
"past the %d an hour SWWAF_ALERT_MAX_PER_HOUR allows", heldBack,
|
||||||
q.hour.Start.Format(time.RFC3339), q.params.MaxPerHour))
|
q.hour.Start.Format(time.RFC3339), q.params.MaxPerHour))
|
||||||
}
|
}
|
||||||
|
|
||||||
if repeats > 0 {
|
if repeats > 0 {
|
||||||
reasons = append(reasons, fmt.Sprintf("%d repeats held back by "+
|
reasons = append(reasons, fmt.Sprintf("%d repeats held back by "+
|
||||||
"SWW_ALERT_COOLDOWN that no later alert gives", repeats))
|
"SWWAF_ALERT_COOLDOWN that no later alert gives", repeats))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(reasons) > 0 {
|
if len(reasons) > 0 {
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ func TestWouldSendOnlyForTheChosenEvents(t *testing.T) {
|
|||||||
|
|
||||||
if q.WouldSend(alerts.EventBan, netblock(1)) ||
|
if q.WouldSend(alerts.EventBan, netblock(1)) ||
|
||||||
q.WouldSend(alerts.EventPermanentBan, netblock(1)) {
|
q.WouldSend(alerts.EventPermanentBan, netblock(1)) {
|
||||||
t.Error("a ban alert would be sent, though SWW_ALERT_EVENTS leaves it out")
|
t.Error("a ban alert would be sent, though SWWAF_ALERT_EVENTS leaves it out")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !q.WouldSend(alerts.EventFileError, netip.Prefix{}) {
|
if !q.WouldSend(alerts.EventFileError, netip.Prefix{}) {
|
||||||
@@ -184,8 +184,8 @@ func TestWouldSendWithOnlySlackOrOnlyNtfySet(t *testing.T) {
|
|||||||
onlyNtfy.NtfyURL = parseURL(ntfyURL)
|
onlyNtfy.NtfyURL = parseURL(ntfyURL)
|
||||||
|
|
||||||
for setting, params := range map[string]alerts.Params{
|
for setting, params := range map[string]alerts.Params{
|
||||||
"SWW_ALERT_SLACK_WEBHOOK_URL": onlySlack,
|
"SWWAF_ALERT_SLACK_WEBHOOK_URL": onlySlack,
|
||||||
"SWW_ALERT_NTFY_URL": onlyNtfy,
|
"SWWAF_ALERT_NTFY_URL": onlyNtfy,
|
||||||
} {
|
} {
|
||||||
if !alerts.New(params).WouldSend(alerts.EventBan, netblock(1)) {
|
if !alerts.New(params).WouldSend(alerts.EventBan, netblock(1)) {
|
||||||
t.Errorf("a ban alert would not be sent with only %s set", setting)
|
t.Errorf("a ban alert would not be sent with only %s set", setting)
|
||||||
@@ -359,7 +359,7 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
|
|||||||
"as_name": "",
|
"as_name": "",
|
||||||
"country": "",
|
"country": "",
|
||||||
"reason": "3 alerts held back in the hour from 2000-01-01T00:00:00Z, " +
|
"reason": "3 alerts held back in the hour from 2000-01-01T00:00:00Z, " +
|
||||||
"past the 2 an hour SWW_ALERT_MAX_PER_HOUR allows",
|
"past the 2 an hour SWWAF_ALERT_MAX_PER_HOUR allows",
|
||||||
"detail": map[string]any{
|
"detail": map[string]any{
|
||||||
"hour": started,
|
"hour": started,
|
||||||
"count": float64(3),
|
"count": float64(3),
|
||||||
@@ -525,7 +525,7 @@ func TestFailedRequestIsSentAgainWithBackoff(t *testing.T) {
|
|||||||
wantCounts(t, q, 0, int64(len(want)), 0, 0)
|
wantCounts(t, q, 0, int64(len(want)), 0, 0)
|
||||||
|
|
||||||
if !strings.Contains(log.String(),
|
if !strings.Contains(log.String(),
|
||||||
`"msg":"sending an alert to SWW_ALERT_WEBHOOK_URL failed"`) {
|
`"msg":"sending an alert to SWWAF_ALERT_WEBHOOK_URL failed"`) {
|
||||||
t.Errorf("process log %q names no failure", log.String())
|
t.Errorf("process log %q names no failure", log.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,7 +595,7 @@ func TestRefusedAlertIsGivenUpAndTheNextSent(t *testing.T) {
|
|||||||
wantCounts(t, q, 1, 3, 0, 1)
|
wantCounts(t, q, 1, 3, 0, 1)
|
||||||
|
|
||||||
if !strings.Contains(log.String(),
|
if !strings.Contains(log.String(),
|
||||||
`"msg":"gave up an alert SWW_ALERT_WEBHOOK_URL refused"`) {
|
`"msg":"gave up an alert SWWAF_ALERT_WEBHOOK_URL refused"`) {
|
||||||
t.Errorf("process log %q names no alert given up", log.String())
|
t.Errorf("process log %q names no alert given up", log.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -619,7 +619,7 @@ func TestFailedRequestIsLoggedWithoutTheURL(t *testing.T) {
|
|||||||
|
|
||||||
logged := log.String()
|
logged := log.String()
|
||||||
if !strings.Contains(logged,
|
if !strings.Contains(logged,
|
||||||
`"msg":"sending an alert to SWW_ALERT_WEBHOOK_URL failed"`) ||
|
`"msg":"sending an alert to SWWAF_ALERT_WEBHOOK_URL failed"`) ||
|
||||||
strings.Contains(logged, "alerts.example") || strings.Contains(logged, "team=ops") {
|
strings.Contains(logged, "alerts.example") || strings.Contains(logged, "team=ops") {
|
||||||
t.Errorf("process log %q names no failure, or names the URL", logged)
|
t.Errorf("process log %q names no failure, or names the URL", logged)
|
||||||
}
|
}
|
||||||
@@ -782,8 +782,8 @@ func TestSlackAndNtfyAreSentTheSummaryAndTheRepeatsHeldBack(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const summary = "1 alerts held back in the hour from 2000-01-01T00:00:00Z, " +
|
const summary = "1 alerts held back in the hour from 2000-01-01T00:00:00Z, " +
|
||||||
"past the 1 an hour SWW_ALERT_MAX_PER_HOUR allows; 1 repeats held back " +
|
"past the 1 an hour SWWAF_ALERT_MAX_PER_HOUR allows; 1 repeats held back " +
|
||||||
"by SWW_ALERT_COOLDOWN that no later alert gives\nsuppressed repeats: 1"
|
"by SWWAF_ALERT_COOLDOWN that no later alert gives\nsuppressed repeats: 1"
|
||||||
|
|
||||||
wantSlackMessage(t, slack[1], "*"+instance+": summary*\n"+summary)
|
wantSlackMessage(t, slack[1], "*"+instance+": summary*\n"+summary)
|
||||||
wantNtfyMessage(t, ntfy[1], instance+": summary", "default bar_chart", summary)
|
wantNtfyMessage(t, ntfy[1], instance+": summary", "default bar_chart", summary)
|
||||||
@@ -901,7 +901,7 @@ func TestDestinationThatDoesNotAnswerHoldsUpNeitherOther(t *testing.T) {
|
|||||||
|
|
||||||
logged := log.String()
|
logged := log.String()
|
||||||
if !strings.Contains(logged,
|
if !strings.Contains(logged,
|
||||||
`"msg":"sending an alert to SWW_ALERT_SLACK_WEBHOOK_URL failed"`) ||
|
`"msg":"sending an alert to SWWAF_ALERT_SLACK_WEBHOOK_URL failed"`) ||
|
||||||
strings.Contains(logged, "hooks.slack.example") ||
|
strings.Contains(logged, "hooks.slack.example") ||
|
||||||
strings.Contains(logged, "T0123") {
|
strings.Contains(logged, "T0123") {
|
||||||
t.Errorf("process log %q names no failure, or names the URL", logged)
|
t.Errorf("process log %q names no failure, or names the URL", logged)
|
||||||
@@ -1254,7 +1254,7 @@ type eventMessage struct {
|
|||||||
title, priorityAndTag, text string
|
title, priorityAndTag, text string
|
||||||
}
|
}
|
||||||
|
|
||||||
// anAlertForEachEvent returns an alert for each event SWW_ALERT_EVENTS
|
// anAlertForEachEvent returns an alert for each event SWWAF_ALERT_EVENTS
|
||||||
// names, the first in observe mode, each with its message.
|
// names, the first in observe mode, each with its message.
|
||||||
func anAlertForEachEvent() []eventMessage {
|
func anAlertForEachEvent() []eventMessage {
|
||||||
return []eventMessage{
|
return []eventMessage{
|
||||||
|
|||||||
+12
-12
@@ -28,16 +28,16 @@ const maxCounters = 20000
|
|||||||
// alerts name them.
|
// alerts name them.
|
||||||
const (
|
const (
|
||||||
// ScopeClient is one client: an IPv4 address, or an IPv6 netblock of
|
// ScopeClient is one client: an IPv4 address, or an IPv6 netblock of
|
||||||
// SWW_IPV6_GROUP_PREFIX.
|
// SWWAF_IPV6_GROUP_PREFIX.
|
||||||
ScopeClient = "client"
|
ScopeClient = "client"
|
||||||
// ScopeNet is the netblock around a client, SWW_ANOMALY_NET_V4_PREFIX
|
// ScopeNet is the netblock around a client, SWWAF_ANOMALY_NET_V4_PREFIX
|
||||||
// or SWW_ANOMALY_NET_V6_PREFIX long.
|
// or SWWAF_ANOMALY_NET_V6_PREFIX long.
|
||||||
ScopeNet = "net"
|
ScopeNet = "net"
|
||||||
// ScopeASN is an AS number.
|
// ScopeASN is an AS number.
|
||||||
ScopeASN = "asn"
|
ScopeASN = "asn"
|
||||||
// ScopeTotal is the whole service.
|
// ScopeTotal is the whole service.
|
||||||
ScopeTotal = "total"
|
ScopeTotal = "total"
|
||||||
// ScopeWatch is a named netblock of SWW_WATCH_NETS.
|
// ScopeWatch is a named netblock of SWWAF_WATCH_NETS.
|
||||||
ScopeWatch = "watch"
|
ScopeWatch = "watch"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ type Thresholds struct {
|
|||||||
BytesPerHour int64
|
BytesPerHour int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NamedNetblock is a netblock SWW_WATCH_NETS names.
|
// NamedNetblock is a netblock SWWAF_WATCH_NETS names.
|
||||||
type NamedNetblock struct {
|
type NamedNetblock struct {
|
||||||
Name string
|
Name string
|
||||||
Netblock netip.Prefix
|
Netblock netip.Prefix
|
||||||
@@ -70,14 +70,14 @@ type NamedNetblock struct {
|
|||||||
|
|
||||||
// Params are what New needs.
|
// Params are what New needs.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
// The thresholds of each scope: SWW_ANOMALY_CLIENT_*,
|
// The thresholds of each scope: SWWAF_ANOMALY_CLIENT_*,
|
||||||
// SWW_ANOMALY_NET_*, SWW_ANOMALY_ASN_*, SWW_ANOMALY_TOTAL_* and
|
// SWWAF_ANOMALY_NET_*, SWWAF_ANOMALY_ASN_*, SWWAF_ANOMALY_TOTAL_* and
|
||||||
// SWW_WATCH_*.
|
// SWWAF_WATCH_*.
|
||||||
Client, Net, ASN, Total, Watch Thresholds
|
Client, Net, ASN, Total, Watch Thresholds
|
||||||
// NetV4Prefix and NetV6Prefix are the lengths of the netblock around a
|
// NetV4Prefix and NetV6Prefix are the lengths of the netblock around a
|
||||||
// client (SWW_ANOMALY_NET_V4_PREFIX and SWW_ANOMALY_NET_V6_PREFIX).
|
// client (SWWAF_ANOMALY_NET_V4_PREFIX and SWWAF_ANOMALY_NET_V6_PREFIX).
|
||||||
NetV4Prefix, NetV6Prefix int
|
NetV4Prefix, NetV6Prefix int
|
||||||
// NamedNetblocks are SWW_WATCH_NETS.
|
// NamedNetblocks are SWWAF_WATCH_NETS.
|
||||||
NamedNetblocks []NamedNetblock
|
NamedNetblocks []NamedNetblock
|
||||||
// Alerts receive the anomaly alerts.
|
// Alerts receive the anomaly alerts.
|
||||||
Alerts *alerts.Queue
|
Alerts *alerts.Queue
|
||||||
@@ -107,13 +107,13 @@ type Request struct {
|
|||||||
Host string
|
Host string
|
||||||
// Client is the client's address, and ClientGroup the client it is
|
// Client is the client's address, and ClientGroup the client it is
|
||||||
// counted as: its IPv4 address, or the IPv6 netblock of
|
// counted as: its IPv4 address, or the IPv6 netblock of
|
||||||
// SWW_IPV6_GROUP_PREFIX its address is in.
|
// SWWAF_IPV6_GROUP_PREFIX its address is in.
|
||||||
Client netip.Addr
|
Client netip.Addr
|
||||||
ClientGroup netip.Prefix
|
ClientGroup netip.Prefix
|
||||||
// ASN, ASName and Country are the client's as looked up, each "" when
|
// ASN, ASName and Country are the client's as looked up, each "" when
|
||||||
// unknown.
|
// unknown.
|
||||||
ASN, ASName, Country string
|
ASN, ASName, Country string
|
||||||
// Bytes are the request's bytes, as SWW_BYTES_COUNT counts them.
|
// Bytes are the request's bytes, as SWWAF_BYTES_COUNT counts them.
|
||||||
Bytes int64
|
Bytes int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ type Notes struct {
|
|||||||
LimitPercentSetting string `json:"limit_percent_setting,omitempty"`
|
LimitPercentSetting string `json:"limit_percent_setting,omitempty"`
|
||||||
// RuleID and Target are, for a ban for a clear sign of attack, the id
|
// RuleID and Target are, for a ban for a clear sign of attack, the id
|
||||||
// of the rule file rule that matched, and its target; TrapPath is, for
|
// of the rule file rule that matched, and its target; TrapPath is, for
|
||||||
// one for a request for a path in SWW_TRAP_PATHS, that path as the
|
// one for a request for a path in SWWAF_TRAP_PATHS, that path as the
|
||||||
// client sent it.
|
// client sent it.
|
||||||
RuleID string `json:"rule_id,omitempty"`
|
RuleID string `json:"rule_id,omitempty"`
|
||||||
Target string `json:"target,omitempty"`
|
Target string `json:"target,omitempty"`
|
||||||
@@ -199,7 +199,7 @@ type Ledger struct {
|
|||||||
// v4Lengths and v6Lengths are the lengths of the IPv4 and IPv6
|
// v4Lengths and v6Lengths are the lengths of the IPv4 and IPv6
|
||||||
// netblocks that have been banned. Check looks for a ban at each of
|
// netblocks that have been banned. Check looks for a ban at each of
|
||||||
// them, so that a ban read from bans.json refuses every client in its
|
// them, so that a ban read from bans.json refuses every client in its
|
||||||
// netblock even when it was made with another SWW_BAN_SCOPE_V4_PREFIX,
|
// netblock even when it was made with another SWWAF_BAN_SCOPE_V4_PREFIX,
|
||||||
// or another length of an IPv6 client's netblock.
|
// or another length of an IPv6 client's netblock.
|
||||||
v4Lengths, v6Lengths []int
|
v4Lengths, v6Lengths []int
|
||||||
}
|
}
|
||||||
|
|||||||
+233
-248
@@ -1,6 +1,6 @@
|
|||||||
// Package config reads sww's settings, and is the one place they are
|
// Package config reads sww's settings, and is the one place they are
|
||||||
// read. Every setting is an environment variable. The settings of `sww
|
// read. Every setting is an environment variable. The settings of `sww
|
||||||
// discriminator` start with SWW_, every one of them has a default, and most can be
|
// discriminator` start with SWWAF_, every one of them has a default, and most can be
|
||||||
// given as a file instead. Those of `sww rproxy` start with SWRP_. Each
|
// given as a file instead. Those of `sww rproxy` start with SWRP_. Each
|
||||||
// of them but SWRP_WAF_BACKEND has a default, and none can be given as a file,
|
// of them but SWRP_WAF_BACKEND has a default, and none can be given as a file,
|
||||||
// since rproxy never touches disk. SWW_COMMAND chooses which of the two
|
// since rproxy never touches disk. SWW_COMMAND chooses which of the two
|
||||||
@@ -34,85 +34,85 @@ import (
|
|||||||
// Config is the settings of `sww discriminator`. A time or size limit of
|
// Config is the settings of `sww discriminator`. A time or size limit of
|
||||||
// zero is off.
|
// zero is off.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// ListenAddr is where discriminator listens (SWW_LISTEN_ADDR).
|
// ListenAddr is where discriminator listens (SWWAF_LISTEN_ADDR).
|
||||||
ListenAddr string
|
ListenAddr string
|
||||||
// InstanceName is the name every log line and alert gives as instance,
|
// InstanceName is the name every log line and alert gives as instance,
|
||||||
// and every metric carries as its label instance (SWW_INSTANCE_NAME),
|
// and every metric carries as its label instance (SWWAF_INSTANCE_NAME),
|
||||||
// by default the host's name, which docker sets to the first 12
|
// by default the host's name, which docker sets to the first 12
|
||||||
// characters of the container's id.
|
// characters of the container's id.
|
||||||
InstanceName string
|
InstanceName string
|
||||||
// Observe is true in observe mode, when SWW_MODE is observe rather
|
// Observe is true in observe mode, when SWWAF_MODE is observe rather
|
||||||
// than enforce: a request that SWW_DENY_NETS, a ban, the country
|
// than enforce: a request that SWWAF_DENY_NETS, a ban, the country
|
||||||
// lists, a rate limit, a rule or the Core Rule Set would refuse is
|
// lists, a rate limit, a rule or the Core Rule Set would refuse is
|
||||||
// allowed instead, and no ban is made.
|
// allowed instead, and no ban is made.
|
||||||
Observe bool
|
Observe bool
|
||||||
// IPv6GroupPrefix is the length of the IPv6 netblock that is one client
|
// IPv6GroupPrefix is the length of the IPv6 netblock that is one client
|
||||||
// (SWW_IPV6_GROUP_PREFIX), from 32 to 128.
|
// (SWWAF_IPV6_GROUP_PREFIX), from 32 to 128.
|
||||||
IPv6GroupPrefix int
|
IPv6GroupPrefix int
|
||||||
// MaxTrackedClients is the most clients the table of clients holds, in
|
// MaxTrackedClients is the most clients the table of clients holds, in
|
||||||
// memory and in clients.json (SWW_MAX_TRACKED_CLIENTS).
|
// memory and in clients.json (SWWAF_MAX_TRACKED_CLIENTS).
|
||||||
MaxTrackedClients int
|
MaxTrackedClients int
|
||||||
// AllowNets are the netblocks whose clients skip every check
|
// AllowNets are the netblocks whose clients skip every check
|
||||||
// (SWW_ALLOW_NETS). RateLimitExemptNets are those whose clients the
|
// (SWWAF_ALLOW_NETS). RateLimitExemptNets are those whose clients the
|
||||||
// rate limits neither count nor refuse (SWW_RATE_LIMIT_EXEMPT_NETS).
|
// rate limits neither count nor refuse (SWWAF_RATE_LIMIT_EXEMPT_NETS).
|
||||||
// DenyNets are those whose clients are always refused
|
// DenyNets are those whose clients are always refused
|
||||||
// (SWW_DENY_NETS).
|
// (SWWAF_DENY_NETS).
|
||||||
AllowNets []netip.Prefix
|
AllowNets []netip.Prefix
|
||||||
RateLimitExemptNets []netip.Prefix
|
RateLimitExemptNets []netip.Prefix
|
||||||
DenyNets []netip.Prefix
|
DenyNets []netip.Prefix
|
||||||
// RateLimitPerMinute, RateLimitPerHour and RateLimitPerDay are the
|
// RateLimitPerMinute, RateLimitPerHour and RateLimitPerDay are the
|
||||||
// most requests a client may make in a minute, an hour and a day
|
// most requests a client may make in a minute, an hour and a day
|
||||||
// (SWW_RATE_LIMIT_PER_MINUTE, SWW_RATE_LIMIT_PER_HOUR and
|
// (SWWAF_RATE_LIMIT_PER_MINUTE, SWWAF_RATE_LIMIT_PER_HOUR and
|
||||||
// SWW_RATE_LIMIT_PER_DAY).
|
// SWWAF_RATE_LIMIT_PER_DAY).
|
||||||
RateLimitPerMinute int64
|
RateLimitPerMinute int64
|
||||||
RateLimitPerHour int64
|
RateLimitPerHour int64
|
||||||
RateLimitPerDay int64
|
RateLimitPerDay int64
|
||||||
// RateLimitExemptPaths are the path prefixes whose requests the rate
|
// RateLimitExemptPaths are the path prefixes whose requests the rate
|
||||||
// limits neither count nor refuse (SWW_RATE_LIMIT_EXEMPT_PATHS).
|
// limits neither count nor refuse (SWWAF_RATE_LIMIT_EXEMPT_PATHS).
|
||||||
// Each starts with /.
|
// Each starts with /.
|
||||||
RateLimitExemptPaths []string
|
RateLimitExemptPaths []string
|
||||||
// BytesLimitPerMinute, BytesLimitPerHour and BytesLimitPerDay are the
|
// BytesLimitPerMinute, BytesLimitPerHour and BytesLimitPerDay are the
|
||||||
// most bytes a client's requests may carry in a minute, an hour and a
|
// most bytes a client's requests may carry in a minute, an hour and a
|
||||||
// day (SWW_BYTES_LIMIT_PER_MINUTE, SWW_BYTES_LIMIT_PER_HOUR and
|
// day (SWWAF_BYTES_LIMIT_PER_MINUTE, SWWAF_BYTES_LIMIT_PER_HOUR and
|
||||||
// SWW_BYTES_LIMIT_PER_DAY). BytesCount is which body bytes count
|
// SWWAF_BYTES_LIMIT_PER_DAY). BytesCount is which body bytes count
|
||||||
// toward them (SWW_BYTES_COUNT): response, request or both.
|
// toward them (SWWAF_BYTES_COUNT): response, request or both.
|
||||||
BytesLimitPerMinute int64
|
BytesLimitPerMinute int64
|
||||||
BytesLimitPerHour int64
|
BytesLimitPerHour int64
|
||||||
BytesLimitPerDay int64
|
BytesLimitPerDay int64
|
||||||
BytesCount string
|
BytesCount string
|
||||||
// LookupSource is where each client's AS number and country are
|
// LookupSource is where each client's AS number and country are
|
||||||
// looked up (SWW_LOOKUP_SOURCE): geojs, file, or off for nowhere.
|
// looked up (SWWAF_LOOKUP_SOURCE): geojs, file, or off for nowhere.
|
||||||
// LookupDBPath is the lookup database, the IPinfo Lite file looked up
|
// LookupDBPath is the lookup database, the IPinfo Lite file looked up
|
||||||
// in while LookupSource is file (SWW_LOOKUP_DB_PATH), and "" for any
|
// in while LookupSource is file (SWWAF_LOOKUP_DB_PATH), and "" for any
|
||||||
// other source. A request waits up to LookupTimeout for its client's
|
// other source. A request waits up to LookupTimeout for its client's
|
||||||
// first answer from GeoJS while a setting needs it
|
// first answer from GeoJS while a setting needs it
|
||||||
// (SWW_LOOKUP_TIMEOUT), which cannot be off. AddLookupHeaders is true
|
// (SWWAF_LOOKUP_TIMEOUT), which cannot be off. AddLookupHeaders is true
|
||||||
// when the app is passed the client's AS number and country in headers
|
// when the app is passed the client's AS number and country in headers
|
||||||
// (SWW_ADD_LOOKUP_HEADERS).
|
// (SWWAF_ADD_LOOKUP_HEADERS).
|
||||||
LookupSource string
|
LookupSource string
|
||||||
LookupDBPath string
|
LookupDBPath string
|
||||||
LookupTimeout time.Duration
|
LookupTimeout time.Duration
|
||||||
AddLookupHeaders bool
|
AddLookupHeaders bool
|
||||||
// DeniedCountries are the countries whose clients are refused
|
// DeniedCountries are the countries whose clients are refused
|
||||||
// (SWW_DENIED_COUNTRIES). ExclusivelyAllowedCountries, when not
|
// (SWWAF_DENIED_COUNTRIES). ExclusivelyAllowedCountries, when not
|
||||||
// empty, are the only countries whose clients are let through
|
// empty, are the only countries whose clients are let through
|
||||||
// (SWW_EXCLUSIVELY_ALLOWED_COUNTRIES). Both hold two-letter codes in
|
// (SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES). Both hold two-letter codes in
|
||||||
// capitals, as GeoJS gives them.
|
// capitals, as GeoJS gives them.
|
||||||
DeniedCountries []string
|
DeniedCountries []string
|
||||||
ExclusivelyAllowedCountries []string
|
ExclusivelyAllowedCountries []string
|
||||||
// The biased thresholds. ASNLimitPercent and CountryLimitPercent give
|
// The biased thresholds. ASNLimitPercent and CountryLimitPercent give
|
||||||
// the clients of the AS numbers and the countries they list that
|
// the clients of the AS numbers and the countries they list that
|
||||||
// percentage of every rate limit and byte limit
|
// percentage of every rate limit and byte limit
|
||||||
// (SWW_ASN_LIMIT_PERCENT and SWW_COUNTRY_LIMIT_PERCENT).
|
// (SWWAF_ASN_LIMIT_PERCENT and SWWAF_COUNTRY_LIMIT_PERCENT).
|
||||||
// ASNBytesPercent and CountryBytesPercent give those they list a
|
// ASNBytesPercent and CountryBytesPercent give those they list a
|
||||||
// percentage of the byte limits in place of that one
|
// percentage of the byte limits in place of that one
|
||||||
// (SWW_ASN_BYTES_PERCENT and SWW_COUNTRY_BYTES_PERCENT). Each holds
|
// (SWWAF_ASN_BYTES_PERCENT and SWWAF_COUNTRY_BYTES_PERCENT). Each holds
|
||||||
// percentages from 0 to 100, by AS number, written as AS64496, or by
|
// percentages from 0 to 100, by AS number, written as AS64496, or by
|
||||||
// country, a two-letter code in capitals, as the lookup gives them.
|
// country, a two-letter code in capitals, as the lookup gives them.
|
||||||
// UnknownLimitPercent is the percentage of every limit a client without
|
// UnknownLimitPercent is the percentage of every limit a client without
|
||||||
// a country gets (SWW_UNKNOWN_LIMIT_PERCENT). ASNLimitPercentURL is
|
// a country gets (SWWAF_UNKNOWN_LIMIT_PERCENT). ASNLimitPercentURL is
|
||||||
// where a file of AS:percent lines is fetched from, whose percentages
|
// where a file of AS:percent lines is fetched from, whose percentages
|
||||||
// count as those of ASNLimitPercent do (SWW_ASN_LIMIT_PERCENT_URL), ""
|
// count as those of ASNLimitPercent do (SWWAF_ASN_LIMIT_PERCENT_URL), ""
|
||||||
// while it is unset.
|
// while it is unset.
|
||||||
ASNLimitPercent map[string]int64
|
ASNLimitPercent map[string]int64
|
||||||
CountryLimitPercent map[string]int64
|
CountryLimitPercent map[string]int64
|
||||||
@@ -121,30 +121,30 @@ type Config struct {
|
|||||||
UnknownLimitPercent int64
|
UnknownLimitPercent int64
|
||||||
ASNLimitPercentURL string
|
ASNLimitPercentURL string
|
||||||
// BlocklistURLs are where the blocklists are fetched from
|
// BlocklistURLs are where the blocklists are fetched from
|
||||||
// (SWW_BLOCKLIST_URLS). Each list, and ASNLimitPercentURL's, is
|
// (SWWAF_BLOCKLIST_URLS). Each list, and ASNLimitPercentURL's, is
|
||||||
// fetched again BlocklistRefresh after it was last fetched or tried
|
// fetched again BlocklistRefresh after it was last fetched or tried
|
||||||
// (SWW_BLOCKLIST_REFRESH), which is never less than an hour.
|
// (SWWAF_BLOCKLIST_REFRESH), which is never less than an hour.
|
||||||
// BlocklistAction is what is done with a client a blocklist lists
|
// BlocklistAction is what is done with a client a blocklist lists
|
||||||
// (SWW_BLOCKLIST_ACTION): deny, limit or log; for limit,
|
// (SWWAF_BLOCKLIST_ACTION): deny, limit or log; for limit,
|
||||||
// BlocklistLimitPercent is the percentage of every limit it gets.
|
// BlocklistLimitPercent is the percentage of every limit it gets.
|
||||||
BlocklistURLs []string
|
BlocklistURLs []string
|
||||||
BlocklistRefresh time.Duration
|
BlocklistRefresh time.Duration
|
||||||
BlocklistAction string
|
BlocklistAction string
|
||||||
BlocklistLimitPercent int64
|
BlocklistLimitPercent int64
|
||||||
// DNSBLZones are the DNSBL zones clients are asked about
|
// DNSBLZones are the DNSBL zones clients are asked about
|
||||||
// (SWW_DNSBL_ZONES), through DNSBLResolver (SWW_DNSBL_RESOLVER), or
|
// (SWWAF_DNSBL_ZONES), through DNSBLResolver (SWWAF_DNSBL_RESOLVER), or
|
||||||
// the host's resolver while that is the zero AddrPort.
|
// the host's resolver while that is the zero AddrPort.
|
||||||
// AbuseIPDBKey is the key of the AbuseIPDB account clients are checked
|
// AbuseIPDBKey is the key of the AbuseIPDB account clients are checked
|
||||||
// with (SWW_ABUSEIPDB_KEY), "" while it is unset and none is. A score
|
// with (SWWAF_ABUSEIPDB_KEY), "" while it is unset and none is. A score
|
||||||
// of AbuseIPDBMinScore or more is a hit (SWW_ABUSEIPDB_MIN_SCORE), and
|
// of AbuseIPDBMinScore or more is a hit (SWWAF_ABUSEIPDB_MIN_SCORE), and
|
||||||
// at most AbuseIPDBDailyBudget checks are made a day
|
// at most AbuseIPDBDailyBudget checks are made a day
|
||||||
// (SWW_ABUSEIPDB_DAILY_BUDGET).
|
// (SWWAF_ABUSEIPDB_DAILY_BUDGET).
|
||||||
// ReputationAction is what is done with a client a zone's verdict lists,
|
// ReputationAction is what is done with a client a zone's verdict lists,
|
||||||
// or whose score is a hit (SWW_REPUTATION_ACTION): deny, limit or log;
|
// or whose score is a hit (SWWAF_REPUTATION_ACTION): deny, limit or log;
|
||||||
// for limit, ReputationLimitPercent is the percentage of every limit it
|
// for limit, ReputationLimitPercent is the percentage of every limit it
|
||||||
// gets. A verdict or a score is used for ReputationCacheTTL after it was
|
// gets. A verdict or a score is used for ReputationCacheTTL after it was
|
||||||
// fetched (SWW_REPUTATION_CACHE_TTL), and a query or a check may take
|
// fetched (SWWAF_REPUTATION_CACHE_TTL), and a query or a check may take
|
||||||
// ReputationTimeout (SWW_REPUTATION_TIMEOUT). Neither can be off.
|
// ReputationTimeout (SWWAF_REPUTATION_TIMEOUT). Neither can be off.
|
||||||
DNSBLZones []string
|
DNSBLZones []string
|
||||||
DNSBLResolver netip.AddrPort
|
DNSBLResolver netip.AddrPort
|
||||||
AbuseIPDBKey string
|
AbuseIPDBKey string
|
||||||
@@ -155,97 +155,94 @@ type Config struct {
|
|||||||
ReputationCacheTTL time.Duration
|
ReputationCacheTTL time.Duration
|
||||||
ReputationTimeout time.Duration
|
ReputationTimeout time.Duration
|
||||||
// CrowdSecDecisionsURL is where the decision list of the CrowdSec
|
// CrowdSecDecisionsURL is where the decision list of the CrowdSec
|
||||||
// engine whose local API SWW_CROWDSEC_LAPI_URL names is fetched from:
|
// engine whose local API SWWAF_CROWDSEC_LAPI_URL names is fetched from:
|
||||||
// that URL with v1/decisions added to its path, "" while it is unset and
|
// that URL with v1/decisions added to its path, "" while it is unset and
|
||||||
// none is. CrowdSecKey is the key the engine is asked with
|
// none is. CrowdSecKey is the key the engine is asked with
|
||||||
// (SWW_CROWDSEC_LAPI_KEY).
|
// (SWWAF_CROWDSEC_LAPI_KEY).
|
||||||
CrowdSecDecisionsURL string
|
CrowdSecDecisionsURL string
|
||||||
CrowdSecKey string
|
CrowdSecKey string
|
||||||
// BanResponse is the status a refused client is answered with, 403
|
// BanResponse is the status a refused client is answered with, 403
|
||||||
// or 429, or 0 to close the connection without an answer
|
// or 429, or 0 to close the connection without an answer
|
||||||
// (SWW_BAN_RESPONSE). It answers a banned client, a request that
|
// (SWWAF_BAN_RESPONSE). It answers a banned client, a request that
|
||||||
// breaks a rate limit or matches a ban rule, SWW_DENY_NETS and the
|
// breaks a rate limit or matches a ban rule, SWWAF_DENY_NETS and the
|
||||||
// country lists.
|
// country lists.
|
||||||
BanResponse int
|
BanResponse int
|
||||||
// LimitBanDuration is the ban for a first broken rate limit
|
// LimitBanDuration is the ban for a first broken rate limit
|
||||||
// (SWW_LIMIT_BAN_DURATION). A limit broken again within
|
// (SWWAF_LIMIT_BAN_DURATION). A limit broken again within
|
||||||
// LimitBanRepeatWindow after the last ban ended bans for three times
|
// LimitBanRepeatWindow after the last ban ended bans for three times
|
||||||
// as long as that ban (SWW_LIMIT_BAN_REPEAT_WINDOW), and a ban that
|
// as long as that ban (SWWAF_LIMIT_BAN_REPEAT_WINDOW), and a ban that
|
||||||
// would be longer than MaxBanDuration is permanent instead
|
// would be longer than MaxBanDuration is permanent instead
|
||||||
// (SWW_MAX_BAN_DURATION). None of them can be off.
|
// (SWWAF_MAX_BAN_DURATION). None of them can be off.
|
||||||
LimitBanDuration time.Duration
|
LimitBanDuration time.Duration
|
||||||
LimitBanRepeatWindow time.Duration
|
LimitBanRepeatWindow time.Duration
|
||||||
MaxBanDuration time.Duration
|
MaxBanDuration time.Duration
|
||||||
// AttackBanDuration is the ban for a first clear sign of attack
|
// AttackBanDuration is the ban for a first clear sign of attack
|
||||||
// (SWW_ATTACK_BAN_DURATION). It cannot be off.
|
// (SWWAF_ATTACK_BAN_DURATION). It cannot be off.
|
||||||
AttackBanDuration time.Duration
|
AttackBanDuration time.Duration
|
||||||
// MaxBans is the most bans held (SWW_MAX_BANS).
|
// MaxBans is the most bans held (SWWAF_MAX_BANS).
|
||||||
MaxBans int
|
MaxBans int
|
||||||
// BanScopeV4Prefix is the length of the netblock around an IPv4
|
// BanScopeV4Prefix is the length of the netblock around an IPv4
|
||||||
// client that a ban covers (SWW_BAN_SCOPE_V4_PREFIX).
|
// client that a ban covers (SWWAF_BAN_SCOPE_V4_PREFIX).
|
||||||
BanScopeV4Prefix int
|
BanScopeV4Prefix int
|
||||||
// StateDir is the directory of the state files, an absolute path
|
// StateDir is the directory of the state files, an absolute path
|
||||||
// (SWW_STATE_DIR). bans.json is written StateWriteDelay after a ban
|
// (SWWAF_STATE_DIR). bans.json is written StateWriteDelay after a ban
|
||||||
// is made (SWW_STATE_WRITE_DELAY), and every state file every
|
// is made (SWWAF_STATE_WRITE_DELAY), and every state file every
|
||||||
// StateCounterInterval (SWW_STATE_COUNTER_INTERVAL). Neither can be
|
// StateCounterInterval (SWWAF_STATE_COUNTER_INTERVAL). Neither can be
|
||||||
// off.
|
// off.
|
||||||
StateDir string
|
StateDir string
|
||||||
StateWriteDelay time.Duration
|
StateWriteDelay time.Duration
|
||||||
StateCounterInterval time.Duration
|
StateCounterInterval time.Duration
|
||||||
// LogRequestHeaders are the request headers whose values the request
|
// LogRequestHeaders are the request headers whose values the request
|
||||||
// log gives, in lower case (SWW_LOG_REQUEST_HEADERS).
|
// log gives, in lower case (SWWAF_LOG_REQUEST_HEADERS).
|
||||||
LogRequestHeaders []string
|
LogRequestHeaders []string
|
||||||
// LogLevel is the least severe of the process's own messages that are
|
// LogLevel is the least severe of the process's own messages that are
|
||||||
// written (SWW_LOG_LEVEL). It holds back no request log line.
|
// written (SWWAF_LOG_LEVEL). It holds back no request log line.
|
||||||
LogLevel slog.Level
|
LogLevel slog.Level
|
||||||
// AdminToken is the bearer token an admin sends for the ban endpoints
|
// AdminToken is the bearer token an admin sends for the ban endpoints
|
||||||
// and /_sww/clients/<ip> (SWW_ADMIN_TOKEN), "" while it is
|
// and /_sww/clients/<ip> (SWWAF_ADMIN_TOKEN), "" while it is
|
||||||
// unset and they are off.
|
// unset and they are off.
|
||||||
AdminToken string
|
AdminToken string
|
||||||
// MetricsToken is the bearer token a scraper sends for the metrics
|
// MetricsToken is the bearer token a scraper sends for the metrics
|
||||||
// (SWW_METRICS_TOKEN), "" while it is unset and the metrics are off.
|
// (SWWAF_METRICS_TOKEN), "" while it is unset and the metrics are off.
|
||||||
// MetricsTopN is how many AS numbers and how many countries get series
|
// MetricsTopN is how many AS numbers and how many countries get series
|
||||||
// of their own in the metrics (SWW_METRICS_TOP_N).
|
// of their own in the metrics (SWWAF_METRICS_TOP_N).
|
||||||
MetricsToken string
|
MetricsToken string
|
||||||
MetricsTopN int
|
MetricsTopN int
|
||||||
// RulesDir is the directory of the rule files (SWW_RULES_DIR), read
|
// RulesDir is the directory of the rule files (SWWAF_RULES_DIR), read
|
||||||
// unless RulesEnabled is false (SWW_RULES_ENABLED).
|
// unless RulesEnabled is false (SWWAF_RULES_ENABLED).
|
||||||
RulesDir string
|
RulesDir string
|
||||||
RulesEnabled bool
|
RulesEnabled bool
|
||||||
// WAFMode is what the Core Rule Set does (SWW_WAF_MODE): WAFModeOff,
|
// WAFMode is what the Core Rule Set does (SWWAF_WAF_MODE): WAFModeOff,
|
||||||
// WAFModeDetect or WAFModeBlock. WAFParanoiaLevel is its paranoia
|
// WAFModeDetect or WAFModeBlock. WAFParanoiaLevel is its paranoia
|
||||||
// level, from 1 to 4 (SWW_WAF_PARANOIA_LEVEL), and
|
// level, from 1 to 4 (SWWAF_WAF_PARANOIA_LEVEL), and
|
||||||
// WAFAnomalyThreshold the anomaly score at which a request is a match
|
// WAFAnomalyThreshold the anomaly score at which a request is a match
|
||||||
// (SWW_WAF_ANOMALY_THRESHOLD), 0 while it is off. WAFDisabledRules
|
// (SWWAF_WAF_ANOMALY_THRESHOLD), 0 while it is off. WAFDisabledRules
|
||||||
// are the ids of its rules switched off (SWW_WAF_DISABLED_RULES),
|
// are the ids of its rules switched off (SWWAF_WAF_DISABLED_RULES),
|
||||||
// WAFExemptPaths the path prefixes it does not inspect
|
// WAFExemptPaths the path prefixes it does not inspect
|
||||||
// (SWW_WAF_EXEMPT_PATHS), and WAFBodyLimit the most of a request body
|
// (SWWAF_WAF_EXEMPT_PATHS), and WAFBodyLimit the most of a request body
|
||||||
// it reads (SWW_WAF_BODY_LIMIT), 0 while it is off and it reads none.
|
// it reads (SWWAF_WAF_BODY_LIMIT), 0 while it is off and it reads none.
|
||||||
// CoreRuleSetExclusionsFile is the path of the file of its exclusions
|
WAFMode string
|
||||||
// (SWW_CORE_RULE_SET_EXCLUSIONS_FILE), "" while there is none.
|
WAFParanoiaLevel int
|
||||||
WAFMode string
|
WAFAnomalyThreshold int
|
||||||
WAFParanoiaLevel int
|
WAFDisabledRules []int
|
||||||
WAFAnomalyThreshold int
|
WAFExemptPaths []string
|
||||||
WAFDisabledRules []int
|
WAFBodyLimit int64
|
||||||
WAFExemptPaths []string
|
|
||||||
WAFBodyLimit int64
|
|
||||||
CoreRuleSetExclusionsFile string
|
|
||||||
// TrapPaths are the paths a request for which is a clear sign of
|
// TrapPaths are the paths a request for which is a clear sign of
|
||||||
// attack (SWW_TRAP_PATHS), each starting with / and without a ?.
|
// attack (SWWAF_TRAP_PATHS), each starting with / and without a ?.
|
||||||
TrapPaths []string
|
TrapPaths []string
|
||||||
// ErrorBurstThreshold is the most requests of a client within a minute
|
// ErrorBurstThreshold is the most requests of a client within a minute
|
||||||
// that discriminator may refuse after a rule file or Core Rule Set match; one
|
// that discriminator may refuse after a rule file or Core Rule Set match; one
|
||||||
// more breaks a limit (SWW_ERROR_BURST_THRESHOLD). 0 is off.
|
// more breaks a limit (SWWAF_ERROR_BURST_THRESHOLD). 0 is off.
|
||||||
ErrorBurstThreshold int64
|
ErrorBurstThreshold int64
|
||||||
// LogRemoteURL is where every line on stdout is also sent
|
// LogRemoteURL is where every line on stdout is also sent
|
||||||
// (SWW_LOG_REMOTE_URL), nil while it is unset and nothing is sent.
|
// (SWWAF_LOG_REMOTE_URL), nil while it is unset and nothing is sent.
|
||||||
// LogRemoteTLSCAs are the certificates a syslog+tls endpoint's
|
// LogRemoteTLSCAs are the certificates a syslog+tls endpoint's
|
||||||
// certificate must chain to (SWW_LOG_REMOTE_TLS_CA_FILE), nil while
|
// certificate must chain to (SWWAF_LOG_REMOTE_TLS_CA_FILE), nil while
|
||||||
// it is unset and the host's own are used. LogRemoteBuffer is the most
|
// it is unset and the host's own are used. LogRemoteBuffer is the most
|
||||||
// lines held while they wait to be sent (SWW_LOG_REMOTE_BUFFER).
|
// lines held while they wait to be sent (SWWAF_LOG_REMOTE_BUFFER).
|
||||||
// LogRemoteFacility is the number of the syslog facility
|
// LogRemoteFacility is the number of the syslog facility
|
||||||
// (SWW_LOG_REMOTE_FACILITY), and LogRemoteAppName the APP-NAME
|
// (SWWAF_LOG_REMOTE_FACILITY), and LogRemoteAppName the APP-NAME
|
||||||
// (SWW_LOG_REMOTE_APP_NAME, by default InstanceName), of the records
|
// (SWWAF_LOG_REMOTE_APP_NAME, by default InstanceName), of the records
|
||||||
// the lines are sent in.
|
// the lines are sent in.
|
||||||
LogRemoteURL *url.URL
|
LogRemoteURL *url.URL
|
||||||
LogRemoteTLSCAs *x509.CertPool
|
LogRemoteTLSCAs *x509.CertPool
|
||||||
@@ -253,18 +250,18 @@ type Config struct {
|
|||||||
LogRemoteFacility int
|
LogRemoteFacility int
|
||||||
LogRemoteAppName string
|
LogRemoteAppName string
|
||||||
// AlertWebhookURL is where each alert is posted as JSON
|
// AlertWebhookURL is where each alert is posted as JSON
|
||||||
// (SWW_ALERT_WEBHOOK_URL), nil while it is unset. AlertWebhookHeaders
|
// (SWWAF_ALERT_WEBHOOK_URL), nil while it is unset. AlertWebhookHeaders
|
||||||
// are sent with each (SWW_ALERT_WEBHOOK_HEADERS).
|
// are sent with each (SWWAF_ALERT_WEBHOOK_HEADERS).
|
||||||
// AlertSlackWebhookURL is the Slack incoming webhook each alert is
|
// AlertSlackWebhookURL is the Slack incoming webhook each alert is
|
||||||
// posted to as a message (SWW_ALERT_SLACK_WEBHOOK_URL), and
|
// posted to as a message (SWWAF_ALERT_SLACK_WEBHOOK_URL), and
|
||||||
// AlertNtfyURL the ntfy topic each is published to
|
// AlertNtfyURL the ntfy topic each is published to
|
||||||
// (SWW_ALERT_NTFY_URL), each nil while it is unset; AlertNtfyToken,
|
// (SWWAF_ALERT_NTFY_URL), each nil while it is unset; AlertNtfyToken,
|
||||||
// unless empty, is sent to ntfy with each (SWW_ALERT_NTFY_TOKEN).
|
// unless empty, is sent to ntfy with each (SWWAF_ALERT_NTFY_TOKEN).
|
||||||
// With none of the three URLs set, no alert is sent. AlertEvents are
|
// With none of the three URLs set, no alert is sent. AlertEvents are
|
||||||
// the events alerts are sent for (SWW_ALERT_EVENTS). A repeat of an
|
// the events alerts are sent for (SWWAF_ALERT_EVENTS). A repeat of an
|
||||||
// alert within AlertCooldown is held back (SWW_ALERT_COOLDOWN), and
|
// alert within AlertCooldown is held back (SWWAF_ALERT_COOLDOWN), and
|
||||||
// so is an alert past AlertMaxPerHour in an hour, for the hour's
|
// so is an alert past AlertMaxPerHour in an hour, for the hour's
|
||||||
// summary (SWW_ALERT_MAX_PER_HOUR); 0 is off for both.
|
// summary (SWWAF_ALERT_MAX_PER_HOUR); 0 is off for both.
|
||||||
AlertWebhookURL *url.URL
|
AlertWebhookURL *url.URL
|
||||||
AlertWebhookHeaders http.Header
|
AlertWebhookHeaders http.Header
|
||||||
AlertSlackWebhookURL *url.URL
|
AlertSlackWebhookURL *url.URL
|
||||||
@@ -274,14 +271,14 @@ type Config struct {
|
|||||||
AlertCooldown time.Duration
|
AlertCooldown time.Duration
|
||||||
AlertMaxPerHour int
|
AlertMaxPerHour int
|
||||||
// The anomaly thresholds, which only raise alerts: the most requests
|
// The anomaly thresholds, which only raise alerts: the most requests
|
||||||
// and bytes a minute and an hour per client (SWW_ANOMALY_CLIENT_*),
|
// and bytes a minute and an hour per client (SWWAF_ANOMALY_CLIENT_*),
|
||||||
// per netblock around a client (SWW_ANOMALY_NET_*), per AS number
|
// per netblock around a client (SWWAF_ANOMALY_NET_*), per AS number
|
||||||
// (SWW_ANOMALY_ASN_*), for the whole service (SWW_ANOMALY_TOTAL_*)
|
// (SWWAF_ANOMALY_ASN_*), for the whole service (SWWAF_ANOMALY_TOTAL_*)
|
||||||
// and per named netblock (SWW_WATCH_*), each 0 while it is off.
|
// and per named netblock (SWWAF_WATCH_*), each 0 while it is off.
|
||||||
// AnomalyNetV4Prefix and AnomalyNetV6Prefix are the lengths of the
|
// AnomalyNetV4Prefix and AnomalyNetV6Prefix are the lengths of the
|
||||||
// netblock around a client (SWW_ANOMALY_NET_V4_PREFIX and
|
// netblock around a client (SWWAF_ANOMALY_NET_V4_PREFIX and
|
||||||
// SWW_ANOMALY_NET_V6_PREFIX), and WatchNets the named netblocks
|
// SWWAF_ANOMALY_NET_V6_PREFIX), and WatchNets the named netblocks
|
||||||
// (SWW_WATCH_NETS).
|
// (SWWAF_WATCH_NETS).
|
||||||
AnomalyClient anomaly.Thresholds
|
AnomalyClient anomaly.Thresholds
|
||||||
AnomalyNet anomaly.Thresholds
|
AnomalyNet anomaly.Thresholds
|
||||||
AnomalyASN anomaly.Thresholds
|
AnomalyASN anomaly.Thresholds
|
||||||
@@ -300,7 +297,7 @@ type Config struct {
|
|||||||
// off.
|
// off.
|
||||||
const off = "off"
|
const off = "off"
|
||||||
|
|
||||||
// The values of SWW_WAF_MODE.
|
// The values of SWWAF_WAF_MODE.
|
||||||
const (
|
const (
|
||||||
// WAFModeOff runs no request through the Core Rule Set.
|
// WAFModeOff runs no request through the Core Rule Set.
|
||||||
WAFModeOff = off
|
WAFModeOff = off
|
||||||
@@ -310,8 +307,8 @@ const (
|
|||||||
WAFModeBlock = "block"
|
WAFModeBlock = "block"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fileSource is the SWW_LOOKUP_SOURCE that looks clients up in the
|
// fileSource is the SWWAF_LOOKUP_SOURCE that looks clients up in the
|
||||||
// lookup database, the file SWW_LOOKUP_DB_PATH names.
|
// lookup database, the file SWWAF_LOOKUP_DB_PATH names.
|
||||||
const fileSource = "file"
|
const fileSource = "file"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -321,7 +318,7 @@ const (
|
|||||||
gibibyte = 1 << 30
|
gibibyte = 1 << 30
|
||||||
ipv4Bits = 32
|
ipv4Bits = 32
|
||||||
ipv6Bits = 128
|
ipv6Bits = 128
|
||||||
// minIPv6GroupPrefix is the shortest SWW_IPV6_GROUP_PREFIX, the
|
// minIPv6GroupPrefix is the shortest SWWAF_IPV6_GROUP_PREFIX, the
|
||||||
// netblock a provider is usually given: a shorter one would make one
|
// netblock a provider is usually given: a shorter one would make one
|
||||||
// client of the customers of several providers.
|
// client of the customers of several providers.
|
||||||
minIPv6GroupPrefix = 32
|
minIPv6GroupPrefix = 32
|
||||||
@@ -338,7 +335,7 @@ const (
|
|||||||
// masked is what the log shows for a token that is set, and in place of
|
// masked is what the log shows for a token that is set, and in place of
|
||||||
// a secret in another setting.
|
// a secret in another setting.
|
||||||
masked = "********"
|
masked = "********"
|
||||||
// defaultListenAddr is the default of SWW_LISTEN_ADDR and
|
// defaultListenAddr is the default of SWWAF_LISTEN_ADDR and
|
||||||
// SWRP_LISTEN_ADDR, and defaultUpstreamURL that of SWRP_UPSTREAM_URL.
|
// SWRP_LISTEN_ADDR, and defaultUpstreamURL that of SWRP_UPSTREAM_URL.
|
||||||
defaultListenAddr = ":8080"
|
defaultListenAddr = ":8080"
|
||||||
defaultUpstreamURL = "http://127.0.0.1:8081"
|
defaultUpstreamURL = "http://127.0.0.1:8081"
|
||||||
@@ -367,7 +364,7 @@ var (
|
|||||||
errHeaderTakenOut = errors.New(
|
errHeaderTakenOut = errors.New(
|
||||||
"is taken out of every request by Go's HTTP server, so it can never " +
|
"is taken out of every request by Go's HTTP server, so it can never " +
|
||||||
"be logged")
|
"be logged")
|
||||||
errOnBothLists = errors.New("is in SWW_DENIED_COUNTRIES too")
|
errOnBothLists = errors.New("is in SWWAF_DENIED_COUNTRIES too")
|
||||||
errNotLookupSource = errors.New("is not geojs, file or off")
|
errNotLookupSource = errors.New("is not geojs, file or off")
|
||||||
errNeedsLookups = errors.New("it needs each client looked up")
|
errNeedsLookups = errors.New("it needs each client looked up")
|
||||||
errNeedsDBPath = errors.New("it names the file to look clients up in")
|
errNeedsDBPath = errors.New("it names the file to look clients up in")
|
||||||
@@ -435,7 +432,7 @@ var (
|
|||||||
errNotListURL = errors.New(
|
errNotListURL = errors.New(
|
||||||
"is not an http or https URL without a user or a fragment, " +
|
"is not an http or https URL without a user or a fragment, " +
|
||||||
"such as https://www.spamhaus.org/drop/drop.txt")
|
"such as https://www.spamhaus.org/drop/drop.txt")
|
||||||
errInBlocklistURLs = errors.New("is in SWW_BLOCKLIST_URLS too")
|
errInBlocklistURLs = errors.New("is in SWWAF_BLOCKLIST_URLS too")
|
||||||
errNotAnHourOrMore = errors.New("is not a duration of 1h or more, such as 24h")
|
errNotAnHourOrMore = errors.New("is not a duration of 1h or more, such as 24h")
|
||||||
errNotAction = errors.New(
|
errNotAction = errors.New(
|
||||||
"is not deny, limit:<percent> such as limit:25, or log")
|
"is not deny, limit:<percent> such as limit:25, or log")
|
||||||
@@ -450,7 +447,7 @@ var (
|
|||||||
errNeedsLAPIKey = errors.New("the engine answers no request without it")
|
errNeedsLAPIKey = errors.New("the engine answers no request without it")
|
||||||
errLAPIKeyUnused = errors.New("it is sent only to the engine at that URL")
|
errLAPIKeyUnused = errors.New("it is sent only to the engine at that URL")
|
||||||
errAnotherList = errors.New(
|
errAnotherList = errors.New(
|
||||||
"is in SWW_BLOCKLIST_URLS or is SWW_ASN_LIMIT_PERCENT_URL too")
|
"is in SWWAF_BLOCKLIST_URLS or is SWWAF_ASN_LIMIT_PERCENT_URL too")
|
||||||
)
|
)
|
||||||
|
|
||||||
// FromEnvironment reads the settings with lookupEnv, normally
|
// FromEnvironment reads the settings with lookupEnv, normally
|
||||||
@@ -463,104 +460,102 @@ var (
|
|||||||
func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
|
func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
|
||||||
env := &environment{lookupEnv: lookupEnv}
|
env := &environment{lookupEnv: lookupEnv}
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
ListenAddr: env.address("SWW_LISTEN_ADDR"),
|
ListenAddr: env.address("SWWAF_LISTEN_ADDR"),
|
||||||
InstanceName: env.instanceName(),
|
InstanceName: env.instanceName(),
|
||||||
Observe: env.observe("SWW_MODE", "enforce"),
|
Observe: env.observe("SWWAF_MODE", "enforce"),
|
||||||
IPv6GroupPrefix: env.ipv6GroupPrefix("SWW_IPV6_GROUP_PREFIX", "64"),
|
IPv6GroupPrefix: env.ipv6GroupPrefix("SWWAF_IPV6_GROUP_PREFIX", "64"),
|
||||||
MaxTrackedClients: env.numberNotOff("SWW_MAX_TRACKED_CLIENTS", "20000"),
|
MaxTrackedClients: env.numberNotOff("SWWAF_MAX_TRACKED_CLIENTS", "20000"),
|
||||||
AllowNets: env.netblocks("SWW_ALLOW_NETS", ""),
|
AllowNets: env.netblocks("SWWAF_ALLOW_NETS", ""),
|
||||||
RateLimitExemptNets: env.netblocks("SWW_RATE_LIMIT_EXEMPT_NETS", ""),
|
RateLimitExemptNets: env.netblocks("SWWAF_RATE_LIMIT_EXEMPT_NETS", ""),
|
||||||
DenyNets: env.netblocks("SWW_DENY_NETS", ""),
|
DenyNets: env.netblocks("SWWAF_DENY_NETS", ""),
|
||||||
RateLimitPerMinute: env.count("SWW_RATE_LIMIT_PER_MINUTE", "120"),
|
RateLimitPerMinute: env.count("SWWAF_RATE_LIMIT_PER_MINUTE", "120"),
|
||||||
RateLimitPerHour: env.count("SWW_RATE_LIMIT_PER_HOUR", "1500"),
|
RateLimitPerHour: env.count("SWWAF_RATE_LIMIT_PER_HOUR", "1500"),
|
||||||
RateLimitPerDay: env.count("SWW_RATE_LIMIT_PER_DAY", "6000"),
|
RateLimitPerDay: env.count("SWWAF_RATE_LIMIT_PER_DAY", "6000"),
|
||||||
RateLimitExemptPaths: env.pathPrefixes("SWW_RATE_LIMIT_EXEMPT_PATHS", ""),
|
RateLimitExemptPaths: env.pathPrefixes("SWWAF_RATE_LIMIT_EXEMPT_PATHS", ""),
|
||||||
BytesLimitPerMinute: env.size("SWW_BYTES_LIMIT_PER_MINUTE", "off"),
|
BytesLimitPerMinute: env.size("SWWAF_BYTES_LIMIT_PER_MINUTE", "off"),
|
||||||
BytesLimitPerHour: env.size("SWW_BYTES_LIMIT_PER_HOUR", "50G"),
|
BytesLimitPerHour: env.size("SWWAF_BYTES_LIMIT_PER_HOUR", "50G"),
|
||||||
BytesLimitPerDay: env.size("SWW_BYTES_LIMIT_PER_DAY", "100G"),
|
BytesLimitPerDay: env.size("SWWAF_BYTES_LIMIT_PER_DAY", "100G"),
|
||||||
BytesCount: env.bytesCount("SWW_BYTES_COUNT", "both"),
|
BytesCount: env.bytesCount("SWWAF_BYTES_COUNT", "both"),
|
||||||
LookupSource: env.lookupSource("SWW_LOOKUP_SOURCE", "geojs"),
|
LookupSource: env.lookupSource("SWWAF_LOOKUP_SOURCE", "geojs"),
|
||||||
LookupDBPath: env.value("SWW_LOOKUP_DB_PATH", ""),
|
LookupDBPath: env.value("SWWAF_LOOKUP_DB_PATH", ""),
|
||||||
LookupTimeout: env.durationNotOff("SWW_LOOKUP_TIMEOUT", "800ms"),
|
LookupTimeout: env.durationNotOff("SWWAF_LOOKUP_TIMEOUT", "800ms"),
|
||||||
AddLookupHeaders: env.boolean("SWW_ADD_LOOKUP_HEADERS", "false"),
|
AddLookupHeaders: env.boolean("SWWAF_ADD_LOOKUP_HEADERS", "false"),
|
||||||
DeniedCountries: env.countries("SWW_DENIED_COUNTRIES", ""),
|
DeniedCountries: env.countries("SWWAF_DENIED_COUNTRIES", ""),
|
||||||
ExclusivelyAllowedCountries: env.countries(
|
ExclusivelyAllowedCountries: env.countries(
|
||||||
"SWW_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
||||||
ASNLimitPercent: env.percents("SWW_ASN_LIMIT_PERCENT", ParseASN),
|
ASNLimitPercent: env.percents("SWWAF_ASN_LIMIT_PERCENT", ParseASN),
|
||||||
CountryLimitPercent: env.percents("SWW_COUNTRY_LIMIT_PERCENT", parseCountry),
|
CountryLimitPercent: env.percents("SWWAF_COUNTRY_LIMIT_PERCENT", parseCountry),
|
||||||
ASNBytesPercent: env.percents("SWW_ASN_BYTES_PERCENT", ParseASN),
|
ASNBytesPercent: env.percents("SWWAF_ASN_BYTES_PERCENT", ParseASN),
|
||||||
CountryBytesPercent: env.percents("SWW_COUNTRY_BYTES_PERCENT", parseCountry),
|
CountryBytesPercent: env.percents("SWWAF_COUNTRY_BYTES_PERCENT", parseCountry),
|
||||||
UnknownLimitPercent: env.percent("SWW_UNKNOWN_LIMIT_PERCENT", "100"),
|
UnknownLimitPercent: env.percent("SWWAF_UNKNOWN_LIMIT_PERCENT", "100"),
|
||||||
ASNLimitPercentURL: env.listURL("SWW_ASN_LIMIT_PERCENT_URL"),
|
ASNLimitPercentURL: env.listURL("SWWAF_ASN_LIMIT_PERCENT_URL"),
|
||||||
BlocklistURLs: env.listURLs("SWW_BLOCKLIST_URLS"),
|
BlocklistURLs: env.listURLs("SWWAF_BLOCKLIST_URLS"),
|
||||||
BlocklistRefresh: env.refresh("SWW_BLOCKLIST_REFRESH", "24h"),
|
BlocklistRefresh: env.refresh("SWWAF_BLOCKLIST_REFRESH", "24h"),
|
||||||
DNSBLZones: env.zones("SWW_DNSBL_ZONES"),
|
DNSBLZones: env.zones("SWWAF_DNSBL_ZONES"),
|
||||||
DNSBLResolver: env.resolver("SWW_DNSBL_RESOLVER"),
|
DNSBLResolver: env.resolver("SWWAF_DNSBL_RESOLVER"),
|
||||||
AbuseIPDBKey: env.secret("SWW_ABUSEIPDB_KEY"),
|
AbuseIPDBKey: env.secret("SWWAF_ABUSEIPDB_KEY"),
|
||||||
AbuseIPDBMinScore: env.percent("SWW_ABUSEIPDB_MIN_SCORE", "75"),
|
AbuseIPDBMinScore: env.percent("SWWAF_ABUSEIPDB_MIN_SCORE", "75"),
|
||||||
AbuseIPDBDailyBudget: env.numberNotOff("SWW_ABUSEIPDB_DAILY_BUDGET", "900"),
|
AbuseIPDBDailyBudget: env.numberNotOff("SWWAF_ABUSEIPDB_DAILY_BUDGET", "900"),
|
||||||
ReputationCacheTTL: env.durationNotOff("SWW_REPUTATION_CACHE_TTL", "24h"),
|
ReputationCacheTTL: env.durationNotOff("SWWAF_REPUTATION_CACHE_TTL", "24h"),
|
||||||
ReputationTimeout: env.durationNotOff("SWW_REPUTATION_TIMEOUT", "2s"),
|
ReputationTimeout: env.durationNotOff("SWWAF_REPUTATION_TIMEOUT", "2s"),
|
||||||
CrowdSecDecisionsURL: env.crowdSecDecisionsURL("SWW_CROWDSEC_LAPI_URL"),
|
CrowdSecDecisionsURL: env.crowdSecDecisionsURL("SWWAF_CROWDSEC_LAPI_URL"),
|
||||||
CrowdSecKey: env.secret("SWW_CROWDSEC_LAPI_KEY"),
|
CrowdSecKey: env.secret("SWWAF_CROWDSEC_LAPI_KEY"),
|
||||||
BanResponse: env.banResponse("SWW_BAN_RESPONSE", "403"),
|
BanResponse: env.banResponse("SWWAF_BAN_RESPONSE", "403"),
|
||||||
LimitBanDuration: env.durationNotOff("SWW_LIMIT_BAN_DURATION", "1h"),
|
LimitBanDuration: env.durationNotOff("SWWAF_LIMIT_BAN_DURATION", "1h"),
|
||||||
LimitBanRepeatWindow: env.durationNotOff("SWW_LIMIT_BAN_REPEAT_WINDOW", "24h"),
|
LimitBanRepeatWindow: env.durationNotOff("SWWAF_LIMIT_BAN_REPEAT_WINDOW", "24h"),
|
||||||
MaxBanDuration: env.durationNotOff("SWW_MAX_BAN_DURATION", "7d"),
|
MaxBanDuration: env.durationNotOff("SWWAF_MAX_BAN_DURATION", "7d"),
|
||||||
AttackBanDuration: env.durationNotOff("SWW_ATTACK_BAN_DURATION", "7d"),
|
AttackBanDuration: env.durationNotOff("SWWAF_ATTACK_BAN_DURATION", "7d"),
|
||||||
MaxBans: env.numberNotOff("SWW_MAX_BANS", "5000"),
|
MaxBans: env.numberNotOff("SWWAF_MAX_BANS", "5000"),
|
||||||
BanScopeV4Prefix: env.v4Prefix("SWW_BAN_SCOPE_V4_PREFIX", "32"),
|
BanScopeV4Prefix: env.v4Prefix("SWWAF_BAN_SCOPE_V4_PREFIX", "32"),
|
||||||
StateDir: env.absolutePath("SWW_STATE_DIR", "/var/lib/sww"),
|
StateDir: env.absolutePath("SWWAF_STATE_DIR", "/var/lib/sww"),
|
||||||
StateWriteDelay: env.durationNotOff("SWW_STATE_WRITE_DELAY", "10s"),
|
StateWriteDelay: env.durationNotOff("SWWAF_STATE_WRITE_DELAY", "10s"),
|
||||||
StateCounterInterval: env.durationNotOff("SWW_STATE_COUNTER_INTERVAL", "15m"),
|
StateCounterInterval: env.durationNotOff("SWWAF_STATE_COUNTER_INTERVAL", "15m"),
|
||||||
LogRequestHeaders: env.headerNames("SWW_LOG_REQUEST_HEADERS",
|
LogRequestHeaders: env.headerNames("SWWAF_LOG_REQUEST_HEADERS",
|
||||||
"accept,accept-language,accept-encoding,content-type,origin,range"),
|
"accept,accept-language,accept-encoding,content-type,origin,range"),
|
||||||
LogLevel: env.logLevel("SWW_LOG_LEVEL", "info"),
|
LogLevel: env.logLevel("SWWAF_LOG_LEVEL", "info"),
|
||||||
AdminToken: env.token("SWW_ADMIN_TOKEN"),
|
AdminToken: env.token("SWWAF_ADMIN_TOKEN"),
|
||||||
MetricsToken: env.token("SWW_METRICS_TOKEN"),
|
MetricsToken: env.token("SWWAF_METRICS_TOKEN"),
|
||||||
MetricsTopN: env.numberNotOff("SWW_METRICS_TOP_N", "50"),
|
MetricsTopN: env.numberNotOff("SWWAF_METRICS_TOP_N", "50"),
|
||||||
RulesDir: env.value("SWW_RULES_DIR", "/etc/sww/rules.d"),
|
RulesDir: env.value("SWWAF_RULES_DIR", "/etc/sww/rules.d"),
|
||||||
RulesEnabled: env.boolean("SWW_RULES_ENABLED", "true"),
|
RulesEnabled: env.boolean("SWWAF_RULES_ENABLED", "true"),
|
||||||
WAFMode: env.wafMode("SWW_WAF_MODE", WAFModeBlock),
|
WAFMode: env.wafMode("SWWAF_WAF_MODE", WAFModeBlock),
|
||||||
WAFParanoiaLevel: env.paranoiaLevel("SWW_WAF_PARANOIA_LEVEL", "1"),
|
WAFParanoiaLevel: env.paranoiaLevel("SWWAF_WAF_PARANOIA_LEVEL", "1"),
|
||||||
WAFAnomalyThreshold: env.numberOrOff("SWW_WAF_ANOMALY_THRESHOLD", "5"),
|
WAFAnomalyThreshold: env.numberOrOff("SWWAF_WAF_ANOMALY_THRESHOLD", "5"),
|
||||||
WAFDisabledRules: env.ruleIDs("SWW_WAF_DISABLED_RULES",
|
WAFDisabledRules: env.ruleIDs("SWWAF_WAF_DISABLED_RULES",
|
||||||
"920340,920420,920440,920640,930130,930140"),
|
"920340,920420,920440,920640,930130,930140"),
|
||||||
WAFExemptPaths: env.pathPrefixes("SWW_WAF_EXEMPT_PATHS", ""),
|
WAFExemptPaths: env.pathPrefixes("SWWAF_WAF_EXEMPT_PATHS", ""),
|
||||||
WAFBodyLimit: env.wafBodyLimit("SWW_WAF_BODY_LIMIT", off),
|
WAFBodyLimit: env.wafBodyLimit("SWWAF_WAF_BODY_LIMIT", off),
|
||||||
CoreRuleSetExclusionsFile: env.filePath(
|
TrapPaths: env.trapPaths("SWWAF_TRAP_PATHS"),
|
||||||
"SWW_CORE_RULE_SET_EXCLUSIONS_FILE"),
|
ErrorBurstThreshold: env.count("SWWAF_ERROR_BURST_THRESHOLD", "30"),
|
||||||
TrapPaths: env.trapPaths("SWW_TRAP_PATHS"),
|
LogRemoteURL: env.logRemoteURL("SWWAF_LOG_REMOTE_URL"),
|
||||||
ErrorBurstThreshold: env.count("SWW_ERROR_BURST_THRESHOLD", "30"),
|
LogRemoteTLSCAs: env.certificates("SWWAF_LOG_REMOTE_TLS_CA_FILE"),
|
||||||
LogRemoteURL: env.logRemoteURL("SWW_LOG_REMOTE_URL"),
|
LogRemoteBuffer: env.numberNotOff("SWWAF_LOG_REMOTE_BUFFER", "10000"),
|
||||||
LogRemoteTLSCAs: env.certificates("SWW_LOG_REMOTE_TLS_CA_FILE"),
|
LogRemoteFacility: env.facility("SWWAF_LOG_REMOTE_FACILITY", "local0"),
|
||||||
LogRemoteBuffer: env.numberNotOff("SWW_LOG_REMOTE_BUFFER", "10000"),
|
AlertWebhookURL: env.webhookURL("SWWAF_ALERT_WEBHOOK_URL"),
|
||||||
LogRemoteFacility: env.facility("SWW_LOG_REMOTE_FACILITY", "local0"),
|
AlertWebhookHeaders: env.webhookHeaders("SWWAF_ALERT_WEBHOOK_HEADERS"),
|
||||||
AlertWebhookURL: env.webhookURL("SWW_ALERT_WEBHOOK_URL"),
|
AlertSlackWebhookURL: env.webhookURL("SWWAF_ALERT_SLACK_WEBHOOK_URL"),
|
||||||
AlertWebhookHeaders: env.webhookHeaders("SWW_ALERT_WEBHOOK_HEADERS"),
|
AlertNtfyURL: env.webhookURL("SWWAF_ALERT_NTFY_URL"),
|
||||||
AlertSlackWebhookURL: env.webhookURL("SWW_ALERT_SLACK_WEBHOOK_URL"),
|
AlertNtfyToken: env.secret("SWWAF_ALERT_NTFY_TOKEN"),
|
||||||
AlertNtfyURL: env.webhookURL("SWW_ALERT_NTFY_URL"),
|
AlertEvents: env.alertEvents("SWWAF_ALERT_EVENTS",
|
||||||
AlertNtfyToken: env.secret("SWW_ALERT_NTFY_TOKEN"),
|
|
||||||
AlertEvents: env.alertEvents("SWW_ALERT_EVENTS",
|
|
||||||
strings.Join(alerts.Events(), ",")),
|
strings.Join(alerts.Events(), ",")),
|
||||||
AlertCooldown: env.duration("SWW_ALERT_COOLDOWN", "15m"),
|
AlertCooldown: env.duration("SWWAF_ALERT_COOLDOWN", "15m"),
|
||||||
AlertMaxPerHour: env.numberOrOff("SWW_ALERT_MAX_PER_HOUR", "60"),
|
AlertMaxPerHour: env.numberOrOff("SWWAF_ALERT_MAX_PER_HOUR", "60"),
|
||||||
AnomalyClient: env.thresholds("SWW_ANOMALY_CLIENT_"),
|
AnomalyClient: env.thresholds("SWWAF_ANOMALY_CLIENT_"),
|
||||||
AnomalyNet: env.thresholds("SWW_ANOMALY_NET_"),
|
AnomalyNet: env.thresholds("SWWAF_ANOMALY_NET_"),
|
||||||
AnomalyASN: env.thresholds("SWW_ANOMALY_ASN_"),
|
AnomalyASN: env.thresholds("SWWAF_ANOMALY_ASN_"),
|
||||||
AnomalyTotal: env.thresholds("SWW_ANOMALY_TOTAL_"),
|
AnomalyTotal: env.thresholds("SWWAF_ANOMALY_TOTAL_"),
|
||||||
AnomalyWatch: env.thresholds("SWW_WATCH_"),
|
AnomalyWatch: env.thresholds("SWWAF_WATCH_"),
|
||||||
AnomalyNetV4Prefix: env.v4Prefix("SWW_ANOMALY_NET_V4_PREFIX", "24"),
|
AnomalyNetV4Prefix: env.v4Prefix("SWWAF_ANOMALY_NET_V4_PREFIX", "24"),
|
||||||
AnomalyNetV6Prefix: env.v6Prefix("SWW_ANOMALY_NET_V6_PREFIX", "48"),
|
AnomalyNetV6Prefix: env.v6Prefix("SWWAF_ANOMALY_NET_V6_PREFIX", "48"),
|
||||||
WatchNets: env.namedNetblocks("SWW_WATCH_NETS"),
|
WatchNets: env.namedNetblocks("SWWAF_WATCH_NETS"),
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.LogRemoteAppName = env.appName("SWW_LOG_REMOTE_APP_NAME",
|
cfg.LogRemoteAppName = env.appName("SWWAF_LOG_REMOTE_APP_NAME",
|
||||||
cfg.InstanceName, cfg.LogRemoteURL != nil)
|
cfg.InstanceName, cfg.LogRemoteURL != nil)
|
||||||
cfg.BlocklistAction, cfg.BlocklistLimitPercent = env.action(
|
cfg.BlocklistAction, cfg.BlocklistLimitPercent = env.action(
|
||||||
"SWW_BLOCKLIST_ACTION", "deny")
|
"SWWAF_BLOCKLIST_ACTION", "deny")
|
||||||
cfg.ReputationAction, cfg.ReputationLimitPercent = env.action(
|
cfg.ReputationAction, cfg.ReputationLimitPercent = env.action(
|
||||||
"SWW_REPUTATION_ACTION", "limit:25")
|
"SWWAF_REPUTATION_ACTION", "limit:25")
|
||||||
|
|
||||||
env.checkInstanceNameForNtfy(cfg.InstanceName, cfg.AlertNtfyURL != nil)
|
env.checkInstanceNameForNtfy(cfg.InstanceName, cfg.AlertNtfyURL != nil)
|
||||||
env.checkLookupDBPath(cfg)
|
env.checkLookupDBPath(cfg)
|
||||||
@@ -593,17 +588,17 @@ func Command(lookupEnv func(string) (string, bool)) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiscriminatorListenAddr reads only SWW_LISTEN_ADDR, which may be given as a
|
// DiscriminatorListenAddr reads only SWWAF_LISTEN_ADDR, which may be given as a
|
||||||
// file, as FromEnvironment does. The health check in discriminator's container needs
|
// file, as FromEnvironment does. The health check in discriminator's container needs
|
||||||
// no other setting, so it reads no other, nor a file that another names.
|
// no other setting, so it reads no other, nor a file that another names.
|
||||||
func DiscriminatorListenAddr(lookupEnv func(string) (string, bool)) (string, error) {
|
func DiscriminatorListenAddr(lookupEnv func(string) (string, bool)) (string, error) {
|
||||||
env := &environment{lookupEnv: lookupEnv}
|
env := &environment{lookupEnv: lookupEnv}
|
||||||
listenAddr := env.address("SWW_LISTEN_ADDR")
|
listenAddr := env.address("SWWAF_LISTEN_ADDR")
|
||||||
|
|
||||||
return listenAddr, env.err
|
return listenAddr, env.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstanceName reads only SWW_INSTANCE_NAME, which may be given as a
|
// InstanceName reads only SWWAF_INSTANCE_NAME, which may be given as a
|
||||||
// file, as FromEnvironment does, so that the line saying a setting is
|
// file, as FromEnvironment does, so that the line saying a setting is
|
||||||
// invalid carries it too. A file that cannot be read gives the default
|
// invalid carries it too. A file that cannot be read gives the default
|
||||||
// here, and FromEnvironment then stops the start over it.
|
// here, and FromEnvironment then stops the start over it.
|
||||||
@@ -961,30 +956,30 @@ func (e *environment) lookupSource(name, defaultValue string) string {
|
|||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkLookupDBPath refuses SWW_LOOKUP_SOURCE=file without
|
// checkLookupDBPath refuses SWWAF_LOOKUP_SOURCE=file without
|
||||||
// SWW_LOOKUP_DB_PATH, and SWW_LOOKUP_DB_PATH with any other source:
|
// SWWAF_LOOKUP_DB_PATH, and SWWAF_LOOKUP_DB_PATH with any other source:
|
||||||
// one source at a time.
|
// one source at a time.
|
||||||
func (e *environment) checkLookupDBPath(cfg *Config) {
|
func (e *environment) checkLookupDBPath(cfg *Config) {
|
||||||
switch {
|
switch {
|
||||||
case cfg.LookupSource == fileSource && cfg.LookupDBPath == "":
|
case cfg.LookupSource == fileSource && cfg.LookupDBPath == "":
|
||||||
e.check("SWW_LOOKUP_SOURCE", fmt.Errorf(
|
e.check("SWWAF_LOOKUP_SOURCE", fmt.Errorf(
|
||||||
"is file while SWW_LOOKUP_DB_PATH is unset; %w", errNeedsDBPath))
|
"is file while SWWAF_LOOKUP_DB_PATH is unset; %w", errNeedsDBPath))
|
||||||
case cfg.LookupSource != fileSource && cfg.LookupDBPath != "":
|
case cfg.LookupSource != fileSource && cfg.LookupDBPath != "":
|
||||||
e.check("SWW_LOOKUP_DB_PATH", fmt.Errorf(
|
e.check("SWWAF_LOOKUP_DB_PATH", fmt.Errorf(
|
||||||
"is set while SWW_LOOKUP_SOURCE is %s; %w", cfg.LookupSource, errDBPathUnused))
|
"is set while SWWAF_LOOKUP_SOURCE is %s; %w", cfg.LookupSource, errDBPathUnused))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkCountriesAndLookups refuses a country on both country lists, and,
|
// checkCountriesAndLookups refuses a country on both country lists, and,
|
||||||
// while SWW_LOOKUP_SOURCE is off, each setting that needs clients looked
|
// while SWWAF_LOOKUP_SOURCE is off, each setting that needs clients looked
|
||||||
// up: the country lists, SWW_ADD_LOOKUP_HEADERS, the biased thresholds,
|
// up: the country lists, SWWAF_ADD_LOOKUP_HEADERS, the biased thresholds,
|
||||||
// SWW_ASN_LIMIT_PERCENT_URL among them, of which
|
// SWWAF_ASN_LIMIT_PERCENT_URL among them, of which
|
||||||
// SWW_UNKNOWN_LIMIT_PERCENT needs them only below 100, where it lowers a
|
// SWWAF_UNKNOWN_LIMIT_PERCENT needs them only below 100, where it lowers a
|
||||||
// limit, and the anomaly thresholds per AS number.
|
// limit, and the anomaly thresholds per AS number.
|
||||||
func (e *environment) checkCountriesAndLookups(cfg *Config) {
|
func (e *environment) checkCountriesAndLookups(cfg *Config) {
|
||||||
for _, country := range cfg.ExclusivelyAllowedCountries {
|
for _, country := range cfg.ExclusivelyAllowedCountries {
|
||||||
if slices.Contains(cfg.DeniedCountries, country) {
|
if slices.Contains(cfg.DeniedCountries, country) {
|
||||||
e.check("SWW_EXCLUSIVELY_ALLOWED_COUNTRIES",
|
e.check("SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES",
|
||||||
fmt.Errorf("%q %w", country, errOnBothLists))
|
fmt.Errorf("%q %w", country, errOnBothLists))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -997,52 +992,52 @@ func (e *environment) checkCountriesAndLookups(cfg *Config) {
|
|||||||
name string
|
name string
|
||||||
set bool
|
set bool
|
||||||
}{
|
}{
|
||||||
{"SWW_DENIED_COUNTRIES", len(cfg.DeniedCountries) > 0},
|
{"SWWAF_DENIED_COUNTRIES", len(cfg.DeniedCountries) > 0},
|
||||||
{"SWW_EXCLUSIVELY_ALLOWED_COUNTRIES", len(cfg.ExclusivelyAllowedCountries) > 0},
|
{"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES", len(cfg.ExclusivelyAllowedCountries) > 0},
|
||||||
{"SWW_ADD_LOOKUP_HEADERS", cfg.AddLookupHeaders},
|
{"SWWAF_ADD_LOOKUP_HEADERS", cfg.AddLookupHeaders},
|
||||||
{"SWW_ASN_LIMIT_PERCENT", len(cfg.ASNLimitPercent) > 0},
|
{"SWWAF_ASN_LIMIT_PERCENT", len(cfg.ASNLimitPercent) > 0},
|
||||||
{"SWW_COUNTRY_LIMIT_PERCENT", len(cfg.CountryLimitPercent) > 0},
|
{"SWWAF_COUNTRY_LIMIT_PERCENT", len(cfg.CountryLimitPercent) > 0},
|
||||||
{"SWW_ASN_BYTES_PERCENT", len(cfg.ASNBytesPercent) > 0},
|
{"SWWAF_ASN_BYTES_PERCENT", len(cfg.ASNBytesPercent) > 0},
|
||||||
{"SWW_COUNTRY_BYTES_PERCENT", len(cfg.CountryBytesPercent) > 0},
|
{"SWWAF_COUNTRY_BYTES_PERCENT", len(cfg.CountryBytesPercent) > 0},
|
||||||
{"SWW_UNKNOWN_LIMIT_PERCENT", cfg.UnknownLimitPercent < 100},
|
{"SWWAF_UNKNOWN_LIMIT_PERCENT", cfg.UnknownLimitPercent < 100},
|
||||||
{"SWW_ASN_LIMIT_PERCENT_URL", cfg.ASNLimitPercentURL != ""},
|
{"SWWAF_ASN_LIMIT_PERCENT_URL", cfg.ASNLimitPercentURL != ""},
|
||||||
{"SWW_ANOMALY_ASN_REQUESTS_PER_MINUTE", cfg.AnomalyASN.RequestsPerMinute > 0},
|
{"SWWAF_ANOMALY_ASN_REQUESTS_PER_MINUTE", cfg.AnomalyASN.RequestsPerMinute > 0},
|
||||||
{"SWW_ANOMALY_ASN_REQUESTS_PER_HOUR", cfg.AnomalyASN.RequestsPerHour > 0},
|
{"SWWAF_ANOMALY_ASN_REQUESTS_PER_HOUR", cfg.AnomalyASN.RequestsPerHour > 0},
|
||||||
{"SWW_ANOMALY_ASN_BYTES_PER_MINUTE", cfg.AnomalyASN.BytesPerMinute > 0},
|
{"SWWAF_ANOMALY_ASN_BYTES_PER_MINUTE", cfg.AnomalyASN.BytesPerMinute > 0},
|
||||||
{"SWW_ANOMALY_ASN_BYTES_PER_HOUR", cfg.AnomalyASN.BytesPerHour > 0},
|
{"SWWAF_ANOMALY_ASN_BYTES_PER_HOUR", cfg.AnomalyASN.BytesPerHour > 0},
|
||||||
} {
|
} {
|
||||||
if setting.set {
|
if setting.set {
|
||||||
e.check(setting.name, fmt.Errorf("is set while SWW_LOOKUP_SOURCE is off; %w",
|
e.check(setting.name, fmt.Errorf("is set while SWWAF_LOOKUP_SOURCE is off; %w",
|
||||||
errNeedsLookups))
|
errNeedsLookups))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkASNLimitPercentURL refuses SWW_ASN_LIMIT_PERCENT_URL naming a
|
// checkASNLimitPercentURL refuses SWWAF_ASN_LIMIT_PERCENT_URL naming a
|
||||||
// blocklist too: the file at a URL is fetched as one list or the other.
|
// blocklist too: the file at a URL is fetched as one list or the other.
|
||||||
func (e *environment) checkASNLimitPercentURL(cfg *Config) {
|
func (e *environment) checkASNLimitPercentURL(cfg *Config) {
|
||||||
if slices.Contains(cfg.BlocklistURLs, cfg.ASNLimitPercentURL) {
|
if slices.Contains(cfg.BlocklistURLs, cfg.ASNLimitPercentURL) {
|
||||||
e.check("SWW_ASN_LIMIT_PERCENT_URL",
|
e.check("SWWAF_ASN_LIMIT_PERCENT_URL",
|
||||||
fmt.Errorf("%q %w", cfg.ASNLimitPercentURL, errInBlocklistURLs))
|
fmt.Errorf("%q %w", cfg.ASNLimitPercentURL, errInBlocklistURLs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkCrowdSec refuses SWW_CROWDSEC_LAPI_URL without
|
// checkCrowdSec refuses SWWAF_CROWDSEC_LAPI_URL without
|
||||||
// SWW_CROWDSEC_LAPI_KEY, the key without the URL, and a decision list
|
// SWWAF_CROWDSEC_LAPI_KEY, the key without the URL, and a decision list
|
||||||
// that is fetched as another list too.
|
// that is fetched as another list too.
|
||||||
func (e *environment) checkCrowdSec(cfg *Config) {
|
func (e *environment) checkCrowdSec(cfg *Config) {
|
||||||
decisionsURL := cfg.CrowdSecDecisionsURL
|
decisionsURL := cfg.CrowdSecDecisionsURL
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case decisionsURL != "" && cfg.CrowdSecKey == "":
|
case decisionsURL != "" && cfg.CrowdSecKey == "":
|
||||||
e.check("SWW_CROWDSEC_LAPI_URL", fmt.Errorf(
|
e.check("SWWAF_CROWDSEC_LAPI_URL", fmt.Errorf(
|
||||||
"is set while SWW_CROWDSEC_LAPI_KEY is unset; %w", errNeedsLAPIKey))
|
"is set while SWWAF_CROWDSEC_LAPI_KEY is unset; %w", errNeedsLAPIKey))
|
||||||
case decisionsURL == "" && cfg.CrowdSecKey != "":
|
case decisionsURL == "" && cfg.CrowdSecKey != "":
|
||||||
e.check("SWW_CROWDSEC_LAPI_KEY", fmt.Errorf(
|
e.check("SWWAF_CROWDSEC_LAPI_KEY", fmt.Errorf(
|
||||||
"is set while SWW_CROWDSEC_LAPI_URL is unset; %w", errLAPIKeyUnused))
|
"is set while SWWAF_CROWDSEC_LAPI_URL is unset; %w", errLAPIKeyUnused))
|
||||||
case decisionsURL != "" && (slices.Contains(cfg.BlocklistURLs, decisionsURL) ||
|
case decisionsURL != "" && (slices.Contains(cfg.BlocklistURLs, decisionsURL) ||
|
||||||
decisionsURL == cfg.ASNLimitPercentURL):
|
decisionsURL == cfg.ASNLimitPercentURL):
|
||||||
e.check("SWW_CROWDSEC_LAPI_URL", fmt.Errorf("gives the decision list %q, which %w",
|
e.check("SWWAF_CROWDSEC_LAPI_URL", fmt.Errorf("gives the decision list %q, which %w",
|
||||||
decisionsURL, errAnotherList))
|
decisionsURL, errAnotherList))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1194,16 +1189,6 @@ func (e *environment) logRemoteURL(name string) *url.URL {
|
|||||||
return remote
|
return remote
|
||||||
}
|
}
|
||||||
|
|
||||||
// filePath reads a setting that is the path of a file, "" while it is
|
|
||||||
// unset. Its value names a file already, so, unlike the other settings,
|
|
||||||
// it has no _FILE form. Whoever uses the file reads it.
|
|
||||||
func (e *environment) filePath(name string) string {
|
|
||||||
path, _ := e.lookupEnv(name)
|
|
||||||
e.settings = append(e.settings, slog.String(name, path))
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// certificates reads a setting that is the path of a file of PEM
|
// certificates reads a setting that is the path of a file of PEM
|
||||||
// certificates. Unset or empty, it is nil. Its value names a file
|
// certificates. Unset or empty, it is nil. Its value names a file
|
||||||
// already, so, unlike the other settings, it has no _FILE form.
|
// already, so, unlike the other settings, it has no _FILE form.
|
||||||
@@ -1241,14 +1226,14 @@ func (e *environment) facility(name, defaultValue string) int {
|
|||||||
return number
|
return number
|
||||||
}
|
}
|
||||||
|
|
||||||
// instanceName reads SWW_INSTANCE_NAME, by default the host's name. It
|
// instanceName reads SWWAF_INSTANCE_NAME, by default the host's name. It
|
||||||
// must be valid UTF-8: the metrics library panics on a label that is not.
|
// must be valid UTF-8: the metrics library panics on a label that is not.
|
||||||
func (e *environment) instanceName() string {
|
func (e *environment) instanceName() string {
|
||||||
hostname, _ := os.Hostname() // "" when the host has no name to give
|
hostname, _ := os.Hostname() // "" when the host has no name to give
|
||||||
|
|
||||||
value := e.value("SWW_INSTANCE_NAME", hostname)
|
value := e.value("SWWAF_INSTANCE_NAME", hostname)
|
||||||
if !utf8.ValidString(value) {
|
if !utf8.ValidString(value) {
|
||||||
e.check("SWW_INSTANCE_NAME", fmt.Errorf("%q %w", value, errNotUTF8))
|
e.check("SWWAF_INSTANCE_NAME", fmt.Errorf("%q %w", value, errNotUTF8))
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
@@ -1270,7 +1255,7 @@ func (e *environment) appName(name, instanceName string, sending bool) string {
|
|||||||
case set:
|
case set:
|
||||||
e.check(name, fmt.Errorf("%q %w", value, errNotAppName))
|
e.check(name, fmt.Errorf("%q %w", value, errNotAppName))
|
||||||
case sending:
|
case sending:
|
||||||
e.check(name, fmt.Errorf("is unset, and SWW_INSTANCE_NAME %q, its default, %w",
|
e.check(name, fmt.Errorf("is unset, and SWWAF_INSTANCE_NAME %q, its default, %w",
|
||||||
value, errNotAppName))
|
value, errNotAppName))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1278,19 +1263,19 @@ func (e *environment) appName(name, instanceName string, sending bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkInstanceNameForNtfy refuses an instance name that holds a control
|
// checkInstanceNameForNtfy refuses an instance name that holds a control
|
||||||
// character while ntfySet, SWW_ALERT_NTFY_URL being set: ntfy is sent
|
// character while ntfySet, SWWAF_ALERT_NTFY_URL being set: ntfy is sent
|
||||||
// the instance name in a header, which cannot hold one.
|
// the instance name in a header, which cannot hold one.
|
||||||
func (e *environment) checkInstanceNameForNtfy(instanceName string, ntfySet bool) {
|
func (e *environment) checkInstanceNameForNtfy(instanceName string, ntfySet bool) {
|
||||||
if ntfySet && strings.ContainsFunc(instanceName, unicode.IsControl) {
|
if ntfySet && strings.ContainsFunc(instanceName, unicode.IsControl) {
|
||||||
e.check("SWW_INSTANCE_NAME", fmt.Errorf(
|
e.check("SWWAF_INSTANCE_NAME", fmt.Errorf(
|
||||||
"%q %w, and is sent to ntfy in a header while SWW_ALERT_NTFY_URL is set",
|
"%q %w, and is sent to ntfy in a header while SWWAF_ALERT_NTFY_URL is set",
|
||||||
instanceName, errControlCharacter))
|
instanceName, errControlCharacter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// webhookURL reads a setting that is a URL each alert is posted to:
|
// webhookURL reads a setting that is a URL each alert is posted to:
|
||||||
// SWW_ALERT_WEBHOOK_URL, SWW_ALERT_SLACK_WEBHOOK_URL or
|
// SWWAF_ALERT_WEBHOOK_URL, SWWAF_ALERT_SLACK_WEBHOOK_URL or
|
||||||
// SWW_ALERT_NTFY_URL. Unset or empty, it is nil, and no alert is posted
|
// SWWAF_ALERT_NTFY_URL. Unset or empty, it is nil, and no alert is posted
|
||||||
// there. The log shows ******** in place of its path and query, and an
|
// there. The log shows ******** in place of its path and query, and an
|
||||||
// error shows none of it, since a webhook or an ntfy topic can carry its
|
// error shows none of it, since a webhook or an ntfy topic can carry its
|
||||||
// secret there.
|
// secret there.
|
||||||
@@ -1739,7 +1724,7 @@ func parseCountry(value string) (string, error) {
|
|||||||
|
|
||||||
// ParseASN reads an AS number such as AS64496, in either case, and
|
// ParseASN reads an AS number such as AS64496, in either case, and
|
||||||
// returns it as the lookup gives it: AS and the number, in capitals and
|
// returns it as the lookup gives it: AS and the number, in capitals and
|
||||||
// without leading zeros. The file SWW_ASN_LIMIT_PERCENT_URL names is
|
// without leading zeros. The file SWWAF_ASN_LIMIT_PERCENT_URL names is
|
||||||
// read with it too.
|
// read with it too.
|
||||||
func ParseASN(value string) (string, error) {
|
func ParseASN(value string) (string, error) {
|
||||||
digits, hasAS := strings.CutPrefix(strings.ToUpper(value), "AS")
|
digits, hasAS := strings.CutPrefix(strings.ToUpper(value), "AS")
|
||||||
@@ -1793,7 +1778,7 @@ func parsePercents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParsePercent reads a percentage, a whole number from 0 to 100. The file
|
// ParsePercent reads a percentage, a whole number from 0 to 100. The file
|
||||||
// SWW_ASN_LIMIT_PERCENT_URL names is read with it too.
|
// SWWAF_ASN_LIMIT_PERCENT_URL names is read with it too.
|
||||||
func ParsePercent(value string) (int64, error) {
|
func ParsePercent(value string) (int64, error) {
|
||||||
percent, err := strconv.ParseInt(value, 10, 64)
|
percent, err := strconv.ParseInt(value, 10, 64)
|
||||||
if err != nil || percent < 0 || percent > 100 {
|
if err != nil || percent < 0 || percent > 100 {
|
||||||
@@ -2013,7 +1998,7 @@ const (
|
|||||||
maxLabelLength = 63
|
maxLabelLength = 63
|
||||||
// labelChars are the characters a label of a DNS zone may hold.
|
// labelChars are the characters a label of a DNS zone may hold.
|
||||||
labelChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-"
|
labelChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-"
|
||||||
// dnsPort is the port a resolver is asked on when SWW_DNSBL_RESOLVER
|
// dnsPort is the port a resolver is asked on when SWWAF_DNSBL_RESOLVER
|
||||||
// gives none.
|
// gives none.
|
||||||
dnsPort = 53
|
dnsPort = 53
|
||||||
)
|
)
|
||||||
|
|||||||
+104
-108
@@ -23,99 +23,98 @@ import (
|
|||||||
|
|
||||||
// The settings, by name.
|
// The settings, by name.
|
||||||
const (
|
const (
|
||||||
listenAddr = "SWW_LISTEN_ADDR"
|
listenAddr = "SWWAF_LISTEN_ADDR"
|
||||||
mode = "SWW_MODE"
|
mode = "SWWAF_MODE"
|
||||||
ipv6GroupPrefix = "SWW_IPV6_GROUP_PREFIX"
|
ipv6GroupPrefix = "SWWAF_IPV6_GROUP_PREFIX"
|
||||||
maxTrackedClients = "SWW_MAX_TRACKED_CLIENTS"
|
maxTrackedClients = "SWWAF_MAX_TRACKED_CLIENTS"
|
||||||
allowNets = "SWW_ALLOW_NETS"
|
allowNets = "SWWAF_ALLOW_NETS"
|
||||||
rateLimitExemptNets = "SWW_RATE_LIMIT_EXEMPT_NETS"
|
rateLimitExemptNets = "SWWAF_RATE_LIMIT_EXEMPT_NETS"
|
||||||
denyNets = "SWW_DENY_NETS"
|
denyNets = "SWWAF_DENY_NETS"
|
||||||
rateLimitPerMinute = "SWW_RATE_LIMIT_PER_MINUTE"
|
rateLimitPerMinute = "SWWAF_RATE_LIMIT_PER_MINUTE"
|
||||||
rateLimitPerHour = "SWW_RATE_LIMIT_PER_HOUR"
|
rateLimitPerHour = "SWWAF_RATE_LIMIT_PER_HOUR"
|
||||||
rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY"
|
rateLimitPerDay = "SWWAF_RATE_LIMIT_PER_DAY"
|
||||||
rateLimitExemptPaths = "SWW_RATE_LIMIT_EXEMPT_PATHS"
|
rateLimitExemptPaths = "SWWAF_RATE_LIMIT_EXEMPT_PATHS"
|
||||||
bytesLimitPerMinute = "SWW_BYTES_LIMIT_PER_MINUTE"
|
bytesLimitPerMinute = "SWWAF_BYTES_LIMIT_PER_MINUTE"
|
||||||
bytesLimitPerHour = "SWW_BYTES_LIMIT_PER_HOUR"
|
bytesLimitPerHour = "SWWAF_BYTES_LIMIT_PER_HOUR"
|
||||||
bytesLimitPerDay = "SWW_BYTES_LIMIT_PER_DAY"
|
bytesLimitPerDay = "SWWAF_BYTES_LIMIT_PER_DAY"
|
||||||
bytesCount = "SWW_BYTES_COUNT"
|
bytesCount = "SWWAF_BYTES_COUNT"
|
||||||
lookupSource = "SWW_LOOKUP_SOURCE"
|
lookupSource = "SWWAF_LOOKUP_SOURCE"
|
||||||
lookupDBPath = "SWW_LOOKUP_DB_PATH"
|
lookupDBPath = "SWWAF_LOOKUP_DB_PATH"
|
||||||
lookupTimeout = "SWW_LOOKUP_TIMEOUT"
|
lookupTimeout = "SWWAF_LOOKUP_TIMEOUT"
|
||||||
addLookupHeaders = "SWW_ADD_LOOKUP_HEADERS"
|
addLookupHeaders = "SWWAF_ADD_LOOKUP_HEADERS"
|
||||||
deniedCountries = "SWW_DENIED_COUNTRIES"
|
deniedCountries = "SWWAF_DENIED_COUNTRIES"
|
||||||
allowedCountries = "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"
|
allowedCountries = "SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES"
|
||||||
asnLimitPercent = "SWW_ASN_LIMIT_PERCENT"
|
asnLimitPercent = "SWWAF_ASN_LIMIT_PERCENT"
|
||||||
countryLimitPercent = "SWW_COUNTRY_LIMIT_PERCENT"
|
countryLimitPercent = "SWWAF_COUNTRY_LIMIT_PERCENT"
|
||||||
asnBytesPercent = "SWW_ASN_BYTES_PERCENT"
|
asnBytesPercent = "SWWAF_ASN_BYTES_PERCENT"
|
||||||
countryBytesPercent = "SWW_COUNTRY_BYTES_PERCENT"
|
countryBytesPercent = "SWWAF_COUNTRY_BYTES_PERCENT"
|
||||||
unknownLimitPercent = "SWW_UNKNOWN_LIMIT_PERCENT"
|
unknownLimitPercent = "SWWAF_UNKNOWN_LIMIT_PERCENT"
|
||||||
asnLimitPercentURL = "SWW_ASN_LIMIT_PERCENT_URL"
|
asnLimitPercentURL = "SWWAF_ASN_LIMIT_PERCENT_URL"
|
||||||
blocklistURLs = "SWW_BLOCKLIST_URLS"
|
blocklistURLs = "SWWAF_BLOCKLIST_URLS"
|
||||||
blocklistRefresh = "SWW_BLOCKLIST_REFRESH"
|
blocklistRefresh = "SWWAF_BLOCKLIST_REFRESH"
|
||||||
blocklistAction = "SWW_BLOCKLIST_ACTION"
|
blocklistAction = "SWWAF_BLOCKLIST_ACTION"
|
||||||
dnsblZones = "SWW_DNSBL_ZONES"
|
dnsblZones = "SWWAF_DNSBL_ZONES"
|
||||||
dnsblResolver = "SWW_DNSBL_RESOLVER"
|
dnsblResolver = "SWWAF_DNSBL_RESOLVER"
|
||||||
abuseIPDBKey = "SWW_ABUSEIPDB_KEY"
|
abuseIPDBKey = "SWWAF_ABUSEIPDB_KEY"
|
||||||
abuseIPDBMinScore = "SWW_ABUSEIPDB_MIN_SCORE"
|
abuseIPDBMinScore = "SWWAF_ABUSEIPDB_MIN_SCORE"
|
||||||
abuseIPDBDailyBudget = "SWW_ABUSEIPDB_DAILY_BUDGET"
|
abuseIPDBDailyBudget = "SWWAF_ABUSEIPDB_DAILY_BUDGET"
|
||||||
reputationAction = "SWW_REPUTATION_ACTION"
|
reputationAction = "SWWAF_REPUTATION_ACTION"
|
||||||
reputationCacheTTL = "SWW_REPUTATION_CACHE_TTL"
|
reputationCacheTTL = "SWWAF_REPUTATION_CACHE_TTL"
|
||||||
reputationTimeout = "SWW_REPUTATION_TIMEOUT"
|
reputationTimeout = "SWWAF_REPUTATION_TIMEOUT"
|
||||||
crowdSecURL = "SWW_CROWDSEC_LAPI_URL"
|
crowdSecURL = "SWWAF_CROWDSEC_LAPI_URL"
|
||||||
crowdSecKey = "SWW_CROWDSEC_LAPI_KEY"
|
crowdSecKey = "SWWAF_CROWDSEC_LAPI_KEY"
|
||||||
banResponse = "SWW_BAN_RESPONSE"
|
banResponse = "SWWAF_BAN_RESPONSE"
|
||||||
limitBanDuration = "SWW_LIMIT_BAN_DURATION"
|
limitBanDuration = "SWWAF_LIMIT_BAN_DURATION"
|
||||||
limitBanRepeatWindow = "SWW_LIMIT_BAN_REPEAT_WINDOW"
|
limitBanRepeatWindow = "SWWAF_LIMIT_BAN_REPEAT_WINDOW"
|
||||||
maxBanDuration = "SWW_MAX_BAN_DURATION"
|
maxBanDuration = "SWWAF_MAX_BAN_DURATION"
|
||||||
attackBanDuration = "SWW_ATTACK_BAN_DURATION"
|
attackBanDuration = "SWWAF_ATTACK_BAN_DURATION"
|
||||||
maxBans = "SWW_MAX_BANS"
|
maxBans = "SWWAF_MAX_BANS"
|
||||||
banScopeV4Prefix = "SWW_BAN_SCOPE_V4_PREFIX"
|
banScopeV4Prefix = "SWWAF_BAN_SCOPE_V4_PREFIX"
|
||||||
stateDir = "SWW_STATE_DIR"
|
stateDir = "SWWAF_STATE_DIR"
|
||||||
stateWriteDelay = "SWW_STATE_WRITE_DELAY"
|
stateWriteDelay = "SWWAF_STATE_WRITE_DELAY"
|
||||||
stateCounterInterval = "SWW_STATE_COUNTER_INTERVAL"
|
stateCounterInterval = "SWWAF_STATE_COUNTER_INTERVAL"
|
||||||
adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
adminToken = "SWWAF_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
||||||
metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name
|
metricsToken = "SWWAF_METRICS_TOKEN" //nolint:gosec // the setting's name
|
||||||
metricsTopN = "SWW_METRICS_TOP_N"
|
metricsTopN = "SWWAF_METRICS_TOP_N"
|
||||||
instanceName = "SWW_INSTANCE_NAME"
|
instanceName = "SWWAF_INSTANCE_NAME"
|
||||||
logRequestHeaders = "SWW_LOG_REQUEST_HEADERS"
|
logRequestHeaders = "SWWAF_LOG_REQUEST_HEADERS"
|
||||||
logLevel = "SWW_LOG_LEVEL"
|
logLevel = "SWWAF_LOG_LEVEL"
|
||||||
rulesDir = "SWW_RULES_DIR"
|
rulesDir = "SWWAF_RULES_DIR"
|
||||||
rulesEnabled = "SWW_RULES_ENABLED"
|
rulesEnabled = "SWWAF_RULES_ENABLED"
|
||||||
wafMode = "SWW_WAF_MODE"
|
wafMode = "SWWAF_WAF_MODE"
|
||||||
wafParanoiaLevel = "SWW_WAF_PARANOIA_LEVEL"
|
wafParanoiaLevel = "SWWAF_WAF_PARANOIA_LEVEL"
|
||||||
wafAnomalyThreshold = "SWW_WAF_ANOMALY_THRESHOLD"
|
wafAnomalyThreshold = "SWWAF_WAF_ANOMALY_THRESHOLD"
|
||||||
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
|
wafDisabledRules = "SWWAF_WAF_DISABLED_RULES"
|
||||||
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
|
wafExemptPaths = "SWWAF_WAF_EXEMPT_PATHS"
|
||||||
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
|
wafBodyLimit = "SWWAF_WAF_BODY_LIMIT"
|
||||||
exclusionsFile = "SWW_CORE_RULE_SET_EXCLUSIONS_FILE"
|
trapPaths = "SWWAF_TRAP_PATHS"
|
||||||
trapPaths = "SWW_TRAP_PATHS"
|
errorBurstThreshold = "SWWAF_ERROR_BURST_THRESHOLD"
|
||||||
errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
|
logRemoteURL = "SWWAF_LOG_REMOTE_URL"
|
||||||
logRemoteURL = "SWW_LOG_REMOTE_URL"
|
logRemoteTLSCAFile = "SWWAF_LOG_REMOTE_TLS_CA_FILE"
|
||||||
logRemoteTLSCAFile = "SWW_LOG_REMOTE_TLS_CA_FILE"
|
logRemoteBuffer = "SWWAF_LOG_REMOTE_BUFFER"
|
||||||
logRemoteBuffer = "SWW_LOG_REMOTE_BUFFER"
|
logRemoteFacility = "SWWAF_LOG_REMOTE_FACILITY"
|
||||||
logRemoteFacility = "SWW_LOG_REMOTE_FACILITY"
|
logRemoteAppName = "SWWAF_LOG_REMOTE_APP_NAME"
|
||||||
logRemoteAppName = "SWW_LOG_REMOTE_APP_NAME"
|
alertWebhookURL = "SWWAF_ALERT_WEBHOOK_URL"
|
||||||
alertWebhookURL = "SWW_ALERT_WEBHOOK_URL"
|
alertWebhookHeaders = "SWWAF_ALERT_WEBHOOK_HEADERS"
|
||||||
alertWebhookHeaders = "SWW_ALERT_WEBHOOK_HEADERS"
|
alertSlackWebhookURL = "SWWAF_ALERT_SLACK_WEBHOOK_URL"
|
||||||
alertSlackWebhookURL = "SWW_ALERT_SLACK_WEBHOOK_URL"
|
alertNtfyURL = "SWWAF_ALERT_NTFY_URL"
|
||||||
alertNtfyURL = "SWW_ALERT_NTFY_URL"
|
alertNtfyToken = "SWWAF_ALERT_NTFY_TOKEN" //nolint:gosec // the setting's name
|
||||||
alertNtfyToken = "SWW_ALERT_NTFY_TOKEN" //nolint:gosec // the setting's name
|
alertEvents = "SWWAF_ALERT_EVENTS"
|
||||||
alertEvents = "SWW_ALERT_EVENTS"
|
alertCooldown = "SWWAF_ALERT_COOLDOWN"
|
||||||
alertCooldown = "SWW_ALERT_COOLDOWN"
|
alertMaxPerHour = "SWWAF_ALERT_MAX_PER_HOUR"
|
||||||
alertMaxPerHour = "SWW_ALERT_MAX_PER_HOUR"
|
anomalyNetV4Prefix = "SWWAF_ANOMALY_NET_V4_PREFIX"
|
||||||
anomalyNetV4Prefix = "SWW_ANOMALY_NET_V4_PREFIX"
|
anomalyNetV6Prefix = "SWWAF_ANOMALY_NET_V6_PREFIX"
|
||||||
anomalyNetV6Prefix = "SWW_ANOMALY_NET_V6_PREFIX"
|
watchNets = "SWWAF_WATCH_NETS"
|
||||||
watchNets = "SWW_WATCH_NETS"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The anomaly thresholds: each of the prefixes below, which name a scope,
|
// The anomaly thresholds: each of the prefixes below, which name a scope,
|
||||||
// followed by each of the four ends.
|
// followed by each of the four ends.
|
||||||
const (
|
const (
|
||||||
anomalyClient = "SWW_ANOMALY_CLIENT_"
|
anomalyClient = "SWWAF_ANOMALY_CLIENT_"
|
||||||
anomalyNet = "SWW_ANOMALY_NET_"
|
anomalyNet = "SWWAF_ANOMALY_NET_"
|
||||||
anomalyASN = "SWW_ANOMALY_ASN_"
|
anomalyASN = "SWWAF_ANOMALY_ASN_"
|
||||||
anomalyTotal = "SWW_ANOMALY_TOTAL_"
|
anomalyTotal = "SWWAF_ANOMALY_TOTAL_"
|
||||||
watch = "SWW_WATCH_"
|
watch = "SWWAF_WATCH_"
|
||||||
|
|
||||||
requestsPerMinute = "REQUESTS_PER_MINUTE"
|
requestsPerMinute = "REQUESTS_PER_MINUTE"
|
||||||
requestsPerHour = "REQUESTS_PER_HOUR"
|
requestsPerHour = "REQUESTS_PER_HOUR"
|
||||||
@@ -156,21 +155,21 @@ func loggedAnomalyDefaults() map[string]string {
|
|||||||
return logged
|
return logged
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultAlertEvents is the default of SWW_ALERT_EVENTS, and
|
// defaultAlertEvents is the default of SWWAF_ALERT_EVENTS, and
|
||||||
// defaultAlertCooldown that of SWW_ALERT_COOLDOWN.
|
// defaultAlertCooldown that of SWWAF_ALERT_COOLDOWN.
|
||||||
const (
|
const (
|
||||||
defaultAlertEvents = "ban,permanent_ban,waf_block,anomaly,reputation_hit," +
|
defaultAlertEvents = "ban,permanent_ban,waf_block,anomaly,reputation_hit," +
|
||||||
"source_failure,file_error"
|
"source_failure,file_error"
|
||||||
defaultAlertCooldown = "15m"
|
defaultAlertCooldown = "15m"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultReputationCacheTTL is the default of SWW_REPUTATION_CACHE_TTL.
|
// defaultReputationCacheTTL is the default of SWWAF_REPUTATION_CACHE_TTL.
|
||||||
const defaultReputationCacheTTL = "24h"
|
const defaultReputationCacheTTL = "24h"
|
||||||
|
|
||||||
// defaultWAFDisabledRules is the default of SWW_WAF_DISABLED_RULES.
|
// defaultWAFDisabledRules is the default of SWWAF_WAF_DISABLED_RULES.
|
||||||
const defaultWAFDisabledRules = "920340,920420,920440,920640,930130,930140"
|
const defaultWAFDisabledRules = "920340,920420,920440,920640,930130,930140"
|
||||||
|
|
||||||
// defaultLogRequestHeaders is the default of SWW_LOG_REQUEST_HEADERS.
|
// defaultLogRequestHeaders is the default of SWWAF_LOG_REQUEST_HEADERS.
|
||||||
const defaultLogRequestHeaders = "accept,accept-language,accept-encoding," +
|
const defaultLogRequestHeaders = "accept,accept-language,accept-encoding," +
|
||||||
"content-type,origin,range"
|
"content-type,origin,range"
|
||||||
|
|
||||||
@@ -195,8 +194,8 @@ const (
|
|||||||
otherToken = "fedcba9876543210fedcba9876543210"
|
otherToken = "fedcba9876543210fedcba9876543210"
|
||||||
)
|
)
|
||||||
|
|
||||||
// instance is an SWW_INSTANCE_NAME that is a valid app name too, and
|
// instance is an SWWAF_INSTANCE_NAME that is a valid app name too, and
|
||||||
// remoteURL an SWW_LOG_REMOTE_URL, for the tests that send the lines.
|
// remoteURL an SWWAF_LOG_REMOTE_URL, for the tests that send the lines.
|
||||||
const (
|
const (
|
||||||
instance = "fsn1app1/blog"
|
instance = "fsn1app1/blog"
|
||||||
remoteURL = "syslog+udp://192.0.2.1:514"
|
remoteURL = "syslog+udp://192.0.2.1:514"
|
||||||
@@ -208,7 +207,7 @@ const off = "off"
|
|||||||
// enabled is true, as a setting's value.
|
// enabled is true, as a setting's value.
|
||||||
const enabled = "true"
|
const enabled = "true"
|
||||||
|
|
||||||
// defaultLookupSource is the default of SWW_LOOKUP_SOURCE, and
|
// defaultLookupSource is the default of SWWAF_LOOKUP_SOURCE, and
|
||||||
// fileSource the source that is the lookup database.
|
// fileSource the source that is the lookup database.
|
||||||
const (
|
const (
|
||||||
defaultLookupSource = "geojs"
|
defaultLookupSource = "geojs"
|
||||||
@@ -540,14 +539,13 @@ func TestCoreRuleSetSettings(t *testing.T) {
|
|||||||
environment{
|
environment{
|
||||||
wafMode: config.WAFModeDetect, wafParanoiaLevel: "4", wafAnomalyThreshold: "10",
|
wafMode: config.WAFModeDetect, wafParanoiaLevel: "4", wafAnomalyThreshold: "10",
|
||||||
wafDisabledRules: "942100, 920350", wafExemptPaths: "/api/, /static/",
|
wafDisabledRules: "942100, 920350", wafExemptPaths: "/api/, /static/",
|
||||||
wafBodyLimit: "32K", exclusionsFile: "/etc/sww/crs-exclusions.conf",
|
wafBodyLimit: "32K",
|
||||||
},
|
},
|
||||||
config.Config{
|
config.Config{
|
||||||
WAFMode: config.WAFModeDetect, WAFParanoiaLevel: 4, WAFAnomalyThreshold: 10,
|
WAFMode: config.WAFModeDetect, WAFParanoiaLevel: 4, WAFAnomalyThreshold: 10,
|
||||||
WAFDisabledRules: []int{942100, 920350},
|
WAFDisabledRules: []int{942100, 920350},
|
||||||
WAFExemptPaths: []string{"/api/", "/static/"},
|
WAFExemptPaths: []string{"/api/", "/static/"},
|
||||||
WAFBodyLimit: 32 << 10,
|
WAFBodyLimit: 32 << 10,
|
||||||
CoreRuleSetExclusionsFile: "/etc/sww/crs-exclusions.conf",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -567,8 +565,7 @@ func TestCoreRuleSetSettings(t *testing.T) {
|
|||||||
WAFMode: cfg.WAFMode, WAFParanoiaLevel: cfg.WAFParanoiaLevel,
|
WAFMode: cfg.WAFMode, WAFParanoiaLevel: cfg.WAFParanoiaLevel,
|
||||||
WAFAnomalyThreshold: cfg.WAFAnomalyThreshold,
|
WAFAnomalyThreshold: cfg.WAFAnomalyThreshold,
|
||||||
WAFDisabledRules: cfg.WAFDisabledRules, WAFExemptPaths: cfg.WAFExemptPaths,
|
WAFDisabledRules: cfg.WAFDisabledRules, WAFExemptPaths: cfg.WAFExemptPaths,
|
||||||
WAFBodyLimit: cfg.WAFBodyLimit,
|
WAFBodyLimit: cfg.WAFBodyLimit,
|
||||||
CoreRuleSetExclusionsFile: cfg.CoreRuleSetExclusionsFile,
|
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, tc.want) {
|
if !reflect.DeepEqual(got, tc.want) {
|
||||||
t.Errorf("%v gave\n%+v\nwant\n%+v", tc.env, got, tc.want)
|
t.Errorf("%v gave\n%+v\nwant\n%+v", tc.env, got, tc.want)
|
||||||
@@ -1204,7 +1201,7 @@ func TestSettingNeedingLookupsStopsTheStartWhileTheyAreOff(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set empty, the lists need nothing looked up, and nor does
|
// Set empty, the lists need nothing looked up, and nor does
|
||||||
// SWW_UNKNOWN_LIMIT_PERCENT at 100, which lowers no limit, an anomaly
|
// SWWAF_UNKNOWN_LIMIT_PERCENT at 100, which lowers no limit, an anomaly
|
||||||
// threshold per AS number that is off, any other anomaly threshold, or
|
// threshold per AS number that is off, any other anomaly threshold, or
|
||||||
// a blocklist.
|
// a blocklist.
|
||||||
env := environment{
|
env := environment{
|
||||||
@@ -1442,7 +1439,7 @@ const (
|
|||||||
asnURL = "https://lists.example/asn.txt"
|
asnURL = "https://lists.example/asn.txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The actions of SWW_BLOCKLIST_ACTION, as Config gives them.
|
// The actions of SWWAF_BLOCKLIST_ACTION, as Config gives them.
|
||||||
const (
|
const (
|
||||||
actionDeny = "deny"
|
actionDeny = "deny"
|
||||||
actionLimit = "limit"
|
actionLimit = "limit"
|
||||||
@@ -1538,7 +1535,7 @@ func TestASNLimitPercentURLThatIsABlocklistStopsTheStart(t *testing.T) {
|
|||||||
blocklistURLs: dropURL + "," + asnURL, asnLimitPercentURL: asnURL,
|
blocklistURLs: dropURL + "," + asnURL, asnLimitPercentURL: asnURL,
|
||||||
}.lookupEnv)
|
}.lookupEnv)
|
||||||
|
|
||||||
want := asnLimitPercentURL + `: "` + asnURL + `" is in SWW_BLOCKLIST_URLS too`
|
want := asnLimitPercentURL + `: "` + asnURL + `" is in SWWAF_BLOCKLIST_URLS too`
|
||||||
if err == nil || err.Error() != want {
|
if err == nil || err.Error() != want {
|
||||||
t.Errorf("error %v, want %s", err, want)
|
t.Errorf("error %v, want %s", err, want)
|
||||||
}
|
}
|
||||||
@@ -1841,7 +1838,7 @@ func TestInvalidCrowdSecSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
|
|||||||
notLAPIURL = " is not an http or https URL without a user or a fragment, " +
|
notLAPIURL = " is not an http or https URL without a user or a fragment, " +
|
||||||
"such as http://172.17.0.1:8080"
|
"such as http://172.17.0.1:8080"
|
||||||
anotherList = `gives the decision list "` + lapi + `/v1/decisions", which is in ` +
|
anotherList = `gives the decision list "` + lapi + `/v1/decisions", which is in ` +
|
||||||
`SWW_BLOCKLIST_URLS or is SWW_ASN_LIMIT_PERCENT_URL too`
|
`SWWAF_BLOCKLIST_URLS or is SWWAF_ASN_LIMIT_PERCENT_URL too`
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -2331,7 +2328,6 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
|
|||||||
wafDisabledRules: defaultWAFDisabledRules,
|
wafDisabledRules: defaultWAFDisabledRules,
|
||||||
wafExemptPaths: "",
|
wafExemptPaths: "",
|
||||||
wafBodyLimit: off,
|
wafBodyLimit: off,
|
||||||
exclusionsFile: "",
|
|
||||||
trapPaths: "",
|
trapPaths: "",
|
||||||
errorBurstThreshold: "30",
|
errorBurstThreshold: "30",
|
||||||
logRemoteURL: "",
|
logRemoteURL: "",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (rq *request) answerHealth() {
|
|||||||
|
|
||||||
// answerAdmin answers a request for one of discriminator's own endpoints. Each
|
// answerAdmin answers a request for one of discriminator's own endpoints. Each
|
||||||
// needs a token, sent as Authorization: Bearer <token>: the metrics
|
// needs a token, sent as Authorization: Bearer <token>: the metrics
|
||||||
// SWW_METRICS_TOKEN, the others SWW_ADMIN_TOKEN. A request without it is
|
// SWWAF_METRICS_TOKEN, the others SWWAF_ADMIN_TOKEN. A request without it is
|
||||||
// refused with 401. An endpoint whose token is unset answers 404, as any other
|
// refused with 401. An endpoint whose token is unset answers 404, as any other
|
||||||
// path does.
|
// path does.
|
||||||
func (rq *request) answerAdmin() {
|
func (rq *request) answerAdmin() {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
adminToken = "SWWAF_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
||||||
// adminSecret is the SWW_ADMIN_TOKEN the tests set, and adminBearer
|
// adminSecret is the SWWAF_ADMIN_TOKEN the tests set, and adminBearer
|
||||||
// how a request carries it.
|
// how a request carries it.
|
||||||
adminSecret = "fedcba9876543210fedcba9876543210"
|
adminSecret = "fedcba9876543210fedcba9876543210"
|
||||||
adminBearer = "Bearer " + adminSecret
|
adminBearer = "Bearer " + adminSecret
|
||||||
@@ -337,12 +337,12 @@ func TestAdminEndpointsNeedTheTokenInObserveMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// adminEndpoint is a request to an endpoint SWW_ADMIN_TOKEN opens.
|
// adminEndpoint is a request to an endpoint SWWAF_ADMIN_TOKEN opens.
|
||||||
type adminEndpoint struct {
|
type adminEndpoint struct {
|
||||||
method, path, body string
|
method, path, body string
|
||||||
}
|
}
|
||||||
|
|
||||||
// adminEndpoints returns a request to each endpoint SWW_ADMIN_TOKEN
|
// adminEndpoints returns a request to each endpoint SWWAF_ADMIN_TOKEN
|
||||||
// opens: listing the bans, banning otherClient for an hour, lifting the
|
// opens: listing the bans, banning otherClient for an hour, lifting the
|
||||||
// bans on otherClient, and asking about otherClient.
|
// bans on otherClient, and asking about otherClient.
|
||||||
func adminEndpoints() []adminEndpoint {
|
func adminEndpoints() []adminEndpoint {
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
alertWebhookURL = "SWW_ALERT_WEBHOOK_URL"
|
alertWebhookURL = "SWWAF_ALERT_WEBHOOK_URL"
|
||||||
alertMaxPerHour = "SWW_ALERT_MAX_PER_HOUR"
|
alertMaxPerHour = "SWWAF_ALERT_MAX_PER_HOUR"
|
||||||
// alertInstance is the instance every alert of these tests gives.
|
// alertInstance is the instance every alert of these tests gives.
|
||||||
alertInstance = "fsn1app1/blog"
|
alertInstance = "fsn1app1/blog"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ import (
|
|||||||
// The anomaly thresholds: the prefix of a scope followed by the end of a
|
// The anomaly thresholds: the prefix of a scope followed by the end of a
|
||||||
// count.
|
// count.
|
||||||
const (
|
const (
|
||||||
anomalyClient = "SWW_ANOMALY_CLIENT_"
|
anomalyClient = "SWWAF_ANOMALY_CLIENT_"
|
||||||
anomalyNet = "SWW_ANOMALY_NET_"
|
anomalyNet = "SWWAF_ANOMALY_NET_"
|
||||||
anomalyASN = "SWW_ANOMALY_ASN_"
|
anomalyASN = "SWWAF_ANOMALY_ASN_"
|
||||||
anomalyTotal = "SWW_ANOMALY_TOTAL_"
|
anomalyTotal = "SWWAF_ANOMALY_TOTAL_"
|
||||||
anomalyWatch = "SWW_WATCH_"
|
anomalyWatch = "SWWAF_WATCH_"
|
||||||
|
|
||||||
requestsPerMinute = "REQUESTS_PER_MINUTE"
|
requestsPerMinute = "REQUESTS_PER_MINUTE"
|
||||||
requestsPerHour = "REQUESTS_PER_HOUR"
|
requestsPerHour = "REQUESTS_PER_HOUR"
|
||||||
@@ -35,9 +35,9 @@ const (
|
|||||||
|
|
||||||
// The other anomaly settings.
|
// The other anomaly settings.
|
||||||
const (
|
const (
|
||||||
anomalyNetV4Prefix = "SWW_ANOMALY_NET_V4_PREFIX"
|
anomalyNetV4Prefix = "SWWAF_ANOMALY_NET_V4_PREFIX"
|
||||||
anomalyNetV6Prefix = "SWW_ANOMALY_NET_V6_PREFIX"
|
anomalyNetV6Prefix = "SWWAF_ANOMALY_NET_V6_PREFIX"
|
||||||
watchNets = "SWW_WATCH_NETS"
|
watchNets = "SWWAF_WATCH_NETS"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -164,9 +164,9 @@ func TestEveryRequestIsCountedWhateverIsDoneWithIt(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
allowed = "192.0.2.7" // in SWW_ALLOW_NETS
|
allowed = "192.0.2.7" // in SWWAF_ALLOW_NETS
|
||||||
exempt = "192.0.2.10" // in SWW_RATE_LIMIT_EXEMPT_NETS
|
exempt = "192.0.2.10" // in SWWAF_RATE_LIMIT_EXEMPT_NETS
|
||||||
denied = "192.0.2.20" // in SWW_DENY_NETS
|
denied = "192.0.2.20" // in SWWAF_DENY_NETS
|
||||||
)
|
)
|
||||||
|
|
||||||
s, _, _, queue := startAppWithAlerts(t, readAndAnswer, map[string]string{
|
s, _, _, queue := startAppWithAlerts(t, readAndAnswer, map[string]string{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const (
|
|||||||
// answerTimeLimit is the default of rproxy's SWRP_TIMEOUT_MS, the time
|
// answerTimeLimit is the default of rproxy's SWRP_TIMEOUT_MS, the time
|
||||||
// discriminator has to answer.
|
// discriminator has to answer.
|
||||||
answerTimeLimit = 2 * time.Second
|
answerTimeLimit = 2 * time.Second
|
||||||
// defaultLookupWait is the default of SWW_LOOKUP_TIMEOUT.
|
// defaultLookupWait is the default of SWWAF_LOOKUP_TIMEOUT.
|
||||||
defaultLookupWait = 800 * time.Millisecond
|
defaultLookupWait = 800 * time.Millisecond
|
||||||
// The header limits SPEC.md gives.
|
// The header limits SPEC.md gives.
|
||||||
cookieLimit = 50
|
cookieLimit = 50
|
||||||
@@ -166,7 +166,7 @@ func TestHeaderLimitsHoldWhateverTheCoreRuleSetSettingsSay(t *testing.T) {
|
|||||||
for _, env := range []map[string]string{
|
for _, env := range []map[string]string{
|
||||||
{wafMode: off},
|
{wafMode: off},
|
||||||
{wafMode: "detect"},
|
{wafMode: "detect"},
|
||||||
{wafMode: block, "SWW_WAF_EXEMPT_PATHS": "/"},
|
{wafMode: block, "SWWAF_WAF_EXEMPT_PATHS": "/"},
|
||||||
} {
|
} {
|
||||||
t.Run(fmt.Sprint(env), func(t *testing.T) {
|
t.Run(fmt.Sprint(env), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -399,7 +399,7 @@ func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
|
|||||||
action string
|
action string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "SWW_DENY_NETS", env: map[string]string{denyNets: client},
|
name: "SWWAF_DENY_NETS", env: map[string]string{denyNets: client},
|
||||||
prepare: leaveAsItIs, status: forbidden, action: requestlog.ActionDenied,
|
prepare: leaveAsItIs, status: forbidden, action: requestlog.ActionDenied,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// banResponse is a refusal answered with SWW_BAN_RESPONSE, and logged
|
// banResponse is a refusal answered with SWWAF_BAN_RESPONSE, and logged
|
||||||
// with action.
|
// with action.
|
||||||
func (rq *request) banResponse(action string) *refusal {
|
func (rq *request) banResponse(action string) *refusal {
|
||||||
return &refusal{status: rq.h.config.BanResponse, action: action}
|
return &refusal{status: rq.h.config.BanResponse, action: action}
|
||||||
@@ -85,8 +85,8 @@ func (rq *request) countBytes() {
|
|||||||
// countRefusal counts the request for the error burst once it has been
|
// countRefusal counts the request for the error burst once it has been
|
||||||
// answered, if discriminator refused it after a rule file match, a trap path or a
|
// answered, if discriminator refused it after a rule file match, a trap path or a
|
||||||
// Core Rule Set match, and in observe mode if enforce mode would have: more than
|
// Core Rule Set match, and in observe mode if enforce mode would have: more than
|
||||||
// SWW_ERROR_BURST_THRESHOLD such refusals of the client within a minute
|
// SWWAF_ERROR_BURST_THRESHOLD such refusals of the client within a minute
|
||||||
// break a limit. A client in SWW_ALLOW_NETS, which the checks skip, is
|
// break a limit. A client in SWWAF_ALLOW_NETS, which the checks skip, is
|
||||||
// not counted, and nothing is while the threshold is off.
|
// not counted, and nothing is while the threshold is off.
|
||||||
func (rq *request) countRefusal() {
|
func (rq *request) countRefusal() {
|
||||||
cfg := rq.h.config
|
cfg := rq.h.config
|
||||||
@@ -121,7 +121,7 @@ func (rq *request) countRefusal() {
|
|||||||
|
|
||||||
// countedBytes returns the request's bytes, once it has ended, as the
|
// countedBytes returns the request's bytes, once it has ended, as the
|
||||||
// byte limits and the anomaly thresholds count them: the response's body
|
// byte limits and the anomaly thresholds count them: the response's body
|
||||||
// bytes, the request's, or both, as SWW_BYTES_COUNT says.
|
// bytes, the request's, or both, as SWWAF_BYTES_COUNT says.
|
||||||
func (rq *request) countedBytes() int64 {
|
func (rq *request) countedBytes() int64 {
|
||||||
response, request := rq.out.bytes, rq.requestBytes
|
response, request := rq.out.bytes, rq.requestBytes
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ func (rq *request) countedBytes() int64 {
|
|||||||
// banForLimit bans the client's netblock at now for a broken limit, the
|
// banForLimit bans the client's netblock at now for a broken limit, the
|
||||||
// one hit names, notes the offence for the log line and counts the hit in
|
// one hit names, notes the offence for the log line and counts the hit in
|
||||||
// the metrics. status is what the client was sent, or is sent:
|
// the metrics. status is what the client was sent, or is sent:
|
||||||
// SWW_BAN_RESPONSE for a request over a rate limit, what the report says for
|
// SWWAF_BAN_RESPONSE for a request over a rate limit, what the report says for
|
||||||
// one whose bytes broke a byte limit, the refusal for one that broke the
|
// one whose bytes broke a byte limit, the refusal for one that broke the
|
||||||
// error burst. The ban's notes give the client's limit percentage for a
|
// error burst. The ban's notes give the client's limit percentage for a
|
||||||
// rate limit or a byte limit; the error burst is not lowered. The ban sets
|
// rate limit or a byte limit; the error burst is not lowered. The ban sets
|
||||||
@@ -277,7 +277,7 @@ func (rq *request) banForCrowdSec(now time.Time, decision reputation.Decision) {
|
|||||||
// wouldAlertBan reports whether the alert for a ban on netblock for cause
|
// wouldAlertBan reports whether the alert for a ban on netblock for cause
|
||||||
// made at now would be sent. In observe mode the ban the request would
|
// made at now would be sent. In observe mode the ban the request would
|
||||||
// have made is worked out only then, at most once per
|
// have made is worked out only then, at most once per
|
||||||
// SWW_ALERT_COOLDOWN and never with no webhook set: its notes count the
|
// SWWAF_ALERT_COOLDOWN and never with no webhook set: its notes count the
|
||||||
// netblock's requests, which can mean going through every client.
|
// netblock's requests, which can mean going through every client.
|
||||||
func (rq *request) wouldAlertBan(
|
func (rq *request) wouldAlertBan(
|
||||||
netblock netip.Prefix, now time.Time, cause string,
|
netblock netip.Prefix, now time.Time, cause string,
|
||||||
@@ -343,7 +343,7 @@ func (rq *request) netblockRequests(netblock netip.Prefix) int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// netblock is the netblock a ban on client covers: its IPv4 address,
|
// netblock is the netblock a ban on client covers: its IPv4 address,
|
||||||
// widened to SWW_BAN_SCOPE_V4_PREFIX, or the IPv6 group clientGroup
|
// widened to SWWAF_BAN_SCOPE_V4_PREFIX, or the IPv6 group clientGroup
|
||||||
// counts it in.
|
// counts it in.
|
||||||
func (h *handler) netblock(client netip.Addr) netip.Prefix {
|
func (h *handler) netblock(client netip.Addr) netip.Prefix {
|
||||||
addr := client.Unmap()
|
addr := client.Unmap()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const (
|
|||||||
userAgent = "ban-test/1.0"
|
userAgent = "ban-test/1.0"
|
||||||
// permanent is the log line's ban_expires for a permanent ban.
|
// permanent is the log line's ban_expires for a permanent ban.
|
||||||
permanent = "permanent"
|
permanent = "permanent"
|
||||||
// banResponseClose is the SWW_BAN_RESPONSE that closes the connection.
|
// banResponseClose is the SWWAF_BAN_RESPONSE that closes the connection.
|
||||||
banResponseClose = "close"
|
banResponseClose = "close"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,19 +89,19 @@ func TestBanLengthsFollowTheSettings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A first ban lasts SWW_LIMIT_BAN_DURATION; one within
|
// A first ban lasts SWWAF_LIMIT_BAN_DURATION; one within
|
||||||
// SWW_LIMIT_BAN_REPEAT_WINDOW after it ended, three times as long.
|
// SWWAF_LIMIT_BAN_REPEAT_WINDOW after it ended, three times as long.
|
||||||
wantExpires(breakLimit(), 10*time.Minute)
|
wantExpires(breakLimit(), 10*time.Minute)
|
||||||
clk.advance(10*time.Minute + time.Hour)
|
clk.advance(10*time.Minute + time.Hour)
|
||||||
wantExpires(breakLimit(), 30*time.Minute)
|
wantExpires(breakLimit(), 30*time.Minute)
|
||||||
|
|
||||||
// Later than that, SWW_LIMIT_BAN_DURATION again.
|
// Later than that, SWWAF_LIMIT_BAN_DURATION again.
|
||||||
clk.advance(30*time.Minute + time.Hour + time.Second)
|
clk.advance(30*time.Minute + time.Hour + time.Second)
|
||||||
wantExpires(breakLimit(), 10*time.Minute)
|
wantExpires(breakLimit(), 10*time.Minute)
|
||||||
clk.advance(10 * time.Minute)
|
clk.advance(10 * time.Minute)
|
||||||
wantExpires(breakLimit(), 30*time.Minute)
|
wantExpires(breakLimit(), 30*time.Minute)
|
||||||
|
|
||||||
// 90 minutes would be longer than SWW_MAX_BAN_DURATION: the ban is
|
// 90 minutes would be longer than SWWAF_MAX_BAN_DURATION: the ban is
|
||||||
// permanent.
|
// permanent.
|
||||||
clk.advance(30 * time.Minute)
|
clk.advance(30 * time.Minute)
|
||||||
|
|
||||||
@@ -148,8 +148,8 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
|
|||||||
|
|
||||||
// In the IPv4 cases, client breaks the limit; these two are next to it.
|
// In the IPv4 cases, client breaks the limit; these two are next to it.
|
||||||
const (
|
const (
|
||||||
allowed = "203.0.113.60" // in SWW_ALLOW_NETS
|
allowed = "203.0.113.60" // in SWWAF_ALLOW_NETS
|
||||||
exempt = "203.0.113.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
|
exempt = "203.0.113.50" // in SWWAF_RATE_LIMIT_EXEMPT_NETS
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -164,7 +164,7 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
|
|||||||
nil, []string{otherClient, exempt},
|
nil, []string{otherClient, exempt},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"the IPv4 netblock SWW_BAN_SCOPE_V4_PREFIX sets",
|
"the IPv4 netblock SWWAF_BAN_SCOPE_V4_PREFIX sets",
|
||||||
map[string]string{banScopeV4Prefix: "24"}, client,
|
map[string]string{banScopeV4Prefix: "24"}, client,
|
||||||
[]string{otherClient, exempt}, []string{"203.0.112.9", allowed},
|
[]string{otherClient, exempt}, []string{"203.0.112.9", allowed},
|
||||||
},
|
},
|
||||||
@@ -173,7 +173,7 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
|
|||||||
[]string{"2001:db8:5::ffff:1"}, []string{"2001:db8:5:1::1"},
|
[]string{"2001:db8:5::ffff:1"}, []string{"2001:db8:5:1::1"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"the IPv6 netblock SWW_IPV6_GROUP_PREFIX sets",
|
"the IPv6 netblock SWWAF_IPV6_GROUP_PREFIX sets",
|
||||||
map[string]string{ipv6GroupPrefix: "48"}, "2001:db8:7::1",
|
map[string]string{ipv6GroupPrefix: "48"}, "2001:db8:7::1",
|
||||||
[]string{"2001:db8:7:ffff::1"}, []string{"2001:db8:8::1"},
|
[]string{"2001:db8:7:ffff::1"}, []string{"2001:db8:8::1"},
|
||||||
},
|
},
|
||||||
@@ -231,10 +231,10 @@ func TestBannedClientIsRefusedBeforeItsCountryIsLookedUp(t *testing.T) {
|
|||||||
func TestBanResponseAnswersEveryRefusalButTheSizeLimits(t *testing.T) {
|
func TestBanResponseAnswersEveryRefusalButTheSizeLimits(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const denied = "192.0.2.50" // in SWW_DENY_NETS
|
const denied = "192.0.2.50" // in SWWAF_DENY_NETS
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
setting string // "" leaves SWW_BAN_RESPONSE at its default
|
setting string // "" leaves SWWAF_BAN_RESPONSE at its default
|
||||||
status int // 0 is the connection closed without an answer
|
status int // 0 is the connection closed without an answer
|
||||||
}{
|
}{
|
||||||
{"", http.StatusForbidden},
|
{"", http.StatusForbidden},
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type percentage struct {
|
|||||||
|
|
||||||
// biasedThresholdsSet reports whether a biased threshold can lower a
|
// biasedThresholdsSet reports whether a biased threshold can lower a
|
||||||
// client's limits: one of its lists is not empty,
|
// client's limits: one of its lists is not empty,
|
||||||
// SWW_UNKNOWN_LIMIT_PERCENT is below 100, or SWW_ASN_LIMIT_PERCENT_URL
|
// SWWAF_UNKNOWN_LIMIT_PERCENT is below 100, or SWWAF_ASN_LIMIT_PERCENT_URL
|
||||||
// is set. The client's lookup is then needed before its request goes on.
|
// is set. The client's lookup is then needed before its request goes on.
|
||||||
func biasedThresholdsSet(cfg *config.Config) bool {
|
func biasedThresholdsSet(cfg *config.Config) bool {
|
||||||
return len(cfg.ASNLimitPercent) > 0 || len(cfg.CountryLimitPercent) > 0 ||
|
return len(cfg.ASNLimitPercent) > 0 || len(cfg.CountryLimitPercent) > 0 ||
|
||||||
@@ -31,15 +31,15 @@ func biasedThresholdsSet(cfg *config.Config) bool {
|
|||||||
// up, each "" when unknown, and the blocklists, DNSBL zones and AbuseIPDB
|
// up, each "" when unknown, and the blocklists, DNSBL zones and AbuseIPDB
|
||||||
// that list it. Each is the lowest of those the settings give it, the
|
// that list it. Each is the lowest of those the settings give it, the
|
||||||
// first of them in the order below when several are lowest: the
|
// first of them in the order below when several are lowest: the
|
||||||
// percentage SWW_ASN_LIMIT_PERCENT gives its AS number, the one the file
|
// percentage SWWAF_ASN_LIMIT_PERCENT gives its AS number, the one the file
|
||||||
// SWW_ASN_LIMIT_PERCENT_URL names gives it, the one
|
// SWWAF_ASN_LIMIT_PERCENT_URL names gives it, the one
|
||||||
// SWW_COUNTRY_LIMIT_PERCENT gives its country, for a client without a
|
// SWWAF_COUNTRY_LIMIT_PERCENT gives its country, for a client without a
|
||||||
// country, unless its request skipped the wait for GeoJS,
|
// country, unless its request skipped the wait for GeoJS,
|
||||||
// SWW_UNKNOWN_LIMIT_PERCENT, for a client a blocklist lists, the
|
// SWWAF_UNKNOWN_LIMIT_PERCENT, for a client a blocklist lists, the
|
||||||
// percentage of SWW_BLOCKLIST_ACTION while it is limit, and for a client
|
// percentage of SWWAF_BLOCKLIST_ACTION while it is limit, and for a client
|
||||||
// a DNSBL zone's verdict lists, or whose AbuseIPDB score is a hit, the
|
// a DNSBL zone's verdict lists, or whose AbuseIPDB score is a hit, the
|
||||||
// percentage of SWW_REPUTATION_ACTION while it is limit. For the byte
|
// percentage of SWWAF_REPUTATION_ACTION while it is limit. For the byte
|
||||||
// limits, SWW_ASN_BYTES_PERCENT and SWW_COUNTRY_BYTES_PERCENT take the
|
// limits, SWWAF_ASN_BYTES_PERCENT and SWWAF_COUNTRY_BYTES_PERCENT take the
|
||||||
// place of the first three for an AS number or a country they list.
|
// place of the first three for an AS number or a country they list.
|
||||||
func (rq *request) limitPercentages() (percentage, percentage) {
|
func (rq *request) limitPercentages() (percentage, percentage) {
|
||||||
cfg := rq.h.config
|
cfg := rq.h.config
|
||||||
@@ -47,36 +47,36 @@ func (rq *request) limitPercentages() (percentage, percentage) {
|
|||||||
|
|
||||||
unknown := percentage{percent: whole}
|
unknown := percentage{percent: whole}
|
||||||
if country == "" && !rq.skippedLookupWait {
|
if country == "" && !rq.skippedLookupWait {
|
||||||
unknown = percentage{cfg.UnknownLimitPercent, "SWW_UNKNOWN_LIMIT_PERCENT"}
|
unknown = percentage{cfg.UnknownLimitPercent, "SWWAF_UNKNOWN_LIMIT_PERCENT"}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetched := percentage{percent: whole}
|
fetched := percentage{percent: whole}
|
||||||
if percent, listed := rq.h.lists.ASNLimitPercent(asn); listed {
|
if percent, listed := rq.h.lists.ASNLimitPercent(asn); listed {
|
||||||
fetched = percentage{percent, "SWW_ASN_LIMIT_PERCENT_URL"}
|
fetched = percentage{percent, "SWWAF_ASN_LIMIT_PERCENT_URL"}
|
||||||
}
|
}
|
||||||
|
|
||||||
blocklisted := percentage{percent: whole}
|
blocklisted := percentage{percent: whole}
|
||||||
if rq.blocklisted && cfg.BlocklistAction == "limit" {
|
if rq.blocklisted && cfg.BlocklistAction == "limit" {
|
||||||
blocklisted = percentage{cfg.BlocklistLimitPercent, "SWW_BLOCKLIST_ACTION"}
|
blocklisted = percentage{cfg.BlocklistLimitPercent, "SWWAF_BLOCKLIST_ACTION"}
|
||||||
}
|
}
|
||||||
|
|
||||||
reputationListed := percentage{percent: whole}
|
reputationListed := percentage{percent: whole}
|
||||||
if (rq.dnsblListed || rq.abuseIPDBHit) && cfg.ReputationAction == "limit" {
|
if (rq.dnsblListed || rq.abuseIPDBHit) && cfg.ReputationAction == "limit" {
|
||||||
reputationListed = percentage{cfg.ReputationLimitPercent, "SWW_REPUTATION_ACTION"}
|
reputationListed = percentage{cfg.ReputationLimitPercent, "SWWAF_REPUTATION_ACTION"}
|
||||||
}
|
}
|
||||||
|
|
||||||
asnRequests := lowest(given(cfg.ASNLimitPercent, asn, "SWW_ASN_LIMIT_PERCENT"),
|
asnRequests := lowest(given(cfg.ASNLimitPercent, asn, "SWWAF_ASN_LIMIT_PERCENT"),
|
||||||
fetched)
|
fetched)
|
||||||
countryRequests := given(cfg.CountryLimitPercent, country,
|
countryRequests := given(cfg.CountryLimitPercent, country,
|
||||||
"SWW_COUNTRY_LIMIT_PERCENT")
|
"SWWAF_COUNTRY_LIMIT_PERCENT")
|
||||||
|
|
||||||
asnBytes, countryBytes := asnRequests, countryRequests
|
asnBytes, countryBytes := asnRequests, countryRequests
|
||||||
if _, listed := cfg.ASNBytesPercent[asn]; listed {
|
if _, listed := cfg.ASNBytesPercent[asn]; listed {
|
||||||
asnBytes = given(cfg.ASNBytesPercent, asn, "SWW_ASN_BYTES_PERCENT")
|
asnBytes = given(cfg.ASNBytesPercent, asn, "SWWAF_ASN_BYTES_PERCENT")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, listed := cfg.CountryBytesPercent[country]; listed {
|
if _, listed := cfg.CountryBytesPercent[country]; listed {
|
||||||
countryBytes = given(cfg.CountryBytesPercent, country, "SWW_COUNTRY_BYTES_PERCENT")
|
countryBytes = given(cfg.CountryBytesPercent, country, "SWWAF_COUNTRY_BYTES_PERCENT")
|
||||||
}
|
}
|
||||||
|
|
||||||
return lowest(asnRequests, countryRequests, unknown, blocklisted, reputationListed),
|
return lowest(asnRequests, countryRequests, unknown, blocklisted, reputationListed),
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ import (
|
|||||||
|
|
||||||
// The biased thresholds.
|
// The biased thresholds.
|
||||||
const (
|
const (
|
||||||
asnLimitPercent = "SWW_ASN_LIMIT_PERCENT"
|
asnLimitPercent = "SWWAF_ASN_LIMIT_PERCENT"
|
||||||
countryLimitPercent = "SWW_COUNTRY_LIMIT_PERCENT"
|
countryLimitPercent = "SWWAF_COUNTRY_LIMIT_PERCENT"
|
||||||
asnBytesPercent = "SWW_ASN_BYTES_PERCENT"
|
asnBytesPercent = "SWWAF_ASN_BYTES_PERCENT"
|
||||||
countryBytesPercent = "SWW_COUNTRY_BYTES_PERCENT"
|
countryBytesPercent = "SWWAF_COUNTRY_BYTES_PERCENT"
|
||||||
unknownLimitPercent = "SWW_UNKNOWN_LIMIT_PERCENT"
|
unknownLimitPercent = "SWWAF_UNKNOWN_LIMIT_PERCENT"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -230,7 +230,7 @@ func TestLowestPercentageApplies(t *testing.T) {
|
|||||||
noCountry, "25 from " + unknownLimitPercent,
|
noCountry, "25 from " + unknownLimitPercent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// SWW_UNKNOWN_LIMIT_PERCENT is left at its default, 100.
|
// SWWAF_UNKNOWN_LIMIT_PERCENT is left at its default, 100.
|
||||||
"the AS number's, for a client without a country",
|
"the AS number's, for a client without a country",
|
||||||
map[string]string{asnLimitPercent: "AS64500:25"},
|
map[string]string{asnLimitPercent: "AS64500:25"},
|
||||||
noCountry, "25 from " + asnLimitPercent,
|
noCountry, "25 from " + asnLimitPercent,
|
||||||
@@ -487,7 +487,7 @@ func wantPercent(t *testing.T, what string, percent *int64, setting, want string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// percentText gives a limit percentage and the setting that gave it as
|
// percentText gives a limit percentage and the setting that gave it as
|
||||||
// text, such as "50 from SWW_ASN_LIMIT_PERCENT", or none when both are
|
// text, such as "50 from SWWAF_ASN_LIMIT_PERCENT", or none when both are
|
||||||
// left out.
|
// left out.
|
||||||
func percentText(percent *int64, setting string) string {
|
func percentText(percent *int64, setting string) string {
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ import (
|
|||||||
|
|
||||||
// The byte limit settings.
|
// The byte limit settings.
|
||||||
const (
|
const (
|
||||||
bytesLimitPerMinute = "SWW_BYTES_LIMIT_PER_MINUTE"
|
bytesLimitPerMinute = "SWWAF_BYTES_LIMIT_PER_MINUTE"
|
||||||
bytesLimitPerHour = "SWW_BYTES_LIMIT_PER_HOUR"
|
bytesLimitPerHour = "SWWAF_BYTES_LIMIT_PER_HOUR"
|
||||||
bytesLimitPerDay = "SWW_BYTES_LIMIT_PER_DAY"
|
bytesLimitPerDay = "SWWAF_BYTES_LIMIT_PER_DAY"
|
||||||
bytesCount = "SWW_BYTES_COUNT"
|
bytesCount = "SWWAF_BYTES_COUNT"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The values of SWW_BYTES_COUNT.
|
// The values of SWWAF_BYTES_COUNT.
|
||||||
const (
|
const (
|
||||||
countResponse = "response"
|
countResponse = "response"
|
||||||
countRequest = "request"
|
countRequest = "request"
|
||||||
@@ -40,7 +40,7 @@ const (
|
|||||||
// byteLimit is the byte limit these tests set, as a setting: a request
|
// byteLimit is the byte limit these tests set, as a setting: a request
|
||||||
// with a body and its answer, 100 bytes, go over it.
|
// with a body and its answer, 100 bytes, go over it.
|
||||||
byteLimit = "99"
|
byteLimit = "99"
|
||||||
// minuteBytes is limit_hit for SWW_BYTES_LIMIT_PER_MINUTE.
|
// minuteBytes is limit_hit for SWWAF_BYTES_LIMIT_PER_MINUTE.
|
||||||
minuteBytes = "minute_bytes"
|
minuteBytes = "minute_bytes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -212,8 +212,8 @@ func TestByteLimitsLeaveOutWhatTheRateLimitsLeaveOut(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
allowed = "192.0.2.7" // in SWW_ALLOW_NETS
|
allowed = "192.0.2.7" // in SWWAF_ALLOW_NETS
|
||||||
exempt = "192.0.2.10" // in SWW_RATE_LIMIT_EXEMPT_NETS
|
exempt = "192.0.2.10" // in SWWAF_RATE_LIMIT_EXEMPT_NETS
|
||||||
)
|
)
|
||||||
|
|
||||||
s, _ := startWithAnswers(t, map[string]string{
|
s, _ := startWithAnswers(t, map[string]string{
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func scheme(r *http.Request, isQuestion bool) string {
|
|||||||
|
|
||||||
// clientGroup is the client a request is counted toward: its IPv4
|
// clientGroup is the client a request is counted toward: its IPv4
|
||||||
// address, or its IPv6 group, the netblock its IPv6 address is in of the
|
// address, or its IPv6 group, the netblock its IPv6 address is in of the
|
||||||
// length SWW_IPV6_GROUP_PREFIX sets, a /64 by default, since one abuser
|
// length SWWAF_IPV6_GROUP_PREFIX sets, a /64 by default, since one abuser
|
||||||
// usually holds a whole /64. An IPv4 address in IPv6 form counts as IPv4.
|
// usually holds a whole /64. An IPv4 address in IPv6 form counts as IPv4.
|
||||||
func (h *handler) clientGroup(addr netip.Addr) netip.Prefix {
|
func (h *handler) clientGroup(addr netip.Addr) netip.Prefix {
|
||||||
addr = addr.Unmap()
|
addr = addr.Unmap()
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// checkCoreRuleSet inspects the request with the Core Rule Set, unless
|
// checkCoreRuleSet inspects the request with the Core Rule Set, unless
|
||||||
// SWW_WAF_MODE is off or SWW_WAF_EXEMPT_PATHS exempts its path, as
|
// SWWAF_WAF_MODE is off or SWWAF_WAF_EXEMPT_PATHS exempts its path, as
|
||||||
// pathExempt decides, and notes the rules it matched and its score in the
|
// pathExempt decides, and notes the rules it matched and its score in the
|
||||||
// log line, and the rules in the metrics. A score at or over
|
// log line, and the rules in the metrics. A score at or over
|
||||||
// SWW_WAF_ANOMALY_THRESHOLD is a match: it raises the waf_block alert,
|
// SWWAF_WAF_ANOMALY_THRESHOLD is a match: it raises the waf_block alert,
|
||||||
// and in block mode refuses the request, which is an offence its client's
|
// and in block mode refuses the request, which is an offence its client's
|
||||||
// history counts, and so returns ActionWAFBlocked. It returns "" for a
|
// history counts, and so returns ActionWAFBlocked. It returns "" for a
|
||||||
// request it does not refuse.
|
// request it does not refuse.
|
||||||
@@ -55,7 +55,7 @@ func (rq *request) checkCoreRuleSet() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// alertWAFBlock raises the waf_block alert for the request, which the Core
|
// alertWAFBlock raises the waf_block alert for the request, which the Core
|
||||||
// Rule Set scored at result, at or over SWW_WAF_ANOMALY_THRESHOLD. Its
|
// Rule Set scored at result, at or over SWWAF_WAF_ANOMALY_THRESHOLD. Its
|
||||||
// detail gives the rule ids, the score, the method and the path with the
|
// detail gives the rule ids, the score, the method and the path with the
|
||||||
// query, and, for a request that is not refused for it, the mode: detect,
|
// query, and, for a request that is not refused for it, the mode: detect,
|
||||||
// or observe in observe mode.
|
// or observe in observe mode.
|
||||||
@@ -82,7 +82,7 @@ func (rq *request) alertWAFBlock(result waf.Result) {
|
|||||||
ASN: rq.line.ASN,
|
ASN: rq.line.ASN,
|
||||||
ASName: rq.line.ASName,
|
ASName: rq.line.ASName,
|
||||||
Country: rq.line.Country,
|
Country: rq.line.Country,
|
||||||
Reason: "scored by the Core Rule Set at or over SWW_WAF_ANOMALY_THRESHOLD",
|
Reason: "scored by the Core Rule Set at or over SWWAF_WAF_ANOMALY_THRESHOLD",
|
||||||
Detail: detail,
|
Detail: detail,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -14,13 +14,13 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The Core Rule Set's settings the tests set, besides SWW_WAF_MODE, and
|
// The Core Rule Set's settings the tests set, besides SWWAF_WAF_MODE, and
|
||||||
// its two modes that inspect requests.
|
// its two modes that inspect requests.
|
||||||
const (
|
const (
|
||||||
wafAnomalyThreshold = "SWW_WAF_ANOMALY_THRESHOLD"
|
wafAnomalyThreshold = "SWWAF_WAF_ANOMALY_THRESHOLD"
|
||||||
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
|
wafDisabledRules = "SWWAF_WAF_DISABLED_RULES"
|
||||||
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
|
wafExemptPaths = "SWWAF_WAF_EXEMPT_PATHS"
|
||||||
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
|
wafBodyLimit = "SWWAF_WAF_BODY_LIMIT"
|
||||||
block = "block"
|
block = "block"
|
||||||
detect = "detect"
|
detect = "detect"
|
||||||
)
|
)
|
||||||
@@ -28,7 +28,7 @@ const (
|
|||||||
// formData is the type of a form's body.
|
// formData is the type of a form's body.
|
||||||
const formData = "application/x-www-form-urlencoded"
|
const formData = "application/x-www-form-urlencoded"
|
||||||
|
|
||||||
// sizeLimit is SWW_WAF_BODY_LIMIT in the tests that read bodies, 1K as a
|
// sizeLimit is SWWAF_WAF_BODY_LIMIT in the tests that read bodies, 1K as a
|
||||||
// setting.
|
// setting.
|
||||||
const (
|
const (
|
||||||
sizeLimit = 1 << 10
|
sizeLimit = 1 << 10
|
||||||
@@ -37,7 +37,7 @@ const (
|
|||||||
|
|
||||||
// sqlInjection asks for / with an SQL injection in its query, which only
|
// sqlInjection asks for / with an SQL injection in its query, which only
|
||||||
// the Core Rule Set's rule 942100 matches, with a score of 5, the default
|
// the Core Rule Set's rule 942100 matches, with a score of 5, the default
|
||||||
// SWW_WAF_ANOMALY_THRESHOLD.
|
// SWWAF_WAF_ANOMALY_THRESHOLD.
|
||||||
const sqlInjection = "/?id=1'%20OR%20'1'='1"
|
const sqlInjection = "/?id=1'%20OR%20'1'='1"
|
||||||
|
|
||||||
// wantWAF checks the request log line's waf_rule_ids and waf_score, and
|
// wantWAF checks the request log line's waf_rule_ids and waf_score, and
|
||||||
@@ -125,7 +125,7 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
|
|||||||
) {
|
) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const allowed = "192.0.2.60" // in SWW_ALLOW_NETS
|
const allowed = "192.0.2.60" // in SWWAF_ALLOW_NETS
|
||||||
|
|
||||||
s, _, _ := startWithClock(t, "", map[string]string{
|
s, _, _ := startWithClock(t, "", map[string]string{
|
||||||
wafMode: block,
|
wafMode: block,
|
||||||
@@ -134,7 +134,7 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
|
|||||||
rulesDir: writeRules(t, testRules),
|
rulesDir: writeRules(t, testRules),
|
||||||
})
|
})
|
||||||
|
|
||||||
// A client in SWW_ALLOW_NETS, and a path SWW_WAF_EXEMPT_PATHS
|
// A client in SWWAF_ALLOW_NETS, and a path SWWAF_WAF_EXEMPT_PATHS
|
||||||
// exempts, are not inspected.
|
// exempts, are not inspected.
|
||||||
line := s.request(allowed, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
line := s.request(allowed, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
wantWAF(t, line, nil)
|
wantWAF(t, line, nil)
|
||||||
@@ -153,7 +153,7 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
|
|||||||
requestlog.ActionRuleBlocked)
|
requestlog.ActionRuleBlocked)
|
||||||
wantWAF(t, line, nil)
|
wantWAF(t, line, nil)
|
||||||
|
|
||||||
// With SWW_WAF_MODE off, no request is.
|
// With SWWAF_WAF_MODE off, no request is.
|
||||||
s, _, _ = startWithClock(t, "", map[string]string{wafMode: off})
|
s, _, _ = startWithClock(t, "", map[string]string{wafMode: off})
|
||||||
line = s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
line = s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
wantWAF(t, line, nil)
|
wantWAF(t, line, nil)
|
||||||
@@ -468,7 +468,7 @@ func TestCoreRuleSetMatchRaisesTheWAFBlockAlert(t *testing.T) {
|
|||||||
Host: appHost,
|
Host: appHost,
|
||||||
Client: netip.MustParseAddr(client),
|
Client: netip.MustParseAddr(client),
|
||||||
Netblock: netip.MustParsePrefix(client + "/32"),
|
Netblock: netip.MustParsePrefix(client + "/32"),
|
||||||
Reason: "scored by the Core Rule Set at or over SWW_WAF_ANOMALY_THRESHOLD",
|
Reason: "scored by the Core Rule Set at or over SWWAF_WAF_ANOMALY_THRESHOLD",
|
||||||
Detail: detail,
|
Detail: detail,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
// countryDenied reports whether the country lists refuse the request, by
|
// countryDenied reports whether the country lists refuse the request, by
|
||||||
// the client's country as it was looked up. A client without a country,
|
// the client's country as it was looked up. A client without a country,
|
||||||
// or whose country cannot be found, is refused only by
|
// or whose country cannot be found, is refused only by
|
||||||
// SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
// SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
||||||
func (rq *request) countryDenied() bool {
|
func (rq *request) countryDenied() bool {
|
||||||
denied := rq.h.config.DeniedCountries
|
denied := rq.h.config.DeniedCountries
|
||||||
allowed := rq.h.config.ExclusivelyAllowedCountries
|
allowed := rq.h.config.ExclusivelyAllowedCountries
|
||||||
|
|||||||
@@ -245,11 +245,11 @@ func TestExclusiveListRefusesAPrivateAddressUnlessAllowed(t *testing.T) {
|
|||||||
action string
|
action string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"not in SWW_ALLOW_NETS", "",
|
"not in SWWAF_ALLOW_NETS", "",
|
||||||
http.StatusForbidden, requestlog.ActionCountryDenied,
|
http.StatusForbidden, requestlog.ActionCountryDenied,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"in SWW_ALLOW_NETS", "10.0.0.7,fd00::/8",
|
"in SWWAF_ALLOW_NETS", "10.0.0.7,fd00::/8",
|
||||||
http.StatusOK, requestlog.ActionForward,
|
http.StatusOK, requestlog.ActionForward,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
@@ -279,7 +279,7 @@ func TestExclusiveListRefusesAPrivateAddressUnlessAllowed(t *testing.T) {
|
|||||||
// startGeoJS starts a stand-in for GeoJS, which places fromDE and fromKP,
|
// startGeoJS starts a stand-in for GeoJS, which places fromDE and fromKP,
|
||||||
// each in an AS of its own, and no other address. It returns its URL, and
|
// each in an AS of its own, and no other address. It returns its URL, and
|
||||||
// what returns the addresses it has been asked about. A test that needs
|
// what returns the addresses it has been asked about. A test that needs
|
||||||
// the stand-in to be asked or to answer sets SWW_LOOKUP_TIMEOUT to an
|
// the stand-in to be asked or to answer sets SWWAF_LOOKUP_TIMEOUT to an
|
||||||
// hour, whether or not a request waits for the answer: on the default, a
|
// hour, whether or not a request waits for the answer: on the default, a
|
||||||
// hold-up of the test process can abandon the request to the stand-in, and
|
// hold-up of the test process can abandon the request to the stand-in, and
|
||||||
// leave the client unknown.
|
// leave the client unknown.
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import (
|
|||||||
// test puts in the copy of its decision list, at decisionsURL, that it
|
// test puts in the copy of its decision list, at decisionsURL, that it
|
||||||
// needs, as reputation.json would at start.
|
// needs, as reputation.json would at start.
|
||||||
const (
|
const (
|
||||||
crowdSecURL = "SWW_CROWDSEC_LAPI_URL"
|
crowdSecURL = "SWWAF_CROWDSEC_LAPI_URL"
|
||||||
crowdSecKey = "SWW_CROWDSEC_LAPI_KEY"
|
crowdSecKey = "SWWAF_CROWDSEC_LAPI_KEY"
|
||||||
lapi = "http://crowdsec.example:8080"
|
lapi = "http://crowdsec.example:8080"
|
||||||
decisionsURL = lapi + "/v1/decisions"
|
decisionsURL = lapi + "/v1/decisions"
|
||||||
bouncerKey = "crowdsec-key-0123456789abcdef"
|
bouncerKey = "crowdsec-key-0123456789abcdef"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const (
|
|||||||
// headers and body.
|
// headers and body.
|
||||||
DispositionDeny = "deny"
|
DispositionDeny = "deny"
|
||||||
// DispositionClose tells rproxy to close the visitor's connection
|
// DispositionClose tells rproxy to close the visitor's connection
|
||||||
// without an answer, which SWW_BAN_RESPONSE asks for when it is close.
|
// without an answer, which SWWAF_BAN_RESPONSE asks for when it is close.
|
||||||
// discriminator closing the question's connection instead would read as
|
// discriminator closing the question's connection instead would read as
|
||||||
// discriminator being down, and rproxy would then let the request through.
|
// discriminator being down, and rproxy would then let the request through.
|
||||||
DispositionClose = "close"
|
DispositionClose = "close"
|
||||||
@@ -149,7 +149,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// refusedWhateverTheAnswer reports whether the request is one that does not wait
|
// refusedWhateverTheAnswer reports whether the request is one that does not wait
|
||||||
// for GeoJS, since the checks will refuse it at now whatever GeoJS answers about
|
// for GeoJS, since the checks will refuse it at now whatever GeoJS answers about
|
||||||
// its client: it is over the header limits, a blocklist lists its client while
|
// its client: it is over the header limits, a blocklist lists its client while
|
||||||
// SWW_BLOCKLIST_ACTION is deny, the CrowdSec decision list lists it, or it is
|
// SWWAF_BLOCKLIST_ACTION is deny, the CrowdSec decision list lists it, or it is
|
||||||
// over a whole rate limit. In observe mode, which refuses nothing, none is.
|
// over a whole rate limit. In observe mode, which refuses nothing, none is.
|
||||||
// rproxy lets a request through when discriminator answers too late.
|
// rproxy lets a request through when discriminator answers too late.
|
||||||
func (rq *request) refusedWhateverTheAnswer(now time.Time) bool {
|
func (rq *request) refusedWhateverTheAnswer(now time.Time) bool {
|
||||||
@@ -306,9 +306,10 @@ func (h *handler) refuseQuestion(w http.ResponseWriter, reason string) {
|
|||||||
|
|
||||||
// asTheVisitorSentIt returns a copy of r, a question, as the checks and the
|
// asTheVisitorSentIt returns a copy of r, a question, as the checks and the
|
||||||
// log line are to see it: without the headers rproxy adds for discriminator,
|
// log line are to see it: without the headers rproxy adds for discriminator,
|
||||||
// with body, which has been read of r, and with the length the visitor
|
// with
|
||||||
// announced, as -1 when it announced none. The question's own length covers
|
// body, which has been read of r, and with the length the visitor announced,
|
||||||
// only the part of the body it carries.
|
// as -1 when it announced none. The question's own length covers only the part
|
||||||
|
// of the body it carries.
|
||||||
func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request {
|
func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request {
|
||||||
sent := r.Clone(r.Context())
|
sent := r.Clone(r.Context())
|
||||||
announced := sent.Header.Get(ContentLengthHeader)
|
announced := sent.Header.Get(ContentLengthHeader)
|
||||||
@@ -335,7 +336,7 @@ func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// allow answers a question with DispositionAllow and, while
|
// allow answers a question with DispositionAllow and, while
|
||||||
// SWW_ADD_LOOKUP_HEADERS is set, the client's AS number and country.
|
// SWWAF_ADD_LOOKUP_HEADERS is set, the client's AS number and country.
|
||||||
func (rq *request) allow() {
|
func (rq *request) allow() {
|
||||||
rq.allowed = true
|
rq.allowed = true
|
||||||
|
|
||||||
@@ -361,7 +362,7 @@ func (rq *request) deny(r refusal) {
|
|||||||
rq.refused = &r
|
rq.refused = &r
|
||||||
|
|
||||||
if r.status == 0 {
|
if r.status == 0 {
|
||||||
// SWW_BAN_RESPONSE is close. Written past rq.out, since the log line's
|
// SWWAF_BAN_RESPONSE is close. Written past rq.out, since the log line's
|
||||||
// status stays 0: the visitor is sent nothing.
|
// status stays 0: the visitor is sent nothing.
|
||||||
rq.out.Header().Set(DispositionHeader, DispositionClose)
|
rq.out.Header().Set(DispositionHeader, DispositionClose)
|
||||||
rq.out.ResponseWriter.WriteHeader(http.StatusOK)
|
rq.out.ResponseWriter.WriteHeader(http.StatusOK)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const (
|
|||||||
// reportBodyLimit is the longest body SPEC.md lets a report have.
|
// reportBodyLimit is the longest body SPEC.md lets a report have.
|
||||||
reportBodyLimit = 1 << 20
|
reportBodyLimit = 1 << 20
|
||||||
// anomalyClientPerMinute is the anomaly threshold these tests set.
|
// anomalyClientPerMinute is the anomaly threshold these tests set.
|
||||||
anomalyClientPerMinute = "SWW_ANOMALY_CLIENT_REQUESTS_PER_MINUTE"
|
anomalyClientPerMinute = "SWWAF_ANOMALY_CLIENT_REQUESTS_PER_MINUTE"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The fields of a report, as "discriminator" in SPEC.md names them.
|
// The fields of a report, as "discriminator" in SPEC.md names them.
|
||||||
@@ -181,7 +181,7 @@ func (s *Discriminator) history() ratelimit.History {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// metrics returns what the metrics endpoint answers, for a server started with
|
// metrics returns what the metrics endpoint answers, for a server started with
|
||||||
// the token as SWW_METRICS_TOKEN.
|
// the token as SWWAF_METRICS_TOKEN.
|
||||||
func (s *Discriminator) metrics() string {
|
func (s *Discriminator) metrics() string {
|
||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
@@ -1485,7 +1485,7 @@ func TestALargeBodyAnnouncedAndLongHeadersAreNoReasonToRefuse(t *testing.T) {
|
|||||||
func TestLogLevelHoldsBackNoRequestLine(t *testing.T) {
|
func TestLogLevelHoldsBackNoRequestLine(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s := startDiscriminator(t, "", map[string]string{"SWW_LOG_LEVEL": "error"})
|
s := startDiscriminator(t, "", map[string]string{"SWWAF_LOG_LEVEL": "error"})
|
||||||
|
|
||||||
// A refused question would write a process warning, held back at error.
|
// A refused question would write a process warning, held back at error.
|
||||||
req := s.question(siteA, client, "/")
|
req := s.question(siteA, client, "/")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
|
const errorBurstThreshold = "SWWAF_ERROR_BURST_THRESHOLD"
|
||||||
|
|
||||||
// refused is a request the tests here send, which discriminator refuses after a rule
|
// refused is a request the tests here send, which discriminator refuses after a rule
|
||||||
// file match.
|
// file match.
|
||||||
@@ -48,7 +48,7 @@ func (r refused) send(s *sender, from string) logLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startForErrorBurst is startWithClock with testRules, the metrics token and
|
// startForErrorBurst is startWithClock with testRules, the metrics token and
|
||||||
// SWW_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
|
// SWWAF_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
|
||||||
func startForErrorBurst(
|
func startForErrorBurst(
|
||||||
t *testing.T, threshold string, env map[string]string,
|
t *testing.T, threshold string, env map[string]string,
|
||||||
) (*sender, *clock, *discriminator.Server) {
|
) (*sender, *clock, *discriminator.Server) {
|
||||||
@@ -258,15 +258,15 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
allowed = "192.0.2.60" // in SWW_ALLOW_NETS
|
allowed = "192.0.2.60" // in SWWAF_ALLOW_NETS
|
||||||
exempt = "192.0.2.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
|
exempt = "192.0.2.50" // in SWWAF_RATE_LIMIT_EXEMPT_NETS
|
||||||
)
|
)
|
||||||
|
|
||||||
s, _, _ := startForErrorBurst(t, "1", map[string]string{
|
s, _, _ := startForErrorBurst(t, "1", map[string]string{
|
||||||
allowNets: allowed, rateLimitExemptNets: exempt,
|
allowNets: allowed, rateLimitExemptNets: exempt,
|
||||||
})
|
})
|
||||||
|
|
||||||
// A client in SWW_ALLOW_NETS is not refused, and so not counted.
|
// A client in SWWAF_ALLOW_NETS is not refused, and so not counted.
|
||||||
for range 3 {
|
for range 3 {
|
||||||
s.request(allowed, blockedPath, http.StatusOK, requestlog.ActionForward)
|
s.request(allowed, blockedPath, http.StatusOK, requestlog.ActionForward)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// The headers in which an answer of allow gives rproxy the client's AS number
|
// The headers in which an answer of allow gives rproxy the client's AS number
|
||||||
// and country, for the app, while SWW_ADD_LOOKUP_HEADERS is set. Go writes
|
// and country, for the app, while SWWAF_ADD_LOOKUP_HEADERS is set. Go writes
|
||||||
// every header name in this form, as it sends it and as it receives it, so
|
// every header name in this form, as it sends it and as it receives it, so
|
||||||
// X-Client-ASN arrives as X-Client-Asn; header names are not case-sensitive.
|
// X-Client-ASN arrives as X-Client-Asn; header names are not case-sensitive.
|
||||||
const (
|
const (
|
||||||
@@ -20,7 +20,7 @@ const (
|
|||||||
|
|
||||||
// lookUp looks up the client's AS number and country, in the lookup
|
// lookUp looks up the client's AS number and country, in the lookup
|
||||||
// database or through GeoJS, and notes them for the log line, unless
|
// database or through GeoJS, and notes them for the log line, unless
|
||||||
// SWW_LOOKUP_SOURCE is off or the client is on a private, loopback or
|
// SWWAF_LOOKUP_SOURCE is off or the client is on a private, loopback or
|
||||||
// link-local address, which no lookup can place. The lookup database
|
// link-local address, which no lookup can place. The lookup database
|
||||||
// answers at once. With GeoJS, while a setting needs the answer, such as a
|
// answers at once. With GeoJS, while a setting needs the answer, such as a
|
||||||
// country list or a biased threshold, a new client's request waits for it,
|
// country list or a biased threshold, a new client's request waits for it,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
// and country.
|
// and country.
|
||||||
type asnAndCountry struct{ asn, asName, country string }
|
type asnAndCountry struct{ asn, asName, country string }
|
||||||
|
|
||||||
// fileSource is the SWW_LOOKUP_SOURCE that looks clients up in the
|
// fileSource is the SWWAF_LOOKUP_SOURCE that looks clients up in the
|
||||||
// lookup database.
|
// lookup database.
|
||||||
const fileSource = "file"
|
const fileSource = "file"
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name
|
metricsToken = "SWWAF_METRICS_TOKEN" //nolint:gosec // the setting's name
|
||||||
metricsTopN = "SWW_METRICS_TOP_N"
|
metricsTopN = "SWWAF_METRICS_TOP_N"
|
||||||
// token is the SWW_METRICS_TOKEN the tests set, and bearer how a
|
// token is the SWWAF_METRICS_TOKEN the tests set, and bearer how a
|
||||||
// request carries it.
|
// request carries it.
|
||||||
token = "0123456789abcdef0123456789abcdef"
|
token = "0123456789abcdef0123456789abcdef"
|
||||||
bearer = "Bearer " + token
|
bearer = "Bearer " + token
|
||||||
@@ -113,7 +113,7 @@ func TestMetricsCountTheTraffic(t *testing.T) {
|
|||||||
func TestMetricsCountLimitsAndBans(t *testing.T) {
|
func TestMetricsCountLimitsAndBans(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const denied = "192.0.2.50" // in SWW_DENY_NETS
|
const denied = "192.0.2.50" // in SWWAF_DENY_NETS
|
||||||
|
|
||||||
s, clk, _ := startWithClock(t, "", map[string]string{
|
s, clk, _ := startWithClock(t, "", map[string]string{
|
||||||
metricsToken: token,
|
metricsToken: token,
|
||||||
@@ -124,7 +124,7 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
|
|||||||
maxBanDuration: "2h",
|
maxBanDuration: "2h",
|
||||||
})
|
})
|
||||||
|
|
||||||
// SWW_BAN_RESPONSE=close sends no status at all.
|
// SWWAF_BAN_RESPONSE=close sends no status at all.
|
||||||
s.get(denied, 0, requestlog.ActionDenied)
|
s.get(denied, 0, requestlog.ActionDenied)
|
||||||
|
|
||||||
// A first broken limit bans for an hour.
|
// A first broken limit bans for an hour.
|
||||||
@@ -145,7 +145,7 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
|
|||||||
wantMetric(t, s.scrape(), `sww_active_bans{instance="app"}`, 0)
|
wantMetric(t, s.scrape(), `sww_active_bans{instance="app"}`, 0)
|
||||||
|
|
||||||
// A limit broken again right after would ban for three hours, longer
|
// A limit broken again right after would ban for three hours, longer
|
||||||
// than SWW_MAX_BAN_DURATION, so the ban is permanent.
|
// than SWWAF_MAX_BAN_DURATION, so the ban is permanent.
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(client, 0, requestlog.ActionRateLimited)
|
s.get(client, 0, requestlog.ActionRateLimited)
|
||||||
|
|
||||||
@@ -328,12 +328,12 @@ func TestMetricsCountGeoJSRequestsAndFailures(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// GeoJS fails, so the client counts as coming from an unknown country,
|
// GeoJS fails, so the client counts as coming from an unknown country,
|
||||||
// which SWW_DENIED_COUNTRIES does not refuse.
|
// which SWWAF_DENIED_COUNTRIES does not refuse.
|
||||||
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
|
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
|
||||||
req.Header.Set(forwardedFor, fromDE)
|
req.Header.Set(forwardedFor, fromDE)
|
||||||
wantStatus(t, do(t, req), http.StatusOK)
|
wantStatus(t, do(t, req), http.StatusOK)
|
||||||
|
|
||||||
// The client stops waiting for GeoJS once SWW_LOOKUP_TIMEOUT is up, so
|
// The client stops waiting for GeoJS once SWWAF_LOOKUP_TIMEOUT is up, so
|
||||||
// GeoJS's failure can come after its request has ended.
|
// GeoJS's failure can come after its request has ended.
|
||||||
deadline := time.Now().Add(waitLimit)
|
deadline := time.Now().Add(waitLimit)
|
||||||
metrics := scrape(t, server.Addr)
|
metrics := scrape(t, server.Addr)
|
||||||
|
|||||||
@@ -11,19 +11,19 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// observe is the value of SWW_MODE for observe mode.
|
// observe is the value of SWWAF_MODE for observe mode.
|
||||||
const observe = "observe"
|
const observe = "observe"
|
||||||
|
|
||||||
func TestObserveModeForwardsWhatEnforceModeRefuses(t *testing.T) {
|
func TestObserveModeForwardsWhatEnforceModeRefuses(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
denied = "192.0.2.50" // in SWW_DENY_NETS
|
denied = "192.0.2.50" // in SWWAF_DENY_NETS
|
||||||
banned = otherClient // under a ban read from bans.json
|
banned = otherClient // under a ban read from bans.json
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
setting string // "" leaves SWW_MODE at its default
|
setting string // "" leaves SWWAF_MODE at its default
|
||||||
observe bool
|
observe bool
|
||||||
}{
|
}{
|
||||||
{"", false},
|
{"", false},
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package discriminator
|
package discriminator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -33,15 +32,15 @@ const adminPrefix = "/_sww/"
|
|||||||
const HealthPath = "/_sww/healthz"
|
const HealthPath = "/_sww/healthz"
|
||||||
|
|
||||||
// MetricsPath is where the metrics are, for a request that carries
|
// MetricsPath is where the metrics are, for a request that carries
|
||||||
// SWW_METRICS_TOKEN.
|
// SWWAF_METRICS_TOKEN.
|
||||||
const MetricsPath = "/_sww/metrics"
|
const MetricsPath = "/_sww/metrics"
|
||||||
|
|
||||||
// BansPath is where an admin lists and adds bans, and, followed by / and
|
// BansPath is where an admin lists and adds bans, and, followed by / and
|
||||||
// a client's address, lifts them, with SWW_ADMIN_TOKEN.
|
// a client's address, lifts them, with SWWAF_ADMIN_TOKEN.
|
||||||
const BansPath = "/_sww/bans"
|
const BansPath = "/_sww/bans"
|
||||||
|
|
||||||
// ClientsPath is where an admin asks what discriminator knows of a client, by
|
// ClientsPath is where an admin asks what discriminator knows of a client, by
|
||||||
// the client's address after it, with SWW_ADMIN_TOKEN.
|
// the client's address after it, with SWWAF_ADMIN_TOKEN.
|
||||||
const ClientsPath = "/_sww/clients/"
|
const ClientsPath = "/_sww/clients/"
|
||||||
|
|
||||||
// Params are what New needs.
|
// Params are what New needs.
|
||||||
@@ -54,17 +53,14 @@ type Params struct {
|
|||||||
// ProcessLog receives the process's own messages.
|
// ProcessLog receives the process's own messages.
|
||||||
ProcessLog *slog.Logger
|
ProcessLog *slog.Logger
|
||||||
// GeoJSURL is where clients' AS numbers and countries are looked up
|
// GeoJSURL is where clients' AS numbers and countries are looked up
|
||||||
// while SWW_LOOKUP_SOURCE is geojs, normally lookup.URL.
|
// while SWWAF_LOOKUP_SOURCE is geojs, normally lookup.URL.
|
||||||
GeoJSURL string
|
GeoJSURL string
|
||||||
// AbuseIPDBURL is where clients are checked with AbuseIPDB while
|
// AbuseIPDBURL is where clients are checked with AbuseIPDB while
|
||||||
// SWW_ABUSEIPDB_KEY is set, normally reputation.AbuseIPDBURL.
|
// SWWAF_ABUSEIPDB_KEY is set, normally reputation.AbuseIPDBURL.
|
||||||
AbuseIPDBURL string
|
AbuseIPDBURL string
|
||||||
// LookupFile is the lookup database they are looked up in while
|
// LookupFile is the lookup database they are looked up in while
|
||||||
// SWW_LOOKUP_SOURCE is file, and nil otherwise.
|
// SWWAF_LOOKUP_SOURCE is file, and nil otherwise.
|
||||||
LookupFile *lookup.File
|
LookupFile *lookup.File
|
||||||
// CoreRuleSet is what NewCoreRuleSet returns: what inspects requests
|
|
||||||
// while SWW_WAF_MODE is not off, and nil while it is.
|
|
||||||
CoreRuleSet *waf.CoreRuleSet
|
|
||||||
// Now tells the time by which requests are counted for the rate
|
// Now tells the time by which requests are counted for the rate
|
||||||
// limits, bans are made and run out, and GeoJS's answers are kept,
|
// limits, bans are made and run out, and GeoJS's answers are kept,
|
||||||
// normally time.Now in UTC, the time the state files give.
|
// normally time.Now in UTC, the time the state files give.
|
||||||
@@ -76,14 +72,14 @@ type Params struct {
|
|||||||
// permanent, for each count over an anomaly threshold, for each request
|
// permanent, for each count over an anomaly threshold, for each request
|
||||||
// whose client a blocklist, the CrowdSec decision list, a DNSBL zone or
|
// whose client a blocklist, the CrowdSec decision list, a DNSBL zone or
|
||||||
// AbuseIPDB lists, for each request the Core Rule Set scores at or over
|
// AbuseIPDB lists, for each request the Core Rule Set scores at or over
|
||||||
// SWW_WAF_ANOMALY_THRESHOLD, and for GeoJS failing, a fetch of a list
|
// SWWAF_WAF_ANOMALY_THRESHOLD, and for GeoJS failing, a fetch of a list
|
||||||
// failing, a query to a DNSBL zone or a check with AbuseIPDB failing,
|
// failing, a query to a DNSBL zone or a check with AbuseIPDB failing,
|
||||||
// or the day's AbuseIPDB checks used up.
|
// or the day's AbuseIPDB checks used up.
|
||||||
Alerts *alerts.Queue
|
Alerts *alerts.Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server is the server discriminator runs, with the parts whose state the state
|
// Server is the server discriminator runs, with the parts whose state the state
|
||||||
// files keep, the lookup database, nil unless SWW_LOOKUP_SOURCE is file, the
|
// files keep, the lookup database, nil unless SWWAF_LOOKUP_SOURCE is file, the
|
||||||
// lists fetched from URLs, which its Run fetches, the DNSBL zones' verdicts,
|
// lists fetched from URLs, which its Run fetches, the DNSBL zones' verdicts,
|
||||||
// AbuseIPDB's scores and checks spent, and the metrics.
|
// AbuseIPDB's scores and checks spent, and the metrics.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -149,7 +145,7 @@ func newHandler(params Params) *handler {
|
|||||||
dnsbl: dnsbl,
|
dnsbl: dnsbl,
|
||||||
abuseIPDB: abuseIPDB,
|
abuseIPDB: abuseIPDB,
|
||||||
rules: params.Rules,
|
rules: params.Rules,
|
||||||
coreRuleSet: params.CoreRuleSet,
|
coreRuleSet: newCoreRuleSet(params.Config),
|
||||||
alerts: params.Alerts,
|
alerts: params.Alerts,
|
||||||
}
|
}
|
||||||
h.geojs = lookup.New(lookup.Params{
|
h.geojs = lookup.New(lookup.Params{
|
||||||
@@ -175,7 +171,7 @@ func newHandler(params Params) *handler {
|
|||||||
// newReputation returns the lists fetched from URLs, the DNSBL zones'
|
// newReputation returns the lists fetched from URLs, the DNSBL zones'
|
||||||
// verdicts and AbuseIPDB's scores, as the settings in params name them,
|
// verdicts and AbuseIPDB's scores, as the settings in params name them,
|
||||||
// with none fetched, asked for or checked yet, and adds their metrics to
|
// with none fetched, asked for or checked yet, and adds their metrics to
|
||||||
// m, AbuseIPDB's while SWW_ABUSEIPDB_KEY is set.
|
// m, AbuseIPDB's while SWWAF_ABUSEIPDB_KEY is set.
|
||||||
func newReputation(
|
func newReputation(
|
||||||
params Params, m *metrics.Metrics,
|
params Params, m *metrics.Metrics,
|
||||||
) (*reputation.Lists, *reputation.DNSBL, *reputation.AbuseIPDB) {
|
) (*reputation.Lists, *reputation.DNSBL, *reputation.AbuseIPDB) {
|
||||||
@@ -207,34 +203,29 @@ func newReputation(
|
|||||||
return lists, dnsbl, abuseIPDB
|
return lists, dnsbl, abuseIPDB
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCoreRuleSet returns the Core Rule Set at SWW_WAF_PARANOIA_LEVEL,
|
// newCoreRuleSet returns the Core Rule Set at SWWAF_WAF_PARANOIA_LEVEL,
|
||||||
// without the rules SWW_WAF_DISABLED_RULES switches off, reading bodies
|
// without the rules SWWAF_WAF_DISABLED_RULES switches off, reading bodies
|
||||||
// up to SWW_WAF_BODY_LIMIT, with the exclusions in
|
// up to SWWAF_WAF_BODY_LIMIT, or nil while SWWAF_WAF_MODE is off. It reads at
|
||||||
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE, or nil while SWW_WAF_MODE is off. It
|
// most QuestionBodyMaxBytes less one byte: a question's body of
|
||||||
// reads at most QuestionBodyMaxBytes less one byte: a question's body of
|
|
||||||
// QuestionBodyMaxBytes may be cut short, and the Core Rule Set does not read a
|
// QuestionBodyMaxBytes may be cut short, and the Core Rule Set does not read a
|
||||||
// JSON or XML body over its limit. An exclusions file that waf.New refuses is
|
// JSON or XML body over its limit.
|
||||||
// an error in every mode. While the mode is off, a set file is loaded with the
|
func newCoreRuleSet(cfg *config.Config) *waf.CoreRuleSet {
|
||||||
// Core Rule Set only to check it.
|
if cfg.WAFMode == config.WAFModeOff {
|
||||||
func NewCoreRuleSet(cfg *config.Config) (*waf.CoreRuleSet, error) {
|
return nil
|
||||||
if cfg.WAFMode == config.WAFModeOff && cfg.CoreRuleSetExclusionsFile == "" {
|
|
||||||
return nil, nil //nolint:nilnil // off is no Core Rule Set, and no error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
coreRuleSet, err := waf.New(waf.Params{
|
coreRuleSet, err := waf.New(waf.Params{
|
||||||
ParanoiaLevel: cfg.WAFParanoiaLevel, DisabledRules: cfg.WAFDisabledRules,
|
ParanoiaLevel: cfg.WAFParanoiaLevel, DisabledRules: cfg.WAFDisabledRules,
|
||||||
BodyLimit: min(cfg.WAFBodyLimit, QuestionBodyMaxBytes-1),
|
BodyLimit: min(cfg.WAFBodyLimit, QuestionBodyMaxBytes-1),
|
||||||
ExclusionsFile: cfg.CoreRuleSetExclusionsFile,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("SWW_CORE_RULE_SET_EXCLUSIONS_FILE: %w", err)
|
// The Core Rule Set is built in, and the settings cannot break it:
|
||||||
|
// the paranoia level is from 1 to 4, and the id of no rule switches
|
||||||
|
// nothing off.
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.WAFMode == config.WAFModeOff {
|
return coreRuleSet
|
||||||
return nil, nil //nolint:nilnil // off is no Core Rule Set, and no error
|
|
||||||
}
|
|
||||||
|
|
||||||
return coreRuleSet, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handler is discriminator's handler. It holds what every request shares; what
|
// handler is discriminator's handler. It holds what every request shares; what
|
||||||
|
|||||||
@@ -55,33 +55,33 @@ const (
|
|||||||
|
|
||||||
// The settings the tests set.
|
// The settings the tests set.
|
||||||
const (
|
const (
|
||||||
mode = "SWW_MODE"
|
mode = "SWWAF_MODE"
|
||||||
trustedProxies = "SWRP_TRUSTED_PROXIES"
|
trustedProxies = "SWRP_TRUSTED_PROXIES"
|
||||||
ipv6GroupPrefix = "SWW_IPV6_GROUP_PREFIX"
|
ipv6GroupPrefix = "SWWAF_IPV6_GROUP_PREFIX"
|
||||||
maxTrackedClients = "SWW_MAX_TRACKED_CLIENTS"
|
maxTrackedClients = "SWWAF_MAX_TRACKED_CLIENTS"
|
||||||
allowNets = "SWW_ALLOW_NETS"
|
allowNets = "SWWAF_ALLOW_NETS"
|
||||||
rateLimitExemptNets = "SWW_RATE_LIMIT_EXEMPT_NETS"
|
rateLimitExemptNets = "SWWAF_RATE_LIMIT_EXEMPT_NETS"
|
||||||
denyNets = "SWW_DENY_NETS"
|
denyNets = "SWWAF_DENY_NETS"
|
||||||
rateLimitPerMinute = "SWW_RATE_LIMIT_PER_MINUTE"
|
rateLimitPerMinute = "SWWAF_RATE_LIMIT_PER_MINUTE"
|
||||||
rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY"
|
rateLimitPerDay = "SWWAF_RATE_LIMIT_PER_DAY"
|
||||||
rateLimitExemptPaths = "SWW_RATE_LIMIT_EXEMPT_PATHS"
|
rateLimitExemptPaths = "SWWAF_RATE_LIMIT_EXEMPT_PATHS"
|
||||||
lookupSource = "SWW_LOOKUP_SOURCE"
|
lookupSource = "SWWAF_LOOKUP_SOURCE"
|
||||||
lookupDBPath = "SWW_LOOKUP_DB_PATH"
|
lookupDBPath = "SWWAF_LOOKUP_DB_PATH"
|
||||||
lookupTimeout = "SWW_LOOKUP_TIMEOUT"
|
lookupTimeout = "SWWAF_LOOKUP_TIMEOUT"
|
||||||
addLookupHeaders = "SWW_ADD_LOOKUP_HEADERS"
|
addLookupHeaders = "SWWAF_ADD_LOOKUP_HEADERS"
|
||||||
deniedCountries = "SWW_DENIED_COUNTRIES"
|
deniedCountries = "SWWAF_DENIED_COUNTRIES"
|
||||||
allowedCountries = "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"
|
allowedCountries = "SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES"
|
||||||
banResponse = "SWW_BAN_RESPONSE"
|
banResponse = "SWWAF_BAN_RESPONSE"
|
||||||
limitBanDuration = "SWW_LIMIT_BAN_DURATION"
|
limitBanDuration = "SWWAF_LIMIT_BAN_DURATION"
|
||||||
limitBanRepeatWindow = "SWW_LIMIT_BAN_REPEAT_WINDOW"
|
limitBanRepeatWindow = "SWWAF_LIMIT_BAN_REPEAT_WINDOW"
|
||||||
maxBanDuration = "SWW_MAX_BAN_DURATION"
|
maxBanDuration = "SWWAF_MAX_BAN_DURATION"
|
||||||
maxBans = "SWW_MAX_BANS"
|
maxBans = "SWWAF_MAX_BANS"
|
||||||
banScopeV4Prefix = "SWW_BAN_SCOPE_V4_PREFIX"
|
banScopeV4Prefix = "SWWAF_BAN_SCOPE_V4_PREFIX"
|
||||||
instanceName = "SWW_INSTANCE_NAME"
|
instanceName = "SWWAF_INSTANCE_NAME"
|
||||||
logRequestHeaders = "SWW_LOG_REQUEST_HEADERS"
|
logRequestHeaders = "SWWAF_LOG_REQUEST_HEADERS"
|
||||||
attackBanDuration = "SWW_ATTACK_BAN_DURATION"
|
attackBanDuration = "SWWAF_ATTACK_BAN_DURATION"
|
||||||
rulesDir = "SWW_RULES_DIR"
|
rulesDir = "SWWAF_RULES_DIR"
|
||||||
wafMode = "SWW_WAF_MODE"
|
wafMode = "SWWAF_WAF_MODE"
|
||||||
)
|
)
|
||||||
|
|
||||||
// off is the value that switches a setting off.
|
// off is the value that switches a setting off.
|
||||||
@@ -294,8 +294,8 @@ func startProxyWithGeoJS(
|
|||||||
|
|
||||||
// startProxyWithClock is startProxyWithGeoJS with requests counted and
|
// startProxyWithClock is startProxyWithGeoJS with requests counted and
|
||||||
// bans made by the time now tells, and returns discriminator's server as well, whose
|
// bans made by the time now tells, and returns discriminator's server as well, whose
|
||||||
// Addr is where discriminator listens. Unless env sets SWW_RULES_DIR, it is an empty
|
// Addr is where discriminator listens. Unless env sets SWWAF_RULES_DIR, it is an empty
|
||||||
// directory, of no rules, and unless it sets SWW_INSTANCE_NAME, that is app,
|
// directory, of no rules, and unless it sets SWWAF_INSTANCE_NAME, that is app,
|
||||||
// the label instance of every metric.
|
// the label instance of every metric.
|
||||||
func startProxyWithClock(
|
func startProxyWithClock(
|
||||||
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
||||||
@@ -391,13 +391,13 @@ func startRProxy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newDiscriminator makes discriminator's server, without starting it, and
|
// newDiscriminator makes discriminator's server, without starting it, and
|
||||||
// returns it, what it writes, and the queue of the alerts it raises, as the
|
// returns it, what
|
||||||
// settings in env make it. No alert is sent from the queue: they wait in it,
|
// it writes, and the queue of the alerts it raises, as the settings in env make
|
||||||
// for the test to look at.
|
// it. No alert is sent from the queue: they wait in it, for the test to look at.
|
||||||
// With no geojsURL, there is no stand-in for GeoJS to look clients up at, and
|
// With no geojsURL, there is no stand-in for GeoJS to look clients up at, and
|
||||||
// SWW_LOOKUP_SOURCE is off unless env sets it. While it is file, the lookup
|
// SWWAF_LOOKUP_SOURCE is off unless env sets it. While it is file, the lookup
|
||||||
// database SWW_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB
|
// database SWWAF_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB
|
||||||
// at abuseIPDBURL while env sets SWW_ABUSEIPDB_KEY. SWW_WAF_MODE is off unless
|
// at abuseIPDBURL while env sets SWWAF_ABUSEIPDB_KEY. SWWAF_WAF_MODE is off unless
|
||||||
// env sets it, so that only the tests of the Core Rule Set have their requests
|
// env sets it, so that only the tests of the Core Rule Set have their requests
|
||||||
// inspected by it.
|
// inspected by it.
|
||||||
func newDiscriminator(
|
func newDiscriminator(
|
||||||
@@ -464,11 +464,6 @@ func newDiscriminatorWith(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coreRuleSet, err := discriminator.NewCoreRuleSet(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Core Rule Set: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
server := build(discriminator.Params{
|
server := build(discriminator.Params{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
RequestLog: out,
|
RequestLog: out,
|
||||||
@@ -476,7 +471,6 @@ func newDiscriminatorWith(
|
|||||||
GeoJSURL: geojsURL,
|
GeoJSURL: geojsURL,
|
||||||
AbuseIPDBURL: abuseIPDBURL,
|
AbuseIPDBURL: abuseIPDBURL,
|
||||||
LookupFile: lookupFile,
|
LookupFile: lookupFile,
|
||||||
CoreRuleSet: coreRuleSet,
|
|
||||||
Now: now,
|
Now: now,
|
||||||
Rules: ruleFiles,
|
Rules: ruleFiles,
|
||||||
Alerts: alertQueue,
|
Alerts: alertQueue,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// minute is the window of SWW_RATE_LIMIT_PER_MINUTE, as a log line's
|
// minute is the window of SWWAF_RATE_LIMIT_PER_MINUTE, as a log line's
|
||||||
// limit_hit names it.
|
// limit_hit names it.
|
||||||
const minute = "minute"
|
const minute = "minute"
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ func TestRateLimitRefusesBeforeTheApp(t *testing.T) {
|
|||||||
func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
|
func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// With SWW_IPV6_GROUP_PREFIX at 48, the first two addresses, in two
|
// With SWWAF_IPV6_GROUP_PREFIX at 48, the first two addresses, in two
|
||||||
// /64s of one /48, are one client, and the second's request breaks the
|
// /64s of one /48, are one client, and the second's request breaks the
|
||||||
// limit; the third, in the next /48, is another client.
|
// limit; the third, in the next /48, is another client.
|
||||||
const (
|
const (
|
||||||
@@ -118,7 +118,7 @@ func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
|
|||||||
func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
|
func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const denied = "192.0.2.50" // in SWW_DENY_NETS
|
const denied = "192.0.2.50" // in SWWAF_DENY_NETS
|
||||||
|
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
s, _, server := startWithClock(t, geojsURL, map[string]string{
|
s, _, server := startWithClock(t, geojsURL, map[string]string{
|
||||||
@@ -151,7 +151,7 @@ func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
|
|||||||
// /assets/, and breaks the limit.
|
// /assets/, and breaks the limit.
|
||||||
s.request(client, "/assets", http.StatusForbidden, requestlog.ActionRateLimited)
|
s.request(client, "/assets", http.StatusForbidden, requestlog.ActionRateLimited)
|
||||||
|
|
||||||
// A ban, SWW_DENY_NETS and the country lists still refuse a path
|
// A ban, SWWAF_DENY_NETS and the country lists still refuse a path
|
||||||
// under a prefix.
|
// under a prefix.
|
||||||
s.request(client, "/assets/app.js", http.StatusForbidden, requestlog.ActionBanned)
|
s.request(client, "/assets/app.js", http.StatusForbidden, requestlog.ActionBanned)
|
||||||
s.request(denied, "/assets/app.js", http.StatusForbidden, requestlog.ActionDenied)
|
s.request(denied, "/assets/app.js", http.StatusForbidden, requestlog.ActionDenied)
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// deny is the SWW_BLOCKLIST_ACTION and the SWW_REPUTATION_ACTION that
|
// deny is the SWWAF_BLOCKLIST_ACTION and the SWWAF_REPUTATION_ACTION that
|
||||||
// refuses the requests of a client a source lists.
|
// refuses the requests of a client a source lists.
|
||||||
const deny = "deny"
|
const deny = "deny"
|
||||||
|
|
||||||
// blocklistDenied notes the blocklists that list the client, as
|
// blocklistDenied notes the blocklists that list the client, as
|
||||||
// noteListed does, and reports whether SWW_BLOCKLIST_ACTION, being deny,
|
// noteListed does, and reports whether SWWAF_BLOCKLIST_ACTION, being deny,
|
||||||
// refuses the request. Being limit, it lowers the client's limits instead
|
// refuses the request. Being limit, it lowers the client's limits instead
|
||||||
// (see limitPercentages), and being log, it does nothing more.
|
// (see limitPercentages), and being log, it does nothing more.
|
||||||
func (rq *request) blocklistDenied() bool {
|
func (rq *request) blocklistDenied() bool {
|
||||||
@@ -43,7 +43,7 @@ func (rq *request) crowdSecBanned(now time.Time) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// dnsblDenied notes the DNSBL zones whose verdict lists the client, as
|
// dnsblDenied notes the DNSBL zones whose verdict lists the client, as
|
||||||
// noteListed does, and reports whether SWW_REPUTATION_ACTION, being
|
// noteListed does, and reports whether SWWAF_REPUTATION_ACTION, being
|
||||||
// deny, refuses the request. Being limit, it lowers the client's limits
|
// deny, refuses the request. Being limit, it lowers the client's limits
|
||||||
// instead (see limitPercentages), and being log, it does nothing more. A
|
// instead (see limitPercentages), and being log, it does nothing more. A
|
||||||
// zone without a verdict on the client is asked about it in the
|
// zone without a verdict on the client is asked about it in the
|
||||||
@@ -59,8 +59,8 @@ func (rq *request) dnsblDenied(ctx context.Context) bool {
|
|||||||
|
|
||||||
// abuseIPDBDenied notes AbuseIPDB, as noteHit does, with the score, when
|
// abuseIPDBDenied notes AbuseIPDB, as noteHit does, with the score, when
|
||||||
// its score of the client is a hit, and reports whether
|
// its score of the client is a hit, and reports whether
|
||||||
// SWW_REPUTATION_ACTION, being deny, refuses the request, as dnsblDenied
|
// SWWAF_REPUTATION_ACTION, being deny, refuses the request, as dnsblDenied
|
||||||
// does for a zone. While SWW_ABUSEIPDB_KEY is unset it does nothing. A
|
// does for a zone. While SWWAF_ABUSEIPDB_KEY is unset it does nothing. A
|
||||||
// client without a score is checked in the background, by the request's
|
// client without a score is checked in the background, by the request's
|
||||||
// address, if its history counts an offence, and the request does not
|
// address, if its history counts an offence, and the request does not
|
||||||
// wait for the answer. The score is then used for each address of the
|
// wait for the answer. The score is then used for each address of the
|
||||||
@@ -81,7 +81,7 @@ func (rq *request) abuseIPDBDenied(ctx context.Context) bool {
|
|||||||
|
|
||||||
rq.abuseIPDBHit = true
|
rq.abuseIPDBHit = true
|
||||||
rq.noteHit(bans.ReputationHit{Source: reputation.AbuseIPDBSource, Score: &score},
|
rq.noteHit(bans.ReputationHit{Source: reputation.AbuseIPDBSource, Score: &score},
|
||||||
"scored by AbuseIPDB at or over SWW_ABUSEIPDB_MIN_SCORE")
|
"scored by AbuseIPDB at or over SWWAF_ABUSEIPDB_MIN_SCORE")
|
||||||
|
|
||||||
return rq.h.config.ReputationAction == deny
|
return rq.h.config.ReputationAction == deny
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ import (
|
|||||||
|
|
||||||
// The reputation settings.
|
// The reputation settings.
|
||||||
const (
|
const (
|
||||||
blocklistURLs = "SWW_BLOCKLIST_URLS"
|
blocklistURLs = "SWWAF_BLOCKLIST_URLS"
|
||||||
blocklistAction = "SWW_BLOCKLIST_ACTION"
|
blocklistAction = "SWWAF_BLOCKLIST_ACTION"
|
||||||
asnLimitPercentURL = "SWW_ASN_LIMIT_PERCENT_URL"
|
asnLimitPercentURL = "SWWAF_ASN_LIMIT_PERCENT_URL"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The actions of SWW_BLOCKLIST_ACTION and SWW_REPUTATION_ACTION:
|
// The actions of SWWAF_BLOCKLIST_ACTION and SWWAF_REPUTATION_ACTION:
|
||||||
// limitHalf gives a listed client half of every limit, and limitQuarter a
|
// limitHalf gives a listed client half of every limit, and limitQuarter a
|
||||||
// quarter.
|
// quarter.
|
||||||
const (
|
const (
|
||||||
@@ -127,7 +127,7 @@ func TestBlocklistsComeAfterTheCountryListsAndSkipAllowNets(t *testing.T) {
|
|||||||
loadLists(t, server, map[string][]string{dropURL: {fromDE, fromKP}})
|
loadLists(t, server, map[string][]string{dropURL: {fromDE, fromKP}})
|
||||||
|
|
||||||
// fromKP's country refuses it before the list is looked at, and fromDE,
|
// fromKP's country refuses it before the list is looked at, and fromDE,
|
||||||
// in SWW_ALLOW_NETS, is not checked at all: neither is noted, nor
|
// in SWWAF_ALLOW_NETS, is not checked at all: neither is noted, nor
|
||||||
// alerted.
|
// alerted.
|
||||||
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied))
|
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied))
|
||||||
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward))
|
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward))
|
||||||
@@ -224,7 +224,7 @@ func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(t *testing.T)
|
|||||||
},
|
},
|
||||||
{"none, for an AS number the file does not list", nil, asnKP + ":25", none, none},
|
{"none, for an AS number the file does not list", nil, asnKP + ":25", none, none},
|
||||||
{
|
{
|
||||||
"SWW_ASN_BYTES_PERCENT's in place of the file's for the byte limits",
|
"SWWAF_ASN_BYTES_PERCENT's in place of the file's for the byte limits",
|
||||||
map[string]string{asnBytesPercent: asnDE + ":100"}, asnDEQuarter, fromURL, none,
|
map[string]string{asnBytesPercent: asnDE + ":100"}, asnDEQuarter, fromURL, none,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
@@ -312,9 +312,9 @@ func TestEachBlocklistThatListsAClientRaisesAnAlertOncePerCooldownAndIsCounted(
|
|||||||
|
|
||||||
// The DNSBL settings.
|
// The DNSBL settings.
|
||||||
const (
|
const (
|
||||||
dnsblZones = "SWW_DNSBL_ZONES"
|
dnsblZones = "SWWAF_DNSBL_ZONES"
|
||||||
dnsblResolver = "SWW_DNSBL_RESOLVER"
|
dnsblResolver = "SWWAF_DNSBL_RESOLVER"
|
||||||
reputationAction = "SWW_REPUTATION_ACTION"
|
reputationAction = "SWWAF_REPUTATION_ACTION"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The DNSBL zones these tests name, which are never asked about the
|
// The DNSBL zones these tests name, which are never asked about the
|
||||||
@@ -370,7 +370,7 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
|
|||||||
for _, from := range []string{fromDE, fromKP} {
|
for _, from := range []string{fromDE, fromKP} {
|
||||||
for i := range 3 {
|
for i := range 3 {
|
||||||
line := s.get(from, tc.statuses[i], tc.actions[i])
|
line := s.get(from, tc.statuses[i], tc.actions[i])
|
||||||
// In the order SWW_DNSBL_ZONES names them.
|
// In the order SWWAF_DNSBL_ZONES names them.
|
||||||
wantReputation(t, line, listedBy[from]...)
|
wantReputation(t, line, listedBy[from]...)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent,
|
wantPercent(t, "limit_percent", line.LimitPercent,
|
||||||
line.LimitPercentSetting, tc.percent)
|
line.LimitPercentSetting, tc.percent)
|
||||||
@@ -416,7 +416,7 @@ func TestDNSBLZonesComeAfterTheBlocklistsAndSkipAllowNets(t *testing.T) {
|
|||||||
loadVerdicts(server, map[string][]string{fromKP: {dnsblZone}, fromDE: {dnsblZone}})
|
loadVerdicts(server, map[string][]string{fromKP: {dnsblZone}, fromDE: {dnsblZone}})
|
||||||
|
|
||||||
// The blocklist refuses fromKP before its verdict is looked at, and
|
// The blocklist refuses fromKP before its verdict is looked at, and
|
||||||
// fromDE, in SWW_ALLOW_NETS, is not checked at all: neither is noted
|
// fromDE, in SWWAF_ALLOW_NETS, is not checked at all: neither is noted
|
||||||
// for the zone, nor alerted, nor asked about.
|
// for the zone, nor alerted, nor asked about.
|
||||||
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionDenied),
|
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionDenied),
|
||||||
dropURL)
|
dropURL)
|
||||||
@@ -609,7 +609,7 @@ func TestRequestFromAClientWithoutAVerdictHasTheZoneAskedAboutIt(t *testing.T) {
|
|||||||
|
|
||||||
// The AbuseIPDB settings, and accountKey, the key the tests set.
|
// The AbuseIPDB settings, and accountKey, the key the tests set.
|
||||||
const (
|
const (
|
||||||
abuseIPDBKey = "SWW_ABUSEIPDB_KEY"
|
abuseIPDBKey = "SWWAF_ABUSEIPDB_KEY"
|
||||||
accountKey = "abuseipdb-key-0123456789abcdef"
|
accountKey = "abuseipdb-key-0123456789abcdef"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -800,7 +800,7 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *test
|
|||||||
rateLimitPerMinute: fourAMinute, abuseIPDBKey: accountKey,
|
rateLimitPerMinute: fourAMinute, abuseIPDBKey: accountKey,
|
||||||
reputationAction: tc.action,
|
reputationAction: tc.action,
|
||||||
})
|
})
|
||||||
// At SWW_ABUSEIPDB_MIN_SCORE, 75 by default, and just under it.
|
// At SWWAF_ABUSEIPDB_MIN_SCORE, 75 by default, and just under it.
|
||||||
loadScores(server, map[string]int64{fromDE: 75, fromKP: 74})
|
loadScores(server, map[string]int64{fromDE: 75, fromKP: 74})
|
||||||
|
|
||||||
for i := range 3 {
|
for i := range 3 {
|
||||||
@@ -851,7 +851,7 @@ func TestAbuseIPDBHitRaisesAnAlertWithTheScoreOncePerCooldownAndIsCounted(
|
|||||||
// The alert is made as a DNSBL zone's is, with the score besides.
|
// The alert is made as a DNSBL zone's is, with the score besides.
|
||||||
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != 1 || waiting[0].Event != alerts.EventReputationHit ||
|
if len(waiting) != 1 || waiting[0].Event != alerts.EventReputationHit ||
|
||||||
waiting[0].Reason != "scored by AbuseIPDB at or over SWW_ABUSEIPDB_MIN_SCORE" ||
|
waiting[0].Reason != "scored by AbuseIPDB at or over SWWAF_ABUSEIPDB_MIN_SCORE" ||
|
||||||
waiting[0].Detail["source"] != abuseipdb || waiting[0].Detail["score"] != int64(90) ||
|
waiting[0].Detail["source"] != abuseipdb || waiting[0].Detail["score"] != int64(90) ||
|
||||||
queue.Suppressed() != 1 {
|
queue.Suppressed() != 1 {
|
||||||
t.Errorf("alerts waiting %+v, %d held back, want AbuseIPDB's reputation_hit "+
|
t.Errorf("alerts waiting %+v, %d held back, want AbuseIPDB's reputation_hit "+
|
||||||
@@ -922,7 +922,7 @@ func TestBanNotesNameEachReputationSourceThatListedTheClient(t *testing.T) {
|
|||||||
abuseIPDBKey: accountKey, reputationAction: actionLog,
|
abuseIPDBKey: accountKey, reputationAction: actionLog,
|
||||||
})
|
})
|
||||||
// Every source lists client, and none otherClient, whose score is
|
// Every source lists client, and none otherClient, whose score is
|
||||||
// under SWW_ABUSEIPDB_MIN_SCORE, 75 by default.
|
// under SWWAF_ABUSEIPDB_MIN_SCORE, 75 by default.
|
||||||
loadLists(t, server, map[string][]string{dropURL: {client}})
|
loadLists(t, server, map[string][]string{dropURL: {client}})
|
||||||
loadVerdicts(server, map[string][]string{client: {dnsblZone}, otherClient: nil})
|
loadVerdicts(server, map[string][]string{client: {dnsblZone}, otherClient: nil})
|
||||||
loadScores(server, map[string]int64{client: score, otherClient: 74})
|
loadScores(server, map[string]int64{client: score, otherClient: 74})
|
||||||
@@ -971,7 +971,7 @@ func verdictsFetched() time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadVerdicts puts into server's DNSBL, for each client listedBy names,
|
// loadVerdicts puts into server's DNSBL, for each client listedBy names,
|
||||||
// a verdict of each zone SWW_DNSBL_ZONES names, fetched at
|
// a verdict of each zone SWWAF_DNSBL_ZONES names, fetched at
|
||||||
// verdictsFetched, as reputation.json would at start: one that lists the
|
// verdictsFetched, as reputation.json would at start: one that lists the
|
||||||
// client from each zone listedBy gives for it, and one that does not from
|
// client from each zone listedBy gives for it, and one that does not from
|
||||||
// each other zone.
|
// each other zone.
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ type decision struct {
|
|||||||
lookedUp bool
|
lookedUp bool
|
||||||
lookupAnswer lookup.Answer
|
lookupAnswer lookup.Answer
|
||||||
// skippedLookupWait is true for a request that did not wait for GeoJS and
|
// skippedLookupWait is true for a request that did not wait for GeoJS and
|
||||||
// found no answer kept. SWW_UNKNOWN_LIMIT_PERCENT does not apply to it,
|
// found no answer kept. SWWAF_UNKNOWN_LIMIT_PERCENT does not apply to it,
|
||||||
// so that not waiting never lowers its client's limits.
|
// so that not waiting never lowers its client's limits.
|
||||||
skippedLookupWait bool
|
skippedLookupWait bool
|
||||||
// counted is true for a request the rate limits counted, whose bytes
|
// counted is true for a request the rate limits counted, whose bytes
|
||||||
@@ -161,7 +161,7 @@ func requestHeaders(r *http.Request, names []string) map[string]string {
|
|||||||
|
|
||||||
// check is the one place where a request can be refused. It returns nil to let
|
// check is the one place where a request can be refused. It returns nil to let
|
||||||
// the request through. The checks of checkClient decide: a request they refuse
|
// the request through. The checks of checkClient decide: a request they refuse
|
||||||
// is answered with SWW_BAN_RESPONSE, 403 for a block rule or the Core Rule Set,
|
// is answered with SWWAF_BAN_RESPONSE, 403 for a block rule or the Core Rule Set,
|
||||||
// or 431 for the header limits. In observe mode nothing is refused, and the log
|
// or 431 for the header limits. In observe mode nothing is refused, and the log
|
||||||
// line names what enforce mode would have done. ctx is the request's own context.
|
// line names what enforce mode would have done. ctx is the request's own context.
|
||||||
func (rq *request) check(ctx context.Context) *refusal {
|
func (rq *request) check(ctx context.Context) *refusal {
|
||||||
@@ -185,17 +185,17 @@ func (rq *request) check(ctx context.Context) *refusal {
|
|||||||
|
|
||||||
// checkClient runs the checks on the request's client, and returns the
|
// checkClient runs the checks on the request's client, and returns the
|
||||||
// action of the first that refuses the request, or "" when none does. A
|
// action of the first that refuses the request, or "" when none does. A
|
||||||
// client in SWW_ALLOW_NETS skips them, and is not looked up. For any
|
// client in SWWAF_ALLOW_NETS skips them, and is not looked up. For any
|
||||||
// other client, SWW_DENY_NETS comes first, then a ban on its netblock,
|
// other client, SWWAF_DENY_NETS comes first, then a ban on its netblock,
|
||||||
// so that a client either refuses is not looked up, then the lookup of
|
// so that a client either refuses is not looked up, then the lookup of
|
||||||
// its AS number and country, then the country lists, then the blocklists,
|
// its AS number and country, then the country lists, then the blocklists,
|
||||||
// then the CrowdSec decision list, which bans the client it lists, then
|
// then the CrowdSec decision list, which bans the client it lists, then
|
||||||
// the DNSBL zones' verdicts, and then AbuseIPDB's score; a request any of
|
// the DNSBL zones' verdicts, and then AbuseIPDB's score; a request any of
|
||||||
// them refuses is not counted for the rate limits. Then come the
|
// them refuses is not counted for the rate limits. Then come the
|
||||||
// rate limits, unless the client is in SWW_RATE_LIMIT_EXEMPT_NETS or the
|
// rate limits, unless the client is in SWWAF_RATE_LIMIT_EXEMPT_NETS or the
|
||||||
// request's path is exempt under SWW_RATE_LIMIT_EXEMPT_PATHS, so that
|
// request's path is exempt under SWWAF_RATE_LIMIT_EXEMPT_PATHS, so that
|
||||||
// every other request is counted, each of them by the client's limit
|
// every other request is counted, each of them by the client's limit
|
||||||
// percentages, then SWW_TRAP_PATHS, then the rule files, then the header
|
// percentages, then SWWAF_TRAP_PATHS, then the rule files, then the header
|
||||||
// limits, and last the Core Rule Set. A request exempt from the rate limits
|
// limits, and last the Core Rule Set. A request exempt from the rate limits
|
||||||
// is exempt from the byte limits too. ctx is the request's own context.
|
// is exempt from the byte limits too. ctx is the request's own context.
|
||||||
func (rq *request) checkClient(ctx context.Context) string {
|
func (rq *request) checkClient(ctx context.Context) string {
|
||||||
@@ -266,8 +266,8 @@ func (rq *request) checkClient(ctx context.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// countedByRateLimits reports whether the rate limits count the request: its
|
// countedByRateLimits reports whether the rate limits count the request: its
|
||||||
// client is not in SWW_RATE_LIMIT_EXEMPT_NETS, and its path is not exempt
|
// client is not in SWWAF_RATE_LIMIT_EXEMPT_NETS, and its path is not exempt
|
||||||
// under SWW_RATE_LIMIT_EXEMPT_PATHS.
|
// under SWWAF_RATE_LIMIT_EXEMPT_PATHS.
|
||||||
func (rq *request) countedByRateLimits() bool {
|
func (rq *request) countedByRateLimits() bool {
|
||||||
cfg := rq.h.config
|
cfg := rq.h.config
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ func (rq *request) countedByRateLimits() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pathExempt reports whether r is exempt under prefixes,
|
// pathExempt reports whether r is exempt under prefixes,
|
||||||
// SWW_RATE_LIMIT_EXEMPT_PATHS or SWW_WAF_EXEMPT_PATHS: whether its
|
// SWWAF_RATE_LIMIT_EXEMPT_PATHS or SWWAF_WAF_EXEMPT_PATHS: whether its
|
||||||
// path as sent, the path the app receives, not percent-decoded, starts
|
// path as sent, the path the app receives, not percent-decoded, starts
|
||||||
// with one of prefixes, so that /%61ssets/x is not under /assets/ for an
|
// with one of prefixes, so that /%61ssets/x is not under /assets/ for an
|
||||||
// app whose router matches the path as received. A request whose decoded
|
// app whose router matches the path as received. A request whose decoded
|
||||||
@@ -378,9 +378,9 @@ func (rq *request) addToHistory() {
|
|||||||
|
|
||||||
// countAnomalies counts the request, which has ended, and its bytes, as
|
// countAnomalies counts the request, which has ended, and its bytes, as
|
||||||
// countedBytes gives them, for the anomaly thresholds, whatever was done
|
// countedBytes gives them, for the anomaly thresholds, whatever was done
|
||||||
// with it: a request refused, one from a client in SWW_ALLOW_NETS or
|
// with it: a request refused, one from a client in SWWAF_ALLOW_NETS or
|
||||||
// SWW_RATE_LIMIT_EXEMPT_NETS, and one for a path in
|
// SWWAF_RATE_LIMIT_EXEMPT_NETS, and one for a path in
|
||||||
// SWW_RATE_LIMIT_EXEMPT_PATHS are counted too. It is counted for its
|
// SWWAF_RATE_LIMIT_EXEMPT_PATHS are counted too. It is counted for its
|
||||||
// client's AS number when answerAtTheEnd gives one. With every anomaly
|
// client's AS number when answerAtTheEnd gives one. With every anomaly
|
||||||
// threshold off, the default, it does nothing.
|
// threshold off, the default, it does nothing.
|
||||||
func (rq *request) countAnomalies() {
|
func (rq *request) countAnomalies() {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
// requestIDHeader carries the request's id.
|
// requestIDHeader carries the request's id.
|
||||||
requestIDHeader = "X-Request-ID"
|
requestIDHeader = "X-Request-ID"
|
||||||
// instance is the SWW_INSTANCE_NAME a test sets.
|
// instance is the SWWAF_INSTANCE_NAME a test sets.
|
||||||
instance = "fsn1app1/blog"
|
instance = "fsn1app1/blog"
|
||||||
// ipv6Client is a client on IPv6, and ipv6Group the netblock the rate
|
// ipv6Client is a client on IPv6, and ipv6Group the netblock the rate
|
||||||
// limits count it as.
|
// limits count it as.
|
||||||
@@ -195,7 +195,7 @@ func TestHasAuthorizationAndHasCookieEachComeFromTheirOwnHeader(t *testing.T) {
|
|||||||
func TestTimingsAreInOrder(t *testing.T) {
|
func TestTimingsAreInOrder(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const denied = "192.0.2.50" // in SWW_DENY_NETS
|
const denied = "192.0.2.50" // in SWWAF_DENY_NETS
|
||||||
|
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
// The pause sets the times apart; a hold-up of the test only lengthens
|
// The pause sets the times apart; a hold-up of the test only lengthens
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ func TestEachRuleAction(t *testing.T) {
|
|||||||
line := s.get(client, http.StatusOK, requestlog.ActionForward)
|
line := s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
wantRuleIDs(t, line, "noted")
|
wantRuleIDs(t, line, "noted")
|
||||||
|
|
||||||
// A block rule refuses with 403, whatever SWW_BAN_RESPONSE is, and
|
// A block rule refuses with 403, whatever SWWAF_BAN_RESPONSE is, and
|
||||||
// bans no one.
|
// bans no one.
|
||||||
line = s.request(client, "/blocked", http.StatusForbidden,
|
line = s.request(client, "/blocked", http.StatusForbidden,
|
||||||
requestlog.ActionRuleBlocked)
|
requestlog.ActionRuleBlocked)
|
||||||
wantRuleIDs(t, line, "noted", "blocked")
|
wantRuleIDs(t, line, "noted", "blocked")
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
// A ban rule refuses with SWW_BAN_RESPONSE, and bans the client for
|
// A ban rule refuses with SWWAF_BAN_RESPONSE, and bans the client for
|
||||||
// seven days, the default.
|
// seven days, the default.
|
||||||
line = s.request(client, "/.env", http.StatusTooManyRequests, requestlog.ActionBanned)
|
line = s.request(client, "/.env", http.StatusTooManyRequests, requestlog.ActionBanned)
|
||||||
wantRuleIDs(t, line, "noted", "probe")
|
wantRuleIDs(t, line, "noted", "probe")
|
||||||
@@ -101,7 +101,7 @@ func TestNextClearSignOfAttackAfterABanBansPermanently(t *testing.T) {
|
|||||||
attackBanDuration: "1h",
|
attackBanDuration: "1h",
|
||||||
})
|
})
|
||||||
|
|
||||||
// The first probe bans for SWW_ATTACK_BAN_DURATION.
|
// The first probe bans for SWWAF_ATTACK_BAN_DURATION.
|
||||||
line := s.request(client, "/.env", http.StatusForbidden, requestlog.ActionBanned)
|
line := s.request(client, "/.env", http.StatusForbidden, requestlog.ActionBanned)
|
||||||
if line.BanExpires != requestlog.FormatTime(clk.Now().Add(time.Hour)) {
|
if line.BanExpires != requestlog.FormatTime(clk.Now().Add(time.Hour)) {
|
||||||
t.Errorf("log line has ban_expires %q, want an hour on", line.BanExpires)
|
t.Errorf("log line has ban_expires %q, want an hour on", line.BanExpires)
|
||||||
@@ -122,8 +122,8 @@ func TestRulesComeAfterTheOtherChecks(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
allowed = "192.0.2.60" // in SWW_ALLOW_NETS
|
allowed = "192.0.2.60" // in SWWAF_ALLOW_NETS
|
||||||
exempt = "192.0.2.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
|
exempt = "192.0.2.50" // in SWWAF_RATE_LIMIT_EXEMPT_NETS
|
||||||
)
|
)
|
||||||
|
|
||||||
s, _, server := startWithClock(t, "", map[string]string{
|
s, _, server := startWithClock(t, "", map[string]string{
|
||||||
@@ -133,7 +133,7 @@ func TestRulesComeAfterTheOtherChecks(t *testing.T) {
|
|||||||
rateLimitPerMinute: "1",
|
rateLimitPerMinute: "1",
|
||||||
})
|
})
|
||||||
|
|
||||||
// A client in SWW_ALLOW_NETS is not checked.
|
// A client in SWWAF_ALLOW_NETS is not checked.
|
||||||
line := s.request(allowed, "/.env", http.StatusOK, requestlog.ActionForward)
|
line := s.request(allowed, "/.env", http.StatusOK, requestlog.ActionForward)
|
||||||
wantRuleIDs(t, line)
|
wantRuleIDs(t, line)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// trapPath reports whether the request asks for a path in
|
// trapPath reports whether the request asks for a path in
|
||||||
// SWW_TRAP_PATHS: its path as a path rule sees it, without the query,
|
// SWWAF_TRAP_PATHS: its path as a path rule sees it, without the query,
|
||||||
// as sent or once percent-decoded, is one of them. Such a request is a
|
// as sent or once percent-decoded, is one of them. Such a request is a
|
||||||
// clear sign of attack, as a ban rule's match is: it bans the client's
|
// clear sign of attack, as a ban rule's match is: it bans the client's
|
||||||
// netblock, or in observe mode raises the alert for the ban it would have
|
// netblock, or in observe mode raises the alert for the ban it would have
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func TestAllowNetsSkipEveryCheck(t *testing.T) {
|
|||||||
calls.Add(1)
|
calls.Add(1)
|
||||||
})
|
})
|
||||||
geojsURL, asked := startGeoJS(t)
|
geojsURL, asked := startGeoJS(t)
|
||||||
// fromKP is in SWW_ALLOW_NETS, and in SWW_DENY_NETS too, which
|
// fromKP is in SWWAF_ALLOW_NETS, and in SWWAF_DENY_NETS too, which
|
||||||
// comes after it.
|
// comes after it.
|
||||||
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
|
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
|
||||||
trustedProxies: trustLocalhost,
|
trustedProxies: trustLocalhost,
|
||||||
@@ -36,7 +36,7 @@ func TestAllowNetsSkipEveryCheck(t *testing.T) {
|
|||||||
rateLimitPerMinute: "1",
|
rateLimitPerMinute: "1",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Neither SWW_DENY_NETS, the country lists nor the limit of one
|
// Neither SWWAF_DENY_NETS, the country lists nor the limit of one
|
||||||
// request a minute refuses the client, and its country is not looked
|
// request a minute refuses the client, and its country is not looked
|
||||||
// up.
|
// up.
|
||||||
wantAnswers(t, addr, out, []sentRequest{
|
wantAnswers(t, addr, out, []sentRequest{
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ import (
|
|||||||
// trapPaths is the setting's name, and trapPathList what the tests set it
|
// trapPaths is the setting's name, and trapPathList what the tests set it
|
||||||
// to.
|
// to.
|
||||||
const (
|
const (
|
||||||
trapPaths = "SWW_TRAP_PATHS"
|
trapPaths = "SWWAF_TRAP_PATHS"
|
||||||
trapPathList = "/wp-login.php,/xmlrpc.php"
|
trapPathList = "/wp-login.php,/xmlrpc.php"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
|
func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const allowed = "192.0.2.60" // in SWW_ALLOW_NETS
|
const allowed = "192.0.2.60" // in SWWAF_ALLOW_NETS
|
||||||
|
|
||||||
// A block rule for the same path: the trap path comes first.
|
// A block rule for the same path: the trap path comes first.
|
||||||
s, clk, server := startWithClock(t, "", map[string]string{
|
s, clk, server := startWithClock(t, "", map[string]string{
|
||||||
@@ -38,7 +38,7 @@ func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
|
|||||||
s.request(otherClient, path, http.StatusOK, requestlog.ActionForward)
|
s.request(otherClient, path, http.StatusOK, requestlog.ActionForward)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A client in SWW_ALLOW_NETS is not checked.
|
// A client in SWWAF_ALLOW_NETS is not checked.
|
||||||
s.request(allowed, "/xmlrpc.php", http.StatusOK, requestlog.ActionForward)
|
s.request(allowed, "/xmlrpc.php", http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
// The query is not part of the path.
|
// The query is not part of the path.
|
||||||
@@ -103,8 +103,8 @@ func TestTrapPathsNeedNoRuleFiles(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, _ := startWithClock(t, "", map[string]string{
|
s, _, _ := startWithClock(t, "", map[string]string{
|
||||||
trapPaths: trapPathList,
|
trapPaths: trapPathList,
|
||||||
"SWW_RULES_ENABLED": "false",
|
"SWWAF_RULES_ENABLED": "false",
|
||||||
})
|
})
|
||||||
|
|
||||||
s.request(client, "/xmlrpc.php", http.StatusForbidden, requestlog.ActionBanned)
|
s.request(client, "/xmlrpc.php", http.StatusForbidden, requestlog.ActionBanned)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const quietTime = 2 * time.Second
|
|||||||
// FileParams are what OpenFile needs.
|
// FileParams are what OpenFile needs.
|
||||||
type FileParams struct {
|
type FileParams struct {
|
||||||
// Path is the lookup database, the IPinfo Lite file in its .mmdb form
|
// Path is the lookup database, the IPinfo Lite file in its .mmdb form
|
||||||
// (SWW_LOOKUP_DB_PATH).
|
// (SWWAF_LOOKUP_DB_PATH).
|
||||||
Path string
|
Path string
|
||||||
// Now tells the time, normally time.Now.
|
// Now tells the time, normally time.Now.
|
||||||
Now func() time.Time
|
Now func() time.Time
|
||||||
@@ -221,12 +221,12 @@ func (f *File) use(reader *maxminddb.Reader) {
|
|||||||
func read(path string) (*maxminddb.Reader, error) {
|
func read(path string) (*maxminddb.Reader, error) {
|
||||||
data, err := os.ReadFile(path) //nolint:gosec // the file the admin names
|
data, err := os.ReadFile(path) //nolint:gosec // the file the admin names
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("SWW_LOOKUP_DB_PATH cannot be read: %w", err)
|
return nil, fmt.Errorf("SWWAF_LOOKUP_DB_PATH cannot be read: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := maxminddb.OpenBytes(data)
|
reader, err := maxminddb.OpenBytes(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("SWW_LOOKUP_DB_PATH %s is not a .mmdb file: %w", path, err)
|
return nil, fmt.Errorf("SWWAF_LOOKUP_DB_PATH %s is not a .mmdb file: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader, nil
|
return reader, nil
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ func TestFileThatCannotBeReadIsAnError(t *testing.T) {
|
|||||||
writeFile(t, notDatabase, "not a lookup database\n")
|
writeFile(t, notDatabase, "not a lookup database\n")
|
||||||
|
|
||||||
for path, want := range map[string]string{
|
for path, want := range map[string]string{
|
||||||
missing: "SWW_LOOKUP_DB_PATH cannot be read: open " + missing +
|
missing: "SWWAF_LOOKUP_DB_PATH cannot be read: open " + missing +
|
||||||
": no such file or directory",
|
": no such file or directory",
|
||||||
notDatabase: "SWW_LOOKUP_DB_PATH " + notDatabase +
|
notDatabase: "SWWAF_LOOKUP_DB_PATH " + notDatabase +
|
||||||
" is not a .mmdb file: error opening database: invalid MaxMind DB file",
|
" is not a .mmdb file: error opening database: invalid MaxMind DB file",
|
||||||
} {
|
} {
|
||||||
_, err := OpenFile(FileParams{
|
_, err := OpenFile(FileParams{
|
||||||
@@ -216,7 +216,7 @@ func TestReplacementThatCannotBeReadLeavesTheFileInUseWithOneAlert(t *testing.T)
|
|||||||
Reason: "the lookup database cannot be read, and the one read before stays in use",
|
Reason: "the lookup database cannot be read, and the one read before stays in use",
|
||||||
Detail: map[string]any{
|
Detail: map[string]any{
|
||||||
"file": path,
|
"file": path,
|
||||||
"error": "SWW_LOOKUP_DB_PATH " + path + " is not a .mmdb file: " +
|
"error": "SWWAF_LOOKUP_DB_PATH " + path + " is not a .mmdb file: " +
|
||||||
"error opening database: invalid MaxMind DB file",
|
"error opening database: invalid MaxMind DB file",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Package lookup looks up each client's AS number and country, through
|
// Package lookup looks up each client's AS number and country, through
|
||||||
// the GeoJS web service or in the lookup database, the IPinfo Lite file
|
// the GeoJS web service or in the lookup database, the IPinfo Lite file
|
||||||
// SWW_LOOKUP_DB_PATH names. GeoJS's answers are kept in memory, for at
|
// SWWAF_LOOKUP_DB_PATH names. GeoJS's answers are kept in memory, for at
|
||||||
// most 100,000 clients and for 7 days each, and are written to
|
// most 100,000 clients and for 7 days each, and are written to
|
||||||
// lookups.json and read from it by the state package.
|
// lookups.json and read from it by the state package.
|
||||||
package lookup
|
package lookup
|
||||||
@@ -66,7 +66,7 @@ type Params struct {
|
|||||||
URL string
|
URL string
|
||||||
// Timeout is how long a request waits for its client's first answer,
|
// Timeout is how long a request waits for its client's first answer,
|
||||||
// and how long a request to GeoJS may take before it is abandoned
|
// and how long a request to GeoJS may take before it is abandoned
|
||||||
// (SWW_LOOKUP_TIMEOUT).
|
// (SWWAF_LOOKUP_TIMEOUT).
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
// Wait is true when a setting needs each request's answer before the
|
// Wait is true when a setting needs each request's answer before the
|
||||||
// request goes on. Otherwise no request waits for one.
|
// request goes on. Otherwise no request waits for one.
|
||||||
|
|||||||
+10
-10
@@ -62,8 +62,8 @@ type Metrics struct {
|
|||||||
|
|
||||||
// New returns the metrics, with the Go runtime's and the process's own.
|
// New returns the metrics, with the Go runtime's and the process's own.
|
||||||
// topN is how many countries and how many AS numbers get series of their
|
// topN is how many countries and how many AS numbers get series of their
|
||||||
// own (SWW_METRICS_TOP_N). Every metric carries instanceName
|
// own (SWWAF_METRICS_TOP_N). Every metric carries instanceName
|
||||||
// (SWW_INSTANCE_NAME) as its label instance.
|
// (SWWAF_INSTANCE_NAME) as its label instance.
|
||||||
//
|
//
|
||||||
//nolint:funlen // a few lines for each metric, a list that grows with them
|
//nolint:funlen // a few lines for each metric, a list that grows with them
|
||||||
func New(topN int, instanceName string) *Metrics {
|
func New(topN int, instanceName string) *Metrics {
|
||||||
@@ -102,7 +102,7 @@ func New(topN int, instanceName string) *Metrics {
|
|||||||
offences: counterVec("sww_offences_total",
|
offences: counterVec("sww_offences_total",
|
||||||
"Offences, by kind.", []string{"kind"}),
|
"Offences, by kind.", []string{"kind"}),
|
||||||
wafMatches: counterVec("sww_waf_matches_total",
|
wafMatches: counterVec("sww_waf_matches_total",
|
||||||
"Requests that matched a rule of the Core Rule Set, by SWW_WAF_MODE "+
|
"Requests that matched a rule of the Core Rule Set, by SWWAF_WAF_MODE "+
|
||||||
"and the rule's id.", []string{"mode", "rule_id"}),
|
"and the rule's id.", []string{"mode", "rule_id"}),
|
||||||
countries: newCountries(topN),
|
countries: newCountries(topN),
|
||||||
asns: newASNs(topN),
|
asns: newASNs(topN),
|
||||||
@@ -214,13 +214,13 @@ func (m *Metrics) AddRules(ruleFiles *rules.Files) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddRemoteLog adds the metrics of sending the log lines to
|
// AddRemoteLog adds the metrics of sending the log lines to
|
||||||
// SWW_LOG_REMOTE_URL, read from remote as the metrics are asked for: the
|
// SWWAF_LOG_REMOTE_URL, read from remote as the metrics are asked for: the
|
||||||
// lines sent, those dropped, and those waiting in the buffer.
|
// lines sent, those dropped, and those waiting in the buffer.
|
||||||
func (m *Metrics) AddRemoteLog(remote *remotelog.Sender) {
|
func (m *Metrics) AddRemoteLog(remote *remotelog.Sender) {
|
||||||
m.registry.MustRegister(
|
m.registry.MustRegister(
|
||||||
prometheus.NewCounterFunc(prometheus.CounterOpts{
|
prometheus.NewCounterFunc(prometheus.CounterOpts{
|
||||||
Name: "sww_remote_log_lines_sent_total",
|
Name: "sww_remote_log_lines_sent_total",
|
||||||
Help: "Log lines sent to SWW_LOG_REMOTE_URL.",
|
Help: "Log lines sent to SWWAF_LOG_REMOTE_URL.",
|
||||||
}, func() float64 {
|
}, func() float64 {
|
||||||
return float64(remote.Sent())
|
return float64(remote.Sent())
|
||||||
}),
|
}),
|
||||||
@@ -313,7 +313,7 @@ func (m *Metrics) AddReputation(lists *reputation.Lists, dnsbl *reputation.DNSBL
|
|||||||
// AddAbuseIPDB adds the metrics of AbuseIPDB, with the source abuseipdb,
|
// AddAbuseIPDB adds the metrics of AbuseIPDB, with the source abuseipdb,
|
||||||
// read from abuseIPDB as the metrics are asked for: the checks made, those
|
// read from abuseIPDB as the metrics are asked for: the checks made, those
|
||||||
// that failed, and how many checks the day's budget has left. It is
|
// that failed, and how many checks the day's budget has left. It is
|
||||||
// called once, after AddReputation, while SWW_ABUSEIPDB_KEY is set.
|
// called once, after AddReputation, while SWWAF_ABUSEIPDB_KEY is set.
|
||||||
func (m *Metrics) AddAbuseIPDB(abuseIPDB *reputation.AbuseIPDB) {
|
func (m *Metrics) AddAbuseIPDB(abuseIPDB *reputation.AbuseIPDB) {
|
||||||
source := prometheus.Labels{sourceLabel: reputation.AbuseIPDBSource}
|
source := prometheus.Labels{sourceLabel: reputation.AbuseIPDBSource}
|
||||||
|
|
||||||
@@ -322,7 +322,7 @@ func (m *Metrics) AddAbuseIPDB(abuseIPDB *reputation.AbuseIPDB) {
|
|||||||
m.registry.MustRegister(
|
m.registry.MustRegister(
|
||||||
prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||||
Name: "sww_reputation_daily_budget_remaining",
|
Name: "sww_reputation_daily_budget_remaining",
|
||||||
Help: "Checks of the day's SWW_ABUSEIPDB_DAILY_BUDGET not yet spent.",
|
Help: "Checks of the day's SWWAF_ABUSEIPDB_DAILY_BUDGET not yet spent.",
|
||||||
ConstLabels: source,
|
ConstLabels: source,
|
||||||
}, func() float64 {
|
}, func() float64 {
|
||||||
return float64(abuseIPDB.BudgetLeft())
|
return float64(abuseIPDB.BudgetLeft())
|
||||||
@@ -363,8 +363,8 @@ func (m *Metrics) AddAlerts(queue *alerts.Queue) {
|
|||||||
}),
|
}),
|
||||||
prometheus.NewCounterFunc(prometheus.CounterOpts{
|
prometheus.NewCounterFunc(prometheus.CounterOpts{
|
||||||
Name: "sww_alerts_suppressed_total",
|
Name: "sww_alerts_suppressed_total",
|
||||||
Help: "Alerts held back: repeats within SWW_ALERT_COOLDOWN, and " +
|
Help: "Alerts held back: repeats within SWWAF_ALERT_COOLDOWN, and " +
|
||||||
"alerts past SWW_ALERT_MAX_PER_HOUR, for the hour's summary.",
|
"alerts past SWWAF_ALERT_MAX_PER_HOUR, for the hour's summary.",
|
||||||
ConstLabels: destination,
|
ConstLabels: destination,
|
||||||
}, func() float64 {
|
}, func() float64 {
|
||||||
return float64(queue.Suppressed())
|
return float64(queue.Suppressed())
|
||||||
@@ -442,7 +442,7 @@ func (m *Metrics) RuleMatched(id, action string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WAFMatched counts a request that matched the Core Rule Set's rule id,
|
// WAFMatched counts a request that matched the Core Rule Set's rule id,
|
||||||
// with SWW_WAF_MODE at mode.
|
// with SWWAF_WAF_MODE at mode.
|
||||||
func (m *Metrics) WAFMatched(mode string, id int) {
|
func (m *Metrics) WAFMatched(mode string, id int) {
|
||||||
m.wafMatches.WithLabelValues(mode, strconv.Itoa(id)).Inc()
|
m.wafMatches.WithLabelValues(mode, strconv.Itoa(id)).Inc()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// and bytes counted over a minute, an hour and a day, as the "Counting
|
// and bytes counted over a minute, an hour and a day, as the "Counting
|
||||||
// method" section of SPEC.md describes, which tell when a request takes
|
// method" section of SPEC.md describes, which tell when a request takes
|
||||||
// the client over a rate limit or a byte limit, and each client's history
|
// the client over a rate limit or a byte limit, and each client's history
|
||||||
// since it was first seen. At most SWW_MAX_TRACKED_CLIENTS clients are
|
// since it was first seen. At most SWWAF_MAX_TRACKED_CLIENTS clients are
|
||||||
// kept, in memory, and written to clients.json and read from it by the
|
// kept, in memory, and written to clients.json and read from it by the
|
||||||
// state package.
|
// state package.
|
||||||
package ratelimit
|
package ratelimit
|
||||||
@@ -156,7 +156,7 @@ type Request struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns a Limiter for limits, with no client counted yet, whose
|
// New returns a Limiter for limits, with no client counted yet, whose
|
||||||
// table holds at most maxClients clients (SWW_MAX_TRACKED_CLIENTS). Past
|
// table holds at most maxClients clients (SWWAF_MAX_TRACKED_CLIENTS). Past
|
||||||
// it, the least recently seen client is dropped, with its history, and
|
// it, the least recently seen client is dropped, with its history, and
|
||||||
// starts afresh if it comes back.
|
// starts afresh if it comes back.
|
||||||
func New(limits Limits, maxClients int) *Limiter {
|
func New(limits Limits, maxClients int) *Limiter {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
const limit = 3
|
const limit = 3
|
||||||
|
|
||||||
// tableSize is the most clients the tests' tables hold, the default of
|
// tableSize is the most clients the tests' tables hold, the default of
|
||||||
// SWW_MAX_TRACKED_CLIENTS.
|
// SWWAF_MAX_TRACKED_CLIENTS.
|
||||||
const tableSize = 20000
|
const tableSize = 20000
|
||||||
|
|
||||||
// whole is the percentage of each limit a client gets when nothing lowers
|
// whole is the percentage of each limit a client gets when nothing lowers
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package remotelog sends the lines sww writes on stdout to the
|
// Package remotelog sends the lines sww writes on stdout to the
|
||||||
// remote log endpoint, SWW_LOG_REMOTE_URL, as the "Request log" section
|
// remote log endpoint, SWWAF_LOG_REMOTE_URL, as the "Request log" section
|
||||||
// of SPEC.md describes: each line as the message of an RFC 5424 syslog
|
// of SPEC.md describes: each line as the message of an RFC 5424 syslog
|
||||||
// record, over UDP, TCP or TLS. Lines wait in a bounded buffer, so a slow
|
// record, over UDP, TCP or TLS. Lines wait in a bounded buffer, so a slow
|
||||||
// or unreachable endpoint never holds up a request or stdout.
|
// or unreachable endpoint never holds up a request or stdout.
|
||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The forms of SWW_LOG_REMOTE_URL, by its scheme.
|
// The forms of SWWAF_LOG_REMOTE_URL, by its scheme.
|
||||||
const (
|
const (
|
||||||
SchemeUDP = "syslog+udp"
|
SchemeUDP = "syslog+udp"
|
||||||
SchemeTCP = "syslog+tcp"
|
SchemeTCP = "syslog+tcp"
|
||||||
@@ -56,18 +56,18 @@ const (
|
|||||||
|
|
||||||
// Params are what New needs.
|
// Params are what New needs.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
// URL is the endpoint (SWW_LOG_REMOTE_URL): SchemeUDP, SchemeTCP or
|
// URL is the endpoint (SWWAF_LOG_REMOTE_URL): SchemeUDP, SchemeTCP or
|
||||||
// SchemeTLS, a host and a port.
|
// SchemeTLS, a host and a port.
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
// RootCAs are the certificates a SchemeTLS endpoint's certificate
|
// RootCAs are the certificates a SchemeTLS endpoint's certificate
|
||||||
// must chain to (SWW_LOG_REMOTE_TLS_CA_FILE), nil for the host's.
|
// must chain to (SWWAF_LOG_REMOTE_TLS_CA_FILE), nil for the host's.
|
||||||
RootCAs *x509.CertPool
|
RootCAs *x509.CertPool
|
||||||
// Buffer is the most lines held while they wait to be sent
|
// Buffer is the most lines held while they wait to be sent
|
||||||
// (SWW_LOG_REMOTE_BUFFER).
|
// (SWWAF_LOG_REMOTE_BUFFER).
|
||||||
Buffer int
|
Buffer int
|
||||||
// Facility is the number of the records' syslog facility
|
// Facility is the number of the records' syslog facility
|
||||||
// (SWW_LOG_REMOTE_FACILITY), and AppName their APP-NAME
|
// (SWWAF_LOG_REMOTE_FACILITY), and AppName their APP-NAME
|
||||||
// (SWW_LOG_REMOTE_APP_NAME).
|
// (SWWAF_LOG_REMOTE_APP_NAME).
|
||||||
Facility int
|
Facility int
|
||||||
AppName string
|
AppName string
|
||||||
}
|
}
|
||||||
@@ -243,7 +243,7 @@ func (s *Sender) send(ctx context.Context, processLog *slog.Logger) net.Conn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processLog.Warn("sending to SWW_LOG_REMOTE_URL failed",
|
processLog.Warn("sending to SWWAF_LOG_REMOTE_URL failed",
|
||||||
"error", err.Error(), "connecting_again_in", delay.String())
|
"error", err.Error(), "connecting_again_in", delay.String())
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -586,7 +586,7 @@ func wantRetries(t *testing.T, logged *output, delays ...string) {
|
|||||||
var fields map[string]any
|
var fields map[string]any
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(line), &fields)
|
err := json.Unmarshal([]byte(line), &fields)
|
||||||
if err != nil || fields["msg"] != "sending to SWW_LOG_REMOTE_URL failed" {
|
if err != nil || fields["msg"] != "sending to SWWAF_LOG_REMOTE_URL failed" {
|
||||||
t.Fatalf("logged %q", line)
|
t.Fatalf("logged %q", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,17 +58,17 @@ type Checks struct {
|
|||||||
// AbuseIPDBParams are what NewAbuseIPDB needs.
|
// AbuseIPDBParams are what NewAbuseIPDB needs.
|
||||||
type AbuseIPDBParams struct {
|
type AbuseIPDBParams struct {
|
||||||
// URL is where clients are checked, normally AbuseIPDBURL, with Key,
|
// URL is where clients are checked, normally AbuseIPDBURL, with Key,
|
||||||
// the account's key (SWW_ABUSEIPDB_KEY).
|
// the account's key (SWWAF_ABUSEIPDB_KEY).
|
||||||
URL string
|
URL string
|
||||||
Key string
|
Key string
|
||||||
// MinScore is the least score that is a hit (SWW_ABUSEIPDB_MIN_SCORE),
|
// MinScore is the least score that is a hit (SWWAF_ABUSEIPDB_MIN_SCORE),
|
||||||
// and DailyBudget the most checks made in a day, in UTC
|
// and DailyBudget the most checks made in a day, in UTC
|
||||||
// (SWW_ABUSEIPDB_DAILY_BUDGET).
|
// (SWWAF_ABUSEIPDB_DAILY_BUDGET).
|
||||||
MinScore int64
|
MinScore int64
|
||||||
DailyBudget int
|
DailyBudget int
|
||||||
// CacheTTL is how long a score is used after it was fetched
|
// CacheTTL is how long a score is used after it was fetched
|
||||||
// (SWW_REPUTATION_CACHE_TTL), and Timeout how long a check may take
|
// (SWWAF_REPUTATION_CACHE_TTL), and Timeout how long a check may take
|
||||||
// (SWW_REPUTATION_TIMEOUT).
|
// (SWWAF_REPUTATION_TIMEOUT).
|
||||||
CacheTTL time.Duration
|
CacheTTL time.Duration
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
// Now tells the time, normally time.Now in UTC.
|
// Now tells the time, normally time.Now in UTC.
|
||||||
|
|||||||
@@ -53,15 +53,15 @@ type Verdict struct {
|
|||||||
// DNSBLParams are what NewDNSBL needs.
|
// DNSBLParams are what NewDNSBL needs.
|
||||||
type DNSBLParams struct {
|
type DNSBLParams struct {
|
||||||
// Zones are the DNSBL zones clients are asked about in
|
// Zones are the DNSBL zones clients are asked about in
|
||||||
// (SWW_DNSBL_ZONES).
|
// (SWWAF_DNSBL_ZONES).
|
||||||
Zones []string
|
Zones []string
|
||||||
// Resolver is the resolver they are asked through
|
// Resolver is the resolver they are asked through
|
||||||
// (SWW_DNSBL_RESOLVER), or, while it is the zero AddrPort, the
|
// (SWWAF_DNSBL_RESOLVER), or, while it is the zero AddrPort, the
|
||||||
// host's, as /etc/resolv.conf names it.
|
// host's, as /etc/resolv.conf names it.
|
||||||
Resolver netip.AddrPort
|
Resolver netip.AddrPort
|
||||||
// CacheTTL is how long a verdict is used after it was fetched
|
// CacheTTL is how long a verdict is used after it was fetched
|
||||||
// (SWW_REPUTATION_CACHE_TTL), and Timeout how long a query may take
|
// (SWWAF_REPUTATION_CACHE_TTL), and Timeout how long a query may take
|
||||||
// (SWW_REPUTATION_TIMEOUT).
|
// (SWWAF_REPUTATION_TIMEOUT).
|
||||||
CacheTTL time.Duration
|
CacheTTL time.Duration
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
// Now tells the time, normally time.Now in UTC.
|
// Now tells the time, normally time.Now in UTC.
|
||||||
@@ -127,13 +127,13 @@ func NewDNSBL(params DNSBLParams) *DNSBL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zones returns the zones, in the order SWW_DNSBL_ZONES names them.
|
// Zones returns the zones, in the order SWWAF_DNSBL_ZONES names them.
|
||||||
func (d *DNSBL) Zones() []string {
|
func (d *DNSBL) Zones() []string {
|
||||||
return slices.Clone(d.params.Zones)
|
return slices.Clone(d.params.Zones)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListedBy returns the zones whose verdict on addr, a client's address,
|
// ListedBy returns the zones whose verdict on addr, a client's address,
|
||||||
// lists it, in the order SWW_DNSBL_ZONES names them, each with its key
|
// lists it, in the order SWWAF_DNSBL_ZONES names them, each with its key
|
||||||
// masked, as config.MaskZoneKey masks it, since they go to the request
|
// masked, as config.MaskZoneKey masks it, since they go to the request
|
||||||
// log, the alerts and the metrics. A verdict is used until CacheTTL has
|
// log, the alerts and the metrics. A verdict is used until CacheTTL has
|
||||||
// passed since it was fetched. Each zone without one is asked about addr
|
// passed since it was fetched. Each zone without one is asked about addr
|
||||||
@@ -210,7 +210,7 @@ func (d *DNSBL) Snapshot() []Verdict {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load keeps verdicts, read from reputation.json, in place of those it
|
// Load keeps verdicts, read from reputation.json, in place of those it
|
||||||
// keeps, but for those of a zone SWW_DNSBL_ZONES does not name, and,
|
// keeps, but for those of a zone SWWAF_DNSBL_ZONES does not name, and,
|
||||||
// past maxVerdicts, those fetched longest ago. One fetched CacheTTL ago or
|
// past maxVerdicts, those fetched longest ago. One fetched CacheTTL ago or
|
||||||
// more is neither used nor written, as for any verdict.
|
// more is neither used nor written, as for any verdict.
|
||||||
func (d *DNSBL) Load(verdicts []Verdict) {
|
func (d *DNSBL) Load(verdicts []Verdict) {
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ const (
|
|||||||
// zone and otherZone are the DNSBL zones the tests name.
|
// zone and otherZone are the DNSBL zones the tests name.
|
||||||
zone = "dnsbl.example"
|
zone = "dnsbl.example"
|
||||||
otherZone = "other.example"
|
otherZone = "other.example"
|
||||||
// cacheTTL is the tests' SWW_REPUTATION_CACHE_TTL, and timeout their
|
// cacheTTL is the tests' SWWAF_REPUTATION_CACHE_TTL, and timeout their
|
||||||
// SWW_REPUTATION_TIMEOUT: a second, the least time /etc/resolv.conf
|
// SWWAF_REPUTATION_TIMEOUT: a second, the least time /etc/resolv.conf
|
||||||
// can have Go's resolver wait for one server, so that it is the
|
// can have Go's resolver wait for one server, so that it is the
|
||||||
// DNSBL's own timeout that ends a query, whatever that file says.
|
// DNSBL's own timeout that ends a query, whatever that file says.
|
||||||
cacheTTL = 24 * time.Hour
|
cacheTTL = 24 * time.Hour
|
||||||
@@ -419,7 +419,7 @@ func TestNeitherAVerdictOfAZoneNotNamedNorOnePastItsTTLIsKept(t *testing.T) {
|
|||||||
dnsbl := reputation.NewDNSBL(p)
|
dnsbl := reputation.NewDNSBL(p)
|
||||||
|
|
||||||
// The last verdict still in use, one fetched a TTL ago, and one of a
|
// The last verdict still in use, one fetched a TTL ago, and one of a
|
||||||
// zone SWW_DNSBL_ZONES does not name.
|
// zone SWWAF_DNSBL_ZONES does not name.
|
||||||
inUse := reputation.Verdict{
|
inUse := reputation.Verdict{
|
||||||
Zone: zone, Client: netip.MustParseAddr(listed), Listed: true,
|
Zone: zone, Client: netip.MustParseAddr(listed), Listed: true,
|
||||||
Fetched: now.Add(-cacheTTL + time.Nanosecond),
|
Fetched: now.Add(-cacheTTL + time.Nanosecond),
|
||||||
@@ -471,7 +471,7 @@ func TestAtMost100000VerdictsKeptTheOneFetchedLongestAgoDroppedFirst(t *testing.
|
|||||||
}
|
}
|
||||||
|
|
||||||
//nolint:paralleltest // one at a time, as the comment at the top of this file says
|
//nolint:paralleltest // one at a time, as the comment at the top of this file says
|
||||||
func TestQueriesGoToTheResolverSWWDNSBLResolverNames(t *testing.T) {
|
func TestQueriesGoToTheResolverSWWAFDNSBLResolverNames(t *testing.T) {
|
||||||
resolver := &resolverStandIn{answers: map[string]answer{
|
resolver := &resolverStandIn{answers: map[string]answer{
|
||||||
listedName: {addrs: []string{listing}},
|
listedName: {addrs: []string{listing}},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Package reputation fetches the lists the settings name by URL: the
|
// Package reputation fetches the lists the settings name by URL: the
|
||||||
// blocklists of SWW_BLOCKLIST_URLS, the file of AS:percent lines
|
// blocklists of SWWAF_BLOCKLIST_URLS, the file of AS:percent lines
|
||||||
// SWW_ASN_LIMIT_PERCENT_URL names, and the decision list of the CrowdSec
|
// SWWAF_ASN_LIMIT_PERCENT_URL names, and the decision list of the CrowdSec
|
||||||
// engine SWW_CROWDSEC_LAPI_URL names. It keeps the last good copy of
|
// engine SWWAF_CROWDSEC_LAPI_URL names. It keeps the last good copy of
|
||||||
// each, whole, comment lines included, which is used while a fetch fails,
|
// each, whole, comment lines included, which is used while a fetch fails,
|
||||||
// and when each was last tried. It also asks the DNSBL zones of
|
// and when each was last tried. It also asks the DNSBL zones of
|
||||||
// SWW_DNSBL_ZONES about clients, and keeps their verdicts, and checks
|
// SWWAF_DNSBL_ZONES about clients, and keeps their verdicts, and checks
|
||||||
// clients with AbuseIPDB, and keeps their scores and the checks spent
|
// clients with AbuseIPDB, and keeps their scores and the checks spent
|
||||||
// today. The state package writes all of these to reputation.json and
|
// today. The state package writes all of these to reputation.json and
|
||||||
// reads them from it, so that a restart keeps them too.
|
// reads them from it, so that a restart keeps them too.
|
||||||
@@ -66,18 +66,18 @@ type List struct {
|
|||||||
|
|
||||||
// Params are what New needs.
|
// Params are what New needs.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
// BlocklistURLs are the blocklists (SWW_BLOCKLIST_URLS), and
|
// BlocklistURLs are the blocklists (SWWAF_BLOCKLIST_URLS), and
|
||||||
// ASNLimitPercentURL the file of AS:percent lines
|
// ASNLimitPercentURL the file of AS:percent lines
|
||||||
// (SWW_ASN_LIMIT_PERCENT_URL), "" while it is unset.
|
// (SWWAF_ASN_LIMIT_PERCENT_URL), "" while it is unset.
|
||||||
BlocklistURLs []string
|
BlocklistURLs []string
|
||||||
ASNLimitPercentURL string
|
ASNLimitPercentURL string
|
||||||
// CrowdSecDecisionsURL is the CrowdSec decision list, "" while
|
// CrowdSecDecisionsURL is the CrowdSec decision list, "" while
|
||||||
// SWW_CROWDSEC_LAPI_URL is unset, fetched with CrowdSecKey
|
// SWWAF_CROWDSEC_LAPI_URL is unset, fetched with CrowdSecKey
|
||||||
// (SWW_CROWDSEC_LAPI_KEY).
|
// (SWWAF_CROWDSEC_LAPI_KEY).
|
||||||
CrowdSecDecisionsURL string
|
CrowdSecDecisionsURL string
|
||||||
CrowdSecKey string
|
CrowdSecKey string
|
||||||
// Refresh is how long after a list was last fetched or tried it is
|
// Refresh is how long after a list was last fetched or tried it is
|
||||||
// fetched again (SWW_BLOCKLIST_REFRESH), but for the CrowdSec decision
|
// fetched again (SWWAF_BLOCKLIST_REFRESH), but for the CrowdSec decision
|
||||||
// list, which is fetched again crowdSecRefresh after.
|
// list, which is fetched again crowdSecRefresh after.
|
||||||
Refresh time.Duration
|
Refresh time.Duration
|
||||||
// Now tells the time, normally time.Now in UTC.
|
// Now tells the time, normally time.Now in UTC.
|
||||||
@@ -152,7 +152,7 @@ func New(params Params) *Lists {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// URLs returns the URL of every list: the blocklists' in the order
|
// URLs returns the URL of every list: the blocklists' in the order
|
||||||
// SWW_BLOCKLIST_URLS names them, then SWW_ASN_LIMIT_PERCENT_URL, then
|
// SWWAF_BLOCKLIST_URLS names them, then SWWAF_ASN_LIMIT_PERCENT_URL, then
|
||||||
// the CrowdSec decision list's.
|
// the CrowdSec decision list's.
|
||||||
func (l *Lists) URLs() []string {
|
func (l *Lists) URLs() []string {
|
||||||
urls := slices.Clone(l.params.BlocklistURLs)
|
urls := slices.Clone(l.params.BlocklistURLs)
|
||||||
@@ -168,7 +168,7 @@ func (l *Lists) URLs() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListedBy returns the URLs of the blocklists whose copy lists addr, in
|
// ListedBy returns the URLs of the blocklists whose copy lists addr, in
|
||||||
// the order SWW_BLOCKLIST_URLS names them.
|
// the order SWWAF_BLOCKLIST_URLS names them.
|
||||||
func (l *Lists) ListedBy(addr netip.Addr) []string {
|
func (l *Lists) ListedBy(addr netip.Addr) []string {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
defer l.mu.Unlock()
|
defer l.mu.Unlock()
|
||||||
@@ -551,7 +551,7 @@ func parseNetblock(text string) (netip.Prefix, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parsePercents reads the lines of the file of AS:percent lines, each an
|
// parsePercents reads the lines of the file of AS:percent lines, each an
|
||||||
// AS number, : and a percentage, as SWW_ASN_LIMIT_PERCENT takes them. An
|
// AS number, : and a percentage, as SWWAF_ASN_LIMIT_PERCENT takes them. An
|
||||||
// AS number listed more than once gets the lowest of its percentages.
|
// AS number listed more than once gets the lowest of its percentages.
|
||||||
func parsePercents(lines []string) (entries, error) {
|
func parsePercents(lines []string) (entries, error) {
|
||||||
found := entries{percents: map[string]int64{}}
|
found := entries{percents: map[string]int64{}}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ const (
|
|||||||
dropURL = "https://lists.example/drop.txt"
|
dropURL = "https://lists.example/drop.txt"
|
||||||
torURL = "https://lists.example/tor.txt"
|
torURL = "https://lists.example/tor.txt"
|
||||||
asnURL = "https://lists.example/asn.txt"
|
asnURL = "https://lists.example/asn.txt"
|
||||||
// refresh is the tests' SWW_BLOCKLIST_REFRESH, and cooldown their
|
// refresh is the tests' SWWAF_BLOCKLIST_REFRESH, and cooldown their
|
||||||
// SWW_ALERT_COOLDOWN, longer than it.
|
// SWWAF_ALERT_COOLDOWN, longer than it.
|
||||||
refresh = 24 * time.Hour
|
refresh = 24 * time.Hour
|
||||||
cooldown = 48 * time.Hour
|
cooldown = 48 * time.Hour
|
||||||
// drop is a blocklist as the Spamhaus DROP list is written, with an
|
// drop is a blocklist as the Spamhaus DROP list is written, with an
|
||||||
@@ -116,7 +116,7 @@ func TestClientIsListedByEachBlocklistThatListsIt(t *testing.T) {
|
|||||||
}}
|
}}
|
||||||
lists := start(t, servers, params(torURL, dropURL))
|
lists := start(t, servers, params(torURL, dropURL))
|
||||||
|
|
||||||
// In the order SWW_BLOCKLIST_URLS names them.
|
// In the order SWWAF_BLOCKLIST_URLS names them.
|
||||||
wantListedBy(t, lists, "203.0.113.9", torURL, dropURL)
|
wantListedBy(t, lists, "203.0.113.9", torURL, dropURL)
|
||||||
wantListedBy(t, lists, "203.0.113.8", dropURL)
|
wantListedBy(t, lists, "203.0.113.8", dropURL)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ const (
|
|||||||
// rule.
|
// rule.
|
||||||
ActionRuleBlocked = "rule_blocked"
|
ActionRuleBlocked = "rule_blocked"
|
||||||
// ActionWAFBlocked is a request refused because the Core Rule Set
|
// ActionWAFBlocked is a request refused because the Core Rule Set
|
||||||
// scored it at or over SWW_WAF_ANOMALY_THRESHOLD.
|
// scored it at or over SWWAF_WAF_ANOMALY_THRESHOLD.
|
||||||
ActionWAFBlocked = "waf_blocked"
|
ActionWAFBlocked = "waf_blocked"
|
||||||
// ActionDenied is a request refused because its client is in
|
// ActionDenied is a request refused because its client is in
|
||||||
// SWW_DENY_NETS, in a blocklist while SWW_BLOCKLIST_ACTION is deny,
|
// SWWAF_DENY_NETS, in a blocklist while SWWAF_BLOCKLIST_ACTION is deny,
|
||||||
// or listed by a DNSBL zone, or scored a hit by AbuseIPDB, while
|
// or listed by a DNSBL zone, or scored a hit by AbuseIPDB, while
|
||||||
// SWW_REPUTATION_ACTION is deny.
|
// SWWAF_REPUTATION_ACTION is deny.
|
||||||
ActionDenied = "denied"
|
ActionDenied = "denied"
|
||||||
// ActionCountryDenied is a request refused for its client's country.
|
// ActionCountryDenied is a request refused for its client's country.
|
||||||
ActionCountryDenied = "country_denied"
|
ActionCountryDenied = "country_denied"
|
||||||
@@ -112,7 +112,7 @@ type Line struct {
|
|||||||
ContentType string `json:"content_type,omitempty"`
|
ContentType string `json:"content_type,omitempty"`
|
||||||
// ContentLength is the length of its body the request announced.
|
// ContentLength is the length of its body the request announced.
|
||||||
ContentLength int64 `json:"content_length,omitempty"`
|
ContentLength int64 `json:"content_length,omitempty"`
|
||||||
// RequestHeaders are the headers SWW_LOG_REQUEST_HEADERS names that
|
// RequestHeaders are the headers SWWAF_LOG_REQUEST_HEADERS names that
|
||||||
// the request carried, by name in lower case.
|
// the request carried, by name in lower case.
|
||||||
RequestHeaders map[string]string `json:"request_headers,omitempty"`
|
RequestHeaders map[string]string `json:"request_headers,omitempty"`
|
||||||
HasAuthorization bool `json:"has_authorization,omitempty"`
|
HasAuthorization bool `json:"has_authorization,omitempty"`
|
||||||
@@ -250,8 +250,8 @@ func Milliseconds(d time.Duration) float64 {
|
|||||||
|
|
||||||
// NewProcessLogger returns the logger for the process's own messages:
|
// NewProcessLogger returns the logger for the process's own messages:
|
||||||
// JSON lines on w, marked "type":"process", with the time in the same form
|
// JSON lines on w, marked "type":"process", with the time in the same form
|
||||||
// as a request line's, and instanceName, SWW_INSTANCE_NAME, as instance.
|
// as a request line's, and instanceName, SWWAF_INSTANCE_NAME, as instance.
|
||||||
// It writes only the messages at level, SWW_LOG_LEVEL, or more severe;
|
// It writes only the messages at level, SWWAF_LOG_LEVEL, or more severe;
|
||||||
// the request lines Write writes are never held back.
|
// the request lines Write writes are never held back.
|
||||||
func NewProcessLogger(w io.Writer, instanceName string, level slog.Level) *slog.Logger {
|
func NewProcessLogger(w io.Writer, instanceName string, level slog.Level) *slog.Logger {
|
||||||
handler := slog.NewJSONHandler(w, &slog.HandlerOptions{
|
handler := slog.NewJSONHandler(w, &slog.HandlerOptions{
|
||||||
|
|||||||
@@ -146,9 +146,10 @@ func newTransport() *http.Transport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newDiscriminatorTransport returns what carries questions and reports to
|
// newDiscriminatorTransport returns what carries questions and reports to
|
||||||
// discriminator. It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send
|
// discriminator.
|
||||||
// a visitor's Upgrade header, or a Connection header other than close or
|
// It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send a visitor's Upgrade
|
||||||
// keep-alive, and that would read as discriminator giving no answer.
|
// header, or a Connection header other than close or keep-alive, and that would
|
||||||
|
// read as discriminator giving no answer.
|
||||||
func newDiscriminatorTransport() *http.Transport {
|
func newDiscriminatorTransport() *http.Transport {
|
||||||
transport := newTransport()
|
transport := newTransport()
|
||||||
transport.Protocols = new(http.Protocols)
|
transport.Protocols = new(http.Protocols)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const (
|
|||||||
countryHeader = "X-Client-Country"
|
countryHeader = "X-Client-Country"
|
||||||
contentLengthHeader = "Content-Length"
|
contentLengthHeader = "Content-Length"
|
||||||
// fromDiscriminatorHeader is a header the stand-in for discriminator adds to a deny.
|
// fromDiscriminatorHeader is a header the stand-in for discriminator adds to a deny.
|
||||||
fromDiscriminatorHeader = "X-From-Discriminator"
|
fromDiscriminatorHeader = "X-From-Servewaf"
|
||||||
// addedHeaderValue is the value of a header a test adds to an answer, to see it
|
// addedHeaderValue is the value of a header a test adds to an answer, to see it
|
||||||
// reach the visitor.
|
// reach the visitor.
|
||||||
addedHeaderValue = "yes"
|
addedHeaderValue = "yes"
|
||||||
@@ -268,8 +268,8 @@ func (s *Discriminator) question(t *testing.T) question {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startDiscriminatorAnsweringReports starts a stand-in for discriminator that
|
// startDiscriminatorAnsweringReports starts a stand-in for discriminator that
|
||||||
// answers each question as answer does and each report as report does, and
|
// answers each
|
||||||
// returns its URL.
|
// question as answer does and each report as report does, and returns its URL.
|
||||||
func startDiscriminatorAnsweringReports(
|
func startDiscriminatorAnsweringReports(
|
||||||
t *testing.T, answer, report http.HandlerFunc,
|
t *testing.T, answer, report http.HandlerFunc,
|
||||||
) string {
|
) string {
|
||||||
@@ -1033,8 +1033,9 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startDiscriminatorWriting starts a stand-in for discriminator that reads
|
// startDiscriminatorWriting starts a stand-in for discriminator that reads
|
||||||
// each question and answers it with text, as it is, and returns its URL. It
|
// each question and
|
||||||
// can send what Go's server would not.
|
// answers it with text, as it is, and returns its URL. It can send what Go's
|
||||||
|
// server would not.
|
||||||
func startDiscriminatorWriting(t *testing.T, text string) string {
|
func startDiscriminatorWriting(t *testing.T, text string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package rules reads the rule files: the plain text files in
|
// Package rules reads the rule files: the plain text files in
|
||||||
// SWW_RULES_DIR, one rule to a line, that each request is checked
|
// SWWAF_RULES_DIR, one rule to a line, that each request is checked
|
||||||
// against, as the "Rule files" section of SPEC.md describes. They are read
|
// against, as the "Rule files" section of SPEC.md describes. They are read
|
||||||
// at start, and again once the directory has had no change for a short
|
// at start, and again once the directory has had no change for a short
|
||||||
// time after one is edited, added or removed.
|
// time after one is edited, added or removed.
|
||||||
@@ -40,7 +40,7 @@ const (
|
|||||||
// extension ends the name of every rule file.
|
// extension ends the name of every rule file.
|
||||||
const extension = ".rules"
|
const extension = ".rules"
|
||||||
|
|
||||||
// quietTime is how long SWW_RULES_DIR must go without a change before
|
// quietTime is how long SWWAF_RULES_DIR must go without a change before
|
||||||
// the rule files are read again, so that a file still being written, such
|
// the rule files are read again, so that a file still being written, such
|
||||||
// as one saved in place, appended to or copied in with scp, is read only
|
// as one saved in place, appended to or copied in with scp, is read only
|
||||||
// once whole.
|
// once whole.
|
||||||
@@ -100,9 +100,9 @@ type Rule struct {
|
|||||||
|
|
||||||
// Params are what Load needs.
|
// Params are what Load needs.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
// Dir is the directory of the rule files (SWW_RULES_DIR).
|
// Dir is the directory of the rule files (SWWAF_RULES_DIR).
|
||||||
Dir string
|
Dir string
|
||||||
// Enabled is SWW_RULES_ENABLED: while it is false, no file is read
|
// Enabled is SWWAF_RULES_ENABLED: while it is false, no file is read
|
||||||
// and no rule loaded.
|
// and no rule loaded.
|
||||||
Enabled bool
|
Enabled bool
|
||||||
// ProcessLog receives how many rules were read, and the error in a
|
// ProcessLog receives how many rules were read, and the error in a
|
||||||
@@ -271,7 +271,7 @@ func (f *Files) logRead(count int) {
|
|||||||
func read(dir string) ([]Rule, string, error) {
|
func read(dir string) ([]Rule, string, error) {
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, dir, fmt.Errorf("SWW_RULES_DIR cannot be read: %w", err)
|
return nil, dir, fmt.Errorf("SWWAF_RULES_DIR cannot be read: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rules []Rule
|
var rules []Rule
|
||||||
@@ -299,7 +299,7 @@ func read(dir string) ([]Rule, string, error) {
|
|||||||
// readFile appends the rules of the rule file at path to rules. places
|
// readFile appends the rules of the rule file at path to rules. places
|
||||||
// are where each id read so far is, and gain those of the file.
|
// are where each id read so far is, and gain those of the file.
|
||||||
func readFile(path string, rules []Rule, places map[string]string) ([]Rule, error) {
|
func readFile(path string, rules []Rule, places map[string]string) ([]Rule, error) {
|
||||||
data, err := os.ReadFile(path) //nolint:gosec // a rule file, in SWW_RULES_DIR
|
data, err := os.ReadFile(path) //nolint:gosec // a rule file, in SWWAF_RULES_DIR
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ func TestDirectoryThatDoesNotExistStopsTheStart(t *testing.T) {
|
|||||||
|
|
||||||
dir := filepath.Join(t.TempDir(), "rules.d")
|
dir := filepath.Join(t.TempDir(), "rules.d")
|
||||||
|
|
||||||
wantRefused(t, dir, "SWW_RULES_DIR cannot be read: open "+dir+
|
wantRefused(t, dir, "SWWAF_RULES_DIR cannot be read: open "+dir+
|
||||||
": no such file or directory")
|
": no such file or directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +321,7 @@ func TestEmptyDirectoryLoadsNoRulesAndSaysSo(t *testing.T) {
|
|||||||
func TestRuleFilesOffReadNothing(t *testing.T) {
|
func TestRuleFilesOffReadNothing(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// SWW_RULES_DIR does not exist, which would stop the start.
|
// SWWAF_RULES_DIR does not exist, which would stop the start.
|
||||||
params, _ := newParams(filepath.Join(t.TempDir(), "rules.d"))
|
params, _ := newParams(filepath.Join(t.TempDir(), "rules.d"))
|
||||||
params.Enabled = false
|
params.Enabled = false
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package state keeps sww's state in JSON files in
|
// Package state keeps sww's state in JSON files in
|
||||||
// SWW_STATE_DIR, as the "Persistent state" section of SPEC.md requires
|
// SWWAF_STATE_DIR, as the "Persistent state" section of SPEC.md requires
|
||||||
// and docs/state-files.md lists them:
|
// and docs/state-files.md lists them:
|
||||||
// bans.json holds the bans, clients.json each client's counters and
|
// bans.json holds the bans, clients.json each client's counters and
|
||||||
// history, lookups.json GeoJS's answers, reputation.json the last try and
|
// history, lookups.json GeoJS's answers, reputation.json the last try and
|
||||||
@@ -72,11 +72,11 @@ var (
|
|||||||
|
|
||||||
// Params are what Load needs.
|
// Params are what Load needs.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
// Dir is the directory of the state files (SWW_STATE_DIR).
|
// Dir is the directory of the state files (SWWAF_STATE_DIR).
|
||||||
Dir string
|
Dir string
|
||||||
// WriteDelay is how long after a ban is made bans.json is written
|
// WriteDelay is how long after a ban is made bans.json is written
|
||||||
// (SWW_STATE_WRITE_DELAY), and CounterInterval how often every file
|
// (SWWAF_STATE_WRITE_DELAY), and CounterInterval how often every file
|
||||||
// is (SWW_STATE_COUNTER_INTERVAL).
|
// is (SWWAF_STATE_COUNTER_INTERVAL).
|
||||||
WriteDelay time.Duration
|
WriteDelay time.Duration
|
||||||
CounterInterval time.Duration
|
CounterInterval time.Duration
|
||||||
// Ledger, Limiter, GeoJS, Lists, DNSBL, AbuseIPDB, Alerts and Anomalies
|
// Ledger, Limiter, GeoJS, Lists, DNSBL, AbuseIPDB, Alerts and Anomalies
|
||||||
@@ -185,7 +185,7 @@ type stateFile interface {
|
|||||||
func Load(params Params) (*Files, error) {
|
func Load(params Params) (*Files, error) {
|
||||||
err := checkWritable(params.Dir)
|
err := checkWritable(params.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("SWW_STATE_DIR cannot be written: %w", err)
|
return nil, fmt.Errorf("SWWAF_STATE_DIR cannot be written: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f := &Files{params: params, sums: map[string][sha256.Size]byte{}}
|
f := &Files{params: params, sums: map[string][sha256.Size]byte{}}
|
||||||
@@ -362,7 +362,7 @@ func (f *Files) read(name string) (int, error) {
|
|||||||
func (f *Files) readChanged(name string) ([]byte, bool, error) {
|
func (f *Files) readChanged(name string) ([]byte, bool, error) {
|
||||||
path := filepath.Join(f.params.Dir, name)
|
path := filepath.Join(f.params.Dir, name)
|
||||||
|
|
||||||
data, err := os.ReadFile(path) //nolint:gosec // a state file, in SWW_STATE_DIR
|
data, err := os.ReadFile(path) //nolint:gosec // a state file, in SWWAF_STATE_DIR
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
@@ -1035,7 +1035,7 @@ func write(dir, name string, data []byte) error {
|
|||||||
|
|
||||||
// syncDirectory syncs dir to the disk, so that a rename in it lasts.
|
// syncDirectory syncs dir to the disk, so that a rename in it lasts.
|
||||||
func syncDirectory(dir string) error {
|
func syncDirectory(dir string) error {
|
||||||
directory, err := os.Open(dir) //nolint:gosec // SWW_STATE_DIR itself
|
directory, err := os.Open(dir) //nolint:gosec // SWWAF_STATE_DIR itself
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1045,7 +1045,7 @@ func syncDirectory(dir string) error {
|
|||||||
|
|
||||||
// writeSynced writes data to the file at path, and syncs it to the disk.
|
// writeSynced writes data to the file at path, and syncs it to the disk.
|
||||||
func writeSynced(path string, data []byte) error {
|
func writeSynced(path string, data []byte) error {
|
||||||
//nolint:gosec // a state file's temporary file, in SWW_STATE_DIR
|
//nolint:gosec // a state file's temporary file, in SWWAF_STATE_DIR
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -931,7 +931,7 @@ func TestUnwritableDirectoryStopsTheStart(t *testing.T) {
|
|||||||
filepath.Join(t.TempDir(), "missing"),
|
filepath.Join(t.TempDir(), "missing"),
|
||||||
notADirectory,
|
notADirectory,
|
||||||
} {
|
} {
|
||||||
const want = "SWW_STATE_DIR cannot be written: "
|
const want = "SWWAF_STATE_DIR cannot be written: "
|
||||||
|
|
||||||
_, err := state.Load(newParams(dir))
|
_, err := state.Load(newParams(dir))
|
||||||
if err == nil || !strings.HasPrefix(err.Error(), want) {
|
if err == nil || !strings.HasPrefix(err.Error(), want) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ var errHealthEndpoint = errors.New("the health endpoint answered")
|
|||||||
|
|
||||||
// HealthCheck is the container's health check. In discriminator's container, where
|
// HealthCheck is the container's health check. In discriminator's container, where
|
||||||
// SWW_COMMAND is discriminator, it returns 0 while discriminator answers its health
|
// SWW_COMMAND is discriminator, it returns 0 while discriminator answers its health
|
||||||
// endpoint on 127.0.0.1, at the port in SWW_LISTEN_ADDR. In rproxy's container,
|
// endpoint on 127.0.0.1, at the port in SWWAF_LISTEN_ADDR. In rproxy's container,
|
||||||
// where SWW_COMMAND is rproxy, unset or empty, it returns 0 while rproxy
|
// where SWW_COMMAND is rproxy, unset or empty, it returns 0 while rproxy
|
||||||
// answers its health endpoint on 127.0.0.1, at the port in SWRP_LISTEN_ADDR, and
|
// answers its health endpoint on 127.0.0.1, at the port in SWRP_LISTEN_ADDR, and
|
||||||
// the app accepts connections at the address in SWRP_UPSTREAM_URL. Otherwise,
|
// the app accepts connections at the address in SWRP_UPSTREAM_URL. Otherwise,
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ func TestHealthCheckInDiscriminatorsContainer(t *testing.T) {
|
|||||||
// or invalid token file, or an invalid value of another setting, does not
|
// or invalid token file, or an invalid value of another setting, does not
|
||||||
// fail it.
|
// fail it.
|
||||||
for _, other := range []struct{ name, value string }{
|
for _, other := range []struct{ name, value string }{
|
||||||
{"SWW_METRICS_TOKEN_FILE", filepath.Join(t.TempDir(), "removed")},
|
{"SWWAF_METRICS_TOKEN_FILE", filepath.Join(t.TempDir(), "removed")},
|
||||||
{"SWW_METRICS_TOKEN_FILE", writeFile(t, "too short\n")},
|
{"SWWAF_METRICS_TOKEN_FILE", writeFile(t, "too short\n")},
|
||||||
{"SWW_MODE", "neither"},
|
{"SWWAF_MODE", "neither"},
|
||||||
} {
|
} {
|
||||||
wantHealthCheck(t, map[string]string{
|
wantHealthCheck(t, map[string]string{
|
||||||
commandSetting: discriminatorCommand,
|
commandSetting: discriminatorCommand,
|
||||||
@@ -72,7 +72,7 @@ func TestHealthCheckInDiscriminatorsContainer(t *testing.T) {
|
|||||||
}, 1, "unhealthy: ask discriminator: ")
|
}, 1, "unhealthy: ask discriminator: ")
|
||||||
wantHealthCheck(t, map[string]string{
|
wantHealthCheck(t, map[string]string{
|
||||||
commandSetting: discriminatorCommand, listenAddr: "8080",
|
commandSetting: discriminatorCommand, listenAddr: "8080",
|
||||||
}, 1, "unhealthy: invalid setting: SWW_LISTEN_ADDR: ")
|
}, 1, "unhealthy: invalid setting: SWWAF_LISTEN_ADDR: ")
|
||||||
|
|
||||||
stop()
|
stop()
|
||||||
|
|
||||||
|
|||||||
+11
-20
@@ -29,7 +29,6 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/state"
|
"sneak.berlin/go/smallwebwaf/internal/state"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// shutdownTimeout is how long requests in progress may take to finish
|
// shutdownTimeout is how long requests in progress may take to finish
|
||||||
@@ -38,7 +37,7 @@ import (
|
|||||||
const shutdownTimeout = 5 * time.Second
|
const shutdownTimeout = 5 * time.Second
|
||||||
|
|
||||||
// remoteLogStopTimeout is how long, as discriminator stops, the log lines still
|
// remoteLogStopTimeout is how long, as discriminator stops, the log lines still
|
||||||
// waiting are sent to SWW_LOG_REMOTE_URL before they are given up. stdout has
|
// waiting are sent to SWWAF_LOG_REMOTE_URL before they are given up. stdout has
|
||||||
// carried them.
|
// carried them.
|
||||||
const remoteLogStopTimeout = 2 * time.Second
|
const remoteLogStopTimeout = 2 * time.Second
|
||||||
|
|
||||||
@@ -50,9 +49,9 @@ const usageStatus = 2
|
|||||||
const usage = `usage: sww <command>
|
const usage = `usage: sww <command>
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
rproxy the reverse proxy in front of one app
|
rproxy the reverse proxy in front of one app
|
||||||
discriminator decides for rproxy, and keeps bans, counters and history
|
discriminator decides for rproxy, and keeps bans, counters and history
|
||||||
healthcheck the container's health check
|
healthcheck the container's health check
|
||||||
`
|
`
|
||||||
|
|
||||||
// Params are what RunDiscriminator and RunRProxy need from the process.
|
// Params are what RunDiscriminator and RunRProxy need from the process.
|
||||||
@@ -105,7 +104,7 @@ func printUsage(w io.Writer) int {
|
|||||||
// done. It returns the process's exit status, 1 when discriminator cannot start.
|
// done. It returns the process's exit status, 1 when discriminator cannot start.
|
||||||
func RunDiscriminator(ctx context.Context, params Params) int {
|
func RunDiscriminator(ctx context.Context, params Params) int {
|
||||||
// Until the settings are read, the one message is an invalid setting's
|
// Until the settings are read, the one message is an invalid setting's
|
||||||
// error, which every SWW_LOG_LEVEL lets through.
|
// error, which every SWWAF_LOG_LEVEL lets through.
|
||||||
processLog := requestlog.NewProcessLogger(params.Stdout,
|
processLog := requestlog.NewProcessLogger(params.Stdout,
|
||||||
config.InstanceName(params.LookupEnv), slog.LevelError)
|
config.InstanceName(params.LookupEnv), slog.LevelError)
|
||||||
|
|
||||||
@@ -116,7 +115,7 @@ func RunDiscriminator(ctx context.Context, params Params) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// While SWW_LOG_REMOTE_URL is set, every line on stdout from here on
|
// While SWWAF_LOG_REMOTE_URL is set, every line on stdout from here on
|
||||||
// is sent there too.
|
// is sent there too.
|
||||||
stdout := params.Stdout
|
stdout := params.Stdout
|
||||||
|
|
||||||
@@ -151,14 +150,7 @@ func RunDiscriminator(ctx context.Context, params Params) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
crs, err := discriminator.NewCoreRuleSet(cfg)
|
server, err := newServer(cfg, stdout, processLog, now, ruleFiles, alertQueue)
|
||||||
if err != nil {
|
|
||||||
processLog.Error("cannot use the Core Rule Set", "error", err.Error())
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
server, err := newServer(cfg, stdout, processLog, now, ruleFiles, alertQueue, crs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
processLog.Error("cannot use the lookup database", "error", err.Error())
|
processLog.Error("cannot use the lookup database", "error", err.Error())
|
||||||
|
|
||||||
@@ -178,7 +170,8 @@ func RunDiscriminator(ctx context.Context, params Params) int {
|
|||||||
|
|
||||||
listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", cfg.ListenAddr)
|
listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", cfg.ListenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
processLog.Error("cannot listen on SWW_LISTEN_ADDR", "error", err.Error())
|
processLog.Error("cannot listen on SWWAF_LISTEN_ADDR",
|
||||||
|
"error", err.Error())
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -192,12 +185,11 @@ func RunDiscriminator(ctx context.Context, params Params) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newServer returns the server discriminator runs, with the metrics of the alerts,
|
// newServer returns the server discriminator runs, with the metrics of the alerts,
|
||||||
// after reading the lookup database while SWW_LOOKUP_SOURCE is file. A lookup
|
// after reading the lookup database while SWWAF_LOOKUP_SOURCE is file. A lookup
|
||||||
// database that cannot be read is an error.
|
// database that cannot be read is an error.
|
||||||
func newServer(
|
func newServer(
|
||||||
cfg *config.Config, stdout io.Writer, processLog *slog.Logger,
|
cfg *config.Config, stdout io.Writer, processLog *slog.Logger,
|
||||||
now func() time.Time, ruleFiles *rules.Files, alertQueue *alerts.Queue,
|
now func() time.Time, ruleFiles *rules.Files, alertQueue *alerts.Queue,
|
||||||
crs *waf.CoreRuleSet,
|
|
||||||
) (*discriminator.Server, error) {
|
) (*discriminator.Server, error) {
|
||||||
var lookupFile *lookup.File
|
var lookupFile *lookup.File
|
||||||
|
|
||||||
@@ -222,7 +214,6 @@ func newServer(
|
|||||||
GeoJSURL: lookup.URL,
|
GeoJSURL: lookup.URL,
|
||||||
AbuseIPDBURL: reputation.AbuseIPDBURL,
|
AbuseIPDBURL: reputation.AbuseIPDBURL,
|
||||||
LookupFile: lookupFile,
|
LookupFile: lookupFile,
|
||||||
CoreRuleSet: crs,
|
|
||||||
Now: now,
|
Now: now,
|
||||||
Rules: ruleFiles,
|
Rules: ruleFiles,
|
||||||
Alerts: alertQueue,
|
Alerts: alertQueue,
|
||||||
@@ -281,7 +272,7 @@ func newAlertQueue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newRemoteLogSender returns a sender of the log lines to
|
// newRemoteLogSender returns a sender of the log lines to
|
||||||
// SWW_LOG_REMOTE_URL, with the settings for it.
|
// SWWAF_LOG_REMOTE_URL, with the settings for it.
|
||||||
func newRemoteLogSender(cfg *config.Config) *remotelog.Sender {
|
func newRemoteLogSender(cfg *config.Config) *remotelog.Sender {
|
||||||
return remotelog.New(remotelog.Params{
|
return remotelog.New(remotelog.Params{
|
||||||
URL: cfg.LogRemoteURL,
|
URL: cfg.LogRemoteURL,
|
||||||
|
|||||||
+101
-152
@@ -19,7 +19,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/sww"
|
"sneak.berlin/go/smallwebwaf/internal/sww"
|
||||||
@@ -34,21 +33,21 @@ const (
|
|||||||
testVersion = "test"
|
testVersion = "test"
|
||||||
// localhost is where the tests listen.
|
// localhost is where the tests listen.
|
||||||
localhost = "127.0.0.1"
|
localhost = "127.0.0.1"
|
||||||
listenAddr = "SWW_LISTEN_ADDR"
|
listenAddr = "SWWAF_LISTEN_ADDR"
|
||||||
stateDir = "SWW_STATE_DIR"
|
stateDir = "SWWAF_STATE_DIR"
|
||||||
stateWriteDelay = "SWW_STATE_WRITE_DELAY"
|
stateWriteDelay = "SWWAF_STATE_WRITE_DELAY"
|
||||||
stateCounterInterval = "SWW_STATE_COUNTER_INTERVAL"
|
stateCounterInterval = "SWWAF_STATE_COUNTER_INTERVAL"
|
||||||
rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY"
|
rateLimitPerDay = "SWWAF_RATE_LIMIT_PER_DAY"
|
||||||
rateLimitExemptNets = "SWW_RATE_LIMIT_EXEMPT_NETS"
|
rateLimitExemptNets = "SWWAF_RATE_LIMIT_EXEMPT_NETS"
|
||||||
rulesDir = "SWW_RULES_DIR"
|
rulesDir = "SWWAF_RULES_DIR"
|
||||||
lookupSource = "SWW_LOOKUP_SOURCE"
|
lookupSource = "SWWAF_LOOKUP_SOURCE"
|
||||||
lookupDBPath = "SWW_LOOKUP_DB_PATH"
|
lookupDBPath = "SWWAF_LOOKUP_DB_PATH"
|
||||||
instanceName = "SWW_INSTANCE_NAME"
|
instanceName = "SWWAF_INSTANCE_NAME"
|
||||||
adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
adminToken = "SWWAF_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
||||||
metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name
|
metricsToken = "SWWAF_METRICS_TOKEN" //nolint:gosec // the setting's name
|
||||||
// adminSecret is the SWW_ADMIN_TOKEN the tests set.
|
// adminSecret is the SWWAF_ADMIN_TOKEN the tests set.
|
||||||
adminSecret = "fedcba9876543210fedcba9876543210"
|
adminSecret = "fedcba9876543210fedcba9876543210"
|
||||||
// instance is the SWW_INSTANCE_NAME the tests set where they look at
|
// instance is the SWWAF_INSTANCE_NAME the tests set where they look at
|
||||||
// it.
|
// it.
|
||||||
instance = "fsn1app1/blog"
|
instance = "fsn1app1/blog"
|
||||||
// greeting is what the tests' app answers.
|
// greeting is what the tests' app answers.
|
||||||
@@ -124,14 +123,14 @@ func (o *output) text() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// run runs `sww discriminator` with the settings in env until ctx is done,
|
// run runs `sww discriminator` with the settings in env until ctx is done,
|
||||||
// and returns its exit status. SWW_LOOKUP_SOURCE is off unless env sets it,
|
// and returns its exit status. SWWAF_LOOKUP_SOURCE is off unless env sets it,
|
||||||
// so that no test sends GeoJS its clients' addresses.
|
// so that no test sends GeoJS its clients' addresses.
|
||||||
func run(ctx context.Context, env map[string]string, out *output) int {
|
func run(ctx context.Context, env map[string]string, out *output) int {
|
||||||
return sww.RunDiscriminator(ctx, sww.Params{
|
return sww.RunDiscriminator(ctx, sww.Params{
|
||||||
Version: testVersion,
|
Version: testVersion,
|
||||||
LookupEnv: func(name string) (string, bool) {
|
LookupEnv: func(name string) (string, bool) {
|
||||||
value, ok := env[name]
|
value, ok := env[name]
|
||||||
if !ok && name == "SWW_LOOKUP_SOURCE" {
|
if !ok && name == "SWWAF_LOOKUP_SOURCE" {
|
||||||
return "off", true
|
return "off", true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,8 +146,8 @@ func TestInvalidSettingStopsTheStart(t *testing.T) {
|
|||||||
out := &output{}
|
out := &output{}
|
||||||
|
|
||||||
status := run(t.Context(), map[string]string{
|
status := run(t.Context(), map[string]string{
|
||||||
"SWW_BYTES_LIMIT_PER_MINUTE": "lots",
|
"SWWAF_BYTES_LIMIT_PER_MINUTE": "lots",
|
||||||
instanceName: instance,
|
instanceName: instance,
|
||||||
}, out)
|
}, out)
|
||||||
if status != 1 {
|
if status != 1 {
|
||||||
t.Errorf("exit status %d, want 1", status)
|
t.Errorf("exit status %d, want 1", status)
|
||||||
@@ -159,7 +158,7 @@ func TestInvalidSettingStopsTheStart(t *testing.T) {
|
|||||||
|
|
||||||
if line["type"] != "process" || line["instance"] != instance ||
|
if line["type"] != "process" || line["instance"] != instance ||
|
||||||
line["level"] != "ERROR" ||
|
line["level"] != "ERROR" ||
|
||||||
!strings.HasPrefix(message, "SWW_BYTES_LIMIT_PER_MINUTE: ") {
|
!strings.HasPrefix(message, "SWWAF_BYTES_LIMIT_PER_MINUTE: ") {
|
||||||
t.Errorf("start refused with %v", line)
|
t.Errorf("start refused with %v", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,7 +214,7 @@ func TestAddressInUseStopsTheStart(t *testing.T) {
|
|||||||
t.Errorf("exit status %d, want 1", status)
|
t.Errorf("exit status %d, want 1", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
out.line(t, "msg", "cannot listen on SWW_LISTEN_ADDR")
|
out.line(t, "msg", "cannot listen on SWWAF_LISTEN_ADDR")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServesUntilToldToStop(t *testing.T) {
|
func TestServesUntilToldToStop(t *testing.T) {
|
||||||
@@ -278,11 +277,11 @@ func TestLogLevelHoldsBackTheLessSevereProcessLines(t *testing.T) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
exited <- run(ctx, map[string]string{
|
exited <- run(ctx, map[string]string{
|
||||||
listenAddr: localhost + ":0",
|
listenAddr: localhost + ":0",
|
||||||
stateDir: t.TempDir(),
|
stateDir: t.TempDir(),
|
||||||
rulesDir: t.TempDir(),
|
rulesDir: t.TempDir(),
|
||||||
"SWW_BLOCKLIST_URLS": lists.URL + "/tor.txt",
|
"SWWAF_BLOCKLIST_URLS": lists.URL + "/tor.txt",
|
||||||
"SWW_LOG_LEVEL": "warn",
|
"SWWAF_LOG_LEVEL": "warn",
|
||||||
}, out)
|
}, out)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -385,7 +384,7 @@ func TestStateKeptAcrossRestarts(t *testing.T) {
|
|||||||
func TestBanRefusesItsNetblockAfterARestartWithAnotherScope(t *testing.T) {
|
func TestBanRefusesItsNetblockAfterARestartWithAnotherScope(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const scope = "SWW_BAN_SCOPE_V4_PREFIX"
|
const scope = "SWWAF_BAN_SCOPE_V4_PREFIX"
|
||||||
|
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
listenAddr: localhost + ":0",
|
listenAddr: localhost + ":0",
|
||||||
@@ -505,7 +504,7 @@ func TestRuleFileAddedWhileRunningTakesEffect(t *testing.T) {
|
|||||||
rulesDir: dir,
|
rulesDir: dir,
|
||||||
// The requests sent until the rule takes effect must not break a
|
// The requests sent until the rule takes effect must not break a
|
||||||
// rate limit, whose ban would refuse them too.
|
// rate limit, whose ban would refuse them too.
|
||||||
"SWW_RATE_LIMIT_PER_MINUTE": "off",
|
"SWWAF_RATE_LIMIT_PER_MINUTE": "off",
|
||||||
}
|
}
|
||||||
|
|
||||||
out := runUntilStopped(t, env, func(url string) {
|
out := runUntilStopped(t, env, func(url string) {
|
||||||
@@ -548,60 +547,10 @@ func TestRulesDirThatDoesNotExistStopsTheStart(t *testing.T) {
|
|||||||
|
|
||||||
dir := filepath.Join(t.TempDir(), "rules.d")
|
dir := filepath.Join(t.TempDir(), "rules.d")
|
||||||
|
|
||||||
wantRulesRefused(t, dir, "SWW_RULES_DIR cannot be read: open "+dir+
|
wantRulesRefused(t, dir, "SWWAF_RULES_DIR cannot be read: open "+dir+
|
||||||
": no such file or directory")
|
": no such file or directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCoreRuleSetExclusionsFileThatCannotBeReadStopsTheStart(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "crs-exclusions.conf")
|
|
||||||
out := &output{}
|
|
||||||
|
|
||||||
status := run(t.Context(), map[string]string{
|
|
||||||
listenAddr: localhost + ":0", stateDir: t.TempDir(), rulesDir: t.TempDir(),
|
|
||||||
"SWW_CORE_RULE_SET_EXCLUSIONS_FILE": path,
|
|
||||||
}, out)
|
|
||||||
if status != 1 {
|
|
||||||
t.Fatalf("exit status %d, want 1", status)
|
|
||||||
}
|
|
||||||
|
|
||||||
line := out.line(t, "msg", "cannot use the Core Rule Set")
|
|
||||||
want := "SWW_CORE_RULE_SET_EXCLUSIONS_FILE: read the exclusions file: open " +
|
|
||||||
path + ": no such file or directory"
|
|
||||||
|
|
||||||
if line["error"] != want {
|
|
||||||
t.Errorf("start refused with %v, want the error %q", line, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCoreRuleSetExclusionsFileThatCannotBeUsedStopsTheStartWhileTheCoreRuleSetIsOff(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
broken := filepath.Join(t.TempDir(), "broken.conf")
|
|
||||||
|
|
||||||
err := os.WriteFile(broken, []byte("SecNoSuchDirective On\n"), 0o600)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range []string{filepath.Join(t.TempDir(), "missing.conf"), broken} {
|
|
||||||
out := &output{}
|
|
||||||
|
|
||||||
status := run(t.Context(), map[string]string{
|
|
||||||
listenAddr: localhost + ":0", stateDir: t.TempDir(), rulesDir: t.TempDir(),
|
|
||||||
"SWW_WAF_MODE": config.WAFModeOff, "SWW_CORE_RULE_SET_EXCLUSIONS_FILE": path,
|
|
||||||
}, out)
|
|
||||||
if status != 1 {
|
|
||||||
t.Fatalf("%s: exit status %d, want 1", path, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
out.line(t, "msg", "cannot use the Core Rule Set")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLookupDatabaseReplacedWhileRunningTakesEffect(t *testing.T) {
|
func TestLookupDatabaseReplacedWhileRunningTakesEffect(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -611,14 +560,14 @@ func TestLookupDatabaseReplacedWhileRunningTakesEffect(t *testing.T) {
|
|||||||
writeLookupDatabase(t, path, "DE")
|
writeLookupDatabase(t, path, "DE")
|
||||||
|
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
listenAddr: localhost + ":0",
|
listenAddr: localhost + ":0",
|
||||||
stateDir: t.TempDir(),
|
stateDir: t.TempDir(),
|
||||||
rulesDir: t.TempDir(),
|
rulesDir: t.TempDir(),
|
||||||
metricsToken: token,
|
metricsToken: token,
|
||||||
instanceName: instance,
|
instanceName: instance,
|
||||||
lookupSource: "file",
|
lookupSource: "file",
|
||||||
lookupDBPath: path,
|
lookupDBPath: path,
|
||||||
"SWW_DENIED_COUNTRIES": "kp",
|
"SWWAF_DENIED_COUNTRIES": "kp",
|
||||||
// The requests sent until a replacement takes effect, and those
|
// The requests sent until a replacement takes effect, and those
|
||||||
// for the metrics, must not break a rate limit, whose ban would
|
// for the metrics, must not break a rate limit, whose ban would
|
||||||
// refuse them too.
|
// refuse them too.
|
||||||
@@ -708,12 +657,12 @@ func TestBlocklistTriesAndCopiesKeptInReputationJSONAcrossRestarts(t *testing.T)
|
|||||||
torURL, dropURL := server.URL+torPath, server.URL+dropPath
|
torURL, dropURL := server.URL+torPath, server.URL+dropPath
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
listenAddr: localhost + ":0",
|
listenAddr: localhost + ":0",
|
||||||
stateDir: dir,
|
stateDir: dir,
|
||||||
rulesDir: t.TempDir(),
|
rulesDir: t.TempDir(),
|
||||||
metricsToken: token,
|
metricsToken: token,
|
||||||
instanceName: instance,
|
instanceName: instance,
|
||||||
"SWW_BLOCKLIST_URLS": torURL,
|
"SWWAF_BLOCKLIST_URLS": torURL,
|
||||||
// The requests sent until the list takes effect, and those for the
|
// The requests sent until the list takes effect, and those for the
|
||||||
// metrics, must not break a rate limit, whose ban would refuse them
|
// metrics, must not break a rate limit, whose ban would refuse them
|
||||||
// too.
|
// too.
|
||||||
@@ -729,12 +678,12 @@ func TestBlocklistTriesAndCopiesKeptInReputationJSONAcrossRestarts(t *testing.T)
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Restarted with drop.txt named after it and the server answering, tor.txt
|
// Restarted with drop.txt named after it and the server answering, tor.txt
|
||||||
// waits SWW_BLOCKLIST_REFRESH after its failed try, kept in reputation.json,
|
// waits SWWAF_BLOCKLIST_REFRESH after its failed try, kept in reputation.json,
|
||||||
// while drop.txt, never tried, is fetched at once. Lists are fetched in the
|
// while drop.txt, never tried, is fetched at once. Lists are fetched in the
|
||||||
// order named, so once drop.txt refuses the client, tor.txt has had its turn.
|
// order named, so once drop.txt refuses the client, tor.txt has had its turn.
|
||||||
failing.Store(false)
|
failing.Store(false)
|
||||||
|
|
||||||
env["SWW_BLOCKLIST_URLS"] = torURL + "," + dropURL
|
env["SWWAF_BLOCKLIST_URLS"] = torURL + "," + dropURL
|
||||||
out := runUntilStopped(t, env, func(url string) {
|
out := runUntilStopped(t, env, func(url string) {
|
||||||
for statusFrom(t, url, placed) != http.StatusForbidden {
|
for statusFrom(t, url, placed) != http.StatusForbidden {
|
||||||
time.Sleep(pollInterval)
|
time.Sleep(pollInterval)
|
||||||
@@ -790,11 +739,11 @@ func TestCrowdSecDecisionListKeptInReputationJSONAcrossARestart(t *testing.T) {
|
|||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
listenAddr: localhost + ":0",
|
listenAddr: localhost + ":0",
|
||||||
stateDir: dir,
|
stateDir: dir,
|
||||||
rulesDir: t.TempDir(),
|
rulesDir: t.TempDir(),
|
||||||
"SWW_CROWDSEC_LAPI_URL": engine.URL,
|
"SWWAF_CROWDSEC_LAPI_URL": engine.URL,
|
||||||
"SWW_CROWDSEC_LAPI_KEY": bouncerKey,
|
"SWWAF_CROWDSEC_LAPI_KEY": bouncerKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once the list is fetched, the client's request bans it.
|
// Once the list is fetched, the client's request bans it.
|
||||||
@@ -867,7 +816,7 @@ func TestLookupDatabaseThatCannotBeReadStopsTheStart(t *testing.T) {
|
|||||||
t.Fatalf("exit status %d, want 1", status)
|
t.Fatalf("exit status %d, want 1", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
want := "SWW_LOOKUP_DB_PATH cannot be read: open " + path +
|
want := "SWWAF_LOOKUP_DB_PATH cannot be read: open " + path +
|
||||||
": no such file or directory"
|
": no such file or directory"
|
||||||
|
|
||||||
line := out.line(t, "msg", "cannot use the lookup database")
|
line := out.line(t, "msg", "cannot use the lookup database")
|
||||||
@@ -889,10 +838,10 @@ func TestEveryLineIsAlsoSentToTheRemoteLogEndpoint(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
listenAddr: localhost + ":0",
|
listenAddr: localhost + ":0",
|
||||||
stateDir: t.TempDir(),
|
stateDir: t.TempDir(),
|
||||||
rulesDir: t.TempDir(),
|
rulesDir: t.TempDir(),
|
||||||
"SWW_LOG_REMOTE_URL": "syslog+tcp://" + endpoint.Addr().String(),
|
"SWWAF_LOG_REMOTE_URL": "syslog+tcp://" + endpoint.Addr().String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
out := runUntilStopped(t, env, func(url string) {
|
out := runUntilStopped(t, env, func(url string) {
|
||||||
@@ -944,13 +893,13 @@ func TestStalledRemoteLogEndpointHoldsUpNoRequest(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
listenAddr: localhost + ":0",
|
listenAddr: localhost + ":0",
|
||||||
stateDir: t.TempDir(),
|
stateDir: t.TempDir(),
|
||||||
rulesDir: t.TempDir(),
|
rulesDir: t.TempDir(),
|
||||||
"SWW_LOG_REMOTE_URL": "syslog+tls://" + endpoint.Addr().String(),
|
"SWWAF_LOG_REMOTE_URL": "syslog+tls://" + endpoint.Addr().String(),
|
||||||
"SWW_LOG_REMOTE_BUFFER": "1",
|
"SWWAF_LOG_REMOTE_BUFFER": "1",
|
||||||
metricsToken: token,
|
metricsToken: token,
|
||||||
instanceName: instance,
|
instanceName: instance,
|
||||||
}
|
}
|
||||||
|
|
||||||
out := runUntilStopped(t, env, func(url string) {
|
out := runUntilStopped(t, env, func(url string) {
|
||||||
@@ -1003,12 +952,12 @@ func TestBanIsAlertedAndAnAlertNotSentIsKeptAcrossARestart(t *testing.T) {
|
|||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
listenAddr: localhost + ":0",
|
listenAddr: localhost + ":0",
|
||||||
stateDir: dir,
|
stateDir: dir,
|
||||||
rulesDir: rules,
|
rulesDir: rules,
|
||||||
"SWW_ALERT_WEBHOOK_URL": webhook.url,
|
"SWWAF_ALERT_WEBHOOK_URL": webhook.url,
|
||||||
"SWW_ALERT_WEBHOOK_HEADERS": "Authorization:Bearer " + adminSecret,
|
"SWWAF_ALERT_WEBHOOK_HEADERS": "Authorization:Bearer " + adminSecret,
|
||||||
instanceName: instance,
|
instanceName: instance,
|
||||||
}
|
}
|
||||||
|
|
||||||
runUntilStopped(t, env, func(url string) {
|
runUntilStopped(t, env, func(url string) {
|
||||||
@@ -1056,7 +1005,7 @@ func TestBanIsAlertedAndAnAlertNotSentIsKeptAcrossARestart(t *testing.T) {
|
|||||||
|
|
||||||
webhook.failing.Store(false)
|
webhook.failing.Store(false)
|
||||||
|
|
||||||
env["SWW_ALLOW_NETS"] = localhost
|
env["SWWAF_ALLOW_NETS"] = localhost
|
||||||
env[metricsToken] = token
|
env[metricsToken] = token
|
||||||
|
|
||||||
runUntilStopped(t, env, func(url string) {
|
runUntilStopped(t, env, func(url string) {
|
||||||
@@ -1092,12 +1041,12 @@ func TestBanIsAlertedToSlackAndNtfy(t *testing.T) {
|
|||||||
rulesDir: t.TempDir(),
|
rulesDir: t.TempDir(),
|
||||||
rateLimitPerDay: "1",
|
rateLimitPerDay: "1",
|
||||||
// The metrics are read from 127.0.0.1, which no limit counts.
|
// The metrics are read from 127.0.0.1, which no limit counts.
|
||||||
"SWW_ALLOW_NETS": localhost + "/32",
|
"SWWAF_ALLOW_NETS": localhost + "/32",
|
||||||
metricsToken: token,
|
metricsToken: token,
|
||||||
instanceName: instance,
|
instanceName: instance,
|
||||||
"SWW_ALERT_SLACK_WEBHOOK_URL": slack.url,
|
"SWWAF_ALERT_SLACK_WEBHOOK_URL": slack.url,
|
||||||
"SWW_ALERT_NTFY_URL": ntfy.url,
|
"SWWAF_ALERT_NTFY_URL": ntfy.url,
|
||||||
"SWW_ALERT_NTFY_TOKEN": ntfyToken,
|
"SWWAF_ALERT_NTFY_TOKEN": ntfyToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
runUntilStopped(t, env, func(url string) {
|
runUntilStopped(t, env, func(url string) {
|
||||||
@@ -1162,7 +1111,7 @@ func TestUnwritableStateDirStopsTheStart(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
wantStartRefused(t, filepath.Join(t.TempDir(), "missing"),
|
wantStartRefused(t, filepath.Join(t.TempDir(), "missing"),
|
||||||
"SWW_STATE_DIR cannot be written: ")
|
"SWWAF_STATE_DIR cannot be written: ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// wantStartRefused runs sww with its state files in dir, and
|
// wantStartRefused runs sww with its state files in dir, and
|
||||||
@@ -1268,28 +1217,28 @@ func wantStartingLine(t *testing.T, line map[string]any, dir string) {
|
|||||||
|
|
||||||
settings, _ := line["settings"].(map[string]any)
|
settings, _ := line["settings"].(map[string]any)
|
||||||
want := map[string]any{
|
want := map[string]any{
|
||||||
listenAddr: localhost + ":0",
|
listenAddr: localhost + ":0",
|
||||||
stateDir: dir,
|
stateDir: dir,
|
||||||
"SWW_MODE": "enforce",
|
"SWWAF_MODE": "enforce",
|
||||||
stateWriteDelay: "10s",
|
stateWriteDelay: "10s",
|
||||||
stateCounterInterval: "15m",
|
stateCounterInterval: "15m",
|
||||||
"SWW_ALLOW_NETS": "",
|
"SWWAF_ALLOW_NETS": "",
|
||||||
rateLimitExemptNets: "",
|
rateLimitExemptNets: "",
|
||||||
"SWW_DENY_NETS": "",
|
"SWWAF_DENY_NETS": "",
|
||||||
"SWW_RATE_LIMIT_PER_MINUTE": "120",
|
"SWWAF_RATE_LIMIT_PER_MINUTE": "120",
|
||||||
"SWW_RATE_LIMIT_PER_HOUR": "1500",
|
"SWWAF_RATE_LIMIT_PER_HOUR": "1500",
|
||||||
rateLimitPerDay: "6000",
|
rateLimitPerDay: "6000",
|
||||||
"SWW_RATE_LIMIT_EXEMPT_PATHS": "",
|
"SWWAF_RATE_LIMIT_EXEMPT_PATHS": "",
|
||||||
"SWW_DENIED_COUNTRIES": "",
|
"SWWAF_DENIED_COUNTRIES": "",
|
||||||
"SWW_EXCLUSIVELY_ALLOWED_COUNTRIES": "",
|
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES": "",
|
||||||
"SWW_BAN_RESPONSE": "403",
|
"SWWAF_BAN_RESPONSE": "403",
|
||||||
"SWW_LIMIT_BAN_DURATION": "1h",
|
"SWWAF_LIMIT_BAN_DURATION": "1h",
|
||||||
"SWW_LIMIT_BAN_REPEAT_WINDOW": "24h",
|
"SWWAF_LIMIT_BAN_REPEAT_WINDOW": "24h",
|
||||||
"SWW_MAX_BAN_DURATION": "7d",
|
"SWWAF_MAX_BAN_DURATION": "7d",
|
||||||
"SWW_ATTACK_BAN_DURATION": "7d",
|
"SWWAF_ATTACK_BAN_DURATION": "7d",
|
||||||
"SWW_MAX_BANS": "5000",
|
"SWWAF_MAX_BANS": "5000",
|
||||||
"SWW_BAN_SCOPE_V4_PREFIX": "32",
|
"SWWAF_BAN_SCOPE_V4_PREFIX": "32",
|
||||||
"SWW_RULES_ENABLED": "true",
|
"SWWAF_RULES_ENABLED": "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, value := range want {
|
for name, value := range want {
|
||||||
@@ -1489,7 +1438,7 @@ func onlyBan(t *testing.T, dir string) map[string]any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wantRefused checks that discriminator at url refuses a request from localhost with
|
// wantRefused checks that discriminator at url refuses a request from localhost with
|
||||||
// 403, the default SWW_BAN_RESPONSE.
|
// 403, the default SWWAF_BAN_RESPONSE.
|
||||||
func wantRefused(t *testing.T, url string) {
|
func wantRefused(t *testing.T, url string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -1530,8 +1479,8 @@ func saveUntilAnswered(t *testing.T, path, content, url, from string, status int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// destination is a stand-in for SWW_ALERT_SLACK_WEBHOOK_URL or
|
// destination is a stand-in for SWWAF_ALERT_SLACK_WEBHOOK_URL or
|
||||||
// SWW_ALERT_NTFY_URL. It notes each request it is sent, and answers
|
// SWWAF_ALERT_NTFY_URL. It notes each request it is sent, and answers
|
||||||
// 200.
|
// 200.
|
||||||
type destination struct {
|
type destination struct {
|
||||||
url string
|
url string
|
||||||
@@ -1590,7 +1539,7 @@ func (d *destination) firstPost(t *testing.T) destinationPost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// webhook is a stand-in for SWW_ALERT_WEBHOOK_URL. It notes each alert
|
// webhook is a stand-in for SWWAF_ALERT_WEBHOOK_URL. It notes each alert
|
||||||
// it is sent, and answers 204, or 503 while failing.
|
// it is sent, and answers 204, or 503 while failing.
|
||||||
type webhook struct {
|
type webhook struct {
|
||||||
url string
|
url string
|
||||||
|
|||||||
@@ -1,283 +0,0 @@
|
|||||||
# The eighth change: on gitea's paths, some fields skip the rules that refused
|
|
||||||
# ordinary gitea requests there. They are the fields where gitea sends a search,
|
|
||||||
# a file name or a branch name. Each exclusion runs with the request, before the
|
|
||||||
# rules it switches off, those of phase 1 included, so this file is loaded before
|
|
||||||
# the Core Rule Set's rules. Each comment names a request the rule refused.
|
|
||||||
|
|
||||||
# gitea's searches of code, of issues and pull requests, and of commits send the
|
|
||||||
# text searched for in q, which skips these rules there:
|
|
||||||
# 930110: ../
|
|
||||||
# 930120: .gitignore
|
|
||||||
# 931100: http://127.0.0.1:8080
|
|
||||||
# 932120: Invoke-WebRequest
|
|
||||||
# 932130: $(pwd)
|
|
||||||
# 932140: if (a == b) {
|
|
||||||
# 932160: /bin/sh
|
|
||||||
# 932235: `make check`
|
|
||||||
# 932250: ls -la
|
|
||||||
# 932260: python3
|
|
||||||
# 932380: `whoami`
|
|
||||||
# 933100: <?php echo
|
|
||||||
# 933130: $_GET[
|
|
||||||
# 933135: ${VAR}
|
|
||||||
# 934100: process.env
|
|
||||||
# 934110: http://127.0.0.1:8080
|
|
||||||
# 934130: __proto__
|
|
||||||
# 941100: <script>
|
|
||||||
# 941110: <script>
|
|
||||||
# 941160: <img src=
|
|
||||||
# 941180: document.cookie
|
|
||||||
# 941390: fetch(
|
|
||||||
# 942100: SELECT * FROM users WHERE
|
|
||||||
# 942140: information_schema
|
|
||||||
# 942190: union select
|
|
||||||
# 942230: if (a == b) {
|
|
||||||
# 944100: java.lang.Runtime
|
|
||||||
# 944110: java.lang.Runtime
|
|
||||||
# 944130: java.lang.Runtime
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:explore/code|issues(?:/search)?|pulls|\
|
|
||||||
[^/]+/-/code|\
|
|
||||||
[^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|\
|
|
||||||
commits/(?:branch|tag|commit)/.+/search)|\
|
|
||||||
api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))$" \
|
|
||||||
"id:900600,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930110;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=931100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932130;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932140;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932235;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932250;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932380;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=933100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=933130;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=933135;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=934100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=934110;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=934130;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=941100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=941110;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=941160;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=941180;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=941390;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=942100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=942140;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=942190;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=942230;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=944100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=944110;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:q"
|
|
||||||
|
|
||||||
# A page opened from the results of one of those searches has the search's
|
|
||||||
# address in Referer, which skips these rules then:
|
|
||||||
# 941110: <script>
|
|
||||||
# 941120: onClick={
|
|
||||||
# 941160: <img src=
|
|
||||||
# 942100: LIKE '%foo%'
|
|
||||||
# 944100: java.lang.Runtime
|
|
||||||
SecRule REQUEST_HEADERS:Referer "@rx ^https?://[^/]+/(?:explore/code|\
|
|
||||||
issues(?:/search)?|pulls|[^/]+/-/code|\
|
|
||||||
[^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|\
|
|
||||||
commits/(?:branch|tag|commit)/.+/search)|\
|
|
||||||
api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))\?" \
|
|
||||||
"id:900601,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=941110;REQUEST_HEADERS:Referer,\
|
|
||||||
ctl:ruleRemoveTargetById=941120;REQUEST_HEADERS:Referer,\
|
|
||||||
ctl:ruleRemoveTargetById=941160;REQUEST_HEADERS:Referer,\
|
|
||||||
ctl:ruleRemoveTargetById=942100;REQUEST_HEADERS:Referer,\
|
|
||||||
ctl:ruleRemoveTargetById=944100;REQUEST_HEADERS:Referer"
|
|
||||||
|
|
||||||
# gitea's searches of repositories, users, branches, tags and packages send a
|
|
||||||
# name in q, which skips these rules there:
|
|
||||||
# 930120: .profile
|
|
||||||
# 932120: powershell-scripts
|
|
||||||
# 932260: python3-app
|
|
||||||
# 944130: org.apache.commons.logging
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:explore/repos|user/search_candidates|\
|
|
||||||
[^/]+/-/packages|[^/]+/[^/]+/(?:branches|tags)|api/v1/repos/search)$" \
|
|
||||||
"id:900602,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:q"
|
|
||||||
|
|
||||||
# gitea's other searches for a name send it in q too, which skips only these
|
|
||||||
# rules there:
|
|
||||||
# 930120: .profile
|
|
||||||
# 932120: powershell-scripts
|
|
||||||
# 932260: python3-app
|
|
||||||
# The searches:
|
|
||||||
# - the dashboard's filter of repositories;
|
|
||||||
# - the repositories on a user's or an organisation's page, whose path has
|
|
||||||
# one segment;
|
|
||||||
# - the author filter of the issue and pull request lists;
|
|
||||||
# - the lists of users and organisations, and the API's search for users.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:[^/]+|repo/search|\
|
|
||||||
explore/(?:users|organizations)|api/v1/users/search|\
|
|
||||||
[^/]+/[^/]+/(?:issues|pulls)/posters)$" \
|
|
||||||
"id:900603,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:q"
|
|
||||||
|
|
||||||
# These requests name a file in the path, which skips these rules there:
|
|
||||||
# 920500: notes.txt~
|
|
||||||
# 934100: process.env.d.ts
|
|
||||||
# 941130: docs/a b.xhtml
|
|
||||||
# 944130: java.lang.String.md
|
|
||||||
# The requests:
|
|
||||||
# - a file's page, raw file and download;
|
|
||||||
# - its blame, history, edit, preview and delete pages;
|
|
||||||
# - the API's call for its contents;
|
|
||||||
# - a release attachment's download.
|
|
||||||
SecRule REQUEST_FILENAME \
|
|
||||||
"@rx ^/(?:api/v1/repos/)?[^/]+/[^/]+/(?:src|raw|media|blame|commits|_edit|_preview|_delete|contents|releases/download)/" \
|
|
||||||
"id:900610,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=920500;REQUEST_FILENAME,\
|
|
||||||
ctl:ruleRemoveTargetById=934100;REQUEST_FILENAME,\
|
|
||||||
ctl:ruleRemoveTargetById=941130;REQUEST_FILENAME,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;REQUEST_FILENAME"
|
|
||||||
|
|
||||||
# The requests below name a file in a query parameter. These rules refused
|
|
||||||
# them for the file named:
|
|
||||||
# 930120: .gitignore
|
|
||||||
# 932120: install-script.sh
|
|
||||||
# 932130: [draft].md
|
|
||||||
# 932160: bin/docker-entrypoint
|
|
||||||
# 932260: docker-compose.yml
|
|
||||||
# 934100: process.env.d.ts
|
|
||||||
# 934130: __proto__.json
|
|
||||||
# 941130: index.xhtml
|
|
||||||
# 941180: document.write.js
|
|
||||||
# 942140: information_schema.sql
|
|
||||||
# 944110: java/lang/Runtime.java
|
|
||||||
# 944130: java.lang.String.md
|
|
||||||
# Each request skips the rules that refused it. The fourth change already has
|
|
||||||
# path and redirect_to skip 930120, 932160 and 932260 on every path.
|
|
||||||
|
|
||||||
# A file's commit graph, in file.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/graph$" \
|
|
||||||
"id:900620,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:file,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:file,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:file,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:file,\
|
|
||||||
ctl:ruleRemoveTargetById=934100;ARGS:file,\
|
|
||||||
ctl:ruleRemoveTargetById=934130;ARGS:file,\
|
|
||||||
ctl:ruleRemoveTargetById=941130;ARGS:file,\
|
|
||||||
ctl:ruleRemoveTargetById=941180;ARGS:file,\
|
|
||||||
ctl:ruleRemoveTargetById=942140;ARGS:file,\
|
|
||||||
ctl:ruleRemoveTargetById=944110;ARGS:file,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:file"
|
|
||||||
|
|
||||||
# The API's list of a file's commits, in path.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/api/v1/repos/[^/]+/[^/]+/commits$" \
|
|
||||||
"id:900621,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=934100;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=934130;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=941130;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=941180;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=942140;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=944110;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:path"
|
|
||||||
|
|
||||||
# The API's call for the contents of several files, in body, as JSON.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/api/v1/repos/[^/]+/[^/]+/file-contents$" \
|
|
||||||
"id:900622,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:body,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:body,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:body,\
|
|
||||||
ctl:ruleRemoveTargetById=934100;ARGS:body,\
|
|
||||||
ctl:ruleRemoveTargetById=934130;ARGS:body,\
|
|
||||||
ctl:ruleRemoveTargetById=941130;ARGS:body,\
|
|
||||||
ctl:ruleRemoveTargetById=941180;ARGS:body,\
|
|
||||||
ctl:ruleRemoveTargetById=942140;ARGS:body,\
|
|
||||||
ctl:ruleRemoveTargetById=944110;ARGS:body,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:body"
|
|
||||||
|
|
||||||
# An issue opened from a template, in template.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/issues/new$" \
|
|
||||||
"id:900623,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=932130;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=934100;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=934130;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=941130;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=941180;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=942140;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=944110;ARGS:template,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:template"
|
|
||||||
|
|
||||||
# The sign-in page on the way to a file's edit page, in redirect_to.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/user/login$" \
|
|
||||||
"id:900624,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:redirect_to,\
|
|
||||||
ctl:ruleRemoveTargetById=932130;ARGS:redirect_to,\
|
|
||||||
ctl:ruleRemoveTargetById=934100;ARGS:redirect_to,\
|
|
||||||
ctl:ruleRemoveTargetById=934130;ARGS:redirect_to,\
|
|
||||||
ctl:ruleRemoveTargetById=941130;ARGS:redirect_to,\
|
|
||||||
ctl:ruleRemoveTargetById=941180;ARGS:redirect_to,\
|
|
||||||
ctl:ruleRemoveTargetById=942140;ARGS:redirect_to,\
|
|
||||||
ctl:ruleRemoveTargetById=944110;ARGS:redirect_to,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:redirect_to"
|
|
||||||
|
|
||||||
# An attachment uploaded through the API to a release, an issue or a comment,
|
|
||||||
# in name.
|
|
||||||
SecRule REQUEST_FILENAME \
|
|
||||||
"@rx ^/api/v1/repos/[^/]+/[^/]+/(?:releases|issues|issues/comments)/[0-9]+/assets$" \
|
|
||||||
"id:900625,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:name,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:name,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:name,\
|
|
||||||
ctl:ruleRemoveTargetById=934100;ARGS:name,\
|
|
||||||
ctl:ruleRemoveTargetById=934130;ARGS:name,\
|
|
||||||
ctl:ruleRemoveTargetById=941130;ARGS:name,\
|
|
||||||
ctl:ruleRemoveTargetById=941180;ARGS:name,\
|
|
||||||
ctl:ruleRemoveTargetById=942140;ARGS:name,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:name"
|
|
||||||
|
|
||||||
# A diff's hidden lines, fetched when shown, in path.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/blob_excerpt/[0-9a-f]+$" \
|
|
||||||
"id:900626,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=934100;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=934130;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=941130;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=941180;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=942140;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=944110;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:path"
|
|
||||||
|
|
||||||
# The requests below name a branch or tag in a query parameter, which skips
|
|
||||||
# the list of command names (932260) there. It refused names such as
|
|
||||||
# docker-build, python3-migration and feat/docker-support.
|
|
||||||
|
|
||||||
# Deleting or restoring a branch, in name.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/branches/(?:delete|restore)$" \
|
|
||||||
"id:900630,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:name"
|
|
||||||
|
|
||||||
# A release started from a tag, in tag.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/releases/new$" \
|
|
||||||
"id:900631,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:tag"
|
|
||||||
|
|
||||||
# A branch protection rule opened for editing, in rule_name.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/settings/branches/edit$" \
|
|
||||||
"id:900632,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:rule_name"
|
|
||||||
|
|
||||||
# A directory listing's last commits, in refSubUrl.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/lastcommit/" \
|
|
||||||
"id:900633,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:refSubUrl"
|
|
||||||
|
|
||||||
# An artifact upload from actions/upload-artifact@v3 sends the header
|
|
||||||
# Content-Range, which the Core Rule Set refused (920450).
|
|
||||||
SecRule REQUEST_FILENAME \
|
|
||||||
"@rx ^/api/actions_pipeline/_apis/pipelines/workflows/[0-9]+/artifacts/[^/]+/upload$" \
|
|
||||||
"id:900640,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=920450;REQUEST_HEADERS_NAMES:Content-Range"
|
|
||||||
-393
@@ -1,393 +0,0 @@
|
|||||||
# Ordinary gitea 1.27 requests that the Core Rule Set must let through at the
|
|
||||||
# defaults. Each line is a path and query, and may end with one header after a
|
|
||||||
# space. The tests of internal/waf inspect each as a GET. The Core Rule Set reads
|
|
||||||
# the query and the headers the same way for any method.
|
|
||||||
|
|
||||||
# Searches with ordinary words and symbols.
|
|
||||||
/owner/repo/search?q=package.json
|
|
||||||
/owner/repo/search?q=.gitignore
|
|
||||||
/owner/repo/search?q=docker-compose.yml
|
|
||||||
/owner/repo/search?q=python3
|
|
||||||
/owner/repo/search?q=ssh%20key
|
|
||||||
/owner/repo/search?q=feat%2Fdocker-support
|
|
||||||
/owner/repo/search?q=ls%20-la
|
|
||||||
/owner/repo/search?q=sed%20-i
|
|
||||||
/owner/repo/search?q=%2Fbin%2Fsh
|
|
||||||
/owner/repo/search?q=%60make%20check%60
|
|
||||||
/owner/repo/search?q=%60whoami%60
|
|
||||||
/owner/repo/search?q=fetch%28
|
|
||||||
/owner/repo/search?q=%24%7BVAR%7D
|
|
||||||
/owner/repo/search?q=%24%28pwd%29
|
|
||||||
/owner/repo/search?q=process.env
|
|
||||||
/owner/repo/search?q=%3Cimg%20src%3D
|
|
||||||
/owner/repo/search?q=%3Cscript%3E
|
|
||||||
/owner/repo/search?q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/owner/repo/search?q=union%20select
|
|
||||||
/owner/repo/search?q=http%3A%2F%2F127.0.0.1%3A8080
|
|
||||||
/owner/repo/search?q=..%2F
|
|
||||||
/owner/repo/search?q=%2Fetc%2Fpasswd
|
|
||||||
/owner/repo/search?q=eval%28
|
|
||||||
/owner/repo/search?q=chmod%20%2Bx
|
|
||||||
/owner/repo/search?q=echo%20%24PATH
|
|
||||||
/owner/repo/search?q=java.lang.Runtime
|
|
||||||
/owner/repo/search?q=document.cookie
|
|
||||||
/owner/repo/search?q=alert%281%29
|
|
||||||
/owner/repo/search?q=__proto__
|
|
||||||
/owner/repo/search?q=C%3A%5CWindows%5CSystem32
|
|
||||||
/owner/repo/search?q=Invoke-WebRequest
|
|
||||||
/owner/repo/search?q=if%20%28a%20%3D%3D%20b%29%20%7B
|
|
||||||
/owner/repo/search?q=sudo%20apt%20install
|
|
||||||
/owner/repo/search?q=~%2F.ssh%2Fconfig
|
|
||||||
/owner/repo/search?q=.htaccess
|
|
||||||
/owner/repo/search?q=%24_GET%5B
|
|
||||||
/owner/repo/search?q=%24_POST
|
|
||||||
/owner/repo/search?q=%24_SERVER%5B%27REQUEST_URI%27%5D
|
|
||||||
/owner/repo/search?q=%3C%3Fphp%20echo
|
|
||||||
/owner/repo/search?q=information_schema
|
|
||||||
/owner/repo/search?q=LIKE%20%27%25foo%25%27
|
|
||||||
/owner/repo/search?q=onClick%3D%7B
|
|
||||||
/explore/code?q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/explore/code?q=%3Cimg%20src%3D
|
|
||||||
/owner/-/code?q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/owner/-/code?q=%3Cimg%20src%3D
|
|
||||||
/owner/repo/issues?type=all&state=open&q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/owner/repo/issues?type=all&state=open&q=%3Cimg%20src%3D
|
|
||||||
/owner/repo/issues/search?q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/owner/repo/issues/search?q=%3Cimg%20src%3D
|
|
||||||
/owner/repo/pulls?q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/owner/repo/pulls?q=%3Cimg%20src%3D
|
|
||||||
/issues?type=your_repositories&q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/issues?type=your_repositories&q=%3Cimg%20src%3D
|
|
||||||
/issues/search?q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/issues/search?q=%3Cimg%20src%3D
|
|
||||||
/pulls?type=your_repositories&q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/pulls?type=your_repositories&q=%3Cimg%20src%3D
|
|
||||||
/owner/repo/commits/branch/main/search?q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/owner/repo/commits/branch/main/search?q=%3Cimg%20src%3D
|
|
||||||
/api/v1/repos/issues/search?q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/api/v1/repos/issues/search?q=%3Cimg%20src%3D
|
|
||||||
/api/v1/repos/owner/repo/issues?q=SELECT%20%2A%20FROM%20users%20WHERE
|
|
||||||
/api/v1/repos/owner/repo/issues?q=%3Cimg%20src%3D
|
|
||||||
|
|
||||||
# Searches for the names of repositories, users, branches, tags and packages.
|
|
||||||
/explore/repos?q=python3-app
|
|
||||||
/explore/repos?q=.profile
|
|
||||||
/explore/repos?q=powershell-scripts
|
|
||||||
/api/v1/repos/search?q=docker-build
|
|
||||||
/user/search_candidates?q=docker-ci
|
|
||||||
/owner/repo/branches?q=feat%2Fdocker-support
|
|
||||||
/owner/repo/tags?q=docker-v1.0
|
|
||||||
/owner/-/packages?q=python3-requests
|
|
||||||
/owner/-/packages?q=org.apache.commons.logging
|
|
||||||
/repo/search?q=python3-app
|
|
||||||
/repo/search?q=docker-build
|
|
||||||
/repo/search?q=powershell-scripts
|
|
||||||
/repo/search?q=.profile
|
|
||||||
/owner?tab=repositories&q=python3-app
|
|
||||||
/org?q=python3-app
|
|
||||||
/owner/repo/issues/posters?q=docker-ci
|
|
||||||
/owner/repo/pulls/posters?q=docker-ci
|
|
||||||
/explore/users?q=docker-ci
|
|
||||||
/explore/organizations?q=docker-ci
|
|
||||||
/api/v1/users/search?q=docker-ci
|
|
||||||
|
|
||||||
# Pages, raw files and edit pages of files, and the API's call for their
|
|
||||||
# contents.
|
|
||||||
/owner/repo/src/branch/main/package.json
|
|
||||||
/owner/repo/raw/branch/main/package.json
|
|
||||||
/owner/repo/_edit/main/package.json
|
|
||||||
/api/v1/repos/owner/repo/contents/package.json
|
|
||||||
/owner/repo/src/branch/main/.gitignore
|
|
||||||
/owner/repo/raw/branch/main/.gitignore
|
|
||||||
/owner/repo/_edit/main/.gitignore
|
|
||||||
/api/v1/repos/owner/repo/contents/.gitignore
|
|
||||||
/owner/repo/src/branch/main/docker-compose.yml
|
|
||||||
/owner/repo/raw/branch/main/docker-compose.yml
|
|
||||||
/owner/repo/_edit/main/docker-compose.yml
|
|
||||||
/api/v1/repos/owner/repo/contents/docker-compose.yml
|
|
||||||
/owner/repo/src/branch/main/Dockerfile
|
|
||||||
/owner/repo/raw/branch/main/Dockerfile
|
|
||||||
/owner/repo/_edit/main/Dockerfile
|
|
||||||
/api/v1/repos/owner/repo/contents/Dockerfile
|
|
||||||
/owner/repo/src/branch/main/.htaccess
|
|
||||||
/owner/repo/raw/branch/main/.htaccess
|
|
||||||
/owner/repo/_edit/main/.htaccess
|
|
||||||
/api/v1/repos/owner/repo/contents/.htaccess
|
|
||||||
/owner/repo/src/branch/main/rootfs/etc/passwd
|
|
||||||
/owner/repo/raw/branch/main/rootfs/etc/passwd
|
|
||||||
/owner/repo/_edit/main/rootfs/etc/passwd
|
|
||||||
/api/v1/repos/owner/repo/contents/rootfs/etc/passwd
|
|
||||||
/owner/repo/src/branch/main/bin/docker-entrypoint
|
|
||||||
/owner/repo/raw/branch/main/bin/docker-entrypoint
|
|
||||||
/owner/repo/_edit/main/bin/docker-entrypoint
|
|
||||||
/api/v1/repos/owner/repo/contents/bin/docker-entrypoint
|
|
||||||
/owner/repo/src/branch/main/scripts/python3-setup.sh
|
|
||||||
/owner/repo/raw/branch/main/scripts/python3-setup.sh
|
|
||||||
/owner/repo/_edit/main/scripts/python3-setup.sh
|
|
||||||
/api/v1/repos/owner/repo/contents/scripts/python3-setup.sh
|
|
||||||
/owner/repo/src/branch/main/src/java.base/share/classes/java/lang/Runtime.java
|
|
||||||
/owner/repo/raw/branch/main/src/java.base/share/classes/java/lang/Runtime.java
|
|
||||||
/owner/repo/_edit/main/src/java.base/share/classes/java/lang/Runtime.java
|
|
||||||
/api/v1/repos/owner/repo/contents/src/java.base/share/classes/java/lang/Runtime.java
|
|
||||||
/owner/repo/src/branch/main/java.lang.String.md
|
|
||||||
/owner/repo/raw/branch/main/java.lang.String.md
|
|
||||||
/owner/repo/_edit/main/java.lang.String.md
|
|
||||||
/api/v1/repos/owner/repo/contents/java.lang.String.md
|
|
||||||
/owner/repo/src/branch/main/META-INF/services/org.apache.commons.logging.LogFactory
|
|
||||||
/owner/repo/raw/branch/main/META-INF/services/org.apache.commons.logging.LogFactory
|
|
||||||
/owner/repo/_edit/main/META-INF/services/org.apache.commons.logging.LogFactory
|
|
||||||
/api/v1/repos/owner/repo/contents/META-INF/services/org.apache.commons.logging.LogFactory
|
|
||||||
/owner/repo/src/branch/main/process.env.d.ts
|
|
||||||
/owner/repo/raw/branch/main/process.env.d.ts
|
|
||||||
/owner/repo/_edit/main/process.env.d.ts
|
|
||||||
/api/v1/repos/owner/repo/contents/process.env.d.ts
|
|
||||||
/owner/repo/src/branch/main/document.write.js
|
|
||||||
/owner/repo/raw/branch/main/document.write.js
|
|
||||||
/owner/repo/_edit/main/document.write.js
|
|
||||||
/api/v1/repos/owner/repo/contents/document.write.js
|
|
||||||
/owner/repo/src/branch/main/test/fixtures/__proto__.json
|
|
||||||
/owner/repo/raw/branch/main/test/fixtures/__proto__.json
|
|
||||||
/owner/repo/_edit/main/test/fixtures/__proto__.json
|
|
||||||
/api/v1/repos/owner/repo/contents/test/fixtures/__proto__.json
|
|
||||||
/owner/repo/src/branch/main/install-script.sh
|
|
||||||
/owner/repo/raw/branch/main/install-script.sh
|
|
||||||
/owner/repo/_edit/main/install-script.sh
|
|
||||||
/api/v1/repos/owner/repo/contents/install-script.sh
|
|
||||||
/owner/repo/src/branch/main/src/backend/catalog/information_schema.sql
|
|
||||||
/owner/repo/raw/branch/main/src/backend/catalog/information_schema.sql
|
|
||||||
/owner/repo/_edit/main/src/backend/catalog/information_schema.sql
|
|
||||||
/api/v1/repos/owner/repo/contents/src/backend/catalog/information_schema.sql
|
|
||||||
/owner/repo/src/branch/main/docs/a%20b.xhtml
|
|
||||||
/owner/repo/raw/branch/main/docs/a%20b.xhtml
|
|
||||||
/owner/repo/_edit/main/docs/a%20b.xhtml
|
|
||||||
/api/v1/repos/owner/repo/contents/docs/a%20b.xhtml
|
|
||||||
/owner/repo/src/branch/main/%5Bdraft%5D.md
|
|
||||||
/owner/repo/raw/branch/main/%5Bdraft%5D.md
|
|
||||||
/owner/repo/_edit/main/%5Bdraft%5D.md
|
|
||||||
/api/v1/repos/owner/repo/contents/%5Bdraft%5D.md
|
|
||||||
/owner/repo/src/branch/main/notes.txt~
|
|
||||||
/owner/repo/raw/branch/main/notes.txt~
|
|
||||||
/owner/repo/_edit/main/notes.txt~
|
|
||||||
/api/v1/repos/owner/repo/contents/notes.txt~
|
|
||||||
|
|
||||||
# The download, blame, history, delete and preview pages of the files above
|
|
||||||
# whose names the Core Rule Set checks in the path, and their downloads as
|
|
||||||
# release attachments.
|
|
||||||
/owner/repo/media/branch/main/notes.txt~
|
|
||||||
/owner/repo/blame/branch/main/notes.txt~
|
|
||||||
/owner/repo/commits/branch/main/notes.txt~
|
|
||||||
/owner/repo/_delete/main/notes.txt~
|
|
||||||
/owner/repo/media/branch/main/process.env.d.ts
|
|
||||||
/owner/repo/blame/branch/main/process.env.d.ts
|
|
||||||
/owner/repo/commits/branch/main/process.env.d.ts
|
|
||||||
/owner/repo/_delete/main/process.env.d.ts
|
|
||||||
/owner/repo/media/branch/main/docs/a%20b.xhtml
|
|
||||||
/owner/repo/blame/branch/main/docs/a%20b.xhtml
|
|
||||||
/owner/repo/commits/branch/main/docs/a%20b.xhtml
|
|
||||||
/owner/repo/_delete/main/docs/a%20b.xhtml
|
|
||||||
/owner/repo/media/branch/main/java.lang.String.md
|
|
||||||
/owner/repo/blame/branch/main/java.lang.String.md
|
|
||||||
/owner/repo/commits/branch/main/java.lang.String.md
|
|
||||||
/owner/repo/_delete/main/java.lang.String.md
|
|
||||||
/owner/repo/_preview/main/notes.txt~
|
|
||||||
/owner/repo/_preview/main/process.env.d.ts
|
|
||||||
/owner/repo/_preview/main/java.lang.String.md
|
|
||||||
/owner/repo/releases/download/v1.0/notes.txt~
|
|
||||||
/owner/repo/releases/download/v1.0/process.env.d.ts
|
|
||||||
/owner/repo/releases/download/v1.0/java.lang.String.md
|
|
||||||
/owner/repo/releases/download/v1.0/a%20b.xhtml
|
|
||||||
|
|
||||||
# File names in the query string: the commit graph of a file, the API's lists
|
|
||||||
# of its commits and of its contents, an issue opened from a template, the
|
|
||||||
# sign-in page on the way to its edit page, and a diff's hidden lines.
|
|
||||||
/owner/repo/graph?file=package.json
|
|
||||||
/api/v1/repos/owner/repo/commits?path=package.json
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22package.json%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Fpackage.json
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Fpackage.json
|
|
||||||
/owner/repo/graph?file=.gitignore
|
|
||||||
/api/v1/repos/owner/repo/commits?path=.gitignore
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22.gitignore%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2F.gitignore
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2F.gitignore
|
|
||||||
/owner/repo/graph?file=docker-compose.yml
|
|
||||||
/api/v1/repos/owner/repo/commits?path=docker-compose.yml
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22docker-compose.yml%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Fdocker-compose.yml
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Fdocker-compose.yml
|
|
||||||
/owner/repo/graph?file=.htaccess
|
|
||||||
/api/v1/repos/owner/repo/commits?path=.htaccess
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22.htaccess%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2F.htaccess
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2F.htaccess
|
|
||||||
/owner/repo/graph?file=rootfs%2Fetc%2Fpasswd
|
|
||||||
/api/v1/repos/owner/repo/commits?path=rootfs%2Fetc%2Fpasswd
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22rootfs%2Fetc%2Fpasswd%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Frootfs%2Fetc%2Fpasswd
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Frootfs%2Fetc%2Fpasswd
|
|
||||||
/owner/repo/graph?file=bin%2Fdocker-entrypoint
|
|
||||||
/api/v1/repos/owner/repo/commits?path=bin%2Fdocker-entrypoint
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22bin%2Fdocker-entrypoint%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Fbin%2Fdocker-entrypoint
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Fbin%2Fdocker-entrypoint
|
|
||||||
/owner/repo/graph?file=scripts%2Fpython3-setup.sh
|
|
||||||
/api/v1/repos/owner/repo/commits?path=scripts%2Fpython3-setup.sh
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22scripts%2Fpython3-setup.sh%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Fscripts%2Fpython3-setup.sh
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Fscripts%2Fpython3-setup.sh
|
|
||||||
/owner/repo/graph?file=src%2Fjava.base%2Fshare%2Fclasses%2Fjava%2Flang%2FRuntime.java
|
|
||||||
/api/v1/repos/owner/repo/commits?path=src%2Fjava.base%2Fshare%2Fclasses%2Fjava%2Flang%2FRuntime.java
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22src%2Fjava.base%2Fshare%2Fclasses%2Fjava%2Flang%2FRuntime.java%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Fsrc%2Fjava.base%2Fshare%2Fclasses%2Fjava%2Flang%2FRuntime.java
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Fsrc%2Fjava.base%2Fshare%2Fclasses%2Fjava%2Flang%2FRuntime.java
|
|
||||||
/owner/repo/graph?file=java.lang.String.md
|
|
||||||
/api/v1/repos/owner/repo/commits?path=java.lang.String.md
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22java.lang.String.md%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Fjava.lang.String.md
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Fjava.lang.String.md
|
|
||||||
/owner/repo/graph?file=META-INF%2Fservices%2Forg.apache.commons.logging.LogFactory
|
|
||||||
/api/v1/repos/owner/repo/commits?path=META-INF%2Fservices%2Forg.apache.commons.logging.LogFactory
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22META-INF%2Fservices%2Forg.apache.commons.logging.LogFactory%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2FMETA-INF%2Fservices%2Forg.apache.commons.logging.LogFactory
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2FMETA-INF%2Fservices%2Forg.apache.commons.logging.LogFactory
|
|
||||||
/owner/repo/graph?file=process.env.d.ts
|
|
||||||
/api/v1/repos/owner/repo/commits?path=process.env.d.ts
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22process.env.d.ts%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Fprocess.env.d.ts
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Fprocess.env.d.ts
|
|
||||||
/owner/repo/graph?file=document.write.js
|
|
||||||
/api/v1/repos/owner/repo/commits?path=document.write.js
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22document.write.js%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Fdocument.write.js
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Fdocument.write.js
|
|
||||||
/owner/repo/graph?file=test%2Ffixtures%2F__proto__.json
|
|
||||||
/api/v1/repos/owner/repo/commits?path=test%2Ffixtures%2F__proto__.json
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22test%2Ffixtures%2F__proto__.json%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Ftest%2Ffixtures%2F__proto__.json
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Ftest%2Ffixtures%2F__proto__.json
|
|
||||||
/owner/repo/graph?file=install-script.sh
|
|
||||||
/api/v1/repos/owner/repo/commits?path=install-script.sh
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22install-script.sh%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Finstall-script.sh
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Finstall-script.sh
|
|
||||||
/owner/repo/graph?file=src%2Fbackend%2Fcatalog%2Finformation_schema.sql
|
|
||||||
/api/v1/repos/owner/repo/commits?path=src%2Fbackend%2Fcatalog%2Finformation_schema.sql
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22src%2Fbackend%2Fcatalog%2Finformation_schema.sql%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Fsrc%2Fbackend%2Fcatalog%2Finformation_schema.sql
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Fsrc%2Fbackend%2Fcatalog%2Finformation_schema.sql
|
|
||||||
/owner/repo/graph?file=docs%2Fa%20b.xhtml
|
|
||||||
/api/v1/repos/owner/repo/commits?path=docs%2Fa%20b.xhtml
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22docs%2Fa%20b.xhtml%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2Fdocs%2Fa%20b.xhtml
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2Fdocs%2Fa%20b.xhtml
|
|
||||||
/owner/repo/graph?file=%5Bdraft%5D.md
|
|
||||||
/api/v1/repos/owner/repo/commits?path=%5Bdraft%5D.md
|
|
||||||
/api/v1/repos/owner/repo/file-contents?body=%7B%22files%22%3A%5B%22%5Bdraft%5D.md%22%5D%7D
|
|
||||||
/owner/repo/issues/new?template=.gitea%2FISSUE_TEMPLATE%2F%5Bdraft%5D.md
|
|
||||||
/user/login?redirect_to=%2Fowner%2Frepo%2F_edit%2Fmain%2F%5Bdraft%5D.md
|
|
||||||
/owner/repo/blob_excerpt/0123456789abcdef0123456789abcdef01234567?last_left=10&last_right=10&left=20&right=20&left_hunk_size=5&right_hunk_size=5&path=process.env.d.ts&style=unified&direction=down&wiki=false
|
|
||||||
/owner/repo/blob_excerpt/0123456789abcdef0123456789abcdef01234567?last_left=10&last_right=10&left=20&right=20&left_hunk_size=5&right_hunk_size=5&path=java.lang.String.md&style=unified&direction=down&wiki=false
|
|
||||||
/owner/repo/blob_excerpt/0123456789abcdef0123456789abcdef01234567?last_left=10&last_right=10&left=20&right=20&left_hunk_size=5&right_hunk_size=5&path=install-script.sh&style=unified&direction=down&wiki=false
|
|
||||||
/owner/repo/blob_excerpt/0123456789abcdef0123456789abcdef01234567?last_left=10&last_right=10&left=20&right=20&left_hunk_size=5&right_hunk_size=5&path=information_schema.sql&style=unified&direction=down&wiki=false
|
|
||||||
/owner/repo/blob_excerpt/0123456789abcdef0123456789abcdef01234567?last_left=10&last_right=10&left=20&right=20&left_hunk_size=5&right_hunk_size=5&path=document.write.js&style=unified&direction=down&wiki=false
|
|
||||||
/owner/repo/blob_excerpt/0123456789abcdef0123456789abcdef01234567?last_left=10&last_right=10&left=20&right=20&left_hunk_size=5&right_hunk_size=5&path=__proto__.json&style=unified&direction=down&wiki=false
|
|
||||||
/owner/repo/blob_excerpt/0123456789abcdef0123456789abcdef01234567?last_left=10&last_right=10&left=20&right=20&left_hunk_size=5&right_hunk_size=5&path=src%2Fjava.base%2Fshare%2Fclasses%2Fjava%2Flang%2FRuntime.java&style=unified&direction=down&wiki=false
|
|
||||||
/owner/repo/blob_excerpt/0123456789abcdef0123456789abcdef01234567?last_left=10&last_right=10&left=20&right=20&left_hunk_size=5&right_hunk_size=5&path=index.xhtml&style=unified&direction=down&wiki=false
|
|
||||||
|
|
||||||
# File names of attachments uploaded through the API, to a release, an issue and
|
|
||||||
# a comment.
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=package.json
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=.gitignore
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=docker-compose.yml
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=.htaccess
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=docker-entrypoint
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=python3-setup.sh
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=Runtime.java
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=org.apache.commons.logging.LogFactory
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=%5Bdraft%5D.md
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=notes.txt~
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=install-script.sh
|
|
||||||
/api/v1/repos/owner/repo/issues/1/assets?name=install-script.sh
|
|
||||||
/api/v1/repos/owner/repo/issues/comments/1/assets?name=install-script.sh
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=process.env.d.ts
|
|
||||||
/api/v1/repos/owner/repo/issues/1/assets?name=process.env.d.ts
|
|
||||||
/api/v1/repos/owner/repo/issues/comments/1/assets?name=process.env.d.ts
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=java.lang.String.md
|
|
||||||
/api/v1/repos/owner/repo/issues/1/assets?name=java.lang.String.md
|
|
||||||
/api/v1/repos/owner/repo/issues/comments/1/assets?name=java.lang.String.md
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=information_schema.sql
|
|
||||||
/api/v1/repos/owner/repo/issues/1/assets?name=information_schema.sql
|
|
||||||
/api/v1/repos/owner/repo/issues/comments/1/assets?name=information_schema.sql
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=document.write.js
|
|
||||||
/api/v1/repos/owner/repo/issues/1/assets?name=document.write.js
|
|
||||||
/api/v1/repos/owner/repo/issues/comments/1/assets?name=document.write.js
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=__proto__.json
|
|
||||||
/api/v1/repos/owner/repo/issues/1/assets?name=__proto__.json
|
|
||||||
/api/v1/repos/owner/repo/issues/comments/1/assets?name=__proto__.json
|
|
||||||
/api/v1/repos/owner/repo/releases/1/assets?name=index.xhtml
|
|
||||||
/api/v1/repos/owner/repo/issues/1/assets?name=index.xhtml
|
|
||||||
/api/v1/repos/owner/repo/issues/comments/1/assets?name=index.xhtml
|
|
||||||
|
|
||||||
# An artifact upload from actions/upload-artifact@v3, which sends Content-Range.
|
|
||||||
/api/actions_pipeline/_apis/pipelines/workflows/1/artifacts/9b7c1f3a/upload?itemPath=artifact%2Freport.txt Content-Range: bytes 0-9/10
|
|
||||||
|
|
||||||
# Branch and tag names in the query string: deleting and restoring a branch,
|
|
||||||
# starting a release from a tag, a branch protection rule, a directory listing's
|
|
||||||
# last commits, and a container image push that reuses a layer.
|
|
||||||
/owner/repo/branches/delete?name=docker-build&page=1
|
|
||||||
/owner/repo/branches/restore?branch_id=1&name=docker-build&page=1
|
|
||||||
/owner/repo/releases/new?tag=docker-build
|
|
||||||
/owner/repo/settings/branches/edit?rule_name=docker-build
|
|
||||||
/owner/repo/lastcommit/main/src?refSubUrl=branch%2Fdocker-build
|
|
||||||
/v2/owner/app/blobs/uploads/?mount=sha256%3A0000&from=owner%2Fdocker-build
|
|
||||||
/owner/repo/branches/delete?name=feat%2Fdocker-support&page=1
|
|
||||||
/owner/repo/branches/restore?branch_id=1&name=feat%2Fdocker-support&page=1
|
|
||||||
/owner/repo/releases/new?tag=feat%2Fdocker-support
|
|
||||||
/owner/repo/settings/branches/edit?rule_name=feat%2Fdocker-support
|
|
||||||
/owner/repo/lastcommit/main/src?refSubUrl=branch%2Ffeat%2Fdocker-support
|
|
||||||
/v2/owner/app/blobs/uploads/?mount=sha256%3A0000&from=owner%2Ffeat%2Fdocker-support
|
|
||||||
/owner/repo/branches/delete?name=renovate%2Fdocker-build-push-action-6.x&page=1
|
|
||||||
/owner/repo/branches/restore?branch_id=1&name=renovate%2Fdocker-build-push-action-6.x&page=1
|
|
||||||
/owner/repo/releases/new?tag=renovate%2Fdocker-build-push-action-6.x
|
|
||||||
/owner/repo/settings/branches/edit?rule_name=renovate%2Fdocker-build-push-action-6.x
|
|
||||||
/owner/repo/lastcommit/main/src?refSubUrl=branch%2Frenovate%2Fdocker-build-push-action-6.x
|
|
||||||
/v2/owner/app/blobs/uploads/?mount=sha256%3A0000&from=owner%2Frenovate%2Fdocker-build-push-action-6.x
|
|
||||||
/owner/repo/branches/delete?name=python3-migration&page=1
|
|
||||||
/owner/repo/branches/restore?branch_id=1&name=python3-migration&page=1
|
|
||||||
/owner/repo/releases/new?tag=python3-migration
|
|
||||||
/owner/repo/settings/branches/edit?rule_name=python3-migration
|
|
||||||
/owner/repo/lastcommit/main/src?refSubUrl=branch%2Fpython3-migration
|
|
||||||
/v2/owner/app/blobs/uploads/?mount=sha256%3A0000&from=owner%2Fpython3-migration
|
|
||||||
/owner/repo/branches/delete?name=ansible-roles&page=1
|
|
||||||
/owner/repo/branches/restore?branch_id=1&name=ansible-roles&page=1
|
|
||||||
/owner/repo/releases/new?tag=ansible-roles
|
|
||||||
/owner/repo/settings/branches/edit?rule_name=ansible-roles
|
|
||||||
/owner/repo/lastcommit/main/src?refSubUrl=branch%2Fansible-roles
|
|
||||||
/v2/owner/app/blobs/uploads/?mount=sha256%3A0000&from=owner%2Fansible-roles
|
|
||||||
/owner/repo/branches/delete?name=whoami&page=1
|
|
||||||
/owner/repo/branches/restore?branch_id=1&name=whoami&page=1
|
|
||||||
/owner/repo/releases/new?tag=whoami
|
|
||||||
/owner/repo/settings/branches/edit?rule_name=whoami
|
|
||||||
/owner/repo/lastcommit/main/src?refSubUrl=branch%2Fwhoami
|
|
||||||
/v2/owner/app/blobs/uploads/?mount=sha256%3A0000&from=owner%2Fwhoami
|
|
||||||
|
|
||||||
# Pages a signed-in user opens.
|
|
||||||
/
|
|
||||||
/owner/repo
|
|
||||||
/owner/repo/issues
|
|
||||||
/owner/repo/pulls
|
|
||||||
/owner/repo/releases
|
|
||||||
/owner/repo/wiki
|
|
||||||
/owner/repo/activity
|
|
||||||
/owner/repo/branches
|
|
||||||
/owner/repo/tags
|
|
||||||
/owner/repo/settings
|
|
||||||
/owner/repo/actions
|
|
||||||
/owner/repo/compare/main...main
|
|
||||||
/owner/repo/commits/branch/main
|
|
||||||
/owner/repo/archive/main.zip
|
|
||||||
/notifications
|
|
||||||
/user/settings
|
|
||||||
/user/settings/applications
|
|
||||||
/explore/repos
|
|
||||||
/-/admin
|
|
||||||
+20
-158
@@ -1,9 +1,8 @@
|
|||||||
// Package waf runs the OWASP Core Rule Set 4.25.0, through Coraza, on the
|
// Package waf runs the OWASP Core Rule Set 4.25.0, through Coraza, on the
|
||||||
// method, the URL with its query and the headers of a request, and on its
|
// method, the URL with its query and the headers of a request, and on its
|
||||||
// body while SWW_WAF_BODY_LIMIT is set, with the eight changes sww
|
// body while SWWAF_WAF_BODY_LIMIT is set, with the six changes sww
|
||||||
// makes to it and the exclusions in SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as
|
// makes to it, as "Attack detection" under "Settings" in SPEC.md describes
|
||||||
// "Attack detection" under "Settings" in SPEC.md describes them. It reads no
|
// them. It reads no response.
|
||||||
// response.
|
|
||||||
//
|
//
|
||||||
// sww writes only to its state directory, so Coraza is built with
|
// sww writes only to its state directory, so Coraza is built with
|
||||||
// its no_fs_access tag, as the Dockerfile and script/build build it: of a
|
// its no_fs_access tag, as the Dockerfile and script/build build it: of a
|
||||||
@@ -12,14 +11,10 @@
|
|||||||
package waf
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -31,17 +26,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// directives are the Core Rule Set as sww runs it, with the
|
// directives are the Core Rule Set as sww runs it, with the
|
||||||
// paranoia level for %d, bodyDirectives for the first %s while
|
// paranoia level for %d, and bodyDirectives for %s while
|
||||||
// SWW_WAF_BODY_LIMIT is set, the exclusions file for the second, and
|
// SWWAF_WAF_BODY_LIMIT is set. Each rule sww adds has an id from
|
||||||
// giteaExclusions for the third.
|
// 900000 to 900999, the ids the Core Rule Set keeps for the rules that set
|
||||||
// Each rule sww adds has an id from 900000 to 900999, the ids the
|
// it up, which SWWAF_WAF_DISABLED_RULES refuses, so that no setting
|
||||||
// Core Rule Set keeps for the rules that set it up, which
|
// switches one off. Coraza joins a line ending in \ to the next, without
|
||||||
// SWW_WAF_DISABLED_RULES and the exclusions file refuse, so that no
|
// the spaces at the start of the next.
|
||||||
// setting switches one off. Coraza joins a line ending in \ to the next,
|
|
||||||
// without the spaces at the start of the next.
|
|
||||||
const directives = `
|
const directives = `
|
||||||
# The engine only detects. sww compares the request's anomaly
|
# The engine only detects. sww compares the request's anomaly
|
||||||
# score with SWW_WAF_ANOMALY_THRESHOLD itself, in block and detect mode
|
# score with SWWAF_WAF_ANOMALY_THRESHOLD itself, in block and detect mode
|
||||||
# alike. It reads no body, unless bodyDirectives switch that on.
|
# alike. It reads no body, unless bodyDirectives switch that on.
|
||||||
SecRuleEngine DetectionOnly
|
SecRuleEngine DetectionOnly
|
||||||
SecRequestBodyAccess Off
|
SecRequestBodyAccess Off
|
||||||
@@ -76,14 +69,6 @@ SecArgumentsLimit 1000
|
|||||||
SecRule ARGUMENTS_LIMIT_REACHED "@eq 1" "id:900300,phase:2,pass,\
|
SecRule ARGUMENTS_LIMIT_REACHED "@eq 1" "id:900300,phase:2,pass,\
|
||||||
severity:'CRITICAL',setvar:'tx.inbound_anomaly_score_pl1=+5'"
|
severity:'CRITICAL',setvar:'tx.inbound_anomaly_score_pl1=+5'"
|
||||||
|
|
||||||
# The exclusions file comes before the rules, where the Core Rule Set puts
|
|
||||||
# its own file of exclusions that run before the rules, so that they run
|
|
||||||
# before the rules of phase 1 too.
|
|
||||||
%s
|
|
||||||
|
|
||||||
# The eighth change comes before the rules for the same reason.
|
|
||||||
%s
|
|
||||||
|
|
||||||
# The sixth: only the rules for requests are loaded, and no response is
|
# The sixth: only the rules for requests are loaded, and no response is
|
||||||
# inspected.
|
# inspected.
|
||||||
Include @owasp_crs/REQUEST-*.conf
|
Include @owasp_crs/REQUEST-*.conf
|
||||||
@@ -111,19 +96,10 @@ SecRuleUpdateTargetById 932260 "!ARGS:path|!ARGS:files|!ARGS:skip-to|\
|
|||||||
!ARGS:artifactName|!ARGS:redirect_to"
|
!ARGS:artifactName|!ARGS:redirect_to"
|
||||||
|
|
||||||
# The fifth, for Referer: it is not checked for a Unix command without
|
# The fifth, for Referer: it is not checked for a Unix command without
|
||||||
# arguments, for Java starting a process, or for a Java class name. 944130
|
# arguments, or for Java starting a process. The cookies are left out in
|
||||||
# refused every page opened from the page of java.lang.String.md. The
|
# Inspect.
|
||||||
# cookies are left out in Inspect.
|
|
||||||
SecRuleUpdateTargetById 932340 "!REQUEST_HEADERS:Referer"
|
SecRuleUpdateTargetById 932340 "!REQUEST_HEADERS:Referer"
|
||||||
SecRuleUpdateTargetById 944110 "!REQUEST_HEADERS:Referer"
|
SecRuleUpdateTargetById 944110 "!REQUEST_HEADERS:Referer"
|
||||||
SecRuleUpdateTargetById 944130 "!REQUEST_HEADERS:Referer"
|
|
||||||
|
|
||||||
# The seventh: a container image push that reuses a layer names the image
|
|
||||||
# it takes the layer from in the query parameter from, which is not
|
|
||||||
# checked for command names. 932260 refused the push of a layer from
|
|
||||||
# owner/python3-app.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/v2/.+/blobs/uploads/$" \
|
|
||||||
"id:900500,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:from"
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// bodyDirectives have the Core Rule Set read the part of a request body
|
// bodyDirectives have the Core Rule Set read the part of a request body
|
||||||
@@ -168,13 +144,6 @@ SecRule MULTIPART_STRICT_ERROR "!@eq 0" "id:900450,phase:2,pass,\
|
|||||||
severity:'CRITICAL',setvar:'tx.inbound_anomaly_score_pl1=+5'"
|
severity:'CRITICAL',setvar:'tx.inbound_anomaly_score_pl1=+5'"
|
||||||
`
|
`
|
||||||
|
|
||||||
// giteaExclusions are the eighth change: the fields of gitea's own
|
|
||||||
// requests that skip the rules that refused them there, as Coraza
|
|
||||||
// directives.
|
|
||||||
//
|
|
||||||
//go:embed gitea.conf
|
|
||||||
var giteaExclusions string
|
|
||||||
|
|
||||||
// cookiesNotRead are the cookies the Core Rule Set reads a request
|
// cookiesNotRead are the cookies the Core Rule Set reads a request
|
||||||
// without, the rest of the fifth change.
|
// without, the rest of the fifth change.
|
||||||
//
|
//
|
||||||
@@ -183,102 +152,14 @@ var cookiesNotRead = []string{"gitea_flash", "redirect_to"}
|
|||||||
|
|
||||||
// Params are what New needs.
|
// Params are what New needs.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
// ParanoiaLevel is SWW_WAF_PARANOIA_LEVEL, from 1 to 4.
|
// ParanoiaLevel is SWWAF_WAF_PARANOIA_LEVEL, from 1 to 4.
|
||||||
ParanoiaLevel int
|
ParanoiaLevel int
|
||||||
// DisabledRules are the ids of the rules switched off
|
// DisabledRules are the ids of the rules switched off
|
||||||
// (SWW_WAF_DISABLED_RULES).
|
// (SWWAF_WAF_DISABLED_RULES).
|
||||||
DisabledRules []int
|
DisabledRules []int
|
||||||
// BodyLimit is the most of a request body the Core Rule Set reads
|
// BodyLimit is the most of a request body the Core Rule Set reads
|
||||||
// (SWW_WAF_BODY_LIMIT), 0 while it is off and it reads none.
|
// (SWWAF_WAF_BODY_LIMIT), 0 while it is off and it reads none.
|
||||||
BodyLimit int64
|
BodyLimit int64
|
||||||
// ExclusionsFile is the path of a file of exclusions loaded before the
|
|
||||||
// Core Rule Set's rules (SWW_CORE_RULE_SET_EXCLUSIONS_FILE), "" while
|
|
||||||
// there is none.
|
|
||||||
ExclusionsFile string
|
|
||||||
}
|
|
||||||
|
|
||||||
// The ids of sww's own rules, which an exclusions file may not
|
|
||||||
// name.
|
|
||||||
const (
|
|
||||||
firstOwnRuleID = 900000
|
|
||||||
lastOwnRuleID = 900999
|
|
||||||
)
|
|
||||||
|
|
||||||
// numberOrRange matches a number, or a range of numbers such as
|
|
||||||
// 900000-900999. Coraza reads a number with a + before it too, so the
|
|
||||||
// end of a range may have one.
|
|
||||||
var numberOrRange = regexp.MustCompile(`(\d+)(?:-\+?(\d+))?`)
|
|
||||||
|
|
||||||
// errSwwRuleID is the error of an exclusions file that names an
|
|
||||||
// id of sww's own rules.
|
|
||||||
var errSwwRuleID = errors.New("an id of sww's own rules")
|
|
||||||
|
|
||||||
// errChangesNoRule is the error of an exclusions file that holds a
|
|
||||||
// directive that removes or changes rules as it loads, such as
|
|
||||||
// SecRuleRemoveById. The Core Rule Set's rules load after the file, so
|
|
||||||
// the directive finds none of them.
|
|
||||||
var errChangesNoRule = errors.New("the Core Rule Set's rules load after the file; " +
|
|
||||||
"switch a rule off with SWW_WAF_DISABLED_RULES, or on some requests " +
|
|
||||||
"with a ctl exclusion")
|
|
||||||
|
|
||||||
// directivesIn returns the directives in text as Coraza reads them: it
|
|
||||||
// skips blank lines and comment lines, and joins a line ending in \ to
|
|
||||||
// the next, without the spaces around either.
|
|
||||||
func directivesIn(text string) []string {
|
|
||||||
var directives []string
|
|
||||||
|
|
||||||
joined := ""
|
|
||||||
|
|
||||||
for line := range strings.Lines(text) {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if start, found := strings.CutSuffix(line, `\`); found {
|
|
||||||
joined += start
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
directives = append(directives, joined+line)
|
|
||||||
joined = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return directives
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkExclusions returns an error if exclusions, the text of an
|
|
||||||
// exclusions file, names an id of sww's own rules, alone or in a
|
|
||||||
// range, or holds a directive whose name starts with SecRuleRemove or
|
|
||||||
// SecRuleUpdate.
|
|
||||||
func checkExclusions(exclusions string) error {
|
|
||||||
for _, directive := range directivesIn(exclusions) {
|
|
||||||
for _, match := range numberOrRange.FindAllStringSubmatch(directive, -1) {
|
|
||||||
first, _ := strconv.Atoi(match[1])
|
|
||||||
|
|
||||||
last := first
|
|
||||||
if match[2] != "" {
|
|
||||||
last, _ = strconv.Atoi(match[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
if first <= lastOwnRuleID && last >= firstOwnRuleID {
|
|
||||||
return fmt.Errorf("the exclusions file names %s: %w", match[0],
|
|
||||||
errSwwRuleID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Coraza takes the directive's name up to the first space, without
|
|
||||||
// regard to case.
|
|
||||||
name, _, _ := strings.Cut(directive, " ")
|
|
||||||
if lower := strings.ToLower(name); strings.HasPrefix(lower, "secruleremove") ||
|
|
||||||
strings.HasPrefix(lower, "secruleupdate") {
|
|
||||||
return fmt.Errorf("%s in the exclusions file changes no rule: %w", name,
|
|
||||||
errChangesNoRule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CoreRuleSet is the Core Rule Set, ready to inspect requests. It is safe
|
// CoreRuleSet is the Core Rule Set, ready to inspect requests. It is safe
|
||||||
@@ -288,35 +169,16 @@ type CoreRuleSet struct {
|
|||||||
bodyLimit int64
|
bodyLimit int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns the Core Rule Set with the eight changes, at params'
|
// New returns the Core Rule Set with the six changes, at params'
|
||||||
// paranoia level, without the rules it switches off, reading request
|
// paranoia level, without the rules it switches off, and reading request
|
||||||
// bodies up to params' limit, and with params' exclusions file loaded
|
// bodies up to params' limit.
|
||||||
// before the Core Rule Set's rules. An exclusions file is an error if it
|
|
||||||
// cannot be read or does not parse, or if checkExclusions refuses it.
|
|
||||||
func New(params Params) (*CoreRuleSet, error) {
|
func New(params Params) (*CoreRuleSet, error) {
|
||||||
body := ""
|
body := ""
|
||||||
if params.BodyLimit > 0 {
|
if params.BodyLimit > 0 {
|
||||||
body = fmt.Sprintf(bodyDirectives, params.BodyLimit)
|
body = fmt.Sprintf(bodyDirectives, params.BodyLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
exclusions := ""
|
text := fmt.Sprintf(directives, params.ParanoiaLevel, body)
|
||||||
|
|
||||||
if params.ExclusionsFile != "" {
|
|
||||||
file, err := os.ReadFile(params.ExclusionsFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("read the exclusions file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exclusions = string(file)
|
|
||||||
|
|
||||||
err = checkExclusions(exclusions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions,
|
|
||||||
giteaExclusions)
|
|
||||||
|
|
||||||
if len(params.DisabledRules) > 0 {
|
if len(params.DisabledRules) > 0 {
|
||||||
ids := make([]string, len(params.DisabledRules))
|
ids := make([]string, len(params.DisabledRules))
|
||||||
@@ -404,7 +266,7 @@ func (c *CoreRuleSet) Inspect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// readBody reads body, the body of the request in tx, which has run on
|
// readBody reads body, the body of the request in tx, which has run on
|
||||||
// the request's headers, while SWW_WAF_BODY_LIMIT is set and the body is
|
// the request's headers, while SWWAF_WAF_BODY_LIMIT is set and the body is
|
||||||
// of a kind the Core Rule Set reads: form data and multipart, of which it
|
// of a kind the Core Rule Set reads: form data and multipart, of which it
|
||||||
// reads the first c.bodyLimit bytes, and JSON and XML, which it reads only
|
// reads the first c.bodyLimit bytes, and JSON and XML, which it reads only
|
||||||
// when they are no longer than that, since they cannot be read in part.
|
// when they are no longer than that, since they cannot be read in part.
|
||||||
|
|||||||
+4
-192
@@ -5,7 +5,6 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -15,7 +14,7 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultDisabledRules are the rules SWW_WAF_DISABLED_RULES switches off
|
// defaultDisabledRules are the rules SWWAF_WAF_DISABLED_RULES switches off
|
||||||
// by default.
|
// by default.
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
@@ -120,7 +119,7 @@ func atDefaults(t *testing.T) *waf.CoreRuleSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wantChange checks that crs lets through passes, a gitea request one of
|
// wantChange checks that crs lets through passes, a gitea request one of
|
||||||
// the eight changes is for, and still finds result in refused, a request
|
// the six changes is for, and still finds result in refused, a request
|
||||||
// like it that the change is not for.
|
// like it that the change is not for.
|
||||||
func wantChange(
|
func wantChange(
|
||||||
t *testing.T, crs *waf.CoreRuleSet, passes, refused request, result waf.Result,
|
t *testing.T, crs *waf.CoreRuleSet, passes, refused request, result waf.Result,
|
||||||
@@ -309,14 +308,13 @@ func TestCookiesGiteaFlashAndRedirectToAreNotRead(t *testing.T) {
|
|||||||
matched(930120))
|
matched(930120))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRefererIsNotCheckedForACommandOrJava(t *testing.T) {
|
func TestRefererIsNotCheckedForACommandOrJavaStartingAProcess(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
search = "https://git.example/explore/repos?q=env"
|
search = "https://git.example/explore/repos?q=env"
|
||||||
runtimeJava = "https://git.example/openjdk/jdk/src/branch/master/src/" +
|
runtimeJava = "https://git.example/openjdk/jdk/src/branch/master/src/" +
|
||||||
"java.base/share/classes/java/lang/Runtime.java"
|
"java.base/share/classes/java/lang/Runtime.java"
|
||||||
stringMD = "https://git.example/owner/repo/src/branch/main/java.lang.String.md"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
crs := atDefaults(t)
|
crs := atDefaults(t)
|
||||||
@@ -325,8 +323,6 @@ func TestRefererIsNotCheckedForACommandOrJava(t *testing.T) {
|
|||||||
matched(932340))
|
matched(932340))
|
||||||
wantChange(t, crs, get("/", "Referer: "+runtimeJava),
|
wantChange(t, crs, get("/", "Referer: "+runtimeJava),
|
||||||
get("/", "X-Page: "+runtimeJava), matched(944110))
|
get("/", "X-Page: "+runtimeJava), matched(944110))
|
||||||
wantChange(t, crs, get("/", "Referer: "+stringMD), get("/", "X-Page: "+stringMD),
|
|
||||||
matched(944130))
|
|
||||||
|
|
||||||
// It is still checked for script and SQL injection.
|
// It is still checked for script and SQL injection.
|
||||||
wantResult(t, crs,
|
wantResult(t, crs,
|
||||||
@@ -366,191 +362,7 @@ func TestEachDisabledRuleIsSwitchedOff(t *testing.T) {
|
|||||||
wantResult(t, newCoreRuleSet(t, 1, 920350, 911100), r, waf.Result{})
|
wantResult(t, newCoreRuleSet(t, 1, 920350, 911100), r, waf.Result{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImagePushThatReusesALayerIsNotCheckedForCommandNames(t *testing.T) {
|
// bodyLimit is SWWAF_WAF_BODY_LIMIT in the tests that read bodies.
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := atDefaults(t)
|
|
||||||
reuse := "?mount=sha256:0000&from=owner/python3-app"
|
|
||||||
|
|
||||||
wantChange(t, crs, get("/v2/owner/app/blobs/uploads/"+reuse), get("/upload/"+reuse),
|
|
||||||
matched(932260))
|
|
||||||
|
|
||||||
// A command after the image's name is still refused.
|
|
||||||
wantResult(t, crs,
|
|
||||||
get("/v2/owner/app/blobs/uploads/?from=owner/app;cat%20/etc/passwd"),
|
|
||||||
matched(930120, 932160))
|
|
||||||
}
|
|
||||||
|
|
||||||
// withExclusions returns the Core Rule Set at the defaults, with the
|
|
||||||
// exclusions file at path.
|
|
||||||
func withExclusions(path string) (*waf.CoreRuleSet, error) {
|
|
||||||
return waf.New(waf.Params{
|
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, ExclusionsFile: path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// exclusionsFile writes text to a file of exclusions and returns its path.
|
|
||||||
func exclusionsFile(t *testing.T, text string) string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "exclusions.conf")
|
|
||||||
|
|
||||||
err := os.WriteFile(path, []byte(text), 0o600)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// backupFileExclusion is an exclusions file that switches 920500, a rule
|
|
||||||
// of phase 1, off for the path of a backup file under /files/.
|
|
||||||
const backupFileExclusion = `SecRule REQUEST_FILENAME "@beginsWith /files/" ` +
|
|
||||||
`"id:1,phase:1,pass,nolog,ctl:ruleRemoveTargetById=920500;REQUEST_FILENAME"` + "\n"
|
|
||||||
|
|
||||||
func TestExclusionsFileIsLoadedBeforeTheRules(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs, err := withExclusions(exclusionsFile(t, backupFileExclusion))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("load the exclusions: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantChange(t, crs, get("/files/notes.txt~"), get("/notes.txt~"), matched(920500))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRuleSwitchedOffAndNamedInTheExclusionsFileIsOff(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs, err := waf.New(waf.Params{
|
|
||||||
ParanoiaLevel: 1, DisabledRules: []int{920500},
|
|
||||||
ExclusionsFile: exclusionsFile(t, backupFileExclusion),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("load the exclusions: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantResult(t, crs, get("/notes.txt~"), waf.Result{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExclusionsFileMayNameAnyNumberInAComment(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
_, err := withExclusions(exclusionsFile(t,
|
|
||||||
"# 942100: GET /issues/900123\n"+backupFileExclusion))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("the exclusions did not load: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExclusionsFileThatCannotBeUsedIsAnError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
missing := filepath.Join(t.TempDir(), "missing.conf")
|
|
||||||
|
|
||||||
_, err := withExclusions(missing)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("%s loaded", missing)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct{ text, want string }{
|
|
||||||
{"SecNoSuchDirective On\n", "unknown directive"},
|
|
||||||
// sww's own rules may not be switched off or changed, by
|
|
||||||
// id or in a range, on one line or split over several.
|
|
||||||
{"SecRuleRemoveById 899999-901000\n", "names 899999-901000"},
|
|
||||||
{
|
|
||||||
"SecAction \"id:1,phase:1,pass,nolog,ctl:ruleRemoveById=899999-901000\"\n",
|
|
||||||
"names 899999-901000",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"SecAction \"id:1,phase:1,pass,nolog,ctl:ruleRemoveById=899999-+901000\"\n",
|
|
||||||
"names 899999-+901000",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"SecAction \"id:1,phase:1,pass,nolog,ctl:ruleRemoveTargetById=900500;ARGS:from\"\n",
|
|
||||||
"names 900500",
|
|
||||||
},
|
|
||||||
{"SecRuleRemoveById 899000-\\\n 901000\n", "names 899000-901000"},
|
|
||||||
{
|
|
||||||
"SecAction \"id:1,phase:1,pass,nolog,\\\n" +
|
|
||||||
" ctl:ruleRemoveById=9005\\\n 00\"\n",
|
|
||||||
"names 900500",
|
|
||||||
},
|
|
||||||
// The Core Rule Set's rules load after the file.
|
|
||||||
{"SecRuleRemoveById 942100\n", "SecRuleRemoveById in the exclusions file"},
|
|
||||||
{
|
|
||||||
"SecRuleUpdateTargetById 942100 \"!ARGS:id\"\n",
|
|
||||||
"SecRuleUpdateTargetById in the exclusions file",
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
_, err := withExclusions(exclusionsFile(t, tc.text))
|
|
||||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
||||||
t.Errorf("%q gave the error %v, want one with %q", tc.text, err, tc.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// giteaRequests returns the recorded ordinary gitea requests in testdata.
|
|
||||||
// Each has the Referer of a page that names the same, as a page opened
|
|
||||||
// from it has.
|
|
||||||
func giteaRequests(t *testing.T) []request {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
text, err := os.ReadFile(filepath.Join("testdata", "gitea-requests.txt"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var requests []request
|
|
||||||
|
|
||||||
for line := range strings.Lines(string(text)) {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
target, header, _ := strings.Cut(line, " ")
|
|
||||||
|
|
||||||
headers := []string{"Referer: http://git.example" + target}
|
|
||||||
if header != "" {
|
|
||||||
headers = append(headers, header)
|
|
||||||
}
|
|
||||||
|
|
||||||
requests = append(requests, get(target, headers...))
|
|
||||||
}
|
|
||||||
|
|
||||||
return requests
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOrdinaryGiteaRequestsPassAndAttacksAreRefused(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
|
||||||
|
|
||||||
crs := atDefaults(t)
|
|
||||||
|
|
||||||
for _, r := range giteaRequests(t) {
|
|
||||||
if got := inspect(t, crs, r); got.Score >= threshold {
|
|
||||||
t.Errorf("%s %q refused: %+v", r.target, r.headers[1:], got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In q too, off gitea's search pages.
|
|
||||||
for _, parameter := range []string{"id", "q"} {
|
|
||||||
for _, attack := range []string{
|
|
||||||
"1'%20OR%20'1'='1",
|
|
||||||
"..%2Fcustom%2Fconf%2Fapp.ini",
|
|
||||||
"%3Cscript%3Ealert(1)%3C%2Fscript%3E",
|
|
||||||
} {
|
|
||||||
target := "/owner/repo?" + parameter + "=" + attack
|
|
||||||
if got := inspect(t, crs, get(target)); got.Score < threshold {
|
|
||||||
t.Errorf("%s let through: %+v", target, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// bodyLimit is SWW_WAF_BODY_LIMIT in the tests that read bodies.
|
|
||||||
const bodyLimit = 8 << 10
|
const bodyLimit = 8 << 10
|
||||||
|
|
||||||
// The Content-Type headers of the kinds of body the Core Rule Set reads.
|
// The Content-Type headers of the kinds of body the Core Rule Set reads.
|
||||||
|
|||||||
@@ -20,13 +20,8 @@ main() {
|
|||||||
# the version a build stage derives from the .git in the context.
|
# the version a build stage derives from the .git in the context.
|
||||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
[ -n "$version" ] || version="unknown"
|
[ -n "$version" ] || version="unknown"
|
||||||
revision="$(git rev-parse HEAD 2>/dev/null || true)"
|
|
||||||
[ -n "$revision" ] || revision="unknown"
|
|
||||||
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
||||||
docker build --no-cache \
|
docker build --no-cache \
|
||||||
--build-arg VERSION="$version" \
|
--build-arg VERSION="$version" \
|
||||||
--build-arg REVISION="$revision" \
|
|
||||||
--build-arg CREATED="$created" \
|
|
||||||
-t "$("$SCRIPT_DIR/projectname")" .
|
-t "$("$SCRIPT_DIR/projectname")" .
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,8 @@ main() {
|
|||||||
# the version a build stage derives from the .git in the context.
|
# the version a build stage derives from the .git in the context.
|
||||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
[ -n "$version" ] || version="unknown"
|
[ -n "$version" ] || version="unknown"
|
||||||
revision="$(git rev-parse HEAD 2>/dev/null || true)"
|
|
||||||
[ -n "$revision" ] || revision="unknown"
|
|
||||||
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
||||||
docker build --no-cache \
|
docker build --no-cache \
|
||||||
--build-arg VERSION="$version" \
|
--build-arg VERSION="$version" \
|
||||||
--build-arg REVISION="$revision" \
|
|
||||||
--build-arg CREATED="$created" \
|
|
||||||
-t "$("$SCRIPT_DIR/projectname")" .
|
-t "$("$SCRIPT_DIR/projectname")" .
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-15
@@ -16,10 +16,10 @@
|
|||||||
# - that a new discriminator container on the same volume still refuses the banned
|
# - that a new discriminator container on the same volume still refuses the banned
|
||||||
# client;
|
# client;
|
||||||
# - that a discriminator container with its state files on a new volume mounted at
|
# - that a discriminator container with its state files on a new volume mounted at
|
||||||
# the directory SWW_STATE_DIR_FILE names becomes healthy;
|
# the directory SWWAF_STATE_DIR_FILE names becomes healthy;
|
||||||
# - that one whose SWW_STATE_DIR does not exist logs discriminator's message
|
# - that one whose SWWAF_STATE_DIR does not exist logs discriminator's message
|
||||||
# about it.
|
# about it.
|
||||||
# discriminator runs with SWW_LOOKUP_SOURCE=off, so that no address is sent to
|
# discriminator runs with SWWAF_LOOKUP_SOURCE=off, so that no address is sent to
|
||||||
# GeoJS. The containers, the network, the volumes and both images are removed
|
# GeoJS. The containers, the network, the volumes and both images are removed
|
||||||
# however the script ends. Building the app needs network access, for nixpkgs'
|
# however the script ends. Building the app needs network access, for nixpkgs'
|
||||||
# binary cache. script/check does not run this.
|
# binary cache. script/check does not run this.
|
||||||
@@ -37,7 +37,7 @@ NETWORK="$NAME"
|
|||||||
APP="$NAME-app-container"
|
APP="$NAME-app-container"
|
||||||
DISCRIMINATOR="$NAME-discriminator"
|
DISCRIMINATOR="$NAME-discriminator"
|
||||||
VOLUME="$NAME-state"
|
VOLUME="$NAME-state"
|
||||||
# The volumes of the container whose state directory SWW_STATE_DIR_FILE
|
# The volumes of the container whose state directory SWWAF_STATE_DIR_FILE
|
||||||
# names: its state files, and the file that names their directory.
|
# names: its state files, and the file that names their directory.
|
||||||
MOVED_VOLUME="$NAME-moved-state"
|
MOVED_VOLUME="$NAME-moved-state"
|
||||||
SETTINGS_VOLUME="$NAME-settings"
|
SETTINGS_VOLUME="$NAME-settings"
|
||||||
@@ -94,8 +94,8 @@ start_discriminator() {
|
|||||||
docker run --detach --name "$DISCRIMINATOR" --network "$NETWORK" \
|
docker run --detach --name "$DISCRIMINATOR" --network "$NETWORK" \
|
||||||
--volume "$VOLUME:/var/lib/sww" \
|
--volume "$VOLUME:/var/lib/sww" \
|
||||||
--env SWW_COMMAND=discriminator \
|
--env SWW_COMMAND=discriminator \
|
||||||
--env SWW_RATE_LIMIT_PER_MINUTE=1 \
|
--env SWWAF_RATE_LIMIT_PER_MINUTE=1 \
|
||||||
--env SWW_LOOKUP_SOURCE=off \
|
--env SWWAF_LOOKUP_SOURCE=off \
|
||||||
"$IMAGE" >/dev/null
|
"$IMAGE" >/dev/null
|
||||||
wait_for "discriminator's health check did not pass" healthy "$DISCRIMINATOR"
|
wait_for "discriminator's health check did not pass" healthy "$DISCRIMINATOR"
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ start_app() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# start_moved_discriminator: run discriminator with the state files on a new volume at
|
# start_moved_discriminator: run discriminator with the state files on a new volume at
|
||||||
# /srv/state, which SWW_STATE_DIR_FILE names in a file only the sww
|
# /srv/state, which SWWAF_STATE_DIR_FILE names in a file only the sww
|
||||||
# user can read, as under "Settings given as files" in docs/settings.md, and
|
# user can read, as under "Settings given as files" in docs/settings.md, and
|
||||||
# wait until it is healthy. discriminator can write the new volume only once the run
|
# wait until it is healthy. discriminator can write the new volume only once the run
|
||||||
# script has given it to that user.
|
# script has given it to that user.
|
||||||
@@ -126,8 +126,8 @@ start_moved_discriminator() {
|
|||||||
--volume "$MOVED_VOLUME:/srv/state" \
|
--volume "$MOVED_VOLUME:/srv/state" \
|
||||||
--volume "$SETTINGS_VOLUME:/etc/sww/settings:ro" \
|
--volume "$SETTINGS_VOLUME:/etc/sww/settings:ro" \
|
||||||
--env SWW_COMMAND=discriminator \
|
--env SWW_COMMAND=discriminator \
|
||||||
--env SWW_STATE_DIR_FILE=/etc/sww/settings/state-dir \
|
--env SWWAF_STATE_DIR_FILE=/etc/sww/settings/state-dir \
|
||||||
--env SWW_LOOKUP_SOURCE=off \
|
--env SWWAF_LOOKUP_SOURCE=off \
|
||||||
"$IMAGE" >/dev/null
|
"$IMAGE" >/dev/null
|
||||||
wait_for "discriminator's health check did not pass" healthy "$DISCRIMINATOR"
|
wait_for "discriminator's health check did not pass" healthy "$DISCRIMINATOR"
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ status_from() {
|
|||||||
--max-time 10 --header "X-Forwarded-For: $1" "http://$address$2" || true
|
--max-time 10 --header "X-Forwarded-For: $1" "http://$address$2" || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# refused: a request to the container gets 403, SWW_BAN_RESPONSE's
|
# refused: a request to the container gets 403, SWWAF_BAN_RESPONSE's
|
||||||
# default.
|
# default.
|
||||||
refused() {
|
refused() {
|
||||||
code="$(curl --silent --output /dev/null --write-out '%{http_code}' \
|
code="$(curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||||
@@ -164,7 +164,7 @@ main() {
|
|||||||
trap 'exit 1' HUP INT TERM
|
trap 'exit 1' HUP INT TERM
|
||||||
|
|
||||||
docker build --no-cache -t "$IMAGE" .
|
docker build --no-cache -t "$IMAGE" .
|
||||||
docker build --no-cache --build-arg SWW_IMAGE="$IMAGE" \
|
docker build --no-cache --build-arg SMALLWEBWAF_IMAGE="$IMAGE" \
|
||||||
-t "$APP_IMAGE" deploy/example-app
|
-t "$APP_IMAGE" deploy/example-app
|
||||||
|
|
||||||
docker network create "$NETWORK" >/dev/null
|
docker network create "$NETWORK" >/dev/null
|
||||||
@@ -238,16 +238,16 @@ main() {
|
|||||||
docker rm --force "$DISCRIMINATOR" >/dev/null
|
docker rm --force "$DISCRIMINATOR" >/dev/null
|
||||||
start_moved_discriminator
|
start_moved_discriminator
|
||||||
echo "example-app: the run script gives the state files' directory named" \
|
echo "example-app: the run script gives the state files' directory named" \
|
||||||
"by SWW_STATE_DIR_FILE to the sww user"
|
"by SWWAF_STATE_DIR_FILE to the sww user"
|
||||||
|
|
||||||
docker rm --force "$DISCRIMINATOR" >/dev/null
|
docker rm --force "$DISCRIMINATOR" >/dev/null
|
||||||
docker run --detach --name "$DISCRIMINATOR" --network "$NETWORK" \
|
docker run --detach --name "$DISCRIMINATOR" --network "$NETWORK" \
|
||||||
--env SWW_COMMAND=discriminator \
|
--env SWW_COMMAND=discriminator \
|
||||||
--env SWW_STATE_DIR=/srv/missing \
|
--env SWWAF_STATE_DIR=/srv/missing \
|
||||||
--env SWW_LOOKUP_SOURCE=off \
|
--env SWWAF_LOOKUP_SOURCE=off \
|
||||||
"$IMAGE" >/dev/null
|
"$IMAGE" >/dev/null
|
||||||
wait_for "discriminator did not report the missing state directory" \
|
wait_for "discriminator did not report the missing state directory" \
|
||||||
logged "$DISCRIMINATOR" "SWW_STATE_DIR cannot be written"
|
logged "$DISCRIMINATOR" "SWWAF_STATE_DIR cannot be written"
|
||||||
echo "example-app: discriminator reports a state directory that does not exist"
|
echo "example-app: discriminator reports a state directory that does not exist"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -3,10 +3,10 @@
|
|||||||
# rproxy on the host, with the settings in the environment, until rproxy
|
# rproxy on the host, with the settings in the environment, until rproxy
|
||||||
# exits or the script is told to stop. rproxy listens on port 8080 unless
|
# exits or the script is told to stop. rproxy listens on port 8080 unless
|
||||||
# SWRP_LISTEN_ADDR says otherwise, in front of the app at SWRP_UPSTREAM_URL.
|
# SWRP_LISTEN_ADDR says otherwise, in front of the app at SWRP_UPSTREAM_URL.
|
||||||
# discriminator listens on port 8090 unless SWW_LISTEN_ADDR says otherwise, and
|
# discriminator listens on port 8090 unless SWWAF_LISTEN_ADDR says otherwise, and
|
||||||
# rproxy asks it there unless SWRP_WAF_BACKEND says otherwise. Unless
|
# rproxy asks it there unless SWRP_WAF_BACKEND says otherwise. Unless
|
||||||
# SWW_STATE_DIR is set, the state files go in bin/state, beside the binary,
|
# SWWAF_STATE_DIR is set, the state files go in bin/state, beside the binary,
|
||||||
# and unless SWW_RULES_DIR is set, the rule files are those of share/rules.d,
|
# and unless SWWAF_RULES_DIR is set, the rule files are those of share/rules.d,
|
||||||
# which the image ships.
|
# which the image ships.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
@@ -26,18 +26,18 @@ stop_both() {
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
"$SCRIPT_DIR/build"
|
"$SCRIPT_DIR/build"
|
||||||
if [ -z "${SWW_STATE_DIR+set}" ]; then
|
if [ -z "${SWWAF_STATE_DIR+set}" ]; then
|
||||||
SWW_STATE_DIR="$ROOT/bin/state"
|
SWWAF_STATE_DIR="$ROOT/bin/state"
|
||||||
export SWW_STATE_DIR
|
export SWWAF_STATE_DIR
|
||||||
mkdir -p "$SWW_STATE_DIR"
|
mkdir -p "$SWWAF_STATE_DIR"
|
||||||
fi
|
fi
|
||||||
if [ -z "${SWW_RULES_DIR+set}" ]; then
|
if [ -z "${SWWAF_RULES_DIR+set}" ]; then
|
||||||
SWW_RULES_DIR="$ROOT/share/rules.d"
|
SWWAF_RULES_DIR="$ROOT/share/rules.d"
|
||||||
export SWW_RULES_DIR
|
export SWWAF_RULES_DIR
|
||||||
fi
|
fi
|
||||||
SWW_LISTEN_ADDR="${SWW_LISTEN_ADDR:-:8090}"
|
SWWAF_LISTEN_ADDR="${SWWAF_LISTEN_ADDR:-:8090}"
|
||||||
SWRP_WAF_BACKEND="${SWRP_WAF_BACKEND:-http://127.0.0.1:${SWW_LISTEN_ADDR##*:}}"
|
SWRP_WAF_BACKEND="${SWRP_WAF_BACKEND:-http://127.0.0.1:${SWWAF_LISTEN_ADDR##*:}}"
|
||||||
export SWW_LISTEN_ADDR SWRP_WAF_BACKEND
|
export SWWAF_LISTEN_ADDR SWRP_WAF_BACKEND
|
||||||
|
|
||||||
trap stop_both EXIT
|
trap stop_both EXIT
|
||||||
trap 'exit 1' HUP INT TERM
|
trap 'exit 1' HUP INT TERM
|
||||||
|
|||||||
+7
-7
@@ -24,7 +24,7 @@ main() {
|
|||||||
# give_state_dir_to_sww gives the state directory and every file in
|
# give_state_dir_to_sww gives the state directory and every file in
|
||||||
# it to the sww user, so that a volume mounted there needs no change
|
# it to the sww user, so that a volume mounted there needs no change
|
||||||
# of owner; chown -R changes a symbolic link itself, never what it points
|
# of owner; chown -R changes a symbolic link itself, never what it points
|
||||||
# to. When SWW_STATE_DIR, in either form, cannot be read or is not the
|
# to. When SWWAF_STATE_DIR, in either form, cannot be read or is not the
|
||||||
# absolute path of a directory, no owner is changed, and discriminator starts and
|
# absolute path of a directory, no owner is changed, and discriminator starts and
|
||||||
# stops with a message naming the setting. A change of owner that fails, as
|
# stops with a message naming the setting. A change of owner that fails, as
|
||||||
# on a volume mounted read-only, does not end the script either: discriminator
|
# on a volume mounted read-only, does not end the script either: discriminator
|
||||||
@@ -36,14 +36,14 @@ give_state_dir_to_sww() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# state_dir prints SWW_STATE_DIR, /var/lib/sww when it is unset,
|
# state_dir prints SWWAF_STATE_DIR, /var/lib/sww when it is unset,
|
||||||
# or what is in the file SWW_STATE_DIR_FILE names, less every newline at
|
# or what is in the file SWWAF_STATE_DIR_FILE names, less every newline at
|
||||||
# its end. It fails when both are set or the file cannot be read.
|
# its end. It fails when both are set or the file cannot be read.
|
||||||
state_dir() {
|
state_dir() {
|
||||||
if [[ ! -v SWW_STATE_DIR_FILE ]]; then
|
if [[ ! -v SWWAF_STATE_DIR_FILE ]]; then
|
||||||
printf '%s\n' "${SWW_STATE_DIR-/var/lib/sww}"
|
printf '%s\n' "${SWWAF_STATE_DIR-/var/lib/sww}"
|
||||||
elif [[ ! -v SWW_STATE_DIR ]]; then
|
elif [[ ! -v SWWAF_STATE_DIR ]]; then
|
||||||
cat -- "$SWW_STATE_DIR_FILE" 2>/dev/null
|
cat -- "$SWWAF_STATE_DIR_FILE" 2>/dev/null
|
||||||
else
|
else
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in new issue
Block a user