Skip to content

Instantly share code, notes, and snippets.

@sgmills
Last active December 21, 2022 15:11
Show Gist options
  • Save sgmills/d5f2385647dde2b16124a6f4a8f867c1 to your computer and use it in GitHub Desktop.
Save sgmills/d5f2385647dde2b16124a6f4a8f867c1 to your computer and use it in GitHub Desktop.
#!/bin/bash
#####################################################################################################
#
# SCRIPT: autopkg-update-smart-group.sh
# AUTHOR: Sam Mills (@mostlymac; github.com/sgmills)
# DATE: 08 December 2022
# REV: 1.0
#
#####################################################################################################
# USAGE
#
# ./autopkg-update-smart-group.sh
# Use option --dry-run to report errors and preview smart group before making changes in Jamf Pro
#
#####################################################################################################
# ASSUMPTIONS
#
# It is assumed that all recipes in your recipe list contain a Patch Software Title ID
#
# If this is not true, add the following key to each recipe override and supply the appropriate ID
# <key>PATCH_SOFTWARE_TITLE_ID</key>
# <string>52</string>
#
# The patch software title id can be found in the url for each patch title.
# In the following example, the ID is 52
# https://yourOrg.jamfcloud.com/patch.html?id=52&o=r
#
#---------------------------------------------------------------------------------------------------#
#
# It is assumed that you have added Graham Pugh's LastRecipeRunResult post processor to each recipe
# that you intend to use this method with. This processor creates a JSON file containing the results
# of the last autopkg run for each recipe and requires no input.
#
# See below for an example. This should be the last processor in each recipe:
# <dict>
# <key>Processor</key>
# <string>com.github.grahampugh.recipes.postprocessors/LastRecipeRunResult</string>
# </dict>
#
# For more information on the LastRecipeRunResult post processor, please see the link below:
# https://github.com/autopkg/grahampugh-recipes/blob/main/PostProcessors/LastRecipeRunResult.py
#
#---------------------------------------------------------------------------------------------------#
#
# It is assumed that you have jq installed to parse JSON. To install with homebrew: brew install jq
#
#---------------------------------------------------------------------------------------------------#
#
# It is assumed that your Jamf Pro API user has the following permissions, in additon to any already
# required for Jamf Upload
#
# Read access to Jamf Pro Server Objects > External Patch Sources
# Read access to Jamf Pro Server Settings > Internal Patch Sources
#
#####################################################################################################
# EDITABLE VARIABLES
#
# Adjust the following variables for your particular configuration.
#
# autopkgUser - This should be the user account you're running AutoPkg in
# autopkgUserHome - This should be the home folder location of the AutoPkg user account
# autoPkgCache - This should be the location of your AutoPkg cache directory
#
# Note: The home folder and AutoPkg cache locations are currently set to be automatically discovered
# using the autopkgUser variable.
autopkgUser="autopkg"
autopkgUserHome=$(/usr/bin/dscl . -read /Users/"$autopkgUser" NFSHomeDirectory | awk '{print $2}')
autoPkgCache="$autopkgUserHome/Library/AutoPkg/Cache"
# recipeList - This is the location of the plain text file being used to store
# your list of AutoPkg recipes. For more information about this list, please see
# the link below:
# https://github.com/autopkg/autopkg/wiki/Running-Multiple-Recipes
recipeList="$autopkgUserHome/Library/Application Support/AutoPkgr/recipe_list.txt"
# If you're using Jamf Upload, your Jamf Pro server and API user information should be
# populated automatically. If you're not using Jamf Upload, set this information accordingly.
jamfUser="$( /usr/bin/defaults read "$autopkgUserHome"/Library/Preferences/com.github.autopkg.plist API_USERNAME )"
jamfPass="$( /usr/bin/defaults read "$autopkgUserHome"/Library/Preferences/com.github.autopkg.plist API_PASSWORD )"
jamfURL="$( /usr/bin/defaults read "$autopkgUserHome"/Library/Preferences/com.github.autopkg.plist JSS_URL )"
# groupName - This should be the name of the Jamf Pro Smart Group that will be updated
# groupID - This should be the ID of the Jamf Pro Smart Group that will be updated
# Note: The group must already exisit in Jamf Pro. This script will not create one
groupName="Managed Apps Out-Of-Date"
groupID="123"
# jqLocation - This should be the location of the jq binary.
# Currently set to be automatically discovered
jqLocation="$(/usr/bin/which jq)"
#####################################################################################################
# OPTIONAL VARIABLES
# siteID - This should be the ID of the site for your smart group. Leave blank if no sites
# siteName - This should be the name of the site for your smart group. Leave blank if no sites
siteID=""
siteName=""
# If you would like to exclude any recipes from your recipe list, enter them here.
# Recipe names should be enclosed in quotes and separated by a space as shown below:
# excludedRecipes=("local.jamf.Chrome-patch" "local.jamf.Firefox-patch")
excludedRecipes=()
#####################################################################################################
# USE CAUTION EDITING BELOW THIS LINE
#####################################################################################################
# FUNCTIONS
# Function uses Basic Authentication to get a new bearer token for API authentication
GetJamfProAPIToken() {
api_token=$(/usr/bin/curl -X POST --silent -u "${jamfUser}:${jamfPass}" "${jamfURL}/api/v1/auth/token" | plutil -extract token raw -)
}
# Function to collect all internal and external patch sources and save the patch available titles.
# Saving this data to a file reduces API calls and speeds up operations.
# Takes one argument: internal or external
savePatchAvailableTitles () {
# Get the source ids
patchSourceIDs="$( /usr/bin/curl -s -H "authorization: Bearer ${api_token}" \
"Accept: text/xml" --request GET \
"$jamfURL"/JSSResource/patch"$1"sources | \
/usr/bin/xmllint --format - | \
/usr/bin/grep -e "<id>" | \
/usr/bin/awk -F "<id>|</id>" '{ print $2 }' )"
# For each patch source, append all patches to a file
for id in $patchSourceIDs; do
/usr/bin/curl -s -H "authorization: Bearer ${api_token}" \
"Accept: text/xml" --request GET \
"$jamfURL"/JSSResource/patchavailabletitles/sourceid/"$id" | \
/usr/bin/xmllint --format - >> "$patchAvailableTitles"
done
}
# Function to check if array contains a recipe
containsRecipe () {
local e
for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 1; done
return 0
}
# Function to verify xml and PUT computer group to Jamf Pro
updateSmartGroup () {
echo ""
echo "Verifying XML data"
# If xml is valid, create a new comptuer group from file
if xmllint "$smartGroupData" 1> /dev/null; then
echo "XML is valid. Updating $groupName on Jamf Pro Server $jamfURL"
# Use Jamf Pro API to put data
/usr/bin/curl -s -H "authorization: Bearer ${api_token}" \
-H "Accept: application/xml" -H "Content-type: application/xml" --request PUT \
"${jamfURL}"/JSSResource/computergroups/id/"${groupID}" \
--upload-file "$smartGroupData"
else
echo "XML is invalid! Cannot upload to Jamf Pro. Exiting..."
exit 1
fi
}
# Invalidates the Jamf API token so it can no longer be used
InvalidateToken() {
/usr/bin/curl "${jamfURL}/api/v1/auth/invalidate-token" --silent --header "Authorization: Bearer ${api_token}" -X POST
api_token=""
}
#####################################################################################################
# PRELIMINARY CHECKS
# If the AutoPkg cache directory is missing, stop the script with an error.
if [[ ! -d "$autoPkgCache" ]]; then
echo "AutoPkg cache directory ($autoPkgCache) does not exist. Exiting..."
exit 1
fi
# If the AutoPkg recipe list is missing or unreadable, stop the script with an error.
if [[ ! -r "$recipeList" ]]; then
echo "Recipe list ($recipeList) is missing or unreadable. Exiting..."
exit 1
fi
# If Jamf Pro API user info is missing, stop the script with an error
if [[ -z $jamfUser ]] || [[ -z $jamfPass ]] || [[ -z $jamfURL ]]; then
echo "Jamf Pro API username, password, or URL is missing. Exiting..."
exit 1
fi
# If Jamf Pro smart group info is missing, stop the script with an error
if [[ -z $groupID ]] || [[ -z $groupName ]]; then
echo "Jamf Pro smart group name or id is missing. Exiting..."
exit 1
fi
# If jq is missing, stop the script with an error.
if [[ ! -x "$jqLocation" ]]; then
echo "jq is not installed. Exiting..."
exit 1
fi
#####################################################################################################
# GET A JAMF PRO API TOKEN
# Use function to get an API token
GetJamfProAPIToken
#####################################################################################################
# GET ALL AVAILABLE PATCH TITLES
# xml file for all available patch titles
patchAvailableTitles="/private/tmp/patchAvailableTitles.xml"
# Remove old patch title xml file if needed
rm "$patchAvailableTitles" 2> /dev/null
# Use fuction to collect patch avaialble titles
savePatchAvailableTitles "internal" 2> /dev/null
savePatchAvailableTitles "external" 2> /dev/null
# Check that there is now data in the file
if [[ ! -s "$patchAvailableTitles" ]]; then
echo "No available patch titles found on Jamf Pro server. Exiting..."
fi
#####################################################################################################
# CREATE THE XML FOR SMART GROUP
# xml file for uploading smart group to Jamf Pro
smartGroupData="/private/tmp/smartGroupData.xml"
# Remove old smart group xml file if needed
rm "$smartGroupData" 2> /dev/null
# Add opening tag and set the smart group name
echo "<computer_group>
<name>$groupName</name>
<is_smart>true</is_smart>" >> "$smartGroupData"
# Add site info to smart group xml
# If both site id and site name are supplied, write them
if [[ -n $siteID ]] && [[ -n $siteName ]]; then
echo " <site>
<id>$siteID</id>
<name>$siteName</name>
</site>
<criteria>" >> "$smartGroupData"
# If either site id or site name are missing report error
elif [[ -n $siteID ]] || [[ -n $siteName ]]; then
echo "Either siteID or siteName variable is missing."
exit 1
# If no site info is entered, use default values
else
echo " <site>
<id>-1</id>
<name>None</name>
</site>
<criteria>" >> "$smartGroupData"
fi
#####################################################################################################
# ADD PATCHES TO XML FOR SMART GROUP
# Set priority for smart group items to 0
priority=0
# For each recipe in the list, append to xml data
while IFS="" read -r recipe || [ -n "$recipe" ]; do
# Check if the recipe is excluded
containsRecipe "$recipe" "${excludedRecipes[@]}"
excludedRecpeResult="$?"
# If recipe is in exclusions skip it
if [[ "$excludedRecpeResult" = 1 ]]; then
echo "[ ] $recipe is excluded. Skipping..."
else
# Get the json file with the latest version in it
appLatestVersionJSON="$autoPkgCache/$recipe/latest_version.json"
# Use jq to parse json and extract version number
appVersion="$( "$jqLocation" -r '.version' "$appLatestVersionJSON" )"
# Get the patch title id from autopkg recipe
patchTitleID="$( /usr/local/bin/autopkg info "$recipe" | grep "PATCH_SOFTWARE_TITLE_ID" | awk -F "'" '{print $4}' )"
# Use patch title id to get the patch title name id (identifies actual name even if chagned in Jamf Pro UI)
patchTitleNameID="$( /usr/bin/curl -s -H "authorization: Bearer ${api_token}" \
"Accept: text/xml" --request GET \
"$jamfURL"/JSSResource/patchsoftwaretitles/id/"$patchTitleID" | \
/usr/bin/xmllint --format - | \
/usr/bin/grep -e "<name_id>" )"
# Use the patch title name id to get the jamf patch title name
patchTitleName="$( /usr/bin/grep -A4 "$patchTitleNameID" "$patchAvailableTitles" | \
/usr/bin/awk -F "<app_name>|</app_name>" '{ print $2 }' | xargs )"
# If if any required data is mising, skip recipe
if [[ -z $appLatestVersionJSON ]]; then
echo "[ ] No latest_version.json in $autoPkgCache/$recipe directory. Skipping $recipe..."
elif [[ -z $appVersion ]] || [[ $appVersion == "null" ]]; then
echo "[ ] No version information found in latest_version.json for $recipe. Skipping..."
elif [[ -z $patchTitleID ]]; then
echo "[ ] Unable to determine Patch Title ID. Is it defined in $recipe? Skipping..."
elif [[ -z $patchTitleNameID ]]; then
echo "[ ] Unable to determine the Patch Title name_id for $recipe. Skipping..."
elif [[ -z $patchTitleName ]]; then
echo "[ ] Unable to match name_id: $patchTitleNameID to a Patch Title Name for $recipe. Skipping..."
# If all required data is present, add it to the smart group
else
echo " <criterion>
<name>Patch Reporting: $patchTitleName</name>
<priority>$priority</priority>
<and_or>or</and_or>
<search_type>less than</search_type>
<value>$appVersion</value>
<opening_paren>false</opening_paren>
<closing_paren>false</closing_paren>
</criterion>" >> $smartGroupData
echo "[+] Added $patchTitleName version $appVersion to $groupName XML file"
# Increment priority by 1
((priority=priority+1))
fi
fi
done < "$recipeList"
# Add closing tags to xml for smart group
echo " </criteria>
</computer_group>" >> "$smartGroupData"
#####################################################################################################
# GET INPUTS
# Allow for passing dry-run input
while test $# -gt 0; do
case "$1" in
--dry-run)
# Set the dry-run flag to true
dryRun=1
;;
esac
shift
done
#####################################################################################################
# UPDATE SMART GROUP
# Check for dry run and update smart group accordingly
if [[ $dryRun = 1 ]]; then
echo "Dry run complete. XML data is located at: $smartGroupData"
echo "Nothing uploaded. Nothing changed on Jamf Pro Server $jamfURL"
else
updateSmartGroup
fi
#####################################################################################################
# INVALIDATE TOKEN
# Use function to invalidate Jamf Pro API token
InvalidateToken
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment