├── AppleSoftwareUpdate.sh ├── JamfDeprecationNotifier.sh ├── OSUpdateNotifier.sh ├── OS_Upgrade.sh ├── ResetPrintSystem.sh ├── TCC.db Modifier.sh ├── Trigger_Validation_For_macOS_Installer.sh ├── com.company.reconafterupdate ├── jamfHelperScreen.sh └── tcc_reset.sh /AppleSoftwareUpdate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # This script is meant to be used with Jamf Pro and makes use of Jamf Helper. 4 | # The idea behind this script is that it alerts the user that there are required OS 5 | # updates that need to be installed. Rather than forcing updates to take place through the 6 | # command line using "softwareupdate", the user is encouraged to use the GUI to update. 7 | # In recent OS versions, Apple has done a poor job of testing command line-based workflows 8 | # of updates and failed to account for scenarios where users may or may not be logged in. 9 | # The update process through the GUI has not suffered from these kind of issues. The 10 | # script will allow end users to postpone/defer updates X amount of times and then will 11 | # give them one last change to postpone. 12 | # This script should work rather reliably going back to 10.12 and maybe further, but at 13 | # this point the real testing has only been done on 10.14. 14 | # Please note, that this script does NOT cache updates in advance. The reason for this is 15 | # that sometimes Apple releases updates that get superseded in a short time frame. 16 | # This can result in downloaded updates that are in the /Library/Updates path that cannot 17 | # be removed in 10.14+ due to System Integrity Protection. 18 | # 19 | # JAMF Pro Script Parameters: 20 | # Parameter 4: Optional. Number of postponements allowed. Default: 3 21 | # Parameter 5: Optional. Number of seconds dialog should remain up. Default: 900 seconds 22 | # Parameter 6: Optional. Number of seconds dialog should remain up for Apple Silicon Macs. 23 | # Provides opportunity for user to perform update via Software Update 24 | # preference pane. Default: 1 hour 25 | # Parameter 7: Optional. Contact email, number, or department name used in messaging. 26 | # Default: IT 27 | # Parameter 8: Optional. Set your own custom icon. Default is Apple Software Update icon. 28 | # 29 | # Here is the expected workflow with this script: 30 | # If no user is logged in, the script will install updates through the command line and 31 | # shutdown/restart as required. 32 | # If a user is logged in and there are updates that require a restart, the user will get 33 | # prompted to update or to postpone. 34 | # If a user is logged in and there are no updates that require a restart, the updates will 35 | # get installed in the background (unless either Safari or iTunes are running.) 36 | # 37 | # There are a few exit codes in this script that may indicate points of failure: 38 | # 11: No power source detected while doing CLI update. 39 | # 12: Software Update failed. 40 | # 13: FV encryption is still in progress. 41 | # 14: Incorrect deferral type used. 42 | # 15: Insufficient space to perform update. 43 | 44 | # Potential feature improvement 45 | # Allow user to postpone to a specific time with a popup menu of available times 46 | 47 | ###### ACTUAL WORKING CODE BELOW ####### 48 | setDeferral (){ 49 | # Notes: PlistBuddy "print" will print stderr to stdout when file is not found. 50 | # File Doesn't Exist, Will Create: /path/to/file.plist 51 | # There is some unused code here with the idea that at some point in the future I can 52 | # extend functionality of this script to support hard and relative dates. 53 | BundleID="${1}" 54 | DeferralType="${2}" 55 | DeferralValue="${3}" 56 | DeferralPlist="${4}" 57 | 58 | if [[ "$DeferralType" == "date" ]]; then 59 | DeferralDate="$(/usr/libexec/PlistBuddy -c "print :"$BundleID":date" "$DeferralPlist" 2>/dev/null)" 60 | # Set deferral date 61 | if [[ -n "$DeferralDate" ]] && [[ ! "$DeferralDate" == *"File Doesn't Exist"* ]]; then 62 | # PlistBuddy command example 63 | # /usr/libexec/PlistBuddy -c "set :"$BundleID":date '07/04/2019 11:21:51 +0000'" "$DeferralPlist" 64 | /usr/libexec/PlistBuddy -c "set :"$BundleID":date $DeferralValue" "$DeferralPlist" 2>/dev/null 65 | else 66 | # PlistBuddy command example 67 | # /usr/libexec/PlistBuddy -c "add :"$BundleID":date date '07/04/2019 11:21:51 +0000'" "$DeferralPlist" 68 | /usr/libexec/PlistBuddy -c "add :"$BundleID":date date $DeferralValue" "$DeferralPlist" 2>/dev/null 69 | fi 70 | elif [[ "$DeferralType" == "count" ]]; then 71 | DeferralCount="$(/usr/libexec/PlistBuddy -c "print :"$BundleID":count" "$DeferralPlist" 2>/dev/null)" 72 | # Set deferral count 73 | if [[ -n "$DeferralCount" ]] && [[ ! "$DeferralCount" == *"File Doesn't Exist"* ]]; then 74 | /usr/libexec/PlistBuddy -c "set :"$BundleID":count $DeferralValue" "$DeferralPlist" 2>/dev/null 75 | else 76 | /usr/libexec/PlistBuddy -c "add :"$BundleID":count integer $DeferralValue" "$DeferralPlist" 2>/dev/null 77 | fi 78 | else 79 | echo "Incorrect deferral type used" 80 | exit 14 81 | fi 82 | } 83 | 84 | # Set path where deferral plist will be placed 85 | DeferralPlistPath="/Library/Application Support/JAMF" 86 | [[ ! -d "$DeferralPlistPath" ]] && /bin/mkdir -p "$DeferralPlistPath" 87 | 88 | OSMajorVersion="$(/usr/bin/sw_vers -productVersion | /usr/bin/cut -d '.' -f 1)" 89 | OSMinorVersion="$(/usr/bin/sw_vers -productVersion | /usr/bin/cut -d '.' -f 2)" 90 | OSPatchVersion="$(/usr/bin/sw_vers -productVersion | /usr/bin/cut -d '.' -f 3)" 91 | ArchType="$(/usr/bin/arch)" 92 | DeferralPlist="$DeferralPlistPath/com.custom.deferrals.plist" 93 | BundleID="com.apple.SoftwareUpdate" 94 | DeferralType="count" 95 | DeferralValue="${4}" 96 | TimeOutinSecForForcedCLI="${5}" 97 | TimeOutinSecForForcedGUI="${6}" 98 | ITContact="${7}" 99 | AppleSUIcon="${8}" 100 | 101 | # Set default values 102 | [[ -z "$DeferralValue" ]] && DeferralValue=3 103 | [[ -z "$TimeOutinSecForForcedCLI" ]] && TimeOutinSecForForcedCLI="900" 104 | [[ -z "$TimeOutinSecForForcedGUI" ]] && TimeOutinSecForForcedGUI="3600" 105 | [[ -z "$ITContact" ]] && ITContact="IT" 106 | 107 | CurrentDeferralValue="$(/usr/libexec/PlistBuddy -c "print :"$BundleID":count" "$DeferralPlist" 2>/dev/null)" 108 | 109 | # Set up the deferral value if it does not exist already 110 | if [[ -z "$CurrentDeferralValue" ]] || [[ "$CurrentDeferralValue" == *"File Doesn't Exist"* ]]; then 111 | setDeferral "$BundleID" "$DeferralType" "$DeferralValue" "$DeferralPlist" 112 | CurrentDeferralValue="$(/usr/libexec/PlistBuddy -c "print :"$BundleID":count" "$DeferralPlist" 2>/dev/null)" 113 | fi 114 | 115 | jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper" 116 | jamf="/usr/local/bin/jamf" 117 | 118 | # Path to temporarily store list of software updates. Avoids having to re-run the softwareupdate command multiple times. 119 | ListOfSoftwareUpdates="/tmp/ListOfSoftwareUpdates" 120 | 121 | # If non-existent path has been supplied, set appropriate Software Update icon depending on OS version 122 | if [[ ! -e "$AppleSUIcon" ]]; then 123 | if [[ "$OSMajorVersion" -ge 11 ]] || [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -gt 13 ]]; then 124 | AppleSUIcon="/System/Library/PreferencePanes/SoftwareUpdate.prefPane/Contents/Resources/SoftwareUpdate.icns" 125 | elif [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -eq 13 ]]; then 126 | AppleSUIcon="/System/Library/CoreServices/Install Command Line Developer Tools.app/Contents/Resources/SoftwareUpdate.icns" 127 | elif [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -ge 8 && "$OSMinorVersion" -le 12 ]]; then 128 | AppleSUIcon="/System/Library/CoreServices/Software Update.app/Contents/Resources/SoftwareUpdate.icns" 129 | elif [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -lt 8 ]]; then 130 | AppleSUIcon="/System/Library/CoreServices/Software Update.app/Contents/Resources/Software Update.icns" 131 | fi 132 | fi 133 | 134 | # Path to the alert caution icon 135 | AlertIcon="/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertStopIcon.icns" 136 | 137 | ## Verbiage For Messages ## 138 | # Message to guide user to Software Update process 139 | if [[ "$OSMajorVersion" -ge 11 ]] || [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -ge 14 ]]; then 140 | #SUGuide="by clicking on the Apple menu, clicking System Preferences and clicking Software Update to install any available updates." 141 | SUGuide="by navigating to: 142 | 143 |  > System Preferences > Software Update" 144 | else 145 | #SUGuide="by opening up the App Store located in the Applications folder and clicking on the Updates tab to install any available updates." 146 | SUGuide="by navigating to: 147 | 148 |  > App Store > Updates tab" 149 | fi 150 | 151 | # Message to let user to contact IT 152 | ContactMsg="There seems to have been an error installing the updates. You can try again $SUGuide 153 | 154 | If the error persists, please contact $ITContact." 155 | 156 | # Message to display when computer is running off battery 157 | NoACPower="The computer is currently running off battery and is not plugged into a power source." 158 | 159 | # Standard Update Message 160 | StandardUpdatePrompt="There is a software update available for your Mac that requires a restart. Please click Continue to proceed to Software Update to run this update. If you are unable to start the process at this time, you may choose to postpone by one day. 161 | 162 | Attempts left to postpone: $CurrentDeferralValue 163 | 164 | You may install macOS software updates at any time $SUGuide" 165 | 166 | # Forced Update Message 167 | ForcedUpdatePrompt="There is a software update available for your Mac that requires you to restart. You have already postponed updates the maximum number of times. 168 | 169 | Please save your work and click 'Update' otherwise this message will disappear and the computer will restart automatically." 170 | 171 | # Forced Update Message for Apple Silicon 172 | ForcedUpdatePromptForAS="There is a software update available for your Mac that requires you to restart. You have already postponed updates the maximum number of times. 173 | 174 | Please save your work and install macOS software updates $SUGuide. 175 | 176 | Failure to complete the update will result in the computer shutting down." 177 | 178 | # Shutdown Warning Message 179 | HUDWarningMessage="Please save your work and quit all other applications. This computer will be shutting down soon." 180 | 181 | # Message shown when running CLI updates 182 | HUDMessage="Please save your work and quit all other applications. macOS software updates are being installed in the background. Do not turn off this computer during this time. 183 | 184 | This message will go away when updates are complete and closing it will not stop the update process. 185 | 186 | If you feel too much time has passed, please contact $ITContact. 187 | 188 | " 189 | #Out of Space Message 190 | NoSpacePrompt="Please clear up some space by deleting files and then attempt to do the update $SUGuide. 191 | 192 | If this error persists, please contact $ITContact." 193 | 194 | ## Functions ## 195 | powerCheck() { 196 | # This is meant to be used when doing CLI update installs. 197 | # Updates through the GUI can already determine its own requirements to proceed with 198 | # the update process. 199 | # Let's wait 5 minutes to see if computer gets plugged into power. 200 | for (( i = 1; i <= 5; ++i )); do 201 | if [[ "$(/usr/bin/pmset -g ps | /usr/bin/grep "Battery Power")" = "Now drawing from 'Battery Power'" ]] && [[ $i = 5 ]]; then 202 | echo "$NoACPower" 203 | elif [[ "$(/usr/bin/pmset -g ps | /usr/bin/grep "Battery Power")" = "Now drawing from 'Battery Power'" ]]; then 204 | /bin/sleep 60 205 | else 206 | return 0 207 | fi 208 | done 209 | exit 11 210 | } 211 | 212 | 213 | updateCLI() { 214 | # Behavior of softwareupdate has changed in Big Sur 215 | # -ia seems to download updates and not actually install them. 216 | # Use -iaR for updates to be installed. 217 | # This also means that the script will restart/shutdown immediately 218 | if [[ "$OSMajorVersion" -ge 11 ]]; then 219 | /usr/sbin/softwareupdate -iaR --verbose 1>> "$ListOfSoftwareUpdates" 2>> "$ListOfSoftwareUpdates" & 220 | else 221 | # Install all software updates 222 | /usr/sbin/softwareupdate -ia --verbose 1>> "$ListOfSoftwareUpdates" 2>> "$ListOfSoftwareUpdates" & 223 | fi 224 | 225 | ## Get the Process ID of the last command run in the background ($!) and wait for it to complete (wait) 226 | # If you don't wait, the computer may take a restart action before updates are finished 227 | SUPID=$(echo "$!") 228 | 229 | wait $SUPID 230 | 231 | SU_EC=$? 232 | 233 | echo $SU_EC 234 | 235 | return $SU_EC 236 | } 237 | 238 | 239 | updateRestartAction() { 240 | # On T2 hardware, we need to shutdown on certain updates 241 | # Verbiage found when installing updates that require a shutdown: 242 | # To install these updates, your computer must shut down. Your computer will automatically start up to finish installation. 243 | # Installation will not complete successfully if you choose to restart your computer instead of shutting down. 244 | # Please call halt(8) or select Shut Down from the Apple menu. To automate the shutdown process with softwareupdate(8), use --restart. 245 | if [[ "$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -E "Please call halt")" || "$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -E "your computer must shut down")" ]] && [[ "$SEPType" ]]; then 246 | if [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -eq 13 && "$OSPatchVersion" -ge 4 ]] || [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -ge 14 ]] || [[ "$OSMajorVersion" -ge 11 ]]; then 247 | # Resetting the deferral count 248 | setDeferral "$BundleID" "$DeferralType" "$DeferralValue" "$DeferralPlist" 249 | 250 | echo "Restart Action: Shutdown/Halt" 251 | 252 | /sbin/shutdown -h now 253 | exit 0 254 | fi 255 | fi 256 | # Resetting the deferral count 257 | setDeferral "$BundleID" "$DeferralType" "$DeferralValue" "$DeferralPlist" 258 | 259 | # If no shutdown is required then let's go ahead and restart 260 | echo "Restart Action: Restart" 261 | 262 | /sbin/shutdown -r now 263 | exit 0 264 | } 265 | 266 | 267 | updateGUI() { 268 | # Update through the GUI 269 | if [[ "$OSMajorVersion" -ge 11 ]] || [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -ge 14 ]]; then 270 | /bin/launchctl $LMethod $LID /usr/bin/open "/System/Library/CoreServices/Software Update.app" 271 | elif [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -ge 8 && "$OSMinorVersion" -le 13 ]]; then 272 | /bin/launchctl $LMethod $LID /usr/bin/open macappstore://showUpdatesPage 273 | fi 274 | } 275 | 276 | 277 | fvStatusCheck() { 278 | # Check to see if the encryption process is complete 279 | FVStatus="$(/usr/bin/fdesetup status)" 280 | if [[ $(/usr/bin/grep -q "Encryption in progress" <<< "$FVStatus") ]]; then 281 | echo "The encryption process is still in progress." 282 | echo "$FVStatus" 283 | exit 13 284 | fi 285 | } 286 | 287 | 288 | runUpdates() { 289 | "$jamfHelper" -windowType hud -lockhud -title "Apple Software Update" -description "$HUDMessage""START TIME: $(/bin/date +"%b %d %Y %T")" -icon "$AppleSUIcon" &>/dev/null & 290 | 291 | ## We'll need the pid of jamfHelper to kill it once the updates are complete 292 | JHPID=$(echo "$!") 293 | 294 | ## Run the command to insall software updates 295 | SU_EC="$(updateCLI)" 296 | 297 | ## Kill the jamfHelper. If a restart is needed, the user will be prompted. If not the hud will just go away 298 | /bin/kill -s KILL "$JHPID" &>/dev/null 299 | 300 | # softwareupdate does not exit with error when insufficient space is detected 301 | # which is why we need to get ahead of that error 302 | if [[ "$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -E "Not enough free disk space")" ]]; then 303 | SpaceError=$(echo "$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -E "Not enough free disk space" | /usr/bin/tail -n 1)") 304 | AvailableFreeSpace=$(/bin/df -g / | /usr/bin/awk '(NR == 2){print $4}') 305 | 306 | echo "$SpaceError" 307 | echo "Disk has $AvailableFreeSpace GB of free space." 308 | 309 | "$jamfHelper" -windowType utility -icon "$AlertIcon" -title "Apple Software Update Error" -description "$SpaceError Your disk has $AvailableFreeSpace GB of free space. $NoSpacePrompt" -button1 "OK" & 310 | return 15 311 | fi 312 | 313 | if [[ "$SU_EC" -eq 0 ]]; then 314 | updateRestartAction 315 | else 316 | echo "/usr/bin/softwareupdate failed. Exit Code: $SU_EC" 317 | 318 | "$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "Apple Software Update" -description "$ContactMsg" -button1 "OK" & 319 | return 12 320 | fi 321 | 322 | exit 0 323 | } 324 | 325 | 326 | # Function to do best effort check if using presentation or web conferencing is active 327 | checkForDisplaySleepAssertions() { 328 | Assertions="$(/usr/bin/pmset -g assertions | /usr/bin/awk '/NoDisplaySleepAssertion | PreventUserIdleDisplaySleep/ && match($0,/\(.+\)/) && ! /coreaudiod/ {gsub(/^\ +/,"",$0); print};')" 329 | 330 | # There are multiple types of power assertions an app can assert. 331 | # These specifically tend to be used when an app wants to try and prevent the OS from going to display sleep. 332 | # Scenarios where an app may not want to have the display going to sleep include, but are not limited to: 333 | # Presentation (KeyNote, PowerPoint) 334 | # Web conference software (Zoom, Webex) 335 | # Screen sharing session 336 | # Apps have to make the assertion and therefore it's possible some apps may not get captured. 337 | # Some assertions can be found here: https://developer.apple.com/documentation/iokit/iopmlib_h/iopmassertiontypes 338 | if [[ "$Assertions" ]]; then 339 | echo "The following display-related power assertions have been detected:" 340 | echo "$Assertions" 341 | echo "Exiting script to avoid disrupting user while these power assertions are active." 342 | 343 | exit 0 344 | fi 345 | } 346 | 347 | 348 | # Function to determine the last activity of softwareupdated 349 | checkSoftwareUpdateDEndTime(){ 350 | # Last activity for softwareupdated 351 | LastSULogEndTime="$(/usr/bin/log stats --process 'softwareupdated' | /usr/bin/awk '/end:/{ gsub(/^end:[ \t]*/, "", $0); print}')" 352 | LastSULogEndTimeInEpoch="$(/bin/date -jf "%a %b %d %T %Y" "$LastSULogEndTime" +"%s")" 353 | 354 | # Add a buffer period of time to last activity end time for softwareupdated 355 | # There can be 2-3 minute gaps of inactivity 356 | # Buffer period = 3 minutes/180 seconds 357 | let LastSULogEndTimeInEpochWithBuffer=$LastSULogEndTimeInEpoch+120 358 | 359 | echo "$LastSULogEndTimeInEpochWithBuffer" 360 | } 361 | 362 | # Store list of software updates in /tmp which gets cleared periodically by the OS and on restarts 363 | /usr/sbin/softwareupdate -l 2>&1 > "$ListOfSoftwareUpdates" 364 | 365 | UpdatesNoRestart=$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -i recommended | /usr/bin/grep -v -i restart | /usr/bin/cut -d , -f 1 | /usr/bin/sed -e 's/^[[:space:]]*//' | /usr/bin/sed -e 's/^Title:\ *//') 366 | RestartRequired=$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -i restart | /usr/bin/grep -v '\*' | /usr/bin/cut -d , -f 1 | /usr/bin/sed -e 's/^[[:space:]]*//' | /usr/bin/sed -e 's/^Title:\ *//') 367 | 368 | # Determine Secure Enclave version 369 | SEPType="$(/usr/sbin/system_profiler SPiBridgeDataType | /usr/bin/awk -F: '/Model Name/ { gsub(/.*: /,""); print $0}')" 370 | 371 | # Determine currently logged in user 372 | LoggedInUser="$(/usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }')" 373 | 374 | # Determine logged in user's UID 375 | LoggedInUserID=$(/usr/bin/id -u "$LoggedInUser") 376 | 377 | # Determine launchctl method we will need to use to launch osascript under user context 378 | if [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -le 9 ]]; then 379 | LID=$(/usr/bin/pgrep -x -u "$LoggedInUserID" loginwindow) 380 | LMethod="bsexec" 381 | else 382 | LID=$LoggedInUserID 383 | LMethod="asuser" 384 | fi 385 | 386 | # Let's make sure FileVault isn't encrypting before proceeding any further 387 | fvStatusCheck 388 | 389 | # If there are no system updates, reset timer and exit script 390 | if [[ "$UpdatesNoRestart" == "" ]] && [[ "$RestartRequired" == "" ]]; then 391 | echo "No updates at this time." 392 | setDeferral "$BundleID" "$DeferralType" "$DeferralValue" "$DeferralPlist" 393 | exit 0 394 | fi 395 | 396 | # If we get to this point, there are updates available. 397 | # If there is no one logged in, let's try to run the updates. 398 | if [[ "$LoggedInUser" == "" ]]; then 399 | powerCheck 400 | updateCLI &>/dev/null 401 | updateRestartAction 402 | else 403 | checkForDisplaySleepAssertions 404 | 405 | # Someone is logged in. Prompt if any updates require a restart ONLY IF the update timer has not reached zero 406 | if [[ "$RestartRequired" != "" ]]; then 407 | if [[ "$CurrentDeferralValue" -gt 0 ]]; then 408 | # Reduce the timer by 1. The script will run again the next day 409 | let CurrTimer=$CurrentDeferralValue-1 410 | setDeferral "$BundleID" "$DeferralType" "$CurrTimer" "$DeferralPlist" 411 | 412 | # If someone is logged in and they have not canceled $DeferralValue times already, prompt them to install updates that require a restart and state how many more times they can press 'cancel' before updates run automatically. 413 | HELPER=$("$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "Apple Software Update" -description "$StandardUpdatePrompt" -button1 "Continue" -button2 "Postpone" -cancelButton "2" -defaultButton 2 -timeout "$TimeOutinSecForForcedCLI") 414 | echo "Jamf Helper Exit Code: $HELPER" 415 | 416 | # If they click "Update" then take them to the software update preference pane 417 | if [ "$HELPER" -eq 0 ]; then 418 | updateGUI 419 | fi 420 | 421 | exit 0 422 | else 423 | powerCheck 424 | # We've reached point where updates need to be forced. 425 | if [[ "$ArchType" == "arm64" ]]; then 426 | # For Apple Silicon Macs, behavior needs to be changed: 427 | # Ask the user to install update through GUI with shutdown warning if not completed within X time 428 | # After X time has passed, check to see if update is in progress. 429 | # If not in progress, force shutdown. 430 | 431 | # Capture start time for forced update via GUI 432 | ForceUpdateStartTimeInEpoch="$(/bin/date -jf "%a %b %d %T %Z %Y" "$(/bin/date)" +"%s")" 433 | 434 | # Calculate scheduled end time for forced update via GUI 435 | let ForceUpdateScheduledEndTimeInEpoch=$ForceUpdateStartTimeInEpoch+$TimeOutinSecForForcedGUI 436 | 437 | # If someone is logged in and they run out of deferrals, prompt them to install updates that require a restart via GUI with warning that shutdown will occur. 438 | HELPER=$("$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "Apple Software Update" -description "$ForcedUpdatePromptForAS" -button1 "Update" -defaultButton 1 -timeout "$TimeOutinSecForForcedGUI" -countdown -alignCountdown "right") 439 | echo "Jamf Helper Exit Code: $HELPER" 440 | 441 | # If they click "Update" then take them to the software update preference pane 442 | if [ "$HELPER" -eq 0 ]; then 443 | updateGUI 444 | fi 445 | 446 | echo "Waiting until time out period for forced GUI install has passed." 447 | 448 | # Wait until the time out period for forced GUI installs has passed 449 | while [[ "$(/bin/date -jf "%a %b %d %T %Z %Y" "$(/bin/date)" +"%s")" -lt "$ForceUpdateScheduledEndTimeInEpoch" ]]; do 450 | sleep 60 451 | done 452 | 453 | echo "Time out period for forced GUI install has passed." 454 | echo "Waiting until softwareupdated is no longer logging any activity." 455 | 456 | # Compare end time of last activity of softwareupdated and if more than buffer period time has passed, proceed with shutdown 457 | while [[ "$(/bin/date -jf "%a %b %d %T %Z %Y" "$(/bin/date)" +"%s")" -lt "$(checkSoftwareUpdateDEndTime)" ]]; do 458 | sleep 15 459 | done 460 | 461 | echo "softwareupdated is no longer logging activity." 462 | 463 | # Let user know shutdown is taking place 464 | "$jamfHelper" -windowType hud -icon "$AppleSUIcon" -title "Apple Software Update" -description "$HUDWarningMessage" -button1 "Shut Down" -defaultButton 1 -timeout "60" -countdown -alignCountdown "right" & 465 | echo "Jamf Helper Exit Code: $HELPER" 466 | 467 | # Shutdown computer 468 | /sbin/shutdown -h now 469 | else 470 | # For Intel Macs, an attempt to continue using CLI to install updates will be made 471 | # If someone is logged in and they run out of deferrals, force install updates that require a restart via CLI 472 | # Prompt users to let them initiate the CLI update via Jamf Helper dialog 473 | HELPER=$("$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "Apple Software Update" -description "$ForcedUpdatePrompt" -button1 "Update" -defaultButton 1 -timeout "$TimeOutinSecForForcedCLI" -countdown -alignCountdown "right") 474 | echo "Jamf Helper Exit Code: $HELPER" 475 | # Either they clicked "Updates" or 476 | # Someone tried to quit jamfHelper or the jamfHelper screen timed out 477 | # The Timer is already 0, run the updates automatically, the end user has been warned! 478 | if [[ "$HELPER" -eq "0" ]] || [[ "$HELPER" -eq "239" ]]; then 479 | runUpdates 480 | RunUpdates_EC=$? 481 | 482 | if [[ $RunUpdates_EC -ne 0 ]]; then 483 | exit $RunUpdates_EC 484 | fi 485 | fi 486 | fi 487 | fi 488 | fi 489 | fi 490 | 491 | # Install updates that do not require a restart 492 | # Future Fix: Might want to see if Safari and iTunes are running as sometimes these apps sometimes do not require a restart but do require that the apps be closed 493 | # A simple stop gap to see if either process is running. 494 | if [[ "$UpdatesNoRestart" != "" ]] && [[ ! "$(/bin/ps -axc | /usr/bin/grep -e Safari$)" ]] && [[ ! "$(/bin/ps -axc | /usr/bin/grep -e iTunes$)" ]]; then 495 | powerCheck 496 | updateCLI &>/dev/null 497 | fi 498 | 499 | exit 0 500 | -------------------------------------------------------------------------------- /JamfDeprecationNotifier.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bp88/JSS-Scripts/d337cdffa8052c65aafabeddaa68a90315b1ffa8/JamfDeprecationNotifier.sh -------------------------------------------------------------------------------- /OSUpdateNotifier.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is meant to be used with Jamf Pro and makes use of Jamf Helper. 4 | # The idea behind this script is that it alerts the user that the OS they are running is 5 | # no longer supported. Rather than forcing updates through, this allows you to control the 6 | # pace of notifications the user receives to perform the OS upgrade. If the user opts to 7 | # proceed with the upgrade, they will get taken to a second policy that you've configured 8 | # to do the OS upgrade. 9 | # 10 | # There are three optional dates that you can supply that will dictate the notifications 11 | # the user receives. 12 | # Start Date: This provides a notification to the user but also allows them to delay when 13 | # they will receive the next reminder. 14 | # Nag Date: This provides a notification to the user. The user cannot select when to be 15 | # reminded. This will be set based on the re-notification period set by the admin. 16 | # End Date: This provides a notification to the user when the update can take place. Once 17 | # this date has been reached, you can set it so that the user has X amount of attempts 18 | # left before they are forced to upgrade. User will receive a notification every 24 hour 19 | # 24 hours until deferrals have been exceeded. 20 | # 21 | # Dates can be supplied in either epoch seconds or string format (e.g. Sep 03 12:34:56 -0400 2019) 22 | # The notifications are meant to get increasingly more forceful. 23 | # Because the dates are optional, you can either supply no dates, some of the 24 | # dates (e.g. Start And End Date only), or all of the dates at your discretion. 25 | # What this means is that the user will only see the notification after that date has been 26 | # reached. If no dates are supplied, the user will simply get the same notification as if 27 | # a start date had been set. 28 | # Each date needs to be greater than the previous; your Start date can't be set to a date 29 | # after the End date. 30 | # 31 | # With each notification, the "More Info" button in the Jamf Helper will always open up a 32 | # URL. This URL should provide the user will instructions on how to proceed to update. 33 | # Because it's a URL, you could also provide a Self Service URL as well if you want with 34 | # the assumption you have a detailed description. 35 | # 36 | # This script does try to account for the fact that it's Self Service-centric. That means 37 | # there are a few checks in place to try and give the user the best chance to perform the 38 | # upgrade: 39 | # -Ensure a user is logged in. 40 | # -Power source is connected 41 | # -No app is opened that has a display sleep assertion active 42 | # -Idle time of the computer 43 | # 44 | # Given the above overview, here are the Jamf script parameters: 45 | # Parameter 4: Optional. Enter "major" or "minor" which will dictate if the script alerts 46 | # against a necessary major OS upgrade or minor OS update. If not set, the default 47 | # will be "minor." 48 | # Parameter 5: Optional. The custom trigger name for a Jamf policy that will initiate the 49 | # major OS upgrade. 50 | # Parameter 6: Required. Provide the policy kicking off this script a custom trigger name 51 | # and supply it here. This is used for situations when the user tries to quit 52 | # Jamf Helper notifications. 53 | # Parameter 7: Optional. The Start Date at which point the logic in this script will 54 | # provide the end-user a notification with the option to set when they will receive the 55 | # next reminder. If not set, the user will start to receive notifications the second 56 | # time the script is run based on the re-notification period. Date can be supplied in 57 | # epoch seconds or string format (e.g. Sep 03 12:34:56 -0400 2019). 58 | # Parameter 8: Optional. The Nag Date at which point the logic in this script will provide 59 | # the end-user a notification which they can dismiss. User cannot select when to be 60 | # reminded as this is determined by the renotification period. 61 | # Parameter 9: Optional. The End Date at which point the logic in this script will provide 62 | # the end-user a notification which they can defer only X amount of times (defaults 63 | # to 3 times). The user will get reminded every 24 hours until those deferrals have 64 | # been exhausted. 65 | # Parameter 10: Optional. The re-notification period before the user will get the 66 | # notification again. This becomes applicable when you're passed the Nag date. Default 67 | # to 60 minutes. 68 | # Parameter 11: Optional. The time out period in seconds which determines how long the 69 | # Jamf Helper notification will stay up. Defaults to 90 minutes. 70 | # 71 | # 72 | # Unfortunately, there are not enough Jamf parameters available to use for all the 73 | # customizations allowed in this script. Due to this limitation, there are other variables 74 | # in this script you can change under the section titled "Variables You Can Modify": 75 | # MaxDeferralAttempts: Required. Determines the number of deferral attempts a user has 76 | # after the end date has been reached. 77 | # MaxIdleTime: Required. Number of seconds in which a computer has been idle for too long 78 | # to expect someone to be sitting in front of it. Script will exit if computer has been 79 | # idle longer than this time. 80 | # MoreInfoURL: Optional. A URL that points the user to a page on the macOS upgrade process. 81 | # If left blank, this will default to Apple's macOS upgrade/update page which have been 82 | # active since September 2016. You can optionally use a Self Service URL. 83 | # DelayOptions: Required. A list of comma separated seconds to provide delay options to 84 | # the user. The seconds will show up in Jamf Helper as time values. 3600 = 1 Hour 85 | # ITContact: Optional. Enter either an email address, phone number, or IT department name 86 | # for the end user to contact your team for assistance. Defaults to "IT" if left blank. 87 | # 88 | # Verbiage: 89 | # I've written the verbiage with the idea that the end user would open up Self Service 90 | # to perform upgrade macOS. Maybe this doesn't work in your environment because you don't 91 | # have a Self Service workflow. Or maybe there's just something else you want to change in 92 | # the verbiage. Below are the variable names so that you can alter the verbiage to your 93 | # liking should you want to. There is a bit of variable logic inside the verbiage so 94 | # modify at your own risk. 95 | # 96 | # Variable Names for the notifications: 97 | # ReminderNotification: The text used when Start date has been reached or if no Start 98 | # date has been supplied. 99 | # NaggingNotification: The text used when Nag date has been reached. 100 | # FinalNotification: The text used when End date has been reached but there are still 101 | # deferrals left. 102 | # FinalCallNotificationForCLIUpdate: The text used during a CLI update when End date has 103 | # been reached with no more deferrals left. 104 | # FinalCallNotificationForGUIUpdate: The text used during a forced GUI update when End 105 | # date has been reached with no more deferrals left. 106 | # ShutdownWarningMessage: The text used just before shutting down. 107 | # BackgroundInstallMessage: The text used when CLI updates are being actively performed. 108 | # 109 | # There are a few exit codes in this script that may indicate points of failure: 110 | # 10: Required parameter has been left empty. 111 | # 11: Make sure Start date < Nag date < End date. 112 | # 12: Insufficient space to perform update. 113 | # 13: /usr/bin/softwareupdate failed. 114 | # 15: Incorrect property list type used. Valid types include: "array" "dict" "string" 115 | # "data" "date" "integer" "real" "bool" 116 | # 16: Invalid date entered. 117 | 118 | ########### Variables You Can Modify ########### 119 | # The number of max deferral attempts a user can make after end date 120 | # If left blank, will default to 3 121 | MaxDeferralAttempts="" 122 | 123 | # Max Idle Time 124 | # If computer has been idle longer than this time, script will exit. 125 | # If left blank, will default to 600 seconds (10 minutes) 126 | MaxIdleTime="" 127 | 128 | # Delay options to show user 129 | # Enter time in seconds with commas separated (e.g "0, 3600, 14400, 86400") 130 | # If left blank, will default to: Now, 1hr, 4hrs, 24hrs 131 | DelayOptions="" 132 | 133 | # Comma separated string of process names to ignore when evaluating display sleep assertions. 134 | # If a process you've listed has a display sleep assertion, the script will resume which may result 135 | # in valid display sleep assertions being ignored. 136 | # E.g. "firefox,Google Chrome,Safari,Microsoft Edge,Opera,Amphetamine,caffeinate" 137 | # If left blank, any Display Sleep assertion will be honored. 138 | AssertionsToIgnore="" 139 | 140 | # URL to provide upgrade instructions 141 | # If left blank, it will default to 142 | # https://www.apple.com/macos/how-to-upgrade/ (for major OS updates) 143 | # https://support.apple.com/HT201541 (for minor OS updates) 144 | MoreInfoURL="" 145 | 146 | # Time out period for CLI installs 147 | # This is useful for situations where you want there to be a shorter time out period than 148 | # normal. If left blank, defaults to the regular default time out period. 149 | TimeOutinSecForForcedCLI="" 150 | 151 | # Contact information for IT 152 | # If left blank, will default to "IT" 153 | ITContact="" 154 | ########### End Of Variables You Can Modify ########### 155 | 156 | ########### Variables For Verbiage You Can Modify ########### 157 | setNotificationMessages(){ 158 | if [[ "$UpdateAction" == "major" ]]; then 159 | addMajorText=" major " 160 | addMajorUpgradeText="upgrade" 161 | if [[ "$CustomTriggerNameForUpgradePolicy" ]]; then 162 | StepsToUpgrade="Finder > Applications > Self Service" 163 | else 164 | StepsToUpgrade=" > App Store > search for macOS" 165 | fi 166 | else 167 | addMajorText=" " 168 | addMajorUpgradeText="update" 169 | if [[ "$OSMajorVersion" -ge 11 ]] || [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -ge 14 ]]; then 170 | #SUGuide="by clicking on the Apple menu, clicking System Preferences and clicking Software Update to install any available updates." 171 | StepsToUpgrade=" > System Preferences > Software Update" 172 | else 173 | #SUGuide="by opening up the App Store located in the Applications folder and clicking on the Updates tab to install any available updates." 174 | StepsToUpgrade=" > App Store > Updates tab" 175 | fi 176 | fi 177 | 178 | # Default Reminder notification 179 | ReminderNotification="There is a${addMajorText}software $addMajorUpgradeText available for your Mac that should be installed at your earliest convenience. If you have any questions, please contact $ITContact. 180 | 181 | You may install this macOS software $addMajorUpgradeText at any time by navigating to: 182 | 183 | $StepsToUpgrade 184 | 185 | Please make a selection and then click Proceed:" 186 | 187 | # Default Nagging notification 188 | NaggingNotification="There is a${addMajorText}software $addMajorUpgradeText available for your Mac that should be installed at your earliest convenience. Click Proceed to ensure your computer is in compliance with the latest security updates. If you have any questions, please contact $ITContact. 189 | 190 | You may install this macOS software $addMajorUpgradeText at any time by navigating to: 191 | 192 | $StepsToUpgrade" 193 | 194 | # Default final notification 195 | FinalNotification="There is a${addMajorText}software $addMajorUpgradeText available for your Mac that should be installed as soon as possible. It is required that you $addMajorUpgradeText this computer as soon as possible, but you can you postpone if necessary. 196 | 197 | Attempts left to postpone: $(($MaxDeferralAttempts - $CurrentDeferralValue)) 198 | 199 | Please save your work and click Proceed to start the $addMajorUpgradeText. You may install this macOS software $addMajorUpgradeText at any time by navigating to: 200 | 201 | $StepsToUpgrade 202 | 203 | If you have any questions or concerns, please contact $ITContact." 204 | 205 | 206 | # Default notification when no deferrals are left for Macs on Intel 207 | FinalCallNotificationForCLIUpdate="There is a${addMajorText}software $addMajorUpgradeText available for your Mac that needs to be installed immediately. There are no opportunities left to postpone this $addMajorUpgradeText any further. 208 | 209 | Please save your work and click Proceed otherwise this message will disappear and the computer will restart automatically. 210 | 211 | If you have any questions or concerns, please contact $ITContact." 212 | 213 | # Default notification when no deferrals are left for Macs on Apple Silicon 214 | FinalCallNotificationForGUIUpdate="There is a${addMajorText}software $addMajorUpgradeText available for your Mac that needs to be installed immediately. There are no opportunities left to postpone this $addMajorUpgradeText any further. 215 | 216 | Please save your work and $addMajorUpgradeText by navigating to: 217 | 218 | $StepsToUpgrade 219 | 220 | Failure to complete the $addMajorUpgradeText will result in this computer shutting down." 221 | 222 | # Shutdown Warning Message 223 | ShutdownWarningMessage="Please save your work and quit all other applications. This computer will be shutting down soon." 224 | 225 | # Message shown when running CLI updates 226 | BackgroundInstallMessage="Please save your work and quit all other applications. macOS is being ${addMajorUpgradeText}d in the background. Do not turn off this computer during this time. 227 | 228 | This message will go away when the $addMajorUpgradeText is complete and closing it will not stop the update process. 229 | 230 | If you feel too much time has passed, please contact $ITContact. 231 | 232 | " 233 | } 234 | 235 | #Out of Space Message 236 | NoSpacePrompt="Please clear up some space by deleting files and then attempt to do the update by navigating to: 237 | 238 | $StepsToUpgrade 239 | 240 | If this error persists, please contact $ITContact." 241 | 242 | ContactMsg="There seems to have been an error installing the updates. You can try again by navigating to: 243 | 244 | $StepsToUpgrade 245 | 246 | If the error persists, please contact $ITContact." 247 | 248 | ########### End Of Variables You Can Modify ########### 249 | 250 | ###### ACTUAL WORKING CODE BELOW ####### 251 | setPlistValue(){ 252 | # Notes: PlistBuddy "print" will print stderr to stdout when file is not found. 253 | # File Doesn't Exist, Will Create: /path/to/file.plist 254 | # There is some unused code here with the idea that at some point in the future I can 255 | # extend functionality of this script to support hard and relative dates. 256 | BundleID="${1}" 257 | Key="${2}" 258 | Type="${3}" 259 | Value="${4}" 260 | Plist="${5}" 261 | UsableTypes=("array" "dict" "string" "data" "date" "integer" "real" "bool") # 8 262 | 263 | # Ensure that a valid property list type was requested 264 | count=1 265 | total=${#UsableTypes[@]} 266 | for i in ${UsableTypes[@]}; do 267 | if [[ "$Type" == "$i" ]]; then 268 | break 269 | fi 270 | ((count++)) 271 | if [[ "$count" > "$total" ]]; then 272 | echo "Incorrect property list type used. Valid types include:" 273 | echo '"array" "dict" "string" "data" "date" "integer" "real" "bool"' 274 | exit 15 275 | fi 276 | done 277 | 278 | # Possible types: https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/PropertyList.html 279 | # Array, String, Data, Date, Integer, Boolean, Dictionary, Floating-point value 280 | # array, string, data, date, integer, bool, dict, real 281 | KeyValueExitCode="$(/usr/libexec/PlistBuddy -c "print :$BundleID:$Key" "$Plist" &>/dev/null ; echo $?)" 282 | 283 | if [[ "$KeyValueExitCode" == 0 ]]; then 284 | /usr/libexec/PlistBuddy -c "set :$BundleID:$Key $Value" "$Plist" 2>/dev/null 285 | else 286 | /usr/libexec/PlistBuddy -c "add :$BundleID:$Key $Type $Value" "$Plist" 2>/dev/null 287 | fi 288 | } 289 | 290 | 291 | readPlistValue(){ 292 | # Notes: PlistBuddy "print" will print an error to stdout when file is not found. 293 | # File Doesn't Exist, Will Create: /path/to/file.plist 294 | # It prints to stderr as well when file is not found: 295 | # Print: Entry, ":key:sub_key1", Does Not Exist 296 | BundleID="${1}" 297 | Key="${2}" 298 | Plist="${3}" 299 | 300 | # KeyValue="$(/usr/libexec/PlistBuddy -c "print :$BundleID:$Key" "$Plist" 2>/dev/null)" 301 | KeyValueExitCode="$(/usr/libexec/PlistBuddy -c "print :$BundleID:$Key" "$Plist" &>/dev/null ; echo $?)" 302 | 303 | if [[ "$KeyValueExitCode" == 0 ]]; then 304 | KeyValue="$(/usr/libexec/PlistBuddy -c "print :$BundleID:$Key" "$Plist" 2>/dev/null)" 305 | echo "$KeyValue" 306 | return 0 307 | fi 308 | 309 | return 1 310 | } 311 | 312 | checkParam(){ 313 | if [[ -z "$1" ]]; then 314 | /bin/echo "\$$2 is empty and required. Please fill in the JSS parameter correctly." 315 | exit 10 316 | fi 317 | } 318 | 319 | validateDate(){ 320 | dateValue="${1}" 321 | paramName="${2}" 322 | 323 | # Check if date is empty 324 | [[ -z "$dateValue" ]] && echo "" && return 0 325 | 326 | # Check if date is an integer 327 | [[ "$dateValue" =~ ^[0-9]+$ ]] && echo "$dateValue" && return 0 328 | 329 | # Check if string can be converted to epoch seconds 330 | [[ "$(/bin/date -jf "%b %d %T %z %Y" "$dateValue" +"%s" &>/dev/null; echo $?)" == 0 ]] && echo "$(/bin/date -jf "%b %d %T %z %Y" "$dateValue" +"%s")" && return 0 331 | 332 | echo "The parameter $paramName has the value $dateValue which is not valid." 333 | echo "Dates need to be in either integer (epoch seconds) or time string format (MMM DD HH:MM:SS TZ YYYY) such as Sep 03 12:34:56 -0400 2019." 334 | exit 16 335 | } 336 | 337 | 338 | # Jamf Parameters 339 | MinSupportedOSVersion="${4}" 340 | UpdateAction="${4}" 341 | CustomTriggerNameForUpgradePolicy="${5}" 342 | CustomTriggerNameDeprecationPolicy="${6}" 343 | StartDate="$(validateDate "${7}" "StartDate")"; [[ "$?" == 15 ]] && exit 16 344 | NagDate="$(validateDate "${8}" "NagDate")"; [[ "$?" == 15 ]] && exit 16 345 | EndDate="$(validateDate "${9}" "EndDate")"; [[ "$?" == 15 ]] && exit 16 346 | RenotifyPeriod="${10}" 347 | TimeOutinSec="${11}" 348 | 349 | # Set the time out for CLI installs to be the same as the default time out period 350 | [ -z "$TimeOutinSecForForcedCLI" ] && TimeOutinSecForForcedCLI="$TimeOutinSec" 351 | 352 | # Set Upgrade Action to "minor" if not set to "major" 353 | UpdateAction=$(echo $UpdateAction | /usr/bin/tr '[:upper:]' '[:lower:]') 354 | [[ -z "$UpdateAction" || "$UpdateAction" != "major" ]] && UpdateAction="minor" 355 | 356 | # Path to Jamf Helper and Jamf binary 357 | jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper" 358 | jamf="/usr/local/bin/jamf" 359 | 360 | # OS version 361 | OSMajorVersion="$(/usr/bin/sw_vers -productVersion | /usr/bin/cut -d '.' -f 1)" 362 | OSMinorVersion="$(/usr/bin/sw_vers -productVersion | /usr/bin/cut -d '.' -f 2)" 363 | OSPatchVersion="$(/usr/bin/sw_vers -productVersion | /usr/bin/cut -d '.' -f 3)" 364 | 365 | # Determine architecture 366 | ArchType="$(/usr/bin/arch)" 367 | 368 | # Path to plist containing history on previous runs of script 369 | DeprecationPlist="/Library/Application Support/JAMF/com.custom.deprecations.plist" 370 | BundleID="com.apple.macOS.softwareupdates" 371 | [[ "$UpdateAction" == "major" ]] && BundleID="com.apple.macOS.upgrade" 372 | 373 | # Path to temporarily store list of software updates. Avoids having to re-run the softwareupdate command multiple times. 374 | ListOfSoftwareUpdates="/tmp/ListOfSoftwareUpdates" 375 | 376 | # Determine currently logged in user 377 | LoggedInUser="$(/usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }')" 378 | 379 | # Determine logged in user's UID 380 | LoggedInUserID=$(/usr/bin/id -u "$LoggedInUser") 381 | 382 | # Determine launchctl method we will need to use to launch osascript under user context 383 | if [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -le 9 ]]; then 384 | LID=$(/usr/bin/pgrep -x -u "$LoggedInUserID" loginwindow) 385 | LMethod="bsexec" 386 | else 387 | LID=$LoggedInUserID 388 | LMethod="asuser" 389 | fi 390 | 391 | # Set appropriate Software Update icon depending on OS version 392 | if [[ "$OSMajorVersion" -ge 11 ]] || [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -gt 13 ]]; then 393 | AppleSUIcon="/System/Library/PreferencePanes/SoftwareUpdate.prefPane/Contents/Resources/SoftwareUpdate.icns" 394 | elif [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -eq 13 ]]; then 395 | AppleSUIcon="/System/Library/CoreServices/Install Command Line Developer Tools.app/Contents/Resources/SoftwareUpdate.icns" 396 | elif [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -ge 8 && "$OSMinorVersion" -le 12 ]]; then 397 | AppleSUIcon="/System/Library/CoreServices/Software Update.app/Contents/Resources/SoftwareUpdate.icns" 398 | elif [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -lt 8 ]]; then 399 | AppleSUIcon="/System/Library/CoreServices/Software Update.app/Contents/Resources/Software Update.icns" 400 | fi 401 | 402 | # Set default values if parameters left empty 403 | [ -z "$RenotifyPeriod" ] && RenotifyPeriod="3600" # 60 mins 404 | [ -z "$TimeOutinSec" ] && TimeOutinSec="5400" # 90 mins 405 | [ -z "$MaxDeferralAttempts" ] && MaxDeferralAttempts="3" 406 | [ -z "$MaxIdleTime" ] && MaxIdleTime="600" # 10 mins 407 | [ -z "$DelayOptions" ] && DelayOptions="0, 3600, 14400, 86400" # Now, 1hr, 4hrs, 24hrs 408 | [ -z "$ITContact" ] && ITContact="IT" 409 | if [[ -z "$MoreInfoURL" ]]; then 410 | if [[ "$UpdateAction" == "major" ]]; then 411 | MoreInfoURL="https://www.apple.com/macos/how-to-upgrade" # Apple macOS Upgrade page 412 | else 413 | MoreInfoURL="https://support.apple.com/HT201541" # Apple macOS Software Update page 414 | fi 415 | fi 416 | 417 | # Set Jamf Helper Title Text 418 | [[ "$UpdateAction" == "major" ]] && JamfHelperTitle="macOS Upgrade" || JamfHelperTitle="macOS Software Update" 419 | 420 | # Dates in String Format for plist output 421 | [ -n "$StartDate" ] && StartDateString="$(date -r $StartDate +"%a %b %d %T %Z %Y")" 422 | [ -n "$NagDate" ] && NagDateString="$(date -r $NagDate +"%a %b %d %T %Z %Y")" 423 | [ -n "$EndDate" ] && EndDateString="$(date -r $EndDate +"%a %b %d %T %Z %Y")" 424 | 425 | # Current time in Epoch seconds 426 | CurrentRunEpochTime="$(/bin/date +"%s")" 427 | 428 | # Current time in string format 429 | CurrentRunTimeString="$(/bin/date -r $CurrentRunEpochTime +"%a %b %d %T %Z %Y")" 430 | 431 | # Last time this script was run 432 | LastRunEpochTime="$(readPlistValue "$BundleID" "LastRunEpochTime" "$DeprecationPlist")" 433 | 434 | # Last time software update check ran 435 | LastUpdateCheckEpochTime="$(readPlistValue "$BundleID" "LastUpdateCheckEpochTime" "$DeprecationPlist")" 436 | [[ -z "$LastUpdateCheckEpochTime" ]] && LastUpdateCheckEpochTime="0" 437 | 438 | # Next Reminder Time. If current time is not later than Reminder Time, we should not proceed. 439 | NextReminderEpochTime="$(readPlistValue "$BundleID" "NextReminderEpochTime" "$DeprecationPlist")" 440 | NextReminderTimeString="$(readPlistValue "$BundleID" "NextReminderTimeString" "$DeprecationPlist")" 441 | 442 | # Number of times notifications have been ignored. 443 | TimesIgnored="$(readPlistValue "$BundleID" "TimesIgnored" "$DeprecationPlist")" 444 | 445 | # Current number of postponements left 446 | CurrentDeferralValue="$(readPlistValue "$BundleID" "Deferral" "$DeprecationPlist")" 447 | [ -z "$CurrentDeferralValue" ] && CurrentDeferralValue="0" 448 | 449 | # The last start time the computer fell into a force update 450 | ForceUpdateStartTimeInEpoch="$(readPlistValue "$BundleID" "ForceUpdateStartTimeInEpoch" "$DeprecationPlist")" 451 | ForceUpdateStartTimeString="$(readPlistValue "$BundleID" "ForceUpdateStartTimeString" "$DeprecationPlist")" 452 | 453 | # Number of times a display sleep assertion has been encountered 454 | DisplaySleepAssertionsEncountered="$(readPlistValue "$BundleID" "DisplaySleepAssertionsEncountered" "$DeprecationPlist")" 455 | 456 | # Check Required Jamf Parameters 457 | checkParam "$CustomTriggerNameDeprecationPolicy" "CustomTriggerNameDeprecationPolicy" 458 | 459 | # Name of process to check against when forcing an upgrade/update 460 | [[ "$UpdateAction" == "major" ]] && ProcessToCheck="osinstallersetupd" || ProcessToCheck="softwareupdated" 461 | 462 | # Instantiate Notification Messages 463 | setNotificationMessages 464 | 465 | 466 | # Function to check if user is logged in 467 | checkForLoggedInUser(){ 468 | if [[ -z "$LoggedInUser" ]]; then 469 | echo "No user logged in." 470 | 471 | # In macOS Big Sur, softwareupdate run from a launch daemon while no user is logged in seems to run 472 | if [[ "$RestartRequired" ]] && [[ "$ArchType" != "arm64" ]] && [[ "$OSMajorVersion" -ge 11 ]]; then 473 | echo "Attempting install of software updates while no user is logged in." 474 | 475 | # Capture value of CLI install of updates 476 | SU_EC="$(updateCLI)" 477 | 478 | if [[ "$SU_EC" -ne 0 ]]; then 479 | echo "Attempt to install software update(s) failed." 480 | echo "/usr/bin/softwareupdate failed. Exit Code: $SU_EC" 481 | fi 482 | 483 | # Refresh software update list now that updates have been installed silently in the background 484 | refreshSoftwareUpdateList 485 | 486 | exit 0 487 | fi 488 | 489 | exit 0 490 | fi 491 | } 492 | 493 | # Function to check for a power connection 494 | checkPower(){ 495 | # This is meant to be used when doing CLI update installs. 496 | # Updates through the GUI can already determine its own requirements to proceed with 497 | # the update process. 498 | # Let's wait 5 minutes to see if computer gets plugged into power. 499 | for (( i = 1; i <= 5; ++i )); do 500 | if [[ "$(/usr/bin/pmset -g ps | /usr/bin/grep "Battery Power")" = "Now drawing from 'Battery Power'" ]]; then 501 | echo "Computer is not currently connected to a power source." 502 | /bin/sleep 60 503 | else 504 | return 0 505 | fi 506 | done 507 | 508 | echo "Exiting script as computer is not connected to power." 509 | exit 0 510 | } 511 | 512 | # Function to get current time 513 | getCurrentTime(){ 514 | # Current time in Epoch seconds 515 | CurrentRunEpochTime="$(/bin/date +"%s")" 516 | 517 | # Current time in string format 518 | CurrentRunTimeString="$(/bin/date -r $CurrentRunEpochTime +"%a %b %d %T %Z %Y")" 519 | } 520 | 521 | # Function to set the next reminder time 522 | setNextReminderTime(){ 523 | TimeChosen="${1}" 524 | 525 | getCurrentTime 526 | 527 | # Record current run time as the last run time 528 | echo "Current Run Time: $CurrentRunTimeString" 529 | setPlistValue "$BundleID" "LastRunEpochTime" "integer" "$CurrentRunEpochTime" "$DeprecationPlist" 530 | setPlistValue "$BundleID" "LastRunTimeString" "string" "$CurrentRunTimeString" "$DeprecationPlist" 531 | 532 | # If no time was chosen, default to reminder period of 1 day 533 | [ -z "$TimeChosen" ] && TimeChosen="86400" # 24 Hours 534 | 535 | # Calculate total of Current time + Time Chosen in Epoch seconds 536 | NextReminderEpochTime="$(/usr/bin/bc -l <<< "$CurrentRunEpochTime + $TimeChosen")" 537 | 538 | # Next Run Time in string format 539 | NextReminderTimeString="$(/bin/date -r $NextReminderEpochTime +"%a %b %d %T %Z %Y")" 540 | echo "User will be reminded after: $NextReminderTimeString" 541 | 542 | # Record Next Reminder Time 543 | setPlistValue "$BundleID" "NextReminderEpochTime" "integer" "$NextReminderEpochTime" "$DeprecationPlist" 544 | setPlistValue "$BundleID" "NextReminderTimeString" "string" "$NextReminderTimeString" "$DeprecationPlist" 545 | } 546 | 547 | incrementTimesIgnored(){ 548 | let TimesIgnored=$TimesIgnored+1 549 | 550 | setPlistValue "$BundleID" "TimesIgnored" "integer" "$TimesIgnored" "$DeprecationPlist" 551 | } 552 | 553 | openMoreInfoURL(){ 554 | # Open More Info URL in web browser 555 | /usr/bin/sudo -u "$LoggedInUser" /usr/bin/open "$MoreInfoURL" 556 | } 557 | 558 | # Function to check for idle time 559 | checkForHIDIdleTime(){ 560 | HIDIdleTime="$(/usr/sbin/ioreg -c IOHIDSystem | /usr/bin/awk '/HIDIdleTime/ {print int($NF/1000000000); exit}')" 561 | 562 | if [[ "$HIDIdleTime" -gt "$MaxIdleTime" ]]; then 563 | echo "Exiting script since the computer has been idle for $HIDIdleTime seconds." 564 | exit 0 565 | fi 566 | } 567 | 568 | # Function to do best effort check if using presentation or web conferencing is active 569 | checkForDisplaySleepAssertions(){ 570 | ActiveAssertions="$(/usr/bin/pmset -g assertions | /usr/bin/awk '/NoDisplaySleepAssertion | PreventUserIdleDisplaySleep/ && match($0,/\(.+\)/) && ! /coreaudiod/ {gsub(/^\ +/,"",$0); print};')" 571 | 572 | # There are multiple types of power assertions an app can assert. 573 | # These specifically tend to be used when an app wants to try and prevent the OS from going to display sleep. 574 | # Scenarios where an app may not want to have the display going to sleep include, but are not limited to: 575 | # Presentation (KeyNote, PowerPoint) 576 | # Web conference software (Zoom, Webex) 577 | # Screen sharing session 578 | # Apps have to make the assertion and therefore it's possible some apps may not get captured. 579 | # Some assertions can be found here: https://developer.apple.com/documentation/iokit/iopmlib_h/iopmassertiontypes 580 | if [[ "$ActiveAssertions" ]]; then 581 | echo "The following display-related power assertions have been detected:" 582 | echo "$ActiveAssertions" 583 | 584 | # Track the number of display sleep assertions encountered 585 | [[ -z "$DisplaySleepAssertionsEncountered" ]] && DisplaySleepAssertionsEncountered="0" 586 | 587 | # Convert comma separated string into an array in zsh 588 | #AssertionsToIgnoreList=("${(@s/,/)AssertionsToIgnore}") 589 | 590 | # Capture current $IFS 591 | OIFS=$IFS 592 | # Set IFS to , 593 | IFS=, 594 | 595 | # Loop through list of processes for assertions to ignore 596 | for Assertion in $AssertionsToIgnore; do 597 | if grep -i -q "$Assertion" <<< "$ActiveAssertions"; then 598 | echo "An ignored display sleep assertion has been detected: $Assertion" 599 | echo "All display sleep assertions are going to be ignored." 600 | return 0 601 | fi 602 | done 603 | 604 | # Reset IFS back 605 | IFS=$OIFS 606 | 607 | # Increment postponement by 1 608 | let DisplaySleepAssertionsEncountered=$DisplaySleepAssertionsEncountered+1 609 | 610 | # Update record of display sleep assertions that have been encountered 611 | setPlistValue "$BundleID" "DisplaySleepAssertionsEncountered" "integer" "$DisplaySleepAssertionsEncountered" "$DeprecationPlist" 612 | 613 | echo "Exiting script to avoid disrupting user while these power assertions are active." 614 | 615 | exit 0 616 | fi 617 | } 618 | 619 | # Function to check if user attempted to quit Jamf Helper 620 | checkAttemptToQuit(){ 621 | Value="${1}" 622 | 623 | # Jamf Helper was exited without making a choice 624 | if [[ "$Value" == "239" ]]; then 625 | echo "Jamf Helper was exited without making a choice." 626 | "$jamf" policy -event "$CustomTriggerNameDeprecationPolicy" & 627 | exit 0 628 | fi 629 | } 630 | 631 | # Function to make sure the Start date < Nag date < End date 632 | compareDates(){ 633 | Msg="Make sure Start date ($StartDateString) < Nag date ($NagDateString) < End date ($EndDateString)." 634 | 635 | # No need to compare if no dates have been specified 636 | [[ -z "$StartDate" && -z "$NagDate" && -z "$EndDate" ]] && return 0 637 | 638 | # No need to compare if 2 out of 3 dates have not been specified 639 | [[ -z "$StartDate" && -z "$NagDate" ]] && return 0 640 | [[ -z "$NagDate" && -z "$EndDate" ]] && return 0 641 | [[ -z "$StartDate" && -z "$EndDate" ]] && return 0 642 | 643 | # Check if Start date < Nag date < End date 644 | [[ "$StartDate" && "$NagDate" && "$EndDate" ]] && [[ "$StartDate" > "$NagDate" ]] && [[ "$StartDate" > "$EndDate" ]] && [[ "$NagDate" > "$EndDate" ]] && echo "$Msg" && exit 11 645 | 646 | # Check if Nag date < End date 647 | [[ "$NagDate" && "$EndDate" && "$NagDate" > "$EndDate" ]] && echo "$Msg" && exit 11 648 | 649 | # Check if Start date < Nag date 650 | [[ "$StartDate" && "$NagDate" && "$StartDate" > "$NagDate" ]] && echo "$Msg" && exit 11 651 | 652 | # Check if Start date < End date 653 | [[ "$StartDate" && "$EndDate" && "$StartDate" > "$EndDate" ]] && echo "$Msg" && exit 11 654 | 655 | return 0 656 | } 657 | 658 | isCurrentTimeLessThanStartDate(){ 659 | # Check if current time is greater than the Start Date 660 | if [[ "$StartDate" && "$CurrentRunEpochTime" -lt "$StartDate" ]]; then 661 | echo "We have not yet reached the Start Date $StartDateString" 662 | exit 0 663 | fi 664 | } 665 | 666 | takeUpgradeAction(){ 667 | # If no custom trigger has been provided, we can only reminder or nag user 668 | if [[ "$CustomTriggerNameForUpgradePolicy" ]]; then 669 | "$jamf" policy -event "$CustomTriggerNameForUpgradePolicy" -randomDelaySeconds 0 670 | else 671 | /usr/bin/open "macappstore://search.itunes.apple.com/WebObjects/MZSearch.woa/wa/search?q=apple%20macos&mt=12" 672 | fi 673 | } 674 | 675 | incrementDeferralValue(){ 676 | # Increment postponement by 1 677 | let CurrentDeferralValue=$CurrentDeferralValue+1 678 | 679 | # Record number of postponements 680 | setPlistValue "$BundleID" "Deferral" "integer" "$CurrentDeferralValue" "$DeprecationPlist" 681 | } 682 | 683 | isBeyondPointOfNoReturn(){ 684 | # Check if we're passed the End Date and no deferral attempts are left 685 | if [[ "$NextReminderEpochTime" && "$EndDate" && "$CurrentRunEpochTime" -ge "$NextReminderEpochTime" && "$CurrentRunEpochTime" -ge "$EndDate" ]] && [[ -z "$NextReminderEpochTime" && "$CurrentDeferralValue" -ge "$MaxDeferralAttempts" ]] || [[ "$EndDate" && "$CurrentRunEpochTime" -ge "$EndDate" ]] && [[ "$CurrentDeferralValue" -ge "$MaxDeferralAttempts" ]]; then 686 | # Check for power 687 | checkPower 688 | if [[ "$UpdateAction" == "major" && "$CustomTriggerNameForUpgradePolicy" ]]; then 689 | # Running OS upgrade policy 690 | "$jamf" policy -event "$CustomTriggerNameForUpgradePolicy" -randomDelaySeconds 0 691 | elif [[ "$UpdateAction" == "major" && -z "$CustomTriggerNameForUpgradePolicy" ]]; then 692 | # Relying on the user to perform OS upgrade manually 693 | forceGUISoftwareUpdate 694 | else 695 | # Forcing user to perform minor OS update 696 | forceGUISoftwareUpdate 697 | fi 698 | 699 | exit 0 700 | fi 701 | } 702 | 703 | isPassEndDate(){ 704 | # If StartDate/NagDate have not been set and CurrentRunEpochTime is less than EndDate, exit 705 | if [[ -z "$StartDate" && -z "$NagDate" && "$EndDate" && "$CurrentRunEpochTime" -lt "$EndDate" ]]; then 706 | echo "Current time is $CurrentRunTimeString. End Date $EndDateString has not been reached." 707 | exit 0 708 | fi 709 | 710 | # Check if we're passed the End Date 711 | if [[ "$NextReminderEpochTime" && "$EndDate" && "$CurrentRunEpochTime" -ge "$NextReminderEpochTime" && "$CurrentRunEpochTime" -ge "$EndDate" ]] || [[ -z "$NextReminderEpochTime" && "$EndDate" && "$CurrentRunEpochTime" -ge "$EndDate" ]]; then 712 | Helper=$("$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "$JamfHelperTitle" -description "$FinalNotification" -button1 "Proceed" -button2 "Postpone" -cancelButton "2" -defaultButton 2 -timeout "$TimeOutinSec") 713 | echo "Jamf Helper Exit Code: $Helper" 714 | 715 | checkAttemptToQuit "$Helper" 716 | 717 | # Increment postponement 718 | incrementDeferralValue 719 | 720 | # Note for potential future improvement: 721 | # It's possible a user may click Update X times in a row and use up all their deferrals in one day. 722 | # To avoid that, count Proceed clicks only after 24 hours after passed since the last time a deferral value was set 723 | 724 | # User clicked More Info 725 | if [[ "$Helper" == 2 ]]; then 726 | # Set next reminder to 24 hour 727 | setNextReminderTime 728 | 729 | incrementTimesIgnored 730 | 731 | # Open More Info URL 732 | openMoreInfoURL 733 | 734 | exit 0 735 | fi 736 | 737 | # Set next reminder to 24 hours to provide user time to update via GUI 738 | setNextReminderTime 739 | 740 | updateGUI 741 | 742 | exit 0 743 | fi 744 | } 745 | 746 | isPassNagDate(){ 747 | # If StartDate has not been set and CurrentRunEpochTime is less than NagDate, exit 748 | if [[ -z "$StartDate" && "$NagDate" && "$CurrentRunEpochTime" -lt "$NagDate" ]]; then 749 | echo "Current time is $CurrentRunTimeString. Nag Date $NagDateString has not been reached." 750 | exit 0 751 | fi 752 | 753 | # Append to the Nagging Notification message if an End Date has been supplied 754 | if [[ "$EndDate" ]]; then 755 | NaggingNotification="$NaggingNotification 756 | 757 | After $EndDateString, an $addMajorUpgradeText will be required on this computer." 758 | fi 759 | 760 | # Check if we're passed the Nagging Date 761 | if [[ "$NextReminderEpochTime" && "$NagDate" && "$CurrentRunEpochTime" -ge "$NextReminderEpochTime" && "$CurrentRunEpochTime" -ge "$NagDate" ]] || [[ -z "$NextReminderEpochTime" && "$NagDate" && "$CurrentRunEpochTime" -ge "$NagDate" ]]; then 762 | Helper=$("$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "$JamfHelperTitle" -description "$NaggingNotification" -button1 "Proceed" -button2 "Postpone" -cancelButton "2" -defaultButton 2 -timeout "$TimeOutinSec") 763 | echo "Jamf Helper Exit Code: $Helper" 764 | 765 | checkAttemptToQuit "$Helper" 766 | 767 | # User clicked More Info 768 | if [[ "$Helper" == 2 ]]; then 769 | openMoreInfoURL 770 | 771 | # Set next reminder to renotify period 772 | setNextReminderTime "$RenotifyPeriod" 773 | exit 0 774 | fi 775 | 776 | # Set next reminder to 1 hour to provide user time to update via GUI 777 | setNextReminderTime "3600" 778 | 779 | # Opening Software Update 780 | updateGUI 781 | 782 | exit 0 783 | fi 784 | } 785 | 786 | isReadyForReminder(){ 787 | # Switch variable to record whether next reminder has been changed 788 | NextReminderJustChanged="false" 789 | 790 | # If no reminder is set, set a reminder and record reminder change in a variable 791 | [[ -z "$NextReminderEpochTime" ]] && setNextReminderTime "$RenotifyPeriod" && NextReminderJustChanged="true" 792 | 793 | # 794 | # [[ "$NextReminderEpochTime" && "$NagDate" && "$CurrentRunEpochTime" -ge "$NextReminderEpochTime" && "$CurrentRunEpochTime" -ge "$NagDate" ]] || 795 | # [[ -z "$NextReminderEpochTime" && "$NagDate" && "$CurrentRunEpochTime" -ge "$NagDate" ]] 796 | 797 | # Provide a reminder notification at this point based on whether the following criteria is met: 798 | # Current time is greater than the next reminder time, or 799 | # We are passed the start date for reminders to get triggered 800 | if [[ "$NextReminderJustChanged" == "false" && "$CurrentRunEpochTime" -ge "$NextReminderEpochTime" ]] || [[ "$NextReminderJustChanged" == "true" && "$StartDate" && "$CurrentRunEpochTime" -ge "$StartDate" ]]; then 801 | Helper="$("$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "$JamfHelperTitle" -description "$ReminderNotification" -showDelayOptions "$DelayOptions" -button1 "Proceed" -button2 "More Info" -cancelButton "2" -defaultButton 1 -timeout "$TimeOutinSec")" 802 | echo "Jamf Helper Exit Code: $Helper" 803 | 804 | TimeChosen="${Helper%?}"; echo "TimeChosen: $TimeChosen" 805 | ButtonClicked="${Helper: -1}"; echo "ButtonClicked: $ButtonClicked" 806 | 807 | # Check if Helper is 239 808 | # Regardless of button pressed, ButtonClicked will always be either "1" or "2" 809 | # if TimeChosen is empty then TimeChosen will be "nil" and it means user opted to start upgrade immediately ("0") 810 | # if TimeChosen is not empty then TimeChosen will be delay value (e.g. "3600, 14400, 86400"). 811 | 812 | checkAttemptToQuit "$Helper" 813 | 814 | # User decided to ask for More Info 815 | if [[ "$ButtonClicked" == "2" ]]; then 816 | echo "User opted to get more info." 817 | openMoreInfoURL 818 | 819 | incrementTimesIgnored 820 | 821 | # Restarting policy now that the user has more info 822 | # Expectation is for user to pick time and click Proceed 823 | # "$jamf" policy -event "$CustomTriggerNameDeprecationPolicy" 824 | # exit 0 825 | fi 826 | 827 | # User decided to proceed with OS update immediately or dialog timed out 828 | if [[ "$ButtonClicked" == "1" ]] && [[ -z "$TimeChosen" ]]; then 829 | echo "User selected to start OS update immediately." 830 | 831 | # Set next reminder time for 1 hour to allow installer to run 832 | setNextReminderTime "3600" 833 | 834 | updateGUI 835 | 836 | exit 0 837 | fi 838 | 839 | # Set next reminder time 840 | setNextReminderTime "$TimeChosen" 841 | fi 842 | } 843 | 844 | updateGUI(){ 845 | # If dealing with major OS upgrades, take different action vs showing Software Update page 846 | if [[ "$UpdateAction" == "major" ]]; then 847 | takeUpgradeAction 848 | else 849 | # Update through the GUI for minor OS updates 850 | if [[ "$OSMajorVersion" -ge 11 ]] || [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -ge 14 ]]; then 851 | /bin/launchctl $LMethod $LID /usr/bin/open "/System/Library/CoreServices/Software Update.app" 852 | elif [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -ge 8 && "$OSMinorVersion" -le 13 ]]; then 853 | /bin/launchctl $LMethod $LID /usr/bin/open macappstore://showUpdatesPage 854 | fi 855 | fi 856 | } 857 | 858 | checkLogEndTime(){ 859 | # Determine whether we are doing a major or minor update 860 | # The process "osinstallersetupd" is monitored when performing major updates 861 | # The process "softwareupdated" is monitored when performing minor updates 862 | 863 | # Last time of activity for softwareupdated/osinstallersetupd 864 | LastLogEndTime="$(/usr/bin/log stats --process "$ProcessToCheck" | /usr/bin/awk '/end:/{ gsub(/^end:[ \t]*/, "", $0); print}')" 865 | LastLogEndTimeInEpoch="$(/bin/date -jf "%a %b %d %T %Y" "$LastLogEndTime" +"%s")" 866 | 867 | # Add a buffer period of time to last activity end time for softwareupdated 868 | # There can be 2-3 minute gaps of inactivity 869 | # Buffer period = 3 minutes/180 seconds 870 | let LastLogEndTimeInEpochWithBuffer=$LastLogEndTimeInEpoch+180 871 | 872 | echo "$LastLogEndTimeInEpochWithBuffer" 873 | } 874 | 875 | refreshSoftwareUpdateList(){ 876 | # Restart the softwareupdate daemon to ensure latest updates are being picked up 877 | /bin/launchctl kickstart -k system/com.apple.softwareupdated 878 | 879 | # Allow a few seconds for daemon to startup 880 | /bin/sleep 3 881 | 882 | # Store list of software updates in /tmp which gets cleared periodically by the OS and on restarts 883 | /usr/sbin/softwareupdate -l 2>&1 > "$ListOfSoftwareUpdates" 884 | 885 | setPlistValue "$BundleID" "LastUpdateCheckEpochTime" "integer" "$CurrentRunEpochTime" "$DeprecationPlist" 886 | 887 | # Variables to capture whether updates require a restart or not 888 | UpdatesNoRestart=$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -i recommended | /usr/bin/grep -v -i restart | /usr/bin/cut -d , -f 1 | /usr/bin/sed -e 's/^[[:space:]]*//' | /usr/bin/sed -e 's/^Title:\ *//') 889 | RestartRequired=$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -i restart | /usr/bin/grep -v '\*' | /usr/bin/cut -d , -f 1 | /usr/bin/sed -e 's/^[[:space:]]*//' | /usr/bin/sed -e 's/^Title:\ *//') 890 | } 891 | 892 | checkForSoftwareUpdates(){ 893 | # To avoid checking for updates at every check-in 894 | # Track the last time an update was checked 895 | # If update was performed within X hours, do not check for updates 896 | 897 | # Exit if script is for major updates 898 | [[ "$UpdateAction" == "major" ]] && return 0 899 | 900 | # Compare the difference between now and the last time the script was run 901 | # If more than 4 hours have passed, check for updates again 902 | let TimeDiff=$CurrentRunEpochTime-$LastUpdateCheckEpochTime 903 | if [[ ! -e "$ListOfSoftwareUpdates" ]] || [[ $TimeDiff -gt 14400 ]] || [[ $LastUpdateCheckEpochTime -eq 0 ]]; then 904 | refreshSoftwareUpdateList 905 | fi 906 | 907 | # Variables to capture whether updates require a restart or not 908 | UpdatesNoRestart=$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -i recommended | /usr/bin/grep -v -i restart | /usr/bin/cut -d , -f 1 | /usr/bin/sed -e 's/^[[:space:]]*//' | /usr/bin/sed -e 's/^Title:\ *//') 909 | RestartRequired=$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -i restart | /usr/bin/grep -v '\*' | /usr/bin/cut -d , -f 1 | /usr/bin/sed -e 's/^[[:space:]]*//' | /usr/bin/sed -e 's/^Title:\ *//') 910 | 911 | # Install updates that do not require a restart 912 | # A simple stop gap to see if either Safari or iTunes process is running. 913 | if [[ "$UpdatesNoRestart" && -z "$RestartRequired" ]]; then 914 | if [[ "$(/bin/ps -axc | /usr/bin/grep -e Safari$)" ]]; then 915 | echo "Safari is running. Will not attempt to install software update that does not require restart." 916 | exit 0 917 | fi 918 | if [[ "$(/bin/ps -axc | /usr/bin/grep -e iTunes$)" ]]; then 919 | echo "iTunes is running. Will not attempt to install software update that does not require restart." 920 | exit 0 921 | fi 922 | 923 | echo "Attempting install of software update that does not require restart." 924 | 925 | # Capture value of CLI install of updates 926 | SU_EC="$(updateCLI)" 927 | 928 | if [[ "$SU_EC" -ne 0 ]]; then 929 | echo "Attempt to install software update(s) that did not require restart failed." 930 | echo "/usr/bin/softwareupdate failed. Exit Code: $SU_EC" 931 | fi 932 | 933 | # Refresh software update list now that updates have been installed silently in the background 934 | refreshSoftwareUpdateList 935 | 936 | exit 0 937 | fi 938 | 939 | # Exit if no updates are available 940 | if [[ -z "$RestartRequired" ]] && [[ -z "$UpdatesNoRestart" ]]; then 941 | echo "No updates available." 942 | 943 | # Reset deferrals to 0 944 | setPlistValue "$BundleID" "Deferral" "integer" "0" "$DeprecationPlist" 945 | 946 | # Reset Display Sleep Assertions Encountered to 0 947 | setPlistValue "$BundleID" "DisplaySleepAssertionsEncountered" "integer" "0" "$DeprecationPlist" 948 | exit 0 949 | fi 950 | } 951 | 952 | forceGUISoftwareUpdate(){ 953 | # This function aims to force an upgrade/update through the GUI 954 | # Ask the user to install update through GUI with shutdown warning if not completed within X time 955 | # After X time has passed, check to see if update is in progress. 956 | # If not in progress, force shutdown. 957 | 958 | # For Apple Silicon Macs, this is necessary since CLI install of updates is not possible 959 | # The same goes for OS upgrades where the user is expected to install macOS upgrade outside of Jamf 960 | 961 | # -Since ForceUpdateScheduledEndTimeInEpoch is based on recorded value 962 | # Make sure the difference between the current time and ForceUpdateScheduledEndTimeInEpoch is greater than 30 minutes (1800 seconds) 963 | # otherwise user will not have enough time to perform GUI update 964 | # -Check against an empty ForceUpdateScheduledEndTimeInEpoch 965 | # Possible reasons it may not exist include: 966 | # ForceUpdateStartTimeInEpoch value was reset 967 | # Script was interrupted and never finished 968 | if [[ -z "$ForceUpdateStartTimeInEpoch" ]] || [[ "$ForceUpdateStartTimeInEpoch" -eq 0 ]] || [[ "$(($((ForceUpdateStartTimeInEpoch+$TimeOutinSec))-$CurrentRunEpochTime))" -lt 1800 ]]; then 969 | # Determine the current start time in epoch seconds for forced update via GUI 970 | ForceUpdateStartTimeInEpoch="$(/bin/date -jf "%a %b %d %T %Z %Y" "$(/bin/date)" +"%s")" 971 | ForceUpdateStartTimeString="$(/bin/date -r $ForceUpdateStartTimeInEpoch +"%a %b %d %T %Z %Y")" 972 | 973 | echo "Start Time For Force Update: $ForceUpdateStartTimeString" 974 | 975 | # Record the start time for forced update via GUI the plist 976 | setPlistValue "$BundleID" "ForceUpdateStartEpochTime" "integer" "$ForceUpdateStartEpochTime" "$DeprecationPlist" 977 | setPlistValue "$BundleID" "ForceUpdateStartTimeString" "integer" "$ForceUpdateStartTimeString" "$DeprecationPlist" 978 | fi 979 | 980 | # If the IT admin provided a time out period less than 1 hour, reset to 1 hour to 981 | # provide user time to perform update/upgrade. 982 | [[ "$TimeOutinSec" -lt 3600 ]] && TimeOutinSec="3600" 983 | 984 | # Calculate scheduled end time for forced update via GUI 985 | let ForceUpdateScheduledEndTimeInEpoch=$ForceUpdateStartTimeInEpoch+$TimeOutinSec 986 | 987 | # If someone is logged in and they run out of deferrals, prompt them to install updates that require a restart via GUI with warning that shutdown will occur. 988 | Helper=$("$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "$JamfHelperTitle" -description "$FinalCallNotificationForGUIUpdate" -button1 "Proceed" -defaultButton 1 -timeout "$TimeOutinSec" -countdown -alignCountdown "right") 989 | echo "Jamf Helper Exit Code: $Helper" 990 | 991 | checkAttemptToQuit "$Helper" 992 | 993 | # If they click "Update" then take them to the software update preference pane 994 | if [ "$Helper" -eq 0 ]; then 995 | updateGUI 996 | fi 997 | 998 | echo "Waiting until time out period for forced GUI install has passed." 999 | 1000 | # Wait until the time out period for forced GUI installs has passed 1001 | while [[ "$(/bin/date +"%s")" -lt "$ForceUpdateScheduledEndTimeInEpoch" ]]; do 1002 | sleep 60 1003 | done 1004 | 1005 | echo "Time out period for forced GUI install has passed." 1006 | 1007 | echo "Waiting until $ProcessToCheck is no longer logging any activity." 1008 | 1009 | # Compare end time of last activity of softwareupdated/osinstallersetupd and if more than buffer period time has passed, proceed with shutdown 1010 | while [[ "$(/bin/date +"%s")" -lt "$(checkLogEndTime)" ]]; do 1011 | sleep 15 1012 | done 1013 | 1014 | echo "softwareupdated is no longer logging activity." 1015 | 1016 | # Set the start time for forced update via GUI the plist to 0 1017 | setPlistValue "$BundleID" "ForceUpdateStartEpochTime" "integer" "0" "$DeprecationPlist" 1018 | 1019 | # Let user know shutdown is taking place 1020 | Helper=$("$jamfHelper" -windowType hud -icon "$AppleSUIcon" -title "Shut Down" -description "$ShutdownWarningMessage" -button1 "Shut Down" -defaultButton 1 -timeout "60" -countdown -alignCountdown "right") 1021 | echo "Jamf Helper Exit Code: $Helper" 1022 | 1023 | # Shutdown computer 1024 | echo "Shutting down computer" 1025 | /sbin/shutdown -h now 1026 | } 1027 | 1028 | forceSoftwareUpdate(){ 1029 | # Determine architecture 1030 | ArchType="$(/usr/bin/arch)" 1031 | 1032 | # We've reached a point where an upgrade/update need to be forced 1033 | if [[ "$ArchType" == "arm64" ]]; then 1034 | # For Apple Silicon, we'll force an update through the GUI 1035 | forceGUISoftwareUpdate 1036 | else 1037 | # For Intel Macs, an attempt to continue using CLI to install updates will be made 1038 | # If someone is logged in and they run out of deferrals, force install updates that require a restart via CLI 1039 | # Prompt users to let them initiate the CLI update via Jamf Helper dialog 1040 | Helper=$("$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "$JamfHelperTitle" -description "$FinalCallNotificationForCLIUpdate" -button1 "Proceed" -defaultButton 1 -timeout "$TimeOutinSecForForcedCLI" -countdown -alignCountdown "right") 1041 | echo "Jamf Helper Exit Code: $Helper" 1042 | # Either they clicked "Updates" or 1043 | # Someone tried to quit jamfHelper or the jamfHelper screen timed out 1044 | # The Timer is already 0, run the updates automatically, the end user has been warned! 1045 | if [[ "$Helper" -eq "0" ]] || [[ "$Helper" -eq "239" ]]; then 1046 | runUpdates 1047 | RunUpdates_EC=$? 1048 | 1049 | if [[ $RunUpdates_EC -ne 0 ]]; then 1050 | exit $RunUpdates_EC 1051 | fi 1052 | fi 1053 | fi 1054 | } 1055 | 1056 | runUpdates(){ 1057 | "$jamfHelper" -windowType hud -lockhud -title "$JamfHelperTitle" -description "$BackgroundInstallMessage""START TIME: $(/bin/date +"%b %d %Y %T")" -icon "$AppleSUIcon" &>/dev/null & 1058 | 1059 | ## We'll need the pid of jamfHelper to kill it once the updates are complete 1060 | JHPID=$(echo "$!") 1061 | 1062 | ## Run the command to insall software updates 1063 | SU_EC="$(updateCLI)" 1064 | 1065 | ## Kill the jamfHelper. If a restart is needed, the user will be prompted. If not the hud will just go away 1066 | /bin/kill -s KILL "$JHPID" &>/dev/null 1067 | 1068 | # softwareupdate does not exit with error when insufficient space is detected 1069 | # which is why we need to get ahead of that error 1070 | if [[ "$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -E "Not enough free disk space")" ]]; then 1071 | SpaceError=$(echo "$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -E "Not enough free disk space" | /usr/bin/tail -n 1)") 1072 | AvailableFreeSpace=$(/bin/df -g / | /usr/bin/awk '(NR == 2){print $4}') 1073 | 1074 | echo "$SpaceError" 1075 | echo "Disk has $AvailableFreeSpace GB of free space." 1076 | 1077 | "$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "$JamfHelperTitle Error" -description "$SpaceError Your disk has $AvailableFreeSpace GB of free space. $NoSpacePrompt" -button1 "OK" & 1078 | return 12 1079 | fi 1080 | 1081 | if [[ "$SU_EC" -eq 0 ]]; then 1082 | updateRestartAction 1083 | else 1084 | echo "/usr/bin/softwareupdate failed. Exit Code: $SU_EC" 1085 | 1086 | "$jamfHelper" -windowType utility -icon "$AppleSUIcon" -title "$JamfHelperTitle" -description "$ContactMsg" -button1 "OK" & 1087 | return 13 1088 | fi 1089 | 1090 | exit 0 1091 | } 1092 | 1093 | 1094 | updateCLI(){ 1095 | # Behavior of softwareupdate has changed in Big Sur 1096 | # -ia seems to download updates and not actually install them. 1097 | # Use -iaR for updates to be installed. 1098 | # This also means that the script will restart/shutdown immediately 1099 | if [[ "$OSMajorVersion" -ge 11 ]]; then 1100 | /usr/sbin/softwareupdate -iaR --verbose 1>> "$ListOfSoftwareUpdates" 2>> "$ListOfSoftwareUpdates" & 1101 | else 1102 | # Install all software updates 1103 | /usr/sbin/softwareupdate -ia --verbose 1>> "$ListOfSoftwareUpdates" 2>> "$ListOfSoftwareUpdates" & 1104 | fi 1105 | 1106 | ## Get the Process ID of the last command run in the background ($!) and wait for it to complete (wait) 1107 | # If you don't wait, the computer may take a restart action before updates are finished 1108 | SUPID=$(echo "$!") 1109 | 1110 | wait $SUPID 1111 | 1112 | SU_EC=$? 1113 | 1114 | echo $SU_EC 1115 | 1116 | return $SU_EC 1117 | } 1118 | 1119 | updateRestartAction(){ 1120 | # On T2 hardware, we need to shutdown on certain updates 1121 | # Verbiage found when installing updates that require a shutdown: 1122 | # To install these updates, your computer must shut down. Your computer will automatically start up to finish installation. 1123 | # Installation will not complete successfully if you choose to restart your computer instead of shutting down. 1124 | # Please call halt(8) or select Shut Down from the Apple menu. To automate the shutdown process with softwareupdate(8), use --restart. 1125 | 1126 | # Determine Secure Enclave version 1127 | SEPType="$(/usr/sbin/system_profiler SPiBridgeDataType | /usr/bin/awk -F: '/Model Name/ { gsub(/.*: /,""); print $0}')" 1128 | 1129 | if [[ "$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -E "Please call halt")" || "$(/bin/cat "$ListOfSoftwareUpdates" | /usr/bin/grep -E "your computer must shut down")" ]] && [[ "$SEPType" ]]; then 1130 | if [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -eq 13 && "$OSPatchVersion" -ge 4 ]] || [[ "$OSMajorVersion" -eq 10 && "$OSMinorVersion" -ge 14 ]] || [[ "$OSMajorVersion" -ge 11 ]]; then 1131 | # Resetting the deferral count 1132 | setPlistValue "$BundleID" "Deferral" "integer" "0" "$DeprecationPlist" 1133 | 1134 | echo "Restart Action: Shutdown/Halt" 1135 | 1136 | /sbin/shutdown -h now 1137 | exit 0 1138 | fi 1139 | fi 1140 | # Resetting the deferral count 1141 | setPlistValue "$BundleID" "Deferral" "integer" "0" "$DeprecationPlist" 1142 | 1143 | # If no shutdown is required then let's go ahead and restart 1144 | echo "Restart Action: Restart" 1145 | 1146 | /sbin/shutdown -r now 1147 | exit 0 1148 | } 1149 | 1150 | 1151 | checkForSoftwareUpdates 1152 | checkForLoggedInUser 1153 | checkForHIDIdleTime 1154 | checkForDisplaySleepAssertions 1155 | compareDates 1156 | isCurrentTimeLessThanStartDate 1157 | isBeyondPointOfNoReturn 1158 | isPassEndDate 1159 | isPassNagDate 1160 | isReadyForReminder 1161 | 1162 | # If we've gotten this far then it is not time to prompt the user yet. 1163 | echo "Current time is $CurrentRunTimeString. User will be reminded after $NextReminderTimeString." 1164 | 1165 | exit 0 -------------------------------------------------------------------------------- /OS_Upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Written by: Balmes Pavlov 4 | # 3/28/17 Edit: Updated for 10.12.4 compatibility. Added an additional exit codes and modified script to take into account which startosinstall command 5 | # to use as Apple has modified the options in 10.12.4. Also added functions to reduce code re-use. 6 | # 7 | # 9/26/17 Edit: Updated for 10.13 compatibility. Cleaned up additional code logic. Added FV2 authenticated restart. 8 | # 9 | # 2/27/19 Edit: Updated for 10.14 compatibility. Added support for additional packages. Added better logging of startosinstall failures by logging to /var/log/installmacos_.log. Removed iCloud checking. 10 | # 11 | # 3/27/19 Edit: Redirected some output that would appear in stdout. Resolved CoreStorage conversion detection. Made it explicit what user account is being asked for FV authentication. 12 | # 13 | # 10/28/19 Edit: Updated for 10.15 compatibility. Fixed an issue where variables were not being recalculated. Compartmentalized script further into functions. Reduced duplicate code where possible. 14 | # Added a few new features: 15 | # Function to quit all active apps (based on code from @dwshore: https://github.com/bp88/JSS-Scripts/pull/4#issue-301247270) 16 | # Function to check for expired certificates in the macOS installer app 17 | # Reduced the number of Jamf parameters required by capturing information from the macOS installer itself 18 | # Cleaned up documentation 19 | # 10/2/20 Edit: Updated for macOS 11 compatibility. Due to some issues, I had to comment out the quit all active apps function and code. 20 | # macOS 11 installer has changed enough that checking for expired certificates is not feasible like it was with prior installers. 21 | # 22 | # This script is meant to be used with Jamf Pro. 23 | # It will make sure of the macOS Sierra, High Sierra, Mojave, or Catalina installer app along with some JSS script parameters and is intended to be somewhat easy to modify if used with future OS deployments. 24 | # Required: Parameter $4 is used to determine the full macOS installer app path. Enter the full path (e.g. /Users/Shared/Installer macOS Sierra.app) 25 | # 26 | # Required: Parameter $5 is for the time estimate in minutes. Type out the time in minutes. 27 | # 28 | # Optional: Parameter $6 is for the custom trigger name of a policy that should be used to deploy the macOS installer app. 29 | # If you do not fill this in and the macOS installer app has not be installed on the computer, the script will exit and warn the user. 30 | # You can opt to not use this parameter, but just make sure to deploy the macOS installer app through other means if that's the route you choose to take. 31 | # 32 | # Optional: Parameter $7 is used if you want to add an additional install package to install after the OS upgrade completes. 33 | # This is done through the "--installpackage" option which was introduced in the macOS High Sierra installer app. 34 | # Read the following blog for more details on the caveats with this option: https://managingosx.wordpress.com/2017/11/17/customized-high-sierra-install-issues-and-workarounds/ 35 | # 36 | # Optional: Parameter $8 is used to determine the minimum macOS installer app version just in case you want a specific version of the installer app on the computer. 37 | # This comes in handy in situations where the computer might have the macOS 10.12.2 installer, but you want the 10.12.3 installer at minimum on the computer. 38 | # Why? Apple may have released a specific feature (e.g. disable iCloud Doc Sync via config profile in 10.12.4+) that is in a newer minor update 39 | # that you want to make use of immediately after computer has been upgraded. 40 | # 41 | # Optional: Parameter $9 is used to provide the contact email, number or name of the IT department for the end user to reach out to should issues arise. 42 | # The phrase will be "Please contact " 43 | # 44 | # 45 | # In case you want to examine why the script may have failed, I've provided exit codes. 46 | # Exit Codes 47 | # 1: Missing JSS parameters that are required. 48 | # 2: Minimum required OS value has been provided and the client's OS version is lower. 49 | # 3: Invalid OS version value. Must be in form of 10.12.4 50 | # 4: No power source connected. 51 | # 5: macOS Installer app is missing "InstallESD.dmg" & "startosinstall". Due to 1) bad path has been provided, 2) app is corrupt and missing two big components, or 3) app is not installed. 52 | # 6: macOS Installer app is missing Info.plist 53 | # 7: Invalid value provided for free disk space. 54 | # 8: The minimum OS version required for the macOS installer app version on the client is greater than the macOS installer on the computer. 55 | # 9: The startosinstall exit code was not 0 or 255 which means there has been a failure in the OS upgrade. See log at: /var/log/installmacos_{timestamp}.log and /var/log/install.log and /var/log/system.log 56 | # 11: Insufficient free space on computer. 57 | # 14: Remote users are logged into computer. Not secure when using FV2 Authenticated Restart. 58 | # 16: FV2 Status is either not Encrypted, in progress or something other than Off. 59 | # 17: Logged in user is not on the list of the FileVault enabled users. 60 | # 18: Password mismatch. User may have forgotten their password. 61 | # 19: FileVault error with fdesetup. Authenticated restart unsuccessful. 62 | # 20: Install package to run post-install during OS upgrade does not have a Product ID. Build distribution package using productbuild. More info: https://managingosx.wordpress.com/2017/11/17/customized-high-sierra-install-issues-and-workarounds/ 63 | # 21: CoreStorage conversion is in the middle of conversion. Only relevant on non-APFS. See results of: diskutil cs info / 64 | # 22: Failed to unmount InstallESD. InstallESD.dmg may be mounted by the installer when it is launched through the GUI. However if you quit the GUI InstallAssistant, the app fails to unmount InstallESD which can cause problems on later upgrade attempts. 65 | # 23: Expired certificate found in macOS installer app 66 | # 24: Expired certificate found in one of packages inside macOS installer's InstallESD.dmg 67 | # 25: Could not determine plist value. Plistbuddy returned an empty value. 68 | # 26: Could not determine OS version in the app installer's base image. 69 | # 27: Could not determine plist value. Plistbuddy is trying to read from a file that does not exist. 70 | # 28: Could not mount SharedSupport.dmg 71 | # 29: Could not read the mobile asset xml from the mounted SharedSupport.dmg 72 | # 30: Could not read the OS version from the base image info.plist. 73 | # 31: Invalid OS version value retrieved from the base image info.plist. 74 | 75 | # Variables to determine paths, OS version, disk space, and power connection. Do not edit. 76 | os_ver="$(/usr/bin/sw_vers -productVersion)" 77 | os_major_ver="$(echo $os_ver | /usr/bin/cut -d . -f 1)" 78 | os_minor_ver="$(echo $os_ver | /usr/bin/cut -d . -f 2)" 79 | os_patch_ver="$(echo $os_ver | /usr/bin/cut -d . -f 3)" 80 | jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper" 81 | jamf="/usr/local/bin/jamf" 82 | power_source=$(/usr/bin/pmset -g ps | /usr/bin/grep "Power") 83 | installmacos_log="/var/log/installmacos" 84 | logged_in_user="$(/usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }')" 85 | 86 | # JSS Script Parameters 87 | mac_os_installer_path="${4}" 88 | time="${5}" 89 | custom_trigger_policy_name="${6}" 90 | add_install_pkg="${7}" 91 | approved_min_os_app_ver="${8}" 92 | it_contact="${9}" 93 | app_name="$(echo "$mac_os_installer_path" | /usr/bin/awk '{ gsub(/.*Install /,""); gsub(/.app/,""); print $0}')" 94 | #mac_os_install_ver="$(/usr/bin/defaults read "$mac_os_installer_path"/Contents/Info.plist CFBundleShortVersionString)" 95 | 96 | # Path to various icons used with JAMF Helper 97 | # Feel free to adjust these icon paths 98 | mas_os_icon="$mac_os_installer_path/Contents/Resources/InstallAssistant.icns" 99 | downloadicon="/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericNetworkIcon.icns" 100 | lowbatteryicon="/System/Library/CoreServices/PowerChime.app/Contents/Resources/battery_icon.png" 101 | alerticon="/System/Library/CoreServices/Problem Reporter.app/Contents/Resources/ProblemReporter.icns" 102 | filevaulticon="/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/FileVaultIcon.icns" 103 | 104 | if [[ "$os_major_ver" -ge 11 ]]; then 105 | driveicon="/System/Library/PreferencePanes/StartupDisk.prefPane/Contents/Resources/StartupDisk.icns" 106 | else 107 | driveicon="/System/Library/PreferencePanes/StartupDisk.prefPane/Contents/Resources/StartupDiskPref.icns" 108 | fi 109 | 110 | # iCloud icons may not be available on OS prior to 10.10. 111 | # White iCloud icon 112 | #icloud_icon="/System/Library/CoreServices/cloudphotosd.app/Contents/Resources/iCloud.icns" 113 | # Blue iCloud icon 114 | #icloud_icon="/System/Library/PrivateFrameworks/CloudDocsDaemon.framework/Versions/A/Resources/iCloud Drive.app/Contents/Resources/iCloudDrive.icns" 115 | 116 | # Alternative icons 117 | # driveicon="/System/Library/Extensions/IOSCSIArchitectureModelFamily.kext/Contents/Resources/USBHD.icns" 118 | # lowbatteryicon="/System/Library/CoreServices/Menu Extras/Battery.menu/Contents/Resources/LowBattery.icns" # Last existed in 10.13 119 | # lowbatteryicon="/System/Library/CoreServices/Installer Progress.app/Contents/Resources/LowBatteryEmpty.tiff" 120 | # lowbatteryicon="/System/Library/PrivateFrameworks/EFILogin.framework/Versions/A/Resources/EFIResourceBuilder.bundle/Contents/Resources/battery_empty@2x.png" 121 | # lowbatteryicon="/System/Library/PrivateFrameworks/EFILogin.framework/Versions/A/Resources/EFIResourceBuilder.bundle/Contents/Resources/battery_dead@2x.png" 122 | 123 | # Messages for JAMF Helper to display 124 | # Feel free to modify to your liking. 125 | # Variables that you should edit. 126 | # You can supply an email address or contact number for your end users to contact you. This will appear in JAMF Helper dialogs. 127 | # If left blank, it will default to just "IT" which may not be as helpful to your end users. 128 | 129 | if [[ -z "$it_contact" ]]; then 130 | it_contact="IT" 131 | fi 132 | 133 | download_in_progress_dialog="$app_name is currently downloading. The installation process will begin once the download is complete. Please close all applications." 134 | no_ac_power="The computer is not plugged into a power source. Please plug it into a power source and start the installation again." 135 | inprogress="The upgrade to $app_name is now in progress. Quit all your applications. The computer will restart automatically and you may be prompted to enter your username and password. Once you have authenticated, you can step away for about $time minutes. Do not shutdown or unplug from power during this process." 136 | download_error="The download of macOS has failed. Installation will not proceed. Please contact $it_contact." 137 | upgrade_error="The installation of macOS has failed. If you haven't already, please try shutting down and powering back your computer, then try again. If failure persists, please contact $it_contact." 138 | bad_os="This version of macOS cannot be upgraded from the current operating system you are running. Please contact $it_contact." 139 | generic_error="An unexpected error has occurred. Please contact $it_contact." 140 | already_upgraded="Your computer is already upgraded to $app_name. If there is a problem or you have questions, please contact $it_contact." 141 | forgot_password="You made too many incorrect password attempts. Please contact $it_contact." 142 | fv_error="An unexpected error with Filevault has occurred. Please contact $it_contact." 143 | fv_proceed="Please wait until your computer has restarted as you may need to authenticate to let the installation proceed." 144 | cs_error="An unexpected error with CoreStorage has occurred. Please contact $it_contact." 145 | 146 | # Function that ensures required variables have been set 147 | # These are variables that if left unset will break the script and/or result in weird dialog messages. 148 | # Function requires parameters $1 and $2. $1 is the variable to check and $2 is the variable name. 149 | checkParam (){ 150 | if [[ -z "$1" ]]; then 151 | echo "\$$2 is empty and required. Please fill in the JSS parameter correctly." 152 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 153 | exit 1 154 | fi 155 | } 156 | 157 | checkPowerSource (){ 158 | # Check for Power Source 159 | if [[ "$power_source" = "Now drawing from 'Battery Power'" ]] || [[ "$power_source" != *"AC Power"* ]]; then 160 | echo "$no_ac_power" 161 | "$jamfHelper" -windowType utility -icon "$lowbatteryicon" -title "Connect to Power Source" -description "$no_ac_power" -button1 "Exit" -defaultButton 1 & 162 | exit 4 163 | else 164 | echo "Power source connected to computer." 165 | fi 166 | } 167 | 168 | # Function that uses Python's LooseVersion comparison module to compare versions 169 | # Source: https://macops.ca/easy-version-comparisons-with-python 170 | # compareLooseVersion (){ 171 | # /usr/bin/python - "$1" "$2" << EOF 172 | # import sys 173 | # from distutils.version import LooseVersion as LV 174 | # print LV(sys.argv[1]) >= LV(sys.argv[2]) 175 | # EOF 176 | # } 177 | 178 | # Function to compare macOS versions which usually come in the form of 10.12 or 10.12.3 179 | compareLooseVersion (){ 180 | first_ver="${1}" 181 | second_ver="${2}" 182 | 183 | first_ver_maj="$(echo $first_ver | /usr/bin/cut -d . -f 1)" 184 | second_ver_maj="$(echo $second_ver | /usr/bin/cut -d . -f 1)" 185 | 186 | first_ver_min="$(echo $first_ver | /usr/bin/cut -d . -f 2)" 187 | [[ -z "$first_ver_min" ]] && first_ver_min="0" 188 | 189 | second_ver_min="$(echo $second_ver | /usr/bin/cut -d . -f 2)" 190 | [[ -z "$second_ver_min" ]] && second_ver_min="0" 191 | 192 | first_ver_patch="$(echo $first_ver | /usr/bin/cut -d . -f 3)" 193 | [[ -z "$first_ver_patch" ]] && first_ver_patch="0" 194 | 195 | second_ver_patch="$(echo $second_ver | /usr/bin/cut -d . -f 3)" 196 | [[ -z "$second_ver_patch" ]] && second_ver_patch="0" 197 | 198 | if [[ $first_ver_maj -gt $second_ver_maj ]] || 199 | [[ $first_ver_maj -eq $second_ver_maj && $first_ver_min -ge $second_ver_min ]] || 200 | [[ $first_ver_maj -eq $second_ver_maj && $first_ver_min -eq $second_ver_min && $first_ver_patch -gt $second_ver_patch ]]; then 201 | echo "True" 202 | return 0 203 | fi 204 | 205 | echo "False" 206 | } 207 | 208 | checkForPlistValue (){ 209 | # Pass only 1 value 210 | # This function is meant to account for non-standard PlistBuddy behavior when sending output to 2>/dev/null 211 | # If path to plist does not exist, output will be: "File Doesn't Exist, Will Create: XXXXXXX" 212 | # If plist key does not exist, output will be empty. 213 | 214 | # Originally this function was meant to exit the script. However with bash, if this 215 | # function is called in a variable as part of a subprocess and an error is encountered 216 | # the script never exists like it's supposed to 217 | # This is why you see a lot of repeat code whenever this function is called. 218 | 219 | # Check to make sure only one value has been passed 220 | if [[ "$#" -ne 1 ]]; then 221 | echo "Pass only 1 value to checkForPlistValue function." 222 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 223 | return 25 224 | fi 225 | 226 | value="${1}" 227 | 228 | if [[ -z "$value" ]]; then 229 | return 25 230 | elif [[ "$value" =~ "File Doesn't Exist, Will Create:" ]]; then 231 | return 27 232 | fi 233 | } 234 | 235 | # Function to initiate prompt for FileVault Authenticated Restart 236 | # Based off code from Elliot Jordan's script: https://github.com/homebysix/jss-filevault-reissue/blob/master/reissue_filevault_recovery_key.sh 237 | fvAuthRestart (){ 238 | # Check for remote users. 239 | REMOTE_USERS=$(/usr/bin/who | /usr/bin/grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | /usr/bin/wc -l) 240 | if [[ $REMOTE_USERS -gt 0 ]]; then 241 | echo "Remote users are logged in. Cannot complete." 242 | "$jamfHelper" -windowType utility -icon "$filevaulticon" -title "FileVault Error" -description "$fv_error" -button1 "Exit" -defaultButton 1 & 243 | exit 14 244 | fi 245 | 246 | # Convert POSIX path of logo icon to Mac path for AppleScript 247 | filevaulticon_as="$(/usr/bin/osascript -e 'tell application "System Events" to return POSIX file "'"$filevaulticon"'" as text')" 248 | 249 | # Most of the code below is based on the JAMF reissueKey.sh script: 250 | # https://github.com/JAMFSupport/FileVault2_Scripts/blob/master/reissueKey.sh 251 | 252 | # Check to see if the encryption process is complete 253 | fv_status="$(/usr/bin/fdesetup status)" 254 | 255 | # Write out FileVault status 256 | echo "FileVault Status: $fv_status" 257 | 258 | if grep -q "Encryption in progress" <<< "$fv_status"; then 259 | echo "The encryption process is still in progress. Cannot do FV2 authenticated restart." 260 | "$jamfHelper" -windowType utility -icon "$filevaulticon" -title "FileVault Error" -description "$fv_error" -button1 "Exit" -defaultButton 1 & 261 | exit 16 262 | elif grep -q "FileVault is Off" <<< "$fv_status"; then 263 | echo "Encryption is not active. Cannot do FV2 authenticated restart. Proceeding with script." 264 | return 0 265 | elif ! grep -q "FileVault is On" <<< "$fv_status"; then 266 | echo "Unable to determine encryption status. Cannot do FV2 authenticated restart." 267 | "$jamfHelper" -windowType utility -icon "$filevaulticon" -title "FileVault Error" -description "$fv_error" -button1 "Exit" -defaultButton 1 & 268 | exit 16 269 | fi 270 | 271 | # Check if user is logged in 272 | if [[ -z "$logged_in_user" ]]; then 273 | echo "No user is logged in. Cannot do FV2 authenticated restart. Proceeding with script." 274 | return 0 275 | fi 276 | 277 | # Check if the logged in account is an authorized FileVault 2 user 278 | fv_users="$(/usr/bin/fdesetup list)" 279 | if ! /usr/bin/egrep -q "^${logged_in_user}," <<< "$fv_users"; then 280 | echo "$logged_in_user is not on the list of FileVault enabled users:" 281 | echo "$fv_users" 282 | "$jamfHelper" -windowType utility -icon "$filevaulticon" -title "FileVault Error" -description "$fv_error" -button1 "Exit" -defaultButton 1 -timeout 60 & 283 | exit 17 284 | fi 285 | 286 | # FileVault authenticated restart was introduced in 10.8.3 287 | # However prior to 10.12 it does not let you provide a value of "-1" to the option -delayminutes "-1" 288 | if [[ "$(/usr/bin/fdesetup supportsauthrestart)" != "true" ]] || [[ "$os_major_ver" -eq 10 && "$os_minor_ver" -lt 12 ]]; then 289 | echo "Either FileVault authenticated restart is not supported on this Mac or the OS is older than 10.12. Skipping FV authenticated restart." 290 | echo "User may need to authenticate on reboot. Letting them know via JamfHelper prompt." 291 | "$jamfHelper" -windowType utility -icon "$filevaulticon" -title "FileVault Notice" -description "$fv_proceed" -button1 "Continue" -defaultButton 1 -timeout 60 & 292 | return 1 293 | fi 294 | 295 | ################################ MAIN PROCESS ################################# 296 | 297 | # Get information necessary to display messages in the current user's context. 298 | user_id=$(/usr/bin/id -u "$logged_in_user") 299 | if [[ "$os_major_ver" -eq 10 && "$os_minor_ver" -le 9 ]]; then 300 | l_id=$(/usr/bin/pgrep -x -u "$user_id" loginwindow) 301 | l_method="bsexec" 302 | elif [[ "$os_major_ver" -ge 11 || "$os_major_ver" -eq 10 && "$os_minor_ver" -gt 9 ]]; then 303 | l_id="$user_id" 304 | l_method="asuser" 305 | fi 306 | 307 | # Get the logged in user's password via a prompt. 308 | echo "Prompting $logged_in_user for their Mac password..." 309 | #user_pw="$(/bin/launchctl "$l_method" "$l_id" /usr/bin/osascript -e 'display dialog "Please enter the password for the account you use to log in to your Mac:" default answer "" with title "FileVault Authentication Restart" giving up after 86400 with text buttons {"OK"} default button 1 with hidden answer with icon file "'"${filevaulticon_as//\"/\\\"}"'"' -e 'return text returned of result')" 310 | 311 | user_pw="$(/bin/launchctl "$l_method" "$l_id" /usr/bin/osascript << EOF 312 | return text returned of (display dialog "Please enter the password for the account \"$logged_in_user\" you use to log in to your Mac:" default answer "" with title "FileVault Authentication Restart" giving up after 86400 with text buttons {"OK"} default button 1 with hidden answer with icon file "${filevaulticon_as}") 313 | EOF 314 | )" 315 | # Thanks to James Barclay (@futureimperfect) for this password validation loop. 316 | TRY=1 317 | until /usr/bin/dscl /Search -authonly "$logged_in_user" "$user_pw" &>/dev/null; do 318 | (( TRY++ )) 319 | echo "Prompting $logged_in_user for their Mac password (attempt $TRY)..." 320 | user_pw="$(/bin/launchctl "$l_method" "$l_id" /usr/bin/osascript -e 'display dialog "Sorry, that password was incorrect. Please try again:" default answer "" with title "FileVault Authentication Restart" giving up after 86400 with text buttons {"OK"} default button 1 with hidden answer with icon file "'"${filevaulticon_as//\"/\\\"}"'"' -e 'return text returned of result')" 321 | if (( TRY >= 5 )); then 322 | echo "Password prompt unsuccessful after 5 attempts." 323 | "$jamfHelper" -windowType utility -icon "$filevaulticon" -title "FileVault Authentication Error" -description "$forgot_password" -button1 "Exit" -defaultButton 1 & 324 | exit 18 325 | fi 326 | done 327 | echo "Successfully prompted for Filevault password." 328 | 329 | # Translate XML reserved characters to XML friendly representations. 330 | user_pw_xml=${user_pw//&/&} 331 | user_pw_xml=${user_pw_xml///>} 333 | user_pw_xml=${user_pw_xml//\"/"} 334 | user_pw_xml=${user_pw_xml//\'/'} 335 | 336 | echo "Setting up authenticated restart..." 337 | FDESETUP_OUTPUT="$(/usr/bin/fdesetup authrestart -delayminutes -1 -verbose -inputplist << EOF 338 | 339 | 340 | 341 | 342 | Username 343 | $logged_in_user 344 | Password 345 | $user_pw_xml 346 | 347 | 348 | EOF 349 | )" 350 | 351 | # Test success conditions. 352 | fdesetup_result=$? 353 | 354 | # Clear password variable. 355 | unset user_pw_xml 356 | 357 | if [[ $fdesetup_result -ne 0 ]]; then 358 | echo "$FDESETUP_OUTPUT" 359 | echo "[WARNING] fdesetup exited with return code: $fdesetup_result." 360 | echo "See this page for a list of fdesetup exit codes and their meaning:" 361 | echo "https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man8/fdesetup.8.html" 362 | exit 19 363 | else 364 | echo "$FDESETUP_OUTPUT" 365 | echo "Computer will do a FileVault 2 authenticated restart." 366 | fi 367 | 368 | return 0 369 | } 370 | 371 | # Function to download macOS installer 372 | downloadOSInstaller (){ 373 | # Need to do an inventory update to make sure Jamf can put the computer in scope to initiate the download policy again 374 | "$jamf" recon 375 | 376 | if [[ -n "$custom_trigger_policy_name" ]]; then 377 | "$jamfHelper" -windowType hud -lockhud -title "$app_name (1 of 2)" -description "$download_in_progress_dialog" -icon "$downloadicon" & 378 | JHPID=$(echo "$!") 379 | 380 | "$jamf" policy -event "$custom_trigger_policy_name" -randomDelaySeconds 0 381 | 382 | if [[ $? -ne 0 ]]; then 383 | /bin/kill -s KILL "$JHPID" > /dev/null 1>&2 384 | echo "Jamf policy did not complete successfully." 385 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$download_error" -button1 "Exit" -defaultButton 1 & 386 | exit 25 387 | fi 388 | 389 | /bin/kill -s KILL "$JHPID" > /dev/null 1>&2 390 | else 391 | echo "Warning: Custom Trigger field to download macOS installer is empty. Cannot download macOS installer. Please ensure macOS installer app is already installed on the client." 392 | fi 393 | 394 | return 0 395 | } 396 | 397 | redownloadOSInstaller (){ 398 | echo "Clearing JAMF Downloads/Waiting Room in case there's a bad download and trying again." 399 | /bin/rm -rf "/Library/Application Support/JAMF/Downloads/" 400 | /bin/rm -rf "/Library/Application Support/JAMF/Waiting Room/" 401 | 402 | echo "Clearing $$mac_os_installer_path in case application path is incomplete." 403 | /bin/rm -rf "$mac_os_installer_path" 404 | 405 | downloadOSInstaller 406 | 407 | return $? 408 | } 409 | 410 | checkOSInstallerVersion (){ 411 | installer_app_version="$(/usr/libexec/PlistBuddy -c "print :CFBundleShortVersionString" "$mac_os_installer_path"/Contents/Info.plist 2>/dev/null)" 412 | 413 | # Confirm that value returned by plistbuddy is valid 414 | checkForPlistValue "$installer_app_version" 415 | 416 | if [[ $? -eq 25 ]]; then 417 | echo "Plistbuddy returned an empty value." 418 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 419 | exit 25 420 | elif [[ $? -eq 27 ]]; then 421 | echo "Plistbuddy is trying to read from a file that does not exist." 422 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 423 | exit 27 424 | fi 425 | 426 | if [[ -z "$installer_app_version" ]]; then 427 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Download Failure" -description "$download_error" -button1 "Exit" -defaultButton 1 & 428 | exit 6 429 | fi 430 | 431 | echo "$installer_app_version" 432 | } 433 | 434 | 435 | # Function to check macOS installer app has been installed and if not re-download and do a comparison check between OS installer app version required 436 | checkOSInstaller (){ 437 | count=0 438 | 439 | # Loop through basic check to confirm full installer is on device 440 | # If installer components are missing, attempt to download installer again 441 | for i in {1..2}; do 442 | # For macOS 11, if the SharedSupport.dmg and startosinstall binary are not available chances are the installer is no good. 443 | if [[ ! -e "$mac_os_installer_path/Contents/SharedSupport/SharedSupport.dmg" ]] || [[ ! -e "$mac_os_installer_path/Contents/Resources/startosinstall" ]]; then 444 | ((count++)) 445 | fi 446 | # For macOS 10.15 or lower, if the InstallESD.dmg and startosinstall binary are not available chances are the installer is no good. 447 | if [[ ! -e "$mac_os_installer_path/Contents/SharedSupport/InstallESD.dmg" ]] || [[ ! -e "$mac_os_installer_path/Contents/Resources/startosinstall" ]]; then 448 | ((count++)) 449 | fi 450 | if [[ $count -eq $((2*$i)) ]]; then 451 | redownloadOSInstaller 452 | fi 453 | done 454 | 455 | if [[ $count -eq 4 ]]; then 456 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Download Failure" -description "$download_error" -button1 "Exit" -defaultButton 1 & 457 | exit 5 458 | fi 459 | 460 | return 0 461 | } 462 | 463 | # Check CoreStorage Conversion State 464 | checkCSConversionState (){ 465 | /usr/sbin/diskutil cs info / 2>/dev/null 466 | cs_status=$? 467 | 468 | if [[ "$cs_status" = 1 ]]; then 469 | echo "/ is not a CoreStorage disk. Proceeding." 470 | return 0 471 | fi 472 | 473 | if [[ "$(/usr/sbin/diskutil cs info / | /usr/bin/awk '/Conversion State:/ { print $3 }')" = "Converting" ]]; then 474 | echo "CoreStorage Conversion is in the middle of converting. macOS installer will fail in this state. Stopping upgrade." 475 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$cs_error" -button1 "Exit" -defaultButton 1 & 476 | exit 21 477 | fi 478 | } 479 | 480 | # If user previously opened install macOS app, it may have mounted InstallESD and not unmounted it. 481 | checkForMountedInstallESD (){ 482 | # Unmount InstallESD if mounted 483 | # In some circumstances when Install macOS.app is launched, it auto mounts but never unmounts when the app is closed. 484 | # This may cause errors in startosinstall which is why we need to unmount the disk image 485 | # Future note: re-write to check output from hdiutil info -plist / to see any mounted InstallESD volumes 486 | 487 | vol_name="${1}" 488 | 489 | if [[ -d "$vol_name" ]]; then 490 | /usr/bin/hdiutil detach -force "$vol_name" 491 | if [[ $? != 0 ]]; then 492 | echo "Failed to unmount $vol_name" 493 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 494 | exit 22 495 | fi 496 | fi 497 | } 498 | 499 | checkForFreeSpace (){ 500 | # Set default value for $needed_free_space to 25GB 501 | [[ -z "$needed_free_space" ]] && local needed_free_space="25" 502 | 503 | available_free_space=$(/bin/df -g / | /usr/bin/awk '(NR == 2){print $4}') 504 | insufficient_free_space_for_install_dialog="Your boot drive must have $needed_free_space gigabytes of free space available in order to install $app_name. It currently has $available_free_space gigabytes free. Please free up space and try again. If you need assistance, please contact $it_contact." 505 | 506 | # Check for to make sure free disk space required is a positive integer 507 | if [[ ! "$needed_free_space" =~ ^[0-9]+$ ]]; then 508 | echo "Enter a positive integer value (no decimals) for free disk space required." 509 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 510 | exit 7 511 | fi 512 | 513 | # Checking for two conditions: 1) enough space to download the download installer, and 514 | # 2) the installer is on disk but there is not enough space for what the installer needs to proceed 515 | if [[ "$available_free_space" -lt 25 ]] || [[ "$available_free_space" -lt "$needed_free_space" ]]; then 516 | echo "$insufficient_free_space_for_install_dialog" 517 | "$jamfHelper" -windowType utility -icon "$driveicon" -title "Insufficient Free Space" -description "$insufficient_free_space_for_install_dialog" -button1 "Exit" -defaultButton 1 & 518 | exit 11 519 | else 520 | echo "$available_free_space gigabytes found as free space on boot drive. Proceeding with install." 521 | fi 522 | } 523 | 524 | # From https://github.com/munki/munki/blob/master/code/client/munkilib/osinstaller.py 525 | # Set a secret preference to tell the osinstaller process to exit instead of restart 526 | # this is the equivalent of: 527 | # defaults write /Library/Preferences/.GlobalPreferences IAQuitInsteadOfReboot -bool YES 528 | # 529 | # This preference is referred to in a framework inside the Install macOS.app: 530 | # Contents/Frameworks/OSInstallerSetup.framework/Versions/A/ 531 | # Frameworks/OSInstallerSetupInternal.framework/Versions/A/ 532 | # OSInstallerSetupInternal 533 | # It might go away in future versions of the macOS installer. 534 | disableInstallAssistantRestartPref (){ 535 | /usr/bin/defaults write /Library/Preferences/.GlobalPreferences IAQuitInsteadOfReboot -bool YES 536 | } 537 | 538 | deleteInstallAssistantRestartPref (){ 539 | /usr/bin/defaults delete /Library/Preferences/.GlobalPreferences IAQuitInsteadOfReboot 540 | } 541 | 542 | checkMinReqOSVer (){ 543 | # Check to make sure the computer is running the minimum supported OS as determined by the OS installer. Note: macOS Sierra requires OS 10.7.5 or higher. 544 | # Also confirm that we are dealing with a valid OS version which is in the form of 10.12.4 545 | 546 | if [[ -n "$min_req_os" ]]; then 547 | if [[ "$min_req_os" =~ ^[0-9]+[\.]{1}[0-9]+[\.]{0,1}[0-9]*$ ]]; then 548 | if [[ "$(compareLooseVersion $os_ver $min_req_os)" == "False" ]]; then 549 | echo "Unsupported Operating System. Cannot upgrade $os_major_ver.$os_minor_ver.$os_patch_ver to $min_req_os" 550 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Unsupported OS" -description "$bad_os" -button1 "Exit" -defaultButton 1 551 | exit 2 552 | else 553 | echo "Computer meets the minimum required OS to upgrade. Proceeding." 554 | fi 555 | else 556 | echo "Invalid Minimum OS version value." 557 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 558 | exit 3 559 | fi 560 | else 561 | echo "Minimum OS app version has not been supplied. Skipping check." 562 | fi 563 | } 564 | 565 | # Function to check if a required minimum macOS app installer has been supplied 566 | meetsApprovedMinimumOSInstallerVersion (){ 567 | if [[ "$approved_min_os_app_ver" =~ ^[0-9]+[\.]{1}[0-9]+[\.]{0,1}[0-9]*$ ]]; then 568 | CompareResult="$(compareLooseVersion "$base_os_ver" "$approved_min_os_app_ver")" 569 | 570 | if [[ "$CompareResult" = "False" ]]; then 571 | echo "The macOS installer app version is $base_os_ver. The system admin has set a minimum version requirement of $approved_min_os_app_ver for macOS installers." 572 | echo "Minimum required macOS installer app version is greater than the version of the macOS installer on the client." 573 | echo "Please install the macOS installer app version that meets the minimum requirement set." 574 | echo "Alternatively, you can modify or remove the minimum macOS installer app version requirement." 575 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 576 | exit 8 577 | elif [[ "$CompareResult" = "True" ]]; then 578 | echo "Minimum required macOS installer app version is greater than the version on the client." 579 | fi 580 | else 581 | echo "Invalid Minimum OS version value." 582 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 583 | exit 3 584 | fi 585 | } 586 | 587 | # Function to check that the additional post-install package is available is a proper distribution-style package with a product id 588 | validatePostInstallPKG (){ 589 | if [[ -z "$add_install_pkg" ]]; then 590 | return 0 591 | elif [[ "$(compareLooseVersion 10.13 $base_os_ver)" = True ]] && [[ -n "$add_install_pkg" ]]; then 592 | echo "A post-OS upgrade install package was detected but the macOS installer does not support it so it won't be used." 593 | 594 | if [[ -e "$add_install_pkg" ]]; then 595 | /bin/rm -f "$add_install_pkg" 596 | echo "$add_install_pkg has been deleted." 597 | fi 598 | if [[ -e "$add_install_pkg".cache.xml ]]; then 599 | /bin/rm -f "$add_install_pkg".cache.xml 600 | echo "$add_install_pkg.cache.xml has been deleted." 601 | fi 602 | return 0 603 | fi 604 | 605 | file_name="$(echo "$add_install_pkg" | /usr/bin/awk -F / '{ print $NF }')" 606 | 607 | # Expand package to check that Distribution file includes a product id 608 | /usr/sbin/pkgutil --expand "$add_install_pkg" /tmp/"$file_name" 609 | 610 | # Check Distribution file for a product id 611 | /bin/cat /tmp/"$file_name"/Distribution | /usr/bin/grep "/dev/null 612 | 613 | if [[ $? -eq 0 ]]; then 614 | /bin/rm -rf /tmp/"$file_name" 615 | return 0 616 | else 617 | echo "Install package does not have a Product ID in its Distribution file. Build distribution package using productbuild." 618 | echo "Either use a proper distribution pkg with a product id or leave the Jamf parameter empty." 619 | /bin/rm -rf /tmp/"$file_name" 620 | exit 20 621 | fi 622 | } 623 | 624 | 625 | mountVolume (){ 626 | # Pass 1 argument containing path to disk image to determine its volume name 627 | disk_image="${1}" 628 | 629 | # Mount volume 630 | /usr/bin/hdiutil attach -nobrowse -quiet "$disk_image" 631 | 632 | # Ensure DMG mounted successfully 633 | if [[ "$?" != 0 ]]; then 634 | echo "Unable to mount "$disk_image"." 635 | 636 | return 1 637 | fi 638 | } 639 | 640 | determineVolumeName (){ 641 | # Pass 1 argument containing path to disk image to determine its volume name 642 | disk_image="${1}" 643 | 644 | # Determine the name of the mounted volume based on source image-path 645 | mounted_volumes=$(/usr/bin/hdiutil info -plist) 646 | 647 | finished="false" 648 | c=0 649 | i=0 650 | while [[ "$finished" == "false" ]]; do 651 | if [[ "$(/usr/libexec/PlistBuddy -c "print :images:"$c":image-path" /dev/stdin <<<$mounted_volumes 2>&1)" == *"Does Not Exist"* ]]; then 652 | finished="true" 653 | fi 654 | if [[ "$(/usr/libexec/PlistBuddy -c "print :images:"$c":image-path" /dev/stdin <<<$mounted_volumes 2>&1)" == "$disk_image" ]]; then 655 | while [[ "$finished" == "false" ]]; do 656 | if [[ "$(/usr/libexec/PlistBuddy -c "print :images:"$c":system-entities:"$i":mount-point" /dev/stdin <<<$mounted_volumes 2>&1)" == "/Volumes/"* ]]; then 657 | mounted_volume_name="$(/usr/libexec/PlistBuddy -c "print :images:"$c":system-entities:"$i":mount-point" /dev/stdin <<<$mounted_volumes 2>&1)" 658 | 659 | # Confirm that value returned by plistbuddy is valid 660 | checkForPlistValue "$mounted_volume_name" 661 | 662 | if [[ $? -eq 25 ]]; then 663 | echo "Plistbuddy returned an empty value." 664 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 665 | exit 25 666 | elif [[ $? -eq 27 ]]; then 667 | echo "Plistbuddy is trying to read from a file that does not exist." 668 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 669 | exit 27 670 | fi 671 | 672 | echo "$mounted_volume_name" 673 | finished="true" 674 | else 675 | i=$((i + 1)) 676 | # echo $i 677 | fi 678 | done 679 | else 680 | c=$((c + 1)) 681 | # echo $c 682 | fi 683 | done 684 | } 685 | 686 | 687 | # Function to validate certificates of macOS installer app and the packages contained within InstallESD.pkg 688 | # This function will only fail if a certificate is found and is found to be expired 689 | validateAppExpirationDate (){ 690 | # Function will only work on macOS installers lower than macOS 11/10.16 as the contents of the image that is laid down is different 691 | if [[ "$installer_app_major_ver" -ge 16 ]]; then 692 | echo "Skipping app expiration date validation as this macOS installer is running $installer_app_ver." 693 | return 0 694 | fi 695 | 696 | # Capture current directory to return back to it later 697 | current_dir="$(/bin/pwd)" 698 | 699 | # Setup temporary folder to extract certificates to since codesign does not let us specify an output path 700 | current_time="$(/bin/date +%s)" 701 | temp_path="/tmp/codesign_$current_time" 702 | /bin/mkdir -p "$temp_path" 703 | cd "$temp_path" 704 | 705 | # Extract certificates from app bundle 706 | /usr/bin/codesign -dvvvv --extract-certificates "$mac_os_installer_path" 707 | 708 | # Ensure we were able to extract certificates from installer app 709 | if [[ $? != 0 ]]; then 710 | echo "Could not extract certificates from $mac_os_installer_path" 711 | echo "Will proceed without validating contents of $mac_os_installer_path" 712 | return 1 713 | fi 714 | 715 | # Loop through all codesign files 716 | for code in $(/usr/bin/find "$temp_path" -iname codesign\* -mindepth 1); do 717 | # Analyze expiration date of certificate 718 | # Format of date e.g.: Apr 12 22:34:35 2021 GMT 719 | # Variable to extract expiration date 720 | expiration_date_string=$(/usr/bin/openssl x509 -enddate -noout -inform DER -in "$code" | /usr/bin/awk -F'=' '{print $2}' | /usr/bin/tr -s ' ') 721 | 722 | # Variable to convert expiration date string into epoch seconds 723 | expiration_date_epoch=$(/bin/date -jf "%b %d %H:%M:%S %Y %Z" "$expiration_date_string" +"%s") 724 | 725 | if [[ $expiration_date_epoch -lt $current_time ]]; then 726 | echo "A certificate for the installer application $mac_os_installer_path has expired. Please download a new macOS installer app with a valid certificate." 727 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 728 | exit 23 729 | fi 730 | 731 | # Delete codesign file 732 | /bin/rm -f "$code" 733 | done 734 | 735 | echo "Certificates for the application $mac_os_installer_path are valid." 736 | echo "Proceeding to check certificates of packages inside InstallESD.dmg" 737 | 738 | # Potential statuses given by pkgutil --check-signature 739 | # Not all of these are checked against but leaving here for documentation purposes 740 | expired_pkgutil="Status: signed by a certificate that has since expired" 741 | valid_pkgutil="Status: signed by a certificate trusted by Mac OS X" 742 | untrusted_pkgutil="Status: signed by untrusted certificate" 743 | signed_pkgutil="Status: signed Apple Software" 744 | unsigned_pkgutil="Status: no signature" 745 | 746 | # Mount volume 747 | mountVolume "$mac_os_installer_path/Contents/SharedSupport/InstallESD.dmg" 748 | 749 | # Clean up if mount failed 750 | if [[ $? -ne 0 ]]; then 751 | echo "Unable to mount "$mac_os_installer_path"/Contents/SharedSupport/InstallESD.dmg to validate certificate." 752 | echo "Will proceed without validating contents of "$mac_os_installer_path"/Contents/SharedSupport/InstallESD.dmg." 753 | 754 | # Remove temporary working path 755 | /bin/rm -rf "$temp_path" 756 | 757 | # Return back to previous current directory 758 | cd "$current_dir" 759 | 760 | return 1 761 | fi 762 | 763 | # Determine the name of the mounted volume based on source image-path 764 | volume_name="$(determineVolumeName "$mac_os_installer_path/Contents/SharedSupport/InstallESD.dmg")" 765 | 766 | # Loop through all packages and determine if any of them have expired certificates 767 | IFS=" 768 | " 769 | for pkg in $(/usr/bin/find "$volume_name" -iname \*.pkg); do 770 | pkg_status="$(/usr/sbin/pkgutil --check-signature "$pkg" | /usr/bin/awk '/Status:/{gsub(/ /,""); print $0}')" 771 | if [[ "$pkg_status" == "$expired_pkgutil" ]]; then 772 | echo "$pkg has expired. Please download a new macOS installer with a valid certificate." 773 | 774 | # Remove temporary working path 775 | /bin/rm -rf "$temp_path" 776 | 777 | # Return back to previous current directory 778 | cd "$current_dir" 779 | 780 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 781 | exit 24 782 | fi 783 | done 784 | 785 | unset IFS 786 | 787 | # Unmount volume 788 | /usr/bin/hdiutil detach -force "$volume_name" 789 | 790 | # Remove temporary working path 791 | /bin/rm -rf "$temp_path" 792 | 793 | # Return back to previous current directory 794 | cd "$current_dir" 795 | } 796 | 797 | 798 | # Function that determines what OS is in the macOS installer.app so that the appropriate startosinstall options are used as Apple has changed it with 10.12.4 799 | # Supply a parameter $1 for this function that includes the macOS app installer you are using to upgrade. 800 | installCommand (){ 801 | # disableInstallAssistantRestartPref 802 | 803 | JHPID="$1" 804 | install_log="$2" 805 | running_apple_silicon="$3" 806 | 807 | if [[ "$running_apple_silicon" == 1 ]]; then 808 | # Installer for macOS Big Sur introduce ability to pass credentials through new options 809 | # Required for authorizing installations on Apple Silicon and volume ownership 810 | # --user, an admin user to authorize installation. 811 | # --stdinpass, collect a password from stdin without interaction. 812 | 813 | if [[ -z "$user_pw" ]]; then 814 | # User ID of the logged in user 815 | user_id=$(/usr/bin/id -u "$logged_in_user") 816 | 817 | # Determine appropriate launchctl option to use 818 | if [[ "$os_major_ver" -eq 10 && "$os_minor_ver" -le 9 ]]; then 819 | l_id=$(/usr/bin/pgrep -x -u "$user_id" loginwindow) 820 | l_method="bsexec" 821 | elif [[ "$os_major_ver" -ge 11 || "$os_major_ver" -eq 10 && "$os_minor_ver" -gt 9 ]]; then 822 | l_id="$user_id" 823 | l_method="asuser" 824 | fi 825 | 826 | # Applescript path to macOS installer app icon 827 | mas_os_icon_as="$(/usr/bin/osascript -e 'tell application "System Events" to return POSIX file "'"$mas_os_icon"'" as text')" 828 | 829 | # Kill Jamf Helper window temporarily to avoid hiding upcoming AppleScript dialog 830 | /bin/kill -s KILL "$JHPID" > /dev/null 1>&2 831 | 832 | # Get the logged in user's password via a prompt. 833 | echo "Prompting $logged_in_user for their Mac password..." 834 | 835 | user_pw="$(/bin/launchctl "$l_method" "$l_id" /usr/bin/osascript << EOF 836 | return text returned of (display dialog "Please enter the password for the account \"$logged_in_user\" you use to log in to your Mac:" default answer "" with title "macOS OS Install Authentication" giving up after 86400 with text buttons {"OK"} default button 1 with hidden answer with icon file "${mas_os_icon_as}") 837 | EOF 838 | )" 839 | # Thanks to James Barclay (@futureimperfect) for this password validation loop. 840 | TRY=1 841 | until /usr/bin/dscl /Search -authonly "$logged_in_user" "$user_pw" &>/dev/null; do 842 | (( TRY++ )) 843 | echo "Prompting $logged_in_user for their Mac password (attempt $TRY)..." 844 | user_pw="$(/bin/launchctl "$l_method" "$l_id" /usr/bin/osascript -e 'display dialog "Sorry, that password was incorrect. Please try again:" default answer "" with title "macOS Install Authentication" giving up after 86400 with text buttons {"OK"} default button 1 with hidden answer with icon file "'"${mas_os_icon_as//\"/\\\"}"'"' -e 'return text returned of result')" 845 | if (( TRY >= 5 )); then 846 | echo "Password prompt unsuccessful after 5 attempts." 847 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Authentication Error" -description "$forgot_password" -button1 "Exit" -defaultButton 1 & 848 | exit 18 849 | fi 850 | done 851 | echo "Successfully prompted for user password." 852 | fi 853 | 854 | vol_owner_options="--user $logged_in_user --stdinpass <<<$user_pw" 855 | fi 856 | 857 | # Make use install package 858 | [[ -e "$add_install_pkg" ]] && installpkg_option="--installpackage $add_install_pkg" 859 | 860 | # The startosinstall tool has been updated in various forms. The commands below take advantage of those updates. 861 | if [[ "$(compareLooseVersion $base_os_ver 11.0)" = True ]] && [[ "$running_apple_silicon" == "1" ]]; then 862 | echo "The embedded OS version in the macOS installer app is greater than or equal to macOS 11. Running appropriate startosinstall command to initiate install." 863 | # Initiate the macOS Big Sur silent install 864 | # 30 second delay should give the jamf binary enough time to upload policy results to JSS 865 | # On Apple Silicon, the "--user" and either "--passprompt" or "--stdinpass" need to be used. 866 | # An attempt was made to use --stdinpass but that resulted in the password being logged in the install log due to use of /usr/bin/script 867 | # /usr/bin/script -q -t 1 "$install_log" "$mac_os_installer_path/Contents/Resources/startosinstall" --rebootdelay 30 --agreetolicense ${installpkg_option} --forcequitapps --pidtosignal "$JHPID" --user "$logged_in_user" --stdinpass <<<"$user_pw"; echo "Exit Code:$?" >> "$install_log" & 868 | # 869 | # Settled on using an embedded Expect script so that the password can be passed interactively using --passprompt. 870 | /usr/bin/expect -d <(/bin/cat <> "$install_log" 895 | 896 | # Clear password variable 897 | unset "$user_pw" 898 | elif [[ "$(compareLooseVersion $base_os_ver 11.0)" = True ]] && [[ "$running_apple_silicon" == "0" ]]; then 899 | echo "The embedded OS version in the macOS installer app is greater than or equal to macOS 11. Running appropriate startosinstall command to initiate install." 900 | # Initiate the macOS Big Sur silent install 901 | # 30 second delay should give the jamf binary enough time to upload policy results to JSS 902 | /usr/bin/script -q -t 1 "$install_log" "$mac_os_installer_path/Contents/Resources/startosinstall" --rebootdelay 30 --agreetolicense ${installpkg_option} --forcequitapps --pidtosignal "$JHPID"; echo "Exit Code:$?" >> "$install_log" & 903 | elif [[ "$(compareLooseVersion $base_os_ver 10.15)" = True ]]; then 904 | echo "The embedded OS version in the macOS installer app is greater than or equal to 10.15. Running appropriate startosinstall command to initiate install." 905 | # Initiate the macOS Catalina silent install 906 | # 30 second delay should give the jamf binary enough time to upload policy results to JSS 907 | /usr/bin/script -q -t 1 "$install_log" "$mac_os_installer_path/Contents/Resources/startosinstall" --rebootdelay 30 --agreetolicense ${installpkg_option} --forcequitapps --pidtosignal "$JHPID"; echo "Exit Code:$?" >> "$install_log" & 908 | elif [[ "$(compareLooseVersion $base_os_ver 10.14)" = True ]]; then 909 | echo "The embedded OS version in the macOS installer app is greater than or equal to 10.14. Running appropriate startosinstall command to initiate install." 910 | # Initiate the macOS Mojave silent install 911 | # 30 second delay should give the jamf binary enough time to upload policy results to JSS 912 | /usr/bin/script -q -t 1 "$install_log" "$mac_os_installer_path/Contents/Resources/startosinstall" --rebootdelay 30 --agreetolicense ${installpkg_option} --pidtosignal "$JHPID"; echo "Exit Code:$?" >> "$install_log" & 913 | elif [[ "$(compareLooseVersion $base_os_ver 10.13)" = True ]]; then 914 | echo "The embedded OS version in the macOS installer app is greater than or equal to 10.13. Running appropriate startosinstall command to initiate install." 915 | # Initiate the macOS High Sierra silent install 916 | # 30 second delay should give the jamf binary enough time to upload policy results to JSS 917 | # Left this the same as the previous command in case you want to force upgrades to do APFS. Modify the next line by adding: --converttoapfs YES 918 | # If Apple's installer does not upgrade the Mac to APFS, assume something about your Mac does not pass the "upgrade to APFS" logic. 919 | /usr/bin/script -q -t 1 "$install_log" "$mac_os_installer_path/Contents/Resources/startosinstall" --applicationpath "$mac_os_installer_path" --rebootdelay 30 --agreetolicense ${installpkg_option} --pidtosignal "$JHPID"; echo "Exit Code:$?" >> "$install_log" & 920 | elif [[ "$(compareLooseVersion $base_os_ver 10.12.4)" = True ]]; then 921 | echo "The OS version in the macOS installer app version is greater than 10.12.4 but lower than 10.13. Running appropriate startosinstall command to initiate install." 922 | # Initiate the macOS Sierra silent install (this will also work for High Sierra) 923 | # 30 second delay should give the jamf binary enough time to upload policy results to JSS 924 | /usr/bin/script -q -t 1 "$install_log" "$mac_os_installer_path/Contents/Resources/startosinstall" --applicationpath "$mac_os_installer_path" --rebootdelay 30 --nointeraction --pidtosignal "$JHPID"; echo "Exit Code:$?" >> "$install_log" & 925 | elif [[ "$(compareLooseVersion $base_os_ver 10.12)" = True ]]; then 926 | echo "The OS version in the macOS installer app version is less than 10.12.4. Running appropriate startosinstall command to initiate install." 927 | # Initiate the macOS Seirra silent install 928 | # 30 second delay should give the jamf binary enough time to upload policy results to JSS 929 | /usr/bin/script -q -t 1 "$install_log" "$mac_os_installer_path/Contents/Resources/startosinstall" --applicationpath "$mac_os_installer_path" --volume / --rebootdelay 30 --nointeraction --pidtosignal "$JHPID"; echo "Exit Code:$?" >> "$install_log" & 930 | else 931 | echo "The OS version in the macOS installer app version is less than 10.12. Running appropriate startosinstall command to initiate install." 932 | fi 933 | 934 | # deleteInstallAssistantRestartPref 935 | } 936 | 937 | # Function that goes through the install 938 | # Takes parameter $1 which is optional and is simply meant to add additional text to the jamfHelper header 939 | installOS (){ 940 | heading="${1}" 941 | 942 | # Prompt for user password for FV authenticated restart if supported to avoid installation stalling at FV login window 943 | fvAuthRestart 944 | 945 | # Check for volume owners which is relevant for Macs running on Apple Silicon 946 | confirmVolumeOwner 947 | 948 | # Capture return on confirmVolumeOwner 949 | running_apple_silicon="$(echo $?)" 950 | 951 | # Update message letting end-user know upgrade is going to start. 952 | "$jamfHelper" -windowType hud -lockhud -title "$app_name$heading" -description "$inprogress" -icon "$mas_os_icon" -windowPosition & 953 | 954 | # Get the Process ID of the last command 955 | JHPID=$(echo "$!") 956 | 957 | # Generate log name 958 | install_log="${installmacos_log}_$(/bin/date +%y%m%d%H%M%S)".log 959 | 960 | # Run the os installer command 961 | installCommand "$JHPID" "$install_log" "$running_apple_silicon" 962 | 963 | # The macOS install process successfully exits with code 0 964 | # On the off chance the installer fails, let's warn the user 965 | if [[ "$(/usr/bin/tail -n 1 $install_log | /usr/bin/cut -d : -f 2)" != 0 ]] && [[ "$(/usr/bin/tail -n 1 $install_log | /usr/bin/cut -d : -f 2)" != 255 ]]; then 966 | /bin/kill -s KILL "$JHPID" > /dev/null 1>&2 967 | echo "startosinstall did not succeed. See log at: $install_log and /var/log/install.log and /var/log/system.log" 968 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Installation Failure" -description "$upgrade_error" -button1 "Exit" -defaultButton 1 & 969 | exit 9 970 | fi 971 | 972 | if [[ "$(/usr/bin/tail -n 2 $install_log | /usr/bin/head -n 1)" = "An error occurred installing macOS." ]] || [[ "$(/usr/bin/tail -n 2 $install_log | /usr/bin/head -n 1)" = "Helper tool crashed..." ]]; then 973 | /bin/kill -s KILL "$JHPID" > /dev/null 1>&2 974 | echo "startosinstall did not succeed. See log at: $install_log and /var/log/install.log and /var/log/system.log" 975 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Installation Failure" -description "$upgrade_error" -button1 "Exit" -defaultButton 1 & 976 | exit 9 977 | fi 978 | 979 | # Quit Self Service 980 | /usr/bin/killall "Self Service" 981 | 982 | # Create launch daemon to update inventory post-OS upgrade 983 | createReconAfterUpgradeLaunchDaemon 984 | 985 | # /sbin/shutdown -r now & 986 | 987 | exit 0 988 | } 989 | 990 | # Function to attempt to quit all active applications 991 | # quitAllApps (){ 992 | # # Prompt all running applications to quit before running the installer 993 | # if [[ -z "$logged_in_user" ]]; then 994 | # echo "No user is logged in. No apps to close." 995 | # return 0 996 | # fi 997 | # 998 | # # Get the user id of the logged in user 999 | # user_id=$(/usr/bin/id -u "$logged_in_user") 1000 | # 1001 | # if [[ "$os_major_ver" -eq 10 && "$os_minor_ver" -le 9 ]]; then 1002 | # l_id=$(/usr/bin/pgrep -x -u "$user_id" loginwindow) 1003 | # l_method="bsexec" 1004 | # elif [[ "$os_major_ver" -ge 11 || "$os_major_ver" -eq 10 && "$os_minor_ver" -gt 9 ]]; then 1005 | # l_id=$user_id 1006 | # l_method="asuser" 1007 | # fi 1008 | # 1009 | # exitCode="$(/bin/launchctl $l_method $l_id /usr/bin/osascript < 1055 | 1056 | 1057 | 1058 | Label 1059 | $launch_daemon 1060 | ProgramArguments 1061 | 1062 | /bin/zsh 1063 | -c 1064 | /usr/local/bin/jamf recon && /bin/rm -f /Library/LaunchDaemons/$launch_daemon.plist && /bin/launchctl bootout system/$launch_daemon; 1065 | 1066 | RunAtLoad 1067 | 1068 | StartInterval 1069 | 60 1070 | 1071 | " > "$launch_daemon_path" 1072 | 1073 | # Set proper permissions on launch daemon 1074 | if [[ -e "$launch_daemon_path" ]]; then 1075 | /usr/sbin/chown root:wheel "$launch_daemon_path" 1076 | /bin/chmod 644 "$launch_daemon_path" 1077 | fi 1078 | } 1079 | 1080 | determineBaseOSVersion (){ 1081 | # This needs to be re-worked because of chances in the macOS 11 installer 1082 | # Determine version of the OS included in the installer 1083 | if [[ "$installer_app_major_ver" -ge 16 ]]; then 1084 | # Variable to determine mount failure status 1085 | mount_failure="false" 1086 | 1087 | # Mount volume 1088 | /usr/bin/hdiutil attach -nobrowse -quiet "$mac_os_installer_path"/Contents/SharedSupport/SharedSupport.dmg 1089 | 1090 | # Determine whether SharedSupport.dmg was mounted 1091 | if [[ $? -ne 0 ]]; then 1092 | mount_failure="true" 1093 | 1094 | # Attempt a re-download of the OS installer 1095 | redownloadOSInstaller 1096 | 1097 | # Attempt to mount volume one more time 1098 | /usr/bin/hdiutil attach -nobrowse -quiet "$mac_os_installer_path"/Contents/SharedSupport/SharedSupport.dmg 1099 | 1100 | # Determine whether SharedSupport.dmg was mounted 1101 | if [[ $? -ne 0 ]]; then 1102 | mount_failure="true" 1103 | else 1104 | mount_failure="false" 1105 | fi 1106 | fi 1107 | 1108 | if [[ "$mount_failure" == "true" ]]; then 1109 | echo "Failed to mount SharedSupport.dmg" 1110 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1111 | exit 28 1112 | fi 1113 | 1114 | # Path to mobile asset xml which contains path to zip containing base OS image 1115 | mobile_asset_xml="/Volumes/Shared Support/com_apple_MobileAsset_MacSoftwareUpdate/com_apple_MobileAsset_MacSoftwareUpdate.xml" 1116 | 1117 | # Relative path to mobile asset 1118 | mobile_asset="$(/usr/libexec/PlistBuddy -c "print :Assets:0:__RelativePath" "$mobile_asset_xml" 2>/dev/null)" 1119 | 1120 | if [[ $? -ne 0 ]]; then 1121 | echo "Could not read mobile asset xml from SharedSupport.dmg." 1122 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1123 | exit 29 1124 | fi 1125 | 1126 | # Confirm that value returned by plistbuddy is valid 1127 | checkForPlistValue "$mobile_asset" 1128 | 1129 | if [[ $? -eq 25 ]]; then 1130 | echo "Plistbuddy returned an empty value." 1131 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1132 | exit 25 1133 | elif [[ $? -eq 27 ]]; then 1134 | echo "Plistbuddy is trying to read from a file that does not exist." 1135 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1136 | exit 27 1137 | fi 1138 | 1139 | # Path to base image zip 1140 | base_image_zip="/Volumes/Shared Support/$mobile_asset" 1141 | 1142 | # Extract plist containing OS version to stdout 1143 | #/usr/bin/unzip -j "$base_image_zip" "Info.plist" -d "/private/tmp" 1144 | base_image_info_plist="$(/usr/bin/unzip -pj "$base_image_zip" "Info.plist")" 1145 | 1146 | # Determine base OS image version 1147 | base_os_ver="$(/usr/libexec/PlistBuddy -c "print :MobileAssetProperties:OSVersion" /dev/stdin <<<"$base_image_info_plist" 2>/dev/null)" 1148 | 1149 | # Confirm that a value could be retrieved from plist 1150 | if [[ $? -ne 0 ]]; then 1151 | echo "Could not read the OS version from Info.plist within the base image zip: $base_image_zip." 1152 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1153 | exit 30 1154 | fi 1155 | 1156 | # Confirm that a value containing a valid version could be retrieved from plist 1157 | if [[ ! "$base_os_ver" =~ ^[0-9]+[\.]{1}[0-9]+[\.]{0,1}[0-9]*$ ]]; then 1158 | echo "Invalid version value retrieved from Info.plist within the base image zip: $base_image_zip." 1159 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1160 | exit 31 1161 | fi 1162 | 1163 | # Confirm that value returned by plistbuddy is valid 1164 | checkForPlistValue "$base_os_ver" 1165 | 1166 | if [[ $? -eq 25 ]]; then 1167 | echo "Plistbuddy returned an empty value." 1168 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1169 | exit 25 1170 | elif [[ $? -eq 27 ]]; then 1171 | echo "Plistbuddy is trying to read from a file that does not exist." 1172 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1173 | exit 27 1174 | fi 1175 | 1176 | # Do not forget to detach and unmount volume 1177 | # Determine volume name of disk image 1178 | volume_name="$(determineVolumeName "$mac_os_installer_path"/Contents/SharedSupport/SharedSupport.dmg)" 1179 | 1180 | # Unmount volume 1181 | /usr/bin/hdiutil detach -force "$volume_name" &>/dev/null 1182 | else 1183 | # Determine base OS image version 1184 | base_os_ver="$(/usr/libexec/PlistBuddy -c "print :'System Image Info':version" "$mac_os_installer_path"/Contents/SharedSupport/InstallInfo.plist 2>/dev/null)" 1185 | 1186 | # Confirm that value returned by plistbuddy is valid 1187 | checkForPlistValue "$base_os_ver" 1188 | 1189 | if [[ $? -eq 25 ]]; then 1190 | echo "Plistbuddy returned an empty value." 1191 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1192 | exit 25 1193 | elif [[ $? -eq 27 ]]; then 1194 | echo "Plistbuddy is trying to read from a file that does not exist." 1195 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1196 | exit 27 1197 | fi 1198 | fi 1199 | 1200 | # Ensure $base_os_ver is not empty 1201 | if [[ -z "$base_os_ver" ]]; then 1202 | echo "Could not determine OS version in the app installer's base image." 1203 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1204 | exit 26 1205 | fi 1206 | 1207 | echo "$base_os_ver" 1208 | } 1209 | 1210 | confirmVolumeOwner (){ 1211 | logged_in_user_uuid="$(/usr/bin/dscl . read /Users/$logged_in_user GeneratedUID 2>/dev/null | /usr/bin/awk '{print $2}')" 1212 | 1213 | # Variable to determine architecture of Mac 1214 | platform=$(/usr/bin/arch) 1215 | 1216 | # Variable to hold state on whether to proceed with install 0=no, 1=yes 1217 | running_apple_silicon="0" 1218 | 1219 | # Exit if not running on Apple Silicon 1220 | # Otherwise confirm that logged in user is volume owner 1221 | if [[ "$platform" != "arm64" ]]; then 1222 | echo "Architecture: $platform. No need to check for volume owners." 1223 | else 1224 | # Determine number of APFS users 1225 | total_apfs_users=$(/usr/sbin/diskutil apfs listUsers / | /usr/bin/awk '/\+\-\-/ {print $2}' | /usr/bin/wc -l | /usr/bin/tr -d ' ') 1226 | 1227 | # Get APFS User information in plist format 1228 | apfs_users_plist=$(/usr/sbin/diskutil apfs listUsers / -plist) 1229 | 1230 | # Loop through all APFS Crypto Users to determine if logged in user is volume owner 1231 | for (( n=0; n<$total_apfs_users; n++ )); do 1232 | # Determine APFS Crypto User UUID 1233 | apfs_crypto_user_uuid=$(/usr/libexec/PlistBuddy -c "print :Users:"$n":APFSCryptoUserUUID" /dev/stdin <<<$apfs_users_plist) 1234 | 1235 | # Determine APFS Crypto User Type 1236 | apfs_crypto_user_type=$(/usr/libexec/PlistBuddy -c "print :Users:"$n":APFSCryptoUserType" /dev/stdin <<<$apfs_users_plist) 1237 | 1238 | # Determine if APFS Crypto User is MDM Recovery/Bootstrap Token Key 1239 | if [[ "$apfs_crypto_user_type" == "MDMRecovery" ]]; then 1240 | echo "$apfs_crypto_user_uuid is the MDM Bootstrap Token External Key crypto user." 1241 | # Maybe in the future, it may be useful to check against this crypto user type 1242 | fi 1243 | 1244 | # Compare logged in user's uuid from list of APFS Crypto Users 1245 | if [[ "$logged_in_user_uuid" == "$apfs_crypto_user_uuid" ]]; then 1246 | # Determine volume owner status for APFS Crypto User 1247 | user_volume_owner_status=$(/usr/libexec/PlistBuddy -c "print :Users:"$n":VolumeOwner" /dev/stdin <<<$apfs_users_plist) 1248 | 1249 | if [[ "$user_volume_owner_status" = true ]]; then 1250 | echo "Logged In User: $logged_in_user is a volumer owner." 1251 | running_apple_silicon="1" 1252 | break 1253 | else 1254 | echo "Logged In User: $logged_in_user is not a volumer owner." 1255 | continue 1256 | fi 1257 | 1258 | break 1259 | else 1260 | echo "no match" 1261 | continue 1262 | fi 1263 | done 1264 | 1265 | if [[ "$running_apple_silicon" -eq "1" ]]; then 1266 | echo "proceed with install" 1267 | else 1268 | echo "this install cannot take place. user is not a volume owner." 1269 | fi 1270 | fi 1271 | 1272 | echo "$running_apple_silicon" 1273 | return "$running_apple_silicon" 1274 | } 1275 | 1276 | # Run through a few pre-download checks 1277 | checkParam "$time" "time" 1278 | checkParam "$mac_os_installer_path" "mac_os_installer_path" 1279 | checkPowerSource 1280 | checkCSConversionState 1281 | checkForFreeSpace 1282 | 1283 | # Ensure that macOS installer app is downloaded 1284 | if [[ ! -e "$mac_os_installer_path" ]]; then 1285 | downloadOSInstaller 1286 | 1287 | heading=" (2 of 2)" 1288 | 1289 | # Make sure macOS installer app is on computer 1290 | if [[ ! -e "$mac_os_installer_path" ]]; then 1291 | echo "An unsuccessfull attempt was made to download the macOS installer. Attempting to download again." 1292 | fi 1293 | fi 1294 | 1295 | checkOSInstaller 1296 | 1297 | # Determine OS installer version 1298 | installer_app_ver="$(checkOSInstallerVersion)" 1299 | 1300 | # Confirm that value returned by plistbuddy is valid 1301 | checkForPlistValue "$installer_app_ver" 1302 | 1303 | if [[ $? -eq 25 ]]; then 1304 | echo "Plistbuddy returned an empty value." 1305 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1306 | exit 25 1307 | elif [[ $? -eq 27 ]]; then 1308 | echo "Plistbuddy is trying to read from a file that does not exist." 1309 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1310 | exit 27 1311 | fi 1312 | 1313 | 1314 | installer_app_major_ver="$(echo $installer_app_ver | /usr/bin/cut -d . -f 1)" 1315 | installer_app_minor_ver="$(echo $installer_app_ver | /usr/bin/cut -d . -f 2)" 1316 | installer_app_patch_ver="$(echo $installer_app_ver | /usr/bin/cut -d . -f 3)" 1317 | 1318 | # Variables reliant on installer being on disk 1319 | min_req_os="$(/usr/libexec/PlistBuddy -c "print :LSMinimumSystemVersion" "$mac_os_installer_path"/Contents/Info.plist 2>/dev/null)" 1320 | 1321 | # Confirm that value returned by plistbuddy is valid 1322 | checkForPlistValue "$min_req_os" 1323 | 1324 | if [[ $? -eq 25 ]]; then 1325 | echo "Plistbuddy returned an empty value." 1326 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1327 | exit 25 1328 | elif [[ $? -eq 27 ]]; then 1329 | echo "Plistbuddy is trying to read from a file that does not exist." 1330 | "$jamfHelper" -windowType utility -icon "$alerticon" -title "Error" -description "$generic_error" -button1 "Exit" -defaultButton 1 & 1331 | exit 27 1332 | fi 1333 | 1334 | 1335 | min_req_os_maj="$(echo "$min_req_os" | /usr/bin/cut -d . -f 1)" 1336 | min_req_os_min="$(echo "$min_req_os" | /usr/bin/cut -d . -f 2)" 1337 | min_req_os_patch="$(echo "$min_req_os" | /usr/bin/cut -d . -f 3)" 1338 | [[ -z "$min_req_os_patch" ]] && min_req_os_patch="0" 1339 | required_space="$(/usr/bin/du -hsg "$mac_os_installer_path/Contents/SharedSupport" | /usr/bin/awk '{print $1}')" 1340 | needed_free_space="$(($required_space * 4))" 1341 | 1342 | # Run through a few post-download checks 1343 | checkForFreeSpace 1344 | checkMinReqOSVer 1345 | validateAppExpirationDate 1346 | checkForMountedInstallESD "/Volumes/InstallESD" 1347 | checkForMountedInstallESD "/Volumes/OS X Install ESD" 1348 | validatePostInstallPKG 1349 | # quitAllApps 1350 | 1351 | 1352 | if [[ -n "$approved_min_os_app_ver" ]]; then 1353 | echo "Minimum OS app version has been supplied. Performing check." 1354 | 1355 | # Determine the OS version included in the OS installer app 1356 | determineBaseOSVersion 1357 | 1358 | # Ensure the OS version included in the OS installer app is higher than the approved minimum OS version 1359 | meetsApprovedMinimumOSInstallerVersion 1360 | fi 1361 | 1362 | installOS "$heading" 1363 | 1364 | exit 0 -------------------------------------------------------------------------------- /ResetPrintSystem.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script will reset printing system 4 | # Build off work from http://www.cnet.com/news/what-does-the-reset-print-system-routine-in-os-x-do/ 5 | # and other posts on jamfnation.com 6 | # 7 | # For macOS 10.15, I recommend you use: 8 | # /System/Library/Frameworks/ApplicationServices.framework/Frameworks/PrintCore.framework/Versions/A/printtool --reset 9 | 10 | # Stop CUPS 11 | /bin/launchctl stop org.cups.cupsd 12 | 13 | # Backup Installed Printers Property List 14 | if [[ -e "/Library/Printers/InstalledPrinters.plist" ]]; then 15 | /bin/mv /Library/Printers/InstalledPrinters.plist /Library/Printers/InstalledPrinters.plist.bak 16 | fi 17 | 18 | # Backup the CUPS config file 19 | if [[ -e "/etc/cups/cupsd.conf" ]]; then 20 | /bin/mv /etc/cups/cupsd.conf /etc/cups/cupsd.conf.bak 21 | fi 22 | 23 | # Restore the default config by copying it 24 | if [[ ! -e "/etc/cups/cupsd.conf" ]]; then 25 | /bin/cp /etc/cups/cupsd.conf.default /etc/cups/cupsd.conf 26 | fi 27 | 28 | # Backup the printers config file 29 | if [[ -e "/etc/cups/printers.conf" ]]; then 30 | /bin/mv /etc/cups/printers.conf /etc/cups/printers.conf.bak 31 | fi 32 | 33 | # Start CUPS 34 | /bin/launchctl start org.cups.cupsd 35 | 36 | # Remove all printers 37 | /usr/bin/lpstat -p | /usr/bin/cut -d' ' -f2 | /usr/bin/xargs -I{} /usr/sbin/lpadmin -x {} 38 | 39 | exit 0 -------------------------------------------------------------------------------- /TCC.db Modifier.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # As soon as this is released, I'm going to guess that Apple will squash this: 4 | # Currently options are limited for allowing camera/microphone for certain apps on 5 | # enterprise-owned devices. You can only deny but not allow via configuration profiles. 6 | # If you'd like to "allow" on enterprise-own devices, file feedback with Apple: 7 | # http://feedbackassistant.apple.com 8 | # 9 | # In order for this script to function, the script or the application running the script 10 | # (e.g. Terminal, Jamf Pro) needs to have full disk access. 11 | # I have made this script to work with Jamf Pro. However modifying it to work with other 12 | # tools shouldn't be hard as long as you replace the Jamf parameters with the appropriate 13 | # values. 14 | # Couple of caveats to note: 15 | # -kTCCServiceMicrophone, kTCCServiceCamera, kTCCServiceUbiquity live in user's TCC.db 16 | # -You'll run into an read-only error when attempting to write to system TCC.db 17 | # kTCCServiceScreenCapture lives in system TCC.db. 18 | # 19 | # Credit goes to this discussion for helping me figure out how to generate a csreq blob 20 | # https://stackoverflow.com/questions/52706542/how-to-get-csreq-of-macos-application-on-command-line 21 | # 22 | # Jamf Parameters: 23 | # Required: Parameter $4 is used to provide the full path to the application 24 | # (e.g. /Application/Firefox.app) 25 | # Required: Parameter $5 is used to provide the TCC service name. 26 | # See $tcc_service_list array for valid entries. Of importance to you will be: 27 | # "kTCCServiceMicrophone" and "kTCCServiceCamera" 28 | # 29 | # Exit codes: 30 | # 1: No user is logged in 31 | # 2: Invalid application path provided 32 | # 3: TCC Services were not provided 33 | # 4: The provided TCC service is invalid. See $tcc_service_list array for valid entries. 34 | 35 | 36 | # List of all TCC services as of macOS 10.15 37 | tcc_service_list=( 38 | "kTCCServiceAddressBook" 39 | "kTCCServiceContactsLimited" 40 | "kTCCServiceContactsFull" 41 | "kTCCServiceCalendar" 42 | "kTCCServiceReminders" 43 | "kTCCServiceTwitter" 44 | "kTCCServiceFacebook" 45 | "kTCCServiceSinaWeibo" 46 | "kTCCServiceLiverpool" 47 | "kTCCServiceUbiquity" 48 | "kTCCServiceTencentWeibo" 49 | "kTCCServiceShareKit" 50 | "kTCCServicePhotos" 51 | "kTCCServicePhotosAdd" 52 | "kTCCServiceMicrophone" 53 | "kTCCServiceCamera" 54 | "kTCCServiceWillow" 55 | "kTCCServiceMediaLibrary" 56 | "kTCCServiceSiri" 57 | "kTCCServiceMotion" 58 | "kTCCServiceSpeechRecognition" 59 | "kTCCServiceAppleEvents" 60 | "kTCCServiceLinkedIn" 61 | "kTCCServiceAccessibility" 62 | "kTCCServicePostEvent" 63 | "kTCCServiceListenEvent" 64 | "kTCCServiceLocation" 65 | "kTCCServiceSystemPolicyAllFiles" 66 | "kTCCServiceSystemPolicySysAdminFiles" 67 | "kTCCServiceSystemPolicyDeveloperFiles" 68 | "kTCCServiceSystemPolicyRemovableVolumes" 69 | "kTCCServiceSystemPolicyNetworkVolumes" 70 | "kTCCServiceSystemPolicyDesktopFolder" 71 | "kTCCServiceSystemPolicyDownloadsFolder" 72 | "kTCCServiceSystemPolicyDocumentsFolder" 73 | "kTCCServiceScreenCapture" 74 | "kTCCServiceDeveloperTool" 75 | "kTCCServiceFileProviderPresence" 76 | "kTCCServiceFileProviderDomain" 77 | ) 78 | 79 | # Get current time in epoch seconds 80 | current_time="$(/bin/date +"%s")" 81 | 82 | # Get current logged in user 83 | logged_in_user="$(/usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }')" 84 | 85 | # Get current logged in user's home directory 86 | [[ "$logged_in_user" ]] && logged_in_user_home="$(/usr/bin/dscl /Local/Default read /Users/"$logged_in_user" NFSHomeDirectory | /usr/bin/awk '{print $2}')" 87 | 88 | # Jamf Parameters 89 | app_path="${4}" 90 | service_access="${5}" 91 | permission="1" # allow. if you need to deny, use a configuration profile. 92 | 93 | # Validate parameters 94 | [[ -z "$app_path" || ! -e "$app_path" ]] && echo "Invalid application path." && exit 2 95 | [[ -z "$service_access" ]] && echo "TCC Services not provided" && exit 3 96 | 97 | # Variables Dependent on Jamf Parameters 98 | app_identifier="$(/usr/libexec/PlistBuddy -c "print :CFBundleIdentifier" "$app_path"/Contents/Info.plist 2>/dev/null)" 99 | 100 | # Generate an array of services 101 | svc_list=($(echo $service_access)) 102 | 103 | # Function to get csreq blob 104 | getCSREQBlob(){ 105 | # Get the requirement string from codesign 106 | req_str=$(/usr/bin/codesign -d -r- "$app_path" 2>&1 | /usr/bin/awk -F ' => ' '/designated/{print $2}') 107 | 108 | # Convert the requirements string into it's binary representation 109 | # csreq requires the output to be a file so we just throw it in /tmp 110 | echo "$req_str" | /usr/bin/csreq -r- -b /tmp/csreq.bin 111 | 112 | # Convert the binary form to hex, and print it nicely for use in sqlite 113 | req_hex="X'$(xxd -p /tmp/csreq.bin | /usr/bin/tr -d '\n')'" 114 | 115 | echo "$req_hex" 116 | 117 | # Remove csqeq.bin 118 | /bin/rm -f "/tmp/csreq.bin" 119 | } 120 | 121 | req_hex="$(getCSREQBlob)" 122 | 123 | # Loop through services and provide access 124 | for svc in $svc_list; do 125 | # Check to make sure that a valid TCC service was provided 126 | if [[ ${tcc_service_list[(ie)$svc]} -le ${#tcc_service_list} ]]; then 127 | # Certain TCC services are user specific 128 | if [[ "$svc" == "kTCCServiceMicrophone" ]] || [[ "$svc" == "kTCCServiceCamera" ]] || [[ "$svc" == "kTCCServiceUbiquity" ]]; then 129 | if [[ -z "$logged_in_user" ]]; then 130 | echo "No user logged in. User needs to be logged in to modify their TCC.db with $svc service. Exiting script." 131 | exit 1 132 | fi 133 | 134 | /usr/bin/sqlite3 "$logged_in_user_home/Library/Application Support/com.apple.TCC/TCC.db" "INSERT or REPLACE INTO access (service,client,client_type,allowed,prompt_count,csreq,last_modified) 135 | VALUES('$svc','$app_identifier','0','$permission','1',$req_hex,'$current_time')" 136 | else 137 | /usr/bin/sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "INSERT or REPLACE INTO access (service,client,client_type,allowed,prompt_count,csreq,last_modified) 138 | VALUES('$svc','$app_identifier','0','$permission','1',$req_hex,'$current_time')" 139 | fi 140 | else 141 | echo "$svc is not a valid TCC service" 142 | exit 4 143 | fi 144 | done 145 | 146 | exit 0 -------------------------------------------------------------------------------- /Trigger_Validation_For_macOS_Installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # As part Gatekeeper, macOS runs a code signature validation scan on all apps when they 4 | # are run the first time. This results in some really large apps taking 2-3 minutes 5 | # before they can run. Some apps include: Xcode, Matlab, Mathematica, etc. 6 | # 7 | # The macOS installer app for Big Sur suffers from this and as a result if you try to 8 | # run the "install macOS Big Sur.app" it will ultimately take 2 minutes or more while it scans. 9 | # If you run the app through the GUI, the app will simply bounce in the dock. 10 | # If you run the app through the CLI using "startosinstall", it will show no activity until the scan completes. 11 | # There's no way to force macOS to scan a particular app for its code signature validation 12 | # other than actually trying to run it. 13 | # 14 | # The below code is meant to be used as a workaround by triggering the --usage option 15 | # in "startosinstall" which does not actually run an upgrade. 16 | # macOS will start to scan "Install macOS Big Sur.app" silently in the background 17 | # 18 | # This code is designed to run immediately after "Install macOS Big Sur.app" has been 19 | # installed. The idea is to make the user experience better so that when the user launches 20 | # an OS upgrade whether through the GUI or a Self Service workflow relying on "startosinstall" 21 | # there's no a 2-3 minute period of silence. 22 | # 23 | # File feedback with Apple so that they can improve this experience and macOS installer 24 | # apps can be scanned immediately after install. 25 | # 26 | # This script makes use of Jamf Pro script parameters: 27 | # Parameter 4: supply the full path to the installer (e.g. /Applications/Install macOS Big Sur.app) 28 | 29 | # Path to macOS installer app 30 | mac_os_installer_path="${4}" 31 | 32 | if [[ -e "$mac_os_installer_path" ]]; then 33 | "$mac_os_installer_path"/Contents/Resources/startosinstall --usage &>/dev/null 34 | fi 35 | 36 | exit 0 -------------------------------------------------------------------------------- /com.company.reconafterupdate: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.company.reconafterupdate 7 | ProgramArguments 8 | 9 | /bin/bash 10 | -c 11 | if [[ "$(/usr/bin/mdfind -onlyin /System/Library/CoreServices/ '((kMDItemFSName = "SystemVersion.plist") && InRange(kMDItemDateAdded,$time.today,$time.today(+1)))' -count)" == 1 ]]; then /usr/local/bin/jamf recon && /bin/launchctl bootout system/com.company.reconafterupdate; fi; 12 | 13 | RunAtLoad 14 | 15 | ThrottleInterval 16 | 60 17 | StartInterval 18 | 60 19 | 20 | 21 | -------------------------------------------------------------------------------- /jamfHelperScreen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 05/19/17 4 | # Written by Balmes Pavlov 5 | # 6 | # This script is intended to be used with Jamf Pro. 7 | # The purpose of this script is to load the JAMFHelper dialog in when a policy runs. 8 | # Parameters will be used to determine the Window Type, Header, Description, Icon, and Button Text. They are described in full detail below. 9 | # The JAMFHelper has many more functions beyond what's used in this script. This script was intended to cover the features I'd most likely use 10 | # while making it flexible enough to use in multiple policies. 11 | # 12 | # Parameters: 13 | # Required: $4 is the window type used by JamfHelper. There are only three possible values: fs, hud, and utility. Note: All these window types can be exited using CMD + Q. 14 | # hud: creates an Apple "Heads Up Display" style window 15 | # utility: creates an Apple "Utility" style window 16 | # fs: creates a full screen window the restricts all user input 17 | # Required: $5 is the title text used by JamfHelper for the dialog window. Does not appear in the fullscreen dialog, but you still need to fill it out. 18 | # Required: $6 is the header text used by JamfHelper. 19 | # Required: $7 is the description message used by JamfHelper. 20 | # Optional: $8 is the icon path used by JamfHelper. Do not escape characters. If not using one, leave empty. Heavily recommended to use it otherwise dialogs look weird. 21 | # e.g. /My Directory.app/icon.icns is a valid path. /My\ Directory.app/icon.icns is not a valid path. 22 | # Optional: $9 is the text in the first button. Requires that $4 be set to "utility" or "hud" otherwise the value in this parameter will be ignored. 23 | # Pressing the button will not have any effect other than to cause the dialog window to close. 24 | # 25 | # Exit Codes: 26 | # 1. Indicates that there is a parameter missing. 27 | # 28 | # The following variable is used for JAMF Helper. While it will pick up text from JSS parameter 6, there is extra text that is hard coded which perhaps you want to modify. 29 | # 30 | # IT Contact Info 31 | # You can supply an email address or contact number for your end users to contact you. This will appear in JAMF Helper dialogs. 32 | # If left blank, it will default to just "IT" which may not be as helpful to your end users. 33 | it_contact="IT@contoso.com" 34 | 35 | if [[ -z "$it_contact" ]]; then 36 | it_contact="IT" 37 | fi 38 | 39 | message="${7} 40 | 41 | This update may take up to 20 minutes, but if you're on a slower connection it can take substantially longer. If it takes longer than expected, please contact: $it_contact. 42 | 43 | START TIME: $(/bin/date)" 44 | 45 | # Modify the code below at your own risk. 46 | window_type="${4}" 47 | title="${5}" 48 | header="${6}" 49 | icon="${8}" 50 | button_one="${9}" 51 | 52 | jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper" 53 | path_to_jhpid="/tmp/jamfHelper_PID.txt" 54 | 55 | display_message (){ 56 | shopt -s nocasematch 57 | 58 | if [[ "$1" = "fs" ]]; then 59 | if [[ -n "$3" ]] && [[ -n "$4" ]]; then 60 | if [[ -z "$5" ]]; then 61 | "$jamfHelper" -windowType "$1" -title "$2" -heading "$3" -description "$4" & /bin/echo $! > "$path_to_jhpid" 62 | exit 63 | elif [[ -n "$5" ]]; then 64 | "$jamfHelper" -windowType "$1" -title "$2" -heading "$3" -description "$4" -icon "$5" & /bin/echo $! > "$path_to_jhpid" 65 | exit 66 | fi 67 | elif [[ -z "$2" ]] || [[ -z "$3" ]] || [[ -z "$4" ]]; then 68 | /bin//bin/echo "You are missing a parameter in the JSS. Please make sure to fill in all JSS parameters." 69 | exit 1 70 | fi 71 | elif [[ "$1" = "utility" ]] || [[ "$1" = "hud" ]]; then 72 | if [[ -n "$2" ]] && [[ -n "$3" ]] && [[ -n "$4" ]]; then 73 | if [[ -z "$5" ]] && [[ -n "$6" ]]; then 74 | "$jamfHelper" -windowType "$1" -title "$2" -heading "$3" -description "$4" -button1 "$6" -defaultButton 1 & /bin/echo $! > "$path_to_jhpid" 75 | exit 76 | elif [[ -z "$5" ]] && [[ -z "$6" ]]; then 77 | "$jamfHelper" -windowType "$1" -title "$2" -heading "$3" -description "$4" & /bin/echo $! > "$path_to_jhpid" 78 | exit 79 | elif [[ -n "$5" ]] && [[ -z "$6" ]]; then 80 | "$jamfHelper" -windowType "$1" -title "$2" -heading "$3" -description "$4" -icon "$5" & /bin/echo $! > "$path_to_jhpid" 81 | exit 82 | elif [[ -n "$5" ]] && [[ -n "$6" ]]; then 83 | "$jamfHelper" -windowType "$1" -title "$2" -heading "$3" -description "$4" -icon "$5" -button1 "$6" -defaultButton 1 & /bin/echo $! > "$path_to_jhpid" 84 | exit 85 | fi 86 | fi 87 | fi 88 | 89 | shopt -u nocasematch 90 | } 91 | 92 | display_message "$window_type" "$title" "$header" "$message" "$icon" "$button_one" 93 | 94 | /bin/echo "You are missing a parameter in the JSS. Please make sure to fill in all JSS parameters." 95 | exit 1 -------------------------------------------------------------------------------- /tcc_reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # 11/13/19 4 | # Written by Balmes Pavlov 5 | # 6 | # "tccutil reset All" does not quite work as you'd expect it to in 10.14. 7 | # Therefore we need to create reset each service one by one which is time consuming. 8 | # There's 40 services as of 10.15. I hope to update this in future OS versions with 9 | # whatever additional services get added. 10 | # Note: Location seems to always error out. If you can figure out why, drop me a note. 11 | # 12 | # To determine the list of services make sure you have Xcode CLI tools and run: 13 | # strings /System/Library/PrivateFrameworks/TCC.framework/TCC | grep "^kTCCService[^ ]*$" 14 | # Hopefully it continues to work in future OS versions 15 | # 16 | # Original inspiration: https://gist.github.com/haircut/aeb22c853b0ae4b483a76320ccc8c8e9 17 | # Why? Because no one knows what Python's future on macOS is. 18 | 19 | # Determine macOS major version 20 | os_major_ver="$(/usr/bin/sw_vers -productVersion | /usr/bin/cut -d . -f 2)" 21 | 22 | # List of all TCC services as of macOS 10.14 23 | tcc_service_list14=( 24 | "All" #10.14+ 25 | "AddressBook" #10.14+ 26 | "Calendar" #10.14+ 27 | "Reminders" #10.14+ 28 | "Twitter" #10.14+ 29 | "Facebook" #10.14+ 30 | "SinaWeibo" #10.14+ 31 | "Liverpool" #10.14+ 32 | "Ubiquity" #10.14+ 33 | "TencentWeibo" #10.14+ 34 | "ShareKit" #10.14+ 35 | "Photos" #10.14+ 36 | "PhotosAdd" #10.14+ 37 | "Microphone" #10.14+ 38 | "Camera" #10.14+ 39 | "Willow" #10.14+ 40 | "MediaLibrary" #10.14+ 41 | "Siri" #10.14+ 42 | "AppleEvents" #10.14+ 43 | "LinkedIn" #10.14+ 44 | "Accessibility" #10.14+ 45 | "PostEvent" #10.14+ 46 | "Location" #10.14+ 47 | "SystemPolicyAllFiles" #10.14+ 48 | "SystemPolicySysAdminFiles" #10.14+ 49 | "SystemPolicyDeveloperFiles" #10.14+ 50 | ) 51 | 52 | # List of all additional TCC services as of macOS 10.15 53 | tcc_service_list15=( 54 | "ContactsLimited" #10.15+ 55 | "ContactsFull" #10.15+ 56 | "Motion" #10.15+ 57 | "SpeechRecognition" #10.15+ 58 | "ListenEvent" #10.15+ 59 | "SystemPolicyRemovableVolumes" #10.15+ 60 | "SystemPolicyNetworkVolumes" #10.15+ 61 | "SystemPolicyDesktopFolder" #10.15+ 62 | "SystemPolicyDownloadsFolder" #10.15+ 63 | "SystemPolicyDocumentsFolder" #10.15+ 64 | "ScreenCapture" #10.15+ 65 | "DeveloperTool" #10.15+ 66 | "FileProviderPresence" #10.15+ 67 | "FileProviderDomain" #10.15+ 68 | ) 69 | 70 | # Generate empty array that will determine which services to reset 71 | tcc_service_list=() 72 | 73 | [[ $os_major_ver -le 13 ]] && echo "Unsupported OS" && exit 0 74 | [[ $os_major_ver -ge 14 ]] && tcc_service_list+=($tcc_service_list14) 75 | [[ $os_major_ver -ge 15 ]] && tcc_service_list+=($tcc_service_list15) 76 | 77 | # Loop through all services and reset them 78 | for svc in $tcc_service_list; do 79 | /usr/bin/tccutil reset "$svc" 2>/dev/null 80 | 81 | # Provide feedback on success of reset 82 | if [[ $? -eq 0 ]]; then 83 | echo "Successful Reset: $svc" 84 | else 85 | echo "Failed Reset: $svc" 86 | fi 87 | done --------------------------------------------------------------------------------