Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
This commit is contained in:
@@ -0,0 +1,706 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user