check / check (push) Waiting to run
Every *.rules file in SWWAF_RULES_DIR not named with a leading dot is read at start, and again 2 seconds after the directory's last change. Each request is checked against the rules after the rate limits: log notes a match, block refuses with 403, ban refuses and bans the netblock for SWWAF_ATTACK_BAN_DURATION, made permanent by its next request or attack. path, query and uri are matched as the request line sent them; header:Host and header:Transfer-Encoding are refused. Bans gain a cause. The image ships 00-default.rules. Judgement call: a header sent twice is matched with its values joined by ", ". Judgement call: SWWAF_MAX_BAN_DURATION does not cap a ban for an attack. Not in this unit: offences for rule matches, with the error burst. Model: opus-5-5
144 lines
5.3 KiB
Bash
Executable File
144 lines
5.3 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/example-app: build the image, and on it the example app in
|
|
# deploy/example-app, then run the app's container with a volume for the
|
|
# state files and check that the health check passes, that a request is
|
|
# served through smallwebwaf, that a second one in a minute bans the
|
|
# client, that a probe for /.env bans another client, which its next
|
|
# request bans for good, that `sv stop` stops smallwebwaf in order, that
|
|
# `docker stop` stops the container without having to kill it, and that
|
|
# a new container on the same volume still refuses the banned client. The
|
|
# containers, the volume and both images are removed however the script
|
|
# ends. Building the app needs network access, for nixpkgs' binary cache.
|
|
# script/check does not run this.
|
|
set -eu
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|
|
|
# Named after this run, so that runs in other clones on the same host
|
|
# never touch each other's.
|
|
NAME="$("$SCRIPT_DIR/projectname")-example-$$"
|
|
IMAGE="$NAME-base"
|
|
APP_IMAGE="$NAME-app"
|
|
CONTAINER="$NAME"
|
|
VOLUME="$NAME-state"
|
|
|
|
cleanup() {
|
|
docker rm --force "$CONTAINER" >/dev/null 2>&1 || true
|
|
docker volume rm --force "$VOLUME" >/dev/null 2>&1 || true
|
|
docker rmi --force "$APP_IMAGE" "$IMAGE" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
fail() {
|
|
echo "example-app: $*; the container's output:" >&2
|
|
docker logs "$CONTAINER" >&2 || true
|
|
exit 1
|
|
}
|
|
|
|
# wait_for <what fails> <command>...: run the command every second until
|
|
# it succeeds, for at most a minute.
|
|
wait_for() {
|
|
failure="$1"
|
|
shift
|
|
tries=0
|
|
until "$@"; do
|
|
tries=$((tries + 1))
|
|
[ "$tries" -lt 60 ] || fail "$failure"
|
|
sleep 1
|
|
done
|
|
}
|
|
|
|
healthy() {
|
|
status="$(docker inspect --format '{{.State.Health.Status}}' "$CONTAINER")"
|
|
[ "$status" = healthy ]
|
|
}
|
|
|
|
# logged <text>...: a line of the container's output holds every text,
|
|
# in any order.
|
|
logged() {
|
|
lines="$(docker logs "$CONTAINER" 2>&1)"
|
|
for text in "$@"; do
|
|
lines="$(printf '%s\n' "$lines" | grep -F "$text")" || return 1
|
|
done
|
|
}
|
|
|
|
# start_container: run the app's container, with the state files on the
|
|
# volume and a rate limit of one request a minute, and wait until it is
|
|
# healthy.
|
|
start_container() {
|
|
docker run --detach --name "$CONTAINER" --publish 127.0.0.1::8080 \
|
|
--volume "$VOLUME:/var/lib/smallwebwaf" \
|
|
--env SWWAF_RATE_LIMIT_PER_MINUTE=1 \
|
|
"$APP_IMAGE" >/dev/null
|
|
wait_for "the health check did not pass" healthy
|
|
address="$(docker port "$CONTAINER" 8080/tcp)"
|
|
}
|
|
|
|
# refused: a request to the container gets 403, SWWAF_BAN_RESPONSE's
|
|
# default.
|
|
refused() {
|
|
code="$(curl --silent --output /dev/null --write-out '%{http_code}' \
|
|
--max-time 10 "http://$address/")" || true
|
|
[ "$code" = 403 ]
|
|
}
|
|
|
|
# refused_from <client> <path>: a request for path from client, as
|
|
# X-Forwarded-For names it, gets 403. smallwebwaf believes the header
|
|
# from docker's gateway, a private address.
|
|
refused_from() {
|
|
code="$(curl --silent --output /dev/null --write-out '%{http_code}' \
|
|
--max-time 10 --header "X-Forwarded-For: $1" "http://$address$2")" || true
|
|
[ "$code" = 403 ]
|
|
}
|
|
|
|
main() {
|
|
cd "$ROOT"
|
|
trap cleanup EXIT
|
|
trap 'exit 1' HUP INT TERM
|
|
|
|
docker build --no-cache -t "$IMAGE" .
|
|
docker build --no-cache --build-arg SMALLWEBWAF_IMAGE="$IMAGE" \
|
|
-t "$APP_IMAGE" deploy/example-app
|
|
|
|
docker volume create "$VOLUME" >/dev/null
|
|
start_container
|
|
echo "example-app: the health check passes"
|
|
|
|
page="$(curl --fail --silent --show-error --max-time 10 "http://$address/")" ||
|
|
fail "no answer on port 8080"
|
|
[ "$page" = "hello from the example app" ] || fail "port 8080 answered $page"
|
|
wait_for "smallwebwaf logged no request it forwarded" logged '"action":"forward"'
|
|
echo "example-app: smallwebwaf passes a request to the app and its answer back"
|
|
|
|
refused || fail "a second request in a minute was not refused"
|
|
wait_for "smallwebwaf logged no ban" logged '"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 "smallwebwaf logged no ban for the probe" \
|
|
logged '"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 '"ban_expires":"permanent"'
|
|
echo "example-app: a probe for /.env bans the client, its next request for good"
|
|
|
|
docker exec "$CONTAINER" sv stop smallwebwaf >/dev/null ||
|
|
fail "sv stop smallwebwaf failed"
|
|
wait_for "smallwebwaf did not stop in order" logged '"msg":"stopped"'
|
|
echo "example-app: sv stop stops smallwebwaf in order"
|
|
|
|
docker stop "$CONTAINER" >/dev/null
|
|
status="$(docker inspect --format '{{.State.ExitCode}}' "$CONTAINER")"
|
|
[ "$status" = 0 ] || fail "docker stop left exit status $status"
|
|
echo "example-app: docker stop stops the container in order"
|
|
|
|
docker rm "$CONTAINER" >/dev/null
|
|
start_container
|
|
refused || fail "the new container let the banned client through"
|
|
wait_for "smallwebwaf logged no request refused under the ban" \
|
|
logged '"action":"banned"'
|
|
echo "example-app: a new container on the same volume keeps the ban"
|
|
}
|
|
|
|
main "$@"
|