SmilarScripts / PowerShellSmilar home

WINDOWS / UNINSTALL

Remove ESET
products.

Detect what’s installed. Remove protection first. Verify, then remove the agent.

Pilot before rollout

PowerShell parsing and mocked workflow checks passed. Uses ESET’s documented uninstall parameters. Run detection first, then pilot on a workstation, server and agent-only device before rollout. Live Windows/ESET removal is not yet validated.

Usage

Detect only

.\Remove-EsetProducts.ps1 -DetectOnly

Uninstall without a password

.\Remove-EsetProducts.ps1

Use one password for protection and agent (placeholder only)

.\Remove-EsetProducts.ps1 -UninstallPassword 'YOUR_PASSWORD'

Use separate product and agent passwords

.\Remove-EsetProducts.ps1 -UninstallPassword 'PRODUCT_PASSWORD' -AgentUninstallPassword 'AGENT_PASSWORD'

For an apostrophe in a PowerShell single-quoted value, double it. Double quotes and control characters are rejected if a password retry is needed; remove password protection using an authorised ESET policy in that case.

Remove-EsetProducts.ps1

Raw

#Requires -Version 5.1<#.SYNOPSISRemove ESET security products, verify removal, then remove Management Agent.Version 2.1.0. Windows PowerShell 5.1+, SYSTEM, N-central / NinjaOne..DESCRIPTIONTargets 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 productsare 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 productis first tried WITHOUT a password. On MSI 1603, retry once with the suppliedpassword 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 privilegedmonitoring 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}

Expected results

ResultExit codeMeaning
DETECTED0Detection-only run completed.
REMOVED_OR_ABSENT0Target registrations, core services and identified ESET drivers are absent. Agent-only and already-clean devices are handled automatically.
REBOOT_REQUIRED3010Incomplete: restart required. No reboot requested. Security pending means the agent is retained. Rerun after a planned restart.
FAILEDMSI codeFor example, 1603 for a general failure or 1618 when another installation remains busy.
ERROR / VERIFICATION_FAILED / INTERRUPTED / OTHER_ESET_PRESENT1Review the job output. Agent removal is blocked if security cannot be verified absent, another ESET product remains, or an earlier removal was interrupted.
TIMEOUT1460Installer wait exceeded 15 minutes. It may still be running; check before retrying.

Only exit 0 means the requested removal checks passed (or detection-only completed). Exit 3010 means the workflow is incomplete. Configure your RMM to report it without automatically rebooting or treating it as full removal success.

Output is written to the RMM job log. The script does not enable verbose MSI logging or print the password.

Interrupted jobs and saved state

The script stores non-secret operation state at HKLM:\SYSTEM\Smilar\Scripts\EsetRemoval, shared by 32-bit and 64-bit runs. A pending restart blocks repeated uninstall attempts in the same boot. After a planned restart, it inventories and verifies again. A timeout or interrupted job also blocks retries: check the previous job and confirm Windows Installer is no longer running. Only after reviewing the device, an administrator can clear this one marker to allow a retry; do not clear a reboot-required marker to bypass the restart check.

Script integrity · SHA-256d55491e4ec4774a4603f4c20066587687dd0cb18b64e41112493ab4380edce89