firewall-audit.sh 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. #!/usr/bin/env bash
  2. # mac-ops :: firewall-audit.sh
  3. # Inventory macOS firewall state across all layers:
  4. # 1. Application Layer Firewall (ALF) — System Settings → Network → Firewall
  5. # 2. Packet Filter (pf) — BSD-level packet filtering
  6. # 3. Network Extension content filters (Little Snitch, Lulu, Cisco AnyConnect, etc.)
  7. # 4. Stealth mode + logging state
  8. set -u
  9. while [[ $# -gt 0 ]]; do
  10. case "$1" in
  11. --help|-h)
  12. cat <<EOF
  13. Usage: $0 [options]
  14. --json, --redact, --quiet, --verbose
  15. macOS firewall stack:
  16. ALF (socketfilterfw) Application-layer firewall — blocks incoming
  17. connections per-app. Visible in System Settings.
  18. pf (packet filter) BSD-style packet filtering. Configured via
  19. /etc/pf.conf and /etc/pf.anchors/. Usually
  20. inactive on desktop Macs.
  21. Network Extension filters Third-party (Little Snitch, Lulu, AnyConnect)
  22. implement custom filtering as content filters.
  23. Persist after app quit until disabled.
  24. EOF
  25. exit 0 ;;
  26. *) shift ;;
  27. esac
  28. done
  29. source "$(dirname "$0")/_lib/common.sh"
  30. parse_common_flags "$@"
  31. maybe_filter_self "$@"
  32. ALF="/usr/libexec/ApplicationFirewall/socketfilterfw"
  33. # ----------------------------------------------------------------------------
  34. section "1. APPLICATION LAYER FIREWALL (ALF)"
  35. # ----------------------------------------------------------------------------
  36. if [[ ! -x "$ALF" ]]; then
  37. log_warn "socketfilterfw binary" "not found at $ALF"
  38. else
  39. state=$("$ALF" --getglobalstate 2>/dev/null | tail -1)
  40. case "$state" in
  41. *"enabled"*) log_pass "ALF state" "$state" ;;
  42. *"disabled"*) log_warn "ALF state" "$state — incoming connections unblocked" ;;
  43. *) log_info "ALF state" "$state" ;;
  44. esac
  45. stealth=$("$ALF" --getstealthmode 2>/dev/null | tail -1)
  46. note " $stealth"
  47. block_all=$("$ALF" --getblockall 2>/dev/null | tail -1)
  48. note " $block_all"
  49. allow_signed=$("$ALF" --getallowsigned 2>/dev/null | tail -1)
  50. note " $allow_signed"
  51. # Per-app rules (may require sudo)
  52. rules=$("$ALF" --listapps 2>/dev/null | grep -c "^[0-9]" || echo 0)
  53. log_info "ALF per-app rules" "$rules"
  54. fi
  55. # ----------------------------------------------------------------------------
  56. section "2. PACKET FILTER (pf)"
  57. # ----------------------------------------------------------------------------
  58. if pf_info=$(pfctl -s info 2>&1); then
  59. if echo "$pf_info" | grep -q "Status: Enabled"; then
  60. log_warn "pf state" "Enabled — packet filter active"
  61. note " $(echo "$pf_info" | head -3 | sed 's/^/ /')"
  62. else
  63. log_pass "pf state" "Disabled (default for desktop Macs)"
  64. fi
  65. else
  66. log_info "pf state" "could not query (needs sudo)"
  67. fi
  68. # Anchors loaded
  69. if pf_anchors=$(sudo -n pfctl -s Anchors 2>/dev/null); then
  70. if [[ -n "$pf_anchors" ]]; then
  71. log_info "pf anchors loaded" "$(echo "$pf_anchors" | wc -l | tr -d ' ')"
  72. echo "$pf_anchors" | head -10 | sed 's/^/ /'
  73. fi
  74. fi
  75. # ----------------------------------------------------------------------------
  76. section "3. NETWORK EXTENSION CONTENT FILTERS"
  77. # ----------------------------------------------------------------------------
  78. # These are third-party filters that operate in their own NetworkExtension
  79. # rather than via ALF. They persist after the parent app quits.
  80. ne_filters=$(scutil --nc list 2>/dev/null | grep -iE "filter|firewall" || true)
  81. if [[ -n "$ne_filters" ]]; then
  82. log_info "Network Extension content filters" "$(echo "$ne_filters" | wc -l | tr -d ' ')"
  83. echo "$ne_filters" | sed 's/^/ /'
  84. else
  85. log_pass "Network Extension content filters" "none configured"
  86. fi
  87. # Check for common third-party firewall apps
  88. note ""
  89. note " Installed firewall/network-monitoring apps:"
  90. for app in "Little Snitch" "LuLu" "Murus" "Hands Off" "Radio Silence" "NetIQuette"; do
  91. if [[ -e "/Applications/$app.app" ]]; then
  92. note " /Applications/$app.app"
  93. fi
  94. done
  95. # ----------------------------------------------------------------------------
  96. section "4. FIREWALL LOG SAMPLE"
  97. # ----------------------------------------------------------------------------
  98. # Anything ALF dropped in last hour
  99. recent_blocks=$(log show --last 1h --style compact \
  100. --predicate 'process == "socketfilterfw" OR eventMessage CONTAINS "Deny"' \
  101. 2>/dev/null | grep -iE "(deny|block|drop)" | tail -5)
  102. if [[ -n "$recent_blocks" ]]; then
  103. log_info "Recent firewall denials (1h)" "see below"
  104. echo "$recent_blocks" | sed 's/^/ /'
  105. fi
  106. # ----------------------------------------------------------------------------
  107. section "5. VPN / TUNNEL CONFIGURATION"
  108. # ----------------------------------------------------------------------------
  109. note " Active network services (VPN/tunnel filter):"
  110. networksetup -listallnetworkservices 2>/dev/null | tail -n +2 | \
  111. grep -iE "vpn|tunnel|wireguard|openvpn|warp|nextdns|tailscale|cisco|anyconnect|proton|mullvad" | \
  112. sed 's/^/ /'
  113. # Active tunnels right now
  114. note ""
  115. note " Active utun interfaces:"
  116. ifconfig 2>/dev/null | awk '/^utun[0-9]+:/{ifn=$1; sub(":","",ifn)} /inet[6]? /{if(ifn!="" && $1!~/^fe80/){print " "ifn": "$0; ifn=""}}' | head -10
  117. # ----------------------------------------------------------------------------
  118. emit_summary