Nathan Ziehnert

5 minute read

Do you keep your environment free of rogue installs? I know we try at my company with a well maintained application list, but try as we might we somehow end up with 10 different versions of (INSERT APPLICATION HERE).  This can make remediation for upgrades a pain when the installer doesn’t gracefully upgrade.

Java Cup Falling

There are quick commands to do removals utilizing WMIC, you could just run every potential uninstaller available for the product, or maybe you’re lucky enough to have an uninstaller built specifically for all versions of that application (we thank you Adobe, for the flashplayeruninstaller.exe that you have graciously bestowed upon us). ConfigMgr, when you don’t have a super duper uninstaller, when you’ve decided that WMIC might be too unreliable for your tastes, or when you don’t want to list out every potential uninstall command… enter the PoSH Uninstaller.

PowerShell Bling

I developed (read: am in the process of continuing to develop) this script that should help automate most MSI and some non-MSI uninstalls. It’s an extensible script, so it’s not limited to a couple products… really, the sky is the limit as long as the application has either a GUID or an uninstall command listed in the HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ keys.

Click here for the github repository… or continue reading for some details about it’s inner workings…

SCRIPT INNER WORKINGS The script is relatively short (less than 200 lines including a 60 line Get-Help block) and honestly doesn’t do much except scan the registry and then run uninstall commands.  The basic flow for the app works like this:

This function was written originally by Jonathan Medd and adapted for this script.

If(-not (
	[Security.Principal.WindowsPrincipal] `
	[Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(`
		[Security.Principal.WindowsBuiltInRole] "Administrator")
	)
{
	#We want to throw an error so that the script terminates
	throw "This script requires administrative rights to function properly.  Please re-run this script as an Administrator."
}

The script checks the user security, and then throws an error if the user doesn’t have admin rights. Easy.

Determine OS Architecture

This part is pretty simple as well.  Checks the [System.IntPtr] size and falls back to x86 if the size isn’t a 4 or 8.

if([System.IntPtr]::Size -eq 4){
	$x64 = $false
}elseif([System.IntPtr]::Size -eq 8){
	$x64 = $true
}else{
	Write-Warning "Error in determining OS Architecture... Defaulting to 32-Bit." 
	$x64 = $false
}

Scan The Registry For Apps

The important part of this key is the filtering.  The script will accept a single string or an array of strings for each applicable field (publisher, applications, version, etc) so we run some joins on those arrays to make them RegEx ready.

$itemsToRemove = Get-ItemProperty $UninstallPath | Where-Object {($_.DisplayName -match ($applications -join "|")) -and ((-not $publishers) -or $_.Publisher -match ($publishers -join "|")) -and ((-not $exclusionPublishers) -or $_.Publisher -notmatch ($exclusionPublishers -join "|")) -and ((-not $exclusionVersions) -or $_.DisplayVersion -notmatch ($exclusionVersions -join "|")) -and ((-not $exclusionApplications) -or $_.DisplayName -notmatch ($exclusionApplications -join "|"))} | Select-Object DisplayName, Publisher, DisplayVersion, UninstallString | Sort-Object DisplayName

I would rather be able to filter on the Get-ItemProperty command instead of using Where-Object - mostly because it would be faster, but Get-ItemProperty doesn’t support a filterscript.

Check Removal Type (MSI or Non-MSI)

This is just a little more RegEx fun - using replace.  This particular spot of code is courtesy of /u/pouncer11 on Reddit. During a foreach loop on the results from the scan we create a new variable that tries to extract the GUID directly from the command and then runs a match on the result.

foreach($appToRemove in $appsToRemove){
	$uninstallGUID = $appToRemove.UninstallString -replace '.*({.*}).*','$1'
	
	if($uninstallGUID -match '[0-9a-z]{8}-([0-9a-z]{4}-){3}[0-9a-z]{12}'){
    	if(-not $WhatIf){
			Write-Verbose "Beginning removal of $($appToRemove.DisplayName)"
			if($logging){
				Start-Process -FilePath "$env:systemroot\system32\msiexec.exe" -ArgumentList "/X `"$($uninstallGUID)`" /qn $msiCustomArguments REBOOT=ReallySuppress /norestart /l*vx `"$logFilePath\$($appToRemove.DisplayName)_$($appToRemove.DisplayVersion)_REMOVE.log`"" -Wait -WorkingDirectory $pwd
			}else{
				Start-Process -FilePath "$env:systemroot\system32\msiexec.exe" -ArgumentList "/X `"$($uninstallGUID)`" /qn $msiCustomArguments REBOOT=ReallySuppress /norestart" -Wait -WorkingDirectory $pwd
			}
			Write-Verbose "Removal of $($appToRemove.DisplayName) complete"
		}else{
			Write-Verbose "Removal of $($appToRemove.DisplayName) WOULD take place"	
		}
    }else{
		if($nonMSISupport){
			if(-not $WhatIf){
				Write-Verbose "Attempting removal of $($appToRemove.DisplayName)"
				Start-Process -FilePath "$($appToRemove.UninstallString)" -ArgumentList "$nonMSICustomArguments" -Wait -WorkingDirectory $pwd
				Write-Verbose "Removal of $($appToRemove.DisplayName) complete"
			}else{
				Write-Verbose "Removal of $($appToRemove.DisplayName) WOULD take place"
			}
		}else{
			if(-not $WhatIf){
				Write-Warning "Not a Valid GUID for '$($appToRemove.DisplayName)' must be manually uninstalled, or you must run the command with nonMSISupport enabled."
			}else{
				Write-Verbose "Removal of $($appToRemove.DisplayName) would NOT take place, because Non-MSI support is not turned on"
			}
		}
	}
}

It will only attempt to run the uninstall commands for non-MSI applications IF the swtich for nonMSISupport is enabled.

Random Other Commands

What if… What if we want to see what the script would do without actually running the uninstalls?  Well, I’ve built -WhatIf support into the script as well.  Make sure to use the -Verbose switch to get the output.

msiCustomArguments and nonMSICustomArguments: you can use this to pass custom arguments to the command line for the uninstall commands.  Useful if you need to add a silent switch or similar to a non-MSI install.

CONCLUSION

Maybe you won’t find any use for it in your environment, maybe you will.  I know I plan on continuing to develop this script further to do more fun stuff - and I plan to give it a test run in my environment during my next deployment requiring an install.

As always, thanks for reading and happy admining!

comments powered by Disqus

Table of Contents