Creating a Patch Policy Maintenance Window

kacey3
Contributor II

I've seen several posts about this, and have been struggling with it myself, but I finally found a way to create a sort of maintenance window for Patch Management policies. For our faculty and staff computers, we distribute patches through Self Service patch policies, which is fine. It leaves it to the user to patch when convenient. But for our lab and classroom computers, we don't want to rely on our students to push patches, so we use the automatic installation patch policies. They work fine, but unless the admin updates them during regularly scheduled down-time, the students are forced to quit any applications that need updates shortly after the patch policies are revised. I wanted to find a way to be able to update my patch policies, but not have them trigger until a certain time. Ironically, regular policies have an option to set up a scheduled time using the Client-Side Limitations. Sadly, patch policies do not have this option, so I set about finding a way to emulate this functionality. Here's how I created a maintenance window for my patch policies:

 

Create the Static Group

Create a static group as a holding space for the computers that are in a maintenance window. I simply named mine "Patch Policy Window Active."
Screenshot2023-10-0511.54.48.png

  • We will need both the name of this group, and it's ID (which of course can be found in the URL for the smart group).

 

Set Up Add/Remove Script

Next, I modified a pre-existing script tp add computers to, and remove computers from a specified static group.

#!/bin/sh
##########################################################################################
#
# Static Group Membership - Computers
#   Originally: Add Computer to Static Group
#
# This script uses the Jamf Pro API to get a Bearer Authentication Token and then adds a computer to a Static Group.  
#   In this case it could be the computer the script is run on if you uncomment the particular line.  I actually set the computer name for testing to a specific name
# Adapted from https://derflounder.wordpress.com/2022/01/05/updated-script-for-obtaining-checking-and-renewing-bearer-tokens-for-the-classic-and-jamf-pro-apis/
#        (not all functions in Rich's example are used as this is a very short access to the API)
# Also help from Steve Dagley in https://community.jamf.com/t5/jamf-pro/bearer-token-api-and-adding-computer-to-static-group/td-p/261269
#           and https://community.jamf.com/t5/jamf-pro/script-to-add-computer-to-static-group/m-p/198738
# 2022-03-17 David London
#
# Modified for UNT by KClose
#   Added parameter calls for Jamf Options.
#   Updated static information to match UNT requirements.
#
# Updated 2023-09-27:   Added a remove option as well and updated Parameters.
#
##########################################################################################

### VARIABLES ###

# Get the Static Group Name *REQUIRED*
[ -z "$4" ] && { echo "Static Group Name is required. Exiting."; exit 1; } || { GroupName=$4; }

# Get the Static Group ID *REQUIRED*
[ -z "$5" ] && { echo "Static Group ID is required. Exiting."; exit 1; } || { GroupID=$5; }

# Get Add/Remove. *REQUIRED*
if [[ $6 == [Aa] ]]; then
    changeCommand="Add"
elif [[ $6 == [Rr] ]]; then
    changeCommand="Remove"
else
    echo "Change Command (A/R) is required. Exiting."
    exit 1
fi

# Get the API Username. *REQUIRED*
[ -z "$7" ] && { echo "API Username is required. Exiting."; exit 1; } || { jamfpro_user=$7; }

# Get the API Password. *REQUIRED*
[ -z "$8" ] && { echo "API Password is required. Exiting."; exit 1; } || { jamfpro_password=$8; }

# Get the Jamf URL. *REQUIRED*
[ -z "$9" ] && { echo "Jamf URL is required. Exiting."; exit 1; } || { jamfpro_url=$9; }

# Remove the trailing slash from the Jamf Pro URL if needed.
jamfpro_url=${jamfpro_url%%/}

# Find/Set the computer name.
ComputerName=$(/usr/sbin/scutil --get ComputerName)

# Explicitly set initial value for the api_token variable to null:
api_token=""

# Explicitly set initial value for the token_expiration variable to null:
token_expiration=""

### FUNCTIONS ###

GetJamfProAPIToken() {
    # This function uses Basic Authentication to get a new bearer token for API authentication.
    # Use user account's username and password credentials with Basic Authorization to request a bearer token.

    if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then
        api_token=$(/usr/bin/curl -X POST --silent -u "${jamfpro_user}:${jamfpro_password}" "${jamfpro_url}/api/v1/auth/token" | python -c 'import sys, json; print json.load(sys.stdin)["token"]')
    else
        api_token=$(/usr/bin/curl -X POST --silent -u "${jamfpro_user}:${jamfpro_password}" "${jamfpro_url}/api/v1/auth/token" | plutil -extract token raw -)
    fi
}

APITokenValidCheck() {
    # Verify that API authentication is using a valid token by running an API command
    # which displays the authorization details associated with the current API user. 
    # The API call will only return the HTTP status code.

    api_authentication_check=$(/usr/bin/curl --write-out %{http_code} --silent --output /dev/null "${jamfpro_url}/api/v1/auth" --request GET --header "Authorization: Bearer ${api_token}")
}

InvalidateToken() {
    # Verify that API authentication is using a valid token by running an API command
    # which displays the authorization details associated with the current API user. 
    # The API call will only return the HTTP status code.

    APITokenValidCheck

    # If the api_authentication_check has a value of 200, that means that the current
    # bearer token is valid and can be used to authenticate an API call.

    if [[ ${api_authentication_check} == 200 ]]; then

        # If the current bearer token is valid, an API call is sent to invalidate the token.

        authToken=$(/usr/bin/curl "${jamfpro_url}/api/v1/auth/invalidate-token" --silent  --header "Authorization: Bearer ${api_token}" -X POST)
      
        # Explicitly set value for the api_token variable to null.
        api_token=""
    fi
}

### MAIN SCRIPT ###

GetJamfProAPIToken

apiURL="JSSResource/computergroups/id/${GroupID}"

if [[ $changeCommand == "Add" ]]; then
    echo "Adding computer to $GroupName."
    apiData="<computer_group><id>${GroupID}</id><name>${GroupName}</name><computer_additions><computer><name>$ComputerName</name></computer></computer_additions></computer_group>"
elif [[ "$changeCommand" == "Remove" ]]; then
    echo "Removing computer from $GroupName."
    apiData="<computer_group><id>${GroupID}</id><name>${GroupName}</name><computer_deletions><computer><name>$ComputerName</name></computer></computer_deletions></computer_group>"
else
    echo "Something went wrong - we shouldn't see this error."
    exit 1
fi

curl -s \
	--header "Authorization: Bearer ${api_token}" --header "Content-Type: text/xml" \
	--url "${jamfpro_url}/${apiURL}" \
	--data "${apiData}" \
    --request PUT > /dev/null

InvalidateToken

exit 0

 

  • This script does come with the following parameters defined:
    Screenshot2023-10-0512.09.51.png

 

Create Add/Remove Policies

The next step is to create a pair of standard policies that will add our computers to the static group, and then remove them from the group at a later time.

  • The main thing here is that we are going to use the Client-Side Limitations to restrict when a computer is added to or removed from the static group.
  • This is also where we will need the static group name and ID.
  • Add Computer to Static Group:
    Screenshot2023-10-0512.08.08.pngScreenshot2023-10-0512.08.41.png
  • Remove Computer from Static Group:
    Screenshot2023-10-0512.13.09.png
    Screenshot2023-10-0512.13.30.png
  • Note that in my configuration, the Client-Side Limitations only allow the computers to be added to the group between 2am and 5am, and removed between 5am and 6am.

 

Set Scope on Patch Policies

Lastly we can just set the scope of our patch policies to our Patch Window static group. This way the only computers that are eligible for the patch policy are those that are in the static group that is managed by our patch window policies above.
Screenshot2023-10-0512.16.37.png

    • I do include an inventory update with my policies that add the computers to the static group, mostly to ensure that our records are up to date as to what software needs updating.

 

Conclusion

Expanding on from here, you could create several of the add/remove policies to schedule multiple windows for various groups of computers, or have static groups for specific applications. Once you have the pieces in place, the options for customization open up and give you a lot more control over your patch policies.

I hope that this helps someone because it certainly has given us more control over our patch management strategy and gives us the freedom to update the patch policies without concern that our users will be unexpectedly interrupted.

0 REPLIES 0