#!/usr/bin/env bash set -euo pipefail BASE_URL="${BASE_URL:-http://127.0.0.1:8080}" DB_PATH="${DB_PATH:-fixture/fixture.db}" RUN_ID="${RUN_ID:-$(date +%s)}" echo "[security-smoke] checking service readiness" curl -fsS -H 'X-WP-User: admin' "$BASE_URL/wp-json/calendar/v1/events" >/dev/null echo "[security-smoke] checking register token non-disclosure" register_email="sec-nodisclose-${RUN_ID}@example.test" reg_code="$(curl -sS -o /tmp/sec_register.json -w '%{http_code}' -H 'Content-Type: application/json' \ -d "{\"email\":\"${register_email}\",\"password\":\"strongpass123\"}" \ "$BASE_URL/wp-json/calendar/v1/users/register")" if [ "$reg_code" != "200" ] && [ "$reg_code" != "201" ] && [ "$reg_code" != "409" ]; then echo "[security-smoke] FAIL: unexpected register status $reg_code" exit 1 fi reg_payload="$(cat /tmp/sec_register.json)" if echo "$reg_payload" | grep -q 'verification_token_fixture'; then echo "[security-smoke] FAIL: register response leaked verification token" exit 1 fi echo "[security-smoke] checking forgot-password token non-disclosure" forgot_payload="$(curl -fsS -H 'Content-Type: application/json' \ -d '{"email":"adrians@chezstephens.org.uk"}' \ "$BASE_URL/wp-json/calendar/v1/users/forgot-password")" if echo "$forgot_payload" | grep -q 'reset_token_fixture'; then echo "[security-smoke] FAIL: forgot-password response leaked reset token" exit 1 fi echo "[security-smoke] checking register rate limiting" seen_429=0 rate_prefix="sec-rate-${RUN_ID}" for i in $(seq 1 10); do code="$(curl -sS -o /tmp/sec_reg_$i.json -w '%{http_code}' \ -H 'Content-Type: application/json' \ -d "{\"email\":\"${rate_prefix}-$i@example.test\",\"password\":\"strongpass123\"}" \ "$BASE_URL/wp-json/calendar/v1/users/register")" if [ "$code" = "429" ]; then seen_429=1 break fi done if [ "$seen_429" -ne 1 ]; then echo "[security-smoke] FAIL: expected 429 from register rate limiter" exit 1 fi echo "[security-smoke] checking password hash format in database" python3 - "$DB_PATH" <<'PY' import sqlite3 import sys db_path = sys.argv[1] conn = sqlite3.connect(db_path) cur = conn.cursor() rows = cur.execute("SELECT email, password_hash FROM caldav_users").fetchall() conn.close() if not rows: print("[security-smoke] FAIL: no users found") raise SystemExit(1) for email, pw_hash in rows: if not isinstance(pw_hash, str) or not pw_hash.startswith("pbkdf2_sha256$"): print(f"[security-smoke] FAIL: non-pbkdf2 hash for {email}: {pw_hash!r}") raise SystemExit(1) print("[security-smoke] password hash format OK") PY echo "[security-smoke] checking strict CalDAV resource filename semantics" resource="sec-$(date +%s).ics" cat > /tmp/sec_caldav.ics <<'ICS' BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Security Smoke//EN BEGIN:VEVENT UID:sec-smoke-uid@example.test SUMMARY:Security Smoke Event DTSTART:20260415T100000 DTEND:20260415T110000 END:VEVENT END:VCALENDAR ICS put_code="$(curl -sS -o /tmp/sec_caldav_put.json -w '%{http_code}' \ -u adrians@chezstephens.org.uk:brillig1 \ -X PUT --data-binary @/tmp/sec_caldav.ics \ "$BASE_URL/caldav/calendars/public/$resource")" if [ "$put_code" != "201" ] && [ "$put_code" != "200" ]; then echo "[security-smoke] FAIL: expected 200/201 for CalDAV PUT, got $put_code" exit 1 fi get_code="$(curl -sS -o /tmp/sec_caldav_get.ics -w '%{http_code}' \ -u adrians@chezstephens.org.uk:brillig1 \ "$BASE_URL/caldav/calendars/public/$resource")" if [ "$get_code" != "200" ]; then echo "[security-smoke] FAIL: expected 200 for CalDAV GET on same resource, got $get_code" exit 1 fi missing_code="$(curl -sS -o /tmp/sec_caldav_missing.out -w '%{http_code}' \ -u adrians@chezstephens.org.uk:brillig1 \ "$BASE_URL/caldav/calendars/public/999999.ics")" if [ "$missing_code" != "404" ]; then echo "[security-smoke] FAIL: expected 404 for unknown numeric resource, got $missing_code" exit 1 fi echo "[security-smoke] all checks passed"