#!/usr/bin/env bash # mac-ops :: firewall-audit.sh # Inventory macOS firewall state across all layers: # 1. Application Layer Firewall (ALF) — System Settings → Network → Firewall # 2. Packet Filter (pf) — BSD-level packet filtering # 3. Network Extension content filters (Little Snitch, Lulu, Cisco AnyConnect, etc.) # 4. Stealth mode + logging state set -u while [[ $# -gt 0 ]]; do case "$1" in --help|-h) cat </dev/null | tail -1) case "$state" in *"enabled"*) log_pass "ALF state" "$state" ;; *"disabled"*) log_warn "ALF state" "$state — incoming connections unblocked" ;; *) log_info "ALF state" "$state" ;; esac stealth=$("$ALF" --getstealthmode 2>/dev/null | tail -1) note " $stealth" block_all=$("$ALF" --getblockall 2>/dev/null | tail -1) note " $block_all" allow_signed=$("$ALF" --getallowsigned 2>/dev/null | tail -1) note " $allow_signed" # Per-app rules (may require sudo) rules=$("$ALF" --listapps 2>/dev/null | grep -c "^[0-9]" || echo 0) log_info "ALF per-app rules" "$rules" fi # ---------------------------------------------------------------------------- section "2. PACKET FILTER (pf)" # ---------------------------------------------------------------------------- if pf_info=$(pfctl -s info 2>&1); then if echo "$pf_info" | grep -q "Status: Enabled"; then log_warn "pf state" "Enabled — packet filter active" note " $(echo "$pf_info" | head -3 | sed 's/^/ /')" else log_pass "pf state" "Disabled (default for desktop Macs)" fi else log_info "pf state" "could not query (needs sudo)" fi # Anchors loaded if pf_anchors=$(sudo -n pfctl -s Anchors 2>/dev/null); then if [[ -n "$pf_anchors" ]]; then log_info "pf anchors loaded" "$(echo "$pf_anchors" | wc -l | tr -d ' ')" echo "$pf_anchors" | head -10 | sed 's/^/ /' fi fi # ---------------------------------------------------------------------------- section "3. NETWORK EXTENSION CONTENT FILTERS" # ---------------------------------------------------------------------------- # These are third-party filters that operate in their own NetworkExtension # rather than via ALF. They persist after the parent app quits. ne_filters=$(scutil --nc list 2>/dev/null | grep -iE "filter|firewall" || true) if [[ -n "$ne_filters" ]]; then log_info "Network Extension content filters" "$(echo "$ne_filters" | wc -l | tr -d ' ')" echo "$ne_filters" | sed 's/^/ /' else log_pass "Network Extension content filters" "none configured" fi # Check for common third-party firewall apps note "" note " Installed firewall/network-monitoring apps:" for app in "Little Snitch" "LuLu" "Murus" "Hands Off" "Radio Silence" "NetIQuette"; do if [[ -e "/Applications/$app.app" ]]; then note " /Applications/$app.app" fi done # ---------------------------------------------------------------------------- section "4. FIREWALL LOG SAMPLE" # ---------------------------------------------------------------------------- # Anything ALF dropped in last hour recent_blocks=$(log show --last 1h --style compact \ --predicate 'process == "socketfilterfw" OR eventMessage CONTAINS "Deny"' \ 2>/dev/null | grep -iE "(deny|block|drop)" | tail -5) if [[ -n "$recent_blocks" ]]; then log_info "Recent firewall denials (1h)" "see below" echo "$recent_blocks" | sed 's/^/ /' fi # ---------------------------------------------------------------------------- section "5. VPN / TUNNEL CONFIGURATION" # ---------------------------------------------------------------------------- note " Active network services (VPN/tunnel filter):" networksetup -listallnetworkservices 2>/dev/null | tail -n +2 | \ grep -iE "vpn|tunnel|wireguard|openvpn|warp|nextdns|tailscale|cisco|anyconnect|proton|mullvad" | \ sed 's/^/ /' # Active tunnels right now note "" note " Active utun interfaces:" ifconfig 2>/dev/null | awk '/^utun[0-9]+:/{ifn=$1; sub(":","",ifn)} /inet[6]? /{if(ifn!="" && $1!~/^fe80/){print " "ifn": "$0; ifn=""}}' | head -10 # ---------------------------------------------------------------------------- emit_summary