Good, better and best methods to leave and retrieve client-side breadcrumbs with Jamf Pro
Introduction
As a Jamf Pro administrator managing Apple computers running macOS, you frequently need to leave a client-side record when a policy was executed, which is sometimes referred to as a “breadcrumb”; you also need an easy way to retrieve this data.
Terminal Example
The following example Terminal command will:
- Create a text file on your Desktop:
~/Desktop
- Containing your user name:
id -n -u
- With the date when the command was executed:
date
echo `date` >> ~/Desktop/`id -n -u`_was_here.txt
Copy-pasta the above command into a Terminal window and press Return.
You can press the Up Arrow key once to display the last command again, then press Return.
Repeat this a few more times, then run the following command to read the evidence that You. Were. Here.
ls ~/Desktop/`id -n -u`_was_here.txt && cat ~/Desktop/`id -n -u`_was_here.txt
Options
In the author’s highly biased opinion, here are the good, better and best options for writing and reading client-side breadcrumbs.
Leave
Good
Execute Command
If you have modest needs, Jamf Pro’s built-in Files and Processes > Execute Command feature is a great option.
Policy
In either a new or existing policy, add the Files and Processes payload and specify the following in the Execute Command field, taking note of the included description:
Command to execute on computers. This command is executed as the
root
user
/bin/mkdir -pv /Library/Management/org.churchofjesuschrist && /usr/bin/touch /Library/Management/org.churchofjesuschrist/.breadcrumb
The above example will:
- Create an organizational-specific directory, nested inside the “Management” directory, which is nested inside the
/Library
directory:
/bin/mkdir -pv /Library/Management/org.churchofjesuschrist
- If — and only if — the first command succeeds:
&&
- Create the
.breadcrumb
file inside the organizational-specific directory:/usr/bin/touch /Library/Management/org.churchofjesuschrist/.breadcrumb
While you can’t create files beginning with a period (.) in the Finder …
… doing so via the above command hides the file from most users, as shown in the output of:ls -lah /Library/Management/org.churchofjesuschrist
# ls -lah /Library/Management/org.churchofjesuschrist total 0 drwxr-xr-x 3 root wheel 96B Dec 2 05:02 . drwxr-xr-x 3 root wheel 96B Dec 2 05:02 .. -rw-r--r-- 1 root wheel 0B Dec 2 05:02 .breadcrumb
Better
Temporary File Create (0.0.1)
Leveraging a dedicated script to create temporary files provides both predictability and speed.
Script
After adding the following Temporary File Create script to your Jamf Pro server, adjust the path
variable for your environment …
#!/bin/bash ################################################################################ # # ABOUT # # Create a static file client-side which a JSS Extension Attribute will use # to determine Smart Group membership # ################################################################################ # # HISTORY # # Version 0.0.1, 24-Aug-2015, Dan K. Snelson # Original version # ################################################################################ # Variables path="/Library/Management/org.churchofjesuschrist/" # Hard-coded path file="$4" # Unique filename (i.e., ".issueNewRecoveryKey") # Validate a value has been specified for Parameter 4 if [ -n "${file}" ]; then # Create the directory /bin/mkdir -pv "${path}" # Create the file at the path specified /usr/bin/touch "${path}${file}" # Set the permission on the file /usr/sbin/chown root:wheel "${path}${file}" /usr/sbin/chown 644 "${path}${file}" echo "Created ${file}" else echo "Error: Parameter 4 not populated; exiting." exit 1 fi exit 0
… and specify Unique filename as the value for Parameter 4.
Policy
In either a new or existing policy, add the Scripts payload, select the Temporary File Create script and specify the desired filename.
- Unique filename:
.issueNewRecoveryKey
Pro Tip: Include a period (.) as the first character to the filename to hide it from most users in the Finder
# ls -lah /Library/Management/org.churchofjesuschrist/ total 0 drwxr-xr-x 3 root wheel 96B Dec 2 08:20 . drwxr-xr-x 3 root wheel 96B Dec 2 08:20 .. -rw-r--r-- 1 644 wheel 0B Dec 2 08:20 .issueNewRecoveryKey
Bonus
Temporary File Delete (0.0.1)
The following, undocumented script will remove the file created by its companion script (referenced above).
#!/bin/bash ################################################################################ # # ABOUT # # Remove a static file client-side which a JSS Extension Attribute will use # to determine Smart Group membership # ################################################################################ # # HISTORY # # Version 0.0.1, 24-Aug-2015, Dan K. Snelson # Original version # ################################################################################ # Variables path="/Library/Management/org.churchofjesuschrist/" # Hard-coded path file="$4" # Unique filename (i.e., ".issueNewRecoveryKey") # Validate a value has been specified for Parameter 4 if [ -n "${file}" ]; then # Remove the file at the path specified /bin/rm -fv "${path}${file}" else echo "Error: Parameter 4 not populated; exiting." exit 1 fi exit 0
Best
Property List Writer (0.0.3)
Script
Leverages Jamf Pro Script Parameters to write a given
string
to the specifiedkey
in a hard-codedfilepath
.
- Add the
Property List Writer
script to your Jamf Pro server - Adjust the value of
reverseDomainNameNotation
for your environment - Specify the following for Options > Parameter Labels
- Parameter 4:
Key (i.e., Name of the "key" for which the value will be set)
- Parameter 5:
Value (i.e., The value to which "key" will be set)
- Parameter 4:
- Click Save
#!/bin/bash #################################################################################################### # # Property List Writer # # Leverages Jamf Pro Script Parameters to write a given string to the specified key # in a hard-coded filepath. # # Reference: https://support.apple.com/guide/terminal/edit-property-lists-apda49a1bb2-577e-4721-8f25-ffc0836f6997/mac # #################################################################################################### # # HISTORY # # Version 0.0.1, 19-Mar-2021, Dan K. Snelson (@dan-snelson) # Original version # # Version 0.0.2, 25-Mar-2022, Dan K. Snelson (@dan-snelson) # Remove dependency on client-side functions # # Version 0.0.3, 06-May-2022, Dan K. Snelson (@dan-snelson) # Added timestamp # #################################################################################################### #################################################################################################### # # Global Variables # #################################################################################################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Script Version and Jamf Pro Script Parameters # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # scriptVersion="0.0.3" export PATH=/usr/bin:/bin:/usr/sbin:/sbin reverseDomainNameNotation="org.churchofjesuschrist" filepath="/Library/Preferences/${reverseDomainNameNotation}.plist" scriptLog="/var/log/${reverseDomainNameNotation}.log" timestamp=$( date '+%Y-%m-%d-%H%M%S' ) key="${4}" # Name of the "key" for which the value will be set value="${5}" # The value to which "key" will be set #################################################################################################### # # Pre-flight Checks # #################################################################################################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Pre-flight Check: Client-side Logging # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # if [[ ! -f "${scriptLog}" ]]; then touch "${scriptLog}" fi # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Pre-flight Check: Client-side Script Logging Function # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # function updateScriptLog() { echo -e "$( date +%Y-%m-%d\ %H:%M:%S ) - ${1}" | tee -a "${scriptLog}" } # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Pre-flight Check: Logging Preamble # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # updateScriptLog "\n\n###\n# Property List Writer (${scriptVersion})\n###\n" updateScriptLog "PRE-FLIGHT CHECK: Initiating …" # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Pre-flight Check: Confirm script is running as root # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # if [[ $(id -u) -ne 0 ]]; then updateScriptLog "PRE-FLIGHT CHECK: This script must be run as root; exiting." exit 1 else updateScriptLog "PRE-FLIGHT CHECK: Running as 'root'; proceeding …" fi # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Pre-flight Check: Exit if either "key" or "value" are blank # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # if [[ -z "${key}" ]] || [[ -z "${value}" ]]; then updateScriptLog "PRE-FLIGHT CHECK: Error: Please provide data for both the 'key' and 'value'." exit 1 else updateScriptLog "PRE-FLIGHT CHECK: Both the 'key' and 'value' populated; proceeding …" updateScriptLog "PRE-FLIGHT CHECK: Complete" fi #################################################################################################### # # Functions # #################################################################################################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Write Plist Value # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # function writePlistValue() { # Variables key="${1}" # Name of the "key" for which the value will be set value="${2}" # The value to which "key" will be set updateScriptLog "Write Plist Value: \"${key}\" \"${value}\" " /usr/bin/defaults write "${filepath}" "${key}" -string "${value}" } # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Read Plist Value # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # function readPlistValue() { # Variables key="${1}" # Name of the "key" for which the value will be set updateScriptLog "Read Plist Value:\"${key}\"" writtenValue=$( /usr/bin/defaults read "${filepath}" "${key}" 2>&1 ) updateScriptLog "${writtenValue}" } #################################################################################################### # # Program # #################################################################################################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Backup Plist File # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # updateScriptLog "Backup Plist File" # /bin/cp -v ${filepath}{,-backup-$(date '+%Y-%m-%d-%H%M%S')} # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Write Plist Value # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # writePlistValue "${key}" "${value}" writePlistValue "${key} timestamp" "${timestamp}" # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Read Plist Value # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # readPlistValue "${key}" # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Exit # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # updateScriptLog "So Long, Farewell!" exit 0
Policy
In either a new or existing policy, add the Scripts payload, select the Property List Writer script and specify the desired key
and value
.
For example, if you were leveraging Graham Pugh’s erase-install, specify the following to know to which version — and when — users upgraded to macOS Sonoma:
- Key:
macOS Sonoma Upgrade
- Value:
14.1.2 (23B92)
# defaults read /Library/Preferences/org.churchofjesuschrist.plist { "macOS Sonoma Upgrade" = "14.1.2 (23B92)"; "macOS Sonoma Upgrade timestamp" = "2023-12-02-063100"; }
Retrieve
Good
Extension Attribute
Again, if you have modest needs — while more error-prone — a quick-and-dirty Extension Attribute will get you on your way.
Note: Ensure to accurately specify the filepath
of the desired client-side breadcrumb.
#!/bin/bash # Extension Attribute to read the status of FileVault New Recovery Key if [ -f "/Library/Management/org.churchofjesuschrist/.issueNewRecoveryKey" ] ; then result="True" else result="False" fi echo "<result>$result</result>"
filepath
of the desired client-side breadcrumb.Better
In this case, there is no better option than good; proceed to the best option.
Best
Extension Attribute
Property List Reader (0.0.1)
A script to determine the value of Property List
key
. If thekey
is not found, “N/A” will be returned.
The first time you create a so-called “Property List Reader” Extension Attribute, pay close attention to the value of filepath
, ensuring it exactly matches the value of filepath
specified in the Property List Writer
script.
Once you have your first “Property List Reader” Extension Attribute working as expected, you’ll need only change the key
variable in each additional EA to match what you specified in the corresponding policy.
Hand? Meet glove.
#!/bin/bash ######################################################### # A script to determine the value of Property List key. # # If the key is not found, "N/A" will be returned. # ######################################################### filepath="/Library/Preferences/org.churchofjesuschrist.plist" key="macOS Sonoma Upgrade" # Name of the "key" for which the value will be read value=$( /usr/bin/defaults read "${filepath}" "${key}" 2>&1 ) case "${value}" in *"does not exist" ) RESULT="N/A" ;; * ) timestamp=$( /usr/bin/defaults read "${filepath}" "${key} timestamp" 2>&1 ) RESULT="${value}: ${timestamp}" ;; esac /bin/echo "<result>${RESULT}</result>" exit 0
key="macOS Sonoma Upgrade"