|
#!/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 |