Last active
March 12, 2026 16:26
-
-
Save rvanzon/ccac07c6fe548ebe82e09b0c1ce1e5c6 to your computer and use it in GitHub Desktop.
macos-audit.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # macOS Audit Script | |
| # ------------------ | |
| # Purpose: Collects key security and system information from a macOS workstation. | |
| # Usage: Run this script to generate a plain-text audit report for compliance, troubleshooting, or inventory. | |
| # Output: Creates a timestamped .txt file with device details, FileVault, Firewall, Updates, Tailscale, and Bitwarden status. | |
| set -euo pipefail | |
| ts_print(){ printf "%s %s\n" "$(date '+%F %T')" "$*"; } | |
| g(){ printf "%s \033[32m%s\033[0m\n" "$(date '+%F %T')" "$1"; } | |
| r(){ printf "%s \033[31m%s\033[0m\n" "$(date '+%F %T')" "$1"; } | |
| y(){ printf "%s \033[33m%s\033[0m\n" "$(date '+%F %T')" "$1"; } | |
| c(){ printf "%s \033[36m%s\033[0m\n" "$(date '+%F %T')" "$1"; } | |
| g(){ printf "%s\n" "$1"; } | |
| r(){ printf "%s\n" "$1"; } | |
| y(){ printf "%s\n" "$1"; } | |
| c(){ printf "%s\n" "$1"; } | |
| # determine serial and filename (default outfile can be overridden with first arg) | |
| sn="$(system_profiler SPHardwareDataType 2>/dev/null | awk -F': ' '/Serial Number/{print $2; exit}' || true)" | |
| today="$(date +%F)" | |
| # workstation name and hostname | |
| workstation_name="$(scutil --get ComputerName 2>/dev/null || hostname)" | |
| hostname_full="$(hostname 2>/dev/null || true)" | |
| # sanitize workstation name for filenames: replace spaces with '_' and remove unsafe chars | |
| sanitized_name="$(echo "$workstation_name" | tr ' ' '_' | sed 's/[^A-Za-z0-9._-]//g')" | |
| # simple filename logic: optional first arg is output file, otherwise use date_serial_name | |
| outfile="${1:-${today}_${sn:-unknown}_${sanitized_name:-unknown}_macOS-audit.txt}" | |
| audit_main(){ | |
| sn="${sn}" | |
| osv="$(sw_vers -productVersion)" | |
| bld="$(sw_vers -buildVersion)" | |
| fv="$(fdesetup status 2>/dev/null || true)" | |
| fw="$(/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null || true)" | |
| au="$(softwareupdate --schedule 2>/dev/null || true)" | |
| # online check for available updates | |
| su_list="$(softwareupdate --list 2>/dev/null || true)" | |
| tsbin="$(command -v tailscale 2>/dev/null || true)" | |
| if [ -z "$tsbin" ]; then | |
| for p in /opt/homebrew/bin/tailscale /usr/local/bin/tailscale /Applications/Tailscale.app/Contents/Resources/tailscale /Applications/Tailscale.app/Contents/MacOS/Tailscale /usr/sbin/tailscale; do | |
| [ -x "$p" ] && { tsbin="$p"; break; } | |
| done | |
| fi | |
| tsstate="" | |
| if [ -n "$tsbin" ]; then | |
| # Prefer JSON output if available | |
| if "$tsbin" status --json >/dev/null 2>&1; then | |
| tsstate="$($tsbin status --json 2>/dev/null | jq -r '.BackendState' 2>/dev/null || true)" | |
| else | |
| # Fallback to plain status output parsing | |
| raw="$($tsbin status 2>/dev/null || true)" | |
| if echo "$raw" | grep -iq "logged out\|no state\|not authenticated\|not connected"; then | |
| tsstate="NotRunning" | |
| elif echo "$raw" | grep -iq "running\|connected\|active"; then | |
| tsstate="Running" | |
| else | |
| tsstate="Unknown" | |
| fi | |
| fi | |
| fi | |
| printf "%s macOS device audit\n" "$(date '+%F %T')" | |
| printf "Serial: %s\nName: %s\nHostname: %s\nmacOS: %s (%s)\n" "${sn:-unknown}" "$workstation_name" "$hostname_full" "$osv" "$bld" | |
| if [[ "$fv" == *On.* ]]; then | |
| g "✓ FileVault: ON" | |
| else | |
| r "✗ FileVault: OFF" | |
| fi | |
| if [[ "$fw" == *enabled* || "$fw" == *State* ]]; then | |
| g "✓ Firewall: ON" | |
| else | |
| r "✗ Firewall: OFF" | |
| fi | |
| if [[ "$au" == *on* || "$au" == *On* || "$au" == *Enabled* ]]; then | |
| g "✓ Automatic Updates: ON" | |
| else | |
| y "! Automatic Updates: check manually ($au)" | |
| fi | |
| # check if macOS is up to date via softwareupdate --list | |
| if echo "$su_list" | grep -iq "no new software available\|no updates are available"; then | |
| g "✓ macOS: up to date" | |
| elif [ -z "$(echo "$su_list" | tr -d '[:space:]')" ]; then | |
| y "! macOS update check failed or returned no output" | |
| else | |
| # try to extract listed update names (lines starting with '*') | |
| updates="$(echo "$su_list" | sed -n 's/^[[:space:]]*\* \(.*\)$/\1/p' | tr '\n' ',' | sed 's/,$//')" | |
| if [ -n "$updates" ]; then | |
| r "✗ Updates available: $updates" | |
| else | |
| g "✓ macOS: up to date" | |
| fi | |
| fi | |
| if [ -n "$tsbin" ]; then | |
| if [[ "$tsstate" == "Running" ]]; then | |
| g "✓ Tailscale: signed in + connected" | |
| else | |
| y "! Tailscale: ${tsstate:-status unknown}" | |
| fi | |
| else | |
| y "! Tailscale: not installed" | |
| fi | |
| # Bitwarden checks: only desktop app presence and running state (no CLI checks) | |
| bw_app_installed="" | |
| if [ -d "/Applications/Bitwarden.app" ] || [ -d "/Applications/Bitwarden Desktop.app" ]; then | |
| bw_app_installed="yes" | |
| fi | |
| bw_running="" | |
| if pgrep -f "Bitwarden" >/dev/null 2>&1; then | |
| bw_running="running" | |
| fi | |
| if [ -n "$bw_app_installed" ]; then | |
| if [ -n "$bw_running" ]; then | |
| g "✓ Bitwarden: desktop app installed" | |
| else | |
| y "! Bitwarden: desktop app installed (not active)" | |
| fi | |
| else | |
| y "! Bitwarden: not installed" | |
| fi | |
| } | |
| # run audit and write to outfile, then display file contents | |
| mkdir -p "$(dirname "$outfile")" 2>/dev/null || true | |
| c "Running macOS audit — please wait..." | |
| if audit_main > "$outfile"; then | |
| c "Saved audit to: $outfile" | |
| cat "$outfile" | |
| exit 0 | |
| else | |
| r "Failed to write audit to: $outfile" | |
| exit 2 | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment