Skip to content

Commit 3b69689

Browse files
committed
perf(waf): parallelizes smoke tests with GNU parallel
Replaces sequential curl loops (81s) with GNU parallel (-j10, ~2s). Test specs become a pipe-delimited data array parsed by exported functions. Adds --max-time 10 per curl, --timeout 15 per job, command -v guard, and empty-output infrastructure failure detection.
1 parent 9222965 commit 3b69689

File tree

1 file changed

+98
-79
lines changed

1 file changed

+98
-79
lines changed

scripts/waf-smoke-test.sh

Lines changed: 98 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env bash
22
# WAF Smoke Tests — validates Cloudflare WAF rules for gh.gordoncode.dev
3+
# Requires: GNU parallel (brew install parallel / apt install parallel)
34
#
45
# Usage: pnpm test:waf
56
#
@@ -10,94 +11,112 @@
1011

1112
set -euo pipefail
1213

14+
if ! command -v parallel &>/dev/null; then
15+
printf 'Error: GNU parallel is required (brew install parallel / apt install parallel)\n' >&2
16+
exit 1
17+
fi
18+
1319
BASE="https://gh.gordoncode.dev"
14-
PASS=0
15-
FAIL=0
1620

17-
assert_status() {
18-
local expected="$1" actual="$2" label="$3"
21+
# --- Test runner (exported for GNU parallel) ---
22+
run_test() {
23+
local expected="$1" label="$2"
24+
shift 2
25+
local actual
26+
actual=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 "$@")
1927
if [[ "$actual" == "$expected" ]]; then
20-
echo " PASS [${actual}] ${label}"
21-
PASS=$((PASS + 1))
28+
printf ' PASS [%s] %s\n' "$actual" "$label"
2229
else
23-
echo " FAIL [${actual}] ${label} (expected ${expected})"
24-
FAIL=$((FAIL + 1))
30+
printf ' FAIL [%s] %s (expected %s)\n' "$actual" "$label" "$expected"
31+
return 1
2532
fi
2633
}
27-
28-
fetch() {
29-
curl -s -o /dev/null -w "%{http_code}" "$@"
34+
export -f run_test
35+
36+
# --- Test spec parser (exported for GNU parallel) ---
37+
# Splits pipe-delimited spec into: expected_status | label | curl_args...
38+
run_spec() {
39+
local expected label
40+
IFS='|' read -ra parts <<< "$1"
41+
expected="${parts[0]}"
42+
label="${parts[1]}"
43+
run_test "$expected" "$label" "${parts[@]:2}"
3044
}
45+
export -f run_spec
46+
47+
# --- Test specs: expected_status | label | curl args ... ---
48+
# Pipe-delimited. Fields after label are passed directly to curl.
49+
TESTS=(
50+
# Rule 1: Path Allowlist — allowed paths
51+
"200|GET /|${BASE}/"
52+
"200|GET /login|${BASE}/login"
53+
"200|GET /oauth/callback|${BASE}/oauth/callback"
54+
"200|GET /onboarding|${BASE}/onboarding"
55+
"200|GET /dashboard|${BASE}/dashboard"
56+
"200|GET /settings|${BASE}/settings"
57+
"200|GET /privacy|${BASE}/privacy"
58+
"307|GET /index.html (html_handling redirect)|${BASE}/index.html"
59+
"200|GET /assets/nonexistent.js|${BASE}/assets/nonexistent.js"
60+
"200|GET /api/health|${BASE}/api/health"
61+
"400|POST /api/oauth/token (no body)|-X|POST|${BASE}/api/oauth/token"
62+
"404|GET /api/nonexistent|${BASE}/api/nonexistent"
63+
# Rule 1: Path Allowlist — blocked paths
64+
"403|GET /wp-admin|${BASE}/wp-admin"
65+
"403|GET /wp-login.php|${BASE}/wp-login.php"
66+
"403|GET /.env|${BASE}/.env"
67+
"403|GET /.env.production|${BASE}/.env.production"
68+
"403|GET /.git/config|${BASE}/.git/config"
69+
"403|GET /.git/HEAD|${BASE}/.git/HEAD"
70+
"403|GET /xmlrpc.php|${BASE}/xmlrpc.php"
71+
"403|GET /phpmyadmin/|${BASE}/phpmyadmin/"
72+
"403|GET /phpMyAdmin/|${BASE}/phpMyAdmin/"
73+
"403|GET /.htaccess|${BASE}/.htaccess"
74+
"403|GET /.htpasswd|${BASE}/.htpasswd"
75+
"403|GET /cgi-bin/|${BASE}/cgi-bin/"
76+
"403|GET /admin/|${BASE}/admin/"
77+
"403|GET /wp-content/debug.log|${BASE}/wp-content/debug.log"
78+
"403|GET /config.php|${BASE}/config.php"
79+
"403|GET /backup.zip|${BASE}/backup.zip"
80+
"403|GET /actuator/health|${BASE}/actuator/health"
81+
"403|GET /manager/html|${BASE}/manager/html"
82+
"403|GET /wp-config.php|${BASE}/wp-config.php"
83+
"403|GET /eval-stdin.php|${BASE}/eval-stdin.php"
84+
"403|GET /.aws/credentials|${BASE}/.aws/credentials"
85+
"403|GET /.ssh/id_rsa|${BASE}/.ssh/id_rsa"
86+
"403|GET /robots.txt|${BASE}/robots.txt"
87+
"403|GET /sitemap.xml|${BASE}/sitemap.xml"
88+
"403|GET /favicon.ico|${BASE}/favicon.ico"
89+
"403|GET /random/garbage/path|${BASE}/random/garbage/path"
90+
# Rule 2: Scanner User-Agents — normal UAs
91+
"200|Normal browser UA|-H|User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36|${BASE}/"
92+
"200|Default curl UA|${BASE}/"
93+
# Rule 2: Scanner User-Agents — malicious UAs
94+
"403|Empty User-Agent|-H|User-Agent:|${BASE}/"
95+
"403|UA: sqlmap/1.7|-H|User-Agent: sqlmap/1.7|${BASE}/"
96+
"403|UA: Nikto/2.1.6|-H|User-Agent: Nikto/2.1.6|${BASE}/"
97+
"403|UA: Nmap Scripting Engine|-H|User-Agent: Nmap Scripting Engine|${BASE}/"
98+
"403|UA: masscan/1.3|-H|User-Agent: masscan/1.3|${BASE}/"
99+
"403|UA: Mozilla/5.0 zgrab/0.x|-H|User-Agent: Mozilla/5.0 zgrab/0.x|${BASE}/"
100+
)
101+
102+
# --- Run in parallel (::: passes array elements directly, avoiding stdin quoting issues) ---
103+
TOTAL=${#TESTS[@]}
104+
105+
OUTPUT=$(parallel --will-cite -k -j10 --timeout 15 run_spec ::: "${TESTS[@]}") || true
106+
107+
# Detect infrastructure failure (parallel crashed, no tests ran)
108+
if [[ -z "$OUTPUT" ]]; then
109+
printf 'Error: test harness produced no output — parallel may have failed\n' >&2
110+
exit 2
111+
fi
31112

32-
# ============================================================
33-
# Rule 1: Path Allowlist
34-
# ============================================================
35-
echo "=== Rule 1: Path Allowlist ==="
36-
echo "--- Allowed paths (should pass) ---"
37-
38-
for path in "/" "/login" "/oauth/callback" "/onboarding" "/dashboard" "/settings" "/privacy"; do
39-
status=$(fetch "${BASE}${path}")
40-
assert_status "200" "$status" "GET ${path}"
41-
done
42-
43-
status=$(fetch "${BASE}/index.html")
44-
assert_status "307" "$status" "GET /index.html (html_handling redirect)"
45-
46-
status=$(fetch "${BASE}/assets/nonexistent.js")
47-
assert_status "200" "$status" "GET /assets/nonexistent.js"
48-
49-
status=$(fetch "${BASE}/api/health")
50-
assert_status "200" "$status" "GET /api/health"
51-
52-
status=$(fetch -X POST "${BASE}/api/oauth/token")
53-
assert_status "400" "$status" "POST /api/oauth/token (no body)"
54-
55-
status=$(fetch "${BASE}/api/nonexistent")
56-
assert_status "404" "$status" "GET /api/nonexistent"
57-
58-
echo "--- Blocked paths (should be 403) ---"
59-
60-
for path in "/wp-admin" "/wp-login.php" "/.env" "/.env.production" \
61-
"/.git/config" "/.git/HEAD" "/xmlrpc.php" \
62-
"/phpmyadmin/" "/phpMyAdmin/" "/.htaccess" "/.htpasswd" \
63-
"/cgi-bin/" "/admin/" "/wp-content/debug.log" \
64-
"/config.php" "/backup.zip" "/actuator/health" \
65-
"/manager/html" "/wp-config.php" "/eval-stdin.php" \
66-
"/.aws/credentials" "/.ssh/id_rsa" "/robots.txt" \
67-
"/sitemap.xml" "/favicon.ico" "/random/garbage/path"; do
68-
status=$(fetch "${BASE}${path}")
69-
assert_status "403" "$status" "GET ${path}"
70-
done
71-
72-
# ============================================================
73-
# Rule 2: Scanner User-Agents
74-
# ============================================================
75-
echo ""
76-
echo "=== Rule 2: Scanner User-Agents ==="
77-
echo "--- Normal UAs (should pass) ---"
78-
79-
status=$(fetch -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" "${BASE}/")
80-
assert_status "200" "$status" "Normal browser UA"
81-
82-
status=$(fetch "${BASE}/")
83-
assert_status "200" "$status" "Default curl UA"
84-
85-
echo "--- Malicious UAs (should be 403 — managed challenge, no JS) ---"
86-
87-
status=$(fetch -H "User-Agent:" "${BASE}/")
88-
assert_status "403" "$status" "Empty User-Agent"
89-
90-
for ua in "sqlmap/1.7" "Nikto/2.1.6" "Nmap Scripting Engine" "masscan/1.3" "Mozilla/5.0 zgrab/0.x"; do
91-
status=$(fetch -H "User-Agent: ${ua}" "${BASE}/")
92-
assert_status "403" "$status" "UA: ${ua}"
93-
done
113+
# Count results from output
114+
PASS=$(grep -c "^ PASS" <<< "$OUTPUT" || true)
115+
FAIL=$((TOTAL - PASS))
94116

95-
# ============================================================
96-
# Summary
97-
# ============================================================
98-
echo ""
99-
TOTAL=$((PASS + FAIL))
100-
echo "=== Results: ${PASS}/${TOTAL} passed, ${FAIL} failed ==="
117+
# Print results (preserving order from parallel -k)
118+
printf '%s\n' "$OUTPUT"
119+
printf '\n=== Results: %d/%d passed, %d failed ===\n' "$PASS" "$TOTAL" "$FAIL"
101120
if [[ $FAIL -gt 0 ]]; then
102121
exit 1
103122
fi

0 commit comments

Comments
 (0)