startup-audit.sh 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. #!/usr/bin/env bash
  2. # mac-ops :: startup-audit.sh
  3. # Inventory every auto-start mechanism on this Mac.
  4. #
  5. # Covers:
  6. # - System Settings → Login Items (user-visible)
  7. # - User LaunchAgents ~/Library/LaunchAgents
  8. # - System LaunchAgents /Library/LaunchAgents
  9. # - System LaunchDaemons /Library/LaunchDaemons
  10. # - Apple LaunchAgents /System/Library/LaunchAgents (system-managed, usually skip)
  11. # - Privileged helpers /Library/PrivilegedHelperTools
  12. # - Legacy LoginHook `sudo defaults read com.apple.loginwindow LoginHook`
  13. set -u
  14. for arg in "$@"; do
  15. case "$arg" in
  16. --help|-h)
  17. cat <<EOF
  18. Usage: $0 [options]
  19. --json Emit NDJSON
  20. --redact Mask private addrs / hostnames
  21. --verbose Include /System/Library/LaunchAgents (Apple's own — usually noise)
  22. --quiet Suppress section banners
  23. Reports total counts per mechanism + per-entry detail. To DISABLE an entry,
  24. use scripts/safe-disable-startup.sh.
  25. EOF
  26. exit 0 ;;
  27. esac
  28. done
  29. source "$(dirname "$0")/_lib/common.sh"
  30. parse_common_flags "$@"
  31. maybe_filter_self "$@"
  32. # ----------------------------------------------------------------------------
  33. section "1. LOGIN ITEMS (System Settings → General → Login Items)"
  34. # ----------------------------------------------------------------------------
  35. if items=$(osascript <<'APPLESCRIPT' 2>/dev/null
  36. tell application "System Events"
  37. set output to ""
  38. repeat with li in (every login item)
  39. set output to output & (name of li) & "|" & (path of li) & "|" & (hidden of li) & linefeed
  40. end repeat
  41. return output
  42. end tell
  43. APPLESCRIPT
  44. ); then
  45. if [[ -z "$items" ]]; then
  46. log_pass "Login Items count" "0"
  47. else
  48. count=$(echo "$items" | grep -c '|' || echo 0)
  49. log_info "Login Items count" "$count"
  50. note " Items (name | path | hidden):"
  51. echo "$items" | sed 's/^/ /'
  52. fi
  53. else
  54. log_warn "Login Items" "could not query System Events (TCC may be denying Automation)"
  55. fi
  56. # ----------------------------------------------------------------------------
  57. section "2. USER LAUNCHAGENTS (~/Library/LaunchAgents)"
  58. # ----------------------------------------------------------------------------
  59. agents_dir="$HOME/Library/LaunchAgents"
  60. if [[ -d "$agents_dir" ]]; then
  61. count=$(find "$agents_dir" -maxdepth 1 -name "*.plist" 2>/dev/null | wc -l | tr -d ' ')
  62. log_info "User LaunchAgents count" "$count"
  63. if [[ "$count" -gt 0 ]]; then
  64. note " Plists (label · RunAtLoad · KeepAlive · path):"
  65. for p in "$agents_dir"/*.plist; do
  66. [[ -f "$p" ]] || continue
  67. # Try PlistBuddy first; fall back to plutil; fall back to filename
  68. label=$(/usr/libexec/PlistBuddy -c "Print :Label" "$p" 2>/dev/null) || label=""
  69. [[ -z "$label" ]] && { label=$(plutil -extract Label raw -o - "$p" 2>/dev/null) || label=""; }
  70. [[ -z "$label" ]] && label="$(basename "$p" .plist) (label unread)"
  71. run_at_load=$(plutil -extract RunAtLoad raw -o - "$p" 2>/dev/null) || run_at_load=""
  72. [[ -z "$run_at_load" ]] && run_at_load="no"
  73. keep_alive=$(plutil -extract KeepAlive raw -o - "$p" 2>/dev/null) || keep_alive=""
  74. [[ -z "$keep_alive" ]] && keep_alive="no"
  75. prog=$(plutil -extract ProgramArguments.0 raw -o - "$p" 2>/dev/null) || prog=""
  76. if [[ -z "$prog" ]]; then
  77. prog=$(plutil -extract Program raw -o - "$p" 2>/dev/null) || prog=""
  78. fi
  79. [[ -z "$prog" ]] && prog="(no Program/ProgramArguments)"
  80. printf " %-45s · RunAtLoad=%s · KeepAlive=%s\n %s\n" "$label" "$run_at_load" "$keep_alive" "$prog"
  81. done
  82. fi
  83. else
  84. log_info "User LaunchAgents directory" "absent"
  85. fi
  86. # ----------------------------------------------------------------------------
  87. section "3. SYSTEM LAUNCHAGENTS (/Library/LaunchAgents)"
  88. # ----------------------------------------------------------------------------
  89. sys_agents_dir="/Library/LaunchAgents"
  90. if [[ -d "$sys_agents_dir" ]]; then
  91. count=$(find "$sys_agents_dir" -maxdepth 1 -name "*.plist" 2>/dev/null | wc -l | tr -d ' ')
  92. log_info "System LaunchAgents count" "$count"
  93. if [[ "$count" -gt 0 ]]; then
  94. note " Plists (label · vendor-pattern hint):"
  95. for p in "$sys_agents_dir"/*.plist; do
  96. [[ -f "$p" ]] || continue
  97. label=$(/usr/libexec/PlistBuddy -c "Print :Label" "$p" 2>/dev/null) || label=""
  98. [[ -z "$label" ]] && { label=$(plutil -extract Label raw -o - "$p" 2>/dev/null) || label=""; }
  99. [[ -z "$label" ]] && label="$(basename "$p" .plist) (label unread)"
  100. hint=""
  101. case "$label" in
  102. com.adobe.*) hint="Adobe (Creative Cloud helpers)" ;;
  103. com.docker.*) hint="Docker Desktop" ;;
  104. com.microsoft.*) hint="Microsoft (Office / Edge / Teams)" ;;
  105. com.google.*) hint="Google (Chrome / Drive)" ;;
  106. com.dropbox.*) hint="Dropbox" ;;
  107. com.cisco.*) hint="Cisco (AnyConnect / WebEx)" ;;
  108. com.paragon-*) hint="Paragon (NTFS / ExtFS)" ;;
  109. org.openvpn.*) hint="OpenVPN / Tunnelblick" ;;
  110. com.tailscale.*) hint="Tailscale" ;;
  111. io.nextdns.*) hint="NextDNS" ;;
  112. ch.protonvpn.*) hint="Proton VPN" ;;
  113. esac
  114. printf " %-50s%s\n" "$label" "${hint:+— $hint}"
  115. done
  116. fi
  117. fi
  118. # ----------------------------------------------------------------------------
  119. section "4. SYSTEM LAUNCHDAEMONS (/Library/LaunchDaemons)"
  120. # ----------------------------------------------------------------------------
  121. daemons_dir="/Library/LaunchDaemons"
  122. if [[ -d "$daemons_dir" ]]; then
  123. count=$(find "$daemons_dir" -maxdepth 1 -name "*.plist" 2>/dev/null | wc -l | tr -d ' ')
  124. log_info "System LaunchDaemons count" "$count"
  125. if [[ "$count" -gt 0 ]]; then
  126. note " Plists (label):"
  127. for p in "$daemons_dir"/*.plist; do
  128. [[ -f "$p" ]] || continue
  129. label=$(/usr/libexec/PlistBuddy -c "Print :Label" "$p" 2>/dev/null) || label=""
  130. [[ -z "$label" ]] && { label=$(plutil -extract Label raw -o - "$p" 2>/dev/null) || label=""; }
  131. [[ -z "$label" ]] && label="$(basename "$p" .plist) (label unread)"
  132. printf " %s\n" "$label"
  133. done
  134. fi
  135. fi
  136. # ----------------------------------------------------------------------------
  137. section "5. PRIVILEGED HELPER TOOLS"
  138. # ----------------------------------------------------------------------------
  139. helpers_dir="/Library/PrivilegedHelperTools"
  140. if [[ -d "$helpers_dir" ]]; then
  141. count=$(find "$helpers_dir" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')
  142. if [[ "$count" -gt 5 ]]; then
  143. log_warn "Privileged helper tools" "$count — may include orphans from uninstalled apps"
  144. else
  145. log_info "Privileged helper tools" "$count"
  146. fi
  147. if [[ "$count" -gt 0 ]]; then
  148. note " Helpers:"
  149. find "$helpers_dir" -maxdepth 1 -type f 2>/dev/null | sed 's/^/ /'
  150. fi
  151. fi
  152. # ----------------------------------------------------------------------------
  153. section "6. LEGACY LoginHook (rarely used these days)"
  154. # ----------------------------------------------------------------------------
  155. if hook=$(sudo -n defaults read com.apple.loginwindow LoginHook 2>/dev/null); then
  156. log_warn "LoginHook present" "$hook"
  157. else
  158. log_pass "LoginHook" "none (or sudo declined)"
  159. fi
  160. # ----------------------------------------------------------------------------
  161. section "7. CONFIGURATION PROFILES (may add login items / restrictions)"
  162. # ----------------------------------------------------------------------------
  163. if profile_count=$(profiles list -type configuration 2>/dev/null | grep -c "attribute:"); then
  164. profile_count="${profile_count:-0}"
  165. if [[ "$profile_count" -gt 0 ]]; then
  166. log_info "Configuration profiles (user)" "$profile_count"
  167. note " Run 'sudo profiles list -type configuration' for system-wide profile list."
  168. else
  169. log_pass "Configuration profiles (user)" "0"
  170. fi
  171. fi
  172. # ----------------------------------------------------------------------------
  173. section "8. /System/Library/LaunchAgents (Apple's own — usually noise)"
  174. # ----------------------------------------------------------------------------
  175. if [[ "$VERBOSE" -eq 1 ]]; then
  176. apple_agents=$(find /System/Library/LaunchAgents -maxdepth 1 -name "*.plist" 2>/dev/null | wc -l | tr -d ' ')
  177. log_info "Apple-managed LaunchAgents" "$apple_agents (system-protected; informational only)"
  178. else
  179. note " (skipped — pass --verbose to include Apple-managed agents)"
  180. fi
  181. # ----------------------------------------------------------------------------
  182. emit_summary
  183. if [[ "$JSON_MODE" -eq 0 ]]; then
  184. echo
  185. note " To DISABLE an entry: scripts/safe-disable-startup.sh -n <pattern>"
  186. note " To RE-ENABLE: scripts/safe-disable-startup.sh -n <pattern> --enable"
  187. fi