Maintaining a clean and accurate record of installed software is critical for endpoint visibility, compliance audits, and system integrity. Yet, it’s not uncommon for organizations to encounter “orphaned” antivirus entries in the Windows Security Center—entries that persist even after the antivirus has been removed. These misleading records can cause confusion, false reporting, and unnecessary remediation efforts. This is where PowerShell scripting steps in as a powerful tool to automate and streamline the cleanup process.
In this article, we explore how IT professionals can use PowerShell scripting to remove orphaned entries from the Windows Security Center. The featured script ensures that only truly uninstalled antivirus solutions are purged, reducing manual effort and risk of human error.
Background
Antivirus solutions often register themselves in the Windows Security Center for visibility and integration with system health monitoring tools. However, uninstallation processes don’t always cleanly remove these records. As a result, Managed Service Providers (MSPs) and IT administrators may encounter stale entries indicating that outdated antivirus software is still protecting a system.
These orphaned entries can interfere with compliance reports or trigger false positives in Remote Monitoring and Management (RMM) platforms like NinjaOne. By automating the validation and cleanup of such entries, IT teams can keep their environments accurate and efficient.
The Script
#Requires -Version 5.1 <# .SYNOPSIS Remove a specified Security Center entry if the antivirus is not installed. You can get the antivirus name/Security Center entry in Ninja by navigating to Details > Antivirus. .DESCRIPTION Remove a specified Security Center entry if the antivirus is not installed. You can get the antivirus name/Security Center entry in Ninja by navigating to Details > Antivirus. By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use. Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA). .EXAMPLE -AntivirusName "VIPRE Business Agent" Checking Add and Remove Programs for 'VIPRE Business Agent'. Verifying that 'VIPRE Business Agent' does not exist at path 'C:\Program Files\VIPRE Business Agent\SBAMWSC.EXE'. Verifying that 'VIPRE Business Agent' does not exist at path 'C:\Program Files\VIPRE Business Agent\ViprePPLSvc.exe'. Removing 'VIPRE Business Agent' from the Security Center. Successfully removed 'VIPRE Business Agent' from the Security Center. PARAMETER: -AntivirusName "ReplaceMeWithTheNameOfTheAntivirusToRemove" Specify the name of the Security Center entry you would like to remove. .NOTES Minimum OS Architecture Supported: Windows 10, Windows Server 2016 Release Notes: Initial Release #> [CmdletBinding()] param ( [Parameter()] [String]$AntivirusName ) begin { # If script form variables are used, replace the command line parameters with their value. if ($env:antivirusName -and $env:antivirusName -notlike "null") { $AntivirusName = $env:antivirusName } function Test-IsServer { # Determine the method to retrieve the operating system information based on PowerShell version try { $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop } else { Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop } } catch { Write-Host -Object "[Error] Unable to validate whether or not this device is a server." Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } # Check if the ProductType is "2" or "3", which indicates that the system is a server if ($OS.ProductType -eq "2" -or $OS.ProductType -eq "3") { return $true } } # If the script is run on a server, display an error message and exit if (Test-IsServer) { Write-Host -Object "[Error] The Windows Security Center is not present on Windows Server." exit 1 } # If $AntivirusName exists, trim any leading or trailing whitespace. if ($AntivirusName) { $AntivirusName = $AntivirusName.Trim() } # If $AntivirusName is still not set or empty after the previous checks, display an error and exit the script. if (!$AntivirusName) { Write-Host -Object "[Error] Please provide a valid antivirus name." exit 1 } # Try to retrieve the Security Center entries for antivirus products try { $SecurityCenterEntries = Get-WmiObject -Namespace "root\SecurityCenter2" -ClassName "AntiVirusProduct" -ErrorAction Stop } catch { # If there is an error retrieving the Security Center entries, output an error message and exit the script. Write-Host -Object "[Error] Failed to retrieve any antivirus entries from the Security Center." Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } # Check if no Security Center entries were found or if the number of entries is less than 1, then output an error and exit. if (!$SecurityCenterEntries -or ($SecurityCenterEntries.displayName | Measure-Object | Select-Object -ExpandProperty Count) -lt 1) { Write-Host -Object "[Error] No antivirus entries found in the Security Center." exit 1 } # Check if the antivirus name provided does not exist in the Security Center entries. if ($SecurityCenterEntries.displayName -notcontains $AntivirusName) { Write-Host -Object "[Error] An invalid antivirus name was specified. Please specify one of the following valid antivirus names: " -NoNewline Write-Host -Object $SecurityCenterEntries.displayName -Separator ", " exit 1 } function Find-InstallKey { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $True)] [String]$DisplayName, [Parameter()] [Switch]$UninstallString, [Parameter()] [String]$UserBaseKey ) process { # Initialize a list to store found installation keys $InstallList = New-Object System.Collections.Generic.List[Object] # If no custom user base key is provided, search in the standard HKLM paths if (!$UserBaseKey) { # Search in the 32-bit uninstall registry key and add results to the list $Result = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } if ($Result) { $InstallList.Add($Result) } # Search in the 64-bit uninstall registry key and add results to the list $Result = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } if ($Result) { $InstallList.Add($Result) } } else { # If a custom user base key is provided, search in the corresponding Wow6432Node path and add results to the list $Result = Get-ChildItem -Path "$UserBaseKey\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } if ($Result) { $InstallList.Add($Result) } # Search in the custom user base key for the standard uninstall path and add results to the list $Result = Get-ChildItem -Path "$UserBaseKey\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Get-ItemProperty | Where-Object { $_.DisplayName -like "*$DisplayName*" } if ($Result) { $InstallList.Add($Result) } } # If the UninstallString switch is set, return only the UninstallString property of the found keys if ($UninstallString) { $InstallList | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue } else { $InstallList } } } function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } if (!$ExitCode) { $ExitCode = 0 } } process { # Check if the script is being run with elevated (administrator) privileges. if (!(Test-IsElevated)) { Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." exit 1 } # Inform the user that the script is checking Add and Remove Programs for the specified antivirus. Write-Host -Object "Checking Add and Remove Programs for '$AntivirusName'." # Call the Find-InstallKey function to check if the antivirus is installed by its display name. $IsInstalled = Find-InstallKey -DisplayName $AntivirusName # If the antivirus is found to be installed, display an error and exit the script. if ($IsInstalled) { Write-Host -Object "[Error] '$AntivirusName' is currently installed. Unable to remove the entry from the Security Center." exit 1 } # Retrieve the Security Center entries that match the specified antivirus name. $EntryToRemove = $SecurityCenterEntries | Where-Object { $_.displayName -like $AntivirusName } # Loop through the matched Security Center entries to process each one. $EntryToRemove | ForEach-Object { # Retrieve the paths for the signed product executable and reporting executable. $SignedExe = $_.pathToSignedProductExe $SignedReportingExe = $_.pathToSignedReportingExe if ($SignedExe) { Write-Host -Object "Verifying that '$AntivirusName' does not exist at path '$SignedExe'." } # If the signed product executable path contains environment variables (denoted by '%' signs), attempt to expand them to their full path using the Environment class. if ($SignedExe -and $SignedExe -match '%.*%') { try { $ErrorActionPreference = "Stop" $SignedExe = [Environment]::ExpandEnvironmentVariables($SignedExe) $ErrorActionPreference = "Continue" } catch { Write-Host -Object "[Error] Failed to expand environment variable in '$($_.pathToSignedProductExe)'." Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } } # If the path for the signed reporting executable exists, print a message indicating the verification of its presence. if ($SignedReportingExe) { Write-Host -Object "Verifying that '$AntivirusName' does not exist at path '$SignedReportingExe'." } # If the signed reporting executable path contains environment variables, attempt to expand them. if ($SignedReportingExe -and $SignedReportingExe -match '%.*%') { try { $ErrorActionPreference = "Stop" $SignedReportingExe = [Environment]::ExpandEnvironmentVariables($SignedReportingExe) $ErrorActionPreference = "Continue" } catch { Write-Host -Object "[Error] Failed to expand environment variable in '$($_.pathToSignedReportingExe)'." Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } } # If the signed product executable still exists at the expanded path, output an error and exit the script. if ($SignedExe -and (Test-Path -Path $SignedExe -ErrorAction SilentlyContinue)) { Write-Host -Object "[Error] '$AntivirusName' is currently installed at '$SignedExe'. Unable to remove the entry." exit 1 } # If the signed reporting executable still exists at the expanded path, output an error and exit the script. if ($SignedReportingExe -and (Test-Path -Path $SignedReportingExe -ErrorAction SilentlyContinue)) { Write-Host -Object "[Error] '$AntivirusName' is currently installed at '$SignedReportingExe'. Unable to remove the entry." exit 1 } } # After verifying that the antivirus is not installed at the specified paths, proceed to remove the entries from the Security Center. Write-Host -Object "Removing '$AntivirusName' from the Security Center" # Loop through each entry in $EntryToRemove and attempt to delete it. $EntryToRemove | ForEach-Object { try { $ErrorActionPreference = "Stop" $_.Delete() $ErrorActionPreference = "Continue" Write-Host -Object "Successfully removed '$AntivirusName' from the Security Center." } catch { Write-Host -Object "[Error] Failed to remove '$AntivirusName' from the Security Center." Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } } exit $ExitCode } end { }
Detailed Breakdown
This script is purpose-built to remove specific orphaned antivirus entries from the Windows Security Center—but only after thoroughly validating that the software is no longer installed.
Here’s a detailed walkthrough of its operation:
Parameter Validation and Server Check
The script begins by capturing the antivirus name from either the command-line input or environment variables. It halts immediately if executed on a Windows Server, since the Security Center doesn’t exist in those environments.
Retrieve Existing Entries
It queries the root\SecurityCenter2 WMI namespace to retrieve all currently registered antivirus products.
Antivirus Verification
The script verifies that the provided antivirus name exists in the Security Center list. If not, it displays a list of valid options to guide the user.
Installation Check
Before removal, it searches registry uninstall keys to ensure the antivirus isn’t currently installed. It then checks for any remaining executables at the reported installation paths.
Entry Deletion
Once all checks confirm the software is truly gone, it proceeds to delete the orphaned Security Center entry using the .Delete() method on the WMI object.
Potential Use Cases
Scenario: Remote Cleanup for a Decommissioned AV Tool
An MSP rolls out a new endpoint protection solution but finds remnants of the previous product—VIPRE Business Agent—still listed in Windows Security Center on many machines. Although uninstalled, the entries persist, misleading compliance reports.
Using this PowerShell script via NinjaOne’s scripting module, the MSP automates the cleanup across hundreds of endpoints. Within minutes, only valid antivirus entries remain, improving dashboard accuracy and audit readiness.
Comparisons
Manual Cleanup
Manually removing these entries involves multiple registry edits and WMI calls—time-consuming and error-prone, especially across many systems.
Using Third-Party Tools
Some third-party utilities offer orphaned entry cleanup but lack granular verification or require elevated user interaction. This script offers a lightweight, verifiable, and automated alternative tailored for managed environments.
RMM Policy Triggers
While some RMMs can flag inactive AV tools, they typically don’t offer cleanup capabilities. This script complements such alerts by offering automated remediation.
Frequently Asked Questions
Q: What happens if I run this on a server?
The script exits early, noting that Windows Security Center isn’t available on server OSes.
Q: Can I run this without admin privileges?
No, the script requires elevated rights to access WMI and registry paths for cleanup.
Q: What if the antivirus is still partially installed?
The script checks for both registry keys and known executable paths to ensure it’s fully uninstalled before removing the entry.
Q: Can this script be integrated with NinjaOne?
Yes, you can deploy it as a custom script via NinjaOne and even feed in the antivirus name dynamically via environment variables.
Implications
Accurately reflecting system health is paramount in modern IT management. Leaving behind orphaned antivirus entries introduces noise, skews data analytics, and can falsely indicate protection status to end-users and administrators alike. By using PowerShell to enforce hygiene in the Security Center, IT teams bolster the integrity of their monitoring systems and reduce the attack surface caused by configuration drift.
Recommendations
- Test in a Lab: Validate the script on a test endpoint to ensure compatibility with your antivirus solution.
- Use Descriptive Logging: Modify the script for more granular logs if deploying across many endpoints.
- Leverage RMM Integration: Pair this script with NinjaOne’s scheduling and alerting to ensure continuous compliance.
- Document Entries: Keep a reference list of expected antivirus entries to prevent accidental removal of legitimate entries.
Final Thoughts
Using PowerShell scripting to remove orphaned entries is a smart, scalable way for IT professionals to ensure accurate endpoint reporting. In managed environments, the ability to automate such tasks is not just a convenience—it’s a necessity.
With NinjaOne, scripts like this can be deployed organization-wide in seconds, monitored for success, and integrated into larger compliance and remediation workflows. Whether you’re maintaining dozens or thousands of endpoints, clean data begins with precise tools—and this script is one of them.