SPEC: the one place for behaviour and reasons; docs/ as lookup lists only
check / check (push) Waiting to run

docs/core-rule-set.md, docs/lookup.md, docs/reputation.md and docs/bans.md
restated the spec and are folded back into it and removed. The remaining
docs files hold only lists: settings with their defaults and value forms,
the request log's fields, the metrics, the endpoints, the state files and
their contents, the rule file format and default file, the alert events
and messages, the image's contents and pins, and the gitea notes. Every
behaviour each of them held moves to the spec, said once. The settings
groups have the same names in both files. Stale comments in
internal/state and script/example-app point at the right places.

Model: fable-5-1
This commit is contained in:
2026-10-22 21:51:39 +00:00
parent d712bb1496
commit 4ea0a975c8
17 changed files with 918 additions and 1273 deletions
+12 -59
View File
@@ -1,10 +1,8 @@
# Rule files
`smallwebwaf` reads every `*.rules` file in `SWWAF_RULES_DIR`, in the order of
their names, and checks each request against their rules in that order, unless
the client is in `SWWAF_ALLOW_NETS`. The requirements the files meet are under
"Rule files" in [`SPEC.md`](../SPEC.md). A file whose name starts with `.`, such
as an editor's lock file, is not a rule file.
The format of a rule file, and the default file the image ships. "Rule files" in
[`SPEC.md`](../SPEC.md) says how the files are read, watched and mounted, and
what each action leads to.
## The format
@@ -18,7 +16,7 @@ env-file path ban (?i)^/\.env(\.[a-z]+)?$
scanner-agent user_agent ban (?i)\b(sqlmap|nikto|nuclei|wpscan)\b
```
- The id is letters, digits, `-` and `_`. No two rules share one. The log line,
- The id is letters, digits, `-` and `_`, unique across all files. The log line,
the metrics, the alerts and a ban's notes name the rule by it.
- The target is what the regex is matched against:
- `path`: the path without the query;
@@ -26,42 +24,10 @@ scanner-agent user_agent ban (?i)\b(sqlmap|nikto|nuclei|wpscan)\b
- `query`: the query as sent;
- `method`, `host`, `user_agent` and `referer`;
- `header:<Name>`: any one request header but `Host` and
`Transfer-Encoding`, which Go's HTTP server takes out of every request.
`path` and `uri` are matched both as sent and once percent-decoded, so that
an encoded probe does not slip past. A header sent more than once is matched
with its values joined by `, `. A header not sent is empty text. No body is
read; reading bodies is the Core Rule Set's job, once `SWWAF_WAF_BODY_LIMIT`
switches it on.
- The action is `log`, `block` or `ban`. `log` only notes the match in the log
line. `block` refuses the request with `403` and bans no one; the refusal
counts toward the error burst. `ban` refuses it and bans the client, as a
clear sign of attack. Matching stops at the first rule that refuses. Keep
`ban` for requests no real visitor sends. Anchor a path at the site root with
`^/`: a file of the same name deeper in a site can be ordinary content.
- The regex is in Go's syntax, RE2. It has no backreferences or lookaround, and
takes time linear in the text it reads, so no rule can be made to stall the
proxy. It matches anywhere in the target unless anchored with `^` and `$`.
`(?i)` at its front makes it ignore case.
## Errors
These stop the start, with a message naming the file and line:
- a line that is not a rule;
- a bad `header:` name, or a rule for `Host` or `Transfer-Encoding`;
- a regex that does not compile;
- an id used twice.
So does a `SWWAF_RULES_DIR` that does not exist. An empty directory is not an
error: the log says that no rules were loaded.
While it runs, `smallwebwaf` watches the directory. It reads the files again
once the directory has had no change for 2 seconds, so that a file still being
written is read whole. If the files then hold such an error, the rules stay as
they were. The log and a `file_error` alert name the file and line. Once the
file is fixed, it is read again.
`Transfer-Encoding`.
- The action is `log`, `block` or `ban`.
- The regex is in Go's syntax, RE2. It matches anywhere in the target unless
anchored with `^` and `$`. `(?i)` at its front makes it ignore case.
## The default file
@@ -86,13 +52,10 @@ path-traversal uri block (\.\./){2,}
empty-agent user_agent log ^$
```
It is kept short and limited to patterns that are wrong for every app, with its
path rules anchored at the site root. In front of gitea, `/.env` is a probe,
while `/<owner>/<repo>/src/branch/main/.env.example` is a file in a repository
that any visitor may open. Anything app-specific belongs in a file the app's
`Dockerfile` adds or the deployer mounts. A request for `/wp-login.php`, for
example, is a clear sign of attack in front of gitea and an ordinary login in
front of WordPress. Such a rule goes in a file named to sort after the default:
## A file of your own
An app's `Dockerfile` adds rules of its own in a file named to sort after the
default, such as `50-gitea.rules`:
```
# 50-gitea.rules: WordPress probes, which gitea never serves
@@ -102,13 +65,3 @@ wp-probe path ban (?i)^/(wp-login\.php|xmlrpc\.php|wp-admin/)
```dockerfile
COPY 50-gitea.rules /etc/smallwebwaf/rules.d/50-gitea.rules
```
## Mounting rule files
A directory mounted over `/etc/smallwebwaf/rules.d` replaces the default file;
single files mounted into it add to it. Many editors save a file by replacing
it, and a container keeps seeing the old one when the file alone is mounted. So
rules to be edited while `smallwebwaf` runs belong in a mounted directory, with
a copy of the default file if the defaults are to stay. The lookup database is
mounted the same way, for the same reason. To run without rules, mount an empty
directory or set `SWWAF_RULES_ENABLED=false`.