Compare commits
1
Commits
9a5782de68
...
92ed102595
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92ed102595 |
No files matched your search
@@ -221,7 +221,7 @@ 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. |
|
||||||
@@ -311,7 +311,7 @@ Every setting the code reads, with its default:
|
|||||||
| `SWW_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. |
|
||||||
| `SWW_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`. |
|
||||||
| `SWW_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. |
|
||||||
| `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. |
|
| `SWW_ALERT_EVENTS` | `ban,permanent_ban,core_rule_set_match,anomaly,reputation_hit,source_failure,file_error` | The events alerts are sent for; the hourly `summary` is sent whatever it says. |
|
||||||
| `SWW_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`. |
|
||||||
| `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`. |
|
| `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`. |
|
||||||
| `SWW_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. |
|
||||||
@@ -469,7 +469,7 @@ services:
|
|||||||
# 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
|
SWW_ALERT_NTFY_URL: https://ntfy.example/smallwebwaf
|
||||||
SWW_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy
|
SWW_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy
|
||||||
SWW_ALERT_EVENTS: ban,permanent_ban,waf_block,source_failure,file_error
|
SWW_ALERT_EVENTS: ban,permanent_ban,core_rule_set_match,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
|
||||||
@@ -632,9 +632,10 @@ is set aside as `<name>.bad`, and never stops the running process (see
|
|||||||
- `internal/rproxy`: the reverse proxy in front of one app.
|
- `internal/rproxy`: the reverse proxy in front of one app.
|
||||||
- `internal/discriminator`: `discriminator`'s server, which runs the checks and
|
- `internal/discriminator`: `discriminator`'s server, which runs the checks and
|
||||||
answers its own endpoints.
|
answers its own endpoints.
|
||||||
- `internal/ratelimit`, `internal/bans`, `internal/rules`, `internal/waf`,
|
- `internal/ratelimit`, `internal/bans`, `internal/rules`,
|
||||||
`internal/lookup`, `internal/reputation`, `internal/routewatch`,
|
`internal/coreruleset`, `internal/lookup`, `internal/reputation`,
|
||||||
`internal/anomaly` and `internal/alerts`: one check or source each.
|
`internal/routewatch`, `internal/anomaly` and `internal/alerts`: one check or
|
||||||
|
source each.
|
||||||
- `internal/state`, `internal/requestlog`, `internal/remotelog` and
|
- `internal/state`, `internal/requestlog`, `internal/remotelog` and
|
||||||
`internal/metrics`: the state files, the log lines, the syslog sender and the
|
`internal/metrics`: the state files, the log lines, the syslog sender and the
|
||||||
metrics.
|
metrics.
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ to them.
|
|||||||
of bans is full.
|
of bans is full.
|
||||||
- `internal/rules`: reads the rule files at start and again when they
|
- `internal/rules`: reads the rule files at start and again when they
|
||||||
change, and tells which rules a request matches.
|
change, and tells which rules a request matches.
|
||||||
- `internal/waf`: the Core Rule Set, with the eight changes listed under
|
- `internal/coreruleset`: the Core Rule Set, with the eight changes listed
|
||||||
"Attack detection", as Coraza directives.
|
under "Attack detection", as Coraza directives.
|
||||||
- `internal/lookup`: looks clients up through GeoJS or in the lookup
|
- `internal/lookup`: looks clients up through GeoJS or in the lookup
|
||||||
database.
|
database.
|
||||||
- `internal/reputation`: fetches the lists, asks the DNS blocklists and
|
- `internal/reputation`: fetches the lists, asks the DNS blocklists and
|
||||||
@@ -655,7 +655,7 @@ files, the limits and the bans still apply to the client that sends it.
|
|||||||
match, and scores are still logged. A request with more than 1000 query
|
match, and scores are still logged. A request with more than 1000 query
|
||||||
parameters or body fields adds 5 to its score, since Coraza reads only the
|
parameters or body fields adds 5 to its score, since Coraza reads only the
|
||||||
first 1000. For a match, in `block` and `detect` mode alike, the log line
|
first 1000. For a match, in `block` and `detect` mode alike, the log line
|
||||||
gives the rule ids and the score, and a `waf_block` alert is sent.
|
gives the rule ids and the score, and a `core_rule_set_match` alert is sent.
|
||||||
- `SWW_WAF_DISABLED_RULES` switches off rule ids when an app trips a false
|
- `SWW_WAF_DISABLED_RULES` switches off rule ids when an app trips a false
|
||||||
positive. The default switches off six rules:
|
positive. The default switches off six rules:
|
||||||
- 920340, 920420 and 920640 refuse a request for the type of its body, when
|
- 920340, 920420 and 920640 refuse a request for the type of its body, when
|
||||||
@@ -789,8 +789,8 @@ every app, and no setting undoes them:
|
|||||||
such as `owner/python3-app` would have that push refused.
|
such as `owner/python3-app` would have that push refused.
|
||||||
8. On gitea's paths, some fields skip the rules that refused ordinary gitea
|
8. 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
|
requests there. They are the fields where gitea sends a search, a file name
|
||||||
or a branch name. `internal/waf/gitea.conf` lists each rule and the request
|
or a branch name. `internal/coreruleset/gitea.conf` lists each rule and the
|
||||||
it refused. The fields are:
|
request it refused. The fields are:
|
||||||
- `q` on gitea's searches of code, of issues and pull requests, and of
|
- `q` on gitea's searches of code, of issues and pull requests, and of
|
||||||
commits. A code search holds code, such as `<script>`,
|
commits. A code search holds code, such as `<script>`,
|
||||||
`SELECT * FROM users WHERE` or `../`.
|
`SELECT * FROM users WHERE` or `../`.
|
||||||
@@ -1378,11 +1378,11 @@ these fields, named as the request log's fields of the same meaning:
|
|||||||
- `request_bytes` and `response_bytes`: the body bytes each way, with what a
|
- `request_bytes` and `response_bytes`: the body bytes each way, with what a
|
||||||
WebSocket carried each way, each a whole number from `0` to `1125899906842624`
|
WebSocket carried each way, each a whole number from `0` to `1125899906842624`
|
||||||
(1 PiB);
|
(1 PiB);
|
||||||
- `duration_total`: milliseconds from the request's headers being read to the
|
- `duration_total_ms`: milliseconds from the request's headers being read to the
|
||||||
end of the answer, from `0` to `1000000000000` (about 31 years);
|
end of the answer, from `0` to `1000000000000` (about 31 years);
|
||||||
- `duration_upstream_total`: milliseconds from the hand-over to the app to the
|
- `duration_upstream_total_ms`: milliseconds from the hand-over to the app to
|
||||||
end of the answer, in the same range, and `0` for a request not passed to the
|
the end of the answer, in the same range, and `0` for a request not passed to
|
||||||
app.
|
the app.
|
||||||
|
|
||||||
`discriminator` answers `400` and writes a process warning when the body:
|
`discriminator` answers `400` and writes a process warning when the body:
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -9,9 +9,9 @@ back or dropped.
|
|||||||
- `ban`: a ban `discriminator` made.
|
- `ban`: a ban `discriminator` made.
|
||||||
- `permanent_ban`: a permanent ban it made, or an existing ban made permanent by
|
- `permanent_ban`: a permanent ban it made, or an existing ban made permanent by
|
||||||
a request.
|
a request.
|
||||||
- `waf_block`: the Core Rule Set scored a request at or over the threshold. The
|
- `core_rule_set_match`: the Core Rule Set scored a request at or over the
|
||||||
request was refused unless the alert's `detail` gives `mode`: `detect` or
|
threshold. The request was refused unless the alert's `detail` gives `mode`:
|
||||||
`observe`.
|
`detect` or `observe`.
|
||||||
- `anomaly`: a count over an anomaly threshold.
|
- `anomaly`: a count over an anomaly threshold.
|
||||||
- `reputation_hit`: a request from a client on a blocklist, the CrowdSec
|
- `reputation_hit`: a request from a client on a blocklist, the CrowdSec
|
||||||
decision list or a DNS blocklist, or with an AbuseIPDB score that is a hit.
|
decision list or a DNS blocklist, or with an AbuseIPDB score that is a hit.
|
||||||
@@ -130,7 +130,7 @@ The priority and tag by event:
|
|||||||
|
|
||||||
- `ban`: priority `default`, tag `no_entry`.
|
- `ban`: priority `default`, tag `no_entry`.
|
||||||
- `permanent_ban`: priority `high`, tag `no_entry`.
|
- `permanent_ban`: priority `high`, tag `no_entry`.
|
||||||
- `waf_block`: priority `default`, tag `shield`.
|
- `core_rule_set_match`: priority `default`, tag `shield`.
|
||||||
- `anomaly`: priority `high`, tag `chart_with_upwards_trend`.
|
- `anomaly`: priority `high`, tag `chart_with_upwards_trend`.
|
||||||
- `reputation_hit`: priority `low`, tag `label`.
|
- `reputation_hit`: priority `low`, tag `label`.
|
||||||
- `source_failure` and `file_error`: priority `high`, tag `warning`.
|
- `source_failure` and `file_error`: priority `high`, tag `warning`.
|
||||||
|
|||||||
+6
-5
@@ -28,7 +28,8 @@ body bytes, never headers, whatever `SWW_BYTES_COUNT` says.
|
|||||||
## Attack detection
|
## Attack detection
|
||||||
|
|
||||||
- `sww_rule_matches_total`, by `rule_id` and `action`, and `sww_rules_loaded`;
|
- `sww_rule_matches_total`, by `rule_id` and `action`, and `sww_rules_loaded`;
|
||||||
- `sww_waf_matches_total`, by `mode` and `rule_id`, for the rules that fired.
|
- `sww_core_rule_set_matches_total`, by `mode` and `rule_id`, for the rules that
|
||||||
|
fired.
|
||||||
|
|
||||||
## Countries and AS numbers
|
## Countries and AS numbers
|
||||||
|
|
||||||
@@ -43,8 +44,8 @@ For the `SWW_METRICS_TOP_N` busiest of each, the rest as `other`:
|
|||||||
## Lookups
|
## Lookups
|
||||||
|
|
||||||
- `sww_geojs_requests_total` and `sww_geojs_failures_total`;
|
- `sww_geojs_requests_total` and `sww_geojs_failures_total`;
|
||||||
- `sww_geojs_unanswered_total`: the requests that needed an answer and went on
|
- `sww_requests_without_geojs_answer_total`: the requests that needed an answer
|
||||||
without it;
|
and went on without it;
|
||||||
- with the file source, `sww_lookup_database_last_read_timestamp_seconds` and
|
- with the file source, `sww_lookup_database_last_read_timestamp_seconds` and
|
||||||
`sww_lookup_database_read_failures_total`.
|
`sww_lookup_database_read_failures_total`.
|
||||||
|
|
||||||
@@ -70,8 +71,8 @@ By `file`:
|
|||||||
## Remote log, alerts and the process
|
## Remote log, alerts and the process
|
||||||
|
|
||||||
- while a syslog server is set, `sww_remote_log_lines_sent_total`,
|
- while a syslog server is set, `sww_remote_log_lines_sent_total`,
|
||||||
`sww_remote_log_lines_dropped_total` and `sww_remote_log_buffer_depth`;
|
`sww_remote_log_lines_dropped_total` and `sww_remote_log_lines_waiting`;
|
||||||
- by `destination`, `sww_alerts_sent_total`, `sww_alerts_failed_total`,
|
- by `destination`, `sww_alerts_sent_total`, `sww_alert_send_failures_total`,
|
||||||
`sww_alerts_suppressed_total` and `sww_alerts_dropped_total`;
|
`sww_alerts_suppressed_total` and `sww_alerts_dropped_total`;
|
||||||
- Go's own `go_` metrics and the process's `process_` metrics.
|
- Go's own `go_` metrics and the process's `process_` metrics.
|
||||||
|
|
||||||
|
|||||||
+23
-22
@@ -26,7 +26,7 @@ These questions write no line:
|
|||||||
A `report` line:
|
A `report` line:
|
||||||
|
|
||||||
```
|
```
|
||||||
{"type":"request","discriminator_line":"report","time":"2026-10-03T12:00:00.123Z","instance":"fsn1/discriminator","client_ip":"203.0.113.9","method":"GET","scheme":"https","host":"app.example","path":"/","query":"","protocol":"HTTP/1.1","status":200,"request_bytes":0,"response_bytes":5120,"referer":"","user_agent":"curl/8.9.1","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","peer_ip":"172.18.0.2","forwarded_for":"203.0.113.9","client_group":"203.0.113.9/32","asn":"AS64496","as_name":"Example Net","country":"DE","request_headers":{"accept":"*/*"},"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"waf_score":0,"duration_total":3.217,"duration_checks":0.041,"duration_waf":0.029,"duration_upstream_total":3.104}
|
{"type":"request","discriminator_line":"report","time":"2026-10-03T12:00:00.123Z","instance":"fsn1/discriminator","client_ip":"203.0.113.9","method":"GET","scheme":"https","host":"app.example","path":"/","query":"","protocol":"HTTP/1.1","status":200,"request_bytes":0,"response_bytes":5120,"referer":"","user_agent":"curl/8.9.1","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","peer_ip":"172.18.0.2","forwarded_for":"203.0.113.9","client_group":"203.0.113.9/32","asn":"AS64496","as_name":"Example Net","country":"DE","request_headers":{"accept":"*/*"},"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"core_rule_set_score":0,"duration_total_ms":3.217,"duration_checks_ms":0.041,"duration_core_rule_set_ms":0.029,"duration_upstream_total_ms":3.104}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Every line
|
### Every line
|
||||||
@@ -37,7 +37,7 @@ Every one of these lines has:
|
|||||||
- the standard web log fields;
|
- the standard web log fields;
|
||||||
- `request_id`, `peer_ip` and `client_group`;
|
- `request_id`, `peer_ip` and `client_group`;
|
||||||
- `asn`, `as_name` and `country`;
|
- `asn`, `as_name` and `country`;
|
||||||
- `action` and `duration_total`.
|
- `action` and `duration_total_ms`.
|
||||||
|
|
||||||
### Standard web log fields
|
### Standard web log fields
|
||||||
|
|
||||||
@@ -103,10 +103,10 @@ Every one of these lines has:
|
|||||||
- `permanently_banned`: refused under a ban that never runs out, including a
|
- `permanently_banned`: refused under a ban that never runs out, including a
|
||||||
ban this request made or made permanent;
|
ban this request made or made permanent;
|
||||||
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
|
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
|
||||||
`waf_blocked`: refused by that check;
|
`blocked_by_core_rule_set`: refused by that check;
|
||||||
- `request_header_too_large`: over a header limit;
|
- `request_header_too_large`: over a header limit;
|
||||||
- `admin`: answered at one of `discriminator`'s own endpoints, or `404` for
|
- `own_endpoint`: answered at one of `discriminator`'s own endpoints, or
|
||||||
a path under `/_sww/`.
|
`404` for a path under `/_sww/`.
|
||||||
- `would_action`: in `observe` mode, what `enforce` mode would have done with a
|
- `would_action`: in `observe` mode, what `enforce` mode would have done with a
|
||||||
request it would have refused. `action` then names what was done, normally
|
request it would have refused. `action` then names what was done, normally
|
||||||
`forward`.
|
`forward`.
|
||||||
@@ -140,9 +140,10 @@ Every one of these lines has:
|
|||||||
`byte_limits` bans when the report comes, so only the `report` line names
|
`byte_limits` bans when the report comes, so only the `report` line names
|
||||||
it.
|
it.
|
||||||
|
|
||||||
- `limit_percent` and `limit_percent_setting`: the client's percentage of the
|
- `rate_limit_percent` and `rate_limit_percent_setting`: the client's percentage
|
||||||
rate limits and the setting that lowered it, when one did. `bytes_percent` and
|
of the rate limits and the setting that lowered it, when one did.
|
||||||
`bytes_percent_setting` do the same for the byte limits.
|
`bytes_limit_percent` and `bytes_limit_percent_setting` do the same for the
|
||||||
|
byte limits.
|
||||||
- `counts`: the client's totals as rate limiting counts them, this request
|
- `counts`: the client's totals as rate limiting counts them, this request
|
||||||
included. It is left out when rate limiting did not count the request. It
|
included. It is left out when rate limiting did not count the request. It
|
||||||
holds:
|
holds:
|
||||||
@@ -158,7 +159,7 @@ Every one of these lines has:
|
|||||||
- `limit_hit`: the limit broken:
|
- `limit_hit`: the limit broken:
|
||||||
- `minute`, `hour` or `day` for a rate limit;
|
- `minute`, `hour` or `day` for a rate limit;
|
||||||
- `minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit;
|
- `minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit;
|
||||||
- `error_burst`.
|
- `minute_refusals` for the error burst (`SWW_ERROR_BURST_THRESHOLD`).
|
||||||
|
|
||||||
When the bytes counted for a request and its response break a limit, the
|
When the bytes counted for a request and its response break a limit, the
|
||||||
request has been passed on whole, so its `action` stays `forward`. Only its
|
request has been passed on whole, so its `action` stays `forward`. Only its
|
||||||
@@ -166,8 +167,8 @@ Every one of these lines has:
|
|||||||
|
|
||||||
- `rule_ids`: the rule file rules that matched, in order, up to the one that
|
- `rule_ids`: the rule file rules that matched, in order, up to the one that
|
||||||
refused.
|
refused.
|
||||||
- `waf_score` and `waf_rule_ids`: the Core Rule Set's score and the rules that
|
- `core_rule_set_score` and `core_rule_set_rule_ids`: the Core Rule Set's score
|
||||||
matched, for a request it inspected.
|
and the rules that matched, for a request it inspected.
|
||||||
- `reputation`: the sources the client is on, in this order:
|
- `reputation`: the sources the client is on, in this order:
|
||||||
- blocklist URLs;
|
- blocklist URLs;
|
||||||
- the CrowdSec decision list's URL;
|
- the CrowdSec decision list's URL;
|
||||||
@@ -189,14 +190,14 @@ Every one of these lines has:
|
|||||||
|
|
||||||
All in milliseconds:
|
All in milliseconds:
|
||||||
|
|
||||||
- `duration_total`:
|
- `duration_total_ms`:
|
||||||
- on the `decision` line, from `discriminator` having read the question's
|
- on the `decision` line, from `discriminator` having read the question's
|
||||||
body to the line;
|
body to the line;
|
||||||
- on the `report` line, from `rproxy` having read the request's headers to
|
- on the `report` line, from `rproxy` having read the request's headers to
|
||||||
the end of the answer.
|
the end of the answer.
|
||||||
- `duration_checks`: to the end of the checks;
|
- `duration_checks_ms`: to the end of the checks;
|
||||||
- `duration_waf`: the part of that the Core Rule Set took;
|
- `duration_core_rule_set_ms`: the part of that the Core Rule Set took;
|
||||||
- `duration_upstream_total`: on the `report` line of an allowed request, from
|
- `duration_upstream_total_ms`: on the `report` line of an allowed request, from
|
||||||
the hand-over to the app to the end of the answer.
|
the hand-over to the app to the end of the answer.
|
||||||
|
|
||||||
### Requests to its own endpoints
|
### Requests to its own endpoints
|
||||||
@@ -209,8 +210,8 @@ these meanings where they differ:
|
|||||||
- `scheme`: `http`.
|
- `scheme`: `http`.
|
||||||
- `time`: when the request's headers had been read.
|
- `time`: when the request's headers had been read.
|
||||||
- `request_bytes`: the bytes read of a ban to add, and otherwise `0`.
|
- `request_bytes`: the bytes read of a ban to add, and otherwise `0`.
|
||||||
- `action`: `admin`.
|
- `action`: `own_endpoint`.
|
||||||
- `duration_total`: from the request's headers being read to the line.
|
- `duration_total_ms`: from the request's headers being read to the line.
|
||||||
|
|
||||||
## `rproxy`'s line
|
## `rproxy`'s line
|
||||||
|
|
||||||
@@ -228,10 +229,10 @@ these meanings where they differ:
|
|||||||
a WebSocket carried from the visitor.
|
a WebSocket carried from the visitor.
|
||||||
- `response_bytes`: the bytes of the answer's body, with what a WebSocket
|
- `response_bytes`: the bytes of the answer's body, with what a WebSocket
|
||||||
carried from the app.
|
carried from the app.
|
||||||
- `duration_request`: milliseconds from the request's headers being read until
|
- `duration_request_ms`: milliseconds from the request's headers being read
|
||||||
the app had been sent the whole request, or the answer started if that was
|
until the app had been sent the whole request, or the answer started if that
|
||||||
sooner. It includes the wait for `discriminator`.
|
was sooner. It includes the wait for `discriminator`.
|
||||||
- `duration_response`: milliseconds from then until the end of the answer.
|
- `duration_response_ms`: milliseconds from then until the end of the answer.
|
||||||
- `disposition`: what was done with the request: `allow`, `deny` or `close`. It
|
- `disposition`: what was done with the request: `allow`, `deny` or `close`. It
|
||||||
is `discriminator`'s answer, or the value of `SWRP_DEFAULT_DISPOSITION`. It is
|
is `discriminator`'s answer, or the value of `SWRP_DEFAULT_DISPOSITION`. It is
|
||||||
left out when:
|
left out when:
|
||||||
@@ -245,5 +246,5 @@ these meanings where they differ:
|
|||||||
For example:
|
For example:
|
||||||
|
|
||||||
```
|
```
|
||||||
{"type":"request","time":"2026-10-03T12:00:00.123Z","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","client_ip":"203.0.113.9","host":"app.example","method":"GET","path":"/","query":"","status":200,"request_bytes":0,"response_bytes":5120,"duration_request":1.204,"duration_response":3.104,"disposition":"allow"}
|
{"type":"request","time":"2026-10-03T12:00:00.123Z","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","client_ip":"203.0.113.9","host":"app.example","method":"GET","path":"/","query":"","status":200,"request_bytes":0,"response_bytes":5120,"duration_request_ms":1.204,"duration_response_ms":3.104,"disposition":"allow"}
|
||||||
```
|
```
|
||||||
+1
-1
@@ -324,7 +324,7 @@ the request first.
|
|||||||
- `SWW_ALERT_NTFY_TOKEN` (default unset): an ntfy access token. A control
|
- `SWW_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
|
- `SWW_ALERT_EVENTS` (default
|
||||||
`ban,permanent_ban,waf_block,anomaly,reputation_hit,source_failure,file_error`):
|
`ban,permanent_ban,core_rule_set_match,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
|
- `SWW_ALERT_COOLDOWN` (default `15m`): how long after an alert is sent its
|
||||||
|
|||||||
+2
-2
@@ -64,8 +64,8 @@ since it was first seen:
|
|||||||
- body bytes of its requests, and of the responses it was sent;
|
- body bytes of its requests, and of the responses it was sent;
|
||||||
- responses by status class;
|
- responses by status class;
|
||||||
- offences by kind: `limit`, `attack`, `blocked_by_block_rule` and
|
- offences by kind: `limit`, `attack`, `blocked_by_block_rule` and
|
||||||
`waf_blocked`. A file may also hold `token_refused`, which is kept, and which
|
`blocked_by_core_rule_set`. A file may also hold `token_refused`, which is
|
||||||
nothing adds to.
|
kept, and which nothing adds to.
|
||||||
|
|
||||||
## `lookups.json`
|
## `lookups.json`
|
||||||
|
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ const (
|
|||||||
// EventAnomaly is a count of requests or bytes over an anomaly
|
// EventAnomaly is a count of requests or bytes over an anomaly
|
||||||
// threshold.
|
// threshold.
|
||||||
EventAnomaly = "anomaly"
|
EventAnomaly = "anomaly"
|
||||||
// EventWAFBlock is a request the Core Rule Set scored at or over
|
// EventCoreRuleSetMatch is a request the Core Rule Set scored at or over
|
||||||
// SWW_WAF_ANOMALY_THRESHOLD, refused in block mode, let through in
|
// SWW_WAF_ANOMALY_THRESHOLD, refused in block mode, let through in
|
||||||
// detect mode.
|
// detect mode.
|
||||||
EventWAFBlock = "waf_block"
|
EventCoreRuleSetMatch = "core_rule_set_match"
|
||||||
// EventReputationHit is a request whose client a blocklist, the
|
// EventReputationHit is a request whose client a blocklist, the
|
||||||
// CrowdSec decision list or a DNSBL zone lists, or whose AbuseIPDB score
|
// CrowdSec decision list or a DNSBL zone lists, or whose AbuseIPDB score
|
||||||
// is a hit.
|
// is a hit.
|
||||||
@@ -72,7 +72,7 @@ const (
|
|||||||
// default.
|
// default.
|
||||||
func Events() []string {
|
func Events() []string {
|
||||||
return []string{
|
return []string{
|
||||||
EventBan, EventPermanentBan, EventWAFBlock, EventAnomaly,
|
EventBan, EventPermanentBan, EventCoreRuleSetMatch, EventAnomaly,
|
||||||
EventReputationHit, EventSourceFailure, EventFileError,
|
EventReputationHit, EventSourceFailure, EventFileError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -871,7 +871,7 @@ func ntfyPriority(event string) string {
|
|||||||
return "high"
|
return "high"
|
||||||
case EventReputationHit:
|
case EventReputationHit:
|
||||||
return "low"
|
return "low"
|
||||||
default: // ban, waf_block and summary
|
default: // ban, core_rule_set_match and summary
|
||||||
return "default"
|
return "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -882,7 +882,7 @@ func ntfyTag(event string) string {
|
|||||||
switch event {
|
switch event {
|
||||||
case EventBan, EventPermanentBan:
|
case EventBan, EventPermanentBan:
|
||||||
return "no_entry"
|
return "no_entry"
|
||||||
case EventWAFBlock:
|
case EventCoreRuleSetMatch:
|
||||||
return "shield"
|
return "shield"
|
||||||
case EventAnomaly:
|
case EventAnomaly:
|
||||||
return "chart_with_upwards_trend"
|
return "chart_with_upwards_trend"
|
||||||
|
|||||||
@@ -1281,12 +1281,12 @@ func anAlertForEachEvent() []eventMessage {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
alerts.Alert{
|
alerts.Alert{
|
||||||
Event: alerts.EventWAFBlock, Client: netip.MustParseAddr("192.0.2.1"),
|
Event: alerts.EventCoreRuleSetMatch, Client: netip.MustParseAddr("192.0.2.1"),
|
||||||
Netblock: netip.MustParsePrefix("192.0.2.1/32"),
|
Netblock: netip.MustParsePrefix("192.0.2.1/32"),
|
||||||
Reason: "refused by the Core Rule Set",
|
Reason: "refused by the Core Rule Set",
|
||||||
Host: "app.example",
|
Host: "app.example",
|
||||||
},
|
},
|
||||||
"fsn1app1/blog: waf_block", "default shield",
|
"fsn1app1/blog: core_rule_set_match", "default shield",
|
||||||
"refused by the Core Rule Set\nhost: app.example\nclient: 192.0.2.1\n" +
|
"refused by the Core Rule Set\nhost: app.example\nclient: 192.0.2.1\n" +
|
||||||
"netblock: 192.0.2.1/32",
|
"netblock: 192.0.2.1/32",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ const (
|
|||||||
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.
|
// ScopeWatchedNet is a named netblock of SWW_WATCH_NETS.
|
||||||
ScopeWatch = "watch"
|
ScopeWatchedNet = "watched_net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Scopes returns every scope.
|
// Scopes returns every scope.
|
||||||
func Scopes() []string {
|
func Scopes() []string {
|
||||||
return []string{ScopeClient, ScopeNet, ScopeASN, ScopeTotal, ScopeWatch}
|
return []string{ScopeClient, ScopeNet, ScopeASN, ScopeTotal, ScopeWatchedNet}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The windows a counter counts in, as the alerts name them.
|
// The windows a counter counts in, as the alerts name them.
|
||||||
@@ -257,7 +257,7 @@ func (c *Counters) scopesOf(r Request) []scope {
|
|||||||
for _, named := range p.NamedNetblocks {
|
for _, named := range p.NamedNetblocks {
|
||||||
if named.Netblock.Contains(client) {
|
if named.Netblock.Contains(client) {
|
||||||
all = append(all, scope{
|
all = append(all, scope{
|
||||||
key{scope: ScopeWatch, netblock: named.Netblock, name: named.Name}, p.Watch,
|
key{scope: ScopeWatchedNet, netblock: named.Netblock, name: named.Name}, p.Watch,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-16
@@ -13,7 +13,7 @@ func TestBanWithoutACauseIsAnAdmins(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
netblock := netip.MustParsePrefix("203.0.113.0/24")
|
netblock := netip.MustParsePrefix("203.0.113.0/24")
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
ledger.Load([]bans.Ban{{Netblock: netblock, Start: midnight()}})
|
ledger.Load([]bans.Ban{{Netblock: netblock, Start: midnight()}})
|
||||||
|
|
||||||
if got := ledger.Bans(netblock)[0].Cause; got != bans.CauseAdmin {
|
if got := ledger.Bans(netblock)[0].Cause; got != bans.CauseAdmin {
|
||||||
@@ -24,9 +24,9 @@ func TestBanWithoutACauseIsAnAdmins(t *testing.T) {
|
|||||||
func TestAdminsBansAreNeverDroppedAndDoNotCountTowardMaxBans(t *testing.T) {
|
func TestAdminsBansAreNeverDroppedAndDoNotCountTowardMaxBans(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
rules := defaultRules()
|
params := defaultParams()
|
||||||
rules.MaxBans = 1
|
params.MaxBans = 1
|
||||||
ledger := bans.New(rules)
|
ledger := bans.New(params)
|
||||||
adminsOnly := netip.MustParsePrefix("198.51.100.0/24")
|
adminsOnly := netip.MustParsePrefix("198.51.100.0/24")
|
||||||
both := netip.MustParsePrefix("203.0.113.1/32")
|
both := netip.MustParsePrefix("203.0.113.1/32")
|
||||||
second := netip.MustParsePrefix("203.0.113.2/32")
|
second := netip.MustParsePrefix("203.0.113.2/32")
|
||||||
@@ -64,9 +64,9 @@ func TestAdminsBansAreNeverDroppedAndDoNotCountTowardMaxBans(t *testing.T) {
|
|||||||
func TestEarlierBansCountABanDroppedAfterAnAdminsBan(t *testing.T) {
|
func TestEarlierBansCountABanDroppedAfterAnAdminsBan(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
rules := defaultRules()
|
params := defaultParams()
|
||||||
rules.MaxBans = 2
|
params.MaxBans = 2
|
||||||
ledger := bans.New(rules)
|
ledger := bans.New(params)
|
||||||
netblock := netip.MustParsePrefix("203.0.113.1/32")
|
netblock := netip.MustParsePrefix("203.0.113.1/32")
|
||||||
other := netip.MustParsePrefix("203.0.113.2/32")
|
other := netip.MustParsePrefix("203.0.113.2/32")
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ func TestEarlierBansCountABanDroppedAfterAnAdminsBan(t *testing.T) {
|
|||||||
func TestEarlierBansCountABanAnAdminKeepsOnce(t *testing.T) {
|
func TestEarlierBansCountABanAnAdminKeepsOnce(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.1/32")
|
netblock := netip.MustParsePrefix("203.0.113.1/32")
|
||||||
|
|
||||||
// The netblock breaks a limit twice, the second time after its first
|
// The netblock breaks a limit twice, the second time after its first
|
||||||
@@ -117,7 +117,7 @@ func TestEarlierBansCountABanAnAdminKeepsOnce(t *testing.T) {
|
|||||||
func TestReasonOfTheBansSwwMakes(t *testing.T) {
|
func TestReasonOfTheBansSwwMakes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
|
|
||||||
limit, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.1/32"), midnight(),
|
limit, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.1/32"), midnight(),
|
||||||
bans.Notes{Kind: "requests", Limit: 1000, Window: "minute"})
|
bans.Notes{Kind: "requests", Limit: 1000, Window: "minute"})
|
||||||
@@ -150,7 +150,7 @@ func TestLiftedBanForALimitRefusesNothingAndMakesNoBanLonger(t *testing.T) {
|
|||||||
Lifted: midnight().Add(10 * time.Minute),
|
Lifted: midnight().Add(10 * time.Minute),
|
||||||
}
|
}
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
ledger.Load([]bans.Ban{lifted})
|
ledger.Load([]bans.Ban{lifted})
|
||||||
|
|
||||||
// While it would still last, it refuses nothing, and a limit broken
|
// While it would still last, it refuses nothing, and a limit broken
|
||||||
@@ -181,7 +181,7 @@ func TestLiftedBanForAnAttackRefusesNothingAndMakesNoBanLonger(t *testing.T) {
|
|||||||
|
|
||||||
// A permanent ban for a clear sign of attack, lifted.
|
// A permanent ban for a clear sign of attack, lifted.
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
ledger.Load([]bans.Ban{{
|
ledger.Load([]bans.Ban{{
|
||||||
Netblock: netblock,
|
Netblock: netblock,
|
||||||
Start: midnight(),
|
Start: midnight(),
|
||||||
@@ -211,7 +211,7 @@ func TestLiftedBanForAnAttackRefusesNothingAndMakesNoBanLonger(t *testing.T) {
|
|||||||
func TestLoadEditCountsTheBansAnAdminMade(t *testing.T) {
|
func TestLoadEditCountsTheBansAnAdminMade(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
made, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.1/32"), midnight(),
|
made, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.1/32"), midnight(),
|
||||||
bans.Notes{})
|
bans.Notes{})
|
||||||
atStart := bans.Ban{
|
atStart := bans.Ban{
|
||||||
@@ -246,7 +246,7 @@ func TestAdminsBanIsMadeWhileAnotherLasts(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
netblock := netip.MustParsePrefix("203.0.113.0/24")
|
netblock := netip.MustParsePrefix("203.0.113.0/24")
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
|
|
||||||
// An hour's ban for a broken limit.
|
// An hour's ban for a broken limit.
|
||||||
ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
||||||
@@ -292,7 +292,7 @@ func TestLiftLiftsEveryActiveBanCoveringTheClient(t *testing.T) {
|
|||||||
wide := netip.MustParsePrefix("203.0.113.0/24")
|
wide := netip.MustParsePrefix("203.0.113.0/24")
|
||||||
other := netip.MustParsePrefix("203.0.113.10/32")
|
other := netip.MustParsePrefix("203.0.113.10/32")
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
ledger.Load([]bans.Ban{
|
ledger.Load([]bans.Ban{
|
||||||
// Ended an hour ago.
|
// Ended an hour ago.
|
||||||
{
|
{
|
||||||
@@ -346,7 +346,7 @@ func TestPermanentBanOnAWiderNetblockIsReturnedOverATemporaryBan(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
client := netip.MustParseAddr("203.0.113.9")
|
client := netip.MustParseAddr("203.0.113.9")
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
|
|
||||||
// The temporary ban is made first, so that its netblock is looked at first.
|
// The temporary ban is made first, so that its netblock is looked at first.
|
||||||
ledger.BanForLimit(netip.MustParsePrefix("203.0.113.9/32"), midnight(), bans.Notes{})
|
ledger.BanForLimit(netip.MustParsePrefix("203.0.113.9/32"), midnight(), bans.Notes{})
|
||||||
@@ -360,7 +360,7 @@ func TestOlderPermanentBanIsReturnedOverANewerTemporaryBan(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
netblock := netip.MustParsePrefix("203.0.113.0/24")
|
netblock := netip.MustParsePrefix("203.0.113.0/24")
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
|
|
||||||
ledger.BanForAdmin(netblock, midnight(), time.Time{}, "for good")
|
ledger.BanForAdmin(netblock, midnight(), time.Time{}, "for good")
|
||||||
ledger.BanForAdmin(netblock, midnight().Add(time.Minute), midnight().Add(time.Hour),
|
ledger.BanForAdmin(netblock, midnight().Add(time.Minute), midnight().Add(time.Hour),
|
||||||
|
|||||||
+12
-12
@@ -40,8 +40,8 @@ const repeatFactor = 3
|
|||||||
// maxTextBytes is how much of each text in a ban's notes is kept.
|
// maxTextBytes is how much of each text in a ban's notes is kept.
|
||||||
const maxTextBytes = 256
|
const maxTextBytes = 256
|
||||||
|
|
||||||
// Rules are how long a ban lasts, and how many bans are held.
|
// Params are how long a ban lasts, and how many bans are held.
|
||||||
type Rules struct {
|
type Params struct {
|
||||||
// LimitBanDuration is how long a first ban for a broken limit lasts.
|
// LimitBanDuration is how long a first ban for a broken limit lasts.
|
||||||
LimitBanDuration time.Duration
|
LimitBanDuration time.Duration
|
||||||
// LimitBanRepeatWindow is how soon after the end of the netblock's
|
// LimitBanRepeatWindow is how soon after the end of the netblock's
|
||||||
@@ -180,7 +180,7 @@ type Request struct {
|
|||||||
|
|
||||||
// Ledger holds the bans. It is safe for concurrent use.
|
// Ledger holds the bans. It is safe for concurrent use.
|
||||||
type Ledger struct {
|
type Ledger struct {
|
||||||
rules Rules
|
params Params
|
||||||
// changed receives a value when a ban is made, unless one is waiting
|
// changed receives a value when a ban is made, unless one is waiting
|
||||||
// already.
|
// already.
|
||||||
changed chan struct{}
|
changed chan struct{}
|
||||||
@@ -190,7 +190,7 @@ type Ledger struct {
|
|||||||
// Find make each netblock they find the most recently seen.
|
// Find make each netblock they find the most recently seen.
|
||||||
netblocks *simplelru.LRU[netip.Prefix, *[]Ban]
|
netblocks *simplelru.LRU[netip.Prefix, *[]Ban]
|
||||||
// held is how many bans netblocks holds whose cause is not CauseAdmin,
|
// held is how many bans netblocks holds whose cause is not CauseAdmin,
|
||||||
// at most rules.MaxBans.
|
// at most params.MaxBans.
|
||||||
held int
|
held int
|
||||||
// made is how many bans have been made since the start, by cause: by
|
// made is how many bans have been made since the start, by cause: by
|
||||||
// the ledger, and by an admin, through BanForAdmin or in an edit of
|
// the ledger, and by an admin, through BanForAdmin or in an edit of
|
||||||
@@ -205,7 +205,7 @@ type Ledger struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns a Ledger with no ban yet.
|
// New returns a Ledger with no ban yet.
|
||||||
func New(rules Rules) *Ledger {
|
func New(params Params) *Ledger {
|
||||||
// The ledger drops bans itself, and never those whose cause is
|
// The ledger drops bans itself, and never those whose cause is
|
||||||
// CauseAdmin, however many there are, so the LRU has no limit of its
|
// CauseAdmin, however many there are, so the LRU has no limit of its
|
||||||
// own: it keeps the netblocks in the order they were last seen.
|
// own: it keeps the netblocks in the order they were last seen.
|
||||||
@@ -215,7 +215,7 @@ func New(rules Rules) *Ledger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Ledger{
|
return &Ledger{
|
||||||
rules: rules,
|
params: params,
|
||||||
changed: make(chan struct{}, 1),
|
changed: make(chan struct{}, 1),
|
||||||
netblocks: netblocks,
|
netblocks: netblocks,
|
||||||
made: map[string]int{},
|
made: map[string]int{},
|
||||||
@@ -812,7 +812,7 @@ func (l *Ledger) covering(client netip.Addr) []*[]Ban {
|
|||||||
// unless ban's cause is CauseAdmin, which does not count toward MaxBans.
|
// unless ban's cause is CauseAdmin, which does not count toward MaxBans.
|
||||||
func (l *Ledger) add(ban Ban) {
|
func (l *Ledger) add(ban Ban) {
|
||||||
counted := ban.Cause != CauseAdmin
|
counted := ban.Cause != CauseAdmin
|
||||||
if counted && l.held == l.rules.MaxBans {
|
if counted && l.held == l.params.MaxBans {
|
||||||
l.dropOne()
|
l.dropOne()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -847,7 +847,7 @@ func (l *Ledger) add(ban Ban) {
|
|||||||
// new ban longer. A ban an admin adds to bans.json can start after
|
// new ban longer. A ban an admin adds to bans.json can start after
|
||||||
// another and end before it, so that one is looked for among them all.
|
// another and end before it, so that one is looked for among them all.
|
||||||
func (l *Ledger) limitExpiry(held []Ban, now time.Time) time.Time {
|
func (l *Ledger) limitExpiry(held []Ban, now time.Time) time.Time {
|
||||||
length := l.rules.LimitBanDuration
|
length := l.params.LimitBanDuration
|
||||||
|
|
||||||
var last *Ban
|
var last *Ban
|
||||||
|
|
||||||
@@ -858,18 +858,18 @@ func (l *Ledger) limitExpiry(held []Ban, now time.Time) time.Time {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if last != nil && now.Sub(last.Expires) <= l.rules.LimitBanRepeatWindow {
|
if last != nil && now.Sub(last.Expires) <= l.params.LimitBanRepeatWindow {
|
||||||
lastLength := last.Expires.Sub(last.Start)
|
lastLength := last.Expires.Sub(last.Start)
|
||||||
// This is repeatFactor * lastLength > MaxBanDuration, written so
|
// This is repeatFactor * lastLength > MaxBanDuration, written so
|
||||||
// that it cannot overflow.
|
// that it cannot overflow.
|
||||||
if lastLength > l.rules.MaxBanDuration/repeatFactor {
|
if lastLength > l.params.MaxBanDuration/repeatFactor {
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
length = repeatFactor * lastLength
|
length = repeatFactor * lastLength
|
||||||
}
|
}
|
||||||
|
|
||||||
if length > l.rules.MaxBanDuration {
|
if length > l.params.MaxBanDuration {
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,7 +887,7 @@ func (l *Ledger) attackExpiry(held []Ban, now time.Time) time.Time {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return now.Add(l.rules.AttackBanDuration)
|
return now.Add(l.params.AttackBanDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dropOne drops the earliest ban whose cause is not CauseAdmin of the
|
// dropOne drops the earliest ban whose cause is not CauseAdmin of the
|
||||||
|
|||||||
+27
-27
@@ -15,7 +15,7 @@ const day = 24 * time.Hour
|
|||||||
func TestRepeatsTripleUntilPermanent(t *testing.T) {
|
func TestRepeatsTripleUntilPermanent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
now := midnight()
|
now := midnight()
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ func TestRepeatWindowRunsOut(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
|
|
||||||
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
||||||
@@ -80,9 +80,9 @@ func TestRepeatWindowRunsOut(t *testing.T) {
|
|||||||
func TestFirstBanLongerThanTheMaximumIsPermanent(t *testing.T) {
|
func TestFirstBanLongerThanTheMaximumIsPermanent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
rules := defaultRules()
|
params := defaultParams()
|
||||||
rules.LimitBanDuration = rules.MaxBanDuration + time.Hour
|
params.LimitBanDuration = params.MaxBanDuration + time.Hour
|
||||||
ledger := bans.New(rules)
|
ledger := bans.New(params)
|
||||||
|
|
||||||
ban, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.9/32"), midnight(),
|
ban, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.9/32"), midnight(),
|
||||||
bans.Notes{})
|
bans.Notes{})
|
||||||
@@ -97,9 +97,9 @@ func TestLongestBanSetFarOffDoesNotOverflow(t *testing.T) {
|
|||||||
// With bans of up to 100,000 days, the 14th ban in a row, of 3^13
|
// With bans of up to 100,000 days, the 14th ban in a row, of 3^13
|
||||||
// hours, is within the maximum, and three times as long would not fit
|
// hours, is within the maximum, and three times as long would not fit
|
||||||
// in a time.Duration. The 15th is permanent.
|
// in a time.Duration. The 15th is permanent.
|
||||||
rules := defaultRules()
|
params := defaultParams()
|
||||||
rules.MaxBanDuration = 100000 * day
|
params.MaxBanDuration = 100000 * day
|
||||||
ledger := bans.New(rules)
|
ledger := bans.New(params)
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
now := midnight()
|
now := midnight()
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ func TestLongestBanSetFarOffDoesNotOverflow(t *testing.T) {
|
|||||||
func TestBrokenLimitDuringABanMakesNoOther(t *testing.T) {
|
func TestBrokenLimitDuringABanMakesNoOther(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
|
|
||||||
first, made := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
first, made := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
||||||
@@ -146,7 +146,7 @@ func TestBrokenLimitDuringABanMakesNoOther(t *testing.T) {
|
|||||||
func TestCheckRefusesWhileTheBanLastsAndCountsTheRefusals(t *testing.T) {
|
func TestCheckRefusesWhileTheBanLastsAndCountsTheRefusals(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Requests: 5})
|
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Requests: 5})
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ func TestCheckRefusesWhileTheBanLastsAndCountsTheRefusals(t *testing.T) {
|
|||||||
func TestFindCountsNothing(t *testing.T) {
|
func TestFindCountsNothing(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Requests: 5})
|
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Requests: 5})
|
||||||
|
|
||||||
@@ -200,9 +200,9 @@ func TestFindCountsNothing(t *testing.T) {
|
|||||||
func TestMaxBansDropsTheEarliestBanOfTheNetblockSeenLongestAgo(t *testing.T) {
|
func TestMaxBansDropsTheEarliestBanOfTheNetblockSeenLongestAgo(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
rules := defaultRules()
|
params := defaultParams()
|
||||||
rules.MaxBans = 3
|
params.MaxBans = 3
|
||||||
ledger := bans.New(rules)
|
ledger := bans.New(params)
|
||||||
a := netip.MustParsePrefix("203.0.113.1/32")
|
a := netip.MustParsePrefix("203.0.113.1/32")
|
||||||
b := netip.MustParsePrefix("203.0.113.2/32")
|
b := netip.MustParsePrefix("203.0.113.2/32")
|
||||||
c := netip.MustParsePrefix("203.0.113.3/32")
|
c := netip.MustParsePrefix("203.0.113.3/32")
|
||||||
@@ -239,9 +239,9 @@ func TestFullLedgerDropsTheEarlierBanOfTheNetblockBannedAgain(t *testing.T) {
|
|||||||
|
|
||||||
// With room for one ban, the netblock's ended ban goes to make room for
|
// With room for one ban, the netblock's ended ban goes to make room for
|
||||||
// its new one, whose notes still count it.
|
// its new one, whose notes still count it.
|
||||||
rules := defaultRules()
|
params := defaultParams()
|
||||||
rules.MaxBans = 1
|
params.MaxBans = 1
|
||||||
ledger := bans.New(rules)
|
ledger := bans.New(params)
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
|
|
||||||
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
||||||
@@ -258,7 +258,7 @@ func TestFullLedgerDropsTheEarlierBanOfTheNetblockBannedAgain(t *testing.T) {
|
|||||||
func TestRequestDuringAnAttackBanMakesItPermanent(t *testing.T) {
|
func TestRequestDuringAnAttackBanMakesItPermanent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
notes := bans.Notes{RuleID: "env-file", Target: "path"}
|
notes := bans.Notes{RuleID: "env-file", Target: "path"}
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ func TestRequestDuringAnAttackBanMakesItPermanent(t *testing.T) {
|
|||||||
func TestAttackAfterAnAttackBanHasEndedBansPermanently(t *testing.T) {
|
func TestAttackAfterAnAttackBanHasEndedBansPermanently(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
|
|
||||||
// A ban for a broken limit before does not count.
|
// A ban for a broken limit before does not count.
|
||||||
@@ -336,7 +336,7 @@ func TestAttackAfterAnAttackBanHasEndedBansPermanently(t *testing.T) {
|
|||||||
func TestWouldBanGivesTheBanWithoutMakingIt(t *testing.T) {
|
func TestWouldBanGivesTheBanWithoutMakingIt(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
||||||
wantChanged(t, ledger, true)
|
wantChanged(t, ledger, true)
|
||||||
@@ -380,7 +380,7 @@ func TestWouldBanGivesTheBanWithoutMakingIt(t *testing.T) {
|
|||||||
func TestWouldBePermanentAnswersAsTheBanWouldBeMade(t *testing.T) {
|
func TestWouldBePermanentAnswersAsTheBanWouldBeMade(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
now := midnight()
|
now := midnight()
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@ func TestWouldBePermanentAnswersAsTheBanWouldBeMade(t *testing.T) {
|
|||||||
func TestAttackBanDoesNotLengthenTheNextBanForALimit(t *testing.T) {
|
func TestAttackBanDoesNotLengthenTheNextBanForALimit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
|
|
||||||
// Three times the seven days would be permanent; a limit broken as the
|
// Three times the seven days would be permanent; a limit broken as the
|
||||||
@@ -436,7 +436,7 @@ func TestAttackBanDoesNotLengthenTheNextBanForALimit(t *testing.T) {
|
|||||||
func TestRequestTextsAreCutTo256Bytes(t *testing.T) {
|
func TestRequestTextsAreCutTo256Bytes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
long := strings.Repeat("a", 300)
|
long := strings.Repeat("a", 300)
|
||||||
request := bans.Request{
|
request := bans.Request{
|
||||||
@@ -458,7 +458,7 @@ func TestRequestTextsAreCutTo256Bytes(t *testing.T) {
|
|||||||
func TestLookupFillsTheNotesOfTheNetblocksBansWithoutOne(t *testing.T) {
|
func TestLookupFillsTheNotesOfTheNetblocksBansWithoutOne(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
other := netip.MustParsePrefix("198.51.100.7/32")
|
other := netip.MustParsePrefix("198.51.100.7/32")
|
||||||
|
|
||||||
@@ -494,9 +494,9 @@ func TestLookupFillsTheNotesOfTheNetblocksBansWithoutOne(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultRules are the rules at the settings' defaults.
|
// defaultParams are the params at the settings' defaults.
|
||||||
func defaultRules() bans.Rules {
|
func defaultParams() bans.Params {
|
||||||
return bans.Rules{
|
return bans.Params{
|
||||||
LimitBanDuration: time.Hour,
|
LimitBanDuration: time.Hour,
|
||||||
LimitBanRepeatWindow: day,
|
LimitBanRepeatWindow: day,
|
||||||
MaxBanDuration: 7 * day,
|
MaxBanDuration: 7 * day,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const scenario = "crowdsecurity/ssh-bf"
|
|||||||
func TestCrowdSecBanLastsUntilTheDecisionEnds(t *testing.T) {
|
func TestCrowdSecBanLastsUntilTheDecisionEnds(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
expires := midnight().Add(4 * time.Hour)
|
expires := midnight().Add(4 * time.Hour)
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ func TestCrowdSecBanLastsUntilTheDecisionEnds(t *testing.T) {
|
|||||||
func TestCrowdSecBanIsNeverMadePermanent(t *testing.T) {
|
func TestCrowdSecBanIsNeverMadePermanent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
expires := midnight().Add(4 * time.Hour)
|
expires := midnight().Add(4 * time.Hour)
|
||||||
ledger.BanForCrowdSec(netblock, midnight(), expires, scenario, bans.Notes{})
|
ledger.BanForCrowdSec(netblock, midnight(), expires, scenario, bans.Notes{})
|
||||||
@@ -72,7 +72,7 @@ func TestCrowdSecBanIsNeverMadePermanent(t *testing.T) {
|
|||||||
func TestCrowdSecBanIsCountedAndDoesNotLengthenTheNextBanForALimit(t *testing.T) {
|
func TestCrowdSecBanIsCountedAndDoesNotLengthenTheNextBanForALimit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
|
|
||||||
// Three times the three days would be permanent; a limit broken as the
|
// Three times the three days would be permanent; a limit broken as the
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
func TestChangedAfterABanIsMade(t *testing.T) {
|
func TestChangedAfterABanIsMade(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
|
|
||||||
wantChanged(t, ledger, false)
|
wantChanged(t, ledger, false)
|
||||||
@@ -38,7 +38,7 @@ func TestChangedAfterABanIsMade(t *testing.T) {
|
|||||||
func TestSnapshotListsEveryBanByNetblock(t *testing.T) {
|
func TestSnapshotListsEveryBanByNetblock(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
v6 := netip.MustParsePrefix("2001:db8::/64")
|
v6 := netip.MustParsePrefix("2001:db8::/64")
|
||||||
high := netip.MustParsePrefix("203.0.113.10/32")
|
high := netip.MustParsePrefix("203.0.113.10/32")
|
||||||
low := netip.MustParsePrefix("203.0.113.9/32")
|
low := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
@@ -67,14 +67,14 @@ func TestSnapshotListsEveryBanByNetblock(t *testing.T) {
|
|||||||
func TestLoadedBansCarryOn(t *testing.T) {
|
func TestLoadedBansCarryOn(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
before := bans.New(defaultRules())
|
before := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
ban, _ := before.BanForLimit(netblock, midnight(), bans.Notes{Limit: 1})
|
ban, _ := before.BanForLimit(netblock, midnight(), bans.Notes{Limit: 1})
|
||||||
|
|
||||||
// Loaded into a new ledger, as across a restart, the ban still refuses
|
// Loaded into a new ledger, as across a restart, the ban still refuses
|
||||||
// while it lasts, and once it has ended a broken limit bans for three
|
// while it lasts, and once it has ended a broken limit bans for three
|
||||||
// times as long, with the loaded ban counted among the earlier ones.
|
// times as long, with the loaded ban counted among the earlier ones.
|
||||||
after := bans.New(defaultRules())
|
after := bans.New(defaultParams())
|
||||||
after.Load(before.Snapshot())
|
after.Load(before.Snapshot())
|
||||||
|
|
||||||
_, banned, _ := after.Check(netblock.Addr(), ban.Expires.Add(-time.Second))
|
_, banned, _ := after.Check(netblock.Addr(), ban.Expires.Add(-time.Second))
|
||||||
@@ -96,7 +96,7 @@ func TestLoadedBanRefusesEveryClientInItsNetblock(t *testing.T) {
|
|||||||
// Two entries as an admin might write them, with addresses not masked
|
// Two entries as an admin might write them, with addresses not masked
|
||||||
// to their lengths, the IPv6 one shorter than the /64 an IPv6 client's
|
// to their lengths, the IPv6 one shorter than the /64 an IPv6 client's
|
||||||
// ban covers, beside a ban the ledger makes on one IPv4 address.
|
// ban covers, beside a ban the ledger makes on one IPv4 address.
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
ledger.Load([]bans.Ban{
|
ledger.Load([]bans.Ban{
|
||||||
{Netblock: netip.MustParsePrefix("203.0.113.9/24"), Start: midnight()},
|
{Netblock: netip.MustParsePrefix("203.0.113.9/24"), Start: midnight()},
|
||||||
{Netblock: netip.MustParsePrefix("2001:db8::1/48"), Start: midnight()},
|
{Netblock: netip.MustParsePrefix("2001:db8::1/48"), Start: midnight()},
|
||||||
@@ -145,7 +145,7 @@ func TestPermanentBanStartedBeforeAnEndedOneRefuses(t *testing.T) {
|
|||||||
Expires: midnight().Add(time.Hour),
|
Expires: midnight().Add(time.Hour),
|
||||||
}
|
}
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
ledger.Load([]bans.Ban{permanent, ended})
|
ledger.Load([]bans.Ban{permanent, ended})
|
||||||
|
|
||||||
now := midnight().Add(2 * time.Hour)
|
now := midnight().Add(2 * time.Hour)
|
||||||
@@ -189,7 +189,7 @@ func TestNextBanWorkedOutFromTheBanThatEndedLast(t *testing.T) {
|
|||||||
Expires: midnight().Add(2 * time.Hour),
|
Expires: midnight().Add(2 * time.Hour),
|
||||||
}
|
}
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
ledger.Load([]bans.Ban{nineHours, admins})
|
ledger.Load([]bans.Ban{nineHours, admins})
|
||||||
|
|
||||||
// Once both have ended, a limit broken within the repeat window bans
|
// Once both have ended, a limit broken within the repeat window bans
|
||||||
@@ -219,9 +219,9 @@ func TestLoadKeepsAtMostMaxBansDroppingTheEarliest(t *testing.T) {
|
|||||||
Cause: bans.CauseLimit,
|
Cause: bans.CauseLimit,
|
||||||
}
|
}
|
||||||
|
|
||||||
rules := defaultRules()
|
params := defaultParams()
|
||||||
rules.MaxBans = 1
|
params.MaxBans = 1
|
||||||
ledger := bans.New(rules)
|
ledger := bans.New(params)
|
||||||
ledger.Load([]bans.Ban{later, earlier})
|
ledger.Load([]bans.Ban{later, earlier})
|
||||||
|
|
||||||
held := ledger.Snapshot()
|
held := ledger.Snapshot()
|
||||||
@@ -235,9 +235,9 @@ func TestLoadReplacesTheBansHeld(t *testing.T) {
|
|||||||
|
|
||||||
// Room for three bans, so that the second load, were it added to the
|
// Room for three bans, so that the second load, were it added to the
|
||||||
// two bans held, would drop none of them to make room.
|
// two bans held, would drop none of them to make room.
|
||||||
rules := defaultRules()
|
params := defaultParams()
|
||||||
rules.MaxBans = 3
|
params.MaxBans = 3
|
||||||
ledger := bans.New(rules)
|
ledger := bans.New(params)
|
||||||
kept := bans.Ban{
|
kept := bans.Ban{
|
||||||
Netblock: netip.MustParsePrefix("2001:db8::/64"),
|
Netblock: netip.MustParsePrefix("2001:db8::/64"),
|
||||||
Start: midnight(),
|
Start: midnight(),
|
||||||
@@ -285,7 +285,7 @@ func TestLoadCutsTheTextsTo256Bytes(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
ledger := bans.New(defaultRules())
|
ledger := bans.New(defaultParams())
|
||||||
ledger.Load([]bans.Ban{ban})
|
ledger.Load([]bans.Ban{ban})
|
||||||
|
|
||||||
cut := long[:256]
|
cut := long[:256]
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ const (
|
|||||||
maxParanoiaLevel = 4
|
maxParanoiaLevel = 4
|
||||||
// firstSetupRuleID to lastSetupRuleID are the ids the Core Rule Set
|
// firstSetupRuleID to lastSetupRuleID are the ids the Core Rule Set
|
||||||
// keeps for the rules that set it up, which sww's own rules
|
// keeps for the rules that set it up, which sww's own rules
|
||||||
// have too (see internal/waf). Switching one off would undo a change
|
// have too (see internal/coreruleset). Switching one off would undo a change
|
||||||
// that no setting undoes.
|
// that no setting undoes.
|
||||||
firstSetupRuleID = 900000
|
firstSetupRuleID = 900000
|
||||||
lastSetupRuleID = 900999
|
lastSetupRuleID = 900999
|
||||||
@@ -432,7 +432,7 @@ var (
|
|||||||
"holds a control character, such as the carriage return of a Windows line end",
|
"holds a control character, such as the carriage return of a Windows line end",
|
||||||
)
|
)
|
||||||
errNotAlertEvent = errors.New(
|
errNotAlertEvent = errors.New(
|
||||||
"is not ban, permanent_ban, waf_block, anomaly, reputation_hit, " +
|
"is not ban, permanent_ban, core_rule_set_match, anomaly, reputation_hit, " +
|
||||||
"source_failure or file_error")
|
"source_failure or file_error")
|
||||||
errNotNumberOrOff = errors.New(
|
errNotNumberOrOff = errors.New(
|
||||||
"is not a whole number above zero, such as 60, or off",
|
"is not a whole number above zero, such as 60, or off",
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ func loggedAnomalyDefaults() map[string]string {
|
|||||||
// defaultAlertEvents is the default of SWW_ALERT_EVENTS, and
|
// defaultAlertEvents is the default of SWW_ALERT_EVENTS, and
|
||||||
// defaultAlertCooldown that of SWW_ALERT_COOLDOWN.
|
// defaultAlertCooldown that of SWW_ALERT_COOLDOWN.
|
||||||
const (
|
const (
|
||||||
defaultAlertEvents = "ban,permanent_ban,waf_block,anomaly,reputation_hit," +
|
defaultAlertEvents = "ban,permanent_ban,core_rule_set_match,anomaly,reputation_hit," +
|
||||||
"source_failure,file_error"
|
"source_failure,file_error"
|
||||||
defaultAlertCooldown = "15m"
|
defaultAlertCooldown = "15m"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package waf runs the OWASP Core Rule Set 4.25.0, through Coraza, on the
|
// Package coreruleset 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 SWW_WAF_BODY_LIMIT is set, with the eight changes sww
|
||||||
// makes to it and the exclusions in SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as
|
// makes to it and the exclusions in SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
// 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
|
||||||
// file in a multipart body, Coraza then counts the bytes instead of
|
// file in a multipart body, Coraza then counts the bytes instead of
|
||||||
// writing them to the system's temporary directory.
|
// writing them to the system's temporary directory.
|
||||||
package waf
|
package coreruleset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package coreruleset_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultDisabledRules are the rules SWW_WAF_DISABLED_RULES switches off
|
// defaultDisabledRules are the rules SWW_WAF_DISABLED_RULES switches off
|
||||||
@@ -23,10 +23,12 @@ var defaultDisabledRules = []int{920340, 920420, 920440, 920640, 930130, 930140}
|
|||||||
|
|
||||||
// newCoreRuleSet returns the Core Rule Set at paranoia level level, with
|
// newCoreRuleSet returns the Core Rule Set at paranoia level level, with
|
||||||
// the rules in disabled switched off.
|
// the rules in disabled switched off.
|
||||||
func newCoreRuleSet(t *testing.T, level int, disabled ...int) *waf.CoreRuleSet {
|
func newCoreRuleSet(t *testing.T, level int, disabled ...int) *coreruleset.CoreRuleSet {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
crs, err := waf.New(waf.Params{ParanoiaLevel: level, DisabledRules: disabled})
|
crs, err := coreruleset.New(coreruleset.Params{
|
||||||
|
ParanoiaLevel: level, DisabledRules: disabled,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("load the Core Rule Set: %v", err)
|
t.Fatalf("load the Core Rule Set: %v", err)
|
||||||
}
|
}
|
||||||
@@ -49,7 +51,7 @@ func get(target string, headers ...string) request {
|
|||||||
|
|
||||||
// inspect returns what crs finds in r, sent to git.example by a browser,
|
// inspect returns what crs finds in r, sent to git.example by a browser,
|
||||||
// whose Host, User-Agent and Accept r.headers may replace.
|
// whose Host, User-Agent and Accept r.headers may replace.
|
||||||
func inspect(t *testing.T, crs *waf.CoreRuleSet, r request) waf.Result {
|
func inspect(t *testing.T, crs *coreruleset.CoreRuleSet, r request) coreruleset.Result {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
result, _ := inspectBody(t, crs, r, "")
|
result, _ := inspectBody(t, crs, r, "")
|
||||||
@@ -60,8 +62,8 @@ func inspect(t *testing.T, crs *waf.CoreRuleSet, r request) waf.Result {
|
|||||||
// inspectBody is inspect for r with body, which is announced with its
|
// inspectBody is inspect for r with body, which is announced with its
|
||||||
// Content-Length unless it is "", and returns what crs read of body too.
|
// Content-Length unless it is "", and returns what crs read of body too.
|
||||||
func inspectBody(
|
func inspectBody(
|
||||||
t *testing.T, crs *waf.CoreRuleSet, r request, body string,
|
t *testing.T, crs *coreruleset.CoreRuleSet, r request, body string,
|
||||||
) (waf.Result, string) {
|
) (coreruleset.Result, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
req := httptest.NewRequestWithContext(t.Context(), r.method,
|
req := httptest.NewRequestWithContext(t.Context(), r.method,
|
||||||
@@ -96,7 +98,9 @@ func inspectBody(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wantResult checks what crs finds in r.
|
// wantResult checks what crs finds in r.
|
||||||
func wantResult(t *testing.T, crs *waf.CoreRuleSet, r request, want waf.Result) {
|
func wantResult(
|
||||||
|
t *testing.T, crs *coreruleset.CoreRuleSet, r request, want coreruleset.Result,
|
||||||
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if got := inspect(t, crs, r); !reflect.DeepEqual(got, want) {
|
if got := inspect(t, crs, r); !reflect.DeepEqual(got, want) {
|
||||||
@@ -106,14 +110,14 @@ func wantResult(t *testing.T, crs *waf.CoreRuleSet, r request, want waf.Result)
|
|||||||
|
|
||||||
// matched is the result of a request that the rules ids match, each of
|
// matched is the result of a request that the rules ids match, each of
|
||||||
// them a critical one, which adds 5 to the score.
|
// them a critical one, which adds 5 to the score.
|
||||||
func matched(ids ...int) waf.Result {
|
func matched(ids ...int) coreruleset.Result {
|
||||||
const critical = 5
|
const critical = 5
|
||||||
|
|
||||||
return waf.Result{RuleIDs: ids, Score: critical * len(ids)}
|
return coreruleset.Result{RuleIDs: ids, Score: critical * len(ids)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// atDefaults returns the Core Rule Set as sww runs it by default.
|
// atDefaults returns the Core Rule Set as sww runs it by default.
|
||||||
func atDefaults(t *testing.T) *waf.CoreRuleSet {
|
func atDefaults(t *testing.T) *coreruleset.CoreRuleSet {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return newCoreRuleSet(t, 1, defaultDisabledRules...)
|
return newCoreRuleSet(t, 1, defaultDisabledRules...)
|
||||||
@@ -123,11 +127,12 @@ func atDefaults(t *testing.T) *waf.CoreRuleSet {
|
|||||||
// the eight changes is for, and still finds result in refused, a request
|
// the eight 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 *coreruleset.CoreRuleSet, passes, refused request,
|
||||||
|
result coreruleset.Result,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
wantResult(t, crs, passes, waf.Result{})
|
wantResult(t, crs, passes, coreruleset.Result{})
|
||||||
wantResult(t, crs, refused, result)
|
wantResult(t, crs, refused, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,12 +167,12 @@ func TestExpectAndContentEncodingAreAllowed(t *testing.T) {
|
|||||||
|
|
||||||
wantResult(t, crs,
|
wantResult(t, crs,
|
||||||
request{http.MethodPost, push, []string{pushType, length, "Expect: 100-continue"}},
|
request{http.MethodPost, push, []string{pushType, length, "Expect: 100-continue"}},
|
||||||
waf.Result{})
|
coreruleset.Result{})
|
||||||
wantResult(t, crs,
|
wantResult(t, crs,
|
||||||
request{http.MethodPost, fetch, []string{
|
request{http.MethodPost, fetch, []string{
|
||||||
fetchType, length, "Content-Encoding: gzip",
|
fetchType, length, "Content-Encoding: gzip",
|
||||||
}},
|
}},
|
||||||
waf.Result{})
|
coreruleset.Result{})
|
||||||
|
|
||||||
// Every other header on the Core Rule Set's list stays refused.
|
// Every other header on the Core Rule Set's list stays refused.
|
||||||
for _, header := range []string{
|
for _, header := range []string{
|
||||||
@@ -196,7 +201,7 @@ func TestTransferEncodingIsRead(t *testing.T) {
|
|||||||
"Content-Type: application/x-git-receive-pack-request",
|
"Content-Type: application/x-git-receive-pack-request",
|
||||||
"Transfer-Encoding: chunked",
|
"Transfer-Encoding: chunked",
|
||||||
}},
|
}},
|
||||||
waf.Result{})
|
coreruleset.Result{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMoreParametersThanCorazaKeepsIsAMatch(t *testing.T) {
|
func TestMoreParametersThanCorazaKeepsIsAMatch(t *testing.T) {
|
||||||
@@ -250,7 +255,7 @@ func TestParametersGiteaSendsNamesInSkipTheListsOfFilesPathsAndCommands(t *testi
|
|||||||
for _, value := range []struct {
|
for _, value := range []struct {
|
||||||
name string
|
name string
|
||||||
// result is what a parameter that is not one of gitea's gets.
|
// result is what a parameter that is not one of gitea's gets.
|
||||||
result waf.Result
|
result coreruleset.Result
|
||||||
}{
|
}{
|
||||||
// A file on the list of system files.
|
// A file on the list of system files.
|
||||||
{".gitignore", matched(930120)},
|
{".gitignore", matched(930120)},
|
||||||
@@ -271,7 +276,7 @@ func TestParametersGiteaSendsNamesInSkipTheListsOfFilesPathsAndCommands(t *testi
|
|||||||
wantChange(t, crs, get("/?path=|cat%20/etc/passwd"), get("/?q=|cat%20/etc/passwd"),
|
wantChange(t, crs, get("/?path=|cat%20/etc/passwd"), get("/?q=|cat%20/etc/passwd"),
|
||||||
matched(930120, 932160))
|
matched(930120, 932160))
|
||||||
wantResult(t, crs, get("/?path=../../etc/passwd"),
|
wantResult(t, crs, get("/?path=../../etc/passwd"),
|
||||||
waf.Result{RuleIDs: []int{930100, 930110}, Score: 20})
|
coreruleset.Result{RuleIDs: []int{930100, 930110}, Score: 20})
|
||||||
wantResult(t, crs, get("/?path=1'%20OR%20'1'='1"), matched(942100))
|
wantResult(t, crs, get("/?path=1'%20OR%20'1'='1"), matched(942100))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +346,7 @@ func TestEmptyHeaderIsRead(t *testing.T) {
|
|||||||
|
|
||||||
// An empty User-Agent is a notice, which adds 2.
|
// An empty User-Agent is a notice, which adds 2.
|
||||||
wantResult(t, atDefaults(t), get("/", "User-Agent: "),
|
wantResult(t, atDefaults(t), get("/", "User-Agent: "),
|
||||||
waf.Result{RuleIDs: []int{920330}, Score: 2})
|
coreruleset.Result{RuleIDs: []int{920330}, Score: 2})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParanoiaLevel(t *testing.T) {
|
func TestParanoiaLevel(t *testing.T) {
|
||||||
@@ -350,7 +355,7 @@ func TestParanoiaLevel(t *testing.T) {
|
|||||||
// Accept-Charset is refused from paranoia level 2.
|
// Accept-Charset is refused from paranoia level 2.
|
||||||
r := get("/", "Accept-Charset: utf-8")
|
r := get("/", "Accept-Charset: utf-8")
|
||||||
|
|
||||||
wantResult(t, newCoreRuleSet(t, 1), r, waf.Result{})
|
wantResult(t, newCoreRuleSet(t, 1), r, coreruleset.Result{})
|
||||||
wantResult(t, newCoreRuleSet(t, 2), r, matched(920451))
|
wantResult(t, newCoreRuleSet(t, 2), r, matched(920451))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,8 +367,8 @@ func TestEachDisabledRuleIsSwitchedOff(t *testing.T) {
|
|||||||
r := request{http.MethodTrace, "/", []string{"Host: 192.0.2.1"}}
|
r := request{http.MethodTrace, "/", []string{"Host: 192.0.2.1"}}
|
||||||
|
|
||||||
wantResult(t, newCoreRuleSet(t, 1), r,
|
wantResult(t, newCoreRuleSet(t, 1), r,
|
||||||
waf.Result{RuleIDs: []int{911100, 920350}, Score: 8})
|
coreruleset.Result{RuleIDs: []int{911100, 920350}, Score: 8})
|
||||||
wantResult(t, newCoreRuleSet(t, 1, 920350, 911100), r, waf.Result{})
|
wantResult(t, newCoreRuleSet(t, 1, 920350, 911100), r, coreruleset.Result{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImagePushThatReusesALayerIsNotCheckedForCommandNames(t *testing.T) {
|
func TestImagePushThatReusesALayerIsNotCheckedForCommandNames(t *testing.T) {
|
||||||
@@ -383,8 +388,8 @@ func TestImagePushThatReusesALayerIsNotCheckedForCommandNames(t *testing.T) {
|
|||||||
|
|
||||||
// withExclusions returns the Core Rule Set at the defaults, with the
|
// withExclusions returns the Core Rule Set at the defaults, with the
|
||||||
// exclusions file at path.
|
// exclusions file at path.
|
||||||
func withExclusions(path string) (*waf.CoreRuleSet, error) {
|
func withExclusions(path string) (*coreruleset.CoreRuleSet, error) {
|
||||||
return waf.New(waf.Params{
|
return coreruleset.New(coreruleset.Params{
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, ExclusionsFile: path,
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, ExclusionsFile: path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -422,7 +427,7 @@ func TestExclusionsFileIsLoadedBeforeTheRules(t *testing.T) {
|
|||||||
func TestRuleSwitchedOffAndNamedInTheExclusionsFileIsOff(t *testing.T) {
|
func TestRuleSwitchedOffAndNamedInTheExclusionsFileIsOff(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
crs, err := waf.New(waf.Params{
|
crs, err := coreruleset.New(coreruleset.Params{
|
||||||
ParanoiaLevel: 1, DisabledRules: []int{920500},
|
ParanoiaLevel: 1, DisabledRules: []int{920500},
|
||||||
ExclusionsFile: exclusionsFile(t, backupFileExclusion),
|
ExclusionsFile: exclusionsFile(t, backupFileExclusion),
|
||||||
})
|
})
|
||||||
@@ -430,7 +435,7 @@ func TestRuleSwitchedOffAndNamedInTheExclusionsFileIsOff(t *testing.T) {
|
|||||||
t.Fatalf("load the exclusions: %v", err)
|
t.Fatalf("load the exclusions: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantResult(t, crs, get("/notes.txt~"), waf.Result{})
|
wantResult(t, crs, get("/notes.txt~"), coreruleset.Result{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExclusionsFileMayNameAnyNumberInAComment(t *testing.T) {
|
func TestExclusionsFileMayNameAnyNumberInAComment(t *testing.T) {
|
||||||
@@ -566,10 +571,10 @@ const injection = "1' OR '1'='1"
|
|||||||
|
|
||||||
// readingBodies returns the Core Rule Set as sww runs it by
|
// readingBodies returns the Core Rule Set as sww runs it by
|
||||||
// default, but reading bodies up to bodyLimit.
|
// default, but reading bodies up to bodyLimit.
|
||||||
func readingBodies(t *testing.T) *waf.CoreRuleSet {
|
func readingBodies(t *testing.T) *coreruleset.CoreRuleSet {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
crs, err := waf.New(waf.Params{
|
crs, err := coreruleset.New(coreruleset.Params{
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit,
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -603,7 +608,8 @@ func padded(head, tail string, n int) string {
|
|||||||
// wantBody checks what crs finds in r with body, and that what it read of
|
// wantBody checks what crs finds in r with body, and that what it read of
|
||||||
// body is read.
|
// body is read.
|
||||||
func wantBody(
|
func wantBody(
|
||||||
t *testing.T, crs *waf.CoreRuleSet, r request, body string, want waf.Result,
|
t *testing.T, crs *coreruleset.CoreRuleSet, r request, body string,
|
||||||
|
want coreruleset.Result,
|
||||||
read string,
|
read string,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -627,7 +633,7 @@ func TestBodiesAreReadOnlyWhileBodyLimitIsSet(t *testing.T) {
|
|||||||
{jsonBody, `{"q":"` + injection + `"}`},
|
{jsonBody, `{"q":"` + injection + `"}`},
|
||||||
{xmlBody, "<q>" + injection + "</q>"},
|
{xmlBody, "<q>" + injection + "</q>"},
|
||||||
} {
|
} {
|
||||||
wantBody(t, off, post(tc.header), tc.body, waf.Result{}, "")
|
wantBody(t, off, post(tc.header), tc.body, coreruleset.Result{}, "")
|
||||||
wantBody(t, on, post(tc.header), tc.body, matched(942100), tc.body)
|
wantBody(t, on, post(tc.header), tc.body, matched(942100), tc.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -650,7 +656,7 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
wantBody(t, crs, post(tc.header), tc.attackFirst, matched(942100),
|
wantBody(t, crs, post(tc.header), tc.attackFirst, matched(942100),
|
||||||
tc.attackFirst[:bodyLimit+1])
|
tc.attackFirst[:bodyLimit+1])
|
||||||
wantBody(t, crs, post(tc.header), tc.attackLast, waf.Result{},
|
wantBody(t, crs, post(tc.header), tc.attackLast, coreruleset.Result{},
|
||||||
tc.attackLast[:bodyLimit+1])
|
tc.attackLast[:bodyLimit+1])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,7 +666,7 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
|
|||||||
wantBody(t, crs, post(formData), atLimit, matched(930120, 932160), atLimit)
|
wantBody(t, crs, post(formData), atLimit, matched(930120, 932160), atLimit)
|
||||||
|
|
||||||
pastLimit := padded("pad=", "&q=/etc/passwd", bodyLimit+1)
|
pastLimit := padded("pad=", "&q=/etc/passwd", bodyLimit+1)
|
||||||
wantBody(t, crs, post(formData), pastLimit, waf.Result{}, pastLimit)
|
wantBody(t, crs, post(formData), pastLimit, coreruleset.Result{}, pastLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJSONAndXMLAreReadOnlyWhenNoLargerThanTheLimit(t *testing.T) {
|
func TestJSONAndXMLAreReadOnlyWhenNoLargerThanTheLimit(t *testing.T) {
|
||||||
@@ -676,7 +682,7 @@ func TestJSONAndXMLAreReadOnlyWhenNoLargerThanTheLimit(t *testing.T) {
|
|||||||
wantBody(t, crs, post(tc.header), fits, matched(942100), fits)
|
wantBody(t, crs, post(tc.header), fits, matched(942100), fits)
|
||||||
|
|
||||||
larger := padded(tc.head, tc.tail, bodyLimit+1)
|
larger := padded(tc.head, tc.tail, bodyLimit+1)
|
||||||
wantBody(t, crs, post(tc.header), larger, waf.Result{}, larger)
|
wantBody(t, crs, post(tc.header), larger, coreruleset.Result{}, larger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,7 +700,7 @@ func TestOtherBodiesAreNotRead(t *testing.T) {
|
|||||||
"Content-Type: text/plain",
|
"Content-Type: text/plain",
|
||||||
"Content-Type: application/x-git-receive-pack-request",
|
"Content-Type: application/x-git-receive-pack-request",
|
||||||
} {
|
} {
|
||||||
wantBody(t, crs, post(header), body, waf.Result{}, "")
|
wantBody(t, crs, post(header), body, coreruleset.Result{}, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,8 +728,8 @@ func TestContentEncodingIsRefusedOnTheKindsOfBodyTheCoreRuleSetReads(t *testing.
|
|||||||
// It is allowed on a body of any other kind, and on every body while no
|
// It is allowed on a body of any other kind, and on every body while no
|
||||||
// body is read.
|
// body is read.
|
||||||
fetch := "Content-Type: application/x-git-upload-pack-request"
|
fetch := "Content-Type: application/x-git-upload-pack-request"
|
||||||
wantBody(t, crs, post(fetch, gzip), "a", waf.Result{}, "")
|
wantBody(t, crs, post(fetch, gzip), "a", coreruleset.Result{}, "")
|
||||||
wantBody(t, atDefaults(t), post(formData, gzip), "a", waf.Result{}, "")
|
wantBody(t, atDefaults(t), post(formData, gzip), "a", coreruleset.Result{}, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParametersGiteaSendsNamesInAreLeftOutAmongFormFieldsToo(t *testing.T) {
|
func TestParametersGiteaSendsNamesInAreLeftOutAmongFormFieldsToo(t *testing.T) {
|
||||||
@@ -734,18 +740,18 @@ func TestParametersGiteaSendsNamesInAreLeftOutAmongFormFieldsToo(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
body string
|
body string
|
||||||
want waf.Result
|
want coreruleset.Result
|
||||||
}{
|
}{
|
||||||
{"path=.gitignore", waf.Result{}},
|
{"path=.gitignore", coreruleset.Result{}},
|
||||||
{"q=.gitignore", matched(930120)},
|
{"q=.gitignore", matched(930120)},
|
||||||
{"redirect_uri=" + local, waf.Result{}},
|
{"redirect_uri=" + local, coreruleset.Result{}},
|
||||||
{"next=" + local, matched(931100, 934110)},
|
{"next=" + local, matched(931100, 934110)},
|
||||||
} {
|
} {
|
||||||
wantBody(t, crs, post(formData), tc.body, tc.want, tc.body)
|
wantBody(t, crs, post(formData), tc.body, tc.want, tc.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
body := field("path", ".gitignore") + end
|
body := field("path", ".gitignore") + end
|
||||||
wantBody(t, crs, post(multipart), body, waf.Result{}, body)
|
wantBody(t, crs, post(multipart), body, coreruleset.Result{}, body)
|
||||||
|
|
||||||
body = field("q", ".gitignore") + end
|
body = field("q", ".gitignore") + end
|
||||||
wantBody(t, crs, post(multipart), body, matched(930120), body)
|
wantBody(t, crs, post(multipart), body, matched(930120), body)
|
||||||
@@ -869,7 +875,7 @@ func TestCorazaWritesNoFile(t *testing.T) {
|
|||||||
func TestBodyLimitOf1GLoads(t *testing.T) {
|
func TestBodyLimitOf1GLoads(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, err := waf.New(waf.Params{ParanoiaLevel: 1, BodyLimit: 1 << 30})
|
_, err := coreruleset.New(coreruleset.Params{ParanoiaLevel: 1, BodyLimit: 1 << 30})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("load the Core Rule Set reading bodies up to 1G: %v", err)
|
t.Errorf("load the Core Rule Set reading bodies up to 1G: %v", err)
|
||||||
}
|
}
|
||||||
File renamed without changes.
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Ordinary gitea 1.27 requests that the Core Rule Set must let through at the
|
# 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
|
# 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
|
# space. The tests of internal/coreruleset inspect each as a GET. The Core Rule Set reads
|
||||||
# the query and the headers the same way for any method.
|
# the query and the headers the same way for any method.
|
||||||
|
|
||||||
# Searches with ordinary words and symbols.
|
# Searches with ordinary words and symbols.
|
||||||
@@ -42,7 +42,7 @@ var (
|
|||||||
|
|
||||||
// answerHealth answers a request for the health endpoint.
|
// answerHealth answers a request for the health endpoint.
|
||||||
func (rq *request) answerHealth() {
|
func (rq *request) answerHealth() {
|
||||||
rq.line.Action = requestlog.ActionAdmin
|
rq.line.Action = requestlog.ActionOwnEndpoint
|
||||||
// Set here rather than left to Go's server, which would set it only
|
// Set here rather than left to Go's server, which would set it only
|
||||||
// after the log line has taken the response's headers.
|
// after the log line has taken the response's headers.
|
||||||
rq.out.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
rq.out.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
@@ -55,7 +55,7 @@ func (rq *request) answerHealth() {
|
|||||||
// 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() {
|
||||||
rq.line.Action = requestlog.ActionAdmin
|
rq.line.Action = requestlog.ActionOwnEndpoint
|
||||||
|
|
||||||
token, answer := rq.endpoint()
|
token, answer := rq.endpoint()
|
||||||
|
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ func adminEndpoints() []adminEndpoint {
|
|||||||
|
|
||||||
// admin sends a request with method for path, with body, to discriminator's own
|
// admin sends a request with method for path, with body, to discriminator's own
|
||||||
// address, with the admin token, and checks that it is answered with status, its
|
// address, with the admin token, and checks that it is answered with status, its
|
||||||
// log line's action admin. It returns the answer.
|
// log line's action own_endpoint. It returns the answer.
|
||||||
func (s *sender) admin(method, path, body string, status int) answer {
|
func (s *sender) admin(method, path, body string, status int) answer {
|
||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ func (s *sender) adminRequest(
|
|||||||
|
|
||||||
line := s.out.endedLines(s.t, s.sent+1)[s.sent]
|
line := s.out.endedLines(s.t, s.sent+1)[s.sent]
|
||||||
s.sent++
|
s.sent++
|
||||||
wantLine(s.t, line, status, requestlog.ActionAdmin)
|
wantLine(s.t, line, status, requestlog.ActionOwnEndpoint)
|
||||||
|
|
||||||
return got
|
return got
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func TestEachScopeAndWindowOverItsThresholdAlertsOncePerCooldown(t *testing.T) {
|
|||||||
{anomalyASN, anomaly.ScopeASN, netip.Prefix{}, asnDE, map[string]any{"asn": asnDE}},
|
{anomalyASN, anomaly.ScopeASN, netip.Prefix{}, asnDE, map[string]any{"asn": asnDE}},
|
||||||
{anomalyTotal, anomaly.ScopeTotal, netip.Prefix{}, "the whole service", nil},
|
{anomalyTotal, anomaly.ScopeTotal, netip.Prefix{}, "the whole service", nil},
|
||||||
{
|
{
|
||||||
anomalyWatch, anomaly.ScopeWatch, netip.MustParsePrefix(clientsNet),
|
anomalyWatch, anomaly.ScopeWatchedNet, netip.MustParsePrefix(clientsNet),
|
||||||
"the named netblock office, " + clientsNet, map[string]any{"name": "office"},
|
"the named netblock office, " + clientsNet, map[string]any{"name": "office"},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ func TestSixteenThousandCookiesAreRefusedWellWithinTheTimeLimit(t *testing.T) {
|
|||||||
line := s.out.requestLine(t)
|
line := s.out.requestLine(t)
|
||||||
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
||||||
|
|
||||||
if line.DurationWAF != nil {
|
if line.DurationCoreRuleSetMS != nil {
|
||||||
t.Errorf("the Core Rule Set read the request, for %f ms", *line.DurationWAF)
|
t.Errorf("the Core Rule Set read the request, for %f ms", *line.DurationCoreRuleSetMS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ func TestEachHeaderLimitRefusesARequestOverIt(t *testing.T) {
|
|||||||
wantDisposition(t, got, http.StatusOK, discriminator.DispositionAllow)
|
wantDisposition(t, got, http.StatusOK, discriminator.DispositionAllow)
|
||||||
wantLine(t, line, http.StatusOK, requestlog.ActionForward)
|
wantLine(t, line, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
if line.DurationWAF == nil {
|
if line.DurationCoreRuleSetMS == nil {
|
||||||
t.Error("the Core Rule Set did not read a request within the limits")
|
t.Error("the Core Rule Set did not read a request within the limits")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -220,9 +220,9 @@ func TestObserveModeAllowsARequestOverTheHeaderLimitsUnread(t *testing.T) {
|
|||||||
|
|
||||||
line := s.out.requestLine(t)
|
line := s.out.requestLine(t)
|
||||||
if line.WouldAction != requestlog.ActionRequestHeaderTooLarge ||
|
if line.WouldAction != requestlog.ActionRequestHeaderTooLarge ||
|
||||||
line.DurationWAF != nil {
|
line.DurationCoreRuleSetMS != nil {
|
||||||
t.Errorf("would_action %q and duration_waf %v, want %q and none",
|
t.Errorf("would_action %q and duration_core_rule_set_ms %v, want %q and none",
|
||||||
line.WouldAction, line.DurationWAF, requestlog.ActionRequestHeaderTooLarge)
|
line.WouldAction, line.DurationCoreRuleSetMS, requestlog.ActionRequestHeaderTooLarge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,8 +283,8 @@ func TestNotWaitingForGeoJSNeverLowersAClientsLimit(t *testing.T) {
|
|||||||
line := s.out.requestLine(t)
|
line := s.out.requestLine(t)
|
||||||
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
||||||
|
|
||||||
if line.LimitPercent != nil {
|
if line.RateLimitPercent != nil {
|
||||||
t.Errorf("limit percentage %d, want none", *line.LimitPercent)
|
t.Errorf("limit percentage %d, want none", *line.RateLimitPercent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The client is under no ban, and GeoJS places it in a country.
|
// The client is under no ban, and GeoJS places it in a country.
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func (rq *request) banForLimit(now time.Time, hit ratelimit.Hit, status int) {
|
|||||||
case ratelimit.KindBytes:
|
case ratelimit.KindBytes:
|
||||||
rq.line.LimitHit = hit.Window + "_bytes" // as counts names the byte totals
|
rq.line.LimitHit = hit.Window + "_bytes" // as counts names the byte totals
|
||||||
case ratelimit.KindRefusals:
|
case ratelimit.KindRefusals:
|
||||||
rq.line.LimitHit = requestlog.LimitHitErrorBurst
|
rq.line.LimitHit = requestlog.LimitHitMinuteRefusals
|
||||||
default:
|
default:
|
||||||
rq.line.LimitHit = hit.Window
|
rq.line.LimitHit = hit.Window
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,14 +79,16 @@ func TestEachBiasedThresholdLowersTheRateLimits(t *testing.T) {
|
|||||||
{http.StatusForbidden, requestlog.ActionClientIsRateLimited},
|
{http.StatusForbidden, requestlog.ActionClientIsRateLimited},
|
||||||
} {
|
} {
|
||||||
line := s.get(tc.from, sent.status, sent.action)
|
line := s.get(tc.from, sent.status, sent.action)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
"50 from "+tc.setting)
|
"50 from "+tc.setting)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fromKP, which no setting lists, has the whole limit.
|
// fromKP, which no setting lists, has the whole limit.
|
||||||
for range 3 {
|
for range 3 {
|
||||||
line := s.get(fromKP, http.StatusOK, requestlog.ActionForward)
|
line := s.get(fromKP, http.StatusOK, requestlog.ActionForward)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
none)
|
none)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -119,7 +121,8 @@ func TestEachBiasedThresholdLowersTheByteLimits(t *testing.T) {
|
|||||||
t.Errorf("log line has limit_hit %q, want %s", line.LimitHit, minuteBytes)
|
t.Errorf("log line has limit_hit %q, want %s", line.LimitHit, minuteBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
|
wantPercent(t, "bytes_limit_percent",
|
||||||
|
line.BytesLimitPercent, line.BytesLimitPercentSetting,
|
||||||
"50 from "+tc.setting)
|
"50 from "+tc.setting)
|
||||||
|
|
||||||
// fromKP, which no setting lists, has the whole limit.
|
// fromKP, which no setting lists, has the whole limit.
|
||||||
@@ -128,7 +131,8 @@ func TestEachBiasedThresholdLowersTheByteLimits(t *testing.T) {
|
|||||||
t.Errorf("log line for %s has limit_hit %q, want none", fromKP, line.LimitHit)
|
t.Errorf("log line for %s has limit_hit %q, want none", fromKP, line.LimitHit)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting, none)
|
wantPercent(t, "bytes_limit_percent",
|
||||||
|
line.BytesLimitPercent, line.BytesLimitPercentSetting, none)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,9 +181,11 @@ func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(t *testing.T)
|
|||||||
t.Errorf("log line has limit_hit %q, want %q", line.LimitHit, tc.limitHit)
|
t.Errorf("log line has limit_hit %q, want %q", line.LimitHit, tc.limitHit)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
tc.limitPercent)
|
tc.limitPercent)
|
||||||
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
|
wantPercent(t, "bytes_limit_percent",
|
||||||
|
line.BytesLimitPercent, line.BytesLimitPercentSetting,
|
||||||
tc.bytesPercent)
|
tc.bytesPercent)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -193,10 +199,10 @@ func TestZeroPercentIsAZeroAllowance(t *testing.T) {
|
|||||||
// The first request breaks the limit, and bans the client; the log line
|
// The first request breaks the limit, and bans the client; the log line
|
||||||
// gives the 0.
|
// gives the 0.
|
||||||
line := s.get(unplaced, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
line := s.get(unplaced, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
if line.fields["limit_percent"] != float64(0) ||
|
if line.fields["rate_limit_percent"] != float64(0) ||
|
||||||
line.fields["limit_percent_setting"] != unknownLimitPercent {
|
line.fields["rate_limit_percent_setting"] != unknownLimitPercent {
|
||||||
t.Errorf("log line has limit_percent %v from %v, want 0 from %s",
|
t.Errorf("log line has rate_limit_percent %v from %v, want 0 from %s",
|
||||||
line.fields["limit_percent"], line.fields["limit_percent_setting"],
|
line.fields["rate_limit_percent"], line.fields["rate_limit_percent_setting"],
|
||||||
unknownLimitPercent)
|
unknownLimitPercent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +216,7 @@ func TestLowestPercentageApplies(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
env map[string]string
|
env map[string]string
|
||||||
from string
|
from string
|
||||||
// want is the log line's limit_percent, as percentText gives it.
|
// want is the log line's rate_limit_percent, as percentText gives it.
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -251,7 +257,8 @@ func TestLowestPercentageApplies(t *testing.T) {
|
|||||||
s.get(tc.from, http.StatusOK, requestlog.ActionForward)
|
s.get(tc.from, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
line := s.get(tc.from, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
line := s.get(tc.from, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
tc.want)
|
tc.want)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -294,7 +301,8 @@ func TestClientWithoutAnAnswerInTimeHasTheUnknownLimitPercent(t *testing.T) {
|
|||||||
|
|
||||||
line := out.requestLine(t)
|
line := out.requestLine(t)
|
||||||
wantLine(t, line, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
wantLine(t, line, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
"0 from "+unknownLimitPercent)
|
"0 from "+unknownLimitPercent)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// coreRuleSet inspects the request with the Core Rule Set, unless
|
// coreRuleSet inspects the request with the Core Rule Set, unless
|
||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
// 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 adds the tag exploitsignature,
|
// SWW_WAF_ANOMALY_THRESHOLD is a match: it adds the tag exploitsignature,
|
||||||
// raises the waf_block alert, and in block mode refuses the request with 403,
|
// raises the core_rule_set_match alert, and in block mode refuses the request with 403,
|
||||||
// which is an offence its client's history counts. Its figures are the score
|
// which is an offence its client's history counts. Its figures are the score
|
||||||
// and the threshold.
|
// and the threshold.
|
||||||
type coreRuleSet struct{}
|
type coreRuleSet struct{}
|
||||||
@@ -38,12 +38,12 @@ func (coreRuleSet) check(_ context.Context, rq *request, _ time.Time) *verdict {
|
|||||||
result, read, _ := rq.h.coreRuleSet.Inspect(rq.in, rq.client, rq.in.Body)
|
result, read, _ := rq.h.coreRuleSet.Inspect(rq.in, rq.client, rq.in.Body)
|
||||||
rq.requestBytes = int64(len(read))
|
rq.requestBytes = int64(len(read))
|
||||||
|
|
||||||
rq.line.DurationWAF = new(requestlog.Milliseconds(time.Since(start)))
|
rq.line.DurationCoreRuleSetMS = new(requestlog.Milliseconds(time.Since(start)))
|
||||||
rq.line.WAFRuleIDs = result.RuleIDs
|
rq.line.CoreRuleSetRuleIDs = result.RuleIDs
|
||||||
rq.line.WAFScore = &result.Score
|
rq.line.CoreRuleSetScore = &result.Score
|
||||||
|
|
||||||
for _, id := range result.RuleIDs {
|
for _, id := range result.RuleIDs {
|
||||||
rq.h.metrics.WAFMatched(cfg.WAFMode, id)
|
rq.h.metrics.CoreRuleSetMatched(cfg.WAFMode, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
threshold := cfg.WAFAnomalyThreshold
|
threshold := cfg.WAFAnomalyThreshold
|
||||||
@@ -52,26 +52,26 @@ func (coreRuleSet) check(_ context.Context, rq *request, _ time.Time) *verdict {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rq.addTag(config.ExploitSignatureTag)
|
rq.addTag(config.ExploitSignatureTag)
|
||||||
rq.alertWAFBlock(result)
|
rq.alertCoreRuleSetMatch(result)
|
||||||
|
|
||||||
if cfg.WAFMode == config.WAFModeDetect {
|
if cfg.WAFMode == config.WAFModeDetect {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rq.wafBlocked = true
|
rq.blockedByCoreRuleSet = true
|
||||||
|
|
||||||
return &verdict{
|
return &verdict{
|
||||||
action: requestlog.ActionWAFBlocked,
|
action: requestlog.ActionBlockedByCoreRuleSet,
|
||||||
figures: map[string]any{figureScore: result.Score, "threshold": threshold},
|
figures: map[string]any{figureScore: result.Score, "threshold": threshold},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// alertWAFBlock raises the waf_block alert for the request, which the Core
|
// alertCoreRuleSetMatch raises the core_rule_set_match alert for the request,
|
||||||
// Rule Set scored at result, at or over SWW_WAF_ANOMALY_THRESHOLD. Its
|
// which the Core Rule Set scored at result, at or over SWW_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.
|
||||||
func (rq *request) alertWAFBlock(result waf.Result) {
|
func (rq *request) alertCoreRuleSetMatch(result coreruleset.Result) {
|
||||||
detail := map[string]any{
|
detail := map[string]any{
|
||||||
"rule_ids": result.RuleIDs,
|
"rule_ids": result.RuleIDs,
|
||||||
"score": result.Score,
|
"score": result.Score,
|
||||||
@@ -87,7 +87,7 @@ func (rq *request) alertWAFBlock(result waf.Result) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rq.h.alerts.Raise(alerts.Alert{
|
rq.h.alerts.Raise(alerts.Alert{
|
||||||
Event: alerts.EventWAFBlock,
|
Event: alerts.EventCoreRuleSetMatch,
|
||||||
Host: rq.site,
|
Host: rq.site,
|
||||||
Client: rq.client,
|
Client: rq.client,
|
||||||
Netblock: rq.h.clientGroup(rq.client),
|
Netblock: rq.h.clientGroup(rq.client),
|
||||||
|
|||||||
@@ -40,24 +40,30 @@ const (
|
|||||||
// SWW_WAF_ANOMALY_THRESHOLD.
|
// SWW_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
|
// wantCoreRuleSet checks the request log line's core_rule_set_rule_ids and
|
||||||
// that it has duration_waf, or with no score, that it has none of the
|
// core_rule_set_score, and that it has duration_core_rule_set_ms, or with no
|
||||||
// three: the Core Rule Set did not inspect the request.
|
// score, that it has none of the three: the Core Rule Set did not inspect the
|
||||||
func wantWAF(t *testing.T, line logLine, score *int, ruleIDs ...int) {
|
// request.
|
||||||
|
func wantCoreRuleSet(t *testing.T, line logLine, score *int, ruleIDs ...int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if !slices.Equal(line.WAFRuleIDs, ruleIDs) {
|
if !slices.Equal(line.CoreRuleSetRuleIDs, ruleIDs) {
|
||||||
t.Errorf("log line has waf_rule_ids %v, want %v", line.WAFRuleIDs, ruleIDs)
|
t.Errorf("log line has core_rule_set_rule_ids %v, want %v",
|
||||||
|
line.CoreRuleSetRuleIDs, ruleIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case score == nil && (line.WAFScore != nil || line.DurationWAF != nil):
|
case score == nil &&
|
||||||
t.Errorf("log line has waf_score %v and duration_waf %v, want neither",
|
(line.CoreRuleSetScore != nil || line.DurationCoreRuleSetMS != nil):
|
||||||
line.fields["waf_score"], line.fields["duration_waf"])
|
t.Errorf("log line has core_rule_set_score %v and "+
|
||||||
case score != nil && (line.WAFScore == nil || *line.WAFScore != *score):
|
"duration_core_rule_set_ms %v, want neither",
|
||||||
t.Errorf("log line has waf_score %v, want %d", line.fields["waf_score"], *score)
|
line.fields["core_rule_set_score"], line.fields["duration_core_rule_set_ms"])
|
||||||
case score != nil && line.DurationWAF == nil:
|
case score != nil &&
|
||||||
t.Error("log line has no duration_waf")
|
(line.CoreRuleSetScore == nil || *line.CoreRuleSetScore != *score):
|
||||||
|
t.Errorf("log line has core_rule_set_score %v, want %d",
|
||||||
|
line.fields["core_rule_set_score"], *score)
|
||||||
|
case score != nil && line.DurationCoreRuleSetMS == nil:
|
||||||
|
t.Error("log line has no duration_core_rule_set_ms")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +76,7 @@ func TestCoreRuleSetMatchAddsTheExploitSignatureTag(t *testing.T) {
|
|||||||
status int
|
status int
|
||||||
action string
|
action string
|
||||||
}{
|
}{
|
||||||
{block, block, http.StatusForbidden, requestlog.ActionWAFBlocked},
|
{block, block, http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet},
|
||||||
{detect, detect, http.StatusOK, requestlog.ActionForward},
|
{detect, detect, http.StatusOK, requestlog.ActionForward},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -133,14 +139,14 @@ func TestCoreRuleSetRefusesAttacksInBlockModeAndOnlyLogsThemInDetectMode(t *test
|
|||||||
mode, action string
|
mode, action string
|
||||||
status int
|
status int
|
||||||
}{
|
}{
|
||||||
{block, requestlog.ActionWAFBlocked, http.StatusForbidden},
|
{block, requestlog.ActionBlockedByCoreRuleSet, http.StatusForbidden},
|
||||||
{detect, requestlog.ActionForward, http.StatusOK},
|
{detect, requestlog.ActionForward, http.StatusOK},
|
||||||
} {
|
} {
|
||||||
s, _, _ := startWithClock(t, "", map[string]string{wafMode: tc.mode})
|
s, _, _ := startWithClock(t, "", map[string]string{wafMode: tc.mode})
|
||||||
|
|
||||||
line := s.requestWithHeader(client, attack.path, attack.header,
|
line := s.requestWithHeader(client, attack.path, attack.header,
|
||||||
tc.status, tc.action)
|
tc.status, tc.action)
|
||||||
wantWAF(t, line, &attack.score, attack.ruleIDs...)
|
wantCoreRuleSet(t, line, &attack.score, attack.ruleIDs...)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -152,7 +158,7 @@ func TestCoreRuleSetRefusalNamesItsHeuristicScoreAndThreshold(t *testing.T) {
|
|||||||
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
|
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
|
||||||
|
|
||||||
line := s.request(client, sqlInjection, http.StatusForbidden,
|
line := s.request(client, sqlInjection, http.StatusForbidden,
|
||||||
requestlog.ActionWAFBlocked)
|
requestlog.ActionBlockedByCoreRuleSet)
|
||||||
wantHeuristic(t, line, "core_rule_set", `{"score": 5, "threshold": 5}`)
|
wantHeuristic(t, line, "core_rule_set", `{"score": 5, "threshold": 5}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +169,7 @@ func TestOrdinaryRequestIsInspectedAndPassed(t *testing.T) {
|
|||||||
|
|
||||||
line := s.request(client, "/owner/repo/src/branch/main/README.md?display=source",
|
line := s.request(client, "/owner/repo/src/branch/main/README.md?display=source",
|
||||||
http.StatusOK, requestlog.ActionForward)
|
http.StatusOK, requestlog.ActionForward)
|
||||||
wantWAF(t, line, new(0))
|
wantCoreRuleSet(t, line, new(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
|
func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
|
||||||
@@ -183,26 +189,26 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
|
|||||||
// A client in SWW_ALLOW_NETS, and a path SWW_WAF_EXEMPT_PATHS
|
// A client in SWW_ALLOW_NETS, and a path SWW_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)
|
wantCoreRuleSet(t, line, nil)
|
||||||
line = s.request(client, "/api/v1/repos?id=1'%20OR%20'1'='1", http.StatusOK,
|
line = s.request(client, "/api/v1/repos?id=1'%20OR%20'1'='1", http.StatusOK,
|
||||||
requestlog.ActionForward)
|
requestlog.ActionForward)
|
||||||
wantWAF(t, line, nil)
|
wantCoreRuleSet(t, line, nil)
|
||||||
|
|
||||||
// The prefix is matched as rate limit exempt paths are: a path that
|
// The prefix is matched as rate limit exempt paths are: a path that
|
||||||
// goes up and out of it is inspected.
|
// goes up and out of it is inspected.
|
||||||
line = s.request(client, "/api/../?id=1'%20OR%20'1'='1", http.StatusForbidden,
|
line = s.request(client, "/api/../?id=1'%20OR%20'1'='1", http.StatusForbidden,
|
||||||
requestlog.ActionWAFBlocked)
|
requestlog.ActionBlockedByCoreRuleSet)
|
||||||
wantWAF(t, line, new(25), 930100, 930110, 942100)
|
wantCoreRuleSet(t, line, new(25), 930100, 930110, 942100)
|
||||||
|
|
||||||
// A request a rule file refuses is not inspected.
|
// A request a rule file refuses is not inspected.
|
||||||
line = s.request(otherClient, "/blocked?id=1'%20OR%20'1'='1", http.StatusForbidden,
|
line = s.request(otherClient, "/blocked?id=1'%20OR%20'1'='1", http.StatusForbidden,
|
||||||
requestlog.ActionBlockedByBlockRule)
|
requestlog.ActionBlockedByBlockRule)
|
||||||
wantWAF(t, line, nil)
|
wantCoreRuleSet(t, line, nil)
|
||||||
|
|
||||||
// With SWW_WAF_MODE off, no request is.
|
// With SWW_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)
|
wantCoreRuleSet(t, line, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnomalyThreshold(t *testing.T) {
|
func TestAnomalyThreshold(t *testing.T) {
|
||||||
@@ -216,13 +222,14 @@ func TestAnomalyThreshold(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
line := s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
line := s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
wantWAF(t, line, new(5), 942100)
|
wantCoreRuleSet(t, line, new(5), 942100)
|
||||||
}
|
}
|
||||||
|
|
||||||
s, _, _ := startWithClock(t, "", map[string]string{
|
s, _, _ := startWithClock(t, "", map[string]string{
|
||||||
wafMode: block, wafAnomalyThreshold: "5",
|
wafMode: block, wafAnomalyThreshold: "5",
|
||||||
})
|
})
|
||||||
s.request(client, sqlInjection, http.StatusForbidden, requestlog.ActionWAFBlocked)
|
s.request(client, sqlInjection, http.StatusForbidden,
|
||||||
|
requestlog.ActionBlockedByCoreRuleSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
|
func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
|
||||||
@@ -265,7 +272,7 @@ func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
|
|||||||
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
|
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
|
||||||
line, _ := s.requestWithBody(request.method, client, request.path,
|
line, _ := s.requestWithBody(request.method, client, request.path,
|
||||||
request.header, body, http.StatusOK, requestlog.ActionForward)
|
request.header, body, http.StatusOK, requestlog.ActionForward)
|
||||||
wantWAF(t, line, new(0))
|
wantCoreRuleSet(t, line, new(0))
|
||||||
|
|
||||||
// A list given replaces the default.
|
// A list given replaces the default.
|
||||||
s, _, _ = startWithClock(t, "", map[string]string{
|
s, _, _ = startWithClock(t, "", map[string]string{
|
||||||
@@ -273,12 +280,12 @@ func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
|
|||||||
})
|
})
|
||||||
score := 5 * len(request.ruleIDs)
|
score := 5 * len(request.ruleIDs)
|
||||||
line, _ = s.requestWithBody(request.method, client, request.path,
|
line, _ = s.requestWithBody(request.method, client, request.path,
|
||||||
request.header, body, http.StatusForbidden, requestlog.ActionWAFBlocked)
|
request.header, body, http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet)
|
||||||
wantWAF(t, line, &score, request.ruleIDs...)
|
wantCoreRuleSet(t, line, &score, request.ruleIDs...)
|
||||||
|
|
||||||
// And switches off the rules it lists.
|
// And switches off the rules it lists.
|
||||||
line = s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
line = s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
wantWAF(t, line, new(0))
|
wantCoreRuleSet(t, line, new(0))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,14 +301,14 @@ func TestAttackInAFormBodyIsRefusedOnlyWhileBodiesAreRead(t *testing.T) {
|
|||||||
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
|
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
|
||||||
line, _ := s.requestWithBody(http.MethodPost, client, "/", header, body,
|
line, _ := s.requestWithBody(http.MethodPost, client, "/", header, body,
|
||||||
http.StatusOK, requestlog.ActionForward)
|
http.StatusOK, requestlog.ActionForward)
|
||||||
wantWAF(t, line, new(0))
|
wantCoreRuleSet(t, line, new(0))
|
||||||
|
|
||||||
s, _, _ = startWithClock(t, "", map[string]string{
|
s, _, _ = startWithClock(t, "", map[string]string{
|
||||||
wafMode: block, wafBodyLimit: sizeLimitSetting,
|
wafMode: block, wafBodyLimit: sizeLimitSetting,
|
||||||
})
|
})
|
||||||
line, _ = s.requestWithBody(http.MethodPost, client, "/", header, body,
|
line, _ = s.requestWithBody(http.MethodPost, client, "/", header, body,
|
||||||
http.StatusForbidden, requestlog.ActionWAFBlocked)
|
http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet)
|
||||||
wantWAF(t, line, new(5), 942100)
|
wantCoreRuleSet(t, line, new(5), 942100)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBodiesReachTheAppAsSentWhileBodiesAreRead(t *testing.T) {
|
func TestBodiesReachTheAppAsSentWhileBodiesAreRead(t *testing.T) {
|
||||||
@@ -391,32 +398,33 @@ func TestCoreRuleSetRefusalIsAnOffenceAndCountsTowardTheErrorBurst(t *testing.T)
|
|||||||
})
|
})
|
||||||
|
|
||||||
for range 2 {
|
for range 2 {
|
||||||
s.request(client, sqlInjection, http.StatusForbidden, requestlog.ActionWAFBlocked)
|
s.request(client, sqlInjection, http.StatusForbidden,
|
||||||
|
requestlog.ActionBlockedByCoreRuleSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The third refusal in a minute breaks the error burst, and bans the
|
// The third refusal in a minute breaks the error burst, and bans the
|
||||||
// client.
|
// client.
|
||||||
line := s.request(client, sqlInjection, http.StatusForbidden,
|
line := s.request(client, sqlInjection, http.StatusForbidden,
|
||||||
requestlog.ActionWAFBlocked)
|
requestlog.ActionBlockedByCoreRuleSet)
|
||||||
if line.LimitHit != requestlog.LimitHitErrorBurst ||
|
if line.LimitHit != requestlog.LimitHitMinuteRefusals ||
|
||||||
line.Offence != requestlog.OffenceLimit {
|
line.Offence != requestlog.OffenceLimit {
|
||||||
t.Errorf("log line has limit_hit %q and offence %q, want error_burst and limit",
|
t.Errorf("log line has limit_hit %q and offence %q, want minute_refusals and limit",
|
||||||
line.LimitHit, line.Offence)
|
line.LimitHit, line.Offence)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
|
|
||||||
want := ratelimit.Offences{Limit: 1, WAFBlocked: 3}
|
want := ratelimit.Offences{Limit: 1, BlockedByCoreRuleSet: 3}
|
||||||
if offences := historyOf(t, server, client).Offences; offences != want {
|
if offences := historyOf(t, server, client).Offences; offences != want {
|
||||||
t.Errorf("history counts the offences %+v, want %+v", offences, want)
|
t.Errorf("history counts the offences %+v, want %+v", offences, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics := s.scrape()
|
metrics := s.scrape()
|
||||||
wantMetric(t, metrics,
|
wantMetric(t, metrics,
|
||||||
`sww_waf_matches_total{instance="app",mode="block",rule_id="942100"}`, 3)
|
`sww_core_rule_set_matches_total{instance="app",mode="block",rule_id="942100"}`, 3)
|
||||||
wantMetric(t, metrics,
|
wantMetric(t, metrics,
|
||||||
`sww_offences_total{instance="app",kind="waf_blocked"}`, 3)
|
`sww_offences_total{instance="app",kind="blocked_by_core_rule_set"}`, 3)
|
||||||
wantMetric(t, metrics, `sww_requests_total{action="waf_blocked",`+
|
wantMetric(t, metrics, `sww_requests_total{action="blocked_by_core_rule_set",`+
|
||||||
`instance="app",status_class="4xx"}`, 3)
|
`instance="app",status_class="4xx"}`, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,9 +448,9 @@ func TestDetectModeMatchIsNoOffenceAndNotCountedTowardTheErrorBurst(t *testing.T
|
|||||||
|
|
||||||
metrics := s.scrape()
|
metrics := s.scrape()
|
||||||
wantMetric(t, metrics,
|
wantMetric(t, metrics,
|
||||||
`sww_waf_matches_total{instance="app",mode="detect",rule_id="942100"}`, 3)
|
`sww_core_rule_set_matches_total{instance="app",mode="detect",rule_id="942100"}`, 3)
|
||||||
wantNoSeries(t, metrics,
|
wantNoSeries(t, metrics,
|
||||||
`sww_offences_total{instance="app",kind="waf_blocked"}`)
|
`sww_offences_total{instance="app",kind="blocked_by_core_rule_set"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObserveModeLogsWhatTheCoreRuleSetWouldDo(t *testing.T) {
|
func TestObserveModeLogsWhatTheCoreRuleSetWouldDo(t *testing.T) {
|
||||||
@@ -451,17 +459,17 @@ func TestObserveModeLogsWhatTheCoreRuleSetWouldDo(t *testing.T) {
|
|||||||
s, _, server := startWithClock(t, "", map[string]string{wafMode: block, mode: observe})
|
s, _, server := startWithClock(t, "", map[string]string{wafMode: block, mode: observe})
|
||||||
|
|
||||||
line := s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
line := s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
wantWouldAction(t, line, requestlog.ActionWAFBlocked)
|
wantWouldAction(t, line, requestlog.ActionBlockedByCoreRuleSet)
|
||||||
wantWAF(t, line, new(5), 942100)
|
wantCoreRuleSet(t, line, new(5), 942100)
|
||||||
|
|
||||||
// It is an offence as in enforce mode.
|
// It is an offence as in enforce mode.
|
||||||
want := ratelimit.Offences{WAFBlocked: 1}
|
want := ratelimit.Offences{BlockedByCoreRuleSet: 1}
|
||||||
if offences := historyOf(t, server, client).Offences; offences != want {
|
if offences := historyOf(t, server, client).Offences; offences != want {
|
||||||
t.Errorf("history counts the offences %+v, want %+v", offences, want)
|
t.Errorf("history counts the offences %+v, want %+v", offences, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCoreRuleSetMatchRaisesTheWAFBlockAlert(t *testing.T) {
|
func TestCoreRuleSetMatchRaisesTheCoreRuleSetMatchAlert(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -475,7 +483,7 @@ func TestCoreRuleSetMatchRaisesTheWAFBlockAlert(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"block", map[string]string{wafMode: block},
|
"block", map[string]string{wafMode: block},
|
||||||
http.StatusForbidden, requestlog.ActionWAFBlocked, "",
|
http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet, "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"detect", map[string]string{wafMode: detect},
|
"detect", map[string]string{wafMode: detect},
|
||||||
@@ -510,7 +518,7 @@ func TestCoreRuleSetMatchRaisesTheWAFBlockAlert(t *testing.T) {
|
|||||||
wantAlerts(t, queue, alerts.Alert{
|
wantAlerts(t, queue, alerts.Alert{
|
||||||
Instance: alertInstance,
|
Instance: alertInstance,
|
||||||
Time: clk.Now(),
|
Time: clk.Now(),
|
||||||
Event: alerts.EventWAFBlock,
|
Event: alerts.EventCoreRuleSetMatch,
|
||||||
Host: appHost,
|
Host: appHost,
|
||||||
Client: netip.MustParseAddr(client),
|
Client: netip.MustParseAddr(client),
|
||||||
Netblock: netip.MustParsePrefix(client + "/32"),
|
Netblock: netip.MustParsePrefix(client + "/32"),
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ type Report struct {
|
|||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
RequestBytes int64 `json:"request_bytes"`
|
RequestBytes int64 `json:"request_bytes"`
|
||||||
ResponseBytes int64 `json:"response_bytes"`
|
ResponseBytes int64 `json:"response_bytes"`
|
||||||
DurationTotal float64 `json:"duration_total"`
|
DurationTotalMS float64 `json:"duration_total_ms"`
|
||||||
DurationUpstreamTotal float64 `json:"duration_upstream_total"`
|
DurationUpstreamTotalMS float64 `json:"duration_upstream_total_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns the server `sww discriminator` runs. It answers
|
// New returns the server `sww discriminator` runs. It answers
|
||||||
@@ -208,7 +208,7 @@ func (h *handler) answerQuestion(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// The app never sees this prefix. discriminator's own endpoints are at its
|
// The app never sees this prefix. discriminator's own endpoints are at its
|
||||||
// own address, so this answers as a path with no endpoint does.
|
// own address, so this answers as a path with no endpoint does.
|
||||||
rq.line.Action = requestlog.ActionAdmin
|
rq.line.Action = requestlog.ActionOwnEndpoint
|
||||||
rq.out.Header().Set(DispositionHeader, DispositionDeny)
|
rq.out.Header().Set(DispositionHeader, DispositionDeny)
|
||||||
http.Error(rq.out, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
http.Error(rq.out, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
}
|
}
|
||||||
@@ -405,7 +405,7 @@ func readReport(w http.ResponseWriter, r *http.Request) (Report, error) {
|
|||||||
|
|
||||||
for _, name := range []string{
|
for _, name := range []string{
|
||||||
"request_id", "status", "request_bytes", "response_bytes",
|
"request_id", "status", "request_bytes", "response_bytes",
|
||||||
"duration_total", "duration_upstream_total",
|
"duration_total_ms", "duration_upstream_total_ms",
|
||||||
} {
|
} {
|
||||||
value, there := fields[name]
|
value, there := fields[name]
|
||||||
if !there || string(value) == "null" {
|
if !there || string(value) == "null" {
|
||||||
@@ -431,11 +431,11 @@ func (report *Report) checkRanges() error {
|
|||||||
return fmt.Errorf("%w: request_bytes", errReportValue)
|
return fmt.Errorf("%w: request_bytes", errReportValue)
|
||||||
case report.ResponseBytes < 0 || report.ResponseBytes > maxReportedBytes:
|
case report.ResponseBytes < 0 || report.ResponseBytes > maxReportedBytes:
|
||||||
return fmt.Errorf("%w: response_bytes", errReportValue)
|
return fmt.Errorf("%w: response_bytes", errReportValue)
|
||||||
case report.DurationTotal < 0 || report.DurationTotal > maxReportedMilliseconds:
|
case report.DurationTotalMS < 0 || report.DurationTotalMS > maxReportedMilliseconds:
|
||||||
return fmt.Errorf("%w: duration_total", errReportValue)
|
return fmt.Errorf("%w: duration_total_ms", errReportValue)
|
||||||
case report.DurationUpstreamTotal < 0 ||
|
case report.DurationUpstreamTotalMS < 0 ||
|
||||||
report.DurationUpstreamTotal > maxReportedMilliseconds:
|
report.DurationUpstreamTotalMS > maxReportedMilliseconds:
|
||||||
return fmt.Errorf("%w: duration_upstream_total", errReportValue)
|
return fmt.Errorf("%w: duration_upstream_total_ms", errReportValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -459,18 +459,18 @@ func (h *handler) finishWith(ctx context.Context, held *decision, report Report)
|
|||||||
rq.line.Status = report.Status
|
rq.line.Status = report.Status
|
||||||
rq.line.RequestBytes = report.RequestBytes
|
rq.line.RequestBytes = report.RequestBytes
|
||||||
rq.line.ResponseBytes = report.ResponseBytes
|
rq.line.ResponseBytes = report.ResponseBytes
|
||||||
rq.line.DurationTotal = report.DurationTotal
|
rq.line.DurationTotalMS = report.DurationTotalMS
|
||||||
|
|
||||||
if rq.allowed {
|
if rq.allowed {
|
||||||
rq.line.DurationUpstreamTotal = &report.DurationUpstreamTotal
|
rq.line.DurationUpstreamTotalMS = &report.DurationUpstreamTotalMS
|
||||||
|
|
||||||
rq.runBanningHeuristics(ctx, onReport())
|
rq.runBanningHeuristics(ctx, onReport())
|
||||||
rq.countAnomalies()
|
rq.countAnomalies()
|
||||||
rq.addToHistory()
|
rq.addToHistory()
|
||||||
|
|
||||||
rq.h.metrics.RequestReported(&rq.line,
|
rq.h.metrics.RequestReported(&rq.line,
|
||||||
time.Duration(report.DurationTotal*float64(time.Millisecond)),
|
time.Duration(report.DurationTotalMS*float64(time.Millisecond)),
|
||||||
time.Duration(report.DurationUpstreamTotal*float64(time.Millisecond)))
|
time.Duration(report.DurationUpstreamTotalMS*float64(time.Millisecond)))
|
||||||
}
|
}
|
||||||
|
|
||||||
rq.writeLine()
|
rq.writeLine()
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ const (
|
|||||||
reportStatus = "status"
|
reportStatus = "status"
|
||||||
reportRequestBytes = "request_bytes"
|
reportRequestBytes = "request_bytes"
|
||||||
reportResponseBytes = "response_bytes"
|
reportResponseBytes = "response_bytes"
|
||||||
reportDuration = "duration_total"
|
reportDuration = "duration_total_ms"
|
||||||
reportUpstreamDuration = "duration_upstream_total"
|
reportUpstreamDuration = "duration_upstream_total_ms"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Discriminator is a discriminator running for one test, on a clock set to midnight.
|
// Discriminator is a discriminator running for one test, on a clock set to midnight.
|
||||||
@@ -693,7 +693,7 @@ func TestQuestionUnderTheOwnPrefixIsDeniedWith404AfterTheChecks(t *testing.T) {
|
|||||||
wantDisposition(t, got, http.StatusNotFound, discriminator.DispositionDeny)
|
wantDisposition(t, got, http.StatusNotFound, discriminator.DispositionDeny)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantLine(t, s.out.requestLine(t), http.StatusNotFound, requestlog.ActionAdmin)
|
wantLine(t, s.out.requestLine(t), http.StatusNotFound, requestlog.ActionOwnEndpoint)
|
||||||
|
|
||||||
got := s.ask(otherClient, discriminator.MetricsPath)
|
got := s.ask(otherClient, discriminator.MetricsPath)
|
||||||
wantDisposition(t, got, http.StatusForbidden, discriminator.DispositionDeny)
|
wantDisposition(t, got, http.StatusForbidden, discriminator.DispositionDeny)
|
||||||
@@ -725,9 +725,9 @@ func TestObserveModeRefusalAnsweredAllowCountsForTheErrorBurst(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lines := s.out.requestLines(t, 2)
|
lines := s.out.requestLines(t, 2)
|
||||||
if lines[0].LimitHit != "" || lines[1].LimitHit != requestlog.LimitHitErrorBurst {
|
if lines[0].LimitHit != "" || lines[1].LimitHit != requestlog.LimitHitMinuteRefusals {
|
||||||
t.Errorf("log lines have limit_hit %q and %q, want none and %q",
|
t.Errorf("log lines have limit_hit %q and %q, want none and %q",
|
||||||
lines[0].LimitHit, lines[1].LimitHit, requestlog.LimitHitErrorBurst)
|
lines[0].LimitHit, lines[1].LimitHit, requestlog.LimitHitMinuteRefusals)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,9 +744,9 @@ func TestDeniedQuestionCountsForTheErrorBurst(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lines := s.out.requestLines(t, 2)
|
lines := s.out.requestLines(t, 2)
|
||||||
if lines[0].LimitHit != "" || lines[1].LimitHit != requestlog.LimitHitErrorBurst {
|
if lines[0].LimitHit != "" || lines[1].LimitHit != requestlog.LimitHitMinuteRefusals {
|
||||||
t.Errorf("log lines have limit_hit %q and %q, want none and %q",
|
t.Errorf("log lines have limit_hit %q and %q, want none and %q",
|
||||||
lines[0].LimitHit, lines[1].LimitHit, requestlog.LimitHitErrorBurst)
|
lines[0].LimitHit, lines[1].LimitHit, requestlog.LimitHitMinuteRefusals)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,7 +901,7 @@ func TestDeniedRequestIsFinishedAtItsDecision(t *testing.T) {
|
|||||||
|
|
||||||
got := s.report(discriminator.Report{
|
got := s.report(discriminator.Report{
|
||||||
RequestID: denied.Header.Get(requestIDHeader), Status: http.StatusForbidden,
|
RequestID: denied.Header.Get(requestIDHeader), Status: http.StatusForbidden,
|
||||||
ResponseBytes: 1000, DurationTotal: 2,
|
ResponseBytes: 1000, DurationTotalMS: 2,
|
||||||
})
|
})
|
||||||
wantStatus(t, got, http.StatusNoContent)
|
wantStatus(t, got, http.StatusNoContent)
|
||||||
|
|
||||||
@@ -935,7 +935,7 @@ func TestMetricsCountAnAllowedRequestFromItsReport(t *testing.T) {
|
|||||||
got := s.report(discriminator.Report{
|
got := s.report(discriminator.Report{
|
||||||
RequestID: id, Status: http.StatusBadGateway,
|
RequestID: id, Status: http.StatusBadGateway,
|
||||||
RequestBytes: bodyBytes, ResponseBytes: answerBytes,
|
RequestBytes: bodyBytes, ResponseBytes: answerBytes,
|
||||||
DurationTotal: 5, DurationUpstreamTotal: 4,
|
DurationTotalMS: 5, DurationUpstreamTotalMS: 4,
|
||||||
})
|
})
|
||||||
wantStatus(t, got, http.StatusNoContent)
|
wantStatus(t, got, http.StatusNoContent)
|
||||||
|
|
||||||
@@ -951,7 +951,8 @@ func TestMetricsCountAnAllowedRequestFromItsReport(t *testing.T) {
|
|||||||
unscraped := startDiscriminator(t, "", map[string]string{metricsToken: token})
|
unscraped := startDiscriminator(t, "", map[string]string{metricsToken: token})
|
||||||
|
|
||||||
got = unscraped.report(discriminator.Report{
|
got = unscraped.report(discriminator.Report{
|
||||||
RequestID: unscraped.allowed(client), Status: http.StatusBadGateway, DurationTotal: 5,
|
RequestID: unscraped.allowed(client), Status: http.StatusBadGateway,
|
||||||
|
DurationTotalMS: 5,
|
||||||
})
|
})
|
||||||
wantStatus(t, got, http.StatusNoContent)
|
wantStatus(t, got, http.StatusNoContent)
|
||||||
wantMetric(t, unscraped.metrics(),
|
wantMetric(t, unscraped.metrics(),
|
||||||
@@ -1189,8 +1190,8 @@ func wantReportsFields(t *testing.T, line logLine) {
|
|||||||
if line.DiscriminatorLine != requestlog.DiscriminatorLineReport ||
|
if line.DiscriminatorLine != requestlog.DiscriminatorLineReport ||
|
||||||
line.Status != http.StatusCreated ||
|
line.Status != http.StatusCreated ||
|
||||||
line.RequestBytes != bodyBytes || line.ResponseBytes != answerBytes ||
|
line.RequestBytes != bodyBytes || line.ResponseBytes != answerBytes ||
|
||||||
line.DurationTotal != 12.5 || line.DurationUpstreamTotal == nil ||
|
line.DurationTotalMS != 12.5 || line.DurationUpstreamTotalMS == nil ||
|
||||||
*line.DurationUpstreamTotal != 10.25 {
|
*line.DurationUpstreamTotalMS != 10.25 {
|
||||||
t.Errorf("the report's line lacks the report's fields: %+v", line.Line)
|
t.Errorf("the report's line lacks the report's fields: %+v", line.Line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1205,8 +1206,8 @@ func wantSameDecision(t *testing.T, reported, decided logLine) {
|
|||||||
same.Status = decided.Status
|
same.Status = decided.Status
|
||||||
same.RequestBytes = decided.RequestBytes
|
same.RequestBytes = decided.RequestBytes
|
||||||
same.ResponseBytes = decided.ResponseBytes
|
same.ResponseBytes = decided.ResponseBytes
|
||||||
same.DurationTotal = decided.DurationTotal
|
same.DurationTotalMS = decided.DurationTotalMS
|
||||||
same.DurationUpstreamTotal = decided.DurationUpstreamTotal
|
same.DurationUpstreamTotalMS = decided.DurationUpstreamTotalMS
|
||||||
same.Counts.MinuteBytes = decided.Counts.MinuteBytes
|
same.Counts.MinuteBytes = decided.Counts.MinuteBytes
|
||||||
same.Counts.HourBytes = decided.Counts.HourBytes
|
same.Counts.HourBytes = decided.Counts.HourBytes
|
||||||
same.Counts.DayBytes = decided.Counts.DayBytes
|
same.Counts.DayBytes = decided.Counts.DayBytes
|
||||||
@@ -1224,7 +1225,7 @@ func wantDecisionsFields(t *testing.T, line logLine, id, query, accept string) {
|
|||||||
|
|
||||||
if line.DiscriminatorLine != requestlog.DiscriminatorLineDecision ||
|
if line.DiscriminatorLine != requestlog.DiscriminatorLineDecision ||
|
||||||
line.Status != http.StatusOK || line.ResponseBytes != 0 ||
|
line.Status != http.StatusOK || line.ResponseBytes != 0 ||
|
||||||
line.DurationUpstreamTotal != nil || line.DurationChecks == nil ||
|
line.DurationUpstreamTotalMS != nil || line.DurationChecksMS == nil ||
|
||||||
line.RequestID != id || line.Query != query ||
|
line.RequestID != id || line.Query != query ||
|
||||||
!line.HasAuthorization || !line.HasCookie ||
|
!line.HasAuthorization || !line.HasCookie ||
|
||||||
line.RequestHeaders["accept"] != accept || len(line.RuleIDs) != 1 ||
|
line.RequestHeaders["accept"] != accept || len(line.RuleIDs) != 1 ||
|
||||||
@@ -1260,7 +1261,7 @@ func TestReportWritesTheLastLineWithTheDecisionsAndTheReportsFields(t *testing.T
|
|||||||
got := s.report(discriminator.Report{
|
got := s.report(discriminator.Report{
|
||||||
RequestID: id, Status: http.StatusCreated,
|
RequestID: id, Status: http.StatusCreated,
|
||||||
RequestBytes: bodyBytes, ResponseBytes: answerBytes,
|
RequestBytes: bodyBytes, ResponseBytes: answerBytes,
|
||||||
DurationTotal: 12.5, DurationUpstreamTotal: 10.25,
|
DurationTotalMS: 12.5, DurationUpstreamTotalMS: 10.25,
|
||||||
})
|
})
|
||||||
wantStatus(t, got, http.StatusNoContent)
|
wantStatus(t, got, http.StatusNoContent)
|
||||||
|
|
||||||
@@ -1305,7 +1306,7 @@ func TestEveryDeniedRequestsReportGetsItsLastLine(t *testing.T) {
|
|||||||
0, requestlog.ActionClientDenied,
|
0, requestlog.ActionClientDenied,
|
||||||
},
|
},
|
||||||
{"own prefix", nil, discriminator.HealthPath, http.StatusNotFound,
|
{"own prefix", nil, discriminator.HealthPath, http.StatusNotFound,
|
||||||
requestlog.ActionAdmin},
|
requestlog.ActionOwnEndpoint},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -1317,7 +1318,7 @@ func TestEveryDeniedRequestsReportGetsItsLastLine(t *testing.T) {
|
|||||||
do(t, req)
|
do(t, req)
|
||||||
|
|
||||||
got := s.report(discriminator.Report{
|
got := s.report(discriminator.Report{
|
||||||
RequestID: id, Status: tc.status, ResponseBytes: 8, DurationTotal: 3,
|
RequestID: id, Status: tc.status, ResponseBytes: 8, DurationTotalMS: 3,
|
||||||
})
|
})
|
||||||
wantStatus(t, got, http.StatusNoContent)
|
wantStatus(t, got, http.StatusNoContent)
|
||||||
|
|
||||||
@@ -1325,8 +1326,8 @@ func TestEveryDeniedRequestsReportGetsItsLastLine(t *testing.T) {
|
|||||||
wantLine(t, line, tc.status, tc.action)
|
wantLine(t, line, tc.status, tc.action)
|
||||||
|
|
||||||
if line.DiscriminatorLine != requestlog.DiscriminatorLineReport ||
|
if line.DiscriminatorLine != requestlog.DiscriminatorLineReport ||
|
||||||
line.RequestID != id || line.ResponseBytes != 8 || line.DurationTotal != 3 ||
|
line.RequestID != id || line.ResponseBytes != 8 || line.DurationTotalMS != 3 ||
|
||||||
line.DurationUpstreamTotal != nil {
|
line.DurationUpstreamTotalMS != nil {
|
||||||
t.Errorf("the report's line is %+v", line.Line)
|
t.Errorf("the report's line is %+v", line.Line)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1583,9 +1584,10 @@ func TestCoreRuleSetAlertNamesTheSite(t *testing.T) {
|
|||||||
wantDisposition(t, got, http.StatusForbidden, discriminator.DispositionDeny)
|
wantDisposition(t, got, http.StatusForbidden, discriminator.DispositionDeny)
|
||||||
|
|
||||||
waiting := s.queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := s.queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != 1 || waiting[0].Event != alerts.EventWAFBlock ||
|
if len(waiting) != 1 || waiting[0].Event != alerts.EventCoreRuleSetMatch ||
|
||||||
waiting[0].Host != siteA {
|
waiting[0].Host != siteA {
|
||||||
t.Errorf("alerts waiting are %+v, want a waf_block naming %s", waiting, siteA)
|
t.Errorf("alerts waiting are %+v, want a core_rule_set_match naming %s",
|
||||||
|
waiting, siteA)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func TestMetricsCountTheTraffic(t *testing.T) {
|
|||||||
out.endedLines(t, 2)
|
out.endedLines(t, 2)
|
||||||
|
|
||||||
forward := `{action="forward",instance="app",status_class="2xx"}`
|
forward := `{action="forward",instance="app",status_class="2xx"}`
|
||||||
notFound := `{action="admin",instance="app",status_class="4xx"}`
|
notFound := `{action="own_endpoint",instance="app",status_class="4xx"}`
|
||||||
|
|
||||||
// The request for the metrics is itself under way.
|
// The request for the metrics is itself under way.
|
||||||
metrics := scrape(t, server.Addr)
|
metrics := scrape(t, server.Addr)
|
||||||
@@ -347,7 +347,7 @@ func TestMetricsCountGeoJSRequestsAndFailures(t *testing.T) {
|
|||||||
|
|
||||||
wantMetric(t, metrics, `sww_geojs_requests_total{instance="app"}`, 1)
|
wantMetric(t, metrics, `sww_geojs_requests_total{instance="app"}`, 1)
|
||||||
wantMetric(t, metrics, `sww_geojs_failures_total{instance="app"}`, 1)
|
wantMetric(t, metrics, `sww_geojs_failures_total{instance="app"}`, 1)
|
||||||
wantMetric(t, metrics, `sww_geojs_unanswered_total{instance="app"}`, 1)
|
wantMetric(t, metrics, `sww_requests_without_geojs_answer_total{instance="app"}`, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// keptAnswer returns GeoJS's answer that the client at addr is in
|
// keptAnswer returns GeoJS's answer that the client at addr is in
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/anomaly"
|
"sneak.berlin/go/smallwebwaf/internal/anomaly"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/metrics"
|
"sneak.berlin/go/smallwebwaf/internal/metrics"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/routewatch"
|
"sneak.berlin/go/smallwebwaf/internal/routewatch"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// adminPrefix starts the path of each of discriminator's own endpoints. The app never
|
// adminPrefix starts the path of each of discriminator's own endpoints. The app never
|
||||||
@@ -65,7 +65,7 @@ type Params struct {
|
|||||||
LookupFile *lookup.File
|
LookupFile *lookup.File
|
||||||
// CoreRuleSet is what NewCoreRuleSet returns: what inspects requests
|
// CoreRuleSet is what NewCoreRuleSet returns: what inspects requests
|
||||||
// while SWW_WAF_MODE is not off, and nil while it is.
|
// while SWW_WAF_MODE is not off, and nil while it is.
|
||||||
CoreRuleSet *waf.CoreRuleSet
|
CoreRuleSet *coreruleset.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.
|
||||||
@@ -129,7 +129,7 @@ func newHandler(params Params) *handler {
|
|||||||
BytesPerHour: params.Config.BytesLimitPerHour,
|
BytesPerHour: params.Config.BytesLimitPerHour,
|
||||||
BytesPerDay: params.Config.BytesLimitPerDay,
|
BytesPerDay: params.Config.BytesLimitPerDay,
|
||||||
}, params.Config.MaxTrackedClients),
|
}, params.Config.MaxTrackedClients),
|
||||||
ledger: bans.New(bans.Rules{
|
ledger: bans.New(bans.Params{
|
||||||
LimitBanDuration: params.Config.LimitBanDuration,
|
LimitBanDuration: params.Config.LimitBanDuration,
|
||||||
LimitBanRepeatWindow: params.Config.LimitBanRepeatWindow,
|
LimitBanRepeatWindow: params.Config.LimitBanRepeatWindow,
|
||||||
MaxBanDuration: params.Config.MaxBanDuration,
|
MaxBanDuration: params.Config.MaxBanDuration,
|
||||||
@@ -224,15 +224,15 @@ func newReputation(
|
|||||||
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE, or nil while SWW_WAF_MODE is off. It
|
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE, or nil while SWW_WAF_MODE is off. It
|
||||||
// reads at 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 exclusions file that coreruleset.New refuses is
|
||||||
// an error in every mode. While the mode is off, a set file is loaded with the
|
// an error in every mode. While the mode is off, a set file is loaded with the
|
||||||
// Core Rule Set only to check it.
|
// Core Rule Set only to check it.
|
||||||
func NewCoreRuleSet(cfg *config.Config) (*waf.CoreRuleSet, error) {
|
func NewCoreRuleSet(cfg *config.Config) (*coreruleset.CoreRuleSet, error) {
|
||||||
if cfg.WAFMode == config.WAFModeOff && cfg.CoreRuleSetExclusionsFile == "" {
|
if cfg.WAFMode == config.WAFModeOff && cfg.CoreRuleSetExclusionsFile == "" {
|
||||||
return nil, nil //nolint:nilnil // off is no Core Rule Set, and no error
|
return nil, nil //nolint:nilnil // off is no Core Rule Set, and no error
|
||||||
}
|
}
|
||||||
|
|
||||||
coreRuleSet, err := waf.New(waf.Params{
|
coreRuleSet, err := coreruleset.New(coreruleset.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,
|
ExclusionsFile: cfg.CoreRuleSetExclusionsFile,
|
||||||
@@ -266,7 +266,7 @@ type handler struct {
|
|||||||
abuseIPDB *reputation.AbuseIPDB
|
abuseIPDB *reputation.AbuseIPDB
|
||||||
routeWatch *routewatch.Client
|
routeWatch *routewatch.Client
|
||||||
rules *rules.Files
|
rules *rules.Files
|
||||||
coreRuleSet *waf.CoreRuleSet
|
coreRuleSet *coreruleset.CoreRuleSet
|
||||||
alerts *alerts.Queue
|
alerts *alerts.Queue
|
||||||
// awaitingReport holds the decision on each answered question by its id
|
// awaitingReport holds the decision on each answered question by its id
|
||||||
// until its report comes, and nil for a question still being handled.
|
// until its report comes, and nil for a question still being handled.
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ func (rateLimits) check(_ context.Context, rq *request, now time.Time) *verdict
|
|||||||
}
|
}
|
||||||
|
|
||||||
rq.limitPercent, rq.bytesPercent = rq.limitPercentages()
|
rq.limitPercent, rq.bytesPercent = rq.limitPercentages()
|
||||||
rq.line.LimitPercent, rq.line.LimitPercentSetting = rq.limitPercent.logged()
|
rq.line.RateLimitPercent, rq.line.RateLimitPercentSetting = rq.limitPercent.logged()
|
||||||
rq.line.BytesPercent, rq.line.BytesPercentSetting = rq.bytesPercent.logged()
|
rq.line.BytesLimitPercent, rq.line.BytesLimitPercentSetting = rq.bytesPercent.logged()
|
||||||
|
|
||||||
counts, hit, over := rq.h.limiter.Count(rq.h.clientGroup(rq.client), now,
|
counts, hit, over := rq.h.limiter.Count(rq.h.clientGroup(rq.client), now,
|
||||||
rq.limitPercent.percent)
|
rq.limitPercent.percent)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (refusalLimit) check(_ context.Context, rq *request, now time.Time) *verdic
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !rq.attack && !rq.blockedByBlockRule && !rq.wafBlocked {
|
if !rq.attack && !rq.blockedByBlockRule && !rq.blockedByCoreRuleSet {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ func (refusalLimit) check(_ context.Context, rq *request, now time.Time) *verdic
|
|||||||
status := rq.out.status
|
status := rq.out.status
|
||||||
|
|
||||||
switch rq.line.WouldAction {
|
switch rq.line.WouldAction {
|
||||||
case requestlog.ActionBlockedByBlockRule, requestlog.ActionWAFBlocked:
|
case requestlog.ActionBlockedByBlockRule, requestlog.ActionBlockedByCoreRuleSet:
|
||||||
status = http.StatusForbidden
|
status = http.StatusForbidden
|
||||||
case requestlog.ActionTemporarilyBanned, requestlog.ActionPermanentlyBanned:
|
case requestlog.ActionTemporarilyBanned, requestlog.ActionPermanentlyBanned:
|
||||||
status = cfg.BanResponse
|
status = cfg.BanResponse
|
||||||
|
|||||||
@@ -107,9 +107,9 @@ func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
|
|||||||
// The fourth is answered as the others were, breaks the error
|
// The fourth is answered as the others were, breaks the error
|
||||||
// burst, and bans the client.
|
// burst, and bans the client.
|
||||||
line := tc.refusals[3].send(s, client)
|
line := tc.refusals[3].send(s, client)
|
||||||
if line.LimitHit != requestlog.LimitHitErrorBurst ||
|
if line.LimitHit != requestlog.LimitHitMinuteRefusals ||
|
||||||
line.Offence != requestlog.OffenceLimit {
|
line.Offence != requestlog.OffenceLimit {
|
||||||
t.Errorf("log line has limit_hit %q and offence %q, want error_burst "+
|
t.Errorf("log line has limit_hit %q and offence %q, want minute_refusals "+
|
||||||
"and limit", line.LimitHit, line.Offence)
|
"and limit", line.LimitHit, line.Offence)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +277,8 @@ func TestErrorBurstOffOrAtItsDefault(t *testing.T) {
|
|||||||
line = blockRule.send(s, client)
|
line = blockRule.send(s, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
if broken := line.LimitHit == requestlog.LimitHitErrorBurst; broken != tc.broken {
|
broken := line.LimitHit == requestlog.LimitHitMinuteRefusals
|
||||||
|
if broken != tc.broken {
|
||||||
t.Errorf("the 31st refusal broke the error burst: %t, want %t",
|
t.Errorf("the 31st refusal broke the error burst: %t, want %t",
|
||||||
broken, tc.broken)
|
broken, tc.broken)
|
||||||
}
|
}
|
||||||
@@ -306,8 +307,8 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
|
|||||||
blockRule.send(s, exempt)
|
blockRule.send(s, exempt)
|
||||||
|
|
||||||
line := blockRule.send(s, exempt)
|
line := blockRule.send(s, exempt)
|
||||||
if line.LimitHit != requestlog.LimitHitErrorBurst {
|
if line.LimitHit != requestlog.LimitHitMinuteRefusals {
|
||||||
t.Errorf("log line has limit_hit %q, want error_burst", line.LimitHit)
|
t.Errorf("log line has limit_hit %q, want minute_refusals", line.LimitHit)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(exempt, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
s.get(exempt, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
@@ -367,8 +368,8 @@ func TestObserveModeLogsAndAlertsTheErrorBurst(t *testing.T) {
|
|||||||
line := s.request(client, blockedPath, http.StatusOK, requestlog.ActionForward)
|
line := s.request(client, blockedPath, http.StatusOK, requestlog.ActionForward)
|
||||||
wantWouldAction(t, line, requestlog.ActionBlockedByBlockRule)
|
wantWouldAction(t, line, requestlog.ActionBlockedByBlockRule)
|
||||||
|
|
||||||
if line.LimitHit != requestlog.LimitHitErrorBurst || line.BanExpires != "" {
|
if line.LimitHit != requestlog.LimitHitMinuteRefusals || line.BanExpires != "" {
|
||||||
t.Errorf("log line has limit_hit %q and ban_expires %q, want error_burst "+
|
t.Errorf("log line has limit_hit %q and ban_expires %q, want minute_refusals "+
|
||||||
"and none", line.LimitHit, line.BanExpires)
|
"and none", line.LimitHit, line.BanExpires)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func TestEachBlocklistActionForAListedAddressAndAListedNetblock(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
action string
|
action string
|
||||||
// statuses and actions are those of a listed client's three
|
// statuses and actions are those of a listed client's three
|
||||||
// requests, and percent their limit_percent, as percentText gives it.
|
// requests, and percent their rate_limit_percent, as percentText gives it.
|
||||||
statuses []int
|
statuses []int
|
||||||
actions []string
|
actions []string
|
||||||
percent string
|
percent string
|
||||||
@@ -90,8 +90,8 @@ func TestEachBlocklistActionForAListedAddressAndAListedNetblock(t *testing.T) {
|
|||||||
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])
|
||||||
wantReputation(t, line, dropURL)
|
wantReputation(t, line, dropURL)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent,
|
wantPercent(t, "rate_limit_percent", line.RateLimitPercent,
|
||||||
line.LimitPercentSetting, tc.percent)
|
line.RateLimitPercentSetting, tc.percent)
|
||||||
|
|
||||||
// A request refused for the list is not counted.
|
// A request refused for the list is not counted.
|
||||||
counted := line.fields["counts"] != nil
|
counted := line.fields["counts"] != nil
|
||||||
@@ -106,7 +106,8 @@ func TestEachBlocklistActionForAListedAddressAndAListedNetblock(t *testing.T) {
|
|||||||
for range 3 {
|
for range 3 {
|
||||||
line := s.get(unplaced, http.StatusOK, forward)
|
line := s.get(unplaced, http.StatusOK, forward)
|
||||||
wantReputation(t, line)
|
wantReputation(t, line)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
none)
|
none)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ func TestBlocklistLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T)
|
|||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
action, asnPercent string
|
action, asnPercent string
|
||||||
// want is the upload's limit_percent and bytes_percent, as
|
// want is the upload's rate_limit_percent and bytes_limit_percent, as
|
||||||
// percentText gives them, and limitHit its limit_hit.
|
// percentText gives them, and limitHit its limit_hit.
|
||||||
want, limitHit string
|
want, limitHit string
|
||||||
}{
|
}{
|
||||||
@@ -229,9 +230,11 @@ func TestBlocklistLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T)
|
|||||||
// The upload's 100 bytes are over 49, a quarter of 199, and 99,
|
// The upload's 100 bytes are over 49, a quarter of 199, and 99,
|
||||||
// half of it, and within 199.
|
// half of it, and within 199.
|
||||||
line := s.uploadFrom(fromDE)
|
line := s.uploadFrom(fromDE)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
tc.want)
|
tc.want)
|
||||||
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
|
wantPercent(t, "bytes_limit_percent",
|
||||||
|
line.BytesLimitPercent, line.BytesLimitPercentSetting,
|
||||||
tc.want)
|
tc.want)
|
||||||
|
|
||||||
if line.LimitHit != tc.limitHit {
|
if line.LimitHit != tc.limitHit {
|
||||||
@@ -288,9 +291,11 @@ func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(t *testing.T)
|
|||||||
loadLists(t, server, map[string][]string{asnURL: {"# by AS number", tc.file}})
|
loadLists(t, server, map[string][]string{asnURL: {"# by AS number", tc.file}})
|
||||||
|
|
||||||
line := s.uploadFrom(fromDE)
|
line := s.uploadFrom(fromDE)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
tc.limitPercent)
|
tc.limitPercent)
|
||||||
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
|
wantPercent(t, "bytes_limit_percent",
|
||||||
|
line.BytesLimitPercent, line.BytesLimitPercentSetting,
|
||||||
tc.bytesPercent)
|
tc.bytesPercent)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -385,7 +390,7 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
action string
|
action string
|
||||||
// statuses and actions are those of a listed client's three
|
// statuses and actions are those of a listed client's three
|
||||||
// requests, and percent their limit_percent, as percentText gives it.
|
// requests, and percent their rate_limit_percent, as percentText gives it.
|
||||||
statuses []int
|
statuses []int
|
||||||
actions []string
|
actions []string
|
||||||
percent string
|
percent string
|
||||||
@@ -422,8 +427,8 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
|
|||||||
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 SWW_DNSBL_ZONES names them.
|
||||||
wantReputation(t, line, listedBy[from]...)
|
wantReputation(t, line, listedBy[from]...)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent,
|
wantPercent(t, "rate_limit_percent", line.RateLimitPercent,
|
||||||
line.LimitPercentSetting, tc.percent)
|
line.RateLimitPercentSetting, tc.percent)
|
||||||
|
|
||||||
// A request refused for the verdict is not counted.
|
// A request refused for the verdict is not counted.
|
||||||
counted := line.fields["counts"] != nil
|
counted := line.fields["counts"] != nil
|
||||||
@@ -438,7 +443,8 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
|
|||||||
for range 3 {
|
for range 3 {
|
||||||
line := s.get(unplaced, http.StatusOK, forward)
|
line := s.get(unplaced, http.StatusOK, forward)
|
||||||
wantReputation(t, line)
|
wantReputation(t, line)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
none)
|
none)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +552,7 @@ func TestReputationLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T)
|
|||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
blocklistAction, reputationAction string
|
blocklistAction, reputationAction string
|
||||||
// want is the request's limit_percent and bytes_percent, as
|
// want is the request's rate_limit_percent and bytes_limit_percent, as
|
||||||
// percentText gives them.
|
// percentText gives them.
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
@@ -571,9 +577,11 @@ func TestReputationLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T)
|
|||||||
// Named by the blocklist, then by the zone.
|
// Named by the blocklist, then by the zone.
|
||||||
line := s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
line := s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
||||||
wantReputation(t, line, dropURL, dnsblZone)
|
wantReputation(t, line, dropURL, dnsblZone)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
tc.want)
|
tc.want)
|
||||||
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
|
wantPercent(t, "bytes_limit_percent",
|
||||||
|
line.BytesLimitPercent, line.BytesLimitPercentSetting,
|
||||||
tc.want)
|
tc.want)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -819,7 +827,7 @@ func TestClientRefusedForAnOffenceIsCheckedWithAbuseIPDBAtItsNextRequest(t *test
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"the Core Rule Set", sqlInjection, http.StatusForbidden,
|
"the Core Rule Set", sqlInjection, http.StatusForbidden,
|
||||||
requestlog.ActionWAFBlocked, ratelimit.Offences{WAFBlocked: 1},
|
requestlog.ActionBlockedByCoreRuleSet, ratelimit.Offences{BlockedByCoreRuleSet: 1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"a ban rule", probePath, http.StatusForbidden, requestlog.ActionTemporarilyBanned,
|
"a ban rule", probePath, http.StatusForbidden, requestlog.ActionTemporarilyBanned,
|
||||||
@@ -863,7 +871,7 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *test
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
action string
|
action string
|
||||||
// statuses and actions are those of fromDE's three requests, and
|
// statuses and actions are those of fromDE's three requests, and
|
||||||
// percent their limit_percent, as percentText gives it.
|
// percent their rate_limit_percent, as percentText gives it.
|
||||||
statuses []int
|
statuses []int
|
||||||
actions []string
|
actions []string
|
||||||
percent string
|
percent string
|
||||||
@@ -896,7 +904,8 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *test
|
|||||||
for i := range 3 {
|
for i := range 3 {
|
||||||
line := s.get(fromDE, tc.statuses[i], tc.actions[i])
|
line := s.get(fromDE, tc.statuses[i], tc.actions[i])
|
||||||
wantReputation(t, line, abuseipdb)
|
wantReputation(t, line, abuseipdb)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
tc.percent)
|
tc.percent)
|
||||||
|
|
||||||
// A request refused for the score is not counted.
|
// A request refused for the score is not counted.
|
||||||
@@ -910,7 +919,8 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *test
|
|||||||
for range 3 {
|
for range 3 {
|
||||||
line := s.get(fromKP, http.StatusOK, forward)
|
line := s.get(fromKP, http.StatusOK, forward)
|
||||||
wantReputation(t, line)
|
wantReputation(t, line)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
none)
|
none)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ type decision struct {
|
|||||||
limitPercent, bytesPercent percentage
|
limitPercent, bytesPercent percentage
|
||||||
// attack is true for a request that matched a ban rule or asked for a
|
// attack is true for a request that matched a ban rule or asked for a
|
||||||
// trap path, blockedByBlockRule for one a block rule refused, and
|
// trap path, blockedByBlockRule for one a block rule refused, and
|
||||||
// wafBlocked for one the Core Rule Set refused, each an offence its
|
// blockedByCoreRuleSet for one the Core Rule Set refused, each an offence
|
||||||
// client's history counts.
|
// its client's history counts.
|
||||||
attack, blockedByBlockRule, wafBlocked bool
|
attack, blockedByBlockRule, blockedByCoreRuleSet bool
|
||||||
// reputation is the reputation sources that list the client, for the
|
// reputation is the reputation sources that list the client, for the
|
||||||
// notes of a ban the request makes.
|
// notes of a ban the request makes.
|
||||||
reputation []bans.ReputationHit
|
reputation []bans.ReputationHit
|
||||||
@@ -188,7 +188,7 @@ func (rq *request) check(ctx context.Context) *refusal {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
case action == requestlog.ActionBlockedByBlockRule ||
|
case action == requestlog.ActionBlockedByBlockRule ||
|
||||||
action == requestlog.ActionWAFBlocked:
|
action == requestlog.ActionBlockedByCoreRuleSet:
|
||||||
return &refusal{status: http.StatusForbidden, action: action}
|
return &refusal{status: http.StatusForbidden, action: action}
|
||||||
case action == requestlog.ActionRequestHeaderTooLarge:
|
case action == requestlog.ActionRequestHeaderTooLarge:
|
||||||
return &refusal{status: http.StatusRequestHeaderFieldsTooLarge, action: action}
|
return &refusal{status: http.StatusRequestHeaderFieldsTooLarge, action: action}
|
||||||
@@ -262,8 +262,8 @@ func (rq *request) finish() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Since(rq.start)
|
duration := time.Since(rq.start)
|
||||||
line.DurationTotal = requestlog.Milliseconds(duration)
|
line.DurationTotalMS = requestlog.Milliseconds(duration)
|
||||||
line.DurationChecks = timing(rq.start, rq.checked)
|
line.DurationChecksMS = timing(rq.start, rq.checked)
|
||||||
|
|
||||||
// Counted before the log line is written, so that the metrics count
|
// Counted before the log line is written, so that the metrics count
|
||||||
// every request whose line is out. An allowed request is the exception:
|
// every request whose line is out. An allowed request is the exception:
|
||||||
@@ -312,7 +312,7 @@ func (rq *request) addToHistory() {
|
|||||||
BrokeLimit: rq.line.Offence == requestlog.OffenceLimit,
|
BrokeLimit: rq.line.Offence == requestlog.OffenceLimit,
|
||||||
Attack: rq.attack,
|
Attack: rq.attack,
|
||||||
BlockedByBlockRule: rq.blockedByBlockRule,
|
BlockedByBlockRule: rq.blockedByBlockRule,
|
||||||
WAFBlocked: rq.wafBlocked,
|
BlockedByCoreRuleSet: rq.blockedByCoreRuleSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
rq.h.limiter.AddToHistory(rq.h.clientGroup(rq.client), rq.h.now(), request)
|
rq.h.limiter.AddToHistory(rq.h.clientGroup(rq.client), rq.h.now(), request)
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ func wantBareLine(t *testing.T, line logLine) {
|
|||||||
|
|
||||||
// withTimings returns want with the timings of line.
|
// withTimings returns want with the timings of line.
|
||||||
func withTimings(line logLine, want requestlog.Line) requestlog.Line {
|
func withTimings(line logLine, want requestlog.Line) requestlog.Line {
|
||||||
want.DurationTotal = line.DurationTotal
|
want.DurationTotalMS = line.DurationTotalMS
|
||||||
want.DurationChecks = line.DurationChecks
|
want.DurationChecksMS = line.DurationChecksMS
|
||||||
want.DurationUpstreamTotal = line.DurationUpstreamTotal
|
want.DurationUpstreamTotalMS = line.DurationUpstreamTotalMS
|
||||||
|
|
||||||
return want
|
return want
|
||||||
}
|
}
|
||||||
@@ -226,10 +226,10 @@ func TestTimingsAreInOrder(t *testing.T) {
|
|||||||
|
|
||||||
// A request passed to the app has every timing; one refused, none of
|
// A request passed to the app has every timing; one refused, none of
|
||||||
// the app's; the health check, which runs no check, only the total.
|
// the app's; the health check, which runs no check, only the total.
|
||||||
wantTimings(t, forwarded, "duration_total", "duration_checks",
|
wantTimings(t, forwarded, "duration_total_ms", "duration_checks_ms",
|
||||||
"duration_upstream_total")
|
"duration_upstream_total_ms")
|
||||||
wantTimings(t, refused, "duration_total", "duration_checks")
|
wantTimings(t, refused, "duration_total_ms", "duration_checks_ms")
|
||||||
wantTimings(t, health, "duration_total")
|
wantTimings(t, health, "duration_total_ms")
|
||||||
|
|
||||||
if t.Failed() {
|
if t.Failed() {
|
||||||
return
|
return
|
||||||
@@ -237,9 +237,9 @@ func TestTimingsAreInOrder(t *testing.T) {
|
|||||||
|
|
||||||
// In whole microseconds, as they are logged, so that the sum below is
|
// In whole microseconds, as they are logged, so that the sum below is
|
||||||
// exact.
|
// exact.
|
||||||
total := microseconds(forwarded.DurationTotal)
|
total := microseconds(forwarded.DurationTotalMS)
|
||||||
checks := microseconds(*forwarded.DurationChecks)
|
checks := microseconds(*forwarded.DurationChecksMS)
|
||||||
upstream := microseconds(*forwarded.DurationUpstreamTotal)
|
upstream := microseconds(*forwarded.DurationUpstreamTotalMS)
|
||||||
|
|
||||||
// The checks end before the request is handed to the app.
|
// The checks end before the request is handed to the app.
|
||||||
if checks+upstream > total {
|
if checks+upstream > total {
|
||||||
@@ -247,9 +247,9 @@ func TestTimingsAreInOrder(t *testing.T) {
|
|||||||
total, checks, upstream)
|
total, checks, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *refused.DurationChecks > refused.DurationTotal {
|
if *refused.DurationChecksMS > refused.DurationTotalMS {
|
||||||
t.Errorf("refused request's checks took %v of %v milliseconds",
|
t.Errorf("refused request's checks took %v of %v milliseconds",
|
||||||
*refused.DurationChecks, refused.DurationTotal)
|
*refused.DurationChecksMS, refused.DurationTotalMS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ func TestTagLimitPercentLowersALimitTheLowestWinning(t *testing.T) {
|
|||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
line := s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
line := s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
|
wantPercent(t, "rate_limit_percent",
|
||||||
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
"25 from "+tagLimitPercent)
|
"25 from "+tagLimitPercent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +109,8 @@ func TestTagLimitPercentLowersAByteLimitByAListTag(t *testing.T) {
|
|||||||
t.Errorf("log line has limit_hit %q, want %s", line.LimitHit, minuteBytes)
|
t.Errorf("log line has limit_hit %q, want %s", line.LimitHit, minuteBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
|
wantPercent(t, "bytes_limit_percent",
|
||||||
|
line.BytesLimitPercent, line.BytesLimitPercentSetting,
|
||||||
"50 from "+tagLimitPercent)
|
"50 from "+tagLimitPercent)
|
||||||
|
|
||||||
// fromKP, which the list does not hold, has the whole limit.
|
// fromKP, which the list does not hold, has the whole limit.
|
||||||
@@ -117,5 +119,6 @@ func TestTagLimitPercentLowersAByteLimitByAListTag(t *testing.T) {
|
|||||||
t.Errorf("log line for %s has limit_hit %q, want none", fromKP, line.LimitHit)
|
t.Errorf("log line for %s has limit_hit %q, want none", fromKP, line.LimitHit)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting, none)
|
wantPercent(t, "bytes_limit_percent",
|
||||||
|
line.BytesLimitPercent, line.BytesLimitPercentSetting, none)
|
||||||
}
|
}
|
||||||
@@ -198,7 +198,7 @@ func (g *GeoJS) LookUp(ctx context.Context, client netip.Prefix) Answer {
|
|||||||
|
|
||||||
answer, found := g.kept(client)
|
answer, found := g.kept(client)
|
||||||
if !found {
|
if !found {
|
||||||
g.metrics.GeoJSUnanswered.Inc()
|
g.metrics.RequestsWithoutGeoJSAnswer.Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
w, waiting := g.waiting[client]
|
w, waiting := g.waiting[client]
|
||||||
@@ -296,7 +296,7 @@ func (g *GeoJS) answerOrWait(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if w == nil {
|
if w == nil {
|
||||||
g.metrics.GeoJSUnanswered.Inc()
|
g.metrics.RequestsWithoutGeoJSAnswer.Inc()
|
||||||
|
|
||||||
return Answer{}, nil // too many clients wait already
|
return Answer{}, nil // too many clients wait already
|
||||||
}
|
}
|
||||||
@@ -307,7 +307,7 @@ func (g *GeoJS) answerOrWait(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if w.late {
|
if w.late {
|
||||||
g.metrics.GeoJSUnanswered.Inc()
|
g.metrics.RequestsWithoutGeoJSAnswer.Inc()
|
||||||
|
|
||||||
return Answer{}, nil
|
return Answer{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -846,7 +846,7 @@ func wantAsked(t *testing.T, geojs *standIn, i int, want ...string) {
|
|||||||
func wantUnanswered(t *testing.T, m *metrics.Metrics, want float64) {
|
func wantUnanswered(t *testing.T, m *metrics.Metrics, want float64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
got := testutil.ToFloat64(m.GeoJSUnanswered)
|
got := testutil.ToFloat64(m.RequestsWithoutGeoJSAnswer)
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Errorf("%v requests went without an answer, want %v", got, want)
|
t.Errorf("%v requests went without an answer, want %v", got, want)
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-16
@@ -36,7 +36,7 @@ type Metrics struct {
|
|||||||
upstreamDuration prometheus.Histogram
|
upstreamDuration prometheus.Histogram
|
||||||
rateLimitHits *prometheus.CounterVec
|
rateLimitHits *prometheus.CounterVec
|
||||||
offences *prometheus.CounterVec
|
offences *prometheus.CounterVec
|
||||||
wafMatches *prometheus.CounterVec
|
coreRuleSetMatches *prometheus.CounterVec
|
||||||
// ruleMatches are made by AddRules, and reputationHits by
|
// ruleMatches are made by AddRules, and reputationHits by
|
||||||
// AddReputation.
|
// AddReputation.
|
||||||
ruleMatches *prometheus.CounterVec
|
ruleMatches *prometheus.CounterVec
|
||||||
@@ -45,12 +45,12 @@ type Metrics struct {
|
|||||||
asns *busiest
|
asns *busiest
|
||||||
|
|
||||||
// GeoJSRequests are the requests to GeoJS, and GeoJSFailures those
|
// GeoJSRequests are the requests to GeoJS, and GeoJSFailures those
|
||||||
// that failed. GeoJSUnanswered are the requests that needed their
|
// that failed. RequestsWithoutGeoJSAnswer are the requests that needed
|
||||||
// client's answer, for a setting that acts on it, and went on without
|
// their client's answer, for a setting that acts on it, and went on
|
||||||
// it because GeoJS had not given it in time.
|
// without it because GeoJS had not given it in time.
|
||||||
GeoJSRequests prometheus.Counter
|
GeoJSRequests prometheus.Counter
|
||||||
GeoJSFailures prometheus.Counter
|
GeoJSFailures prometheus.Counter
|
||||||
GeoJSUnanswered prometheus.Counter
|
RequestsWithoutGeoJSAnswer prometheus.Counter
|
||||||
|
|
||||||
stateFileWrites *prometheus.CounterVec
|
stateFileWrites *prometheus.CounterVec
|
||||||
stateFileWriteFailures *prometheus.CounterVec
|
stateFileWriteFailures *prometheus.CounterVec
|
||||||
@@ -101,7 +101,7 @@ func New(topN int, instanceName string) *Metrics {
|
|||||||
[]string{"window", "kind"}),
|
[]string{"window", "kind"}),
|
||||||
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",
|
coreRuleSetMatches: counterVec("sww_core_rule_set_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 SWW_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),
|
||||||
@@ -114,8 +114,8 @@ func New(topN int, instanceName string) *Metrics {
|
|||||||
Name: "sww_geojs_failures_total",
|
Name: "sww_geojs_failures_total",
|
||||||
Help: "Requests to GeoJS that failed.",
|
Help: "Requests to GeoJS that failed.",
|
||||||
}),
|
}),
|
||||||
GeoJSUnanswered: prometheus.NewCounter(prometheus.CounterOpts{
|
RequestsWithoutGeoJSAnswer: prometheus.NewCounter(prometheus.CounterOpts{
|
||||||
Name: "sww_geojs_unanswered_total",
|
Name: "sww_requests_without_geojs_answer_total",
|
||||||
Help: "Requests that needed their client's answer from GeoJS and " +
|
Help: "Requests that needed their client's answer from GeoJS and " +
|
||||||
"went on without it, because GeoJS had not given it in time.",
|
"went on without it, because GeoJS had not given it in time.",
|
||||||
}),
|
}),
|
||||||
@@ -139,9 +139,9 @@ func New(topN int, instanceName string) *Metrics {
|
|||||||
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
||||||
m.inFlight, m.requests, m.requestBytes, m.responseBytes,
|
m.inFlight, m.requests, m.requestBytes, m.responseBytes,
|
||||||
m.requestDuration, m.upstreamDuration,
|
m.requestDuration, m.upstreamDuration,
|
||||||
m.rateLimitHits, m.offences, m.wafMatches,
|
m.rateLimitHits, m.offences, m.coreRuleSetMatches,
|
||||||
m.countries, m.asns,
|
m.countries, m.asns,
|
||||||
m.GeoJSRequests, m.GeoJSFailures, m.GeoJSUnanswered,
|
m.GeoJSRequests, m.GeoJSFailures, m.RequestsWithoutGeoJSAnswer,
|
||||||
m.stateFileWrites, m.stateFileWriteFailures,
|
m.stateFileWrites, m.stateFileWriteFailures,
|
||||||
m.stateFileLastWrite, m.stateFileSize,
|
m.stateFileLastWrite, m.stateFileSize,
|
||||||
m.stateFileEditsTakenIn, m.stateFileEditsSetAside,
|
m.stateFileEditsTakenIn, m.stateFileEditsSetAside,
|
||||||
@@ -232,7 +232,7 @@ func (m *Metrics) AddRemoteLog(remote *remotelog.Sender) {
|
|||||||
return float64(remote.Dropped())
|
return float64(remote.Dropped())
|
||||||
}),
|
}),
|
||||||
prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||||
Name: "sww_remote_log_buffer_depth",
|
Name: "sww_remote_log_lines_waiting",
|
||||||
Help: "Log lines in the buffer, waiting to be sent.",
|
Help: "Log lines in the buffer, waiting to be sent.",
|
||||||
}, func() float64 {
|
}, func() float64 {
|
||||||
return float64(remote.Depth())
|
return float64(remote.Depth())
|
||||||
@@ -355,7 +355,7 @@ func (m *Metrics) AddAlerts(queue *alerts.Queue) {
|
|||||||
return float64(queue.Counts(name).Sent)
|
return float64(queue.Counts(name).Sent)
|
||||||
}),
|
}),
|
||||||
prometheus.NewCounterFunc(prometheus.CounterOpts{
|
prometheus.NewCounterFunc(prometheus.CounterOpts{
|
||||||
Name: "sww_alerts_failed_total",
|
Name: "sww_alert_send_failures_total",
|
||||||
Help: "Requests to the destination that failed.",
|
Help: "Requests to the destination that failed.",
|
||||||
ConstLabels: destination,
|
ConstLabels: destination,
|
||||||
}, func() float64 {
|
}, func() float64 {
|
||||||
@@ -427,7 +427,7 @@ func (m *Metrics) Offences(r ratelimit.Request) {
|
|||||||
"limit": r.BrokeLimit,
|
"limit": r.BrokeLimit,
|
||||||
"attack": r.Attack,
|
"attack": r.Attack,
|
||||||
"blocked_by_block_rule": r.BlockedByBlockRule,
|
"blocked_by_block_rule": r.BlockedByBlockRule,
|
||||||
"waf_blocked": r.WAFBlocked,
|
"blocked_by_core_rule_set": r.BlockedByCoreRuleSet,
|
||||||
} {
|
} {
|
||||||
if committed {
|
if committed {
|
||||||
m.offences.WithLabelValues(kind).Inc()
|
m.offences.WithLabelValues(kind).Inc()
|
||||||
@@ -441,10 +441,10 @@ func (m *Metrics) RuleMatched(id, action string) {
|
|||||||
m.ruleMatches.WithLabelValues(id, action).Inc()
|
m.ruleMatches.WithLabelValues(id, action).Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WAFMatched counts a request that matched the Core Rule Set's rule id,
|
// CoreRuleSetMatched counts a request that matched the Core Rule Set's rule id,
|
||||||
// with SWW_WAF_MODE at mode.
|
// with SWW_WAF_MODE at mode.
|
||||||
func (m *Metrics) WAFMatched(mode string, id int) {
|
func (m *Metrics) CoreRuleSetMatched(mode string, id int) {
|
||||||
m.wafMatches.WithLabelValues(mode, strconv.Itoa(id)).Inc()
|
m.coreRuleSetMatches.WithLabelValues(mode, strconv.Itoa(id)).Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateFileWritten counts a write of the state file name, of size bytes,
|
// StateFileWritten counts a write of the state file name, of size bytes,
|
||||||
|
|||||||
@@ -123,11 +123,12 @@ type Offences struct {
|
|||||||
// Limit is its requests that broke a rate limit, a byte limit or the
|
// Limit is its requests that broke a rate limit, a byte limit or the
|
||||||
// error burst, Attack those that were a clear sign of attack, a match
|
// error burst, Attack those that were a clear sign of attack, a match
|
||||||
// of a ban rule or a request for a trap path, BlockedByBlockRule those a
|
// of a ban rule or a request for a trap path, BlockedByBlockRule those a
|
||||||
// block rule refused, and WAFBlocked those the Core Rule Set refused.
|
// block rule refused, and BlockedByCoreRuleSet those the Core Rule Set
|
||||||
|
// refused.
|
||||||
Limit int64 `json:"limit"`
|
Limit int64 `json:"limit"`
|
||||||
Attack int64 `json:"attack"`
|
Attack int64 `json:"attack"`
|
||||||
BlockedByBlockRule int64 `json:"blocked_by_block_rule"`
|
BlockedByBlockRule int64 `json:"blocked_by_block_rule"`
|
||||||
WAFBlocked int64 `json:"waf_blocked"`
|
BlockedByCoreRuleSet int64 `json:"blocked_by_core_rule_set"`
|
||||||
// TokenRefused is what a clients.json written by an earlier version gives,
|
// TokenRefused is what a clients.json written by an earlier version gives,
|
||||||
// which must still load. Nothing adds to it.
|
// which must still load. Nothing adds to it.
|
||||||
TokenRefused int64 `json:"token_refused,omitempty"`
|
TokenRefused int64 `json:"token_refused,omitempty"`
|
||||||
@@ -148,11 +149,11 @@ type Request struct {
|
|||||||
// BrokeLimit is true for a request that broke a rate limit, a byte
|
// BrokeLimit is true for a request that broke a rate limit, a byte
|
||||||
// limit or the error burst, Attack for one that matched a ban rule or
|
// limit or the error burst, Attack for one that matched a ban rule or
|
||||||
// asked for a trap path, BlockedByBlockRule for one a block rule refused,
|
// asked for a trap path, BlockedByBlockRule for one a block rule refused,
|
||||||
// and WAFBlocked for one the Core Rule Set refused.
|
// and BlockedByCoreRuleSet for one the Core Rule Set refused.
|
||||||
BrokeLimit bool
|
BrokeLimit bool
|
||||||
Attack bool
|
Attack bool
|
||||||
BlockedByBlockRule bool
|
BlockedByBlockRule bool
|
||||||
WAFBlocked bool
|
BlockedByCoreRuleSet bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a Limiter for limits, with no client counted yet, whose
|
// New returns a Limiter for limits, with no client counted yet, whose
|
||||||
@@ -327,8 +328,8 @@ func (l *Limiter) AddToHistory(client netip.Prefix, now time.Time, r Request) {
|
|||||||
h.Offences.BlockedByBlockRule++
|
h.Offences.BlockedByBlockRule++
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.WAFBlocked {
|
if r.BlockedByCoreRuleSet {
|
||||||
h.Offences.WAFBlocked++
|
h.Offences.BlockedByCoreRuleSet++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ const (
|
|||||||
// block rule.
|
// block rule.
|
||||||
ActionBlockedByBlockRule = "blocked_by_block_rule"
|
ActionBlockedByBlockRule = "blocked_by_block_rule"
|
||||||
|
|
||||||
// ActionWAFBlocked is a request refused because the Core Rule Set
|
// ActionBlockedByCoreRuleSet is a request refused because the Core Rule
|
||||||
// scored it at or over SWW_WAF_ANOMALY_THRESHOLD.
|
// Set scored it at or over SWW_WAF_ANOMALY_THRESHOLD.
|
||||||
ActionWAFBlocked = "waf_blocked"
|
ActionBlockedByCoreRuleSet = "blocked_by_core_rule_set"
|
||||||
|
|
||||||
// ActionClientDenied is a request refused because its client is in
|
// ActionClientDenied is a request refused because its client is in
|
||||||
// SWW_DENY_NETS, in a blocklist while SWW_BLOCKLIST_ACTION is deny,
|
// SWW_DENY_NETS, in a blocklist while SWW_BLOCKLIST_ACTION is deny,
|
||||||
@@ -55,9 +55,9 @@ const (
|
|||||||
// ActionCountryDenied is a request refused because its client's country
|
// ActionCountryDenied is a request refused because its client's country
|
||||||
// is not in SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
// is not in SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
||||||
ActionCountryDenied = "country_denied"
|
ActionCountryDenied = "country_denied"
|
||||||
// ActionAdmin is a request discriminator answered at one of its own
|
// ActionOwnEndpoint is a request discriminator answered at one of its own
|
||||||
// endpoints, or a question about a path under /_sww/.
|
// endpoints, or a question about a path under /_sww/.
|
||||||
ActionAdmin = "admin"
|
ActionOwnEndpoint = "own_endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OffenceLimit is the offence a request line names for a request that
|
// OffenceLimit is the offence a request line names for a request that
|
||||||
@@ -65,9 +65,9 @@ const (
|
|||||||
// limit.
|
// limit.
|
||||||
const OffenceLimit = "limit"
|
const OffenceLimit = "limit"
|
||||||
|
|
||||||
// LimitHitErrorBurst is the limit_hit a request line names for a request
|
// LimitHitMinuteRefusals is the limit_hit a request line names for a request
|
||||||
// that broke the error burst.
|
// that broke the error burst.
|
||||||
const LimitHitErrorBurst = "error_burst"
|
const LimitHitMinuteRefusals = "minute_refusals"
|
||||||
|
|
||||||
// The discriminator_line a request line names, which says which of
|
// The discriminator_line a request line names, which says which of
|
||||||
// discriminator's two lines about a question it is.
|
// discriminator's two lines about a question it is.
|
||||||
@@ -141,8 +141,9 @@ type Line struct {
|
|||||||
// WouldAction is, in observe mode, the action enforce mode would have
|
// WouldAction is, in observe mode, the action enforce mode would have
|
||||||
// taken with a request it would have refused: ActionClientDenied,
|
// taken with a request it would have refused: ActionClientDenied,
|
||||||
// ActionTemporarilyBanned, ActionPermanentlyBanned, ActionCountryDenied,
|
// ActionTemporarilyBanned, ActionPermanentlyBanned, ActionCountryDenied,
|
||||||
// ActionClientIsRateLimited, ActionBlockedByBlockRule, ActionWAFBlocked,
|
// ActionClientIsRateLimited, ActionBlockedByBlockRule,
|
||||||
// or, in discriminator, ActionRequestHeaderTooLarge for the header limits.
|
// ActionBlockedByCoreRuleSet, or, in discriminator,
|
||||||
|
// ActionRequestHeaderTooLarge for the header limits.
|
||||||
WouldAction string `json:"would_action,omitempty"`
|
WouldAction string `json:"would_action,omitempty"`
|
||||||
// Heuristic is the name of the heuristic that refused the request, in
|
// Heuristic is the name of the heuristic that refused the request, in
|
||||||
// observe mode too, and HeuristicFigures are the figures it acted on, by
|
// observe mode too, and HeuristicFigures are the figures it acted on, by
|
||||||
@@ -154,15 +155,15 @@ type Line struct {
|
|||||||
// request.
|
// request.
|
||||||
AfterAnswerHeuristic string `json:"after_answer_heuristic,omitempty"`
|
AfterAnswerHeuristic string `json:"after_answer_heuristic,omitempty"`
|
||||||
AfterAnswerHeuristicFigures map[string]any `json:"after_answer_heuristic_figures,omitempty"` //nolint:lll // a struct tag cannot be split
|
AfterAnswerHeuristicFigures map[string]any `json:"after_answer_heuristic_figures,omitempty"` //nolint:lll // a struct tag cannot be split
|
||||||
// LimitPercent and LimitPercentSetting are, for a request the rate
|
// RateLimitPercent and RateLimitPercentSetting are, for a request the
|
||||||
// limits counted whose client a biased threshold gives a percentage of
|
// rate limits counted whose client a biased threshold gives a percentage
|
||||||
// the rate limits below 100, that percentage and the setting that gave
|
// of the rate limits below 100, that percentage and the setting that gave
|
||||||
// it. BytesPercent and BytesPercentSetting are the same for the byte
|
// it. BytesLimitPercent and BytesLimitPercentSetting are the same for the
|
||||||
// limits.
|
// byte limits.
|
||||||
LimitPercent *int64 `json:"limit_percent,omitempty"`
|
RateLimitPercent *int64 `json:"rate_limit_percent,omitempty"`
|
||||||
LimitPercentSetting string `json:"limit_percent_setting,omitempty"`
|
RateLimitPercentSetting string `json:"rate_limit_percent_setting,omitempty"`
|
||||||
BytesPercent *int64 `json:"bytes_percent,omitempty"`
|
BytesLimitPercent *int64 `json:"bytes_limit_percent,omitempty"`
|
||||||
BytesPercentSetting string `json:"bytes_percent_setting,omitempty"`
|
BytesLimitPercentSetting string `json:"bytes_limit_percent_setting,omitempty"`
|
||||||
// Counts are, for a request the rate limits counted, the client's
|
// Counts are, for a request the rate limits counted, the client's
|
||||||
// requests as they counted them with this one, and its bytes as the
|
// requests as they counted them with this one, and its bytes as the
|
||||||
// byte limits counted them, with this request's once it has ended if
|
// byte limits counted them, with this request's once it has ended if
|
||||||
@@ -170,15 +171,15 @@ type Line struct {
|
|||||||
Counts ratelimit.Counts `json:"counts,omitzero"`
|
Counts ratelimit.Counts `json:"counts,omitzero"`
|
||||||
// RuleIDs are the ids of the rule file rules the request matched.
|
// RuleIDs are the ids of the rule file rules the request matched.
|
||||||
RuleIDs []string `json:"rule_ids,omitempty"`
|
RuleIDs []string `json:"rule_ids,omitempty"`
|
||||||
// WAFRuleIDs are the ids of the Core Rule Set's rules the request
|
// CoreRuleSetRuleIDs are the ids of the Core Rule Set's rules the request
|
||||||
// matched, and WAFScore its anomaly score, nil for a request the Core
|
// matched, and CoreRuleSetScore its anomaly score, nil for a request the
|
||||||
// Rule Set did not inspect.
|
// Core Rule Set did not inspect.
|
||||||
WAFRuleIDs []int `json:"waf_rule_ids,omitempty"`
|
CoreRuleSetRuleIDs []int `json:"core_rule_set_rule_ids,omitempty"`
|
||||||
WAFScore *int `json:"waf_score,omitempty"`
|
CoreRuleSetScore *int `json:"core_rule_set_score,omitempty"`
|
||||||
// LimitHit is the window whose limit the request went over, named as
|
// LimitHit is the window whose limit the request went over, named as
|
||||||
// Counts names its count: minute, hour or day for a rate limit, and
|
// Counts names its count: minute, hour or day for a rate limit, and
|
||||||
// minute_bytes, hour_bytes or day_bytes for a byte limit; or
|
// minute_bytes, hour_bytes or day_bytes for a byte limit; or
|
||||||
// LimitHitErrorBurst for the error burst.
|
// LimitHitMinuteRefusals for the error burst.
|
||||||
LimitHit string `json:"limit_hit,omitempty"`
|
LimitHit string `json:"limit_hit,omitempty"`
|
||||||
// Reputation are the URLs of the blocklists that list the client, then
|
// Reputation are the URLs of the blocklists that list the client, then
|
||||||
// that of the CrowdSec decision list when it does, then the DNSBL zones
|
// that of the CrowdSec decision list when it does, then the DNSBL zones
|
||||||
@@ -197,15 +198,15 @@ type Line struct {
|
|||||||
// ends: a time, or "permanent".
|
// ends: a time, or "permanent".
|
||||||
BanExpires string `json:"ban_expires,omitempty"`
|
BanExpires string `json:"ban_expires,omitempty"`
|
||||||
|
|
||||||
// The timings, in milliseconds. DurationChecks is the time until the
|
// The timings. DurationChecksMS is the time until the checks were done, and
|
||||||
// checks were done, and DurationWAF the part of it the Core Rule Set
|
// DurationCoreRuleSetMS the part of it the Core Rule Set took.
|
||||||
// took. DurationUpstreamTotal is what the report of an allowed request gives,
|
// DurationUpstreamTotalMS is what the report of an allowed request gives,
|
||||||
// from the hand-over to the app until the end of its answer. Each but
|
// from the hand-over to the app until the end of its answer. Each but
|
||||||
// DurationTotal is nil for a request that did not get that far.
|
// DurationTotalMS is nil for a request that did not get that far.
|
||||||
DurationTotal float64 `json:"duration_total"`
|
DurationTotalMS float64 `json:"duration_total_ms"`
|
||||||
DurationChecks *float64 `json:"duration_checks,omitempty"`
|
DurationChecksMS *float64 `json:"duration_checks_ms,omitempty"`
|
||||||
DurationWAF *float64 `json:"duration_waf,omitempty"`
|
DurationCoreRuleSetMS *float64 `json:"duration_core_rule_set_ms,omitempty"`
|
||||||
DurationUpstreamTotal *float64 `json:"duration_upstream_total,omitempty"`
|
DurationUpstreamTotalMS *float64 `json:"duration_upstream_total_ms,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RProxyLine is rproxy's short line in the request log about one request. The
|
// RProxyLine is rproxy's short line in the request log about one request. The
|
||||||
@@ -225,11 +226,11 @@ type RProxyLine struct {
|
|||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
RequestBytes int64 `json:"request_bytes"`
|
RequestBytes int64 `json:"request_bytes"`
|
||||||
ResponseBytes int64 `json:"response_bytes"`
|
ResponseBytes int64 `json:"response_bytes"`
|
||||||
// DurationRequest runs in milliseconds from the request's headers being read
|
// DurationRequestMS runs from the request's headers being read until the app
|
||||||
// until the app has been sent the whole request, or the answer has started
|
// has been sent the whole request, or the answer has started if that is
|
||||||
// if that is sooner. DurationResponse runs from then until the answer ends.
|
// sooner. DurationResponseMS runs from then until the answer ends.
|
||||||
DurationRequest float64 `json:"duration_request"`
|
DurationRequestMS float64 `json:"duration_request_ms"`
|
||||||
DurationResponse float64 `json:"duration_response"`
|
DurationResponseMS float64 `json:"duration_response_ms"`
|
||||||
// Disposition is the valid disposition discriminator answered or, while
|
// Disposition is the valid disposition discriminator answered or, while
|
||||||
// DefaultDisposition is true, the value of SWRP_DEFAULT_DISPOSITION. It is ""
|
// DefaultDisposition is true, the value of SWRP_DEFAULT_DISPOSITION. It is ""
|
||||||
// when neither gave one.
|
// when neither gave one.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) {
|
|||||||
ClientIP: "203.0.113.9",
|
ClientIP: "203.0.113.9",
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Action: requestlog.ActionForward,
|
Action: requestlog.ActionForward,
|
||||||
DurationTotal: requestlog.Milliseconds(1500 * time.Microsecond),
|
DurationTotalMS: requestlog.Milliseconds(1500 * time.Microsecond),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("write: %v", err)
|
t.Fatalf("write: %v", err)
|
||||||
@@ -43,7 +43,7 @@ func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) {
|
|||||||
want := map[string]any{
|
want := map[string]any{
|
||||||
"type": "request", "time": "2026-10-03T12:00:00.000Z",
|
"type": "request", "time": "2026-10-03T12:00:00.000Z",
|
||||||
"client_ip": "203.0.113.9", "status": 200.0, "action": "forward",
|
"client_ip": "203.0.113.9", "status": 200.0, "action": "forward",
|
||||||
"duration_total": 1.5,
|
"duration_total_ms": 1.5,
|
||||||
}
|
}
|
||||||
for name, value := range want {
|
for name, value := range want {
|
||||||
if fields[name] != value {
|
if fields[name] != value {
|
||||||
@@ -55,9 +55,9 @@ func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) {
|
|||||||
"forwarded_for", "content_type", "content_length", "request_headers",
|
"forwarded_for", "content_type", "content_length", "request_headers",
|
||||||
"has_authorization", "has_cookie", "websocket", "response_content_type",
|
"has_authorization", "has_cookie", "websocket", "response_content_type",
|
||||||
"upstream_status", "cache_control", "location", "aborted", "counts",
|
"upstream_status", "cache_control", "location", "aborted", "counts",
|
||||||
"limit_hit", "offence", "ban_expires", "duration_checks",
|
"limit_hit", "offence", "ban_expires", "duration_checks_ms",
|
||||||
"duration_upstream_connect", "duration_upstream_first_byte",
|
"duration_upstream_connect", "duration_upstream_first_byte",
|
||||||
"duration_upstream_total",
|
"duration_upstream_total_ms",
|
||||||
}
|
}
|
||||||
for _, name := range unset {
|
for _, name := range unset {
|
||||||
_, present := fields[name]
|
_, present := fields[name]
|
||||||
@@ -82,7 +82,7 @@ func TestWriteRProxyLineWritesOneJSONLineWithOnlyItsFields(t *testing.T) {
|
|||||||
|
|
||||||
const want = `{"type":"request","time":"","request_id":"an-id","client_ip":"",` +
|
const want = `{"type":"request","time":"","request_id":"an-id","client_ip":"",` +
|
||||||
`"host":"","method":"","path":"","query":"","status":0,"request_bytes":0,` +
|
`"host":"","method":"","path":"","query":"","status":0,"request_bytes":0,` +
|
||||||
`"response_bytes":0,"duration_request":0,"duration_response":0,` +
|
`"response_bytes":0,"duration_request_ms":0,"duration_response_ms":0,` +
|
||||||
`"disposition":"allow"}` + "\n"
|
`"disposition":"allow"}` + "\n"
|
||||||
if out.String() != want {
|
if out.String() != want {
|
||||||
t.Errorf("wrote %s, want %s", out.String(), want)
|
t.Errorf("wrote %s, want %s", out.String(), want)
|
||||||
|
|||||||
@@ -371,8 +371,8 @@ func (rq *request) finish(ctx context.Context) {
|
|||||||
Status: rq.out.status,
|
Status: rq.out.status,
|
||||||
RequestBytes: requestBytes,
|
RequestBytes: requestBytes,
|
||||||
ResponseBytes: responseBytes,
|
ResponseBytes: responseBytes,
|
||||||
DurationRequest: requestlog.Milliseconds(requestEnd.Sub(rq.start)),
|
DurationRequestMS: requestlog.Milliseconds(requestEnd.Sub(rq.start)),
|
||||||
DurationResponse: requestlog.Milliseconds(end.Sub(requestEnd)),
|
DurationResponseMS: requestlog.Milliseconds(end.Sub(requestEnd)),
|
||||||
Disposition: rq.disposition,
|
Disposition: rq.disposition,
|
||||||
DefaultDisposition: rq.byDefault,
|
DefaultDisposition: rq.byDefault,
|
||||||
})
|
})
|
||||||
@@ -389,10 +389,10 @@ func (rq *request) finish(ctx context.Context) {
|
|||||||
Status: min(rq.out.status, reportStatusMax),
|
Status: min(rq.out.status, reportStatusMax),
|
||||||
RequestBytes: requestBytes,
|
RequestBytes: requestBytes,
|
||||||
ResponseBytes: responseBytes,
|
ResponseBytes: responseBytes,
|
||||||
DurationTotal: requestlog.Milliseconds(end.Sub(rq.start)),
|
DurationTotalMS: requestlog.Milliseconds(end.Sub(rq.start)),
|
||||||
}
|
}
|
||||||
if !rq.upstreamStart.IsZero() {
|
if !rq.upstreamStart.IsZero() {
|
||||||
report.DurationUpstreamTotal = requestlog.Milliseconds(end.Sub(rq.upstreamStart))
|
report.DurationUpstreamTotalMS = requestlog.Milliseconds(end.Sub(rq.upstreamStart))
|
||||||
}
|
}
|
||||||
|
|
||||||
rq.h.sendReport(ctx, report)
|
rq.h.sendReport(ctx, report)
|
||||||
|
|||||||
@@ -727,7 +727,7 @@ func TestDenyGivesTheVisitorDiscriminatorsAnswer(t *testing.T) {
|
|||||||
report := standIn.report(t)
|
report := standIn.report(t)
|
||||||
if report.Status != http.StatusForbidden ||
|
if report.Status != http.StatusForbidden ||
|
||||||
report.ResponseBytes != int64(len(deniedBody)) ||
|
report.ResponseBytes != int64(len(deniedBody)) ||
|
||||||
report.DurationUpstreamTotal != 0 {
|
report.DurationUpstreamTotalMS != 0 {
|
||||||
t.Errorf("report %+v, of a request denied", report)
|
t.Errorf("report %+v, of a request denied", report)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,9 +871,9 @@ func TestALineForARequestClosedWithoutAnAnswerHasNoNegativeTime(t *testing.T) {
|
|||||||
|
|
||||||
line := out.requestLine(t)
|
line := out.requestLine(t)
|
||||||
|
|
||||||
duration, ok := line["duration_response"].(float64)
|
duration, ok := line["duration_response_ms"].(float64)
|
||||||
if !ok || duration < 0 {
|
if !ok || duration < 0 {
|
||||||
t.Errorf("line %v, want a duration_response that is not negative", line)
|
t.Errorf("line %v, want a duration_response_ms that is not negative", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1428,8 +1428,8 @@ func TestReportGivesWhatHappened(t *testing.T) {
|
|||||||
if report.RequestID == "" || report.Status != http.StatusOK ||
|
if report.RequestID == "" || report.Status != http.StatusOK ||
|
||||||
report.RequestBytes != int64(len(body)) ||
|
report.RequestBytes != int64(len(body)) ||
|
||||||
report.ResponseBytes != int64(len(greeting)) ||
|
report.ResponseBytes != int64(len(greeting)) ||
|
||||||
report.DurationTotal <= 0 || report.DurationUpstreamTotal <= 0 ||
|
report.DurationTotalMS <= 0 || report.DurationUpstreamTotalMS <= 0 ||
|
||||||
report.DurationUpstreamTotal > report.DurationTotal {
|
report.DurationUpstreamTotalMS > report.DurationTotalMS {
|
||||||
t.Errorf("report %+v, of %d bytes up and %d back", report, len(body), len(greeting))
|
t.Errorf("report %+v, of %d bytes up and %d back", report, len(body), len(greeting))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1748,7 +1748,7 @@ func TestLogLineGivesTheRequestAsSent(t *testing.T) {
|
|||||||
t.Errorf("default_disposition is %v, want it left out", line["default_disposition"])
|
t.Errorf("default_disposition is %v, want it left out", line["default_disposition"])
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range []string{"duration_request", "duration_response"} {
|
for _, name := range []string{"duration_request_ms", "duration_response_ms"} {
|
||||||
duration, ok := line[name].(float64)
|
duration, ok := line[name].(float64)
|
||||||
if !ok || duration <= 0 {
|
if !ok || duration <= 0 {
|
||||||
t.Errorf("%s is %v, want a time", name, line[name])
|
t.Errorf("%s is %v, want a time", name, line[name])
|
||||||
|
|||||||
@@ -891,7 +891,7 @@ func missingFromCounter(counter anomaly.Counter) string {
|
|||||||
return "netblock"
|
return "netblock"
|
||||||
case scope == anomaly.ScopeASN && counter.ASN == "":
|
case scope == anomaly.ScopeASN && counter.ASN == "":
|
||||||
return "asn"
|
return "asn"
|
||||||
case scope == anomaly.ScopeWatch && counter.Name == "":
|
case scope == anomaly.ScopeWatchedNet && counter.Name == "":
|
||||||
return "name"
|
return "name"
|
||||||
case countsWithoutStart(counter.Minute):
|
case countsWithoutStart(counter.Minute):
|
||||||
return "minute.start"
|
return "minute.start"
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ const filledAlertsJSON = `{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"scope": "watch",
|
"scope": "watched_net",
|
||||||
"netblock": "203.0.113.0/24",
|
"netblock": "203.0.113.0/24",
|
||||||
"name": "office",
|
"name": "office",
|
||||||
"hour": {
|
"hour": {
|
||||||
@@ -831,12 +831,12 @@ func TestAlertsJSONEntryWithoutAFieldItNeedsStopsTheStart(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"an anomaly counter of a named netblock without its name",
|
"an anomaly counter of a named netblock without its name",
|
||||||
`{"version": 1, "anomaly_counters": [` +
|
`{"version": 1, "anomaly_counters": [` +
|
||||||
`{"scope": "watch", "netblock": "203.0.113.0/24"}]}`,
|
`{"scope": "watched_net", "netblock": "203.0.113.0/24"}]}`,
|
||||||
`: anomaly_counters entry 1 has no "name"`,
|
`: anomaly_counters entry 1 has no "name"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"an anomaly counter of a named netblock without its netblock",
|
"an anomaly counter of a named netblock without its netblock",
|
||||||
`{"version": 1, "anomaly_counters": [{"scope": "watch", "name": "office"}]}`,
|
`{"version": 1, "anomaly_counters": [{"scope": "watched_net", "name": "office"}]}`,
|
||||||
`: anomaly_counters entry 1 has no "netblock"`,
|
`: anomaly_counters entry 1 has no "netblock"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1729,7 +1729,7 @@ func newParams(dir string) state.Params {
|
|||||||
Dir: dir,
|
Dir: dir,
|
||||||
WriteDelay: time.Hour,
|
WriteDelay: time.Hour,
|
||||||
CounterInterval: time.Hour,
|
CounterInterval: time.Hour,
|
||||||
Ledger: bans.New(bans.Rules{
|
Ledger: bans.New(bans.Params{
|
||||||
LimitBanDuration: time.Hour,
|
LimitBanDuration: time.Hour,
|
||||||
LimitBanRepeatWindow: 24 * time.Hour,
|
LimitBanRepeatWindow: 24 * time.Hour,
|
||||||
MaxBanDuration: 7 * 24 * time.Hour,
|
MaxBanDuration: 7 * 24 * time.Hour,
|
||||||
|
|||||||
+2
-2
@@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
||||||
@@ -29,7 +30,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
|
||||||
@@ -197,7 +197,7 @@ func RunDiscriminator(ctx context.Context, params Params) int {
|
|||||||
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,
|
crs *coreruleset.CoreRuleSet,
|
||||||
) (*discriminator.Server, error) {
|
) (*discriminator.Server, error) {
|
||||||
var lookupFile *lookup.File
|
var lookupFile *lookup.File
|
||||||
|
|
||||||
|
|||||||
@@ -962,7 +962,7 @@ func TestStalledRemoteLogEndpointHoldsUpNoRequest(t *testing.T) {
|
|||||||
metrics := metricsText(t, url+"_sww/metrics", token)
|
metrics := metricsText(t, url+"_sww/metrics", token)
|
||||||
for _, series := range []string{
|
for _, series := range []string{
|
||||||
`sww_remote_log_lines_sent_total{instance="fsn1app1/blog"} 0`,
|
`sww_remote_log_lines_sent_total{instance="fsn1app1/blog"} 0`,
|
||||||
`sww_remote_log_buffer_depth{instance="fsn1app1/blog"} 1`,
|
`sww_remote_log_lines_waiting{instance="fsn1app1/blog"} 1`,
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(metrics, "\n"+series+"\n") {
|
if !strings.Contains(metrics, "\n"+series+"\n") {
|
||||||
t.Errorf("no %q in the metrics:\n%s", series, metrics)
|
t.Errorf("no %q in the metrics:\n%s", series, metrics)
|
||||||
@@ -1068,8 +1068,12 @@ func TestBanIsAlertedAndAnAlertNotSentIsKeptAcrossARestart(t *testing.T) {
|
|||||||
metrics := metricsWith(t, url+"_sww/metrics", token,
|
metrics := metricsWith(t, url+"_sww/metrics", token,
|
||||||
"\nsww_alerts_sent_total"+ofWebhook+" 1\n")
|
"\nsww_alerts_sent_total"+ofWebhook+" 1\n")
|
||||||
|
|
||||||
for _, series := range []string{"failed", "suppressed", "dropped"} {
|
for _, series := range []string{
|
||||||
zero := "\nsww_alerts_" + series + "_total" + ofWebhook + " 0\n"
|
"sww_alert_send_failures_total",
|
||||||
|
"sww_alerts_suppressed_total",
|
||||||
|
"sww_alerts_dropped_total",
|
||||||
|
} {
|
||||||
|
zero := "\n" + series + ofWebhook + " 0\n"
|
||||||
if !strings.Contains(metrics, zero) {
|
if !strings.Contains(metrics, zero) {
|
||||||
t.Errorf("no %q in the metrics:\n%s", zero, metrics)
|
t.Errorf("no %q in the metrics:\n%s", zero, metrics)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user