This is a blog article originally posted on
This article is split into two separate posts, so for the full article, please proceed to Part 2
Provide your users a "heads-up display" of critical computer compliance information via swiftDialog
Provide your users a "heads-up display" of critical computer compliance information via swiftDialog
Background
More than six years ago,William SmithpublishedBuild a Computer Information script for your Help Desk; we implemented a customized version in the fall of that same year.
Last week, during a conversation with one of our rock-star TSRs — whom I'll refer to as "John" — we decided it was time forswiftDialog-ized reboot.
Features
The following compliance checks and information reporting are included in version1.9.0, which operates in "test" mode by default. (ChangeoperationModetoproductionwhen ready to deploy in production.)
Compliance Checks
- macOS Version
- Available Updates (including deferred updates)
- System Integrity Protection
- Firewall
- FileVault Encryption
- Last Reboot
- Free Disk Space
- MDM Profile
- MDM Certificate Expiration
- Apple Push Notification service
- Jamf Pro Check-in
- Jamf Pro Inventory
- BeyondTrust Privilege Management*
- Cisco Umbrella*
- CrowdStrike Falcon*
- Palo Alto GlobalProtect*
- Network Quality Test
- Update Computer Inventory
*Setup Your Mac Validations
Information Reporting
- IT SupportTelephoneEmailWebsiteKnowledge Base Article
- Telephone
- Website
- Knowledge Base Article
- User InformationFull NameUser NameUser IDLocation ServicesMicrosoft OneDrive Sync DatePlatform Single Sign-on Extension
- Full Name
- User Name
- User ID
- Location Services
- Microsoft OneDrive Sync Date
- Platform Single Sign-on Extension
- Computer InformationmacOS version (build)Computer NameSerial NumberWi-Fi SSIDWi-FI IP AddressVPN IP Address
- macOS version (build)
- Computer Name
- Serial Number
- Wi-Fi SSID
- Wi-FI IP Address
- VPN IP Address
- Jamf Pro Information**Site
- Site
- Telephone
- Website
- Knowledge Base Article
- Full Name
- User Name
- User ID
- Location Services
- Microsoft OneDrive Sync Date
- Platform Single Sign-on Extension
- macOS version (build)
- Computer Name
- Serial Number
- Wi-Fi SSID
- Wi-FI IP Address
- VPN IP Address
- Site
**Payload Variables for Configuration Profiles
Policy Log Reporting
- Warning when logged-in user is a member ofadmin
- Deferred Software Updates
- Logged-In User Group Membership
- Kerberos SSOe
- SSH
- Time Machine
- Battery Cycle Count
- Network Time Server
- Jamf Pro ID
Configuration
Complete the following steps to add the Computer Compliance check for your users.
- Review and adjust theGlobal Variablesas required for your environmentscriptLog(i.e., the location of your client-side logs)operationMode(i.e., Change toproductionwhen ready to deploy in production)
- scriptLog(i.e., the location of your client-side logs)
- operationMode(i.e., Change toproductionwhen ready to deploy in production)
- scriptLog(i.e., the location of your client-side logs)
- operationMode(i.e., Change toproductionwhen ready to deploy in production)
############################################################################################
#
# Global Variables
#
############################################################################################
export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin/
# Script Version
scriptVersion="1.9.0"
# Client-side Log
scriptLog="/var/log/org.churchofjesuschrist.log"
# Elapsed Time
SECONDS="0"
# Operation Mode [ test | production ]
operationMode="test"
- Set your preferredOrganization VariablesorganizationColorScheme(i.e., your organization's corporate colors)previousMinorOS(i.e., the number of previous minor OS versions which can pass the OS compliance check; thanks, @robjschroeder!)allowedFreeDiskPercentage(i.e., the allowed percentage of free disk space)networkQualityTestMaximumAge(i.e., frequently at which the time-intensive Network Quality Test should be executed)allowedUptimeMinutes(i.e., the allowed number of minutes since the last reboot)excessiveUptimeAlertStyle(i.e., should excessive uptime result in a "warning" or "error" ?)completionTimer(i.e., number of seconds before the dialog is auto-closed)
- organizationColorScheme(i.e., your organization's corporate colors)
- previousMinorOS(i.e., the number of previous minor OS versions which can pass the OS compliance check; thanks, @robjschroeder!)
- allowedFreeDiskPercentage(i.e., the allowed percentage of free disk space)
- networkQualityTestMaximumAge(i.e., frequently at which the time-intensive Network Quality Test should be executed)
- allowedUptimeMinutes(i.e., the allowed number of minutes since the last reboot)
- excessiveUptimeAlertStyle(i.e., should excessive uptime result in a "warning" or "error" ?)
- completionTimer(i.e., number of seconds before the dialog is auto-closed)
- organizationColorScheme(i.e., your organization's corporate colors)
- previousMinorOS(i.e., the number of previous minor OS versions which can pass the OS compliance check; thanks, @robjschroeder!)
- allowedFreeDiskPercentage(i.e., the allowed percentage of free disk space)
- networkQualityTestMaximumAge(i.e., frequently at which the time-intensive Network Quality Test should be executed)
- allowedUptimeMinutes(i.e., the allowed number of minutes since the last reboot)
- excessiveUptimeAlertStyle(i.e., should excessive uptime result in a "warning" or "error" ?)
- completionTimer(i.e., number of seconds before the dialog is auto-closed)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Organization Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Script Human-readabale Name
humanReadableScriptName="Computer Compliance"
# Organization's Script Name
organizationScriptName="CC"
# Organization's Color Scheme
organizationColorScheme="weight=semibold,colour1=#ef9d51,colour2=#ef7951"
# Organization's Kerberos Realm (leave blank to disable check)
kerberosRealm=""
# "Anticipation" Duration (in seconds)
anticipationDuration="2"
# How many previous minor OS versions will be marked as compliant
previousMinorOS="2"
# Allowed percentage of free disk space
allowedFreeDiskPercentage="10"
# Network Quality Test Maximum Age
# Leverages `date -v-`; One of either y, m, w, d, H, M or S
# must be used to specify which part of the date is to be adjusted
networkQualityTestMaximumAge="1H"
# Allowed number of uptime minutes
# - 1 day = 24 hours × 60 minutes/hour = 1,440 minutes
# - 7 days, multiply: 7 × 1,440 minutes = 10,080 minutes
allowedUptimeMinutes="10080"
# Should excessive uptime result in a "warning" or "error" ?
excessiveUptimeAlertStyle="warning"
# Completion Timer (in seconds)
completionTimer="60"
- If you've deployed a Configuration Profile forJamf Pro variables, specify the Preference Domain, shown below asjamfProVariables. (Note to self:Locate or write a blog post about "Jamf Pro Payload Variables for Configuration Profiles.")
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Jamf Pro Configuration Profile Variable
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Organization's Client-side Jamf Pro Variables
jamfProVariables="org.churchofjesuschrist.jamfprovariables.plist"
# Property List File
plistFilepath="/Library/Managed Preferences/${jamfProVariables}"
if [[ -e "${plistFilepath}" ]]; then
# Jamf Pro ID
jamfProID=$( defaults read "${plistFilepath}" "Jamf Pro ID" 2>&1 )
# Site Name
jamfProSiteName=$( defaults read "${plistFilepath}" "Site Name" 2>&1 )
fi
- Adjust the various IT Support-related variables for your environment
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# IT Support Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
supportTeamName="IT Support"
supportTeamPhone="+1 (801) 555-1212"
supportTeamEmail="rescue@domain.org"
supportTeamWebsite="https://support.domain.org"
supportTeamHyperlink="[${supportTeamWebsite}](${supportTeamWebsite})"
supportKB="KB8675309"
supportKBURL="[${supportKB}](https://servicenow.domain.org/support?id=kb_article_view&sysparm_article=${supportKB})"
Latest version available onGitHub.
#!/bin/zsh --no-rcs
# shellcheck shell=bash
####################################################################################################
#
# Name: Computer Compliance
#
# Purpose: Provides users a "heads-up display" of critical computer compliance information via swiftDialog
#
# Information: https://snelson.us/2025/04/computer-compliance-0-0-2/
#
# Inspired by:
# - @talkingmoose's [Build a Computer Information script for your Help Desk](https://www.jamf.com/jamf-nation/discussions/29208/build-a-computer-information-script-for-your-help-desk)
#
####################################################################################################
#
# HISTORY
#
# Version 1.0.0, 15-Apr-2025, Dan K. Snelson (@dan-snelson)
# - First "official" release
#
# Version 1.1.0, 17-Apr-2025, Dan K. Snelson (@dan-snelson)
# - Added output of "/usr/libexec/mdmclient AvailableOSUpdates" to $scriptLog
#
# Version 1.2.0, 19-Apr-2025, Dan K. Snelson (@dan-snelson)
# - Added `operationMode` [ test | production ]
#
# Version 1.3.0, 23-Apr-2025, Dan K. Snelson (@dan-snelson)
# - Added sudoers check
#
# Version 1.4.0, 28-Apr-2025, Dan K. Snelson (@dan-snelson)
# - Added `timer` option to swiftDialog
# - Added forcible-quit for all other running dialogs
#
# Version 1.5.0, 29-Apr-2025, Dan K. Snelson (@dan-snelson)
# - Added `jamf recon` as final "check"
# - Improved logging output
#
# Version 1.6.0, 30-Apr-2025, Dan K. Snelson (@dan-snelson)
# - Added countdown progress bar to `quitScript` function (thanks, @samg and @bartreadon!)
#
# Version 1.7.0, 07-May-2025, Dan K. Snelson (@dan-snelson)
# - Updated `checkOS` function to display macOS version and build to user
# - Removed OS version from `infobox`
#
# Version 1.8.0, 17-May-2025, Dan K. Snelson (@dan-snelson)
# - Added "warning" when logged-in user is a member of 'admin'
#
# Version 1.9.0, 10-Jun-2025, Dan K. Snelson (@dan-snelson)
# - Updates for macOS 26
#
####################################################################################################
####################################################################################################
#
# Global Variables
#
####################################################################################################
export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin/
# Script Version
scriptVersion="1.9.0"
# Client-side Log
scriptLog="/var/log/org.churchofjesuschrist.log"
# Elapsed Time
SECONDS="0"
# Operation Mode [ test | production ]
operationMode="${4:-"test"}"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Organization Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Script Human-readabale Name
humanReadableScriptName="Computer Compliance"
# Organization's Script Name
organizationScriptName="CC"
# Organization's Color Scheme
organizationColorScheme="weight=semibold,colour1=#ef9d51,colour2=#ef7951"
# Organization's Kerberos Realm (leave blank to disable check)
kerberosRealm=""
# "Anticipation" Duration (in seconds)
anticipationDuration="2"
# How many previous minor OS versions will be marked as compliant
previousMinorOS="2"
# Allowed percentage of free disk space
allowedFreeDiskPercentage="10"
# Network Quality Test Maximum Age
# Leverages `date -v-`; One of either y, m, w, d, H, M or S
# must be used to specify which part of the date is to be adjusted
networkQualityTestMaximumAge="1H"
# Allowed number of uptime minutes
# - 1 day = 24 hours × 60 minutes/hour = 1,440 minutes
# - 7 days, multiply: 7 × 1,440 minutes = 10,080 minutes
allowedUptimeMinutes="10080"
# Should excessive uptime result in a "warning" or "error" ?
excessiveUptimeAlertStyle="warning
# Completion Timer (in seconds)
completinTimer="60"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Jamf Pro Configuration Profile Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Organization's Client-side Jamf Pro Variables
jamfProVariables="org.churchofjesuschrist.jamfprovariables.plist"
# Property List File
plistFilepath="/Library/Managed Preferences/${jamfProVariables}"
if [[ -e "${plistFilepath}" ]]; then
# Jamf Pro ID
jamfProID=$( defaults read "${plistFilepath}" "Jamf Pro ID" 2>&1 )
# Site Name
jamfProSiteName=$( defaults read "${plistFilepath}" "Site Name" 2>&1 )
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Operating System Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
osVersion=$( sw_vers -productVersion )
osVersionExtra=$( sw_vers -productVersionExtra )
osBuild=$( sw_vers -buildVersion )
osMajorVersion=$( echo "${osVersion}" | awk -F '.' '{print $1}' )
if [[ -n $osVersionExtra ]] && [[ "${osMajorVersion}" -ge 13 ]]; then osVersion="${osVersion} ${osVersionExtra}"; fi
serialNumber=$( ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformSerialNumber/{print $4}' )
computerName=$( scutil --get ComputerName | /usr/bin/sed 's/’//' )
computerModel=$( sysctl -n hw.model )
localHostName=$( scutil --get LocalHostName )
batteryCycleCount=$( ioreg -r -c "AppleSmartBattery" | /usr/bin/grep '"CycleCount" = ' | /usr/bin/awk '{ print $3 }' | /usr/bin/sed s/\"//g )
ssid=$( system_profiler SPAirPortDataType | awk '/Current Network Information:/ { getline; print substr($0, 13, (length($0) - 13)); exit }' )
sshStatus=$( systemsetup -getremotelogin | awk -F ": " '{ print $2 }' )
networkTimeServer=$( systemsetup -getnetworktimeserver )
locationServices=$( defaults read /var/db/locationd/Library/Preferences/ByHost/com.apple.locationd LocationServicesEnabled )
locationServicesStatus=$( [ "${locationServices}" = "1" ] && echo "Enabled" || echo "Disabled" )
sudoStatus=$( visudo -c )
sudoAllLines=$( awk '/\(ALL\)/' /etc/sudoers | tr '\t\n#' ' ' )
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Logged-in User Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
loggedInUser=$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }' )
loggedInUserFullname=$( id -F "${loggedInUser}" )
loggedInUserFirstname=$( echo "$loggedInUserFullname" | sed -E 's/^.*, // ; s/([^ ]*).*/\1/' | sed 's/\(.\{25\}\).*/\1…/' | awk '{print ( $0 == toupper($0) ? toupper(substr($0,1,1))substr(tolower($0),2) : toupper(substr($0,1,1))substr($0,2) )}' )
loggedInUserID=$( id -u "${loggedInUser}" )
loggedInUserGroupMembership=$( id -Gn "${loggedInUser}" )
if [[ ${loggedInUserGroupMembership} == *"admin"* ]]; then localAdminWarning="WARNING: '$loggedInUser' IS A MEMBER OF 'admin'; "; fi
loggedInUserHomeDirectory=$( dscl . read "/Users/${loggedInUser}" NFSHomeDirectory | awk -F ' ' '{print $2}' )
# Kerberos Single Sign-on Extension
if [[ -n "${kerberosRealm}" ]]; then
/usr/bin/su \- "${loggedInUser}" -c "/usr/bin/app-sso -i ${kerberosRealm}" > /var/tmp/app-sso.plist
ssoLoginTest=$( /usr/libexec/PlistBuddy -c "Print:login_date" /var/tmp/app-sso.plist 2>&1 )
if [[ ${ssoLoginTest} == *"Does Not Exist"* ]]; then
kerberosSSOeResult="${loggedInUser} NOT logged in"
else
username=$( /usr/libexec/PlistBuddy -c "Print:upn" /var/tmp/app-sso.plist | awk -F@ '{print $1}' )
kerberosSSOeResult="${username}"
fi
/bin/rm -f /var/tmp/app-sso.plist
fi
# Platform Single Sign-on Extension
pssoeEmail=$( dscl . read /Users/"${loggedInUser}" dsAttrTypeStandard:AltSecurityIdentities 2>/dev/null | awk -F'SSO:' '/PlatformSSO/ {print $2}' )
if [[ -n "${pssoeEmail}" ]]; then
platformSSOeResult="${pssoeEmail}"
else
platformSSOeResult="${loggedInUser} NOT logged in"
fi
# Last modified time of user's Microsoft OneDrive sync file (thanks, @pbowden-msft!)
if [[ -d "${loggedInUserHomeDirectory}/Library/Application Support/OneDrive/settings/Business1/" ]]; then
DataFile=$( ls -t "${loggedInUserHomeDirectory}"/Library/Application\ Support/OneDrive/settings/Business1/*.ini | head -n 1 )
EpochTime=$( stat -f %m "$DataFile" )
UTCDate=$( date -u -r $EpochTime '+%d-%b-%Y' )
oneDriveSyncDate="${UTCDate}"
else
oneDriveSyncDate="Not Configured"
fi
# Time Machine Backup Date
tmDestinationInfo=$( tmutil destinationinfo 2>/dev/null )
if [[ "${tmDestinationInfo}" == *"No destinations configured"* ]]; then
tmStatus="Not configured"
tmLastBackup=""
else
tmDestinations=$( tmutil destinationinfo 2>/dev/null | grep "Name" | awk -F ':' '{print $NF}' | awk '{$1=$1};1')
tmStatus="${tmDestinations//$'\n'/, }"
tmBackupDates=$( tmutil latestbackup 2>/dev/null | awk -F "/" '{print $NF}' | cut -d'.' -f1 )
if [[ -z $tmBackupDates ]]; then
tmLastBackup="Last backup date(s) unknown; connect destination(s)"
else
tmLastBackup="; Date(s): ${tmBackupDates//$'\n'/, }"
fi
fi
####################################################################################################
#
# Networking Variables
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Wi-Fi IP Address
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
networkServices=$( networksetup -listallnetworkservices | grep -v asterisk )
while IFS= read aService
do
activePort=$( /usr/sbin/networksetup -getinfo "$aService" | /usr/bin/grep "IP address" | /usr/bin/grep -v "IPv6" )
if [ "$activePort" != "" ] && [ "$activeServices" != "" ]; then
activeServices="$activeServices\n$aService $activePort"
elif [ "$activePort" != "" ] && [ "$activeServices" = "" ]; then
activeServices="$aService $activePort"
fi
done <<< "$networkServices"
wiFiIpAddress=$( echo "$activeServices" | /usr/bin/sed '/^$/d' | head -n 1)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Palo Alto Networks GlobalProtect VPN IP address
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
globalProtectTest="/Applications/GlobalProtect.app"
if [[ -e "${globalProtectTest}" ]] ; then
interface=$( ifconfig | grep -B1 "10.25" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1 )
if [[ -z "$interface" ]]; then
globalProtectStatus="Inactive"
else
globalProtectIP=$( ifconfig | grep -A2 -E "${interface}" | grep inet | awk '{ print $2 }' )
globalProtectStatus="${globalProtectIP}"
fi
else
globalProtectStatus="GlobalProtect is NOT installed"
fi
####################################################################################################
#
# swiftDialog Variables
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Dialog binary
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# swiftDialog Binary Path
dialogBinary="/usr/local/bin/dialog"
case ${operationMode} in
"test" ) dialogBinary="${dialogBinary} --verbose --resizable --debug red" ;;
esac
# swiftDialog JSON File
dialogJSONFile=$( mktemp -u /var/tmp/dialogJSONFile_${organizationScriptName}.XXXX )
# swiftDialog Command File
dialogCommandFile=$( mktemp /var/tmp/dialogCommandFile_${organizationScriptName}.XXXX )
# Set Permissions on Dialog Command Files
chmod 644 "${dialogCommandFile}"
# The total number of steps for the progress bar, plus two (i.e., "progress: increment")
progressSteps="20"
# Set initial icon based on whether the Mac is a desktop or laptop
if system_profiler SPPowerDataType | grep -q "Battery Power"; then
icon="SF=laptopcomputer.and.arrow.down,${organizationColorScheme}"
else
icon="SF=desktopcomputer.and.arrow.down,${organizationColorScheme}"
fi
# Create `overlayicon` from Self Service's custom icon (thanks, @meschwartz!)
xxd -p -s 260 "$(defaults read /Library/Preferences/com.jamfsoftware.jamf self_service_app_path)"/Icon$'\r'/..namedfork/rsrc | xxd -r -p > /var/tmp/overlayicon.icns
overlayicon="/var/tmp/overlayicon.icns"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# IT Support Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
supportTeamName="IT Support"
supportTeamPhone="+1 (801) 555-1212"
supportTeamEmail="rescue@domain.org"
supportTeamWebsite="https://support.domain.org"
supportTeamHyperlink="[${supportTeamWebsite}](${supportTeamWebsite})"
supportKB="KB8675309"
infobuttonaction="https://servicenow.domain.org/support?id=kb_article_view&sysparm_article=${supportKB}"
supportKBURL="[${supportKB}](${infobuttonaction})"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Help Message Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
helpmessage="For assistance, please contact: **${supportTeamName}**<br>- **Telephone:** ${supportTeamPhone}<br>- **Email:** ${supportTeamEmail}<br>- **Website:** ${supportTeamWebsite}<br>- **Knowledge Base Article:** ${supportKBURL}<br><br>**User Information:**<br>- **Full Name:** ${loggedInUserFullname}<br>- **User Name:** ${loggedInUser}<br>- **User ID:** ${loggedInUserID}<br>- **Location Services:** ${locationServicesStatus}<br>- **Microsoft OneDrive Sync Date:** ${oneDriveSyncDate}<br>- **Platform SSOe:** ${platformSSOeResult}<br><br>**Computer Information:**<br>- **macOS:** ${osVersion} (${osBuild})<br>- **Computer Name:** ${computerName}<br>- **Serial Number:** ${serialNumber}<br>- **Wi-Fi:** ${ssid}<br>- ${wiFiIpAddress}<br>- **VPN IP:** ${globalProtectStatus}<br><br>**Jamf Pro Information:**<br>- **Site:** ${jamfProSiteName}"
helpimage="qr=${infobuttonaction}"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Main Dialog Window
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
dialogJSON='
{
"commandfile" : "'"${dialogCommandFile}"'",
"ontop" : true,
"moveable" : true,
"windowbuttons" : "min",
"quitkey" : "k",
"title" : "'"${humanReadableScriptName} (${scriptVersion})"'",
"icon" : "'"${icon}"'",
"overlayicon" : "'"${overlayicon}"'",
"message" : "none",
"iconsize" : "198.0",
"infobox" : "**User:** '"{userfullname}"'<br><br>**Computer Model:** '"{computermodel}"'<br><br>**Serial Number:** '"{serialnumber}"' ",
"infobuttontext" : "'"${supportKB}"'",
"infobuttonaction" : "'"${infobuttonaction}"'",
"button1text" : "Wait",
"button1disabled" : "true",
"helpmessage" : "'"${helpmessage}"'",
"helpimage" : "'"${helpimage}"'",
"position" : "center",
"progress" : "'"${progressSteps}"'",
"progresstext" : "Please wait …",
"height" : "750",
"width" : "900",
"messagefont" : "size=14",
"titlefont" : "shadow=true, size=24",
"listitem" : [
{"title" : "macOS Version", "subtitle" : "Organizational standards are the current and immediately previous versions of macOS", "icon" : "SF=01.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Available Updates", "subtitle" : "Keep your Mac up-to-date to ensure its security and performance", "icon" : "SF=02.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "System Integrity Protection", "subtitle" : "System Integrity Protection (SIP) in macOS protects the entire system by preventing the execution of unauthorized code.", "icon" : "SF=03.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Firewall", "subtitle" : "The built-in macOS firewall helps protect your Mac from unauthorized access.", "icon" : "SF=04.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "FileVault Encryption", "subtitle" : "FileVault is built-in to macOS and provides full-disk encryption to help prevent unauthorized access to your Mac", "icon" : "SF=05.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Last Reboot", "subtitle" : "Restart your Mac regularly — at least once a week — can help resolve many common issues", "icon" : "SF=06.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Free Disk Space", "subtitle" : "See KB0080685 Disk Usage to help identify the 50 largest directories", "icon" : "SF=07.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "MDM Profile", "subtitle" : "The presence of the Jamf Pro MDM profile helps ensure your Mac is enrolled", "icon" : "SF=08.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "MDM Certficate Expiration", "subtitle" : "Validate the expiration date of the Jamf Pro MDM certficate", "icon" : "SF=09.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Apple Push Notification service", "subtitle" : "Validate communication between Apple, Jamf Pro and your Mac", "icon" : "SF=10.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Jamf Pro Check-In", "subtitle" : "Your Mac should check-in with the Jamf Pro MDM server multiple times each day", "icon" : "SF=11.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Jamf Pro Inventory", "subtitle" : "Your Mac should submit its inventory to the Jamf Pro MDM server daily", "icon" : "SF=12.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "BeyondTrust Privilege Management", "subtitle" : "Privilege Management for Mac pairs powerful least-privilege management and application control", "icon" : "SF=13.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Cisco Umbrella", "subtitle" : "Cisco Umbrella combines multiple security functions so you can extend data protection anywhere.", "icon" : "SF=14.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "CrowdStrike Falcon", "subtitle" : "Technology, intelligence, and expertise come together in CrowdStrike Falcon to deliver security that works.", "icon" : "SF=15.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Palo Alto GlobalProtect", "subtitle" : "Virtual Private Network (VPN) connection to Church headquarters", "icon" : "SF=16.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Network Quality Test", "subtitle" : "Various networking-related tests of your Mac’s Internet connection", "icon" : "SF=17.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"},
{"title" : "Computer Inventory", "subtitle" : "The listing of your Mac’s apps and settings", "icon" : "SF=18.circle.fill,'"${organizationColorScheme}"'", "status" : "pending", "statustext" : "Pending …"}
]
}
'
echo "${dialogJSON}" > "${dialogJSONFile}"
####################################################################################################
#
# Functions
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Client-side Logging
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function updateScriptLog() {
echo "${organizationScriptName} ($scriptVersion): $( date +%Y-%m-%d\ %H:%M:%S ) - ${1}" | tee -a "${scriptLog}"
}
function preFlight() {
updateScriptLog "[PRE-FLIGHT] ${1}"
}
function logComment() {
updateScriptLog " ${1}"
}
function notice() {
updateScriptLog "[NOTICE] ${1}"
}
function info() {
updateScriptLog "[INFO] ${1}"
}
function errorOut(){
updateScriptLog "[ERROR] ${1}"
}
function error() {
updateScriptLog "[ERROR] ${1}"
let errorCount++
}
function warning() {
updateScriptLog "[WARNING] ${1}"
let errorCount++
}
function fatal() {
updateScriptLog "[FATAL ERROR] ${1}"
exit 1
}
function quitOut(){
updateScriptLog "[QUIT] ${1}"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Update the running dialog
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function dialogUpdate(){
sleep 0.3
echo "$1" >> "$dialogCommandFile"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Run command as logged-in user (thanks, @scriptingosx!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function runAsUser() {
info "Run \"$@\" as \"$loggedInUserID\" … "
launchctl asuser "$loggedInUserID" sudo -u "$loggedInUser" "$@"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Parse JSON via osascript and JavaScript
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function get_json_value() {
JSON="$1" osascript -l 'JavaScript' \
-e 'const env = $.NSProcessInfo.processInfo.environment.objectForKey("JSON").js' \
-e "JSON.parse(env).$2"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Quit Script (thanks, @bartreadon!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function quitScript() {
quitOut "Exiting …"
notice "${localAdminWarning}User: ${loggedInUserFullname} (${loggedInUser}) [${loggedInUserID}] ${loggedInUserGroupMembership}; sudo Check: ${sudoStatus}; sudoers: ${sudoAllLines}; Kerberos SSOe: ${kerberosSSOeResult}; Platform SSOe: ${platformSSOeResult}; Location Services: ${locationServicesStatus}; SSH: ${sshStatus}; Microsoft OneDrive Sync Date: ${oneDriveSyncDate}; Time Machine Backup Date: ${tmStatus} ${tmLastBackup}; Battery Cycle Count: ${batteryCycleCount}; Wi-Fi: ${ssid}; ${wiFiIpAddress}; VPN IP: ${globalProtectStatus}; ${networkTimeServer}; Jamf Pro ID: ${jamfProID}; Site: ${jamfProSiteName}"
if [[ -n "${overallCompliance}" ]]; then
dialogUpdate "icon: SF=xmark.circle.fill,weight=bold,colour1=#BB1717,colour2=#F31F1F"
dialogUpdate "title: Computer Non-compliant (as of $( date '+%Y-%m-%d-%H%M%S' ))"
errorOut "${overallCompliance}"
exitCode="1"
else
dialogUpdate "icon: SF=checkmark.circle.fill,weight=bold,colour1=#00ff44,colour2=#075c1e"
dialogUpdate "title: Computer Compliant (as of $( date '+%Y-%m-%d-%H%M%S' ))"
fi
dialogUpdate "progress: 100"
dialogUpdate "progresstext: Elapsed Time: $(printf '%dh:%dm:%ds\n' $((SECONDS/3600)) $((SECONDS%3600/60)) $((SECONDS%60)))"
dialogUpdate "button1text: Close"
dialogUpdate "button1: enable"
sleep "${anticipationDuration}"
# Progress countdown (thanks, @samg and @bartreadon!)
dialogUpdate "progress: reset"
while true; do
if [[ ${completionTimer} -lt ${progressSteps} ]]; then
dialogUpdate "progress: ${completionTimer}"
fi
dialogUpdate "progresstext: Closing automatically in ${completionTimer} seconds …"
sleep 1
((completionTimer--))
if [[ ${completionTimer} -lt 0 ]]; then break; fi;
done
dialogUpdate "quit:"
# Remove the dialog command file
rm -rf "${dialogCommandFile}"
# Remove the dialog JSON file
rm -rf "${dialogJSONFile}"
# Remove overlay icon
rm -rf "${overlayicon}"
# Remove default dialog.log
rm -rf /var/tmp/dialog.log
notice "Elapsed Time: $(printf '%dh:%dm:%ds\n' $((SECONDS/3600)) $((SECONDS%3600/60)) $((SECONDS%60)))"
quitOut "Goodbye!"
exit "${exitCode}"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Kill a specified process (thanks, @grahampugh!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function killProcess() {
process="$1"
if process_pid=$( pgrep -a "${process}" 2>/dev/null ) ; then
info "Attempting to terminate the '$process' process …"
info "(Termination message indicates success.)"
kill "$process_pid" 2> /dev/null
if pgrep -a "$process" >/dev/null ; then
error "'$process' could not be terminated."
fi
else
info "The '$process' process isn’t running."
fi
}
####################################################################################################
#
# Pre-flight Checks
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Client-side Logging
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ ! -f "${scriptLog}" ]]; then
touch "${scriptLog}"
if [[ -f "${scriptLog}" ]]; then
preFlight "Created specified scriptLog: ${scriptLog}"
else
fatal "Unable to create specified scriptLog '${scriptLog}'; exiting.\n\n(Is this script running as 'root' ?)"
fi
else
# preFlight "Specified scriptLog '${scriptLog}' exists; writing log entries to it"
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Logging Preamble
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
preFlight "\n\n###\n# $humanReadableScriptName (${scriptVersion})\n# https://snelson.us/2025/04/computer-compliance-0-0-2/\n###\n"
preFlight "Initiating …"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Computer Information
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
preFlight "${computerName} (S/N ${serialNumber})"
preFlight "${loggedInUserFullname} (${loggedInUser}) [${loggedInUserID}]"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Confirm script is running as root
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ $(id -u) -ne 0 ]]; then
fatal "This script must be run as root; exiting."
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Confirm jamf.log exists
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ ! -f "/private/var/log/jamf.log" ]]; then
fatal "jamf.log missing; exiting."
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Validate / install swiftDialog (Thanks big bunches, @acodega!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function dialogInstall() {
# Get the URL of the latest PKG From the Dialog GitHub repo
dialogURL=$(curl -L --silent --fail "https://api.github.com/repos/swiftDialog/swiftDialog/releases/latest" | awk -F '"' "/browser_download_url/ && /pkg\"/ { print \$4; exit }")
# Expected Team ID of the downloaded PKG
expectedDialogTeamID="PWA5E9TQ59"
preFlight "Installing swiftDialog..."
# Create temporary working directory
workDirectory=$( /usr/bin/basename "$0" )
tempDirectory=$( /usr/bin/mktemp -d "/private/tmp/$workDirectory.XXXXXX" )
# Download the installer package
/usr/bin/curl --location --silent "$dialogURL" -o "$tempDirectory/Dialog.pkg"
# Verify the download
teamID=$(spctl -a -vv -t install "$tempDirectory/Dialog.pkg" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()')
# Install the package if Team ID validates
if [[ "$expectedDialogTeamID" == "$teamID" ]]; then
installer -pkg "$tempDirectory/Dialog.pkg" -target /
sleep 2
dialogVersion=$( /usr/local/bin/dialog --version )
preFlight "swiftDialog version ${dialogVersion} installed; proceeding..."
else
# Display a so-called "simple" dialog if Team ID fails to validate
osascript -e 'display dialog "Please advise your Support Representative of the following error:\r\r• Dialog Team ID verification failed\r\r" with title "Error" buttons {"Close"} with icon caution'
completionActionOption="Quit"
exitCode="1"
quitScript
fi
# Remove the temporary working directory when done
/bin/rm -Rf "$tempDirectory"
}
function dialogCheck() {
# Check for Dialog and install if not found
if [ ! -e "/Library/Application Support/Dialog/Dialog.app" ]; then
preFlight "swiftDialog not found. Installing..."
dialogInstall
else
dialogVersion=$(/usr/local/bin/dialog --version)
if [[ "${dialogVersion}" < "${swiftDialogMinimumRequiredVersion}" ]]; then
preFlight "swiftDialog version ${dialogVersion} found but swiftDialog ${swiftDialogMinimumRequiredVersion} or newer is required; updating..."
dialogInstall
else
preFlight "swiftDialog version ${dialogVersion} found; proceeding..."
fi
fi
}
dialogCheck
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Forcible-quit for all other running dialogs
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
preFlight "Forcible-quit for all other running dialogs …"
killProcess "Dialog"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Complete
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
preFlight "Complete"
####################################################################################################
#
# Compliance Check Functions
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Compliant OS Version (thanks, @robjschroeder!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function checkOS() {
notice "Checking macOS version compatibility..."
dialogUpdate "icon: SF=pencil.and.list.clipboard,${organizationColorScheme}"
dialogUpdate "listitem: index: ${1}, status: wait, statustext: Checking …"
dialogUpdate "progress: increment"
dialogUpdate "progresstext: Comparing installed OS version with compliant version …"
sleep "${anticipationDuration}"
if [[ "${osBuild}" =~ [a-zA-Z]$ ]]; then
logComment "OS Build, ${osBuild}, ends with a letter; skipping"
osResult="Beta macOS ${osVersion} (${osBuild})"
dialogUpdate "listitem: index: ${1}, status: error, statustext: ${osResult}"
warning "${osResult}"
else
# logComment "OS Build, ${osBuild}, ends with a number; proceeding …"
# N-rule variable [How many previous minor OS path versions will be marked as compliant]
n="${previousMinorOS}"
# URL to the online JSON data
online_json_url="https://sofafeed.macadmins.io/v1/macos_data_feed.json"
user_agent="SOFA-Jamf-EA-macOSVersionCheck/1.0"
# local store
json_cache_dir="/private/tmp/sofa"
json_cache="$json_cache_dir/macos_data_feed.json"
etag_cache="$json_cache_dir/macos_data_feed_etag.txt"
# ensure local cache folder exists
/bin/mkdir -p "$json_cache_dir"
# check local vs online using etag
if [[ -f "$etag_cache" && -f "$json_cache" ]]; then
# logComment "e-tag stored, will download only if e-tag doesn’t match"
etag_old=$(/bin/cat "$etag_cache")
/usr/bin/curl --compressed --silent --etag-compare "$etag_cache" --etag-save "$etag_cache" --header "User-Agent: $user_agent" "$online_json_url" --output "$json_cache"
etag_new=$(/bin/cat "$etag_cache")
if [[ "$etag_old" == "$etag_new" ]]; then
# logComment "Cached ETag matched online ETag - cached json file is up to date"
else
# logComment "Cached ETag did not match online ETag, so downloaded new SOFA json file"
fi
else
# logComment "No e-tag cached, proceeding to download SOFA json file"
/usr/bin/curl --compressed --location --max-time 3 --silent --header "User-Agent: $user_agent" "$online_json_url" --etag-save "$etag_cache" --output "$json_cache"
fi
# 1. Get model (DeviceID)
model=$(sysctl -n hw.model)
# logComment "Model Identifier: $model"
# check that the model is virtual or is in the feed at all
if [[ $model == "VirtualMac"* ]]; then
model="Macmini9,1"
elif ! grep -q "$model" "$json_cache"; then
warning "Unsupported Hardware"
# return 1
fi
# 2. Get current system OS
system_version=$( /usr/bin/sw_vers -productVersion )
system_os=$(cut -d. -f1 <<< "$system_version")
# system_version="15.3"
# logComment "System Version: $system_version"
if [[ $system_version == *".0" ]]; then
system_version=${system_version%.0}
logComment "Corrected System Version: $system_version"
fi
# exit if less than macOS 12
if [[ "$system_os" -lt 12 ]]; then
osResult="Unsupported macOS"
result "$osResult"
dialogUpdate "listitem: index: 1, status: fail, statustext: $osResult"
# return 1
fi
# 3. Identify latest compatible major OS
latest_compatible_os=$(/usr/bin/plutil -extract "Models.$model.SupportedOS.0" raw -expect string "$json_cache" | /usr/bin/head -n 1)
# logComment "Latest Compatible macOS: $latest_compatible_os"
# 4. Get OSVersions.Latest.ProductVersion
latest_version_match=false
security_update_within_30_days=false
n_rule=false
for i in {0..3}; do
os_version=$(/usr/bin/plutil -extract "OSVersions.$i.OSVersion" raw "$json_cache" | /usr/bin/head -n 1)
if [[ -z "$os_version" ]]; then
break
fi
latest_product_version=$(/usr/bin/plutil -extract "OSVersions.$i.Latest.ProductVersion" raw "$json_cache" | /usr/bin/head -n 1)
if [[ "$latest_product_version" == "$system_version" ]]; then
latest_version_match=true
break
fi
num_security_releases=$(/usr/bin/plutil -extract "OSVersions.$i.SecurityReleases" raw "$json_cache" | xargs | awk '{ print $1}' )
if [[ -n "$num_security_releases" ]]; then
for ((j=0; j<num_security_releases; j++)); do
security_release_product_version=$(/usr/bin/plutil -extract "OSVersions.$i.SecurityReleases.$j.ProductVersion" raw "$json_cache" | /usr/bin/head -n 1)
if [[ "${system_version}" == "${security_release_product_version}" ]]; then
security_release_date=$(/usr/bin/plutil -extract "OSVersions.$i.SecurityReleases.$j.ReleaseDate" raw "$json_cache" | /usr/bin/head -n 1)
security_release_date_epoch=$(date -jf "%Y-%m-%dT%H:%M:%SZ" "$security_release_date" +%s)
days_ago_30=$(date -v-30d +%s)
if [[ $security_release_date_epoch -ge $days_ago_30 ]]; then
security_update_within_30_days=true
fi
if (( $j <= "$n" )); then
n_rule=true
fi
fi
done
fi
done
if [[ "$latest_version_match" == true ]] || [[ "$security_update_within_30_days" == true ]] || [[ "$n_rule" == true ]]; then
osResult="macOS ${osVersion} (${osBuild})"
dialogUpdate "listitem: index: ${1}, status: success, statustext: ${osResult}"
info "${osResult}"
else
osResult="macOS ${osVersion} (${osBuild})"
dialogUpdate "listitem: index: ${1}, status: fail, statustext: ${osResult}"
errorOut "${osResult}"
overallCompliance+="Failed: ${1}; "
fi
fi
}
🚨 Please go to Part 2