Worklet: Enforced Application Uninstall for Windows

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.

4 Likes

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

1 Like

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: (:slight_smile: [Start-Process], InvalidOperationException + FullyQualifiedErrorId : InvalidOperationException,Microsoft.PowerShell.Commands.StartProcessCommand

1 Like

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

1 Like

<#
.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 }

1 Like

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”

1 Like

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

1 Like

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

Thanks Jason!

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.

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!

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,

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.jpeg" alt="A screenshot of a cell phone

Description automatically generated">