#!/usr/bin/env bash

#########################################################################################
#### apache-security-auditor.sh — Audit Apache httpd configuration for security issues####
#### Checks server info, TLS, headers, directories, modules, and file permissions     ####
#### Requires: bash 4+, root access                                                   ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.01                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   sudo ./apache-security-auditor.sh --full                                      ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Colors (pre-initialized) ─────────────────────────────────────────
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""

setup_colors() {
    if [[ "${COLOR:-auto}" == "never" ]]; then
        return
    fi
    if [[ "${COLOR:-auto}" == "always" ]] || [[ -t 1 ]]; then
        RED='\033[0;31m'
        GREEN='\033[0;32m'
        YELLOW='\033[0;33m'
        BLUE='\033[0;34m'
        CYAN='\033[0;36m'
        BOLD='\033[1m'
        DIM='\033[2m'
        RESET='\033[0m'
    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 "${DIM}[DEBUG]${RESET} $*"; fi; }
die()     { err "$*"; exit 1; }

# ── Severity counters ────────────────────────────────────────────────
TOTAL_CRIT=0
TOTAL_WARN=0
TOTAL_INFO=0
TOTAL_OK=0

flag_crit() { ((TOTAL_CRIT++)) || true; }
flag_warn() { ((TOTAL_WARN++)) || true; }
flag_info() { ((TOTAL_INFO++)) || true; }
flag_ok()   { ((TOTAL_OK++)) || true; }

# ── Defaults ──────────────────────────────────────────────────────────
RUN_MODE=""
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
APACHE_CONF=""
APACHECTL=""
APACHE_CONF_DIR=""
APACHE_RUN_USER=""
PLATFORM=""

# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
START_TIME=""

# ── Platform detection ───────────────────────────────────────────────
detect_platform() {
    if [[ -n "$APACHE_CONF" ]]; then
        if command -v apache2ctl &>/dev/null; then
            APACHECTL="apache2ctl"
            APACHE_CONF_DIR="$(dirname "$APACHE_CONF")"
            APACHE_RUN_USER="www-data"
            PLATFORM="Debian/Ubuntu (apache2)"
        elif command -v httpd &>/dev/null; then
            APACHECTL="httpd"
            APACHE_CONF_DIR="$(dirname "$APACHE_CONF")"
            APACHE_RUN_USER="apache"
            PLATFORM="RHEL/CentOS (httpd)"
        else
            die "Cannot find apache2ctl or httpd"
        fi
        return
    fi

    if command -v apache2ctl &>/dev/null && [[ -f /etc/apache2/apache2.conf ]]; then
        APACHECTL="apache2ctl"
        APACHE_CONF="/etc/apache2/apache2.conf"
        APACHE_CONF_DIR="/etc/apache2"
        APACHE_RUN_USER="www-data"
        PLATFORM="Debian/Ubuntu (apache2)"
    elif command -v httpd &>/dev/null && [[ -f /etc/httpd/conf/httpd.conf ]]; then
        APACHECTL="httpd"
        APACHE_CONF="/etc/httpd/conf/httpd.conf"
        APACHE_CONF_DIR="/etc/httpd"
        APACHE_RUN_USER="apache"
        PLATFORM="RHEL/CentOS (httpd)"
    else
        die "Cannot detect Apache installation — use --config to specify config path"
    fi

    verbose "Platform: ${PLATFORM}"
    verbose "Config: ${APACHE_CONF}"
    verbose "Config dir: ${APACHE_CONF_DIR}"
}

# ── Get all config files ─────────────────────────────────────────────
get_config_files() {
    local files=()

    if [[ -f "$APACHE_CONF" ]]; then
        files+=("$APACHE_CONF")
    fi

    local included
    included=$($APACHECTL -t -D DUMP_INCLUDES 2>/dev/null | grep -oP '\(\*\) \K.*|^ *\K/.*' || true)
    if [[ -n "$included" ]]; then
        while IFS= read -r f; do
            [[ -f "$f" ]] && files+=("$f")
        done <<< "$included"
    fi

    for d in "${APACHE_CONF_DIR}/sites-enabled" "${APACHE_CONF_DIR}/conf-enabled" \
             "${APACHE_CONF_DIR}/conf.d" "${APACHE_CONF_DIR}/conf.modules.d"; do
        if [[ -d "$d" ]]; then
            while IFS= read -r f; do
                files+=("$f")
            done < <(find "$d" -name '*.conf' -type f 2>/dev/null)
        fi
    done

    printf '%s\n' "${files[@]}" | sort -u
}

# ── Search across all config files ───────────────────────────────────
search_config() {
    local pattern="$1"
    local config_files
    config_files=$(get_config_files)

    while IFS= read -r f; do
        [[ -z "$f" ]] && continue
        grep -iP "$pattern" "$f" 2>/dev/null || true
    done <<< "$config_files"
}

# ── Table header ─────────────────────────────────────────────────────
print_table_header() {
    printf "  %-32s %-14s %s\n" "CHECK" "STATUS" "SEVERITY"
    printf "  %s\n" "$(printf '%.0s─' {1..65})"
}

# ── Table row ────────────────────────────────────────────────────────
print_row() {
    local check="$1" status="$2" severity="$3"
    local color=""
    case "$severity" in
        CRITICAL) color="$RED"; flag_crit ;;
        WARN)     color="$YELLOW"; flag_warn ;;
        INFO)     color="$CYAN"; flag_info ;;
        OK)       color="$GREEN"; flag_ok ;;
    esac
    printf "  %-32s %-14s %b%s%b\n" "$check" "$status" "$color" "$severity" "$RESET"
}

# ══════════════════════════════════════════════════════════════════════
#                       SERVER INFO AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_server_info() {
    log "Auditing server information exposure..."
    echo ""
    print_table_header

    # ServerTokens
    local tokens
    tokens=$(search_config '^\s*ServerTokens' | tail -1 | awk '{print $2}')
    if [[ -z "$tokens" ]]; then
        print_row "ServerTokens" "Full (default)" "CRITICAL"
    elif [[ "${tokens,,}" == "prod" || "${tokens,,}" == "productonly" ]]; then
        print_row "ServerTokens" "Prod" "OK"
    elif [[ "${tokens,,}" == "major" ]]; then
        print_row "ServerTokens" "Major" "WARN"
    else
        print_row "ServerTokens" "$tokens" "CRITICAL"
    fi

    # ServerSignature
    local sig
    sig=$(search_config '^\s*ServerSignature' | tail -1 | awk '{print $2}')
    if [[ -z "$sig" ]]; then
        print_row "ServerSignature" "On (default)" "CRITICAL"
    elif [[ "${sig,,}" == "off" ]]; then
        print_row "ServerSignature" "Off" "OK"
    else
        print_row "ServerSignature" "$sig" "CRITICAL"
    fi

    # TraceEnable
    local trace
    trace=$(search_config '^\s*TraceEnable' | tail -1 | awk '{print $2}')
    if [[ -z "$trace" ]]; then
        print_row "TraceEnable" "On (default)" "WARN"
    elif [[ "${trace,,}" == "off" ]]; then
        print_row "TraceEnable" "Off" "OK"
    else
        print_row "TraceEnable" "$trace" "WARN"
    fi

    echo ""
}

# ══════════════════════════════════════════════════════════════════════
#                       TLS AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_tls() {
    log "Auditing TLS configuration..."
    echo ""
    print_table_header

    # Check if mod_ssl is loaded
    if ! $APACHECTL -M 2>/dev/null | grep -q ssl_module; then
        print_row "mod_ssl" "not loaded" "INFO"
        echo ""
        return
    fi
    print_row "mod_ssl" "loaded" "OK"

    # SSLProtocol
    local proto
    proto=$(search_config '^\s*SSLProtocol' | tail -1)
    if [[ -z "$proto" ]]; then
        print_row "SSLProtocol" "not set (default)" "WARN"
    else
        if echo "$proto" | grep -iqP '(\+SSLv3|\+TLSv1\.0|\+TLSv1[^.]|[^-]TLSv1[^.23])'; then
            print_row "SSLProtocol (legacy)" "enabled" "CRITICAL"
        elif echo "$proto" | grep -iqP '(\+TLSv1\.1|[^-]TLSv1\.1)'; then
            print_row "SSLProtocol (TLSv1.1)" "enabled" "CRITICAL"
        else
            print_row "SSLProtocol" "modern only" "OK"
        fi
    fi

    # SSLCipherSuite
    local ciphers
    ciphers=$(search_config '^\s*SSLCipherSuite' | tail -1)
    if [[ -z "$ciphers" ]]; then
        print_row "SSLCipherSuite" "not set" "WARN"
    else
        print_row "SSLCipherSuite" "configured" "OK"
    fi

    # SSLHonorCipherOrder
    local honor
    honor=$(search_config '^\s*SSLHonorCipherOrder' | tail -1 | awk '{print $2}')
    if [[ -z "$honor" ]]; then
        print_row "SSLHonorCipherOrder" "not set" "WARN"
    elif [[ "${honor,,}" == "on" ]]; then
        print_row "SSLHonorCipherOrder" "on" "OK"
    else
        print_row "SSLHonorCipherOrder" "$honor" "WARN"
    fi

    # HSTS
    local hsts
    hsts=$(search_config 'Strict-Transport-Security')
    if [[ -z "$hsts" ]]; then
        print_row "HSTS Header" "missing" "WARN"
    else
        print_row "HSTS Header" "set" "OK"
    fi

    # OCSP Stapling
    local ocsp
    ocsp=$(search_config '^\s*SSLUseStapling' | tail -1 | awk '{print $2}')
    if [[ -z "$ocsp" ]]; then
        print_row "OCSP Stapling" "not configured" "WARN"
    elif [[ "${ocsp,,}" == "on" ]]; then
        print_row "OCSP Stapling" "on" "OK"
    else
        print_row "OCSP Stapling" "$ocsp" "WARN"
    fi

    echo ""
}

# ══════════════════════════════════════════════════════════════════════
#                       SECURITY HEADERS AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_headers() {
    log "Auditing security headers..."
    echo ""
    print_table_header

    # Check if mod_headers is loaded
    if ! $APACHECTL -M 2>/dev/null | grep -q headers_module; then
        print_row "mod_headers" "not loaded" "WARN"
        echo ""
        return
    fi

    local headers=(
        "X-Content-Type-Options"
        "X-Frame-Options"
        "Content-Security-Policy"
        "Referrer-Policy"
        "Permissions-Policy"
    )

    for header in "${headers[@]}"; do
        local found
        found=$(search_config "Header.*set.*${header}")
        if [[ -n "$found" ]]; then
            print_row "$header" "set" "OK"
        else
            print_row "$header" "missing" "WARN"
        fi
    done

    echo ""
}

# ══════════════════════════════════════════════════════════════════════
#                       DIRECTORY AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_directories() {
    log "Auditing directory and file restrictions..."
    echo ""
    print_table_header

    # Options Indexes (enabled = bad)
    local indexes_on
    indexes_on=$(search_config '^\s*Options\b.*\bIndexes\b' | grep -v '\-Indexes' || true)
    if [[ -n "$indexes_on" ]]; then
        print_row "Options Indexes" "enabled" "WARN"
    else
        print_row "Options Indexes" "disabled" "OK"
    fi

    # AllowOverride All
    local override_all
    override_all=$(search_config '^\s*AllowOverride\s+All' || true)
    if [[ -n "$override_all" ]]; then
        print_row "AllowOverride" "All (permissive)" "WARN"
    else
        print_row "AllowOverride" "restricted" "OK"
    fi

    # Sensitive file protection (.git, .env, .htpasswd)
    local sensitive_protection
    sensitive_protection=$(search_config '(FilesMatch|Files|Directory).*(\\.git|\\.env|\\.htpasswd)' || true)
    if [[ -n "$sensitive_protection" ]]; then
        print_row "Sensitive file blocking" "configured" "OK"
    else
        print_row "Sensitive file blocking" "not configured" "CRITICAL"
    fi

    # Check for root directory restriction
    local root_deny
    root_deny=$(search_config '^\s*Require\s+all\s+denied' || true)
    if [[ -n "$root_deny" ]]; then
        print_row "Root directory denied" "yes" "OK"
    else
        print_row "Root directory denied" "not found" "WARN"
    fi

    echo ""
}

# ══════════════════════════════════════════════════════════════════════
#                       MODULES AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_modules() {
    log "Auditing modules..."
    echo ""
    print_table_header

    local loaded_modules
    loaded_modules=$($APACHECTL -M 2>/dev/null || true)

    # mod_security
    if echo "$loaded_modules" | grep -q "security2_module"; then
        print_row "mod_security" "loaded" "OK"
    else
        print_row "mod_security" "not loaded" "INFO"
    fi

    # mod_status
    if echo "$loaded_modules" | grep -q "status_module"; then
        local status_restricted
        status_restricted=$(search_config '(<Location\s+/server-status)' || true)
        if [[ -n "$status_restricted" ]]; then
            local has_require
            has_require=$(search_config 'Require\s+(ip|local|host)' || true)
            if [[ -n "$has_require" ]]; then
                print_row "mod_status" "restricted" "OK"
            else
                print_row "mod_status" "unrestricted" "CRITICAL"
            fi
        else
            print_row "mod_status" "loaded (no location)" "WARN"
        fi
    else
        print_row "mod_status" "not loaded" "OK"
    fi

    # mod_info
    if echo "$loaded_modules" | grep -q "info_module"; then
        print_row "mod_info" "loaded" "WARN"
    else
        print_row "mod_info" "not loaded" "OK"
    fi

    # mod_autoindex
    if echo "$loaded_modules" | grep -q "autoindex_module"; then
        print_row "mod_autoindex" "loaded" "WARN"
    else
        print_row "mod_autoindex" "not loaded" "OK"
    fi

    echo ""
}

# ══════════════════════════════════════════════════════════════════════
#                       PERMISSIONS AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_permissions() {
    log "Auditing file permissions..."
    echo ""
    print_table_header

    # Main config file
    if [[ -f "$APACHE_CONF" ]]; then
        local conf_perms
        conf_perms=$(stat -c '%a' "$APACHE_CONF" 2>/dev/null)
        if [[ "$conf_perms" -le 644 ]]; then
            print_row "Config ($APACHE_CONF)" "$conf_perms" "OK"
        else
            print_row "Config ($APACHE_CONF)" "$conf_perms" "WARN"
        fi
    fi

    # .htpasswd files
    local htpasswd_files
    htpasswd_files=$(find "$APACHE_CONF_DIR" /var/www -name '.htpasswd' -type f 2>/dev/null || true)
    if [[ -n "$htpasswd_files" ]]; then
        while IFS= read -r f; do
            local perms
            perms=$(stat -c '%a' "$f" 2>/dev/null)
            if [[ "$perms" -le 640 ]]; then
                print_row ".htpasswd ($f)" "$perms" "OK"
            else
                print_row ".htpasswd ($f)" "$perms" "WARN"
            fi
        done <<< "$htpasswd_files"
    else
        verbose "No .htpasswd files found"
    fi

    # Document root world-writable check
    local docroots
    docroots=$(search_config '^\s*DocumentRoot' | awk '{print $2}' | tr -d '"' | sort -u)
    if [[ -n "$docroots" ]]; then
        while IFS= read -r dr; do
            [[ -z "$dr" || ! -d "$dr" ]] && continue
            local dr_perms
            dr_perms=$(stat -c '%a' "$dr" 2>/dev/null)
            if [[ "${dr_perms: -1}" -ge 6 ]]; then
                print_row "Docroot ($dr)" "${dr_perms} (world-writable)" "CRITICAL"
            else
                print_row "Docroot ($dr)" "$dr_perms" "OK"
            fi
        done <<< "$docroots"
    fi

    echo ""
}

# ══════════════════════════════════════════════════════════════════════
#                       SUMMARY
# ══════════════════════════════════════════════════════════════════════
print_summary() {
    local elapsed
    elapsed=$(( $(date +%s) - START_TIME ))

    echo ""
    echo "  ══════════════════════════════════════════"
    echo "  Apache Security Audit Summary"
    echo "  ══════════════════════════════════════════"
    printf "  %-20s %b%d%b\n" "CRITICAL:" "$RED" "$TOTAL_CRIT" "$RESET"
    printf "  %-20s %b%d%b\n" "WARN:" "$YELLOW" "$TOTAL_WARN" "$RESET"
    printf "  %-20s %b%d%b\n" "INFO:" "$CYAN" "$TOTAL_INFO" "$RESET"
    printf "  %-20s %b%d%b\n" "OK:" "$GREEN" "$TOTAL_OK" "$RESET"
    echo "  ──────────────────────────────────────────"
    printf "  Completed in %ds\n" "$elapsed"
    echo ""

    if [[ "$TOTAL_CRIT" -gt 0 ]]; then
        echo -e "  ${RED}${BOLD}Action required:${RESET} ${TOTAL_CRIT} critical finding(s)"
        echo ""
        echo "  Top recommendations:"
        echo "    • Set ServerTokens Prod and ServerSignature Off"
        echo "    • Disable SSLv3, TLSv1, and TLSv1.1"
        echo "    • Restrict mod_status to localhost with Require ip 127.0.0.1"
        echo "    • Block access to .git, .env, and .htpasswd files"
        echo "    • Fix world-writable document root permissions"
        echo ""
    elif [[ "$TOTAL_WARN" -gt 0 ]]; then
        echo -e "  ${YELLOW}Review recommended:${RESET} ${TOTAL_WARN} warning(s)"
        echo ""
        echo "  Suggestions:"
        echo "    • Add security headers (CSP, X-Frame-Options, HSTS)"
        echo "    • Enable OCSP stapling for TLS"
        echo "    • Disable mod_info and mod_autoindex in production"
        echo "    • Set TraceEnable Off"
        echo ""
    else
        echo -e "  ${GREEN}All checks passed${RESET}"
        echo ""
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                       USAGE
# ══════════════════════════════════════════════════════════════════════
show_help() {
    cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} — Apache Security Auditor

Audit Apache httpd configuration for common security misconfigurations.
Read-only — never modifies any files.

${BOLD}MODES${RESET}
  --full              Run all audit checks
  --server-info       Check ServerTokens, ServerSignature, TraceEnable
  --tls               Check TLS protocols, ciphers, HSTS, OCSP
  --headers           Check security response headers
  --directories       Check directory listing, AllowOverride, sensitive files
  --modules           Check mod_security, mod_status, mod_info, mod_autoindex
  --permissions       Check file permissions on configs and document roots

${BOLD}OPTIONS${RESET}
  --config PATH       Path to main Apache config file (auto-detect if omitted)
  --verbose           Debug output
  --no-color          Disable colored output
  --help              Show this help message

${BOLD}EXAMPLES${RESET}
  # Full audit
  sudo ${SCRIPT_NAME} --full

  # Check only server info exposure
  sudo ${SCRIPT_NAME} --server-info

  # Custom config path
  sudo ${SCRIPT_NAME} --full --config /opt/apache/conf/httpd.conf

  # Pipe-friendly output
  sudo ${SCRIPT_NAME} --full --no-color

${BOLD}EXIT CODES${RESET}
  0   All checks passed
  1   Warnings found (review recommended)
  2   Critical findings (action required)
EOF
}

# ══════════════════════════════════════════════════════════════════════
#                       PARSE ARGS
# ══════════════════════════════════════════════════════════════════════
parse_args() {
    local modes=()

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --full)
                modes=(server-info tls headers directories modules permissions)
                shift ;;
            --server-info)
                modes+=(server-info); shift ;;
            --tls)
                modes+=(tls); shift ;;
            --headers)
                modes+=(headers); shift ;;
            --directories)
                modes+=(directories); shift ;;
            --modules)
                modes+=(modules); shift ;;
            --permissions)
                modes+=(permissions); shift ;;
            --config)
                APACHE_CONF="${2:?--config requires a value}"; shift 2 ;;
            --verbose)
                VERBOSE="true"; shift ;;
            --no-color)
                COLOR="never"; shift ;;
            --help|-h)
                setup_colors; show_help; exit 0 ;;
            *)
                die "Unknown option: $1 (see --help)" ;;
        esac
    done

    if [[ ${#modes[@]} -eq 0 ]]; then
        err "No audit mode specified"
        echo "Run ${SCRIPT_NAME} --help for usage" >&2
        exit 1
    fi

    RUN_MODE="${modes[*]}"
}

# ══════════════════════════════════════════════════════════════════════
#                       MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
    parse_args "$@"
    setup_colors
    detect_platform

    START_TIME=$(date +%s)

    echo ""
    echo -e "${BOLD}Apache Security Auditor${RESET}"
    echo -e "Host:     $(hostname)"
    echo -e "Config:   ${APACHE_CONF}"
    echo -e "Platform: ${PLATFORM}"
    echo -e "Mode:     ${RUN_MODE}"
    echo -e "Time:     $(date -u +%Y-%m-%dT%H:%M:%SZ)"
    echo ""

    for mode in $RUN_MODE; do
        case "$mode" in
            server-info)  audit_server_info ;;
            tls)          audit_tls ;;
            headers)      audit_headers ;;
            directories)  audit_directories ;;
            modules)      audit_modules ;;
            permissions)  audit_permissions ;;
        esac
    done

    print_summary

    if [[ "$TOTAL_CRIT" -gt 0 ]]; then
        exit 2
    elif [[ "$TOTAL_WARN" -gt 0 ]]; then
        exit 1
    fi
    exit 0
}

main "$@"
