macOS system health toolkit with 5 audit modules (packages, environment, security, cleanup, resources) and cleanup executor. Fixed bugs from review: - Replace GNU timeout with perl alarm (macOS compatible) - Remove Linux-only ps --sort flag, use portable sort - Add JSON escaping to all audit scripts - Remove redundant classify_size branch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
123 lines
5.5 KiB
Bash
Executable File
123 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# audit_security.sh — Security posture assessment for macOS (read-only)
|
|
# Output: JSON lines to stdout, one per finding
|
|
set -euo pipefail
|
|
|
|
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; }
|
|
|
|
json_finding() {
|
|
local severity="$1" finding action details
|
|
finding=$(json_escape "$2")
|
|
action=$(json_escape "$3")
|
|
details=$(json_escape "$4")
|
|
printf '{"module":"security","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \
|
|
"$severity" "$finding" "$action" "$details"
|
|
}
|
|
|
|
# --- SIP (System Integrity Protection) ---
|
|
sip_status=$(csrutil status 2>/dev/null || echo "unknown")
|
|
if echo "$sip_status" | grep -q "enabled"; then
|
|
json_finding "info" "SIP enabled" "none" ""
|
|
elif echo "$sip_status" | grep -q "disabled"; then
|
|
json_finding "critical" "SIP is disabled" "report" "System Integrity Protection should be enabled"
|
|
else
|
|
json_finding "warning" "SIP status unknown" "report" "$sip_status"
|
|
fi
|
|
|
|
# --- Gatekeeper ---
|
|
gatekeeper_status=$(spctl --status 2>/dev/null || echo "unknown")
|
|
if echo "$gatekeeper_status" | grep -q "assessments enabled"; then
|
|
json_finding "info" "Gatekeeper enabled" "none" ""
|
|
elif echo "$gatekeeper_status" | grep -q "assessments disabled"; then
|
|
json_finding "critical" "Gatekeeper is disabled" "report" "Enable via: sudo spctl --master-enable"
|
|
else
|
|
json_finding "warning" "Gatekeeper status unknown" "report" "$gatekeeper_status"
|
|
fi
|
|
|
|
# --- Firewall ---
|
|
fw_tool="/usr/libexec/ApplicationFirewall/socketfilterfw"
|
|
if [ -x "$fw_tool" ]; then
|
|
fw_status=$("$fw_tool" --getglobalstate 2>/dev/null || echo "unknown")
|
|
if echo "$fw_status" | grep -qi "enabled"; then
|
|
json_finding "info" "Firewall enabled" "none" ""
|
|
else
|
|
json_finding "critical" "Firewall is disabled" "report" "Enable in System Settings > Network > Firewall"
|
|
fi
|
|
else
|
|
# Fallback to defaults
|
|
fw_default=$(defaults read /Library/Preferences/com.apple.alf globalstate 2>/dev/null || echo "unknown")
|
|
case "$fw_default" in
|
|
0) json_finding "critical" "Firewall is disabled" "report" "Enable in System Settings > Network > Firewall" ;;
|
|
1|2) json_finding "info" "Firewall enabled" "none" "" ;;
|
|
*) json_finding "warning" "Firewall status unknown" "report" "" ;;
|
|
esac
|
|
fi
|
|
|
|
# --- FileVault ---
|
|
fv_status=$(fdesetup status 2>/dev/null || echo "unknown")
|
|
if echo "$fv_status" | grep -q "FileVault is On"; then
|
|
json_finding "info" "FileVault enabled (disk encryption)" "none" ""
|
|
elif echo "$fv_status" | grep -q "FileVault is Off"; then
|
|
json_finding "critical" "FileVault is off (disk not encrypted)" "report" "Enable in System Settings > Privacy and Security > FileVault"
|
|
else
|
|
json_finding "warning" "FileVault status unknown" "report" "$fv_status"
|
|
fi
|
|
|
|
# --- Open listening ports ---
|
|
listening_ports=$(lsof -iTCP -sTCP:LISTEN -nP 2>/dev/null | tail -n +2 || echo "")
|
|
port_count=$(echo "$listening_ports" | grep -c . 2>/dev/null || echo 0)
|
|
|
|
if [ "$port_count" -gt 0 ]; then
|
|
# Extract unique port/process combos
|
|
port_summary=$(echo "$listening_ports" | awk '{print $1":"$9}' | sort -u | head -10 | tr '\n' ', ' | sed 's/,$//')
|
|
json_finding "info" "${port_count} listening TCP connections" "review" "$port_summary"
|
|
fi
|
|
|
|
# --- SSH config ---
|
|
ssh_config="/etc/ssh/sshd_config"
|
|
if [ -f "$ssh_config" ]; then
|
|
# Check password auth
|
|
if grep -qi "^PasswordAuthentication yes" "$ssh_config" 2>/dev/null; then
|
|
json_finding "warning" "SSH password authentication enabled" "report" "Consider key-based auth only"
|
|
fi
|
|
# Check root login
|
|
if grep -qi "^PermitRootLogin yes" "$ssh_config" 2>/dev/null; then
|
|
json_finding "warning" "SSH root login permitted" "report" "Consider disabling root SSH login"
|
|
fi
|
|
fi
|
|
|
|
# --- Remote login ---
|
|
remote_login=$(systemsetup -getremotelogin 2>/dev/null || echo "unknown")
|
|
if echo "$remote_login" | grep -qi "on"; then
|
|
json_finding "info" "Remote Login (SSH) is enabled" "review" "Disable if not needed: System Settings > General > Sharing"
|
|
fi
|
|
|
|
# --- Remote management ---
|
|
remote_mgmt=$(defaults read /Library/Preferences/com.apple.RemoteManagement ARD_AllLocalUsers 2>/dev/null || echo "not configured")
|
|
if [ "$remote_mgmt" = "1" ]; then
|
|
json_finding "warning" "Remote Management enabled for all users" "review" "Check System Settings > General > Sharing"
|
|
fi
|
|
|
|
# --- Software updates ---
|
|
# Just check if updates are available (this can be slow, so we time-limit it)
|
|
# macOS lacks GNU timeout — use perl alarm as a portable alternative
|
|
update_check=$(perl -e 'alarm 30; exec @ARGV' -- softwareupdate -l 2>&1 || echo "timeout or error")
|
|
if echo "$update_check" | grep -q "No new software available"; then
|
|
json_finding "info" "macOS software is up to date" "none" ""
|
|
elif echo "$update_check" | grep -q "timeout or error"; then
|
|
json_finding "info" "Software update check timed out" "none" "Run softwareupdate -l manually"
|
|
else
|
|
update_count=$(echo "$update_check" | grep -c "^\*" 2>/dev/null || echo 0)
|
|
json_finding "warning" "${update_count} software update(s) available" "softwareupdate" "Run: softwareupdate -l for details"
|
|
fi
|
|
|
|
# --- Screen lock ---
|
|
screen_lock_delay=$(defaults -currentHost read com.apple.screensaver idleTime 2>/dev/null || echo "not set")
|
|
ask_password=$(defaults read com.apple.screensaver askForPassword 2>/dev/null || echo "unknown")
|
|
|
|
if [ "$ask_password" = "0" ]; then
|
|
json_finding "warning" "Screen lock password not required" "report" "Enable in System Settings > Lock Screen"
|
|
elif [ "$screen_lock_delay" != "not set" ] && [ "$screen_lock_delay" -gt 300 ] 2>/dev/null; then
|
|
json_finding "info" "Screen lock delay: ${screen_lock_delay}s (consider reducing)" "report" ""
|
|
fi
|