Skip to content

Instantly share code, notes, and snippets.

@rvanzon
Last active March 12, 2026 16:26
Show Gist options
  • Select an option

  • Save rvanzon/ccac07c6fe548ebe82e09b0c1ce1e5c6 to your computer and use it in GitHub Desktop.

Select an option

Save rvanzon/ccac07c6fe548ebe82e09b0c1ce1e5c6 to your computer and use it in GitHub Desktop.
macos-audit.sh
#!/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