#!/bin/bash
set -euo pipefail

# CVE-2026-33017 - Unauthenticated RCE in Langflow via public flow build endpoint
# Reproduction orchestrator. Runs 2 vulnerable + 2 fixed attempts against the
# real langflow Docker images and aggregates runtime evidence.

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"

# Tee everything into the dedicated log while still showing it on stdout/stderr.
exec > >(tee -a "$LOGS/reproduction_steps.log") 2>&1

VULN_IMAGE="langflowai/langflow:1.8.1"
FIXED_IMAGE="langflowai/langflow:1.9.0"
VULN_ATTEMPTS=2
FIXED_ATTEMPTS=2
PORT=7860

log() { echo "[repro] $*"; }

# ---- project cache context (reference; we use Docker images for the product) ----
CACHE_CTX="$ROOT/project_cache_context.json"
if [ -f "$CACHE_CTX" ]; then
  PREPARED=$(python3 -c "import json;print(json.load(open('$CACHE_CTX')).get('prepared',False))" 2>/dev/null || echo False)
  log "project cache prepared=$PREPARED (using Docker images for the real product)"
else
  log "no project_cache_context.json; using Docker images for the real product"
fi

pull_image() {
  local image="$1"
  if docker images --format '{{.Repository}}:{{.Tag}}' | grep -qx "$image"; then
    log "image $image already present"
    return 0
  fi
  log "pulling $image ..."
  docker pull "$image"
}

run_attempt() {
  local role="$1"
  local attempt="$2"
  local image="$3"
  local container="langflow-${role}-${attempt}"
  local token
  token=$(python3 -c 'import secrets; print(secrets.token_hex(8))')

  log "----------------------------------------------------------------------"
  log "attempt role=$role attempt=$attempt image=$image token=$token"
  log "----------------------------------------------------------------------"

  docker rm -f "$container" >/dev/null 2>&1 || true
  log "$ docker run -d --rm --name $container -e LANGFLOW_AUTO_LOGIN=true -e LANGFLOW_PORT=$PORT -e LANGFLOW_HOST=0.0.0.0 $image python -m langflow run --host 0.0.0.0 --port $PORT --backend-only --no-open-browser"
  docker run -d --rm --name "$container" \
    -e LANGFLOW_AUTO_LOGIN=true \
    -e LANGFLOW_PORT="$PORT" \
    -e LANGFLOW_HOST=0.0.0.0 \
    "$image" \
    python -m langflow run --host 0.0.0.0 --port "$PORT" --backend-only --no-open-browser \
    >"$LOGS/container_${role}_${attempt}.log" 2>&1

  # copy the exploit helper into the running container
  docker cp "$REPRO_DIR/repro_attempt.py" "$container:/tmp/repro_attempt.py"

  log "running exploit helper inside $container ..."
  set +e
  docker exec -e ROLE="$role" -e TOKEN="$token" "$container" python3 /tmp/repro_attempt.py \
    >"$LOGS/result_${role}_${attempt}.json" 2>"$LOGS/result_${role}_${attempt}_stderr.log"
  local rc=$?
  set -e
  log "attempt $role/$attempt rc=$rc"

  # persist the proof file (if any) out of the container for evidence
  docker cp "$container:/tmp/rce-proof" "$LOGS/proof_${role}_${attempt}.txt" >/dev/null 2>&1 || true
  # also capture container logs for this attempt
  docker logs "$container" >"$LOGS/container_${role}_${attempt}.log" 2>&1 || true

  docker rm -f "$container" >/dev/null 2>&1 || true
  return $rc
}

log "CVE-2026-33017 reproduction: unauthenticated RCE via /api/v1/build_public_tmp/{flow_id}/flow"
log "logs: $LOGS"

# sanity: docker must be available
if ! command -v docker >/dev/null 2>&1; then
  log "ERROR: docker not found in PATH"
  exit 2
fi

pull_image "$VULN_IMAGE"
pull_image "$FIXED_IMAGE"

vuln_ok=0
vuln_results=()
for i in $(seq 1 "$VULN_ATTEMPTS"); do
  if run_attempt "vuln" "$i" "$VULN_IMAGE"; then
    vuln_ok=$((vuln_ok + 1))
  fi
  vuln_results+=("$(cat "$LOGS/result_vuln_${i}.json" 2>/dev/null || echo '{}')")
done

fixed_ok=0
fixed_results=()
for i in $(seq 1 "$FIXED_ATTEMPTS"); do
  if run_attempt "fixed" "$i" "$FIXED_IMAGE"; then
    fixed_ok=$((fixed_ok + 1))
  fi
  fixed_results+=("$(cat "$LOGS/result_fixed_${i}.json" 2>/dev/null || echo '{}')")
done

log "----------------------------------------------------------------------"
log "RESULTS: vulnerable RCE successes=$vuln_ok/$VULN_ATTEMPTS  fixed closed=$((FIXED_ATTEMPTS - fixed_ok))/$FIXED_ATTEMPTS (fixed_ok=$fixed_ok must be 0)"
log "----------------------------------------------------------------------"

# Confirmed requires: every vulnerable attempt wrote the proof AND every fixed
# attempt did NOT write the proof (negative control fails closed).
if [ "$vuln_ok" -eq "$VULN_ATTEMPTS" ] && [ "$fixed_ok" -eq 0 ]; then
  OUTCOME="confirmed"
  TARGET_REACHED=true
  HEALTHCHECK=true
  SERVICE_STARTED=true
  NOTES="RCE confirmed on vulnerable langflow:1.8.1 (proof file written with id output + token); fixed langflow:1.9.0 closed the data parameter path (no proof)."
else
  OUTCOME="not_confirmed"
  TARGET_REACHED=false
  HEALTHCHECK=false
  SERVICE_STARTED=false
  NOTES="Could not reproduce the expected vulnerable/fixed divergence. vuln_ok=$vuln_ok fixed_ok=$fixed_ok"
fi
log "outcome=$OUTCOME"

# ---- proof artifacts list ----
PROOF_ARTIFACTS=()
for i in $(seq 1 "$VULN_ATTEMPTS"); do
  PROOF_ARTIFACTS+=("logs/proof_vuln_${i}.txt")
  PROOF_ARTIFACTS+=("logs/result_vuln_${i}.json")
  PROOF_ARTIFACTS+=("logs/result_vuln_${i}_stderr.log")
  PROOF_ARTIFACTS+=("logs/container_vuln_${i}.log")
done
for i in $(seq 1 "$FIXED_ATTEMPTS"); do
  PROOF_ARTIFACTS+=("logs/result_fixed_${i}.json")
  PROOF_ARTIFACTS+=("logs/result_fixed_${i}_stderr.log")
  PROOF_ARTIFACTS+=("logs/container_fixed_${i}.log")
  PROOF_ARTIFACTS+=("logs/proof_fixed_${i}.txt")
done
# Only keep artifacts that actually exist on disk (e.g. fixed attempts write no proof file).
EXISTING_ARTIFACTS=()
for a in "${PROOF_ARTIFACTS[@]}"; do
  if [ -f "$ROOT/$a" ]; then EXISTING_ARTIFACTS+=("$a"); fi
done
ARTIFACT_JSON=$(printf '%s\n' "${EXISTING_ARTIFACTS[@]}" | jq -R . | jq -s .)

# ---- runtime manifest ----
jq -n \
  --arg entrypoint_kind "api_remote" \
  --arg entrypoint_detail "/api/v1/build_public_tmp/{flow_id}/flow" \
  --argjson service_started "$SERVICE_STARTED" \
  --argjson healthcheck_passed "$HEALTHCHECK" \
  --argjson target_path_reached "$TARGET_REACHED" \
  --arg runtime_stack 'docker,langflow,fastapi' \
  --argjson proof_artifacts "$ARTIFACT_JSON" \
  --arg notes "$NOTES" \
  '{
      entrypoint_kind: $entrypoint_kind,
      entrypoint_detail: $entrypoint_detail,
      service_started: $service_started,
      healthcheck_passed: $healthcheck_passed,
      target_path_reached: $target_path_reached,
      runtime_stack: ($runtime_stack | split(",")),
      proof_artifacts: $proof_artifacts,
      notes: $notes
  }' > "$REPRO_DIR/runtime_manifest.json"
log "wrote runtime manifest -> $REPRO_DIR/runtime_manifest.json"

# ---- structured verdict ----
if [ "$OUTCOME" = "confirmed" ]; then
  CLAIM="confirmed"; REPRO="confirmed"; SCOPE="production_path"
  OBSERVED="code_execution"; CONF="high"
else
  CLAIM="unknown"; REPRO="not_confirmed"; SCOPE="production_path"
  OBSERVED="none"; CONF="unknown"
fi
jq -n \
  --arg claim_outcome "$CLAIM" \
  --argjson claim_block_reason null \
  --arg repro_result "$REPRO" \
  --arg validated_surface "api_remote" \
  --arg evidence_scope "$SCOPE" \
  --arg claimed_impact_class "code_execution" \
  --arg observed_impact_class "$OBSERVED" \
  --arg exploitability_confidence "$CONF" \
  --arg attacker_controlled_input "JSON body with malicious custom component code (top-level os.system) in POST /api/v1/build_public_tmp/{flow_id}/flow" \
  --arg trigger_path "/api/v1/build_public_tmp/{flow_id}/flow -> start_flow_build -> build_graph_from_data -> create_class -> prepare_global_scope -> exec" \
  '{
      claim_outcome: $claim_outcome,
      claim_block_reason: $claim_block_reason,
      repro_result: $repro_result,
      validated_surface: $validated_surface,
      evidence_scope: $evidence_scope,
      claimed_impact_class: $claimed_impact_class,
      observed_impact_class: $observed_impact_class,
      exploitability_confidence: $exploitability_confidence,
      attacker_controlled_input: $attacker_controlled_input,
      trigger_path: $trigger_path,
      end_to_end_target_reached: ($repro_result == "confirmed"),
      sanitizer_used: false,
      crash_observed: false,
      read_write_primitive_observed: false,
      exploit_chain_demonstrated: true,
      blocking_mitigation: null,
      inferred: false
  }' > "$REPRO_DIR/validation_verdict.json"
log "wrote verdict -> $REPRO_DIR/validation_verdict.json"

# ---- proof-carry cache (best-effort) ----
if [ -f "$CACHE_CTX" ]; then
  PC_DIR=$(python3 -c "import json,os; d=json.load(open('$CACHE_CTX')); print(os.path.join(d.get('project_cache_dir',''),'pruva','.pruva')) if d.get('prepared') else print('')" 2>/dev/null || echo "")
else
  PC_DIR=""
fi
if [ -n "$PC_DIR" ]; then
  LATEST="$PC_DIR/proof-carry/latest_attempt"
  mkdir -p "$LATEST/repro" "$LATEST/logs"
  cp -f "$REPRO_DIR/reproduction_steps.sh" "$REPRO_DIR/repro_attempt.py" "$REPRO_DIR/runtime_manifest.json" "$REPRO_DIR/validation_verdict.json" "$LATEST/repro/" 2>/dev/null || true
  cp -f "$LOGS/reproduction_steps.log" "$LATEST/logs/" 2>/dev/null || true
  for i in $(seq 1 "$VULN_ATTEMPTS"); do cp -f "$LOGS/result_vuln_${i}.json" "$LOGS/proof_vuln_${i}.txt" "$LATEST/logs/" 2>/dev/null || true; done
  for i in $(seq 1 "$FIXED_ATTEMPTS"); do cp -f "$LOGS/result_fixed_${i}.json" "$LOGS/proof_fixed_${i}.txt" "$LATEST/logs/" 2>/dev/null || true; done
  if [ "$OUTCOME" = "confirmed" ]; then
    CONFDIR="$PC_DIR/proof-carry/latest_confirmed"
    mkdir -p "$CONFDIR/repro" "$CONFDIR/logs"
    cp -f "$REPRO_DIR/reproduction_steps.sh" "$REPRO_DIR/repro_attempt.py" "$REPRO_DIR/runtime_manifest.json" "$REPRO_DIR/validation_verdict.json" "$CONFDIR/repro/" 2>/dev/null || true
    cp -f "$LOGS/reproduction_steps.log" "$CONFDIR/logs/" 2>/dev/null || true
    for i in $(seq 1 "$VULN_ATTEMPTS"); do cp -f "$LOGS/result_vuln_${i}.json" "$LOGS/proof_vuln_${i}.txt" "$CONFDIR/logs/" 2>/dev/null || true; done
    for i in $(seq 1 "$FIXED_ATTEMPTS"); do cp -f "$LOGS/result_fixed_${i}.json" "$LOGS/proof_fixed_${i}.txt" "$CONFDIR/logs/" 2>/dev/null || true; done
  fi
  log "proof-carry artifacts cached under $PC_DIR/proof-carry/"
fi

if [ "$OUTCOME" = "confirmed" ]; then
  log "VULNERABILITY CONFIRMED"
  exit 0
else
  log "VULNERABILITY NOT CONFIRMED"
  exit 1
fi
