# 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, Cameron Granger # 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 Oct 13" $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" $ScriptFailed = "Script Failed!" $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__") { $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 } 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 } 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 } # 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 } elseif ($OrganizationKey.length -lt 1) { $err = "Invalid OrganizationKey specified (length should be > 0)!" LogMessage $err throw $ScriptFailed + " " + $err exit 1 } 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)) { Stop-Service -Name "$HuntressAgentServiceName" } else { LogMessage "$($HuntressAgentServiceName) not found, nothing to stop" } if (Confirm-ServiceExists($HuntressUpdaterServiceName)) { Stop-Service -Name "$HuntressUpdaterServiceName" } 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] { $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 support@huntress.com if it fails >2 times") LogMessage $err LogMessage $SupportMessage throw $ScriptFailed + " " + $err + " " + $SupportMessage } } # 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 { $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 } } # 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 { $msg = $_.Exception.Message $err = "WARNING: Failed to download the Huntress Installer ($attempt/$attempts), retrying in $delay seconds." LogMessage $msg 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 support@huntress.io if the problem persists." LogMessage $err throw $ScriptFailed + " " + $err } $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" $msg = ($err + "`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 $msg throw $ScriptFailed + " " + $err + " " + $SupportMessage } # 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 $err = "ERROR: Installer failed to complete in $timeout seconds. Possible interference from a security product?" LogMessage $err LogMessage $SupportMessage throw $ScriptFailed + " " + $err + " " + $SupportMessage } } # 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) ) { $err = "ERROR: $file did not exist. Check your AV/security software quarantine" LogMessage $err LogMessage $SupportMessage throw $ScriptFailed + " " + $err + " " + $SupportMessage } 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 "Warning: 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 { LogMessage "$($svc) service is missing! $($SupportMessage)" throw "$($ScriptFailed) $($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))) { throw "$($ScriptFailed) ERROR: restart of service $($svc) failed. $($SupportMessage)" } } 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) ) { $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 } # 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 } } } # 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) { $err = ("ERROR: The agent did not register. Check the log (%ProgramFiles%\Huntress\HuntressAgent.log) for errors.") LogMessage $err LogMessage $SupportMessage throw $ScriptFailed + " " + $err + " " + $SupportMessage } 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).'" LogMessage $err copyLogAndExit } } # 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 ( $service ) { 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://eetee.huntress.io/{0}"-f $file_name), ("https://huntress-installers.s3.us-east-1.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-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.io/agent/connectivity/{0}" -f $file_name), ("https://huntresscdn.com/agent/connectivity/{0}" -f $file_name), ("https://update.huntress.io/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 { # gather info on the host for logging purposes LogMessage "Script type: '$ScriptType'" LogMessage "Script version: '$ScriptVersion'" # 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 try { $uptime = ((Get-Date) - (GCIM Win32_OperatingSystem).LastBootUpTime).days } catch { LogMessage "Unable to determine system uptime" $uptime = 0 } 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 } } # 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. function copyLogAndExit { 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 (!$uninstall){ if (!(Test-Path -path $agentPath)) {New-Item $agentPath -Type Directory} Copy-Item -Path $DebugLog -Destination $logLocation -Force Write-Output "'$($DebugLog)' copied to '$logLocation'." } Write-Output "Script complete" exit 0 } # 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 } ######################################################################################### # begin main function # ######################################################################################### function main () { # Start the script with logging as much as we can as soon as we can. All your logging are belong to us, Zero Wang. logInfo 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)" } # if run with the uninstall flag, exit so we don't reinstall the agent after if ($uninstall) { LogMessage "Uninstalling Huntress agent" uninstallHuntress copyLogAndExit } # 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 } # 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) { LogMessage "The Huntress Agent is already installed in $agentPath. Exiting with no changes. Suggest using -reregister or -reinstall flags. Asset count = $assetCount" copyLogAndExit } } } Get-Installer Install-Huntress $OrganizationKey fixServices Test-Installation LogMessage "Huntress Agent successfully installed!" copyLogAndExit } try { main Write-InstallScriptInfo } catch { $ErrorMessage = $_.Exception.Message LogMessage $ErrorMessage copyLogAndExit } LogMessage "Script Complete"