#!/usr/bin/env bash
#
# AlertKick Linux hardening audit - read-only.
# https://alertkick.com/tools/hardening-score
#
# WHAT THIS DOES: reads config and system state, prints a score and findings.
# WHAT THIS DOES NOT DO: change anything, install anything, or phone home.
# There is not a single write, curl, wget, or apt in this script - read it
# before you run it (that's the whole point of a security tool).
#
# Usage:   bash hardening-audit.sh
# Root is not required, but a few checks read root-only files (sshd effective
# config, some logs) and will say "need root" when run as a normal user.

set -u
LC_ALL=C

PASS=0; TOTAL=0
declare -a GOOD FIX WARN

ok()   { GOOD+=("$1"); PASS=$((PASS+1)); TOTAL=$((TOTAL+1)); }
bad()  { FIX+=("$1");  TOTAL=$((TOTAL+1)); }
warn() { WARN+=("$1"); }

have() { command -v "$1" >/dev/null 2>&1; }

# --- SSH ---------------------------------------------------------------------
SSHD_EFFECTIVE=""
if have sshd && [ "$(id -u)" = 0 ]; then
  SSHD_EFFECTIVE="$(sshd -T 2>/dev/null)"
fi
sshd_val() { # look up effective value, else fall back to grepping config
  local key="$1"
  if [ -n "$SSHD_EFFECTIVE" ]; then
    echo "$SSHD_EFFECTIVE" | awk -v k="$key" 'tolower($1)==tolower(k){print tolower($2); found=1} END{if(!found) print "?"}'
  else
    grep -rhiE "^[[:space:]]*${key}[[:space:]]" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/ 2>/dev/null \
      | tail -1 | awk '{print tolower($2)}' | grep . || echo "?"
  fi
}

if have sshd; then
  v="$(sshd_val permitrootlogin)"
  case "$v" in
    no|prohibit-password|forced-commands-only) ok "SSH: root login restricted (PermitRootLogin $v)";;
    "?") warn "SSH: could not read PermitRootLogin (run as root for effective config)";;
    *) bad "SSH: PermitRootLogin is '$v' - set 'PermitRootLogin no'";;
  esac

  v="$(sshd_val passwordauthentication)"
  case "$v" in
    no) ok "SSH: password authentication disabled";;
    "?") warn "SSH: could not read PasswordAuthentication (run as root)";;
    *) bad "SSH: password auth enabled - set 'PasswordAuthentication no' and use keys";;
  esac

  v="$(sshd_val permitemptypasswords)"
  [ "$v" = "no" ] && ok "SSH: empty passwords rejected" || { [ "$v" = "?" ] || bad "SSH: PermitEmptyPasswords is '$v' - set 'no'"; }
else
  warn "SSH: sshd not found - skipping SSH checks"
fi

# --- Firewall ----------------------------------------------------------------
fw_active=0
if have ufw && ufw status 2>/dev/null | grep -qi "Status: active"; then fw_active=1; fi
if have firewall-cmd && firewall-cmd --state 2>/dev/null | grep -qi running; then fw_active=1; fi
if have nft && nft list ruleset 2>/dev/null | grep -q 'hook input'; then fw_active=1; fi
if [ "$(id -u)" != 0 ]; then
  warn "Firewall: run as root to reliably read firewall state"
elif [ "$fw_active" = 1 ]; then ok "Firewall: an active packet filter is present"
else bad "Firewall: no active firewall detected - enable ufw/firewalld/nftables with default-deny inbound"
fi

# --- Automatic updates -------------------------------------------------------
auto=0
[ -f /etc/apt/apt.conf.d/20auto-upgrades ] && grep -q '"1"' /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null && auto=1
have dnf-automatic && systemctl is-enabled --quiet dnf-automatic.timer 2>/dev/null && auto=1
systemctl is-enabled --quiet unattended-upgrades 2>/dev/null && auto=1
[ "$auto" = 1 ] && ok "Updates: automatic security updates are enabled" \
  || bad "Updates: automatic security updates not detected - enable unattended-upgrades or dnf-automatic"

# Pending reboot after kernel update
if [ -f /var/run/reboot-required ]; then warn "Updates: a reboot is pending (/var/run/reboot-required)"; fi
if have needs-restarting && ! needs-restarting -r >/dev/null 2>&1; then warn "Updates: needs-restarting reports a reboot is required"; fi

# --- Accounts ----------------------------------------------------------------
empty_pw="$(awk -F: '($2==""){print $1}' /etc/shadow 2>/dev/null)"
if [ "$(id -u)" != 0 ]; then warn "Accounts: run as root to check for empty passwords"
elif [ -z "$empty_pw" ]; then ok "Accounts: no accounts with empty passwords"
else bad "Accounts: empty password for: $(echo $empty_pw | tr '\n' ' ')"; fi

uid0="$(awk -F: '($3==0){print $1}' /etc/passwd 2>/dev/null | grep -vx root)"
[ -z "$uid0" ] && ok "Accounts: root is the only UID 0 account" \
  || bad "Accounts: extra UID 0 account(s): $uid0 - these are root-equivalent"

# --- SUID / world-writable spot checks --------------------------------------
ww="$(find /etc -maxdepth 2 -perm -0002 -type f 2>/dev/null | head -5)"
[ -z "$ww" ] && ok "Permissions: no world-writable files in /etc" \
  || bad "Permissions: world-writable files under /etc: $(echo $ww | tr '\n' ' ')"

# --- Auditing / logging ------------------------------------------------------
if systemctl is-active --quiet auditd 2>/dev/null || pgrep -x auditd >/dev/null 2>&1; then
  ok "Auditing: auditd is running"
else
  bad "Auditing: auditd not running - install and enable it for a tamper-resistant record"
fi

# --- Kernel hardening sysctls ------------------------------------------------
sc() { sysctl -n "$1" 2>/dev/null; }
[ "$(sc kernel.randomize_va_space)" = 2 ] && ok "Kernel: ASLR fully enabled" || bad "Kernel: ASLR not at 2 (kernel.randomize_va_space)"
[ "$(sc net.ipv4.tcp_syncookies)" = 1 ] && ok "Kernel: TCP syncookies on" || warn "Kernel: tcp_syncookies not enabled"
v="$(sc kernel.yama.ptrace_scope)"; { [ "$v" = 1 ] || [ "$v" = 2 ] || [ "$v" = 3 ]; } && ok "Kernel: ptrace scope restricted" || warn "Kernel: yama ptrace_scope is ${v:-unset} - set to 1"

# --- FIM presence ------------------------------------------------------------
if have aide || have tripwire || [ -d /var/lib/aide ] || systemctl is-active --quiet alertkick-agent 2>/dev/null; then
  ok "Integrity: a file integrity mechanism is present"
else
  bad "Integrity: no file integrity monitoring detected - you would not know if a system file changed"
fi

# --- Score -------------------------------------------------------------------
PCT=0; [ "$TOTAL" -gt 0 ] && PCT=$(( PASS * 100 / TOTAL ))
if   [ "$PCT" -ge 90 ]; then GRADE="A"
elif [ "$PCT" -ge 75 ]; then GRADE="B"
elif [ "$PCT" -ge 60 ]; then GRADE="C"
elif [ "$PCT" -ge 40 ]; then GRADE="D"
else GRADE="F"; fi

bold=$(tput bold 2>/dev/null || true); rst=$(tput sgr0 2>/dev/null || true)
red=$(tput setaf 1 2>/dev/null || true); grn=$(tput setaf 2 2>/dev/null || true); ylw=$(tput setaf 3 2>/dev/null || true)

echo
echo "${bold}AlertKick hardening audit - $(hostname) - $(date '+%Y-%m-%d %H:%M')${rst}"
echo "======================================================"
echo "${bold}Score: ${PASS}/${TOTAL} checks passed  (${PCT}%)  Grade: ${GRADE}${rst}"
echo
[ ${#FIX[@]}  -gt 0 ] && { echo "${red}${bold}Fix these:${rst}";  for i in "${FIX[@]}";  do echo "  ${red}x${rst} $i"; done; echo; }
[ ${#WARN[@]} -gt 0 ] && { echo "${ylw}${bold}Review:${rst}";     for i in "${WARN[@]}"; do echo "  ${ylw}!${rst} $i"; done; echo; }
[ ${#GOOD[@]} -gt 0 ] && { echo "${grn}${bold}Passing:${rst}";    for i in "${GOOD[@]}"; do echo "  ${grn}v${rst} $i"; done; echo; }

echo "------------------------------------------------------"
echo "This is a snapshot. Config drifts the moment you start using the box."
echo "Fix the gaps with the full 20-step checklist (each step has a verify command):"
echo "  https://alertkick.com/blog/linux-server-security-checklist"
echo
