kext-audit.sh 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. #!/usr/bin/env bash
  2. # mac-ops :: kext-audit.sh
  3. # Inventory loaded kernel extensions + system extensions.
  4. #
  5. # Why: kexts and system extensions run with kernel privileges. A misbehaving
  6. # one can panic the system, leak memory, or hold a system-wide lock. They're
  7. # the #1 cause of "Mac kernel panic" on machines that get them.
  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. Reports:
  16. 1. Loaded kexts (kextstat) — third-party highlighted
  17. 2. Installed system extensions (systemextensionsctl list)
  18. 3. Kext load failures from log
  19. 4. Pending kext approval (apps that requested kext but were denied)
  20. 5. SIP and kernel security policy state
  21. On Apple Silicon (M1+), kexts are deprecated in favor of system extensions
  22. which run in userspace. This script reports both because some legacy products
  23. still ship kexts even on Apple Silicon (via boot policy reduction).
  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. # ----------------------------------------------------------------------------
  33. section "1. LOADED KEXTS"
  34. # ----------------------------------------------------------------------------
  35. total_kexts=$(kextstat -l 2>/dev/null | wc -l | tr -d ' ')
  36. log_info "Loaded kexts (total)" "$total_kexts"
  37. # Third-party kexts — anything not com.apple.*
  38. third_party=$(kextstat -l 2>/dev/null | awk '{print $6}' | grep -v "^com.apple\." | grep -v "^$" | sort -u)
  39. third_party_count=$(echo "$third_party" | grep -c . 2>/dev/null || echo 0)
  40. if [[ "$third_party_count" -gt 0 ]]; then
  41. log_warn "Third-party kexts" "$third_party_count — primary panic suspects"
  42. note " Third-party kexts (loaded right now):"
  43. echo "$third_party" | sed 's/^/ /'
  44. else
  45. log_pass "Third-party kexts" "0 — clean kernel"
  46. fi
  47. # ----------------------------------------------------------------------------
  48. section "2. SYSTEM EXTENSIONS"
  49. # ----------------------------------------------------------------------------
  50. if command -v systemextensionsctl >/dev/null 2>&1; then
  51. sysext_out=$(systemextensionsctl list 2>/dev/null)
  52. if [[ -n "$sysext_out" ]]; then
  53. # The output has 0+ extensions per team. Skip the header line.
  54. ext_lines=$(echo "$sysext_out" | grep -E "^\s*\*?\s+[a-fA-F0-9]" || true)
  55. if [[ -n "$ext_lines" ]]; then
  56. ext_count=$(echo "$ext_lines" | wc -l | tr -d ' ')
  57. log_info "Installed system extensions" "$ext_count"
  58. note " System extensions (team-id, bundle-id, name, state):"
  59. echo "$ext_lines" | head -20 | sed 's/^/ /'
  60. else
  61. log_pass "Installed system extensions" "0"
  62. fi
  63. fi
  64. else
  65. log_info "systemextensionsctl" "not available (older macOS?)"
  66. fi
  67. # ----------------------------------------------------------------------------
  68. section "3. RECENT KEXT LOAD FAILURES"
  69. # ----------------------------------------------------------------------------
  70. load_fails=$(log show --last 7d --style compact \
  71. --predicate '(process == "kextd" OR process == "kernel") AND (eventMessage CONTAINS[c] "kext" AND (messageType == "Error" OR messageType == "Fault"))' \
  72. 2>/dev/null | head -20)
  73. if [[ -n "$load_fails" ]]; then
  74. n=$(echo "$load_fails" | wc -l | tr -d ' ')
  75. log_warn "Kext load failures (7d)" "$n events"
  76. echo "$load_fails" | head -5 | sed 's/^/ /'
  77. else
  78. log_pass "Kext load failures (7d)" "none"
  79. fi
  80. # ----------------------------------------------------------------------------
  81. section "4. PENDING KEXT APPROVAL"
  82. # ----------------------------------------------------------------------------
  83. # Apps that have requested kext load but were denied — usually because user
  84. # hasn't approved in System Settings → Privacy & Security
  85. pending=$(log show --last 30d --style compact \
  86. --predicate 'eventMessage CONTAINS[c] "kext approval"' \
  87. 2>/dev/null | tail -5)
  88. if [[ -n "$pending" ]]; then
  89. log_warn "Pending kext approvals (30d)" "see below"
  90. echo "$pending" | sed 's/^/ /'
  91. else
  92. log_pass "Pending kext approvals" "none"
  93. fi
  94. # ----------------------------------------------------------------------------
  95. section "5. SECURITY POLICY"
  96. # ----------------------------------------------------------------------------
  97. # SIP status
  98. sip_status=$(csrutil status 2>/dev/null | head -1 | awk -F': *' '{print $2}' | tr -d '.')
  99. case "$sip_status" in
  100. *enabled*) log_pass "SIP" "$sip_status" ;;
  101. *disabled*) log_warn "SIP" "$sip_status — kernel security weakened" ;;
  102. *) log_info "SIP" "${sip_status:-unknown}" ;;
  103. esac
  104. # On Apple Silicon: bputil reports boot policy
  105. if is_apple_silicon && command -v bputil >/dev/null 2>&1; then
  106. note " Apple Silicon boot policy (requires sudo for detail):"
  107. sudo -n bputil -d 2>/dev/null | grep -E "Security Policy|Manage Kernel Extensions|Allow User Kernel Extensions" | head -5 | sed 's/^/ /' || \
  108. note " (sudo required for full bputil read)"
  109. fi
  110. # Apple Silicon specific kext loading state
  111. if is_apple_silicon; then
  112. kext_loading=$(kmutil showloaded 2>/dev/null | wc -l | tr -d ' ')
  113. log_info "kmutil showloaded count" "$kext_loading"
  114. fi
  115. # ----------------------------------------------------------------------------
  116. section "6. VENDOR PATTERNS"
  117. # ----------------------------------------------------------------------------
  118. # Known panic-prone vendors
  119. note " Scanning for known-problematic kexts:"
  120. for pattern in "eltima" "paragon" "eset" "kaspersky" "norton" "sophos" "bitdefender"; do
  121. matches=$(kextstat -l 2>/dev/null | awk '{print $6}' | grep -i "$pattern" || true)
  122. if [[ -n "$matches" ]]; then
  123. log_warn "Vendor kext: $pattern" "$(echo "$matches" | wc -l | tr -d ' ') loaded"
  124. echo "$matches" | head -3 | sed 's/^/ /'
  125. fi
  126. done
  127. # ----------------------------------------------------------------------------
  128. emit_summary
  129. if [[ "$JSON_MODE" -eq 0 ]]; then
  130. echo
  131. note " To uninstall a system extension:"
  132. note " systemextensionsctl uninstall <team-id> <bundle-id>"
  133. note " To inspect a specific kext:"
  134. note " kextstat -l | grep <name>"
  135. note " kmutil showloaded | grep <name> # Apple Silicon"
  136. fi