diff --git a/README.md b/README.md index bb24460..a38e0aa 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Install or Defer -This framework will enforce the installation of pending Apple security updates on Jamf Pro-managed Macs. Users will have the option to __Run Updates__ or __Defer__. After a specified amount of time passes, the Mac will be forced to install the updates, then restart automatically if any updates require it. +This framework will enforce the installation of pending Apple security updates on Jamf Pro-managed Macs. Users will have the option to __Run Updates__ or __Defer__. After a specified amount of time passes, the Mac will be prompted to install the updates, then restart automatically if any updates require it. ![Install or Defer prompt](img/install-or-defer-fullscreen.png) -This workflow is most useful for updates that require a restart and include important security-related patches (e.g. macOS Catalina 10.15.7 Supplemental), but also applies to critical security updates that don't require a restart (e.g. Safari 14.0.1). Basically, anything Software Update marks as "recommended" or requiring a restart is in scope. +This workflow is most useful for updates that require a restart and include important security-related patches (e.g. macOS Catalina 10.15.7 Supplemental), but also applies to security updates that don't require a restart (e.g. Safari 14.0.3). Basically, anything Software Update marks as "recommended" or requiring a restart is in scope. This framework is distributed in the form of a [munkipkg](https://github.com/munki/munki-pkg) project, which allows easy creation of a new installer package when changes are made to the script or to the LaunchDaemon that runs it (despite the name, packages generated with munkipkg don't require Munki; they work great with Jamf Pro). See the [Installer creation](#installer-creation) section below for specific steps on creating the installer for this framework. @@ -14,7 +14,6 @@ This framework is distributed in the form of a [munkipkg](https://github.com/mun Here's what needs to be in place in order to use this framework: - The current version of this framework has been tested on __macOS High Sierra, Mojave, Catalina, and Big Sur__, but older versions should continue to function normally for previous macOS builds (note, however, that those versions of macOS are no longer receiving regular security updates from Apple and thus may not benefit from this framework). -- This framework has only been tested on __Intel Macs__, and currently exits with no update enforcement action if run on Apple Silicon Macs. `softwareupdate` binary behavior has changed on Apple Silicon and further testing on native hardware is required before we can update the script for compatibility. Stay tuned! [#45](https://github.com/mpanighetti/install-or-defer/issues/45) - Target Macs must be __enrolled in Jamf Pro__ and have the `jamfHelper` binary installed. ## Optional @@ -36,7 +35,7 @@ The following is assumed to be the case when implementing this framework: Here's how everything works, once it's configured: -1. When a new critical Apple security update is released, the Jamf Pro administrator creates a smart group for Macs that need this update, and adds it to the existing policy scope. +1. When a new desired Apple security update is released, the Jamf Pro administrator creates a smart group for Macs that need this update, and adds it to the existing policy scope. 2. Macs that meet the smart group criteria run the policy at next check-in. 3. The policy installs a package that places a LaunchDaemon and a script. 4. The LaunchDaemon executes the script, which performs the following actions: @@ -60,10 +59,11 @@ Here's how everything works, once it's configured: ## Limitations -The framework has two major limitations: +The framework has three major limitations: -- Sequential updates cannot be installed as a group (e.g. Security Update 2019-002 Mojave cannot be installed unless 10.14.6 is already installed). If multiple sequential critical updates are available, they are treated as two separate rounds of prompting/deferring. As a result, Macs requiring sequential updates may take more than one deferral/enforcement cycle (default 3 days) to be fully patched. +- Sequential updates cannot be installed as a group (e.g. Security Update 2019-002 Mojave cannot be installed unless 10.14.6 is already installed). If multiple sequential security updates are available, they are treated as two separate rounds of prompting/deferring. As a result, Macs requiring sequential updates may take more than one deferral and enforcement cycle (default 3 days) to be fully patched. - Reasonable attempts have been made to make this workflow enforceable, but there's nothing stopping an administrator of a Mac from unloading the LaunchDaemon or resetting the preference file. +- On Apple Silicon Macs, running `softwareupdate --download` and `softwareupdate --install` via script are unsupported. When this framework is run on arm64 architecture, enforcement takes a "softer" form, instead opening System Preferences - Software Update and leaving a persistent prompt in place until the updates are applied. ## Settings customization @@ -184,18 +184,22 @@ Upload this package (created with munkipkg above) to the Jamf Pro server via Jam Create a smart group for each software update or operating system patch you wish to enforce. Here are some examples to serve as guides, using regular expressions to allow for fewer criteria: -- __Critical Update Needed: macOS Catalina 10.15.2__ - - `Operating System Build` `matches regex` `^19[A-B]` -- __Critical Update Needed: Security Update 2019-007 High Sierra__ - - `Operating System Build` `matches regex` `^17G\d{1,4}$` +- __Critical Update Needed: macOS Catalina 10.15.7__ + - `Operating System Build` `matches regex` `^19[A-G]` +- __Critical Update Needed: Security Update 2021-002 Mojave__ + - `Operating System Build` `matches regex` `^18G\d{1,3}$` + - `or` `Operating System Build` `matches regex` `^18G[1-7]\d{3}$` + - `or` `Operating System Build` `matches regex` `^18G80[0-1]\d$` + - `or` `Operating System Build` `matches regex` `^18G802[0-1]$` For completion's sake, here's an example of an update that won't require a restart but is still tagged as `Recommended: YES` in the `softwareupdate` catalog: -- __Critical Update Needed: Safari 13.0.4__ +- __Critical Update Needed: Safari 14.0.3__ - `Application Title` `is` `Safari.app` - `and` `(` `Application Version` `matches regex` `^\d\.` - - `or` `Application Version` `matches regex` `^1[0-2]\.` - - `or` `Application Version` `matches regex` `^13\.0\.[0-3]` `)` + - `or` `Application Version` `matches regex` `^1[0-3]\.` + - `or` `Application Version` `matches regex` `^14\.0$` + - `or` `Application Version` `matches regex` `^14\.0\.[0-2]` `)` ### Policy @@ -298,7 +302,7 @@ Once the Testing steps above have been followed, there are only a few steps rema ## Rollback -If major problems are detected with the critical update prompt or installation workflow, disable the __Install or Defer__ policy. This will prevent computers from being newly prompted for installation of updates. +If major problems are detected with the update prompt or installation workflow, disable the __Install or Defer__ policy. This will prevent computers from being newly prompted for installation of updates. Note that any computers which have already received the framework push will continue through the motions of alerting, deferring, updating, and restarting. If you need to remove the framework from your fleet and stop it from running, you could write an uninstall script using the preinstall script as a foundation (it would basically just need to unload the LaunchDaemons and remove the resource files). diff --git a/build-info.plist b/build-info.plist index 76da8a1..1a44915 100644 --- a/build-info.plist +++ b/build-info.plist @@ -17,6 +17,6 @@ suppress_bundle_relocation version - 4.0.2 + 4.1 diff --git a/payload/Library/Scripts/Install or Defer.sh b/payload/Library/Scripts/Install or Defer.sh index 1c80920..ae5b574 100755 --- a/payload/Library/Scripts/Install or Defer.sh +++ b/payload/Library/Scripts/Install or Defer.sh @@ -7,13 +7,13 @@ # LaunchDaemon, will prompt users to install Apple system # updates that the IT department has deemed "critical." Users # will have the option to Run Updates or Defer. After a -# specified amount of time, the update will be forced. If -# updates requiring a restart were found in the update check, -# the system restarts automatically. +# specified amount of time, the update will be forced on +# Intel Macs, and if updates requiring a restart were found +# in that update check, the system restarts automatically. # Authors: Mario Panighetti and Elliot Jordan # Created: 2017-03-09 -# Last Modified: 2020-12-17 -# Version: 4.0.2 +# Last Modified: 2021-03-10 +# Version: 4.1 # ### @@ -49,20 +49,35 @@ SCRIPT_PATH="/Library/Scripts/Install or Defer.sh" # The message users will receive when updates are available, shown above the # "Run Updates" and "Defer" buttons. -MSG_ACT_OR_DEFER_HEADING="Critical updates are available" -MSG_ACT_OR_DEFER="Your Mac needs to run critical security updates<< which require a restart>>. Please save your work, quit all applications, and click Run Updates. +MSG_ACT_OR_DEFER_HEADING="Updates are available" +MSG_ACT_OR_DEFER="Your Mac needs to run the following updates<< which require a restart>>: -{{If now is not a good time, you may defer this message until later. }}Updates will install automatically after %DEFER_HOURS% hours<<, forcing your Mac to restart in the process>>. +UPDATE_LIST -If you have any questions, please contact IT." +Please save your work, quit all applications, and click Run Updates. {{If now is not a good time, you may defer this message until later. }}These updates will be required after %DEFER_HOURS% hours<<, forcing your Mac to restart after they run>>. + +Please contact IT for any questions." # The message users will receive after the deferral deadline has been reached. MSG_ACT_HEADING="Please run updates now" -MSG_ACT="Please save your work, then run all available macOS security updates<< (restart your Mac when prompted)>>. If no action is taken, updates will be installed automatically." +MSG_ACT="Your Mac is about to run the following updates<< and restart>>: + +UPDATE_LIST + +Please save your work and quit any of the above applications. You can manually run these updates in System Preferences - Software Update." + +# The message users will receive when a manual update action is required. +MSG_ACT_NOW="Your Mac needs to run the following updates<< which require a restart>>: + +UPDATE_LIST + +Please save your work, quit all other applications, then open System Preferences - Software Update and run all available updates.<< Your Mac will restart when updates are finished running.>>" # The message users will receive while updates are running in the background. MSG_UPDATING_HEADING="Running updates" -MSG_UPDATING="Running macOS security updates in the background.<< Your Mac will restart automatically when this is finished.>>" +MSG_UPDATING="Running the following updates in the background<< (your Mac will restart automatically when this is finished)>>: + +UPDATE_LIST" #################################### TIMING ################################### @@ -103,15 +118,16 @@ convert_seconds () { } -# Caches all available critical system updates, or exits if no critical updates -# are available. +# Checks for recommended macOS updates, or exits if no such updates are +# available. check_for_updates () { echo "Checking for pending system updates..." UPDATE_CHECK=$(/usr/sbin/softwareupdate --list 2>&1) - # Determine whether any critical updates are available, and if any require - # a restart. If no updates need to be installed, bail out. + # Determine whether any recommended macOS updates are available. + # If a restart is required for any pending updates, then run all available + # software updates. if [[ "$UPDATE_CHECK" =~ (Action: restart|\[restart\]) ]]; then INSTALL_WHICH="all" RESTART_FLAG="--restart" @@ -119,7 +135,9 @@ check_for_updates () { # (retains restart warnings). MSG_ACT_OR_DEFER="$(echo "$MSG_ACT_OR_DEFER" | /usr/bin/sed 's/[\<\<|\>\>]//g')" MSG_ACT="$(echo "$MSG_ACT" | /usr/bin/sed 's/[\<\<|\>\>]//g')" + MSG_ACT_NOW="$(echo "$MSG_ACT_NOW" | /usr/bin/sed 's/[\<\<|\>\>]//g')" MSG_UPDATING="$(echo "$MSG_UPDATING" | /usr/bin/sed 's/[\<\<|\>\>]//g')" + # Otherwise, only target recommended updates. elif [[ "$UPDATE_CHECK" =~ (Recommended: YES|\[recommended\]) ]]; then INSTALL_WHICH="recommended" RESTART_FLAG="" @@ -127,16 +145,34 @@ check_for_updates () { # (removes restart warnings). MSG_ACT_OR_DEFER="$(echo "$MSG_ACT_OR_DEFER" | /usr/bin/sed 's/\<\<.*\>\>//g')" MSG_ACT="$(echo "$MSG_ACT" | /usr/bin/sed 's/\<\<.*\>\>//g')" + MSG_ACT_NOW="$(echo "$MSG_ACT_NOW" | /usr/bin/sed 's/\<\<.*\>\>//g')" MSG_UPDATING="$(echo "$MSG_UPDATING" | /usr/bin/sed 's/\<\<.*\>\>//g')" + # If no recommended updates need to be installed, bail out. else - echo "No critical updates available." + echo "No recommended updates available." exit_without_updating fi - # Download updates (all updates if a restart is required for any, otherwise - # just recommended updates). - echo "Caching $INSTALL_WHICH system updates..." - /usr/sbin/softwareupdate --download --$INSTALL_WHICH --no-scan + # Capture update names and versions. + if [[ "$OS_MAJOR" -eq 10 && "$OS_MINOR" -lt 15 ]]; then + UPDATE_LIST="$(echo "$UPDATE_CHECK" | /usr/bin/awk -F'[\(\)]' '/recommended/ {print $1 $2}')" + else + UPDATE_LIST="$(echo "$UPDATE_CHECK" | /usr/bin/awk -F'[:,]' '/Title:/ {print $2 $4}')" + fi + # Convert update list from multiline to comma-separated list. + UPDATE_LIST="$(echo "$UPDATE_LIST" | /usr/bin/tr '\n' ',' | /usr/bin/sed 's/^ *//; s/,/, /g; s/, $//')" + # Populate the list of pending updates in message text. + MSG_ACT_OR_DEFER="$(echo "$MSG_ACT_OR_DEFER" | /usr/bin/sed "s/UPDATE_LIST/$UPDATE_LIST/")" + MSG_ACT="$(echo "$MSG_ACT" | /usr/bin/sed "s/UPDATE_LIST/$UPDATE_LIST/")" + MSG_ACT_NOW="$(echo "$MSG_ACT_NOW" | /usr/bin/sed "s/UPDATE_LIST/$UPDATE_LIST/")" + MSG_UPDATING="$(echo "$MSG_UPDATING" | /usr/bin/sed "s/UPDATE_LIST/$UPDATE_LIST/")" + + # Download updates for Intel Macs (all updates if a restart is required for + # any, otherwise just recommended updates). + if [[ "$PLATFORM_ARCH" = "i386" ]]; then + echo "Caching $INSTALL_WHICH system updates..." + /usr/sbin/softwareupdate --download --$INSTALL_WHICH --no-scan + fi } @@ -190,31 +226,64 @@ EOF # by previous checks). run_updates () { - # Display HUD with updating message. - "$JAMFHELPER" -windowType "hud" -windowPosition "ur" -icon "$LOGO" -title "$MSG_UPDATING_HEADING" -description "$MSG_UPDATING" -lockHUD & + # On Apple Silicon Macs, running softwareupdate --install via script is + # currently unsupported, so we'll just inform the user with a persistent + # alert and open the Software Update window for manual update. + if [[ "$PLATFORM_ARCH" = "arm64" ]]; then - # Run Apple system updates. - echo "Running $INSTALL_WHICH Apple system updates..." - # macOS Big Sur requires triggering the restart as part of the softwareupdate action, meaning the script will not be able to run its clean_up functions until the next time it is run. - if [[ "$OS_MAJOR" -gt 10 ]] && [[ "$INSTALL_WHICH" = "all" ]]; then - echo "System will restart as soon as the update is finished. Cleanup tasks will run on a subsequent update check." - fi - # shellcheck disable=SC2086 - UPDATE_OUTPUT_CAPTURE="$(/usr/sbin/softwareupdate --install --${INSTALL_WHICH} ${RESTART_FLAG} --no-scan 2>&1)" - echo "Finished running Apple updates." - - # Trigger restart if script found an update which requires it. - if [[ "$INSTALL_WHICH" = "all" ]]; then - # Shut down the Mac if BridgeOS received an update requiring it. - if [[ "$UPDATE_OUTPUT_CAPTURE" == *"select Shut Down from the Apple menu"* ]]; then - trigger_restart "shut down" - # Otherwise, restart the Mac. - else - trigger_restart "restart" + echo "This is an Apple Silicon Mac with pending updates. Displaying persistent alert until updates are applied..." + + # Loop this check until softwareupdate --list shows no more pending + # recommended updates. + while [[ $(/usr/sbin/softwareupdate --list) == *"Recommended: YES"* ]]; do + + # Display persistent HUD with update prompt message. + echo "Prompting to install updates now and opening System Preferences - Software Update..." + "$JAMFHELPER" -windowType "hud" -windowPosition "ur" -icon "$LOGO" -title "$MSG_ACT_HEADING" -description "$MSG_ACT_NOW" -lockHUD & + + # Open System Preferences - Software Update in current user context. + CURRENT_USER=$(/usr/bin/stat -f%Su "/dev/console") + USER_ID=$(/usr/bin/id -u "$CURRENT_USER") + /bin/launchctl asuser "$USER_ID" open "/System/Library/PreferencePanes/SoftwareUpdate.prefPane" + + # Leave the alert up for 60 seconds before looping. + sleep 60 + + # Clear out jamfHelper alert before looping to prevent pileups. + echo "Killing any active jamfHelper notifications..." + /usr/bin/killall jamfHelper 2>"/dev/null" + + done + + else + + # Display HUD with updating message. + "$JAMFHELPER" -windowType "hud" -windowPosition "ur" -icon "$LOGO" -title "$MSG_UPDATING_HEADING" -description "$MSG_UPDATING" -lockHUD & + + # Run Apple system updates. + echo "Running $INSTALL_WHICH Apple system updates..." + # macOS Big Sur requires triggering the restart as part of the softwareupdate action, meaning the script will not be able to run its clean_up functions until the next time it is run. + if [[ "$OS_MAJOR" -gt 10 ]] && [[ "$INSTALL_WHICH" = "all" ]]; then + echo "System will restart as soon as the update is finished. Cleanup tasks will run on a subsequent update check." + fi + # shellcheck disable=SC2086 + UPDATE_OUTPUT_CAPTURE="$(/usr/sbin/softwareupdate --install --${INSTALL_WHICH} ${RESTART_FLAG} --no-scan 2>&1)" + echo "Finished running Apple updates." + + # Trigger restart if script found an update which requires it. + if [[ "$INSTALL_WHICH" = "all" ]]; then + # Shut down the Mac if BridgeOS received an update requiring it. + if [[ "$UPDATE_OUTPUT_CAPTURE" == *"select Shut Down from the Apple menu"* ]]; then + trigger_restart "shut down" + # Otherwise, restart the Mac. + else + trigger_restart "restart" + fi fi - fi - clean_up + clean_up + + fi } @@ -258,7 +327,7 @@ trigger_restart () { # Immediately attempt a "soft" restart. echo "Attempting a \"soft\" $1..." - CURRENT_USER=$(/usr/bin/stat -f%Su /dev/console) + CURRENT_USER=$(/usr/bin/stat -f%Su "/dev/console") USER_ID=$(/usr/bin/id -u "$CURRENT_USER") /bin/launchctl asuser "$USER_ID" osascript -e "tell application \"System Events\" to $1" @@ -346,15 +415,6 @@ fi # Determine platform architecture. PLATFORM_ARCH=$(/usr/bin/arch) -# This script has currently been tested on Intel Macs, and will exit with error -# when run on Apple Silicon Macs. -# This logic should be updated after the script has been tested and updated for -# Apple Silicon Mac compatibility. -if [[ "$PLATFORM_ARCH" = "arm64" ]]; then - echo "❌ ERROR: This script has not been tested on Apple Silicon Macs, unable to proceed." - BAILOUT=true -fi - # We need to be connected to the internet in order to download updates. if nc -zw1 swscan.apple.com 443; then # Check if a custom CatalogURL is set and if it is available @@ -398,10 +458,17 @@ fi ################################ MAIN PROCESS ################################# # Validate logo file. If no logo is provided or if the file cannot be found at -# specified path, default to the Software Update icon. +# specified path, default to the Software Update preference pane icon. if [[ -z "$LOGO" ]] || [[ ! -f "$LOGO" ]]; then - echo "No logo provided, or no logo exists at specified path. Using Software Update icon." - LOGO="/System/Library/CoreServices/Software Update.app/Contents/Resources/SoftwareUpdate.icns" + echo "No logo provided, or no image file exists at specified path. Using Software Update icon." + # macOS High Sierra is the only supported macOS that does not have a + # Software Update prefPane, so we'll use the Software Update.app icon + # instead. + if [[ "$OS_MAJOR" -eq 10 && "$OS_MINOR" -eq 13 ]]; then + LOGO="/System/Library/CoreServices/Software Update.app/Contents/Resources/SoftwareUpdate.icns" + else + LOGO="/System/Library/PreferencePanes/SoftwareUpdate.prefPane/Contents/Resources/SoftwareUpdate.icns" + fi fi # Validate max deferral time and whether to skip deferral. To customize these @@ -421,6 +488,9 @@ else fi echo "Maximum deferral time: $(convert_seconds "$MAX_DEFERRAL_TIME")" +# Check for updates, exit if none found, otherwise cache locally and continue. +check_for_updates + # Perform first run tasks, including calculating deadline. FORCE_DATE=$(/usr/bin/defaults read "$PLIST" UpdatesForcedAfter 2>"/dev/null") if [[ -z $FORCE_DATE || $FORCE_DATE -gt $(( $(/bin/date +%s) + MAX_DEFERRAL_TIME )) ]]; then @@ -443,9 +513,6 @@ if [[ -n "$DEFERRED_UNTIL" ]] && (( DEFERRED_UNTIL > $(/bin/date +%s) && FORCE_D exit 0 fi -# Check for updates, exit if none found, otherwise cache locally and continue. -check_for_updates - # Make a note of the time before displaying the prompt. PROMPT_START=$(/bin/date +%s)