#!/bin/bash
# =============================================================================
# security-detect.sh -- Anomaly & Threat Detection Tool
# =============================================================================
# Category:    security
# Bird:        falcon
# Tier:        2
# Node-aware:  yes
# Absorbs:     anomaly.sh, security-anomaly.sh, threat.sh, security-threat.sh
#
# Standard Actions:
#   --send [--check-only|--force]  Full anomaly + threat scan
#   --status                       JSON pill for dashboard
#   --digest                       Summary report
#   --flush                        Reset recovery state + truncate logs
#   --anomaly                      Anomaly scan only (legacy compat)
#   --threat                       Threat scan only (legacy compat)
#   --world                        World-readable file scan
#
# Execution Spine:
#   caller -> s3rv1c3agent -> policy classify -> veil (if write) -> this tool -> DB
# =============================================================================

TOOL_NAME="security-detect"
TOOL_VERSION="3.0.0"
TOOL_TIER=2
TOOL_BIRD="falcon"
TOOL_LOG="/opt/binary/logs/tools/${TOOL_NAME}.log"
TOOL_CONFIG="/etc/binary/${TOOL_NAME}.conf"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ -f "${SCRIPT_DIR}/common.sh" ]]; then source "${SCRIPT_DIR}/common.sh"
elif [[ -f "${SCRIPT_DIR}/../common.sh" ]]; then source "${SCRIPT_DIR}/../common.sh"
else echo "FATAL: common.sh not found" >&2; exit 1; fi

export BIRD_ID="${TOOL_BIRD}"

# Thresholds
RECOVERY_THRESHOLD=${RECOVERY_THRESHOLD:-3}
DISK_CRIT=${DISK_CRIT:-95}
MEM_CRIT=${MEM_CRIT:-95}
RECOVERY_COOLDOWN=${RECOVERY_COOLDOWN:-300}
RECOVERY_LAST="/opt/binary/data/last-recovery.ts"

[ -f "$TOOL_CONFIG" ] && source "$TOOL_CONFIG" 2>/dev/null
[ -f "/etc/binary/anomaly.conf" ] && source "/etc/binary/anomaly.conf" 2>/dev/null

CRITICAL_SERVICES=(nginx pdns postfix dovecot mariadb php-fpm fail2ban sshd)

_is_svc() { systemctl cat "${1}.service" &>/dev/null; }

_installed=()
_detect() {
    _installed=()
    for svc in "${CRITICAL_SERVICES[@]}"; do
        _is_svc "$svc" && _installed+=("$svc")
    done
}

ANOMALY_COUNT=0
ANOMALY_DETAILS=""
CHANGES_MADE=""
_anomaly() { ANOMALY_COUNT=$((ANOMALY_COUNT + 1)); ANOMALY_DETAILS="${ANOMALY_DETAILS}  [${1}] ${2}\n"; echo "  [ANOMALY] ${1}: ${2}"; }
_ok() { echo "  [OK] $1"; }
_change() { CHANGES_MADE="${CHANGES_MADE}  ${1}\n"; echo "  [CHANGED] ${1}"; }

# =============================================================================
# --send  (combined anomaly + threat scan)
# =============================================================================
_do_send_impl() {
    _detect
    ANOMALY_COUNT=0; ANOMALY_DETAILS=""; CHANGES_MADE=""
    echo "=== Detection Scan: $(ts_local) ==="
    echo "  Node: ${NODE_ID}"
    echo "  Installed: ${_installed[*]}"
    echo ""

    # Service anomalies
    echo "--- Services ---"
    for svc in "${_installed[@]}"; do
        local state; state=$(systemctl is-active "$svc" 2>/dev/null)
        if [ "$state" = "active" ]; then _ok "$svc"
        else _anomaly "service" "${svc} is ${state}"; fi
    done

    # Resource anomalies
    echo ""; echo "--- Resources ---"
    local disk_pct; disk_pct=$(df / 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%')
    if [ "${disk_pct:-0}" -ge "$DISK_CRIT" ] 2>/dev/null; then
        _anomaly "disk" "Root at ${disk_pct}%"
    else _ok "Disk: ${disk_pct:-?}%"; fi

    local mem_total mem_used mem_pct
    mem_total=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}')
    mem_used=$(free -m 2>/dev/null | awk '/^Mem:/{print $3}')
    if [ "${mem_total:-0}" -gt 0 ] 2>/dev/null; then
        mem_pct=$((mem_used * 100 / mem_total))
    else mem_pct=0; fi
    if [ "$mem_pct" -ge "$MEM_CRIT" ] 2>/dev/null; then
        _anomaly "memory" "RAM at ${mem_pct}%"
    else _ok "Memory: ${mem_pct}%"; fi

    # Threat scan: world-readable secrets, suspicious processes
    echo ""; echo "--- Threat Scan ---"
    local threats=0
    # World-readable sensitive files
    local world_files
    world_files=$(find /etc/binary /opt/binary/keys 2>/dev/null -maxdepth 2 -perm -o=r -type f 2>/dev/null | head -20)
    if [ -n "$world_files" ]; then
        local wc; wc=$(echo "$world_files" | wc -l)
        _anomaly "threat" "${wc} world-readable file(s) in sensitive paths"
        threats=$((threats + wc))
    else _ok "No world-readable secrets found"; fi

    # Suspicious processes
    local suspicious
    suspicious=$(ps aux 2>/dev/null | grep -iE '(nc -l|ncat|socat|reverse|meterpreter)' | grep -v grep | head -5)
    if [ -n "$suspicious" ]; then
        _anomaly "threat" "Suspicious process detected"
        threats=$((threats + 1))
    else _ok "No suspicious processes"; fi

    # Failed SSH
    local ssh_fails
    ssh_fails=$(journalctl -u sshd --since "1 hour ago" --no-pager 2>/dev/null | grep -c "Failed password" || echo 0)
    if [ "${ssh_fails:-0}" -gt 50 ] 2>/dev/null; then
        _anomaly "threat" "${ssh_fails} failed SSH logins in last hour"
    else _ok "SSH: ${ssh_fails:-0} failed logins (1h)"; fi

    echo ""
    if [ "$ANOMALY_COUNT" -eq 0 ]; then
        echo "  RESULT: HEALTHY -- 0 detections"
        tool_audit "send" "result=healthy anomalies=0 threats=0 node=${NODE_ID}"
    else
        echo "  RESULT: ${ANOMALY_COUNT} detection(s)"
        echo -e "$ANOMALY_DETAILS"
        tool_audit "send" "result=alert anomalies=${ANOMALY_COUNT} node=${NODE_ID}"
    fi
    return $ANOMALY_COUNT
}

_do_status_impl() {
    _detect
    local errors=0 total=${#_installed[@]}
    for svc in "${_installed[@]}"; do
        local s; s=$(systemctl is-active "$svc" 2>/dev/null)
        [ "$s" != "active" ] && errors=$((errors + 1))
    done
    local disk_pct; disk_pct=$(df / 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%')
    local status="healthy"
    [ "$errors" -gt 0 ] && status="degraded"
    [ "$errors" -ge 3 ] && status="down"
    cat <<EOF
{"tool":"${TOOL_NAME}","node":"${NODE_ID}","status":"${status}","services_up":$((total - errors)),"services_total":${total},"disk_pct":${disk_pct:-0},"checked_at":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
EOF
}

_do_digest_impl() {
    echo "=== Detection Report: ${NODE_ID} ==="
    echo "  Tool: ${TOOL_NAME} v${TOOL_VERSION}"
    echo "  Time: $(ts_local)"
    echo ""
    _do_send_impl
}

_do_flush_impl() {
    echo "=== Flushing ${TOOL_NAME} state ==="
    [ -f "$RECOVERY_LAST" ] && rm -f "$RECOVERY_LAST" && _change "Removed recovery timestamp"
    [ -f "$TOOL_LOG" ] && truncate -s 0 "$TOOL_LOG" && _change "Truncated tool log"
    echo "Flush complete on ${NODE_ID}"
}

tool_help() {
    echo "${TOOL_NAME} v${TOOL_VERSION} -- Anomaly & Threat Detection"
    echo ""
    echo "Detection:"
    echo "  --send              Full anomaly + threat scan"
    echo "  --anomaly           Anomaly scan only"
    echo "  --threat            Threat scan only"
    echo "  --intrusion         Rootkit + crypto-miner scan"
    echo "  --canary            Canary trap file check"
    echo "  --world-readable    World-readable secrets audit"
    echo ""
    echo "Jail Management:"
    echo "  --fail2ban <act>    fail2ban (status|jails|banned|unban <ip>)"
    echo "  --crowdsec <act>    CrowdSec (status|decisions|alerts)"
    echo ""
    echo "Verify:"
    echo "  --verify            Full detection verification"
    echo "  --status            JSON pill"
    echo "  --digest            Report"
    echo "  --flush             Reset"
}

# --fail2ban
_do_fail2ban_impl() {
    local action="${1:-status}"
    echo "=== fail2ban: ${NODE_ID} ==="
    if ! command -v fail2ban-client &>/dev/null; then
        echo "  fail2ban not installed"; return 0
    fi
    case "$action" in
        status)
            fail2ban-client status 2>/dev/null | sed 's/^/  /'
            ;;
        jails)
            local jails; jails=$(fail2ban-client status 2>/dev/null | grep "Jail list" | sed 's/.*:\s*//')
            for j in ${jails//,/ }; do
                echo "  --- ${j} ---"
                fail2ban-client status "$j" 2>/dev/null | grep -E "(Currently|Total)" | sed 's/^/    /'
            done
            ;;
        banned)
            local jails; jails=$(fail2ban-client status 2>/dev/null | grep "Jail list" | sed 's/.*:\s*//')
            for j in ${jails//,/ }; do
                local banned; banned=$(fail2ban-client status "$j" 2>/dev/null | grep "Banned IP list" | sed 's/.*:\s*//')
                [ -n "$banned" ] && echo "  ${j}: ${banned}"
            done
            ;;
        unban)
            local ip="${2:-}"
            [ -z "$ip" ] && { echo "Usage: --fail2ban unban <ip>"; return 1; }
            fail2ban-client unban "$ip" 2>/dev/null
            tool_audit "fail2ban_unban" "ip=${ip} node=${NODE_ID}"
            echo "  Unbanned: ${ip}"
            ;;
        *)
            echo "  Actions: status|jails|banned|unban <ip>"
            ;;
    esac
}

# --crowdsec
_do_crowdsec_impl() {
    local action="${1:-status}"
    echo "=== CrowdSec: ${NODE_ID} ==="
    if ! command -v cscli &>/dev/null; then
        echo "  CrowdSec not installed"; return 0
    fi
    case "$action" in
        status)    cscli metrics 2>/dev/null | head -20 | sed 's/^/  /' ;;
        decisions) cscli decisions list 2>/dev/null | head -20 | sed 's/^/  /' ;;
        alerts)    cscli alerts list --limit 10 2>/dev/null | sed 's/^/  /' ;;
        *)         echo "  Actions: status|decisions|alerts" ;;
    esac
}

# --canary
_do_canary_impl() {
    echo "=== Canary: ${NODE_ID} ==="
    local canary_files=("/opt/binary/data/.canary" "/etc/binary/.canary" "/tmp/.canary_trap")
    local pass=0 fail=0
    for f in "${canary_files[@]}"; do
        if [ -f "$f" ]; then
            local age; age=$(stat -c %Y "$f" 2>/dev/null)
            local now; now=$(date +%s)
            local delta=$(( now - ${age:-0} ))
            if [ "$delta" -lt 300 ]; then
                echo "  [ALERT] ${f} modified ${delta}s ago — possible breach"
                fail=$((fail+1))
            else
                echo "  [OK] ${f} intact (age ${delta}s)"
                pass=$((pass+1))
            fi
        else
            echo "  [SKIP] ${f} not deployed"
        fi
    done
    echo "  Pass: ${pass}  Alert: ${fail}"
    tool_audit "canary" "pass=${pass} fail=${fail} node=${NODE_ID}"
}

# --intrusion
_do_intrusion_impl() {
    echo "=== Intrusion Scan: ${NODE_ID} ==="
    # Check for rootkits (basic)
    echo "--- Rootkit indicators ---"
    local rk_found=0
    for f in /dev/.udev /dev/shm/.* /tmp/.ICE-unix/.* /tmp/.X11-unix/.*; do
        [ -e "$f" ] && { echo "  [WARN] Suspicious: ${f}"; rk_found=$((rk_found+1)); }
    done
    [ "$rk_found" -eq 0 ] && echo "  [OK] No rootkit indicators"

    # Check for crypto miners
    echo ""; echo "--- Crypto-miner indicators ---"
    local miners; miners=$(ps aux 2>/dev/null | grep -iE '(minerd|cpuminer|cgminer|bfgminer|stratum)' | grep -v grep | head -5)
    if [ -n "$miners" ]; then
        echo "  [ALERT] Possible miner process detected"
        echo "$miners" | sed 's/^/    /'
    else echo "  [OK] No miner processes detected"; fi

    # Check for unauthorized SUID binaries
    echo ""; echo "--- SUID check ---"
    local suid; suid=$(find /usr/local /opt /tmp -perm -4000 -type f 2>/dev/null | head -10)
    if [ -n "$suid" ]; then
        echo "  [WARN] SUID binaries in unusual locations:"
        echo "$suid" | sed 's/^/    /'
    else echo "  [OK] No unusual SUID binaries"; fi
    tool_audit "intrusion" "rootkits=${rk_found} node=${NODE_ID}"
}

# --verify
_do_verify_impl() {
    echo "=== Detection Verification: ${NODE_ID} ==="
    local pass=0 fail=0
    # Service checks
    _detect
    for svc in "${_installed[@]}"; do
        if systemctl is-active "$svc" &>/dev/null; then pass=$((pass+1))
        else echo "  [FAIL] ${svc} not active"; fail=$((fail+1)); fi
    done
    # fail2ban running
    if systemctl is-active fail2ban &>/dev/null; then echo "  [OK] fail2ban active"; pass=$((pass+1))
    else echo "  [WARN] fail2ban not active"; fi
    # Disk not critical
    local dp; dp=$(df / 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%')
    if [ "${dp:-0}" -lt "$DISK_CRIT" ] 2>/dev/null; then echo "  [OK] Disk: ${dp}%"; pass=$((pass+1))
    else echo "  [FAIL] Disk: ${dp}%"; fail=$((fail+1)); fi
    echo ""
    echo "  Passed: ${pass}  Failed: ${fail}"
    tool_audit "verify" "pass=${pass} fail=${fail} node=${NODE_ID}"
    return $fail
}

_do_world_readable_impl() {
    echo "=== World-Readable Audit: ${NODE_ID} ==="
    local count=0
    while IFS= read -r f; do
        echo "  [WARN] ${f}"
        count=$((count + 1))
    done < <(find /etc/binary /opt/binary/keys 2>/dev/null -maxdepth 2 -perm -o=r -type f 2>/dev/null)
    [ "$count" -eq 0 ] && echo "  [OK] No world-readable files found"
    echo "  Total: ${count}"
    tool_audit "world_readable" "count=${count} node=${NODE_ID}"
}

do_send()   { tool_wrap "send"   _do_send_impl "$@"; }
do_status() { tool_wrap "status" _do_status_impl "$@"; }
do_digest() { tool_wrap "digest" _do_digest_impl "$@"; }
do_flush()  { tool_wrap "flush"  _do_flush_impl "$@"; }

tool_init
trap 'tool_complete' EXIT
case "${1:---help}" in
    --send)           shift; do_send "$@" ;;
    --status)         shift; do_status "$@" ;;
    --digest)         shift; do_digest "$@" ;;
    --flush)          shift; do_flush "$@" ;;
    --anomaly)        shift; do_send --check-only "$@" ;;
    --threat)         shift; do_send "$@" ;;
    --intrusion)      tool_wrap "intrusion" _do_intrusion_impl ;;
    --canary)         tool_wrap "canary" _do_canary_impl ;;
    --fail2ban)       shift; tool_wrap "fail2ban" _do_fail2ban_impl "$@" ;;
    --crowdsec)       shift; tool_wrap "crowdsec" _do_crowdsec_impl "$@" ;;
    --world-readable) tool_wrap "world_readable" _do_world_readable_impl ;;
    --verify)         tool_wrap "verify" _do_verify_impl ;;
    --help)           tool_help ;;
    *)                handle_standard_command "$@" ;;
esac

