#!/usr/bin/env bash
#===============================================================================
# chequeo-servidor.sh — Chequeo diario de salud de un VPS Ubuntu (Ebenezer IT · ebenezer.org.es)
#-------------------------------------------------------------------------------
# Uso:
#   sudo ./chequeo-servidor.sh            # informe completo, coloreado
#   sudo ./chequeo-servidor.sh --plain    # sin color (para cron / correo)
#   sudo ./chequeo-servidor.sh --quiet    # solo muestra WARN/FAIL + resumen
#
# Código de salida:  0 = todo OK   |   1 = hay avisos (WARN)   |   2 = hay fallos (FAIL)
# (útil para que cron te avise por correo solo cuando algo va mal)
#
# Requiere ejecutarse como root (o con sudo): varias comprobaciones lo necesitan.
#===============================================================================
set -uo pipefail

#------------------------------- CONFIGURACIÓN ---------------------------------
DOMAIN="tudominio.com"   # <-- CAMBIA ESTO por tu dominio
CERT_FILE="/etc/letsencrypt/live/${DOMAIN}/fullchain.pem"

# Servicios systemd que deben estar activos
SERVICES=(ssh nginx fail2ban)   # añade los tuyos: postfix dovecot docker crowdsec ...

# Contenedores Docker que deben estar corriendo
DOCKER_CONTAINERS=()   # ej.: (wg-easy adguardhome) — vacío si no usas Docker

# Puertos TCP que deben estar a la escucha  (etiqueta:puerto)
TCP_PORTS=("ssh:22" "https:443")   # etiqueta:puerto — añade 25, 587, 993... si tienes correo

# Umbrales
DISK_WARN=80      # % de uso de / a partir del cual avisar
DISK_CRIT=90      # % de uso de / que se considera fallo
MEM_WARN=90       # % de uso de RAM a partir del cual avisar
SWAP_WARN=60      # % de uso de swap a partir del cual avisar
CERT_WARN_DAYS=21 # avisar si el certificado caduca en menos de N días
ERR_WARN=25       # nº de errores en el journal (24h) a partir del cual avisar
#-------------------------------------------------------------------------------

# --- Parseo de argumentos ---
PLAIN=0; QUIET=0
for a in "$@"; do
  case "$a" in
    --plain) PLAIN=1 ;;
    --quiet) QUIET=1 ;;
  esac
done

# --- Colores (solo si es terminal y no se pidió --plain) ---
if [[ -t 1 && $PLAIN -eq 0 ]]; then
  G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; B=$'\e[36m'; BOLD=$'\e[1m'; DIM=$'\e[2m'; N=$'\e[0m'
else
  G=""; Y=""; R=""; B=""; BOLD=""; DIM=""; N=""
fi

# --- Contadores y funciones de reporte ---
OKC=0; WARNC=0; FAILC=0
PROBLEMS=()
ok()      { [[ $QUIET -eq 0 ]] && printf "  ${G}[ OK ]${N} %s\n" "$1"; OKC=$((OKC+1)); }
warn()    { printf "  ${Y}[WARN]${N} %s\n" "$1"; WARNC=$((WARNC+1)); PROBLEMS+=("${Y}WARN${N} $1"); }
fail()    { printf "  ${R}[FAIL]${N} %s\n" "$1"; FAILC=$((FAILC+1)); PROBLEMS+=("${R}FAIL${N} $1"); }
info()    { [[ $QUIET -eq 0 ]] && printf "  ${DIM}[info]${N} %s\n" "$1"; }
section() { [[ $QUIET -eq 0 ]] && printf "\n${BOLD}${B}== %s ==${N}\n" "$1"; }
have()    { command -v "$1" >/dev/null 2>&1; }

# --- Aviso si no es root ---
if [[ $EUID -ne 0 ]]; then
  printf "${Y}Aviso:${N} no te estás ejecutando como root; algunas comprobaciones pueden fallar. Usa 'sudo'.\n\n"
fi

#------------------------------- CABECERA --------------------------------------
printf "${BOLD}CHEQUEO DE SALUD — %s${N}\n" "$(hostname -f 2>/dev/null || hostname)"
printf "${DIM}%s | kernel %s | uptime:%s${N}\n" "$(date '+%Y-%m-%d %H:%M:%S %Z')" "$(uname -r)" "$(uptime -p | sed 's/^up //')"

#--------------------------- 1. ESTADO GLOBAL ----------------------------------
section "Estado global del sistema"
sysstate=$(systemctl is-system-running 2>/dev/null || true)
case "$sysstate" in
  running)  ok "systemd: running" ;;
  degraded) fail "systemd: DEGRADED — hay unidades fallidas (ver abajo)" ;;
  *)        warn "systemd: estado '$sysstate'" ;;
esac
failed_units=$(systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}' | tr '\n' ' ' | sed 's/ $//')
if [[ -n "$failed_units" ]]; then
  fail "Unidades fallidas: $failed_units"
else
  ok "Sin unidades fallidas"
fi

#--------------------------- 2. SERVICIOS CLAVE --------------------------------
section "Servicios clave"
for svc in "${SERVICES[@]}"; do
  st=$(systemctl is-active "$svc" 2>/dev/null || true)
  if [[ "$st" == "active" ]]; then
    ok "$svc: active"
  elif [[ "$st" == "activating" ]]; then
    warn "$svc: activating (no terminó de arrancar)"
  else
    # Ignora servicios no instalados
    if systemctl list-unit-files "${svc}.service" 2>/dev/null | grep -q "${svc}.service"; then
      fail "$svc: $st"
    else
      info "$svc: no instalado (omitido)"
    fi
  fi
done

#--------------------------- 3. CONTENEDORES DOCKER ----------------------------
if have docker; then
  section "Contenedores Docker"
  for c in "${DOCKER_CONTAINERS[@]}"; do
    if docker ps --filter "name=^/${c}$" --filter "status=running" --format '{{.Names}}' 2>/dev/null | grep -q "^${c}$"; then
      ok "contenedor '$c': Up"
    else
      fail "contenedor '$c': NO está corriendo"
    fi
  done
fi

#--------------------------- 4. RECURSOS ---------------------------------------
section "Recursos (disco, memoria, carga)"
# Disco /
duse=$(df -P / | awk 'NR==2{gsub("%","",$5); print $5}')
if   (( duse >= DISK_CRIT )); then fail "Disco / al ${duse}% (crítico, umbral ${DISK_CRIT}%)"
elif (( duse >= DISK_WARN )); then warn "Disco / al ${duse}% (umbral ${DISK_WARN}%)"
else ok "Disco / al ${duse}%"; fi
# Otras particiones al 90%+
while read -r line; do
  warn "Partición casi llena: $line"
done < <(df -P -x tmpfs -x devtmpfs 2>/dev/null | awk 'NR>1 && $1!="/" {gsub("%","",$5); if($5>=90) print $6" ("$5"%)"}')

# Memoria
mem_pct=$(free | awk '/^Mem:/{printf "%.0f", $3/$2*100}')
if (( mem_pct >= MEM_WARN )); then warn "RAM al ${mem_pct}% (umbral ${MEM_WARN}%)"; else ok "RAM al ${mem_pct}%"; fi
# Swap
swap_pct=$(free | awk '/^Swap:/{ if($2>0) printf "%.0f", $3/$2*100; else print 0}')
if (( swap_pct >= SWAP_WARN )); then warn "Swap al ${swap_pct}% (umbral ${SWAP_WARN}%)"; else ok "Swap al ${swap_pct}%"; fi
# Carga vs núcleos
cores=$(nproc)
load1=$(cut -d' ' -f1 /proc/loadavg)
if awk "BEGIN{exit !($load1 > $cores)}"; then
  warn "Carga (1m) = ${load1} supera los ${cores} núcleos"
else
  ok "Carga (1m) = ${load1} (núcleos: ${cores})"
fi

#--------------------------- 5. REINICIO PENDIENTE -----------------------------
section "Actualizaciones y reinicio"
if [[ -f /var/run/reboot-required ]]; then
  warn "Reinicio pendiente ($(cat /var/run/reboot-required.pkgs 2>/dev/null | tr '\n' ' '))"
else
  ok "No hay reinicio pendiente"
fi
# Paquetes actualizables
if [[ -x /usr/lib/update-notifier/apt-check ]]; then
  read -r upd sec < <(/usr/lib/update-notifier/apt-check 2>&1 | tr ';' ' ')
  upd=${upd:-0}; sec=${sec:-0}
  if   (( sec > 0 )); then warn "${upd} paquetes actualizables (${sec} de seguridad)"
  elif (( upd > 0 )); then info "${upd} paquetes actualizables (0 de seguridad)"
  else ok "Sistema al día (0 paquetes)"; fi
elif have apt; then
  upd=$(apt list --upgradable 2>/dev/null | grep -c upgradable || true)
  (( upd > 0 )) && info "${upd} paquetes actualizables (según el último apt update)" || ok "Sin actualizaciones pendientes"
fi

#--------------------------- 6. CERTIFICADO TLS --------------------------------
section "Certificado TLS ($DOMAIN)"
if [[ -f "$CERT_FILE" ]] && have openssl; then
  end=$(openssl x509 -enddate -noout -in "$CERT_FILE" 2>/dev/null | cut -d= -f2)
  if [[ -n "$end" ]]; then
    days=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
    if   (( days < 0 ));               then fail "Certificado CADUCADO hace $(( -days )) días"
    elif (( days < CERT_WARN_DAYS ));  then warn "Certificado caduca en ${days} días"
    else ok "Certificado válido ${days} días más"; fi
  else
    warn "No se pudo leer la fecha del certificado"
  fi
else
  info "Certificado no encontrado en $CERT_FILE (omitido)"
fi

#--------------------------- 7. APPARMOR ---------------------------------------
if have aa-status; then
  section "AppArmor"
  if aa-status --enabled 2>/dev/null; then
    nprof=$(aa-status 2>/dev/null | awk '/profiles are loaded/{print $1}')
    ok "AppArmor activo (${nprof:-?} perfiles cargados)"
    if aa-status 2>/dev/null | grep -q "ejabberdctl"; then
      ok "Perfil de ejabberd cargado"
    else
      warn "El perfil de ejabberd NO aparece cargado"
    fi
  else
    warn "AppArmor no está habilitado"
  fi
fi

#--------------------------- 8. CROWDSEC ---------------------------------------
if have cscli; then
  section "CrowdSec"
  if systemctl is-active crowdsec >/dev/null 2>&1; then
    # Bouncer conectado
    if cscli bouncers list -o raw 2>/dev/null | tail -n +2 | grep -q .; then
      ok "Bouncer(s) registrados: $(cscli bouncers list -o raw 2>/dev/null | tail -n +2 | cut -d, -f1 | tr '\n' ' ')"
    else
      warn "CrowdSec activo pero sin bouncers registrados (no bloquea)"
    fi
    ndec=$(cscli decisions list -o raw 2>/dev/null | tail -n +2 | wc -l)
    info "Decisiones activas (bloqueos locales): ${ndec}"
  else
    fail "CrowdSec no está activo"
  fi
fi

#--------------------------- 9. FAIL2BAN ---------------------------------------
if have fail2ban-client; then
  section "Fail2ban"
  jails=$(fail2ban-client status 2>/dev/null | awk -F: '/Jail list/{gsub(/[ \t]/,"",$2); print $2}')
  if [[ -n "$jails" ]]; then
    total_ban=0
    for j in ${jails//,/ }; do
      b=$(fail2ban-client status "$j" 2>/dev/null | awk -F: '/Currently banned/{gsub(/[ \t]/,"",$2); print $2}')
      total_ban=$(( total_ban + ${b:-0} ))
    done
    ok "Jails activas: ${jails} | IPs baneadas ahora: ${total_ban}"
  else
    warn "Fail2ban sin jails activas"
  fi
fi

#--------------------------- 10. PUERTOS A LA ESCUCHA --------------------------
if have ss; then
  section "Puertos a la escucha"
  listening=$(ss -H -tln 2>/dev/null | awk '{print $4}' | sed 's/.*://' | sort -u)
  for entry in "${TCP_PORTS[@]}"; do
    label="${entry%%:*}"; port="${entry##*:}"
    if grep -qx "$port" <<< "$listening"; then
      ok "${label} (TCP ${port}) escuchando"
    else
      fail "${label} (TCP ${port}) NO está escuchando"
    fi
  done
fi

#--------------------------- 11. COLA DE CORREO -------------------------------
if have mailq || have postqueue; then
  section "Cola de correo"
  qn=$(postqueue -p 2>/dev/null | awk 'END{print $5}')
  [[ "$qn" =~ ^[0-9]+$ ]] || qn=0
  if   (( qn > 50 )); then warn "Cola de correo con ${qn} mensajes"
  elif (( qn > 0 ));  then info "Cola de correo con ${qn} mensajes"
  else ok "Cola de correo vacía"; fi
fi

#--------------------------- 12. ERRORES RECIENTES ----------------------------
if have journalctl; then
  section "Errores del sistema (últimas 24h)"
  nerr=$(journalctl -p err --since "24 hours ago" --no-pager -q 2>/dev/null | wc -l)
  if   (( nerr >= ERR_WARN )); then warn "${nerr} errores (prioridad err) en 24h — revisa: journalctl -p err --since '24h ago'"
  elif (( nerr > 0 ));         then info "${nerr} errores (prioridad err) en 24h"
  else ok "Sin errores de prioridad 'err' en 24h"; fi
fi

#------------------------------- RESUMEN ---------------------------------------
printf "\n${BOLD}== Resumen ==${N}\n"
printf "  ${G}%d OK${N}   ${Y}%d avisos${N}   ${R}%d fallos${N}\n" "$OKC" "$WARNC" "$FAILC"
if (( ${#PROBLEMS[@]} > 0 )); then
  printf "\n${BOLD}Requiere atención:${N}\n"
  for p in "${PROBLEMS[@]}"; do printf "  - %b\n" "$p"; done
fi

if   (( FAILC > 0 )); then printf "\n${R}${BOLD}>>> ESTADO: FALLO — revisa el servidor.${N}\n"; exit 2
elif (( WARNC > 0 )); then printf "\n${Y}${BOLD}>>> ESTADO: CON AVISOS.${N}\n";              exit 1
else                       printf "\n${G}${BOLD}>>> ESTADO: TODO EN ORDEN.${N}\n";            exit 0
fi
