#!/usr/bin/env zsh # Copyright (c) 2025 Huntress Labs, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the Huntress Labs nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL HUNTRESS LABS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # The Huntress installer needs an Account Key and an Organization Key (a user # specified name or description) which is used to affiliate an Agent with a # specific Organization within the Huntress Partner's Account. These keys can be # hard coded below or passed in when the script is run. # For more details, see our KB article # https://support.huntress.io/hc/en-us/articles/25013857741331-Critical-Steps-for-Complete-macOS-EDR-Deployment ############################################################################## ## Begin user modified variables ############################################################################## # Replace __ACCOUNT_KEY__ with your account secret key (from your Huntress portal's "download agent" section) defaultAccountKey="__ACCOUNT_KEY__" # If you have a preferred "placeholder" organization name for Mac agents, you can set that below. # Otherwise, provide the appropriate Organization Key when running the script in your RMM. defaultOrgKey="Mac Agents" # Put the name of your RMM below. This helps our support team understand which RMM tools # are being used to deploy the Huntress macOS Agent. Simply replace the text in quotes below. rmm="Unspecified RMM" # Option to install the system extension after the Huntress Agent is installed. In order for this to happen # without security prompts on the endpoint, permissions need to be applied to the endpoint by an MDM before this script # is run. See the following KB article for instructions: # https://support.huntress.io/hc/en-us/articles/21286543756947-Instructions-for-the-MDM-Configuration-for-macOS install_system_extension=true ############################################################################## ## Do not modify anything below this line ############################################################################## scriptVersion="April 15, 2025" version="1.1 - $scriptVersion" dd=$(date "+%Y-%m-%d %H:%M:%S") log_file="/tmp/HuntressInstaller.log" log_file_location="/Users/Shared/" install_script="/tmp/HuntressMacInstall.sh" invalid_key="Invalid account secret key" pattern="[a-f0-9]{32}" # Using logger function to provide helpful logs within RMM tools in addition to log file logger() { echo "$dd -- $*"; echo "$dd -- $*" >> $log_file; } # Copies the log from a temp location to /users/shared/ and exits with the given code # Using this folder as /tmp/ is wiped on reboot, Huntress folders are protected by TP, and because any user should have access to this folder copyLog() { # capture exit command for script finish-up local exitCode="$?" if [ -d $log_file_location ]; then logger "Copying log file to /Users/Shared/" cp "$log_file" "${log_file_location}/HuntressInstaller.log" fi if [ $exitCode -ne "0" ]; then logger "Exit with error, please send ${log_file_location}HuntressInstaller.log to support." fi exit "$exitCode" } trap copyLog EXIT # Log system info for troubleshooting logger "macOS version: $(sw_vers --ProductVersion)" logger "Free disk space: "$(df -Pk . | sed 1d | grep -v used | awk '{ print $4 "\t" }') logger $(top -l 1 | head -n 7 | tail -n 1) logger $(top -l 1 | head -n 3 | tail -n 1) logger "System uptime: $(uptime)" logger "User id (should be 0): "$(id -u) logger "Huntress install script last updated $scriptVersion" # Check for root if [ $EUID -ne 0 ]; then logger "This script must be run as root, exiting..." exit 1 fi # Clean up any old installer scripts. if [ -f "$install_script" ]; then logger "Installer file present in /tmp; deleting." rm -f "$install_script" fi ## ## This section handles the assigning `=` character for options. ## Since most RMMs treat spaces as delimiters in Mac Scripting, ## we have to use `=` to assign the option value, but must remove ## it because, well, bash. https://stackoverflow.com/a/28466267/519360 ## usage() { cat < --organization_key= -a, --account_key The account key to use for this agent install -o, --organization_key The org key to use for this agent install -t, --tags A comma-separated list of agent tags to use for this agent install -i, --install_system_extension If passed, automatically install the system extension -h, --help Print this message EOF } reinstall=false while getopts "a:o:t:ihr-:" OPT; do if [ "$OPT" = "-" ]; then OPT="${OPTARG%%=*}" # extract long option name OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty) OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=` else # the user used a short option, but we still want to strip the assigning `=` OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=` fi case "$OPT" in a | account_key) account_key="$OPTARG" ;; o | organization_key) organization_key="$OPTARG" ;; t | tags) tags="$OPTARG" ;; i | install_system_extension) logger "Running with System Extension immediate install option" install_system_extension=true ;; r | reinstall) logger "Running with the -reinstall flag" reinstall=true ;; h | help) usage exit 0 ;; ??*) logger "Illegal option --$OPT" exit 2 ;; # bad long option \? ) exit 2 ;; # bad short option (error reported via getopts) esac done shift $((OPTIND-1)) # remove parsed options and args from $@ list # try/catch, if the connectivity tester fails to execute we'll log that as an error. for hostn in "update.huntress.io" "huntress.io" "eetee.huntress.io" "huntress-installers.s3.amazonaws.com" "huntress-updates.s3.amazonaws.com" "huntress-uploads.s3.us-west-2.amazonaws.com" "huntress-user-uploads.s3.amazonaws.com" "huntress-rio.s3.amazonaws.com" "huntress-survey-results.s3.amazonaws.com"; do logger "$(nc -z -v $hostn 443 2>&1)" || (logger "error occured during network connectivity test") done # Check for existing Huntress install, if already installed exit with error. Bypass if using the reinstall flag. if [ $reinstall = false ]; then if [ -d "/Applications/Huntress.app/Contents/Macos" ]; then logger "Huntress assets found, checking for running processes" numServicesStopped=0 for HuntressProcess in "HuntressAgent" "HuntressUpdater"; do if [ $(pgrep "$HuntressProcess" > /dev/null) ]; then logger "Warning: process $HuntressProcess is stopped" numServicesStopped++ else logger "Process $HuntressProcess is running" fi done if [ $numServicesStopped -gt 0 ]; then logger "Installation appears damaged, suggest running with the -reinstall flag" else logger "Installation found and processes are running. If you suspect this agent is damaged try running this script with the -reinstall flag" fi exit 1 fi fi logger "=========== INSTALL START AT $dd ===============" logger "=========== $rmm Deployment Script | Version: $version ===============" # validate options passed to script, remove all invalid characters except spaces are converted to dash if [ -z "$organization_key" ]; then organizationKey=$(echo "$defaultOrgKey" | tr -dc '[:alnum:]- ' | tr ' ' '-' | xargs) logger "--organization_key parameter not present, using defaultOrgKey instead: $defaultOrgKey, formatted to $organizationKey " else organizationKey=$(echo "$organization_key" | tr -dc '[:alnum:]- ' | tr ' ' '-' | xargs) logger "--organization_key parameter present, set to: $organization_key, formatted to $organizationKey " fi if ! [[ "$account_key" =~ $pattern ]]; then logger "Invalid --account_key provided, checking defaultAccountKey..." accountKey=$(echo "$defaultAccountKey" | xargs) if ! [[ $accountKey =~ $pattern ]]; then # account key is invalid if script gets to this branch, so write the key unmasked for troubleshooting logger "ERROR: Invalid --account_key, $accountKey was provided. Please check Huntress support documentation." exit 1 fi else accountKey=$(echo "$account_key" | xargs) fi if [ -n "$tags" ]; then logger "using tags: $tags" fi if [ "$install_system_extension" = true ]; then logger "automatically installing system extension" fi # Hide most of the account key in the logs, keeping the front and tail end for troubleshooting masked="$(echo "${accountKey:0:4}")" masked+="************************" masked+="$(echo "${accountKey: (-4)}")" # OPTIONS REQUIRED (account key could be invalid in this branch, so mask it) if [ -z "$accountKey" ] || [ -z "$organizationKey" ] then logger "Error: --account_key and --organization_key are both required" >> $log_file logger "Account key: $masked and Org Key: $organizationKey were provided" echo usage exit 1 fi logger "Provided Huntress key: $masked" logger "Provided Organization Key: $organizationKey" result=$(curl -w "%{http_code}" -L "https://huntress.io/script/darwin/$accountKey" -o "$install_script") if [ $? != "0" ]; then logger "ERROR: Download failed with error: $result" exit 1 fi if grep -Fq "$invalid_key" "$install_script"; then logger "ERROR: --account_key is invalid. You entered: $accountKey" exit 1 fi if [ "$install_system_extension" = true ]; then install_result="$(/bin/bash "$install_script" -a "$accountKey" -o "$organizationKey" -t "$tags" -v --install_system_extension)" else install_result="$(/bin/bash "$install_script" -a "$accountKey" -o "$organizationKey" -t "$tags" -v)" fi logger "=============== Begin Installer Logs ===============" if [ $? != "0" ]; then logger "Installer Error: $install_result" exit 1 fi logger "$install_result" logger "=========== INSTALL FINISHED AT $dd ===============" exit 0