# 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. # # 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 # 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/4404004936339-Deploying-Huntress-with-PowerShell # Usage (remove brackets [] and substitute for your value): # powershell -executionpolicy bypass -f ./InstallHuntress.powershellv2.ps1 [-acctkey ] [-orgkey ] [-tags ] [-reregister] [-reinstall] [-uninstall] # # example: # powershell -executionpolicy bypass -f ./InstallHuntress.powershellv2.ps1 -acctkey "0b8a694b2eb7b642069" -orgkey "Buzzword Company Name" -tags "production,US West" # Optional command line params, this has to be the first line in the script. param ( [string]$acctkey, [string]$orgkey, [string]$tags, [switch]$reregister, [switch]$reinstall, [switch]$uninstall ) ############################################################################################################## ## ---====>> DO NOT REMOVE OR COMMENT OUT ANYTHING IN THIS SCRIPT! <<====--- ## Modifications should only be done to the right side of the assignment statements, and in this section only. ## Do not modify any variable names, change variable types, or introduce commenting characters ## ## Begin user modified variables section ############################################################################################################## # Replace __ACCOUNT_KEY__ with your account secret key (from your Huntress portal's "download agent" section) $AccountKey = "__ACCOUNT_KEY__" # Replace __ORGANIZATION_KEY__ with a unique identifier for the organization/client (your choice of naming scheme) $OrganizationKey = "__ORGANIZATION_KEY__" # Replace __TAGS__ with one or more tags, separated by commas (leave the next line unmodified if you don't want to use Tags) $TagsKey = "__TAGS__" # Set to "Continue" to enable verbose logging. $DebugPreference = "SilentlyContinue" # Legacy, spinning HDD, or overloaded machines may require tuning this value. Most modern end points install in 10 seconds # 3rd party security software (AV/EDR/etc) may significantly slow down the install if Huntress exclusions aren't properly put in! # Read more about exclusions here https://support.huntress.io/hc/en-us/articles/4404005178771 $timeout = 120 # number of seconds to wait before continuing the install # Currently a fresh install of Huntress + EDR is approximately 100mb, double this for safety as fresh installs can bloat up in size slightly at first # This can vary based on several factors including process creation rate, if EDR is installed or not, as well as number of users in the c:\users folder $estimatedSpaceNeeded = 200111222 ############################################################################## ## Do not modify anything below this line ############################################################################## # These are used by the Huntress support team when troubleshooting. $ScriptVersion = "Version 2, major revision 8, 2025 Dec 24" $ScriptType = "PowerShell" # variables used throughout this script $X64 = 64 $X86 = 32 $InstallerName = "HuntressInstaller.exe" $InstallerPath = Join-Path $Env:TMP $InstallerName $HuntressKeyPath = "HKLM:\SOFTWARE\Huntress Labs\Huntress" $HuntressRegKey = "HKLM:\SOFTWARE\Huntress Labs" $SupportMessage = "Please send the error message to support@huntress.com" $HuntressAgentServiceName = "HuntressAgent" $HuntressUpdaterServiceName = "HuntressUpdater" $HuntressEDRServiceName = "HuntressRio" $Vendor = "Huntress" $ScriptInfoName = "HuntressPoShInstaller.json" # attempt to use a more central temporary location for the log file rather than the installing users folder if (Test-Path (Join-Path $env:SystemRoot "\temp")) { $DebugLog = Join-Path $env:SystemRoot "\temp\HuntressPoShInstaller.log" } else { $DebugLog = Join-Path $Env:TMP HuntressPoShInstaller.log } # Find poorly written code faster with the most stringent setting. Set-StrictMode -Version Latest # Pull various software versions for logging purposes $PoShVersion = $PsVersionTable.PsVersion.Major $KernelVersion = [System.Environment]::OSVersion.Version # Check kernel version to download the appropriate installer for the OS version # kernel 6.1+ can use the regular Huntress agent, kernel versions 6.0 and lower require the legacy installer $LegacyCommandsRequired = $false if ($KernelVersion.Major -eq 6) { if ($KernelVersion.Minor -lt 1) { $LegacyCommandsRequired = $true } } elseif ($KernelVersion.Major -lt 6) { $LegacyCommandsRequired = $true } # Check for an account key specified on the command line. if ( ! [string]::IsNullOrEmpty($acctkey) ) { $AccountKey = $acctkey } # Check for an organization key specified on the command line. if ( ! [string]::IsNullOrEmpty($orgkey) ) { $OrganizationKey = $orgkey } # Check for tags specified on the command line. if ( ! [string]::IsNullOrEmpty($tags) ) { $TagsKey = $tags } # pick the appropriate file to download based on the OS version if ($LegacyCommandsRequired -eq $true) { # For Windows Vista, Server 2008 (PoSh 2, kernel <= 6.0) $DownloadURL = "https://update.huntress.io/legacy_download/" + $AccountKey + "/" + $InstallerName } else { # For Windows 7+, Server 2008 R2+ (PoSh 3+) $DownloadURL = "https://update.huntress.io/download/" + $AccountKey + "/" + $InstallerName } # 32bit PoSh on 64bit Windows is unable to interact with certain assets, so we check for this condition first with PoSh $PowerShellArch = $X86 # 8 byte pointer is 64bit if ([IntPtr]::size -eq 8) { $PowerShellArch = $X64 } # Now we grab the Windows architecture $WindowsArchitecture = $X86 if ($env:ProgramW6432) { $WindowsArchitecture = $X64 } # Check for Legacy OS, any kernel below 6.2 cannot run Huntress EDR (so we skip that check) $services = @($HuntressAgentServiceName, $HuntressUpdaterServiceName, $HuntressEDRServiceName) if ( ($KernelVersion.major -eq 6 -and $KernelVersion.minor -lt 2) -or ($KernelVersion.major -lt 6) ) { $services = @($HuntressAgentServiceName, $HuntressUpdaterServiceName) } # Checking to see if Huntress was installed before this script was run $isHuntressInstalled = $false if ((test-path "c:\program files\Huntress\HuntressAgent.exe") -OR (test-path "c:\program files (x86)\Huntress\HuntressAgent.exe")){ $isHuntressInstalled = $true } # time stamps for logging purposes function Get-TimeStamp { return "[{0:yyyy/MM/dd} {0:HH:mm:ss}]" -f (Get-Date) } # adds time stamp to a message and then writes that to the log file function LogMessage ($msg) { Add-Content $DebugLog "$(Get-TimeStamp) $msg" Write-Output "$(Get-TimeStamp) $msg" } # test that all required parameters were passed, and that they are in the correct format function Test-Parameters { LogMessage "Verifying received parameters..." # If reregister and reinstall were both flagged, just reregister as it is the more robust option if ($reregister -and $reinstall) { LogMessage "Specified -reregister and -reinstall, defaulting to reregister." $reinstall = $false } # Ensure we have an account key (hard coded or passed params) and that it's in the correct form if ($AccountKey -eq "__ACCOUNT_KEY__") { 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) { 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]')) { 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__") { 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) { copyLogAndExit -throwError "Invalid OrganizationKey specified (length should be > 0)!" } LogMessage "Parameters verified." } # Force kill a process by process name function KillProcessByName { param( [Parameter(Mandatory = $true)] [string]$ProcessName ) $processes = Get-Process | Where-Object { $_.ProcessName -eq $ProcessName } $processCount = $processes | Measure-Object | Select-Object -ExpandProperty Count if ($processCount -eq 0) { LogMessage "No processes with the name '$ProcessName' are currently running." } else { foreach ($process in $processes) { try { $processID = $process.Id Stop-Process -Id $processID -Force LogMessage "Killed process '$ProcessName' (ID $processID) successfully." } catch { LogMessage "Failed to kill process '$ProcessName' (ID $processID): $($_.Exception.Message)" } } } } # check to see if the Huntress service exists (agent or updater) function Confirm-ServiceExists ($service) { if ([string]::IsNullOrEmpty($service)) { return $false } if (Get-Service $service -ErrorAction SilentlyContinue) { return $true } return $false } # check to see if the Huntress service is running (agent or updater) function Confirm-ServiceRunning ($service) { if ([string]::IsNullOrEmpty($service)) { return $false } $arrService = Get-Service $service -ErrorAction SilentlyContinue if ($null -eq $arrService) { return $false } $status = $arrService.Status.ToString() if ($status.ToLower() -eq 'running') { return $true } return $false } # Stop the Agent and Updater services function StopHuntressServices { LogMessage "Stopping Huntress services..." if (Confirm-ServiceExists($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)) { 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" } } # Ensure the installer was not modified during download by validating the file signature. function verifyInstaller ($file) { $varChain = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Chain try { $varChain.Build((Get-AuthenticodeSignature -FilePath "$file").SignerCertificate) | out-null } catch [System.Management.Automation.MethodInvocationException] { 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" } } # Prevent conflicting file from preventing creation of installation directory. function prepareAgentPath { $path = getAgentPath if (Test-Path $path -PathType Leaf) { $backup = "$path.bak" $err = "WARNING: '$path' already exists and is not a directory, renaming to '$backup'." LogMessage $err Rename-Item -Path $path -NewName $backup -Force } } # download the Huntress installer function Get-Installer { $msg = "Downloading installer to '$InstallerPath'..." LogMessage $msg # Ensure a secure TLS version is used. $ProtocolsSupported = [enum]::GetValues('Net.SecurityProtocolType') if ( ($ProtocolsSupported -contains 'Tls13') -and ($ProtocolsSupported -contains 'Tls12') ) { # Use only TLS 1.3 or 1.2 LogMessage "Using TLS 1.3 or 1.2..." [Net.ServicePointManager]::SecurityProtocol = ( [Enum]::ToObject([Net.SecurityProtocolType], 12288) -bOR [Enum]::ToObject([Net.SecurityProtocolType], 3072) ) } else { LogMessage "Using TLS 1.2..." try { # In certain .NET 4.0 patch levels, SecurityProtocolType does not have a TLS 1.2 entry. # 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 { copyLogAndExit -throwError "ERROR: Unable to use a secure version of TLS. Please verify Hotfix KB3140245 is installed. Error: ($_.Exception.Message)" } } # Delete stale installer before downloading the most recent installer if (Test-Path $InstallerPath -PathType Leaf) { $err = "WARNING: '$InstallerPath' already exists, deleting stale Huntress Installer." LogMessage $err Remove-Item -Path $InstallerPath -Force -ErrorAction SilentlyContinue } # Attempt to download the correct installer for the given OS, retry if it fails $attempts = 6 $delay = 60 for ($attempt = 1; $attempt -le $attempts; $attempt++) { $WebClient = New-Object System.Net.WebClient try { $WebClient.DownloadFile($DownloadURL, $InstallerPath) break } catch { $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) ) { copyLogAndExit -throwError "ERROR: Failed to download the Huntress Installer. Try accessing $($DownloadURL) from the host where the download failed. Please contact support@huntress.io if the problem persists." } $msg = "Installer downloaded to '$InstallerPath'..." LogMessage $msg } # check if the agent downloaded, is a valid install file, if those match up then run the installer 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 `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 support@huntresslabs.com") LogMessage $err copyLogAndExit -throwError $err } # verify the installer's integrity verifyInstaller($InstallerPath) LogMessage "Executing installer..." prepareAgentPath # if $Tags value exists install using the provided tags, otherwise no tags if (($Tags) -or ($TagsKey -ne "__TAGS__")) { $process = Start-Process $InstallerPath "/ACCT_KEY=`"$AccountKey`" /ORG_KEY=`"$OrganizationKey`" /TAGS=`"$TagsKey`" /S" -PassThru } else { $process = Start-Process $InstallerPath "/ACCT_KEY=`"$AccountKey`" /ORG_KEY=`"$OrganizationKey`" /S" -PassThru } try { $process | Wait-Process -Timeout $timeout -ErrorAction Stop } catch { $process | Stop-Process -Force copyLogAndExit -throwError "ERROR: Installer failed to complete in $timeout seconds. Possible interference from a security product?" } } # Test that the Huntress agent was able to install, register, and start service correctly function Test-Installation { # Get the file locations of some of the Huntress executables and setting up some registry related variables $HuntressDirectory = getAgentPath $hUpdaterPath = Join-Path $HuntressDirectory "hUpdate.exe" $HuntressAgentPath = Join-Path $HuntressDirectory "HuntressAgent.exe" $HuntressUpdaterPath = Join-Path $HuntressDirectory "HuntressUpdater.exe" $AgentIdKeyValueName = "AgentId" $OrganizationKeyValueName = "OrganizationKey" $TagsValueName = "Tags" LogMessage "Verifying installation..." # Watch the agent logs for registration event, log if succeeded, waiting no longer than 10 seconds before outputting failure to log $didAgentRegister = $false for ($i = 0; $i -le 40; $i++) { if (Test-Path "$($HuntressDirectory)\HuntressAgent.log") { $linesFromLog = Get-Content "$($HuntressDirectory)\HuntressAgent.log" | Select-Object -last 6 ForEach ($line in $linesFromLog) { if ($line -like "*registered agent*") { LogMessage "Agent successfully registered in $($i/4) seconds" $didAgentRegister = $true Start-Sleep -Milliseconds 250 $i=100 } } } Start-Sleep -Milliseconds 250 } # If the agent didn't register, log the tail of HuntressAgent.log so Support can see the reason registration failed if ( ! $didAgentRegister) { $err = "WARNING: It does not appear the agent has successfully registered. Check 3rd party AV exclusion lists to ensure Huntress is excluded." LogMessage ($err + $SupportMessage) if (Test-Path "$($HuntressDirectory)\HuntressAgent.log") { $linesFromLog = Get-Content "$($HuntressDirectory)\HuntressAgent.log" | Select-Object -last 8 LogMessage "Newest 8 lines of HuntressAgent.log:" ForEach ($line in $linesFromLog) { LogMessage $line } } else { LogMessage "HuntressAgent.log not found after post-registration failure! Likely 3rd party interference (AV/ThreatLocker)." } } # Ensure the critical files were created. foreach ( $file in ($HuntressAgentPath, $HuntressUpdaterPath, $hUpdaterPath) ) { if ( ! (Test-Path $file) ) { copyLogAndExit -throwError "ERROR: $file did not exist. Check your AV/security software quarantine" } LogMessage "'$file' is present." } # Check for Legacy OS, any kernel below 6.2 cannot run Huntress EDR (so we skip that check) if ( ($KernelVersion.major -eq 6 -and $KernelVersion.minor -lt 2) -or ($KernelVersion.major -lt 6) ) { LogMessage "WARNING: Legacy OS detected, Huntress EDR will not be installed" } else { LogMessage "Huntress EDR will be installed automatically in < 24 hours." } # Ensure the services are installed and running. foreach ($svc in $services) { # check if the service is installed if ( ! (Confirm-ServiceExists($svc))) { # if Huntress was installed before this script started and Rio is missing then we log that, but continue with this script if ($svc -eq $HuntressEDRServiceName) { if ($isHuntressInstalled) { LogMessage "Information: Huntress Process Insights (aka Rio) is installed automatically by the Huntress portal. It can take up to 24 hours to show up" LogMessage "See more about compatibility here: https://support.huntress.io/hc/en-us/articles/4410699983891-Supported-Operating-Systems-System-Requirements-Compatibility" } else { LogMessage "New install detected. It may take 24 hours for Huntress EDR (Rio) to install!" } } else { copyLogAndExit -throwError "$($svc) service is missing! + $($SupportMessage)" } } # check if the service is running, attempt to restart if not (only for base agent). elseif ( (! (Confirm-ServiceRunning($svc))) -AND ($svc -eq $HuntressAgentServiceName)) { Start-Service $svc # if still not running, log and give up, else inform of success if (! (Confirm-ServiceRunning($svc))) { LogMessage "ERROR: The $($svc) service is not running. Attempting to restart" Start-Service $svc if (! (Confirm-ServiceRunning($svc))) { copyLogAndExit -throwError "ERROR: restart of service $($svc) failed." } } else { LogMessage "'$svc' is running." } } } # look for a condition that prevents checking registry keys, if not then check for registry keys if ( ($PowerShellArch -eq $X86) -and ($WindowsArchitecture -eq $X64) ) { LogMessage "WARNING: Can't verify registry settings due to 32bit PowerShell on 64bit host. Please run PowerShell in 64 bit mode" } else { # Ensure the Huntress registry key is present. if ( ! (Test-Path $HuntressKeyPath) ) { 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) ) { copyLogAndExit -throwError "ERROR: The registry value $value did not exist within $HuntressKeyPath. You may need to reinstall with the -reregister flag" } } } # Verify the agent registered (if not blocked by 32/64 bit incompatibilities). if ( ($PowerShellArch -eq $X86) -and ($WindowsArchitecture -eq $X64) ) { LogMessage "WARNING: Can't verify agent registration due to 32bit PowerShell on 64bit host." } else { If ($HuntressKeyObject.$AgentIdKeyValueName -eq 0) { copyLogAndExit -throwError "ERROR: The agent did not register. Check the log (%ProgramFiles%\Huntress\HuntressAgent.log) for errors. Missing $($HuntressKeyObject.$AgentIdKeyValueName)" } LogMessage "Agent registered." } LogMessage "Installation verified!" } # prepare to reregister by stopping the Huntress service and deleting all the registry keys function PrepReregister { LogMessage "Preparing to re-register agent..." StopHuntressServices $HuntressKeyPath = "HKLM:\SOFTWARE\Huntress Labs\Huntress" Remove-Item -Path "$HuntressKeyPath" -Recurse -ErrorAction SilentlyContinue } # looks at the Huntress log to return true if the agent is orphaned, false if the agent is active AB function isOrphan { # find the Huntress log file or state that it can't be found if (Test-Path 'C:\Program Files\Huntress\HuntressAgent.log') { $Path = 'C:\Program Files\Huntress\HuntressAgent.log' } elseif (Test-Path 'C:\Program Files (x86)\Huntress\HuntressAgent.log') { $Path = 'C:\Program Files (x86)\Huntress\HuntressAgent.log' } elseif ($isHuntressInstalled) { LogMessage "Unable to locate log file, thus unable to check if orphaned" return $false } else { LogMessage "New machine, no need to run through orphan checker" return $false } # if the log was found, look through the last 10 lines for the orphaned agent error code if ($Path -match 'HuntressAgent.log') { $linesFromLog = Get-Content $Path | Select-Object -last 10 ForEach ($line in $linesFromLog) { if ($line -like "*bad status code: 401*") { LogMessage "Agent appears to be orphaned: $($line)" return $true } } } return $false } # Check if the script is being run with admin access AB function testAdministrator { $user = [Security.Principal.WindowsIdentity]::GetCurrent(); (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } # Ensure the disk has enough space for the install files + agent, then write results to the log AB function checkFreeDiskSpace { # Using an older disk query to be backwards compatible with PoSh 2, catch WMI errors and check repository try { $freeSpace = (Get-WmiObject -query "Select * from Win32_LogicalDisk where DeviceID='c:'" | Select-Object FreeSpace).FreeSpace } catch { LogMessage "WMI issues discovered (free space query), attempting to fix the repository" winmgmt -verifyrepository $drives = get-psdrive foreach ($drive in $drives) { if ($drive.Name -eq "C") { $freeSpace = $drive.Free } } } $freeSpaceNice = $freeSpace.ToString('N0') if ($freeSpace -lt $estimatedSpaceNeeded) { $err = "Low disk space detected, you may have troubles completing this install. Only $($freeSpaceNice) bytes remaining (need about $($estimatedSpaceNeeded.ToString('N0'))." LogMessage $err } else { LogMessage "Free disk space: $($freeSpaceNice)" } } # determine the path in which Huntress is installed AB function getAgentPath { # Ensure we resolve the correct Huntress directory regardless of operating system or process architecture. if ($WindowsArchitecture -eq $X64) { return (Join-Path $Env:ProgramW6432 "Huntress") } else { return (Join-Path $Env:ProgramFiles "Huntress") } } # attempt to run a process and log the results AB function runProcess ($process, $flags, $name){ try { $proc = Start-Process $process $flags -PassThru Wait-Process -Timeout $timeout -ErrorAction Stop -InputObject $proc LogMessage "$($name) finished" } catch { $e = $_.Exception $msg = $e.Message # Gather all the exceptions and their children while ($e.InnerException) { $e = $e.InnerException $msg += "`n" + $e.Message } # Try to kill hung processs if ($proc) { Stop-Process $proc.id -Force -ErrorAction SilentlyContinue } $err = "ERROR: $($name) running as '$($process) $($flags)' failed to complete in $timeout seconds, full error message: '$($msg).'" copyLogAndExit -throwError $err } } # Fully uninstall the agent AB function uninstallHuntress { $agentPath = getAgentPath $updaterPath = Join-Path $agentPath "HuntressUpdater.exe" $exeAgentPath = Join-Path $agentPath "HuntressAgent.exe" $uninstallerPath = Join-Path $agentPath "Uninstall.exe" $wasUninstallerRun = $false # speed this up by stopping services first Stop-Service "huntressrio" -ErrorAction SilentlyContinue Stop-Service "huntressupdater" -ErrorAction SilentlyContinue Stop-Service "huntressagent" -ErrorAction SilentlyContinue # Force kill the executables so they're not hangin around KillProcessByName "HuntressAgent.exe" KillProcessByName "HuntressUpdater.exe" KillProcessByName "HuntressRio.exe" # attempt to use the built in uninstaller, if not found use the uninstallers built into the Agent and Updater if (Test-Path $agentPath) { # run uninstaller.exe, if not found run the Agent's built in uninstaller and the Updater's built in uninstaller if (Test-Path $uninstallerPath) { runProcess "$($uninstallerPath)" "/S" "Uninstall.exe" $wasUninstallerRun = $true } elseif (Test-Path $exeAgentPath) { runProcess "$($exeAgentPath)" "/S" "Huntress Agent uninstaller" $wasUninstallerRun = $true } elseif (Test-Path $updaterPath) { runProcess "$($updaterPath)" "/S" "Updater uninstaller" $wasUninstallerRun = $true } else { LogMessage "Agent path found but no uninstallers found. Attempting to manually uninstall" } } else { $err = "Note: unable to find Huntress install folder. Attempting to manually uninstall." LogMessage $err } # if uninstaller was run, loop until Huntress assets are all successfully removed, or exit & report if timer exceeds 15 seconds if ($wasUninstallerRun) { for ($i = 0; $i -le 15; $i++) { if ((Test-Path $exeAgentPath) -OR (Test-Path $HuntressRegKey)){ Start-Sleep 1 } else { LogMessage "Agent successfully uninstall in $($i) seconds" $i = 100 } if ($i -eq 15) { $err = "Uninstall not complete after $($i) seconds" LogMessage $err } } } # look for the Huntress directory, if found then delete if (Test-Path $agentPath) { Remove-Item -LiteralPath $agentPath -Force -Recurse -ErrorAction SilentlyContinue LogMessage "Manual cleanup of Huntress folder: success" } else { LogMessage "Manual cleanup of Huntress folder: folder not found" } # look for the registry keys, if exist then delete if (Test-Path $HuntressRegKey) { Get-Item -path $HuntressRegKey | Remove-Item -recurse LogMessage "Manually deleted Huntress registry keys" } else { LogMessage "No registry keys found, uninstallation complete" } # if Huntress services still exist, then delete $services = @("HuntressRio", "HuntressAgent", "HuntressUpdater", "Huntmon") foreach ($service in $services) { 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 } } } # grab the currently installed agent version AB function getAgentVersion { $exeAgentPath = Join-Path (getAgentPath) "HuntressAgent.exe" $agentVersion = (Get-Item $exeAgentPath).VersionInfo.FileVersion return $agentVersion } # ensure all the Huntress services are running AB function repairAgent { # check that service exists before we attempt to start it $HuntressService = Get-Service -name "HuntressAgent" -ErrorAction SilentlyContinue $UpdaterService = Get-Service -name "HuntressUpdater" -ErrorAction SilentlyContinue $RioService = Get-Service -name "HuntressRio" -ErrorAction SilentlyContinue $DidRepairFinish = $true # if each service doesn't exist we'll be returning false, else start the service if ($null -eq $HuntressService){ LogMessage "Repair was unable to find the HuntressService, this machine will need Huntress uninstalled and reinstalled in order to maintain security" $DidRepairFinish = $false } else { Start-Service HuntressAgent LogMessage "Repair started HuntressAgent service" } if ($null -eq $UpdaterService){ LogMessage "Repair was unable to find the UpdaterService, this machine will need Huntress uninstalled and reinstalled in order to continue receiving updates." $DidRepairFinish = $false } else { Start-Service HuntressUpdater LogMessage "Repair started HuntressUpdater service" } # For Rio/EDR we don't return false as we don't know if it's a fresh install that hasn't received Rio yet, but still attempt to restart service if (($null -eq $RioService) -AND $isHuntressInstalled){ LogMessage "Repair was unable to find the RioService. If this is a fresh install it may take up to 24 hours for Rio to install. Otherwise please contact support to ensure EDR coverage." } elseif ($null -eq $RioService) { LogMessage "Fresh install detected, it can take up to 24 hours for Rio to install." } else { Start-Service HuntressRio LogMessage "Repair started HuntressRio service" } return $DidRepairFinish } # Agent will not function when communication is blocked so exit the script if too much communication is blocked AB # return true if connectivity is acceptable, false if too many connections fail function testNetworkConnectivity { # number of URL's that can fail the connectivity before the agent refuses to install (the test fails incorrectly sometimes, so 1 failure is acceptable) $connectivityTolerance = 1 # Avoid "IE first start" errors by disabling the first run customize option Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main" -Name "DisableFirstRunCustomize" -Value 2 $file_name = "96bca0cef10f45a8f7cf68c4485f23a4.txt" $URLs = @(("https://update.huntress.io/agent/connectivity/{0}" -f $file_name), ("https://huntress.io/agent/connectivity/{0}" -f $file_name), ("https://eetee.huntress.io/{0}"-f $file_name), ("https://huntresscdn.com/agent/connectivity/{0}" -f $file_name), ("https://huntress-installers.s3.amazonaws.com/agent/connectivity/{0}" -f $file_name), ("https://huntress-updates.s3.amazonaws.com/agent/connectivity/{0}" -f $file_name), ("https://huntress-uploads.s3.us-west-2.amazonaws.com/agent/connectivity/{0}" -f $file_name), ("https://huntress-user-uploads.s3.amazonaws.com/agent/connectivity/{0}" -f $file_name), ("https://huntress-rio.s3.amazonaws.com/agent/connectivity/{0}" -f $file_name), ("https://huntress-survey-results.s3.amazonaws.com/agent/connectivity/{0}" -f $file_name), ("https://huntress-log-uploads.s3.amazonaws.com/agent/connectivity/{0}" -f $file_name)) foreach ($URL in $URLs) { $StatusCode = 0 try { $Response = Invoke-WebRequest -Uri $URL -TimeoutSec 5 -ErrorAction Stop -ContentType "text/plain" -UseBasicParsing # This will only execute if the Invoke-WebRequest is successful. $StatusCode = $Response.StatusCode # Convert from bytes, if necessary if ($Response.Content.GetType() -eq [System.Byte[]]) { $StrContent = [System.Text.Encoding]::UTF8.GetString($Response.Content) } 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)" } if ($StatusCode -ne 200) { $err = "WARNING, connectivity to Huntress URL's is being interrupted. You MUST open port 443 for $($URL) in order for the Huntress agent to function." LogMessage $err $connectivityTolerance -- } elseif (!$ContentMatch) { $err = "WARNING, successful connection to Huntress URL, however, content did not match expected. Ensure no proxy or content filtering is preventing access!" LogMessage $err $connectivityTolerance -- LogMessage "Content: $($StrContent)" } else { LogMessage "Connection succeeded to $($URL)" } } if ($connectivityTolerance -lt 0) { LogMessage "Please fix the closed port 443 for the above domains before attempting to install" $err = "Too many connections failed $($connectivityTolerance), exiting" LogMessage "$($err), $($SupportMessage)" return $false } return $true } # Log useful data about the machine for troubleshooting AB function logInfo { # if Huntress was already installed, pull version info and TP status LogMessage "Script cursory check, is Huntress installed already: $($isHuntressInstalled)" if ($isHuntressInstalled){ LogMessage "Agent version $(getAgentVersion) found" } if (Confirm-ServiceRunning $HuntressEDRServiceName){ $checkTP = (Confirm-ServiceRunning $HuntressAgentServiceName) if ( $null -eq $checkTP ) { LogMessage "Warning: Tamper Protection may be enabled; you may need to disable TP or run this as SYSTEM to repair, upgrade, or reinstall this agent. `n" } else { LogMessage "Pass: Tamper Protection not detected, or this script is running as SYSTEM `n" } } # Log OS details LogMessage $(systeminfo) LogMessage "Host Kernel Version: $($KernelVersion)" LogMessage "Detected Architecture (Windows 32/64 bit): '$($WindowsArchitecture)'" # Log PowerShell details LogMessage "PowerShell Architecture (PoSh 32/64 bit): '$PowerShellArch'" LogMessage "PowerShell version: $($PoShVersion).$($PSversionTable.PsVersion.Minor)" LogMessage "Powershell legacy detected: $($LegacyCommandsRequired)" if ($LegacyCommandsRequired) { LogMessage "Warning! Older version of PowerShell detected" } # Logging other details about the machine checkFreeDiskSpace LogMessage "Installer location: '$InstallerPath'" LogMessage "Installer log: '$DebugLog'" LogMessage "Administrator access: $(testAdministrator)" $userContext = whoami if ($userContext -eq "nt authority\system") { LogMessage "Pass: Run under the SYSTEM user." } else { LogMessage "Warning: Not run under the SYSTEM user, you may have issues with Huntress Tamper Protection" } # Log machine uptime, use -1 to call attention to machines that have issues running the GCIM command try { $uptime = ((Get-Date) - (GCIM Win32_OperatingSystem).LastBootUpTime).days } catch { LogMessage "Unable to determine system uptime" $uptime = -1 } if ($uptime -gt 9) { LogMessage "Warning, high uptime detected This machine may need a reboot in order to resolve Windows update-based file locks." } else { LogMessage "Days of uptime: $($uptime)" } # Logging TCP/IP configuration to ensure connectivity LogMessage "$(ipconfig)" # Log status of AD joined and the (in)ability to contact a DC try { $domainJoined = (gwmi win32_computersystem).PartOfDomain } catch { LogMessage "Warning, unable to determine if domain joined" $domainJoined = $false } if ( $domainJoined ) { try { $secureChannelStatus = Test-ComputerSecureChannel } catch { LogMessage "Warning, unable to Test-ComputerSecureChannel. If this isn't a DC, then the trust relationship with the DC may be broken" $secureChannelStatus = $false } if ( ! $secureChannelStatus) { LogMessage "Warning, AD joined machine without DC connectivity. Some services may be impacted such as Managed AV and in some rare cases Host Isolation." } else { LogMessage "AD joined and DC connectivity verified!" } } $areURLsAvailable = testNetworkConnectivity if ( $areURLsAvailable ) { LogMessage "Network Connectivity verified!" } else { copyLogAndExit -throwError "ERROR: Network connectivity problem detected!" } } # This function copies the Huntress DebugLog to a more permanent location as it's incredibly helpful for troubleshooting. AB # 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 ( [string]::IsNullOrEmpty($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 copy the log to the huntress directory if (!$uninstall){ if (!(Test-Path -path $agentPath)) {New-Item $agentPath -Type Directory} 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" } } # if no error was passed, exit gracefully, otherwise throw an error and exit if ($throwError -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. # Services in the Disabled state cannot be manually started, and TP will stop partners from fixing this themselves. AB function fixServices { $servicesOnInstall = @($HuntressAgentServiceName, $HuntressUpdaterServiceName) # Ensure the services are installed before repairing the state foreach ($svc in $servicesOnInstall) { if ( (Confirm-ServiceExists($svc))) { # repairing service state if ( $(Get-Service $svc).StartType -ne "automatic") { LogMessage "Disabled service $svc detected, attempting to set startup type to automatic." c:\Windows\System32\sc.exe config $svc start=auto } } } } function Get-ScriptInfoPath { $results = getAgentPath return Join-Path -Path $results -ChildPath $ScriptInfoName } # Get the hash of this currently running PowerShell script file function Get-Sha256Hash { try { # Get the hash of this file return (Get-FileHash -Path $PSCommandPath -Algorithm SHA256).Hash } catch { # catch failures in this function and return an empty hash $ErrorMessage = $_.Exception.Message return "", "Unable to retrieve script hash: $ErrorMessage" } } # Get the operation we running for this script function Get-ScriptOperation { $operation = "Install" if ($reregister -eq $true) { $operation = "Reregister" } elseif ($reinstall -eq $true) { $operation = "Reinstall" } return $operation } function Write-InstallScriptInfo { $hold = $ErrorActionPreference $ErrorActionPreference = "Stop" try { if ($uninstall) { # No need to track installation on an uninstall LogMessage "No script information will be saved for uninstall" return } [array]$hashResult = Get-Sha256Hash if ($hashResult.Count -eq 2) { LogMessage $hashResult[1] } $info = [PSCustomObject]@{ vendor = $Vendor sha256 = $hashResult[0] operation = Get-ScriptOperation } $path = Get-ScriptInfoPath $json = $info | ConvertTo-Json -Compress # We make a "best-effort" attempt to write to the file Set-Content -Path $path -Value $json } catch { $ErrorMessage = $_.Exception.Message LogMessage "Unable to save installation script information: $ErrorMessage" } $ErrorActionPreference = $hold } # Logging Visual C++ info for a Windows 8.1 specific issue function libraryCheck { # Since this issue only affects Win 8.1, check the OS version before logging. try { if ( (Get-CimInstance -classname Win32_OperatingSystem | Select-Object caption) -notlike "*8.1*") { LogMessage "Windows 8.1 not detected, not checking for missing dependencies" return } } catch { LogMessage "Unable to determine system OS. Checking for dependencies just in case, but this Visual C++ check is only relevant for Windows 8.1 machines!" } # Fleet Health Check: UCRT + VC Redistributables $Results = [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME KB2919355 = "Missing, install KB2919355 https://www.microsoft.com/en-us/download/details.aspx?id=42327" KB2999226 = "Missing, install KB2999226 https://www.microsoft.com/en-ie/download/details.aspx?id=51109" UCRT_Version = "None, install Universal CRT https://support.microsoft.com/en-us/topic/update-for-universal-c-runtime-in-windows-c0514201-7fe6-95a3-b0a5-287930f3560c" VCRedist_x64 = "Not Found, install x64 Visual C++ Redistributable v14 https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-c-redistributable-v14" VCRedist_x86 = "Not Found, install x86 Visual C++ Redistributable v14 https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-c-redistributable-v14" } # 1. Check for KBs $Hotfixes = Get-HotFix | Select-Object -ExpandProperty HotFixID if ($Hotfixes -contains "KB2919355") { $Results.KB2919355 = "Installed`n" } if ($Hotfixes -contains "KB2999226") { $Results.KB2999226 = "Installed`n" } # 2. Check for UCRT DLL Version if (Test-Path "$env:windir\System32\ucrtbase.dll") { $Results.UCRT_Version = "$((Get-Item "$env:windir\System32\ucrtbase.dll").VersionInfo.ProductVersion) (version 10.0.14393+ is recommended)" } # 3. Check for VC Redist 2015-2022 via Registry (Fastest for Fleet) $UninstallKeys = @( "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" ) Get-ItemProperty $UninstallKeys | Where-Object { $_.psobject.Properties["DisplayName"] } | Where-Object { $_.DisplayName -like "*Visual C++*" } | ForEach-Object { if ($_.DisplayName -like "*x64*") { $Results.VCRedist_x64 = "$($_.DisplayVersion) (version 14+ is recommended)" } if ($_.DisplayName -like "*x86*") { $Results.VCRedist_x86 = "$($_.DisplayVersion) (version 14+ is recommended)" } } # Save each of the name and property values on a new line in the installer log foreach ($property in $Results.PSObject.Properties) { if ($null -ne $property) { LogMessage "$($property.Name) - $($property.Value)" } } } ######################################################################################### # begin main function # ######################################################################################### function main () { # Start the script with logging to capture useful data for troubleshooting. All your logging are belong to us, Zero Wang. LogMessage "Script type: '$ScriptType'" LogMessage "Script version: '$ScriptVersion'" LogMessage "Script flags: Reregister=$reregister Reinstall=$reinstall Uninstall=$uninstall " if ($AccountKey.length -lt 8) { LogMessage "Invalid key length, found $($AccountKey.length) (should be 32). Account key value: $AccountKey" } else { $masked = $AccountKey.Substring(0,4) + "************************" + $AccountKey.SubString($AccountKey.length-4,4) LogMessage "Pre-trim variables: account key=[$masked] org key=[$OrganizationKey] (brackets are in place to show trailing/leading spaces)" } logInfo # if run with the uninstall flag, exit afterward so we don't reinstall the agent after if ($uninstall) { LogMessage "Uninstalling Huntress agent" uninstallHuntress copyLogAndExit } # Logging some additional info for a temporary issue with Windows 8.1 and missing Visual C++ dependencies libraryCheck # if the agent is orphaned, switch to the full uninstall/reinstall (reregister flag) if ( !($reregister)) { $orphanStatus = isOrphan if ( $orphanStatus -eq $true ) { $err = 'Huntress Agent is orphaned, unable to use the provided flag. Switching to uninstall/reinstall (reregister flag)' LogMessage "$err" $reregister = $true } } # if run with no flags and no account key print usage and exit if (!$reregister -and !$uninstall -and !$reinstall -and ($AccountKey -eq "__ACCOUNT_KEY__")) { LogMessage "No flags or account key found! Exiting." LogMessage "Usage (remove brackets [] and substitute for your value):" LogMessage "powershell -executionpolicy bypass -f ./InstallHuntress.powershellv2.ps1 [-acctkey ] [-orgkey ] [-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 -throwError "No flags or account key found! Exiting." } # trim keys for blanks before use $AccountKey = $AccountKey.Trim() $OrganizationKey = $OrganizationKey.Trim() # check that all the parameters that were passed are valid Test-Parameters # Hide most of the account key in the logs, keeping the front and tail end for troubleshooting if ($AccountKey -ne "__Account_Key__") { $masked = $AccountKey.Substring(0,4) + "************************" + $AccountKey.SubString($AccountKey.length-4,4) LogMessage "AccountKey: '$masked'" LogMessage "OrganizationKey: '$OrganizationKey'" LogMessage "Tags: $($Tags)" } # reregister > reinstall > uninstall > install (in decreasing order of impact) # reregister = reinstall + delete registry keys # reinstall = stop Huntress service + reinstall if ($reregister) { LogMessage "Re-register agent: '$reregister'" if ( !(Confirm-ServiceExists($HuntressAgentServiceName))) { LogMessage "Run with the -reregister flag but the service wasn't found. Attempting to install...." } PrepReregister } elseif ($reinstall) { LogMessage "Re-install agent: '$reinstall'" if ( !(Confirm-ServiceExists($HuntressAgentServiceName)) ) { $err = "Script was run w/ reinstall flag but there's nothing to reinstall. Attempting to clean remnants, then install the agent fresh." LogMessage "$err" uninstallHuntress } StopHuntressServices } else { LogMessage "Checking for HuntressAgent install..." $agentPath = getAgentPath if ( (Test-Path $agentPath) -eq $true) { $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) { copyLogAndExit -throwError "The Huntress Agent is already installed in $agentPath. Exiting with no changes. Suggest using -reregister or -reinstall flags. Asset count = $assetCount" } } } Get-Installer Install-Huntress $OrganizationKey fixServices Test-Installation LogMessage "Huntress Agent successfully installed!" } try { main Write-InstallScriptInfo } catch { copyLogAndExit -throwError $_.Exception.Message } LogMessage "Script Complete" copyLogAndExit