├── .gitattributes ├── .gitignore ├── Extension Attributes ├── currentdeferral-ea.sh ├── lastdefercount-ea.sh └── lastupdate-ea.sh ├── Jamf Pro Scripts ├── cached pkg installer.sh ├── cached pkg processor.sh ├── download macos installer.sh └── self service macos upgrade-or-erase.sh ├── LICENSE ├── Package ├── ROOT │ ├── Applications │ │ └── Utilities │ │ │ └── README.txt │ └── Library │ │ └── LaunchDaemons │ │ └── com.corp.patcher-every2h.plist └── scripts │ ├── postinstall.sh │ └── preinstall.sh └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /Extension Attributes/currentdeferral-ea.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # EA to show how many deferrals are currently used 4 | 5 | # Reads from the deferral file on the mac if it exists 6 | # We return an output as an integer only for proper audit and processing later. 7 | 8 | test=$( /usr/bin/plutil -extract deferral raw -o - /usr/local/corp/cachedapps/appupdates.plist ) 9 | 10 | if [ ! -z "$test" ]; 11 | then 12 | echo "$test" 13 | else 14 | echo "0" 15 | fi 16 | -------------------------------------------------------------------------------- /Extension Attributes/lastdefercount-ea.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # EA to show how many deferrals were used at last update 4 | 5 | # Reads from the deferral file on the mac if it exists 6 | # We return an output as an integer only for proper audit and processing later. 7 | 8 | test=$( /usr/bin/plutil -extract lastdeferral raw -o - /usr/local/corp/cachedapps/appupdates.plist ) 9 | 10 | if [ ! -z "$test" ]; 11 | then 12 | echo "$test" 13 | else 14 | echo "0" 15 | fi 16 | -------------------------------------------------------------------------------- /Extension Attributes/lastupdate-ea.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # EA to show the date of last update 4 | 5 | # Reads from the deferral file on the mac if it exists 6 | # We return an output as a Jamf compliant date YYYY-MM-DD hh:mm:ss 7 | 8 | test=$( /usr/bin/plutil -extract lastupdate raw -o - /usr/local/corp/cachedapps/appupdates.plist | /usr/bin/tr 'T' ' ' | /usr/bin/tr -d "Z" ) 9 | 10 | if [ ! -z "$test" ]; 11 | then 12 | echo "$test" 13 | else 14 | echo "1970-01-01 09:00:00" 15 | fi 16 | -------------------------------------------------------------------------------- /Jamf Pro Scripts/cached pkg installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Main patching and installer script 4 | # Meant to be run periodically from launchd on macOS endpoint. 5 | # Now with silent loginwindow support. 6 | # richard@richard-purves.com - 17-03-2023 - v2.3 7 | 8 | # Logging output to a file for testing 9 | #set -x 10 | #logfile=/Users/Shared/cachedappinstaller.log 11 | #exec > $logfile 2>&1 12 | 13 | # Set user display messages here 14 | msgtitlenewsoft="New Software Available" 15 | msgprogresstitle="Updating Software" 16 | msgosupgtitle="Upgrading macOS" 17 | msgnewsoftware="Important software updates are available! 18 | 19 | The following new software is ready for upgrade:" 20 | msgnewsoftforced="Important software updates are available! 21 | 22 | You have run out of allowed install deferrals. 23 | 24 | The following software will be upgraded now:" 25 | msgrebootwarning="Your computer now needs to restart to complete the updates." 26 | msgpowerwarning="Your computer is about to upgrade installed software. 27 | 28 | Please ensure you are connected to AC Power. 29 | 30 | Once you are plugged in, please click the Proceed button." 31 | msgosupgradewarning="Your computer will perform a major macOS upgrade. 32 | 33 | Please ensure you are connected to AC Power. 34 | 35 | Your computer will restart and the OS upgrade process will continue. It will take up to 90 minutes to complete. 36 | 37 | IT IS VERY IMPORTANT YOU DO NOT INTERRUPT THIS PROCESS." 38 | 39 | # Script variables here 40 | alloweddeferral="5" 41 | forcedupdate="0" 42 | blockingapps=( "Microsoft PowerPoint" "Keynote" "zoom.us" ) 43 | silent="0" 44 | waitroom="/Library/Application Support/JAMF/Waiting Room" 45 | workfolder="/usr/local/corp" 46 | infofolder="$workfolder/cachedapps" 47 | imgfolder="$workfolder/imgs" 48 | updatefilename="appupdates.plist" 49 | updatefile="$infofolder/$updatefilename" 50 | pbjson="/private/tmp/progressbar.json" 51 | canceljson="/private/tmp/progresscancel.json" 52 | installoutput="/private/tmp/installout.log" 53 | stosout="/private/tmp/upgrade.log" 54 | jsspkginfo="/private/tmp/jsspkginfo.tsv" 55 | 56 | jssurl=$( /usr/bin/defaults read /Library/Preferences/com.jamfsoftware.jamf.plist jss_url ) 57 | jb=$( which jamf ) 58 | osa="/usr/bin/osascript" 59 | pbapp="/Applications/Utilities/Progress.app" 60 | pb="$pbapp/Contents/MacOS/Progress" 61 | installiconpath="/System/Library/CoreServices/Installer.app/Contents/Resources" 62 | updateicon="/System/Library/PreferencePanes/SoftwareUpdate.prefPane/Contents/Resources/SoftwareUpdate.icns" 63 | currentuser=$( /usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk -F': ' '/[[:space:]]+Name[[:space:]]:/ { if ( $2 != "loginwindow" ) { print $2 }}' ) 64 | 65 | if [ ! -z "$currentuser" ]; 66 | then 67 | curuserid=$( id -u "$currentuser" 2>/dev/null ) 68 | else 69 | curuserid="501" 70 | fi 71 | 72 | homefolder=$( dscl . -read /Users/$currentuser NFSHomeDirectory | awk '{ print $2 }' ) 73 | bootvolname=$( /usr/sbin/diskutil info / | /usr/bin/awk '/Volume Name:/ { print substr($0, index($0,$3)) ; }' ) 74 | 75 | # Check that the info folder exists, create if missing and set appropriate permissions 76 | /bin/mkdir -p "$infofolder" 77 | /bin/chmod 755 "$infofolder" 78 | /usr/sbin/chown root:wheel "$infofolder" 79 | 80 | #################### 81 | # # 82 | # Let's get to it! # 83 | # # 84 | #################### 85 | 86 | # Keep the mac awake while this runs. 87 | /usr/bin/caffeinate -dis & 88 | 89 | # Stop IFS splitting on spaces 90 | OIFS=$IFS 91 | IFS=$'\n' 92 | 93 | # Is anyone logged in? Engage silent mode. 94 | [ "$currentuser" = "loginwindow" ] || [ -z "$currentuser" ] && silent="1" 95 | echo "Current user: $currentuser" 96 | 97 | # Is the screen locked? Quit if idle time under eight hours. 98 | if [ "$(/usr/libexec/PlistBuddy -c "print :IOConsoleUsers:0:CGSSessionScreenIsLocked" /dev/stdin 2>/dev/null <<< "$(/usr/sbin/ioreg -n Root -d1 -a)")" = "true" ]; 99 | then 100 | idletime=$( /usr/sbin/ioreg -c IOHIDSystem | /usr/bin/awk '/HIDIdleTime/ {print int($NF/1000000000); exit}') 101 | echo "Screen Locked. User idle time: $idletime" 102 | [ "$idletime" -lt 14400 ] && { echo "Idle time less than four hours. Exiting."; exit 0; } 103 | else 104 | echo "Screen Unlocked." 105 | fi 106 | 107 | # Blocking application check here. Find the foreground app with lsappinfo assuming silent mode isn't engaged 108 | if [ "$silent" = "0" ]; 109 | then 110 | foregroundapp=$( /usr/bin/lsappinfo list | /usr/bin/grep -B 4 "(in front)" | /usr/bin/awk -F '\\) "|" ASN' 'NF > 1 { print $2 }' ) 111 | 112 | # check for blocking apps 113 | for app ($blockingapps) 114 | do 115 | [ "$app" = "$foregroundapp" ] && { echo "Blocking app: $app"; exit 0; } 116 | done 117 | fi 118 | 119 | ######################################## 120 | # 121 | # Find and process any cached installers 122 | # 123 | ######################################## 124 | 125 | # Do we have a temp file that still exists? Clear it if so to avoid duplicate entries 126 | [ -f "$jsspkginfo" ] && /bin/rm "$jsspkginfo" 127 | 128 | # Find all previously cached files 129 | cachedpkg=($( find "$infofolder" -type f \( -iname "*.plist" ! -iname "$updatefilename" \) )) 130 | 131 | # Did we find any? Quit if not. 132 | [ ${#cachedpkg[@]} = 0 ] && { echo "No cached files found. Exiting."; exit 0; } 133 | 134 | # Process the array of files into a tsv for later sorting 135 | for pkgfilename ($cachedpkg) 136 | do 137 | # Now read out all the info we've collected from the cache file. 138 | priority=$( /usr/bin/plutil -extract Priority raw -o - "${pkgfilename}" ) 139 | pkgname=$( /usr/bin/plutil -extract PkgName raw -o - "${pkgfilename}" ) 140 | displayname=$( /usr/bin/plutil -extract DisplayName raw -o - "${pkgfilename}" ) 141 | fullpath=$( /usr/bin/plutil -extract FullPath raw -o - "${pkgfilename}" ) 142 | reboot=$( /usr/bin/plutil -extract Reboot raw -o - "${pkgfilename}" ) 143 | feu=$( /usr/bin/plutil -extract FEU raw -o - "${pkgfilename}" ) 144 | fut=$( /usr/bin/plutil -extract FUT raw -o - "${pkgfilename}" ) 145 | osinstall=$( /usr/bin/plutil -extract OSInstaller raw -o - "${pkgfilename}" ) 146 | 147 | # Check for any spurious no name filenames. Skip if found. 148 | [ "$pkgfilename" = ".plist" ] && continue 149 | 150 | # Check to see if any of the critical fields are blank. Skip if so. 151 | [ -z "$pkgname" ] || [ -z "$fullpath" ] && continue 152 | 153 | # Check to see if we've a match in the cached folder. Skip if not. 154 | # We check for both file and directory in case of dmg, flat pkg or non flat pkg. 155 | [ ! -f "$fullpath/$pkgname" ] && [ ! -d "$fullpath/$pkgname" ] && continue 156 | 157 | # Store everything into a tsv temporary file 158 | echo -e "${priority}\t${pkgname}\t${displayname}\t${fullpath}\t${reboot}\t${feu}\t${fut}\t${osinstall}" >> "$jsspkginfo" 159 | 160 | # Clean up of variables before next loop or end of loop 161 | unset priority pkgname displayname fullpath reboot feu fut osinstall 162 | done 163 | 164 | # Did we even write out a tsv file 165 | if [ ! -f "$jsspkginfo" ]; 166 | then 167 | # Output fail message, then clean up files and folders 168 | echo "No processed tsv file detected. Aborting." 169 | /usr/bin/find "$infofolder" -type f \( -iname "*.plist" ! -iname "$updatefilename" \) -exec rm {} \; 170 | /usr/bin/find "$waitroom" \( -iname \*.pkg -o -iname \*.cache.xml \) -exec rm {} \; 171 | exit 0 172 | fi 173 | 174 | # Sort the file using priority number, then alphabetical order on filename 175 | /usr/bin/sort "$jsspkginfo" -o "$jsspkginfo" 176 | 177 | # Check to see if there's a macOS installer 178 | # placed a space between the -d and the $ due to suspected Jamf PI being logged. 179 | osinstall=$( /usr/bin/tail -n 1 "$jsspkginfo" | /usr/bin/cut -f2 -d $'\t' ) 180 | [[ "$osinstall" == *"Install macOS"* ]] && osinstall="1" || osinstall="0" 181 | 182 | ################################ 183 | # 184 | # Prompt the user about updating 185 | # 186 | ################################ 187 | 188 | # Do we have a defer file. Initialize one if not. 189 | [ ! -f "$updatefile" ] && /usr/bin/defaults write "$updatefile" deferral -int 0 190 | 191 | # Read deferral count 192 | deferred=$( /usr/bin/defaults read "$updatefile" deferral ) 193 | 194 | # Work out icon path for osascript. It likes paths in the old : separated format 195 | [ -f "$installiconpath/Installer.icns" ] && icon="$installiconpath/Installer.icns" 196 | [ -f "$installiconpath/AppIcon.icns" ] && icon="$installiconpath/AppIcon.icns" 197 | iconposix=$( echo $icon | /usr/bin/sed 's/\//:/g' ) 198 | iconposix="$bootvolname$iconposix" 199 | 200 | # Prepare list of apps to install in readable format 201 | # Remove trailing blank lines to avoid parsing issues 202 | while read line || [ -n "$line" ]; 203 | do 204 | applist+=$( echo $line | cut -f2 -d$'\t' )"\\n" 205 | done < "$jsspkginfo" 206 | 207 | applist=$( echo $applist | awk /./ ) 208 | 209 | # Check deferral count. Prompt user if under, otherwise force the issue. 210 | # If silent mode then skip all this and just do it 211 | if [ "$silent" = "0" ]; 212 | then 213 | if [ "$deferred" -lt "$alloweddeferral" ]; 214 | then 215 | # Prompt user that updates are ready. Allow deferral. 216 | test=$( /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "'"$msgnewsoftware"'\n\n'"$applist"'\n\nAuto deferral in 60 seconds." giving up after 60 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Install", "Defer"} default button 2' ) 217 | 218 | # Did we defer? 219 | if [ $( echo $test | /usr/bin/grep -c -e "Defer" -e "gave up:true" ) = "1" ]; 220 | then 221 | # Increment counter and store. 222 | deferred=$(( deferred + 1 )) 223 | /usr/bin/defaults write "$updatefile" deferral -int "$deferred" 224 | 225 | # Notify user how many deferrals are left and exit. 226 | /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "You have used '"$deferred"' of '"$alloweddeferral"' allowed upgrade deferrals." giving up after 60 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Ok"} default button 1' 227 | exit 0 228 | fi 229 | else 230 | # Prompt user that updates are happening right now. 231 | forced="1" 232 | /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "'"$msgnewsoftforced"'\n\n'"$applist"'\n\nThe upgrade will start in 60 seconds." giving up after 60 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Install"} default button 1' 233 | fi 234 | fi 235 | 236 | # Store deferrals used, time of last update and then reset the deferral count 237 | /usr/bin/defaults write "$updatefile" lastdeferral -int $( /usr/bin/defaults read "$updatefile" deferral ) 238 | /usr/bin/defaults write "$updatefile" lastupdate -date "$( /bin/date "+%Y-%m-%d %H:%M:%S" )" 239 | /usr/bin/defaults write "$updatefile" deferral -int 0 240 | 241 | ################################### 242 | # 243 | # Check to see if we're on AC power 244 | # 245 | ################################### 246 | 247 | if [ "$silent" = "0" ]; 248 | then 249 | # Check if device is on battery or ac power 250 | # Valid reports are `Battery Power` or `AC Power` 251 | pwrAdapter=$( /usr/bin/pmset -g ps | /usr/bin/grep "Now drawing" | /usr/bin/cut -d "'" -f2 ) 252 | 253 | # Warn the user if not on AC power 254 | count=1 255 | while [ "$count" -le "3" ]; 256 | do 257 | [ "$pwrAdapter" = "AC Power" ] && break 258 | count=$(( count + 1 )) 259 | /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "'"$msgpowerwarning"'" giving up after 60 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Proceed"} default button 1' 260 | pwrAdapter=$( /usr/bin/pmset -g ps | /usr/bin/grep "Now drawing" | /usr/bin/cut -d "'" -f2 ) 261 | done 262 | 263 | ################################ 264 | # 265 | # Close all running applications 266 | # 267 | ################################ 268 | 269 | # Less severe way of closing only the applications we need to 270 | # Iterate through previously captured tsv file and strip off versioning 271 | # Now strip off any trailing whitespace to stop things failing 272 | # Finally work out PID numbers from a wildcard search of what's left and ... 273 | # kill -9 them all. 274 | while read line || [ -n "$line" ]; 275 | do 276 | app=$( echo $line | /usr/bin/cut -f3 -d$'\t' | /usr/bin/sed "s/[-0-9.]*$//" ) 277 | [ "${app: -1}" = " " ] && app=${app[1,-2]} 278 | pids=$( /bin/ps ax | /usr/bin/grep -i "$app" | /usr/bin/grep -v grep | /usr/bin/awk '{ print $1 }' ) 279 | [ ! -z "$pids" ] && echo $pids | /usr/bin/xargs kill -9 280 | done 281 | fi 282 | 283 | ###################################### 284 | # 285 | # Start installing from the build list 286 | # 287 | ###################################### 288 | 289 | # Store total number of applications to install 290 | # Displayed to user later during progress bar 291 | appcounter=0 292 | totalappnumber=${#cachedpkg[@]} 293 | 294 | # Invoke our progress bar application. 295 | # Set initial message and then run the app as a background app. 296 | cat < "$pbjson" 297 | { 298 | "percentage": -1, 299 | "title": "$msgprogresstitle", 300 | "message": "Preparing to upgrade ...", 301 | "icon": "$updateicon" 302 | } 303 | EOF 304 | 305 | [ "$silent" = "0" ] && $pb $pbjson $canceljson & 306 | 307 | # Read the tsv line by line and install 308 | while read line 309 | do 310 | # Increment the app counter 311 | appcounter=$(( appcounter + 1 )) 312 | 313 | # Batch process the line we just read out into its component parts 314 | pkgname=$( echo "$line" | cut -f2 ) 315 | displayname=$( echo "$line" | cut -f3 ) 316 | fullpath=$( echo "$line" | cut -f4 ) 317 | reboot=$( echo "$line" | cut -f5 ) 318 | feu=$( echo "$line" | cut -f6 ) 319 | fut=$( echo "$line" | cut -f7 ) 320 | osinstall=$( echo "$line" | cut -f8 ) 321 | 322 | # Does this or any other installer require a restart. 323 | # Mark it so with an empty file. 324 | [ "$reboot" = "1" ] && touch /private/tmp/.apppatchreboot 325 | 326 | # Have the Jamf FEU/FUT options been set for this package 327 | [ "$feu" = "1" ] && installoption="$installoption -feu" 328 | [ "$fut" = "1" ] && installoption="$installoption -fut" 329 | 330 | # Is this an OS install. Break out of the loop. Handle this separately. 331 | [ "$osinstall" = "1" ] && continue 332 | 333 | # Perform the installation as a background task with the correct options 334 | # Output progress to a text file. We'll use that next. 335 | /usr/sbin/installer -target / -package "${fullpath}/${pkgname}" -verboseR > "$installoutput" & 336 | 337 | while :; 338 | do 339 | # Wait three seconds for Progress to update, then work out current percentage. 340 | # We make sure the percentage never hits 100 or the progress bar will stop. 341 | sleep 1 342 | 343 | # Did someone try to hit the cancel button? Kill the generated file and restart the progress bar. 344 | # They had a chance to defer earlier. 345 | if [ "$forced" = "0" ]; 346 | then 347 | if [ -f "$canceljson" ]; 348 | then 349 | /bin/rm "$canceljson" 350 | /usr/bin/killall Progress 2>/dev/null 351 | /bin/rm /private/tmp/.apppatchreboot 352 | break 353 | fi 354 | else 355 | [ -f "$canceljson" ] && { /bin/rm "$canceljson"; killall Progress; $pb $pbjson $canceljson &; } 356 | fi 357 | 358 | percent=$( /bin/cat "$installoutput" | /usr/bin/grep "installer:%" | /usr/bin/cut -d"%" -f2 | /usr/bin/awk '{ print int($1) }' | /usr/bin/tail -n1 ) 359 | [ "$percent" -eq 100 ] && percent="99" 360 | 361 | # Correct the file that Progress is using to display the bar. 362 | cat < "$pbjson" 363 | { 364 | "percentage": $percent, 365 | "title": "Installing $displayname", 366 | "message": "Install $percent% completed - (Updating $appcounter of $totalappnumber)", 367 | "icon": "$updateicon" 368 | } 369 | EOF 370 | 371 | # Check to see if we've had a finished install. Break out the loop if so. 372 | complete=$( /bin/cat "$installoutput" | /usr/bin/grep -c -E "successful|failed" ) 373 | [ "$complete" != "0" ] && { /bin/rm -f "$installoutput"; break; } 374 | done 375 | 376 | # Clean up of variables before next loop or end of loop 377 | unset priority pkgname displayname fullpath reboot feu fut 378 | 379 | done < "$jsspkginfo" 380 | 381 | # Finally end the progress bar 382 | if [ "$osinstall" = "0" ]; 383 | then 384 | cat < "$pbjson" 385 | { 386 | "percentage": 99, 387 | "title": "$msgprogresstitle", 388 | "message": "Application Updates Completed", 389 | "icon": "$updateicon" 390 | } 391 | EOF 392 | fi 393 | 394 | # Kill Progress and warn the user if any impending reboots, if not in silent mode 395 | if [ "$silent" = "0" ]; 396 | then 397 | [ -f /private/tmp/.apppatchreboot ] && "$osa" -e 'display dialog "'"$msgrebootwarning"'" giving up after 15 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Ok"} default button 1' 398 | fi 399 | 400 | ############################ 401 | # 402 | # OS Upgrade code goes here 403 | # 404 | ############################ 405 | 406 | if [ "$osinstall" = "1" ]; 407 | then 408 | echo "Update macOS initiated" 409 | # Work out where startosinstaller binary is located 410 | # Most of this work should be done already from the cached file info. 411 | startos=$( /usr/bin/find "$fullpath/$pkgname" -iname "startosinstall" -type f ) 412 | 413 | # Set up a future interminate progress bar here. We'll invoke this after. 414 | cat < "$pbjson" 415 | { 416 | "percentage": -1, 417 | "title": "$msgosupgtitle", 418 | "message": "Preparing to upgrade ..", 419 | "icon": "$updateicon" 420 | } 421 | EOF 422 | 423 | # Attempt to suppress certain update dialogs 424 | /usr/bin/touch "$homefolder"/.skipbuddy 425 | 426 | # Create a post install script and launchd for use after the OS upgrade. 427 | # This is why we don't need to run a recon immediately after the script runs. We do it later. 428 | 429 | cat << "EOF" > /usr/local/corp/finishOSInstall.sh 430 | #!/bin/zsh 431 | 432 | # First Run Script after an OS upgrade. 433 | 434 | # Wait until /var/db/.AppleUpgrade disappears 435 | while [ -e /var/db/.AppleUpgrade ]; do sleep 5; done 436 | 437 | # Wait until the upgrade process completes 438 | INSTALLER_PROGRESS_PROCESS=$( /usr/bin/pgrep -l "Installer Progress" ) 439 | until [ "$INSTALLER_PROGRESS_PROCESS" = "" ]; 440 | do 441 | sleep 15 442 | INSTALLER_PROGRESS_PROCESS=$( /usr/bin/pgrep -l "Installer Progress" ) 443 | done 444 | 445 | # Look for a user 446 | loggedinuser=$( /usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk -F': ' '/[[:space:]]+Name[[:space:]]:/ { if ( $2 != "loginwindow" ) { print $2 }}' ) 447 | 448 | # If loginwindow, setup assistant or no user, then we're in a DEP environment. 449 | if [ "$loggedinuser" = "loginwindow" ] || [ "$loggedinuser" = "_mbsetupuser" ] || [ "$loggedinuser" = "root" ] || [ -z "$loggedinuser" ]; 450 | then 451 | # Now check to see if Setup Assistant is a running process. Exit if so. 452 | [ $( /usr/bin/pgrep "Setup Assistant" ) ] && exit 0 453 | 454 | # We're fine to restart loginwindow at this point. 455 | /usr/bin/killall -9 loginwindow 456 | fi 457 | 458 | # Update Device Information 459 | /usr/local/bin/jamf manage 460 | /usr/local/bin/jamf recon 461 | /usr/local/bin/jamf policy 462 | 463 | # Remove LaunchDaemon 464 | /bin/rm -f /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist 465 | 466 | # Remove Script 467 | /bin/rm -f /usr/local/corp/finishOSInstall.sh 468 | 469 | exit 0 470 | EOF 471 | 472 | /usr/sbin/chown root:admin /usr/local/corp/finishOSInstall.sh 473 | /bin/chmod 755 /usr/local/corp/finishOSInstall.sh 474 | 475 | # Create a LaunchDaemon to run the above script 476 | cat << "EOF" > /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist 477 | 478 | 479 | 480 | 481 | Label 482 | com.corp.cleanupOSInstall 483 | ProgramArguments 484 | 485 | /bin/zsh 486 | -c 487 | /usr/local/corp/finishOSInstall.sh 488 | 489 | RunAtLoad 490 | 491 | 492 | 493 | EOF 494 | 495 | /usr/sbin/chown root:wheel /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist 496 | /bin/chmod 644 /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist 497 | 498 | # Are we running on Intel or Arm based macs? Apple Silicon macs require user credentials. 499 | if [ $( /usr/bin/arch ) = "arm64" ]; 500 | then 501 | # Apple Silicon macs. We need to prompt for the users credentials or this won't work. Skip this totally if in silent mode. 502 | if [ "$silent" = "0" ]; 503 | then 504 | 505 | # Work out appropriate icon for use 506 | icon="/System/Applications/Utilities/Keychain Access.app/Contents/Resources/AppIcon.icns" 507 | iconposix=$( echo $icon | /usr/bin/sed 's/\//:/g' ) 508 | iconposix="$bootvolname$iconposix" 509 | 510 | # Warn user of what's about to happen 511 | /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "We about to upgrade your macOS and need you to authenticate to continue.\n\nPlease enter your password on the next screen.\n\nPlease contact IT Helpdesk with any issues." giving up after 60 with icon file "'"$iconposix"'" with title "macOS Upgrade" buttons {"OK"} default button 1' 512 | 513 | # Loop three times for password validation 514 | count=1 515 | while [ "$count" -le 3 ]; 516 | do 517 | 518 | # Prompt for a password. Verify it works a maximum of three times before quitting out. 519 | # Also have timeout on the prompt so it doesn't just sit there. 520 | password=$( /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "Please enter your macOS login password:" default answer "" with title "macOS Update - Authentication Required" giving up after 300 with text buttons {"OK"} default button 1 with hidden answer with icon file "'"$iconposix"'" ' -e 'return text returned of result' '' ) 521 | 522 | # Escape any spaces in the password 523 | escapepassword=$( echo ${password} | /usr/bin/sed 's/ /\\\ /g' ) 524 | 525 | # Ok verify the input we got is correct 526 | validpassword=$( /usr/bin/expect </dev/null 560 | /usr/bin/killall caffeinate 2>/dev/null 561 | /bin/rm -f /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist 562 | /bin/rm -f "$workfolder"/finishOSInstall.sh 563 | /usr/bin/find "$infofolder" -type f \( -iname "*.plist" ! -iname "$updatefilename" \) -exec rm {} \; 564 | /usr/bin/find "$waitroom" \( -iname \*.pkg -o -iname \*.cache.xml \) -exec rm {} \; 565 | exit 1 566 | fi 567 | 568 | # Temporarily disable Jamf Connect Login 569 | /usr/local/bin/authchanger -reset 570 | 571 | # Invoke startosinstall to perform the OS upgrade with the accepted credential. Run that in background. 572 | # Then start up the progress bar app. 573 | "$startos" --agreetolicense --forcequitapps --user "$currentuser" --stdinpass <<< "$password" &> "$stosout" & 574 | fi 575 | else 576 | # Intel macs. We can just go for it. 577 | 578 | # Temporarily disable Jamf Connect Login 579 | /usr/local/bin/authchanger -reset 580 | 581 | # Invoke startosinstall to perform the OS upgrade. Run that in background. 582 | # Then start up the progress bar app. 583 | "$startos" --agreetolicense --forcequitapps &> "$stosout" & 584 | fi 585 | 586 | # Code to allow people to cancel the update window. Also will not proceed until startosinstall is complete. 587 | while :; 588 | do 589 | sleep 1 590 | 591 | # Stops the user cancelling the progress bar by force reloading it if we're not forcing things. 592 | if [ "$forced" = "0" ]; 593 | then 594 | if [ -f "$canceljson" ]; 595 | then 596 | /bin/rm "$canceljson" 597 | /usr/bin/killall startosinstall 2>/dev/null 598 | break 3 599 | fi 600 | else 601 | [ -f "$canceljson" ] && { /bin/rm "$canceljson"; killall Progress; $pb $pbjson $canceljson &; } 602 | fi 603 | 604 | # This was such a pain to work out. We have to cat the entire file out, 605 | # then use grep to find the particular line we want but this is also a trap. 606 | # Apple's startosinstall is using ^M characters to backspace and overwrite the percentage line in Terminal. 607 | # Unfortunately nohup is capturing all those characters and not doing that. We must then clean up. 608 | # So change all those to unix linefeeds with tr, grab the latest (last) line and ... 609 | # finally awk to convert to a suitable integer for use with Progress. 610 | percent=$( /bin/cat "$stosout" | /usr/bin/grep "Preparing: " | /usr/bin/tr '\r' '\n' | /usr/bin/tail -n1 | /usr/bin/awk '{ print int($2) }' ) 611 | waittest=$( /bin/cat "$stosout" | /usr/bin/grep -c "Waiting to restart" | /usr/bin/tr '\r' '\n' ) 612 | 613 | # Trap edge cases of numbers being 0, which won't display or 100 which stops the progress bar. 614 | [ "$percent" -eq 0 ] && percent="1" 615 | [ "$percent" -ge 99 ] && percent="99" 616 | 617 | cat < "$pbjson" 618 | { 619 | "percentage": $percent, 620 | "title": "$msgosupgtitle", 621 | "message": "macOS upgrade $percent% completed. Please wait.", 622 | "icon": "$updateicon" 623 | } 624 | EOF 625 | # If we detected the restart message, break out the loop here. 626 | [ "$waittest" = "1" ] && break 627 | 628 | # If startosinstall quit suddenly, break here too 629 | [ -z $( /usr/bin/pgrep startosinstall ) ] && { error=1; break; } 630 | done 631 | fi 632 | 633 | # Did startosinstall quit part way? Warn user. 634 | if [ "$error" = "1" ]; 635 | then 636 | /bin/launchctl asuser "$curuserid" "$osa" -e 'display dialog "The upgrade encountered an unexpected error.\n\nPlease try again later." giving up after 60 with icon file "'"$iconposix"'" with title "Error" buttons {"OK"} default button 1' 637 | fi 638 | 639 | # Run a jamf recon here so we don't overdo it by having it run every policy, only on success. 640 | # Unless we're doing an OS install, we have other ways for that above. 641 | # Was a reboot requested? We should oblige IF we're not doing an OS upgrade 642 | # Give it a 1 minute delay to allow for policy reporting to finish 643 | if [ "$osinstall" = "0" ]; 644 | then 645 | $jb recon 646 | if [ -f "/private/tmp/.apppatchreboot" ]; 647 | then 648 | /bin/rm /private/tmp/.apppatchreboot 649 | /sbin/shutdown -r +0.1 & 650 | fi 651 | fi 652 | 653 | # Stop caffeinate so we can sleep again, then clean up files 654 | /usr/bin/killall Progress 2>/dev/null 655 | /usr/bin/killall caffeinate 2>/dev/null 656 | /bin/rm -f "$jsspkginfo" 657 | 658 | # Clean these ONLY if we didn't cancel out 659 | if [ ! -f "$canceljson" ]; 660 | then 661 | /usr/bin/find "$infofolder" -type f \( -iname "*.plist" ! -iname "$updatefilename" \) -exec rm {} \; 662 | /usr/bin/find "$waitroom" \( -iname \*.pkg -o -iname \*.cache.xml \) -exec rm {} \; 663 | fi 664 | 665 | # Reset IFS 666 | IFS=$OIFS 667 | 668 | # All done 669 | exit 0 670 | -------------------------------------------------------------------------------- /Jamf Pro Scripts/cached pkg processor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Cached application pkg processor script 4 | # Meant to be run after an application installer is cached to the mac 5 | # richard@richard-purves.com - 07-02-2022 - v1.3 6 | 7 | # Variables here 8 | waitroom="/Library/Application Support/JAMF/Waiting Room" 9 | infofolder="/usr/local/corp/cachedapps" 10 | apiusr="" 11 | apipwd="" 12 | jssurl=$( /usr/bin/defaults read /Library/Preferences/com.jamfsoftware.jamf.plist jss_url ) 13 | 14 | # Check that the info folder exists 15 | # Create if missing and set appropriate permissions 16 | /bin/mkdir -p "$infofolder" 17 | /bin/chmod 755 "$infofolder" 18 | /usr/sbin/chown root:wheel "$infofolder" 19 | 20 | # Let's get to it! 21 | 22 | # Check to see if the OS Install switch has been used 23 | osappbundle="$4" 24 | 25 | # Lastly work out todays date. 26 | # We may use this in the future, but at the moment it probably won't be. 27 | tdydate=$( /bin/date -u +"%Y-%m-%dT%H:%M:%SZ" ) 28 | 29 | # Special case for the OS installer switch 30 | if [ -z "$osappbundle" ]; 31 | then 32 | # We're not an OS install bundle 33 | /bin/echo "OS Installer not specified" 34 | 35 | # Find the most recent file in the Jamf Waiting Room folder 36 | # We're specifically looking for .pkg or .pkg.zip files. We don't want to deal with DMG packages and should not. 37 | # We also have to avoid any .pkg.cache.xml files as well. These contain info but it's not useful to us. 38 | 39 | # Find all the *.pkg or *.pkg.zip files in thw waiting room. Sort them by last modified date, old to new and then grab the last one. 40 | cachedpkg=$( find "$waitroom" -type f \( -iname \*.pkg -o -iname \*.pkg.zip \) -print0 |\ 41 | xargs -0 ls -tr |\ 42 | tail -n1 ) 43 | 44 | # Did we pick up a file? Fail here if not. 45 | [[ -z "$cachedpkg" ]] && { echo "No package found."; exit 1; } 46 | 47 | # Strip off the full file path so we just have the name 48 | # Then remove the file extension from the name 49 | # Finally work out the cache folder patch 50 | pkgfilename=$( /usr/bin/basename "$cachedpkg" ) 51 | displayname=${pkgfilename%.*} 52 | cachepath=$( /usr/bin/dirname "$cachedpkg" ) 53 | 54 | # Use a Jamf API request to pull the pkg record from Jamf Pro. 55 | # Use xmllint's xpath to parse through the xml output to get the field we want. 56 | # Strip off the xml open and close tags, then store in a variable. 57 | pkgrecord=$( /usr/bin/curl -H "Accept: application/xml" -s -u "${apiusr}:${apipwd}" "${jssurl}JSSResource/packages/name/${pkgfilename}" -X GET ) 58 | priority=$( echo $pkgrecord | /usr/bin/xmllint --format --xpath '//package/priority' - | /usr/bin/sed -e 's/<[^>]*>//g' ) 59 | reboot=$( echo $pkgrecord | /usr/bin/xmllint --format --xpath '//package/reboot_required' - | /usr/bin/sed -e 's/<[^>]*>//g' ) 60 | feu=$( echo $pkgrecord | /usr/bin/xmllint --format --xpath '//package/fill_existing_users' - | /usr/bin/sed -e 's/<[^>]*>//g' ) 61 | fut=$( echo $pkgrecord | /usr/bin/xmllint --format --xpath '//package/fill_user_template' - | /usr/bin/sed -e 's/<[^>]*>//g' ) 62 | 63 | # Now write out all the info we've collected to our cache file. We'll process these in another script. 64 | # The name of the file should be date-name.plist. 65 | # Create the file with plutil and then insert the keys required afterwards. Original defaults code was throwing weird errors from macOS 12. 66 | /usr/bin/plutil -create xml1 "$infofolder"/"${pkgfilename}".plist 67 | /usr/bin/plutil -insert PkgName -string "$pkgfilename" "$infofolder"/"${pkgfilename}".plist 68 | /usr/bin/plutil -insert FullPath -string "$cachepath" "$infofolder"/"${pkgfilename}".plist 69 | /usr/bin/plutil -insert DisplayName -string "$displayname" "$infofolder"/"${pkgfilename}".plist 70 | /usr/bin/plutil -insert Priority -integer "$priority" "$infofolder"/"${pkgfilename}".plist 71 | /usr/bin/plutil -insert Reboot -bool "$reboot" "$infofolder"/"${pkgfilename}".plist 72 | /usr/bin/plutil -insert CacheDate -date "$tdydate" "$infofolder"/"${pkgfilename}".plist 73 | /usr/bin/plutil -insert FEU -bool "$feu" "$infofolder"/"${pkgfilename}".plist 74 | /usr/bin/plutil -insert FUT -bool "$fut" "$infofolder"/"${pkgfilename}".plist 75 | /usr/bin/plutil -insert OSInstaller -bool FALSE "$infofolder"/"${pkgfilename}".plist 76 | 77 | # Look for any already cached pkgs and cache files. We already have a processed name from earlier. 78 | # Count number of dash characters in filename, then count number of spaces 79 | dashes=$( echo "$displayname" | awk -F\- '{print NF-1}' ) 80 | spaces=$( echo "$displayname" | awk -F\ '{print NF-1}' ) 81 | 82 | # Slice the name after the delimiter. 83 | # If we have dashes, process those otherwise move to spaces. 84 | # Thanks to many things here's the insanity that we have: 85 | # echo out the name of the package. Reverse it so it's backwards. 86 | # split the reversed name and tell cut command to include everything forward. 87 | # reverse everything again to get the correct output. 88 | # I wish there was something better than this but limited on our cli tools. 89 | if [ "$dashes" -gt "0" ]; 90 | then 91 | name=$( echo $displayname | rev | cut -d"-" -f2- | rev ) 92 | elif [ "$spaces" -gt "0" ]; 93 | then 94 | name=$( echo $pkgname | rev | cut -d" " -f2- | rev ) 95 | else 96 | echo "ERROR: Can't split filename. Can't detect duplicate names." 97 | exit 1 98 | fi 99 | 100 | # Remove any duplicates using some zsh search to exclude most recent modified date files. 101 | # Direct output from the rm commands to null because if there's no duplicates, it'll error. 102 | /bin/rm ${waitroom}/${name}*.pkg(.om[2,-1]) 2>&1 > /dev/null 103 | /bin/rm ${waitroom}/${name}*.pkg.zip(.om[2,-1]) 2>&1 > /dev/null 104 | /bin/rm ${waitroom}/${name}*.pkg.cache.xml(.om[2,-1]) 2>&1 > /dev/null 105 | /bin/rm ${infofolder}/*${name}*.plist(.om[2,-1]) 2>&1 > /dev/null 106 | else 107 | # We are an OS installer. Special rules apply 108 | /bin/echo "OS Installer specified" 109 | 110 | # First find the app installer 111 | app=$( /usr/bin/find /Applications -iname "Install macOS*" -type d -maxdepth 1 ) 112 | 113 | # Did we pick up an installer? 114 | if [ ! -z "$app" ]; 115 | then 116 | # We did 117 | echo "Installer found: $app" 118 | 119 | # Work out it's name and path 120 | appname=$( /usr/bin/basename $app ) 121 | apppath=$( /usr/bin/dirname $app ) 122 | 123 | # Write out a mostly hard coded file for later processing. Make sure OSInstaller is set for later use. 124 | /usr/bin/plutil -create xml1 "$infofolder"/"$appname".plist 125 | /usr/bin/plutil -insert PkgName -string "$appname" "$infofolder"/"$appname".plist 126 | /usr/bin/plutil -insert FullPath -string "$apppath" "$infofolder"/"$appname".plist 127 | /usr/bin/plutil -insert DisplayName -string "$appname" "$infofolder"/"$appname".plist 128 | /usr/bin/plutil -insert Priority -integer 20 "$infofolder"/"$appname".plist 129 | /usr/bin/plutil -insert Reboot -bool FALSE "$infofolder"/"$appname".plist 130 | /usr/bin/plutil -insert CacheDate -date "$tdydate" "$infofolder"/"$appname".plist 131 | /usr/bin/plutil -insert FEU -bool FALSE "$infofolder"/"$appname".plist 132 | /usr/bin/plutil -insert FUT -bool FALSE "$infofolder"/"$appname".plist 133 | /usr/bin/plutil -insert OSInstaller -bool TRUE "$infofolder"/"$appname".plist 134 | else 135 | echo "Installer not found: $app" 136 | fi 137 | fi 138 | 139 | # All done. Package is ready for next cache install cycle. 140 | exit 0 141 | -------------------------------------------------------------------------------- /Jamf Pro Scripts/download macos installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Script to download the latest macOS installer 4 | # richard@richard-purves.com - 12-29-2021 - v1.7 5 | 6 | # Logging output to a file for testing 7 | #time=$( date "+%d%m%y-%H%M" ) 8 | #set -x 9 | #logfile=/Users/Shared/oscache-"$time".log 10 | #exec > $logfile 2>&1 11 | 12 | exitcode=0 13 | 14 | # Any OS specified? 15 | downloados="$4" 16 | [ -z "$downloados" ] && { /bin/echo "No OS version specified. Defaulting to 12.1"; downloados="12.1"; } 17 | 18 | # Check if we're already running 19 | if [ -f "/private/tmp/.downloadmacos" ]; 20 | then 21 | # Work out last modified date on the touch file. Convert to epoch. 22 | # If it's above 7200 it's been over two hours. We can ignore. 23 | currentdate=$( /bin/date -j -f "%a %b %d %T %Z %Y" "$( /bin/date )" "+%s" ) 24 | lastmodified=$( /usr/bin/stat -x /private/tmp/.downloadmacos | grep "Access: " | cut -d" " -f2- ) 25 | lastmodepoch=$( /bin/date -j -f "%a %b %d %T %Y" "$lastmodified" "+%s" ) 26 | diff="$((currentdate-lastmodepoch))" 27 | echo "Difference: $diff" 28 | 29 | [ "$diff" -lt 7200 ] && { echo "Download already in progress."; exit 0; } 30 | fi 31 | 32 | # Place a check file to stop this running multiple times 33 | [ ! -f "/private/tmp/.downloadmacos" ] && /usr/bin/touch /private/tmp/.downloadmacos 34 | 35 | # Find and delete any existing macOS installers 36 | /usr/bin/find /Applications -iname "Install macOS*" -type d -maxdepth 1 -exec rm -rf {} \; 37 | 38 | # A quick disk space check 39 | diskinfo=$( /usr/sbin/diskutil info -plist / ) 40 | freespace=$( /usr/libexec/PlistBuddy -c "Print :APFSContainerFree" /dev/stdin <<< "$diskinfo" 2>/dev/null || /usr/libexec/PlistBuddy -c "Print :FreeSpace" /dev/stdin <<< "$diskinfo" 2>/dev/null || /usr/libexec/PlistBuddy -c "Print :AvailableSpace" /dev/stdin <<< "$diskinfo" 2>/dev/null ) 41 | requiredspace=$(( 45 * 1000 ** 3 )) 42 | 43 | if [ "$freespace" -ge "$requiredspace" ]; 44 | then 45 | /bin/echo "Disk Check: OK - $((freespace / 1000 ** 3)) GB Free Space Detected" 46 | else 47 | /bin/echo "Disk Check: ERROR - $((freespace / 1000 ** 3)) GB Free Space Detected." 48 | exit 1 49 | fi 50 | 51 | # Reset the softwareupdate daemon 52 | /usr/bin/defaults delete /Library/Preferences/com.apple.SoftwareUpdate.plist 53 | rm /Library/Preferences/com.apple.SoftwareUpdate.plist 54 | /bin/launchctl kickstart -k system/com.apple.softwareupdated 55 | 56 | # Now use softwareupdate to cache the latest app bundle 57 | echo "macOS 10.15 or later" 58 | echo "Downloading macOS $downloados from Software Update" 59 | ( cd /Applications ; /usr/sbin/softwareupdate --fetch-full-installer --full-installer-version "$downloados" ) 60 | [ ! $? = "0" ] && exitcode=1 61 | 62 | # Now find and hide the installer as people keep deleting it 63 | app=$( /usr/bin/find /Applications -iname "Install macOS*" -type d -maxdepth 1 ) 64 | [ ! -z "$app" ] && /usr/bin/chflags hidden "$app" 65 | 66 | # Clean our download flag file 67 | rm -f /private/tmp/.downloadmacos 68 | 69 | # Run a recon 70 | /usr/local/bin/jamf recon 71 | 72 | exit $exitcode 73 | -------------------------------------------------------------------------------- /Jamf Pro Scripts/self service macos upgrade-or-erase.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Script to either upgrade or erase and reinstall macOS on a target device 4 | # richard@richard-purves.com - 09-16-2021 5 | 6 | # Relies on https://github.com/adriannier/swift-progress to provide an accurate progress bar. 7 | 8 | # Choices are "upgrade" or "erase". Users will never see "erase", that one is locked to IT only. 9 | mode="$4" 10 | [ -z "$4" ] && { /bin/echo "ERROR - No operating mode specified"; exit 1; } 11 | mode=$( echo $mode | tr '[:upper:]' '[:lower:]' ) 12 | 13 | # Which OS do we want to keep 14 | osexclude="$5" 15 | [ -z "$5" ] && { /bin/echo "ERROR - No OS name specified"; exit 1; } 16 | 17 | # Any OS specified? 18 | downloados="$6" 19 | [ -z "$6" ] && { /bin/echo "No OS version specified. Defaulting to 11.4"; downloados="11.4"; } 20 | 21 | # 22 | # Set up the variables here 23 | # 24 | 25 | # Work out machine type 26 | [[ $( /usr/bin/arch ) = "arm64" ]] && asmac=true || asmac=false 27 | 28 | # Work out where helper apps exist 29 | jb=$( which jamf ) 30 | osa=$( /usr/bin/which osascript ) 31 | pbapp="/Applications/Utilities/Progress.app" 32 | pb="$pbapp/Contents/MacOS/Progress" 33 | 34 | # Work out current user 35 | currentuser=$( /usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk -F': ' '/[[:space:]]+Name[[:space:]]:/ { if ( $2 != "loginwindow" ) { print $2 }}' ) 36 | userid=$( /usr/bin/id -u $currentuser ) 37 | 38 | # Now what is the boot volume called, just in case someone renamed it 39 | bootvolname=$( /usr/sbin/diskutil info / | /usr/bin/awk '/Volume Name:/ { print substr($0, index($0,$3)) ; }' ) 40 | 41 | # Work out update icon location 42 | updateicon="/System/Library/PreferencePanes/SoftwareUpdate.prefPane/Contents/Resources/SoftwareUpdate.icns" 43 | iconposix=$( echo $updateicon | /usr/bin/sed 's/\//:/g' ) 44 | iconposix="$bootvolname$iconposix" 45 | 46 | # File locations 47 | workfolder="/private/tmp" 48 | pbjson="$workfolder/progressbar.json" 49 | canceljson="$workfolder/cancelfile.json" 50 | 51 | # Display messages are set here 52 | [[ "$mode" = "upgrade" ]] && msgosupgtitle="Upgrading macOS" || msgosupgtitle="Erasing macOS" 53 | msgpowerwarning="Your computer is about to upgrade installed software. 54 | 55 | Please ensure you are connected to AC Power. 56 | 57 | Once you are plugged in, please click the Proceed button." 58 | msgdltitle="Downloading macOS" 59 | msgdlfailed="The macOS download failed. 60 | 61 | Please check your network connection and try again later." 62 | 63 | # Keep the mac awake while this runs. 64 | /usr/bin/caffeinate -dis & 65 | 66 | # Check for Progress.app and install if not 67 | [ ! -d "/Applications/Utilities/Progress.app" ] && $jb policy -event patchinstall 68 | 69 | # 70 | # Check to see if we're on AC power 71 | # 72 | 73 | # Valid reports are `Battery Power` or `AC Power` 74 | pwrAdapter=$( /usr/bin/pmset -g ps | /usr/bin/grep "Now drawing" | /usr/bin/cut -d "'" -f2 ) 75 | 76 | # Warn the user if not on AC power 77 | count=1 78 | while [[ "$count" -le "3" ]]; 79 | do 80 | [[ "$pwrAdapter" = "AC Power" ]] && break 81 | count=$(( count + 1 )) 82 | /bin/launchctl asuser "$userid" "$osa" -e 'display dialog "'"$msgpowerwarning"'" giving up after 30 with icon file "'"$iconposix"'" with title "'"$msgtitlenewsoft"'" buttons {"Proceed"} default button 1' 83 | pwrAdapter=$( /usr/bin/pmset -g ps | /usr/bin/grep "Now drawing" | /usr/bin/cut -d "'" -f2 ) 84 | done 85 | 86 | # 87 | # Clear any existing installers and download the specified one 88 | # 89 | 90 | # Find all macOS installers and clear them out 91 | /usr/bin/find /Applications -iname "Install macOS*" -type d -maxdepth 1 -exec rm -rf {} \; 92 | 93 | # 94 | # Download the latest approved macOS 95 | # 96 | 97 | # Reset the softwareupdate daemon 98 | /usr/bin/defaults delete /Library/Preferences/com.apple.SoftwareUpdate.plist 99 | rm /Library/Preferences/com.apple.SoftwareUpdate.plist 100 | /bin/launchctl kickstart -k system/com.apple.softwareupdated 101 | 102 | # Prep the progress bar info 103 | cat < "$pbjson" 104 | { 105 | "percentage": -1, 106 | "title": "$msgdltitle $osexclude $downloados", 107 | "message": "Preparing to download ...", 108 | "icon": "$updateicon" 109 | } 110 | EOF 111 | 112 | # Now use softwareupdate to cache the latest app bundle 113 | ( cd /Applications ; /usr/sbin/softwareupdate --fetch-full-installer --full-installer-version "$downloados" &> /private/tmp/su.log & ) 114 | $pb $pbjson $canceljson & 115 | 116 | while :; 117 | do 118 | 119 | sleep 2 120 | [ -f "$canceljson" ] && { /bin/rm "$canceljson"; killall Progress; $pb $pbjson $canceljson &; } 121 | 122 | percent=$( /bin/cat /private/tmp/su.log | /usr/bin/grep "Installing: " | /usr/bin/tr '\r' '\n' | /usr/bin/tail -n1 | /usr/bin/awk '{ print int($2) }' ) 123 | [ "$percent" -eq 0 ] && percent="1" 124 | [ "$percent" -ge 99 ] && percent="99" 125 | 126 | complete=$( /bin/cat /private/tmp/su.log | /usr/bin/tr '\r' '\n' | /usr/bin/grep -c "Install finished successfully" ) 127 | failed=$( /bin/cat /private/tmp/su.log | /usr/bin/tr '\r' '\n' | /usr/bin/grep -c "Install failed with error" ) 128 | 129 | [[ "$complete" == "1" ]] || [[ "$failed" == "1" ]] && { sleep 3; break; } 130 | 131 | cat < "$pbjson" 132 | { 133 | "percentage": $percent, 134 | "title": "$msgdltitle $osexclude $downloados", 135 | "message": "macOS $percent% downloaded. Please wait.", 136 | "icon": "$updateicon" 137 | } 138 | EOF 139 | 140 | done 141 | 142 | [[ "$complete" == "1" ]] && message="macOS Download Completed" || message="macOS Download Failed" 143 | 144 | cat < "$pbjson" 145 | { 146 | "percentage": 100, 147 | "title": "$msgdltitle $osexclude $downloados", 148 | "message": "$message", 149 | "icon": "$updateicon" 150 | } 151 | EOF 152 | 153 | sleep 2 154 | /usr/bin/killall Progress 2>/dev/null 155 | 156 | # Error out if fail at this point 157 | if [ "$failed" = "1" ]; 158 | then 159 | /usr/bin/killall Progress 160 | /bin/rm -f "$pbjson" 161 | /bin/rm -f "$canceljson" 162 | /bin/rm -f /private/tmp/su.log 163 | /bin/launchctl asuser "$userid" "$osa" -e 'display dialog "'"$msgdlfailed"'" giving up after 15 with icon file "'"$iconposix"'" with title "'"$msgdltitle"'" buttons {"Ok"} default button 1' 164 | exit 0 165 | fi 166 | 167 | # 168 | # Proceed with the OS work 169 | # 170 | 171 | startos=$( /usr/bin/find "/Applications" -iname "startosinstall" -type f ) 172 | 173 | [ "$mode" = "upgrade" ] && msgosupgtitle="Upgrading macOS" || msgosupgtitle="Erasing macOS to $downloados" 174 | 175 | cat < "$pbjson" 176 | { 177 | "percentage": -1, 178 | "title": "$msgosupgtitle", 179 | "message": "Preparing to $mode ..", 180 | "icon": "$updateicon" 181 | } 182 | EOF 183 | 184 | if [[ "$asmac" == "true" ]]; 185 | then 186 | # Apple Silicon macs. We need to prompt for the users credentials or this won't work. Skip this totally if in silent mode. 187 | 188 | # Warn user of what's about to happen 189 | /bin/launchctl asuser "$userid" "$osa" -e 'display dialog "We about to upgrade your macOS and need you to authenticate to continue.\n\nPlease enter your password on the next screen.\n\nPlease contact IT Helpdesk with any issues." giving up after 30 with icon file "'"$iconposix"'" with title "macOS Upgrade" buttons {"OK"} default button 1' 190 | 191 | # Loop three times for password validation 192 | count=1 193 | while [[ "$count" -le 3 ]]; 194 | do 195 | 196 | # Prompt for a password. Verify it works a maximum of three times before quitting out. 197 | # Also have timeout on the prompt so it doesn't just sit there. 198 | password=$( "$os" -e 'display dialog "Please enter your macOS login password:" default answer "" with title "macOS Update - Authentication Required" giving up after 300 with text buttons {"OK"} default button 1 with hidden answer with icon file "'"$iconposix"'" ' -e 'return text returned of result' '' ) 199 | 200 | # Escape any spaces in the password 201 | escapepassword=$( echo ${password} | /usr/bin/sed 's/ /\\\ /g' ) 202 | 203 | # Ok verify the input we got is correct 204 | validpassword=$( /usr/bin/expect </dev/null 238 | /usr/bin/killall caffeinate 2>/dev/null 239 | /bin/rm -f "$pbjson" 240 | /bin/rm -f "$canceljson" 241 | /bin/rm -f /private/tmp/su.log 242 | exit 1 243 | fi 244 | fi 245 | 246 | $pb $pbjson $canceljson & 247 | 248 | cat << "EOF" > /usr/local/corp/finishOSInstall.sh 249 | #!/bin/zsh 250 | 251 | # First Run Script after an OS upgrade. 252 | 253 | # Wait until /var/db/.AppleUpgrade disappears 254 | while [ -e /var/db/.AppleUpgrade ]; do sleep 5; done 255 | 256 | # Wait until the upgrade process completes 257 | INSTALLER_PROGRESS_PROCESS=$( /usr/bin/pgrep -l "Installer Progress" ) 258 | until [ "$INSTALLER_PROGRESS_PROCESS" = "" ]; 259 | do 260 | sleep 15 261 | INSTALLER_PROGRESS_PROCESS=$( /usr/bin/pgrep -l "Installer Progress" ) 262 | done 263 | 264 | # Update Device Information 265 | /usr/local/bin/jamf manage 266 | /usr/local/bin/jamf recon 267 | /usr/local/bin/jamf policy 268 | 269 | # Remove LaunchDaemon 270 | /bin/rm -f /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist 271 | 272 | # Remove Script 273 | /bin/rm -f /usr/local/corp/finishOSInstall.sh 274 | 275 | exit 0 276 | EOF 277 | 278 | /usr/sbin/chown root:admin /usr/local/corp/finishOSInstall.sh 279 | /bin/chmod 755 /usr/local/corp/finishOSInstall.sh 280 | 281 | cat << "EOF" > /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist 282 | 283 | 284 | 285 | 286 | Label 287 | com.corp.cleanupOSInstall 288 | ProgramArguments 289 | 290 | /bin/zsh 291 | -c 292 | /usr/local/corp/finishOSInstall.sh 293 | 294 | RunAtLoad 295 | 296 | 297 | 298 | EOF 299 | 300 | /usr/sbin/chown root:wheel /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist 301 | /bin/chmod 644 /Library/LaunchDaemons/com.corp.cleanupOSInstall.plist 302 | 303 | if [ "$mode" = "upgrade" ]; 304 | then 305 | # macOS upgrade mode 306 | if [[ "$asmac" == "true" ]]; 307 | then 308 | # Apple Silicon Macs 309 | echo "apple silicon upgrade startosinstall" 310 | "$startos" --agreetolicense --rebootdelay 120 --forcequitapps --user "$currentuser" --stdinpass <<< "$password" &> /private/tmp/upgrade.log & 311 | else 312 | # Intel Macs 313 | echo "intel upgrade startosinstall" 314 | "$startos" --agreetolicense --rebootdelay 120 --forcequitapps &> /private/tmp/update.log & 315 | fi 316 | fi 317 | 318 | if [ "$mode" = "erase" ]; 319 | then 320 | # macos erase to default mode 321 | if [[ "$asmac" == "true" ]]; 322 | then 323 | # Apple Silicon Macs 324 | echo "apple silicon erase startosinstall" 325 | "$startos" --agreetolicense --eraseinstall --newvolumename "Macintosh HD" --rebootdelay 120 --forcequitapps --user "$currentuser" --stdinpass <<< "$password" &> /private/tmp/update.log & 326 | else 327 | # Intel Macs 328 | echo "intel erase startosinstall" 329 | "$startos" --agreetolicense --eraseinstall --newvolumename "Macintosh HD" --rebootdelay 120 --forcequitapps &> /private/tmp/update.log & 330 | fi 331 | fi 332 | 333 | while :; 334 | do 335 | sleep 2 336 | [ -f "$canceljson" ] && { /bin/rm "$canceljson"; killall Progress; $pb $pbjson $canceljson &; } 337 | 338 | percent=$( /bin/cat /private/tmp/update.log | /usr/bin/grep "Preparing: " | /usr/bin/tr '\r' '\n' | /usr/bin/tail -n1 | /usr/bin/awk '{ print int($2) }' ) 339 | 340 | [ "$percent" -eq 0 ] && percent="1" 341 | [ "$percent" -ge 99 ] && percent="99" 342 | 343 | test=$( /bin/cat /private/tmp/update.log | /usr/bin/tr '\r' '\n' | /usr/bin/grep -c "Waiting to restart" ) 344 | [[ "$test" == "1" ]] && { break; } 345 | 346 | cat < "$pbjson" 347 | { 348 | "percentage": $percent, 349 | "title": "$msgosupgtitle", 350 | "message": "macOS $mode $percent% completed. Please wait.", 351 | "icon": "$updateicon" 352 | } 353 | EOF 354 | done 355 | 356 | sleep 3 357 | 358 | cat < "$pbjson" 359 | { 360 | "percentage": 100, 361 | "title": "$msgosupgtitle", 362 | "message": "macOS $mode completed. Restart IMMINENT.", 363 | "icon": "$updateicon" 364 | } 365 | EOF 366 | 367 | # Clean up all files 368 | /usr/bin/killall Progress 2>/dev/null 369 | /usr/bin/killall caffeinate 2>/dev/null 370 | /bin/rm -f "$pbjson" 371 | /bin/rm -f "$canceljson" 372 | /bin/rm -f /private/tmp/su.log 373 | /bin/rm -f /private/tmp/update.log 374 | 375 | exit 376 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Richard Purves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package/ROOT/Applications/Utilities/README.txt: -------------------------------------------------------------------------------- 1 | Progress.app to go here 2 | https://github.com/adriannier/swift-progress 3 | -------------------------------------------------------------------------------- /Package/ROOT/Library/LaunchDaemons/com.corp.patcher-every2h.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.corp.patcher-every2h 7 | ProgramArguments 8 | 9 | /usr/local/bin/jamf 10 | policy 11 | -event 12 | apppatch 13 | -randomDelaySeconds 14 | 300 15 | 16 | StartInterval 17 | 7200 18 | UserName 19 | root 20 | 21 | 22 | -------------------------------------------------------------------------------- /Package/scripts/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Post installation script 4 | 5 | # Work out locations of files 6 | pb=$( /usr/bin/find /Applications -type d -iname "Progress.app" -maxdepth 2 ) 7 | ld=$( find /Library/LaunchDaemons -iname "*apppatcher*.plist" -type f -maxdepth 1 ) 8 | 9 | # Clear any quarantine flags because they randomly set for some reason best known to Apple 10 | /usr/bin/xattr -r -d com.apple.quarantine $pb 11 | /usr/bin/xattr -r -d com.apple.quarantine $ld 12 | 13 | # Hide the progress app 14 | /usr/bin/chflags hidden "$pb" 15 | 16 | # Finally load the launchdaemon 17 | /bin/launchctl load "$ld" 18 | 19 | exit 20 | -------------------------------------------------------------------------------- /Package/scripts/preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Remove any existing patch setup preinstall script 4 | 5 | # Work out locations of files 6 | /usr/bin/find /Applications -type d -iname "Progress.app" -maxdepth 2 -exec rm -rf {} \; 7 | /usr/bin/find /Library/LaunchDaemons -iname "*apppatcher*.plist" -type f -maxdepth 1 -exec rm -f {} \; 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mac Patcher and Upgrader 2 | ======================== 3 | 4 | This is a series of scripts and a deployment pkg to plug into your Jamf Pro system for both Application and OS based upgrades. 5 | 6 | ### Overview ### 7 | 8 | * Gives a nice UI prompt to end users showing updates are available. 9 | * Deferment mechanism allowing users not to update immediately unless they run out. 10 | * Will update installed applications. 11 | * If available, will upgrade the installed OS. 12 | * Accurate progress bar information such as: 13 | * * Install 23% completed - (Updating 2 of 5) 14 | * * macOS upgrade 37% completed. Please wait. 15 | * Default behavior is to NOT restart, but enabling that option on a pkg in Jamf will ensure particular upgrade(s) restarts the mac. 16 | * Supports both Intel and Apple Silicon macs and all their little foibles. 17 | * Uses familiar Jamf Pro constructs such as triggers, smart groups and policies. 18 | * Local record of deferments used, last update time and last number of deferrments used for that update. 19 | This is readable via standard defaults read commands in the supplied extension attributes. 20 | 21 | ### Video Examples 22 | 23 | Example User Dialogs 24 | Shows time outs, deferrments and eventual force patching. 25 | 26 | https://user-images.githubusercontent.com/5807892/118586786-38043a80-b750-11eb-8dd6-540ffea0e857.mp4 27 | 28 | 29 | Patching Dialog (sped up) 30 | Shows screen lock out, application updating and macOS upgrading. 31 | 32 | Please note this is from an older version and there's more accurate progress bar on Application upgrades now! 33 | 34 | https://user-images.githubusercontent.com/5807892/118586556-d512a380-b74f-11eb-8411-cb52f067da86.mp4 35 | 36 | 37 | Requirements 38 | ------------ 39 | * macOS 12 and above 40 | * Jamf Pro. v10 is preferred but we're not using anything version specific. 41 | * Cloud Distribution point or On-Prem HTTP distribution server(s). 42 | * Progress Bar requires the [swift-progress](https://github.com/adriannier/swift-progress) app from [Adrian Nier](https://github.com/adriannier). 43 | 44 | How to use 45 | ---------- 46 | 47 | 1) Make sure your deployment pkgs have the format "name-version". e.g. "Cyberduck-7.9.0.pkg" 48 | This ensures the app processing script will function. You will get an error if it can't parse the name! 49 | 2) Download the latest swift-progress release from the link above. 50 | 3) Make a signed deployment pkg with the files and folders in the "Package" folder. 51 | 4) Check then upload that pkg to your Jamf Pro instance. You will deploy this via existing mechanisms and/or deploy processes to your fleet. 52 | 5) Implement the Extension Attributes to aid in your reporting and audits. 53 | 6) Implement the provided Configuration Profile so that PPPC will not interfere with this script. (Customise for your own corporate overlords!) 54 | 7) Upload the main scripts to your Jamf Pro instance. 55 | 8) The cached pkg processor script can utilise the parameter four value. Call this "OS Installer (blank for no)". 56 | 9) The main patcher installer script can also utilise parameter four for Self Service runs. Call this "Self Service policy (blank for no)". 57 | 58 | API User Required 59 | ----------------- 60 | 61 | Create a user account on your Jamf Pro instance. Give it an appropriate username and complex password. The ONLY permission that it requires is read access to "Packages". 62 | 63 | The cached pkg processor will need those credentials in order to properly process the cached installer. 64 | 65 | Jamf Pro Extension Attributes 66 | ----------------------------- 67 | 68 | Upload the three supplied extension attribute scripts. You may need to customise the folder path they are using. 69 | 70 | The EA's provide auditing information that may be useful such as current deferrals used, last update time and last number of deferrals used at last update. 71 | 72 | Jamf Pro Smart Groups 73 | --------------------- 74 | 75 | ##### Example Application Update Group 76 | 77 | You will need one of these per application. In this example, I'll be using CyberDuck. 78 | 79 | * **Name:** `Update Cyberduck 7.9.0` 80 | * **Criteria 1:** `"Application Title"` `"is"` `"Cyberduck.app"` 81 | * **Criteria 2:** `"and"` `"Application Version"` `"is not"` `"7.9.0"` 82 | * **Criteria 3:** `"and"` `"Cached Packages"` `"does not have"` `"Cyberduck-7.9.0.pkg"` 83 | 84 | What this does is to see if CyberDuck is installed, check the installed version and to then see if we've already cached an upgrade installer. That way we don't run unnecessarily. 85 | 86 | Every time you update an application in Jamf, the appropriate group **must** be updated as well! 87 | 88 | ##### Example macOS Update Group 89 | 90 | This is a special case and is not formatted like the other groups. For this example, macOS 11.3.1 is the latest version being offered. 91 | 92 | * **Name:** `Update macOS Installer` 93 | * **Criteria 1:** `"Operating System Version"` `"less than"` `"11.3.1"` 94 | * **Criteria 2:** `"Application Title"` `"does not have"` `"Install macOS Big Sur.app"` 95 | 96 | Jamf Pro Scripts 97 | ---------------- 98 | 99 | ##### main patcher installer.sh 100 | 101 | This requires the simplest policy of all, as it's called every two hours by the LaunchDaemon in the deployment pkg. 102 | 103 | * **Name:** `Run Patcher` 104 | * **Trigger:** `Custom` 105 | * **Trigger event:** `apppatch` 106 | * **Execution Frequency:** `ongoing` 107 | * **Scripts:** `Set this to run the main patcher installer script` 108 | * **Maintenance:** `Enable Update Inventory` 109 | 110 | This is the script that does all the user prompting, displays the progress bars and performs clean up. 111 | 112 | (OPTIONAL) Duplicate this policy, make it a Self Service policy and ensure parameter 4 is set to yes. That will provide a better user experience for manual updating. 113 | 114 | ##### cached pkg processor.sh 115 | 116 | Once again, we'll use Cyberduck as an example but you'll have to create this per application. 117 | 118 | * **Name:** `Cache Cyberduck` 119 | * **Trigger:** `Recurring Check-in` 120 | * **Execution Frequency:** `ongoing` 121 | * **Packages:** `Set this to **cache** Cyberduck-7.9.0.pkg` 122 | * **Scripts:** `Set this to run the cached pkg processor script` 123 | * **Maintenance:** `Enable Update Inventory` 124 | 125 | Scope any policies using this to the appropriate smart group for that application you created earlier. 126 | 127 | ##### download macos installer.sh 128 | 129 | This again is a special case because the script is using softwareupdate to download the Install macOS app bundle directly to the Applications folder. As a result the policy is different to caching an application installer. 130 | 131 | * **Name:** `Cache macOS Installer` 132 | * **Trigger:** `Recurring Check-in` 133 | * **Execution Frequency:** `ongoing` 134 | * **Scripts:** `Run the download macos installer script as "Before"` 135 | * **Scripts:** `Set this to run the cached pkg processor script as "After" and setting script parameter 4 to "yes"` 136 | * **Maintenance:** `Enable Update Inventory` 137 | 138 | Scope this policy to the macOS update group from earlier. 139 | 140 | The script will attempt to download the latest macOS installer (caching server really useful here) and the special setting on the cached pkg script will cause that to look for an installer app bundle instead of a cached pkg. 141 | 142 | ### Enjoy! ### 143 | 144 | ### Special Thanks ### 145 | 146 | [Lachlan Stewart](https://github.com/loceee). Without [patchoo](http://patchoo.github.io/patchoo) to inspire (and borrow ideas), this wouldn't exist. 147 | 148 | [Joshua Roskos](https://github.com/kc9wwh). Your work on macOS startosinstall was invaluable and this is a massive extension on your work. 149 | --------------------------------------------------------------------------------