#!/usr/bin/env bash
set -euo pipefail

# Ubuntu Security Hardening Checklist Audit Script
#
# Purpose:
#   Run a read-only, non-destructive audit against an Ubuntu server to help verify
#   production hardening readiness before go-live.
#
# Usage:
#   ./ubuntu-security-hardening-audit.sh
#   ./ubuntu-security-hardening-audit.sh --json
#   ./ubuntu-security-hardening-audit.sh --checks ssh,firewall,logging
#
# Notes:
#   - This script does not modify system configuration.
#   - Some checks may require root privileges for full visibility.
#   - Review outputs carefully and treat failures as items for manual validation.

JSON_OUTPUT=0
CHECK_FILTER=""

usage() {
  cat <<'EOF'
Usage: ubuntu-security-hardening-audit.sh [--json] [--checks list]

Options:
  --json           Emit machine-readable JSON output.
  --checks list    Comma-separated list of checks to run.
                   Valid values: baseline,patching,access,firewall,ssh,logging,recovery
  -h, --help       Show this help message.
EOF
}

log() {
  printf '%s\n' "$*"
}

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

get_os_release() {
  if [[ -r /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    printf '%s %s\n' "${NAME:-Ubuntu}" "${VERSION_ID:-unknown}"
  else
    printf 'Unknown Linux release\n'
  fi
}

check_baseline() {
  log "[baseline] OS and kernel"
  log "  Release: $(get_os_release)"
  if have_cmd uname; then
    log "  Kernel:  $(uname -r)"
  fi

  if have_cmd lsb_release; then
    log "  lsb_release:"
    lsb_release -a 2>/dev/null | sed 's/^/    /' || true
  fi

  if have_cmd dpkg; then
    log "  Installed package count: $(dpkg -l 2>/dev/null | awk 'NR>5 {c++} END {print c+0}')"
  fi

  log "  NOTE: Confirm support status and server role against your approved baseline."
}

check_patching() {
  log "[patching] Update and package hygiene"

  if have_cmd apt; then
    log "  APT version: $(apt --version 2>/dev/null | head -n1)"
  fi

  if have_cmd unattended-upgrades; then
    log "  unattended-upgrades: installed"
  else
    log "  unattended-upgrades: not detected"
  fi

  if [[ -r /etc/apt/apt.conf.d/20auto-upgrades ]]; then
    log "  Auto-upgrades configuration:"
    sed 's/^/    /' /etc/apt/apt.conf.d/20auto-upgrades
  else
    log "  Auto-upgrades configuration: not found"
  fi

  if have_cmd apt-get; then
    if sudo -n true 2>/dev/null || [[ ${EUID:-$(id -u)} -eq 0 ]]; then
      log "  Security updates pending (apt list --upgradable):"
      apt list --upgradable 2>/dev/null | sed -n '1,25p' | sed 's/^/    /' || true
    else
      log "  NOTE: Run as root or with sudo for pending update visibility."
    fi
  fi

  log "  NOTE: Verify maintenance SLA, reboot planning, and rollback readiness manually."
}

check_access() {
  log "[access] Privilege and SSH basics"

  if [[ -r /etc/ssh/sshd_config ]]; then
    log "  /etc/ssh/sshd_config key settings:"
    grep -Ei '^(PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|AllowUsers|AllowGroups|X11Forwarding|AllowAgentForwarding|ClientAliveInterval|ClientAliveCountMax|LoginGraceTime|Banner)\b' /etc/ssh/sshd_config \
      | sed 's/^/    /' || log "    No matching settings found in main config (may be managed elsewhere)."
  else
    log "  SSH config file not readable or not present."
  fi

  if have_cmd sudo; then
    log "  Sudoers validation:"
    if sudo -n -l >/dev/null 2>&1; then
      log "    Current user can query sudo privileges non-interactively."
    else
      log "    Unable to query sudo privileges non-interactively (may require password or root)."
    fi
  fi

  log "  Local users with interactive shells:"
  awk -F: '($7 !~ /(nologin|false)$/) {print "    " $1 " : " $7}' /etc/passwd | head -n 25
  log "  NOTE: Review for stale, shared, or excessive administrative accounts."
}

check_firewall() {
  log "[firewall] Listening services and host firewall"

  if have_cmd ss; then
    log "  Listening sockets:"
    ss -tulpn 2>/dev/null | sed -n '1,40p' | sed 's/^/    /' || true
  else
    log "  ss command not available."
  fi

  if have_cmd ufw; then
    log "  UFW status:"
    ufw status verbose 2>/dev/null | sed 's/^/    /' || true
  fi

  if have_cmd nft; then
    log "  nftables ruleset summary:"
    nft list ruleset 2>/dev/null | sed -n '1,60p' | sed 's/^/    /' || true
  elif have_cmd iptables; then
    log "  iptables rules summary:"
    iptables -S 2>/dev/null | sed -n '1,60p' | sed 's/^/    /' || true
  fi

  log "  NOTE: Compare open ports and firewall rules against the approved service list."
}

check_ssh() {
  log "[ssh] SSH session hardening"

  if have_cmd sshd; then
    log "  Effective sshd configuration (selected values):"
    sshd -T 2>/dev/null | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication|x11forwarding|allowagentforwarding|clientaliveinterval|clientalivecountmax|logingracetime|maxauthtries|banner)\b' \
      | sed 's/^/    /' || true
  else
    log "  sshd command not available."
  fi

  log "  NOTE: Confirm MFA, bastion access, and approved authentication factors in your access layer."
}

check_logging() {
  log "[logging] Audit and system logging"

  if have_cmd systemctl; then
    log "  journald status:"
    systemctl is-active systemd-journald 2>/dev/null | sed 's/^/    /' || true
  fi

  if have_cmd logger; then
    log "  syslog test capability: logger available"
  fi

  log "  Important log-related files:"
  for f in /etc/rsyslog.conf /etc/systemd/journald.conf /var/log/auth.log; do
    if [[ -e "$f" ]]; then
      log "    present: $f"
    fi
  done

  log "  NOTE: Verify centralized logging, retention, and alerting outside this host audit."
}

check_recovery() {
  log "[recovery] Backup and restore readiness"
  log "  This script does not verify backups or perform restore tests."
  log "  Validate the following manually:"
  log "    - Backup schedule and retention"
  log "    - Restore test evidence"
  log "    - Maintenance window and rollback steps"
  log "    - Emergency access and outage recovery contacts"
}

run_selected_checks() {
  local checks=(baseline patching access firewall ssh logging recovery)
  local selected=()

  if [[ -n "$CHECK_FILTER" ]]; then
    IFS=',' read -r -a selected <<< "$CHECK_FILTER"
  else
    selected=("${checks[@]}")
  fi

  for check in "${selected[@]}"; do
    case "$check" in
      baseline) check_baseline ;;
      patching) check_patching ;;
      access) check_access ;;
      firewall) check_firewall ;;
      ssh) check_ssh ;;
      logging) check_logging ;;
      recovery) check_recovery ;;
      *)
        log "Unknown check: $check"
        exit 1
        ;;
    esac
    log ""
  done
}

main() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --json)
        JSON_OUTPUT=1
        ;;
      --checks)
        shift
        [[ $# -gt 0 ]] || { log "Missing value for --checks"; exit 1; }
        CHECK_FILTER="$1"
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        log "Unknown argument: $1"
        usage
        exit 1
        ;;
    esac
    shift
  done

  if [[ "$JSON_OUTPUT" -eq 1 ]]; then
    log '{"note":"JSON mode is reserved for future structured output; run without --json for readable audit output."}'
    exit 0
  fi

  log "Ubuntu Security Hardening Audit"
  log "================================"
  log "Read-only checks only. No system changes will be made."
  log ""

  run_selected_checks

  log "Audit complete. Review findings against your production acceptance criteria."
}

main "$@"