#!/usr/bin/env bash
#
# onx-modsec-audit-search — ModSec audit log parse. DRIVER-AWARE + format-agnostic (v3.41).
#
# Aktif (veya istenen) driver'ın audit log yolunu kullanır:
#   apache → /var/log/httpd/modsec_audit.log
#   nginx  → /var/log/nginx/modsec_audit.log
#   ols    → /usr/local/lsws/logs/modsec_audit.log
#   caddy  → /var/log/caddy/modsec_audit.log
#
# İki formatı da parse eder:
#   • JSON-lines  (libmodsecurity v3, SecAuditLogFormat JSON) — jq per satır
#   • native serial (Apache modsec2 "--ID-A--" 2-tire; libmodsecurity v3 "---ID---A--"
#     3-tire) — v3.66: TEK awk pass → JSON. Eski blok-dosya + bash-eval + grep|head
#     yaklaşımı set -e/pipefail/SIGPIPE/eval-quoting nedeniyle gerçek (mesajlı,
#     HTTP/2, URL-encoded) loglarda SESSİZCE 0 event döndürüyordu — kaldırıldı.
#
# Input:  {limit, since_hours, rule_id, ip, driver?}
# Output: {ok, events:[...], total, driver, log_file, log_format}

set -uo pipefail

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=_lib/common.sh
source "${SCRIPT_DIR}/_lib/common.sh"
set +e   # common.sh'in set -e'sini geri al — parse adımları (grep/awk no-match) abort etmesin

input="$(cat 2>/dev/null || echo '{}')"
echo "$input" | jq -e 'type == "object"' >/dev/null 2>&1 || input='{}'
limit="$(echo "$input"  | jq -r '.limit // 50')"
hours="$(echo "$input"  | jq -r '.since_hours // 24')"
filter_rule="$(echo "$input" | jq -r '.rule_id // empty')"
filter_ip="$(echo "$input"   | jq -r '.ip // empty')"
req_driver="$(echo "$input"  | jq -r '.driver // "auto"')"

[[ "$limit" =~ ^[0-9]+$ ]] || limit=50
[[ "$hours" =~ ^[0-9]+$ ]] || hours=24
[[ "$limit" -gt 100 ]] && limit=100

DRIVER=$(onx_ws_resolve_driver "$req_driver")
LOG_FILE=$(onx_modsec_audit_log "$DRIVER")

if [[ -z "$LOG_FILE" || ! -r "$LOG_FILE" ]]; then
    jq -nc --arg p "${LOG_FILE:-}" --arg driver "$DRIVER" \
        '{ok:false,error:"audit log not readable",path:$p,driver:$driver,events:[],total:0}' >&2
    exit 2
fi

# tail buffer: limit*200 veya hours*8000 (büyüğü), max 500K satır
tail_lines=$((limit * 200))
hours_buffer=$((hours * 8000))
[[ $hours_buffer -gt $tail_lines ]] && tail_lines=$hours_buffer
[[ $tail_lines -gt 500000 ]] && tail_lines=500000

# ── Format detect (ilk non-empty satır JSON object mı?) ──────────────────────
first_line=$(grep -m1 -v '^[[:space:]]*$' "$LOG_FILE" 2>/dev/null | head -c 8192 || true)
LOG_FORMAT="native"
if echo "$first_line" | jq -e 'type == "object"' >/dev/null 2>&1; then
    LOG_FORMAT="json"
fi

events_json="[]"
count=0

if [[ "$LOG_FORMAT" == "json" ]]; then
    # ── libmodsecurity v3 JSON-lines ─────────────────────────────────────────
    events_json=$(tail -n "$tail_lines" "$LOG_FILE" 2>/dev/null | jq -cs \
        --arg frule "$filter_rule" --arg fip "$filter_ip" --argjson lim "$limit" '
        def sevword($s): ($s|tostring|ascii_upcase) as $u |
          {"0":"EMERGENCY","1":"ALERT","2":"CRITICAL","3":"ERROR","4":"WARNING","5":"NOTICE","6":"INFO","7":"DEBUG"}[$u] // $u;
        [ .[]
          | select(type=="object")
          | (.transaction // {}) as $tx
          | ($tx.messages // []) as $msgs
          | ($msgs[0].details // {}) as $d0
          | {
              event_id: (($tx.unique_id // $tx.id // "") | tostring),
              source_ip: (($tx.client_ip // "0.0.0.0") | tostring),
              domain_name: (($tx.request.headers.Host // $tx.request.headers.host // "") | tostring),
              uri: (($tx.request.uri // $tx.request.request_line // "/") | tostring),
              method: (($tx.request.method // "GET") | tostring),
              rule_id: (($d0.ruleId // "") | tostring),
              rule_tag: ((($d0.tags // [])[0]) // "" | tostring),
              severity: sevword($d0.severity // ""),
              reason: (($msgs[0].message // "") | tostring),
              response_code: (($tx.response.http_code // 0) | tonumber? // 0),
              event_at: (($tx.time_stamp // "") | tostring)
            }
          | .action_taken = (if (.response_code == 403 or .response_code == 406) then "blocked" else "passed" end)
          | select(.action_taken == "blocked" or ((.rule_id // "") | length) > 0)
          | select(($frule == "") or (.rule_id == $frule))
          | select(($fip == "") or (.source_ip == $fip))
        ] | .[(length - $lim):]
    ' 2>/dev/null || echo "[]")
    count=$(echo "$events_json" | jq 'length' 2>/dev/null || echo 0)

else
    # ── native serial — TEK awk pass → JSON array (robust; eval/subprocess YOK) ──
    # awk her event'i (A→Z bloğu) doğrudan JSON object'e çevirir. SADECE Sec*
    # alanları; gawk gerçek (mesajlı/HTTP2/url-encoded) logu sorunsuz işler.
    raw=$(tail -n "$tail_lines" "$LOG_FILE" 2>/dev/null | awk '
    function jesc(s){ gsub(/\\/,"\\\\",s); gsub(/"/,"\\\"",s); gsub(/[\r\n\t]/," ",s); return s }
    function sevword(s,  u){ u=toupper(s);
      if(u=="0"||u=="EMERGENCY")return"EMERGENCY"; if(u=="1"||u=="ALERT")return"ALERT";
      if(u=="2"||u=="CRITICAL")return"CRITICAL";   if(u=="3"||u=="ERROR")return"ERROR";
      if(u=="4"||u=="WARNING")return"WARNING";     if(u=="5"||u=="NOTICE")return"NOTICE";
      if(u=="6"||u=="INFO")return"INFO";           if(u=="7"||u=="DEBUG")return"DEBUG"; return u }
    function is_b(l){ return (l ~ /^-+[A-Za-z0-9]+-+[A-Z]-+$/) }
    function sl(l){ if (match(l, /[A-Z]-+$/)) return substr(l, RSTART, 1); return "" }
    function reset(){ aL="";bF="";host="";xff="";resp="";rid="";rtag="";sev="";msg="";sect="";started=0 }
    function flush(  ts,rest,a,n,eid,ip,b,nb,method,uri,action,i){
      if (started==0) return
      ts="";eid="";ip="0.0.0.0"
      if (match(aL,/^\[[^]]+\]/)) { ts=substr(aL,2,RLENGTH-2); rest=substr(aL,RLENGTH+1) } else rest=aL
      n=split(rest,a,/[ \t]+/); i=1; while(i<=n&&a[i]=="")i++; eid=a[i]; i++; while(i<=n&&a[i]=="")i++; if(i<=n)ip=a[i]
      if (xff ~ /^[0-9.]+$/ && xff!="") ip=xff
      nb=split(bF,b,/[ \t]+/); method=(nb>=1&&b[1]!=""?b[1]:"?"); uri=(nb>=2&&b[2]!=""?b[2]:"/")
      action=(resp=="403"||resp=="406")?"blocked":"passed"
      if (action!="blocked" && rid=="") { reset(); return }
      if (emitted>0) out=out","
      out=out "{\"event_id\":\"" jesc(eid) "\",\"source_ip\":\"" jesc(ip) "\",\"domain_name\":\"" jesc(host) "\",\"uri\":\"" jesc(uri) "\",\"method\":\"" jesc(method) "\",\"rule_id\":\"" jesc(rid) "\",\"rule_tag\":\"" jesc(rtag) "\",\"severity\":\"" jesc(sevword(sev)) "\",\"reason\":\"" jesc(msg) "\",\"action_taken\":\"" action "\",\"response_code\":" (resp~/^[0-9]+$/?resp:"0") ",\"event_at\":\"" jesc(ts) "\"}"
      emitted++; reset()
    }
    BEGIN { out="["; emitted=0; reset() }
    {
      if (is_b($0)) { L=sl($0); if(L=="A"){ flush(); started=1 } sect=L; next }
      if (started==0) next
      if (sect=="A" && aL=="") aL=$0
      else if (sect=="B" && bF=="") bF=$0
      else if (sect=="B" && tolower($0) ~ /^host:/) { host=$0; sub(/^[Hh]ost:[ \t]*/,"",host); gsub(/\r/,"",host) }
      else if (sect=="B" && tolower($0) ~ /^x-forwarded-for:/) { xff=$0; sub(/^[^:]*:[ \t]*/,"",xff); sub(/,.*$/,"",xff); gsub(/[ \t\r]/,"",xff) }
      else if (sect=="F" && $0 ~ /^HTTP\/[0-9.]+ [0-9]+/) { split($0,p,/[ \t]+/); resp=p[2] }
      else if (sect=="H") {
        if (rid=="" && match($0,/\[id "[0-9]+"\]/)) { m=substr($0,RSTART,RLENGTH); gsub(/\[id "/,"",m); gsub(/"\]/,"",m); rid=m }
        if (rtag=="" && match($0,/\[tag "[^"]+"\]/)) { m=substr($0,RSTART,RLENGTH); gsub(/\[tag "/,"",m); gsub(/"\]/,"",m); rtag=m }
        if (sev=="" && match($0,/severity "[A-Za-z0-9]+"/)) { m=substr($0,RSTART,RLENGTH); gsub(/severity "/,"",m); gsub(/"/,"",m); sev=m }
        if (msg=="" && match($0,/\[msg "[^"]+"\]/)) { m=substr($0,RSTART,RLENGTH); gsub(/\[msg "/,"",m); gsub(/"\]/,"",m); msg=m }
      }
    }
    END { flush(); print out "]" }
    ' 2>/dev/null || echo "[]")

    # Filtre (rule_id/ip) + en yeni $limit kayıt (log kronolojik → son N en yeni)
    events_json=$(printf '%s' "$raw" | jq -c \
        --argjson lim "$limit" --arg frule "$filter_rule" --arg fip "$filter_ip" '
        (if type=="array" then . else [] end)
        | map(select(($frule=="" or .rule_id==$frule) and ($fip=="" or .source_ip==$fip)))
        | .[(length - $lim):]
    ' 2>/dev/null || echo "[]")
    count=$(printf '%s' "$events_json" | jq 'length' 2>/dev/null || echo 0)
fi

jq -nc --argjson events "$events_json" --argjson total "$count" \
    --arg driver "$DRIVER" --arg log_file "$LOG_FILE" --arg log_format "$LOG_FORMAT" \
    '{ok:true, events:$events, total:$total, driver:$driver, log_file:$log_file, log_format:$log_format}' 2>/dev/null \
    || echo "{\"ok\":false,\"events\":[],\"total\":${count:-0},\"driver\":\"${DRIVER}\",\"warn\":\"jq assemble failed\"}"
