#!/bin/bash
# =============================================================================
# verify_fix.sh — CVE-2026-20253 (SVD-2026-0603) Fix Verification
#
# Verifies the proposed fix for the Splunk Enterprise PostgreSQL sidecar
# vulnerability in two ways:
#
#   PART 1 (Static): Apply proposed_fix.diff to the reconstructed vulnerable
#     source tree and validate that the fix elements are present:
#       - sanitizeDatabase() rejects libpq connection-string injection
#       - validateBackupName() rejects path separators / absolute paths
#       - requireAuth middleware enforces Splunk token authentication
#       - Fixed service account (no -U from client Authorization header)
#
#   PART 2 (Runtime, if Docker available): Apply the [postgres] disabled=true
#     configuration mitigation to a real splunk/splunk:10.0.6 container,
#     restart, and verify the unauthenticated endpoint is blocked (HTTP 303
#     "See Other" — sidecar not running — instead of the vulnerable HTTP 400
#     "Failed to decode request" which means the sidecar processed the request).
#     Also verify no arbitrary file can be created.
#
# Idempotent: removes prior test containers/networks and re-creates the
# verification environment from scratch on each run.
# =============================================================================
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUNDLE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
FIX_DIFF="$SCRIPT_DIR/proposed_fix.diff"

# Color helpers
pass() { echo "[+] PASS: $*"; }
fail() { echo "[!] FAIL: $*"; }
info() { echo "[i] $*"; }

# Docker detection
if sudo -n true 2>/dev/null; then DOCKER="sudo docker"; else DOCKER="docker"; fi
HAS_DOCKER=false
if $DOCKER info >/dev/null 2>&1; then HAS_DOCKER=true; fi

RESULT_STATIC=false
RESULT_RUNTIME=false
RESULT_RUNTIME_SKIPPED=false

# =============================================================================
# PART 1: Static verification — apply patch and validate fix elements
# =============================================================================
info "=== PART 1: Static verification (source patch) ==="

WORKDIR="/tmp/cve2026_20253_verify"
rm -rf "$WORKDIR"
mkdir -p "$WORKDIR/postgres-service/internal/postgres" \
         "$WORKDIR/postgres-service/api"

# Copy the vulnerable (pre-fix) source into the workdir
cp "$BUNDLE_DIR/coding/src/postgres-service/internal/postgres/postgres_recovery_manager.go" \
   "$WORKDIR/postgres-service/internal/postgres/postgres_recovery_manager.go"
cp "$BUNDLE_DIR/coding/src/postgres-service/api/server.go" \
   "$WORKDIR/postgres-service/api/server.go"

info "Applying proposed_fix.diff to reconstructed source..."
cd "$WORKDIR"
if patch -p1 < "$FIX_DIFF" 2>&1; then
  pass "Patch applied cleanly"
else
  fail "Patch failed to apply"
  exit 1
fi

# Validate fix elements in postgres_recovery_manager.go
MGR="$WORKDIR/postgres-service/internal/postgres/postgres_recovery_manager.go"
SRV="$WORKDIR/postgres-service/api/server.go"

STATIC_OK=true

# 1. sanitizeDatabase function present
if grep -q "func sanitizeDatabase" "$MGR"; then
  pass "sanitizeDatabase() function present — rejects connection-string injection"
else
  fail "sanitizeDatabase() function missing"
  STATIC_OK=false
fi

# 2. sanitizeDatabase rejects key=value (connection string syntax)
if grep -q "validDatabaseName" "$MGR" && grep -q "regexp" "$MGR"; then
  pass "Database validation uses strict regex (alphanumeric + underscore only)"
else
  fail "Database validation missing or insufficient"
  STATIC_OK=false
fi

# 3. validateBackupName function present
if grep -q "func validateBackupName" "$MGR"; then
  pass "validateBackupName() function present — rejects path separators"
else
  fail "validateBackupName() function missing"
  STATIC_OK=false
fi

# 4. validateBackupName rejects path separators
if grep -q 'ContainsAny' "$MGR"; then
  pass "backupName validation rejects path separators (/ and \\)"
else
  fail "backupName path-separator check missing"
  STATIC_OK=false
fi

# 5. requireAuth middleware present in server.go
if grep -q "func.*requireAuth" "$SRV"; then
  pass "requireAuth() middleware present — enforces Splunk token authentication"
else
  fail "requireAuth() middleware missing"
  STATIC_OK=false
fi

# 6. Authentication rejects Basic-auth and missing credentials
if grep -q "must use Splunk token" "$SRV"; then
  pass "Authentication rejects Basic-auth and missing credentials (401)"
else
  fail "Authentication rejection message missing"
  STATIC_OK=false
fi

# 7. ExtractUserFromAuthHeader removed (fixed service account)
if grep -q "ExtractUserFromAuthHeader" "$MGR" || grep -q "ExtractUserFromAuthHeader" "$SRV"; then
  fail "ExtractUserFromAuthHeader still present — -U user still from client header"
  STATIC_OK=false
else
  pass "ExtractUserFromAuthHeader removed — fixed service account used for -U"
fi

# 8. Fixed service account: m.pgUser used instead of request user
if grep -q 'm\.pgUser' "$MGR" && ! grep -q 'user string' "$MGR"; then
  pass "Fixed service account (m.pgUser) used for pg_dump/pg_restore -U"
else
  fail "Service account hardening incomplete"
  STATIC_OK=false
fi

# 9. BackupRestoreRequest replaced by BackupRequest/RestoreRequest (split schemas)
if grep -q "type BackupRequest struct" "$MGR" && grep -q "type RestoreRequest struct" "$MGR"; then
  pass "Schema split: BackupRequest and RestoreRequest (backupFile → backupName)"
else
  fail "Schema split missing"
  STATIC_OK=false
fi

# 10. backupName field (not backupFile)
if grep -q 'BackupName.*json:"backupName"' "$MGR" && ! grep -q 'BackupFile.*json:"backupFile"' "$MGR"; then
  pass "backupFile field replaced by validated backupName"
else
  fail "backupFile still present or backupName missing"
  STATIC_OK=false
fi

# 11. server.conf mitigation file present
if [ -f "$WORKDIR/opt/splunk/etc/system/local/server.conf" ]; then
  if grep -q '\[postgres\]' "$WORKDIR/opt/splunk/etc/system/local/server.conf" && \
     grep -q 'disabled = true' "$WORKDIR/opt/splunk/etc/system/local/server.conf"; then
    pass "server.conf mitigation present ([postgres] disabled = true)"
  else
    fail "server.conf mitigation incomplete"
    STATIC_OK=false
  fi
else
  fail "server.conf mitigation file not created by patch"
  STATIC_OK=false
fi

if $STATIC_OK; then
  pass "PART 1 PASSED: All static fix elements verified"
  RESULT_STATIC=true
else
  fail "PART 1 FAILED: Some fix elements missing"
fi

# =============================================================================
# PART 2: Runtime verification (Docker) — config mitigation on real product
# =============================================================================
info ""
info "=== PART 2: Runtime verification (config mitigation on real product) ==="

if ! $HAS_DOCKER; then
  info "Docker not available — skipping runtime verification"
  RESULT_RUNTIME_SKIPPED=true
else
  VULN_IMG="splunk/splunk:10.0.6"
  ATTPG_IMG="postgres:16-alpine"
  NET="cve2026_verify_net"
  SPLUNK_PW="Splunk123!"
  VULN_CONTAINER="cve2026-verify-vuln"
  ATTPG_CONTAINER="cve2026-verify-att"

  info "Cleaning up any prior verification containers..."
  $DOCKER rm -f "$VULN_CONTAINER" "$ATTPG_CONTAINER" 2>/dev/null || true
  $DOCKER network rm "$NET" 2>/dev/null || true

  info "Ensuring images are present..."
  for img in "$VULN_IMG" "$ATTPG_IMG"; do
    $DOCKER image inspect "$img" >/dev/null 2>&1 || { info "pulling $img"; $DOCKER pull "$img"; }
  done

  info "Creating Docker network and attacker container..."
  $DOCKER network create "$NET" >/dev/null 2>&1 || true
  $DOCKER run -d --name "$ATTPG_CONTAINER" --network "$NET" \
    -e POSTGRES_USER=test -e POSTGRES_DB=testdb -e POSTGRES_HOST_AUTH_METHOD=trust \
    "$ATTPG_IMG" >/dev/null
  # Ensure curl is available in attacker container
  $DOCKER exec "$ATTPG_CONTAINER" sh -c "command -v curl >/dev/null 2>&1 || apk add --no-cache curl >/dev/null 2>&1" || true
  for i in $(seq 1 30); do
    $DOCKER exec "$ATTPG_CONTAINER" pg_isready -U test >/dev/null 2>&1 && break
    sleep 1
  done
  pass "Attacker container ready"

  info "Starting vulnerable Splunk Enterprise 10.0.6..."
  $DOCKER run -d --name "$VULN_CONTAINER" --hostname "$VULN_CONTAINER" --network "$NET" \
    -e SPLUNK_PASSWORD="$SPLUNK_PW" \
    -e SPLUNK_START_ARGS=--accept-license \
    -e SPLUNK_GENERAL_TERMS=--accept-sgt-current-at-splunk-com \
    "$VULN_IMG" >/dev/null

  # Wait for healthy
  info "Waiting for Splunk to become healthy..."
  VULN_HEALTHY=false
  for i in $(seq 1 48); do
    ST="$($DOCKER inspect --format '{{.State.Health.Status}}' "$VULN_CONTAINER" 2>/dev/null || echo "")"
    if [ "$ST" = "healthy" ]; then
      VULN_HEALTHY=true
      pass "Splunk 10.0.6 healthy"
      break
    fi
    sleep 10
  done
  if ! $VULN_HEALTHY; then
    fail "Splunk 10.0.6 did not become healthy"
    exit 1
  fi

  # Wait for sidecar endpoint to be reachable (vulnerable: 400 "Failed to decode")
  info "Waiting for PostgreSQL sidecar endpoint to register (vulnerable)..."
  SIDE_READY=false
  for i in $(seq 1 36); do
    CODE="$($DOCKER exec "$ATTPG_CONTAINER" sh -c "curl -sS -k -m 10 -X POST -H 'Authorization: Basic Og==' -o /tmp/v.txt -w '%{http_code}' 'http://$VULN_CONTAINER:8000/en-US/splunkd/__raw/v1/postgres/recovery/backup'" 2>/dev/null || echo 000)"
    BODY="$($DOCKER exec "$ATTPG_CONTAINER" sh -c "cat /tmp/v.txt 2>/dev/null | head -c 80" 2>/dev/null)"
    if [ "$CODE" = "400" ] && echo "$BODY" | grep -qi "Failed to decode"; then
      SIDE_READY=true
      pass "Vulnerable sidecar reachable: HTTP 400 'Failed to decode request' (confirms vulnerability)"
      break
    fi
    sleep 5
  done
  if ! $SIDE_READY; then
    fail "Sidecar endpoint not ready (last code=$CODE body=$BODY)"
    exit 1
  fi

  # --- Apply the configuration mitigation ---
  info "Applying configuration mitigation: [postgres] disabled = true"
  $DOCKER exec -u splunk "$VULN_CONTAINER" bash -c "
    echo '[postgres]
disabled = true' >> /opt/splunk/etc/system/local/server.conf
  " 2>/dev/null
  pass "server.conf mitigation applied"

  info "Restarting Splunk to apply mitigation..."
  $DOCKER exec -u splunk "$VULN_CONTAINER" bash -c "/opt/splunk/bin/splunk restart 2>&1 | tail -3" 2>/dev/null
  sleep 30

  # Verify Splunk is running after restart
  $DOCKER exec -u splunk "$VULN_CONTAINER" bash -c "/opt/splunk/bin/splunk status 2>&1 | head -2" 2>/dev/null

  # --- Verify the vulnerability is mitigated ---
  info "Verifying endpoint is blocked after mitigation..."
  BLOCKED=false
  for i in $(seq 1 24); do
    CODE="$($DOCKER exec "$ATTPG_CONTAINER" sh -c "curl -sS -k -m 10 -X POST -H 'Authorization: Basic Og==' -o /tmp/a.txt -w '%{http_code}' 'http://$VULN_CONTAINER:8000/en-US/splunkd/__raw/v1/postgres/recovery/backup'" 2>/dev/null || echo 000)"
    BODY="$($DOCKER exec "$ATTPG_CONTAINER" sh -c "cat /tmp/a.txt 2>/dev/null | head -c 80" 2>/dev/null)"
    info "Attempt $i: HTTP=$CODE body=$(echo "$BODY" | head -c 60)"
    # 303 = sidecar not registered (mitigated); 400 = sidecar reached (still vulnerable)
    if [ "$CODE" = "303" ] || [ "$CODE" = "404" ] || [ "$CODE" = "401" ] || [ "$CODE" = "403" ]; then
      BLOCKED=true
      break
    fi
    if [ "$CODE" = "400" ] && echo "$BODY" | grep -qi "Failed to decode"; then
      # Sidecar still running — mitigation not effective
      break
    fi
    sleep 10
  done

  if $BLOCKED; then
    pass "RUNTIME: Endpoint blocked (HTTP $CODE) — sidecar no longer reachable (mitigation effective)"
  else
    fail "RUNTIME: Endpoint still vulnerable (HTTP $CODE) — mitigation not effective"
  fi

  # Check sidecar process status (informational — the endpoint-blocked and
  # no-file-creation checks above are the definitive mitigation indicators).
  # The disabled=true setting stops the sidecar supervisor-managed process;
  # the PostgreSQL database server may linger briefly during shutdown.
  info "Checking PostgreSQL sidecar process status (informational)..."
  sleep 10
  SIDECAR_PROC="$($DOCKER exec -u splunk "$VULN_CONTAINER" bash -c "ps aux | grep 'pkg-run/pkg-postgres' | grep -v grep | wc -l" 2>/dev/null || echo "?")"
  if [ "${SIDECAR_PROC:-1}" = "0" ]; then
    pass "RUNTIME: PostgreSQL sidecar supervisor process stopped (disabled = true effective)"
  else
    info "RUNTIME: Sidecar process count=$SIDECAR_PROC (may be shutting down; endpoint already blocked)"
  fi

  # Verify no arbitrary file can be created
  info "Verifying no arbitrary file can be created via the endpoint..."
  TEST_FILE="/tmp/cve2026_verify_notruncate"
  $DOCKER exec -u splunk "$VULN_CONTAINER" bash -c "rm -f $TEST_FILE" 2>/dev/null || true
  BODY="{\"database\":\"hostaddr=127.0.0.1 dbname=testdb\",\"backupFile\":\"$TEST_FILE\"}"
  $DOCKER exec "$ATTPG_CONTAINER" sh -c "curl -sS -k -m 10 -X POST -H 'Authorization: Basic dGVzdDo=' -H 'Content-Type: application/json' -d '$BODY' -o /dev/null -w '%{http_code}' 'http://$VULN_CONTAINER:8000/en-US/splunkd/__raw/v1/postgres/recovery/backup'" 2>/dev/null || true
  sleep 3
  FILE_EXISTS="$($DOCKER exec -u splunk "$VULN_CONTAINER" bash -c "test -f $TEST_FILE && echo yes || echo no" 2>/dev/null || echo no)"
  if [ "$FILE_EXISTS" = "no" ]; then
    pass "RUNTIME: No arbitrary file created — file operations blocked"
  else
    fail "RUNTIME: File was created despite mitigation"
    BLOCKED=false
  fi

  if $BLOCKED; then
    pass "PART 2 PASSED: Runtime mitigation verified on real Splunk 10.0.6"
    RESULT_RUNTIME=true
  else
    fail "PART 2 FAILED: Runtime mitigation not fully effective"
  fi

  # Cleanup
  info "Cleaning up verification containers..."
  $DOCKER rm -f "$VULN_CONTAINER" "$ATTPG_CONTAINER" 2>/dev/null || true
  $DOCKER network rm "$NET" 2>/dev/null || true
fi

# =============================================================================
# Summary
# =============================================================================
echo ""
echo "=========================================="
echo "  CVE-2026-20253 Fix Verification Summary"
echo "=========================================="
echo "  PART 1 (Static source patch):  $(if $RESULT_STATIC; then echo 'PASS'; else echo 'FAIL'; fi)"
if $RESULT_RUNTIME_SKIPPED; then
  echo "  PART 2 (Runtime mitigation):   SKIP (no Docker)"
elif $RESULT_RUNTIME; then
  echo "  PART 2 (Runtime mitigation):   PASS"
else
  echo "  PART 2 (Runtime mitigation):   FAIL"
fi
echo "=========================================="

if $RESULT_STATIC && ( $RESULT_RUNTIME || $RESULT_RUNTIME_SKIPPED ); then
  pass "OVERALL: Fix verification PASSED"
  exit 0
else
  fail "OVERALL: Fix verification FAILED"
  exit 1
fi
