Skip to content
176 changes: 86 additions & 90 deletions Powershell/InstallHuntress.powershellv2.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# 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.
#
# Authors: Alan Bishop, Sharon Martin, John Ferrell, Dave Kleinatland, Cameron Granger
# Authors: Alan Bishop, Sharon Martin, John Ferrell, Dave Kleinatland, Evan Shewchuk


# 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
Expand Down Expand Up @@ -72,7 +72,7 @@ $estimatedSpaceNeeded = 200111222
##############################################################################

# These are used by the Huntress support team when troubleshooting.
$ScriptVersion = "Version 2, major revision 8, 2025 Oct 13"
$ScriptVersion = "Version 2, major revision 8, 2025 Dec 22"
$ScriptType = "PowerShell"

# variables used throughout this script
Expand All @@ -82,7 +82,6 @@ $InstallerName = "HuntressInstaller.exe"
$InstallerPath = Join-Path $Env:TMP $InstallerName
$HuntressKeyPath = "HKLM:\SOFTWARE\Huntress Labs\Huntress"
$HuntressRegKey = "HKLM:\SOFTWARE\Huntress Labs"
$ScriptFailed = "Script Failed!"
$SupportMessage = "Please send the error message to [email protected]"
$HuntressAgentServiceName = "HuntressAgent"
$HuntressUpdaterServiceName = "HuntressUpdater"
Expand Down Expand Up @@ -187,33 +186,18 @@ function Test-Parameters {

# Ensure we have an account key (hard coded or passed params) and that it's in the correct form
if ($AccountKey -eq "__ACCOUNT_KEY__") {
$err = "AccountKey not set! Suggest using the -acctkey flag followed by your account key (you can find it in the Downloads section of your Huntress portal)."
LogMessage $err
throw $ScriptFailed + " " + $err
exit 1
copyLogAndExit -throwError "AccountKey not set! Suggest using the -acctkey flag followed by your account key (you can find it in the Downloads section of your Huntress portal)."
} elseif ($AccountKey.length -ne 32) {
$err = "Invalid AccountKey specified (incorrect length)! Suggest double checking the key was copy/pasted in its entirety. Length = $($AccountKey.length) expected value = 32"
LogMessage $err
throw $ScriptFailed + " " + $err
exit 1
copyLogAndExit -throwError "Invalid AccountKey specified (incorrect length)! Suggest double checking the key was copy/pasted in its entirety. Length = $($AccountKey.length) expected value = 32"
} elseif (($AccountKey -match '[^a-zA-Z0-9]')) {
$err = "Invalid AccountKey specified (invalid characters found)! Suggest double checking the key was copy/pasted fully"
LogMessage $err
throw $ScriptFailed + " " + $err
exit 1
copyLogAndExit -throwError "Invalid AccountKey specified (invalid characters found)! Suggest double checking the key was copy/pasted fully"
}

# Ensure we have an organization key (hard coded or passed params).
if ($OrganizationKey -eq "__ORGANIZATION_KEY__") {
$err = "OrganizationKey not specified! This is a user defined identifier set by you (usually your customer's organization name)"
LogMessage $err
throw $ScriptFailed + " " + $err
exit 1
copyLogAndExit -throwError "OrganizationKey not specified! This is a user defined identifier set by you (usually your customer's organization name)"
} elseif ($OrganizationKey.length -lt 1) {
$err = "Invalid OrganizationKey specified (length should be > 0)!"
LogMessage $err
throw $ScriptFailed + " " + $err
exit 1
copyLogAndExit -throwError "Invalid OrganizationKey specified (length should be > 0)!"
}
LogMessage "Parameters verified."
}
Expand Down Expand Up @@ -280,12 +264,20 @@ function Confirm-ServiceRunning ($service) {
function StopHuntressServices {
LogMessage "Stopping Huntress services..."
if (Confirm-ServiceExists($HuntressAgentServiceName)) {
Stop-Service -Name "$HuntressAgentServiceName"
try {
Stop-Service -Name "$HuntressAgentServiceName" -ErrorAction SilentlyContinue
} catch {
LogMessage "Unable to stop HuntressAgent, possible Tamper Protection interference."
}
} else {
LogMessage "$($HuntressAgentServiceName) not found, nothing to stop"
}
if (Confirm-ServiceExists($HuntressUpdaterServiceName)) {
Stop-Service -Name "$HuntressUpdaterServiceName"
try {
Stop-Service -Name "$HuntressUpdaterServiceName" -ErrorAction SilentlyContinue
} catch {
LogMessage "Unable to stop HuntressUpdater, possible Tamper Protection interference."
}
} else {
LogMessage "$($HuntressUpdaterServiceName) not found, nothing to stop"
}
Expand All @@ -297,12 +289,7 @@ function verifyInstaller ($file) {
try {
$varChain.Build((Get-AuthenticodeSignature -FilePath "$file").SignerCertificate) | out-null
} catch [System.Management.Automation.MethodInvocationException] {
$err = ( "ERROR: '$file' did not contain a valid digital certificate. " +
"Something may have corrupted/modified the file during the download process. " +
"Suggest trying again, contact [email protected] if it fails >2 times")
LogMessage $err
LogMessage $SupportMessage
throw $ScriptFailed + " " + $err + " " + $SupportMessage
copyLogAndExit -throwError "ERROR: '$file' did not contain a valid digital certificate, something may have corrupted the file. Try again and contact Support if the 2nd attempt fails"
}
}

Expand Down Expand Up @@ -337,11 +324,7 @@ function Get-Installer {
# Rather than check for 'Tls12', we force-set TLS 1.2 and catch the error if it's truly unsupported.
[Net.ServicePointManager]::SecurityProtocol = [Enum]::ToObject([Net.SecurityProtocolType], 3072)
} catch {
$msg = $_.Exception.Message
$err = "ERROR: Unable to use a secure version of TLS. Please verify Hotfix KB3140245 is installed."
LogMessage $msg
LogMessage $err
throw $ScriptFailed + " " + $msg + " " + $err
copyLogAndExit -throwError "ERROR: Unable to use a secure version of TLS. Please verify Hotfix KB3140245 is installed. Error: ($_.Exception.Message)"
}
}

Expand All @@ -361,19 +344,15 @@ function Get-Installer {
$WebClient.DownloadFile($DownloadURL, $InstallerPath)
break
} catch {
$msg = $_.Exception.Message
$err = "WARNING: Failed to download the Huntress Installer ($attempt/$attempts), retrying in $delay seconds."
LogMessage $msg
$err = "WARNING: Failed to download the Huntress Installer ($attempt/$attempts), retrying in $delay seconds. Error: $_.Exception.Message"
LogMessage $err
Start-Sleep -Seconds $delay
}
}

# Ensure the file downloaded correctly, if not, throw error
if ( ! (Test-Path $InstallerPath) ) {
$err = "ERROR: Failed to download the Huntress Installer. Try accessing $($DownloadURL) from the host where the download failed. Please contact [email protected] if the problem persists."
LogMessage $err
throw $ScriptFailed + " " + $err
copyLogAndExit -throwError "ERROR: Failed to download the Huntress Installer. Try accessing $($DownloadURL) from the host where the download failed. Please contact [email protected] if the problem persists."
}

$msg = "Installer downloaded to '$InstallerPath'..."
Expand All @@ -385,13 +364,12 @@ function Install-Huntress ($OrganizationKey) {
# check that the installer downloaded and wasn't quarantined
LogMessage "Checking for installer '$InstallerPath'..."
if ( ! (Test-Path $InstallerPath) ) {
$err = "ERROR: The installer was unexpectedly removed from $InstallerPath"
$msg = ($err + "`n"+
$err = ("ERROR: The installer was unexpectedly removed from $InstallerPath `n"+
"A security product may have quarantined the installer. Please check " +
"your logs. If the issue continues to occur, please send the log to the Huntress " +
"Team for help at [email protected]")
LogMessage $msg
throw $ScriptFailed + " " + $err + " " + $SupportMessage
LogMessage $err
copyLogAndExit -throwError $err
}

# verify the installer's integrity
Expand All @@ -410,10 +388,7 @@ function Install-Huntress ($OrganizationKey) {
$process | Wait-Process -Timeout $timeout -ErrorAction Stop
} catch {
$process | Stop-Process -Force
$err = "ERROR: Installer failed to complete in $timeout seconds. Possible interference from a security product?"
LogMessage $err
LogMessage $SupportMessage
throw $ScriptFailed + " " + $err + " " + $SupportMessage
copyLogAndExit -throwError "ERROR: Installer failed to complete in $timeout seconds. Possible interference from a security product?"
}
}

Expand Down Expand Up @@ -464,10 +439,7 @@ function Test-Installation {
# Ensure the critical files were created.
foreach ( $file in ($HuntressAgentPath, $HuntressUpdaterPath, $hUpdaterPath) ) {
if ( ! (Test-Path $file) ) {
$err = "ERROR: $file did not exist. Check your AV/security software quarantine"
LogMessage $err
LogMessage $SupportMessage
throw $ScriptFailed + " " + $err + " " + $SupportMessage
copyLogAndExit -throwError "ERROR: $file did not exist. Check your AV/security software quarantine"
}
LogMessage "'$file' is present."
}
Expand All @@ -492,8 +464,7 @@ function Test-Installation {
LogMessage "New install detected. It may take 24 hours for Huntress EDR (Rio) to install!"
}
} else {
LogMessage "$($svc) service is missing! $($SupportMessage)"
throw "$($ScriptFailed) $($svc) service is missing! + $($SupportMessage)"
copyLogAndExit -throwError "$($svc) service is missing! + $($SupportMessage)"
}
}
# check if the service is running, attempt to restart if not (only for base agent).
Expand All @@ -504,7 +475,7 @@ function Test-Installation {
LogMessage "ERROR: The $($svc) service is not running. Attempting to restart"
Start-Service $svc
if (! (Confirm-ServiceRunning($svc))) {
throw "$($ScriptFailed) ERROR: restart of service $($svc) failed. $($SupportMessage)"
copyLogAndExit -throwError "ERROR: restart of service $($svc) failed."
}
} else {
LogMessage "'$svc' is running."
Expand All @@ -519,20 +490,14 @@ function Test-Installation {
} else {
# Ensure the Huntress registry key is present.
if ( ! (Test-Path $HuntressKeyPath) ) {
$err = "ERROR: The registry key '$HuntressKeyPath' did not exist. You may need to reinstall with the -reregister flag"
LogMessage $err
LogMessage $SupportMessage
throw $ScriptFailed + " " + $err + " " + $SupportMessage
copyLogAndExit -throwError "ERROR: The registry key '$HuntressKeyPath' did not exist. You may need to reinstall with the -reregister flag"
}

# Ensure the Huntress registry values are present.
$HuntressKeyObject = Get-ItemProperty $HuntressKeyPath
foreach ( $value in ($AgentIdKeyValueName, $OrganizationKeyValueName, $TagsValueName) ) {
If ( ! (Get-Member -inputobject $HuntressKeyObject -name $value -Membertype Properties) ) {
$err = "ERROR: The registry value $value did not exist within $HuntressKeyPath. You may need to reinstall with the -reregister flag"
LogMessage $err
LogMessage $SupportMessage
throw $ScriptFailed + " " + $err + " " + $SupportMessage
copyLogAndExit -throwError "ERROR: The registry value $value did not exist within $HuntressKeyPath. You may need to reinstall with the -reregister flag"
}
}
}
Expand All @@ -542,10 +507,7 @@ function Test-Installation {
LogMessage "WARNING: Can't verify agent registration due to 32bit PowerShell on 64bit host."
} else {
If ($HuntressKeyObject.$AgentIdKeyValueName -eq 0) {
$err = ("ERROR: The agent did not register. Check the log (%ProgramFiles%\Huntress\HuntressAgent.log) for errors.")
LogMessage $err
LogMessage $SupportMessage
throw $ScriptFailed + " " + $err + " " + $SupportMessage
copyLogAndExit -throwError "ERROR: The agent did not register. Check the log (%ProgramFiles%\Huntress\HuntressAgent.log) for errors. Missing $($HuntressKeyObject.$AgentIdKeyValueName)"
}
LogMessage "Agent registered."
}
Expand Down Expand Up @@ -649,8 +611,7 @@ function runProcess ($process, $flags, $name){
}

$err = "ERROR: $($name) running as '$($process) $($flags)' failed to complete in $timeout seconds, full error message: '$($msg).'"
LogMessage $err
copyLogAndExit
copyLogAndExit -throwError $err
}
}

Expand Down Expand Up @@ -727,7 +688,7 @@ function uninstallHuntress {
# if Huntress services still exist, then delete
$services = @("HuntressRio", "HuntressAgent", "HuntressUpdater", "Huntmon")
foreach ($service in $services) {
if ( $service ) {
if (Get-Service -name $service -ErrorAction SilentlyContinue) {
LogMessage "Service $($service) detected post uninstall, attempting to remove"
c:\Windows\System32\sc.exe STOP $service
c:\Windows\System32\sc.exe DELETE $service
Expand Down Expand Up @@ -811,14 +772,12 @@ function testNetworkConnectivity {
# Convert from bytes, if necessary
if ($Response.Content.GetType() -eq [System.Byte[]]) {
$StrContent = [System.Text.Encoding]::UTF8.GetString($Response.Content)
}else {
} else {
$StrContent = $Response.Content.ToString().Trim()
}

# Remove all newlines from the content
$StrContent = [string]::join("",($StrContent.Split("`n")))


$ContentMatch = $StrContent -eq "96bca0cef10f45a8f7cf68c4485f23a4"
} catch {
LogMessage "Error: $($_.Exception.Message)"
Expand Down Expand Up @@ -937,28 +896,68 @@ function logInfo {
if ( $areURLsAvailable) {
LogMessage "Network Connectivity verified!"
} else {
copyLogAndExit
copyLogAndExit -throwError "ERROR: Network connectivity problem detected!"
}

# Search both 64-bit and 32-bit uninstall registry paths for the required C++ library (version 12+ recommended)
$installedApps = (Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' )
$count = 0
foreach ( $app in $installedApps) {
try {
if ( $app.DisplayName -like "*Visual C++*") {
LogMessage "Name: $($app.DisplayName) Version: $($app.DisplayVersion)"
count++
}
} catch {
# do nothing for blank entries
}
}
if ( $count -eq 0 ) {
LogMessage "Warning: Visual C++ library not found! You may have issues with Huntress running." +
" Highly recommend installing C++ and trying the deploy again: https://www.microsoft.com/en-us/download/details.aspx?id=48145"
}
}

# DebugLog contains useful info not found in surveys, so copy to Huntress folder for higher visibility with future troubleshooting AB
# In the past we copied to the users temp folder, difficult to find on machines with lots of profiles. Solved this by always placing the log in the normal Huntress folder.
# This function copies the Huntress DebugLog to a more permanent location as it's incredibly helpful for troubleshooting.
# Exits with a code 0 if $throwError wasn't passed, otherwise throws the error contained in the $throwError string
function copyLogAndExit {
param (
[string]$throwError
)

if ( $null -eq $throwError ) {
$throwError="0"
}

# log the error message first
if ($throwError -ne "0") {
LogMessage "WARNING: Script errors detected, operation may not have completed! $throwError `n$SupportMessage"
}

# sleep to ensure file operations have completed
Start-Sleep 1
$agentPath = getAgentPath
$logLocation = Join-Path $agentPath "HuntressPoShInstaller.log"

# If this is an unistall, we'll leave the log in the C:\temp dir otherwise,
# we'll copy the log to the huntress directory
# If this is an unistall, we'll leave the log in the C:\temp dir, otherwise copy the log to the huntress directory
if (!$uninstall){
if (!(Test-Path -path $agentPath)) {New-Item $agentPath -Type Directory}
Copy-Item -Path $DebugLog -Destination $logLocation -Force
Write-Output "'$($DebugLog)' copied to '$logLocation'."
try {
Copy-Item -Path $DebugLog -Destination $logLocation -Force -ErrorAction SilentlyContinue
Write-Output "'$($DebugLog)' copied to '$logLocation'."
} catch {
Write-Output "Unable to copy Installer log, possible Tamper Protection interference. Look in \Windows\temp\ for HuntressPoShInstaller.log"
}
}

Write-Output "Script complete"
exit 0
# if no error was passed, exit gracefully, otherwise throw an error and exit
if ($error -eq "0") {
Write-Output "Script complete!"
exit 0
} else {
Write-Output "WARNING: Script errors detected, operation may not have completed! Error: [$throwError]"
throw $throwError
}
}

# Sometimes previous installs can be stuck with services in the Disabled state, this function attempts to set the state to Automatic.
Expand Down Expand Up @@ -1081,7 +1080,7 @@ function main () {
LogMessage "powershell -executionpolicy bypass -f ./InstallHuntress.powershellv2.ps1 [-acctkey <account_key>] [-orgkey <organization_key>] [-tags <tags>] [-reregister] [-reinstall] [-uninstall] `n"
LogMessage "Example:"
LogMessage 'powershell -executionpolicy bypass -f ./InstallHuntress.powershellv2.ps1 -acctkey "0b8a694b2eb7b642069" -orgkey "Buzzword Company Name" -tags "production,US West" '
copyLogAndExit
copyLogAndExit -throwError "No flags or account key found! Exiting."
}


Expand Down Expand Up @@ -1124,8 +1123,7 @@ function main () {
$assetCount = (Get-ChildItem -Path $agentPath -File | Measure-Object).count
# to avoid issues with a single file blocking installs, only exit script if multiple files are found and script not run with -reregister or -reinstall
if ($assetCount -gt 1) {
LogMessage "The Huntress Agent is already installed in $agentPath. Exiting with no changes. Suggest using -reregister or -reinstall flags. Asset count = $assetCount"
copyLogAndExit
copyLogAndExit -throwError "The Huntress Agent is already installed in $agentPath. Exiting with no changes. Suggest using -reregister or -reinstall flags. Asset count = $assetCount"
}
}
}
Expand All @@ -1142,9 +1140,7 @@ try {
main
Write-InstallScriptInfo
} catch {
$ErrorMessage = $_.Exception.Message
LogMessage $ErrorMessage
copyLogAndExit
copyLogAndExit -throwError $_.Exception.Message
}

LogMessage "Script Complete"
Loading