#!/usr/bin/env bash

#########################################################################################
#### permissions-fixer.sh — Find and fix common file permission problems             ####
#### Scans web roots, shared dirs, home dirs, /tmp for broken permissions, ACLs,     ####
#### orphaned ownership, and missing setgid bits                                     ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.0                                                                     ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./permissions-fixer.sh                              # audit mode              ####
####   ./permissions-fixer.sh --fix                        # apply fixes             ####
####   ./permissions-fixer.sh --scan /var/www /srv/shared   # specific paths         ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
SCAN_PATHS=()
FIX_MODE="false"
DRY_RUN="false"
WEB_USER=""
SKIP_WEB="false"
SKIP_HOME="false"
SKIP_MOUNT="false"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
YES="false"

# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
COUNT_WARN=0
COUNT_FAIL=0
COUNT_FIX=0
COUNT_SKIP=0

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

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

die() { err "$*"; exit 1; }

tag_fail() { echo -e "  ${RED}[FAIL]${RESET} $*"; ((COUNT_FAIL++)) || true; }
tag_warn() { echo -e "  ${YELLOW}[WARN]${RESET} $*"; ((COUNT_WARN++)) || true; }
tag_fix()  { echo -e "  ${GREEN}[FIX]${RESET}  $*"; ((COUNT_FIX++)) || true; }
tag_skip() { echo -e "  ${DIM}[SKIP]${RESET} $*"; ((COUNT_SKIP++)) || true; }

section_header() {
    echo ""
    echo -e "  ${BOLD}${CYAN}── $1 ──${RESET}"
    echo ""
}

# ── Helpers ───────────────────────────────────────────────────────────
apply_fix() {
    local description="$1"
    shift
    if [[ "$FIX_MODE" == "true" ]]; then
        if [[ "$DRY_RUN" == "true" ]]; then
            tag_skip "(dry-run) $description"
        else
            if "$@" 2>/dev/null; then
                tag_fix "$description"
            else
                err "Failed to apply: $description"
            fi
        fi
    fi
}

detect_web_user() {
    if [[ -n "$WEB_USER" ]]; then
        verbose "Web user override: $WEB_USER"
        return
    fi
    for proc in nginx apache2 httpd; do
        if pgrep -x "$proc" &>/dev/null; then
            case "$proc" in
                nginx)   WEB_USER="$(ps -eo user,comm --no-headers | awk '$2=="nginx" && $1!="root" {print $1; exit}')" ;;
                apache2) WEB_USER="$(ps -eo user,comm --no-headers | awk '$2=="apache2" && $1!="root" {print $1; exit}')" ;;
                httpd)   WEB_USER="$(ps -eo user,comm --no-headers | awk '$2=="httpd" && $1!="root" {print $1; exit}')" ;;
            esac
            if [[ -n "$WEB_USER" ]]; then
                verbose "Detected web user from running $proc: $WEB_USER"
                return
            fi
        fi
    done
    for candidate in www-data nginx apache; do
        if id "$candidate" &>/dev/null; then
            WEB_USER="$candidate"
            verbose "Detected web user from passwd: $WEB_USER"
            return
        fi
    done
    WEB_USER=""
    verbose "Could not detect web server user"
}

# ══════════════════════════════════════════════════════════════════════
#                       CHECK FUNCTIONS
# ══════════════════════════════════════════════════════════════════════

check_web_root() {
    if [[ "$SKIP_WEB" == "true" ]]; then
        verbose "Skipping web root checks (--skip-web)"
        return
    fi

    section_header "Web Root Permissions"

    detect_web_user
    if [[ -z "$WEB_USER" ]]; then
        tag_warn "No web server user detected — skipping web root checks"
        log "Use --web-user USER to specify manually"
        return
    fi

    log "Web server user: ${WEB_USER}"

    local web_root="/var/www"
    for p in "${SCAN_PATHS[@]}"; do
        if [[ "$p" == /var/www* ]] || [[ "$p" == /srv/www* ]]; then
            web_root="$p"
            break
        fi
    done

    if [[ ! -d "$web_root" ]]; then
        verbose "Web root ${web_root} does not exist — skipping"
        return
    fi

    log "Scanning web root: ${web_root}"

    # Check file ownership
    while IFS= read -r file; do
        [[ -z "$file" ]] && continue
        local owner
        owner=$(stat -c '%U' "$file" 2>/dev/null || echo "UNKNOWN")
        tag_fail "Not owned by ${WEB_USER}: ${file} (owner: ${owner})"
        apply_fix "chown ${WEB_USER}: ${file}" chown "${WEB_USER}:" "$file"
    done < <(find "$web_root" -xdev -not -user "$WEB_USER" -not -path "*/\.git/*" 2>/dev/null || true)

    # Check directory permissions (should be 755)
    while IFS= read -r dir; do
        [[ -z "$dir" ]] && continue
        local perms
        perms=$(stat -c '%a' "$dir" 2>/dev/null || echo "0")
        if [[ "$perms" != "755" ]]; then
            tag_warn "Directory not 755: ${dir} (${perms})"
            apply_fix "chmod 755 ${dir}" chmod 755 "$dir"
        else
            verbose "OK: ${dir} (${perms})"
        fi
    done < <(find "$web_root" -xdev -type d -not -path "*/\.git/*" 2>/dev/null || true)

    # Check file permissions (should be 644)
    while IFS= read -r file; do
        [[ -z "$file" ]] && continue
        local perms
        perms=$(stat -c '%a' "$file" 2>/dev/null || echo "0")
        if [[ "$perms" != "644" ]]; then
            tag_warn "File not 644: ${file} (${perms})"
            apply_fix "chmod 644 ${file}" chmod 644 "$file"
        else
            verbose "OK: ${file} (${perms})"
        fi
    done < <(find "$web_root" -xdev -type f -not -path "*/\.git/*" 2>/dev/null || true)
}

check_world_writable() {
    section_header "World-Writable Files"

    local exclude_args=(-not -path "/tmp/*" -not -path "/var/tmp/*" -not -path "/dev/shm/*"
                        -not -path "/proc/*" -not -path "/sys/*")
    local found=0

    for scan_dir in "${SCAN_PATHS[@]}"; do
        [[ -d "$scan_dir" ]] || continue
        [[ "$scan_dir" == "/tmp" || "$scan_dir" == "/var/tmp" || "$scan_dir" == "/dev/shm" ]] && continue
        while IFS= read -r file; do
            [[ -z "$file" ]] && continue
            tag_fail "World-writable: ${file} ($(stat -c '%a %U:%G' "$file" 2>/dev/null))"
            apply_fix "chmod o-w ${file}" chmod o-w "$file"
            ((found++)) || true
        done < <(find "$scan_dir" -xdev -perm -0002 -not -type l "${exclude_args[@]}" 2>/dev/null || true)
    done

    if [[ "$found" -eq 0 ]]; then
        log "No world-writable files found outside /tmp"
    fi
}

check_orphaned_files() {
    section_header "Orphaned Files (No Valid Owner/Group)"

    local found=0
    for scan_dir in "${SCAN_PATHS[@]}"; do
        [[ -d "$scan_dir" ]] || continue
        while IFS= read -r file; do
            [[ -z "$file" ]] && continue
            local uid gid
            uid=$(stat -c '%u' "$file" 2>/dev/null || echo "?")
            gid=$(stat -c '%g' "$file" 2>/dev/null || echo "?")
            tag_fail "Orphaned file: ${file} (UID=${uid} GID=${gid})"
            ((found++)) || true
        done < <(find "$scan_dir" -xdev \( -nouser -o -nogroup \) -not -path "/proc/*" -not -path "/sys/*" 2>/dev/null || true)
    done

    if [[ "$found" -eq 0 ]]; then
        log "No orphaned files found"
    else
        log "Orphaned files require manual owner assignment — not auto-fixed"
    fi
}

check_setgid_shared() {
    section_header "Shared Directory Setgid"

    local found=0
    local dirs_to_check=()
    for p in "${SCAN_PATHS[@]}"; do
        if [[ "$p" == /srv* ]]; then
            dirs_to_check+=("$p")
        fi
    done
    [[ -d "/srv" ]] && dirs_to_check+=("/srv")

    # Deduplicate
    local -A seen=()
    local unique_dirs=()
    for d in "${dirs_to_check[@]}"; do
        if [[ -z "${seen[$d]+x}" ]]; then
            seen[$d]=1
            unique_dirs+=("$d")
        fi
    done

    for scan_dir in "${unique_dirs[@]}"; do
        [[ -d "$scan_dir" ]] || continue
        while IFS= read -r dir; do
            [[ -z "$dir" ]] && continue
            local perms
            perms=$(stat -c '%a' "$dir" 2>/dev/null || echo "0")
            # Check if group-writable (bit 1 of group octal) but not setgid
            local group_octal="${perms:1:1}"
            if (( group_octal % 2 == 0 )); then
                verbose "Not group-writable, skipping: $dir"
                continue
            fi
            # Check setgid
            local mode
            mode=$(stat -c '%04a' "$dir" 2>/dev/null || echo "0000")
            if [[ "${mode:0:1}" != "2" && "${mode:0:1}" != "3" && "${mode:0:1}" != "6" && "${mode:0:1}" != "7" ]]; then
                tag_warn "Group-writable but no setgid: ${dir} (${mode})"
                apply_fix "chmod g+s ${dir}" chmod g+s "$dir"
                ((found++)) || true
            else
                verbose "OK setgid: ${dir} (${mode})"
            fi
        done < <(find "$scan_dir" -xdev -type d -not -path "/proc/*" -not -path "/sys/*" 2>/dev/null || true)
    done

    if [[ "$found" -eq 0 ]]; then
        log "No missing setgid bits found on shared directories"
    fi
}

check_home_dirs() {
    if [[ "$SKIP_HOME" == "true" ]]; then
        verbose "Skipping home directory checks (--skip-home)"
        return
    fi

    section_header "Home Directory Permissions"

    [[ -d "/home" ]] || { verbose "/home does not exist — skipping"; return; }

    local found=0
    for home_dir in /home/*/; do
        [[ -d "$home_dir" ]] || continue
        home_dir="${home_dir%/}"
        local perms
        perms=$(stat -c '%a' "$home_dir" 2>/dev/null || echo "0")

        # Check home dir is 700 or 750
        if [[ "$perms" != "700" && "$perms" != "750" ]]; then
            tag_warn "Home dir too open: ${home_dir} (${perms})"
            apply_fix "chmod 750 ${home_dir}" chmod 750 "$home_dir"
            ((found++)) || true
        else
            verbose "OK: ${home_dir} (${perms})"
        fi

        # Check .ssh directory
        local ssh_dir="${home_dir}/.ssh"
        if [[ -d "$ssh_dir" ]]; then
            local ssh_perms
            ssh_perms=$(stat -c '%a' "$ssh_dir" 2>/dev/null || echo "0")
            if [[ "$ssh_perms" != "700" ]]; then
                tag_fail "SSH dir not 700: ${ssh_dir} (${ssh_perms})"
                apply_fix "chmod 700 ${ssh_dir}" chmod 700 "$ssh_dir"
                ((found++)) || true
            else
                verbose "OK: ${ssh_dir} (${ssh_perms})"
            fi

            # Check authorized_keys
            if [[ -f "${ssh_dir}/authorized_keys" ]]; then
                local ak_perms
                ak_perms=$(stat -c '%a' "${ssh_dir}/authorized_keys" 2>/dev/null || echo "0")
                if [[ "$ak_perms" != "600" ]]; then
                    tag_fail "authorized_keys not 600: ${ssh_dir}/authorized_keys (${ak_perms})"
                    apply_fix "chmod 600 ${ssh_dir}/authorized_keys" chmod 600 "${ssh_dir}/authorized_keys"
                    ((found++)) || true
                fi
            fi

            # Check private keys
            while IFS= read -r key_file; do
                [[ -z "$key_file" ]] && continue
                local key_perms
                key_perms=$(stat -c '%a' "$key_file" 2>/dev/null || echo "0")
                if [[ "$key_perms" != "600" ]]; then
                    tag_fail "Private key not 600: ${key_file} (${key_perms})"
                    apply_fix "chmod 600 ${key_file}" chmod 600 "$key_file"
                    ((found++)) || true
                fi
            done < <(find "$ssh_dir" -maxdepth 1 -type f \( -name "id_*" -not -name "*.pub" \) 2>/dev/null || true)
        fi
    done

    if [[ "$found" -eq 0 ]]; then
        log "All home directory permissions look correct"
    fi
}

check_broken_acls() {
    section_header "Broken ACL Masks"

    if ! command -v getfacl &>/dev/null; then
        log "getfacl not installed — skipping ACL checks"
        return
    fi

    local found=0
    for scan_dir in "${SCAN_PATHS[@]}"; do
        [[ -d "$scan_dir" ]] || continue
        while IFS= read -r file; do
            [[ -z "$file" ]] && continue
            # Only check files that actually have ACLs (+ in ls output or via getfacl)
            local acl_output
            acl_output=$(getfacl -p "$file" 2>/dev/null || true)
            [[ -z "$acl_output" ]] && continue

            local has_named_entries="false"
            local mask_line=""
            while IFS= read -r line; do
                case "$line" in
                    user:?*:*)  has_named_entries="true" ;;
                    group:?*:*) has_named_entries="true" ;;
                    mask::*)    mask_line="$line" ;;
                esac
            done <<< "$acl_output"

            if [[ "$has_named_entries" == "false" || -z "$mask_line" ]]; then
                continue
            fi

            local mask_perms="${mask_line##*::}"
            # Check each named entry against the mask
            while IFS= read -r line; do
                local entry_perms=""
                case "$line" in
                    user:?*:*) entry_perms="${line##*:}" ;;
                    group:?*:*) entry_perms="${line##*:}" ;;
                    *) continue ;;
                esac

                # Compare: if entry has bits the mask doesn't, effective is reduced
                local reduced="false"
                if [[ "$entry_perms" == *r* && "$mask_perms" != *r* ]]; then reduced="true"; fi
                if [[ "$entry_perms" == *w* && "$mask_perms" != *w* ]]; then reduced="true"; fi
                if [[ "$entry_perms" == *x* && "$mask_perms" != *x* ]]; then reduced="true"; fi

                if [[ "$reduced" == "true" ]]; then
                    tag_warn "ACL mask restricts effective perms: ${file} (entry=${entry_perms}, mask=${mask_perms})"
                    ((found++)) || true
                    break
                fi
            done <<< "$acl_output"
        done < <(find "$scan_dir" -xdev -maxdepth 3 -not -path "/proc/*" -not -path "/sys/*" 2>/dev/null | head -5000 || true)
    done

    if [[ "$found" -eq 0 ]]; then
        log "No broken ACL masks found"
    else
        log "ACL mask issues require manual review — not auto-fixed"
    fi
}

check_immutable_files() {
    section_header "Immutable Files (chattr +i)"

    if ! command -v lsattr &>/dev/null; then
        log "lsattr not installed — skipping immutable file checks"
        return
    fi

    local found=0
    for scan_dir in "${SCAN_PATHS[@]}"; do
        [[ -d "$scan_dir" ]] || continue
        while IFS= read -r line; do
            [[ -z "$line" ]] && continue
            # lsattr format: "----i---------e------- /path/to/file"
            # skip directory headers like "/tmp/subdir:" (no attribute field)
            [[ "$line" =~ ^[[:space:]]*[-a-zA-Z]+[[:space:]]+ ]] || continue
            local attrs file
            attrs="${line%% *}"
            file="${line#* }"
            # check for immutable flag (4th char in attrs is 'i')
            if [[ "$attrs" == ????i* ]]; then
                tag_warn "Immutable file: ${file}"
                ((found++)) || true
            fi
        done < <(lsattr -R "$scan_dir" 2>/dev/null | grep -v "^lsattr:" || true)
    done

    if [[ "$found" -eq 0 ]]; then
        log "No immutable files found"
    else
        log "Immutable files reported for awareness — not auto-fixed"
    fi
}

check_mount_options() {
    if [[ "$SKIP_MOUNT" == "true" ]]; then
        verbose "Skipping mount option checks (--skip-mount)"
        return
    fi

    section_header "Mount Option Audit"

    local found=0
    local -A mount_checks=(
        ["/tmp"]="noexec nosuid nodev"
        ["/var/tmp"]="noexec nosuid nodev"
        ["/dev/shm"]="noexec nosuid nodev"
    )

    for mount_point in "${!mount_checks[@]}"; do
        local expected_opts="${mount_checks[$mount_point]}"
        local current_opts
        current_opts=$(findmnt -n -o OPTIONS "$mount_point" 2>/dev/null || echo "")

        if [[ -z "$current_opts" ]]; then
            verbose "${mount_point} is not a separate mount point — skipping"
            continue
        fi

        log "Checking mount: ${mount_point} (${current_opts})"

        for opt in $expected_opts; do
            if echo "$current_opts" | grep -qw "$opt"; then
                verbose "OK: ${mount_point} has ${opt}"
            else
                tag_fail "Missing mount option: ${mount_point} needs ${opt}"
                ((found++)) || true
            fi
        done
    done

    if [[ "$found" -eq 0 ]]; then
        log "All checked mount points have recommended security options"
    else
        log "Mount option changes require fstab edit + remount — not auto-fixed"
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                       CONFIRMATION
# ══════════════════════════════════════════════════════════════════════

confirm_fix() {
    if [[ "$FIX_MODE" != "true" || "$DRY_RUN" == "true" || "$YES" == "true" ]]; then
        return 0
    fi

    echo ""
    echo -e "  ${BOLD}${YELLOW}⚠  WARNING: --fix mode will modify file permissions${RESET}"
    echo -e "  ${YELLOW}Back up critical data before proceeding.${RESET}"
    echo ""
    read -rp "  Continue with fixes? [y/N] " answer
    case "$answer" in
        [yY][eE][sS]|[yY]) return 0 ;;
        *) log "Aborted by user."; exit 0 ;;
    esac
}

require_root_for_fix() {
    if [[ "$FIX_MODE" == "true" && "$DRY_RUN" != "true" && $EUID -ne 0 ]]; then
        die "--fix mode requires root privileges. Run with sudo."
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                       SUMMARY
# ══════════════════════════════════════════════════════════════════════

print_summary() {
    echo ""
    echo -e "  ${BOLD}══════════════════════════════════════════${RESET}"
    echo -e "  ${BOLD}Permissions Fixer Summary${RESET}"
    echo -e "  ${BOLD}══════════════════════════════════════════${RESET}"
    echo ""

    printf "  %-30s %b\n" "Warnings [WARN]:" "${YELLOW}${COUNT_WARN}${RESET}"
    printf "  %-30s %b\n" "Security risks [FAIL]:" "${RED}${COUNT_FAIL}${RESET}"
    printf "  %-30s %b\n" "Fixes applied [FIX]:" "${GREEN}${COUNT_FIX}${RESET}"
    printf "  %-30s %b\n" "Skipped (dry-run) [SKIP]:" "${DIM}${COUNT_SKIP}${RESET}"

    local total=$((COUNT_WARN + COUNT_FAIL + COUNT_FIX + COUNT_SKIP))
    echo ""
    printf "  %-30s %d\n" "Total findings:" "$total"

    if [[ "$FIX_MODE" == "true" && "$DRY_RUN" == "true" ]]; then
        echo ""
        log "Dry-run mode — no changes were made. Re-run without --dry-run to apply."
    fi

    echo ""
}

# ══════════════════════════════════════════════════════════════════════
#                       USAGE
# ══════════════════════════════════════════════════════════════════════

usage() {
    cat <<EOF
${SCRIPT_NAME} — Find and fix common file permission problems

USAGE:
  ${SCRIPT_NAME} [OPTIONS]

OPTIONS:
  --scan PATH [PATH...]   Scan specific paths (default: /var/www /home /srv /tmp)
  --fix                   Apply fixes (default: audit/report only). Requires root.
  --dry-run               Show what --fix would do without doing it
  --web-user USER         Override web server user detection
  --skip-web              Skip web root checks
  --skip-home             Skip home directory checks
  --skip-mount            Skip mount option checks
  --yes                   Skip confirmation prompt for --fix mode
  --verbose               Show all files checked, not just issues
  --no-color              Disable color output
  -h, --help              Show this help

EXAMPLES:
  # Audit mode (default) — report issues without changing anything
  ./permissions-fixer.sh

  # Scan specific paths
  ./permissions-fixer.sh --scan /var/www /srv/shared

  # Preview fixes without applying
  ./permissions-fixer.sh --fix --dry-run

  # Apply fixes (requires root)
  sudo ./permissions-fixer.sh --fix

  # Apply fixes non-interactively
  sudo ./permissions-fixer.sh --fix --yes

  # Override web server user
  ./permissions-fixer.sh --web-user nginx

  # Skip home and mount checks
  ./permissions-fixer.sh --skip-home --skip-mount

EXIT CODES:
  0   No issues found
  1   Warnings found
  2   Security risks found
EOF
}

# ══════════════════════════════════════════════════════════════════════
#                       ARGUMENT PARSING
# ══════════════════════════════════════════════════════════════════════

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --scan)
                shift
                while [[ $# -gt 0 && "$1" != --* ]]; do
                    SCAN_PATHS+=("$1")
                    shift
                done
                ;;
            --fix)
                FIX_MODE="true"; shift ;;
            --dry-run)
                DRY_RUN="true"; shift ;;
            --web-user)
                WEB_USER="$2"; shift 2 ;;
            --skip-web)
                SKIP_WEB="true"; shift ;;
            --skip-home)
                SKIP_HOME="true"; shift ;;
            --skip-mount)
                SKIP_MOUNT="true"; shift ;;
            --yes)
                YES="true"; shift ;;
            --verbose)
                VERBOSE="true"; shift ;;
            --no-color)
                COLOR="never"; shift ;;
            --help|-h)
                usage
                exit 0 ;;
            *)
                err "Unknown option: $1"
                echo "Run ${SCRIPT_NAME} --help for usage" >&2
                exit 1 ;;
        esac
    done

    # Default scan paths if none specified
    if [[ ${#SCAN_PATHS[@]} -eq 0 ]]; then
        SCAN_PATHS=(/var/www /home /srv /tmp)
    fi
}

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

main() {
    setup_colors
    parse_args "$@"
    setup_colors    # re-init in case --no-color was passed

    local mode_label="Audit"
    if [[ "$FIX_MODE" == "true" && "$DRY_RUN" == "true" ]]; then
        mode_label="Dry-run"
    elif [[ "$FIX_MODE" == "true" ]]; then
        mode_label="Fix"
    fi

    echo ""
    echo -e "${BOLD}Permissions Fixer — $(hostname -f 2>/dev/null || hostname)${RESET}"
    echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}"
    echo -e "${DIM}Mode: ${mode_label}${RESET}"
    echo -e "${DIM}Scanning: ${SCAN_PATHS[*]}${RESET}"

    require_root_for_fix
    confirm_fix

    check_web_root
    check_world_writable
    check_orphaned_files
    check_setgid_shared
    check_home_dirs
    check_broken_acls
    check_immutable_files
    check_mount_options
    print_summary

    # Exit code based on severity
    if [[ "$COUNT_FAIL" -gt 0 ]]; then
        exit 2
    elif [[ "$COUNT_WARN" -gt 0 ]]; then
        exit 1
    fi
    exit 0
}

main "$@"
