Last active
January 2, 2025 14:50
-
-
Save talkingmoose/4a950a715ad07ae3baecce2f46b0a7e5 to your computer and use it in GitHub Desktop.
Download JSON or XML files for multiple Jamf Pro object types.
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/zsh | |
# set -x # show command | |
:<<ABOUT_THIS_SCRIPT | |
Written by:William Smith | |
Technical Enablement Manager | |
Jamf | |
[email protected] | |
https://gist.github.com/talkingmoose/4a950a715ad07ae3baecce2f46b0a7e5 | |
Originally posted: August 19, 2024 | |
Purpose: Download JSON or XML files for multiple Jamf Pro object types. | |
Instructions: | |
1. In Jamf Pro click Settings > System > API roles and clients. | |
2. Under API Roles create a new API role such as "Read Jamf Pro objects". | |
Set Privileges to include: | |
Read Accounts Read macOS Configuration Profiles | |
Read Advanced Computer Searches Read Mobile Device Applications | |
Read Advanced Mobile Device Searches Read Mobile Device Extension Attributes | |
Read Advanced User Content Searches Read Mobile Device PreStage Enrollments | |
Read Advanced User Searches Read Network Segments | |
Read Buildings Read Packages | |
Read Categories Read Patch Management Software Titles | |
Read Cloud Distribution Point Read Policies | |
Read Computer Extension Attributes Read Printers | |
Read Computer PreStage Enrollments Read Restricted Software | |
Read Departments Read Scripts | |
Read Directory Bindings Read Sites | |
Read Distribution Points Read Smart Computer Groups | |
Read Dock Items Read Smart Mobile Device Groups | |
Read eBooks Read Smart User Groups | |
Read Enrollment Customizations Read Static Computer Groups | |
Read Infrastructure Managers Read Static Mobile Device Groups | |
Read iOS Configuration Profiles Read Static User Groups | |
Read JSON Web Token Configuration Read User Extension Attributes | |
Read LDAP Servers Read Volume Purchasing Locations | |
Read Mac Applications Read Webhooks | |
Under API Clients create a new API client such as "Back up Jamf Pro objects". | |
Set API roles to: | |
Read Jamf Pro objects | |
Set access token lifetime to a higher value such as 300 seconds (5 minutes). | |
The script will renew the token as needed. | |
Enable the API client and copy the client ID and client secret. | |
3. Update the jamfProURL, clientID, and clientSecret variables below. | |
4. Specify a saveLocation such as "/Users/Shared" or a folder managed | |
by file sync service that offers version control (e.g. Dropbox or GitHub). | |
5. By default, this script will download and create object JSON or XML | |
files for all the OBJECT TYPES listed below. Add a "#" to the beginning | |
of those object types to comment them and prevent download. | |
NOTE: You may need to add additional object types for download and | |
add them to the API role. | |
Except where otherwise noted, this work is licensed under | |
http://creativecommons.org/licenses/by/4.0/ | |
"If you fail to plan, you are planning to fail." | |
— Benjamin Franklin | |
ABOUT_THIS_SCRIPT | |
jamfProURL="https://talkingmoose.jamfcloud.com" | |
clientID="3c21600a-9740-4fb0-906b-3e6d9986f540" | |
clientSecret="MEE2yrbMcvH_KQPnOjf4HrXFQoK8tONTKlXdIiKrkxoIEJK2YcQlr_2Ek73eRGVi" | |
saveLocation="/Users/Shared/" | |
# create a log file with the same name as the script | |
currentScript=$( /usr/bin/basename -s .zsh "$0" ) | |
log="${saveLocation}Jamf Pro Objects/$currentScript.log" | |
# when checkResponseCode is called in the script, return the HTTP status code | |
# with its human-readable meaning | |
function checkResponseCode() { | |
httpErrorCodes="000 No HTTP code received | |
200 Request successful | |
201 Request to create or update object successful | |
204 No content (successful) | |
400 Bad request | |
401 Authentication failed | |
403 Invalid permissions | |
404 Object/resource not found | |
409 Conflict | |
422 Unprocessable Content | |
429 Too many requests | |
500 Internal server error | |
503 Service unavailable" | |
responseCode=${1: -3} | |
code=$( /usr/bin/grep "$responseCode" <<< "$httpErrorCodes" ) | |
echo "$code" | |
} | |
# when logcomment is called in the script, write a time-stamped comment | |
# to the log | |
function logcomment() { | |
/bin/date "+%Y-%m-%d %H:%M:%S $1" >> "$log" | |
echo "$1" | |
} | |
# when logresult is called in the script, evaluate whether the prior command | |
# succeeded or failed and write a time-stammped result to the log | |
function logresult() { | |
if [ $? = 0 ] ; then | |
/bin/date "+%Y-%m-%d %H:%M:%S $1" >> "$log" | |
else | |
/bin/date "+%Y-%m-%d %H:%M:%S $2" >> "$log" | |
fi | |
} | |
# when requestAccessToken is called in the script, expire the current | |
# access token, then request a new access token that will live for 10 minutes (as specified in the API client settings | |
# in Jamf Pro | |
function requestAccessToken { | |
# destroy current access token | |
destroyAccessToken | |
# request access token | |
logcomment "Requesting new access token." | |
accessTokenResponse=$( /usr/bin/curl \ | |
--data-urlencode "grant_type=client_credentials" \ | |
--data-urlencode "client_id=$clientID" \ | |
--data-urlencode "client_secret=$clientSecret" \ | |
--header "Content-Type: application/x-www-form-urlencoded" \ | |
--request POST \ | |
--silent \ | |
--url "$jamfProURL/api/oauth/token" \ | |
--write-out "%{http_code}" ) | |
# report the status of the access token | |
checkResponseCode "$accessTokenResponse" | |
# extract token data from response | |
accessToken=${accessTokenResponse%???} | |
# parse token from response | |
token=$( /usr/bin/plutil -extract access_token raw - <<< "$accessToken" ) | |
# parse token life from response | |
tokenLife=$( /usr/bin/plutil -extract expires_in raw - <<< "$accessToken" ) | |
logcomment "Access token expires in $tokenLife seconds" | |
# Get the access token lifetime set in API roles and clients | |
# and renew 15 seconds before it expires | |
renewalTime=$(( $( date '+%s' ) + $tokenLife - 15 )) | |
} | |
function destroyAccessToken { | |
# expire the access token | |
logcomment "Destroying current access token." | |
expireToken=$( /usr/bin/curl \ | |
--header "Authorization: Bearer $token" \ | |
--request POST \ | |
--silent \ | |
--url "$jamfProURL/api/v1/auth/invalidate-token" \ | |
--write-out "%{http_code}" ) | |
# report the status of the access token | |
checkResponseCode "$expireToken" | |
} | |
# when downloadClassicObjectsXML is called in the script, | |
# 1. get a list of IDs for the given Jamf Pro object type | |
# 2. download the object XML for each ID found | |
function downloadClassicObjectsXML { | |
logcomment "Downloading list of $1 IDs." | |
# create list of object IDs | |
ids=$( /usr/bin/curl \ | |
--header "accept: text/xml" \ | |
--header "Authorization: Bearer $token" \ | |
--request GET \ | |
--silent \ | |
--url "$jamfProURL/JSSResource${2}" \ | |
--write-out "%{http_code}" ) | |
# print the HTTP response code and its meaning | |
checkResponseCode "$ids" | |
# convert XML into human-readable XML | |
formattedXML=$( /usr/bin/xmllint --format - <<< "${ids%???}" ) | |
# extract a list of all the object ID numbers and sort them | |
idsList=$( /usr/bin/sed -n 's/.*<id>\(.*\)<\/id>.*/\1/p' <<< "$formattedXML" | /usr/bin/sort --human-numeric-sort --unique ) | |
if [[ "$idsList" = "" ]]; then | |
logcomment "Found no $1 objects" | |
return | |
fi | |
# create directories for saving files as needed | |
/bin/mkdir -p "$saveLocation/Jamf Pro Objects/$1" | |
# download object XML for each ID | |
while IFS= read anID | |
do | |
now=$( /bin/date +"%s" ) | |
if [[ "$renewalTime" -lt "$now" ]]; then | |
requestAccessToken | |
fi | |
logcomment "Downloading \"$1 ID $anID\" XML" | |
# download object XML | |
objectXML=$( /usr/bin/curl \ | |
--header "accept: text/xml" \ | |
--header "Authorization: Bearer $token" \ | |
--request GET \ | |
--silent \ | |
--url "$jamfProURL/JSSResource${2}/id/$anID" \ | |
--write-out "%{http_code}" ) | |
formattedObjectXML=$( /usr/bin/xmllint --format - <<< "${objectXML%???}" ) | |
checkResponseCode "$objectXML" | |
objectName=$( /usr/bin/awk -F '[><]' '/displayName|name|packageName/ { print $3; exit }' <<< "$formattedObjectXML" ) | |
# extract data from request | |
echo "$( /usr/bin/xmllint --format - <<< ${objectXML%???} )" > "${saveLocation}Jamf Pro Objects/$1/$objectName (ID $anID).xml" | |
logresult "Downloaded \"$objectName (ID $anID).xml\"" "FAILED downloading \"$objectName (ID $anID).xml\"" | |
done <<< "$idsList" | |
} | |
# when downloadObjectsJSON is called in the script, | |
# 1. get a list of IDs for the given Jamf Pro object type | |
# 2. download the object JSON for each ID found | |
function downloadObjectsJSON { | |
logcomment "Downloading list of $1 IDs." | |
# create list of object IDs | |
ids=$( /usr/bin/curl \ | |
--header "accept: application/json" \ | |
--header "Authorization: Bearer $token" \ | |
--request GET \ | |
--silent \ | |
--url "$jamfProURL/api${2}" \ | |
--write-out "%{http_code}" ) | |
# print the HTTP response code and its meaning | |
checkResponseCode "$ids" | |
# extract a list of all the object ID numbers and sort them | |
idsList=$( /usr/bin/awk -F '"' '/"id"/ { print $4 }' <<< "${ids%???}" | /usr/bin/sort --human-numeric-sort --unique ) | |
if [[ "$idsList" = "" ]]; then | |
logcomment "Found no $1 objects" | |
return | |
fi | |
# create directories for saving files as needed | |
/bin/mkdir -p "$saveLocation/Jamf Pro Objects/$1" | |
# download object JSON for each ID | |
while IFS= read anID | |
do | |
now=$( /bin/date +"%s" ) | |
if [[ "$renewalTime" -lt "$now" ]]; then | |
requestAccessToken | |
fi | |
logcomment "Downloading \"$1 ID $anID\" JSON" | |
# download object JSON | |
objectJSON=$( /usr/bin/curl \ | |
--header "accept: application/json" \ | |
--header "Authorization: Bearer $token" \ | |
--request GET \ | |
--silent \ | |
--url "$jamfProURL/api${2}/$anID" \ | |
--write-out "%{http_code}" ) | |
checkResponseCode "$objectJSON" | |
objectName=$( /usr/bin/awk -F '"' '/displayName|name|packageName/ { print $4; exit }' <<< "$objectJSON" ) | |
# extract data from request | |
echo "${objectJSON%???}" > "$saveLocation/Jamf Pro Objects/$1/$objectName (ID $anID).json" | |
logresult "Downloaded \"$objectName (ID $anID).json\"" "FAILED downloading \"$objectName (ID $anID).json\"" | |
done <<< "$idsList" | |
} | |
# timestamp when the script begins | |
startTime=$( /bin/date '+%s' ) | |
requestAccessToken | |
# OBJECT TYPES | |
# Each line below downloads every object's data for each object type specified. | |
# First determine whether to use the Classic or Jamf Pro API. | |
# Provide a human-readable name for the object type in the first parameter. | |
# Provide the name of the endpoint in the second parameter. | |
# Use a "#" at the beginning of each line you want to prevent from downloading. | |
downloadClassicObjectsXML "Advanced Computer Searches" "/advancedcomputersearches" | |
downloadClassicObjectsXML "Computer Groups" "/computergroups" | |
downloadClassicObjectsXML "Directory Bindings" "/directorybindings" | |
downloadClassicObjectsXML "Distribution Points" "/distributionpoints" | |
downloadClassicObjectsXML "Dock Items" "/dockitems" | |
# downloadClassicObjectsXML "Healthcare Listener" "/healthcarelistener" | |
# downloadClassicObjectsXML "Healthcare Listener Rule" "/healthcarelistenerrule" | |
downloadClassicObjectsXML "Infrastructure Manager" "/infrastructuremanager" | |
downloadClassicObjectsXML "JSON Web Token Configurations" "/jsonwebtokenconfigurations" | |
downloadClassicObjectsXML "LDAP Servers" "/ldapservers" | |
downloadClassicObjectsXML "Mac Applications" "/macapplications" | |
downloadClassicObjectsXML "Mobile Device Applications" "/mobiledeviceapplications" | |
downloadClassicObjectsXML "Mobile Device Configuration Profiles" "/mobiledeviceconfigurationprofiles" | |
downloadClassicObjectsXML "Mobile Device Extension Attributes" "/mobiledeviceextensionattributes" | |
downloadClassicObjectsXML "Mobile Device Groups" "/mobiledevicegroups" | |
downloadClassicObjectsXML "Network Segments" "/networksegments" | |
downloadClassicObjectsXML "Computer Configuration Profiles" "/osxconfigurationprofiles" | |
downloadClassicObjectsXML "Policies" "/policies" | |
downloadClassicObjectsXML "Printers" "/printers" | |
downloadClassicObjectsXML "Restricted Software" "/restrictedsoftware" | |
downloadClassicObjectsXML "Sites" "/sites" | |
downloadClassicObjectsXML "User Extension Attributes" "/userextensionattributes" | |
downloadClassicObjectsXML "User Groups" "/usergroups" | |
downloadClassicObjectsXML "Webhooks" "/webhooks" | |
downloadObjectsJSON "Advanced Mobile Device Searches" "/v1/advanced-mobile-device-searches" | |
downloadObjectsJSON "Buildings" "/v1/buildings" | |
downloadObjectsJSON "Computer Extension Attributes" "/v1/computer-extension-attributes" | |
downloadObjectsJSON "Computer PreStages" "/v3/computer-prestages" | |
downloadObjectsJSON "Departments" "/v1/departments" | |
downloadObjectsJSON "eBooks" "/v1/ebooks" | |
downloadObjectsJSON "Enrollment Customizations" "/v2/enrollment-customizations" | |
downloadObjectsJSON "Mobile Device PreStages" "/v2/mobile-device-prestages" | |
downloadObjectsJSON "Packages" "/v1/packages" | |
downloadObjectsJSON "Patch Software Title Configurations" "/v2/patch-software-title-configurations" | |
downloadObjectsJSON "Scripts" "/v1/scripts" | |
downloadObjectsJSON "Volume Purchasing Locations" "/v1/volume-purchasing-locations" | |
destroyAccessToken | |
# timestamp when the script ends | |
stopTime=$( /bin/date '+%s' ) | |
# calculate script run time | |
logcomment "Script run time: $(($stopTime-$startTime)) seconds | |
" | |
exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment