Enforced Application Uninstall for Windows

  • 23 August 2019
  • 40 replies
  • 2233 views

Userlevel 5

Hey Guys,


This is one I’ve been thinking about how best to write for a bit. It took some consideration on how to handle the 64-bit scenario because the Automox Agent is a 32-bit process. The ScriptBlock method I used here worked wonders–and can be re-purposed for anything that needs a 64-bit shell.


So all you have to do here is define $appName in both blocks (make sure they match!). If it finds any matches, the Evaluation returns as Non-Compliant (since uninstalled is the desired state). It uses similar detection in the Remediation to find the applications “UninstallString” which can be used to run an uninstall–but adding parameters/arguments for silent execution. Since EXE arguments are non-standard, you may need to customize that bit, but /S works for most of them.


There are a ton of comments inside to make it clear what’s happening, but feel free to ask any questions that might come up.


Evaluation


<#
.SYNOPSIS
Check for presence of specified application on the target device

.DESCRIPTION
Read 32-bit and 64-bit registry to find matching applications

Exits with 0 for compliance, 1 for Non-Compliance.
Non-Compliant devices will run Remediation Code at the Policy's next scheduled date.

.NOTES
A scriptblock is used to workaround the limitations of 32-bit powershell.exe.
This allows us to redirect the operations to a 64-bit powershell.exe and read
the 64-bit registry without .NET workarounds.

.LINK
http://www.automox.com
#>

# The ScriptBlock method used here is to allow a 32-bit agent process
# to access the 64-bit registry on 64-bit Windows. This is necessary if the application
# isn't known to be 32-bit only.


$scriptblock = {
#Define Registry Location for the 64-bit and 32-bit Uninstall keys
$uninstReg = @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall','HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')

# Define the App Name to look for
# Look at a machine with the application installed unless you're sure the formatting of the name/version
# Specifically the DisplayName. This is what you see in Add/Remove Programs. This doesn't have to be exact.
# Default behavior uses -match which is essentially "DisplayName contains VLC"
##################
$appName = 'Steam'
##################

# Get all entries that match our criteria. DisplayName matches $appname
$installed = @(Get-ChildItem $uninstReg -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { ($_.DisplayName -match $appName) })

# If any matches were present, $installed will be populated. If none, then $installed is NULL and this IF statement will be false.
# The return value here is what the ScriptBlock will send back to us after we run it.
# 1 for Non-Compliant, 0 for Compliant
if ($installed) {
return 1
} else { return 0 }
}


$exitCode = & "$env:SystemRoot\sysnative\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Bypass -WindowStyle Hidden -NoProfile -NonInteractive -Command $scriptblock
Exit $exitCode

Remediation


<#
.SYNOPSIS
Uninstall matching applications from the target device.

.DESCRIPTION
Read 32-bit and 64-bit registry to determine the UninstallString for
each matching application. Then use the UninstallString to uninstall
the matching applications.

Exits with 0 for Success, 1 for Failure.

.NOTES
A scriptblock is used to workaround the limitations of 32-bit powershell.exe.
This allows us to redirect the operations to a 64-bit powershell.exe and read
the 64-bit registry without .NET workarounds.

.LINK
http://www.automox.com
#>

# BIG CAVEAT HERE
# If your application uses an EXE instead of "msiexec" in it's
# UninstallString, the SILENT argument isn't standardized.
# It's also possible that a reboot suppressing argument can be important
# You may need to look that up in the vendor's documentation.

# Change this in the ArgumentList below on this line
# $process = Start-Process $uninstString -ArgumentList '/S /NORESTART'

# The current ArgumentList is specific to Steam which uses /S for silent
# If your application doesn't have a standard UninstallString, you can
# define it directly, but that is rare.

$scriptblock = {
#Define Registry Location for Uninstall keys
$uninstReg = @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall','HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')

# Look at a machine with the application installed unless you're sure the formatting of the name/version
##################
$appName = 'Steam'
##################

# Get all entries that match our criteria. DisplayName matches $appname, DisplayVersion less than current
$installed = @(Get-ChildItem $uninstReg -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { ($_.DisplayName -match $appName) })

# Initialize an array to store the uninstalled app information
$uninstalled = @()

# Start a loop in-case you get more than one match, uninstall each.
foreach ($version in $installed) {
#For every version found, run the uninstall string
$uninstString = $version.UninstallString
#If exe run as written + silent argument, if msiexec run as msi using the name of the reg key as the msi guid.
if ($uninstString -match 'msiexec') {
$process = Start-Process msiexec.exe -ArgumentList "/x $($version.PSChildName) /qn REBOOT=ReallySuppress" -Wait -PassThru
} else {
$process = Start-Process $uninstString -ArgumentList '/S' -Wait -PassThru
}

# Check exit code for success/fail
# Using 3 "-eq" statements becuase older PowerShell doesn't support "-in"
# If unsuccessful, don't add to uninstalled list.
if ( ($process.ExitCode -eq '0') -or ($process.ExitCode -eq '1641') -or ($process.ExitCode -eq '3010') ) {
$uninstalled += $version.PSPath
}
}

return $uninstalled
}


$uninstalledApps = & "$env:SystemRoot\sysnative\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Bypass -WindowStyle Hidden -NoProfile -NonInteractive -Command $scriptblock

# Use Write-Output so you can see a result in the Activity Log
Write-Output "$uninstalledApps"

if ($uninstalledApps) {
Exit 0
} else { Exit 1 }

Edited to add success/fail check on the uninstall process.


This topic has been closed for comments

40 replies

Userlevel 7

Great worklet @rich! I like how you chose Steam as the example “unwanted” application to remove. 😀

I tried this out with Skype, but it didn’t work. This is what I have. I did the same thing you did here, but with Skype instead of steam for the variable.


This is what I got on the report when testing.


powershell.exe : This command cannot be run due to the error: The system cannot find the file specified. At C:\ProgramData\amagent\execDir079000749\execcmd721931816.ps1:73 char:20 + … alledApps = & "$env:SystemRoot\sysnative\WindowsPowerShell\v1.0\power … + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (🙂 [Start-Process], InvalidOperationException + FullyQualifiedErrorId : InvalidOperationException,Microsoft.PowerShell.Commands.StartProcessCommand

Userlevel 5

Hi Mike can you post your code here so that I can take a look?

<#

.SYNOPSIS

Check for presence of specified application on the target device


.DESCRIPTION

Read 32-bit and 64-bit registry to find matching applications


Exits with 0 for compliance, 1 for Non-Compliance.

Non-Compliant devices will run Remediation Code at the Policy’s next scheduled date.


.NOTES

A scriptblock is used to workaround the limitations of 32-bit powershell.exe.

This allows us to redirect the operations to a 64-bit powershell.exe and read

the 64-bit registry without .NET workarounds.


.LINK




#>


The ScriptBlock method used here is to allow a 32-bit agent process


to access the 64-bit registry on 64-bit Windows. This is necessary if the application


isn’t known to be 32-bit only.


$scriptblock = {

#Define Registry Location for the 64-bit and 32-bit Uninstall keys

$uninstReg = @(‘HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall’,‘HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall’)


# Define the App Name to look for
# Look at a machine with the application installed unless you're sure the formatting of the name/version
# Specifically the DisplayName. This is what you see in Add/Remove Programs. This doesn't have to be exact.
# Default behavior uses -match which is essentially "DisplayName contains VLC"
##################
$appName = 'Skype'
##################

# Get all entries that match our criteria. DisplayName matches $appname
$installed = @(Get-ChildItem $uninstReg -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { ($_.DisplayName -match $appName) })

# If any matches were present, $installed will be populated. If none, then $installed is NULL and this IF statement will be false.
# The return value here is what the ScriptBlock will send back to us after we run it.
# 1 for Non-Compliant, 0 for Compliant
if ($installed) {
return 1
} else { return 0 }

}


$exitCode = & “$env:SystemRoot\sysnative\WindowsPowerShell\v1.0\powershell.exe” -ExecutionPolicy Bypass -WindowStyle Hidden -NoProfile -NonInteractive -Command $scriptblock

Exit $exitCode


<#

.SYNOPSIS

Uninstall matching applications from the target device.


.DESCRIPTION

Read 32-bit and 64-bit registry to determine the UninstallString for

each matching application. Then use the UninstallString to uninstall

the matching applications.


Exits with 0 for Success, 1 for Failure.


.NOTES

A scriptblock is used to workaround the limitations of 32-bit powershell.exe.

This allows us to redirect the operations to a 64-bit powershell.exe and read

the 64-bit registry without .NET workarounds.


.LINK




#>


BIG CAVEAT HERE


If your application uses an EXE instead of “msiexec” in it’s


UninstallString, the SILENT argument isn’t standardized.


It’s also possible that a reboot suppressing argument can be important


You may need to look that up in the vendor’s documentation.


Change this in the ArgumentList below on this line


$process = Start-Process $uninstString -ArgumentList ‘/S /NORESTART’


The current ArgumentList is specific to Skype which uses /S for silent


If your application doesn’t have a standard UninstallString, you can


define it directly, but that is rare.


$scriptblock = {

#Define Registry Location for Uninstall keys

$uninstReg = @(‘HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall’,‘HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall’)


# Look at a machine with the application installed unless you're sure the formatting of the name/version
##################
$appName = 'Skype'
##################

# Get all entries that match our criteria. DisplayName matches $appname, DisplayVersion less than current
$installed = @(Get-ChildItem $uninstReg -ErrorAction SilentlyContinue | Get-ItemProperty | Where-Object { ($_.DisplayName -match $appName) })

# Initialize an array to store the uninstalled app information
$uninstalled = @()

# Start a loop in-case you get more than one match, uninstall each.
foreach ($version in $installed) {
#For every version found, run the uninstall string
$uninstString = $version.UninstallString
#If exe run as written + silent argument, if msiexec run as msi using the name of the reg key as the msi guid.
if ($uninstString -match 'msiexec') {
$process = Start-Process msiexec.exe -ArgumentList "/x $($version.PSChildName) /qn REBOOT=ReallySuppress" -Wait -PassThru
} else {
$process = Start-Process $uninstString -ArgumentList '/S' -Wait -PassThru
}

# Check exit code for success/fail
# Using 3 "-eq" statements becuase older PowerShell doesn't support "-in"
# If unsuccessful, don't add to uninstalled list.
if ( ($process.ExitCode -eq '0') -or ($process.ExitCode -eq '1641') -or ($process.ExitCode -eq '3010') ) {
$uninstalled += $version.PSPath
}
}

return $uninstalled

}


$uninstalledApps = & “$env:SystemRoot\sysnative\WindowsPowerShell\v1.0\powershell.exe” -ExecutionPolicy Bypass -WindowStyle Hidden -NoProfile -NonInteractive -Command $scriptblock


Use Write-Output so you can see a result in the Activity Log


Write-Output “$uninstalledApps”


if ($uninstalledApps) {

Exit 0

} else { Exit 1 }

It’s been posted. I’m not exactly a script expert. I tried using the exact name as found in add/remove programs as well, which is “Skype for Business Basic 2016 - en-us”

Userlevel 5

Have you already tried running it locally on one of your machines?

Seems to be a problem with this line.


$exitCode = & “$env:SystemRoot\sysnative\WindowsPowerShell\v1.0\powershell.exe” -ExecutionPolicy Bypass -WindowStyle Hidden -NoProfile -NonInteractive -Command $scriptblock


returns


& : The term ‘C:\Windows\sysnative\WindowsPowerShell\v1.0\powershell.exe’ is not recognized as the name of a cmdlet,


function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the


path is correct and try again.


At line:1 char:15




  • … xitCode = & "$env:SystemRoot\sysnative\WindowsPowerShell\v1.0\powersh …




  •             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



  • CategoryInfo : ObjectNotFound: (C:\Windows\sysn…\powershell.exe:String) [], CommandNotFoundException




  • FullyQualifiedErrorId : CommandNotFoundException



Userlevel 5

It’s broken on my end as well. I’ll get that fixed and let you know when its ready

Thanks Jason!

Userlevel 5

Ok its working as is if you have the desktop version except for a pop up we need to account for. Are you using the windows store app?

I’m not really sure what you mean by “using” it. I mean, I think it’s available on our windows 10 computers for end-users. Would you have time for a quick call?


385-242-4783

We use skype for business, so I imagine most people here are using that version which is from Office 365, I think. I don’t think the version from the store is the same, though I wouldn’t mind getting rid of all installations of Skype.

Userlevel 5

If this is the only error, it looks like the issue is that the script doesn’t handle for 32-bit Windows (7/8/10). Or, you’re testing in 64-bit PowerShell.


It sounds like you’re testing locally, so for the latter, launch “PowerShell ISE (x86)” instead, and you’ll get a different result (or whichever application you’re using to execute this). This is because SysNative is a virtual directory that only exists when you’re running a 32-bit powershell.exe (as Automox does) on a 64-bit Windows OS.


For the former, you would want to check to see if you’re on 32-bit Windows, then use “System32” in that path instead of “SysNative”.

Jason,


Have you had a chance to see if you can fix that worklet to uninstall skype for us?


There was also Microsoft Teams you said you had an engineer working on.


Thanks!

Userlevel 5

Can you run this in powershell for me? Let me know what it returns. This checks for the windows store version.

Get-AppxPackage skypeapp

It returned nothing on a machine with Skype. The Skype version being used is 14.56.102.0. The powershell did not return anything.

I’ve run this on two separate machines and both times returned nothing. Have you had a chance to check into this further?


Thanks,

Userlevel 5

Currently I dont have an office 365 version of skype to test with but I’m working on finding one. If you install the desktop version it will return and uninstall. Not much help right now but we will get this figured out.

Here is the version we are trying to remove. We don’t care about any other Skype versions, we just want the users using Teams instead of Skype for Business. I think it is installed when Office 365 is installed.


<img width=“508” height=“157” style=“width:5.2916in;height:1.6354in” id=“Picture_x0020_10” src="/uploads/db2103/original/2X/c/c6b261ad5d96e17ed920662b357a2d49f0d33366.jpg" alt="A screenshot of a cell phone


Description automatically generated">

I think I figured out what’s going on here. Most of our users have Skype for Business installed through the Office Installation, in which case it’s not in Add/Remove programs, so there’s nothing we can really do without uninstalling office

and getting a customer Office install from MS. So, we can just forget this whole thing.


We are still very interested in checking for, installing and updating Microsoft Teams. Teams does seem to be a deperate component that appears in Add/remove programs. Can you check for an update on how that’s coming with your Engineer?


Thanks,

Userlevel 7

 

 
 

mikec:

 

 

Here is the version we are trying to remove. We don’t care about any other Skype versions, we just want the users using Teams instead of Skype for Business. I think it is installed when Office 365 is installed.

 

<img width=“508” height=“157” style=“width:5.2916in;height:1.6354in” id=“Picture_x0020_10” src="/uploads/db2103/original/2X/c/c6b261ad5d96e17ed920662b357a2d49f0d33366.jpg" alt="A screenshot of a cell phone

 

Description automatically generated">

 

 

Are you using the reply by email feature? Unfortunately that doesn’t work for image uploads, so you’ll need to go to https://community.automox.com/ and post manually if you’re wanting to share a screenshot.

 

image001


This does not show up in Add/Remove programs when it’s installed along with Office 365, which is the case for most of our users, but it is the version we would like to remove.

Userlevel 5

Looks like you nailed the problem. Just found this on the microsoft site.


Step 2: Remove Skype for Business from your computer


IMPORTANT : If you are using Office 365Skype for Business , you can’t delete it from your computer without also uninstalling the rest of the Office suite. This is because it’s integrated with the other Office apps. The following instructions are for customers who have standalone versions of Skype for Business.

Userlevel 5

I checked on Teams. It still in active engineering so they are still working on it. A couple of tests have gone well for Mac’s but the windows 10 devices are causing some problems. If you would like to put in a ticket I can attach it to the escalation ticket and you’ll get updates on it’s progress on a weekly basis.

You can use intercom through the console or send an email to support@automox.com

Sent an email to support.