Author SHA1 Message Date
sneak 7fcd79c182 SWW_APPS: an app's Core Rule Set fixes apply only to the hostnames that serve it (closes #220)
check / check (push) Successful in 4m25s
gitea's fixes (its parameters, its cookies and gitea.conf) now apply
only to a hostname SWW_APPS lists gitea for, or to every hostname while
it is unset. Each app's fixes are one .conf file, whose first rule skips
the rest unless Inspect sets tx.app_<name>, and one entry in apps.

A hostname in SWW_APPS that is not a DNS name, such as *.example.com,
stops the start. Unset, SWW_APPS is logged at start as null; set
empty, as "".

Model: opus-5-5
2026-11-09 04:49:19 +00:00
81 changed files with 1346 additions and 2101 deletions

No files matched your search

+17 -18
View File
@@ -173,7 +173,7 @@ does.
the request have `"action":"forward"`.
- To see a refusal without banning yourself, run `discriminator` with
`SWW_MODE=observe` and ask for `/.env`. The request reaches the app.
`discriminator`'s log lines have `"would_action":"temporarily_banned"` and
`discriminator`'s log lines have `"would_action":"banned"` and
`"rule_ids":["env-file"]`. A ban you get in `enforce` mode is lifted as
"Clients and bans" below describes.
- With `SWW_METRICS_TOKEN` set, this answers with the metrics, from a host on
@@ -221,7 +221,7 @@ account, a key, a token or an alert destination stays off until you give one.
Every setting the code reads, with its default:
| 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. |
@@ -280,6 +280,7 @@ Every setting the code reads, with its default:
| `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_APPS` | unset | The apps each hostname serves, such as `git.example.com:gitea;cloud.example.com:gitea`; only those apps' fixes apply there. Unset, every app's fixes apply to every hostname. |
| `SWW_BLOCKLIST_URLS` | empty | Blocklists to fetch, text files of addresses and netblocks one to a line, such as `https://www.spamhaus.org/drop/drop.txt`. |
| `SWW_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`. |
@@ -311,7 +312,7 @@ Every setting the code reads, with its default:
| `SWW_ALERT_SLACK_WEBHOOK_URL` | unset | A Slack incoming webhook each alert is posted to as a message. |
| `SWW_ALERT_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_EVENTS` | `ban,permanent_ban,waf_block,anomaly,reputation_hit,source_failure,file_error` | The events alerts are sent for; the hourly `summary` is sent whatever it says. |
| `SWW_ALERT_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. |
@@ -469,7 +470,7 @@ services:
# Alerts to an ntfy topic, with its token read from a file.
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
SWW_ALERT_EVENTS: ban,permanent_ban,waf_block,source_failure,file_error
volumes:
- discriminator-state:/var/lib/sww
- /srv/ipinfo:/var/lib/ipinfo:ro
@@ -581,16 +582,15 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
headers of every request, and no body unless `SWW_WAF_BODY_LIMIT` is set. In
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 nine ways so that ordinary requests
get through to a code forge and to Drupal. On a code forge they include git
over HTTP and sign-in through Git Credential Manager. On gitea's own paths,
its searches, file names and branch names get through too. On Drupal's own
paths, its searches, names and return addresses 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. The Core Rule Set lets
through the ordinary requests recorded from each of them:
- gitea;
- Drupal.
- `smallwebwaf` changes the Core Rule Set in six ways so that it lets ordinary
requests to a code forge through, such as git over HTTP and sign-in through
Git Credential Manager. No setting undoes the changes; "Attack detection" in
[`SPEC.md`](SPEC.md) lists them.
- Each supported app also has its own fixes, which let its ordinary requests
through. gitea's let its searches, file names and branch names through.
`SWW_APPS` names the apps each hostname serves. A hostname gets only its own
apps' fixes, and a hostname it does not list gets none. Unset, every app's
fixes apply to every hostname.
- `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
@@ -637,10 +637,9 @@ is set aside as `<name>.bad`, and never stops the running process (see
- `internal/rproxy`: the reverse proxy in front of one app.
- `internal/discriminator`: `discriminator`'s server, which runs the checks and
answers its own endpoints.
- `internal/ratelimit`, `internal/bans`, `internal/rules`,
`internal/coreruleset`, `internal/lookup`, `internal/reputation`,
`internal/routewatch`, `internal/anomaly` and `internal/alerts`: one check or
source each.
- `internal/ratelimit`, `internal/bans`, `internal/rules`, `internal/waf`,
`internal/lookup`, `internal/reputation`, `internal/routewatch`,
`internal/anomaly` and `internal/alerts`: one check or source each.
- `internal/state`, `internal/requestlog`, `internal/remotelog` and
`internal/metrics`: the state files, the log lines, the syslog sender and the
metrics.
+82 -94
View File
@@ -81,8 +81,8 @@ to them.
of bans is full.
- `internal/rules`: reads the rule files at start and again when they
change, and tells which rules a request matches.
- `internal/coreruleset`: the Core Rule Set, with the nine changes listed
under "Attack detection", as Coraza directives.
- `internal/waf`: the Core Rule Set, with the six changes and the apps'
fixes listed under "Attack detection", as Coraza directives.
- `internal/lookup`: looks clients up through GeoJS or in the lookup
database.
- `internal/reputation`: fetches the lists, asks the DNS blocklists and
@@ -321,13 +321,6 @@ ban is dropped alike. A scanner whose ban was dropped that way is refused and
banned again by its next probe. A ban whose cause is `admin` is never dropped
and does not count toward `SWW_MAX_BANS`, so such bans can never fill the table.
A request refused under a ban that runs out is logged with the action
`temporarily_banned`. Under a permanent ban the action is `permanently_banned`.
A request refused for a clear sign of attack or a CrowdSec decision takes the
action for the ban it made. A request that makes a ban permanent is logged as
`permanently_banned`. A request over a rate limit is logged as
`client_is_rate_limited`, whichever ban it makes.
## Settings
`docs/settings.md` lists every setting with its default and the values it takes,
@@ -587,10 +580,9 @@ lower case, except a country code, which is in capitals. `SWW_TAG_DENY` and
Two settings act on tags:
- `SWW_TAG_DENY` lists tags whose requests are refused with the action
`client_denied` (`tag_deny`). The check runs twice: just before the rate
limits, and again after the Core Rule Set, so that `exploitsignature` can
refuse too.
- `SWW_TAG_DENY` lists tags whose requests are refused with the action `denied`
(`tag_deny`). The check runs twice: just before the rate limits, and again
after the Core Rule Set, so that `exploitsignature` can refuse too.
- `SWW_TAG_LIMIT_PERCENT` lists tags with a percentage from 1 to 99. Their
clients get that percentage of every request and byte limit. The lowest
percentage among a client's tags applies. `exploitsignature` cannot be listed,
@@ -655,7 +647,7 @@ files, the limits and the bans still apply to the client that sends it.
match, and scores are still logged. A request with more than 1000 query
parameters or body fields adds 5 to its score, since Coraza reads only the
first 1000. For a match, in `block` and `detect` mode alike, the log line
gives the rule ids and the score, and a `core_rule_set_match` alert is sent.
gives the rule ids and the score, and a `waf_block` alert is sent.
- `SWW_WAF_DISABLED_RULES` switches off rule ids when an app trips a false
positive. The default switches off six rules:
- 920340, 920420 and 920640 refuse a request for the type of its body, when
@@ -674,8 +666,7 @@ files, the limits and the bans still apply to the client that sends it.
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` names a file of exclusions, written as
Coraza directives. An exclusion switches one rule off for some fields or paths
only, where `SWW_WAF_DISABLED_RULES` switches it off everywhere. The file
holds an app's own exclusions; the eighth and ninth changes below hold gitea's
and Drupal's.
holds an app's own exclusions; gitea's fixes below hold gitea's.
- `discriminator` loads the file after `smallwebwaf`'s own setup rules and
before the Core Rule Set's rules. The Core Rule Set loads its own
`REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf` at the same point.
@@ -721,9 +712,9 @@ files, the limits and the bans still apply to the client that sends it.
scanner from an ordinary client. In front of a code forge, registry and git
clients get `404` and `401` by design, hundreds of times a minute.
`smallwebwaf` changes the Core Rule Set 4.25.0 in nine ways, since in front of
gitea and Drupal it would otherwise refuse ordinary requests. The changes hold
in front of every app, and no setting undoes them:
`smallwebwaf` changes the Core Rule Set 4.25.0 in six ways, since in front of
gitea it would otherwise refuse ordinary requests. The changes hold in front of
every app, and no setting undoes them:
1. `PUT`, `PATCH` and `DELETE` are allowed methods besides `GET`, `HEAD`, `POST`
and `OPTIONS`. APIs, container image pushes and package uploads use them.
@@ -740,8 +731,52 @@ in front of every app, and no setting undoes them:
and tea, which gitea registers for OAuth sign-in, ask to be sent back to
`http://127.0.0.1` on the user's own machine. The server never fetches that
address.
4. Ten query parameters are not checked against the Core Rule Set's lists of
system files (930120), shell paths (932160) and command names (932260):
4. `Referer` is not checked for a Unix command given without arguments (932340),
for Java starting a process (944110) or for a Java class name (944130).
`Referer` names the page a request comes from, on everything that page loads
and on every link followed from it. The Core Rule Set takes some page
addresses for attacks:
- a search for `env`;
- OpenJDK's `Runtime.java`, whose path holds both `runtime` and `java.`;
- a file named `java.lang.String.md`.
Every request from such a page would be refused. `Referer` keeps the other
rules, except after some gitea searches (gitea's fixes). An app that passes
`Referer` to a shell must check it itself.
5. Responses are not inspected. A raw file from a repository, such as a shell
script, looks to the response rules like source code leaking from the server.
6. The `from` parameter of a container registry's upload path,
`/v2/<image>/blobs/uploads/`, is not checked for command names (932260). When
a push reuses a layer, `from` names the image the layer comes from. An image
such as `owner/python3-app` would have that push refused.
Each supported app also has its own fixes, which let its ordinary requests
through. An app's fixes hold for the hostnames `SWW_APPS` lists the app for:
- `SWW_APPS` holds entries separated by semicolons. Each is a hostname, a colon,
then one or more apps separated by commas, for example
`git.example.com:gitea;code.example.com:gitea`.
- The request's `Host` is matched by exact name, without its port and without
regard to case. There are no wildcards.
- A hostname not listed gets no app's fixes, so every rule checks it in full.
Set empty, `SWW_APPS` lists no hostname.
- Unset, every app's fixes hold for every hostname. An app's fixes then weaken
the check of any other app that serves the same paths or fields.
- Any of these mistakes stops the start:
- an unknown app;
- a hostname that is not a DNS name, such as `*.example.com`;
- a hostname listed twice;
- an app listed twice for one hostname;
- an entry without a colon.
`gitea` is the only app supported. Each app's fixes are in one file, such as
`internal/waf/gitea.conf`, and one entry of `apps` in `internal/waf/waf.go`.
gitea's fixes are:
1. Ten query parameters are not checked against the Core Rule Set's lists of
system files (930120), shell paths (932160) and command names (932260), on
any path:
- `path`, `files`, `skip-to` and `sub_path`;
- `ref`, `sha`, `branch` and `workflow`;
- `artifactName` and `redirect_to`.
@@ -754,44 +789,21 @@ in front of every app, and no setting undoes them:
catch reaches the app, such as the command `|cat /etc/passwd`. Path
traversal, SQL and script injection, and PHP, Java and Node.js code are
still refused there. Other parameters keep all three rules, except on the
gitea and Drupal paths the eighth and ninth changes name. An app that uses
one of these parameters as a file on the server, or passes it to a shell,
must check the value itself. A rule file can refuse it instead.
gitea paths the third fix names.
5. Two cookies are left out, and `Referer` keeps fewer rules:
- The Core Rule Set reads the request without the `gitea_flash` and
2. The Core Rule Set reads the request without the `gitea_flash` and
`redirect_to` cookies. gitea puts a message naming what was just done into
`gitea_flash`. It puts into `redirect_to` the page to return to after
signing in. The Core Rule Set takes file names in them, such as
`package.json`, for attacks. A browser sends such a cookie with every
request until gitea replaces it. gitea cannot replace it while those
requests are refused, so one refusal would keep the browser out of the
whole site. gitea shows the message with any script removed and returns
only to its own pages. Every other cookie is read in full. An app that
reads a cookie of either name must check it itself.
- `Referer` is not checked for a Unix command given without arguments
(932340), for Java starting a process (944110) or for a Java class name
(944130). `Referer` names the page a request comes from, on everything
that page loads and on every link followed from it. The Core Rule Set
takes some page addresses for attacks:
- a search for `env`;
- OpenJDK's `Runtime.java`, whose path holds both `runtime` and `java.`;
- a file named `java.lang.String.md`.
Every request from such a page would be refused. `Referer` keeps the
other rules, except after some gitea searches (the eighth change). An
app that passes `Referer` to a shell must check it itself.
6. Responses are not inspected. A raw file from a repository, such as a shell
script, looks to the response rules like source code leaking from the server.
7. The `from` parameter of a container registry's upload path,
`/v2/<image>/blobs/uploads/`, is not checked for command names (932260). When
a push reuses a layer, `from` names the image the layer comes from. An image
such as `owner/python3-app` would have that push refused.
8. On gitea's paths, some fields skip the rules that refused ordinary gitea
`gitea_flash`. It puts into `redirect_to` the page to return to after signing
in. The Core Rule Set takes file names in them, such as `package.json`, for
attacks. A browser sends such a cookie with every request until gitea
replaces it. gitea cannot replace it while those requests are refused, so one
refusal would keep the browser out of the whole site. gitea shows the message
with any script removed and returns only to its own pages. Every other cookie
is read in full.
3. On gitea's paths, some fields skip the rules that refused ordinary gitea
requests there. They are the fields where gitea sends a search, a file name
or a branch name. `internal/coreruleset/gitea.conf` lists each rule and the
request it refused. The fields are:
or a branch name. `internal/waf/gitea.conf` lists each rule and the request
it refused. The fields are:
- `q` on gitea's searches of code, of issues and pull requests, and of
commits. A code search holds code, such as `<script>`,
`SELECT * FROM users WHERE` or `../`.
@@ -806,8 +818,7 @@ in front of every app, and no setting undoes them:
- packages.
A user's or an organisation's page is any path of one segment, so on
every such path, on any app, `q` skips these three rules, for example on
`/search`.
every such path `q` skips these three rules, for example on `/search`.
Some of these searches also skip the rule for Java class names (944130),
which refused `org.apache.commons.logging`.
@@ -845,37 +856,14 @@ in front of every app, and no setting undoes them:
`actions/upload-artifact@v3` (920450).
Elsewhere these fields keep every rule. The paths are those of gitea at the
site root. An app other than gitea that serves one of them must check these
fields itself.
9. On Drupal's paths, some fields skip the rules that refused ordinary Drupal
requests there. `internal/coreruleset/drupal.conf` lists each rule and the
request it refused. These query fields skip the lists of system files and
command names (930120, 932120, 932230, 932235, 932250, 932260):
- On every page under `/admin`: `title`, `name`, `user`, `filename`,
`filemime`, `info`, `subject`, `author_name`, `search`, `q` and
`destination`.
- `destination` on the pages Drupal links to with one, such as
`/node/1/edit`, `/user/login` and `/user/logout`.
- `keys` on `/search` and the pages under it.
- `q` on `/entity_reference_autocomplete/`.
- Every `filter[...]` field on `/jsonapi/`.
These fields of a body skip the rules that refused them. Each applies only
when the query has no parameter of that name, so it matters only while
`SWW_WAF_BODY_LIMIT` is set:
- The title, summary, tags, address, revision note, comment subject, media
name and image text on the node, comment and media forms, for the rules
above.
- The body and comment body on those forms and through JSON:API, which hold
HTML and any code it shows.
- `pass` when signing in (930120, 932260).
Elsewhere these fields keep every rule. The paths are those of Drupal at the
site root.
The parameter and cookie names in these changes are matched without regard to
case, as Coraza matches them.
On a hostname that gets gitea's fixes, an app other than gitea must check these
fields and cookies itself. Listing each hostname's own apps in `SWW_APPS` keeps
the full check for the others.
The parameter names in these changes and fixes are matched without regard to
case, and the cookie names exactly.
### Ban lengths, response and scope
@@ -1373,9 +1361,9 @@ over either one:
`Transfer-Encoding` do not count. `X-Request-ID` and `X-Forwarded-Proto` do.
Real clients never reach these limits. The answer is `deny` with `431`, and the
log line's action is `request_header_too_large`. The refusal makes no ban and is
no offence. The rate limits count the request as usual, so a client that keeps
sending such requests breaks a limit.
log line's action is `too_large`. The refusal makes no ban and is no offence.
The rate limits count the request as usual, so a client that keeps sending such
requests breaks a limit.
The limits apply on every path and in every `SWW_WAF_MODE`. A client in
`SWW_ALLOW_NETS` skips them, as it skips the Core Rule Set. In `observe` mode
@@ -1405,11 +1393,11 @@ these fields, named as the request log's fields of the same meaning:
- `request_bytes` and `response_bytes`: the body bytes each way, with what a
WebSocket carried each way, each a whole number from `0` to `1125899906842624`
(1 PiB);
- `duration_total_ms`: milliseconds from the request's headers being read to the
- `duration_total`: milliseconds from the request's headers being read to the
end of the answer, from `0` to `1000000000000` (about 31 years);
- `duration_upstream_total_ms`: milliseconds from the hand-over to the app to
the end of the answer, in the same range, and `0` for a request not passed to
the app.
- `duration_upstream_total`: milliseconds from the hand-over to the app to the
end of the answer, in the same range, and `0` for a request not passed to the
app.
`discriminator` answers `400` and writes a process warning when the body:
+4 -4
View File
@@ -9,9 +9,9 @@ back or dropped.
- `ban`: a ban `discriminator` made.
- `permanent_ban`: a permanent ban it made, or an existing ban made permanent by
a request.
- `core_rule_set_match`: the Core Rule Set scored a request at or over the
threshold. The request was refused unless the alert's `detail` gives `mode`:
`detect` or `observe`.
- `waf_block`: the Core Rule Set scored a request at or over the threshold. The
request was refused unless the alert's `detail` gives `mode`: `detect` or
`observe`.
- `anomaly`: a count over an anomaly threshold.
- `reputation_hit`: a request from a client on a blocklist, the CrowdSec
decision list or a DNS blocklist, or with an AbuseIPDB score that is a hit.
@@ -130,7 +130,7 @@ The priority and tag by event:
- `ban`: priority `default`, tag `no_entry`.
- `permanent_ban`: priority `high`, tag `no_entry`.
- `core_rule_set_match`: priority `default`, tag `shield`.
- `waf_block`: priority `default`, tag `shield`.
- `anomaly`: priority `high`, tag `chart_with_upwards_trend`.
- `reputation_hit`: priority `low`, tag `label`.
- `source_failure` and `file_error`: priority `high`, tag `warning`.
+5 -6
View File
@@ -28,8 +28,7 @@ body bytes, never headers, whatever `SWW_BYTES_COUNT` says.
## Attack detection
- `sww_rule_matches_total`, by `rule_id` and `action`, and `sww_rules_loaded`;
- `sww_core_rule_set_matches_total`, by `mode` and `rule_id`, for the rules that
fired.
- `sww_waf_matches_total`, by `mode` and `rule_id`, for the rules that fired.
## Countries and AS numbers
@@ -44,8 +43,8 @@ For the `SWW_METRICS_TOP_N` busiest of each, the rest as `other`:
## Lookups
- `sww_geojs_requests_total` and `sww_geojs_failures_total`;
- `sww_requests_without_geojs_answer_total`: the requests that needed an answer
and went on without it;
- `sww_geojs_unanswered_total`: the requests that needed an answer and went on
without it;
- with the file source, `sww_lookup_database_last_read_timestamp_seconds` and
`sww_lookup_database_read_failures_total`.
@@ -71,8 +70,8 @@ By `file`:
## Remote log, alerts and the process
- while a syslog server is set, `sww_remote_log_lines_sent_total`,
`sww_remote_log_lines_dropped_total` and `sww_remote_log_lines_waiting`;
- by `destination`, `sww_alerts_sent_total`, `sww_alert_send_failures_total`,
`sww_remote_log_lines_dropped_total` and `sww_remote_log_buffer_depth`;
- by `destination`, `sww_alerts_sent_total`, `sww_alerts_failed_total`,
`sww_alerts_suppressed_total` and `sww_alerts_dropped_total`;
- Go's own `go_` metrics and the process's `process_` metrics.
+28 -31
View File
@@ -26,7 +26,7 @@ These questions write no line:
A `report` line:
```
{"type":"request","discriminator_line":"report","time":"2026-10-03T12:00:00.123Z","instance":"fsn1/discriminator","client_ip":"203.0.113.9","method":"GET","scheme":"https","host":"app.example","path":"/","query":"","protocol":"HTTP/1.1","status":200,"request_bytes":0,"response_bytes":5120,"referer":"","user_agent":"curl/8.9.1","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","peer_ip":"172.18.0.2","forwarded_for":"203.0.113.9","client_group":"203.0.113.9/32","asn":"AS64496","as_name":"Example Net","country":"DE","request_headers":{"accept":"*/*"},"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"core_rule_set_score":0,"duration_total_ms":3.217,"duration_checks_ms":0.041,"duration_core_rule_set_ms":0.029,"duration_upstream_total_ms":3.104}
{"type":"request","discriminator_line":"report","time":"2026-10-03T12:00:00.123Z","instance":"fsn1/discriminator","client_ip":"203.0.113.9","method":"GET","scheme":"https","host":"app.example","path":"/","query":"","protocol":"HTTP/1.1","status":200,"request_bytes":0,"response_bytes":5120,"referer":"","user_agent":"curl/8.9.1","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","peer_ip":"172.18.0.2","forwarded_for":"203.0.113.9","client_group":"203.0.113.9/32","asn":"AS64496","as_name":"Example Net","country":"DE","request_headers":{"accept":"*/*"},"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"waf_score":0,"duration_total":3.217,"duration_checks":0.041,"duration_waf":0.029,"duration_upstream_total":3.104}
```
### Every line
@@ -37,7 +37,7 @@ Every one of these lines has:
- the standard web log fields;
- `request_id`, `peer_ip` and `client_group`;
- `asn`, `as_name` and `country`;
- `action` and `duration_total_ms`.
- `action` and `duration_total`.
### Standard web log fields
@@ -96,17 +96,15 @@ 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_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
ban this request made or made permanent;
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
`blocked_by_core_rule_set`: refused by that check;
- `request_header_too_large`: over a header limit;
- `own_endpoint`: answered at one of `discriminator`'s own endpoints, or
`404` for a path under `/_sww/`.
- `denied`: refused for `SWW_DENY_NETS`, for a reputation source with the
action `deny`, or for a tag in `SWW_TAG_DENY`;
- `banned`: refused under a ban, or for a `ban` rule, a trap path or a
CrowdSec decision, each of which bans the client;
- `country_denied`, `rate_limited`, `rule_blocked` and `waf_blocked`:
refused by that check;
- `too_large`: over a header limit;
- `admin`: answered at one of `discriminator`'s own endpoints, or `404` for
a path under `/_sww/`.
- `would_action`: in `observe` mode, what `enforce` mode would have done with a
request it would have refused. `action` then names what was done, normally
`forward`.
@@ -140,10 +138,9 @@ Every one of these lines has:
`byte_limits` bans when the report comes, so only the `report` line names
it.
- `rate_limit_percent` and `rate_limit_percent_setting`: the client's percentage
of the rate limits and the setting that lowered it, when one did.
`bytes_limit_percent` and `bytes_limit_percent_setting` do the same for the
byte limits.
- `limit_percent` and `limit_percent_setting`: the client's percentage of the
rate limits and the setting that lowered it, when one did. `bytes_percent` and
`bytes_percent_setting` do the same for the byte limits.
- `counts`: the client's totals as rate limiting counts them, this request
included. It is left out when rate limiting did not count the request. It
holds:
@@ -159,7 +156,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_ERROR_BURST_THRESHOLD`).
- `error_burst`.
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
@@ -167,8 +164,8 @@ Every one of these lines has:
- `rule_ids`: the rule file rules that matched, in order, up to the one that
refused.
- `core_rule_set_score` and `core_rule_set_rule_ids`: the Core Rule Set's score
and the rules that matched, for a request it inspected.
- `waf_score` and `waf_rule_ids`: the Core Rule Set's score and the rules that
matched, for a request it inspected.
- `reputation`: the sources the client is on, in this order:
- blocklist URLs;
- the CrowdSec decision list's URL;
@@ -190,14 +187,14 @@ Every one of these lines has:
All in milliseconds:
- `duration_total_ms`:
- `duration_total`:
- on the `decision` line, from `discriminator` having read the question's
body to the line;
- on the `report` line, from `rproxy` having read the request's headers to
the end of the answer.
- `duration_checks_ms`: to the end of the checks;
- `duration_core_rule_set_ms`: the part of that the Core Rule Set took;
- `duration_upstream_total_ms`: on the `report` line of an allowed request, from
- `duration_checks`: to the end of the checks;
- `duration_waf`: the part of that the Core Rule Set took;
- `duration_upstream_total`: on the `report` line of an allowed request, from
the hand-over to the app to the end of the answer.
### Requests to its own endpoints
@@ -210,8 +207,8 @@ these meanings where they differ:
- `scheme`: `http`.
- `time`: when the request's headers had been read.
- `request_bytes`: the bytes read of a ban to add, and otherwise `0`.
- `action`: `own_endpoint`.
- `duration_total_ms`: from the request's headers being read to the line.
- `action`: `admin`.
- `duration_total`: from the request's headers being read to the line.
## `rproxy`'s line
@@ -229,10 +226,10 @@ these meanings where they differ:
a WebSocket carried from the visitor.
- `response_bytes`: the bytes of the answer's body, with what a WebSocket
carried from the app.
- `duration_request_ms`: milliseconds from the request's headers being read
until the app had been sent the whole request, or the answer started if that
was sooner. It includes the wait for `discriminator`.
- `duration_response_ms`: milliseconds from then until the end of the answer.
- `duration_request`: milliseconds from the request's headers being read until
the app had been sent the whole request, or the answer started if that was
sooner. It includes the wait for `discriminator`.
- `duration_response`: 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 `SWRP_DEFAULT_DISPOSITION`. It is
left out when:
@@ -246,5 +243,5 @@ these meanings where they differ:
For example:
```
{"type":"request","time":"2026-10-03T12:00:00.123Z","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","client_ip":"203.0.113.9","host":"app.example","method":"GET","path":"/","query":"","status":200,"request_bytes":0,"response_bytes":5120,"duration_request_ms":1.204,"duration_response_ms":3.104,"disposition":"allow"}
{"type":"request","time":"2026-10-03T12:00:00.123Z","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","client_ip":"203.0.113.9","host":"app.example","method":"GET","path":"/","query":"","status":200,"request_bytes":0,"response_bytes":5120,"duration_request":1.204,"duration_response":3.104,"disposition":"allow"}
```
+19 -1
View File
@@ -255,6 +255,24 @@ the request first.
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_APPS` (default unset): the apps each hostname serves. A request gets the
fixes of its hostname's apps only (see "Attack detection" in
[`SPEC.md`](../SPEC.md)).
- Entries are separated by semicolons. Each is a hostname, a colon, then one
or more apps separated by commas, for example
`git.example.com:gitea;code.example.com:gitea`.
- `gitea` is the only app supported.
- The request's `Host` is matched by exact name, without its port and
without regard to case. There are no wildcards.
- A hostname not listed gets no app's fixes, so every rule checks it in
full. Set empty, it lists no hostname.
- Unset, every app's fixes apply to every hostname.
- Any of these mistakes stops the start:
- an unknown app;
- a hostname that is not a DNS name, such as `*.example.com`;
- a hostname listed twice;
- an app listed twice for one hostname;
- an entry without a colon.
- `SWW_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`).
@@ -324,7 +342,7 @@ the request first.
- `SWW_ALERT_NTFY_TOKEN` (default unset): an ntfy access token. A control
character in it is refused.
- `SWW_ALERT_EVENTS` (default
`ban,permanent_ban,core_rule_set_match,anomaly,reputation_hit,source_failure,file_error`):
`ban,permanent_ban,waf_block,anomaly,reputation_hit,source_failure,file_error`):
the event types alerts are sent for, from those in [`alerts.md`](alerts.md)
but `summary`. Any other name is refused.
- `SWW_ALERT_COOLDOWN` (default `15m`): how long after an alert is sent its
+2 -3
View File
@@ -63,9 +63,8 @@ since it was first seen:
- total requests, and how many were forwarded and how many refused;
- body bytes of its requests, and of the responses it was sent;
- responses by status class;
- offences by kind: `limit`, `attack`, `blocked_by_block_rule` and
`blocked_by_core_rule_set`. A file may also hold `token_refused`, which is
kept, and which nothing adds to.
- offences by kind: `limit`, `attack`, `rule_blocked` and `waf_blocked`. A file
may also hold `token_refused`, which is kept, and which nothing adds to.
## `lookups.json`
+10 -12
View File
@@ -44,10 +44,10 @@ const (
// EventAnomaly is a count of requests or bytes over an anomaly
// threshold.
EventAnomaly = "anomaly"
// EventCoreRuleSetMatch is a request the Core Rule Set scored at or over
// EventWAFBlock is a request the Core Rule Set scored at or over
// SWW_WAF_ANOMALY_THRESHOLD, refused in block mode, let through in
// detect mode.
EventCoreRuleSetMatch = "core_rule_set_match"
EventWAFBlock = "waf_block"
// EventReputationHit is a request whose client a blocklist, the
// CrowdSec decision list or a DNSBL zone lists, or whose AbuseIPDB score
// is a hit.
@@ -72,7 +72,7 @@ const (
// default.
func Events() []string {
return []string{
EventBan, EventPermanentBan, EventCoreRuleSetMatch, EventAnomaly,
EventBan, EventPermanentBan, EventWAFBlock, EventAnomaly,
EventReputationHit, EventSourceFailure, EventFileError,
}
}
@@ -220,7 +220,7 @@ type State struct {
// requests to it failed, and how many alerts were dropped from its full
// queue or given up as it refused them.
type Counts struct {
Sent, SendFailures, Dropped int64
Sent, Failed, Dropped int64
}
// Queue takes the alerts raised, holds back those it must, and sends the
@@ -264,7 +264,7 @@ type destination struct {
// waiting are the alerts waiting to be sent, oldest first.
waiting []*Alert
sent, sendFailures, dropped atomic.Int64
sent, failed, dropped atomic.Int64
}
// cooldownKey is what makes an alert a repeat of another: the same event
@@ -426,9 +426,7 @@ func (q *Queue) Counts(name string) Counts {
for _, d := range q.destinations {
if d.name == name {
return Counts{
Sent: d.sent.Load(),
SendFailures: d.sendFailures.Load(),
Dropped: d.dropped.Load(),
Sent: d.sent.Load(), Failed: d.failed.Load(), Dropped: d.dropped.Load(),
}
}
}
@@ -677,7 +675,7 @@ func (d *destination) run(ctx context.Context) {
retryAt = time.Time{}
case errors.Is(err, errRefused):
d.remove(alert)
d.sendFailures.Add(1)
d.failed.Add(1)
d.dropped.Add(1)
retryDelay = 0
@@ -686,7 +684,7 @@ func (d *destination) run(ctx context.Context) {
d.processLog.Warn("gave up an alert "+d.setting+" refused",
"event", alert.Event, "error", err.Error())
case ctx.Err() == nil: // not cut off as sww stops
d.sendFailures.Add(1)
d.failed.Add(1)
retryDelay = min(max(retryDelayFactor*retryDelay, firstRetryDelay),
maxRetryDelay)
@@ -873,7 +871,7 @@ func ntfyPriority(event string) string {
return "high"
case EventReputationHit:
return "low"
default: // ban, core_rule_set_match and summary
default: // ban, waf_block and summary
return "default"
}
}
@@ -884,7 +882,7 @@ func ntfyTag(event string) string {
switch event {
case EventBan, EventPermanentBan:
return "no_entry"
case EventCoreRuleSetMatch:
case EventWAFBlock:
return "shield"
case EventAnomaly:
return "chart_with_upwards_trend"
+6 -6
View File
@@ -897,7 +897,7 @@ func TestDestinationThatDoesNotAnswerHoldsUpNeitherOther(t *testing.T) {
synctest.Wait()
wantDestinationCounts(t, q, alerts.DestinationSlack,
alerts.Counts{Sent: 3, SendFailures: 1})
alerts.Counts{Sent: 3, Failed: 1})
logged := log.String()
if !strings.Contains(logged,
@@ -934,7 +934,7 @@ func TestDropsAreCountedForEachDestination(t *testing.T) {
for destination, want := range map[string]alerts.Counts{
alerts.DestinationWebhook: {Sent: all},
alerts.DestinationSlack: {Dropped: 1},
alerts.DestinationNtfy: {SendFailures: all, Dropped: all},
alerts.DestinationNtfy: {Failed: all, Dropped: all},
} {
wantDestinationCounts(t, q, destination, want)
}
@@ -1223,12 +1223,12 @@ func wantEvents(t *testing.T, webhook *standIn, want ...string) {
// requests to it that it counts as failed, the alerts it counts as held
// back, and those it counts as dropped for the webhook.
func wantCounts(
t *testing.T, q *alerts.Queue, sent, sendFailures, suppressed, dropped int64,
t *testing.T, q *alerts.Queue, sent, failed, suppressed, dropped int64,
) {
t.Helper()
wantDestinationCounts(t, q, alerts.DestinationWebhook,
alerts.Counts{Sent: sent, SendFailures: sendFailures, Dropped: dropped})
alerts.Counts{Sent: sent, Failed: failed, Dropped: dropped})
if q.Suppressed() != suppressed {
t.Errorf("%d alerts held back, want %d", q.Suppressed(), suppressed)
@@ -1281,12 +1281,12 @@ func anAlertForEachEvent() []eventMessage {
},
{
alerts.Alert{
Event: alerts.EventCoreRuleSetMatch, Client: netip.MustParseAddr("192.0.2.1"),
Event: alerts.EventWAFBlock, Client: netip.MustParseAddr("192.0.2.1"),
Netblock: netip.MustParsePrefix("192.0.2.1/32"),
Reason: "refused by the Core Rule Set",
Host: "app.example",
},
"fsn1app1/blog: core_rule_set_match", "default shield",
"fsn1app1/blog: waf_block", "default shield",
"refused by the Core Rule Set\nhost: app.example\nclient: 192.0.2.1\n" +
"netblock: 192.0.2.1/32",
},
+5 -5
View File
@@ -37,13 +37,13 @@ const (
ScopeASN = "asn"
// ScopeTotal is the whole service.
ScopeTotal = "total"
// ScopeWatchedNet is a named netblock of SWW_WATCH_NETS.
ScopeWatchedNet = "watched_net"
// ScopeWatch is a named netblock of SWW_WATCH_NETS.
ScopeWatch = "watch"
)
// Scopes returns every scope.
func Scopes() []string {
return []string{ScopeClient, ScopeNet, ScopeASN, ScopeTotal, ScopeWatchedNet}
return []string{ScopeClient, ScopeNet, ScopeASN, ScopeTotal, ScopeWatch}
}
// The windows a counter counts in, as the alerts name them.
@@ -257,7 +257,7 @@ func (c *Counters) scopesOf(r Request) []scope {
for _, named := range p.NamedNetblocks {
if named.Netblock.Contains(client) {
all = append(all, scope{
key{scope: ScopeWatchedNet, netblock: named.Netblock, name: named.Name}, p.Watch,
key{scope: ScopeWatch, netblock: named.Netblock, name: named.Name}, p.Watch,
})
}
}
@@ -391,7 +391,7 @@ func alertFor(r Request, k key, o overThreshold) alerts.Alert {
detail["asn"] = k.asn
case ScopeTotal:
counted = "the whole service"
default: // watched_net
default: // watch
counted = "the named netblock " + k.name + ", " + k.netblock.String()
detail["name"] = k.name
}
+14 -59
View File
@@ -13,7 +13,7 @@ func TestBanWithoutACauseIsAnAdmins(t *testing.T) {
t.Parallel()
netblock := netip.MustParsePrefix("203.0.113.0/24")
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
ledger.Load([]bans.Ban{{Netblock: netblock, Start: midnight()}})
if got := ledger.Bans(netblock)[0].Cause; got != bans.CauseAdmin {
@@ -24,9 +24,9 @@ func TestBanWithoutACauseIsAnAdmins(t *testing.T) {
func TestAdminsBansAreNeverDroppedAndDoNotCountTowardMaxBans(t *testing.T) {
t.Parallel()
params := defaultParams()
params.MaxBans = 1
ledger := bans.New(params)
rules := defaultRules()
rules.MaxBans = 1
ledger := bans.New(rules)
adminsOnly := netip.MustParsePrefix("198.51.100.0/24")
both := netip.MustParsePrefix("203.0.113.1/32")
second := netip.MustParsePrefix("203.0.113.2/32")
@@ -64,9 +64,9 @@ func TestAdminsBansAreNeverDroppedAndDoNotCountTowardMaxBans(t *testing.T) {
func TestEarlierBansCountABanDroppedAfterAnAdminsBan(t *testing.T) {
t.Parallel()
params := defaultParams()
params.MaxBans = 2
ledger := bans.New(params)
rules := defaultRules()
rules.MaxBans = 2
ledger := bans.New(rules)
netblock := netip.MustParsePrefix("203.0.113.1/32")
other := netip.MustParsePrefix("203.0.113.2/32")
@@ -93,7 +93,7 @@ func TestEarlierBansCountABanDroppedAfterAnAdminsBan(t *testing.T) {
func TestEarlierBansCountABanAnAdminKeepsOnce(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.1/32")
// The netblock breaks a limit twice, the second time after its first
@@ -117,7 +117,7 @@ func TestEarlierBansCountABanAnAdminKeepsOnce(t *testing.T) {
func TestReasonOfTheBansSwwMakes(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
limit, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.1/32"), midnight(),
bans.Notes{Kind: "requests", Limit: 1000, Window: "minute"})
@@ -150,7 +150,7 @@ func TestLiftedBanForALimitRefusesNothingAndMakesNoBanLonger(t *testing.T) {
Lifted: midnight().Add(10 * time.Minute),
}
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
ledger.Load([]bans.Ban{lifted})
// While it would still last, it refuses nothing, and a limit broken
@@ -181,7 +181,7 @@ func TestLiftedBanForAnAttackRefusesNothingAndMakesNoBanLonger(t *testing.T) {
// A permanent ban for a clear sign of attack, lifted.
netblock := netip.MustParsePrefix("203.0.113.9/32")
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
ledger.Load([]bans.Ban{{
Netblock: netblock,
Start: midnight(),
@@ -211,7 +211,7 @@ func TestLiftedBanForAnAttackRefusesNothingAndMakesNoBanLonger(t *testing.T) {
func TestLoadEditCountsTheBansAnAdminMade(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
made, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.1/32"), midnight(),
bans.Notes{})
atStart := bans.Ban{
@@ -246,7 +246,7 @@ func TestAdminsBanIsMadeWhileAnotherLasts(t *testing.T) {
t.Parallel()
netblock := netip.MustParsePrefix("203.0.113.0/24")
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
// An hour's ban for a broken limit.
ledger.BanForLimit(netblock, midnight(), bans.Notes{})
@@ -292,7 +292,7 @@ func TestLiftLiftsEveryActiveBanCoveringTheClient(t *testing.T) {
wide := netip.MustParsePrefix("203.0.113.0/24")
other := netip.MustParsePrefix("203.0.113.10/32")
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
ledger.Load([]bans.Ban{
// Ended an hour ago.
{
@@ -341,48 +341,3 @@ func TestLiftLiftsEveryActiveBanCoveringTheClient(t *testing.T) {
wantChanged(t, ledger, false)
}
func TestPermanentBanOnAWiderNetblockIsReturnedOverATemporaryBan(t *testing.T) {
t.Parallel()
client := netip.MustParseAddr("203.0.113.9")
ledger := bans.New(defaultParams())
// The temporary ban is made first, so that its netblock is looked at first.
ledger.BanForLimit(netip.MustParsePrefix("203.0.113.9/32"), midnight(), bans.Notes{})
ledger.BanForAdmin(netip.MustParsePrefix("203.0.113.0/24"), midnight(), time.Time{},
"for good")
wantPermanentBan(t, ledger, client, midnight().Add(time.Minute))
}
func TestOlderPermanentBanIsReturnedOverANewerTemporaryBan(t *testing.T) {
t.Parallel()
netblock := netip.MustParsePrefix("203.0.113.0/24")
ledger := bans.New(defaultParams())
ledger.BanForAdmin(netblock, midnight(), time.Time{}, "for good")
ledger.BanForAdmin(netblock, midnight().Add(time.Minute), midnight().Add(time.Hour),
"for an hour")
wantPermanentBan(t, ledger, netblock.Addr(), midnight().Add(2*time.Minute))
}
// wantPermanentBan checks that Find and Check return a permanent ban for
// client at now.
func wantPermanentBan(
t *testing.T, ledger *bans.Ledger, client netip.Addr, now time.Time,
) {
t.Helper()
for name, look := range map[string]func(netip.Addr, time.Time) (bans.Ban, bool, bool){
"Find": ledger.Find,
"Check": ledger.Check,
} {
ban, banned, _ := look(client, now)
if !banned || !ban.Permanent() {
t.Errorf("%s returned %+v and banned %v, want a permanent ban", name, ban, banned)
}
}
}
+19 -41
View File
@@ -40,8 +40,8 @@ const repeatFactor = 3
// maxTextBytes is how much of each text in a ban's notes is kept.
const maxTextBytes = 256
// Params are how long a ban lasts, and how many bans are held.
type Params struct {
// Rules are how long a ban lasts, and how many bans are held.
type Rules struct {
// LimitBanDuration is how long a first ban for a broken limit lasts.
LimitBanDuration time.Duration
// LimitBanRepeatWindow is how soon after the end of the netblock's
@@ -180,7 +180,7 @@ type Request struct {
// Ledger holds the bans. It is safe for concurrent use.
type Ledger struct {
params Params
rules Rules
// changed receives a value when a ban is made, unless one is waiting
// already.
changed chan struct{}
@@ -190,7 +190,7 @@ type Ledger struct {
// Find make each netblock they find the most recently seen.
netblocks *simplelru.LRU[netip.Prefix, *[]Ban]
// held is how many bans netblocks holds whose cause is not CauseAdmin,
// at most params.MaxBans.
// at most rules.MaxBans.
held int
// made is how many bans have been made since the start, by cause: by
// the ledger, and by an admin, through BanForAdmin or in an edit of
@@ -205,7 +205,7 @@ type Ledger struct {
}
// New returns a Ledger with no ban yet.
func New(params Params) *Ledger {
func New(rules Rules) *Ledger {
// The ledger drops bans itself, and never those whose cause is
// CauseAdmin, however many there are, so the LRU has no limit of its
// own: it keeps the netblocks in the order they were last seen.
@@ -215,7 +215,7 @@ func New(params Params) *Ledger {
}
return &Ledger{
params: params,
rules: rules,
changed: make(chan struct{}, 1),
netblocks: netblocks,
made: map[string]int{},
@@ -274,27 +274,16 @@ func (l *Ledger) Find(client netip.Addr, now time.Time) (Ban, bool, bool) {
// activeBan returns the ban in bans, a netblock's bans oldest first, that
// is active at now, or nil when none is. If several are, it returns the
// newest permanent one, or the newest when none is permanent. Every ban is
// looked at, since a ban an admin adds to bans.json can start before the
// netblock's others and outlast them.
// one that started last. Every ban is looked at, since a ban an admin adds
// to bans.json can start before the netblock's others and outlast them.
func activeBan(bans []Ban, now time.Time) *Ban {
var newest *Ban
for i := len(bans) - 1; i >= 0; i-- {
if !bans[i].ActiveAt(now) {
continue
}
if bans[i].Permanent() {
if bans[i].ActiveAt(now) {
return &bans[i]
}
if newest == nil {
newest = &bans[i]
}
}
return newest
return nil
}
// BanForLimit bans netblock at now for a broken limit, with notes, and
@@ -754,16 +743,13 @@ func (l *Ledger) markChanged() {
}
// active returns the ban active at now on a netblock client is in, or
// nil. If several netblocks have one, it returns a permanent ban when
// there is one.
// nil.
func (l *Ledger) active(client netip.Addr, now time.Time) *Ban {
lengths := l.v6Lengths
if client.Is4() {
lengths = l.v4Lengths
}
var first *Ban
for _, length := range lengths {
bans, found := l.netblocks.Get(netip.PrefixFrom(client, length).Masked())
if !found {
@@ -771,20 +757,12 @@ func (l *Ledger) active(client netip.Addr, now time.Time) *Ban {
}
ban := activeBan(*bans, now)
if ban == nil {
continue
}
if ban.Permanent() {
if ban != nil {
return ban
}
if first == nil {
first = ban
}
}
return first
return nil
}
// covering returns the bans of each netblock held that client is in,
@@ -812,7 +790,7 @@ func (l *Ledger) covering(client netip.Addr) []*[]Ban {
// unless ban's cause is CauseAdmin, which does not count toward MaxBans.
func (l *Ledger) add(ban Ban) {
counted := ban.Cause != CauseAdmin
if counted && l.held == l.params.MaxBans {
if counted && l.held == l.rules.MaxBans {
l.dropOne()
}
@@ -847,7 +825,7 @@ func (l *Ledger) add(ban Ban) {
// new ban longer. A ban an admin adds to bans.json can start after
// another and end before it, so that one is looked for among them all.
func (l *Ledger) limitExpiry(held []Ban, now time.Time) time.Time {
length := l.params.LimitBanDuration
length := l.rules.LimitBanDuration
var last *Ban
@@ -858,18 +836,18 @@ func (l *Ledger) limitExpiry(held []Ban, now time.Time) time.Time {
}
}
if last != nil && now.Sub(last.Expires) <= l.params.LimitBanRepeatWindow {
if last != nil && now.Sub(last.Expires) <= l.rules.LimitBanRepeatWindow {
lastLength := last.Expires.Sub(last.Start)
// This is repeatFactor * lastLength > MaxBanDuration, written so
// that it cannot overflow.
if lastLength > l.params.MaxBanDuration/repeatFactor {
if lastLength > l.rules.MaxBanDuration/repeatFactor {
return time.Time{}
}
length = repeatFactor * lastLength
}
if length > l.params.MaxBanDuration {
if length > l.rules.MaxBanDuration {
return time.Time{}
}
@@ -887,7 +865,7 @@ func (l *Ledger) attackExpiry(held []Ban, now time.Time) time.Time {
}
}
return now.Add(l.params.AttackBanDuration)
return now.Add(l.rules.AttackBanDuration)
}
// dropOne drops the earliest ban whose cause is not CauseAdmin of the
+27 -27
View File
@@ -15,7 +15,7 @@ const day = 24 * time.Hour
func TestRepeatsTripleUntilPermanent(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
now := midnight()
@@ -62,7 +62,7 @@ func TestRepeatWindowRunsOut(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
@@ -80,9 +80,9 @@ func TestRepeatWindowRunsOut(t *testing.T) {
func TestFirstBanLongerThanTheMaximumIsPermanent(t *testing.T) {
t.Parallel()
params := defaultParams()
params.LimitBanDuration = params.MaxBanDuration + time.Hour
ledger := bans.New(params)
rules := defaultRules()
rules.LimitBanDuration = rules.MaxBanDuration + time.Hour
ledger := bans.New(rules)
ban, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.9/32"), midnight(),
bans.Notes{})
@@ -97,9 +97,9 @@ func TestLongestBanSetFarOffDoesNotOverflow(t *testing.T) {
// With bans of up to 100,000 days, the 14th ban in a row, of 3^13
// hours, is within the maximum, and three times as long would not fit
// in a time.Duration. The 15th is permanent.
params := defaultParams()
params.MaxBanDuration = 100000 * day
ledger := bans.New(params)
rules := defaultRules()
rules.MaxBanDuration = 100000 * day
ledger := bans.New(rules)
netblock := netip.MustParsePrefix("203.0.113.9/32")
now := midnight()
@@ -121,7 +121,7 @@ func TestLongestBanSetFarOffDoesNotOverflow(t *testing.T) {
func TestBrokenLimitDuringABanMakesNoOther(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
first, made := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
@@ -146,7 +146,7 @@ func TestBrokenLimitDuringABanMakesNoOther(t *testing.T) {
func TestCheckRefusesWhileTheBanLastsAndCountsTheRefusals(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Requests: 5})
@@ -178,7 +178,7 @@ func TestCheckRefusesWhileTheBanLastsAndCountsTheRefusals(t *testing.T) {
func TestFindCountsNothing(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Requests: 5})
@@ -200,9 +200,9 @@ func TestFindCountsNothing(t *testing.T) {
func TestMaxBansDropsTheEarliestBanOfTheNetblockSeenLongestAgo(t *testing.T) {
t.Parallel()
params := defaultParams()
params.MaxBans = 3
ledger := bans.New(params)
rules := defaultRules()
rules.MaxBans = 3
ledger := bans.New(rules)
a := netip.MustParsePrefix("203.0.113.1/32")
b := netip.MustParsePrefix("203.0.113.2/32")
c := netip.MustParsePrefix("203.0.113.3/32")
@@ -239,9 +239,9 @@ func TestFullLedgerDropsTheEarlierBanOfTheNetblockBannedAgain(t *testing.T) {
// With room for one ban, the netblock's ended ban goes to make room for
// its new one, whose notes still count it.
params := defaultParams()
params.MaxBans = 1
ledger := bans.New(params)
rules := defaultRules()
rules.MaxBans = 1
ledger := bans.New(rules)
netblock := netip.MustParsePrefix("203.0.113.9/32")
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
@@ -258,7 +258,7 @@ func TestFullLedgerDropsTheEarlierBanOfTheNetblockBannedAgain(t *testing.T) {
func TestRequestDuringAnAttackBanMakesItPermanent(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
notes := bans.Notes{RuleID: "env-file", Target: "path"}
@@ -304,7 +304,7 @@ func TestRequestDuringAnAttackBanMakesItPermanent(t *testing.T) {
func TestAttackAfterAnAttackBanHasEndedBansPermanently(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
// A ban for a broken limit before does not count.
@@ -336,7 +336,7 @@ func TestAttackAfterAnAttackBanHasEndedBansPermanently(t *testing.T) {
func TestWouldBanGivesTheBanWithoutMakingIt(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
wantChanged(t, ledger, true)
@@ -380,7 +380,7 @@ func TestWouldBanGivesTheBanWithoutMakingIt(t *testing.T) {
func TestWouldBePermanentAnswersAsTheBanWouldBeMade(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
now := midnight()
@@ -414,7 +414,7 @@ func TestWouldBePermanentAnswersAsTheBanWouldBeMade(t *testing.T) {
func TestAttackBanDoesNotLengthenTheNextBanForALimit(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
// Three times the seven days would be permanent; a limit broken as the
@@ -436,7 +436,7 @@ func TestAttackBanDoesNotLengthenTheNextBanForALimit(t *testing.T) {
func TestRequestTextsAreCutTo256Bytes(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
long := strings.Repeat("a", 300)
request := bans.Request{
@@ -458,7 +458,7 @@ func TestRequestTextsAreCutTo256Bytes(t *testing.T) {
func TestLookupFillsTheNotesOfTheNetblocksBansWithoutOne(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
other := netip.MustParsePrefix("198.51.100.7/32")
@@ -494,9 +494,9 @@ func TestLookupFillsTheNotesOfTheNetblocksBansWithoutOne(t *testing.T) {
}
}
// defaultParams are the params at the settings' defaults.
func defaultParams() bans.Params {
return bans.Params{
// defaultRules are the rules at the settings' defaults.
func defaultRules() bans.Rules {
return bans.Rules{
LimitBanDuration: time.Hour,
LimitBanRepeatWindow: day,
MaxBanDuration: 7 * day,
+3 -3
View File
@@ -15,7 +15,7 @@ const scenario = "crowdsecurity/ssh-bf"
func TestCrowdSecBanLastsUntilTheDecisionEnds(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
expires := midnight().Add(4 * time.Hour)
@@ -49,7 +49,7 @@ func TestCrowdSecBanLastsUntilTheDecisionEnds(t *testing.T) {
func TestCrowdSecBanIsNeverMadePermanent(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
expires := midnight().Add(4 * time.Hour)
ledger.BanForCrowdSec(netblock, midnight(), expires, scenario, bans.Notes{})
@@ -72,7 +72,7 @@ func TestCrowdSecBanIsNeverMadePermanent(t *testing.T) {
func TestCrowdSecBanIsCountedAndDoesNotLengthenTheNextBanForALimit(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
// Three times the three days would be permanent; a limit broken as the
+14 -14
View File
@@ -14,7 +14,7 @@ import (
func TestChangedAfterABanIsMade(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
wantChanged(t, ledger, false)
@@ -38,7 +38,7 @@ func TestChangedAfterABanIsMade(t *testing.T) {
func TestSnapshotListsEveryBanByNetblock(t *testing.T) {
t.Parallel()
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
v6 := netip.MustParsePrefix("2001:db8::/64")
high := netip.MustParsePrefix("203.0.113.10/32")
low := netip.MustParsePrefix("203.0.113.9/32")
@@ -67,14 +67,14 @@ func TestSnapshotListsEveryBanByNetblock(t *testing.T) {
func TestLoadedBansCarryOn(t *testing.T) {
t.Parallel()
before := bans.New(defaultParams())
before := bans.New(defaultRules())
netblock := netip.MustParsePrefix("203.0.113.9/32")
ban, _ := before.BanForLimit(netblock, midnight(), bans.Notes{Limit: 1})
// Loaded into a new ledger, as across a restart, the ban still refuses
// while it lasts, and once it has ended a broken limit bans for three
// times as long, with the loaded ban counted among the earlier ones.
after := bans.New(defaultParams())
after := bans.New(defaultRules())
after.Load(before.Snapshot())
_, banned, _ := after.Check(netblock.Addr(), ban.Expires.Add(-time.Second))
@@ -96,7 +96,7 @@ func TestLoadedBanRefusesEveryClientInItsNetblock(t *testing.T) {
// Two entries as an admin might write them, with addresses not masked
// to their lengths, the IPv6 one shorter than the /64 an IPv6 client's
// ban covers, beside a ban the ledger makes on one IPv4 address.
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
ledger.Load([]bans.Ban{
{Netblock: netip.MustParsePrefix("203.0.113.9/24"), Start: midnight()},
{Netblock: netip.MustParsePrefix("2001:db8::1/48"), Start: midnight()},
@@ -145,7 +145,7 @@ func TestPermanentBanStartedBeforeAnEndedOneRefuses(t *testing.T) {
Expires: midnight().Add(time.Hour),
}
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
ledger.Load([]bans.Ban{permanent, ended})
now := midnight().Add(2 * time.Hour)
@@ -189,7 +189,7 @@ func TestNextBanWorkedOutFromTheBanThatEndedLast(t *testing.T) {
Expires: midnight().Add(2 * time.Hour),
}
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
ledger.Load([]bans.Ban{nineHours, admins})
// Once both have ended, a limit broken within the repeat window bans
@@ -219,9 +219,9 @@ func TestLoadKeepsAtMostMaxBansDroppingTheEarliest(t *testing.T) {
Cause: bans.CauseLimit,
}
params := defaultParams()
params.MaxBans = 1
ledger := bans.New(params)
rules := defaultRules()
rules.MaxBans = 1
ledger := bans.New(rules)
ledger.Load([]bans.Ban{later, earlier})
held := ledger.Snapshot()
@@ -235,9 +235,9 @@ func TestLoadReplacesTheBansHeld(t *testing.T) {
// Room for three bans, so that the second load, were it added to the
// two bans held, would drop none of them to make room.
params := defaultParams()
params.MaxBans = 3
ledger := bans.New(params)
rules := defaultRules()
rules.MaxBans = 3
ledger := bans.New(rules)
kept := bans.Ban{
Netblock: netip.MustParsePrefix("2001:db8::/64"),
Start: midnight(),
@@ -285,7 +285,7 @@ func TestLoadCutsTheTextsTo256Bytes(t *testing.T) {
}},
}
ledger := bans.New(defaultParams())
ledger := bans.New(defaultRules())
ledger.Load([]bans.Ban{ban})
cut := long[:256]
+94 -3
View File
@@ -30,6 +30,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/anomaly"
"sneak.berlin/go/smallwebwaf/internal/remotelog"
"sneak.berlin/go/smallwebwaf/internal/waf"
)
// Config is the settings of `sww discriminator`. A time or size limit of
@@ -235,6 +236,11 @@ type Config struct {
WAFExemptPaths []string
WAFBodyLimit int64
CoreRuleSetExclusionsFile string
// Apps are the names of the apps each hostname serves, by hostname in
// lowercase (SWW_APPS): a request gets only its hostname's apps' fixes
// to the Core Rule Set. Apps is nil while SWW_APPS is unset, and every
// request gets every app's fixes.
Apps map[string][]string
// TrapPaths are the paths a request for which is a clear sign of
// attack (SWW_TRAP_PATHS), each starting with / and without a ?.
TrapPaths []string
@@ -336,7 +342,7 @@ const (
maxParanoiaLevel = 4
// firstSetupRuleID to lastSetupRuleID are the ids the Core Rule Set
// keeps for the rules that set it up, which sww's own rules
// have too (see internal/coreruleset). Switching one off would undo a change
// have too (see internal/waf). Switching one off would undo a change
// that no setting undoes.
firstSetupRuleID = 900000
lastSetupRuleID = 900999
@@ -404,6 +410,11 @@ var (
errNotTrapPath = errors.New(
"is not a path starting with / and without a ?, such as /wp-login.php",
)
errNotAppsEntry = errors.New(
"is not a hostname, : and a list of apps, such as git.example.com:gitea",
)
errNotHostname = errors.New("is not a DNS name such as git.example.com")
errNotApp = errors.New("is not an app sww has fixes for; the apps are")
errNotWAFMode = errors.New("is not off, detect or block")
errNotParanoiaLevel = errors.New("is not a paranoia level, from 1 to 4")
errNotRuleID = errors.New(
@@ -432,7 +443,7 @@ var (
"holds a control character, such as the carriage return of a Windows line end",
)
errNotAlertEvent = errors.New(
"is not ban, permanent_ban, core_rule_set_match, anomaly, reputation_hit, " +
"is not ban, permanent_ban, waf_block, anomaly, reputation_hit, " +
"source_failure or file_error")
errNotNumberOrOff = errors.New(
"is not a whole number above zero, such as 60, or off",
@@ -577,6 +588,7 @@ func FromEnvironment(
WAFBodyLimit: env.wafBodyLimit("SWW_WAF_BODY_LIMIT", off),
CoreRuleSetExclusionsFile: env.filePath(
"SWW_CORE_RULE_SET_EXCLUSIONS_FILE"),
Apps: env.apps("SWW_APPS"),
TrapPaths: env.trapPaths("SWW_TRAP_PATHS"),
ErrorBurstThreshold: env.count("SWW_ERROR_BURST_THRESHOLD", "30"),
LogRemoteURL: env.logRemoteURL("SWW_LOG_REMOTE_URL"),
@@ -832,6 +844,25 @@ func (e *environment) pathPrefixes(name, defaultValue string) []string {
return prefixes
}
// apps reads the setting that is the apps each hostname serves. It is nil
// while the setting is unset, and logged as null, unlike a setting set
// empty.
func (e *environment) apps(name string) map[string][]string {
value, set := e.lookup(name)
if !set {
e.settings = append(e.settings, slog.Any(name, nil))
return nil
}
e.settings = append(e.settings, slog.String(name, value))
apps, err := parseApps(value)
e.check(name, err)
return apps
}
// trapPaths reads the setting that is the list of trap paths. It is empty
// by default.
func (e *environment) trapPaths(name string) []string {
@@ -1849,6 +1880,65 @@ func parseTrapPaths(value string) ([]string, error) {
return paths, nil
}
// parseApps reads the apps each hostname serves: entries separated by
// semicolons, each a hostname, a colon, and a comma-separated list of the
// names waf.AppNames returns, such as
// git.example.com:gitea;cloud.example.com:nextcloud,wordpress. Each
// hostname is a DNS name, without a wildcard. It returns them by hostname
// in lowercase, since a hostname is matched without regard to case. An
// empty value lists no hostname.
func parseApps(value string) (map[string][]string, error) {
apps := map[string][]string{}
if strings.TrimSpace(value) == "" {
return apps, nil
}
for entry := range strings.SplitSeq(value, ";") {
hostname, list, found := strings.Cut(entry, ":")
if !found {
return nil, fmt.Errorf("%q %w", entry, errNotAppsEntry)
}
hostname = strings.ToLower(strings.TrimSpace(hostname))
if hostname == "" {
return nil, fmt.Errorf("%q %w", entry, errNotAppsEntry)
}
if !isZone(hostname) {
return nil, fmt.Errorf("%q %w", hostname, errNotHostname)
}
if _, listed := apps[hostname]; listed {
return nil, fmt.Errorf("%q %w", hostname, errListedTwice)
}
names, err := parseList(list)
if err != nil {
return nil, err
}
if len(names) == 0 {
return nil, fmt.Errorf("%q %w", entry, errNotAppsEntry)
}
for i, name := range names {
if !slices.Contains(waf.AppNames(), name) {
return nil, fmt.Errorf("%q %w %s", name, errNotApp,
strings.Join(waf.AppNames(), ", "))
}
if slices.Contains(names[:i], name) {
return nil, fmt.Errorf("%q %w", name, errListedTwice)
}
}
apps[hostname] = names
}
return apps, nil
}
// parseRuleIDs reads a comma-separated list of the ids of Core Rule Set
// rules, each a whole number above zero and outside firstSetupRuleID to
// lastSetupRuleID.
@@ -2455,7 +2545,8 @@ func MaskZoneKey(zone string) string {
return masked + "." + rest
}
// isZone reports whether each label of zone is as parseZones takes it.
// isZone reports whether each label of zone is as parseZones takes it. It
// also checks the hostnames of SWW_APPS, which are DNS names too.
func isZone(zone string) bool {
for label := range strings.SplitSeq(zone, ".") {
badChar := strings.ContainsFunc(label, func(char rune) bool {
+89 -1
View File
@@ -89,6 +89,7 @@ const (
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
exclusionsFile = "SWW_CORE_RULE_SET_EXCLUSIONS_FILE"
apps = "SWW_APPS"
trapPaths = "SWW_TRAP_PATHS"
errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
logRemoteURL = "SWW_LOG_REMOTE_URL"
@@ -160,7 +161,7 @@ func loggedAnomalyDefaults() map[string]string {
// 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," +
defaultAlertEvents = "ban,permanent_ban,waf_block,anomaly,reputation_hit," +
"source_failure,file_error"
defaultAlertCooldown = "15m"
)
@@ -582,6 +583,92 @@ func TestCoreRuleSetSettings(t *testing.T) {
}
}
func TestApps(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
env environment
want map[string][]string
}{
{environment{}, nil},
{environment{apps: ""}, map[string][]string{}},
{
environment{apps: "Git.Example.com:gitea; cloud.example.com : gitea "},
map[string][]string{
"git.example.com": {"gitea"}, "cloud.example.com": {"gitea"},
},
},
} {
cfg := fromEnvironment(t, tc.env)
if !reflect.DeepEqual(cfg.Apps, tc.want) {
t.Errorf("%v gave %#v, want %#v", tc.env, cfg.Apps, tc.want)
}
}
}
// Unset and set empty, SWW_APPS has opposite effects, so it logs differently.
func TestUnsetAndEmptyAppsAreLoggedDifferently(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
env environment
want string
}{
{environment{}, `"` + apps + `":null`},
{environment{apps: ""}, `"` + apps + `":""`},
} {
var out bytes.Buffer
slog.New(slog.NewJSONHandler(&out, nil)).
Info("starting", "settings", fromEnvironment(t, tc.env))
if !strings.Contains(out.String(), tc.want) {
t.Errorf("%v logged %s, want %s in it", tc.env, out.String(), tc.want)
}
}
}
func TestInvalidAppsStopsTheStart(t *testing.T) {
t.Parallel()
const (
notEntry = " is not a hostname, : and a list of apps, " +
"such as git.example.com:gitea"
notHostname = " is not a DNS name such as git.example.com"
)
for _, tc := range []struct{ value, want string }{
{"git.example.com:gitea;blog.example.com", `"blog.example.com"` + notEntry},
{"git.example.com:gitea;", `""` + notEntry},
{":gitea", `":gitea"` + notEntry},
{"git.example.com:", `"git.example.com:"` + notEntry},
{"*.example.com:gitea", `"*.example.com"` + notHostname},
{"git.example.com/:gitea", `"git.example.com/"` + notHostname},
{"git example.com:gitea", `"git example.com"` + notHostname},
{
"git.example.com:gitea,nextcloud",
`"nextcloud" is not an app sww has fixes for; the apps are gitea`,
},
{
"git.example.com:gitea;GIT.example.com:gitea",
`"git.example.com" is listed twice`,
},
{"git.example.com:gitea,gitea", `"gitea" is listed twice`},
{"git.example.com:gitea,", `"gitea," has an empty item in its list`},
} {
t.Run(tc.value, func(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(environment{apps: tc.value}.lookupEnv)
want := apps + ": " + tc.want
if err == nil || err.Error() != want {
t.Errorf("error %v, want %s", err, want)
}
})
}
}
func TestWAFBodyLimitOf64K(t *testing.T) {
t.Parallel()
@@ -2589,6 +2676,7 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
wafExemptPaths: "",
wafBodyLimit: off,
exclusionsFile: "",
apps: "", // logged as null, which decodes as ""
trapPaths: "",
errorBurstThreshold: "30",
logRemoteURL: "",
-150
View File
@@ -1,150 +0,0 @@
# The ninth change: on Drupal's paths, some fields skip the rules that refused
# ordinary Drupal requests there. They are the fields where Drupal sends a
# search, a name, a file name or an address. Each exclusion runs with the
# 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.
#
# Six rules read a search or a name as a command or a system file. Each query
# exclusion below removes these six, and only these, from its fields:
# 930120: .htaccess
# 932120: powershell
# 932230: ls -la, in the address of a page of results
# 932235: python3, in the address of a page of results
# 932250: ls -la
# 932260: python3
# Every admin page's filters and autocompletes, and the page to return to:
# title: /admin/content?title=python3
# name: /admin/content/media?name=python3
# user: /admin/people?user=python3
# filename, filemime: /admin/content/files?filename=python3
# info: /admin/content/block?info=python3
# subject, author_name: /admin/content/comment?subject=python3
# search: /admin/config/search/path?search=python3
# destination: /admin/content?destination=/admin/content%3Ftitle%3Dpython3
SecRule REQUEST_FILENAME "@rx ^/admin(?:/|$)" \
"id:900800,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:title|name|user|filename|filemime|info|subject|author_name|search|q|destination)$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^(?:title|name|user|filename|filemime|info|subject|author_name|search|q|destination)$/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:title|name|user|filename|filemime|info|subject|author_name|search|q|destination)$/,\
ctl:ruleRemoveTargetById=932235;ARGS:/^(?:title|name|user|filename|filemime|info|subject|author_name|search|q|destination)$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:title|name|user|filename|filemime|info|subject|author_name|search|q|destination)$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:title|name|user|filename|filemime|info|subject|author_name|search|q|destination)$/"
# The page to return to, on the other pages Drupal links to with one, such as
# /node/1/edit?destination=/search/node%3Fkeys%3Dpython3 and
# /user/login?destination=/search/node%3Fkeys%3Dpython3.
SecRule REQUEST_FILENAME \
"@rx ^/(?:node/(?:add/[^/]+|[0-9]+/[a-z]+)|comment/[0-9]+/[a-z]+|media/(?:add/[^/]+|[0-9]+/[a-z]+)|taxonomy/term/[0-9]+/[a-z]+|user/[^/]+(?:/[a-z]+)?)$" \
"id:900801,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:destination,\
ctl:ruleRemoveTargetById=932120;ARGS:destination,\
ctl:ruleRemoveTargetById=932230;ARGS:destination,\
ctl:ruleRemoveTargetById=932235;ARGS:destination,\
ctl:ruleRemoveTargetById=932250;ARGS:destination,\
ctl:ruleRemoveTargetById=932260;ARGS:destination"
# The text searched for on the search pages: /search/node?keys=python3.
SecRule REQUEST_FILENAME "@rx ^/search(?:/|$)" \
"id:900802,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:keys,\
ctl:ruleRemoveTargetById=932120;ARGS:keys,\
ctl:ruleRemoveTargetById=932230;ARGS:keys,\
ctl:ruleRemoveTargetById=932235;ARGS:keys,\
ctl:ruleRemoveTargetById=932250;ARGS:keys,\
ctl:ruleRemoveTargetById=932260;ARGS:keys"
# What is typed into a field that autocompletes a tag or a user:
# /entity_reference_autocomplete/taxonomy_term/default/KEY?q=python3.
SecRule REQUEST_FILENAME "@beginsWith /entity_reference_autocomplete/" \
"id:900803,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:q,\
ctl:ruleRemoveTargetById=932120;ARGS:q,\
ctl:ruleRemoveTargetById=932230;ARGS:q,\
ctl:ruleRemoveTargetById=932235;ARGS:q,\
ctl:ruleRemoveTargetById=932250;ARGS:q,\
ctl:ruleRemoveTargetById=932260;ARGS:q"
# The values a JSON:API client filters by, in fields named after what they
# filter: /jsonapi/node/article?filter[title]=python3 and
# filter[f][condition][value]=python3.
SecRule REQUEST_FILENAME "@beginsWith /jsonapi/" \
"id:900804,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:/^filter\[/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^filter\[/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^filter\[/,\
ctl:ruleRemoveTargetById=932235;ARGS:/^filter\[/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^filter\[/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^filter\[/"
# The fields below are in bodies. Saving content sends its title, summary, tags,
# address, revision note, comment subject, media name and image text, which
# skip the same six rules there. A title such as "python3, ls -la and .htaccess"
# refused saving a node.
SecRule REQUEST_FILENAME \
"@rx ^/(?:node/(?:add/[^/]+|[0-9]+/edit)|comment/(?:reply/node/[0-9]+/comment|[0-9]+/edit)|media/(?:add/[^/]+|[0-9]+/edit))$" \
"id:900810,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^(?:title|subject|name|body|revision_log|path|field_tags|field_)/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^(?:(?:title|subject|name|revision_log)\[0\]\[value\]|body\[0\]\[summary\]|path\[0\]\[alias\]|field_tags\[target_id\]|field_[a-z0-9_]+\[0\]\[(?:alt|title)\])$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^(?:(?:title|subject|name|revision_log)\[0\]\[value\]|body\[0\]\[summary\]|path\[0\]\[alias\]|field_tags\[target_id\]|field_[a-z0-9_]+\[0\]\[(?:alt|title)\])$/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:(?:title|subject|name|revision_log)\[0\]\[value\]|body\[0\]\[summary\]|path\[0\]\[alias\]|field_tags\[target_id\]|field_[a-z0-9_]+\[0\]\[(?:alt|title)\])$/,\
ctl:ruleRemoveTargetById=932235;ARGS:/^(?:(?:title|subject|name|revision_log)\[0\]\[value\]|body\[0\]\[summary\]|path\[0\]\[alias\]|field_tags\[target_id\]|field_[a-z0-9_]+\[0\]\[(?:alt|title)\])$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:(?:title|subject|name|revision_log)\[0\]\[value\]|body\[0\]\[summary\]|path\[0\]\[alias\]|field_tags\[target_id\]|field_[a-z0-9_]+\[0\]\[(?:alt|title)\])$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:(?:title|subject|name|revision_log)\[0\]\[value\]|body\[0\]\[summary\]|path\[0\]\[alias\]|field_tags\[target_id\]|field_[a-z0-9_]+\[0\]\[(?:alt|title)\])$/"
# The rich text editor sends HTML in body and comment_body. Each of these rules
# refused saving a node or a comment whose HTML held an image and a code block
# with a shell script, an SQL query and JavaScript, so they skip those fields:
# 930120, 932120, 932125, 932140, 932160, 932230, 932235, 932250, 932260,
# 934100, 941160, 941390, 942170 and 942230.
SecRule REQUEST_FILENAME \
"@rx ^/(?:node/(?:add/[^/]+|[0-9]+/edit)|comment/(?:reply/node/[0-9]+/comment|[0-9]+/edit))$" \
"id:900811,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^(?:comment_)?body\[/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=932140;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=932235;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=934100;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=941390;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=942170;ARGS:/^(?:comment_)?body\[0\]\[value\]$/,\
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:comment_)?body\[0\]\[value\]$/"
# A JSON:API client saving a node or a comment sends the same HTML in the
# JSON field body.value or comment_body.value, and a title or summary or
# subject beside it, with the rules above.
SecRule REQUEST_FILENAME "@beginsWith /jsonapi/" \
"id:900812,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.data\.attributes\./ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.data\.attributes\.(?:title|subject|body\.summary|(?:comment_)?body\.value)$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.data\.attributes\.(?:title|subject|body\.summary|(?:comment_)?body\.value)$/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.data\.attributes\.(?:title|subject|body\.summary|(?:comment_)?body\.value)$/,\
ctl:ruleRemoveTargetById=932235;ARGS:/^json\.data\.attributes\.(?:title|subject|body\.summary|(?:comment_)?body\.value)$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.data\.attributes\.(?:title|subject|body\.summary|(?:comment_)?body\.value)$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.data\.attributes\.(?:title|subject|body\.summary|(?:comment_)?body\.value)$/,\
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.data\.attributes\.(?:comment_)?body\.value$/,\
ctl:ruleRemoveTargetById=932140;ARGS:/^json\.data\.attributes\.(?:comment_)?body\.value$/,\
ctl:ruleRemoveTargetById=932160;ARGS:/^json\.data\.attributes\.(?:comment_)?body\.value$/,\
ctl:ruleRemoveTargetById=934100;ARGS:/^json\.data\.attributes\.(?:comment_)?body\.value$/,\
ctl:ruleRemoveTargetById=941160;ARGS:/^json\.data\.attributes\.(?:comment_)?body\.value$/,\
ctl:ruleRemoveTargetById=941390;ARGS:/^json\.data\.attributes\.(?:comment_)?body\.value$/,\
ctl:ruleRemoveTargetById=942170;ARGS:/^json\.data\.attributes\.(?:comment_)?body\.value$/,\
ctl:ruleRemoveTargetById=942230;ARGS:/^json\.data\.attributes\.(?:comment_)?body\.value$/"
# Signing in, in pass or the JSON field pass, where a password holds any
# characters:
# 930120: a password ending in /etc/hosts
# 932260: a password starting with python3
SecRule REQUEST_FILENAME "@streq /user/login" \
"id:900813,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^(?:json\.)?pass$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^(?:json\.)?pass$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?pass$/"
-219
View File
@@ -1,219 +0,0 @@
package coreruleset_test
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
)
// drupalRequest is a line of testdata/drupal-requests.txt.
type drupalRequest struct {
Method string `json:"method"`
Target string `json:"target"`
Headers []string `json:"headers"`
Body string `json:"body"`
}
// drupalRequests returns the recorded ordinary Drupal requests.
func drupalRequests(t *testing.T) []drupalRequest {
t.Helper()
text, err := os.ReadFile(filepath.Join("testdata", "drupal-requests.txt"))
if err != nil {
t.Fatal(err)
}
var requests []drupalRequest
for line := range strings.Lines(string(text)) {
if strings.HasPrefix(line, "#") {
continue
}
var r drupalRequest
err := json.Unmarshal([]byte(line), &r)
if err != nil {
t.Fatalf("%q: %v", line, err)
}
requests = append(requests, r)
}
return requests
}
// drupalThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
const drupalThreshold = 5
// drupalAddArticle is Drupal's form for a new article.
const drupalAddArticle = "/node/add/article"
func TestOrdinaryDrupalRequestsPass(t *testing.T) {
t.Parallel()
defaults, bodies := atDefaults(t), readingBodies(t)
for _, recorded := range drupalRequests(t) {
r := request{recorded.Method, recorded.Target, recorded.Headers}
got, _ := inspectBody(t, defaults, r, recorded.Body)
if got.Score >= drupalThreshold {
t.Errorf("%s %s refused: %+v", r.method, r.target, got)
}
got, _ = inspectBody(t, bodies, r, recorded.Body)
if got.Score >= drupalThreshold {
t.Errorf("%s %s %.200q refused while bodies are read: %+v",
r.method, r.target, recorded.Body, got)
}
}
}
// drupalAttacks are an SQL injection, a path traversal and a script
// injection, as a query value.
//
//nolint:gochecknoglobals // a constant cannot be a list
var drupalAttacks = []string{
"1'%20OR%20'1'='1",
"..%2F..%2Fsites%2Fdefault%2Fsettings.php",
"%3Cscript%3Ealert(1)%3C%2Fscript%3E",
}
func TestDrupalAttacksAreRefusedInTheExcludedQueryFields(t *testing.T) {
t.Parallel()
crs := atDefaults(t)
for _, attack := range drupalAttacks {
for _, target := range []string{
"/?id=",
"/admin/content?id=",
"/admin/content?title=",
"/admin/content/media?name=",
"/admin/people?user=",
"/admin/content/files?filename=",
"/admin/content/files?filemime=",
"/admin/content/block?info=",
"/admin/content/comment?subject=",
"/admin/content/comment?author_name=",
"/admin/config/search/path?search=",
"/admin/content?destination=",
"/admin/foo?q=",
"/node/1/edit?destination=",
"/user/login?destination=",
"/search/node?keys=",
"/search?keys=",
"/entity_reference_autocomplete/user/default/KEY?q=",
"/jsonapi/node/article?filter%5Btitle%5D=",
"/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=",
} {
if got := inspect(t, crs, get(target+attack)); got.Score < drupalThreshold {
t.Errorf("%s%s let through: %+v", target, attack, got)
}
}
}
}
func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
t.Parallel()
crs := readingBodies(t)
const reply = "/comment/reply/node/1/comment"
for _, attack := range drupalAttacks {
for _, sent := range []struct{ target, body string }{
{drupalAddArticle, "title%5B0%5D%5Bvalue%5D=" + attack},
{drupalAddArticle, "body%5B0%5D%5Bvalue%5D=" + attack},
{"/node/1/edit", "body%5B0%5D%5Bsummary%5D=" + attack},
{reply, "comment_body%5B0%5D%5Bvalue%5D=" + attack},
{reply, "subject%5B0%5D%5Bvalue%5D=" + attack},
{"/user/login", "pass=" + attack},
} {
r := request{http.MethodPost, sent.target, []string{formData}}
got, _ := inspectBody(t, crs, r, sent.body)
if got.Score < drupalThreshold {
t.Errorf("POST %s %q let through: %+v", sent.target, sent.body, got)
}
}
}
for _, attack := range []string{
`1' OR '1'='1`,
`<script>alert(1)</script>`,
} {
for _, field := range []string{
`"title":"` + attack + `"`,
`"body":{"value":"` + attack + `"}`,
} {
r := request{
http.MethodPost, "/jsonapi/node/article",
[]string{"Content-Type: application/vnd.api+json"},
}
body := `{"data":{"attributes":{` + field + `}}}`
got, _ := inspectBody(t, crs, r, body)
if got.Score < drupalThreshold {
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
}
}
}
}
func TestDrupalExclusionsAreNarrow(t *testing.T) {
t.Parallel()
crs := atDefaults(t)
for _, passes := range []string{
"/admin/content?title=python3",
"/search/node?keys=python3",
"/entity_reference_autocomplete/user/default/KEY?q=python3",
"/jsonapi/node/article?filter%5Btitle%5D=python3",
"/node/1/edit?destination=python3",
} {
if got := inspect(t, crs, get(passes)); got.Score >= drupalThreshold {
t.Errorf("%s refused: %+v", passes, got)
}
}
for _, refused := range []string{
"/?title=python3",
"/admin/content?other=python3",
"/?keys=python3",
"/node/1?keys=python3",
"/?q=python3",
"/?filter%5Btitle%5D=python3",
"/python3-notes?destination=python3",
} {
if got := inspect(t, crs, get(refused)); got.Score < drupalThreshold {
t.Errorf("%s let through: %+v", refused, got)
}
}
}
func TestDrupalBodyFieldsStayCheckedInTheQuery(t *testing.T) {
t.Parallel()
crs := readingBodies(t)
// The same field name in the query keeps the rules, in the query and
// in the body.
for _, sent := range []struct{ path, field string }{
{drupalAddArticle, "title%5B0%5D%5Bvalue%5D=python3"},
{drupalAddArticle, "body%5B0%5D%5Bvalue%5D=python3"},
{"/user/login", "pass=python3"},
} {
r := request{http.MethodPost, sent.path + "?" + sent.field, []string{formData}}
got, _ := inspectBody(t, crs, r, sent.field)
if got.Score < drupalThreshold {
t.Errorf("POST %s let through: %+v", r.target, got)
}
}
}
-499
View File
@@ -1,499 +0,0 @@
# Ordinary Drupal 11.4 requests that the Core Rule Set must let through at the
# defaults. They were recorded from a site driven by a headless Chrome and an
# API client. Each line is one request as JSON. Host names, passwords and
# session cookies are invented.
{"method": "GET", "target": "/", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/core/themes/olivero/fonts/metropolis/Metropolis-Regular.woff2", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/"], "body": ""}
{"method": "GET", "target": "/core/themes/olivero/fonts/metropolis/Metropolis-Bold.woff2", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_lWnmNx7fODjwXZN0k-hinSYDGBOLz2ZYZzrxc1QYUZU.css?delta=0&language=en&theme=olivero&include=eJxtyUEOgCAMBdELgZypSEMaC59QxHB7E-PChZtZzIPK5I6QFZHU21gqNTu8u9KUTENQfetSqK8_Mt5R0xcbLu6cfFw-KvbD2bLBJUQydlP4svB0K0in8g33sTSr", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_nwzDa--ZTKor2DnrN42UKJfPZoODz_psWl5O8HnrUdg.css?delta=1&language=en&theme=olivero&include=eJxtyUEOgCAMBdELgZypSEMaC59QxHB7E-PChZtZzIPK5I6QFZHU21gqNTu8u9KUTENQfetSqK8_Mt5R0xcbLu6cfFw-KvbD2bLBJUQydlP4svB0K0in8g33sTSr", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_e1nubGx6j-CohkDnzUhS1xlK-HQ91BkCHHZe4AcOWIY.js?scope=footer&delta=0&language=en&theme=olivero&include=eJzLz8ksSy3K10_PyU9KzNEtLqnMycxL18mHCucllmWmJ5Zk5ufpFhRl5iYWVQIA53wTlA", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/"], "body": ""}
{"method": "GET", "target": "/core/themes/olivero/favicon.ico", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/"], "body": ""}
{"method": "GET", "target": "/node", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_nwzDa--ZTKor2DnrN42UKJfPZoODz_psWl5O8HnrUdg.css?delta=1&language=en&theme=olivero&include=eJxtyTEOwCAIAMAPaXwTKjGkCAaMDb_v0qFD1ztlOmhaBmsFzr6DSUbSlwUODdikkpfRBIu_cmwq_ZtLbzTsuUaurO1KHr5xlgqOD1NaLUA", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/search"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_P3HoRIw1akOqumfw52UfPdxVjrxJAO2SSqPwOEc_8DQ.css?delta=0&language=en&theme=olivero&include=eJxtyTEOwCAIAMAPaXwTKjGkCAaMDb_v0qFD1ztlOmhaBmsFzr6DSUbSlwUODdikkpfRBIu_cmwq_ZtLbzTsuUaurO1KHr5xlgqOD1NaLUA", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/search"], "body": ""}
{"method": "GET", "target": "/search/node", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/user/login", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_P3HoRIw1akOqumfw52UfPdxVjrxJAO2SSqPwOEc_8DQ.css?delta=0&language=en&theme=olivero&include=eJxtycEKwCAIANAfmvRNWhIy08ho9Pe7jLHDru-5yuLhqaoTKsTcKlYPf9hwScUpbtCHNBz7r4KzW_lm94sHF6ANpJ7PNyZSHLFjckuEwTfCXjJF", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/user/login"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_nlF7hWQsRVrlEPbc96OMGevOv3MrfMiKMVvcv6_6Joc.css?delta=1&language=en&theme=olivero&include=eJxtycEKwCAIANAfmvRNWhIy08ho9Pe7jLHDru-5yuLhqaoTKsTcKlYPf9hwScUpbtCHNBz7r4KzW_lm94sHF6ANpJ7PNyZSHLFjckuEwTfCXjJF", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/user/login"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_aFq4-wv4d0gPBP9K98TVdWfWzXoJLtTyXg_BN3tNojw.js?scope=footer&delta=0&language=en&theme=olivero&include=eJzLz8ksSy3K10_PyU9KzNEtLqnMycxL18mHCucllmWmJ5Zk5ufpFhRl5iYWVcKlShKTigEIZxiZ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/login"], "body": ""}
{"method": "GET", "target": "/user/password", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/user/register", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/no-such-page", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/rss.xml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/favicon.ico", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/rss.xml"], "body": ""}
{"method": "GET", "target": "/user/login?destination=/node/1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/contact", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search/node?keys=hello%20world", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search/node?keys=python3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search/node?keys=powershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search/node?keys=ls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search/node?keys=.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search/node?keys=wp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search/node?keys=docker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search/node?keys=cat%20%2Fetc%2Fhosts", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search/node?keys=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "POST", "target": "/user/login", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/user/login"], "body": "name=admin&pass=example-password-1&form_build_id=form-66TNlUp8tQI0lgPvIXpN9McAK6NI-RbJZJy3xQUipSQ&form_id=user_login_form&op=Log+in"}
{"method": "GET", "target": "/user/1?check_logged_in=1", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "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/user/login", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/assets/vendor/jquery/jquery.min.js?v=4.0.0", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/assets/vendor/htmx/htmx.min.js?v=2.0.4", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=olivero&include=eJxFi1EKgCAQBS9keaRYbZFHsoqu0vFbovDvMTMvIB0VlX34hotFlG8dlP3ZRqW8L7JlyNUtafxLiorJr3BCE4kURTzs0sT8Yg_UBCny", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_qhwQ8jvh5-NzL8fRjdtXDUU3Co41JbElOhNplEhbA0U.css?delta=0&language=en&theme=olivero&include=eJx9zUEOxCAIheELaT0TWtKQIhjBdnr7cTZ1N9sPXv6i4vjxAZz2PhrwVl6JTHJaKNoxFa1NBcVtE7joACeVGJ2c8f-HKmfoMQ93lbBOiWanyywuC8p0YdfEWmYfyg_t1YpmcOCCpjd23GN-Yp6TM9hjjjVlMPwCqYxVTQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_oc7ElP8_y3MBtFrEyAhgHKJSA-SMDBidNXaZSY7_mDc.css?delta=1&language=en&theme=olivero&include=eJx9zUEOxCAIheELaT0TWtKQIhjBdnr7cTZ1N9sPXv6i4vjxAZz2PhrwVl6JTHJaKNoxFa1NBcVtE7joACeVGJ2c8f-HKmfoMQ93lbBOiWanyywuC8p0YdfEWmYfyg_t1YpmcOCCpjd23GN-Yp6TM9hjjjVlMPwCqYxVTQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_Rj8FErIgCRmm_fqnNH-MBKqak3Tzzlv2cmHnva1IUDE.js?scope=footer&delta=1&language=en&theme=olivero&include=eJxFi1EKgCAQBS9keaRYbZFHsoqu0vFbovDvMTMvIB0VlX34hotFlG8dlP3ZRqW8L7JlyNUtafxLiorJr3BCE4kURTzs0sT8Yg_UBCny", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_GwAx9-XxvwjT99hmLVXFfHhAt3uBqjr-skP9WQDCcoM.js?scope=footer&delta=3&language=en&theme=olivero&include=eJxFi1EKgCAQBS9keaRYbZFHsoqu0vFbovDvMTMvIB0VlX34hotFlG8dlP3ZRqW8L7JlyNUtafxLiorJr3BCE4kURTzs0sT8Yg_UBCny", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_3uG4RM-Em2VD212gfyh-PIM42Pr7XCSwv3TXIbnBX9A.css?delta=0&language=en&theme=olivero&include=eJzLz8ksSy3K189LLMtMTyzJzM_TLSjKzE0sqgQAjl4KkQ&exclude=eJx9jVEOgzAMQy_U0iNNaYlQRYirNmXj9oNNwB9_T_aTnaDGH-skYay9kAzpSrxknZtrWzNeQqTGLqHyKVKyvPJPcpAdK0LBmyuPPm4-CtLsYp5eJRcOJ1zqXu8Xxwi0_YcTlgJltTYorXmio_PeAIlUfexm0Gc1m7C7k3DjILSh2xdIDF3F", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_JYK8tPdiju1DBZ5kGKcUua-GioSL5qptDKhEKxqe4cM.css?delta=0&language=en&theme=olivero&include=eJzLz8ksSy3K189LLMtMTyzJzM_TLU5Nzs9LSSyqBACkMwtV&exclude=eJx9jkEOwyAMBD8E4UmVIVZkxbERmLT8vrSNkpx6W61nNY60PDJlDPEILqkYvqwBh7m0DDxdjWeStQ6kYEi6ZRUUq5PATgsYqXhvZIz_CVWOUHxsZio_9DBBMtrxa3HXJFxxYujazCkPrmhgTeOrz0ylnu3NlgttUPp5yvrEgrOP3ccxXl3t1XALESq-Afi0aIE", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=block%3Ablock%3Dolivero_breadcrumbs%3Alangcode%3Den&tokens%5B%5D=DupSXj2bNdZYw9OEb6f4T6BX3qXPnA1znFW4miVijJU"}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=block%3Ablock%3Dolivero_site_branding%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_page_title%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_powered%3Alangcode%3Den&tokens%5B%5D=1rv_5u0s4iGV_AWmQmPdRm4CsxIu8wsjE8sFkt9yGF4&tokens%5B%5D=pGkK5C6k5HhOIlpSVObsmKdf5DVWqjhEzJfvUSZdanw&tokens%5B%5D=jmjvcBoCtAymF3-B_C5BHjXRBqYcwKstu9qZq-rxCDw"}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=block%3Ablock%3Dolivero_account_menu%3Alangcode%3Den%7Cmenu%3Amenu%3Daccount%3Alangcode%3Den&tokens%5B%5D=henkHDCYz37bUB9MGiVxhW9dClN1HHpXLkE9blnvU8I"}
{"method": "GET", "target": "/sites/default/files/js/js_5hB-Wy2Hxb85RQLTESYetivjeaSJY2GQ6oXCxZeNfeA.js?scope=footer&delta=0&language=en&theme=olivero&include=eJzLz8ksSy3K189LLMtMTyzJzM_TLSjKzE0sqgQAjl4KkQ&exclude=eJx9jVEOgzAMQy_U0iNNaYlQRYirNmXj9oNNwB9_T_aTnaDGH-skYay9kAzpSrxknZtrWzNeQqTGLqHyKVKyvPJPcpAdK0LBmyuPPm4-CtLsYp5eJRcOJ1zqXu8Xxwi0_YcTlgJltTYorXmio_PeAIlUfexm0Gc1m7C7k3DjILSh2xdIDF3F", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=block%3Ablock%3Dolivero_main_menu%3Alangcode%3Den%7Cmenu%3Amenu%3Dmain%3Alangcode%3Den&tokens%5B%5D=8Gi0ikQVIh2gC601Y5UBe2vi21hWy8qqpz--u2aRwZU"}
{"method": "GET", "target": "/admin/modules", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=claro&include=eJxVjkEOwzAIBD_kJG-iDopQMViAo_b3dZtakY_MALuZwXTbrVXgVeBcAh6eskrgKxrwUDdZmOT5XTEcEnLQiT8x8azMUB0nWNAdjpn1UEZDryreP6VehA4IUtmo55pc5f4s-dsDyzi-prXo3hj9Azu2UZU", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/modules", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_V22smjChlNYtGFhubSnrJiBbIXxpjPF7r7uLdgFcWYg.css?delta=0&language=en&theme=claro&include=eJxVjlEKAzEIBS-0bs5kshKkRoMxpe3pG9hC6O84b7AIuqUqllFgxFtY61FMg14xUdLls6Ocm8AyHmMpTqlY66akMU7FJ1cMNgUIM8nokGeE6a3-OhzUVmHEH3UaFIC9Ezpqofuo5g2FP3TseOL1h-vabPYFJPxMGw", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/modules", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_5uXlz2e2qK3icTms7ZW2kFOaDl2e9vq8lXYWMY0zmJw.css?delta=1&language=en&theme=claro&include=eJxVjlEKAzEIBS-0bs5kshKkRoMxpe3pG9hC6O84b7AIuqUqllFgxFtY61FMg14xUdLls6Ocm8AyHmMpTqlY66akMU7FJ1cMNgUIM8nokGeE6a3-OhzUVmHEH3UaFIC9Ezpqofuo5g2FP3TseOL1h-vabPYFJPxMGw", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/modules", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_Yfs9-4VfSKB714k1kHFtY4JRa_hGedSW2uxnik6dlsA.js?scope=footer&delta=1&language=en&theme=claro&include=eJxVjkEOwzAIBD_kJG-iDopQMViAo_b3dZtakY_MALuZwXTbrVXgVeBcAh6eskrgKxrwUDdZmOT5XTEcEnLQiT8x8azMUB0nWNAdjpn1UEZDryreP6VehA4IUtmo55pc5f4s-dsDyzi-prXo3hj9Azu2UZU", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/modules", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/admin/modules", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/admin/modules", "Cookie: SESSexample=example-session-value"], "body": "text=&modules%5Bmedia%5D%5Benable%5D=1&modules%5Bmedia_library%5D%5Benable%5D=1&modules%5Bbasic_auth%5D%5Benable%5D=1&modules%5Bjsonapi%5D%5Benable%5D=1&modules%5Brest%5D%5Benable%5D=1&form_build_id=form-rTmWsVrCT6WepF1-_TDWkfTkaaNLlMY8wxMtnsYWGvo&form_token=YAgSiY0E1e9HGDw2HnSiS67ixWkvvnJtCDwS6VQFU-Y&form_id=system_modules&op=Install"}
{"method": "GET", "target": "/admin/modules/list/confirm", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "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/admin/modules", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=claro&include=eJxFi1EKgCAQBS9keaRYbZFHsoqu0vFbovDvMTMvIB0VlX34hotFlG8dlP3ZRqW8L7JlyNUtafxLiorJr3BCE4kURTzs0sT8Yg_UBCny", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/modules/list/confirm", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_Rj8FErIgCRmm_fqnNH-MBKqak3Tzzlv2cmHnva1IUDE.js?scope=footer&delta=1&language=en&theme=claro&include=eJxFi1EKgCAQBS9keaRYbZFHsoqu0vFbovDvMTMvIB0VlX34hotFlG8dlP3ZRqW8L7JlyNUtafxLiorJr3BCE4kURTzs0sT8Yg_UBCny", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/modules/list/confirm", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_5uXlz2e2qK3icTms7ZW2kFOaDl2e9vq8lXYWMY0zmJw.css?delta=1&language=en&theme=claro&include=eJxFjVEKAzEIBS-0bs5kUglSo4sxpe3pK2whv8O8eU3QrXSxigIzPsLaj2Ya9I6FUh6-LpRzE0jjOVNxKs3GZUoa81R8ccdgU4Awk4oOdUWY3uq_w0EjCzNuquYDhb907H3hvHJNebMfMydBAQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/modules/list/confirm", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_dwzS5I9OMr00BcXlUn2t2KsKkO9EWWCZE6LZ1FmemH0.css?delta=0&language=en&theme=claro&include=eJxFjVEKAzEIBS-0bs5kUglSo4sxpe3pK2whv8O8eU3QrXSxigIzPsLaj2Ya9I6FUh6-LpRzE0jjOVNxKs3GZUoa81R8ccdgU4Awk4oOdUWY3uq_w0EjCzNuquYDhb907H3hvHJNebMfMydBAQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/modules/list/confirm", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_vwLE_CZTWTR3i1176WdafZPd0GuZTOaM96RTRN5CTp0.js?scope=footer&delta=3&language=en&theme=claro&include=eJxFi1EKgCAQBS9keaRYbZFHsoqu0vFbovDvMTMvIB0VlX34hotFlG8dlP3ZRqW8L7JlyNUtafxLiorJr3BCE4kURTzs0sT8Yg_UBCny", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/modules/list/confirm", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/admin/modules/list/confirm", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/admin/modules/list/confirm", "Cookie: SESSexample=example-session-value"], "body": "confirm=1&form_build_id=form-g3r5ZjoUbNVxAXsFI-wm70qV7waoi0wi6zuZPBDsUrU&form_token=gkxg1871lXWQF8ZemxwSn2XUPNst5Hpx-KNLlJwJ5DU&form_id=system_modules_confirm_form&op=Continue"}
{"method": "GET", "target": "/admin/config/services/jsonapi", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_2XXM5MZ_MIzS5taV4QGJgA5WOMfNbkQyy93p1WQyRNI.css?delta=0&language=en&theme=claro&include=eJxFzFEKxCAMRdENNXVN0YqEpnklxjLT1Y8wH_5eDrcoO1JTZFbq8VWxthVY1E8M1nT4uFn3VWiKs0_iNRVcN6xa9N34kcYhMKIANLNTHhGwPzX4xSpv3ZZMMqduc7_aDwZeOK8", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/config/services/jsonapi", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_VrhKC7-kxWGUIEsCT4AAFSWF6Z4UJH3OTQdHX4_R8pM.css?delta=1&language=en&theme=claro&include=eJxFzFEKxCAMRdENNXVN0YqEpnklxjLT1Y8wH_5eDrcoO1JTZFbq8VWxthVY1E8M1nT4uFn3VWiKs0_iNRVcN6xa9N34kcYhMKIANLNTHhGwPzX4xSpv3ZZMMqduc7_aDwZeOK8", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/config/services/jsonapi", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/admin/config/services/jsonapi", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/admin/config/services/jsonapi", "Cookie: SESSexample=example-session-value"], "body": "read_only=rw&form_build_id=form-WZnlHr3p1LhSxN6dPUx293-ltyG4QtYSRksVbr20Mzg&form_token=3lcJI3eAzFu8-0sfi7vAMI3eU2V91VUFYTwTcOLf7l8&form_id=jsonapi_settings&op=Save+configuration"}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=claro&include=eJxVzGEKgDAIQOELrXYmMRmSuaFudPxGUNHf98HDqkFndJC8WW8gK75lEdbdE1ajBwGDB93w6we5Q6GkMLhAcNXMc2M67WsXMjYquA", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/config/services/jsonapi", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_v-Yw0xKoOugqNPSjJ5Uq4iDp8HsOpoK3gpMm6zQyQf0.js?scope=footer&delta=1&language=en&theme=claro&include=eJxVzGEKgDAIQOELrXYmMRmSuaFudPxGUNHf98HDqkFndJC8WW8gK75lEdbdE1ajBwGDB93w6we5Q6GkMLhAcNXMc2M67WsXMjYquA", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/config/services/jsonapi", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_xN42W8Ek2DB8BqQV6yHIuvbUBLGM5TZ2vbsUruJAfoM.css?delta=0&language=en&theme=claro&include=eJxLzi9K1U8pKi1IzNErSi1OLdFNLChITSxKzEtOBQCdwwrv&exclude=eJxFjEEOwjAMBD-UNE9CjokiC9cbJU5FeT0VBXobrWaWYV6ePknTvc9GuvB_iSr2GIHRSzL0lVReJbBSR6qKfBjD90Oqp_M9IHbZyicORptUcoGlCxelHdPPiLE2WDEfy2XE6IBm6jFPd1jIUm9NWkk_eAMPukfa", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_oqDrM0IhMLn8AZOW50puF3Yhjjr8wcLaRDch0LJwjvg.js?scope=footer&delta=0&language=en&theme=claro&include=eJxLzkksytdPKSotSMzRy0ss0y1JTCoGAFlaCBM&exclude=eJxFjEEOwjAMBD-UNE9CjokiC9cbJU5FeT0VBXobrWaWYV6ePknTvc9GuvB_iSr2GIHRSzL0lVReJbBSR6qKfBjD90Oqp_M9IHbZyicORptUcoGlCxelHdPPiLE2WDEfy2XE6IBm6jFPd1jIUm9NWkk_eAMPukfa", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=claro&include=eJxljdEKgCAMRX_I8pNi2pDRmEOn9PlJFBK9Xc45cAOlTUnRh2e4mMXwtAbs99IUeJ1kYZKjjqTgKyEadbzFhxsExoJVs9QR_F1FxmhOoFMCoyyexk2R4Se7AGS2PkQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_Rj8FErIgCRmm_fqnNH-MBKqak3Tzzlv2cmHnva1IUDE.js?scope=footer&delta=1&language=en&theme=claro&include=eJxljdEKgCAMRX_I8pNi2pDRmEOn9PlJFBK9Xc45cAOlTUnRh2e4mMXwtAbs99IUeJ1kYZKjjqTgKyEadbzFhxsExoJVs9QR_F1FxmhOoFMCoyyexk2R4Se7AGS2PkQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_BlPFDilS_oEhwF0HDxZ480ulASjw5IudrBkkXsaaxvI.css?delta=0&language=en&theme=claro&include=eJxljkkOAyEMBD_E8iYDFkEx9siYyfL6oEwkDrn0oVQqdSZQiZUkAflhL2pcXRY2fNoEikXnARQ28cu4j6Uoxiz9EEa2ERjOVsGasPcmQgnUp2kmfKm_jkEivCEU1H8-RO2iLNqB2hvd7sa2LigveTN3NnyM-N3QpUzCD4lnUc8", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/content", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_65vRiN4T0Zm5WPkTX4WXHxzogBsxjBWtemrAP7Ianv0.css?delta=1&language=en&theme=claro&include=eJxljkkOAyEMBD_E8iYDFkEx9siYyfL6oEwkDrn0oVQqdSZQiZUkAflhL2pcXRY2fNoEikXnARQ28cu4j6Uoxiz9EEa2ERjOVsGasPcmQgnUp2kmfKm_jkEivCEU1H8-RO2iLNqB2hvd7sa2LigveTN3NnyM-N3QpUzCD4lnUc8", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/content", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_CcEISLR1pk_U-fpVvH09p2TLaOmRHxcIbXaUgsAf38o.js?scope=footer&delta=3&language=en&theme=claro&include=eJxljdEKgCAMRX_I8pNi2pDRmEOn9PlJFBK9Xc45cAOlTUnRh2e4mMXwtAbs99IUeJ1kYZKjjqTgKyEadbzFhxsExoJVs9QR_F1FxmhOoFMCoyyexk2R4Se7AGS2PkQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=claro&include=eJxVjNEKgCAMRX-o8pNi2pDRmGNO6fOTKKS3yzmHGynvSoohvmNJRRwvb8DhsKbA2yQrk5x1JIafhOTU8RE_7hAZDasWqSNYBDplcCoSaNyZjGayGwP4NQI", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content/files", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_Rj8FErIgCRmm_fqnNH-MBKqak3Tzzlv2cmHnva1IUDE.js?scope=footer&delta=1&language=en&theme=claro&include=eJxVjNEKgCAMRX-o8pNi2pDRmGNO6fOTKKS3yzmHGynvSoohvmNJRRwvb8DhsKbA2yQrk5x1JIafhOTU8RE_7hAZDasWqSNYBDplcCoSaNyZjGayGwP4NQI", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content/files", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_65vRiN4T0Zm5WPkTX4WXHxzogBsxjBWtemrAP7Ianv0.css?delta=1&language=en&theme=claro&include=eJxFjUkOAyEMBD_E8ibDWMiKsUfGTJbXB2UicelDqbq7MpjmxlqA4_A3k7RQVRxfPoHzYfMETpvEZTzGUgxz1X6qoPhIAhc1cFKJ0VW5gMUy3VVu9b_jUBiHmt9U1DowfTDsfqZ1ZbLkzcJF-Bz5l6nrMRm_XhtIpA", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/content/files", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_DAGcX-roZwuoTrgJMQuFtrMOlf7B6qygq9ldzK5J9fQ.css?delta=0&language=en&theme=claro&include=eJxFjUkOAyEMBD_E8ibDWMiKsUfGTJbXB2UicelDqbq7MpjmxlqA4_A3k7RQVRxfPoHzYfMETpvEZTzGUgxz1X6qoPhIAhc1cFKJ0VW5gMUy3VVu9b_jUBiHmt9U1DowfTDsfqZ1ZbLkzcJF-Bz5l6nrMRm_XhtIpA", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/content/files", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_MzYQPMFC12WkHnUb4AyqSN5rpid4chCag_aR1pChSsQ.js?scope=footer&delta=3&language=en&theme=claro&include=eJxVjNEKgCAMRX-o8pNi2pDRmGNO6fOTKKS3yzmHGynvSoohvmNJRRwvb8DhsKbA2yQrk5x1JIafhOTU8RE_7hAZDasWqSNYBDplcCoSaNyZjGayGwP4NQI", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content/files", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=claro", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/admin/content/files", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=entity.view.edit_form%3Aview%3Dfiles%3Alocation%3Dpage%26name%3Dfiles%26display_id%3Dpage_1%26langcode%3Den&tokens%5B%5D=Cypp2HJrw0AOe1SofB-g18guU4gfVM4DwiYMLTrp7Vo"}
{"method": "GET", "target": "/admin/content/media", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=claro&include=eJxVjFEKgCAQBS9UeaRYbZFHyyq6SsdPopD-HjPD84h7Rmbn37GEpMaXNRJ3lJZJtklWgZ51JIU_ScHQ-RE_buSFKwsHW5Q6IhmSOoyrosNPdgMrQjM0", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content/media", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_Rj8FErIgCRmm_fqnNH-MBKqak3Tzzlv2cmHnva1IUDE.js?scope=footer&delta=1&language=en&theme=claro&include=eJxVjFEKgCAQBS9UeaRYbZFHyyq6SsdPopD-HjPD84h7Rmbn37GEpMaXNRJ3lJZJtklWgZ51JIU_ScHQ-RE_buSFKwsHW5Q6IhmSOoyrosNPdgMrQjM0", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content/media", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_PIXqWZRZfDre6leYgjG1ppL5f4T6fgWnaJOocJBHvSs.js?scope=footer&delta=3&language=en&theme=claro&include=eJxVjFEKgCAQBS9UeaRYbZFHyyq6SsdPopD-HjPD84h7Rmbn37GEpMaXNRJ3lJZJtklWgZ51JIU_ScHQ-RE_buSFKwsHW5Q6IhmSOoyrosNPdgMrQjM0", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content/media", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=claro", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/admin/content/media", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=entity.view.edit_form%3Aview%3Dmedia%3Alocation%3Dpage%26name%3Dmedia%26display_id%3Dmedia_page_list%26langcode%3Den&tokens%5B%5D=gRSv7UHLkroVt8nCE9bjv39cQ2rcWbPgIomvr8XYvPU"}
{"method": "GET", "target": "/admin/content/block", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=claro", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/admin/content/block", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=entity.view.edit_form%3Aview%3Dblock_content%3Alocation%3Dpage%26name%3Dblock_content%26display_id%3Dpage_1%26langcode%3Den&tokens%5B%5D=7taM7aKonK2Mcsm70tCmCBC6-KRZ5TStMivh9X4pq-8"}
{"method": "GET", "target": "/admin/content/comment", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=olivero&include=eJxFjEEOgDAIBD9U7ZsQm4aI0CAa-3uJifU6s7Oo4uX2EzivdjbgGQeZmGQ7EqqVTwI6XeUVSeCiCk4qmSIxCf-zpBxL01xZl_g6vEdVB_6XUzPawfoD-vI22A", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content/comment", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_qhwQ8jvh5-NzL8fRjdtXDUU3Co41JbElOhNplEhbA0U.css?delta=0&language=en&theme=olivero&include=eJxtjkEOwyAMBD8E4U2GWAjF2Mh20vL7ckpy6HVGmt0i7Pj1Eyjteg6grdwkUuPDQhHFVKQPYWS3jeFqFbwJx-gilEFjPt2Fw6NSWxXl1XtYEGoXqqRKklfefK6FeuNXeGjroPOfMlwP97cc8kHFPeYZM0k5gk1z7CmD4Q_pfVgx", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/content/comment", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_xXCDbP-RAFtr_qi4RQmy83L1Stl0GIDonFQNXSDjcIM.css?delta=1&language=en&theme=olivero&include=eJxtjkEOwyAMBD8E4U2GWAjF2Mh20vL7ckpy6HVGmt0i7Pj1Eyjteg6grdwkUuPDQhHFVKQPYWS3jeFqFbwJx-gilEFjPt2Fw6NSWxXl1XtYEGoXqqRKklfefK6FeuNXeGjroPOfMlwP97cc8kHFPeYZM0k5gk1z7CmD4Q_pfVgx", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/content/comment", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_hu2qYVXcDNCCkSE_yDBAQwtnjk6xIKxwvzSYExQhhJk.js?scope=footer&delta=1&language=en&theme=olivero&include=eJxFjEEOgDAIBD9U7ZsQm4aI0CAa-3uJifU6s7Oo4uX2EzivdjbgGQeZmGQ7EqqVTwI6XeUVSeCiCk4qmSIxCf-zpBxL01xZl_g6vEdVB_6XUzPawfoD-vI22A", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/content/comment", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure/types", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure/types/manage/article", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure/types/manage/article/fields", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure/taxonomy", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=claro&include=eJxVzVEKxDAIBNALtc2RikkluCsqxpQev6HsUvInb4YxU92NDFP-HUtRCbyiA6fDuwFvr6xM8m2j4vgPoQSd-ASTHwSsdYMPXLO7Wu4RKhMHZEbHZipt7C0CJ1UIUkk0vruMzms3QydHCQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/structure/taxonomy", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_a0l6DFNDHHdFIetyfjHJ-uIfWwXSO0EmaV_goLTsK6Y.css?delta=0&language=en&theme=claro&include=eJxVjlsOwyAMBC8UwpkcgpBbx4uMqdKevkitRPI7mn0kIUMsgo0kNH8La1kS1PPpnSTu1ivJOkkYxrMNxXJMOCo0q7dV6cWFnKEhOCAbWdi6O_Sn_nvoQecN7EyCckeGeo0q7CDhT17mSOTxx3TYk30BjpRPQg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/structure/taxonomy", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_BphLjOXn9B0C8-cuBOEKIDKSXP4pW8ECiyxNm922aI0.css?delta=1&language=en&theme=claro&include=eJxVjlsOwyAMBC8UwpkcgpBbx4uMqdKevkitRPI7mn0kIUMsgo0kNH8La1kS1PPpnSTu1ivJOkkYxrMNxXJMOCo0q7dV6cWFnKEhOCAbWdi6O_Sn_nvoQecN7EyCckeGeo0q7CDhT17mSOTxx3TYk30BjpRPQg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/structure/taxonomy", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_-vK4LyWpDr9lb8GbXxZjkSnHhHJxu3Daihrs4NQA5Sw.js?scope=footer&delta=1&language=en&theme=claro&include=eJxVzVEKxDAIBNALtc2RikkluCsqxpQev6HsUvInb4YxU92NDFP-HUtRCbyiA6fDuwFvr6xM8m2j4vgPoQSd-ASTHwSsdYMPXLO7Wu4RKhMHZEbHZipt7C0CJ1UIUkk0vruMzms3QydHCQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/structure/taxonomy", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_0BtosoKuAqM5GtLkuevTJnnLoBkSvptjlR0qQYYDruk.js?scope=footer&delta=3&language=en&theme=claro&include=eJxVzVEKxDAIBNALtc2RikkluCsqxpQev6HsUvInb4YxU92NDFP-HUtRCbyiA6fDuwFvr6xM8m2j4vgPoQSd-ASTHwSsdYMPXLO7Wu4RKhMHZEbHZipt7C0CJ1UIUkk0vruMzms3QydHCQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/structure/taxonomy", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure/taxonomy/manage/tags/overview", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=claro&include=eJxljeEKgCAMhF-o8pFi6pCRzDGn9PhJFBL9O777jvOUdiFB55-whMKGpzXILmoTyNskayY-6lAU3xKCUce7-HADnzEqpD9VrFK4jtnC0CmBUWFH40R5OJNd3Lg9Yg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/structure/taxonomy/manage/tags/overview", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_MzYQPMFC12WkHnUb4AyqSN5rpid4chCag_aR1pChSsQ.js?scope=footer&delta=3&language=en&theme=claro&include=eJxljeEKgCAMhF-o8pFi6pCRzDGn9PhJFBL9O777jvOUdiFB55-whMKGpzXILmoTyNskayY-6lAU3xKCUce7-HADnzEqpD9VrFK4jtnC0CmBUWFH40R5OJNd3Lg9Yg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/structure/taxonomy/manage/tags/overview", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_VrhKC7-kxWGUIEsCT4AAFSWF6Z4UJH3OTQdHX4_R8pM.css?delta=1&language=en&theme=claro&include=eJxFzdEKwzAIheEXappnOkklyKwWY8q2p1-gg9z-fB6rwC03sQJJPT7C2rZqGvSOAcmHjwuyr5KmePVJnHK18zIljb4rbm4INk0pzKTAUxkRpg_97wSK0OFoT1XzE8Jf2tZ95vnKdeLVfjY9QQ8", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/structure/taxonomy/manage/tags/overview", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_beYwnrkENjJYreCPf0waaN0M5ZTXCT9GLjw5onkeDjw.css?delta=0&language=en&theme=claro&include=eJxFzdEKwzAIheEXappnOkklyKwWY8q2p1-gg9z-fB6rwC03sQJJPT7C2rZqGvSOAcmHjwuyr5KmePVJnHK18zIljb4rbm4INk0pzKTAUxkRpg_97wSK0OFoT1XzE8Jf2tZ95vnKdeLVfjY9QQ8", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/structure/taxonomy/manage/tags/overview", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_FTAfAnA5r42E9GEn9GkVGDPIg8L0IeULw-SoXT7uZoM.js?scope=footer&delta=1&language=en&theme=claro&include=eJxljeEKgCAMhF-o8pFi6pCRzDGn9PhJFBL9O777jvOUdiFB55-whMKGpzXILmoTyNskayY-6lAU3xKCUce7-HADnzEqpD9VrFK4jtnC0CmBUWFH40R5OJNd3Lg9Yg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/structure/taxonomy/manage/tags/overview", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure/block", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/js/js_LNOTk1lFh5UgLuvg2TMtESHrajbvYlKejbiPYedl_nM.js?scope=header&delta=0&language=en&theme=claro&include=eJxtjVEKxDAIRC-UbY60aCJFmlUxpuzxN5QtpdA_570ZRF7fxkYZ_0fCpmXL1YdBW47wgBaoH5ZUVIK-MaCd9iKvxrL1WXE6JZTgnQ5x49XVcESo3HAANnLqptLnLgnsvEKwSub5xWV2LvYD225Okg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/structure/block", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_SCXoTr9vN6hZiPM6C11NFP3oxLk6RnPB0IqXzA11Yqw.css?delta=0&language=en&theme=claro&include=eJxtjkEOwyAMBD8U4E0GrMaKsZFjoqqvL1VacelxZ0erzazlSNVGB475EyLURrIVFcenD-Bfu0hgkuOcimEq2roKip9R4KIHOKmE4KqcwUIe7iq3-t2ppv0PdsiMO0JFu7moNWB64baGE80PJlNf7A0P1Uwv", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/structure/block", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_XNws43iU5klsxMgxmee1XUhMpXK6wUYiEyUy32Zk_Z4.css?delta=1&language=en&theme=claro&include=eJxtjkEOwyAMBD8U4E0GrMaKsZFjoqqvL1VacelxZ0erzazlSNVGB475EyLURrIVFcenD-Bfu0hgkuOcimEq2roKip9R4KIHOKmE4KqcwUIe7iq3-t2ppv0PdsiMO0JFu7moNWB64baGE80PJlNf7A0P1Uwv", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/structure/block", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure/views", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css_9jp0Ks7ap-7-ZCIp65X0Z6Dp_lLzTnFyXr9PrernROs.css?delta=0&language=en&theme=claro&include=eJxVj0EOwyAMBD8UwpMqBxBya2xkTJr29UVqJJLraLy7DgQqPpNsQK7Zh5DzEoQtHdaBfNRegdZJ3DBebSiafJBShRNbWxl2zGAo7JyJ0Abqtm4m_FfPHHjCcQMRgSTfkUq9nrJoAcJvWmaJx7FHediTLTumd3t09BAL8np-8wMX8lhJ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/structure/views", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css__5y-CaSwUlbXduvNLQPZBVyDUdtabORZbS0ZhFIiGQo.css?delta=1&language=en&theme=claro&include=eJxVj0EOwyAMBD8UwpMqBxBya2xkTJr29UVqJJLraLy7DgQqPpNsQK7Zh5DzEoQtHdaBfNRegdZJ3DBebSiafJBShRNbWxl2zGAo7JyJ0Abqtm4m_FfPHHjCcQMRgSTfkUq9nrJoAcJvWmaJx7FHediTLTumd3t09BAL8np-8wMX8lhJ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/structure/views", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure/views/view/content", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/sites/default/files/css/css__5y-CaSwUlbXduvNLQPZBVyDUdtabORZbS0ZhFIiGQo.css?delta=1&language=en&theme=claro&include=eJxtj1sKwzAMBC8Ux0cq69gYtYoUZDl9nL6GBkKgv8Nod7UwTGNlTeDQ_M0kdVpUvLy8g2O2voHnk4RhPNpQrMRF102liLdZsFOFk0oIrsoJFlJ3V_mpRw7ueF1AJrDWKzLd_pw6EpdsOGRRW8H0KdNZHWmsNBnyyaadyrPdOkXklWQ-fvwC-TFgqQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/css,*/*;q=0.1", "Referer: http://example.com/admin/structure/views/view/content", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/admin/structure/views/view/content/preview/page_1?_wrapper_format=drupal_ajax", "headers": ["Host: example.com", "X-Requested-With: XMLHttpRequest", "Accept-Language: en-US,en;q=0.9", "Accept: application/json, text/javascript, */*; q=0.01", "Content-Type: application/x-www-form-urlencoded; charset=UTF-8", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Origin: http://example.com", "Referer: http://example.com/admin/structure/views/view/content", "Cookie: SESSexample=example-session-value"], "body": "live_preview=1&view_args=&form_build_id=form-F3rj5YfR2asqg9sweZNjFfkXTvF0TvHucDW4zRpICrQ&form_token=2RiXwzIOzgnAYh4frV_Aykfi5NgGwuk_dGZCLtCUxjg&form_id=view_preview_form&_triggering_element_name=op&_triggering_element_value=Update+preview&_drupal_ajax=1&ajax_page_state%5Btheme%5D=claro&ajax_page_state%5Btheme_token%5D=5e5HEgjfZ80aNQxNU9VRZE4_hDmt-aGjuwXr5qcCy0E&ajax_page_state%5Blibraries%5D=eJxVjlEKgzAQBS9kkiOVF13CtuuuJBurPX1LbRH_hmF4vMzltvBCKf9gGAXVUhHLkNB8F9YyjKZOm3dImmpfIPE04VM82ieplEabF1NSb1GxcoGzaQhuJhk15O5ueqS_HYzOK303Ln5iiJWIO7aLbw6ndlGOLDRVlMOq1RnCLxrOC-nEKNit-7AyPdutc_pDxDSzvgGedGns"}
{"method": "GET", "target": "/admin/structure/menu", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/appearance", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/themes/claro/screenshot.png", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/appearance", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/themes/olivero/screenshot.png", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/appearance", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/themes/stark/screenshot.png", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/appearance", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/themes/default_admin/screenshot.png", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/admin/appearance", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/modules/uninstall", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/system/site-information", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/search/pages", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/search/path", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/search/path/add", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/content/formats", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/content/formats/manage/basic_html", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/assets/vendor/ckeditor5/ckeditor5-dll/ckeditor5-dll.js?v=47.6.3-drupal.1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/admin/config/content/formats/manage/basic_html", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/modules/ckeditor5/icons/redo.svg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/sites/default/files/css/css_psPaPw47aVeauGJEroVt9ZJCrdzD9yk6tjgLd9RaH3I.css?delta=0&language=en&theme=claro&include=eJx9kkFOxDAMRS9E2xUXAMGKS7iJSa1x7OIkMzCnJ50iVSzqZfyffuxvhwtGqmrPE0lFE-ARYiZ5CifCCExJMko9R2YoFByZNVy-mlY8Z4JGfNm4cyRaW4FzV-EcwlJ6qwRczpnPxlyCITpTLwiRJDmAGt1VKvAHiTMYZUieLNGNlkFScx2YxAmNqTjuhlmv-K6WwaHKorfHbpxMizYL-NZFN7WyYujLeV3AIHTFc6w_7IxdYd7kvgH8rg142s9jPCrDlkz_QA2noHlV6UmXUeBKCSqpDENV5RlsmFutKjv65_Pwjwbpf7VbgyEMhoXuuGuy5cfb8_A-2j1qv316VjY", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/modules/ckeditor5/icons/undo.svg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/sites/default/files/css/css_psPaPw47aVeauGJEroVt9ZJCrdzD9yk6tjgLd9RaH3I.css?delta=0&language=en&theme=claro&include=eJx9kkFOxDAMRS9E2xUXAMGKS7iJSa1x7OIkMzCnJ50iVSzqZfyffuxvhwtGqmrPE0lFE-ARYiZ5CifCCExJMko9R2YoFByZNVy-mlY8Z4JGfNm4cyRaW4FzV-EcwlJ6qwRczpnPxlyCITpTLwiRJDmAGt1VKvAHiTMYZUieLNGNlkFScx2YxAmNqTjuhlmv-K6WwaHKorfHbpxMizYL-NZFN7WyYujLeV3AIHTFc6w_7IxdYd7kvgH8rg142s9jPCrDlkz_QA2noHlV6UmXUeBKCSqpDENV5RlsmFutKjv65_Pwjwbpf7VbgyEMhoXuuGuy5cfb8_A-2j1qv316VjY", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/modules/ckeditor5/icons/special-characters.svg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/sites/default/files/css/css_psPaPw47aVeauGJEroVt9ZJCrdzD9yk6tjgLd9RaH3I.css?delta=0&language=en&theme=claro&include=eJx9kkFOxDAMRS9E2xUXAMGKS7iJSa1x7OIkMzCnJ50iVSzqZfyffuxvhwtGqmrPE0lFE-ARYiZ5CifCCExJMko9R2YoFByZNVy-mlY8Z4JGfNm4cyRaW4FzV-EcwlJ6qwRczpnPxlyCITpTLwiRJDmAGt1VKvAHiTMYZUieLNGNlkFScx2YxAmNqTjuhlmv-K6WwaHKorfHbpxMizYL-NZFN7WyYujLeV3AIHTFc6w_7IxdYd7kvgH8rg142s9jPCrDlkz_QA2noHlV6UmXUeBKCSqpDENV5RlsmFutKjv65_Pwjwbpf7VbgyEMhoXuuGuy5cfb8_A-2j1qv316VjY", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/modules/ckeditor5/icons/underline.svg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/sites/default/files/css/css_psPaPw47aVeauGJEroVt9ZJCrdzD9yk6tjgLd9RaH3I.css?delta=0&language=en&theme=claro&include=eJx9kkFOxDAMRS9E2xUXAMGKS7iJSa1x7OIkMzCnJ50iVSzqZfyffuxvhwtGqmrPE0lFE-ARYiZ5CifCCExJMko9R2YoFByZNVy-mlY8Z4JGfNm4cyRaW4FzV-EcwlJ6qwRczpnPxlyCITpTLwiRJDmAGt1VKvAHiTMYZUieLNGNlkFScx2YxAmNqTjuhlmv-K6WwaHKorfHbpxMizYL-NZFN7WyYujLeV3AIHTFc6w_7IxdYd7kvgH8rg142s9jPCrDlkz_QA2noHlV6UmXUeBKCSqpDENV5RlsmFutKjv65_Pwjwbpf7VbgyEMhoXuuGuy5cfb8_A-2j1qv316VjY", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/modules/ckeditor5/icons/code-block.svg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/sites/default/files/css/css_psPaPw47aVeauGJEroVt9ZJCrdzD9yk6tjgLd9RaH3I.css?delta=0&language=en&theme=claro&include=eJx9kkFOxDAMRS9E2xUXAMGKS7iJSa1x7OIkMzCnJ50iVSzqZfyffuxvhwtGqmrPE0lFE-ARYiZ5CifCCExJMko9R2YoFByZNVy-mlY8Z4JGfNm4cyRaW4FzV-EcwlJ6qwRczpnPxlyCITpTLwiRJDmAGt1VKvAHiTMYZUieLNGNlkFScx2YxAmNqTjuhlmv-K6WwaHKorfHbpxMizYL-NZFN7WyYujLeV3AIHTFc6w_7IxdYd7kvgH8rg142s9jPCrDlkz_QA2noHlV6UmXUeBKCSqpDENV5RlsmFutKjv65_Pwjwbpf7VbgyEMhoXuuGuy5cfb8_A-2j1qv316VjY", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/core/modules/ckeditor5/icons/strikethrough.svg", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/sites/default/files/css/css_psPaPw47aVeauGJEroVt9ZJCrdzD9yk6tjgLd9RaH3I.css?delta=0&language=en&theme=claro&include=eJx9kkFOxDAMRS9E2xUXAMGKS7iJSa1x7OIkMzCnJ50iVSzqZfyffuxvhwtGqmrPE0lFE-ARYiZ5CifCCExJMko9R2YoFByZNVy-mlY8Z4JGfNm4cyRaW4FzV-EcwlJ6qwRczpnPxlyCITpTLwiRJDmAGt1VKvAHiTMYZUieLNGNlkFScx2YxAmNqTjuhlmv-K6WwaHKorfHbpxMizYL-NZFN7WyYujLeV3AIHTFc6w_7IxdYd7kvgH8rg142s9jPCrDlkz_QA2noHlV6UmXUeBKCSqpDENV5RlsmFutKjv65_Pwjwbpf7VbgyEMhoXuuGuy5cfb8_A-2j1qv316VjY", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/media/image-styles", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/development/performance", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people/create", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people/permissions", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people/roles", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/reports", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/reports/dblog", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=claro", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/admin/reports/dblog", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=entity.view.edit_form%3Aview%3Dwatchdog%3Alocation%3Dpage%26name%3Dwatchdog%26display_id%3Dpage%26langcode%3Den&tokens%5B%5D=grWQXBmsSApIJa_jk7wHiazbuliDJvCCmFVgCnqCxiM"}
{"method": "GET", "target": "/admin/reports/status", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/reports/updates", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/help", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/1/edit", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/modules/browse", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/add", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media-grid", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=claro", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/admin/content/media-grid", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=entity.view.edit_form%3Aview%3Dmedia_library%3Alocation%3Dpage%26name%3Dmedia_library%26display_id%3Dpage%26langcode%3Den&tokens%5B%5D=j9Y8Hgn09m7XQjCW5BfTzqp2SoYZRr5vB6MwyUrqz1w"}
{"method": "GET", "target": "/admin/content?title=hello%20world&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people?user=hello%20world&status=All&role=All&permission=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=hello%20world&filemime=&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media?name=hello%20world&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/block?info=hello%20world&type=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=hello%20world&author_name=", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?title=python3&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people?user=python3&status=All&role=All&permission=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=python3&filemime=&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media?name=python3&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/block?info=python3&type=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=python3&author_name=", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?title=powershell&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people?user=powershell&status=All&role=All&permission=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=powershell&filemime=&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media?name=powershell&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/block?info=powershell&type=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=powershell&author_name=", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?title=ls%20-la&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people?user=ls%20-la&status=All&role=All&permission=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=ls%20-la&filemime=&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media?name=ls%20-la&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/block?info=ls%20-la&type=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=ls%20-la&author_name=", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?title=.htaccess&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people?user=.htaccess&status=All&role=All&permission=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=.htaccess&filemime=&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media?name=.htaccess&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/block?info=.htaccess&type=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=.htaccess&author_name=", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?title=wp-config.php&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people?user=wp-config.php&status=All&role=All&permission=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=wp-config.php&filemime=&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media?name=wp-config.php&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/block?info=wp-config.php&type=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=wp-config.php&author_name=", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?title=docker-compose.yml&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people?user=docker-compose.yml&status=All&role=All&permission=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=docker-compose.yml&filemime=&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media?name=docker-compose.yml&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/block?info=docker-compose.yml&type=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=docker-compose.yml&author_name=", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?title=cat%20%2Fetc%2Fhosts&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people?user=cat%20%2Fetc%2Fhosts&status=All&role=All&permission=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=cat%20%2Fetc%2Fhosts&filemime=&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media?name=cat%20%2Fetc%2Fhosts&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/block?info=cat%20%2Fetc%2Fhosts&type=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=cat%20%2Fetc%2Fhosts&author_name=", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?title=SELECT%20rows%20FROM%20table&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/people?user=SELECT%20rows%20FROM%20table&status=All&role=All&permission=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=SELECT%20rows%20FROM%20table&filemime=&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media?name=SELECT%20rows%20FROM%20table&type=All&status=All&langcode=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/block?info=SELECT%20rows%20FROM%20table&type=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=SELECT%20rows%20FROM%20table&author_name=", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/add/article", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/add/page", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/edit", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/revisions", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/python3-notes", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/edit?destination=/admin/content%3Ftitle%3Dpython3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/delete?destination=/admin/content", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/2", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/taxonomy/term/1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?title=python3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?title=python3+tutorial&type=All&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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/admin/content", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/logout", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/logout/confirm", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/user/logout/confirm", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/user/logout/confirm", "Cookie: SESSexample=example-session-value"], "body": "confirm=1&form_build_id=form-wNgbewUygC9ULMoc4WLVpiOljzg8b-UFbevQU1lKKvE&form_token=PQZYrhskFDg3-YXEJlI1LMqcsi5g2Caq1winJy1aiTc&form_id=user_logout_confirm&op=Log+out"}
{"method": "POST", "target": "/user/login", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/user/login"], "body": "name=admin&pass=example-password-1&form_build_id=form-PFgoUZSU-B2s6Yi6wsv6gMF8hyctFZ4jGpnB1KbbKuc&form_id=user_login_form&op=Log+in"}
{"method": "POST", "target": "/admin/modules", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/admin/modules", "Cookie: SESSexample=example-session-value"], "body": "text=&form_build_id=form-Go6Ty-vaNH5prRcXZPri8UInSJEfkCpFomJ5z7FIJeA&form_token=dpSXO0ij0wI2RSPBdwAf24WP27sJX5XdMsyNckiHRYQ&form_id=system_modules&op=Install"}
{"method": "POST", "target": "/admin/config/services/jsonapi", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/admin/config/services/jsonapi", "Cookie: SESSexample=example-session-value"], "body": "read_only=rw&form_build_id=form-kZj8LFAOUYdKbamTIGwQWGeUPyZ9Pl_o7NMqCuQgRNA&form_token=U9cEhbE1Jq8CgS_JsoF1uTGkum0lp1R9dR05PQpXVIk&form_id=jsonapi_settings&op=Save+configuration"}
{"method": "POST", "target": "/contextual/render?theme=claro", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/admin/content/comment", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=entity.view.edit_form%3Aview%3Dcomment%3Alocation%3Dpage%26name%3Dcomment%26display_id%3Dpage_published%26langcode%3Den&tokens%5B%5D=tJYB9Yp9K7KoqCw51MrweOw1EWlIhkKnWWiqWfZwOBk"}
{"method": "POST", "target": "/admin/structure/menu/parents", "headers": ["Host: example.com", "X-Requested-With: XMLHttpRequest", "Accept-Language: en-US,en;q=0.9", "Accept: application/json, text/javascript, */*; q=0.01", "Content-Type: application/x-www-form-urlencoded; charset=UTF-8", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Origin: http://example.com", "Referer: http://example.com/admin/structure/types/manage/article", "Cookie: SESSexample=example-session-value"], "body": "menus%5B%5D=main"}
{"method": "POST", "target": "/admin/structure/views/view/content/preview/page_1?_wrapper_format=drupal_ajax", "headers": ["Host: example.com", "X-Requested-With: XMLHttpRequest", "Accept-Language: en-US,en;q=0.9", "Accept: application/json, text/javascript, */*; q=0.01", "Content-Type: application/x-www-form-urlencoded; charset=UTF-8", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Origin: http://example.com", "Referer: http://example.com/admin/structure/views/view/content", "Cookie: SESSexample=example-session-value"], "body": "live_preview=1&view_args=&form_build_id=form-YRTiLLJme3R_EAzYjDmEPFge7ba8Rqcr0e4ufPocUHA&form_token=aoWE2rnl7MeEF8CixG6jr69agDcL4EYtVBLeif_2MmI&form_id=view_preview_form&_triggering_element_name=op&_triggering_element_value=Update+preview&_drupal_ajax=1&ajax_page_state%5Btheme%5D=claro&ajax_page_state%5Btheme_token%5D=9R7R5tkIUlJUOLq74Y2Ibs3Vzk97sOkPnSer_Xb74Kk&ajax_page_state%5Blibraries%5D=eJxVjlEKgzAQBS9kkiOVF13CtuuuJBurPX1LbRH_hmF4vMzltvBCKf9gGAXVUhHLkNB8F9YyjKZOm3dImmpfIPE04VM82ieplEabF1NSb1GxcoGzaQhuJhk15O5ueqS_HYzOK303Ln5iiJWIO7aLbw6ndlGOLDRVlMOq1RnCLxrOC-nEKNit-7AyPdutc_pDxDSzvgGedGns"}
{"method": "POST", "target": "/ckeditor5/upload-image/basic_html?token=Sus-IXsV0UwqrRe2_CrWc-iHgk_6pDumClZIZ6i-Wu0", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "Accept: application/json", "0: text/javascript", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAcXJMZbPBLzalDiX", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Origin: http://example.com", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": "------WebKitFormBoundaryAcXJMZbPBLzalDiX\r\nContent-Disposition: form-data; name=\"upload\"; filename=\"photo.png\"\r\nContent-Type: image/png\r\n\r\n‰PNG\r\n\u001a\n\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0006\u0000\u0000\u0000\u001f\u0015ĉ\u0000\u0000\u0000\rIDATxÚcd`ø_\u000f\u0000\u0002‡\u0001€ëGº’\u0000\u0000\u0000\u0000IEND®B`‚\r\n------WebKitFormBoundaryAcXJMZbPBLzalDiX--\r\n"}
{"method": "POST", "target": "/node/add/article?element_parents=field_image/widget/0&ajax_form=1&_wrapper_format=drupal_ajax", "headers": ["Host: example.com", "X-Requested-With: XMLHttpRequest", "Accept-Language: en-US,en;q=0.9", "Accept: application/json, text/javascript, */*; q=0.01", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary0Ba7aU6AsCP0MAhK", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Origin: http://example.com", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": "------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"changed\"\r\n\r\n1794214637\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"title[0][value]\"\r\n\r\nPython3 and PowerShell notes\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"form_build_id\"\r\n\r\nform-LT5Y5U-7qf8iSfEX4mtEU7w-26XnFEmjxTB6AGmAKpw\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"form_token\"\r\n\r\nHAq_JMl1Tn3xeTNd4PzFsSkcbDhCFjHl2uV2RZpF2H8\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"form_id\"\r\n\r\nnode_article_form\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"files[field_image_0]\"; filename=\"python3-diagram.png\"\r\nContent-Type: image/png\r\n\r\n‰PNG\r\n\u001a\n\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0006\u0000\u0000\u0000\u001f\u0015ĉ\u0000\u0000\u0000\rIDATxÚcd`ø_\u000f\u0000\u0002‡\u0001€ëGº’\u0000\u0000\u0000\u0000IEND®B`‚\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"field_image[0][fids]\"\r\n\r\n\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"body[0][summary]\"\r\n\r\n\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"body[0][value]\"\r\n\r\n<p>Run ls -la in python3 and cat /etc/hosts on the box. See docker-compose.yml.</p><p>Second paragraph of ordinary words, with a list.<img data-entity-uuid=\"6e465287-5af3-436f-92e8-199fce782a2e\" data-entity-type=\"file\" src=\"/sites/default/files/inline-images/photo.png\" width=\"1\" height=\"1\"></p>\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"body[0][format]\"\r\n\r\nbasic_html\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"field_tags[target_id]\"\r\n\r\n\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"revision_log[0][value]\"\r\n\r\n\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"menu[title]\"\r\n\r\n\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"menu[description]\"\r\n\r\n\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"menu[menu_parent]\"\r\n\r\nmain:\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"menu[weight]\"\r\n\r\n0\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"comment[0][status]\"\r\n\r\n2\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"path[0][alias]\"\r\n\r\n\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"uid[0][target_id]\"\r\n\r\nadmin (1)\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"created[0][value][date]\"\r\n\r\n2026-11-09\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"created[0][value][time]\"\r\n\r\n08:57:17\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"promote[value]\"\r\n\r\n1\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"status[value]\"\r\n\r\n1\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"_triggering_element_name\"\r\n\r\nfield_image_0_upload_button\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"_triggering_element_value\"\r\n\r\nUpload\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"_drupal_ajax\"\r\n\r\n1\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"ajax_page_state[theme]\"\r\n\r\nclaro\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"ajax_page_state[theme_token]\"\r\n\r\n9R7R5tkIUlJUOLq74Y2Ibs3Vzk97sOkPnSer_Xb74Kk\r\n------WebKitFormBoundary0Ba7aU6AsCP0MAhK\r\nContent-Disposition: form-data; name=\"ajax_page_state[libraries]\"\r\n\r\neJyNU0luwzAM_JBln_qBAsmpQJ8Q0Aptq6FERaLSJq-vVG9ZevDJ5HA4JEdwa_qDNx6bdgoqfcKjEQ5vjXGCwQHVx5B8_iyVLZxa8xHfifVpExutHyCauIk8iKWd643btG1tLPTbmAItZSZB4KbjYJV8s9JMybo4wT1xC6SiXMm4vtJsLTppZqkxzXCe8SMJaK3MLine truncated
{"method": "GET", "target": "/sites/default/files/styles/thumbnail/public/2026-11/python3-diagram.png.avif?itok=Oi6kqsz7", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/taxonomy_term/default%3Ataxonomy_term/N5jux6RugFa9-QOKzy0aUSVM4RQohzzwpEObdpBNOxc?q=docker-compose.yml", "headers": ["Host: example.com", "X-Requested-With: XMLHttpRequest", "Accept-Language: en-US,en;q=0.9", "Accept: application/json, text/javascript, */*; q=0.01", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/node/add/article", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarys6Wog4PIiyr6c8nk", "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/node/add/article", "Cookie: SESSexample=example-session-value"], "body": "------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"changed\"\r\n\r\n1794214637\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"title[0][value]\"\r\n\r\nPython3 and PowerShell notes\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"form_build_id\"\r\n\r\nform-GIwPSC0mk5OfJnBDDPrIGJXA59_fo-i-i4rc6hLJ2zo\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"form_token\"\r\n\r\nHAq_JMl1Tn3xeTNd4PzFsSkcbDhCFjHl2uV2RZpF2H8\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"form_id\"\r\n\r\nnode_article_form\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"field_image[0][alt]\"\r\n\r\nA python3 diagram\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"field_image[0][fids]\"\r\n\r\n2\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"body[0][summary]\"\r\n\r\n\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"body[0][value]\"\r\n\r\n<p>Run ls -la in python3 and cat /etc/hosts on the box. See docker-compose.yml.</p><p>Second paragraph of ordinary words, with a list.<img data-entity-uuid=\"6e465287-5af3-436f-92e8-199fce782a2e\" data-entity-type=\"file\" src=\"/sites/default/files/inline-images/photo.png\" width=\"1\" height=\"1\"></p>\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"body[0][format]\"\r\n\r\nbasic_html\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"field_tags[target_id]\"\r\n\r\npython3, powershell, docker-compose.yml\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"revision_log[0][value]\"\r\n\r\n\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"menu[title]\"\r\n\r\n\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"menu[description]\"\r\n\r\n\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"menu[menu_parent]\"\r\n\r\nmain:\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"menu[weight]\"\r\n\r\n0\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"comment[0][status]\"\r\n\r\n2\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"path[0][alias]\"\r\n\r\n/python3-notes\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"uid[0][target_id]\"\r\n\r\nadmin (1)\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"created[0][value][date]\"\r\n\r\n2026-11-09\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"created[0][value][time]\"\r\n\r\n08:57:17\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"promote[value]\"\r\n\r\n1\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"status[value]\"\r\n\r\n1\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk\r\nContent-Disposition: form-data; name=\"op\"\r\n\r\nSave\r\n------WebKitFormBoundarys6Wog4PIiyr6c8nk--\r\n"}
{"method": "GET", "target": "/sites/default/files/styles/wide/public/2026-11/python3-diagram.png.avif?itok=zatrIDi8", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/python3-notes", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/python3-notes", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=node%3Anode%3D1%3Achanged%3D1794214658%26langcode%3Den&tokens%5B%5D=P1IJAagXdZ3uwIjUq2LBL0UYnJGTwcH1OA3KvfqFqg4"}
{"method": "POST", "target": "/node/add/page", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/node/add/page", "Cookie: SESSexample=example-session-value"], "body": "title%5B0%5D%5Bvalue%5D=About+.htaccess+files&changed=1794214659&form_build_id=form-LaoMm59z_hyaytPZGp-adyYwJZGmbXRc7440YWGi8Do&form_token=H47HAbieKRgy6mosBoIwM8QZMBUCGpYRpFnf7HpzKDg&form_id=node_page_form&body%5B0%5D%5Bsummary%5D=&body%5B0%5D%5Bvalue%5D=%3Cp%3EPlain+words+about+.htaccess+and+wp-config.php.%3C%2Fp%3E&body%5B0%5D%5Bformat%5D=basic_html&revision_log%5B0%5D%5Bvalue%5D=&menu%5Btitle%5D=&menu%5Bdescription%5D=&menu%5Bmenu_parent%5D=main%3A&menu%5Bweight%5D=0&path%5B0%5D%5Balias%5D=&uid%5B0%5D%5Btarget_id%5D=admin+%281%29&created%5B0%5D%5Bvalue%5D%5Bdate%5D=2026-11-09&created%5B0%5D%5Bvalue%5D%5Btime%5D=08%3A57%3A39&status%5Bvalue%5D=1&op=Save"}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/node/2", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=node%3Anode%3D2%3Achanged%3D1794214661%26langcode%3Den&tokens%5B%5D=9yhNCr_Hp4HSQvBSBYBWNCSG8ryM0kTuFsj7JELWqS0"}
{"method": "POST", "target": "/node/1/edit", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfBvrh6e5Bo80VnMG", "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/node/1/edit", "Cookie: SESSexample=example-session-value"], "body": "------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"changed\"\r\n\r\n1794214658\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"title[0][value]\"\r\n\r\nPython3 and PowerShell notes\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"form_build_id\"\r\n\r\nform-YOV6ivVNUPhhYH59NVKyp3pCw7S5FRZ-MpqVk7Tekkw\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"form_token\"\r\n\r\nRtIgLequzuSUKJ0eJYfweZtDlUU-P2Qm0dliQmbROBI\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"form_id\"\r\n\r\nnode_article_edit_form\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"field_image[0][alt]\"\r\n\r\nA python3 diagram\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"field_image[0][fids]\"\r\n\r\n2\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"body[0][summary]\"\r\n\r\n\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"body[0][value]\"\r\n\r\n<p>Run ls -la in python3 and cat /etc/hosts on the box. See docker-compose.yml.</p><p>Second paragraph of ordinary words, with a list.<img src=\"/sites/default/files/inline-images/photo.png\" data-entity-uuid=\"6e465287-5af3-436f-92e8-199fce782a2e\" data-entity-type=\"file\" width=\"1\" height=\"1\"> Edited with SELECT rows FROM table.</p>\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"body[0][format]\"\r\n\r\nbasic_html\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"field_tags[target_id]\"\r\n\r\npython3 (1), powershell (2), docker-compose.yml (3)\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"revision\"\r\n\r\n1\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"revision_log[0][value]\"\r\n\r\n\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"menu[title]\"\r\n\r\n\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"menu[description]\"\r\n\r\n\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"menu[menu_parent]\"\r\n\r\nmain:\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"menu[weight]\"\r\n\r\n0\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"comment[0][status]\"\r\n\r\n2\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"path[0][alias]\"\r\n\r\n/python3-notes\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"uid[0][target_id]\"\r\n\r\nadmin (1)\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"created[0][value][date]\"\r\n\r\n2026-11-09\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"created[0][value][time]\"\r\n\r\n08:57:17\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"promote[value]\"\r\n\r\n1\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"status[value]\"\r\n\r\n1\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG\r\nContent-Disposition: form-data; name=\"op\"\r\n\r\nSave\r\n------WebKitFormBoundaryfBvrh6e5Bo80VnMG--\r\n"}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/python3-notes", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=node%3Anode%3D1%3Achanged%3D1794214664%26langcode%3Den&tokens%5B%5D=gbyDF1Z3LcCvTyCIXXmz1gj9CG3LaEd37N1nZ2PpLNo"}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/taxonomy/term/1", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=entity.view.edit_form%3Aview%3Dtaxonomy_term%3Alocation%3Dpage%26name%3Dtaxonomy_term%26display_id%3Dpage_1%26langcode%3Den&ids%5B%5D=taxonomy_term%3Ataxonomy_term%3D1%3Achanged%3D1794214658%26langcode%3Den&tokens%5B%5D=GAe0zLesmOPq7bQZ_NH5jfUk6MU99eEngNvruqUxOgw&tokens%5B%5D=rydUkwhJOkTb6iVeuq6z0HGrGVXmL37cXZOt5fmvGkY"}
{"method": "GET", "target": "/sites/default/files/styles/medium/public/2026-11/python3-diagram.png.avif?itok=ytQ2uihe", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/taxonomy/term/1", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/comment/reply/node/1/comment", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/node/1", "Cookie: SESSexample=example-session-value"], "body": "form_build_id=form-rUcrz45wKTlwScU2KPvW4Uh-elT8_0ep1yDKub-fR9o&form_token=cLurkaLTS5RG_R8Vt9iKrHp5kETeTXRUPCXJRxJLz48&form_id=comment_comment_form&subject%5B0%5D%5Bvalue%5D=Python3+question&comment_body%5B0%5D%5Bvalue%5D=%3Cp%3EDoes+ls+-la+work+under+powershell%3F%3C%2Fp%3E&comment_body%5B0%5D%5Bformat%5D=basic_html&op=Save"}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/python3-notes", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=comment%3Acomment%3D1%3Achanged%3D1794214675%26langcode%3Den&tokens%5B%5D=fF-4OT40LdhhlYhHgviv64T0nVqkE5MGiUWVIAcU8lo"}
{"method": "GET", "target": "/entity_reference_autocomplete/taxonomy_term/default%3Ataxonomy_term/N5jux6RugFa9-QOKzy0aUSVM4RQohzzwpEObdpBNOxc?q=hello%20world", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/taxonomy_term/default%3Ataxonomy_term/N5jux6RugFa9-QOKzy0aUSVM4RQohzzwpEObdpBNOxc?q=python3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/taxonomy_term/default%3Ataxonomy_term/N5jux6RugFa9-QOKzy0aUSVM4RQohzzwpEObdpBNOxc?q=powershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/taxonomy_term/default%3Ataxonomy_term/N5jux6RugFa9-QOKzy0aUSVM4RQohzzwpEObdpBNOxc?q=ls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/taxonomy_term/default%3Ataxonomy_term/N5jux6RugFa9-QOKzy0aUSVM4RQohzzwpEObdpBNOxc?q=.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/taxonomy_term/default%3Ataxonomy_term/N5jux6RugFa9-QOKzy0aUSVM4RQohzzwpEObdpBNOxc?q=wp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/taxonomy_term/default%3Ataxonomy_term/N5jux6RugFa9-QOKzy0aUSVM4RQohzzwpEObdpBNOxc?q=cat%20%2Fetc%2Fhosts", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/taxonomy_term/default%3Ataxonomy_term/N5jux6RugFa9-QOKzy0aUSVM4RQohzzwpEObdpBNOxc?q=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/user/default/SIVyLDB1JOFz6YeZ_a7tOUbBYsPkGYqCWDMCbv80Fl8?q=hello%20world", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/user/default/SIVyLDB1JOFz6YeZ_a7tOUbBYsPkGYqCWDMCbv80Fl8?q=python3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/user/default/SIVyLDB1JOFz6YeZ_a7tOUbBYsPkGYqCWDMCbv80Fl8?q=powershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/user/default/SIVyLDB1JOFz6YeZ_a7tOUbBYsPkGYqCWDMCbv80Fl8?q=ls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/user/default/SIVyLDB1JOFz6YeZ_a7tOUbBYsPkGYqCWDMCbv80Fl8?q=.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/user/default/SIVyLDB1JOFz6YeZ_a7tOUbBYsPkGYqCWDMCbv80Fl8?q=wp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/user/default/SIVyLDB1JOFz6YeZ_a7tOUbBYsPkGYqCWDMCbv80Fl8?q=docker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/user/default/SIVyLDB1JOFz6YeZ_a7tOUbBYsPkGYqCWDMCbv80Fl8?q=cat%20%2Fetc%2Fhosts", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/entity_reference_autocomplete/user/default/SIVyLDB1JOFz6YeZ_a7tOUbBYsPkGYqCWDMCbv80Fl8?q=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: */*", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/user/logout/confirm", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/user/logout/confirm", "Cookie: SESSexample=example-session-value"], "body": "confirm=1&form_build_id=form-vhMpEh9d_WRMwXQqxQWWiugdMAZ9cRfVnudOBsxwQNo&form_token=CXyUg6iH9UxK0n9bFsPllZanXhtbFSoc6km1jtSxrh4&form_id=user_logout_confirm&op=Log+out"}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=block%3Ablock%3Dolivero_site_branding%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_main_menu%3Alangcode%3Den%7Cmenu%3Amenu%3Dmain%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_account_menu%3Alangcode%3Den%7Cmenu%3Amenu%3Daccount%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_page_title%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_powered%3Alangcode%3Den&tokens%5B%5D=1rv_5u0s4iGV_AWmQmPdRm4CsxIu8wsjE8sFkt9yGF4&tokens%5B%5D=8Gi0ikQVIh2gC601Y5UBe2vi21hWy8qqpz--u2aRwZU&tokens%5B%5D=henkHDCYz37bUB9MGiVxhW9dClN1HHpXLkE9blnvU8I&tokens%5B%5D=pGkK5C6k5HhOIlpSVObsmKdf5DVWqjhEzJfvUSZdanw&tokens%5B%5D=jmjvcBoCtAymF3-B_C5BHjXRBqYcwKstu9qZq-rxCDw"}
{"method": "POST", "target": "/admin/config/search/pages", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/admin/config/search/pages", "Cookie: SESSexample=example-session-value"], "body": "cron_limit=100&minimum_word_size=3&overlap_cjk=1&search_type=node_search&op=Add+search+page&form_build_id=form-h9TNUOku6qlPzCQIfLOM6VobwiD7LjgYKi4l8e6-n8I&form_token=iGIiBHs6fcxoEK9qZxxFkjh3rbC2M9Y2R_JRat7N93c&form_id=search_admin_settings"}
{"method": "GET", "target": "/admin/config/search/pages/add/node_search", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "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/admin/config/search/pages", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/admin/config/search/pages/add/node_search", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/admin/config/search/pages/add/node_search", "Cookie: SESSexample=example-session-value"], "body": "label=ContentContent&id=content&path=node&form_build_id=form-WUJFS4YPNPKnEeJoXCUT_dOzqiNC1753LO8pTBEuVRw&form_token=ttZsmIX1d92dXP47AP1OI3G9wC0R501kWB_G2nlqxTo&form_id=search_page_add_form&op=Save"}
{"method": "POST", "target": "/admin/config/search/pages", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/admin/config/search/pages", "Cookie: SESSexample=example-session-value"], "body": "cron_limit=100&minimum_word_size=3&overlap_cjk=1&search_type=user_search&op=Add+search+page&entities%5Bcontent%5D%5Bweight%5D=-10&form_build_id=form-zkfBqVKODA5_wbOFsX_3NFPhSC_gnINh2M7rHYmFSwQ&form_token=iGIiBHs6fcxoEK9qZxxFkjh3rbC2M9Y2R_JRat7N93c&form_id=search_admin_settings"}
{"method": "GET", "target": "/admin/config/search/pages/add/user_search", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "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/admin/config/search/pages", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/admin/config/search/pages/add/user_search", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/admin/config/search/pages/add/user_search", "Cookie: SESSexample=example-session-value"], "body": "label=UsersUsers&id=users&path=user&form_build_id=form-qdd4t_ZEijkL16Cm3Mjpt4ADl-oOomMwZAbvMbuCL84&form_token=ttZsmIX1d92dXP47AP1OI3G9wC0R501kWB_G2nlqxTo&form_id=search_page_add_form&op=Save"}
{"method": "GET", "target": "/admin/config/people/accounts", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/admin/config/people/accounts", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/admin/config/people/accounts", "Cookie: SESSexample=example-session-value"], "body": "anonymous=Anonymous&user_register=visitors&user_email_verification=1&user_password_strength=1&user_cancel_method=user_cancel_block&mail_notification_address=&user_mail_register_admin_created_subject=An+administrator+created+an+account+for+you+at+%5Bsite%3Aname%5D&user_mail_register_admin_created_body=%5Buser%3Adisplay-name%5D%2C%0D%0A%0D%0AA+site+administrator+at+%5Bsite%3Aname%5D+has+created+an+account+for+you.+You+may+now+log+in+by+clicking+this+link+or+copying+and+pasting+it+into+your+browser%3A%0D%0A%0D%0A%5Buser%3Aone-time-login-url%5D%0D%0A%0D%0AThis+link+can+only+be+used+once+to+log+in+and+will+lead+you+to+a+page+where+you+can+set+your+password.%0D%0A%0D%0AAfter+setting+your+password%2C+you+will+be+able+to+log+in+at+%5Bsite%3Alogin-url%5D+in+the+future+using%3A%0D%0A%0D%0Ausername%3A+%5Buser%3Aname%5D%0D%0Apassword%3A+Your+password%0D%0A%0D%0A--++%5Bsite%3Aname%5D+team&user_mail_register_pending_approval_subject=Account+details+for+%5Buser%3Adisplay-name%5D+at+%5Bsite%3Aname%5D+%28pending+admin+approval%29&user_mail_register_pending_approval_body=%5Buser%3Adisplay-name%5D%2C%0D%0A%0D%0AThank+you+for+registering+at+%5Bsite%3Aname%5D.+Your+application+for+an+account+is+currently+pending+approval.+Once+it+has+been+approved%2C+you+will+receive+another+email+containing+information+about+how+to+log+in%2C+set+your+password%2C+and+other+details.%0D%0A%0D%0A--++%5Bsite%3Aname%5D+team&register_pending_approval_admin_subject=Account+details+for+%5Buser%3Adisplay-name%5D+at+%5Bsite%3Aname%5D+%28pending+admin+approval%29&register_pending_approval_admin_body=%5Buser%3Adisplay-name%5D+has+applied+for+an+account.%0D%0A%0D%0A%5Buser%3Aedit-url%5D&user_mail_register_no_approval_required_subject=Account+details+for+%5Buser%3Adisplay-name%5D+at+%5Bsite%3Aname%5D&user_mail_register_no_approval_required_body=%5Buser%3Adisplay-name%5D%2C%0D%0A%0D%0AThank+you+for+registering+at+%5Bsite%3Aname%5D.+You+may+now+log+in+by+clicking+this+link+or+copying+and+pasting+it+into+your+browser%3A%0D%0A%0D%0A%5Buser%3Aone-time-login-url%5D%0D%0A%0D%0AThis+link+can+only+be+used+once+to+log+in+and+will+lead+you+to+a+page+where+you+can+set+your+password.%0D%0A%0D%0AAfter+setting+your+password%2C+you+will+be+able+to+log+in+at+%5Bsite%3Alogin-url%5D+in+the+future+using%3A%0D%0A%0D%0Ausername%3A+%5Buser%3Aname%5D%0D%0Apassword%3A+Your+password%0D%0A%0D%0A--++%5Bsite%3Aname%5D+team&user_mail_status_activated_notify=1&user_mail_status_activated_subject=Account+details+for+%5Buser%3Adisplay-name%5D+at+%5Bsite%3Aname%5D+%28approved%29&user_mail_status_activated_body=%5Buser%3Adisplay-name%5D%2C%0D%0A%0D%0AYour+account+at+%5Bsite%3Aname%5D+has+been+activated.%0D%0A%0D%0AYou+may+now+log+in+by+clicking+this+link+or+copying+and+pasting+it+into+your+browser%3A%0D%0A%0D%0A%5Buser%3Aone-time-login-url%5D%0D%0A%0D%0AThis+link+can+only+be+used+once+to+log+in+and+will+lead+you+to+a+page+where+you+can+set+your+password.%0D%0A%0D%0AAfter+setting+your+password%2C+you+will+be+able+to+log+in+at+%5Bsite%3Alogin-url%5D+in+the+future+using%3A%0D%0A%0D%0Ausername%3A+%5Buser%3Aaccount-name%5D%0D%0Apassword%3A+Your+password%0D%0A%0D%0A--++%5Bsite%3Aname%5D+team&user_mail_status_blocked_subject=Account+details+for+%5Buser%3Adisplay-name%5D+at+%5Bsite%3Aname%5D+%28blocked%29&user_mail_status_blocked_body=%5Buser%3Adisplay-name%5D%2C%0D%0A%0D%0AYour+account+on+%5Bsite%3Aname%5D+has+been+blocked.%0D%0A%0D%0A--++%5Bsite%3Aname%5D+team&user_mail_cancel_confirm_subject=Account+cancellation+request+for+%5Buser%3Adisplay-name%5D+at+%5Bsite%3Aname%5D&user_mail_cancel_confirm_body=%5Buser%3Adisplay-name%5D%0D%0A%0D%0AA+request+to+cancel+your+account+has+been+made+at+%5Bsite%3Aname%5D.%0D%0A%0D%0AYou+may+now+cancel+your+account+on+%5Bsite%3Aurl-brief%5D+by+clicking+this+link+or+copying+and+pasting+it+into+your+browser%3A%0D%0A%0D%0A%5Buser%3Acancel-url%5D%0D%0A%0D%0ANOTE%3A+The+cancellation+of+your+account+is+not+reversible.%0D%0A%0D%0AThis+link+expires+in+one+day+and+nothing+will+happen+if+it+is+not+used.%0D%0A%0D%0A--++%5Bsite%3Aname%5D+team&user_mail_status_canceled_subject=Account+details+for+%5Buser%3Adisplay-name%5D+at+%5Bsite%3Aname%5D+%28canceled%29&user_mail_status_canceled_body=%5Buser%3Adisplay-name%5D%2C%0D%0A%0D%0AYour+account+on+%5Bsite%3Aname%5D+has+been+canceled.%0D%0A%0D%0A--++%5Bsite%3Aname%5D+team&user_mail_password_reset_subject=Replacement+login+informaLine truncated
{"method": "GET", "target": "/media/add/image", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/reports/search", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/reports/access-denied", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/reports/page-not-found", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/comment/1/edit", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/comment/1/delete", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/media/1/edit", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure/media", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/taxonomy/term/1", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=entity.view.edit_form%3Aview%3Dtaxonomy_term%3Alocation%3Dpage%26name%3Dtaxonomy_term%26display_id%3Dpage_1%26langcode%3Den&ids%5B%5D=taxonomy_term%3Ataxonomy_term%3D1%3Achanged%3D1794214658%26langcode%3Den&ids%5B%5D=node%3Anode%3D1%3Achanged%3D1794214664%26langcode%3Den&tokens%5B%5D=GAe0zLesmOPq7bQZ_NH5jfUk6MU99eEngNvruqUxOgw&tokens%5B%5D=rydUkwhJOkTb6iVeuq6z0HGrGVXmL37cXZOt5fmvGkY&tokens%5B%5D=gbyDF1Z3LcCvTyCIXXmz1gj9CG3LaEd37N1nZ2PpLNo"}
{"method": "GET", "target": "/taxonomy/term/1/edit", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/structure/taxonomy/manage/tags/add", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/user?keys=hello%20world", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/node?keys=hello%20world&page=1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/user?keys=python3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/node?keys=python3&page=1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/user?keys=powershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/node?keys=powershell&page=1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/user?keys=ls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/node?keys=ls%20-la&page=1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/user?keys=.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/node?keys=.htaccess&page=1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/user?keys=wp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/node?keys=wp-config.php&page=1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/user?keys=docker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/node?keys=docker-compose.yml&page=1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/user?keys=cat%20%2Fetc%2Fhosts", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/node?keys=cat%20%2Fetc%2Fhosts&page=1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/user?keys=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/search/node?keys=SELECT%20rows%20FROM%20table&page=1", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/user/logout/confirm", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/user/logout/confirm", "Cookie: SESSexample=example-session-value"], "body": "confirm=1&form_build_id=form-2mqij9gEz7sEEx0AFXDMNhEi7UGMmIKDKOtaH__AdcM&form_token=4rgwkD4H8ojOv1RvJymJhN4_mA6DPv6hOdwHxXCOjMM&form_id=user_logout_confirm&op=Log+out"}
{"method": "GET", "target": "/search?keys=hello%20world", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search?keys=python3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search?keys=powershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search?keys=ls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search?keys=.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search?keys=wp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search?keys=docker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search?keys=cat%20%2Fetc%2Fhosts", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/search?keys=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "POST", "target": "/user/register", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryS0AxXBAlk7QxCkUx", "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/user/register"], "body": "------WebKitFormBoundaryS0AxXBAlk7QxCkUx\r\nContent-Disposition: form-data; name=\"mail\"\r\n\r\nnew.person@example.com\r\n------WebKitFormBoundaryS0AxXBAlk7QxCkUx\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nnew_person\r\n------WebKitFormBoundaryS0AxXBAlk7QxCkUx\r\nContent-Disposition: form-data; name=\"user_picture[0][fids]\"\r\n\r\n\r\n------WebKitFormBoundaryS0AxXBAlk7QxCkUx\r\nContent-Disposition: form-data; name=\"user_picture[0][display]\"\r\n\r\n1\r\n------WebKitFormBoundaryS0AxXBAlk7QxCkUx\r\nContent-Disposition: form-data; name=\"form_build_id\"\r\n\r\nform-Ylo4JYJ_UfFvdR3PaUXBHi1yzjp7M4C4e1XY6hAE1y0\r\n------WebKitFormBoundaryS0AxXBAlk7QxCkUx\r\nContent-Disposition: form-data; name=\"form_id\"\r\n\r\nuser_register_form\r\n------WebKitFormBoundaryS0AxXBAlk7QxCkUx\r\nContent-Disposition: form-data; name=\"op\"\r\n\r\nCreate new account\r\n------WebKitFormBoundaryS0AxXBAlk7QxCkUx--\r\n"}
{"method": "POST", "target": "/user/password", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/user/password"], "body": "name=new.person%40example.com&form_build_id=form-Zkr9kQgDYORrLzpGPYPnF0LHbbHjoangOOgglubhsdE&form_id=user_pass&op=Submit"}
{"method": "POST", "target": "/user/login?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"admin\", \"pass\": \"example-password-1\"}"}
{"method": "GET", "target": "/user/login_status?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/session/token", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/jsonapi", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/page", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/taxonomy_term/tags", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/user/user", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/file/file", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/media/image", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/comment/comment", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?include=uid,field_tags,field_image&sort=-created,title&page%5Blimit%5D=10&page%5Boffset%5D=0&fields%5Bnode--article%5D=title,body,created", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Btitle%5D=hello%20world", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bpath%5D=title&filter%5Bf%5D%5Bcondition%5D%5Boperator%5D=CONTAINS&filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=hello%20world", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bg%5D%5Bgroup%5D%5Bconjunction%5D=OR&filter%5Bt%5D%5Bcondition%5D%5Bpath%5D=field_tags.name&filter%5Bt%5D%5Bcondition%5D%5Bvalue%5D=hello%20world&filter%5Bt%5D%5Bcondition%5D%5BmemberOf%5D=g", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/taxonomy_term/tags?filter%5Bname%5D=hello%20world", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/user/user?filter%5Bname%5D=hello%20world", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/file/file?filter%5Bfilename%5D=hello%20world", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/media/image?filter%5Bname%5D=hello%20world", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Btitle%5D=python3", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bpath%5D=title&filter%5Bf%5D%5Bcondition%5D%5Boperator%5D=CONTAINS&filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=python3", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bg%5D%5Bgroup%5D%5Bconjunction%5D=OR&filter%5Bt%5D%5Bcondition%5D%5Bpath%5D=field_tags.name&filter%5Bt%5D%5Bcondition%5D%5Bvalue%5D=python3&filter%5Bt%5D%5Bcondition%5D%5BmemberOf%5D=g", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/taxonomy_term/tags?filter%5Bname%5D=python3", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/user/user?filter%5Bname%5D=python3", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/file/file?filter%5Bfilename%5D=python3", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/media/image?filter%5Bname%5D=python3", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Btitle%5D=powershell", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bpath%5D=title&filter%5Bf%5D%5Bcondition%5D%5Boperator%5D=CONTAINS&filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=powershell", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bg%5D%5Bgroup%5D%5Bconjunction%5D=OR&filter%5Bt%5D%5Bcondition%5D%5Bpath%5D=field_tags.name&filter%5Bt%5D%5Bcondition%5D%5Bvalue%5D=powershell&filter%5Bt%5D%5Bcondition%5D%5BmemberOf%5D=g", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/taxonomy_term/tags?filter%5Bname%5D=powershell", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/user/user?filter%5Bname%5D=powershell", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/file/file?filter%5Bfilename%5D=powershell", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/media/image?filter%5Bname%5D=powershell", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Btitle%5D=ls%20-la", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bpath%5D=title&filter%5Bf%5D%5Bcondition%5D%5Boperator%5D=CONTAINS&filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=ls%20-la", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bg%5D%5Bgroup%5D%5Bconjunction%5D=OR&filter%5Bt%5D%5Bcondition%5D%5Bpath%5D=field_tags.name&filter%5Bt%5D%5Bcondition%5D%5Bvalue%5D=ls%20-la&filter%5Bt%5D%5Bcondition%5D%5BmemberOf%5D=g", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/taxonomy_term/tags?filter%5Bname%5D=ls%20-la", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/user/user?filter%5Bname%5D=ls%20-la", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/file/file?filter%5Bfilename%5D=ls%20-la", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/media/image?filter%5Bname%5D=ls%20-la", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Btitle%5D=.htaccess", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bpath%5D=title&filter%5Bf%5D%5Bcondition%5D%5Boperator%5D=CONTAINS&filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=.htaccess", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bg%5D%5Bgroup%5D%5Bconjunction%5D=OR&filter%5Bt%5D%5Bcondition%5D%5Bpath%5D=field_tags.name&filter%5Bt%5D%5Bcondition%5D%5Bvalue%5D=.htaccess&filter%5Bt%5D%5Bcondition%5D%5BmemberOf%5D=g", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/taxonomy_term/tags?filter%5Bname%5D=.htaccess", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/user/user?filter%5Bname%5D=.htaccess", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/file/file?filter%5Bfilename%5D=.htaccess", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/media/image?filter%5Bname%5D=.htaccess", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Btitle%5D=wp-config.php", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bpath%5D=title&filter%5Bf%5D%5Bcondition%5D%5Boperator%5D=CONTAINS&filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=wp-config.php", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bg%5D%5Bgroup%5D%5Bconjunction%5D=OR&filter%5Bt%5D%5Bcondition%5D%5Bpath%5D=field_tags.name&filter%5Bt%5D%5Bcondition%5D%5Bvalue%5D=wp-config.php&filter%5Bt%5D%5Bcondition%5D%5BmemberOf%5D=g", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/taxonomy_term/tags?filter%5Bname%5D=wp-config.php", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/user/user?filter%5Bname%5D=wp-config.php", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/file/file?filter%5Bfilename%5D=wp-config.php", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/media/image?filter%5Bname%5D=wp-config.php", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Btitle%5D=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bpath%5D=title&filter%5Bf%5D%5Bcondition%5D%5Boperator%5D=CONTAINS&filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bg%5D%5Bgroup%5D%5Bconjunction%5D=OR&filter%5Bt%5D%5Bcondition%5D%5Bpath%5D=field_tags.name&filter%5Bt%5D%5Bcondition%5D%5Bvalue%5D=docker-compose.yml&filter%5Bt%5D%5Bcondition%5D%5BmemberOf%5D=g", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/taxonomy_term/tags?filter%5Bname%5D=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/user/user?filter%5Bname%5D=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/file/file?filter%5Bfilename%5D=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/media/image?filter%5Bname%5D=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Btitle%5D=cat%20/etc/hosts", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bpath%5D=title&filter%5Bf%5D%5Bcondition%5D%5Boperator%5D=CONTAINS&filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=cat%20/etc/hosts", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bg%5D%5Bgroup%5D%5Bconjunction%5D=OR&filter%5Bt%5D%5Bcondition%5D%5Bpath%5D=field_tags.name&filter%5Bt%5D%5Bcondition%5D%5Bvalue%5D=cat%20/etc/hosts&filter%5Bt%5D%5Bcondition%5D%5BmemberOf%5D=g", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/taxonomy_term/tags?filter%5Bname%5D=cat%20/etc/hosts", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/user/user?filter%5Bname%5D=cat%20/etc/hosts", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/file/file?filter%5Bfilename%5D=cat%20/etc/hosts", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/media/image?filter%5Bname%5D=cat%20/etc/hosts", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Btitle%5D=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bpath%5D=title&filter%5Bf%5D%5Bcondition%5D%5Boperator%5D=CONTAINS&filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article?filter%5Bg%5D%5Bgroup%5D%5Bconjunction%5D=OR&filter%5Bt%5D%5Bcondition%5D%5Bpath%5D=field_tags.name&filter%5Bt%5D%5Bcondition%5D%5Bvalue%5D=SELECT%20rows%20FROM%20table&filter%5Bt%5D%5Bcondition%5D%5BmemberOf%5D=g", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/taxonomy_term/tags?filter%5Bname%5D=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/user/user?filter%5Bname%5D=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/file/file?filter%5Bfilename%5D=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/media/image?filter%5Bname%5D=SELECT%20rows%20FROM%20table", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "POST", "target": "/jsonapi/node/article", "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: zG78nvG2BJfVxmuln0IVMVLxP3mMFJbS5E-H_v9caxs"], "body": "{\"data\": {\"type\": \"node--article\", \"attributes\": {\"title\": \"Python3 and PowerShell via API\", \"body\": {\"value\": \"<p>Run ls -la in python3 and cat /etc/hosts; see <a href=\\\"/python3-notes\\\">notes</a>.</p>\", \"format\": \"basic_html\"}}}}"}
{"method": "PATCH", "target": "/jsonapi/node/article/47f40026-bda0-41dc-b81a-f1a13535af4c", "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: zG78nvG2BJfVxmuln0IVMVLxP3mMFJbS5E-H_v9caxs"], "body": "{\"data\": {\"type\": \"node--article\", \"id\": \"47f40026-bda0-41dc-b81a-f1a13535af4c\", \"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/47f40026-bda0-41dc-b81a-f1a13535af4c?include=uid", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article/47f40026-bda0-41dc-b81a-f1a13535af4c/relationships/uid", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article/47f40026-bda0-41dc-b81a-f1a13535af4c/uid", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "POST", "target": "/jsonapi/node/article/field_image", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json", "Content-Type: application/octet-stream", "Content-Disposition: file; filename=\"photo.png\"", "X-Csrf-Token: zG78nvG2BJfVxmuln0IVMVLxP3mMFJbS5E-H_v9caxs"], "body": "‰PNG\r\n\u001a\n\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0006\u0000\u0000\u0000\u001f\u0015ĉ\u0000\u0000\u0000\rIDATxÚcd`ø_\u000f\u0000\u0002‡\u0001€ëGº’\u0000\u0000\u0000\u0000IEND®B`‚"}
{"method": "POST", "target": "/jsonapi/node/article/47f40026-bda0-41dc-b81a-f1a13535af4c/field_image", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json", "Content-Type: application/octet-stream", "Content-Disposition: file; filename=\"photo.png\"", "X-Csrf-Token: zG78nvG2BJfVxmuln0IVMVLxP3mMFJbS5E-H_v9caxs"], "body": "‰PNG\r\n\u001a\n\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0006\u0000\u0000\u0000\u001f\u0015ĉ\u0000\u0000\u0000\rIDATxÚcd`ø_\u000f\u0000\u0002‡\u0001€ëGº’\u0000\u0000\u0000\u0000IEND®B`‚"}
{"method": "DELETE", "target": "/jsonapi/node/article/47f40026-bda0-41dc-b81a-f1a13535af4c", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json", "X-Csrf-Token: zG78nvG2BJfVxmuln0IVMVLxP3mMFJbS5E-H_v9caxs"], "body": ""}
{"method": "GET", "target": "/node/1?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "PATCH", "target": "/node/1?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Content-Type: application/json", "X-Csrf-Token: zG78nvG2BJfVxmuln0IVMVLxP3mMFJbS5E-H_v9caxs"], "body": "{\"type\": [{\"target_id\": \"article\"}], \"title\": [{\"value\": \"Edited: python3\"}]}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"new.person@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": ""}
{"method": "GET", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb/relationships/uid", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "GET", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb/uid", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
{"method": "POST", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb/field_image", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json", "Content-Type: application/octet-stream", "Content-Disposition: file; filename=\"photo.png\"", "X-Csrf-Token: fK1nVGRmImLpZRnGKVLo_fRY4zvgCTRWbMAXJ0023ho"], "body": "‰PNG\r\n\u001a\n\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0006\u0000\u0000\u0000\u001f\u0015ĉ\u0000\u0000\u0000\rIDATxÚcd`ø_\u000f\u0000\u0002‡\u0001€ëGº’\u0000\u0000\u0000\u0000IEND®B`‚"}
{"method": "DELETE", "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", "X-Csrf-Token: fK1nVGRmImLpZRnGKVLo_fRY4zvgCTRWbMAXJ0023ho"], "body": ""}
{"method": "POST", "target": "/user/logout?_format=json&token=aAL70H9lx1XrjIVcHIF12FLWwoi0YEwunehLXbSKZv4", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/user/login", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/user/login"], "body": "name=admin&pass=example-password-1&form_build_id=form-lt4MGc7V3i3el7smsATqAMVVJ1KzV_yV3U3nOcZ7FWc&form_id=user_login_form&op=Log+in"}
{"method": "GET", "target": "/search/user?keys=admin", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media-grid?name=python3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/media-grid?name=ls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/user/login", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/user/login"], "body": "name=admin&pass=python3-%2Fetc%2Fhosts-1&form_build_id=form-lt4MGc7V3i3el7smsATqAMVVJ1KzV_yV3U3nOcZ7FWc&form_id=user_login_form&op=Log+in"}
{"method": "GET", "target": "/user/login?destination=/search/node%3Fkeys%3Dpython3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/user/login?destination=/admin/content%3Ftitle%3Dpowershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "GET", "target": "/user/login?destination=/python3-notes", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/user/1?check_logged_in=1", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=block%3Ablock%3Dolivero_site_branding%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_main_menu%3Alangcode%3Den%7Cmenu%3Amenu%3Dmain%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_account_menu%3Alangcode%3Den%7Cmenu%3Amenu%3Daccount%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_breadcrumbs%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_page_title%3Alangcode%3Den&ids%5B%5D=block%3Ablock%3Dolivero_powered%3Alangcode%3Den&tokens%5B%5D=1rv_5u0s4iGV_AWmQmPdRm4CsxIu8wsjE8sFkt9yGF4&tokens%5B%5D=8Gi0ikQVIh2gC601Y5UBe2vi21hWy8qqpz--u2aRwZU&tokens%5B%5D=henkHDCYz37bUB9MGiVxhW9dClN1HHpXLkE9blnvU8I&tokens%5B%5D=DupSXj2bNdZYw9OEb6f4T6BX3qXPnA1znFW4miVijJU&tokens%5B%5D=pGkK5C6k5HhOIlpSVObsmKdf5DVWqjhEzJfvUSZdanw&tokens%5B%5D=jmjvcBoCtAymF3-B_C5BHjXRBqYcwKstu9qZq-rxCDw"}
{"method": "POST", "target": "/editor/filter_xss/full_html", "headers": ["Host: example.com", "X-Requested-With: XMLHttpRequest", "Accept-Language: en-US,en;q=0.9", "Accept: application/json, text/javascript, */*; q=0.01", "Content-Type: application/x-www-form-urlencoded; charset=UTF-8", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Origin: http://example.com", "Referer: http://example.com/node/add/article", "Cookie: SESSexample=example-session-value"], "body": "value=&original_format_id=basic_html"}
{"method": "POST", "target": "/node/add/article", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryRCXQGBEZqRVGYEzF", "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/node/add/article", "Cookie: SESSexample=example-session-value"], "body": "------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"changed\"\r\n\r\n1794214990\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"title[0][value]\"\r\n\r\nSELECT a table: python3, ls -la and .htaccess\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"form_build_id\"\r\n\r\nform-Vl4RHWAalDXt4xlz9gkdfBjyRGaJHUU6FRlyea9EKkc\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"form_token\"\r\n\r\nI1fDBOjGA9cTabBxXpM8PsNoHDj_jgNpDUfryW87JZQ\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"form_id\"\r\n\r\nnode_article_form\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"field_image[0][fids]\"\r\n\r\n\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"body[0][summary]\"\r\n\r\nNotes on python3 and powershell\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"body[0][value]\"\r\n\r\n<h2>Notes on python3 and powershell</h2><p>Run <code>ls -la</code> then <a href=\"/python3-notes\">read the notes</a> or <a href=\"https://example.com/docs/setup.php?a=1&amp;b=2\">the docs</a>.</p><pre><code class=\"language-plaintext language-bash\">#!/bin/sh\r\ncat /etc/hosts | grep example &amp;&amp; ls -la\r\npython3 setup.py install\r\ncurl -s https://example.com/install.sh | sh\r\nSELECT name, mail FROM users WHERE uid = 1;\r\nconsole.log('ok'); document.getElementById('x');\r\nif (a == b) { echo $PATH; }\r\n</code></pre><blockquote><p>Select a table from the catalog, then union the results.</p></blockquote><ul><li data-list-item-id=\"e0931e5bf5859e9ac55e1b80ed4b3ad82\">one</li><li data-list-item-id=\"e681549baddb3c36b7bcba5605e5831c2\">two</li></ul><table class=\"table\"><tbody><tr><td>python3</td><td>3.13</td></tr></tbody></table><p><img src=\"/sites/default/files/inline-images/photo.png\" data-entity-uuid=\"11111111-2222-4333-8444-555555555555\" data-entity-type=\"file\" alt=\"A photo\" width=\"1\" height=\"1\"></p>\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"body[0][format]\"\r\n\r\nfull_html\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"field_tags[target_id]\"\r\n\r\n\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"revision_log[0][value]\"\r\n\r\nAdded ls -la and python3 notes\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"menu[title]\"\r\n\r\n\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"menu[description]\"\r\n\r\n\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"menu[menu_parent]\"\r\n\r\nmain:\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"menu[weight]\"\r\n\r\n0\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"comment[0][status]\"\r\n\r\n2\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"path[0][alias]\"\r\n\r\n/powershell-and-python3\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"uid[0][target_id]\"\r\n\r\nadmin (1)\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"created[0][value][date]\"\r\n\r\n2026-11-09\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"created[0][value][time]\"\r\n\r\n09:03:10\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"promote[value]\"\r\n\r\n1\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"status[value]\"\r\n\r\n1\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF\r\nContent-Disposition: form-data; name=\"op\"\r\n\r\nSave\r\n------WebKitFormBoundaryRCXQGBEZqRVGYEzF--\r\n"}
{"method": "GET", "target": "/powershell-and-python3", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "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/node/add/article", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/powershell-and-python3", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=node%3Anode%3D5%3Achanged%3D1794214996%26langcode%3Den&tokens%5B%5D=ita10VPYAXlTHzj0MBr00dObG7ftbasU9C1DjE2zOWI"}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/node/1", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=node%3Anode%3D1%3Achanged%3D1794214664%26langcode%3Den&ids%5B%5D=comment%3Acomment%3D1%3Achanged%3D1794214675%26langcode%3Den&tokens%5B%5D=gbyDF1Z3LcCvTyCIXXmz1gj9CG3LaEd37N1nZ2PpLNo&tokens%5B%5D=fF-4OT40LdhhlYhHgviv64T0nVqkE5MGiUWVIAcU8lo"}
{"method": "POST", "target": "/comment/reply/node/1/comment", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/node/1", "Cookie: SESSexample=example-session-value"], "body": "form_build_id=form-r1mZS5F4tqXIdq44fjWFYuN-awBCxpW-K-BvoAAmkyk&form_token=8K6l4FhFSzSgw_JeQMFcuQ7zkooQehyqSkLCSKhLp5I&form_id=comment_comment_form&subject%5B0%5D%5Bvalue%5D=Python3+on+.htaccess&comment_body%5B0%5D%5Bvalue%5D=%3Cp%3EDoes+%3Ccode%3Els+-la%3C%2Fcode%3E+work+under+powershell+or+python3%3F%3C%2Fp%3E&comment_body%5B0%5D%5Bformat%5D=basic_html&op=Save"}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/python3-notes", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=comment%3Acomment%3D1%3Achanged%3D1794214675%26langcode%3Den&ids%5B%5D=comment%3Acomment%3D2%3Achanged%3D1794214999%26langcode%3Den&tokens%5B%5D=fF-4OT40LdhhlYhHgviv64T0nVqkE5MGiUWVIAcU8lo&tokens%5B%5D=ChhtBe1arN5N5VhzviQHD_VDytZgbZVB3u-2qOufzK8"}
{"method": "POST", "target": "/media/add/image?element_parents=field_media_image/widget/0&ajax_form=1&_wrapper_format=drupal_ajax", "headers": ["Host: example.com", "X-Requested-With: XMLHttpRequest", "Accept-Language: en-US,en;q=0.9", "Accept: application/json, text/javascript, */*; q=0.01", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryE7nrnHw1Pab3ZFls", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Origin: http://example.com", "Referer: http://example.com/media/add/image", "Cookie: SESSexample=example-session-value"], "body": "------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"files[field_media_image_0]\"; filename=\"powershell-setup.png\"\r\nContent-Type: image/png\r\n\r\n‰PNG\r\n\u001a\n\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0006\u0000\u0000\u0000\u001f\u0015ĉ\u0000\u0000\u0000\rIDATxÚcd`ø_\u000f\u0000\u0002‡\u0001€ëGº’\u0000\u0000\u0000\u0000IEND®B`‚\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"field_media_image[0][fids]\"\r\n\r\n\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"form_build_id\"\r\n\r\nform-VXabaXRkKe3hFQCBi49Nj8rAr5cTRZX5WNNCr-orKCE\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"form_token\"\r\n\r\n8VNEl_v41Qp_-ifKyOaSbJFXK8Q95OOmPua9eGqiTOw\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"form_id\"\r\n\r\nmedia_image_add_form\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"revision_log_message[0][value]\"\r\n\r\n\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"path[0][alias]\"\r\n\r\n\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"uid[0][target_id]\"\r\n\r\nadmin (1)\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"created[0][value][date]\"\r\n\r\n2026-11-09\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"created[0][value][time]\"\r\n\r\n09:03:20\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"advanced__active_tab\"\r\n\r\nedit-revision-information\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"status[value]\"\r\n\r\n1\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"_triggering_element_name\"\r\n\r\nfield_media_image_0_upload_button\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"_triggering_element_value\"\r\n\r\nUpload\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"_drupal_ajax\"\r\n\r\n1\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"ajax_page_state[theme]\"\r\n\r\nclaro\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"ajax_page_state[theme_token]\"\r\n\r\ngtuOMELGPT-cRRR7C5yAGfDEzvpCn6gTPnJ4IefXf4E\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls\r\nContent-Disposition: form-data; name=\"ajax_page_state[libraries]\"\r\n\r\neJxVj1ESwjAIRC_UtGeiERWlUAnpWE9votVM_naW5cFGBtNpwRNBOKstQ1RxfHoGnk6WV-CxOYFJ7qlEDKeoy6qC4mkU2OgCTiohuCrPYGHO7irf6MGB6LThh9H72bXSGB27QVRmWFNvlovk--_X5icHx9RZ9WswhGCY6NVjNjSnWCo5zMcWlZomZXZ7ZLR9bCekKOCKOBP_EVUPrfvU5Miwa_ZhBb_-0lW_AQOBjSg\r\n------WebKitFormBoundaryE7nrnHw1Pab3ZFls--\r\n"}
{"method": "GET", "target": "/sites/default/files/styles/thumbnail/public/2026-11/powershell-setup_3.png.avif?itok=yeYeqKgQ", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/media/add/image", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/search/path?search=python3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/moderated?title=python3&moderation_state=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/contextual/render?theme=claro", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/admin/content/moderated?title=python3&moderation_state=All", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=entity.view.edit_form%3Aview%3Dmoderated_content%3Alocation%3Dpage%26name%3Dmoderated_content%26display_id%3Dmoderated_content%26langcode%3Den&tokens%5B%5D=ZPCy2PWMDA1qxRmOrOGS24WKpHYbJym8Bd048r6MnIw"}
{"method": "GET", "target": "/admin/content/files?filename=&filemime=python3&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=&author_name=python3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/edit?destination=/search/node%3Fkeys%3Dpython3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?destination=/admin/content%3Ftitle%3Dpython3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/delete?destination=/admin/content%3Ftitle%3Dpython3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/logout?destination=/search/node%3Fkeys%3Dpython3", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/search/path?search=powershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/moderated?title=powershell&moderation_state=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=&filemime=powershell&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=&author_name=powershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/login?destination=/search/node%3Fkeys%3Dpowershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/edit?destination=/search/node%3Fkeys%3Dpowershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?destination=/admin/content%3Ftitle%3Dpowershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/delete?destination=/admin/content%3Ftitle%3Dpowershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/logout?destination=/search/node%3Fkeys%3Dpowershell", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/search/path?search=ls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/moderated?title=ls%20-la&moderation_state=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=&filemime=ls%20-la&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=&author_name=ls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/login?destination=/search/node%3Fkeys%3Dls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/edit?destination=/search/node%3Fkeys%3Dls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?destination=/admin/content%3Ftitle%3Dls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/delete?destination=/admin/content%3Ftitle%3Dls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/logout?destination=/search/node%3Fkeys%3Dls%20-la", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/search/path?search=.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/moderated?title=.htaccess&moderation_state=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=&filemime=.htaccess&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=&author_name=.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/login?destination=/search/node%3Fkeys%3D.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/edit?destination=/search/node%3Fkeys%3D.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?destination=/admin/content%3Ftitle%3D.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/delete?destination=/admin/content%3Ftitle%3D.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/logout?destination=/search/node%3Fkeys%3D.htaccess", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/search/path?search=wp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/moderated?title=wp-config.php&moderation_state=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=&filemime=wp-config.php&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=&author_name=wp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/login?destination=/search/node%3Fkeys%3Dwp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/edit?destination=/search/node%3Fkeys%3Dwp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?destination=/admin/content%3Ftitle%3Dwp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/delete?destination=/admin/content%3Ftitle%3Dwp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/logout?destination=/search/node%3Fkeys%3Dwp-config.php", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/config/search/path?search=docker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/moderated?title=docker-compose.yml&moderation_state=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/files?filename=&filemime=docker-compose.yml&status=All", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content/comment?subject=&author_name=docker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/login?destination=/search/node%3Fkeys%3Ddocker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/edit?destination=/search/node%3Fkeys%3Ddocker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/admin/content?destination=/admin/content%3Ftitle%3Ddocker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/1/delete?destination=/admin/content%3Ftitle%3Ddocker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/user/logout?destination=/search/node%3Fkeys%3Ddocker-compose.yml", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "GET", "target": "/node/2/delete", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "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", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/node/2/delete", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "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/node/2/delete", "Cookie: SESSexample=example-session-value"], "body": "confirm=1&form_build_id=form-hnGbBTfqMwKkGf8rHG1D2s_--brUB7ERWenxjTAV3LA&form_token=VhXEXVRRiAuV4qccGvDVSnGrooVZfRHrKiG51GM3yms&form_id=node_page_delete_form&op=Delete"}
{"method": "POST", "target": "/contextual/render?theme=olivero", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Content-Type: application/x-www-form-urlencoded;charset=UTF-8", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Cookie: SESSexample=example-session-value"], "body": "ids%5B%5D=entity.view.edit_form%3Aview%3Dfrontpage%3Alocation%3Dpage%26name%3Dfrontpage%26display_id%3Dpage_1%26langcode%3Den&tokens%5B%5D=gzNq81gUc0srM04Gao-KDKP3JhZ8i_LinPcztlc970A"}
{"method": "POST", "target": "/media/add/image?element_parents=field_media_image/widget/0&ajax_form=1&_wrapper_format=drupal_ajax", "headers": ["Host: example.com", "X-Requested-With: XMLHttpRequest", "Accept-Language: en-US,en;q=0.9", "Accept: application/json, text/javascript, */*; q=0.01", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywImR5Ybat1GIxzuU", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Origin: http://example.com", "Referer: http://example.com/media/add/image", "Cookie: SESSexample=example-session-value"], "body": "------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"files[field_media_image_0]\"; filename=\"powershell-setup.png\"\r\nContent-Type: image/png\r\n\r\n‰PNG\r\n\u001a\n\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0006\u0000\u0000\u0000\u001f\u0015ĉ\u0000\u0000\u0000\rIDATxÚcd`ø_\u000f\u0000\u0002‡\u0001€ëGº’\u0000\u0000\u0000\u0000IEND®B`‚\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"field_media_image[0][fids]\"\r\n\r\n\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"form_build_id\"\r\n\r\nform-w_XtjjjeoarI94S-L4lSdIzOHi_x9Kgcm8noqTZQNB8\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"form_token\"\r\n\r\nhe-agwYf9hgBeNvqmmhGH-VoLUAjNTrQ-e47x0x0Jxs\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"form_id\"\r\n\r\nmedia_image_add_form\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"revision_log_message[0][value]\"\r\n\r\n\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"path[0][alias]\"\r\n\r\n\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"uid[0][target_id]\"\r\n\r\nadmin (1)\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"created[0][value][date]\"\r\n\r\n2026-11-09\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"created[0][value][time]\"\r\n\r\n09:05:03\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"advanced__active_tab\"\r\n\r\nedit-revision-information\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"status[value]\"\r\n\r\n1\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"_triggering_element_name\"\r\n\r\nfield_media_image_0_upload_button\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"_triggering_element_value\"\r\n\r\nUpload\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"_drupal_ajax\"\r\n\r\n1\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"ajax_page_state[theme]\"\r\n\r\nclaro\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"ajax_page_state[theme_token]\"\r\n\r\nnwY4HfJ_Ywq9D0w8idDat-HyUFFrbgnBlO-ktsMr2r8\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU\r\nContent-Disposition: form-data; name=\"ajax_page_state[libraries]\"\r\n\r\neJxVj1ESwjAIRC_UtGeiERWlUAnpWE9votVM_naW5cFGBtNpwRNBOKstQ1RxfHoGnk6WV-CxOYFJ7qlEDKeoy6qC4mkU2OgCTiohuCrPYGHO7irf6MGB6LThh9H72bXSGB27QVRmWFNvlovk--_X5icHx9RZ9WswhGCY6NVjNjSnWCo5zMcWlZomZXZ7ZLR9bCekKOCKOBP_EVUPrfvU5Miwa_ZhBb_-0lW_AQOBjSg\r\n------WebKitFormBoundarywImR5Ybat1GIxzuU--\r\n"}
{"method": "GET", "target": "/sites/default/files/styles/thumbnail/public/2026-11/powershell-setup_4.png.avif?itok=iF2vDKZ9", "headers": ["Host: example.com", "Accept-Language: en-US,en;q=0.9", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/media/add/image", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "POST", "target": "/media/add/image", "headers": ["Host: example.com", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryMo0GuX6XENCyw7uS", "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/media/add/image", "Cookie: SESSexample=example-session-value"], "body": "------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"field_media_image[0][alt]\"\r\n\r\nA powershell logo\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"field_media_image[0][fids]\"\r\n\r\n16\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"form_build_id\"\r\n\r\nform-dg7NXZ7td0XPIB6_GTT6JAprMPLoOBmXE6djXbPE4iI\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"form_token\"\r\n\r\nhe-agwYf9hgBeNvqmmhGH-VoLUAjNTrQ-e47x0x0Jxs\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"form_id\"\r\n\r\nmedia_image_add_form\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"revision_log_message[0][value]\"\r\n\r\n\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"path[0][alias]\"\r\n\r\n\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"uid[0][target_id]\"\r\n\r\nadmin (1)\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"created[0][value][date]\"\r\n\r\n2026-11-09\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"created[0][value][time]\"\r\n\r\n09:05:03\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"advanced__active_tab\"\r\n\r\nedit-revision-information\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"status[value]\"\r\n\r\n1\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS\r\nContent-Disposition: form-data; name=\"op\"\r\n\r\nSave\r\n------WebKitFormBoundaryMo0GuX6XENCyw7uS--\r\n"}
{"method": "POST", "target": "/user/login?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"admin\", \"pass\": \"python3-/etc/hosts-1\"}"}
{"method": "POST", "target": "/jsonapi/node/article", "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: lRai9w8Ufs5XV4lf0RjtPEHPjM5YyEGC1vnmmCEgd08"], "body": "{\"data\": {\"type\": \"node--article\", \"attributes\": {\"title\": \"SELECT a table: python3, ls -la and .htaccess\", \"body\": {\"value\": \"<h2>Notes on python3 and powershell</h2><p>Run <code>ls -la</code> then <a href=\\\"/python3-notes\\\">read the notes</a> or <a href=\\\"https://example.com/docs/setup.php?a=1&amp;b=2\\\">the docs</a>.</p><pre><code class=\\\"language-bash\\\">#!/bin/sh\\ncat /etc/hosts | grep example &amp;&amp; ls -la\\npython3 setup.py install\\ncurl -s https://example.com/install.sh | sh\\nSELECT name, mail FROM users WHERE uid = 1;\\nconsole.log('ok'); document.getElementById('x');\\nif (a == b) { echo $PATH; }\\n</code></pre><blockquote><p>Select a table from the catalog, then union the results.</p></blockquote><ul><li>one</li><li>two</li></ul><table><tbody><tr><td>python3</td><td>3.13</td></tr></tbody></table><p><img src=\\\"/sites/default/files/inline-images/photo.png\\\" alt=\\\"A photo\\\" width=\\\"1\\\" height=\\\"1\\\" data-entity-type=\\\"file\\\" data-entity-uuid=\\\"11111111-2222-4333-8444-555555555555\\\"></p>\", \"format\": \"full_html\", \"summary\": \"Notes on python3 and powershell\"}}}}"}
{"method": "PATCH", "target": "/jsonapi/node/article/d71c9e89-d9bd-4822-ba15-54bef9d26522", "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: lRai9w8Ufs5XV4lf0RjtPEHPjM5YyEGC1vnmmCEgd08"], "body": "{\"data\": {\"type\": \"node--article\", \"id\": \"d71c9e89-d9bd-4822-ba15-54bef9d26522\", \"attributes\": {\"title\": \"Edited: docker-compose.yml\", \"body\": {\"value\": \"<h2>Notes on python3 and powershell</h2><p>Run <code>ls -la</code> then <a href=\\\"/python3-notes\\\">read the notes</a> or <a href=\\\"https://example.com/docs/setup.php?a=1&amp;b=2\\\">the docs</a>.</p><pre><code class=\\\"language-bash\\\">#!/bin/sh\\ncat /etc/hosts | grep example &amp;&amp; ls -la\\npython3 setup.py install\\ncurl -s https://example.com/install.sh | sh\\nSELECT name, mail FROM users WHERE uid = 1;\\nconsole.log('ok'); document.getElementById('x');\\nif (a == b) { echo $PATH; }\\n</code></pre><blockquote><p>Select a table from the catalog, then union the results.</p></blockquote><ul><li>one</li><li>two</li></ul><table><tbody><tr><td>python3</td><td>3.13</td></tr></tbody></table><p><img src=\\\"/sites/default/files/inline-images/photo.png\\\" alt=\\\"A photo\\\" width=\\\"1\\\" height=\\\"1\\\" data-entity-type=\\\"file\\\" data-entity-uuid=\\\"11111111-2222-4333-8444-555555555555\\\"></p>\", \"format\": \"full_html\"}}}}"}
{"method": "POST", "target": "/jsonapi/comment/comment", "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: lRai9w8Ufs5XV4lf0RjtPEHPjM5YyEGC1vnmmCEgd08"], "body": "{\"data\": {\"type\": \"comment--comment\", \"attributes\": {\"entity_type\": \"node\", \"field_name\": \"comment\", \"subject\": \"Python3 question\", \"comment_body\": {\"value\": \"<h2>Notes on python3 and powershell</h2><p>Run <code>ls -la</code> then <a href=\\\"/python3-notes\\\">read the notes</a> or <a href=\\\"https://example.com/docs/setup.php?a=1&amp;b=2\\\">the docs</a>.</p><pre><code class=\\\"language-bash\\\">#!/bin/sh\\ncat /etc/hosts | grep example &amp;&amp; ls -la\\npython3 setup.py install\\ncurl -s https://example.com/install.sh | sh\\nSELECT name, mail FROM users WHERE uid = 1;\\nconsole.log('ok'); document.getElementById('x');\\nif (a == b) { echo $PATH; }\\n</code></pre><blockquote><p>Select a table from the catalog, then union the results.</p></blockquote><ul><li>one</li><li>two</li></ul><table><tbody><tr><td>python3</td><td>3.13</td></tr></tbody></table><p><img src=\\\"/sites/default/files/inline-images/photo.png\\\" alt=\\\"A photo\\\" width=\\\"1\\\" height=\\\"1\\\" data-entity-type=\\\"file\\\" data-entity-uuid=\\\"11111111-2222-4333-8444-555555555555\\\"></p>\", \"format\": \"basic_html\"}}, \"relationships\": {\"entity_id\": {\"data\": {\"type\": \"node--article\", \"id\": \"d71c9e89-d9bd-4822-ba15-54bef9d26522\"}}}}}"}
+1 -1
View File
@@ -50,7 +50,7 @@ func (abuseIPDB) check(ctx context.Context, rq *request, _ time.Time) *verdict {
}
return &verdict{
action: requestlog.ActionClientDenied,
action: requestlog.ActionDenied,
figures: map[string]any{
figureScore: score, "min_score": rq.h.config.AbuseIPDBMinScore,
},
+2 -2
View File
@@ -42,7 +42,7 @@ var (
// answerHealth answers a request for the health endpoint.
func (rq *request) answerHealth() {
rq.line.Action = requestlog.ActionOwnEndpoint
rq.line.Action = requestlog.ActionAdmin
// Set here rather than left to Go's server, which would set it only
// after the log line has taken the response's headers.
rq.out.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -55,7 +55,7 @@ func (rq *request) answerHealth() {
// refused with 401. An endpoint whose token is unset answers 404, as any other
// path does.
func (rq *request) answerAdmin() {
rq.line.Action = requestlog.ActionOwnEndpoint
rq.line.Action = requestlog.ActionAdmin
token, answer := rq.endpoint()
+11 -11
View File
@@ -40,7 +40,7 @@ func TestAdminEndpointsAreOffWhileTheTokenIsUnset(t *testing.T) {
// An empty token does not match the unset one either.
for _, authorization := range []string{adminBearer, bearer, "Bearer ", ""} {
for _, e := range adminEndpoints() {
s.ownEndpointRequest(authorization, e.method, e.path, e.body, http.StatusNotFound)
s.adminRequest(authorization, e.method, e.path, e.body, http.StatusNotFound)
}
}
@@ -67,7 +67,7 @@ func TestAdminEndpointsNeedTheAdminToken(t *testing.T) {
for _, authorization := range []string{
"", bearer, "Bearer " + strings.ToUpper(adminSecret), "Basic " + adminSecret,
} {
got := s.ownEndpointRequest(authorization, e.method, e.path, e.body,
got := s.adminRequest(authorization, e.method, e.path, e.body,
http.StatusUnauthorized)
if got.header.Get("WWW-Authenticate") != "Bearer" {
t.Errorf("%s %s with %q was answered without WWW-Authenticate: Bearer",
@@ -121,7 +121,7 @@ func TestBanAddedListedAndLiftedThroughTheEndpoints(t *testing.T) {
wantBans(t, s.admin(http.MethodPost, discriminator.BansPath, banOtherClient,
http.StatusOK), want)
line := s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
line := s.get(client, http.StatusForbidden, requestlog.ActionBanned)
if line.BanExpires != requestlog.FormatTime(expires) {
t.Errorf("the ban ends at %s, want %s", line.BanExpires, expires)
}
@@ -276,7 +276,7 @@ func TestClientEndpointShowsTheClientAndItsBans(t *testing.T) {
// limit of two a minute, and bans it.
s.get(otherClient, http.StatusOK, requestlog.ActionForward)
s.get(otherClient, http.StatusOK, requestlog.ActionForward)
s.get(otherClient, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(otherClient, http.StatusForbidden, requestlog.ActionRateLimited)
// Asked about by its address in IPv6 form too.
for _, addr := range []string{otherClient, "::ffff:" + otherClient} {
@@ -329,7 +329,7 @@ func TestAdminEndpointsNeedTheTokenInObserveMode(t *testing.T) {
})
for _, e := range adminEndpoints() {
s.ownEndpointRequest("", e.method, e.path, e.body, http.StatusUnauthorized)
s.adminRequest("", e.method, e.path, e.body, http.StatusUnauthorized)
}
if held := server.Ledger.Snapshot(); len(held) != 0 {
@@ -356,16 +356,16 @@ func adminEndpoints() []adminEndpoint {
// admin sends a request with method for path, with body, to discriminator's own
// address, with the admin token, and checks that it is answered with status, its
// log line's action own_endpoint. It returns the answer.
// log line's action admin. It returns the answer.
func (s *sender) admin(method, path, body string, status int) answer {
s.t.Helper()
return s.ownEndpointRequest(adminBearer, method, path, body, status)
return s.adminRequest(adminBearer, method, path, body, status)
}
// ownEndpointRequest is admin with authorization as the request's Authorization
// header unless it is "".
func (s *sender) ownEndpointRequest(
// adminRequest is admin with authorization as the request's Authorization header
// unless it is "".
func (s *sender) adminRequest(
authorization, method, path, body string, status int,
) answer {
s.t.Helper()
@@ -380,7 +380,7 @@ func (s *sender) ownEndpointRequest(
line := s.out.endedLines(s.t, s.sent+1)[s.sent]
s.sent++
wantLine(s.t, line, status, requestlog.ActionOwnEndpoint)
wantLine(s.t, line, status, requestlog.ActionAdmin)
return got
}
+7 -9
View File
@@ -31,14 +31,14 @@ func TestBanForABrokenLimitRaisesABanAlertWithItsNotes(t *testing.T) {
start := clk.Now()
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(client, http.StatusForbidden, requestlog.ActionRateLimited)
netblock := netip.MustParsePrefix("203.0.113.0/24")
ban := server.Ledger.Bans(netblock)[0]
// A request refused under the ban raises no other alert.
clk.advance(time.Minute)
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
wantAlerts(t, queue, banAlert(alerts.EventBan, start, client, bans.Ban{
Netblock: netblock, Cause: bans.CauseLimit,
@@ -62,24 +62,22 @@ func TestAttackBanRaisesABanAlertThenAPermanentBanAlert(t *testing.T) {
// The probe bans the client for seven days, and its next request makes
// the ban permanent. The request after that changes nothing.
s.request(client, "/.env", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.request(client, "/.env", http.StatusForbidden, requestlog.ActionBanned)
attackBan := server.Ledger.Bans(netblock)[0]
clk.advance(time.Minute)
s.get(client, http.StatusForbidden, requestlog.ActionPermanentlyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
permanentBan := server.Ledger.Bans(netblock)[0]
s.get(client, http.StatusForbidden, requestlog.ActionPermanentlyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
// Another client's probe after its first ban has run out without a
// request makes a permanent ban at once.
s.request(otherClient, "/.env", http.StatusForbidden,
requestlog.ActionTemporarilyBanned)
s.request(otherClient, "/.env", http.StatusForbidden, requestlog.ActionBanned)
clk.advance(7 * 24 * time.Hour)
s.request(otherClient, "/.env", http.StatusForbidden,
requestlog.ActionPermanentlyBanned)
s.request(otherClient, "/.env", http.StatusForbidden, requestlog.ActionBanned)
otherBans := server.Ledger.Bans(other)
+2 -2
View File
@@ -73,7 +73,7 @@ func TestEachScopeAndWindowOverItsThresholdAlertsOncePerCooldown(t *testing.T) {
{anomalyASN, anomaly.ScopeASN, netip.Prefix{}, asnDE, map[string]any{"asn": asnDE}},
{anomalyTotal, anomaly.ScopeTotal, netip.Prefix{}, "the whole service", nil},
{
anomalyWatch, anomaly.ScopeWatchedNet, netip.MustParsePrefix(clientsNet),
anomalyWatch, anomaly.ScopeWatch, netip.MustParsePrefix(clientsNet),
"the named netblock office, " + clientsNet, map[string]any{"name": "office"},
},
} {
@@ -187,7 +187,7 @@ func TestEveryRequestIsCountedWhateverIsDoneWithIt(t *testing.T) {
{allowed, "/", http.StatusOK, requestlog.ActionForward},
{exempt, "/", http.StatusOK, requestlog.ActionForward},
{client, "/static/app.js", http.StatusOK, requestlog.ActionForward},
{denied, "/", http.StatusForbidden, requestlog.ActionClientDenied},
{denied, "/", http.StatusForbidden, requestlog.ActionDenied},
} {
for range 3 {
s.request(sent.from, sent.path, sent.status, sent.action)
+25 -31
View File
@@ -36,8 +36,8 @@ const (
// tooLarge is how discriminator refuses a request over the header limits.
tooLarge = http.StatusRequestHeaderFieldsTooLarge
// silentAbuseIPDBURL is where AbuseIPDB never answers: at the stand-in
// that silentGeoJSURL names too.
silentAbuseIPDBURL = "silent://abuseipdb/api/v2/check"
// that unansweredGeoJSURL names too.
silentAbuseIPDBURL = "unanswered://abuseipdb/api/v2/check"
// wholeLimit is the percentage of a rate limit that lowers it not at all.
wholeLimit = 100
// clientsDecision is the CrowdSec decision list holding client alone.
@@ -84,10 +84,10 @@ func TestSixteenThousandCookiesAreRefusedWellWithinTheTimeLimit(t *testing.T) {
wantDisposition(t, got, tooLarge, discriminator.DispositionDeny)
line := s.out.requestLine(t)
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
wantLine(t, line, tooLarge, requestlog.ActionTooLarge)
if line.DurationCoreRuleSetMS != nil {
t.Errorf("the Core Rule Set read the request, for %f ms", *line.DurationCoreRuleSetMS)
if line.DurationWAF != nil {
t.Errorf("the Core Rule Set read the request, for %f ms", *line.DurationWAF)
}
}
@@ -157,7 +157,7 @@ func TestEachHeaderLimitRefusesARequestOverIt(t *testing.T) {
if tc.refused {
wantDisposition(t, got, tooLarge, discriminator.DispositionDeny)
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
wantLine(t, line, tooLarge, requestlog.ActionTooLarge)
return
}
@@ -165,7 +165,7 @@ func TestEachHeaderLimitRefusesARequestOverIt(t *testing.T) {
wantDisposition(t, got, http.StatusOK, discriminator.DispositionAllow)
wantLine(t, line, http.StatusOK, requestlog.ActionForward)
if line.DurationCoreRuleSetMS == nil {
if line.DurationWAF == nil {
t.Error("the Core Rule Set did not read a request within the limits")
}
})
@@ -219,10 +219,9 @@ func TestObserveModeAllowsARequestOverTheHeaderLimitsUnread(t *testing.T) {
wantDisposition(t, got, http.StatusOK, discriminator.DispositionAllow)
line := s.out.requestLine(t)
if line.WouldAction != requestlog.ActionRequestHeaderTooLarge ||
line.DurationCoreRuleSetMS != nil {
t.Errorf("would_action %q and duration_core_rule_set_ms %v, want %q and none",
line.WouldAction, line.DurationCoreRuleSetMS, requestlog.ActionRequestHeaderTooLarge)
if line.WouldAction != requestlog.ActionTooLarge || line.DurationWAF != nil {
t.Errorf("would_action %q and duration_waf %v, want %q and none",
line.WouldAction, line.DurationWAF, requestlog.ActionTooLarge)
}
}
@@ -241,7 +240,7 @@ func TestRequestsOverTheHeaderLimitsCountTowardTheRateLimits(t *testing.T) {
wantDisposition(t, ask(), http.StatusForbidden, discriminator.DispositionDeny)
lines := s.out.requestLines(t, 2)
wantLine(t, lines[1], http.StatusForbidden, requestlog.ActionClientIsRateLimited)
wantLine(t, lines[1], http.StatusForbidden, requestlog.ActionRateLimited)
}
func TestTheHeaderLimitsComeAfterTheTrapPathsAndTheRuleFiles(t *testing.T) {
@@ -250,8 +249,8 @@ func TestTheHeaderLimitsComeAfterTheTrapPathsAndTheRuleFiles(t *testing.T) {
for _, tc := range []struct {
name, path, action string
}{
{"a trap path", "/wp-login.php", requestlog.ActionTemporarilyBanned},
{"a block rule", "/blocked", requestlog.ActionBlockedByBlockRule},
{"a trap path", "/wp-login.php", requestlog.ActionBanned},
{"a block rule", "/blocked", requestlog.ActionRuleBlocked},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@@ -281,10 +280,10 @@ func TestNotWaitingForGeoJSNeverLowersAClientsLimit(t *testing.T) {
wantDisposition(t, do(t, req), tooLarge, discriminator.DispositionDeny)
line := s.out.requestLine(t)
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
wantLine(t, line, tooLarge, requestlog.ActionTooLarge)
if line.RateLimitPercent != nil {
t.Errorf("limit percentage %d, want none", *line.RateLimitPercent)
if line.LimitPercent != nil {
t.Errorf("limit percentage %d, want none", *line.LimitPercent)
}
// The client is under no ban, and GeoJS places it in a country.
@@ -305,8 +304,7 @@ func TestAClientGeoJSPlacedInNoCountryCountsAsUnknownWithoutWaiting(t *testing.T
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
wantDisposition(t, do(t, req), http.StatusForbidden, discriminator.DispositionDeny)
wantLine(t, s.out.requestLine(t), http.StatusForbidden,
requestlog.ActionClientIsRateLimited)
wantLine(t, s.out.requestLine(t), http.StatusForbidden, requestlog.ActionRateLimited)
}
func TestObserveModeWaitsForGeoJSForARequestOverTheHeaderLimits(t *testing.T) {
@@ -340,7 +338,7 @@ func newDiscriminatorInBubble(
settings := map[string]string{tagDeny: "country=KP"}
maps.Copy(settings, env)
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now, settings)
server, out, _ := newDiscriminator(t, unansweredGeoJSURL, time.Now, settings)
return server, out
}
@@ -414,37 +412,33 @@ func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
}{
{
name: "SWW_DENY_NETS", env: map[string]string{denyNets: client},
prepare: leaveAsItIs, status: forbidden, action: requestlog.ActionClientDenied,
prepare: leaveAsItIs, status: forbidden, action: requestlog.ActionDenied,
},
{
name: "a ban",
prepare: banClient, status: forbidden, action: requestlog.ActionTemporarilyBanned,
prepare: banClient, status: forbidden, action: requestlog.ActionBanned,
},
{
name: "the rate limits", env: map[string]string{rateLimitPerMinute: "1"},
prepare: countClient, status: forbidden,
action: requestlog.ActionClientIsRateLimited,
prepare: countClient, status: forbidden, action: requestlog.ActionRateLimited,
},
{
name: "the rate limits, with a biased threshold",
env: map[string]string{rateLimitPerMinute: "1", unknownLimitPercent: "50"},
prepare: countClient, status: forbidden,
action: requestlog.ActionClientIsRateLimited,
prepare: countClient, status: forbidden, action: requestlog.ActionRateLimited,
},
{
name: "a blocklist", env: map[string]string{blocklistURLs: dropURL},
prepare: listClient, status: forbidden, action: requestlog.ActionClientDenied,
prepare: listClient, status: forbidden, action: requestlog.ActionDenied,
},
{
name: "the CrowdSec decision list",
env: map[string]string{crowdSecURL: lapi, crowdSecKey: bouncerKey},
prepare: decideOnClient, status: forbidden,
action: requestlog.ActionTemporarilyBanned,
prepare: decideOnClient, status: forbidden, action: requestlog.ActionBanned,
},
{
name: "the header limits", cookie: cookies(cookieLimit + 1),
prepare: leaveAsItIs, status: tooLarge,
action: requestlog.ActionRequestHeaderTooLarge,
prepare: leaveAsItIs, status: tooLarge, action: requestlog.ActionTooLarge,
},
} {
t.Run(tc.name, func(t *testing.T) {
+12 -26
View File
@@ -50,23 +50,13 @@ func (activeBan) check(_ context.Context, rq *request, now time.Time) *verdict {
}
return &verdict{
action: banAction(ban.Permanent()),
action: requestlog.ActionBanned,
figures: map[string]any{
figureNetblock: ban.Netblock.String(), "ban_expires": rq.line.BanExpires,
},
}
}
// banAction is the action of a request refused under a ban or for making one:
// permanent says the ban never runs out.
func banAction(permanent bool) string {
if permanent {
return requestlog.ActionPermanentlyBanned
}
return requestlog.ActionTemporarilyBanned
}
// limitFigures are the figures of a heuristic that broke hit, a limit: its
// window, the count and the limit.
func limitFigures(hit ratelimit.Hit) map[string]any {
@@ -106,7 +96,7 @@ func (rq *request) banForLimit(now time.Time, hit ratelimit.Hit, status int) {
case ratelimit.KindBytes:
rq.line.LimitHit = hit.Window + "_bytes" // as counts names the byte totals
case ratelimit.KindRefusals:
rq.line.LimitHit = requestlog.LimitHitMinuteRefusals
rq.line.LimitHit = requestlog.LimitHitErrorBurst
default:
rq.line.LimitHit = hit.Window
}
@@ -134,9 +124,9 @@ func (rq *request) banForLimit(now time.Time, hit ratelimit.Hit, status int) {
switch hit.Kind {
case ratelimit.KindRequests:
notes.LimitPercent, notes.LimitPercentSetting = rq.rateLimitPercent.logged()
notes.LimitPercent, notes.LimitPercentSetting = rq.limitPercent.logged()
case ratelimit.KindBytes:
notes.LimitPercent, notes.LimitPercentSetting = rq.bytesLimitPercent.logged()
notes.LimitPercent, notes.LimitPercentSetting = rq.bytesPercent.logged()
}
if rq.h.config.Observe {
@@ -161,11 +151,11 @@ func (rq *request) banForLimit(now time.Time, hit ratelimit.Hit, status int) {
// attack, which notes name: the ban rule that matched, or the trap path
// asked for. It fills in the rest of the notes. In observe mode it makes
// no ban, and raises the alert for the ban it would have made, if that
// alert would be sent. It returns the action for that ban.
func (rq *request) banForAttack(now time.Time, notes bans.Notes) string {
// alert would be sent.
func (rq *request) banForAttack(now time.Time, notes bans.Notes) {
netblock := rq.h.netblock(rq.client)
if rq.h.config.Observe && !rq.wouldAlertBan(netblock, now, bans.CauseAttack) {
return banAction(rq.h.ledger.WouldBePermanent(netblock, now, bans.CauseAttack))
return
}
notes.ASN = rq.line.ASN
@@ -181,7 +171,7 @@ func (rq *request) banForAttack(now time.Time, notes bans.Notes) string {
rq.alertBan(ban)
}
return banAction(ban.Permanent())
return
}
ban, made := rq.h.ledger.BanForAttack(netblock, now, notes)
@@ -190,18 +180,16 @@ func (rq *request) banForAttack(now time.Time, notes bans.Notes) string {
if made {
rq.alertBan(ban)
}
return banAction(ban.Permanent())
}
// banForCrowdSec bans the client's netblock at now until decision,
// CrowdSec's decision on the client, ends. In observe mode it makes no
// ban, and raises the alert for the ban it would have made, if that alert
// would be sent. It returns the action for that ban.
func (rq *request) banForCrowdSec(now time.Time, decision reputation.Decision) string {
// would be sent.
func (rq *request) banForCrowdSec(now time.Time, decision reputation.Decision) {
netblock := rq.h.netblock(rq.client)
if rq.h.config.Observe && !rq.wouldAlertBan(netblock, now, bans.CauseCrowdSec) {
return banAction(rq.h.ledger.WouldBePermanent(netblock, now, bans.CauseCrowdSec))
return
}
notes := bans.Notes{
@@ -220,7 +208,7 @@ func (rq *request) banForCrowdSec(now time.Time, decision reputation.Decision) s
rq.alertBan(ban)
}
return banAction(ban.Permanent())
return
}
ban, made := rq.h.ledger.BanForCrowdSec(netblock, now, decision.Expires,
@@ -230,8 +218,6 @@ func (rq *request) banForCrowdSec(now time.Time, decision reputation.Decision) s
if made {
rq.alertBan(ban)
}
return banAction(ban.Permanent())
}
// wouldAlertBan reports whether the alert for a ban on netblock for cause
+25 -27
View File
@@ -40,7 +40,7 @@ func TestBrokenLimitBansTheClient(t *testing.T) {
// client for an hour, the default.
s.get(client, http.StatusOK, requestlog.ActionForward)
line := s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
line := s.get(client, http.StatusForbidden, requestlog.ActionRateLimited)
if line.LimitHit != minute || line.Offence != requestlog.OffenceLimit ||
line.BanExpires != expires {
t.Errorf("log line has limit_hit %q, offence %q and ban_expires %q, "+
@@ -51,7 +51,7 @@ func TestBrokenLimitBansTheClient(t *testing.T) {
// Every request while the ban lasts is refused.
clk.advance(time.Hour - time.Second)
line = s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
line = s.get(client, http.StatusForbidden, requestlog.ActionBanned)
if line.BanExpires != expires || line.Offence != "" || line.LimitHit != "" {
t.Errorf("log line has ban_expires %q, offence %q and limit_hit %q, "+
"want %s and neither of the others", line.BanExpires, line.Offence,
@@ -78,9 +78,7 @@ func TestBanLengthsFollowTheSettings(t *testing.T) {
breakLimit := func() string {
s.get(client, http.StatusOK, requestlog.ActionForward)
line := s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
return line.BanExpires
return s.get(client, http.StatusForbidden, requestlog.ActionRateLimited).BanExpires
}
wantExpires := func(got string, length time.Duration) {
t.Helper()
@@ -113,7 +111,7 @@ func TestBanLengthsFollowTheSettings(t *testing.T) {
}
clk.advance(365 * 24 * time.Hour)
s.get(client, http.StatusForbidden, requestlog.ActionPermanentlyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
}
func TestBanIsNotCountedAndResetsTheCounters(t *testing.T) {
@@ -125,10 +123,10 @@ func TestBanIsNotCountedAndResetsTheCounters(t *testing.T) {
// client for an hour.
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(client, http.StatusForbidden, requestlog.ActionRateLimited)
for range 3 {
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
}
// Later the same day the client has its whole allowance again: the
@@ -137,7 +135,7 @@ func TestBanIsNotCountedAndResetsTheCounters(t *testing.T) {
clk.advance(time.Hour)
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(client, http.StatusForbidden, requestlog.ActionRateLimited)
banned := server.Ledger.Bans(netip.MustParsePrefix(client + "/32"))
if len(banned) != 2 || banned[0].Notes.Refused != 3 {
@@ -192,10 +190,10 @@ func TestBanCoversTheClientsNetblock(t *testing.T) {
s, _, _ := startWithClock(t, "", env)
s.get(tc.breaker, http.StatusOK, requestlog.ActionForward)
s.get(tc.breaker, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(tc.breaker, http.StatusForbidden, requestlog.ActionRateLimited)
for _, sent := range tc.refused {
s.get(sent, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(sent, http.StatusForbidden, requestlog.ActionBanned)
}
for _, sent := range tc.let {
@@ -218,9 +216,9 @@ func TestBannedClientIsRefusedBeforeItsCountryIsLookedUp(t *testing.T) {
// fromDE's ban covers otherClient, which is refused unasked about.
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited)
line := s.get(otherClient, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
line := s.get(otherClient, http.StatusForbidden, requestlog.ActionBanned)
if line.Country != "" {
t.Errorf("log line has country %q, want none", line.Country)
}
@@ -239,9 +237,9 @@ func TestRefusalUnderABanNamesItsHeuristicNetblockAndEnd(t *testing.T) {
expires := requestlog.FormatTime(clk.Now().Add(time.Hour))
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(client, http.StatusForbidden, requestlog.ActionRateLimited)
line := s.get(otherClient, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
line := s.get(otherClient, http.StatusForbidden, requestlog.ActionBanned)
wantHeuristic(t, line, "active_ban",
`{"netblock": "`+clientsNet+`", "ban_expires": "`+expires+`"}`)
}
@@ -277,11 +275,11 @@ func TestBanResponseAnswersEveryRefusalButTheSizeLimits(t *testing.T) {
s, _, _ := startWithClock(t, geojsURL, env)
s.get(denied, tc.status, requestlog.ActionClientDenied)
s.get(fromKP, tc.status, requestlog.ActionClientDenied)
s.get(denied, tc.status, requestlog.ActionDenied)
s.get(fromKP, tc.status, requestlog.ActionDenied)
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
s.get(fromDE, tc.status, requestlog.ActionClientIsRateLimited)
s.get(fromDE, tc.status, requestlog.ActionTemporarilyBanned)
s.get(fromDE, tc.status, requestlog.ActionRateLimited)
s.get(fromDE, tc.status, requestlog.ActionBanned)
})
}
}
@@ -299,9 +297,9 @@ func TestBanNotes(t *testing.T) {
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
s.request(fromDE, "/repo/commits?page=2",
http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(fromDE, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(fromDE, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
http.StatusForbidden, requestlog.ActionRateLimited)
s.get(fromDE, http.StatusForbidden, requestlog.ActionBanned)
s.get(fromDE, http.StatusForbidden, requestlog.ActionBanned)
netblock := netip.MustParsePrefix(fromDE + "/32")
want := bans.Ban{
@@ -344,7 +342,7 @@ func TestBanNotes(t *testing.T) {
// The next ban counts this one among the earlier.
clk.advance(time.Hour)
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited)
got = ledger.Bans(netblock)
if len(got) != 2 || got[1].Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
@@ -362,12 +360,12 @@ func TestMaxBansDropsTheBanOfTheNetblockSeenLongestAgo(t *testing.T) {
// One ban is held, so otherClient's ban drops client's.
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(client, http.StatusForbidden, requestlog.ActionRateLimited)
s.get(otherClient, http.StatusOK, requestlog.ActionForward)
s.get(otherClient, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(otherClient, http.StatusForbidden, requestlog.ActionRateLimited)
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(otherClient, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(otherClient, http.StatusForbidden, requestlog.ActionBanned)
}
// clock is the time a test sets, by which sww counts requests and
@@ -415,7 +413,7 @@ func startWithClock(
// sender sends requests to rproxy one after another, each on a connection of its
// own, and checks each one's answer and log line. discriminatorAddr is where
// discriminator's own endpoints are, which ownEndpointRequest asks. Every request
// discriminator's own endpoints are, which adminRequest asks. Every request
// discriminator gets must come through the sender, since the log lines are
// matched to them in order.
type sender struct {
+27 -35
View File
@@ -76,19 +76,17 @@ func TestEachBiasedThresholdLowersTheRateLimits(t *testing.T) {
}{
{http.StatusOK, requestlog.ActionForward},
{http.StatusOK, requestlog.ActionForward},
{http.StatusForbidden, requestlog.ActionClientIsRateLimited},
{http.StatusForbidden, requestlog.ActionRateLimited},
} {
line := s.get(tc.from, sent.status, sent.action)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
"50 from "+tc.setting)
}
// fromKP, which no setting lists, has the whole limit.
for range 3 {
line := s.get(fromKP, http.StatusOK, requestlog.ActionForward)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
none)
}
})
@@ -121,8 +119,7 @@ func TestEachBiasedThresholdLowersTheByteLimits(t *testing.T) {
t.Errorf("log line has limit_hit %q, want %s", line.LimitHit, minuteBytes)
}
wantPercent(t, "bytes_limit_percent",
line.BytesLimitPercent, line.BytesLimitPercentSetting,
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
"50 from "+tc.setting)
// fromKP, which no setting lists, has the whole limit.
@@ -131,8 +128,7 @@ func TestEachBiasedThresholdLowersTheByteLimits(t *testing.T) {
t.Errorf("log line for %s has limit_hit %q, want none", fromKP, line.LimitHit)
}
wantPercent(t, "bytes_limit_percent",
line.BytesLimitPercent, line.BytesLimitPercentSetting, none)
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting, none)
})
}
}
@@ -143,9 +139,9 @@ func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(t *testing.T)
for _, tc := range []struct {
name string
env map[string]string
// rateLimitPercent and bytesLimitPercent are the log line's, as
// percentText gives them, and limitHit is its limit_hit.
rateLimitPercent, bytesLimitPercent, limitHit string
// limitPercent and bytesPercent are the log line's, as percentText
// gives them, and limitHit is its limit_hit.
limitPercent, bytesPercent, limitHit string
}{
{
"lowering the byte limits alone",
@@ -181,12 +177,10 @@ func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(t *testing.T)
t.Errorf("log line has limit_hit %q, want %q", line.LimitHit, tc.limitHit)
}
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
tc.rateLimitPercent)
wantPercent(t, "bytes_limit_percent",
line.BytesLimitPercent, line.BytesLimitPercentSetting,
tc.bytesLimitPercent)
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
tc.limitPercent)
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
tc.bytesPercent)
})
}
}
@@ -198,15 +192,15 @@ func TestZeroPercentIsAZeroAllowance(t *testing.T) {
// The first request breaks the limit, and bans the client; the log line
// gives the 0.
line := s.get(unplaced, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
if line.fields["rate_limit_percent"] != float64(0) ||
line.fields["rate_limit_percent_setting"] != unknownLimitPercent {
t.Errorf("log line has rate_limit_percent %v from %v, want 0 from %s",
line.fields["rate_limit_percent"], line.fields["rate_limit_percent_setting"],
line := s.get(unplaced, http.StatusForbidden, requestlog.ActionRateLimited)
if line.fields["limit_percent"] != float64(0) ||
line.fields["limit_percent_setting"] != unknownLimitPercent {
t.Errorf("log line has limit_percent %v from %v, want 0 from %s",
line.fields["limit_percent"], line.fields["limit_percent_setting"],
unknownLimitPercent)
}
s.get(unplaced, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(unplaced, http.StatusForbidden, requestlog.ActionBanned)
}
func TestLowestPercentageApplies(t *testing.T) {
@@ -216,7 +210,7 @@ func TestLowestPercentageApplies(t *testing.T) {
name string
env map[string]string
from string
// want is the log line's rate_limit_percent, as percentText gives it.
// want is the log line's limit_percent, as percentText gives it.
want string
}{
{
@@ -256,9 +250,8 @@ func TestLowestPercentageApplies(t *testing.T) {
// A quarter of 4 requests a minute: the second breaks the limit.
s.get(tc.from, http.StatusOK, requestlog.ActionForward)
line := s.get(tc.from, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
line := s.get(tc.from, http.StatusForbidden, requestlog.ActionRateLimited)
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
tc.want)
})
}
@@ -276,7 +269,7 @@ func TestUnknownLimitPercentGivesEveryClientWithoutACountryItsPercentage(t *test
for _, from := range []string{unplaced, "10.0.0.8"} {
s.get(from, http.StatusOK, requestlog.ActionForward)
s.get(from, http.StatusOK, requestlog.ActionForward)
s.get(from, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(from, http.StatusForbidden, requestlog.ActionRateLimited)
}
// One in a country has the whole limit.
@@ -291,7 +284,7 @@ func TestClientWithoutAnAnswerInTimeHasTheUnknownLimitPercent(t *testing.T) {
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
// says, with a GeoJS that never answers.
synctest.Test(t, func(t *testing.T) {
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
server, out, _ := newDiscriminator(t, unansweredGeoJSURL, time.Now,
map[string]string{unknownLimitPercent: "0"})
// Once the time the request waits for its answer is up, the client
@@ -300,9 +293,8 @@ func TestClientWithoutAnAnswerInTimeHasTheUnknownLimitPercent(t *testing.T) {
serveFromDE(t, server, http.MethodGet, http.NoBody)
line := out.requestLine(t)
wantLine(t, line, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
wantLine(t, line, http.StatusForbidden, requestlog.ActionRateLimited)
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
"0 from "+unknownLimitPercent)
})
}
@@ -335,7 +327,7 @@ func TestRequestWaitsForItsLookupWhileABiasedThresholdIsSet(t *testing.T) {
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
// says, with a GeoJS that never answers.
synctest.Test(t, func(t *testing.T) {
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
server, out, _ := newDiscriminator(t, unansweredGeoJSURL, time.Now,
map[string]string{
lookupTimeout: timeout.String(), tagLists: "vpn=" + vpnList, tc.setting: tc.value,
})
@@ -382,7 +374,7 @@ func TestBanForALoweredLimitGivesThePercentageInItsNotesAndItsAlert(t *testing.T
// A quarter of 12 requests a minute is 3: the fourth breaks it.
"a rate limit",
map[string]string{rateLimitPerMinute: "12", tagLimitPercent: asnTagDEQuarter},
3, http.StatusForbidden, requestlog.ActionClientIsRateLimited,
3, http.StatusForbidden, requestlog.ActionRateLimited,
"requests per minute over the limit of 3", "25 from " + tagLimitPercent,
},
{
+1 -1
View File
@@ -28,7 +28,7 @@ func (blocklists) check(_ context.Context, rq *request, _ time.Time) *verdict {
}
return &verdict{
action: requestlog.ActionClientDenied,
action: requestlog.ActionDenied,
figures: map[string]any{"lists": listedBy},
}
}
+4 -2
View File
@@ -6,6 +6,8 @@ package discriminator
import (
"context"
"time"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// byteLimits counts the request's bytes, as countedBytes gives them, for the
@@ -26,7 +28,7 @@ func (byteLimits) check(_ context.Context, rq *request, now time.Time) *verdict
}
counts, hit, over := rq.h.limiter.CountBytes(rq.h.clientGroup(rq.client), now,
rq.countedBytes(), rq.bytesLimitPercent.percent)
rq.countedBytes(), rq.bytesPercent.percent)
rq.line.Counts.MinuteBytes = counts.MinuteBytes
rq.line.Counts.HourBytes = counts.HourBytes
rq.line.Counts.DayBytes = counts.DayBytes
@@ -37,5 +39,5 @@ func (byteLimits) check(_ context.Context, rq *request, now time.Time) *verdict
rq.banForLimit(now, hit, rq.out.status)
return &verdict{figures: limitFigures(hit)}
return &verdict{action: requestlog.ActionBanned, figures: limitFigures(hit)}
}
+6 -6
View File
@@ -90,7 +90,7 @@ func TestEachByteLimitBansOnceTheResponseHasEnded(t *testing.T) {
line.BanExpires, tc.window, expires)
}
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
wantMetric(t, s.scrape(), `sww_rate_limit_hits_total{`+
`instance="`+alertInstance+`",kind="bytes",window="`+tc.window+`"}`, 1)
@@ -125,7 +125,7 @@ func TestResponseOverAByteLimitByItselfIsPassedOnWhole(t *testing.T) {
len(got.body), got.err, line.LimitHit, answerBytes, minuteBytes)
}
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
}
func TestBytesOfAnAnswerThatBreaksOffAreCounted(t *testing.T) {
@@ -147,7 +147,7 @@ func TestBytesOfAnAnswerThatBreaksOffAreCounted(t *testing.T) {
answerBytes, minuteBytes, expires)
}
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
}
func TestWebSocketBytesAreCountedOnceItCloses(t *testing.T) {
@@ -176,7 +176,7 @@ func TestWebSocketBytesAreCountedOnceItCloses(t *testing.T) {
line.LimitHit, line.Counts.MinuteBytes, minuteBytes, tc.counted)
}
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
})
}
}
@@ -218,7 +218,7 @@ func TestBytesCountSaysWhichBytesCount(t *testing.T) {
}
}
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
})
}
}
@@ -386,7 +386,7 @@ func TestObserveModeLeavesOutTheBytesOfARequestEnforceModeRefuses(t *testing.T)
// counted, and the byte limit is not broken. Its line gives the bytes
// counted before it.
line := s.upload()
wantWouldAction(t, line, requestlog.ActionClientIsRateLimited)
wantWouldAction(t, line, requestlog.ActionRateLimited)
if line.LimitHit != minute || line.Counts.MinuteBytes != bodyBytes+answerBytes {
t.Errorf("log line has limit_hit %q and minute_bytes %v, want minute and %d",
+13 -13
View File
@@ -9,8 +9,8 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/waf"
)
// coreRuleSet inspects the request with the Core Rule Set, unless
@@ -18,7 +18,7 @@ import (
// pathExempt decides, and notes the rules it matched and its score in the
// 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,
// raises the waf_block 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{}
@@ -38,12 +38,12 @@ func (coreRuleSet) check(_ context.Context, rq *request, _ time.Time) *verdict {
result, read, _ := rq.h.coreRuleSet.Inspect(rq.in, rq.client, rq.in.Body)
rq.requestBytes = int64(len(read))
rq.line.DurationCoreRuleSetMS = new(requestlog.Milliseconds(time.Since(start)))
rq.line.CoreRuleSetRuleIDs = result.RuleIDs
rq.line.CoreRuleSetScore = &result.Score
rq.line.DurationWAF = new(requestlog.Milliseconds(time.Since(start)))
rq.line.WAFRuleIDs = result.RuleIDs
rq.line.WAFScore = &result.Score
for _, id := range result.RuleIDs {
rq.h.metrics.CoreRuleSetMatched(cfg.WAFMode, id)
rq.h.metrics.WAFMatched(cfg.WAFMode, id)
}
threshold := cfg.WAFAnomalyThreshold
@@ -52,26 +52,26 @@ func (coreRuleSet) check(_ context.Context, rq *request, _ time.Time) *verdict {
}
rq.addTag(config.ExploitSignatureTag)
rq.alertCoreRuleSetMatch(result)
rq.alertWAFBlock(result)
if cfg.WAFMode == config.WAFModeDetect {
return nil
}
rq.blockedByCoreRuleSet = true
rq.wafBlocked = true
return &verdict{
action: requestlog.ActionBlockedByCoreRuleSet,
action: requestlog.ActionWAFBlocked,
figures: map[string]any{figureScore: result.Score, "threshold": threshold},
}
}
// alertCoreRuleSetMatch raises the core_rule_set_match alert for the request,
// which the Core Rule Set scored at result, at or over SWW_WAF_ANOMALY_THRESHOLD. Its
// alertWAFBlock raises the waf_block alert for the request, which the Core
// Rule Set scored at result, at or over SWW_WAF_ANOMALY_THRESHOLD. Its
// 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) {
func (rq *request) alertWAFBlock(result waf.Result) {
detail := map[string]any{
"rule_ids": result.RuleIDs,
"score": result.Score,
@@ -87,7 +87,7 @@ func (rq *request) alertCoreRuleSetMatch(result coreruleset.Result) {
}
rq.h.alerts.Raise(alerts.Alert{
Event: alerts.EventCoreRuleSetMatch,
Event: alerts.EventWAFBlock,
Host: rq.site,
Client: rq.client,
Netblock: rq.h.clientGroup(rq.client),
+75 -61
View File
@@ -10,6 +10,7 @@ import (
"testing"
"sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/discriminator"
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
@@ -21,6 +22,7 @@ const (
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
apps = "SWW_APPS"
block = "block"
detect = "detect"
)
@@ -40,30 +42,24 @@ const (
// 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
// core_rule_set_score, and that it has duration_core_rule_set_ms, or with no
// score, that it has none of the three: the Core Rule Set did not inspect the
// request.
func wantCoreRuleSet(t *testing.T, line logLine, score *int, ruleIDs ...int) {
// wantWAF checks the request log line's waf_rule_ids and waf_score, and
// that it has duration_waf, or with no score, that it has none of the
// three: the Core Rule Set did not inspect the request.
func wantWAF(t *testing.T, line logLine, score *int, ruleIDs ...int) {
t.Helper()
if !slices.Equal(line.CoreRuleSetRuleIDs, ruleIDs) {
t.Errorf("log line has core_rule_set_rule_ids %v, want %v",
line.CoreRuleSetRuleIDs, ruleIDs)
if !slices.Equal(line.WAFRuleIDs, ruleIDs) {
t.Errorf("log line has waf_rule_ids %v, want %v", line.WAFRuleIDs, ruleIDs)
}
switch {
case score == nil &&
(line.CoreRuleSetScore != nil || line.DurationCoreRuleSetMS != nil):
t.Errorf("log line has core_rule_set_score %v and "+
"duration_core_rule_set_ms %v, want neither",
line.fields["core_rule_set_score"], line.fields["duration_core_rule_set_ms"])
case score != nil &&
(line.CoreRuleSetScore == nil || *line.CoreRuleSetScore != *score):
t.Errorf("log line has core_rule_set_score %v, want %d",
line.fields["core_rule_set_score"], *score)
case score != nil && line.DurationCoreRuleSetMS == nil:
t.Error("log line has no duration_core_rule_set_ms")
case score == nil && (line.WAFScore != nil || line.DurationWAF != nil):
t.Errorf("log line has waf_score %v and duration_waf %v, want neither",
line.fields["waf_score"], line.fields["duration_waf"])
case score != nil && (line.WAFScore == nil || *line.WAFScore != *score):
t.Errorf("log line has waf_score %v, want %d", line.fields["waf_score"], *score)
case score != nil && line.DurationWAF == nil:
t.Error("log line has no duration_waf")
}
}
@@ -76,7 +72,7 @@ func TestCoreRuleSetMatchAddsTheExploitSignatureTag(t *testing.T) {
status int
action string
}{
{block, block, http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet},
{block, block, http.StatusForbidden, requestlog.ActionWAFBlocked},
{detect, detect, http.StatusOK, requestlog.ActionForward},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -98,8 +94,7 @@ func TestDetectedAttackIsRefusedByTagDenyLast(t *testing.T) {
wafMode: detect, tagDeny: "exploitsignature",
})
line := s.request(client, sqlInjection, http.StatusForbidden,
requestlog.ActionClientDenied)
line := s.request(client, sqlInjection, http.StatusForbidden, requestlog.ActionDenied)
wantHeuristic(t, line, "tag_deny", `{"tag": "exploitsignature"}`)
}
@@ -139,14 +134,14 @@ func TestCoreRuleSetRefusesAttacksInBlockModeAndOnlyLogsThemInDetectMode(t *test
mode, action string
status int
}{
{block, requestlog.ActionBlockedByCoreRuleSet, http.StatusForbidden},
{block, requestlog.ActionWAFBlocked, http.StatusForbidden},
{detect, requestlog.ActionForward, http.StatusOK},
} {
s, _, _ := startWithClock(t, "", map[string]string{wafMode: tc.mode})
line := s.requestWithHeader(client, attack.path, attack.header,
tc.status, tc.action)
wantCoreRuleSet(t, line, &attack.score, attack.ruleIDs...)
wantWAF(t, line, &attack.score, attack.ruleIDs...)
}
})
}
@@ -158,7 +153,7 @@ func TestCoreRuleSetRefusalNamesItsHeuristicScoreAndThreshold(t *testing.T) {
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
line := s.request(client, sqlInjection, http.StatusForbidden,
requestlog.ActionBlockedByCoreRuleSet)
requestlog.ActionWAFBlocked)
wantHeuristic(t, line, "core_rule_set", `{"score": 5, "threshold": 5}`)
}
@@ -169,7 +164,7 @@ func TestOrdinaryRequestIsInspectedAndPassed(t *testing.T) {
line := s.request(client, "/owner/repo/src/branch/main/README.md?display=source",
http.StatusOK, requestlog.ActionForward)
wantCoreRuleSet(t, line, new(0))
wantWAF(t, line, new(0))
}
func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
@@ -189,26 +184,26 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
// 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)
wantWAF(t, line, nil)
line = s.request(client, "/api/v1/repos?id=1'%20OR%20'1'='1", http.StatusOK,
requestlog.ActionForward)
wantCoreRuleSet(t, line, nil)
wantWAF(t, line, nil)
// The prefix is matched as rate limit exempt paths are: a path that
// goes up and out of it is inspected.
line = s.request(client, "/api/../?id=1'%20OR%20'1'='1", http.StatusForbidden,
requestlog.ActionBlockedByCoreRuleSet)
wantCoreRuleSet(t, line, new(25), 930100, 930110, 942100)
requestlog.ActionWAFBlocked)
wantWAF(t, line, new(25), 930100, 930110, 942100)
// A request a rule file refuses is not inspected.
line = s.request(otherClient, "/blocked?id=1'%20OR%20'1'='1", http.StatusForbidden,
requestlog.ActionBlockedByBlockRule)
wantCoreRuleSet(t, line, nil)
requestlog.ActionRuleBlocked)
wantWAF(t, line, nil)
// 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)
wantWAF(t, line, nil)
}
func TestAnomalyThreshold(t *testing.T) {
@@ -222,14 +217,13 @@ func TestAnomalyThreshold(t *testing.T) {
})
line := s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
wantCoreRuleSet(t, line, new(5), 942100)
wantWAF(t, line, new(5), 942100)
}
s, _, _ := startWithClock(t, "", map[string]string{
wafMode: block, wafAnomalyThreshold: "5",
})
s.request(client, sqlInjection, http.StatusForbidden,
requestlog.ActionBlockedByCoreRuleSet)
s.request(client, sqlInjection, http.StatusForbidden, requestlog.ActionWAFBlocked)
}
func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
@@ -272,7 +266,7 @@ func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
line, _ := s.requestWithBody(request.method, client, request.path,
request.header, body, http.StatusOK, requestlog.ActionForward)
wantCoreRuleSet(t, line, new(0))
wantWAF(t, line, new(0))
// A list given replaces the default.
s, _, _ = startWithClock(t, "", map[string]string{
@@ -280,16 +274,37 @@ func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
})
score := 5 * len(request.ruleIDs)
line, _ = s.requestWithBody(request.method, client, request.path,
request.header, body, http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet)
wantCoreRuleSet(t, line, &score, request.ruleIDs...)
request.header, body, http.StatusForbidden, requestlog.ActionWAFBlocked)
wantWAF(t, line, &score, request.ruleIDs...)
// And switches off the rules it lists.
line = s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
wantCoreRuleSet(t, line, new(0))
wantWAF(t, line, new(0))
})
}
}
func TestGiteasFixesApplyOnlyToAHostnameThatServesGitea(t *testing.T) {
t.Parallel()
// Only gitea's fixes let a file name through in path, a parameter
// gitea sends file names in.
const fileName = "/?path=.gitignore"
s := startDiscriminator(t, "", map[string]string{
wafMode: block, apps: siteA + ":gitea",
})
wantDisposition(t, do(t, s.question(siteA, client, fileName)),
http.StatusOK, discriminator.DispositionAllow)
wantDisposition(t, do(t, s.question(siteB, client, fileName)),
http.StatusForbidden, discriminator.DispositionDeny)
lines := s.out.requestLines(t, 2)
wantLine(t, lines[0], http.StatusOK, requestlog.ActionForward)
wantLine(t, lines[1], http.StatusForbidden, requestlog.ActionWAFBlocked)
}
func TestAttackInAFormBodyIsRefusedOnlyWhileBodiesAreRead(t *testing.T) {
t.Parallel()
@@ -301,14 +316,14 @@ func TestAttackInAFormBodyIsRefusedOnlyWhileBodiesAreRead(t *testing.T) {
s, _, _ := startWithClock(t, "", map[string]string{wafMode: block})
line, _ := s.requestWithBody(http.MethodPost, client, "/", header, body,
http.StatusOK, requestlog.ActionForward)
wantCoreRuleSet(t, line, new(0))
wantWAF(t, line, new(0))
s, _, _ = startWithClock(t, "", map[string]string{
wafMode: block, wafBodyLimit: sizeLimitSetting,
})
line, _ = s.requestWithBody(http.MethodPost, client, "/", header, body,
http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet)
wantCoreRuleSet(t, line, new(5), 942100)
http.StatusForbidden, requestlog.ActionWAFBlocked)
wantWAF(t, line, new(5), 942100)
}
func TestBodiesReachTheAppAsSentWhileBodiesAreRead(t *testing.T) {
@@ -398,33 +413,32 @@ func TestCoreRuleSetRefusalIsAnOffenceAndCountsTowardTheErrorBurst(t *testing.T)
})
for range 2 {
s.request(client, sqlInjection, http.StatusForbidden,
requestlog.ActionBlockedByCoreRuleSet)
s.request(client, sqlInjection, http.StatusForbidden, requestlog.ActionWAFBlocked)
}
// The third refusal in a minute breaks the error burst, and bans the
// client.
line := s.request(client, sqlInjection, http.StatusForbidden,
requestlog.ActionBlockedByCoreRuleSet)
if line.LimitHit != requestlog.LimitHitMinuteRefusals ||
requestlog.ActionWAFBlocked)
if line.LimitHit != requestlog.LimitHitErrorBurst ||
line.Offence != requestlog.OffenceLimit {
t.Errorf("log line has limit_hit %q and offence %q, want minute_refusals and limit",
t.Errorf("log line has limit_hit %q and offence %q, want error_burst and limit",
line.LimitHit, line.Offence)
}
s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
want := ratelimit.Offences{Limit: 1, BlockedByCoreRuleSet: 3}
want := ratelimit.Offences{Limit: 1, WAFBlocked: 3}
if offences := historyOf(t, server, client).Offences; offences != want {
t.Errorf("history counts the offences %+v, want %+v", offences, want)
}
metrics := s.scrape()
wantMetric(t, metrics,
`sww_core_rule_set_matches_total{instance="app",mode="block",rule_id="942100"}`, 3)
`sww_waf_matches_total{instance="app",mode="block",rule_id="942100"}`, 3)
wantMetric(t, metrics,
`sww_offences_total{instance="app",kind="blocked_by_core_rule_set"}`, 3)
wantMetric(t, metrics, `sww_requests_total{action="blocked_by_core_rule_set",`+
`sww_offences_total{instance="app",kind="waf_blocked"}`, 3)
wantMetric(t, metrics, `sww_requests_total{action="waf_blocked",`+
`instance="app",status_class="4xx"}`, 3)
}
@@ -448,9 +462,9 @@ func TestDetectModeMatchIsNoOffenceAndNotCountedTowardTheErrorBurst(t *testing.T
metrics := s.scrape()
wantMetric(t, metrics,
`sww_core_rule_set_matches_total{instance="app",mode="detect",rule_id="942100"}`, 3)
`sww_waf_matches_total{instance="app",mode="detect",rule_id="942100"}`, 3)
wantNoSeries(t, metrics,
`sww_offences_total{instance="app",kind="blocked_by_core_rule_set"}`)
`sww_offences_total{instance="app",kind="waf_blocked"}`)
}
func TestObserveModeLogsWhatTheCoreRuleSetWouldDo(t *testing.T) {
@@ -459,17 +473,17 @@ func TestObserveModeLogsWhatTheCoreRuleSetWouldDo(t *testing.T) {
s, _, server := startWithClock(t, "", map[string]string{wafMode: block, mode: observe})
line := s.request(client, sqlInjection, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionBlockedByCoreRuleSet)
wantCoreRuleSet(t, line, new(5), 942100)
wantWouldAction(t, line, requestlog.ActionWAFBlocked)
wantWAF(t, line, new(5), 942100)
// It is an offence as in enforce mode.
want := ratelimit.Offences{BlockedByCoreRuleSet: 1}
want := ratelimit.Offences{WAFBlocked: 1}
if offences := historyOf(t, server, client).Offences; offences != want {
t.Errorf("history counts the offences %+v, want %+v", offences, want)
}
}
func TestCoreRuleSetMatchRaisesTheCoreRuleSetMatchAlert(t *testing.T) {
func TestCoreRuleSetMatchRaisesTheWAFBlockAlert(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
@@ -483,7 +497,7 @@ func TestCoreRuleSetMatchRaisesTheCoreRuleSetMatchAlert(t *testing.T) {
}{
{
"block", map[string]string{wafMode: block},
http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet, "",
http.StatusForbidden, requestlog.ActionWAFBlocked, "",
},
{
"detect", map[string]string{wafMode: detect},
@@ -518,7 +532,7 @@ func TestCoreRuleSetMatchRaisesTheCoreRuleSetMatchAlert(t *testing.T) {
wantAlerts(t, queue, alerts.Alert{
Instance: alertInstance,
Time: clk.Now(),
Event: alerts.EventCoreRuleSetMatch,
Event: alerts.EventWAFBlock,
Host: appHost,
Client: netip.MustParseAddr(client),
Netblock: netip.MustParsePrefix(client + "/32"),
+2 -1
View File
@@ -30,9 +30,10 @@ func (crowdSec) check(_ context.Context, rq *request, now time.Time) *verdict {
rq.addTag(config.CrowdSecTag)
rq.noteHit(bans.ReputationHit{Source: rq.h.config.CrowdSecDecisionsURL},
"listed by the CrowdSec decision list")
rq.banForCrowdSec(now, decision)
return &verdict{
action: rq.banForCrowdSec(now, decision),
action: requestlog.ActionBanned,
figures: map[string]any{
"scenario": decision.Scenario,
"expires": requestlog.FormatTime(decision.Expires),
+7 -7
View File
@@ -39,7 +39,7 @@ func TestClientTheCrowdSecDecisionListListsIsBannedUntilTheDecisionEnds(t *testi
expires := requestlog.FormatTime(fetched.Add(4 * time.Hour))
// Its first request is refused, and bans it until the decision ends.
line := s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
line := s.get(client, http.StatusForbidden, requestlog.ActionBanned)
wantReputation(t, line, decisionsURL)
if line.BanExpires != expires {
@@ -72,7 +72,7 @@ func TestClientTheCrowdSecDecisionListListsIsBannedUntilTheDecisionEnds(t *testi
// ban, and once it has ended the client is let through.
clk.advance(4*time.Hour - time.Second)
line = s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
line = s.get(client, http.StatusForbidden, requestlog.ActionBanned)
wantReputation(t, line)
if line.BanExpires != expires {
@@ -105,7 +105,7 @@ func TestCrowdSecBanNamesItsHeuristicScenarioAndEnd(t *testing.T) {
`"scenario": "crowdsecurity/ssh-bf", "scope": "Ip", "type": "ban", `+
`"value": "`+client+`"}]`)
line := s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
line := s.get(client, http.StatusForbidden, requestlog.ActionBanned)
wantHeuristic(t, line, "crowdsec", `{"scenario": "crowdsecurity/ssh-bf", `+
`"expires": "`+requestlog.FormatTime(clk.Now().Add(4*time.Hour))+`"}`)
wantTags(t, line, "crowdsec")
@@ -129,9 +129,9 @@ func TestEndedCrowdSecDecisionNoLongerBansThoughTheCopyStillHoldsIt(t *testing.T
// Just before its end, the decision bans a client in the netblock, and
// one on an IPv6 address bans the address's group, the /64.
clk.advance(time.Minute - time.Nanosecond)
s.get("198.51.100.7", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get("2001:db8::9", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get("2001:db8::5", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get("198.51.100.7", http.StatusForbidden, requestlog.ActionBanned)
s.get("2001:db8::9", http.StatusForbidden, requestlog.ActionBanned)
s.get("2001:db8::5", http.StatusForbidden, requestlog.ActionBanned)
// Once it has ended, it bans no other client, and the bans it made end
// with it.
@@ -164,7 +164,7 @@ func TestObserveModeForwardsAClientTheCrowdSecDecisionListListsAndAlertsTheBan(
`"value": "`+client+`"}]`)
line := s.get(client, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionTemporarilyBanned)
wantWouldAction(t, line, requestlog.ActionBanned)
wantReputation(t, line, decisionsURL)
if held := server.Ledger.Snapshot(); len(held) != 0 {
+1 -1
View File
@@ -19,7 +19,7 @@ func (denyNets) check(_ context.Context, rq *request, _ time.Time) *verdict {
for _, netblock := range rq.h.config.DenyNets {
if netblock.Contains(rq.client) {
return &verdict{
action: requestlog.ActionClientDenied,
action: requestlog.ActionDenied,
figures: map[string]any{figureNetblock: netblock.String()},
}
}
+14 -14
View File
@@ -93,8 +93,8 @@ type Report struct {
Status int `json:"status"`
RequestBytes int64 `json:"request_bytes"`
ResponseBytes int64 `json:"response_bytes"`
DurationTotalMS float64 `json:"duration_total_ms"`
DurationUpstreamTotalMS float64 `json:"duration_upstream_total_ms"`
DurationTotal float64 `json:"duration_total"`
DurationUpstreamTotal float64 `json:"duration_upstream_total"`
}
// New returns the server `sww discriminator` runs. It answers
@@ -183,7 +183,7 @@ func (h *handler) answerQuestion(w http.ResponseWriter, r *http.Request) {
rq.line.DiscriminatorLine = requestlog.DiscriminatorLineDecision
refused := rq.check(r.Context())
rq.checked = time.Now()
underOwnPrefix := strings.HasPrefix(r.URL.Path, ownEndpointPrefix)
underOwnPrefix := strings.HasPrefix(r.URL.Path, adminPrefix)
if refused == nil && !underOwnPrefix {
// In observe mode, what enforce mode would have refused counts here.
@@ -208,7 +208,7 @@ func (h *handler) answerQuestion(w http.ResponseWriter, r *http.Request) {
// The app never sees this prefix. discriminator's own endpoints are at its
// own address, so this answers as a path with no endpoint does.
rq.line.Action = requestlog.ActionOwnEndpoint
rq.line.Action = requestlog.ActionAdmin
rq.out.Header().Set(DispositionHeader, DispositionDeny)
http.Error(rq.out, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
@@ -405,7 +405,7 @@ func readReport(w http.ResponseWriter, r *http.Request) (Report, error) {
for _, name := range []string{
"request_id", "status", "request_bytes", "response_bytes",
"duration_total_ms", "duration_upstream_total_ms",
"duration_total", "duration_upstream_total",
} {
value, there := fields[name]
if !there || string(value) == "null" {
@@ -431,11 +431,11 @@ func (report *Report) checkRanges() error {
return fmt.Errorf("%w: request_bytes", errReportValue)
case report.ResponseBytes < 0 || report.ResponseBytes > maxReportedBytes:
return fmt.Errorf("%w: response_bytes", errReportValue)
case report.DurationTotalMS < 0 || report.DurationTotalMS > maxReportedMilliseconds:
return fmt.Errorf("%w: duration_total_ms", errReportValue)
case report.DurationUpstreamTotalMS < 0 ||
report.DurationUpstreamTotalMS > maxReportedMilliseconds:
return fmt.Errorf("%w: duration_upstream_total_ms", errReportValue)
case report.DurationTotal < 0 || report.DurationTotal > maxReportedMilliseconds:
return fmt.Errorf("%w: duration_total", errReportValue)
case report.DurationUpstreamTotal < 0 ||
report.DurationUpstreamTotal > maxReportedMilliseconds:
return fmt.Errorf("%w: duration_upstream_total", errReportValue)
}
return nil
@@ -459,18 +459,18 @@ func (h *handler) finishWith(ctx context.Context, held *decision, report Report)
rq.line.Status = report.Status
rq.line.RequestBytes = report.RequestBytes
rq.line.ResponseBytes = report.ResponseBytes
rq.line.DurationTotalMS = report.DurationTotalMS
rq.line.DurationTotal = report.DurationTotal
if rq.allowed {
rq.line.DurationUpstreamTotalMS = &report.DurationUpstreamTotalMS
rq.line.DurationUpstreamTotal = &report.DurationUpstreamTotal
rq.runBanningHeuristics(ctx, onReport())
rq.countAnomalies()
rq.addToHistory()
rq.h.metrics.RequestReported(&rq.line,
time.Duration(report.DurationTotalMS*float64(time.Millisecond)),
time.Duration(report.DurationUpstreamTotalMS*float64(time.Millisecond)))
time.Duration(report.DurationTotal*float64(time.Millisecond)),
time.Duration(report.DurationUpstreamTotal*float64(time.Millisecond)))
}
rq.writeLine()
+31 -33
View File
@@ -48,8 +48,8 @@ const (
reportStatus = "status"
reportRequestBytes = "request_bytes"
reportResponseBytes = "response_bytes"
reportDuration = "duration_total_ms"
reportUpstreamDuration = "duration_upstream_total_ms"
reportDuration = "duration_total"
reportUpstreamDuration = "duration_upstream_total"
)
// Discriminator is a discriminator running for one test, on a clock set to midnight.
@@ -639,7 +639,7 @@ func TestQuestionDeniedGetsTheBanResponse(t *testing.T) {
got := s.ask(client, "/")
wantDisposition(t, got, status, discriminator.DispositionDeny)
wantBody(t, got, http.StatusText(status)+"\n")
wantLine(t, s.out.requestLine(t), status, requestlog.ActionClientDenied)
wantLine(t, s.out.requestLine(t), status, requestlog.ActionDenied)
})
}
}
@@ -655,8 +655,8 @@ func TestBanResponseCloseIsAnAnswerOfItsOwn(t *testing.T) {
wantDisposition(t, got, http.StatusOK, discriminator.DispositionClose)
wantBody(t, got, "")
// The visitor is sent nothing.
wantLine(t, s.out.requestLine(t), 0, requestlog.ActionClientDenied)
wantMetric(t, s.metrics(), `sww_requests_total{action="client_denied",instance="`+
wantLine(t, s.out.requestLine(t), 0, requestlog.ActionDenied)
wantMetric(t, s.metrics(), `sww_requests_total{action="denied",instance="`+
alertInstance+`",status_class="none"}`, 1)
}
@@ -671,12 +671,12 @@ func TestBlockRuleIsDeniedWith403AndABanRuleWithTheBanResponse(t *testing.T) {
got := s.ask(client, "/blocked")
wantDisposition(t, got, http.StatusForbidden, discriminator.DispositionDeny)
wantLine(t, s.out.requestLine(t), http.StatusForbidden,
requestlog.ActionBlockedByBlockRule)
requestlog.ActionRuleBlocked)
got = s.ask(client, "/.env")
wantDisposition(t, got, http.StatusTooManyRequests, discriminator.DispositionDeny)
wantLine(t, s.out.requestLines(t, 2)[1], http.StatusTooManyRequests,
requestlog.ActionTemporarilyBanned)
requestlog.ActionBanned)
}
func TestQuestionUnderTheOwnPrefixIsDeniedWith404AfterTheChecks(t *testing.T) {
@@ -693,7 +693,7 @@ func TestQuestionUnderTheOwnPrefixIsDeniedWith404AfterTheChecks(t *testing.T) {
wantDisposition(t, got, http.StatusNotFound, discriminator.DispositionDeny)
}
wantLine(t, s.out.requestLine(t), http.StatusNotFound, requestlog.ActionOwnEndpoint)
wantLine(t, s.out.requestLine(t), http.StatusNotFound, requestlog.ActionAdmin)
got := s.ask(otherClient, discriminator.MetricsPath)
wantDisposition(t, got, http.StatusForbidden, discriminator.DispositionDeny)
@@ -709,7 +709,7 @@ func TestObserveModeAllowsAndLogsWhatEnforceModeWouldHaveDone(t *testing.T) {
line := s.out.requestLine(t)
wantLine(t, line, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionClientDenied)
wantWouldAction(t, line, requestlog.ActionDenied)
}
func TestObserveModeRefusalAnsweredAllowCountsForTheErrorBurst(t *testing.T) {
@@ -725,9 +725,9 @@ func TestObserveModeRefusalAnsweredAllowCountsForTheErrorBurst(t *testing.T) {
}
lines := s.out.requestLines(t, 2)
if lines[0].LimitHit != "" || lines[1].LimitHit != requestlog.LimitHitMinuteRefusals {
if lines[0].LimitHit != "" || lines[1].LimitHit != requestlog.LimitHitErrorBurst {
t.Errorf("log lines have limit_hit %q and %q, want none and %q",
lines[0].LimitHit, lines[1].LimitHit, requestlog.LimitHitMinuteRefusals)
lines[0].LimitHit, lines[1].LimitHit, requestlog.LimitHitErrorBurst)
}
}
@@ -744,9 +744,9 @@ func TestDeniedQuestionCountsForTheErrorBurst(t *testing.T) {
}
lines := s.out.requestLines(t, 2)
if lines[0].LimitHit != "" || lines[1].LimitHit != requestlog.LimitHitMinuteRefusals {
if lines[0].LimitHit != "" || lines[1].LimitHit != requestlog.LimitHitErrorBurst {
t.Errorf("log lines have limit_hit %q and %q, want none and %q",
lines[0].LimitHit, lines[1].LimitHit, requestlog.LimitHitMinuteRefusals)
lines[0].LimitHit, lines[1].LimitHit, requestlog.LimitHitErrorBurst)
}
}
@@ -894,14 +894,14 @@ func TestDeniedRequestIsFinishedAtItsDecision(t *testing.T) {
t.Errorf("history is %+v, want the refused request alone", history)
}
const series = `sww_requests_total{action="client_is_rate_limited",instance="` +
const series = `sww_requests_total{action="rate_limited",instance="` +
alertInstance + `",status_class="4xx"}`
wantMetric(t, s.metrics(), series, 1)
got := s.report(discriminator.Report{
RequestID: denied.Header.Get(requestIDHeader), Status: http.StatusForbidden,
ResponseBytes: 1000, DurationTotalMS: 2,
ResponseBytes: 1000, DurationTotal: 2,
})
wantStatus(t, got, http.StatusNoContent)
@@ -935,7 +935,7 @@ func TestMetricsCountAnAllowedRequestFromItsReport(t *testing.T) {
got := s.report(discriminator.Report{
RequestID: id, Status: http.StatusBadGateway,
RequestBytes: bodyBytes, ResponseBytes: answerBytes,
DurationTotalMS: 5, DurationUpstreamTotalMS: 4,
DurationTotal: 5, DurationUpstreamTotal: 4,
})
wantStatus(t, got, http.StatusNoContent)
@@ -951,8 +951,7 @@ func TestMetricsCountAnAllowedRequestFromItsReport(t *testing.T) {
unscraped := startDiscriminator(t, "", map[string]string{metricsToken: token})
got = unscraped.report(discriminator.Report{
RequestID: unscraped.allowed(client), Status: http.StatusBadGateway,
DurationTotalMS: 5,
RequestID: unscraped.allowed(client), Status: http.StatusBadGateway, DurationTotal: 5,
})
wantStatus(t, got, http.StatusNoContent)
wantMetric(t, unscraped.metrics(),
@@ -1190,8 +1189,8 @@ func wantReportsFields(t *testing.T, line logLine) {
if line.DiscriminatorLine != requestlog.DiscriminatorLineReport ||
line.Status != http.StatusCreated ||
line.RequestBytes != bodyBytes || line.ResponseBytes != answerBytes ||
line.DurationTotalMS != 12.5 || line.DurationUpstreamTotalMS == nil ||
*line.DurationUpstreamTotalMS != 10.25 {
line.DurationTotal != 12.5 || line.DurationUpstreamTotal == nil ||
*line.DurationUpstreamTotal != 10.25 {
t.Errorf("the report's line lacks the report's fields: %+v", line.Line)
}
}
@@ -1206,8 +1205,8 @@ func wantSameDecision(t *testing.T, reported, decided logLine) {
same.Status = decided.Status
same.RequestBytes = decided.RequestBytes
same.ResponseBytes = decided.ResponseBytes
same.DurationTotalMS = decided.DurationTotalMS
same.DurationUpstreamTotalMS = decided.DurationUpstreamTotalMS
same.DurationTotal = decided.DurationTotal
same.DurationUpstreamTotal = decided.DurationUpstreamTotal
same.Counts.MinuteBytes = decided.Counts.MinuteBytes
same.Counts.HourBytes = decided.Counts.HourBytes
same.Counts.DayBytes = decided.Counts.DayBytes
@@ -1225,7 +1224,7 @@ func wantDecisionsFields(t *testing.T, line logLine, id, query, accept string) {
if line.DiscriminatorLine != requestlog.DiscriminatorLineDecision ||
line.Status != http.StatusOK || line.ResponseBytes != 0 ||
line.DurationUpstreamTotalMS != nil || line.DurationChecksMS == nil ||
line.DurationUpstreamTotal != nil || line.DurationChecks == nil ||
line.RequestID != id || line.Query != query ||
!line.HasAuthorization || !line.HasCookie ||
line.RequestHeaders["accept"] != accept || len(line.RuleIDs) != 1 ||
@@ -1261,7 +1260,7 @@ func TestReportWritesTheLastLineWithTheDecisionsAndTheReportsFields(t *testing.T
got := s.report(discriminator.Report{
RequestID: id, Status: http.StatusCreated,
RequestBytes: bodyBytes, ResponseBytes: answerBytes,
DurationTotalMS: 12.5, DurationUpstreamTotalMS: 10.25,
DurationTotal: 12.5, DurationUpstreamTotal: 10.25,
})
wantStatus(t, got, http.StatusNoContent)
@@ -1299,14 +1298,14 @@ func TestEveryDeniedRequestsReportGetsItsLastLine(t *testing.T) {
}{
{
"deny", map[string]string{denyNets: client}, "/", http.StatusForbidden,
requestlog.ActionClientDenied,
requestlog.ActionDenied,
},
{
"close", map[string]string{denyNets: client, banResponse: banResponseClose}, "/",
0, requestlog.ActionClientDenied,
0, requestlog.ActionDenied,
},
{"own prefix", nil, discriminator.HealthPath, http.StatusNotFound,
requestlog.ActionOwnEndpoint},
requestlog.ActionAdmin},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@@ -1318,7 +1317,7 @@ func TestEveryDeniedRequestsReportGetsItsLastLine(t *testing.T) {
do(t, req)
got := s.report(discriminator.Report{
RequestID: id, Status: tc.status, ResponseBytes: 8, DurationTotalMS: 3,
RequestID: id, Status: tc.status, ResponseBytes: 8, DurationTotal: 3,
})
wantStatus(t, got, http.StatusNoContent)
@@ -1326,8 +1325,8 @@ func TestEveryDeniedRequestsReportGetsItsLastLine(t *testing.T) {
wantLine(t, line, tc.status, tc.action)
if line.DiscriminatorLine != requestlog.DiscriminatorLineReport ||
line.RequestID != id || line.ResponseBytes != 8 || line.DurationTotalMS != 3 ||
line.DurationUpstreamTotalMS != nil {
line.RequestID != id || line.ResponseBytes != 8 || line.DurationTotal != 3 ||
line.DurationUpstreamTotal != nil {
t.Errorf("the report's line is %+v", line.Line)
}
})
@@ -1584,10 +1583,9 @@ func TestCoreRuleSetAlertNamesTheSite(t *testing.T) {
wantDisposition(t, got, http.StatusForbidden, discriminator.DispositionDeny)
waiting := s.queue.Snapshot().Waiting[alerts.DestinationWebhook]
if len(waiting) != 1 || waiting[0].Event != alerts.EventCoreRuleSetMatch ||
if len(waiting) != 1 || waiting[0].Event != alerts.EventWAFBlock ||
waiting[0].Host != siteA {
t.Errorf("alerts waiting are %+v, want a core_rule_set_match naming %s",
waiting, siteA)
t.Errorf("alerts waiting are %+v, want a waf_block naming %s", waiting, siteA)
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ func (dnsbl) check(ctx context.Context, rq *request, _ time.Time) *verdict {
}
return &verdict{
action: requestlog.ActionClientDenied,
action: requestlog.ActionDenied,
figures: map[string]any{"zones": listedBy},
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ func (headerLimits) check(_ context.Context, rq *request, _ time.Time) *verdict
}
return &verdict{
action: requestlog.ActionRequestHeaderTooLarge,
action: requestlog.ActionTooLarge,
figures: map[string]any{
"fields": fields, "fields_limit": maxHeaderFields,
"cookies": cookies, "cookies_limit": maxCookies,
+1 -2
View File
@@ -21,8 +21,7 @@ func TestHealthEndpointCountsTowardNoLimit(t *testing.T) {
)
for range healthChecks {
got := s.ownEndpointRequest("", http.MethodGet, discriminator.HealthPath, "",
http.StatusOK)
got := s.adminRequest("", http.MethodGet, discriminator.HealthPath, "", http.StatusOK)
if string(got.body) != "ok\n" || got.header.Get("Content-Type") != contentType {
t.Errorf("health endpoint answered %q with Content-Type %q, want ok "+
+4 -4
View File
@@ -31,9 +31,9 @@ func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) {
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
clk.advance(time.Second)
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited)
clk.advance(time.Second)
s.get(fromDE, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(fromDE, http.StatusForbidden, requestlog.ActionBanned)
want := ratelimit.History{
FirstSeen: start,
@@ -100,8 +100,8 @@ func TestOwnEndpointsAreNotInTheHistory(t *testing.T) {
s, _, server := startWithClock(t, "", map[string]string{metricsToken: token})
s.ownEndpointRequest("", http.MethodGet, discriminator.HealthPath, "", http.StatusOK)
s.ownEndpointRequest("", http.MethodGet, discriminator.MetricsPath, "",
s.adminRequest("", http.MethodGet, discriminator.HealthPath, "", http.StatusOK)
s.adminRequest("", http.MethodGet, discriminator.MetricsPath, "",
http.StatusUnauthorized)
s.scrape()
+10 -10
View File
@@ -40,7 +40,7 @@ func TestEveryClientIsLookedUpWithoutWaitingWhileNoSettingNeedsIt(t *testing.T)
// comes too. None waits for GeoJS.
for _, line := range []logLine{
s.get(fromDE, http.StatusOK, requestlog.ActionForward),
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited),
s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited),
s.get(fromKP, http.StatusOK, requestlog.ActionForward),
} {
got := asnAndCountry{line.ASN, line.ASName, line.Country}
@@ -179,7 +179,7 @@ func TestASNumberAndNameInTheLogLineTheHistoryTheBanNotesAndTheAlert(t *testing.
for _, line := range []logLine{
s.get(fromDE, http.StatusOK, requestlog.ActionForward),
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited),
s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited),
} {
if got := (asnAndCountry{line.ASN, line.ASName, line.Country}); got != want {
t.Errorf("log line has %+v, want %+v", got, want)
@@ -264,7 +264,7 @@ func TestClientsAreLookedUpInTheLookupDatabaseAndGeoJSIsNotAsked(t *testing.T) {
want asnAndCountry
}{
{s.get(fromDE, http.StatusOK, requestlog.ActionForward), de},
{s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited), de},
{s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited), de},
{
s.get(fromKP, http.StatusForbidden, requestlog.ActionCountryDenied),
asnAndCountry{asnKP, asNameKP, "KP"},
@@ -385,7 +385,7 @@ func TestRequestWaitsAsLongAsTheLookupTimeoutSays(t *testing.T) {
// is never reached.
const timeout = 3 * time.Second
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
server, out, _ := newDiscriminator(t, unansweredGeoJSURL, time.Now,
map[string]string{
lookupTimeout: timeout.String(),
allowedCountries: "DE",
@@ -405,25 +405,25 @@ func TestRequestWaitsAsLongAsTheLookupTimeoutSays(t *testing.T) {
})
}
// silentGeoJSURL is where a GeoJS that never answers is asked: a
// unansweredGeoJSURL is where a GeoJS that never answers is asked: a
// request to it waits, without the network, until it is abandoned.
// TestMain registers it with Go's default transport, through which GeoJS
// is asked.
const silentGeoJSURL = "silent://geojs/v1/ip/geo.json"
const unansweredGeoJSURL = "unanswered://geojs/v1/ip/geo.json"
func TestMain(m *testing.M) {
transport, _ := http.DefaultTransport.(*http.Transport)
transport.RegisterProtocol("silent", silentGeoJS{})
transport.RegisterProtocol("unanswered", unansweredGeoJS{})
transport.RegisterProtocol("abuseipdb", abuseIPDBStandIn{})
m.Run()
}
// silentGeoJS is the GeoJS at silentGeoJSURL.
type silentGeoJS struct{}
// unansweredGeoJS is the GeoJS at unansweredGeoJSURL.
type unansweredGeoJS struct{}
// RoundTrip waits until req is abandoned.
func (silentGeoJS) RoundTrip(req *http.Request) (*http.Response, error) {
func (unansweredGeoJS) RoundTrip(req *http.Request) (*http.Response, error) {
<-req.Context().Done()
return nil, req.Context().Err()
+10 -10
View File
@@ -32,7 +32,7 @@ func TestMetricsAreOffWhileTheTokenIsUnset(t *testing.T) {
// An empty token does not match the unset one either.
for _, authorization := range []string{bearer, "Bearer ", ""} {
s.ownEndpointRequest(authorization, http.MethodGet, discriminator.MetricsPath, "",
s.adminRequest(authorization, http.MethodGet, discriminator.MetricsPath, "",
http.StatusNotFound)
}
}
@@ -60,7 +60,7 @@ func TestMetricsNeedTheTokenAndOtherPathsAreNotFound(t *testing.T) {
{http.MethodGet, "/_sww/bans", bearer, http.StatusNotFound},
{http.MethodPost, discriminator.HealthPath, "", http.StatusNotFound},
} {
got := s.ownEndpointRequest(tc.authorization, tc.method, tc.path, "", tc.status)
got := s.adminRequest(tc.authorization, tc.method, tc.path, "", tc.status)
if tc.status == http.StatusUnauthorized &&
got.header.Get("WWW-Authenticate") != "Bearer" {
@@ -91,7 +91,7 @@ func TestMetricsCountTheTraffic(t *testing.T) {
out.endedLines(t, 2)
forward := `{action="forward",instance="app",status_class="2xx"}`
notFound := `{action="own_endpoint",instance="app",status_class="4xx"}`
notFound := `{action="admin",instance="app",status_class="4xx"}`
// The request for the metrics is itself under way.
metrics := scrape(t, server.Addr)
@@ -125,15 +125,15 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
})
// SWW_BAN_RESPONSE=close sends no status at all.
s.get(denied, 0, requestlog.ActionClientDenied)
s.get(denied, 0, requestlog.ActionDenied)
// A first broken limit bans for an hour.
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, 0, requestlog.ActionClientIsRateLimited)
s.get(client, 0, requestlog.ActionRateLimited)
metrics := s.scrape()
wantMetric(t, metrics,
`sww_requests_total{action="client_denied",instance="app",status_class="none"}`, 1)
`sww_requests_total{action="denied",instance="app",status_class="none"}`, 1)
wantMetric(t, metrics, `sww_rate_limit_hits_total{instance="app",`+
`kind="requests",window="minute"}`, 1)
wantMetric(t, metrics, `sww_offences_total{instance="app",kind="limit"}`, 1)
@@ -147,7 +147,7 @@ func TestMetricsCountLimitsAndBans(t *testing.T) {
// A limit broken again right after would ban for three hours, longer
// than SWW_MAX_BAN_DURATION, so the ban is permanent.
s.get(client, http.StatusOK, requestlog.ActionForward)
s.get(client, 0, requestlog.ActionClientIsRateLimited)
s.get(client, 0, requestlog.ActionRateLimited)
metrics = s.scrape()
wantMetric(t, metrics, `sww_rate_limit_hits_total{instance="app",`+
@@ -347,7 +347,7 @@ func TestMetricsCountGeoJSRequestsAndFailures(t *testing.T) {
wantMetric(t, metrics, `sww_geojs_requests_total{instance="app"}`, 1)
wantMetric(t, metrics, `sww_geojs_failures_total{instance="app"}`, 1)
wantMetric(t, metrics, `sww_requests_without_geojs_answer_total{instance="app"}`, 1)
wantMetric(t, metrics, `sww_geojs_unanswered_total{instance="app"}`, 1)
}
// keptAnswer returns GeoJS's answer that the client at addr is in
@@ -381,8 +381,8 @@ func scrape(t *testing.T, addr string) string {
func (s *sender) scrape() string {
s.t.Helper()
return string(s.ownEndpointRequest(bearer, http.MethodGet, discriminator.MetricsPath,
"", http.StatusOK).body)
return string(s.adminRequest(bearer, http.MethodGet, discriminator.MetricsPath, "",
http.StatusOK).body)
}
// metric returns the value of series in metrics, which are in the
+7 -95
View File
@@ -7,7 +7,6 @@ import (
"testing"
"time"
"sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
@@ -15,93 +14,6 @@ import (
// observe is the value of SWW_MODE for observe mode.
const observe = "observe"
func TestBanActionSaysWhetherTheBanRunsOut(t *testing.T) {
t.Parallel()
const trapPath = "/wp-login.php"
for _, tc := range []struct {
name string
cause string
// endsIn is how long after the request the ban ends: negative for a
// ban that has ended, and zero for a permanent ban.
endsIn time.Duration
path string
want string
}{
{
"temporary ban", bans.CauseAdmin, time.Hour, "/",
requestlog.ActionTemporarilyBanned,
},
{"permanent ban", bans.CauseAdmin, 0, "/", requestlog.ActionPermanentlyBanned},
{
"request that makes a ban permanent", bans.CauseAttack, time.Hour, "/",
requestlog.ActionPermanentlyBanned,
},
{
"trap path after an attack ban ended", bans.CauseAttack, -time.Hour,
trapPath, requestlog.ActionPermanentlyBanned,
},
{
"first trap path after another ban ended", bans.CauseAdmin, -time.Hour,
trapPath, requestlog.ActionTemporarilyBanned,
},
} {
for _, modeValue := range []string{"enforce", observe} {
t.Run(tc.name+" in "+modeValue, func(t *testing.T) {
t.Parallel()
s, clk, server := startWithClock(t, "", map[string]string{
trapPaths: trapPathList, mode: modeValue,
})
ban := bans.Ban{
Netblock: netip.MustParsePrefix(client + "/32"),
Start: clk.Now().Add(-2 * time.Hour),
Cause: tc.cause,
}
if tc.endsIn != 0 {
ban.Expires = clk.Now().Add(tc.endsIn)
}
server.Ledger.Load([]bans.Ban{ban})
if modeValue == observe {
line := s.request(client, tc.path, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, tc.want)
return
}
s.request(client, tc.path, http.StatusForbidden, tc.want)
})
}
}
}
func TestObserveModeNamesAPermanentBanWhoseAlertWouldBeSent(t *testing.T) {
t.Parallel()
s, clk, server, queue := startWithAlerts(t, map[string]string{
trapPaths: trapPathList, mode: observe,
})
server.Ledger.Load([]bans.Ban{{
Netblock: netip.MustParsePrefix(client + "/32"),
Start: clk.Now().Add(-2 * time.Hour),
Expires: clk.Now().Add(-time.Hour),
Cause: bans.CauseAttack,
}})
line := s.request(client, "/wp-login.php", http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionPermanentlyBanned)
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
if len(waiting) != 1 || waiting[0].Event != alerts.EventPermanentBan {
t.Errorf("alerts waiting are %+v, want one %s", waiting, alerts.EventPermanentBan)
}
}
func TestObserveModeNamesTheHeuristicThatWouldHaveRefused(t *testing.T) {
t.Parallel()
@@ -110,7 +22,7 @@ func TestObserveModeNamesTheHeuristicThatWouldHaveRefused(t *testing.T) {
})
line := s.get(client, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionClientDenied)
wantWouldAction(t, line, requestlog.ActionDenied)
wantHeuristic(t, line, "deny_nets", `{"netblock": "`+client+`/32"}`)
}
@@ -157,10 +69,10 @@ func TestObserveModeForwardsWhatEnforceModeRefuses(t *testing.T) {
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
for _, sent := range []struct{ from, refusal string }{
{denied, requestlog.ActionClientDenied},
{banned, requestlog.ActionTemporarilyBanned},
{fromKP, requestlog.ActionClientDenied},
{fromDE, requestlog.ActionClientIsRateLimited},
{denied, requestlog.ActionDenied},
{banned, requestlog.ActionBanned},
{fromKP, requestlog.ActionDenied},
{fromDE, requestlog.ActionRateLimited},
} {
if !tc.observe {
line := s.get(sent.from, http.StatusForbidden, sent.refusal)
@@ -198,7 +110,7 @@ func TestObserveModeMakesNoBanAndKeepsTheBansItHas(t *testing.T) {
for range 2 {
line := s.get(client, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionClientIsRateLimited)
wantWouldAction(t, line, requestlog.ActionRateLimited)
if line.LimitHit != minute || line.Offence != requestlog.OffenceLimit ||
line.BanExpires != "" {
@@ -211,7 +123,7 @@ func TestObserveModeMakesNoBanAndKeepsTheBansItHas(t *testing.T) {
// The ban read from bans.json refuses nothing, and so counts no
// refusal in its notes, but is kept.
line := s.get(otherClient, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionTemporarilyBanned)
wantWouldAction(t, line, requestlog.ActionBanned)
if line.BanExpires != requestlog.FormatTime(kept.Expires) {
t.Errorf("log line has ban_expires %q, want %s", line.BanExpires,
+13 -11
View File
@@ -16,18 +16,18 @@ import (
"sneak.berlin/go/smallwebwaf/internal/anomaly"
"sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
"sneak.berlin/go/smallwebwaf/internal/lookup"
"sneak.berlin/go/smallwebwaf/internal/metrics"
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
"sneak.berlin/go/smallwebwaf/internal/reputation"
"sneak.berlin/go/smallwebwaf/internal/routewatch"
"sneak.berlin/go/smallwebwaf/internal/rules"
"sneak.berlin/go/smallwebwaf/internal/waf"
)
// ownEndpointPrefix starts the path of each of discriminator's own endpoints. The app
// never sees a path under it.
const ownEndpointPrefix = "/_sww/"
// adminPrefix starts the path of each of discriminator's own endpoints. The app never
// sees a path under it.
const adminPrefix = "/_sww/"
// HealthPath is the health endpoint of discriminator and of rproxy, which the
// container's health check asks.
@@ -65,7 +65,7 @@ type Params struct {
LookupFile *lookup.File
// CoreRuleSet is what NewCoreRuleSet returns: what inspects requests
// while SWW_WAF_MODE is not off, and nil while it is.
CoreRuleSet *coreruleset.CoreRuleSet
CoreRuleSet *waf.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,
// normally time.Now in UTC, the time the state files give.
@@ -129,7 +129,7 @@ func newHandler(params Params) *handler {
BytesPerHour: params.Config.BytesLimitPerHour,
BytesPerDay: params.Config.BytesLimitPerDay,
}, params.Config.MaxTrackedClients),
ledger: bans.New(bans.Params{
ledger: bans.New(bans.Rules{
LimitBanDuration: params.Config.LimitBanDuration,
LimitBanRepeatWindow: params.Config.LimitBanRepeatWindow,
MaxBanDuration: params.Config.MaxBanDuration,
@@ -221,21 +221,23 @@ func newReputation(
// 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
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE and the fixes of the apps in SWW_APPS,
// 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
// JSON or XML body over its limit. An exclusions file that waf.New refuses is
// an error in every mode. While the mode is off, a set file is loaded with the
// Core Rule Set only to check it.
func NewCoreRuleSet(cfg *config.Config) (*coreruleset.CoreRuleSet, error) {
func NewCoreRuleSet(cfg *config.Config) (*waf.CoreRuleSet, error) {
if cfg.WAFMode == config.WAFModeOff && cfg.CoreRuleSetExclusionsFile == "" {
return nil, nil //nolint:nilnil // off is no Core Rule Set, and no error
}
coreRuleSet, err := coreruleset.New(coreruleset.Params{
coreRuleSet, err := waf.New(waf.Params{
ParanoiaLevel: cfg.WAFParanoiaLevel, DisabledRules: cfg.WAFDisabledRules,
BodyLimit: min(cfg.WAFBodyLimit, QuestionBodyMaxBytes-1),
ExclusionsFile: cfg.CoreRuleSetExclusionsFile,
Apps: cfg.Apps,
})
if err != nil {
return nil, fmt.Errorf("SWW_CORE_RULE_SET_EXCLUSIONS_FILE: %w", err)
@@ -266,7 +268,7 @@ type handler struct {
abuseIPDB *reputation.AbuseIPDB
routeWatch *routewatch.Client
rules *rules.Files
coreRuleSet *coreruleset.CoreRuleSet
coreRuleSet *waf.CoreRuleSet
alerts *alerts.Queue
// awaitingReport holds the decision on each answered question by its id
// until its report comes, and nil for a question still being handled.
+5 -9
View File
@@ -28,14 +28,12 @@ func (rateLimits) check(_ context.Context, rq *request, now time.Time) *verdict
return nil
}
rq.rateLimitPercent, rq.bytesLimitPercent = rq.limitPercentages()
rq.line.RateLimitPercent, rq.line.RateLimitPercentSetting =
rq.rateLimitPercent.logged()
rq.line.BytesLimitPercent, rq.line.BytesLimitPercentSetting =
rq.bytesLimitPercent.logged()
rq.limitPercent, rq.bytesPercent = rq.limitPercentages()
rq.line.LimitPercent, rq.line.LimitPercentSetting = rq.limitPercent.logged()
rq.line.BytesPercent, rq.line.BytesPercentSetting = rq.bytesPercent.logged()
counts, hit, over := rq.h.limiter.Count(rq.h.clientGroup(rq.client), now,
rq.rateLimitPercent.percent)
rq.limitPercent.percent)
rq.line.Counts = counts
if !over {
@@ -44,9 +42,7 @@ func (rateLimits) check(_ context.Context, rq *request, now time.Time) *verdict
rq.banForLimit(now, hit, rq.h.config.BanResponse)
return &verdict{
action: requestlog.ActionClientIsRateLimited, figures: limitFigures(hit),
}
return &verdict{action: requestlog.ActionRateLimited, figures: limitFigures(hit)}
}
// overAWholeRateLimit reports whether the heuristic rate_limits will refuse the
+8 -11
View File
@@ -59,7 +59,7 @@ func TestRateLimitRefusesBeforeTheApp(t *testing.T) {
if sent.want == http.StatusOK {
wantLine(t, line, http.StatusOK, requestlog.ActionForward)
} else {
wantLine(t, line, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
wantLine(t, line, http.StatusForbidden, requestlog.ActionRateLimited)
if line.LimitHit != minute {
t.Errorf("log line has limit_hit %q, want minute", line.LimitHit)
@@ -79,7 +79,7 @@ func TestRateLimitRefusalNamesItsHeuristicWindowCountAndLimit(t *testing.T) {
s.get(client, http.StatusOK, requestlog.ActionForward)
line := s.get(client, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
line := s.get(client, http.StatusForbidden, requestlog.ActionRateLimited)
wantHeuristic(t, line, "rate_limits", `{"window": "minute", "count": 2, "limit": 1}`)
}
@@ -102,7 +102,7 @@ func TestIPv6GroupPrefixSetsTheClientTheLimitsCount(t *testing.T) {
status int
action string
}{
{rateLimitPerMinute, "1", http.StatusForbidden, requestlog.ActionClientIsRateLimited},
{rateLimitPerMinute, "1", http.StatusForbidden, requestlog.ActionRateLimited},
{bytesLimitPerMinute, byteLimit, http.StatusOK, requestlog.ActionForward},
} {
t.Run(tc.setting, func(t *testing.T) {
@@ -160,17 +160,14 @@ func TestRateLimitExemptPathsAreNeitherCountedNorRefused(t *testing.T) {
// A path outside every prefix is counted: /assets is not under
// /assets/, and breaks the limit.
s.request(client, "/assets", http.StatusForbidden,
requestlog.ActionClientIsRateLimited)
s.request(client, "/assets", http.StatusForbidden, requestlog.ActionRateLimited)
// 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)
s.request(denied, "/assets/app.js", http.StatusForbidden,
requestlog.ActionClientDenied)
s.request(client, "/assets/app.js", http.StatusForbidden, requestlog.ActionBanned)
s.request(denied, "/assets/app.js", http.StatusForbidden, requestlog.ActionDenied)
s.request(fromKP, "/assets/app.js",
http.StatusForbidden, requestlog.ActionClientDenied)
http.StatusForbidden, requestlog.ActionDenied)
}
func TestRateLimitCountsPathsThatAreNotExempt(t *testing.T) {
@@ -214,7 +211,7 @@ func TestRateLimitCountsPathsThatAreNotExempt(t *testing.T) {
// Counted, the second request breaks the limit of one request
// a minute.
s.request(client, sent, http.StatusOK, requestlog.ActionForward)
s.request(client, sent, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.request(client, sent, http.StatusForbidden, requestlog.ActionRateLimited)
})
}
}
+4 -4
View File
@@ -28,7 +28,7 @@ func (refusalLimit) check(_ context.Context, rq *request, now time.Time) *verdic
return nil
}
if !rq.attack && !rq.blockedByBlockRule && !rq.blockedByCoreRuleSet {
if !rq.attack && !rq.ruleBlocked && !rq.wafBlocked {
return nil
}
@@ -42,13 +42,13 @@ func (refusalLimit) check(_ context.Context, rq *request, now time.Time) *verdic
status := rq.out.status
switch rq.line.WouldAction {
case requestlog.ActionBlockedByBlockRule, requestlog.ActionBlockedByCoreRuleSet:
case requestlog.ActionRuleBlocked, requestlog.ActionWAFBlocked:
status = http.StatusForbidden
case requestlog.ActionTemporarilyBanned, requestlog.ActionPermanentlyBanned:
case requestlog.ActionBanned:
status = cfg.BanResponse
}
rq.banForLimit(now, hit, status)
return &verdict{figures: limitFigures(hit)}
return &verdict{action: requestlog.ActionBanned, figures: limitFigures(hit)}
}
+17 -30
View File
@@ -37,10 +37,9 @@ func (r refused) send(s *sender, from string) logLine {
switch r {
case blockRule:
return s.request(from, blockedPath, http.StatusForbidden,
requestlog.ActionBlockedByBlockRule)
requestlog.ActionRuleBlocked)
case banRule:
return s.request(from, probePath, http.StatusForbidden,
requestlog.ActionTemporarilyBanned)
return s.request(from, probePath, http.StatusForbidden, requestlog.ActionBanned)
}
s.t.Fatalf("no request for the refusal %d", r)
@@ -72,20 +71,9 @@ func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
name string
// refusals are four, one over the threshold of three.
refusals []refused
// nextAction is the action of the client's next request: the next
// request after a ban for a ban rule makes it permanent.
nextAction string
}{
{
"block rule",
[]refused{blockRule, blockRule, blockRule, blockRule},
requestlog.ActionTemporarilyBanned,
},
{
"a ban rule last",
[]refused{blockRule, blockRule, blockRule, banRule},
requestlog.ActionPermanentlyBanned,
},
{"block rule", []refused{blockRule, blockRule, blockRule, blockRule}},
{"a ban rule last", []refused{blockRule, blockRule, blockRule, banRule}},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@@ -107,13 +95,13 @@ func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
// The fourth is answered as the others were, breaks the error
// burst, and bans the client.
line := tc.refusals[3].send(s, client)
if line.LimitHit != requestlog.LimitHitMinuteRefusals ||
if line.LimitHit != requestlog.LimitHitErrorBurst ||
line.Offence != requestlog.OffenceLimit {
t.Errorf("log line has limit_hit %q and offence %q, want minute_refusals "+
t.Errorf("log line has limit_hit %q and offence %q, want error_burst "+
"and limit", line.LimitHit, line.Offence)
}
s.get(client, http.StatusForbidden, tc.nextAction)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
})
}
}
@@ -181,7 +169,7 @@ func TestErrorBurstBanNotesHistoryAndMetrics(t *testing.T) {
t.Fatalf("bans\n%+v\nwant\n%+v", got, want)
}
wantOffences := ratelimit.Offences{Limit: 1, BlockedByBlockRule: 3}
wantOffences := ratelimit.Offences{Limit: 1, RuleBlocked: 3}
if offences := historyOf(t, server, client).Offences; offences != wantOffences {
t.Errorf("history counts the offences %+v, want %+v", offences, wantOffences)
}
@@ -191,7 +179,7 @@ func TestErrorBurstBanNotesHistoryAndMetrics(t *testing.T) {
`kind="refusals",window="minute"}`, 1)
wantMetric(t, metrics, `sww_offences_total{instance="app",kind="limit"}`, 1)
wantMetric(t, metrics,
`sww_offences_total{instance="app",kind="blocked_by_block_rule"}`, 3)
`sww_offences_total{instance="app",kind="rule_blocked"}`, 3)
wantMetric(t, metrics, `sww_bans_made_total{cause="limit",instance="app"}`, 1)
}
@@ -277,8 +265,7 @@ func TestErrorBurstOffOrAtItsDefault(t *testing.T) {
line = blockRule.send(s, client)
}
broken := line.LimitHit == requestlog.LimitHitMinuteRefusals
if broken != tc.broken {
if broken := line.LimitHit == requestlog.LimitHitErrorBurst; broken != tc.broken {
t.Errorf("the 31st refusal broke the error burst: %t, want %t",
broken, tc.broken)
}
@@ -307,11 +294,11 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
blockRule.send(s, exempt)
line := blockRule.send(s, exempt)
if line.LimitHit != requestlog.LimitHitMinuteRefusals {
t.Errorf("log line has limit_hit %q, want minute_refusals", line.LimitHit)
if line.LimitHit != requestlog.LimitHitErrorBurst {
t.Errorf("log line has limit_hit %q, want error_burst", line.LimitHit)
}
s.get(exempt, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(exempt, http.StatusForbidden, requestlog.ActionBanned)
}
func TestErrorBurstBanSetsTheRefusalsBackToZero(t *testing.T) {
@@ -354,7 +341,7 @@ func TestObserveModeLogsAndAlertsTheErrorBurst(t *testing.T) {
for range 2 {
line := s.request(otherClient, blockedPath, http.StatusOK,
requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionTemporarilyBanned)
wantWouldAction(t, line, requestlog.ActionBanned)
if line.LimitHit != "" {
t.Errorf("log line has limit_hit %q, want none", line.LimitHit)
@@ -366,10 +353,10 @@ func TestObserveModeLogsAndAlertsTheErrorBurst(t *testing.T) {
s.request(client, blockedPath, http.StatusOK, requestlog.ActionForward)
line := s.request(client, blockedPath, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionBlockedByBlockRule)
wantWouldAction(t, line, requestlog.ActionRuleBlocked)
if line.LimitHit != requestlog.LimitHitMinuteRefusals || line.BanExpires != "" {
t.Errorf("log line has limit_hit %q and ban_expires %q, want minute_refusals "+
if line.LimitHit != requestlog.LimitHitErrorBurst || line.BanExpires != "" {
t.Errorf("log line has limit_hit %q and ban_expires %q, want error_burst "+
"and none", line.LimitHit, line.BanExpires)
}
+46 -56
View File
@@ -49,12 +49,12 @@ const (
func TestEachBlocklistActionForAListedAddressAndAListedNetblock(t *testing.T) {
t.Parallel()
forward, denied := requestlog.ActionForward, requestlog.ActionClientDenied
forward, denied := requestlog.ActionForward, requestlog.ActionDenied
for _, tc := range []struct {
action string
// statuses and actions are those of a listed client's three
// requests, and percent their rate_limit_percent, as percentText gives it.
// requests, and percent their limit_percent, as percentText gives it.
statuses []int
actions []string
percent string
@@ -66,7 +66,7 @@ func TestEachBlocklistActionForAListedAddressAndAListedNetblock(t *testing.T) {
{
// Half of 4 requests a minute: the third breaks the limit.
limitHalf, []int{http.StatusOK, http.StatusOK, http.StatusForbidden},
[]string{forward, forward, requestlog.ActionClientIsRateLimited},
[]string{forward, forward, requestlog.ActionRateLimited},
"50 from " + blocklistAction,
},
{
@@ -90,8 +90,8 @@ func TestEachBlocklistActionForAListedAddressAndAListedNetblock(t *testing.T) {
for i := range 3 {
line := s.get(from, tc.statuses[i], tc.actions[i])
wantReputation(t, line, dropURL)
wantPercent(t, "rate_limit_percent", line.RateLimitPercent,
line.RateLimitPercentSetting, tc.percent)
wantPercent(t, "limit_percent", line.LimitPercent,
line.LimitPercentSetting, tc.percent)
// A request refused for the list is not counted.
counted := line.fields["counts"] != nil
@@ -106,8 +106,7 @@ func TestEachBlocklistActionForAListedAddressAndAListedNetblock(t *testing.T) {
for range 3 {
line := s.get(unplaced, http.StatusOK, forward)
wantReputation(t, line)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
none)
}
@@ -163,7 +162,7 @@ func TestReputationRefusalNamesItsHeuristicAndWhatListedTheClient(t *testing.T)
s, server, _ := startWithLookups(t, tc.env)
tc.load(t, server)
line := s.get(client, http.StatusForbidden, requestlog.ActionClientDenied)
line := s.get(client, http.StatusForbidden, requestlog.ActionDenied)
wantHeuristic(t, line, tc.heuristic, tc.figures)
})
}
@@ -194,7 +193,7 @@ func TestObserveModeForwardsAClientABlocklistDeniesAndAlertsIt(t *testing.T) {
loadLists(t, server, map[string][]string{dropURL: {fromDE}})
line := s.get(fromDE, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionClientDenied)
wantWouldAction(t, line, requestlog.ActionDenied)
wantReputation(t, line, dropURL)
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
@@ -208,7 +207,7 @@ func TestBlocklistLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T)
for _, tc := range []struct {
action, asnPercent string
// want is the upload's rate_limit_percent and bytes_limit_percent, as
// want is the upload's limit_percent and bytes_percent, as
// percentText gives them, and limitHit its limit_hit.
want, limitHit string
}{
@@ -230,11 +229,9 @@ func TestBlocklistLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T)
// The upload's 100 bytes are over 49, a quarter of 199, and 99,
// half of it, and within 199.
line := s.uploadFrom(fromDE)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
tc.want)
wantPercent(t, "bytes_limit_percent",
line.BytesLimitPercent, line.BytesLimitPercentSetting,
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
tc.want)
if line.LimitHit != tc.limitHit {
@@ -256,9 +253,9 @@ func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(t *testing.T)
name string
env map[string]string
file string
// rateLimitPercent and bytesLimitPercent are the upload's, as
// percentText gives them.
rateLimitPercent, bytesLimitPercent string
// limitPercent and bytesPercent are the upload's, as percentText
// gives them.
limitPercent, bytesPercent string
}{
{"the file's alone", nil, asnDEQuarter, fromURL, fromURL},
{
@@ -291,12 +288,10 @@ func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(t *testing.T)
loadLists(t, server, map[string][]string{asnURL: {"# by AS number", tc.file}})
line := s.uploadFrom(fromDE)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
tc.rateLimitPercent)
wantPercent(t, "bytes_limit_percent",
line.BytesLimitPercent, line.BytesLimitPercentSetting,
tc.bytesLimitPercent)
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
tc.limitPercent)
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
tc.bytesPercent)
})
}
}
@@ -385,12 +380,12 @@ const (
func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
t.Parallel()
forward, denied := requestlog.ActionForward, requestlog.ActionClientDenied
forward, denied := requestlog.ActionForward, requestlog.ActionDenied
for _, tc := range []struct {
action string
// statuses and actions are those of a listed client's three
// requests, and percent their rate_limit_percent, as percentText gives it.
// requests, and percent their limit_percent, as percentText gives it.
statuses []int
actions []string
percent string
@@ -402,7 +397,7 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
{
// Half of 4 requests a minute: the third breaks the limit.
limitHalf, []int{http.StatusOK, http.StatusOK, http.StatusForbidden},
[]string{forward, forward, requestlog.ActionClientIsRateLimited},
[]string{forward, forward, requestlog.ActionRateLimited},
"50 from " + reputationAction,
},
{
@@ -427,8 +422,8 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
line := s.get(from, tc.statuses[i], tc.actions[i])
// In the order SWW_DNSBL_ZONES names them.
wantReputation(t, line, listedBy[from]...)
wantPercent(t, "rate_limit_percent", line.RateLimitPercent,
line.RateLimitPercentSetting, tc.percent)
wantPercent(t, "limit_percent", line.LimitPercent,
line.LimitPercentSetting, tc.percent)
// A request refused for the verdict is not counted.
counted := line.fields["counts"] != nil
@@ -443,8 +438,7 @@ func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
for range 3 {
line := s.get(unplaced, http.StatusOK, forward)
wantReputation(t, line)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
none)
}
@@ -474,7 +468,7 @@ func TestDNSBLZonesComeAfterTheBlocklistsAndSkipAllowNets(t *testing.T) {
// The blocklist refuses fromKP before its verdict is looked at, and
// 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),
wantReputation(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionDenied),
dropURL)
wantReputation(t, s.get(fromDE, http.StatusOK, requestlog.ActionForward))
@@ -538,7 +532,7 @@ func TestObserveModeForwardsAClientADNSBLZoneDeniesAndAlertsIt(t *testing.T) {
loadVerdicts(server, map[string][]string{fromDE: {dnsblZone}})
line := s.get(fromDE, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionClientDenied)
wantWouldAction(t, line, requestlog.ActionDenied)
wantReputation(t, line, dnsblZone)
waiting := queue.Snapshot().Waiting[alerts.DestinationWebhook]
@@ -552,7 +546,7 @@ func TestReputationLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T)
for _, tc := range []struct {
blocklistAction, reputationAction string
// want is the request's rate_limit_percent and bytes_limit_percent, as
// want is the request's limit_percent and bytes_percent, as
// percentText gives them.
want string
}{
@@ -577,11 +571,9 @@ func TestReputationLimitTakesPartInTheLowestPercentageOfEveryLimit(t *testing.T)
// Named by the blocklist, then by the zone.
line := s.get(fromDE, http.StatusOK, requestlog.ActionForward)
wantReputation(t, line, dropURL, dnsblZone)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
tc.want)
wantPercent(t, "bytes_limit_percent",
line.BytesLimitPercent, line.BytesLimitPercentSetting,
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
tc.want)
})
}
@@ -749,8 +741,8 @@ func TestOnlyAClientThatHasCommittedAnOffenceIsCheckedWithAbuseIPDB(t *testing.T
s.get(fromDE, http.StatusOK, forward)
s.get(fromDE, http.StatusOK, forward)
s.get(fromKP, http.StatusOK, forward)
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(fromDE, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited)
s.get(fromDE, http.StatusForbidden, requestlog.ActionBanned)
wantAbuseIPDBChecks(t, server, 0)
// Once the ban has ended, fromDE's first request has it checked in the
@@ -789,7 +781,7 @@ func TestIPv6ClientCostsOneAbuseIPDBCheckWhicheverOfItsAddressesSends(t *testing
s.get(addresses[0], http.StatusOK, forward)
}
s.get(addresses[0], http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(addresses[0], http.StatusForbidden, requestlog.ActionRateLimited)
clk.advance(time.Hour)
// Once the ban has ended, which set its counters back to zero, a
@@ -822,20 +814,20 @@ func TestClientRefusedForAnOffenceIsCheckedWithAbuseIPDBAtItsNextRequest(t *test
want ratelimit.Offences
}{
{
"a block rule", blockedPath, http.StatusForbidden,
requestlog.ActionBlockedByBlockRule, ratelimit.Offences{BlockedByBlockRule: 1},
"a block rule", blockedPath, http.StatusForbidden, requestlog.ActionRuleBlocked,
ratelimit.Offences{RuleBlocked: 1},
},
{
"the Core Rule Set", sqlInjection, http.StatusForbidden,
requestlog.ActionBlockedByCoreRuleSet, ratelimit.Offences{BlockedByCoreRuleSet: 1},
requestlog.ActionWAFBlocked, ratelimit.Offences{WAFBlocked: 1},
},
{
"a ban rule", probePath, http.StatusForbidden, requestlog.ActionTemporarilyBanned,
"a ban rule", probePath, http.StatusForbidden, requestlog.ActionBanned,
ratelimit.Offences{Attack: 1},
},
{
"a trap path", "/xmlrpc.php", http.StatusForbidden,
requestlog.ActionTemporarilyBanned, ratelimit.Offences{Attack: 1},
"a trap path", "/xmlrpc.php", http.StatusForbidden, requestlog.ActionBanned,
ratelimit.Offences{Attack: 1},
},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -866,12 +858,12 @@ func TestClientRefusedForAnOffenceIsCheckedWithAbuseIPDBAtItsNextRequest(t *test
func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *testing.T) {
t.Parallel()
forward, denied := requestlog.ActionForward, requestlog.ActionClientDenied
forward, denied := requestlog.ActionForward, requestlog.ActionDenied
for _, tc := range []struct {
action string
// statuses and actions are those of fromDE's three requests, and
// percent their rate_limit_percent, as percentText gives it.
// percent their limit_percent, as percentText gives it.
statuses []int
actions []string
percent string
@@ -883,7 +875,7 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *test
{
// Half of 4 requests a minute: the third breaks the limit.
limitHalf, []int{http.StatusOK, http.StatusOK, http.StatusForbidden},
[]string{forward, forward, requestlog.ActionClientIsRateLimited},
[]string{forward, forward, requestlog.ActionRateLimited},
"50 from " + reputationAction,
},
{
@@ -904,8 +896,7 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *test
for i := range 3 {
line := s.get(fromDE, tc.statuses[i], tc.actions[i])
wantReputation(t, line, abuseipdb)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
tc.percent)
// A request refused for the score is not counted.
@@ -919,8 +910,7 @@ func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(t *test
for range 3 {
line := s.get(fromKP, http.StatusOK, forward)
wantReputation(t, line)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
none)
}
@@ -982,7 +972,7 @@ func TestWithoutAnAbuseIPDBKeyNoClientIsCheckedNorAScoreUsed(t *testing.T) {
// fromDE's score is not used, and once it has committed an offence it
// is not checked either.
wantReputation(t, s.get(fromDE, http.StatusOK, forward))
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited)
clk.advance(time.Hour)
wantReputation(t, s.get(fromDE, http.StatusOK, forward))
wantAbuseIPDBChecks(t, server, 0)
@@ -1006,10 +996,10 @@ func TestBanNotesNameEachReputationSourceThatListedTheClient(t *testing.T) {
}{
{"for a broken rate limit", func(s *sender, from string) {
s.get(from, http.StatusOK, requestlog.ActionForward)
s.get(from, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(from, http.StatusForbidden, requestlog.ActionRateLimited)
}},
{"for a clear sign of attack", func(s *sender, from string) {
s.request(from, probePath, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.request(from, probePath, http.StatusForbidden, requestlog.ActionBanned)
}},
} {
t.Run(tc.name, func(t *testing.T) {
+13 -14
View File
@@ -48,16 +48,16 @@ type decision struct {
// so that not waiting never lowers its client's limits.
skippedLookupWait bool
// counted is true for a request the rate limits counted, whose bytes
// the byte limits count once it has ended. rateLimitPercent and
// bytesLimitPercent are then its client's limit percentages for the rate
// the byte limits count once it has ended. limitPercent and
// bytesPercent are then its client's limit percentages for the rate
// limits and for the byte limits.
counted bool
rateLimitPercent, bytesLimitPercent percentage
limitPercent, bytesPercent percentage
// attack is true for a request that matched a ban rule or asked for a
// trap path, blockedByBlockRule for one a block rule refused, and
// blockedByCoreRuleSet for one the Core Rule Set refused, each an offence
// its client's history counts.
attack, blockedByBlockRule, blockedByCoreRuleSet bool
// trap path, ruleBlocked for one a block rule refused, and wafBlocked for
// one the Core Rule Set refused, each an offence its client's history
// counts.
attack, ruleBlocked, wafBlocked bool
// reputation is the reputation sources that list the client, for the
// notes of a ban the request makes.
reputation []bans.ReputationHit
@@ -187,10 +187,9 @@ func (rq *request) check(ctx context.Context) *refusal {
rq.line.WouldAction = action
return nil
case action == requestlog.ActionBlockedByBlockRule ||
action == requestlog.ActionBlockedByCoreRuleSet:
case action == requestlog.ActionRuleBlocked || action == requestlog.ActionWAFBlocked:
return &refusal{status: http.StatusForbidden, action: action}
case action == requestlog.ActionRequestHeaderTooLarge:
case action == requestlog.ActionTooLarge:
return &refusal{status: http.StatusRequestHeaderFieldsTooLarge, action: action}
default:
return rq.banResponse(action)
@@ -262,8 +261,8 @@ func (rq *request) finish() {
}
duration := time.Since(rq.start)
line.DurationTotalMS = requestlog.Milliseconds(duration)
line.DurationChecksMS = timing(rq.start, rq.checked)
line.DurationTotal = requestlog.Milliseconds(duration)
line.DurationChecks = timing(rq.start, rq.checked)
// Counted before the log line is written, so that the metrics count
// every request whose line is out. An allowed request is the exception:
@@ -311,8 +310,8 @@ func (rq *request) addToHistory() {
ResponseBytes: rq.out.bytes,
BrokeLimit: rq.line.Offence == requestlog.OffenceLimit,
Attack: rq.attack,
BlockedByBlockRule: rq.blockedByBlockRule,
BlockedByCoreRuleSet: rq.blockedByCoreRuleSet,
RuleBlocked: rq.ruleBlocked,
WAFBlocked: rq.wafBlocked,
}
rq.h.limiter.AddToHistory(rq.h.clientGroup(rq.client), rq.h.now(), request)
+12 -12
View File
@@ -156,9 +156,9 @@ func wantBareLine(t *testing.T, line logLine) {
// withTimings returns want with the timings of line.
func withTimings(line logLine, want requestlog.Line) requestlog.Line {
want.DurationTotalMS = line.DurationTotalMS
want.DurationChecksMS = line.DurationChecksMS
want.DurationUpstreamTotalMS = line.DurationUpstreamTotalMS
want.DurationTotal = line.DurationTotal
want.DurationChecks = line.DurationChecks
want.DurationUpstreamTotal = line.DurationUpstreamTotal
return want
}
@@ -226,10 +226,10 @@ func TestTimingsAreInOrder(t *testing.T) {
// A request passed to the app has every timing; one refused, none of
// the app's; the health check, which runs no check, only the total.
wantTimings(t, forwarded, "duration_total_ms", "duration_checks_ms",
"duration_upstream_total_ms")
wantTimings(t, refused, "duration_total_ms", "duration_checks_ms")
wantTimings(t, health, "duration_total_ms")
wantTimings(t, forwarded, "duration_total", "duration_checks",
"duration_upstream_total")
wantTimings(t, refused, "duration_total", "duration_checks")
wantTimings(t, health, "duration_total")
if t.Failed() {
return
@@ -237,9 +237,9 @@ func TestTimingsAreInOrder(t *testing.T) {
// In whole microseconds, as they are logged, so that the sum below is
// exact.
total := microseconds(forwarded.DurationTotalMS)
checks := microseconds(*forwarded.DurationChecksMS)
upstream := microseconds(*forwarded.DurationUpstreamTotalMS)
total := microseconds(forwarded.DurationTotal)
checks := microseconds(*forwarded.DurationChecks)
upstream := microseconds(*forwarded.DurationUpstreamTotal)
// The checks end before the request is handed to the app.
if checks+upstream > total {
@@ -247,9 +247,9 @@ func TestTimingsAreInOrder(t *testing.T) {
total, checks, upstream)
}
if *refused.DurationChecksMS > refused.DurationTotalMS {
if *refused.DurationChecks > refused.DurationTotal {
t.Errorf("refused request's checks took %v of %v milliseconds",
*refused.DurationChecksMS, refused.DurationTotalMS)
*refused.DurationChecks, refused.DurationTotal)
}
}
@@ -112,7 +112,7 @@ func TestRouteWatchTagsAreOnALineDenyNetsRefused(t *testing.T) {
waitForRouteWatch(t, server, fromKP, 32)
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied)
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionDenied)
wantHeuristic(t, line, "deny_nets", `{"netblock": "`+fromKP+`/32"}`)
wantTags(t, line, "asn=64511", "prefix="+prefixKP)
}
@@ -222,7 +222,7 @@ func TestTagDenyRefusesOnARouteWatchPrefix(t *testing.T) {
waitForRouteWatch(t, server, fromKP, 32)
waitForRouteWatch(t, server, fromDE, 32)
wantHeuristic(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied),
wantHeuristic(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionDenied),
"tag_deny", `{"tag": "prefix=`+prefixKP+`"}`)
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
}
@@ -241,5 +241,5 @@ func TestTagLimitPercentWorksWithAnIPv6Prefix(t *testing.T) {
// Half of two is one: the second request breaks the limit.
line := s.get(fromV6, http.StatusOK, requestlog.ActionForward)
wantTags(t, line, "asn=64496", "prefix="+prefixV6)
s.get(fromV6, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(fromV6, http.StatusForbidden, requestlog.ActionRateLimited)
}
+7 -10
View File
@@ -16,10 +16,9 @@ import (
// ruleFiles checks the request against the rules of the rule files at now,
// notes the ids of those it matches in the log line, adds the tag
// exploitsignature when it matches any, and refuses the request
// when the last rule it matches refuses it: with ActionBlockedByBlockRule for a
// block rule, and for a ban rule with the action for the ban it makes. A ban
// rule bans the client's netblock for a clear sign of attack, or in observe
// mode raises the alert for the ban
// when the last rule it matches refuses it: with ActionRuleBlocked for a block
// rule, and ActionBanned for a ban rule. A ban rule bans the client's netblock
// for a clear sign of attack, or in observe mode raises the alert for the ban
// it would have made. Either rule's match is noted as an offence, for the
// client's history. Its figures are the rule's id and action.
type ruleFiles struct{}
@@ -46,16 +45,14 @@ func (ruleFiles) check(_ context.Context, rq *request, now time.Time) *verdict {
switch last.Action {
case rules.ActionBlock:
rq.blockedByBlockRule = true
rq.ruleBlocked = true
return &verdict{action: requestlog.ActionBlockedByBlockRule, figures: figures}
return &verdict{action: requestlog.ActionRuleBlocked, figures: figures}
case rules.ActionBan:
rq.attack = true
rq.banForAttack(now, bans.Notes{RuleID: last.ID, Target: last.Target})
return &verdict{
action: rq.banForAttack(now, bans.Notes{RuleID: last.ID, Target: last.Target}),
figures: figures,
}
return &verdict{action: requestlog.ActionBanned, figures: figures}
default:
return nil
}
+18 -24
View File
@@ -37,14 +37,13 @@ func TestEachRuleAction(t *testing.T) {
// A block rule refuses with 403, whatever SWW_BAN_RESPONSE is, and
// bans no one.
line = s.request(client, "/blocked", http.StatusForbidden,
requestlog.ActionBlockedByBlockRule)
requestlog.ActionRuleBlocked)
wantRuleIDs(t, line, "noted", "blocked")
s.get(client, http.StatusOK, requestlog.ActionForward)
// 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)
line = s.request(client, "/.env", http.StatusTooManyRequests, requestlog.ActionBanned)
wantRuleIDs(t, line, "noted", "probe")
if line.BanExpires != requestlog.FormatTime(start.Add(7*24*time.Hour)) {
@@ -83,7 +82,7 @@ func TestEachRuleAction(t *testing.T) {
// against the rules, and makes the ban permanent.
clk.advance(time.Hour)
line = s.get(client, http.StatusTooManyRequests, requestlog.ActionPermanentlyBanned)
line = s.get(client, http.StatusTooManyRequests, requestlog.ActionBanned)
wantRuleIDs(t, line)
if line.BanExpires != permanent {
@@ -91,7 +90,7 @@ func TestEachRuleAction(t *testing.T) {
}
clk.advance(365 * 24 * time.Hour)
s.get(client, http.StatusTooManyRequests, requestlog.ActionPermanentlyBanned)
s.get(client, http.StatusTooManyRequests, requestlog.ActionBanned)
}
func TestEachRuleActionAddsTheExploitSignatureTag(t *testing.T) {
@@ -104,7 +103,7 @@ func TestEachRuleActionAddsTheExploitSignatureTag(t *testing.T) {
wantTags(t, s.request(client, "/noted", http.StatusOK, requestlog.ActionForward),
"exploitsignature")
wantTags(t, s.request(client, "/blocked", http.StatusForbidden,
requestlog.ActionBlockedByBlockRule), "exploitsignature")
requestlog.ActionRuleBlocked), "exploitsignature")
wantTags(t, s.get(client, http.StatusOK, requestlog.ActionForward))
}
@@ -116,11 +115,10 @@ func TestRuleRefusalNamesItsHeuristicAndTheRule(t *testing.T) {
})
line := s.request(client, "/blocked", http.StatusForbidden,
requestlog.ActionBlockedByBlockRule)
requestlog.ActionRuleBlocked)
wantHeuristic(t, line, "rule_files", `{"rule_id": "blocked", "rule_action": "block"}`)
line = s.request(client, "/.env", http.StatusForbidden,
requestlog.ActionTemporarilyBanned)
line = s.request(client, "/.env", http.StatusForbidden, requestlog.ActionBanned)
wantHeuristic(t, line, "rule_files", `{"rule_id": "probe", "rule_action": "ban"}`)
}
@@ -133,8 +131,7 @@ func TestNextClearSignOfAttackAfterABanBansPermanently(t *testing.T) {
})
// The first probe bans for SWW_ATTACK_BAN_DURATION.
line := s.request(client, "/.env", http.StatusForbidden,
requestlog.ActionTemporarilyBanned)
line := s.request(client, "/.env", http.StatusForbidden, requestlog.ActionBanned)
if line.BanExpires != requestlog.FormatTime(clk.Now().Add(time.Hour)) {
t.Errorf("log line has ban_expires %q, want an hour on", line.BanExpires)
}
@@ -144,8 +141,7 @@ func TestNextClearSignOfAttackAfterABanBansPermanently(t *testing.T) {
clk.advance(time.Hour)
s.get(client, http.StatusOK, requestlog.ActionForward)
line = s.request(client, "/.env", http.StatusForbidden,
requestlog.ActionPermanentlyBanned)
line = s.request(client, "/.env", http.StatusForbidden, requestlog.ActionBanned)
if line.BanExpires != permanent {
t.Errorf("log line has ban_expires %q, want permanent", line.BanExpires)
}
@@ -173,8 +169,7 @@ func TestRulesComeAfterTheOtherChecks(t *testing.T) {
// A probe over the rate limit breaks the limit before any rule sees
// it.
s.get(client, http.StatusOK, requestlog.ActionForward)
line = s.request(client, "/.env", http.StatusForbidden,
requestlog.ActionClientIsRateLimited)
line = s.request(client, "/.env", http.StatusForbidden, requestlog.ActionRateLimited)
wantRuleIDs(t, line)
limitBan := server.Ledger.Bans(netip.MustParsePrefix(client + "/32"))
@@ -184,7 +179,7 @@ func TestRulesComeAfterTheOtherChecks(t *testing.T) {
// A client the rate limits do not apply to is still checked.
s.get(exempt, http.StatusOK, requestlog.ActionForward)
s.request(exempt, "/.env", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.request(exempt, "/.env", http.StatusForbidden, requestlog.ActionBanned)
}
func TestObserveModeLogsWhatTheRulesWouldDo(t *testing.T) {
@@ -196,11 +191,11 @@ func TestObserveModeLogsWhatTheRulesWouldDo(t *testing.T) {
})
line := s.request(client, "/blocked", http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionBlockedByBlockRule)
wantWouldAction(t, line, requestlog.ActionRuleBlocked)
wantRuleIDs(t, line, "blocked")
line = s.request(client, "/.env", http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionTemporarilyBanned)
wantWouldAction(t, line, requestlog.ActionBanned)
wantRuleIDs(t, line, "probe")
if line.BanExpires != "" {
@@ -224,10 +219,9 @@ func TestMetricsCountRuleMatchesAndBansForAnAttack(t *testing.T) {
metricsToken: token,
})
s.request(client, "/blocked", http.StatusForbidden,
requestlog.ActionBlockedByBlockRule)
s.request(client, "/.env", http.StatusForbidden, requestlog.ActionTemporarilyBanned)
s.get(client, http.StatusForbidden, requestlog.ActionPermanentlyBanned)
s.request(client, "/blocked", http.StatusForbidden, requestlog.ActionRuleBlocked)
s.request(client, "/.env", http.StatusForbidden, requestlog.ActionBanned)
s.get(client, http.StatusForbidden, requestlog.ActionBanned)
metrics := s.scrape()
wantMetric(t, metrics,
@@ -235,10 +229,10 @@ func TestMetricsCountRuleMatchesAndBansForAnAttack(t *testing.T) {
wantMetric(t, metrics,
`sww_rule_matches_total{action="ban",instance="app",rule_id="probe"}`, 1)
wantMetric(t, metrics, `sww_rules_loaded{instance="app"}`, 2)
wantMetric(t, metrics, `sww_requests_total{action="blocked_by_block_rule",`+
wantMetric(t, metrics, `sww_requests_total{action="rule_blocked",`+
`instance="app",status_class="4xx"}`, 1)
wantMetric(t, metrics,
`sww_offences_total{instance="app",kind="blocked_by_block_rule"}`, 1)
`sww_offences_total{instance="app",kind="rule_blocked"}`, 1)
wantMetric(t, metrics, `sww_offences_total{instance="app",kind="attack"}`, 1)
wantMetric(t, metrics, `sww_bans_made_total{cause="attack",instance="app"}`, 1)
wantMetric(t, metrics, `sww_bans_made_total{cause="limit",instance="app"}`, 0)
+8 -8
View File
@@ -68,7 +68,7 @@ func TestRequestFromAllowNetsIsNotCounted(t *testing.T) {
{listedAddr, http.StatusOK, requestlog.ActionForward},
{listedAddr, http.StatusOK, requestlog.ActionForward},
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
{unlistedAddr, http.StatusForbidden, requestlog.ActionClientIsRateLimited},
{unlistedAddr, http.StatusForbidden, requestlog.ActionRateLimited},
})
}
@@ -92,7 +92,7 @@ func TestDenyNetsRefuseBeforeTheLookup(t *testing.T) {
wantStatus(t, do(t, req), http.StatusForbidden)
line := out.endedLine(t)
wantLine(t, line, http.StatusForbidden, requestlog.ActionClientDenied)
wantLine(t, line, http.StatusForbidden, requestlog.ActionDenied)
if len(asked()) != 0 {
t.Errorf("GeoJS was asked about %v, want nothing", asked())
@@ -108,7 +108,7 @@ func TestDenyNetsRefusalNamesItsHeuristicAndNetblock(t *testing.T) {
s, _, _ := startWithClock(t, "", map[string]string{denyNets: clientsNet})
line := s.get(client, http.StatusForbidden, requestlog.ActionClientDenied)
line := s.get(client, http.StatusForbidden, requestlog.ActionDenied)
wantHeuristic(t, line, "deny_nets", `{"netblock": "`+clientsNet+`"}`)
}
@@ -125,10 +125,10 @@ func TestRequestRefusedByDenyNetsIsNotCounted(t *testing.T) {
// listedAddr's refused requests are not counted, so the first request
// from unlistedAddr is within the limit of one a minute.
wantAnswers(t, addr, out, []sentRequest{
{listedAddr, http.StatusForbidden, requestlog.ActionClientDenied},
{listedAddr, http.StatusForbidden, requestlog.ActionClientDenied},
{listedAddr, http.StatusForbidden, requestlog.ActionDenied},
{listedAddr, http.StatusForbidden, requestlog.ActionDenied},
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
{unlistedAddr, http.StatusForbidden, requestlog.ActionClientIsRateLimited},
{unlistedAddr, http.StatusForbidden, requestlog.ActionRateLimited},
})
}
@@ -152,8 +152,8 @@ func TestRateLimitExemptNetsAreNeitherCountedNorRefused(t *testing.T) {
{listedAddr, http.StatusOK, requestlog.ActionForward},
{listedAddr, http.StatusOK, requestlog.ActionForward},
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
{unlistedAddr, http.StatusForbidden, requestlog.ActionClientIsRateLimited},
{fromKP, http.StatusForbidden, requestlog.ActionClientDenied},
{unlistedAddr, http.StatusForbidden, requestlog.ActionRateLimited},
{fromKP, http.StatusForbidden, requestlog.ActionDenied},
})
}
+1 -1
View File
@@ -20,7 +20,7 @@ func (tagDeny) check(_ context.Context, rq *request, _ time.Time) *verdict {
for _, tag := range rq.line.Tags {
if slices.Contains(rq.h.config.TagDeny, tag) {
return &verdict{
action: requestlog.ActionClientDenied,
action: requestlog.ActionDenied,
figures: map[string]any{"tag": tag},
}
}
+9 -12
View File
@@ -14,7 +14,7 @@ func TestTagDenyRefusesALookupTagAndNamesTheTag(t *testing.T) {
// in capitals.
s, _, _ := startWithLookups(t, map[string]string{tagDeny: "Country=kp"})
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied)
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionDenied)
wantHeuristic(t, line, "tag_deny", `{"tag": "country=KP"}`)
wantTags(t, line, "asn=64511", "country=KP")
@@ -26,7 +26,7 @@ func TestTagDenyRefusesAnASNTag(t *testing.T) {
s, _, _ := startWithLookups(t, map[string]string{tagDeny: "asn=64511"})
wantHeuristic(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied),
wantHeuristic(t, s.get(fromKP, http.StatusForbidden, requestlog.ActionDenied),
"tag_deny", `{"tag": "asn=64511"}`)
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
}
@@ -41,7 +41,7 @@ func TestTagDenyRefusesAListTagBeforeTheRateLimitsCount(t *testing.T) {
// Each is refused for its tag: none is counted to break the limit of one.
for range 3 {
wantHeuristic(t, s.get(fromDE, http.StatusForbidden, requestlog.ActionClientDenied),
wantHeuristic(t, s.get(fromDE, http.StatusForbidden, requestlog.ActionDenied),
"tag_deny", `{"tag": "torexit"}`)
}
}
@@ -58,7 +58,7 @@ func TestTagDenyRefusesAMixedCaseZoneByItsLowerCaseTag(t *testing.T) {
})
loadVerdicts(server, map[string][]string{fromDE: {mixedCaseZone}})
wantHeuristic(t, s.get(fromDE, http.StatusForbidden, requestlog.ActionClientDenied),
wantHeuristic(t, s.get(fromDE, http.StatusForbidden, requestlog.ActionDenied),
"tag_deny", `{"tag": "dnsbl=`+dnsblZone+`"}`)
}
@@ -69,7 +69,7 @@ func TestTagDenyRefusesAnExploitSignatureOfARuleThatOnlyLogs(t *testing.T) {
rulesDir: writeRules(t, "noted path log ^/\n"), tagDeny: "exploitsignature",
})
line := s.get(client, http.StatusForbidden, requestlog.ActionClientDenied)
line := s.get(client, http.StatusForbidden, requestlog.ActionDenied)
wantHeuristic(t, line, "tag_deny", `{"tag": "exploitsignature"}`)
wantTags(t, line, "exploitsignature")
}
@@ -87,9 +87,8 @@ func TestTagLimitPercentLowersALimitTheLowestWinning(t *testing.T) {
// A quarter of 4 requests a minute: the second breaks the limit.
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
line := s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting,
line := s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited)
wantPercent(t, "limit_percent", line.LimitPercent, line.LimitPercentSetting,
"25 from "+tagLimitPercent)
}
@@ -109,8 +108,7 @@ func TestTagLimitPercentLowersAByteLimitByAListTag(t *testing.T) {
t.Errorf("log line has limit_hit %q, want %s", line.LimitHit, minuteBytes)
}
wantPercent(t, "bytes_limit_percent",
line.BytesLimitPercent, line.BytesLimitPercentSetting,
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting,
"50 from "+tagLimitPercent)
// fromKP, which the list does not hold, has the whole limit.
@@ -119,6 +117,5 @@ func TestTagLimitPercentLowersAByteLimitByAListTag(t *testing.T) {
t.Errorf("log line for %s has limit_hit %q, want none", fromKP, line.LimitHit)
}
wantPercent(t, "bytes_limit_percent",
line.BytesLimitPercent, line.BytesLimitPercentSetting, none)
wantPercent(t, "bytes_percent", line.BytesPercent, line.BytesPercentSetting, none)
}
+3 -3
View File
@@ -80,7 +80,7 @@ func TestTagListsTagARequestRefusedBeforeTheLookup(t *testing.T) {
})
loadLists(t, server, map[string][]string{vpnList: {fromDE, fromKP}})
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionClientDenied)
line := s.get(fromKP, http.StatusForbidden, requestlog.ActionDenied)
if line.Heuristic != "deny_nets" {
t.Errorf("log line has heuristic %q, want deny_nets", line.Heuristic)
}
@@ -89,9 +89,9 @@ func TestTagListsTagARequestRefusedBeforeTheLookup(t *testing.T) {
// The second request breaks the limit of one a minute, which bans the client.
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
s.get(fromDE, http.StatusForbidden, requestlog.ActionClientIsRateLimited)
s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited)
line = s.get(fromDE, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
line = s.get(fromDE, http.StatusForbidden, requestlog.ActionBanned)
if line.Heuristic != "active_ban" {
t.Errorf("log line has heuristic %q, want active_ban", line.Heuristic)
}
+3 -1
View File
@@ -8,6 +8,7 @@ import (
"time"
"sneak.berlin/go/smallwebwaf/internal/bans"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rules"
)
@@ -29,9 +30,10 @@ func (trapPaths) check(_ context.Context, rq *request, now time.Time) *verdict {
}
rq.attack = true
rq.banForAttack(now, bans.Notes{TrapPath: path})
return &verdict{
action: rq.banForAttack(now, bans.Notes{TrapPath: path}),
action: requestlog.ActionBanned,
figures: map[string]any{"path": path},
}
}
+6 -8
View File
@@ -43,7 +43,7 @@ func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
// The query is not part of the path.
line := s.request(client, "/wp-login.php?redirect_to=x", http.StatusTooManyRequests,
requestlog.ActionTemporarilyBanned)
requestlog.ActionBanned)
wantRuleIDs(t, line)
if line.BanExpires != requestlog.FormatTime(start.Add(7*24*time.Hour)) {
@@ -77,7 +77,7 @@ func TestTrapPathBansAsABanRuleDoes(t *testing.T) {
}
// The next request is refused under the ban, and makes it permanent.
line = s.get(client, http.StatusTooManyRequests, requestlog.ActionPermanentlyBanned)
line = s.get(client, http.StatusTooManyRequests, requestlog.ActionBanned)
if line.BanExpires != permanent {
t.Errorf("log line has ban_expires %q, want permanent", line.BanExpires)
}
@@ -89,7 +89,7 @@ func TestTrapPathRefusalNamesItsHeuristicAndPath(t *testing.T) {
s, _, _ := startWithClock(t, "", map[string]string{trapPaths: trapPathList})
line := s.request(client, "/wp-login.php?redirect_to=x", http.StatusForbidden,
requestlog.ActionTemporarilyBanned)
requestlog.ActionBanned)
wantHeuristic(t, line, "trap_paths", `{"path": "/wp-login.php"}`)
}
@@ -100,8 +100,7 @@ func TestTrapPathMatchesThePathDecodedOnce(t *testing.T) {
// %2577 decodes once to %77, not to w.
s.request(otherClient, "/%2577p-login.php", http.StatusOK, requestlog.ActionForward)
s.request(client, "/%77p-login.php", http.StatusForbidden,
requestlog.ActionTemporarilyBanned)
s.request(client, "/%77p-login.php", http.StatusForbidden, requestlog.ActionBanned)
// The ban's notes give the path as the client sent it.
got := server.Ledger.Bans(netip.MustParsePrefix(client + "/32"))
@@ -118,8 +117,7 @@ func TestTrapPathsNeedNoRuleFiles(t *testing.T) {
"SWW_RULES_ENABLED": "false",
})
s.request(client, "/xmlrpc.php", http.StatusForbidden,
requestlog.ActionTemporarilyBanned)
s.request(client, "/xmlrpc.php", http.StatusForbidden, requestlog.ActionBanned)
}
func TestObserveModeLogsWhatATrapPathWouldDo(t *testing.T) {
@@ -131,7 +129,7 @@ func TestObserveModeLogsWhatATrapPathWouldDo(t *testing.T) {
})
line := s.request(client, "/xmlrpc.php", http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionTemporarilyBanned)
wantWouldAction(t, line, requestlog.ActionBanned)
// No ban was made.
s.get(client, http.StatusOK, requestlog.ActionForward)
+3 -3
View File
@@ -198,7 +198,7 @@ func (g *GeoJS) LookUp(ctx context.Context, client netip.Prefix) Answer {
answer, found := g.kept(client)
if !found {
g.metrics.RequestsWithoutGeoJSAnswer.Inc()
g.metrics.GeoJSUnanswered.Inc()
}
w, waiting := g.waiting[client]
@@ -296,7 +296,7 @@ func (g *GeoJS) answerOrWait(
}
if w == nil {
g.metrics.RequestsWithoutGeoJSAnswer.Inc()
g.metrics.GeoJSUnanswered.Inc()
return Answer{}, nil // too many clients wait already
}
@@ -307,7 +307,7 @@ func (g *GeoJS) answerOrWait(
}
if w.late {
g.metrics.RequestsWithoutGeoJSAnswer.Inc()
g.metrics.GeoJSUnanswered.Inc()
return Answer{}, nil
}
+8 -8
View File
@@ -362,7 +362,7 @@ func TestWithoutWaitTheRequestGoesOnAtOnceAndTheAnswerIsGivenWhenItComes(
mu.Unlock()
wantCountry(t, g, client, germany)
wantRequestsWithoutGeoJSAnswer(t, m, 0)
wantUnanswered(t, m, 0)
})
}
@@ -596,7 +596,7 @@ func TestClientsWithoutAnAnswerAreCounted(t *testing.T) {
// GeoJS fails, so the first client goes without an answer, and GeoJS
// is left alone for a second, which does not pass in this test.
wantCountry(t, g, clients(), "")
wantRequestsWithoutGeoJSAnswer(t, m, 1)
wantUnanswered(t, m, 1)
// Meanwhile each new client goes without one at once, while there is
// room for it among the 10,000 that may wait.
@@ -604,11 +604,11 @@ func TestClientsWithoutAnAnswerAreCounted(t *testing.T) {
wantCountry(t, g, clients(), "")
}
wantRequestsWithoutGeoJSAnswer(t, m, 10000)
wantUnanswered(t, m, 10000)
// One more, for which there is no room, goes without one too.
wantCountry(t, g, clients(), "")
wantRequestsWithoutGeoJSAnswer(t, m, 10001)
wantUnanswered(t, m, 10001)
})
}
@@ -841,12 +841,12 @@ func wantAsked(t *testing.T, geojs *standIn, i int, want ...string) {
}
}
// wantRequestsWithoutGeoJSAnswer checks how many requests m counts as having
// gone without an answer from GeoJS.
func wantRequestsWithoutGeoJSAnswer(t *testing.T, m *metrics.Metrics, want float64) {
// wantUnanswered checks how many requests m counts as having gone without
// an answer from GeoJS.
func wantUnanswered(t *testing.T, m *metrics.Metrics, want float64) {
t.Helper()
got := testutil.ToFloat64(m.RequestsWithoutGeoJSAnswer)
got := testutil.ToFloat64(m.GeoJSUnanswered)
if got != want {
t.Errorf("%v requests went without an answer, want %v", got, want)
}
+18 -18
View File
@@ -36,7 +36,7 @@ type Metrics struct {
upstreamDuration prometheus.Histogram
rateLimitHits *prometheus.CounterVec
offences *prometheus.CounterVec
coreRuleSetMatches *prometheus.CounterVec
wafMatches *prometheus.CounterVec
// ruleMatches are made by AddRules, and reputationHits by
// AddReputation.
ruleMatches *prometheus.CounterVec
@@ -45,12 +45,12 @@ type Metrics struct {
asns *busiest
// GeoJSRequests are the requests to GeoJS, and GeoJSFailures those
// that failed. RequestsWithoutGeoJSAnswer are the requests that needed
// their client's answer, for a setting that acts on it, and went on
// without it because GeoJS had not given it in time.
// that failed. GeoJSUnanswered are the requests that needed their
// client's answer, for a setting that acts on it, and went on without
// it because GeoJS had not given it in time.
GeoJSRequests prometheus.Counter
GeoJSFailures prometheus.Counter
RequestsWithoutGeoJSAnswer prometheus.Counter
GeoJSUnanswered prometheus.Counter
stateFileWrites *prometheus.CounterVec
stateFileWriteFailures *prometheus.CounterVec
@@ -101,7 +101,7 @@ func New(topN int, instanceName string) *Metrics {
[]string{"window", "kind"}),
offences: counterVec("sww_offences_total",
"Offences, by kind.", []string{"kind"}),
coreRuleSetMatches: counterVec("sww_core_rule_set_matches_total",
wafMatches: counterVec("sww_waf_matches_total",
"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),
@@ -114,8 +114,8 @@ func New(topN int, instanceName string) *Metrics {
Name: "sww_geojs_failures_total",
Help: "Requests to GeoJS that failed.",
}),
RequestsWithoutGeoJSAnswer: prometheus.NewCounter(prometheus.CounterOpts{
Name: "sww_requests_without_geojs_answer_total",
GeoJSUnanswered: prometheus.NewCounter(prometheus.CounterOpts{
Name: "sww_geojs_unanswered_total",
Help: "Requests that needed their client's answer from GeoJS and " +
"went on without it, because GeoJS had not given it in time.",
}),
@@ -139,9 +139,9 @@ func New(topN int, instanceName string) *Metrics {
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
m.inFlight, m.requests, m.requestBytes, m.responseBytes,
m.requestDuration, m.upstreamDuration,
m.rateLimitHits, m.offences, m.coreRuleSetMatches,
m.rateLimitHits, m.offences, m.wafMatches,
m.countries, m.asns,
m.GeoJSRequests, m.GeoJSFailures, m.RequestsWithoutGeoJSAnswer,
m.GeoJSRequests, m.GeoJSFailures, m.GeoJSUnanswered,
m.stateFileWrites, m.stateFileWriteFailures,
m.stateFileLastWrite, m.stateFileSize,
m.stateFileEditsTakenIn, m.stateFileEditsSetAside,
@@ -232,7 +232,7 @@ func (m *Metrics) AddRemoteLog(remote *remotelog.Sender) {
return float64(remote.Dropped())
}),
prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Name: "sww_remote_log_lines_waiting",
Name: "sww_remote_log_buffer_depth",
Help: "Log lines in the buffer, waiting to be sent.",
}, func() float64 {
return float64(remote.Depth())
@@ -355,11 +355,11 @@ func (m *Metrics) AddAlerts(queue *alerts.Queue) {
return float64(queue.Counts(name).Sent)
}),
prometheus.NewCounterFunc(prometheus.CounterOpts{
Name: "sww_alert_send_failures_total",
Name: "sww_alerts_failed_total",
Help: "Requests to the destination that failed.",
ConstLabels: destination,
}, func() float64 {
return float64(queue.Counts(name).SendFailures)
return float64(queue.Counts(name).Failed)
}),
prometheus.NewCounterFunc(prometheus.CounterOpts{
Name: "sww_alerts_suppressed_total",
@@ -426,8 +426,8 @@ func (m *Metrics) Offences(r ratelimit.Request) {
for kind, committed := range map[string]bool{
"limit": r.BrokeLimit,
"attack": r.Attack,
"blocked_by_block_rule": r.BlockedByBlockRule,
"blocked_by_core_rule_set": r.BlockedByCoreRuleSet,
"rule_blocked": r.RuleBlocked,
"waf_blocked": r.WAFBlocked,
} {
if committed {
m.offences.WithLabelValues(kind).Inc()
@@ -441,10 +441,10 @@ func (m *Metrics) RuleMatched(id, action string) {
m.ruleMatches.WithLabelValues(id, action).Inc()
}
// CoreRuleSetMatched counts a request that matched the Core Rule Set's rule id,
// WAFMatched counts a request that matched the Core Rule Set's rule id,
// with SWW_WAF_MODE at mode.
func (m *Metrics) CoreRuleSetMatched(mode string, id int) {
m.coreRuleSetMatches.WithLabelValues(mode, strconv.Itoa(id)).Inc()
func (m *Metrics) WAFMatched(mode string, id int) {
m.wafMatches.WithLabelValues(mode, strconv.Itoa(id)).Inc()
}
// StateFileWritten counts a write of the state file name, of size bytes,
+12 -13
View File
@@ -122,13 +122,12 @@ type Responses struct {
type Offences struct {
// Limit is its requests that broke a rate limit, a byte limit or the
// error burst, Attack those that were a clear sign of attack, a match
// of a ban rule or a request for a trap path, BlockedByBlockRule those a
// block rule refused, and BlockedByCoreRuleSet those the Core Rule Set
// refused.
// of a ban rule or a request for a trap path, RuleBlocked those a block
// rule refused, and WAFBlocked those the Core Rule Set refused.
Limit int64 `json:"limit"`
Attack int64 `json:"attack"`
BlockedByBlockRule int64 `json:"blocked_by_block_rule"`
BlockedByCoreRuleSet int64 `json:"blocked_by_core_rule_set"`
RuleBlocked int64 `json:"rule_blocked"`
WAFBlocked int64 `json:"waf_blocked"`
// TokenRefused is what a clients.json written by an earlier version gives,
// which must still load. Nothing adds to it.
TokenRefused int64 `json:"token_refused,omitempty"`
@@ -148,12 +147,12 @@ type Request struct {
ResponseBytes int64
// BrokeLimit is true for a request that broke a rate limit, a byte
// limit or the error burst, Attack for one that matched a ban rule or
// asked for a trap path, BlockedByBlockRule for one a block rule refused,
// and BlockedByCoreRuleSet for one the Core Rule Set refused.
// asked for a trap path, RuleBlocked for one a block rule refused, and
// WAFBlocked for one the Core Rule Set refused.
BrokeLimit bool
Attack bool
BlockedByBlockRule bool
BlockedByCoreRuleSet bool
RuleBlocked bool
WAFBlocked bool
}
// New returns a Limiter for limits, with no client counted yet, whose
@@ -324,12 +323,12 @@ func (l *Limiter) AddToHistory(client netip.Prefix, now time.Time, r Request) {
h.Offences.Attack++
}
if r.BlockedByBlockRule {
h.Offences.BlockedByBlockRule++
if r.RuleBlocked {
h.Offences.RuleBlocked++
}
if r.BlockedByCoreRuleSet {
h.Offences.BlockedByCoreRuleSet++
if r.WAFBlocked {
h.Offences.WAFBlocked++
}
}
+54 -60
View File
@@ -20,44 +20,39 @@ const (
// the app.
ActionForward = "forward"
// ActionRequestHeaderTooLarge is a request over the header limits.
ActionRequestHeaderTooLarge = "request_header_too_large"
// ActionTooLarge is a request over the header limits.
ActionTooLarge = "too_large"
// ActionClientIsRateLimited is a request refused because it took its
// client over a rate limit, which bans the client.
ActionClientIsRateLimited = "client_is_rate_limited"
// ActionRateLimited is a request refused because it took its client
// over a rate limit, which bans the client.
ActionRateLimited = "rate_limited"
// ActionTemporarilyBanned is a request refused because a ban that runs
// out covers its client, or because it matched a ban rule, asked for a
// trap path or the CrowdSec decision list lists its client, and so made
// such a ban.
ActionTemporarilyBanned = "temporarily_banned"
// ActionBanned is a request refused because a ban covers its client,
// or because it matched a ban rule, asked for a trap path or the
// CrowdSec decision list lists its client, each of which bans the
// client.
ActionBanned = "banned"
// ActionPermanentlyBanned is a request refused because a ban that never
// runs out covers its client, or because the request made such a ban or
// made its client's ban permanent.
ActionPermanentlyBanned = "permanently_banned"
// ActionRuleBlocked is a request refused because it matched a block
// rule.
ActionRuleBlocked = "rule_blocked"
// ActionBlockedByBlockRule is a request refused because it matched a
// block rule.
ActionBlockedByBlockRule = "blocked_by_block_rule"
// ActionWAFBlocked is a request refused because the Core Rule Set
// scored it at or over SWW_WAF_ANOMALY_THRESHOLD.
ActionWAFBlocked = "waf_blocked"
// ActionBlockedByCoreRuleSet is a request refused because the Core Rule
// Set scored it at or over SWW_WAF_ANOMALY_THRESHOLD.
ActionBlockedByCoreRuleSet = "blocked_by_core_rule_set"
// ActionClientDenied is a request refused because its client is in
// ActionDenied is a request refused because its client is in
// SWW_DENY_NETS, in a blocklist while SWW_BLOCKLIST_ACTION is deny,
// or listed by a DNSBL zone, or scored a hit by AbuseIPDB, while
// SWW_REPUTATION_ACTION is deny, or that carries a tag in SWW_TAG_DENY.
ActionClientDenied = "client_denied"
ActionDenied = "denied"
// ActionCountryDenied is a request refused because its client's country
// is not in SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
ActionCountryDenied = "country_denied"
// ActionOwnEndpoint is a request discriminator answered at one of its own
// ActionAdmin is a request discriminator answered at one of its own
// endpoints, or a question about a path under /_sww/.
ActionOwnEndpoint = "own_endpoint"
ActionAdmin = "admin"
)
// OffenceLimit is the offence a request line names for a request that
@@ -65,9 +60,9 @@ const (
// limit.
const OffenceLimit = "limit"
// LimitHitMinuteRefusals is the limit_hit a request line names for a request
// LimitHitErrorBurst is the limit_hit a request line names for a request
// that broke the error burst.
const LimitHitMinuteRefusals = "minute_refusals"
const LimitHitErrorBurst = "error_burst"
// The discriminator_line a request line names, which says which of
// discriminator's two lines about a question it is.
@@ -139,11 +134,10 @@ type Line struct {
// The decision.
Action string `json:"action"`
// WouldAction is, in observe mode, the action enforce mode would have
// taken with a request it would have refused: ActionClientDenied,
// ActionTemporarilyBanned, ActionPermanentlyBanned, ActionCountryDenied,
// ActionClientIsRateLimited, ActionBlockedByBlockRule,
// ActionBlockedByCoreRuleSet, or, in discriminator,
// ActionRequestHeaderTooLarge for the header limits.
// taken with a request it would have refused: ActionDenied,
// ActionBanned, ActionCountryDenied, ActionRateLimited,
// ActionRuleBlocked, ActionWAFBlocked, or, in discriminator, ActionTooLarge
// for the header limits.
WouldAction string `json:"would_action,omitempty"`
// Heuristic is the name of the heuristic that refused the request, in
// observe mode too, and HeuristicFigures are the figures it acted on, by
@@ -155,15 +149,15 @@ type Line struct {
// request.
AfterAnswerHeuristic string `json:"after_answer_heuristic,omitempty"`
AfterAnswerHeuristicFigures map[string]any `json:"after_answer_heuristic_figures,omitempty"` //nolint:lll // a struct tag cannot be split
// RateLimitPercent and RateLimitPercentSetting are, for a request the
// rate limits counted whose client a biased threshold gives a percentage
// of the rate limits below 100, that percentage and the setting that gave
// it. BytesLimitPercent and BytesLimitPercentSetting are the same for the
// byte limits.
RateLimitPercent *int64 `json:"rate_limit_percent,omitempty"`
RateLimitPercentSetting string `json:"rate_limit_percent_setting,omitempty"`
BytesLimitPercent *int64 `json:"bytes_limit_percent,omitempty"`
BytesLimitPercentSetting string `json:"bytes_limit_percent_setting,omitempty"`
// LimitPercent and LimitPercentSetting are, for a request the rate
// limits counted whose client a biased threshold gives a percentage of
// the rate limits below 100, that percentage and the setting that gave
// it. BytesPercent and BytesPercentSetting are the same for the byte
// limits.
LimitPercent *int64 `json:"limit_percent,omitempty"`
LimitPercentSetting string `json:"limit_percent_setting,omitempty"`
BytesPercent *int64 `json:"bytes_percent,omitempty"`
BytesPercentSetting string `json:"bytes_percent_setting,omitempty"`
// Counts are, for a request the rate limits counted, the client's
// requests as they counted them with this one, and its bytes as the
// byte limits counted them, with this request's once it has ended if
@@ -171,15 +165,15 @@ type Line struct {
Counts ratelimit.Counts `json:"counts,omitzero"`
// RuleIDs are the ids of the rule file rules the request matched.
RuleIDs []string `json:"rule_ids,omitempty"`
// CoreRuleSetRuleIDs are the ids of the Core Rule Set's rules the request
// matched, and CoreRuleSetScore its anomaly score, nil for a request the
// Core Rule Set did not inspect.
CoreRuleSetRuleIDs []int `json:"core_rule_set_rule_ids,omitempty"`
CoreRuleSetScore *int `json:"core_rule_set_score,omitempty"`
// WAFRuleIDs are the ids of the Core Rule Set's rules the request
// matched, and WAFScore its anomaly score, nil for a request the Core
// Rule Set did not inspect.
WAFRuleIDs []int `json:"waf_rule_ids,omitempty"`
WAFScore *int `json:"waf_score,omitempty"`
// LimitHit is the window whose limit the request went over, named as
// Counts names its count: minute, hour or day for a rate limit, and
// minute_bytes, hour_bytes or day_bytes for a byte limit; or
// LimitHitMinuteRefusals for the error burst.
// LimitHitErrorBurst for the error burst.
LimitHit string `json:"limit_hit,omitempty"`
// Reputation are the URLs of the blocklists that list the client, then
// that of the CrowdSec decision list when it does, then the DNSBL zones
@@ -198,15 +192,15 @@ type Line struct {
// ends: a time, or "permanent".
BanExpires string `json:"ban_expires,omitempty"`
// The timings. DurationChecksMS is the time until the checks were done, and
// DurationCoreRuleSetMS the part of it the Core Rule Set took.
// DurationUpstreamTotalMS is what the report of an allowed request gives,
// The timings, in milliseconds. DurationChecks is the time until the
// checks were done, and DurationWAF the part of it the Core Rule Set
// took. DurationUpstreamTotal is what the report of an allowed request gives,
// from the hand-over to the app until the end of its answer. Each but
// DurationTotalMS is nil for a request that did not get that far.
DurationTotalMS float64 `json:"duration_total_ms"`
DurationChecksMS *float64 `json:"duration_checks_ms,omitempty"`
DurationCoreRuleSetMS *float64 `json:"duration_core_rule_set_ms,omitempty"`
DurationUpstreamTotalMS *float64 `json:"duration_upstream_total_ms,omitempty"`
// DurationTotal is nil for a request that did not get that far.
DurationTotal float64 `json:"duration_total"`
DurationChecks *float64 `json:"duration_checks,omitempty"`
DurationWAF *float64 `json:"duration_waf,omitempty"`
DurationUpstreamTotal *float64 `json:"duration_upstream_total,omitempty"`
}
// RProxyLine is rproxy's short line in the request log about one request. The
@@ -226,11 +220,11 @@ type RProxyLine struct {
Status int `json:"status"`
RequestBytes int64 `json:"request_bytes"`
ResponseBytes int64 `json:"response_bytes"`
// DurationRequestMS runs from the request's headers being read until the app
// has been sent the whole request, or the answer has started if that is
// sooner. DurationResponseMS runs from then until the answer ends.
DurationRequestMS float64 `json:"duration_request_ms"`
DurationResponseMS float64 `json:"duration_response_ms"`
// DurationRequest runs in milliseconds from the request's headers being read
// until the app has been sent the whole request, or the answer has started
// if that is sooner. DurationResponse runs from then until the answer ends.
DurationRequest float64 `json:"duration_request"`
DurationResponse float64 `json:"duration_response"`
// Disposition is the valid disposition discriminator answered or, while
// DefaultDisposition is true, the value of SWRP_DEFAULT_DISPOSITION. It is ""
// when neither gave one.
+5 -5
View File
@@ -22,7 +22,7 @@ func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) {
ClientIP: "203.0.113.9",
Status: 200,
Action: requestlog.ActionForward,
DurationTotalMS: requestlog.Milliseconds(1500 * time.Microsecond),
DurationTotal: requestlog.Milliseconds(1500 * time.Microsecond),
})
if err != nil {
t.Fatalf("write: %v", err)
@@ -43,7 +43,7 @@ func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) {
want := map[string]any{
"type": "request", "time": "2026-10-03T12:00:00.000Z",
"client_ip": "203.0.113.9", "status": 200.0, "action": "forward",
"duration_total_ms": 1.5,
"duration_total": 1.5,
}
for name, value := range want {
if fields[name] != value {
@@ -55,9 +55,9 @@ func TestWriteWritesOneJSONLineMarkedRequest(t *testing.T) {
"forwarded_for", "content_type", "content_length", "request_headers",
"has_authorization", "has_cookie", "websocket", "response_content_type",
"upstream_status", "cache_control", "location", "aborted", "counts",
"limit_hit", "offence", "ban_expires", "duration_checks_ms",
"limit_hit", "offence", "ban_expires", "duration_checks",
"duration_upstream_connect", "duration_upstream_first_byte",
"duration_upstream_total_ms",
"duration_upstream_total",
}
for _, name := range unset {
_, present := fields[name]
@@ -82,7 +82,7 @@ func TestWriteRProxyLineWritesOneJSONLineWithOnlyItsFields(t *testing.T) {
const want = `{"type":"request","time":"","request_id":"an-id","client_ip":"",` +
`"host":"","method":"","path":"","query":"","status":0,"request_bytes":0,` +
`"response_bytes":0,"duration_request_ms":0,"duration_response_ms":0,` +
`"response_bytes":0,"duration_request":0,"duration_response":0,` +
`"disposition":"allow"}` + "\n"
if out.String() != want {
t.Errorf("wrote %s, want %s", out.String(), want)
+4 -4
View File
@@ -371,8 +371,8 @@ func (rq *request) finish(ctx context.Context) {
Status: rq.out.status,
RequestBytes: requestBytes,
ResponseBytes: responseBytes,
DurationRequestMS: requestlog.Milliseconds(requestEnd.Sub(rq.start)),
DurationResponseMS: requestlog.Milliseconds(end.Sub(requestEnd)),
DurationRequest: requestlog.Milliseconds(requestEnd.Sub(rq.start)),
DurationResponse: requestlog.Milliseconds(end.Sub(requestEnd)),
Disposition: rq.disposition,
DefaultDisposition: rq.byDefault,
})
@@ -389,10 +389,10 @@ func (rq *request) finish(ctx context.Context) {
Status: min(rq.out.status, reportStatusMax),
RequestBytes: requestBytes,
ResponseBytes: responseBytes,
DurationTotalMS: requestlog.Milliseconds(end.Sub(rq.start)),
DurationTotal: requestlog.Milliseconds(end.Sub(rq.start)),
}
if !rq.upstreamStart.IsZero() {
report.DurationUpstreamTotalMS = requestlog.Milliseconds(end.Sub(rq.upstreamStart))
report.DurationUpstreamTotal = requestlog.Milliseconds(end.Sub(rq.upstreamStart))
}
rq.h.sendReport(ctx, report)
+5 -5
View File
@@ -45,9 +45,9 @@ const (
idleTimeout = 120 * time.Second
)
// ownEndpointPrefix starts the path of every request for sww itself, which
// adminPrefix starts the path of every request for sww itself, which
// never reaches the app.
const ownEndpointPrefix = "/_sww/"
const adminPrefix = "/_sww/"
// allowedMethods lists the methods rproxy passes on, as an Allow header gives
// them. It answers any other method 405 itself.
@@ -211,8 +211,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// passToApp passes the request on to the app, unless a limit or its path keeps
// it from the app. discriminator answers a path under ownEndpointPrefix itself, so
// such a request gets here only when SWRP_DEFAULT_DISPOSITION allowed it. ctx is the
// it from the app. discriminator answers a path under adminPrefix itself, so such a
// request gets here only when SWRP_DEFAULT_DISPOSITION allowed it. ctx is the
// request's own context.
func (rq *request) passToApp(ctx context.Context) {
maxBytes := rq.h.config.MaxRequestSize
@@ -220,7 +220,7 @@ func (rq *request) passToApp(ctx context.Context) {
switch {
case rq.refused.Load() != 0 || (maxBytes > 0 && rq.in.ContentLength > maxBytes):
rq.answer(http.StatusRequestEntityTooLarge)
case strings.HasPrefix(rq.in.URL.Path, ownEndpointPrefix):
case strings.HasPrefix(rq.in.URL.Path, adminPrefix):
rq.answer(http.StatusNotFound)
default:
rq.forward(ctx)
+6 -6
View File
@@ -727,7 +727,7 @@ func TestDenyGivesTheVisitorDiscriminatorsAnswer(t *testing.T) {
report := standIn.report(t)
if report.Status != http.StatusForbidden ||
report.ResponseBytes != int64(len(deniedBody)) ||
report.DurationUpstreamTotalMS != 0 {
report.DurationUpstreamTotal != 0 {
t.Errorf("report %+v, of a request denied", report)
}
@@ -871,9 +871,9 @@ func TestALineForARequestClosedWithoutAnAnswerHasNoNegativeTime(t *testing.T) {
line := out.requestLine(t)
duration, ok := line["duration_response_ms"].(float64)
duration, ok := line["duration_response"].(float64)
if !ok || duration < 0 {
t.Errorf("line %v, want a duration_response_ms that is not negative", line)
t.Errorf("line %v, want a duration_response that is not negative", line)
}
}
}
@@ -1428,8 +1428,8 @@ func TestReportGivesWhatHappened(t *testing.T) {
if report.RequestID == "" || report.Status != http.StatusOK ||
report.RequestBytes != int64(len(body)) ||
report.ResponseBytes != int64(len(greeting)) ||
report.DurationTotalMS <= 0 || report.DurationUpstreamTotalMS <= 0 ||
report.DurationUpstreamTotalMS > report.DurationTotalMS {
report.DurationTotal <= 0 || report.DurationUpstreamTotal <= 0 ||
report.DurationUpstreamTotal > report.DurationTotal {
t.Errorf("report %+v, of %d bytes up and %d back", report, len(body), len(greeting))
}
@@ -1748,7 +1748,7 @@ func TestLogLineGivesTheRequestAsSent(t *testing.T) {
t.Errorf("default_disposition is %v, want it left out", line["default_disposition"])
}
for _, name := range []string{"duration_request_ms", "duration_response_ms"} {
for _, name := range []string{"duration_request", "duration_response"} {
duration, ok := line[name].(float64)
if !ok || duration <= 0 {
t.Errorf("%s is %v, want a time", name, line[name])
+7 -7
View File
@@ -64,7 +64,7 @@ var (
errCause = errors.New("is not limit, attack, admin or crowdsec")
errExpires = errors.New("is not after its start")
errDestination = errors.New("is not webhook, slack or ntfy")
errScope = errors.New("is not client, net, asn, total or watched_net")
errScope = errors.New("is not client, net, asn, total or watch")
errWaitingList = errors.New(`waiting is a list, but now lists the alerts by ` +
`destination: put the list under "webhook", as "waiting": {"webhook": [...]}, ` +
`or remove the file`)
@@ -858,8 +858,8 @@ func (f *alertsFile) check([]byte) error {
}
// checkAnomalyCounters refuses an anomaly counter whose scope is not
// client, net, asn, total or watched_net, most likely misspelt, and one
// without a field it needs, as missingFromCounter tells.
// client, net, asn, total or watch, most likely misspelt, and one without
// a field it needs, as missingFromCounter tells.
func checkAnomalyCounters(counters []anomaly.Counter) error {
for i, counter := range counters {
if !slices.Contains(anomaly.Scopes(), counter.Scope) {
@@ -879,9 +879,9 @@ func checkAnomalyCounters(counters []anomaly.Counter) error {
// missingFromCounter returns the first field counter, an anomaly counter,
// needs and has not, or "" when it has them all: what tells it from the
// others in its scope, without which it would never be counted again, the
// netblock of a client, net or watched_net counter, the AS number of an
// asn one and the name of a watched_net one; and the start of a window in
// which it has requests or bytes, without which they would be dropped.
// netblock of a client, net or watch counter, the AS number of an asn one
// and the name of a watch one; and the start of a window in which it has
// requests or bytes, without which they would be dropped.
func missingFromCounter(counter anomaly.Counter) string {
scope := counter.Scope
@@ -891,7 +891,7 @@ func missingFromCounter(counter anomaly.Counter) string {
return "netblock"
case scope == anomaly.ScopeASN && counter.ASN == "":
return "asn"
case scope == anomaly.ScopeWatchedNet && counter.Name == "":
case scope == anomaly.ScopeWatch && counter.Name == "":
return "name"
case countsWithoutStart(counter.Minute):
return "minute.start"
+5 -5
View File
@@ -208,7 +208,7 @@ const filledAlertsJSON = `{
}
},
{
"scope": "watched_net",
"scope": "watch",
"netblock": "203.0.113.0/24",
"name": "office",
"hour": {
@@ -534,7 +534,7 @@ func TestFileThatDoesNotParseStopsTheStart(t *testing.T) {
`{"version": 1, "anomaly_counters": [{"scope": "total"}, ` +
`{"scope": "nett", "netblock": "203.0.113.0/24"}]}`,
`: anomaly_counters entry 2's scope "nett" is not client, net, asn, total ` +
`or watched_net`,
`or watch`,
},
{
"a copy of a list with a line that does not read", reputationJSON,
@@ -831,12 +831,12 @@ func TestAlertsJSONEntryWithoutAFieldItNeedsStopsTheStart(t *testing.T) {
{
"an anomaly counter of a named netblock without its name",
`{"version": 1, "anomaly_counters": [` +
`{"scope": "watched_net", "netblock": "203.0.113.0/24"}]}`,
`{"scope": "watch", "netblock": "203.0.113.0/24"}]}`,
`: anomaly_counters entry 1 has no "name"`,
},
{
"an anomaly counter of a named netblock without its netblock",
`{"version": 1, "anomaly_counters": [{"scope": "watched_net", "name": "office"}]}`,
`{"version": 1, "anomaly_counters": [{"scope": "watch", "name": "office"}]}`,
`: anomaly_counters entry 1 has no "netblock"`,
},
{
@@ -1729,7 +1729,7 @@ func newParams(dir string) state.Params {
Dir: dir,
WriteDelay: time.Hour,
CounterInterval: time.Hour,
Ledger: bans.New(bans.Params{
Ledger: bans.New(bans.Rules{
LimitBanDuration: time.Hour,
LimitBanRepeatWindow: 24 * time.Hour,
MaxBanDuration: 7 * 24 * time.Hour,
+2 -2
View File
@@ -22,7 +22,6 @@ import (
"sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
"sneak.berlin/go/smallwebwaf/internal/discriminator"
"sneak.berlin/go/smallwebwaf/internal/lookup"
"sneak.berlin/go/smallwebwaf/internal/remotelog"
@@ -30,6 +29,7 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rules"
"sneak.berlin/go/smallwebwaf/internal/state"
"sneak.berlin/go/smallwebwaf/internal/waf"
)
// shutdownTimeout is how long requests in progress may take to finish
@@ -197,7 +197,7 @@ func RunDiscriminator(ctx context.Context, params Params) int {
func newServer(
cfg *config.Config, stdout io.Writer, processLog *slog.Logger,
now func() time.Time, ruleFiles *rules.Files, alertQueue *alerts.Queue,
crs *coreruleset.CoreRuleSet,
crs *waf.CoreRuleSet,
) (*discriminator.Server, error) {
var lookupFile *lookup.File
+8 -13
View File
@@ -373,13 +373,13 @@ func TestStateKeptAcrossRestarts(t *testing.T) {
out := runUntilStopped(t, env, func(url string) {
wantRefused(t, url)
})
out.line(t, "action", "client_is_rate_limited")
out.line(t, "action", "rate_limited")
// After another, the ban still refuses it.
out = runUntilStopped(t, env, func(url string) {
wantRefused(t, url)
})
out.line(t, "action", "temporarily_banned")
out.line(t, "action", "banned")
}
func TestBanRefusesItsNetblockAfterARestartWithAnotherScope(t *testing.T) {
@@ -526,7 +526,7 @@ func TestRuleFileAddedWhileRunningTakesEffect(t *testing.T) {
time.Sleep(pollInterval)
}
})
out.line(t, "action", "blocked_by_block_rule")
out.line(t, "action", "rule_blocked")
}
func TestRuleFileThatDoesNotParseStopsTheStart(t *testing.T) {
@@ -740,7 +740,7 @@ func TestBlocklistTriesAndCopiesKeptInReputationJSONAcrossRestarts(t *testing.T)
time.Sleep(pollInterval)
}
})
wantDeniedByList(t, out.line(t, "action", "client_denied"), dropURL)
wantDeniedByList(t, out.line(t, "action", "denied"), dropURL)
if fetches := torFetches.Load(); fetches != 1 {
t.Errorf("tor.txt fetched %d times, want once, before the restart", fetches)
@@ -843,8 +843,7 @@ func wantDeniedByList(t *testing.T, line map[string]any, listURL string) {
t.Helper()
reputation, _ := line["reputation"].([]any)
if line["action"] != "client_denied" || len(reputation) != 1 ||
reputation[0] != listURL {
if line["action"] != "denied" || len(reputation) != 1 || reputation[0] != listURL {
t.Errorf("request log line %v, want one denied for %s", line, listURL)
}
}
@@ -962,7 +961,7 @@ func TestStalledRemoteLogEndpointHoldsUpNoRequest(t *testing.T) {
metrics := metricsText(t, url+"_sww/metrics", token)
for _, series := range []string{
`sww_remote_log_lines_sent_total{instance="fsn1app1/blog"} 0`,
`sww_remote_log_lines_waiting{instance="fsn1app1/blog"} 1`,
`sww_remote_log_buffer_depth{instance="fsn1app1/blog"} 1`,
} {
if !strings.Contains(metrics, "\n"+series+"\n") {
t.Errorf("no %q in the metrics:\n%s", series, metrics)
@@ -1068,12 +1067,8 @@ func TestBanIsAlertedAndAnAlertNotSentIsKeptAcrossARestart(t *testing.T) {
metrics := metricsWith(t, url+"_sww/metrics", token,
"\nsww_alerts_sent_total"+ofWebhook+" 1\n")
for _, series := range []string{
"sww_alert_send_failures_total",
"sww_alerts_suppressed_total",
"sww_alerts_dropped_total",
} {
zero := "\n" + series + ofWebhook + " 0\n"
for _, series := range []string{"failed", "suppressed", "dropped"} {
zero := "\nsww_alerts_" + series + "_total" + ofWebhook + " 0\n"
if !strings.Contains(metrics, zero) {
t.Errorf("no %q in the metrics:\n%s", zero, metrics)
}
@@ -1,8 +1,51 @@
# The eighth change: on gitea's paths, some fields skip the rules that refused
# ordinary gitea requests there. They are the fields where gitea sends a search,
# a file name or a branch name. Each exclusion runs with the request, before the
# rules it switches off, those of phase 1 included, so this file is loaded before
# the Core Rule Set's rules. Each comment names a request the rule refused.
# gitea's fixes: some fields skip the rules that refused ordinary gitea requests.
# They are the fields where gitea sends a search, a file name or a branch name.
# Each exclusion runs with the request, before the rules it switches off, those
# of phase 1 included, so this file is loaded before the Core Rule Set's rules.
# Each comment names a request the rule refused.
# A request whose hostname SWW_APPS does not list gitea for skips every rule
# below. Inspect sets tx.app_gitea on the others.
SecRule &TX:app_gitea "@eq 0" \
"id:900590,phase:1,pass,nolog,skipAfter:END_GITEA"
# The query parameters in which gitea sends names within a repository or its
# own records, or a page of its own site, are not checked against the lists of
# system files, shell paths and command names, on any path:
# 930120: .gitignore
# 932160: bin/docker-entrypoint
# 932260: docker-build
SecAction "id:900591,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:path,\
ctl:ruleRemoveTargetById=930120;ARGS:files,\
ctl:ruleRemoveTargetById=930120;ARGS:skip-to,\
ctl:ruleRemoveTargetById=930120;ARGS:sub_path,\
ctl:ruleRemoveTargetById=930120;ARGS:ref,\
ctl:ruleRemoveTargetById=930120;ARGS:sha,\
ctl:ruleRemoveTargetById=930120;ARGS:branch,\
ctl:ruleRemoveTargetById=930120;ARGS:workflow,\
ctl:ruleRemoveTargetById=930120;ARGS:artifactName,\
ctl:ruleRemoveTargetById=930120;ARGS:redirect_to,\
ctl:ruleRemoveTargetById=932160;ARGS:path,\
ctl:ruleRemoveTargetById=932160;ARGS:files,\
ctl:ruleRemoveTargetById=932160;ARGS:skip-to,\
ctl:ruleRemoveTargetById=932160;ARGS:sub_path,\
ctl:ruleRemoveTargetById=932160;ARGS:ref,\
ctl:ruleRemoveTargetById=932160;ARGS:sha,\
ctl:ruleRemoveTargetById=932160;ARGS:branch,\
ctl:ruleRemoveTargetById=932160;ARGS:workflow,\
ctl:ruleRemoveTargetById=932160;ARGS:artifactName,\
ctl:ruleRemoveTargetById=932160;ARGS:redirect_to,\
ctl:ruleRemoveTargetById=932260;ARGS:path,\
ctl:ruleRemoveTargetById=932260;ARGS:files,\
ctl:ruleRemoveTargetById=932260;ARGS:skip-to,\
ctl:ruleRemoveTargetById=932260;ARGS:sub_path,\
ctl:ruleRemoveTargetById=932260;ARGS:ref,\
ctl:ruleRemoveTargetById=932260;ARGS:sha,\
ctl:ruleRemoveTargetById=932260;ARGS:branch,\
ctl:ruleRemoveTargetById=932260;ARGS:workflow,\
ctl:ruleRemoveTargetById=932260;ARGS:artifactName,\
ctl:ruleRemoveTargetById=932260;ARGS:redirect_to"
# gitea's searches of code, of issues and pull requests, and of commits send the
# text searched for in q, which skips these rules there:
@@ -155,8 +198,8 @@ SecRule REQUEST_FILENAME \
# 942140: information_schema.sql
# 944110: java/lang/Runtime.java
# 944130: java.lang.String.md
# Each request skips the rules that refused it. The fourth change already has
# path and redirect_to skip 930120, 932160 and 932260 on every path.
# Each request skips the rules that refused it. Rule 900591 already has path and
# redirect_to skip 930120, 932160 and 932260 on every path.
# A file's commit graph, in file.
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/graph$" \
@@ -281,3 +324,5 @@ SecRule REQUEST_FILENAME \
"@rx ^/api/actions_pipeline/_apis/pipelines/workflows/[0-9]+/artifacts/[^/]+/upload$" \
"id:900640,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=920450;REQUEST_HEADERS_NAMES:Content-Range"
SecMarker END_GITEA
@@ -1,6 +1,6 @@
# Ordinary gitea 1.27 requests that the Core Rule Set must let through at the
# defaults. Each line is a path and query, and may end with one header after a
# space. The tests of internal/coreruleset inspect each as a GET. The Core Rule Set reads
# space. The tests of internal/waf inspect each as a GET. The Core Rule Set reads
# the query and the headers the same way for any method.
# Searches with ordinary words and symbols.
@@ -1,21 +1,23 @@
// Package coreruleset runs the OWASP Core Rule Set 4.25.0, through Coraza, on the
// Package waf runs the OWASP Core Rule Set 4.25.0, through Coraza, on the
// method, the URL with its query and the headers of a request, and on its
// body while SWW_WAF_BODY_LIMIT is set, with the nine 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.
// body while SWW_WAF_BODY_LIMIT is set, with the six changes sww
// makes to it, the fixes for the apps SWW_APPS names, and the exclusions in
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as "Attack detection" under "Settings"
// in SPEC.md describes them. It reads no response.
//
// sww writes only to its state directory, so Coraza is built with
// its no_fs_access tag, as the Dockerfile and script/build build it: of a
// file in a multipart body, Coraza then counts the bytes instead of
// writing them to the system's temporary directory.
package coreruleset
package waf
import (
_ "embed"
"errors"
"fmt"
"io"
"maps"
"net"
"net/http"
"net/netip"
"os"
@@ -32,8 +34,8 @@ 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_BODY_LIMIT is set, the exclusions file for the second,
// giteaExclusions for the third and drupalExclusions for the fourth.
// SWW_WAF_BODY_LIMIT is set, the exclusions file for the second, and the
// directives of every app in apps for the third.
// 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_DISABLED_RULES and the exclusions file refuse, so that no
@@ -81,47 +83,28 @@ SecRule ARGUMENTS_LIMIT_REACHED "@eq 1" "id:900300,phase:2,pass,\
# before the rules of phase 1 too.
%s
# The eighth change comes before the rules for the same reason.
# The apps' fixes come before the rules for the same reason.
%s
# So does the ninth.
%s
# The sixth: only the rules for requests are loaded, and no response is
# The fifth: only the rules for requests are loaded, and no response is
# inspected.
Include @owasp_crs/REQUEST-*.conf
# The third: redirect_uri is not checked for a URL naming an IP address or
# localhost. Coraza matches a parameter name here, and in the fourth,
# without regard to case. ARGS holds the fields of a form data or multipart
# body Coraza reads as well as the query parameters, so a field of one of
# these names is left out too.
# localhost. Coraza matches a parameter name here without regard to case.
# ARGS holds the fields of a form data or multipart body Coraza reads as
# well as the query parameters, so a field of this name is left out too.
SecRuleUpdateTargetById 931100 "!ARGS:redirect_uri"
SecRuleUpdateTargetById 934110 "!ARGS:redirect_uri"
# The fourth: the query parameters in which gitea sends names within a
# repository or its own records, or a page of its own site, are not
# checked against the lists of system files, shell paths and command
# names. Coraza takes one rule id per directive.
SecRuleUpdateTargetById 930120 "!ARGS:path|!ARGS:files|!ARGS:skip-to|\
!ARGS:sub_path|!ARGS:ref|!ARGS:sha|!ARGS:branch|!ARGS:workflow|\
!ARGS:artifactName|!ARGS:redirect_to"
SecRuleUpdateTargetById 932160 "!ARGS:path|!ARGS:files|!ARGS:skip-to|\
!ARGS:sub_path|!ARGS:ref|!ARGS:sha|!ARGS:branch|!ARGS:workflow|\
!ARGS:artifactName|!ARGS:redirect_to"
SecRuleUpdateTargetById 932260 "!ARGS:path|!ARGS:files|!ARGS:skip-to|\
!ARGS:sub_path|!ARGS:ref|!ARGS:sha|!ARGS:branch|!ARGS:workflow|\
!ARGS:artifactName|!ARGS:redirect_to"
# The fifth, for Referer: it is not checked for a Unix command without
# arguments, for Java starting a process, or for a Java class name. 944130
# refused every page opened from the page of java.lang.String.md. The
# cookies are left out in Inspect.
# The fourth: Referer is not checked for a Unix command without arguments,
# for Java starting a process, or for a Java class name. 944130 refused
# every page opened from the page of java.lang.String.md.
SecRuleUpdateTargetById 932340 "!REQUEST_HEADERS:Referer"
SecRuleUpdateTargetById 944110 "!REQUEST_HEADERS:Referer"
SecRuleUpdateTargetById 944130 "!REQUEST_HEADERS:Referer"
# The seventh: a container image push that reuses a layer names the image
# The sixth: a container image push that reuses a layer names the image
# it takes the layer from in the query parameter from, which is not
# checked for command names. 932260 refused the push of a layer from
# owner/python3-app.
@@ -171,25 +154,36 @@ SecRule MULTIPART_STRICT_ERROR "!@eq 0" "id:900450,phase:2,pass,\
severity:'CRITICAL',setvar:'tx.inbound_anomaly_score_pl1=+5'"
`
// giteaExclusions are the eighth change: the fields of gitea's own
// requests that skip the rules that refused them there, as Coraza
// directives.
//
// app is an app whose ordinary requests the Core Rule Set would refuse,
// with the fixes that let them pass.
type app struct {
// directives are Coraza exclusions, loaded before the Core Rule Set's
// rules. Their first rule skips the rest unless Inspect sets the
// transaction variable app_ and the app's name, and they end with the
// marker it skips to.
directives string
// cookies are the cookies the Core Rule Set reads a request without.
cookies []string
}
//go:embed gitea.conf
var giteaExclusions string
var giteaDirectives string
// drupalExclusions are the ninth change: the fields of Drupal's own
// requests that skip the rules that refused them there, as Coraza
// directives.
// apps are the apps whose fixes sww has, by the name SWW_APPS gives each.
//
//go:embed drupal.conf
var drupalExclusions string
//nolint:gochecknoglobals // a constant cannot be a map
var apps = map[string]app{
"gitea": {
directives: giteaDirectives,
cookies: []string{"gitea_flash", "redirect_to"},
},
}
// cookiesNotRead are the cookies the Core Rule Set reads a request
// without, the rest of the fifth change.
//
//nolint:gochecknoglobals // a constant cannot be a list
var cookiesNotRead = []string{"gitea_flash", "redirect_to"}
// AppNames returns the names of the apps whose fixes sww has, in
// alphabetical order.
func AppNames() []string {
return slices.Sorted(maps.Keys(apps))
}
// Params are what New needs.
type Params struct {
@@ -205,6 +199,10 @@ type Params struct {
// Core Rule Set's rules (SWW_CORE_RULE_SET_EXCLUSIONS_FILE), "" while
// there is none.
ExclusionsFile string
// Apps are the names of the apps each hostname serves, by hostname in
// lowercase (SWW_APPS). A request gets the fixes of its hostname's
// apps only, or of every app while Apps is nil.
Apps map[string][]string
}
// The ids of sww's own rules, which an exclusions file may not
@@ -294,15 +292,17 @@ func checkExclusions(exclusions string) error {
// CoreRuleSet is the Core Rule Set, ready to inspect requests. It is safe
// for concurrent use.
type CoreRuleSet struct {
engine coraza.WAF
waf coraza.WAF
bodyLimit int64
apps map[string][]string
}
// New returns the Core Rule Set with the nine 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
// cannot be read or does not parse, or if checkExclusions refuses it.
// New returns the Core Rule Set with the six changes and the apps' fixes,
// 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 cannot be read or does not parse, or if checkExclusions refuses
// it.
func New(params Params) (*CoreRuleSet, error) {
body := ""
if params.BodyLimit > 0 {
@@ -325,8 +325,13 @@ func New(params Params) (*CoreRuleSet, error) {
}
}
var fixes strings.Builder
for _, name := range AppNames() {
fixes.WriteString(apps[name].directives + "\n")
}
text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions,
giteaExclusions, drupalExclusions)
fixes.String())
if len(params.DisabledRules) > 0 {
ids := make([]string, len(params.DisabledRules))
@@ -337,14 +342,14 @@ func New(params Params) (*CoreRuleSet, error) {
text += "SecRuleRemoveById " + strings.Join(ids, " ") + "\n"
}
engine, err := coraza.NewWAF(coraza.NewWAFConfig().
waf, err := coraza.NewWAF(coraza.NewWAFConfig().
WithRootFS(coreruleset.FS).
WithDirectives(text))
if err != nil {
return nil, fmt.Errorf("load the Core Rule Set: %w", err)
}
return &CoreRuleSet{engine: engine, bodyLimit: params.BodyLimit}, nil
return &CoreRuleSet{waf: waf, bodyLimit: params.BodyLimit, apps: params.Apps}, nil
}
// Result is what the Core Rule Set found in a request.
@@ -356,25 +361,37 @@ type Result struct {
Score int
}
// Inspect runs the Core Rule Set on r, a request from client: on its
// method, its URL with the query, and its headers, the Cookie header
// without the cookies in cookiesNotRead, and on body, r's body as the
// caller has it, as readBody reads it. It returns what it found, what it
// read of body, and the error that ended the reading early, if one did.
// Inspect runs the Core Rule Set on r, a request from client, with the
// fixes of the apps appsFor names for r's hostname: on its method, its URL
// with the query, and its headers, the Cookie header without those apps'
// cookies, and on body, r's body as the caller has it, as readBody reads
// it. It returns what it found, what it read of body, and the error that
// ended the reading early, if one did.
func (c *CoreRuleSet) Inspect(
r *http.Request, client netip.Addr, body io.Reader,
) (Result, []byte, error) {
tx := c.engine.NewTransaction()
tx := c.waf.NewTransaction()
// Closing would remove the files Coraza wrote, and it writes none.
defer func() { _ = tx.Close() }()
// The transaction's variables are set only through Coraza's interface
// for plugins.
state := tx.(plugintypes.TransactionState) //nolint:forcetypeassert // every one is
var cookiesNotRead []string
for _, name := range c.appsFor(r.Host) {
state.Variables().TX().Set("app_"+name, []string{"1"})
cookiesNotRead = append(cookiesNotRead, apps[name].cookies...)
}
tx.ProcessConnection(client.String(), 0, "", 0)
tx.ProcessURI(r.URL.String(), r.Method, r.Proto)
for name, values := range r.Header {
for _, value := range values {
if name == "Cookie" {
value = withoutCookiesNotRead(value)
value = withoutCookies(value, cookiesNotRead)
if value == "" {
continue // it held those cookies alone
}
@@ -467,9 +484,26 @@ func score(tx types.Transaction) int {
return n
}
// withoutCookiesNotRead returns value, a Cookie header's, without the
// cookies in cookiesNotRead.
func withoutCookiesNotRead(value string) string {
// appsFor returns the names of the apps whose fixes a request for host,
// its Host header, gets: every app while SWW_APPS is unset, and otherwise
// those SWW_APPS lists for host's name, without the port and in
// lowercase.
func (c *CoreRuleSet) appsFor(host string) []string {
if c.apps == nil {
return AppNames()
}
hostname, _, err := net.SplitHostPort(host)
if err != nil {
hostname = host // it has no port
}
return c.apps[strings.ToLower(hostname)]
}
// withoutCookies returns value, a Cookie header's, without the cookies
// named in cookiesNotRead.
func withoutCookies(value string, cookiesNotRead []string) string {
var kept []string
for cookie := range strings.SplitSeq(value, ";") {
@@ -1,4 +1,4 @@
package coreruleset_test
package waf_test
import (
"net/http"
@@ -12,7 +12,7 @@ import (
"strings"
"testing"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
"sneak.berlin/go/smallwebwaf/internal/waf"
)
// defaultDisabledRules are the rules SWW_WAF_DISABLED_RULES switches off
@@ -23,12 +23,10 @@ var defaultDisabledRules = []int{920340, 920420, 920440, 920640, 930130, 930140}
// newCoreRuleSet returns the Core Rule Set at paranoia level level, with
// the rules in disabled switched off.
func newCoreRuleSet(t *testing.T, level int, disabled ...int) *coreruleset.CoreRuleSet {
func newCoreRuleSet(t *testing.T, level int, disabled ...int) *waf.CoreRuleSet {
t.Helper()
crs, err := coreruleset.New(coreruleset.Params{
ParanoiaLevel: level, DisabledRules: disabled,
})
crs, err := waf.New(waf.Params{ParanoiaLevel: level, DisabledRules: disabled})
if err != nil {
t.Fatalf("load the Core Rule Set: %v", err)
}
@@ -51,7 +49,7 @@ func get(target string, headers ...string) request {
// inspect returns what crs finds in r, sent to git.example by a browser,
// whose Host, User-Agent and Accept r.headers may replace.
func inspect(t *testing.T, crs *coreruleset.CoreRuleSet, r request) coreruleset.Result {
func inspect(t *testing.T, crs *waf.CoreRuleSet, r request) waf.Result {
t.Helper()
result, _ := inspectBody(t, crs, r, "")
@@ -62,8 +60,8 @@ func inspect(t *testing.T, crs *coreruleset.CoreRuleSet, r request) coreruleset.
// inspectBody is inspect for r with body, which is announced with its
// Content-Length unless it is "", and returns what crs read of body too.
func inspectBody(
t *testing.T, crs *coreruleset.CoreRuleSet, r request, body string,
) (coreruleset.Result, string) {
t *testing.T, crs *waf.CoreRuleSet, r request, body string,
) (waf.Result, string) {
t.Helper()
req := httptest.NewRequestWithContext(t.Context(), r.method,
@@ -98,9 +96,7 @@ func inspectBody(
}
// wantResult checks what crs finds in r.
func wantResult(
t *testing.T, crs *coreruleset.CoreRuleSet, r request, want coreruleset.Result,
) {
func wantResult(t *testing.T, crs *waf.CoreRuleSet, r request, want waf.Result) {
t.Helper()
if got := inspect(t, crs, r); !reflect.DeepEqual(got, want) {
@@ -110,29 +106,28 @@ func wantResult(
// matched is the result of a request that the rules ids match, each of
// them a critical one, which adds 5 to the score.
func matched(ids ...int) coreruleset.Result {
func matched(ids ...int) waf.Result {
const critical = 5
return coreruleset.Result{RuleIDs: ids, Score: critical * len(ids)}
return waf.Result{RuleIDs: ids, Score: critical * len(ids)}
}
// atDefaults returns the Core Rule Set as sww runs it by default.
func atDefaults(t *testing.T) *coreruleset.CoreRuleSet {
func atDefaults(t *testing.T) *waf.CoreRuleSet {
t.Helper()
return newCoreRuleSet(t, 1, defaultDisabledRules...)
}
// wantChange checks that crs lets through passes, a request one of the
// nine changes is for, and still finds result in refused, a request like
// it that the change is not for.
// wantChange checks that crs lets through passes, a gitea request one of
// the six changes or gitea's fixes are for, and still finds result in
// refused, a request like it that they are not for.
func wantChange(
t *testing.T, crs *coreruleset.CoreRuleSet, passes, refused request,
result coreruleset.Result,
t *testing.T, crs *waf.CoreRuleSet, passes, refused request, result waf.Result,
) {
t.Helper()
wantResult(t, crs, passes, coreruleset.Result{})
wantResult(t, crs, passes, waf.Result{})
wantResult(t, crs, refused, result)
}
@@ -167,12 +162,12 @@ func TestExpectAndContentEncodingAreAllowed(t *testing.T) {
wantResult(t, crs,
request{http.MethodPost, push, []string{pushType, length, "Expect: 100-continue"}},
coreruleset.Result{})
waf.Result{})
wantResult(t, crs,
request{http.MethodPost, fetch, []string{
fetchType, length, "Content-Encoding: gzip",
}},
coreruleset.Result{})
waf.Result{})
// Every other header on the Core Rule Set's list stays refused.
for _, header := range []string{
@@ -201,7 +196,7 @@ func TestTransferEncodingIsRead(t *testing.T) {
"Content-Type: application/x-git-receive-pack-request",
"Transfer-Encoding: chunked",
}},
coreruleset.Result{})
waf.Result{})
}
func TestMoreParametersThanCorazaKeepsIsAMatch(t *testing.T) {
@@ -255,7 +250,7 @@ func TestParametersGiteaSendsNamesInSkipTheListsOfFilesPathsAndCommands(t *testi
for _, value := range []struct {
name string
// result is what a parameter that is not one of gitea's gets.
result coreruleset.Result
result waf.Result
}{
// A file on the list of system files.
{".gitignore", matched(930120)},
@@ -276,7 +271,7 @@ func TestParametersGiteaSendsNamesInSkipTheListsOfFilesPathsAndCommands(t *testi
wantChange(t, crs, get("/?path=|cat%20/etc/passwd"), get("/?q=|cat%20/etc/passwd"),
matched(930120, 932160))
wantResult(t, crs, get("/?path=../../etc/passwd"),
coreruleset.Result{RuleIDs: []int{930100, 930110}, Score: 20})
waf.Result{RuleIDs: []int{930100, 930110}, Score: 20})
wantResult(t, crs, get("/?path=1'%20OR%20'1'='1"), matched(942100))
}
@@ -346,7 +341,7 @@ func TestEmptyHeaderIsRead(t *testing.T) {
// An empty User-Agent is a notice, which adds 2.
wantResult(t, atDefaults(t), get("/", "User-Agent: "),
coreruleset.Result{RuleIDs: []int{920330}, Score: 2})
waf.Result{RuleIDs: []int{920330}, Score: 2})
}
func TestParanoiaLevel(t *testing.T) {
@@ -355,7 +350,7 @@ func TestParanoiaLevel(t *testing.T) {
// Accept-Charset is refused from paranoia level 2.
r := get("/", "Accept-Charset: utf-8")
wantResult(t, newCoreRuleSet(t, 1), r, coreruleset.Result{})
wantResult(t, newCoreRuleSet(t, 1), r, waf.Result{})
wantResult(t, newCoreRuleSet(t, 2), r, matched(920451))
}
@@ -367,8 +362,8 @@ func TestEachDisabledRuleIsSwitchedOff(t *testing.T) {
r := request{http.MethodTrace, "/", []string{"Host: 192.0.2.1"}}
wantResult(t, newCoreRuleSet(t, 1), r,
coreruleset.Result{RuleIDs: []int{911100, 920350}, Score: 8})
wantResult(t, newCoreRuleSet(t, 1, 920350, 911100), r, coreruleset.Result{})
waf.Result{RuleIDs: []int{911100, 920350}, Score: 8})
wantResult(t, newCoreRuleSet(t, 1, 920350, 911100), r, waf.Result{})
}
func TestImagePushThatReusesALayerIsNotCheckedForCommandNames(t *testing.T) {
@@ -386,10 +381,100 @@ func TestImagePushThatReusesALayerIsNotCheckedForCommandNames(t *testing.T) {
matched(930120, 932160))
}
// giteaFixed are gitea requests that only gitea's fixes let through, one
// for each kind of fix, each with what the Core Rule Set finds in it
// without them.
//
//nolint:gochecknoglobals // a constant cannot be a list
var giteaFixed = []struct {
request request
result waf.Result
}{
// A search of code, on the path of gitea's search.
{get("/explore/code?q=SELECT%20*%20FROM%20users%20WHERE"), matched(942100)},
// A file name, in a parameter gitea sends names in, on any path.
{get("/?path=.gitignore"), matched(930120)},
// A file name, in a cookie of gitea's.
{
get("/owner/repo", "Cookie: gitea_flash=success%3DFile%2Bpackage.json%2Bdeleted"),
matched(930120),
},
}
// The hostnames of the tests of SWW_APPS: the one that serves gitea, and
// one that serves another app.
const (
giteaHost = "git.example"
otherHost = "blog.example"
)
// withHost returns r sent to host.
func withHost(r request, host string) request {
return request{r.method, r.target, append([]string{"Host: " + host}, r.headers...)}
}
// servingApps returns the Core Rule Set at the defaults, with SWW_APPS
// giving apps.
func servingApps(t *testing.T, apps map[string][]string) *waf.CoreRuleSet {
t.Helper()
crs, err := waf.New(waf.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, Apps: apps,
})
if err != nil {
t.Fatalf("load the Core Rule Set: %v", err)
}
return crs
}
func TestAppsUnsetGivesEveryHostnameEveryAppsFixes(t *testing.T) {
t.Parallel()
crs := servingApps(t, nil)
for _, fixed := range giteaFixed {
for _, host := range []string{giteaHost, otherHost} {
wantResult(t, crs, withHost(fixed.request, host), waf.Result{})
}
}
}
func TestListedHostnameGetsItsAppsFixes(t *testing.T) {
t.Parallel()
crs := servingApps(t, map[string][]string{giteaHost: {"gitea"}})
// The hostname is matched without the port and without regard to case.
for _, host := range []string{giteaHost, "GIT.Example:8443"} {
for _, fixed := range giteaFixed {
wantResult(t, crs, withHost(fixed.request, host), waf.Result{})
}
}
}
func TestUnlistedHostnameGetsTheFullCheck(t *testing.T) {
t.Parallel()
for _, apps := range []map[string][]string{
{giteaHost: {"gitea"}},
// SWW_APPS set empty lists no hostname.
{},
} {
crs := servingApps(t, apps)
for _, host := range []string{otherHost, otherHost + ":8443", "git.example.org"} {
for _, fixed := range giteaFixed {
wantResult(t, crs, withHost(fixed.request, host), fixed.result)
}
}
}
}
// withExclusions returns the Core Rule Set at the defaults, with the
// exclusions file at path.
func withExclusions(path string) (*coreruleset.CoreRuleSet, error) {
return coreruleset.New(coreruleset.Params{
func withExclusions(path string) (*waf.CoreRuleSet, error) {
return waf.New(waf.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, ExclusionsFile: path,
})
}
@@ -427,7 +512,7 @@ func TestExclusionsFileIsLoadedBeforeTheRules(t *testing.T) {
func TestRuleSwitchedOffAndNamedInTheExclusionsFileIsOff(t *testing.T) {
t.Parallel()
crs, err := coreruleset.New(coreruleset.Params{
crs, err := waf.New(waf.Params{
ParanoiaLevel: 1, DisabledRules: []int{920500},
ExclusionsFile: exclusionsFile(t, backupFileExclusion),
})
@@ -435,7 +520,7 @@ func TestRuleSwitchedOffAndNamedInTheExclusionsFileIsOff(t *testing.T) {
t.Fatalf("load the exclusions: %v", err)
}
wantResult(t, crs, get("/notes.txt~"), coreruleset.Result{})
wantResult(t, crs, get("/notes.txt~"), waf.Result{})
}
func TestExclusionsFileMayNameAnyNumberInAComment(t *testing.T) {
@@ -571,10 +656,10 @@ const injection = "1' OR '1'='1"
// readingBodies returns the Core Rule Set as sww runs it by
// default, but reading bodies up to bodyLimit.
func readingBodies(t *testing.T) *coreruleset.CoreRuleSet {
func readingBodies(t *testing.T) *waf.CoreRuleSet {
t.Helper()
crs, err := coreruleset.New(coreruleset.Params{
crs, err := waf.New(waf.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit,
})
if err != nil {
@@ -608,8 +693,7 @@ func padded(head, tail string, n int) string {
// wantBody checks what crs finds in r with body, and that what it read of
// body is read.
func wantBody(
t *testing.T, crs *coreruleset.CoreRuleSet, r request, body string,
want coreruleset.Result,
t *testing.T, crs *waf.CoreRuleSet, r request, body string, want waf.Result,
read string,
) {
t.Helper()
@@ -633,7 +717,7 @@ func TestBodiesAreReadOnlyWhileBodyLimitIsSet(t *testing.T) {
{jsonBody, `{"q":"` + injection + `"}`},
{xmlBody, "<q>" + injection + "</q>"},
} {
wantBody(t, off, post(tc.header), tc.body, coreruleset.Result{}, "")
wantBody(t, off, post(tc.header), tc.body, waf.Result{}, "")
wantBody(t, on, post(tc.header), tc.body, matched(942100), tc.body)
}
}
@@ -656,7 +740,7 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
} {
wantBody(t, crs, post(tc.header), tc.attackFirst, matched(942100),
tc.attackFirst[:bodyLimit+1])
wantBody(t, crs, post(tc.header), tc.attackLast, coreruleset.Result{},
wantBody(t, crs, post(tc.header), tc.attackLast, waf.Result{},
tc.attackLast[:bodyLimit+1])
}
@@ -666,7 +750,7 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
wantBody(t, crs, post(formData), atLimit, matched(930120, 932160), atLimit)
pastLimit := padded("pad=", "&q=/etc/passwd", bodyLimit+1)
wantBody(t, crs, post(formData), pastLimit, coreruleset.Result{}, pastLimit)
wantBody(t, crs, post(formData), pastLimit, waf.Result{}, pastLimit)
}
func TestJSONAndXMLAreReadOnlyWhenNoLargerThanTheLimit(t *testing.T) {
@@ -682,7 +766,7 @@ func TestJSONAndXMLAreReadOnlyWhenNoLargerThanTheLimit(t *testing.T) {
wantBody(t, crs, post(tc.header), fits, matched(942100), fits)
larger := padded(tc.head, tc.tail, bodyLimit+1)
wantBody(t, crs, post(tc.header), larger, coreruleset.Result{}, larger)
wantBody(t, crs, post(tc.header), larger, waf.Result{}, larger)
}
}
@@ -700,7 +784,7 @@ func TestOtherBodiesAreNotRead(t *testing.T) {
"Content-Type: text/plain",
"Content-Type: application/x-git-receive-pack-request",
} {
wantBody(t, crs, post(header), body, coreruleset.Result{}, "")
wantBody(t, crs, post(header), body, waf.Result{}, "")
}
}
@@ -728,8 +812,8 @@ func TestContentEncodingIsRefusedOnTheKindsOfBodyTheCoreRuleSetReads(t *testing.
// It is allowed on a body of any other kind, and on every body while no
// body is read.
fetch := "Content-Type: application/x-git-upload-pack-request"
wantBody(t, crs, post(fetch, gzip), "a", coreruleset.Result{}, "")
wantBody(t, atDefaults(t), post(formData, gzip), "a", coreruleset.Result{}, "")
wantBody(t, crs, post(fetch, gzip), "a", waf.Result{}, "")
wantBody(t, atDefaults(t), post(formData, gzip), "a", waf.Result{}, "")
}
func TestParametersGiteaSendsNamesInAreLeftOutAmongFormFieldsToo(t *testing.T) {
@@ -740,18 +824,18 @@ func TestParametersGiteaSendsNamesInAreLeftOutAmongFormFieldsToo(t *testing.T) {
for _, tc := range []struct {
body string
want coreruleset.Result
want waf.Result
}{
{"path=.gitignore", coreruleset.Result{}},
{"path=.gitignore", waf.Result{}},
{"q=.gitignore", matched(930120)},
{"redirect_uri=" + local, coreruleset.Result{}},
{"redirect_uri=" + local, waf.Result{}},
{"next=" + local, matched(931100, 934110)},
} {
wantBody(t, crs, post(formData), tc.body, tc.want, tc.body)
}
body := field("path", ".gitignore") + end
wantBody(t, crs, post(multipart), body, coreruleset.Result{}, body)
wantBody(t, crs, post(multipart), body, waf.Result{}, body)
body = field("q", ".gitignore") + end
wantBody(t, crs, post(multipart), body, matched(930120), body)
@@ -875,7 +959,7 @@ func TestCorazaWritesNoFile(t *testing.T) {
func TestBodyLimitOf1GLoads(t *testing.T) {
t.Parallel()
_, err := coreruleset.New(coreruleset.Params{ParanoiaLevel: 1, BodyLimit: 1 << 30})
_, err := waf.New(waf.Params{ParanoiaLevel: 1, BodyLimit: 1 << 30})
if err != nil {
t.Errorf("load the Core Rule Set reading bodies up to 1G: %v", err)
}
+4 -5
View File
@@ -187,16 +187,15 @@ main() {
echo "example-app: rproxy passes a request to the app and its answer back"
refused || fail "a second request in a minute was not refused"
wait_for "discriminator logged no ban" \
logged "$DISCRIMINATOR" '"action":"client_is_rate_limited"'
wait_for "discriminator logged no ban" logged "$DISCRIMINATOR" '"action":"rate_limited"'
echo "example-app: a second request in a minute bans the client"
refused_from 203.0.113.9 /.env || fail "a probe for /.env was not refused"
wait_for "discriminator logged no ban for the probe" \
logged "$DISCRIMINATOR" '"action":"temporarily_banned"' '"rule_ids":["env-file"]'
logged "$DISCRIMINATOR" '"action":"banned"' '"rule_ids":["env-file"]'
refused_from 203.0.113.9 / || fail "the client of the probe was let through"
wait_for "the client's next request did not make its ban permanent" \
logged "$DISCRIMINATOR" '"action":"permanently_banned"' '"ban_expires":"permanent"'
logged "$DISCRIMINATOR" '"ban_expires":"permanent"'
echo "example-app: a probe for /.env bans the client, its next request for good"
docker exec "$DISCRIMINATOR" sv stop sww >/dev/null ||
@@ -233,7 +232,7 @@ main() {
start_app
refused || fail "the new discriminator container let the banned client through"
wait_for "discriminator logged no request refused under the ban" \
logged "$DISCRIMINATOR" '"action":"temporarily_banned"'
logged "$DISCRIMINATOR" '"action":"banned"'
echo "example-app: a new discriminator container on the same volume keeps the ban"
docker rm --force "$DISCRIMINATOR" >/dev/null