#!/usr/bin/env bash
# =============================================================================
# onx-modsec-install-driver — Aktif (veya belirtilen) webserver için ModSec kur.
#
# Drivers:
#   apache → mod_security + mod_security_crs (zaten kurulu olabilir)
#   ols    → built-in mod_security aktif et (lsws config)
#   nginx  → nginx-mod-modsecurity (EPEL/upstream repo)
#   caddy  → Coraza WAF custom build via xcaddy + coraza-caddy plugin (v3.26)
#            Go 1.21+ ve xcaddy yoksa graceful refuse (requires_manual=true).
#
# Input (stdin JSON):
#   { "driver": "auto|apache|ols|nginx|caddy" }
#
# Output (stdout JSON):
#   { "driver": "...", "installed": true|false, "requires_manual": true|false,
#     "actions": [...], "errors": [...] }
# =============================================================================

set -euo pipefail

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
source "${SCRIPT_DIR}/_lib/common.sh"

require_root

INPUT_RAW="$(cat 2>/dev/null || echo '{}')"
DRIVER=$(onx_json_get "${INPUT_RAW}" "driver" "auto")

# Auto-detect aktif driver
if [[ "${DRIVER}" == "auto" ]]; then
    for ws_pair in "httpd:apache" "openlitespeed:ols" "lshttpd:ols" "nginx:nginx" "caddy:caddy"; do
        unit="${ws_pair%%:*}"
        key="${ws_pair##*:}"
        if systemctl is-active --quiet "$unit" 2>/dev/null; then
            DRIVER="$key"
            break
        fi
    done
fi

case "${DRIVER}" in
    apache|ols|nginx|caddy) : ;;
    *) onx_die 1 "Geçersiz driver: ${DRIVER} (apache|ols|nginx|caddy)" ;;
esac

ACTIONS=()
ERRORS=()
INSTALLED="false"
REQUIRES_MANUAL="false"

onx_log "modsec-install-driver: driver=${DRIVER}"

# Shared CRS rules directory (driver-agnostic)
SHARED_RULES="/etc/onoxsoft/modsec-rules"
mkdir -p "${SHARED_RULES}" 2>/dev/null || true

# v3.18: crs-setup.conf eksikse .example'dan kopyala (standart ModSec setup)
# Apache mod_security_crs package /etc/httpd/modsecurity.d/owasp-crs/crs-setup.conf.example
# bırakır ama .conf'a kopyalamaz — admin manuel yapmalı. Otomatize ediyoruz.
CRS_DIR_APACHE="/etc/httpd/modsecurity.d/owasp-crs"
if [[ -f "${CRS_DIR_APACHE}/crs-setup.conf.example" ]] && [[ ! -f "${CRS_DIR_APACHE}/crs-setup.conf" ]]; then
    cp -a "${CRS_DIR_APACHE}/crs-setup.conf.example" "${CRS_DIR_APACHE}/crs-setup.conf"
    chmod 0644 "${CRS_DIR_APACHE}/crs-setup.conf"
    ACTIONS+=("crs-setup.conf .example'dan kopyalandı")
fi

# ── Phase 2 (Nginx/OLS): Shared CRS v3.3.x download ──────────────────────────
# libmodsecurity v3 ile uyumlu CRS branch'i (CRS 4.x v2 syntax'ı kaldırdı, hala
# bazı rule'lar v3'te crash ediyor — 3.3.x stable). Apache mod_security_crs
# package'i v3.x kullanır ama bu /etc/httpd/modsecurity.d/owasp-crs altında —
# Nginx/OLS için ayrı, paylaşımlı path: /etc/onoxsoft/modsec-rules/owasp-crs-v3
# Tek download, hem Nginx hem OLS aynı path'i Include eder.
SHARED_CRS_V3="${SHARED_RULES}/owasp-crs-v3"
ensure_shared_crs_v3() {
    # Idempotent: zaten varsa skip (CRS update script ayrı flow)
    if [[ -d "${SHARED_CRS_V3}/rules" ]] && [[ -f "${SHARED_CRS_V3}/crs-setup.conf" ]]; then
        return 0
    fi

    if ! command -v curl >/dev/null 2>&1; then
        ERRORS+=("curl yok — shared CRS v3 indirilemiyor")
        return 1
    fi

    local tmpdir tarball extract_dir
    tmpdir=$(mktemp -d)
    tarball="${tmpdir}/crs.tar.gz"
    extract_dir="${tmpdir}/extract"
    mkdir -p "${extract_dir}"

    # CRS 3.3.7 — v3-compat stable branch (4.x bazı rule'lar libmodsecurity v3'te crash)
    local crs_url="https://github.com/coreruleset/coreruleset/archive/refs/tags/v3.3.7.tar.gz"
    if ! curl -sSL --max-time 180 -o "${tarball}" "${crs_url}" 2>/dev/null; then
        rm -rf "${tmpdir}"
        ERRORS+=("CRS v3.3.7 indirilemedi: ${crs_url}")
        return 1
    fi

    if ! tar -xzf "${tarball}" -C "${extract_dir}" --strip-components=1 2>/dev/null; then
        rm -rf "${tmpdir}"
        ERRORS+=("CRS tarball extract başarısız")
        return 1
    fi

    if [[ ! -d "${extract_dir}/rules" ]]; then
        rm -rf "${tmpdir}"
        ERRORS+=("CRS tarball bozuk (rules/ yok)")
        return 1
    fi

    mkdir -p "${SHARED_CRS_V3}"
    cp -a "${extract_dir}/rules" "${SHARED_CRS_V3}/"
    [[ -f "${extract_dir}/crs-setup.conf.example" ]] && \
        cp -a "${extract_dir}/crs-setup.conf.example" "${SHARED_CRS_V3}/crs-setup.conf"
    for f in CHANGES.md CHANGES VERSION LICENSE README.md; do
        [[ -f "${extract_dir}/${f}" ]] && cp -a "${extract_dir}/${f}" "${SHARED_CRS_V3}/"
    done
    chmod -R 0644 "${SHARED_CRS_V3}"/*.conf "${SHARED_CRS_V3}"/rules/*.conf 2>/dev/null || true
    find "${SHARED_CRS_V3}" -type d -exec chmod 0755 {} \; 2>/dev/null || true
    rm -rf "${tmpdir}"
    ACTIONS+=("CRS v3.3.7 indirildi → ${SHARED_CRS_V3}")
    return 0
}

# ── Neutral panel-managed ModSec layer (driver-agnostik) ─────────────────────
# v3.41: Tüm v3 driver'lar (Nginx/OLS/Caddy) ${NEUTRAL_DIR}/*.conf'u crs-setup ile
# rules ARASINDA Include eder. onx-modsec-enable/whitelist/rule-toggle/custom-rule
# buraya yazar; web sunucusu değişse bile panel-managed state KORUNUR (switch
# sonrası WebServerSwitchJob bu install-driver'ı yeniden çağırır, neutral layer
# zaten yerinde olduğu için yeni driver kaldığı yerden devam eder).
# Exclusion'lar ctl:ruleRemoveById (RUNTIME) kullanır → load-order bağımsız.
NEUTRAL_DIR="${ONX_MODSEC_NEUTRAL_DIR}/${ONX_MODSEC_NEUTRAL_SUBDIR}"
ensure_neutral_layer() {
    mkdir -p "${NEUTRAL_DIR}" 2>/dev/null || true
    chmod 0755 "${ONX_MODSEC_NEUTRAL_DIR}" "${NEUTRAL_DIR}" 2>/dev/null || true
    # Engine defaults — yalnızca YOKSA seed et (admin ayarlarını/switch state'i ezme).
    if [[ ! -f "${NEUTRAL_DIR}/00-engine.conf" ]]; then
        cat > "${NEUTRAL_DIR}/00-engine.conf" <<'NEUTRAL_EOF'
# Onoxsoft Panel managed — DO NOT EDIT (UI: Admin > ModSecurity > Settings)
# Driver-agnostik: tüm web sunucuları Include eder (crs-setup'tan SONRA).
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecRequestBodyLimitAction Reject
SecAuditEngine RelevantOnly
SecAuditLogParts ABIJDEFHZ
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
# Paranoia: id 900100 (CRS'in 900000'i ile çakışmaz; crs-setup sonrası override)
SecAction "id:900100,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=1,setvar:tx.blocking_paranoia_level=1,setvar:tx.detection_paranoia_level=1"
NEUTRAL_EOF
        chmod 0644 "${NEUTRAL_DIR}/00-engine.conf"
        ACTIONS+=("Neutral engine defaults seeded: ${NEUTRAL_DIR}/00-engine.conf")
    else
        ACTIONS+=("Neutral layer mevcut (state korundu): ${NEUTRAL_DIR}")
    fi
}

case "${DRIVER}" in
    # ─── APACHE ─────────────────────────────────────────────────────────────
    apache)
        if ! rpm -q mod_security >/dev/null 2>&1; then
            if dnf install -y mod_security mod_security_crs >/dev/null 2>&1; then
                ACTIONS+=("Apache mod_security + mod_security_crs kuruldu")
                INSTALLED="true"
            else
                ERRORS+=("dnf install mod_security başarısız")
            fi
        else
            ACTIONS+=("Apache mod_security zaten kurulu")
            INSTALLED="true"
        fi

        # Verify
        if command -v httpd >/dev/null && httpd -M 2>&1 | grep -qiE 'security2_module'; then
            ACTIONS+=("security2_module httpd'ye yüklenmiş")
        fi
        ;;

    # ─── OPENLITESPEED ──────────────────────────────────────────────────────
    # v3.19 SYNTAX FIX:
    # Eski v3.16 format `module modSecurity { ... }` OLS tarafından ignore ediliyordu
    # → OLS başlatılıyor ama ModSec aktif değil (audit log 0 byte kalıyor).
    # Doğru format: top-level directives + backtick-wrapped modsecurityRules.
    #
    # v3.26 CASE FIX:
    # OLS 1.7+ parser tüm directive'leri LOWERCASE bekler. v3.19'un camelCase
    # `modSecurity` / `modSecurityRules` formu sessizce ignore ediliyordu —
    # SecRuleEngine etkin olsa bile rule'lar yüklenmiyor, audit log 0 byte.
    # Doğru form: lowercase `modsecurity` + `modsecurityRules`.
    #
    # v3.26 CRS v3 PATH FIX:
    # OLS libmodsecurity v3.0.14 kullanır. Apache CRS 4.x rule'ları v2 syntax
    # (secmarker/secaction/skipAfter/t:none chains) kullanır → v3'te "Not support"
    # warning ile ~%30 rule sessizce skip edilir. Çözüm: ayrı `owasp-crs-v3/`
    # dizinine CRS 3.3.x indir (v3 compatible son major) — Apache CRS 4.x ile
    # çakışmaz, driver bağımsız.
    # Ref: https://openlitespeed.org/kb/modsecurity-configuration/
    ols)
        OLS_CONF="/usr/local/lsws/conf/httpd_config.conf"
        if [[ ! -f "${OLS_CONF}" ]]; then
            onx_die 2 "OLS config bulunamadı: ${OLS_CONF}"
        fi

        # Backup (her run)
        cp -a "${OLS_CONF}" "${OLS_CONF}.bak-$(date +%Y%m%d%H%M%S)"

        # OLS de libmodsecurity v3 — shared CRS v3 indirildiğinden emin ol
        ensure_shared_crs_v3 || true

        # CRS rules path (önce shared v3, sonra Apache fallback)
        if [[ -d "${SHARED_CRS_V3}/rules" ]]; then
            CRS_DIR="${SHARED_CRS_V3}"
        elif [[ -d "/etc/httpd/modsecurity.d/owasp-crs/rules" ]]; then
            CRS_DIR="/etc/httpd/modsecurity.d/owasp-crs"
        else
            CRS_DIR="${SHARED_RULES}/owasp-crs"
        fi

        # Eski v3.16 yanlış bloğu varsa SİL (marker pattern: # ─── ONOXSOFT ModSecurity)
        # ya da eski "module modSecurity {...}" formu varsa
        BEGIN_MARK="# ─── ONOXSOFT ModSecurity"
        END_MARK="# ─── /ONOXSOFT ModSecurity ─"
        if grep -qF "${BEGIN_MARK}" "${OLS_CONF}"; then
            # Marker'lı blok varsa BEGIN→END arasını sil (sed range delete).
            # v3.41 FIX: END_MARK '/' içeriyor ("# ─── /ONOXSOFT...") → '/' sed adres
            # ayracını erken kapatıp "unknown command: O" hatası veriyordu. '|' ayracı
            # (\|...|) kullan → '/' literal kalır.
            sed -i "\|${BEGIN_MARK}|,\|${END_MARK}|d" "${OLS_CONF}"
            ACTIONS+=("Önceki ONOXSOFT ModSec bloğu temizlendi (yeniden yazılacak)")
        fi
        # Eski v3.16 `module modSecurity { ... }` formatı varsa da temizle
        if grep -qE '^module modSecurity \{' "${OLS_CONF}"; then
            # Brace match'li silme — module modSecurity { ile başlayıp } ile biten satıra kadar
            python3 -c "
import re, sys
p = '${OLS_CONF}'
content = open(p).read()
# 'module modSecurity { ... }' bloğunu sil (multiline)
pattern = re.compile(r'\n?module modSecurity \{[^}]*\}\n?', re.DOTALL)
new = pattern.sub('\n', content)
open(p, 'w').write(new)
" 2>/dev/null || true
            ACTIONS+=("Eski v3.16 'module modSecurity {}' bloğu silindi")
        fi

        # Doğru format ekle (v3.26): LOWERCASE directives + backtick block
        # OLS 1.7+ parser camelCase'i ignore eder — modsecurity/modsecurityRules zorunlu.
        # v3.19'un `modSecurity`/`modSecurityRules` formu sessizce skip ediliyordu →
        # SecRuleEngine bile çalışmıyor, audit log 0 byte.
        # v3.42 OLS KÖKLÜ FIX:
        # Top-level `modsecurity on` + `modsecurityRules \`...Include...\`` yapısı,
        # CRS dosyalarını OLS'nin PlainConf parser'ından geçiriyor ve kuralların
        # çoğunu "Not support" deyip DÜŞÜRÜYORDU (response-phase, secmarker,
        # skipAfter, macro-operator) → SecRuleEngine etkisiz, SQLi engellenmiyor.
        # DOĞRU yol (OLS resmi): `module mod_security { modsecurity on;
        # modsecurity_rules_file <dosya> }` — bu dosya DOĞRUDAN libmodsecurity v3'e
        # verilir (PlainConf değil), tüm CRS eksiksiz yüklenir. Modül adı 'mod_security'
        # (alt çizgili); eski 'modSecurity' camelCase yok sayılıyordu.
        # Ref: https://docs.openlitespeed.org/modules/modsecurity/
        ensure_neutral_layer

        # libmodsecurity'nin parse edeceği includes dosyası — driver-spesifik audit/tmp
        # + crs-setup + neutral onox.d + CRS rules. Include glob'larını libmodsecurity
        # işler (OLS PlainConf değil).
        # v3.43 AUDIT FIX: OLS modsec audit log handle'ı SADECE inline
        # `modsecurity_rules` bloğundan açılıyor; SecAuditLog'u modsecurity_rules_file
        # içine koyunca log dosyası hiç açılmıyordu (bloklar 403 ama audit boş).
        # Çözüm (OLS KB): SecAuditEngine/SecAuditLog/SecRuleEngine inline; CRS + neutral
        # modsecurity_rules_file ile (libmodsecurity tam parse eder).
        OLS_INCLUDES="${ONX_MODSEC_NEUTRAL_DIR}/ols-modsec-includes.conf"
        cat > "${OLS_INCLUDES}" <<EOF
# ONOXSOFT OLS ModSecurity includes — libmodsecurity v3 (modsecurity_rules_file)
# CRS + neutral panel-managed (engine/exclusion/whitelist/custom: ${NEUTRAL_DIR}/*.conf)
Include ${CRS_DIR}/crs-setup.conf
Include ${NEUTRAL_DIR}/*.conf
Include ${CRS_DIR}/rules/*.conf
EOF
        chmod 0644 "${OLS_INCLUDES}"

        cat >> "${OLS_CONF}" <<EOF

${BEGIN_MARK} (v3.43 module mod_security + audit inline) ───
module mod_security {
modsecurity  on
modsecurity_rules \`
  SecRuleEngine On
  SecRequestBodyAccess On
  SecResponseBodyAccess On
  SecAuditEngine RelevantOnly
  SecAuditLog /usr/local/lsws/logs/modsec_audit.log
  SecAuditLogParts ABIJDEFHZ
  SecAuditLogType Serial
  SecAuditLogRelevantStatus "^(?:5|4(?!04))"
  SecTmpDir /var/lib/onoxsoft/modsec/tmp
  SecDataDir /var/lib/onoxsoft/modsec/data
  SecDebugLogLevel 0
\`
modsecurity_rules_file ${OLS_INCLUDES}
ls_enabled 1
}
${END_MARK}───────────────────────────────────────
EOF
        ACTIONS+=("OLS module mod_security v3.43 (audit inline + CRS via rules_file → ${OLS_INCLUDES}) eklendi")
        INSTALLED="true"

        # Persistent SecTmpDir/SecDataDir — /tmp reboot'ta + systemd-tmpfiles ile
        # silinir → modsec collection (ip/global) verisi kaybolur → kararsızlık.
        mkdir -p /var/lib/onoxsoft/modsec/data /var/lib/onoxsoft/modsec/tmp 2>/dev/null
        chown -R nobody:nobody /var/lib/onoxsoft/modsec 2>/dev/null || true
        chmod -R 0700 /var/lib/onoxsoft/modsec 2>/dev/null || true

        # Audit log + permissions (nobody = OLS runtime user)
        mkdir -p /usr/local/lsws/logs 2>/dev/null
        touch /usr/local/lsws/logs/modsec_audit.log 2>/dev/null || true
        chown nobody:nobody /usr/local/lsws/logs/modsec_audit.log 2>/dev/null \
            || chown lsadm:lsadm /usr/local/lsws/logs/modsec_audit.log 2>/dev/null || true
        chmod 0644 /usr/local/lsws/logs/modsec_audit.log 2>/dev/null || true

        # Reload OLS — restart (graceful)
        # v3.41 FIX: lswsctrl restart stdout'a yazıyor ("[OK] Send SIGUSR1...") →
        # JSON çıktısını kirletip jq parse hatası veriyordu. stdout'u /dev/null'a al.
        if [[ -x /usr/local/lsws/bin/lswsctrl ]]; then
            if /usr/local/lsws/bin/lswsctrl restart >/dev/null 2>/tmp/lsws-restart-err.$$; then
                ACTIONS+=("OLS restart edildi")
            else
                err=$(cat /tmp/lsws-restart-err.$$ 2>/dev/null | tail -3 | tr '\n' '; ')
                ERRORS+=("OLS restart başarısız: ${err}")
            fi
            rm -f /tmp/lsws-restart-err.$$ 2>/dev/null
        fi

        # ─── v3.26 Smoke test: SQLi tetikle, ModSec yakaladı mı? ─────────────
        # En az bir vhost varsa (Example/Default dışı) localhost'a SQLi at;
        # CRS 942100 (libinjection) tetiklemeli → 403 + audit log büyümeli.
        # Sonuç ACTIONS/ERRORS array'ine yazılır (JSON output'a yansır).
        VHOST_DOMAIN=""
        if [[ -d /usr/local/lsws/conf/vhosts ]]; then
            VHOST_DOMAIN=$(ls /usr/local/lsws/conf/vhosts/ 2>/dev/null | grep -vE '^(Example|Default|\.)' | head -1)
        fi
        if [[ -n "${VHOST_DOMAIN}" ]] && command -v curl >/dev/null 2>&1; then
            # OLS restart için bekle (max 5sn polling)
            for i in 1 2 3 4 5; do
                if /usr/local/lsws/bin/lswsctrl status 2>/dev/null | grep -qi running; then break; fi
                sleep 1
            done
            sleep 2  # ekstra settle

            audit_before=$(stat -c '%s' /usr/local/lsws/logs/modsec_audit.log 2>/dev/null || echo 0)
            # CRS 942100/942110/942150 libinjection ile match etmeli
            smoke_code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 \
                "https://localhost:443/?id=1%27%20OR%201=1--" \
                -H "Host: ${VHOST_DOMAIN}" 2>/dev/null || echo "000")
            sleep 1  # log flush
            audit_after=$(stat -c '%s' /usr/local/lsws/logs/modsec_audit.log 2>/dev/null || echo 0)

            if [[ "${audit_after}" -gt "${audit_before}" ]]; then
                ACTIONS+=("Smoke test OK: SQLi yakalandı (HTTP ${smoke_code}, audit log ${audit_before}→${audit_after} bytes)")
            elif [[ "${smoke_code}" == "403" ]] || [[ "${smoke_code}" == "406" ]]; then
                ACTIONS+=("Smoke test: HTTP ${smoke_code} engellendi ama audit log boş — SecAuditEngine / log path kontrolü gerekli")
            else
                ERRORS+=("Smoke test UYARI: SQLi engellenmedi (HTTP ${smoke_code}, audit log ${audit_before}→${audit_after} bytes). CRS rules path / SecRuleEngine kontrolü gerekli.")
            fi
        fi
        ;;

    # ─── NGINX (Phase 2 production-ready) ───────────────────────────────────
    # libmodsecurity v3 + ngx_http_modsecurity_module (EPEL)
    # CRS 3.3.x — v3 compatible (4.x bazı rule'lar v3'te crash)
    nginx)
        # nginx binary mevcudiyeti
        if ! command -v nginx >/dev/null 2>&1; then
            ERRORS+=("nginx kurulu değil — önce 'dnf install -y nginx' gerek")
            onx_die 3 "nginx binary bulunamadı"
        fi

        # (a) EPEL + nginx-mod-modsecurity install (idempotent)
        if ! rpm -q nginx-mod-modsecurity >/dev/null 2>&1; then
            # EPEL repo kurulu mu?
            if ! rpm -q epel-release >/dev/null 2>&1; then
                if dnf install -y epel-release >/dev/null 2>&1; then
                    ACTIONS+=("epel-release kuruldu")
                else
                    ERRORS+=("epel-release install başarısız")
                fi
            fi
            # Şimdi modül
            if dnf install -y nginx-mod-modsecurity >/dev/null 2>&1; then
                ACTIONS+=("nginx-mod-modsecurity kuruldu (EPEL)")
                INSTALLED="true"
            else
                ERRORS+=("nginx-mod-modsecurity dnf install başarısız — EPEL repo ekli mi? Manuel kurulum (libmodsecurity + ngx_http_modsecurity_module compile) scope dışı.")
                onx_die 4 "nginx-mod-modsecurity paketi yok"
            fi
        else
            ACTIONS+=("nginx-mod-modsecurity zaten kurulu")
            INSTALLED="true"
        fi

        # Module dosyası beklenen yerde mi?
        NGINX_MODULE="/usr/lib64/nginx/modules/ngx_http_modsecurity_module.so"
        if [[ ! -f "${NGINX_MODULE}" ]]; then
            # AlmaLinux 9 alt dizin variant'ı kontrol
            if [[ -f "/usr/lib/nginx/modules/ngx_http_modsecurity_module.so" ]]; then
                NGINX_MODULE="/usr/lib/nginx/modules/ngx_http_modsecurity_module.so"
            else
                ERRORS+=("ngx_http_modsecurity_module.so dosyası bulunamadı (rpm bozuk?)")
            fi
        fi

        # (e) Shared CRS v3.3.x download (OLS ile paylaşımlı path)
        ensure_shared_crs_v3 || true

        # (b) nginx.conf'a load_module — TOP (events block'tan ÖNCE)
        # Idempotency: önce mevcut load_module satırını ara
        NGINX_CONF="/etc/nginx/nginx.conf"
        if [[ ! -f "${NGINX_CONF}" ]]; then
            ERRORS+=("nginx.conf bulunamadı: ${NGINX_CONF}")
            onx_die 5 "nginx.conf yok"
        fi

        # v3.28: EPEL paketi /usr/share/nginx/modules/mod-modsecurity.conf bırakır.
        # nginx.conf'ta `include /usr/share/nginx/modules/*.conf;` varsa otomatik
        # yüklenir → bizim manuel inject DUPLICATE LOAD hatasına sebep olur:
        #   "module ngx_http_modsecurity_module is already loaded"
        # Çözüm: önce EPEL auto-load conf'larını tara, varsa inject ETME.
        EPEL_AUTOLOAD="false"
        for ep_conf in \
            /usr/share/nginx/modules/mod-modsecurity.conf \
            /etc/nginx/modules/mod-modsecurity.conf \
            /etc/nginx/modules-enabled/mod-modsecurity.conf
        do
            [[ -f "$ep_conf" ]] || continue
            if grep -qE '^[[:space:]]*load_module[[:space:]]+.*ngx_http_modsecurity_module\.so' "$ep_conf" 2>/dev/null; then
                EPEL_AUTOLOAD="true"
                ACTIONS+=("EPEL paket conf'u otomatik yüklüyor: ${ep_conf} (load_module inject atlanıyor)")
                break
            fi
        done

        # Bizim manuel injection (sadece EPEL conf yoksa)
        if [[ "$EPEL_AUTOLOAD" != "true" ]]; then
            if ! grep -qE '^[[:space:]]*load_module[[:space:]]+modules/ngx_http_modsecurity_module\.so' "${NGINX_CONF}"; then
                # Backup
                cp -a "${NGINX_CONF}" "${NGINX_CONF}.bak-$(date +%Y%m%d%H%M%S)"
                # En üste ekle (events block'tan önce — nginx load_module sadece top-level kabul eder)
                sed -i '1i load_module modules/ngx_http_modsecurity_module.so;' "${NGINX_CONF}"
                ACTIONS+=("load_module ngx_http_modsecurity_module.so → nginx.conf top'a eklendi (EPEL auto-load yok)")
            else
                ACTIONS+=("load_module zaten nginx.conf'ta var")
            fi
        fi

        # v3.28: Önceki v3.27 ile inject edilmiş artık DUPLICATE olan satırı temizle
        # (EPEL paket conf'u zaten yüklediği için bizimki "already loaded" hatası verir)
        if [[ "$EPEL_AUTOLOAD" == "true" ]] && \
           grep -qE '^load_module[[:space:]]+modules/ngx_http_modsecurity_module\.so' "${NGINX_CONF}" 2>/dev/null; then
            cp -a "${NGINX_CONF}" "${NGINX_CONF}.bak-cleanup-$(date +%Y%m%d%H%M%S)"
            sed -i '/^load_module[[:space:]]\+modules\/ngx_http_modsecurity_module\.so/d' "${NGINX_CONF}"
            ACTIONS+=("Önceki duplicate load_module satırı nginx.conf'tan temizlendi (EPEL conf'u kullanılacak)")
        fi

        # (c) Main config — /etc/nginx/modsec/main.conf
        NGINX_MODSEC_DIR="/etc/nginx/modsec"
        mkdir -p "${NGINX_MODSEC_DIR}"

        # CRS path resolution: shared v3 > Apache v2 fallback
        if [[ -d "${SHARED_CRS_V3}/rules" ]]; then
            CRS_INCLUDE_SETUP="${SHARED_CRS_V3}/crs-setup.conf"
            CRS_INCLUDE_RULES="${SHARED_CRS_V3}/rules/*.conf"
        elif [[ -d "${CRS_DIR_APACHE}/rules" ]]; then
            # Fallback: Apache CRS path (mod_security_crs v3.x — uyumlu)
            CRS_INCLUDE_SETUP="${CRS_DIR_APACHE}/crs-setup.conf"
            CRS_INCLUDE_RULES="${CRS_DIR_APACHE}/rules/*.conf"
        else
            CRS_INCLUDE_SETUP=""
            CRS_INCLUDE_RULES=""
            ERRORS+=("CRS rules dizini yok — shared v3 download başarısız ve Apache CRS de kurulu değil")
        fi

        # main.conf'u her zaman yeniden yaz (idempotent — değer drift'ini önler)
        # v3.41: Engine/exclusion/whitelist/custom artık neutral layer'da
        # (${NEUTRAL_DIR}). main.conf sadece driver-spesifik audit/tmp + CRS +
        # neutral Include içerir. Neutral crs-setup'tan SONRA, rules'tan ÖNCE.
        ensure_neutral_layer
        {
            echo "# ONOXSOFT ModSecurity v3 main.conf (Nginx — libmodsecurity v3)"
            echo "# Auto-generated by onx-modsec-install-driver. Manual edits overwritten on next run."
            echo "# Engine/exclusion/whitelist/custom: ${NEUTRAL_DIR}/*.conf (panel-managed)"
            echo ""
            echo "SecAuditLog /var/log/nginx/modsec_audit.log"
            echo "SecAuditLogType Serial"
            echo "SecTmpDir /tmp"
            echo "SecDataDir /tmp"
            echo "SecDebugLogLevel 0"
            echo ""
            if [[ -n "${CRS_INCLUDE_SETUP}" && -f "${CRS_INCLUDE_SETUP}" ]]; then
                echo "Include ${CRS_INCLUDE_SETUP}"
            fi
            echo "# Panel-managed (crs-setup SONRASI, rules ÖNCESİ):"
            echo "Include ${NEUTRAL_DIR}/*.conf"
            if [[ -n "${CRS_INCLUDE_RULES}" ]]; then
                echo "Include ${CRS_INCLUDE_RULES}"
            fi
        } > "${NGINX_MODSEC_DIR}/main.conf"
        chmod 0644 "${NGINX_MODSEC_DIR}/main.conf"
        ACTIONS+=("main.conf yazıldı (neutral-layer) → ${NGINX_MODSEC_DIR}/main.conf")

        # (d) /etc/nginx/conf.d/modsecurity.conf snippet (auto-included http context)
        SNIPPET="/etc/nginx/conf.d/modsecurity.conf"
        # Idempotent — her zaman yeniden yaz (path drift'i önler)
        cat > "${SNIPPET}" <<EOF
# ONOXSOFT ModSecurity Nginx http-context snippet
# Auto-included by /etc/nginx/nginx.conf (http block conf.d/*.conf glob)
# Tüm server block'lar miras alır — per-vhost disable için server block'a
# "modsecurity off;" eklenebilir.
modsecurity on;
modsecurity_rules_file ${NGINX_MODSEC_DIR}/main.conf;
EOF
        chmod 0644 "${SNIPPET}"
        ACTIONS+=("Snippet yazıldı → ${SNIPPET} (http context global enable)")

        # (f) Audit log permissions
        mkdir -p /var/log/nginx
        if [[ ! -f /var/log/nginx/modsec_audit.log ]]; then
            touch /var/log/nginx/modsec_audit.log
        fi
        chown nginx:nginx /var/log/nginx/modsec_audit.log 2>/dev/null || true
        chmod 0644 /var/log/nginx/modsec_audit.log

        # (g) nginx config test + reload
        NGINX_TEST_ERR=$(mktemp)
        if nginx -t 2>"${NGINX_TEST_ERR}"; then
            ACTIONS+=("nginx -t başarılı")
            if systemctl reload nginx 2>/dev/null; then
                ACTIONS+=("nginx reload OK")
            else
                # reload başarısız — restart deneme
                if systemctl restart nginx 2>/dev/null; then
                    ACTIONS+=("nginx restart edildi (reload başarısız olduğu için)")
                else
                    ERRORS+=("nginx reload ve restart ikisi de başarısız")
                fi
            fi
        else
            err_tail=$(tail -5 "${NGINX_TEST_ERR}" 2>/dev/null | tr '\n' '; ' | head -c 500)
            ERRORS+=("nginx -t başarısız (reload yapılmadı): ${err_tail}")
        fi
        rm -f "${NGINX_TEST_ERR}"
        ;;

    # ─── CADDY (v3.26 Phase 2 — Coraza WAF) ─────────────────────────────────
    # Caddy'nin native ModSec'i yok. Standart yaklaşım: Coraza WAF (Go-based,
    # ModSec rule language uyumlu) + coraza-caddy plugin. Caddy single Go
    # binary olduğu için plugin eklemek `xcaddy build` ile rebuild gerektirir
    # (Go toolchain 1.21+ + git + network).
    #
    # Strateji:
    #   1) Go + xcaddy mevcudiyet check (yoksa go install xcaddy dene)
    #   2) Hiçbiri yoksa GRACEFUL FAIL → installed=false + requires_manual=true
    #      + clear talimat (controller "ModSec Kur" butonu gri yapacak)
    #   3) Varsa: xcaddy build → custom binary → /usr/bin/caddy symlink → restart
    #   4) Coraza site-block snippet + global directive order üret
    caddy)
        CADDY_BIN="/usr/bin/caddy"
        CADDY_BIN_CUSTOM="/usr/local/bin/caddy-coraza"
        CORAZA_CONF="/etc/caddy/coraza.conf"
        CORAZA_INCLUDE="/etc/caddy/coraza-include.caddy"
        CADDY_BUILD_DIR="/tmp/onx-caddy-build"
        XCADDY_BIN=""
        GO_OK="false"
        XCADDY_OK="false"

        # Idempotency check: already Coraza?
        if [[ -x "${CADDY_BIN}" ]] && "${CADDY_BIN}" list-modules 2>/dev/null | grep -qi "coraza"; then
            ACTIONS+=("Caddy zaten Coraza WAF destekli (list-modules'de coraza var)")
            INSTALLED="true"
        fi

        # Go (>=1.21) detection
        if command -v go >/dev/null 2>&1; then
            GO_VER=$(go version 2>/dev/null | grep -oE 'go[0-9]+\.[0-9]+' | head -1 | tr -d 'go')
            GO_MAJ=$(echo "${GO_VER}" | cut -d. -f1)
            GO_MIN=$(echo "${GO_VER}" | cut -d. -f2)
            if [[ -n "${GO_MAJ}" && -n "${GO_MIN}" ]]; then
                if (( GO_MAJ > 1 || (GO_MAJ == 1 && GO_MIN >= 21) )); then
                    GO_OK="true"
                    ACTIONS+=("Go ${GO_VER} tespit edildi (>=1.21 OK)")
                else
                    ERRORS+=("Go ${GO_VER} eski — Coraza için 1.21+ gerek")
                fi
            fi
        fi

        # xcaddy detect / install via go
        if [[ "${GO_OK}" == "true" ]]; then
            if command -v xcaddy >/dev/null 2>&1; then
                XCADDY_BIN=$(command -v xcaddy)
                XCADDY_OK="true"
                ACTIONS+=("xcaddy zaten PATH'te: ${XCADDY_BIN}")
            else
                GOBIN_DEFAULT="$(go env GOPATH 2>/dev/null)/bin"
                if [[ -x "${GOBIN_DEFAULT}/xcaddy" ]]; then
                    XCADDY_BIN="${GOBIN_DEFAULT}/xcaddy"
                    XCADDY_OK="true"
                    ACTIONS+=("xcaddy bulundu (GOPATH/bin): ${XCADDY_BIN}")
                else
                    onx_log "Installing xcaddy via go install..."
                    if GOFLAGS='-mod=mod' go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest >/dev/null 2>/tmp/xcaddy-install.err; then
                        if [[ -x "${GOBIN_DEFAULT}/xcaddy" ]]; then
                            XCADDY_BIN="${GOBIN_DEFAULT}/xcaddy"
                            XCADDY_OK="true"
                            ACTIONS+=("xcaddy go install ile kuruldu: ${XCADDY_BIN}")
                        fi
                    else
                        err=$(tail -2 /tmp/xcaddy-install.err 2>/dev/null | tr '\n' '; ')
                        ERRORS+=("xcaddy go install başarısız: ${err}")
                    fi
                fi
            fi
        fi

        # ── HONEST FALLBACK: Go/xcaddy yoksa graceful refuse ──────────────
        # NOT: onx_die ÇAĞIRMIYORUZ — graceful JSON exit, controller karar verir
        if [[ "${GO_OK}" != "true" || "${XCADDY_OK}" != "true" ]] && [[ "${INSTALLED}" != "true" ]]; then
            ERRORS+=("Caddy Coraza WAF için Go 1.21+ ve xcaddy gerek. Sunucuda kurulu değil.")
            ERRORS+=("Çözüm 1: dnf install -y golang-bin git && go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest")
            ERRORS+=("Çözüm 2: Apache/Nginx/OLS'ye geç (onx-webserver-switch driver=apache) — bu driver'larda ModSec hazır çalışıyor")
            ERRORS+=("Çözüm 3: Coraza binary release indir (https://github.com/corazawaf/coraza-caddy/releases) — curated build yoksa rebuild zorunlu")
            REQUIRES_MANUAL="true"
        fi

        # ── xcaddy build (only if both available + not already installed) ─
        if [[ "${GO_OK}" == "true" && "${XCADDY_OK}" == "true" && "${INSTALLED}" != "true" ]]; then
            mkdir -p "${CADDY_BUILD_DIR}"
            (
                cd "${CADDY_BUILD_DIR}" || exit 1
                onx_log "xcaddy build başlıyor: coraza-caddy/v2..."
                "${XCADDY_BIN}" build \
                    --with github.com/corazawaf/coraza-caddy/v2 \
                    --output "${CADDY_BIN_CUSTOM}" >/dev/null 2>/tmp/xcaddy-build.err
            )
            BUILD_RC=$?

            if [[ ${BUILD_RC} -eq 0 && -x "${CADDY_BIN_CUSTOM}" ]]; then
                ACTIONS+=("xcaddy build başarılı: ${CADDY_BIN_CUSTOM}")
                chmod 0755 "${CADDY_BIN_CUSTOM}"

                # setcap (port 80/443 root olmadan bind)
                if setcap 'cap_net_bind_service=+ep' "${CADDY_BIN_CUSTOM}" 2>/dev/null; then
                    ACTIONS+=("setcap cap_net_bind_service uygulandı")
                else
                    ERRORS+=("setcap uygulanamadı — port 443 binding fail riski")
                fi

                # Backup orig (idempotent — overwrite etmez)
                if [[ -x "${CADDY_BIN}" && ! -L "${CADDY_BIN}" && ! -f "${CADDY_BIN}.orig" ]]; then
                    cp -a "${CADDY_BIN}" "${CADDY_BIN}.orig"
                    ACTIONS+=("Orijinal /usr/bin/caddy → ${CADDY_BIN}.orig yedeklendi")
                fi

                # Symlink swap (rpm update bozmasın diye symlink — package upgrade /usr/bin/caddy'yi override etmez)
                ln -sfn "${CADDY_BIN_CUSTOM}" "${CADDY_BIN}"
                ACTIONS+=("/usr/bin/caddy → ${CADDY_BIN_CUSTOM} symlink yapıldı")

                # v3.29: Build + symlink başarılı = INSTALLED true.
                # Validate ve restart sadece BONUS doğrulamalar; restart fail
                # multi-webserver pattern'de NORMAL (Apache/OLS port 80/443 tutar).
                # Eskiden restart fail INSTALLED=false rapor ediyordu, yanıltıcıydı.
                INSTALLED="true"

                # Validate (mevcut Caddyfile breaks olmadı mı)
                if "${CADDY_BIN}" validate --config /etc/caddy/Caddyfile --adapter caddyfile 2>/tmp/caddy-validate.err; then
                    ACTIONS+=("caddy validate OK (Coraza eklenmiş hali)")
                else
                    err=$(tail -3 /tmp/caddy-validate.err 2>/dev/null | tr '\n' '; ')
                    ERRORS+=("caddy validate başarısız (config Coraza directive bekliyor olabilir): ${err}")
                fi

                # Restart denemesi — fail acceptable (port conflict normal in multi-webserver)
                if systemctl restart caddy 2>/tmp/caddy-restart.err; then
                    ACTIONS+=("systemctl restart caddy OK")
                else
                    err=$(tail -2 /tmp/caddy-restart.err 2>/dev/null | tr '\n' '; ')
                    # Port conflict (başka webserver aktif) ERROR değil INFO:
                    if echo "$err" | grep -qiE 'address already in use|bind|port'; then
                        ACTIONS+=("Restart deferred: port 80/443 başka driver tarafından tutuluyor (Caddy hazır, switch yapılınca otomatik aktif olur)")
                    else
                        ERRORS+=("systemctl restart caddy başarısız: ${err}")
                    fi
                fi
            else
                err=$(tail -5 /tmp/xcaddy-build.err 2>/dev/null | tr '\n' '; ')
                ERRORS+=("xcaddy build başarısız (rc=${BUILD_RC}): ${err}")
                ERRORS+=("Olası sebepler: proxy.golang.org erişimi yok, disk doluluk, Go module hash mismatch, network firewall")
            fi
            rm -f /tmp/xcaddy-build.err 2>/dev/null
        fi

        # ── Coraza config example + snippet (idempotent) ──────────────────
        # Mevcut vhost'ları toplu rewrite ETMİYORUZ — admin per-site
        # `import coraza-include` ile aktive eder. Yeni eklenen domainler için
        # vhost stub'una entegrasyon ayrı task (TODO: onx-vhost-add-caddy update).
        CRS_RULES_DIR="${SHARED_CRS_V3}"
        [[ ! -d "${CRS_RULES_DIR}/rules" ]] && CRS_RULES_DIR="${SHARED_RULES}/owasp-crs"
        [[ ! -d "${CRS_RULES_DIR}/rules" ]] && CRS_RULES_DIR="/etc/httpd/modsecurity.d/owasp-crs"

        # v3.41: neutral layer (engine/exclusion/whitelist/custom) — Coraza da Include eder
        ensure_neutral_layer

        if [[ ! -f "${CORAZA_CONF}" ]]; then
            cat > "${CORAZA_CONF}" <<EOF
# ONOXSOFT Coraza WAF config — v3.26
# Per-site enable pattern (admin vhost'a manual ekler):
#
# example.com {
#     coraza_waf {
#         load_owasp_crs
#         directives \`
#             Include ${CRS_RULES_DIR}/crs-setup.conf
#             Include ${CRS_RULES_DIR}/rules/*.conf
#             SecRuleEngine On
#             SecRequestBodyAccess On
#             SecResponseBodyAccess On
#             SecRequestBodyLimit 13107200
#             SecRequestBodyNoFilesLimit 131072
#             SecAuditEngine RelevantOnly
#             SecAuditLog /var/log/caddy/modsec_audit.log
#             SecAuditLogParts ABIJDEFHZ
#             SecAuditLogType Serial
#         \`
#     }
#     # ... rest of site config (root, file_server, php_fastcgi, ...)
# }
#
# Global directive order (Caddyfile başında ŞART):
#   {
#       order coraza_waf first
#   }
EOF
            ACTIONS+=("Coraza config örneği yazıldı: ${CORAZA_CONF}")
        fi

        if [[ ! -f "${CORAZA_INCLUDE}" ]]; then
            cat > "${CORAZA_INCLUDE}" <<EOF
# Coraza WAF site-block snippet — admin "import coraza-include" ile aktive eder:
#   example.com {
#       import coraza-include
#       root * /home/users/onx_foo/example.com/public_html
#       php_fastcgi unix//run/php-fpm/example.sock
#       file_server
#   }
(coraza-include) {
    coraza_waf {
        load_owasp_crs
        directives \`
            SecAuditLog /var/log/caddy/modsec_audit.log
            SecAuditLogType Serial
            SecTmpDir /tmp
            SecDataDir /tmp
            Include ${CRS_RULES_DIR}/crs-setup.conf
            Include ${NEUTRAL_DIR}/*.conf
            Include ${CRS_RULES_DIR}/rules/*.conf
        \`
    }
}
EOF
            ACTIONS+=("Coraza snippet yazıldı (neutral-layer): ${CORAZA_INCLUDE} (admin 'import coraza-include' ile per-site enable eder)")
        fi

        # Global directive order — `order coraza_waf first` Caddyfile'a inject
        CADDYFILE="/etc/caddy/Caddyfile"
        if [[ "${INSTALLED}" == "true" && -f "${CADDYFILE}" ]] && ! grep -qE "order[[:space:]]+coraza_waf" "${CADDYFILE}"; then
            if grep -qE "^[[:space:]]*\{" "${CADDYFILE}"; then
                # Mevcut global block içine inject (ilk { satırından sonra)
                sed -i '0,/^[[:space:]]*\{/{s//&\n\torder coraza_waf first/}' "${CADDYFILE}"
                ACTIONS+=("Caddyfile global block'a 'order coraza_waf first' eklendi")
            else
                # Global block yok — başa ekle
                TMPF=$(mktemp -t onx-caddyfile-XXXXXX)
                {
                    printf '# v3.26: Coraza WAF directive order\n'
                    printf '{\n'
                    printf '\torder coraza_waf first\n'
                    printf '}\n'
                    printf '\n'
                    cat "${CADDYFILE}"
                } > "${TMPF}"
                mv -f "${TMPF}" "${CADDYFILE}"
                chmod 0644 "${CADDYFILE}"
                ACTIONS+=("Caddyfile başına 'order coraza_waf first' global block eklendi")
            fi
        fi

        # Audit log + permissions
        mkdir -p /var/log/caddy 2>/dev/null || true
        if [[ ! -f /var/log/caddy/modsec_audit.log ]]; then
            touch /var/log/caddy/modsec_audit.log 2>/dev/null || true
            chown caddy:caddy /var/log/caddy/modsec_audit.log 2>/dev/null \
                || chown root:root /var/log/caddy/modsec_audit.log 2>/dev/null || true
            chmod 0644 /var/log/caddy/modsec_audit.log 2>/dev/null || true
            ACTIONS+=("modsec_audit.log oluşturuldu: /var/log/caddy/modsec_audit.log (caddy:caddy 0644)")
        fi
        ;;
esac

# ── Shared CRS rules symlink (multi-driver consistency) ──────────────────────
# /etc/httpd/modsecurity.d/owasp-crs varsa, /etc/onoxsoft/modsec-rules/owasp-crs'e link
if [[ -d /etc/httpd/modsecurity.d/owasp-crs ]] && [[ ! -L "${SHARED_RULES}/owasp-crs" ]]; then
    ln -sfn /etc/httpd/modsecurity.d/owasp-crs "${SHARED_RULES}/owasp-crs"
    ACTIONS+=("CRS rules symlink: ${SHARED_RULES}/owasp-crs → /etc/httpd/modsecurity.d/owasp-crs")
fi

# v3.18: jq -Rn (NOT -Rsn) — -s slurp'lar tüm satırları tek string'e birleştiriyordu
# (actions: ["A\nB\n"], errors: ["\n"]). -Rn line-by-line raw input okur.
# Boş array case: ACTIONS=() → printf -> "\n" (1 satır boş) → jq filter atar → []
if [[ ${#ACTIONS[@]} -eq 0 ]]; then
    ACTIONS_JSON='[]'
else
    ACTIONS_JSON=$(printf '%s\n' "${ACTIONS[@]}" | jq -Rn '[inputs | select(length > 0)]')
fi
if [[ ${#ERRORS[@]} -eq 0 ]]; then
    ERRORS_JSON='[]'
else
    ERRORS_JSON=$(printf '%s\n' "${ERRORS[@]}" | jq -Rn '[inputs | select(length > 0)]')
fi

json_ok "$(jq -n \
    --arg driver "${DRIVER}" \
    --argjson installed "${INSTALLED}" \
    --argjson requires_manual "${REQUIRES_MANUAL}" \
    --argjson actions "${ACTIONS_JSON}" \
    --argjson errors "${ERRORS_JSON}" \
    '{driver:$driver, installed:$installed, requires_manual:$requires_manual, actions:$actions, errors:$errors}')"

onx_log "modsec-install-driver complete: driver=${DRIVER} installed=${INSTALLED} actions=${#ACTIONS[@]} errors=${#ERRORS[@]}"
