In der IT-Welt ist die Verwaltung von Software auf mehreren Rechnern eine komplexe und wichtige Aufgabe. Eine der häufigsten Herausforderungen für IT-Administratoren ist die effiziente und standardisierte Deinstallation von Windows-Anwendungen.
Die manuelle Deinstallation, insbesondere bei mehreren Endpunkten, ist nicht nur zeitaufwändig, sondern auch fehleranfällig. Hier kommen PowerShell-Skripte ins Spiel.
Dieses Skript ermöglicht die automatisierte Anwendungsdeinstallation mit PowerShell unter Verwendung von UninstallString und benutzerdefinierten Argumenten. Es bietet so einen optimierten Ansatz zur Verwaltung von Software-Entfernungen.
Die Wichtigkeit eines automatisierten Deinstallationsskripts
Die Deinstallation von Software, insbesondere in großen Unternehmen, ist keine einfache Aufgabe. IT-Experten und Managed Service Provider (MSPs) haben oft mit einer Vielzahl von Anwendungen zu tun, die auf verschiedenen Rechnern installiert sind. Jede Anwendung kann unterschiedliche Deinstallationsbefehle erfordern oder unterschiedlichen Protokollen folgen.
Das Skript, das wir analysieren, ist so konzipiert, dass es mit diesen Nuancen umgehen kann und automatisch die notwendigen Argumente wie /qn /norestart oder /S hinzufügt, die für eine im Hintergrund laufende und neustartfreie Deinstallation unerlässlich sind. Diese Automatisierung ist entscheidend für die Wahrung der Beständigkeit und Effizienz in großen IT-Umgebungen.
Das Skript:
#Requires -Version 5.1 <# .SYNOPSIS Uninstall an application using the UninstallString and custom arguments. This script will auto-add /qn /norestart or /S arguments. This script will only uninstall apps that follow typical uninstall patterns such as msiexec /X{GUID} /qn /norestart. .DESCRIPTION Uninstall an application using the UninstallString and custom arguments. This script will auto-add /qn /norestart or /S arguments. This script will only uninstall apps that follow typical uninstall patterns such as msiexec /X{GUID} /qn /norestart. .EXAMPLE -Name "VLC media Player" Beginning uninstall of VLC media player using MsiExec.exe /X{9675011C-2395-4AD7-B1CC-92910F991F58} /qn /norestart... Exit Code for VLC media player: 0 Successfully uninstalled your requested apps! PARAMETER: -Name "ReplaceMeWithNameOfApp" Exact name of the application to uninstall, separated by commas. E.g., 'VLC media player, Everything 1.4.1.1024 (x64)'. PARAMETER: -Arguments "/SILENT, /NOREBOOT" Additional arguments to use when uninstalling the app, separated by commas. E.g., '/SILENT, /NOREBOOT'. PARAMETER: -Reboot Schedules a reboot for 1 minute after the uninstall process succeeds. PARAMETER: -Timeout "ReplaceMeWithTheNumberOfMinutesToWait" Specify the amount of time in minutes to wait for the uninstall process to complete. If the process exceeds this time, the script and uninstall process will be terminated. .NOTES Minimum OS Architecture Supported: Windows 10, Windows Server 2016 Release Notes: Initial Release .COMPONENT Misc #> [CmdletBinding()] param ( [Parameter()] [String]$Name, [Parameter()] [String]$Arguments, [Parameter()] [switch]$Reboot = [System.Convert]::ToBoolean($env:reboot), [Parameter()] [int]$Timeout = 10 ) begin { # Replace parameters with dynamic script variables if ($env:nameOfAppToUninstall -and $env:nameOfAppToUninstall -notlike "null") { $Name = $env:nameOfAppToUninstall } if ($env:arguments -and $env:arguments -notlike "null") { $Arguments = $env:arguments } if ($env:timeoutInMinutes -and $env:timeoutInMinutes -notlike "null") { $Timeout = $env:timeoutInMinutes } # Check if application name is provided if (-not $Name) { Write-Host -Object "[Error] No name given, please enter in the name of an app to uninstall!" exit 1 } # Check if timeout is provided if (-not $Timeout) { Write-Host -Object "[Error] No timeout given!" Write-Host -Object "[Error] Please enter in a timeout that's greater than or equal to 1 minute or less than or equal to 60 minutes." exit 1 } # Validate the timeout is within the acceptable range if ($Timeout -lt 1 -or $Timeout -gt 60) { Write-Host -Object "[Error] An invalid timeout was given of $Timeout minutes." Write-Host -Object "[Error] Please enter in a timeout that's greater than or equal to 1 minute or less than or equal to 60 minutes." exit 1 } # Create a list to hold application names after splitting $AppNames = New-Object System.Collections.Generic.List[String] $Name -split ',' | ForEach-Object { $AppNames.Add($_.Trim()) } # Function to check if the script is run with elevated permissions function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } # Get all users registry hive locations function Get-UserHives { param ( [Parameter()] [ValidateSet('AzureAD', 'DomainAndLocal', 'All')] [String]$Type = "All", [Parameter()] [String[]]$ExcludedUsers, [Parameter()] [switch]$IncludeDefault ) # User account SID's follow a particular pattern depending on if they're Azure AD, a Domain account, or a local "workgroup" account. $Patterns = switch ($Type) { "AzureAD" { "S-1-12-1-(\d+-?){4}$" } "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" } "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" } } # We'll need the NTUSER.DAT file to load each user's registry hive. So we grab it if their account SID matches the above pattern. $UserProfiles = Foreach ($Pattern in $Patterns) { Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | Where-Object { $_.PSChildName -match $Pattern } | Select-Object @{Name = "SID"; Expression = { $_.PSChildName } }, @{Name = "UserName"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } }, @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } }, @{Name = "Path"; Expression = { $_.ProfileImagePath } } } # There are some situations where grabbing the .Default user's info is needed. switch ($IncludeDefault) { $True { $DefaultProfile = "" | Select-Object UserName, SID, UserHive, Path $DefaultProfile.UserName = "Default" $DefaultProfile.SID = "DefaultProfile" $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT" $DefaultProfile.Path = "C:\Users\Default" $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.UserName } } } $UserProfiles | Where-Object { $ExcludedUsers -notcontains $_.UserName } } # Function to find the uninstallation key of an application function Find-UninstallKey { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $True)] [String]$DisplayName, [Parameter()] [Switch]$UninstallString ) process { $UninstallList = New-Object System.Collections.Generic.List[Object] # Search for uninstall key in 32-bit registry location $Result = Get-ChildItem "Registry::HKEY_USERS\*\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -match "$([regex]::Escape($DisplayName))" } if ($Result) { $UninstallList.Add($Result) } # Search for uninstall key in 32-bit user locations $Result = Get-ChildItem HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | Where-Object { $_.DisplayName -match "$([regex]::Escape($DisplayName))" } if ($Result) { $UninstallList.Add($Result) } # Search for uninstall key in 64-bit registry location $Result = Get-ChildItem HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | Where-Object { $_.DisplayName -match "$([regex]::Escape($DisplayName))" } if ($Result) { $UninstallList.Add($Result) } # Search for uninstall key in 64-bit user locations $Result = Get-ChildItem "Registry::HKEY_USERS\*\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -match "$([regex]::Escape($DisplayName))" } if ($Result) { $UninstallList.Add($Result) } # Optionally return the DisplayName and UninstallString if ($UninstallString) { $UninstallList | ForEach-Object { $_ | Select-Object DisplayName, UninstallString -ErrorAction SilentlyContinue } } else { $UninstallList } } } # Initialize the exit code variable $ExitCode = 0 } process { # Check for administrative privileges if (-not (Test-IsElevated)) { Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." exit 1 } # Load unloaded profiles $UserProfiles = Get-UserHives -Type "All" $ProfileWasLoaded = New-Object System.Collections.Generic.List[string] # Loop through each profile on the machine. Foreach ($UserProfile in $UserProfiles) { # Load user's NTUSER.DAT if it's not already loaded. If ((Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) { Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($UserProfile.SID) `"$($UserProfile.UserHive)`"" -Wait -WindowStyle Hidden $ProfileWasLoaded.Add("$($UserProfile.SID)") } } # Retrieve similar applications based on names provided $SimilarAppsToName = $AppNames | ForEach-Object { Find-UninstallKey -DisplayName $_ -UninstallString } if (-not $SimilarAppsToName) { Write-Host "[Error] The requested app(s) was not found and none were found that are similar!" exit 1 } # Unload all hives that were loaded for this script. ForEach ($UserHive in $ProfileWasLoaded) { If ($ProfileWasLoaded -eq $false) { [gc]::Collect() Start-Sleep 1 Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($UserHive)" -Wait -WindowStyle Hidden | Out-Null } } # Create a list to store apps that are confirmed for uninstallation $AppsToUninstall = New-Object System.Collections.Generic.List[Object] $SimilarAppsToName | ForEach-Object { foreach ($AppName in $AppNames) { if ($AppName -eq $_.DisplayName) { # A matching app has been found $ExactMatch = $True if ($_.UninstallString) { # Uninstall string is available $UninstallStringFound = $True # Add app to uninstall list $AppsToUninstall.Add($_) } } } } # Check if any exact matches were found if (-not $ExactMatch) { Write-Host "[Error] Your requested apps were not found. Please see the below list and try again." $SimilarAppsToName | Format-Table DisplayName | Out-String | Write-Host exit 1 } # Check if uninstall strings were found for the apps if (-not $UninstallStringFound) { Write-Host "[Error] No uninstall string found for any of your requested apps!" exit 1 } # Check if there are apps without uninstall strings or not found at all $AppNames | ForEach-Object { if ($AppsToUninstall.DisplayName -notcontains $_) { Write-Host "[Error] Either the uninstall string was not present or the app itself was not found for one of your selected apps! See the below list of similar apps and try again." $SimilarAppsToName | Format-Table DisplayName | Out-String | Write-Host $ExitCode = 1 } } # Convert timeout from minutes to seconds $TimeoutInSeconds = $Timeout * 60 $StartTime = Get-Date # Process each app to uninstall $AppsToUninstall | ForEach-Object { $AdditionalArguments = New-Object System.Collections.Generic.List[String] # If the uninstall string contains msiexec that's what our executable will be. if($_.UninstallString -match "msiexec"){ $Executable = "msiexec.exe" } # If it contains a filepath we'll use that as our executable. if($_.UninstallString -notmatch "msiexec" -and $_.UninstallString -match '[a-zA-Z]:\\(?:[^\\\/:*?"<>|\r\n]+\\)*[^\\\/:*?"<>|\r\n]*'){ $Executable = $Matches[0] } # Confirm we have an executable. if(-not $Executable){ Write-Host -Object "[Error] Unable to find uninstall executable!" exit 1 } # Split uninstall string into executable and possible arguments $PossibleArguments = $_.UninstallString -split ' ' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match "^/"} # Decide executable and additional arguments based on uninstall string analysis $i = 0 foreach ($PossibleArgument in $PossibleArguments) { if (-not ($PossibleArgument -match "^/I{") -and $PossibleArgument) { $AdditionalArguments.Add($PossibleArgument) } if ($PossibleArgument -match "^/I{") { $AdditionalArguments.Add("$($PossibleArgument -replace '/I', '/X')") } $i++ } # Add custom arguments from the user if ($Arguments) { $Arguments.Split(',') | ForEach-Object { $AdditionalArguments.Add($_.Trim()) } } # Add the usual silent uninstall arguments if not present if($Executable -match "Msiexec"){ if($AdditionalArguments -notcontains "/qn"){ $AdditionalArguments.Add("/qn") } if($AdditionalArguments -notcontains "/norestart"){ $AdditionalArguments.Add("/norestart") } }elseif($Executable -match "\.exe"){ if($AdditionalArguments -notcontains "/S"){ $AdditionalArguments.Add("/S") } if($AdditionalArguments -notcontains "/norestart"){ $AdditionalArguments.Add("/norestart") } } # Verify that executable for uninstallation is found if (-not $Executable) { Write-Host "[Error] Could not find the executable from the uninstall string!" exit 1 } # Start the uninstallation process Write-Host -Object "Beginning uninstall of $($_.DisplayName) using $Executable $AdditionalArguments..." try{ if ($AdditionalArguments) { $Uninstall = Start-Process $Executable -ArgumentList $AdditionalArguments -NoNewWindow -PassThru } else { $Uninstall = Start-Process $Executable -NoNewWindow -PassThru } }catch{ Write-Host "[Error] $($_.Exception.Message)" return } # Calculate the remaining time for the uninstall process and enforce timeout $TimeElapsed = (Get-Date) - $StartTime $RemainingTime = $TimeoutInSeconds - $TimeElapsed.TotalSeconds # Wait for the uninstall process to complete within the remaining time try { $Uninstall | Wait-Process -Timeout $RemainingTime -ErrorAction Stop } catch { Write-Host -Object "[Alert] The uninstall process for $($_.DisplayName) has exceeded the specified timeout of $Timeout minutes." Write-Host -Object "[Alert] The script is now terminating." $Uninstall | Stop-Process -Force $ExitCode = 1 } # Check and report the exit code of the uninstallation process Write-Host -Object "Exit code for $($_.DisplayName): $($Uninstall.ExitCode)" if ($Uninstall.ExitCode -ne 0) { Write-Host -Object "[Error] Exit code does not indicate success!" $ExitCode = 1 } } # Pause for 30 seconds before final checks Start-Sleep -Seconds 30 $UserProfiles = Get-UserHives -Type "All" $ProfileWasLoaded = New-Object System.Collections.Generic.List[string] # Loop through each profile on the machine. Foreach ($UserProfile in $UserProfiles) { # Load user's NTUSER.DAT if it's not already loaded. If ((Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) { Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($UserProfile.SID) `"$($UserProfile.UserHive)`"" -Wait -WindowStyle Hidden $ProfileWasLoaded.Add("$($UserProfile.SID)") } } # Re-check for any remaining apps to confirm they were uninstalled $SimilarAppsToName = $AppNames | ForEach-Object { Find-UninstallKey -DisplayName $_ } $SimilarAppsToName | ForEach-Object { foreach ($AppName in $AppNames) { if ($_.DisplayName -eq $AppName) { Write-Host -Object "[Error] Failed to uninstall $($_.DisplayName)." $UninstallFailure = $True $ExitCode = 1 } } } # Unload all hives that were loaded for this script. ForEach ($UserHive in $ProfileWasLoaded) { If ($ProfileWasLoaded -eq $false) { [gc]::Collect() Start-Sleep 1 Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($UserHive)" -Wait -WindowStyle Hidden | Out-Null } } # Confirm successful uninstallation if no failures detected if (-not $UninstallFailure) { Write-Host "Successfully uninstalled your requested apps!" } # Handle reboot if requested and there were no uninstall failures if ($Reboot -and -not $UninstallFailure) { Write-Host -Object "[Alert] a reboot was requested. Scheduling restart for 60 seconds from now..." Start-Process shutdown.exe -ArgumentList "/r /t 60" -Wait -NoNewWindow } # Exit script with the final exit code exit $ExitCode } end { }
Wie das Skript funktioniert
Dieses Skript wurde entwickelt, um Anwendungen auf der Grundlage ihrer Namen zu deinstallieren und behandelt eine Vielzahl von Szenarien, die während des Deinstallationsprozesses auftreten können. Im Folgenden wird Schritt für Schritt beschrieben, wie das Skript funktioniert:
1. Parameterdefinition und Eingabebehandlung:
- Das Skript beginnt mit der Definition mehrerer Parameter, z. B. -Name für den Anwendungsnamen, -Argumente für zusätzliche Deinstallationsargumente, -Reboot, um einen Neustart zu planen, und -Timeout, um anzugeben, wie lange das Skript auf den Deinstallationsprozess warten soll.
- Das Skript prüft dann, ob diese Parameter vorhanden sind, und validiert sie, indem es sicherstellt, dass sie bestimmte Kriterien erfüllen, z. B. dass der Timeout-Wert zwischen 1 und 60 Minuten liegt.
2. Profil- und Benutzer:in-Hive-Behandlung:
- Es enthält eine Funktion zum Laden und Verwalten von Benutzerprofilen und Registrierungs-Hives, die für den Zugriff auf die Deinstallationsstrings für jede Anwendung erforderlich sind. Dies ist von entscheidender Bedeutung, da die Deinstallation einer Anwendung häufig den Zugriff auf die Registrierung erfordert, um den richtigen Deinstallationspfad zu finden.
3. Identifizierung von Anwendungen und Abrufen von Deinstallationsschlüsseln:
- Das Skript verwendet eine Funktion namens Find-UninstallKey, um die Registrierung zu durchsuchen und den Deinstallationsschlüssel zu finden, der dem angegebenen Anwendungsnamen zugeordnet ist. Dies geschieht an verschiedenen Stellen der Registrierung, sowohl in der 32-Bit- als auch in der 64-Bit-Version, um eine umfassende Abdeckung zu gewährleisten.
4. Deinstallationsprozess:
- Sobald der Deinstallationsstring identifiziert ist, verarbeitet das Skript ihn, um den ausführbaren Pfad und die für die Deinstallation erforderlichen Argumente zu extrahieren.
- Es stellt sicher, dass notwendige Argumente für eine im Hintergrund laufende Deinstallation wie /qn, /norestart oder /S hinzugefügt werden, wenn sie nicht bereits vorhanden sind. Dadurch wird sichergestellt, dass die Deinstallation ohne Benutzerinteraktion und ohne Durchsetzung eines Systemneustarts erfolgt, sofern nicht anders angegeben.
5. Fehlerbehandlung und finale Checks:
- Das Skript enthält eine solide Fehlerbehandlungslösung, die sicherstellt, dass der Administrator informiert wird, wenn während der Deinstallation etwas schief läuft, und das Skript mit einem entsprechenden Fehlercode beendet wird.
- Nach der Deinstallation prüft das Skript erneut, ob die Anwendung tatsächlich entfernt wurde, und stellt sicher, dass keine Restkomponenten zurückbleiben.
Anwendung in der Praxis: Eine Fallstudie
Stellen Sie sich ein Szenario vor, in dem ein IT-Administrator eine veraltete Version von VLC Media Player von 200 Computern in einem Unternehmen deinstallieren muss. Dies manuell zu tun, wäre unglaublich zeitaufwändig. Stattdessen könnte der Administrator dieses PowerShell-Skript auf allen Computern bereitstellen und dabei „VLC Media Player“ als Anwendungsname angeben.
Das Skript würde automatisch den passenden Deinstallationsstring finden, die Deinstallation im Hintergrund ausführen und sogar einen Neustart planen, falls erforderlich. Der gesamte Prozess, der Tage hätte dauern können, ist innerhalb von Minuten abgeschlossen.
Vergleich mit anderen Methoden
In der Regel verwenden Administratoren die Systemsteuerung oder ein Deinstallationstool eines Drittanbieters, um Anwendungen zu entfernen. Diese Methoden erfordern jedoch manuelle Eingriffe, sind nicht skalierbar und lassen oft die Flexibilität vermissen, benutzerdefinierte Argumente hinzuzufügen oder im Hintergrund laufende Deinstallationen effektiv zu behandeln.
Dieses Skript hingegen bietet einen hochgradig automatisierten und anpassbaren Ansatz, der es IT-Experten ermöglicht, Deinstallationen auf mehreren Rechnern effizient und mit minimalem manuellen Aufwand zu verwalten.
Häufig gestellte Fragen
- Was geschieht, wenn der Name der Anwendung nicht gefunden wird? Wenn das Skript keine Anwendung mit dem angegebenen Namen findet, sucht es nach ähnlichen Namen und gibt eine Liste möglicher Übereinstimmungen aus. So kann der Administrator die Eingabe anpassen und es erneut versuchen.
- Kann das Skript mehrere Anwendungen auf einmal verarbeiten? Ja, der Parameter -Name kann eine durch Kommata getrennte Liste von Anwendungen akzeptieren, sodass das Skript mehrere Anwendungen auf einmal deinstallieren kann.
- Was passiert, wenn der Deinstallationsprozess das Timeout überschreitet? Das Skript bricht den Deinstallationsprozess ab und beendet ihn mit einem Fehlercode, um sicherzustellen, dass kein Prozess unendlich lange läuft.
Auswirkungen auf IT-Sicherheit und Management
Die Automatisierung des Deinstallationsprozesses mit Skripten wie diesem hat erhebliche Auswirkungen auf die IT-Sicherheit und das Management. Indem sie sicherstellen, dass veraltete oder anfällige Software auf allen Rechnern schnell und konsequent entfernt wird, können Unternehmen das Risiko von Sicherheitsverletzungen verringern. Darüber hinaus werden durch die Automatisierung menschliche Fehler reduziert, sodass keine Anwendung aufgrund eines Versehens deinstalliert wird.
Best Practices für die Verwendung dieses Skripts
- Testen Sie immer in einer kontrollierten Umgebung: Bevor Sie das Skript auf mehreren Rechnern bereitstellen, testen Sie es in einer kontrollierten Umgebung, um sicherzustellen, dass es wie erwartet funktioniert.
- Führen Sie Protokolle: Führen Sie Protokolle der Deinstallationsvorgänge zu Audit-Zwecken und zur Fehlersuche.
- Führen Sie vor der Deinstallation Backups durch: Stellen Sie sicher, dass wichtige Daten vor der Ausführung von Deinstallationsskripten gesichert werden, insbesondere bei Anwendungen, die Daten lokal speichern könnten.
- Aktualisieren Sie das Skript regelmäßig: Wenn neue Anwendungen und Updates herauskommen, aktualisieren Sie das Skript, um neue Deinstallationsstrings und -methoden zu behandeln.
Abschließende Überlegungen
PowerShell-Skripte wie das hier beschriebene sind unverzichtbare Tools für IT-Experten, die große Netzwerke verwalten. Durch die Automatisierung der Deinstallation von Anwendungen sparen sie Zeit, reduzieren Fehler und erhöhen die Sicherheit. NinjaOne kann diese Skripte mit seinen kompletten IT-Management-Tools ergänzen und zusätzliche Automatisierungs-, Überwachungs- und Steuerungsebenen bieten. Dadurch wird sichergestellt, dass Ihre IT-Infrastruktur stabil, sicher und auf dem neuesten Stand bleibt.