Skip to main content

I figured this would be helpful here. We use it to remotely grab logs from a Mac. Throw the script in a check-in policy and assign a computer to it. At next check-in, you will have their logs.

This script will output the network quality. It then uses the API Role/Client 'Computer Attachments' (Update Computers, Read Computers, Create Computers) to pull the JAMF Computer ID using Mac serial number. It then runs sysdiagnose to create logs. It will then upload the logs to the Attachments section in the JSS portal. I set access token time to 300 because the file it uploads can be 400MB.

Logs can be then downloaded from JAMF - Computers - Computer - Attachments.

#!/bin/zsh --no-rcs

## AUTHOR: Joshua Clark
## DATE: 09/06/2025
## PURPOSE: This script uses the API Role/Client 'Computer Attachments'
## to pull the computer id using Mac serial number. It then runs
## sysdiagnose to create logs. It will then upload the logs to the Attachments
## section in the JSS portal.

## NOTE: Client ID and secret are only good for PROD.

client_id='XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXX' #Computer Attachments
client_secret='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
url='https://ORGANIZATION.jamfcloud.com'

getAccessToken() {
response=$(curl --silent --location --request POST "${url}/api/oauth/token" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=${client_id}" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_secret=${client_secret}")
access_token=$(echo "$response" | plutil -extract access_token raw -)
token_expires_in=$(echo "$response" | plutil -extract expires_in raw -)
token_expiration_epoch=$(($current_epoch + $token_expires_in - 1))
}

checkTokenExpiration() {
current_epoch=$(date +%s)
if [[ token_expiration_epoch -ge current_epoch ]]
then
echo $(date +%Y/%m/%d_%H:%M:%S)" - Token valid until the following epoch time: " "$token_expiration_epoch"
else
echo $(date +%Y/%m/%d_%H:%M:%S)" - No valid token available, getting new token"
getAccessToken
fi
}

invalidateToken() {
responseCode=$(curl -w "%{http_code}" -H "Authorization: Bearer ${access_token}" $url/api/v1/auth/invalidate-token -X POST -s -o /dev/null)
if [[ ${responseCode} == 204 ]]
then
echo $(date +%Y/%m/%d_%H:%M:%S)" - Token successfully invalidated"
access_token=""
token_expiration_epoch="0"
elif [[ ${responseCode} == 401 ]]
then
echo $(date +%Y/%m/%d_%H:%M:%S)" - Token already invalid"
else
echo $(date +%Y/%m/%d_%H:%M:%S)" - An unknown error occurred invalidating the token"
fi
}

rawurlencode() {
local string="${1}"
local strlen=${#string}
local encoded=""
local pos c o

for (( pos=0 ; pos<strlen ; pos++ )); do
c=${string:$pos:1}
case "$c" in
[-._~a-zA-Z0-9] ) o="${c}" ;;
* ) printf -v o '%%%02x' "'$c"
esac
encoded+="${o}"
done
echo "${encoded}"
}

## CHECK NETWORK QUALITY
echo $(date +%Y/%m/%d_%H:%M:%S)" - Checking network quality..."
/usr/bin/networkquality > "/tmp/networkquality.txt"
cat "/tmp/networkquality.txt"

## GET TOKEN
checkTokenExpiration

## GET COMPUTER ID BY SERIAL
SERIALNUMBER=$(system_profiler SPHardwareDataType | grep Serial | awk 'BEGIN {FS=":"}; {print $2}' | xargs)
MAC_ID=$(/usr/bin/curl \
"${url}/JSSResource/computers/serialnumber/${SERIALNUMBER}" \
--request GET \
--silent \
--header "accept: text/xml" \
--header "Authorization: Bearer ${access_token}" | \
xmllint --xpath '/computer/general/id/text()' -)

echo $(date +%Y/%m/%d_%H:%M:%S)" - The Computer ID is $MAC_ID."

## RUNS DIAGNOSE
echo $(date +%Y/%m/%d_%H:%M:%S)" - Removing old sysdiagnose files from /tmp... "
rm -fv /tmp/sysdiagnose_*
sleep 3
echo $(date +%Y/%m/%d_%H:%M:%S)" - Running sysdiagnose and storing to /tmp... "
/usr/bin/sysdiagnose -u -f /tmp > /dev/null
echo $(date +%Y/%m/%d_%H:%M:%S)" - Process sysdiagnose complete... "
FILE_TO_UPLOAD_PATH=$(ls /tmp/sysdiagnose_* )

echo $(date +%Y/%m/%d_%H:%M:%S)" - Output file is ${FILE_TO_UPLOAD_PATH}"
sleep 3

## CANCEL TOKEN
invalidateToken

## GET TOKEN
checkTokenExpiration

## SEND FILE
if [[ -f "${FILE_TO_UPLOAD_PATH}" ]]; then
echo $(date +%Y/%m/%d_%H:%M:%S)" - Uploading file to ${url}..."
/usr/bin/curl -X POST \
-H "Authorization: Bearer ${access_token}" \
-F name=@"${FILE_TO_UPLOAD_PATH}" \
${url}/JSSResource/fileuploads/computers/id/${MAC_ID}
fi

## CANCEL TOKEN
invalidateToken

OUTPUT:

==== SUMMARY ====
Uplink capacity: 32.446 Mbps
Downlink capacity: 308.621 Mbps
Responsiveness: Low (954.598 milliseconds | 62 RPM)
Idle Latency: 80.120 milliseconds | 748 RPM
2025/10/06_13:49:12 - No valid token available, getting new token
2025/10/06_13:49:13 - The Computer ID is 4558.
2025/10/06_13:49:13 - Removing old sysdiagnose files from /tmp... 
/tmp/sysdiagnose_2025.10.06_13-34-54-0500_macOS_MacBookPro18-2_24G231.tar.gz
2025/10/06_13:49:16 - Running sysdiagnose and storing to /tmp... 
2025/10/06_13:51:59 - Process sysdiagnose complete... 
2025/10/06_13:51:59 - Output file is /tmp/sysdiagnose_2025.10.06_13-49-16-0500_macOS_MacBookPro18-2_24G231.tar.gz
2025/10/06_13:52:02 - Token successfully invalidated
2025/10/06_13:52:02 - No valid token available, getting new token
2025/10/06_13:52:03 - Uploading file to https://ORGANIZATION.jamfcloud.com...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0  351M    0     0    0 2175k      0  2534k  0:02:21 --:--:--  0:02:21 2535k
  1  351M    0     0    1 3903k      0  2100k  0:02:51  0:00:01  0:02:50 2100k
...

Don't upload logs to Jamf’s database.  It’s not made to handle it.  Offload the stuff you have to a cloud storage (Sharepoint, Dropbox, Box, etc.)


@Chubs I came here to say the same thing. Jamf advises against using attachments for anything due to the database not being designed to hold the data and it clogging things up. Its beyond me why Jamf stull supports API uploads like this.