Skip to content

Commit d616132

Browse files
committed
fix(ci): moves WAF smoke tests from gitignored hack/ to tracked scripts/
1 parent a283084 commit d616132

File tree

4 files changed

+116
-1
lines changed

4 files changed

+116
-1
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jobs:
1616
- run: pnpm install --frozen-lockfile
1717
- run: pnpm run typecheck
1818
- run: pnpm test
19+
- name: WAF smoke tests
20+
run: pnpm test:waf
1921
- name: Verify CSP hash
2022
run: node scripts/verify-csp-hash.mjs
2123
- name: Install Playwright browsers

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"test:watch": "vitest --config vitest.workspace.ts",
1313
"deploy": "wrangler deploy",
1414
"typecheck": "tsc --noEmit",
15-
"test:e2e": "E2E_PORT=$(node -e \"const s=require('net').createServer();s.listen(0,()=>{console.log(s.address().port);s.close()})\") playwright test"
15+
"test:e2e": "E2E_PORT=$(node -e \"const s=require('net').createServer();s.listen(0,()=>{console.log(s.address().port);s.close()})\") playwright test",
16+
"test:waf": "bash scripts/waf-smoke-test.sh"
1617
},
1718
"dependencies": {
1819
"@kobalte/core": "^0.13.11",

prek.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ pass_filenames = false
4040
always_run = true
4141
priority = 0
4242

43+
[[repos.hooks]]
44+
id = "waf"
45+
name = "WAF smoke tests"
46+
language = "system"
47+
entry = "pnpm test:waf"
48+
pass_filenames = false
49+
always_run = true
50+
priority = 0
51+
4352
[[repos.hooks]]
4453
id = "e2e"
4554
name = "Playwright E2E tests"

scripts/waf-smoke-test.sh

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env bash
2+
# WAF Smoke Tests — validates Cloudflare WAF rules for gh.gordoncode.dev
3+
#
4+
# Usage: pnpm test:waf
5+
#
6+
# Rules validated:
7+
# 1. Path Allowlist — blocks all paths except known SPA routes, /assets/*, /api/*
8+
# 2. Scanner User-Agents — challenges empty/malicious User-Agent strings
9+
# Rate limit rule exists but is not tested here (triggers a 10-minute IP block).
10+
11+
set -euo pipefail
12+
13+
BASE="https://gh.gordoncode.dev"
14+
PASS=0
15+
FAIL=0
16+
17+
assert_status() {
18+
local expected="$1" actual="$2" label="$3"
19+
if [[ "$actual" == "$expected" ]]; then
20+
echo " PASS [${actual}] ${label}"
21+
PASS=$((PASS + 1))
22+
else
23+
echo " FAIL [${actual}] ${label} (expected ${expected})"
24+
FAIL=$((FAIL + 1))
25+
fi
26+
}
27+
28+
fetch() {
29+
curl -s -o /dev/null -w "%{http_code}" "$@"
30+
}
31+
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
94+
95+
# ============================================================
96+
# Summary
97+
# ============================================================
98+
echo ""
99+
TOTAL=$((PASS + FAIL))
100+
echo "=== Results: ${PASS}/${TOTAL} passed, ${FAIL} failed ==="
101+
if [[ $FAIL -gt 0 ]]; then
102+
exit 1
103+
fi

0 commit comments

Comments
 (0)