diff --git a/scripts/uninstall-shopdbagent.ps1 b/scripts/uninstall-shopdbagent.ps1 new file mode 100644 index 00000000..b4819ff3 --- /dev/null +++ b/scripts/uninstall-shopdbagent.ps1 @@ -0,0 +1,189 @@ +# uninstall-shopdbagent.ps1 +# ShopDB Endpoint Agent uninstaller. Run from an ELEVATED (admin) PowerShell on +# the endpoint. Reverses everything install-shopdbagent.ps1 and the site agent +# generator's install.ps1 create: +# - stops and deletes the Windows service +# - kills any running agent/tray processes +# - removes the tray logon Run key (64-bit and 32-bit views) +# - removes the credential hive HKLM\SOFTWARE\GE\ShopDB (both views) +# - removes the Defender path exclusion for the install directory +# - deletes the install directory and the ProgramData data/log tree +# +# Every step is best-effort and idempotent: a missing service, key, process, or +# folder is a no-op, so the script is safe to re-run and safe on a partially +# installed host. Nothing here fails the run just because an artifact is already +# gone; only an unexpected error on a present artifact surfaces as a warning. + +[CmdletBinding()] +param( + [string] $InstallDir = "C:\Program Files\ShopDB\EndpointAgent", + [string] $ServiceName = "ShopDbEndpointAgent", + [string] $TrayExeName = "shopdb-endpoint-tray.exe", + + # Also remove any server TLS certificate the site-agent installer imported + # into the machine Trusted Root store. Off by default because a root cert is + # shared trust material: only pull it when you are sure no other service on + # this host relies on it. Match is by subject CN substring. + [string] $RemoveRootCertMatching = "", + + # Keep the ProgramData logs (C:\ProgramData\ShopDB) for post-mortem instead + # of deleting them with everything else. + [switch] $KeepLogs +) + +$ErrorActionPreference = "Stop" + +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() +$principal = New-Object Security.Principal.WindowsPrincipal($identity) +if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw "This script must be run from an elevated (Run as administrator) PowerShell." +} + +# Delete an HKLM value or subkey from BOTH the 64-bit and 32-bit registry views. +# The installer writes both views (a stale WOW6432Node copy would otherwise be +# read first by the agent), so a clean uninstall has to clear both. +function Remove-HklmBothViews { + param( + [Parameter(Mandatory)] [string] $SubKey, + [string] $Name + ) + foreach ($view in @([Microsoft.Win32.RegistryView]::Registry64, + [Microsoft.Win32.RegistryView]::Registry32)) { + $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey( + [Microsoft.Win32.RegistryHive]::LocalMachine, $view) + try { + if ($Name) { + $key = $base.OpenSubKey($SubKey, $true) + if ($key) { + try { + if ($null -ne $key.GetValue($Name)) { $key.DeleteValue($Name, $false) } + } + finally { $key.Dispose() } + } + } + else { + if ($base.OpenSubKey($SubKey)) { + $base.DeleteSubKeyTree($SubKey, $false) + } + } + } + catch { + Write-Warning " registry cleanup ($view $SubKey $Name): $($_.Exception.Message)" + } + finally { $base.Dispose() } + } +} + +Write-Host "== ShopDB endpoint agent uninstall ==" -ForegroundColor Cyan +Write-Host "InstallDir : $InstallDir" +Write-Host "ServiceName : $ServiceName" + +# 1. Stop and delete the Windows service. +$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($service) { + Write-Host "[1/6] Stopping and deleting service $ServiceName ..." + if ($service.Status -ne "Stopped") { + try { Stop-Service -Name $ServiceName -Force -ErrorAction Stop } + catch { Write-Warning " could not stop service: $($_.Exception.Message)" } + } + sc.exe delete $ServiceName | Out-Null + Start-Sleep -Seconds 2 +} +else { + Write-Host "[1/6] Service $ServiceName not present; skipping." +} + +# 2. Kill any lingering agent/tray processes (service host, tray, and anything +# still running out of the install directory holding files open). +Write-Host "[2/6] Stopping agent and tray processes ..." +$trayBase = [System.IO.Path]::GetFileNameWithoutExtension($TrayExeName) +foreach ($name in @("shopdbagent", $trayBase)) { + foreach ($proc in (Get-Process -Name $name -ErrorAction SilentlyContinue)) { + try { Stop-Process -Id $proc.Id -Force -ErrorAction Stop; Write-Host " killed $name (pid $($proc.Id))" } + catch { Write-Warning " could not stop $name (pid $($proc.Id)): $($_.Exception.Message)" } + } +} +foreach ($proc in (Get-Process -ErrorAction SilentlyContinue | + Where-Object { $_.Path -and $_.Path.StartsWith($InstallDir, [StringComparison]::OrdinalIgnoreCase) })) { + try { Stop-Process -Id $proc.Id -Force -ErrorAction Stop; Write-Host " killed $($proc.ProcessName) from install dir (pid $($proc.Id))" } + catch { Write-Warning " could not stop pid $($proc.Id): $($_.Exception.Message)" } +} +Start-Sleep -Seconds 1 + +# 3. Remove the tray logon Run key entry (both views) and the credential hive. +Write-Host "[3/6] Removing registry entries ..." +Remove-HklmBothViews -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name "ShopDbEndpointTray" +Remove-HklmBothViews -SubKey "SOFTWARE\GE\ShopDB" +# Drop the parent GE key only if we emptied it (some sites nest siblings under +# GE such as SFLD; never tree-delete a key we do not own). +foreach ($view in @([Microsoft.Win32.RegistryView]::Registry64, + [Microsoft.Win32.RegistryView]::Registry32)) { + $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey( + [Microsoft.Win32.RegistryHive]::LocalMachine, $view) + try { + $ge = $base.OpenSubKey("SOFTWARE\GE") + if ($ge -and $ge.SubKeyCount -eq 0 -and $ge.ValueCount -eq 0) { + $ge.Dispose() + $base.DeleteSubKey("SOFTWARE\GE", $false) + Write-Host " removed empty SOFTWARE\GE ($view)" + } + elseif ($ge) { $ge.Dispose() } + } + catch { Write-Warning " GE key cleanup ($view): $($_.Exception.Message)" } + finally { $base.Dispose() } +} + +# 4. Remove the Defender path exclusion the installer added for the install dir. +Write-Host "[4/6] Removing Defender exclusion ..." +if (Get-Command Remove-MpPreference -ErrorAction SilentlyContinue) { + try { + Remove-MpPreference -ExclusionPath $InstallDir -ErrorAction Stop + Write-Host " removed Defender exclusion for $InstallDir" + } + catch { Write-Warning " could not remove Defender exclusion (tamper protection or policy?): $($_.Exception.Message)" } +} +else { + Write-Host " Defender cmdlets not present; skipping." +} + +# 5. Optionally remove an imported server root certificate. +if (-not [string]::IsNullOrWhiteSpace($RemoveRootCertMatching)) { + Write-Host "[5/6] Removing Trusted Root certs matching '$RemoveRootCertMatching' ..." + $matches = Get-ChildItem Cert:\LocalMachine\Root -ErrorAction SilentlyContinue | + Where-Object { $_.Subject -like "*$RemoveRootCertMatching*" } + foreach ($cert in $matches) { + try { + Remove-Item -Path $cert.PSPath -Force -ErrorAction Stop + Write-Host " removed $($cert.Subject) [$($cert.Thumbprint)]" + } + catch { Write-Warning " could not remove $($cert.Thumbprint): $($_.Exception.Message)" } + } + if (-not $matches) { Write-Host " no matching root certs found." } +} +else { + Write-Host "[5/6] Skipping root-cert removal (pass -RemoveRootCertMatching to enable)." +} + +# 6. Delete the install directory and the ProgramData data/log tree. +Write-Host "[6/6] Removing files ..." +if (Test-Path $InstallDir) { + try { Remove-Item -Path $InstallDir -Recurse -Force -ErrorAction Stop; Write-Host " removed $InstallDir" } + catch { Write-Warning " could not remove $InstallDir (a file may still be locked): $($_.Exception.Message)" } +} +else { Write-Host " $InstallDir not present." } + +$dataDir = Join-Path $env:ProgramData "ShopDB" +if (-not $KeepLogs -and (Test-Path $dataDir)) { + try { Remove-Item -Path $dataDir -Recurse -Force -ErrorAction Stop; Write-Host " removed $dataDir" } + catch { Write-Warning " could not remove $dataDir : $($_.Exception.Message)" } +} +elseif ($KeepLogs) { Write-Host " keeping $dataDir (-KeepLogs)." } + +# Also prune an empty C:\Program Files\ShopDB parent left behind. +$parent = Split-Path -Parent $InstallDir +if ((Test-Path $parent) -and -not (Get-ChildItem -Path $parent -Force -ErrorAction SilentlyContinue)) { + try { Remove-Item -Path $parent -Force -ErrorAction Stop; Write-Host " removed empty $parent" } catch { } +} + +Write-Host "`nUninstall complete." -ForegroundColor Green +Write-Host "Verify with: Get-Service $ServiceName ; Get-Process shopdbagent,$trayBase ; reg query 'HKLM\SOFTWARE\GE\ShopDB'"