Last active
December 21, 2022 15:11
-
-
Save sgmills/d5f2385647dde2b16124a6f4a8f867c1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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