A proof-of-concept, caveat emptor workflow for securely executing a repository-hosted script
Background
While EDR tools can excel at running one-off code on a limited number of endpoints, device management solutions are often best suited for executing predefined policies at scale.
EDR Script Runner strives to strike a balance between the immediate, dynamic needs of threat hunting teams and the reliability of a MDM server, by securely executing a repository-hosted script, only when necessary.
Workflow
- An authorized administrator edits
edrScript.zsh
, generates its hash (asedrScriptHash.txt
), then commits and pushes both to a private, secure repository - The script — and its hash — are ready to be downloaded client-side to the target Mac
- The MDM server instructs the Mac to execute
edrScriptRunner.zsh
, which first validates the checksum, provided by either:edrScriptHash.txt
- Jamf Pro Policy Parameter
- After the checksum has been validated,
edrScript.zsh
is downloaded client-side and the script’sscriptModificationTimestamp
variable is compared to the last client-side execution- If the modification dates differ, the script is executed as
root
(caveat emptor)
- If the modification dates differ, the script is executed as
Configuration
Complete the following steps to securely (?) execute a repository-hosted script with Jamf Pro.
A. Customize the scripts for your environment
edrScript.zsh
- Review and adjust the Global Variables as required for your environment
scriptLog
(i.e., the location of your client-side logs)
#################################################################################################### # # Global Variables # #################################################################################################### export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin/ # Script Version scriptVersion="0.0.3" # Client-side Log scriptLog="/var/log/org.test.edr.log" # Script Modification Timestamp (i.e., $( date '+%Y-%m-%d-%H%M%S' | tr -d '\n' | pbcopy ) ) scriptModificationTimestamp="2024-09-18-045533" # Initialize SECONDS SECONDS="0"
- Set your preferred Organization Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Organization Variables # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Script Human-readabale Name humanReadableScriptName="EDR Script" # Organization's Script Name organizationScriptName="EDR"
- Edit the script’s actual
command
as desired
#################################################################################################### # # Program # #################################################################################################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Proof-of-concept Command Substitution: `jamf about` # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # notice "Proof-of-concept Command Substitution" logComment "jamf about" command=$( /usr/local/bin/jamf about ) if [[ "$?" == "0" ]]; then logComment "Successful execution" logComment "jamf about: ${command}" else errorOut "Execution error" fi
edrScriptRunner.zsh
- Review and adjust the Global Variables as required for your environment
scriptLog
(i.e., the location of your client-side logs)
#################################################################################################### # # Global Variables # #################################################################################################### export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin/ # Script Version scriptVersion="0.0.5" # Client-side Log scriptLog="/var/log/org.test.log" # Initialize SECONDS SECONDS="0" # Temporary Working Directory workDirectory=$( basename "${0%%.*}" ) tempDirectory=$( mktemp -d "/private/tmp/$workDirectory.XXXXXX" )
- Set your preferred Organization Variables
orgRepo
(i.e., the location of your organization’s private, secure code repository)orgFile
(i.e., the actual file name of the script which will be executed)orgPlist
(i.e., your organization’s client-side.plist
)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Organization Variables # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Script Human-readabale Name humanReadableScriptName="EDR Script Runner" # Organization's Script Name organizationScriptName="EDRr" # Organization's Repository orgRepo="https://raw.githubusercontent.com/dan-snelson/Jamf-Pro-Scripts/development/EDR/" # Organization's File orgFile="edrScript.zsh" # Organization's .plist orgPlist="/Library/Preferences/org.test.plist"
- Set your preferred script defaults (which will be trumped in a policy’s script parameters)
- Parameter 4:
checksumSource
(i.e., script (default) or policy) - Parameter 5:
expectedScriptChecksum
(i.e., the actual checksum oforgFile
)openssl dgst -sha256 "edrScript.zsh" | awk -F'= ' '{print $2}' > edrScriptHash.txt
- Parameter 4:
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Jamf Pro Script Parameters # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Parameter 4: Checksum Source [ script (default) | policy ] checksumSource="${4:-"script"}" # Parameter 5: Expected Script Checksum case ${checksumSource} in script ) expectedScriptChecksum=$( curl --location --silent --fail "${orgRepo}/${orgFile%%.*}Hash.txt" ) ;; policy | * ) expectedScriptChecksum="${5:-"55b9765ed20cd1563382e79d5af7f5505fb6be55488c9b1c7effbfe2b64c8c3b"}" ;; esac
B. Add the edrScriptRunner.zsh script to your Jamf Pro server
- Add the
edrScriptRunner.zsh
script to your Jamf Pro server - Specify the following for Options > Parameter Labels
- Parameter 4:
Checksum Source [ script (default) | policy ]
- Parameter 5:
Expected Script Checksum (used when "Checksum Source" is set to "policy")
- Parameter 4:
- Click Save
C. Create a Jamf Pro Policy to execute edrScriptRunner.zsh
- Create a new Jamf Pro Policy, using the following as a guide for Options > General:
- Set Display Name to
EDR Script Runner (0.0.5)
- Enable Trigger > Recurring Check-in
- Set Execution Frequency to
Once every month
- Set Display Name to
- Select the Scripts payload and add the
edrScriptRunner.zsh
script, specifying the following Parameter Values- Checksum Source:
script
🔥 allowsroot
execution with no interaction from the Device Management teampolicy
requires coordination between the SecOps and Device Management teams to update the checksum and flush policy logs
- Expected Script Checksum: actual checksum of
orgFile
- Checksum Source:
- Scope the policy as desired
- Click Save
D. Testing
Checksum Generation
Each time the orgFile
script is updated, execute the following command to update its checksum:
openssl dgst -sha256 "edrScript.zsh" | awk -F'= ' '{print $2}' > edrScriptHash.txt
Repeated Executions
To test repeated executions, SecOps and Device Management teams will need to actively coordinate to:
- Flush policy logs
- Delete client-side
.plist
keys:defaults delete "${orgPlist}" "EDR Script Runner"
- Manually execute the Jamf Pro policy