Application Uninstall Script | SCCM/MECM | Configuration Baseline

Background

I had been looking for a method to automate the removal of certain Win32 Apps in cases where they were previously installed manually by users or support staff on Windows endpoints. I came across various articles such as the one here, which offer a couple of options, such as:

  • Invoke the Uninstall method using WMI
  • Use the Uninstall-Package cmdlet

Neither of these options proved reliable, and upon further reading, although the Get/Uninstall-Package cmdlets can read the data for Win32 apps, it seems that it’s more suited to MSIX and APPX apps.

Previously, I’ve used the Uninstall Strings found in the applications Registry Hive to implement something similar, so I’ve refined a solution to reliably uninstall unwanted software from endpoints, and keep it that way, using SCCMs Configuration Baseline feature.

Overview

An SCCM Configuration Baseline which uses a Discovery and Remediation PowerShell script to silently uninstall unwanted Software. The frequency at which this runs can be defined in the Baseline.

How to use

I’ve configured the “Collect Uninstall Strings” script (See the Scripts heading) to make it easier to collect the data you need to define your uninstall parameters. Create an SCCM Run Script which can be used to collect the appropriate:

  • DisplayName
  • DisplayVersion
  • UninstallString

These properties can then be used to define our Uninstall strings in the Discovery and Remediation Scripts. To offer an example using Winrar:

Run the “Collect Uninstall Strings” PowerShell script from SCCM and enter the approximate name for your application using the required lookup value parameter:

If only one entry is found using the lookup value, the result will look like this, with the DisplayName, DisplayVersion and UninstallString separated by a pipeline:

Ok, so we have our values to work with. Let’s add them to the Discovery and Remediation scripts respectively. See the example from the next image with a few other versions included. You’ll want to define your own here, but this example includes uninstall methods for particular versions of WinRAR.

Note – You may need to determine the silent install parameters yourself. See the guide here if you’re not familiar with techniques on how to do this.

Next, add your Configuration Baseline Discovery and Remediation Scripts. If you’re not familiar with how Configuration Baselines work, I’d advise you to check out the Microsoft article here.

I don’t intend to run through how to to setup the baseline, but you’ll need to know how I’ve configured the script to detect compliance. The baseline must Equal the String Value “Compliant” to be marked as compliant.

Scripts

Collect Uninstall Strings (SCCM Run Script)
#MECM_CollectUninstallStrings
#Version | 1.0
#Contact | Josh Woods | joshua@jwblog.uk
#Parameters
Param(
[Parameter(Mandatory=$True)]
[string]$Name
)
#Get List of Applications based on Name
$List1 = Get-ItemProperty -path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' | where-object {$_.DisplayName -Like "*$Name*"}
$List2 = Get-ItemProperty -path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' | where-object {$_.DisplayName -Like "*$Name*"}
$List = $List1 + $List2
if ($List.count -gt 1) {
foreach ($Item in $List) {
#Write Output to MECM Console
Write-output "$($Item.DisplayName) | $($Item.DisplayVersion) | $($Item.UninstallString)"
}
}
else {
#Write Output to MECM Console
Write-output "$($List.DisplayName) | $($List.DisplayVersion) | $($List.UninstallString)"
}
Discovery Script
#ApplicationUninstall_ConfigurationBaseline
#To be used in MECM/SCCM as a Configuration Baseline
#Discovery Script
#Version | 1.0
#Contact | Josh Woods | joshua@jwblog.uk
###################
#Variables
###################
#Configuration
$Version = "1.0"
#Uninstall List
$Uninstallers = @(
[pscustomobject]@{RegistryDisplayName='WinRAR 5.70 (64-bit)';RegistryDisplayVersion='5.70.0';UninstallStringDir='C:\Program Files\WinRAR\uninstall.exe';UninstallStringArguments='/s'}
[pscustomobject]@{RegistryDisplayName='WinRAR 6.02 (64-bit)';RegistryDisplayVersion='6.02.0';UninstallStringDir='C:\Program Files\WinRAR\uninstall.exe';UninstallStringArguments='/s'}
[pscustomobject]@{RegistryDisplayName='WinRAR 6.11 (64-bit)';RegistryDisplayVersion='6.11.0';UninstallStringDir='C:\Program Files\WinRAR\uninstall.exe';UninstallStringArguments='/s'}
[pscustomobject]@{RegistryDisplayName='WinRAR 6.22 (64-bit)';RegistryDisplayVersion='6.22.0';UninstallStringDir='C:\Program Files\WinRAR\uninstall.exe';UninstallStringArguments='/s'}
[pscustomobject]@{RegistryDisplayName='WinRAR 6.23 (64-bit)';RegistryDisplayVersion='6.23.0';UninstallStringDir='C:\Program Files\WinRAR\uninstall.exe';UninstallStringArguments='/s'}
)
###################
#Variables End
###################
#Stop Install on Error and output to log
try {
#Log
#Start Log Transcript
Start-Transcript -Path "C:\Windows\Logs\ApplicationUninstall_ConfigurationBaseline_Discovery_$Version.txt" -append | Out-Null
#Get List of Applications based on Name
$List1 = Get-ItemProperty -path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
$List2 = Get-ItemProperty -path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
$List = $List1 + $List2
if ($Uninstallers.count -gt 1) {
$Match = @()
$DiscoveredApplications = @()
foreach ($Uninstall in $Uninstallers) {
$InstallCheck = $null
$InstallCheck = $List | where-object {($_.DisplayName -eq $Uninstall.RegistryDisplayName) -and ($_.DisplayVersion -eq $Uninstall.RegistryDisplayVersion)} | select-object -expandproperty DisplayName
if ($InstallCheck -eq $Uninstall.RegistryDisplayName) {
#Write-Output "$($Uninstall.RegistryDisplayName) Installed"
$DiscoveredApplications = $DiscoveredApplications + $($Uninstall.RegistryDisplayName)
$Match = $Match + "ApplicationExists"
}
}
}
else {
$InstallCheck = $null
$InstallCheck = $List | where-object {($_.DisplayName -eq $Uninstallers.RegistryDisplayName) -and ($_.DisplayVersion -eq $Uninstallers.RegistryDisplayVersion)} | select-object -expandproperty DisplayName
if ($InstallCheck -eq $Uninstallers.RegistryDisplayName) {
#Write-Output "$($Uninstallers.RegistryDisplayName) Installed"
$DiscoveredApplications = $($Uninstall.RegistryDisplayName)
$Match = "ApplicationExists"
}
}
}
catch {
#Failed. Write error and exit.
Write-Output "Error: $($_.Exception.Message)"
#Output Logs to File
Stop-Transcript
}
#Run Remediation Script if an app is listed
if ($Match -NotContains "ApplicationExists") {
Write-Host "Compliant"
}
else {
Write-Host "Remediation Script Will Run. Following Apps found | $($Uninstall.RegistryDisplayName)"
}
view raw Discovery.ps1 hosted with ❤ by GitHub
Remediation Script
#ApplicationUninstall_ConfigurationBaseline
#To be used in MECM/SCCM as a Configuration Baseline
#Uninstalls a set of Defined Applications. Frequently checked according to the Configuration Baseline run frequency.
#Remediation Script
#Version | 1.0
#Contact | Josh Woods | joshua@jwblog.uk
#References
###################
#Variables
###################
#Configuration
$Version = "1.0"
#Uninstall List
$Uninstallers = @(
[pscustomobject]@{RegistryDisplayName='WinRAR 5.70 (64-bit)';RegistryDisplayVersion='5.70.0';UninstallStringDir='C:\Program Files\WinRAR\uninstall.exe';UninstallStringArguments='/s'}
[pscustomobject]@{RegistryDisplayName='WinRAR 6.02 (64-bit)';RegistryDisplayVersion='6.02.0';UninstallStringDir='C:\Program Files\WinRAR\uninstall.exe';UninstallStringArguments='/s'}
[pscustomobject]@{RegistryDisplayName='WinRAR 6.11 (64-bit)';RegistryDisplayVersion='6.11.0';UninstallStringDir='C:\Program Files\WinRAR\uninstall.exe';UninstallStringArguments='/s'}
[pscustomobject]@{RegistryDisplayName='WinRAR 6.22 (64-bit)';RegistryDisplayVersion='6.22.0';UninstallStringDir='C:\Program Files\WinRAR\uninstall.exe';UninstallStringArguments='/s'}
[pscustomobject]@{RegistryDisplayName='WinRAR 6.23 (64-bit)';RegistryDisplayVersion='6.23.0';UninstallStringDir='C:\Program Files\WinRAR\uninstall.exe';UninstallStringArguments='/s'}
)
#Uninstall Values retreived from the following locations
#(Get-ItemProperty -path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' | Select-object DisplayName, DisplayVersion | sort-object)
#(Get-ItemProperty -path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' | Select-object DisplayName, DisplayVersion | sort-object)
#Example Values
#Get-ItemProperty -path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' | where-object {$_.DisplayName -Like "*WinRAR*"}
#Get-ItemProperty -path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' | where-object {$_.DisplayName -Like "*WinRAR*"}
###################
#Variables End
###################
#Get List of Applications based on Name
$List1 = Get-ItemProperty -path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
$List2 = Get-ItemProperty -path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
$List = $List1 + $List2
#Stop Install on Error and output to log
try {
#Log
#Start Log Transcript
Start-Transcript -Path "C:\Windows\Logs\ApplicationUninstall_ConfigurationBaseline_Remediation_$Version.txt" -append
########################################################################################################################################################################################################################################
#This section will uninstall a previous version based on the $RegistryDisplayName variable which uses the applications DisplayName.
########################################################################################################################################################################################################################################
if ($Uninstallers.count -gt 1) {
foreach ($Uninstall in $Uninstallers) {
$InstallCheck = $null
$InstallCheck = $List | where-object {($_.DisplayName -eq $Uninstall.RegistryDisplayName) -and ($_.DisplayVersion -eq $Uninstall.RegistryDisplayVersion)} | select-object -expandproperty DisplayName
if ($InstallCheck -eq $Uninstall.RegistryDisplayName) {
#Run Uninstall String
Write-Output "Uninstalling $($Uninstall.RegistryDisplayName) based on Uninstall String…"
Start-Process "$($Uninstall.UninstallStringDir)" -ArgumentList "$($Uninstall.UninstallStringArguments)" -Wait
}
}
}
else {
$InstallCheck = $null
$InstallCheck = $List | where-object {($_.DisplayName -eq $Uninstallers.RegistryDisplayName) -and ($_.DisplayVersion -eq $Uninstallers.RegistryDisplayVersion)} | select-object -expandproperty DisplayName
if ($InstallCheck -eq $Uninstallers.RegistryDisplayName) {
#Run Uninstall String
Write-Output "Uninstalling $($Uninstallers.RegistryDisplayName) based on Uninstall String…"
Start-Process "$($Uninstallers.UninstallStringDir)" -ArgumentList "$($Uninstallers.UninstallStringArguments)" -Wait
}
}
########################################################################################################################################################################################################################################
#Output Logs to File
Stop-Transcript
}
catch {
#Failed. Write error and exit.
Write-Output "Error: $($_.Exception.Message)"
#Output Logs to File
Stop-Transcript
}
view raw Remediation.ps1 hosted with ❤ by GitHub