#!/usr/bin/env bash
# ============================================================
# HalaVoice — server compromise (IOC) scanner & cleaner
#
# DEFENSIVE / incident-response tool. Default mode is a READ-ONLY
# scan that reports indicators of compromise. Cleanup is opt-in.
#
#   ops/security-scan.sh                 # scan only (read-only)
#   ops/security-scan.sh --clean         # scan + quarantine known IOCs (prompts YES)
#   ops/security-scan.sh --clean --yes   # non-interactive cleanup (for a trusted operator)
#   ops/security-scan.sh --quarantine DIR # where removed artifacts are backed up
#
# Cleanup ONLY touches high-confidence attacker artifacts (malicious
# cron lines + attacker sudoers drop-ins), always backing them up first.
# It NEVER deletes users, SSH keys, or system binaries — those are
# reported for you to judge.
#
# ⚠️  A compromised host cannot be fully trusted after cleanup. Treat
#     this as triage/containment; plan a clean rebuild + credential
#     rotation. Puppet-managed crontabs may be re-poisoned automatically.
#
# Exit code: 0 = clean, 1 = IOCs found.
# ============================================================
set -uo pipefail

CLEAN=0 ; ASSUME_YES=0
QUARANTINE="/root/ioc-quarantine-$(date -u +%Y%m%d-%H%M%S)"
while [ $# -gt 0 ]; do
  case "$1" in
    --clean) CLEAN=1 ;;
    --yes) ASSUME_YES=1 ;;
    --quarantine) QUARANTINE="$2"; shift ;;
    -h|--help) sed -n '2,30p' "$0"; exit 0 ;;
    *) echo "unknown arg: $1" >&2; exit 2 ;;
  esac
  shift
done

if [ -t 1 ]; then
  RED=$'\033[1;31m'; YEL=$'\033[1;33m'; GRN=$'\033[1;32m'; CYN=$'\033[1;36m'; DIM=$'\033[2m'; NC=$'\033[0m'
else
  RED=''; YEL=''; GRN=''; CYN=''; DIM=''; NC=''   # clean logs when piped/cron
fi
FINDINGS=0
hit()  { FINDINGS=$((FINDINGS+1)); echo "${RED}[IOC]${NC} $*"; }
warnx(){ echo "${YEL}[?]${NC} $*"; }
info() { echo "${CYN}[*]${NC} $*"; }
okx()  { echo "${GRN}[ok]${NC} $*"; }
sec()  { echo; echo "${CYN}==== $* ====${NC}"; }

[ "$(id -u)" = "0" ] || warnx "not running as root — some checks will be incomplete (re-run with sudo)"

# High-confidence malicious patterns (known from this host + generic).
IOC_REGEX='cavsystem\.com|Kermit123|curl[^|]*\|[[:space:]]*(sh|bash)|wget[^|]*\|[[:space:]]*(sh|bash)|/dev/tcp/|base64[[:space:]]+-d[[:space:]]*\|[[:space:]]*(sh|bash)|chpasswd|\.onion|xmrig|minerd|kinsing|kdevtmpfsi'

mkdir_q() { [ "$CLEAN" = 1 ] && mkdir -p "$QUARANTINE" 2>/dev/null || true; }
confirm() {
  [ "$ASSUME_YES" = 1 ] && return 0
  read -r -p "  ${YEL}$1${NC} [type YES]: " a; [ "$a" = "YES" ]
}

# ------------------------------------------------------------
# 1) CRON — every location an attacker plants persistence
# ------------------------------------------------------------
sec "Cron persistence"
CRON_FILES=()
[ -f /etc/crontab ] && CRON_FILES+=(/etc/crontab)
for d in /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly; do
  [ -d "$d" ] && while IFS= read -r f; do CRON_FILES+=("$f"); done < <(find "$d" -maxdepth 1 -type f 2>/dev/null)
done
for d in /var/spool/cron /var/spool/cron/crontabs; do
  [ -d "$d" ] && while IFS= read -r f; do CRON_FILES+=("$f"); done < <(find "$d" -maxdepth 1 -type f 2>/dev/null)
done

BAD_CRON=()
for f in "${CRON_FILES[@]}"; do
  if grep -nE "$IOC_REGEX" "$f" 2>/dev/null | grep -vE '^\s*[0-9]+:\s*#' >/dev/null 2>&1; then
    hit "malicious cron in $f:"
    grep -nE "$IOC_REGEX" "$f" 2>/dev/null | grep -vE '^\s*[0-9]+:\s*#' | sed "s/^/      ${DIM}/;s/$/${NC}/"
    BAD_CRON+=("$f")
  fi
done
[ ${#BAD_CRON[@]} -eq 0 ] && okx "no malicious patterns in cron"

# ------------------------------------------------------------
# 2) SUDOERS — attacker NOPASSWD drop-ins
# Auto-cleanable ONLY when a detected malicious cron line CREATES the
# file (e.g. `...>/etc/sudoers.d/99-pakchoi`). Other blanket-NOPASSWD
# drop-ins (e.g. cPanel wp-toolkit, Ant Media) are legitimate and are
# reported for REVIEW only — never auto-removed.
# ------------------------------------------------------------
sec "Sudoers"
BAD_SUDO=()
# sudoers files that the malicious cron writes to = high-confidence attacker
declare -A ATTACKER_SUDO=()
for f in "${BAD_CRON[@]:-}"; do
  [ -n "$f" ] || continue
  while IFS= read -r s; do [ -n "$s" ] && ATTACKER_SUDO["$s"]=1; done < <(
    grep -oE '/etc/sudoers\.d/[A-Za-z0-9._-]+' "$f" 2>/dev/null | sort -u)
done
if [ -d /etc/sudoers.d ]; then
  while IFS= read -r f; do
    grep -qE 'NOPASSWD' "$f" 2>/dev/null || continue
    if [ -n "${ATTACKER_SUDO[$f]:-}" ] || grep -qE "$IOC_REGEX" "$f" 2>/dev/null; then
      hit "attacker sudoers drop-in (created by malicious cron): $f"
      sed "s/^/      ${DIM}/;s/$/${NC}/" "$f" 2>/dev/null | head -3
      BAD_SUDO+=("$f")
    elif grep -qE 'ALL[[:space:]]*=\(ALL\)[[:space:]]*NOPASSWD:[[:space:]]*ALL' "$f" 2>/dev/null; then
      warnx "blanket NOPASSWD drop-in (review — likely legit, e.g. cPanel/AntMedia): $f"
    else
      warnx "NOPASSWD entry (review): $f"
    fi
  done < <(find /etc/sudoers.d -maxdepth 1 -type f 2>/dev/null)
fi
[ ${#BAD_SUDO[@]} -eq 0 ] && okx "no attacker-created sudoers drop-ins"

# ------------------------------------------------------------
# 3) USERS — uid 0, empty passwords, recent additions
# ------------------------------------------------------------
sec "Accounts"
# process substitution (not a pipe) so hit() increments the real counter
while read -r u; do
  [ -n "$u" ] && hit "non-root account with UID 0: $u  ${DIM}(note: pakchoi=uid0 is this app's run user)${NC}"
done < <(awk -F: '($3==0 && $1!="root"){print $1}' /etc/passwd 2>/dev/null)
if [ -r /etc/shadow ]; then
  while read -r u; do
    [ -n "$u" ] && hit "account with EMPTY password: $u"
  done < <(awk -F: '($2==""){print $1}' /etc/shadow 2>/dev/null)
fi
info "recently-added users (by /etc/passwd mtime order, last 5):"
ls -lt /etc/passwd >/dev/null 2>&1 && tail -5 /etc/passwd | awk -F: '{print "      "$1" (uid "$3", shell "$7")"}'

# ------------------------------------------------------------
# 4) SSH — backdoor authorized_keys
# ------------------------------------------------------------
sec "SSH authorized_keys"
FOUND_KEYS=0
while IFS= read -r ak; do
  [ -f "$ak" ] || continue
  n=$(grep -cvE '^\s*#|^\s*$' "$ak" 2>/dev/null || echo 0)
  [ "$n" -gt 0 ] || continue
  FOUND_KEYS=1
  warnx "$ak has $n key(s) — verify each belongs to you:"
  while IFS= read -r line; do
    [ -z "$line" ] && continue
    cmt=$(echo "$line" | awk '{print $NF}')
    echo "      ${DIM}$cmt${NC}"
  done < <(grep -vE '^\s*#|^\s*$' "$ak" 2>/dev/null)
done < <(find /root /home -maxdepth 3 -name authorized_keys 2>/dev/null)
[ "$FOUND_KEYS" = 0 ] && okx "no authorized_keys files found"

# ------------------------------------------------------------
# 5) STARTUP / PRELOAD persistence
# ------------------------------------------------------------
sec "Startup & preload hooks"
if [ -s /etc/ld.so.preload ]; then hit "/etc/ld.so.preload is non-empty (rootkit hook):"; sed 's/^/      /' /etc/ld.so.preload; else okx "/etc/ld.so.preload empty/absent"; fi
[ -n "${LD_PRELOAD:-}" ] && hit "LD_PRELOAD set in environment: $LD_PRELOAD"
for rc in /etc/rc.local /etc/rc.d/rc.local; do
  [ -f "$rc" ] && grep -qE "$IOC_REGEX" "$rc" 2>/dev/null && { hit "malicious content in $rc"; grep -nE "$IOC_REGEX" "$rc" | sed 's/^/      /'; }
done
for prof in /etc/profile /etc/bash.bashrc /root/.bashrc /root/.bash_profile /etc/profile.d/*.sh; do
  [ -f "$prof" ] && grep -qE "$IOC_REGEX" "$prof" 2>/dev/null && hit "suspicious shell-init hook: $prof"
done
# systemd units modified in the last 14 days
if command -v systemctl >/dev/null 2>&1; then
  RECENT_UNITS=$(find /etc/systemd/system /run/systemd/system -name '*.service' -o -name '*.timer' 2>/dev/null | xargs -r ls -lt 2>/dev/null | head -6)
  [ -n "$RECENT_UNITS" ] && { info "newest systemd units (review for unknowns):"; echo "$RECENT_UNITS" | awk '{print "      "$6" "$7" "$8" "$9}'; }
fi

# ------------------------------------------------------------
# 6) NETWORK / PROCESSES
# ------------------------------------------------------------
sec "Network & processes"
if command -v ss >/dev/null 2>&1; then
  info "listening sockets:"
  ss -tlnp 2>/dev/null | awk 'NR>1{print "      "$4"  "$6}' | head -25
fi
# processes running from tmp / with deleted binaries (classic malware)
info "processes running from /tmp, /dev/shm or /var/tmp:"
BADPROC=$(ls -l /proc/*/exe 2>/dev/null | grep -E '/tmp/|/dev/shm/|/var/tmp/' || true)
[ -n "$BADPROC" ] && { hit "process executing from a temp dir:"; echo "$BADPROC" | sed 's/^/      /'; } || okx "none"
DELPROC=$(ls -l /proc/*/exe 2>/dev/null | grep '(deleted)' || true)
[ -n "$DELPROC" ] && warnx "process(es) with deleted on-disk binary (could be legit restarts, or malware):" && echo "$DELPROC" | sed 's/^/      /' | head -5

# ------------------------------------------------------------
# 7) SUSPICIOUS FILES in temp dirs
# ------------------------------------------------------------
sec "Executables in temp dirs"
TMPX=$(find /tmp /dev/shm /var/tmp -maxdepth 2 -type f -perm -u+x 2>/dev/null | head -20)
[ -n "$TMPX" ] && { warnx "executable files in temp dirs (review):"; echo "$TMPX" | sed 's/^/      /'; } || okx "no executables in temp dirs"

# ------------------------------------------------------------
# CLEANUP (opt-in)
# ------------------------------------------------------------
if [ "$CLEAN" = 1 ] && { [ ${#BAD_CRON[@]} -gt 0 ] || [ ${#BAD_SUDO[@]} -gt 0 ]; }; then
  sec "Cleanup (quarantine → $QUARANTINE)"
  echo "${YEL}This removes only the malicious cron lines and attacker sudoers drop-ins shown above.${NC}"
  echo "${YEL}Backups are saved to the quarantine dir. This does NOT fully remediate a compromised host.${NC}"
  if confirm "Proceed with cleanup?"; then
    mkdir_q
    # cron: strip only the offending lines, back up the whole file first
    for f in "${BAD_CRON[@]}"; do
      cp -a "$f" "$QUARANTINE/$(echo "$f" | tr '/' '_').bak" 2>/dev/null
      grep -vE "$IOC_REGEX" "$f" > "$f.ioc-clean" 2>/dev/null && cat "$f.ioc-clean" > "$f" && rm -f "$f.ioc-clean"
      okx "cleaned cron lines in $f (backup in quarantine)"
      case "$f" in
        /etc/crontab|/etc/cron.d/*) warnx "  $f may be puppet-managed and could be RE-poisoned — verify puppet manifests / escalate to host." ;;
      esac
    done
    # sudoers: quarantine attacker drop-in, validate before keeping the change
    for f in "${BAD_SUDO[@]}"; do
      cp -a "$f" "$QUARANTINE/$(echo "$f" | tr '/' '_').bak" 2>/dev/null
      rm -f "$f"
      if command -v visudo >/dev/null 2>&1 && ! visudo -c >/dev/null 2>&1; then
        warnx "  visudo reports sudoers now invalid — RESTORING $f from quarantine"
        cp -a "$QUARANTINE/$(echo "$f" | tr '/' '_').bak" "$f"
      else
        okx "removed attacker sudoers drop-in $f (backup in quarantine)"
      fi
    done
    echo
    echo "${YEL}NEXT STEPS (containment is not remediation):${NC}"
    echo "  1. Rotate ALL credentials (DB, gh token, API keys, SSH). Assume they leaked."
    echo "  2. Inspect & remove unknown SSH authorized_keys and unexpected accounts."
    echo "  3. Escalate to the hosting provider; a compromised root host should be rebuilt clean."
    echo "  4. Re-run this scan to confirm the IOCs stay gone (watch for puppet re-adding them)."
  else
    warnx "cleanup skipped by user"
  fi
elif [ "$CLEAN" = 1 ]; then
  okx "nothing auto-cleanable (no malicious cron/sudoers artifacts)"
fi

# ------------------------------------------------------------
sec "Summary"
if [ "$FINDINGS" -gt 0 ]; then
  echo "${RED}$FINDINGS indicator(s) of compromise found.${NC} Review above; run with --clean to quarantine the auto-cleanable ones."
  exit 1
else
  okx "No high-confidence IOCs detected. (Absence of IOCs is not proof of a clean host.)"
  exit 0
fi
