#!/usr/bin/env bash

#########################################################################################
#### linux-baseline-checks.sh — Post-provision baseline security validation          ####
#### Zero external dependencies. Validates freshly provisioned or hardened servers.   ####
#### Requires: bash 4+, curl (optional), openssl (optional), ss                      ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.00                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./linux-baseline-checks.sh                                                    ####
####   ./linux-baseline-checks.sh --skip-updates --format tap                        ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
SKIP_SSH="${SKIP_SSH:-false}"
SKIP_FIREWALL="${SKIP_FIREWALL:-false}"
SKIP_UPDATES="${SKIP_UPDATES:-false}"
EXPECTED_SERVICES="${EXPECTED_SERVICES:-}"
CURL_TIMEOUT="${CURL_TIMEOUT:-10}"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"   # text, tap, junit
JUNIT_FILE="${JUNIT_FILE:-baseline-results.xml}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"

# ── State ─────────────────────────────────────────────────────────────
PASS=0
FAIL=0
SKIP=0
TOTAL=0
RESULTS=()
START_TIME=""

# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
    if [[ "$COLOR" == "never" ]]; then
        RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
        return
    fi
    if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
        RED='\033[0;31m'
        GREEN='\033[0;32m'
        YELLOW='\033[0;33m'
        BLUE='\033[0;34m'
        BOLD='\033[1m'
        RESET='\033[0m'
    else
        RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
    fi
}

# ── Logging ───────────────────────────────────────────────────────────
log()     { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err()     { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[DEBUG]${RESET} $*"; fi; }

# ── Test Result Recording ─────────────────────────────────────────────
record_pass() {
    local name="$1"
    local detail="${2:-}"
    ((PASS++)) || true
    ((TOTAL++)) || true
    RESULTS+=("PASS|${name}|${detail}")
    if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
        echo "ok ${TOTAL} - ${name}"
    else
        echo -e "  ${GREEN}✓${RESET} ${name}${detail:+ — ${detail}}"
    fi
}

record_fail() {
    local name="$1"
    local detail="${2:-}"
    ((FAIL++)) || true
    ((TOTAL++)) || true
    RESULTS+=("FAIL|${name}|${detail}")
    if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
        echo "not ok ${TOTAL} - ${name}"
        [[ -n "$detail" ]] && echo "  # ${detail}"
    else
        echo -e "  ${RED}✗${RESET} ${name}${detail:+ — ${detail}}"
    fi
}

record_skip() {
    local name="$1"
    local reason="${2:-}"
    ((SKIP++)) || true
    ((TOTAL++)) || true
    RESULTS+=("SKIP|${name}|${reason}")
    if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
        echo "ok ${TOTAL} - ${name} # SKIP ${reason}"
    else
        echo -e "  ${YELLOW}⊘${RESET} ${name}${reason:+ — ${reason}}"
    fi
}

# ── Helper: read sshd config value ───────────────────────────────────
sshd_config_value() {
    local key="$1"
    local value=""
    # Check drop-in configs first, then main config
    if [[ -d /etc/ssh/sshd_config.d ]]; then
        value=$(grep -rhi "^${key}" /etc/ssh/sshd_config.d/ 2>/dev/null | tail -1 | awk '{print $2}') || true
    fi
    if [[ -z "$value" ]]; then
        value=$(grep -hi "^${key}" /etc/ssh/sshd_config 2>/dev/null | tail -1 | awk '{print $2}') || true
    fi
    echo "$value"
}

# ══════════════════════════════════════════════════════════════════════
#                          TEST SUITES
# ══════════════════════════════════════════════════════════════════════

# ── 1. SSH Hardening ─────────────────────────────────────────────────
test_ssh() {
    if [[ "$SKIP_SSH" == "true" ]]; then
        echo ""
        echo -e "${BOLD}SSH Hardening${RESET}"
        record_skip "SSH hardening" "SKIP_SSH=true"
        return
    fi

    echo ""
    echo -e "${BOLD}SSH Hardening${RESET}"

    if [[ ! -f /etc/ssh/sshd_config ]]; then
        record_skip "SSH hardening" "sshd_config not found"
        return
    fi

    # PermitRootLogin
    local root_login
    root_login=$(sshd_config_value "PermitRootLogin")
    verbose "PermitRootLogin = ${root_login:-<unset>}"

    if [[ "$root_login" == "no" || "$root_login" == "prohibit-password" ]]; then
        record_pass "PermitRootLogin disabled" "${root_login}"
    elif [[ -z "$root_login" ]]; then
        record_fail "PermitRootLogin disabled" "not explicitly set (default may allow root)"
    else
        record_fail "PermitRootLogin disabled" "set to ${root_login}"
    fi

    # PasswordAuthentication
    local pass_auth
    pass_auth=$(sshd_config_value "PasswordAuthentication")
    verbose "PasswordAuthentication = ${pass_auth:-<unset>}"

    if [[ "$pass_auth" == "no" ]]; then
        record_pass "PasswordAuthentication disabled" "key-only auth enforced"
    elif [[ -z "$pass_auth" ]]; then
        record_fail "PasswordAuthentication disabled" "not explicitly set (default is yes)"
    else
        record_fail "PasswordAuthentication disabled" "set to ${pass_auth}"
    fi

    # Protocol 2 (older systems may have Protocol directive)
    local protocol
    protocol=$(sshd_config_value "Protocol")
    verbose "Protocol = ${protocol:-<unset>}"

    if [[ -z "$protocol" || "$protocol" == "2" ]]; then
        record_pass "SSH Protocol 2" "${protocol:-default (2)}"
    else
        record_fail "SSH Protocol 2" "set to ${protocol}"
    fi

    # Non-standard port detection
    local ssh_port
    ssh_port=$(sshd_config_value "Port")
    if [[ -z "$ssh_port" ]]; then
        ssh_port="22"
    fi
    verbose "SSH Port = ${ssh_port}"

    if [[ "$ssh_port" != "22" ]]; then
        record_pass "SSH non-standard port" "port ${ssh_port}"
    else
        record_fail "SSH non-standard port" "still using default port 22"
    fi

    # PubkeyAuthentication
    local pubkey_auth
    pubkey_auth=$(sshd_config_value "PubkeyAuthentication")
    verbose "PubkeyAuthentication = ${pubkey_auth:-<unset>}"

    if [[ -z "$pubkey_auth" || "$pubkey_auth" == "yes" ]]; then
        record_pass "PubkeyAuthentication enabled" "${pubkey_auth:-default (yes)}"
    else
        record_fail "PubkeyAuthentication enabled" "set to ${pubkey_auth}"
    fi
}

# ── 2. Firewall ──────────────────────────────────────────────────────
test_firewall() {
    if [[ "$SKIP_FIREWALL" == "true" ]]; then
        echo ""
        echo -e "${BOLD}Firewall${RESET}"
        record_skip "Firewall" "SKIP_FIREWALL=true"
        return
    fi

    echo ""
    echo -e "${BOLD}Firewall${RESET}"

    local fw_found=false

    # ufw
    if command -v ufw &>/dev/null; then
        local ufw_status
        ufw_status=$(ufw status 2>/dev/null | head -1) || ufw_status=""
        verbose "ufw status: ${ufw_status}"

        if [[ "$ufw_status" == *"active"* ]]; then
            record_pass "ufw active" "firewall enabled"
            fw_found=true

            # Default deny
            local ufw_default
            ufw_default=$(ufw status verbose 2>/dev/null | grep "Default:" | head -1) || ufw_default=""
            if [[ "$ufw_default" == *"deny"* || "$ufw_default" == *"reject"* ]]; then
                record_pass "ufw default deny policy" "${ufw_default}"
            else
                record_fail "ufw default deny policy" "${ufw_default:-unknown}"
            fi
        else
            record_fail "ufw active" "${ufw_status:-not running}"
        fi
    fi

    # firewalld
    if command -v firewall-cmd &>/dev/null; then
        local fwd_state
        fwd_state=$(firewall-cmd --state 2>/dev/null) || fwd_state=""
        verbose "firewalld state: ${fwd_state}"

        if [[ "$fwd_state" == "running" ]]; then
            record_pass "firewalld active" "firewall running"
            fw_found=true

            # Default zone
            local default_zone
            default_zone=$(firewall-cmd --get-default-zone 2>/dev/null) || default_zone=""
            if [[ -n "$default_zone" ]]; then
                record_pass "firewalld default zone" "${default_zone}"
            fi
        elif [[ -z "$fwd_state" ]]; then
            verbose "firewalld not running"
        else
            record_fail "firewalld active" "${fwd_state}"
        fi
    fi

    # iptables (fallback)
    if [[ "$fw_found" == "false" ]] && command -v iptables &>/dev/null; then
        local ipt_rules
        ipt_rules=$(iptables -L INPUT -n 2>/dev/null | wc -l) || ipt_rules=0
        verbose "iptables INPUT rules: ${ipt_rules}"

        if [[ "$ipt_rules" -gt 2 ]]; then
            record_pass "iptables rules present" "$((ipt_rules - 2)) rules in INPUT chain"
            fw_found=true

            # Check default policy
            local ipt_policy
            ipt_policy=$(iptables -L INPUT -n 2>/dev/null | head -1 | grep -oP '\(policy \K[^)]+') || ipt_policy=""
            if [[ "$ipt_policy" == "DROP" || "$ipt_policy" == "REJECT" ]]; then
                record_pass "iptables default deny policy" "INPUT policy ${ipt_policy}"
            else
                record_fail "iptables default deny policy" "INPUT policy ${ipt_policy:-ACCEPT}"
            fi
        fi
    fi

    if [[ "$fw_found" == "false" ]]; then
        record_fail "Firewall active" "no firewall detected (ufw/firewalld/iptables)"
    fi
}

# ── 3. Time Sync ─────────────────────────────────────────────────────
test_timesync() {
    echo ""
    echo -e "${BOLD}Time Sync${RESET}"

    local sync_found=false

    # chrony
    if command -v chronyc &>/dev/null; then
        local chrony_tracking
        chrony_tracking=$(chronyc tracking 2>/dev/null) || chrony_tracking=""

        if [[ -n "$chrony_tracking" ]]; then
            record_pass "chronyd running" "chrony active"
            sync_found=true

            # Check sources
            local chrony_sources
            chrony_sources=$(chronyc sources 2>/dev/null | grep -c '^\^' 2>/dev/null) || chrony_sources=0
            if [[ "$chrony_sources" -gt 0 ]]; then
                record_pass "NTP sources reachable" "${chrony_sources} source(s) configured"
            else
                record_fail "NTP sources reachable" "no sources responding"
            fi

            # Drift check (system time offset)
            local offset
            offset=$(echo "$chrony_tracking" | grep -oP 'System time\s*:\s*\K[0-9.]+') || offset=""
            if [[ -n "$offset" ]]; then
                # Compare as string — anything under 1 second is fine
                local int_offset
                int_offset=$(echo "$offset" | cut -d. -f1)
                if [[ "${int_offset:-0}" -eq 0 ]]; then
                    record_pass "Time drift acceptable" "${offset} seconds offset"
                else
                    record_fail "Time drift acceptable" "${offset} seconds offset (>1s)"
                fi
            fi
        fi
    fi

    # systemd-timesyncd
    if [[ "$sync_found" == "false" ]] && command -v timedatectl &>/dev/null; then
        local timesync_status
        timesync_status=$(timedatectl show --property=NTPSynchronized --value 2>/dev/null) || timesync_status=""
        verbose "NTPSynchronized = ${timesync_status}"

        if [[ "$timesync_status" == "yes" ]]; then
            record_pass "systemd-timesyncd synchronized" "NTP active"
            sync_found=true
        elif systemctl is-active systemd-timesyncd &>/dev/null; then
            record_pass "systemd-timesyncd running" "service active"
            sync_found=true
            record_fail "NTP synchronized" "timesyncd running but not synchronized"
        fi
    fi

    if [[ "$sync_found" == "false" ]]; then
        record_fail "Time sync service" "no NTP service detected (chrony/systemd-timesyncd)"
    fi
}

# ── 4. Services ──────────────────────────────────────────────────────
test_services() {
    echo ""
    echo -e "${BOLD}Services${RESET}"

    # sshd running
    if systemctl is-active sshd &>/dev/null || systemctl is-active ssh &>/dev/null; then
        record_pass "sshd running" "SSH daemon active"
    else
        record_fail "sshd running" "SSH daemon not active"
    fi

    # cron / systemd-timers
    if systemctl is-active cron &>/dev/null || systemctl is-active crond &>/dev/null; then
        record_pass "cron active" "cron daemon running"
    elif systemctl list-timers --no-pager 2>/dev/null | grep -q "\.timer"; then
        record_pass "systemd timers active" "timers configured"
    else
        record_fail "cron/timers active" "no cron or systemd timers found"
    fi

    # Unnecessary services check
    local unnecessary_services=("cups" "cups-browsed" "avahi-daemon" "telnet" "rsh" "rlogin" "tftp" "xinetd")
    local bad_found=false
    local bad_list=""

    for svc in "${unnecessary_services[@]}"; do
        if systemctl is-active "$svc" &>/dev/null; then
            bad_found=true
            bad_list="${bad_list:+${bad_list}, }${svc}"
        fi
    done

    if [[ "$bad_found" == "true" ]]; then
        record_fail "No unnecessary services" "running: ${bad_list}"
    else
        record_pass "No unnecessary services" "cups/avahi/telnet/rsh/tftp not running"
    fi

    # Expected services (user-defined)
    if [[ -n "$EXPECTED_SERVICES" ]]; then
        IFS=',' read -ra exp_svcs <<< "$EXPECTED_SERVICES"
        for svc in "${exp_svcs[@]}"; do
            svc=$(echo "$svc" | xargs)  # trim whitespace
            if systemctl is-active "$svc" &>/dev/null; then
                record_pass "Expected service: ${svc}" "running"
            else
                record_fail "Expected service: ${svc}" "not running"
            fi
        done
    fi
}

# ── 5. Users ─────────────────────────────────────────────────────────
test_users() {
    echo ""
    echo -e "${BOLD}Users${RESET}"

    # UID 0 accounts (should only be root)
    local uid0_accounts
    uid0_accounts=$(awk -F: '$3 == 0 { print $1 }' /etc/passwd 2>/dev/null) || uid0_accounts=""
    local uid0_count
    uid0_count=$(echo "$uid0_accounts" | grep -c . 2>/dev/null) || uid0_count=0

    if [[ "$uid0_count" -eq 1 && "$uid0_accounts" == "root" ]]; then
        record_pass "No extra UID 0 accounts" "only root has UID 0"
    elif [[ "$uid0_count" -gt 1 ]]; then
        record_fail "No extra UID 0 accounts" "UID 0: ${uid0_accounts//$'\n'/, }"
    else
        record_pass "No extra UID 0 accounts" "only root has UID 0"
    fi

    # Empty passwords
    if [[ -r /etc/shadow ]]; then
        local empty_real
        empty_real=$(awk -F: '$2 == "" { print $1 }' /etc/shadow 2>/dev/null) || empty_real=""

        if [[ -z "$empty_real" ]]; then
            record_pass "No empty passwords" "all accounts have passwords or are locked"
        else
            record_fail "No empty passwords" "empty password: ${empty_real//$'\n'/, }"
        fi
    else
        record_skip "No empty passwords" "/etc/shadow not readable (run as root)"
    fi

    # Password aging (PASS_MAX_DAYS in login.defs)
    if [[ -f /etc/login.defs ]]; then
        local max_days
        max_days=$(grep -E "^PASS_MAX_DAYS" /etc/login.defs 2>/dev/null | awk '{print $2}') || max_days=""
        verbose "PASS_MAX_DAYS = ${max_days:-<unset>}"

        if [[ -n "$max_days" && "$max_days" -le 365 && "$max_days" -gt 0 ]]; then
            record_pass "Password aging configured" "PASS_MAX_DAYS=${max_days}"
        elif [[ -n "$max_days" && "$max_days" -gt 365 ]]; then
            record_fail "Password aging configured" "PASS_MAX_DAYS=${max_days} (>365)"
        else
            record_fail "Password aging configured" "PASS_MAX_DAYS not set or invalid"
        fi
    else
        record_skip "Password aging configured" "/etc/login.defs not found"
    fi

    # /etc/shadow permissions
    if [[ -f /etc/shadow ]]; then
        local shadow_perms
        shadow_perms=$(stat -c '%a' /etc/shadow 2>/dev/null) || shadow_perms=""
        verbose "/etc/shadow permissions: ${shadow_perms}"

        if [[ "$shadow_perms" == "640" || "$shadow_perms" == "600" || "$shadow_perms" == "000" ]]; then
            record_pass "/etc/shadow permissions" "${shadow_perms}"
        else
            record_fail "/etc/shadow permissions" "${shadow_perms} (expected 640 or stricter)"
        fi
    fi
}

# ── 6. Filesystem ────────────────────────────────────────────────────
test_filesystem() {
    echo ""
    echo -e "${BOLD}Filesystem${RESET}"

    # /tmp noexec/nosuid
    local tmp_opts
    tmp_opts=$(findmnt -n -o OPTIONS /tmp 2>/dev/null) || tmp_opts=""
    verbose "/tmp mount options: ${tmp_opts:-<not mounted separately>}"

    if [[ -n "$tmp_opts" ]]; then
        local tmp_pass=true
        local tmp_detail=""

        if [[ "$tmp_opts" == *"noexec"* ]]; then
            tmp_detail="noexec"
        else
            tmp_pass=false
        fi

        if [[ "$tmp_opts" == *"nosuid"* ]]; then
            tmp_detail="${tmp_detail:+${tmp_detail},}nosuid"
        else
            tmp_pass=false
        fi

        if [[ "$tmp_pass" == "true" ]]; then
            record_pass "/tmp noexec,nosuid" "${tmp_detail}"
        else
            record_fail "/tmp noexec,nosuid" "missing options (current: ${tmp_opts})"
        fi
    else
        record_fail "/tmp noexec,nosuid" "/tmp not mounted as separate partition"
    fi

    # /var separate partition
    local var_mount
    var_mount=$(findmnt -n -o SOURCE /var 2>/dev/null) || var_mount=""
    local root_mount
    root_mount=$(findmnt -n -o SOURCE / 2>/dev/null) || root_mount=""
    verbose "/var source: ${var_mount:-<same as root>}"

    if [[ -n "$var_mount" && "$var_mount" != "$root_mount" ]]; then
        record_pass "/var separate partition" "${var_mount}"
    else
        record_fail "/var separate partition" "not on separate partition"
    fi

    # /home separate partition
    local home_mount
    home_mount=$(findmnt -n -o SOURCE /home 2>/dev/null) || home_mount=""
    verbose "/home source: ${home_mount:-<same as root>}"

    if [[ -n "$home_mount" && "$home_mount" != "$root_mount" ]]; then
        record_pass "/home separate partition" "${home_mount}"
    else
        record_fail "/home separate partition" "not on separate partition"
    fi

    # Sticky bit on /tmp
    local sticky
    sticky=$(stat -c '%a' /tmp 2>/dev/null) || sticky=""
    verbose "/tmp mode: ${sticky}"

    if [[ "${sticky:0:1}" == "1" || "$sticky" == "1777" ]]; then
        record_pass "Sticky bit on /tmp" "mode ${sticky}"
    else
        record_fail "Sticky bit on /tmp" "mode ${sticky:-unknown} (expected 1777)"
    fi
}

# ── 7. Kernel ─────────────────────────────────────────────────────────
test_kernel() {
    echo ""
    echo -e "${BOLD}Kernel${RESET}"

    # ASLR
    local aslr
    aslr=$(cat /proc/sys/kernel/randomize_va_space 2>/dev/null) || aslr=""
    verbose "ASLR (randomize_va_space) = ${aslr}"

    if [[ "$aslr" == "2" ]]; then
        record_pass "ASLR enabled" "randomize_va_space=2 (full)"
    elif [[ "$aslr" == "1" ]]; then
        record_pass "ASLR enabled" "randomize_va_space=1 (partial)"
    else
        record_fail "ASLR enabled" "randomize_va_space=${aslr:-not readable}"
    fi

    # SYN cookies
    local syncookies
    syncookies=$(cat /proc/sys/net/ipv4/tcp_syncookies 2>/dev/null) || syncookies=""
    verbose "tcp_syncookies = ${syncookies}"

    if [[ "$syncookies" == "1" ]]; then
        record_pass "SYN cookies enabled" "tcp_syncookies=1"
    else
        record_fail "SYN cookies enabled" "tcp_syncookies=${syncookies:-not readable}"
    fi

    # IP forwarding (should be disabled unless router)
    local ip_forward
    ip_forward=$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null) || ip_forward=""
    verbose "ip_forward = ${ip_forward}"

    if [[ "$ip_forward" == "0" ]]; then
        record_pass "IP forwarding disabled" "ip_forward=0"
    else
        record_fail "IP forwarding disabled" "ip_forward=${ip_forward:-not readable} (enabled — expected unless router)"
    fi

    # Core dumps restricted
    local core_pattern
    core_pattern=$(cat /proc/sys/kernel/core_pattern 2>/dev/null) || core_pattern=""
    local core_limit
    core_limit=$(ulimit -c 2>/dev/null) || core_limit=""
    verbose "core_pattern = ${core_pattern}"
    verbose "ulimit -c = ${core_limit}"

    if [[ "$core_limit" == "0" ]]; then
        record_pass "Core dumps restricted" "ulimit -c = 0"
    elif [[ "$core_pattern" == *"systemd-coredump"* || "$core_pattern" == "|"* ]]; then
        record_pass "Core dumps restricted" "handled by ${core_pattern%% *}"
    else
        record_fail "Core dumps restricted" "core dumps enabled (ulimit -c = ${core_limit})"
    fi
}

# ── 8. Updates ────────────────────────────────────────────────────────
test_updates() {
    if [[ "$SKIP_UPDATES" == "true" ]]; then
        echo ""
        echo -e "${BOLD}Updates${RESET}"
        record_skip "Pending updates" "SKIP_UPDATES=true"
        return
    fi

    echo ""
    echo -e "${BOLD}Updates${RESET}"

    # Detect package manager and check for pending security updates
    if command -v apt-get &>/dev/null; then
        verbose "Package manager: apt"
        local updates
        updates=$(apt-get -s upgrade 2>/dev/null | grep -c "^Inst" 2>/dev/null) || updates=0
        local security
        security=$(apt-get -s upgrade 2>/dev/null | grep -c "security" 2>/dev/null) || security=0

        if [[ "$security" -gt 0 ]]; then
            record_fail "Security updates pending" "${security} security, ${updates} total"
        elif [[ "$updates" -gt 0 ]]; then
            record_pass "No security updates pending" "${updates} non-security updates available"
        else
            record_pass "System up to date" "no pending updates"
        fi

    elif command -v dnf &>/dev/null; then
        verbose "Package manager: dnf"
        local sec_updates
        sec_updates=$(dnf updateinfo list --security 2>/dev/null | grep -c "security" 2>/dev/null) || sec_updates=0

        if [[ "$sec_updates" -gt 0 ]]; then
            record_fail "Security updates pending" "${sec_updates} security updates"
        else
            record_pass "No security updates pending" "dnf reports no security updates"
        fi

    elif command -v yum &>/dev/null; then
        verbose "Package manager: yum"
        local yum_sec
        yum_sec=$(yum updateinfo list security 2>/dev/null | grep -c "security" 2>/dev/null) || yum_sec=0

        if [[ "$yum_sec" -gt 0 ]]; then
            record_fail "Security updates pending" "${yum_sec} security updates"
        else
            record_pass "No security updates pending" "yum reports no security updates"
        fi

    else
        record_skip "Pending updates" "no supported package manager found (apt/dnf/yum)"
    fi
}

# ── 9. Permissions ───────────────────────────────────────────────────
test_permissions() {
    echo ""
    echo -e "${BOLD}Permissions${RESET}"

    # /etc/passwd (should be 644)
    local passwd_perms
    passwd_perms=$(stat -c '%a' /etc/passwd 2>/dev/null) || passwd_perms=""
    verbose "/etc/passwd permissions: ${passwd_perms}"

    if [[ "$passwd_perms" == "644" ]]; then
        record_pass "/etc/passwd permissions" "${passwd_perms}"
    else
        record_fail "/etc/passwd permissions" "${passwd_perms} (expected 644)"
    fi

    # /etc/shadow (should be 640 or stricter)
    if [[ -f /etc/shadow ]]; then
        local shadow_perms
        shadow_perms=$(stat -c '%a' /etc/shadow 2>/dev/null) || shadow_perms=""

        if [[ "$shadow_perms" == "640" || "$shadow_perms" == "600" || "$shadow_perms" == "000" ]]; then
            record_pass "/etc/shadow permissions" "${shadow_perms}"
        else
            record_fail "/etc/shadow permissions" "${shadow_perms} (expected 640 or stricter)"
        fi
    fi

    # /etc/gshadow (should be 640 or stricter)
    if [[ -f /etc/gshadow ]]; then
        local gshadow_perms
        gshadow_perms=$(stat -c '%a' /etc/gshadow 2>/dev/null) || gshadow_perms=""
        verbose "/etc/gshadow permissions: ${gshadow_perms}"

        if [[ "$gshadow_perms" == "640" || "$gshadow_perms" == "600" || "$gshadow_perms" == "000" ]]; then
            record_pass "/etc/gshadow permissions" "${gshadow_perms}"
        else
            record_fail "/etc/gshadow permissions" "${gshadow_perms} (expected 640 or stricter)"
        fi
    fi

    # World-writable files in /etc
    local ww_files
    ww_files=$(find /etc -maxdepth 2 -type f -perm -0002 2>/dev/null | head -10) || ww_files=""
    local ww_count
    ww_count=$(echo "$ww_files" | grep -c . 2>/dev/null) || ww_count=0

    if [[ -z "$ww_files" ]]; then
        record_pass "No world-writable files in /etc" "clean"
    else
        record_fail "No world-writable files in /etc" "${ww_count} found"
    fi
}

# ── 10. Logging ──────────────────────────────────────────────────────
test_logging() {
    echo ""
    echo -e "${BOLD}Logging${RESET}"

    # rsyslog or journald
    local logging_found=false

    if systemctl is-active rsyslog &>/dev/null; then
        record_pass "rsyslog running" "syslog daemon active"
        logging_found=true
    fi

    if systemctl is-active systemd-journald &>/dev/null; then
        record_pass "journald running" "systemd journal active"
        logging_found=true
    fi

    if [[ "$logging_found" == "false" ]]; then
        record_fail "Logging service" "neither rsyslog nor journald running"
    fi

    # /var/log permissions
    local varlog_perms
    varlog_perms=$(stat -c '%a' /var/log 2>/dev/null) || varlog_perms=""
    verbose "/var/log permissions: ${varlog_perms}"

    if [[ "$varlog_perms" == "755" || "$varlog_perms" == "750" || "$varlog_perms" == "700" ]]; then
        record_pass "/var/log permissions" "${varlog_perms}"
    else
        record_fail "/var/log permissions" "${varlog_perms} (expected 755 or stricter)"
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                          OUTPUT
# ══════════════════════════════════════════════════════════════════════

print_summary() {
    local end_time
    end_time=$(date +%s)
    local duration=$(( end_time - START_TIME ))
    local hostname
    hostname=$(hostname 2>/dev/null || echo "unknown")

    echo ""
    echo -e "${BOLD}────────────────────────────────────────${RESET}"
    echo -e "${BOLD}Summary${RESET}  ${hostname}"
    echo -e "  ${GREEN}${PASS} passed${RESET}  ${RED}${FAIL} failed${RESET}  ${YELLOW}${SKIP} skipped${RESET}  (${duration}s)"
    echo -e "${BOLD}────────────────────────────────────────${RESET}"

    if [[ $FAIL -eq 0 ]]; then
        echo -e "${GREEN}${BOLD}All checks passed.${RESET}"
    else
        echo -e "${RED}${BOLD}${FAIL} check(s) failed.${RESET}"
    fi
}

print_tap_header() {
    echo "TAP version 13"
}

print_tap_footer() {
    echo "1..${TOTAL}"
    echo "# pass ${PASS}"
    echo "# fail ${FAIL}"
    echo "# skip ${SKIP}"
}

write_junit() {
    local end_time
    end_time=$(date +%s)
    local duration=$(( end_time - START_TIME ))

    cat > "$JUNIT_FILE" <<JUNIT_EOF
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
  <testsuite name="linux-baseline-checks" tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
JUNIT_EOF

    for result in "${RESULTS[@]}"; do
        local status name detail
        status=$(echo "$result" | cut -d'|' -f1)
        name=$(echo "$result" | cut -d'|' -f2)
        detail=$(echo "$result" | cut -d'|' -f3)

        # XML-escape the values
        name=$(echo "$name" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
        detail=$(echo "$detail" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')

        case "$status" in
            PASS)
                echo "    <testcase name=\"${name}\" classname=\"baseline\">" >> "$JUNIT_FILE"
                [[ -n "$detail" ]] && echo "      <system-out>${detail}</system-out>" >> "$JUNIT_FILE"
                echo "    </testcase>" >> "$JUNIT_FILE"
                ;;
            FAIL)
                echo "    <testcase name=\"${name}\" classname=\"baseline\">" >> "$JUNIT_FILE"
                echo "      <failure message=\"${detail}\">FAILED: ${name} — ${detail}</failure>" >> "$JUNIT_FILE"
                echo "    </testcase>" >> "$JUNIT_FILE"
                ;;
            SKIP)
                echo "    <testcase name=\"${name}\" classname=\"baseline\">" >> "$JUNIT_FILE"
                echo "      <skipped message=\"${detail}\"/>" >> "$JUNIT_FILE"
                echo "    </testcase>" >> "$JUNIT_FILE"
                ;;
        esac
    done

    echo "  </testsuite>" >> "$JUNIT_FILE"
    echo "</testsuites>" >> "$JUNIT_FILE"

    log "JUnit report written to ${JUNIT_FILE}"
}

# ══════════════════════════════════════════════════════════════════════
#                          MAIN
# ══════════════════════════════════════════════════════════════════════

usage() {
    cat <<EOF
Usage: $(basename "$0") [OPTIONS]

Post-provision baseline security validation for Linux servers.
Zero external dependencies — bash, coreutils, ss only. curl/openssl optional.

Options:
  --skip-ssh          Skip SSH hardening checks
  --skip-firewall     Skip firewall checks
  --skip-updates      Skip pending updates check
  --format FORMAT     Output: text (default), tap, junit
  --junit-file FILE   JUnit output path (default: baseline-results.xml)
  --verbose           Show debug output
  --no-color          Disable colored output
  --help              Show this help

Environment variables:
  SKIP_SSH            Skip SSH checks (same as --skip-ssh)
  SKIP_FIREWALL       Skip firewall checks (same as --skip-firewall)
  SKIP_UPDATES        Skip updates check (same as --skip-updates)
  EXPECTED_SERVICES   Comma-separated list of services to verify running
  CURL_TIMEOUT        HTTP timeout in seconds (default: 10)
  OUTPUT_FORMAT       Output format (same as --format)
  JUNIT_FILE          JUnit output path (same as --junit-file)

Examples:
  # Run all checks
  sudo ./$(basename "$0")

  # Skip SSH and updates, JUnit output
  sudo ./$(basename "$0") --skip-ssh --skip-updates --format junit

  # TAP output for CI
  sudo ./$(basename "$0") --format tap

  # Check specific expected services
  EXPECTED_SERVICES="nginx,prometheus-node-exporter" sudo ./$(basename "$0")
EOF
}

main() {
    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --skip-ssh)       SKIP_SSH=true ;;
            --skip-firewall)  SKIP_FIREWALL=true ;;
            --skip-updates)   SKIP_UPDATES=true ;;
            --format)         OUTPUT_FORMAT="$2"; shift ;;
            --junit-file)     JUNIT_FILE="$2"; shift ;;
            --verbose)        VERBOSE=true ;;
            --no-color)       COLOR=never ;;
            --help|-h)        usage; exit 0 ;;
            *)                err "Unknown option: $1"; usage; exit 1 ;;
        esac
        shift
    done

    setup_colors

    START_TIME=$(date +%s)

    if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
        print_tap_header
    else
        echo ""
        echo -e "${BOLD}Linux Baseline Checks${RESET}"
        echo -e "Host:   $(hostname 2>/dev/null || echo 'unknown')"
        echo -e "Time:   $(date -u +%Y-%m-%dT%H:%M:%SZ)"
        echo -e "Kernel: $(uname -r)"
        echo ""
    fi

    # Run test suites
    test_ssh
    test_firewall
    test_timesync
    test_services
    test_users
    test_filesystem
    test_kernel
    test_updates
    test_permissions
    test_logging

    # Output
    if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
        print_tap_footer
    elif [[ "$OUTPUT_FORMAT" == "junit" ]]; then
        print_summary
        write_junit
    else
        print_summary
    fi

    # Exit code
    [[ $FAIL -eq 0 ]] && exit 0 || exit 1
}

main "$@"
