#Requires -Version 5.1 <# .SYNOPSIS Remove ESET security products, verify removal, then remove Management Agent. Version 2.1.0. Windows PowerShell 5.1+, SYSTEM, N-central / NinjaOne. .DESCRIPTION Targets Endpoint Security, Endpoint Antivirus, Server Security (including "for Microsoft Windows Server" / "for Windows Server"), and Management Agent. Reads both registry views; never queries Win32_Product. Unrelated ESET products are left installed and block agent removal to preserve their management. All MSI calls use /qn /norestart REBOOT=ReallySuppress. No restart is scheduled. A restart may still be required by ESET; 3010 is INCOMPLETE, never full success. Agent removal requires security registrations and core services / ESET drivers to be absent, with no pending security uninstall from this run or an earlier run this boot. A small HKLM SYSTEM marker (shared across 32/64-bit hosts) preserves reboot/interrupted-operation state across runs. The script does not delete drivers/files or bypass tamper/password protection. -UninstallPassword or ESET_UNINSTALL_PASSWORD: security product password. -AgentUninstallPassword or ESET_AGENT_UNINSTALL_PASSWORD: agent password. If the agent password is omitted, the security password is reused. Each product is first tried WITHOUT a password. On MSI 1603, retry once with the supplied password if present. 1603 is generic: it does not prove password protection. Unprotected products can therefore succeed regardless of a supplied password. Missing/wrong passwords on protected products still fail; protection is not bypassed. Double quotes and control characters in passwords are unsupported. Passwords are never printed. MSI command lines can be captured by privileged monitoring or installer logging policy. Use protected RMM inputs, never source. Exit 0: verified targeted registrations/core services / ESET drivers absent, or detection only. Exit 3010: restart required; no reboot performed; rerun after a planned restart. Exit 1: validation failed, other ESET products present, or interrupted removal. Exit 1460: MSI wait timeout; it may still be running. Do not launch a duplicate. Other MSI failures retain their code (e.g. 1603, 1618). Use a 150-minute RMM timeout for a device containing all four target products; each password mode has a 15-minute MSI wait plus bounded busy retries; validation waits up to 30s. Most devices have fewer products and finish sooner. Pilot required: actual Windows/ESET removal has not been exercised here. .EXAMPLE .\Remove-EsetProducts.ps1 -DetectOnly .EXAMPLE .\Remove-EsetProducts.ps1 -UninstallPassword 'PRODUCT_PASSWORD' -AgentUninstallPassword 'AGENT_PASSWORD' #> [CmdletBinding()] param( [Parameter(Position = 0)][AllowEmptyString()] [string]$UninstallPassword = $env:ESET_UNINSTALL_PASSWORD, [AllowEmptyString()] [string]$AgentUninstallPassword = $env:ESET_AGENT_UNINSTALL_PASSWORD, [switch]$DetectOnly ) $ErrorActionPreference = 'Stop' $script:StatePath = 'HKLM:\SYSTEM\Smilar\Scripts\EsetRemoval' function Write-Status([string]$Message) { Write-Host ('[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message) } function Get-EsetKind([string]$DisplayName) { switch -Regex ($DisplayName) { '^ESET (Endpoint Security|Endpoint Antivirus)$' { return 'Security' } '^ESET Server Security(?: for (?:Microsoft )?Windows Server)?$' { return 'Security' } '^ESET Management Agent$' { return 'Agent' } default { return 'Other' } } } function Get-EsetInventory { $views = @([Microsoft.Win32.RegistryView]::Registry32) if ([Environment]::Is64BitOperatingSystem) { $views += [Microsoft.Win32.RegistryView]::Registry64 } $seen = @{} foreach ($view in $views) { $base = $null $uninstall = $null try { $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey( [Microsoft.Win32.RegistryHive]::LocalMachine, $view) $uninstall = $base.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall') if ($null -eq $uninstall) { continue } foreach ($name in $uninstall.GetSubKeyNames()) { $key = $null try { $key = $uninstall.OpenSubKey($name) if ($null -eq $key) { continue } $displayName = [string]$key.GetValue('DisplayName') if ($displayName -notmatch '^ESET(?:\s|$)') { continue } $kind = Get-EsetKind $displayName if ([string]$key.GetValue('Publisher') -notmatch '^ESET(?:\b|,)') { throw 'Unexpected publisher for an ESET entry.' } if ($kind -ne 'Other' -and $name -notmatch '^\{[0-9A-Fa-f]{8}-(?:[0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}\}$') { throw 'ESET registration has no MSI product code. Manual review required.' } if (-not $seen.ContainsKey($name)) { $seen[$name] = $true [pscustomobject]@{ Name = $displayName Kind = $kind ProductCode = $name Version = [string]$key.GetValue('DisplayVersion') } } } finally { if ($null -ne $key) { $key.Dispose() } } } } finally { if ($null -ne $uninstall) { $uninstall.Dispose() } if ($null -ne $base) { $base.Dispose() } } } } function Test-SystemContext { return [Security.Principal.WindowsIdentity]::GetCurrent().IsSystem } function Get-CoreServices([string]$Stage) { # Query failure is fatal; it is never treated as an empty service list. $names = if ($Stage -eq 'Security') { @('ekrn', 'EhttpSrv') } else { @('ERAAgent') } @(Get-Service -ErrorAction Stop | Where-Object { $_.Name -in $names }) if ($Stage -eq 'Security') { # Include registered ESET drivers even if stopped; never unload them. @(Get-CimInstance -ClassName Win32_SystemDriver -ErrorAction Stop | Where-Object { $_.DisplayName -match '^ESET(?:\s|$)' -or $_.PathName -match '\\ESET\\' }) } } function Set-RemovalState([string]$Stage, [string]$Status, [string]$Boot) { $null = New-Item -Path $script:StatePath -Force # Write Status last; an incomplete marker is handled as an error next run. $null = New-ItemProperty -Path $script:StatePath -Name Stage -Value $Stage -PropertyType String -Force $null = New-ItemProperty -Path $script:StatePath -Name Boot -Value $Boot -PropertyType String -Force $null = New-ItemProperty -Path $script:StatePath -Name Status -Value $Status -PropertyType String -Force } function Clear-RemovalState { if (Test-Path $script:StatePath) { Remove-Item -Path $script:StatePath -Recurse -Force } } function Wait-StageAbsent([string]$Stage) { # Verification: exact target registrations plus core service registrations. for ($i = 0; $i -le 6; $i++) { $remaining = @(Get-EsetInventory | Where-Object { $_.Kind -eq $Stage }) $services = @(Get-CoreServices $Stage) if ($remaining.Count -eq 0 -and $services.Count -eq 0) { return $true } if ($i -lt 6) { Start-Sleep -Seconds 5 } } return $false } function Invoke-EsetMsi($Product, [string]$Password) { $msiexec = Join-Path $env:WINDIR 'System32\msiexec.exe' if ([Environment]::Is64BitOperatingSystem -and -not [Environment]::Is64BitProcess) { $msiexec = Join-Path $env:WINDIR 'Sysnative\msiexec.exe' } $arguments = '/x {0} /qn /norestart REBOOT=ReallySuppress' -f $Product.ProductCode try { if (-not [string]::IsNullOrEmpty($Password)) { $quotedPassword = [regex]::Replace($Password, '(\\+)$', '$1$1') $arguments += ' PASSWORD="{0}"' -f $quotedPassword } for ($attempt = 1; $attempt -le 3; $attempt++) { Write-Status ('Uninstalling {0}; attempt {1}; restart suppressed.' -f $Product.Name, $attempt) $start = New-Object System.Diagnostics.ProcessStartInfo $start.FileName = $msiexec $start.Arguments = $arguments $start.UseShellExecute = $false $start.CreateNoWindow = $true # Avoid unnecessarily passing RMM password variables to the child. $start.EnvironmentVariables.Remove('ESET_UNINSTALL_PASSWORD') $start.EnvironmentVariables.Remove('ESET_AGENT_UNINSTALL_PASSWORD') $process = [System.Diagnostics.Process]::Start($start) $start.Arguments = '' try { if (-not $process.WaitForExit(900000)) { return 1460 } $code = $process.ExitCode } finally { $process.Dispose() } if ($code -ne 1618 -or $attempt -eq 3) { return $code } Write-Status 'Windows Installer busy (1618); retrying in 30 seconds.' Start-Sleep -Seconds 30 } } finally { $Password = $null; $quotedPassword = $null; $arguments = $null } } function Invoke-Removal { if (-not (Test-SystemContext)) { Write-Status 'RESULT=ERROR; Select LocalSystem / SYSTEM as the RMM account.' return 1 } # A global mutex prevents concurrent copies of this script on the device. $mutex = New-Object System.Threading.Mutex($false, 'Global\SmilarEsetRemoval') $locked = $false try { try { $locked = $mutex.WaitOne(0) } catch [System.Threading.AbandonedMutexException] { $locked = $true } if (-not $locked) { Write-Status 'RESULT=BUSY; Another copy is running.'; return 1618 } $inventory = @(Get-EsetInventory) foreach ($product in $inventory) { Write-Status ('DETECTED: {0} {1}; category={2}; product={3}' -f $product.Name, $product.Version, $product.Kind, $product.ProductCode) } $securityServices = @(Get-CoreServices 'Security') $agentServices = @(Get-CoreServices 'Agent') Write-Status ('CORE_COMPONENTS: security={0}; agent={1}' -f $securityServices.Count, $agentServices.Count) $state = if (Test-Path $script:StatePath) { Get-ItemProperty $script:StatePath } else { $null } if ($DetectOnly) { if ($null -ne $state) { Write-Status ('SAVED_STATE: {0}; stage={1}' -f $state.Status, $state.Stage) } Write-Status 'RESULT=DETECTED; Inventory complete. No uninstall or state changes made.' return 0 } $boot = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop).LastBootUpTime.ToUniversalTime().ToString('o') if ($null -ne $state) { if (-not $state.Boot -or $state.Stage -notin @('Security', 'Agent') -or $state.Status -notin @('InProgress', 'RebootRequired')) { Write-Status 'RESULT=ERROR; Incomplete saved removal state. Review the previous job before changing its HKLM marker.' return 1 } if ($state.Boot -eq $boot) { if ($state.Status -eq 'RebootRequired') { Write-Status 'RESULT=REBOOT_REQUIRED; Earlier removal is awaiting a planned restart. No further uninstall attempted.' return 3010 } Write-Status 'RESULT=INTERRUPTED; Earlier MSI may still be running. Review Windows Installer and the previous job before retrying. The HKLM state marker is documented on the script page.' return 1 } # A restart has occurred. Re-inventory and verify normally below. Clear-RemovalState } foreach ($stage in @('Security', 'Agent')) { $inventory = @(Get-EsetInventory) if ($stage -eq 'Agent') { if (-not (Wait-StageAbsent 'Security')) { Write-Status 'RESULT=VERIFICATION_FAILED; Security registration or core services / ESET drivers remain. Agent retained.' return 1 } if (@($inventory | Where-Object { $_.Kind -eq 'Other' }).Count -gt 0) { Write-Status 'RESULT=OTHER_ESET_PRESENT; Unsupported ESET software remains. Agent retained; review detected products.' return 1 } } foreach ($product in @($inventory | Where-Object { $_.Kind -eq $stage })) { Set-RemovalState $stage 'InProgress' $boot $password = if ($stage -eq 'Agent') { $AgentUninstallPassword } else { $UninstallPassword } # Try without PASSWORD first, even when an RMM password is supplied. $code = Invoke-EsetMsi $product '' if ($code -eq 1603 -and -not [string]::IsNullOrEmpty($password)) { Write-Status 'MSI_EXIT_CODE=1603 without password. This is a general failure; trying once with the supplied password.' if ($password -match '["\x00-\x1F\x7F]') { Clear-RemovalState Write-Status 'RESULT=ERROR; Supplied password has unsupported characters. No password retry or further uninstall attempted.' return 1 } $code = Invoke-EsetMsi $product $password } $password = $null Write-Status ('MSI_EXIT_CODE={0}; product={1}' -f $code, $product.Name) if ($code -eq 3010 -or $code -eq 1641) { Set-RemovalState $stage 'RebootRequired' $boot Write-Status ('RESULT=REBOOT_REQUIRED; stage={0}; removal incomplete. No restart requested by this script. Rerun after a planned restart; agent removal is deferred if security is pending.' -f $stage) if ($code -eq 1641) { Write-Status 'WARNING: MSI reported a restart initiated despite suppression. Review installer behaviour.' } return 3010 } if ($code -eq 1460) { Write-Status 'RESULT=TIMEOUT; MSI may still be running. State retained; no installer killed and no further uninstall attempted.' return 1460 } if ($code -notin @(0, 1605, 1614)) { Clear-RemovalState Write-Status 'RESULT=FAILED; Check password/policy and Windows Installer events. No further uninstall attempted.' return $code } # Recheck this exact product immediately; stage verification follows. if (@(Get-EsetInventory | Where-Object { $_.ProductCode -eq $product.ProductCode }).Count -gt 0) { Clear-RemovalState Write-Status 'RESULT=VERIFICATION_FAILED; Product registration remains. No further uninstall attempted.' return 1 } Clear-RemovalState } if (-not (Wait-StageAbsent $stage)) { Write-Status ('RESULT=VERIFICATION_FAILED; {0} registration or core services / ESET drivers remain. No further uninstall attempted.' -f $stage) return 1 } Write-Status ('VERIFIED: {0} target registrations and core services / ESET drivers absent.' -f $stage) } # Catch security software reappearing during agent removal. if (@(Get-EsetInventory).Count -gt 0 -or @(Get-CoreServices 'Security').Count -gt 0 -or @(Get-CoreServices 'Agent').Count -gt 0) { Write-Status 'RESULT=VERIFICATION_FAILED; ESET software or core services / ESET drivers reappeared. Review deployment policies.' return 1 } Write-Status 'RESULT=REMOVED_OR_ABSENT; Target registrations and core services / ESET drivers verified absent. No restart requested. This is not a forensic check for every residual file or driver.' return 0 } finally { if ($locked) { $mutex.ReleaseMutex() } $mutex.Dispose() } } try { if (-not $PSBoundParameters.ContainsKey('AgentUninstallPassword') -and $null -eq $env:ESET_AGENT_UNINSTALL_PASSWORD) { $AgentUninstallPassword = $UninstallPassword } $result = Invoke-Removal exit $result } catch { # Process exceptions may contain passwords; do not print exception text. Write-Status 'RESULT=ERROR; Detection, MSI execution or validation failed. Agent removal was not continued. Review local Windows Installer events and any saved removal state.' exit 1 } finally { $UninstallPassword = $null; $AgentUninstallPassword = $null; $password = $null }