How to Check Which Antivirus is Installed in Windows Using PowerShell

Ensuring that antivirus software is properly installed, up-to-date, and actively running is critical for maintaining a secure IT environment. IT professionals and Managed Service Providers (MSPs) often need reliable tools to verify the status of antivirus solutions across multiple systems. This PowerShell script provides a powerful, automated solution for detecting antivirus software, checking its definitions and status, and exporting results to custom fields for further analysis.

Background

In today’s digital landscape, the prevalence of cyber threats necessitates robust antivirus protection. IT departments and MSPs must constantly monitor and verify the status of antivirus solutions to ensure they are providing adequate protection. This script addresses a common challenge: efficiently determining which antivirus software is installed on a system, its version, the age of its definitions, and whether it is currently running. By automating these checks, the script saves valuable time and reduces the risk of human error.

The Script

#Requires -Version 4.0

<#
.SYNOPSIS
    This script will attempt to detect the installed antivirus (currently supports 11) and get the age of its definitions, version, and whether or not the antivirus is currently running. It can export the results to one or more custom fields. This script is a best effort and should be treated as such; we do recommend verifying any results.
.DESCRIPTION
    This script will attempt to detect the installed antivirus (currently supports 11) and get the age of its definitions, version, and whether or not the antivirus is currently running. 
    It can export the results to one or more custom fields. 
    This script is a best effort and should be treated as such; we do recommend verifying any results.
    
    AV List: BitDefender,Carbon Black,Crowdstrike,Cylance,ESET,Huntress,MalwareBytes,MDE,SentinelOne,Sophos,Vipre,Webroot

.EXAMPLE
    (No Parameters)
    Desktop Windows Detected, Switching to WMI Method....

    [Alert] The AV definitions are out of date!

    Name             Installed Definitions UpToDate Running Service  Version    
    ----             --------- ----------- -------- ------- -------  -------    
    Sentinel Agent   Yes       2023/05/10      True Yes     Active   21.7.1219  
    Windows Defender Yes       2023/03/31     False No      Inactive 4.18.2302.7

PARAMETER: -ExcludeAV "BitDefender"
    A comma separated list of AVs to exclude.
.EXAMPLE
    -ExcludeAV "BitDefender"

    Name        Installed Version   Definitions UpToDate CurrentlyRunning HasRunningService
    ----        --------- -------   ----------- -------- ---------------- -----------------
    SentinelOne Yes       21.7.1219 2023/04/27      True Yes              Yes

PARAMETER: -ExclusionsFromCustomField "ReplaceWithTextCustomField"
    The name of a text custom field that contains your desired ExcludeAV comma separated list.
    ex. "ExcludedAVs" where you have entered in your desired ExcludeAV list in the "ExcludedAVs" custom field rather than in a parameter.
.EXAMPLE
    -ExclusionsFromCustomField "ExcludeAVs"

    Name        Installed Version   Definitions UpToDate CurrentlyRunning HasRunningService
    ----        --------- -------   ----------- -------- ---------------- -----------------
    SentinelOne Yes       21.7.1219 2023/04/27      True Yes              Yes

PARAMETER: -OutOfDate "7"
    Script will consider the AV to be out of date if the definitions are older than x days.
.EXAMPLE
    -OutOfDate "1"

    Desktop Windows Detected, Switching to WMI Method....

    [Alert] The AV definitions are out of date!

    Name             Installed Definitions UpToDate Running Service  Version    
    ----             --------- ----------- -------- ------- -------  -------    
    Sentinel Agent   Yes       2023/05/10     False Yes     Active   21.7.1219  
    Windows Defender Yes       2023/03/31     False No      Inactive 4.18.2302.7


PARAMETER: -ShowNotFound
    Script will show AV's it checked for but didn't find.
.EXAMPLE
    -ShowNotFound

    Name             Installed Definitions UpToDate Running Service  Version    
    ----             --------- ----------- -------- ------- -------  -------    
    BitDefender      No                       False No      Inactive     
    CarbonBlack      No                       False No      Inactive 
    ...

PARAMETER: -ExportAll "ReplaceWithNameOfAMultiLineCustomField"
    The name of a multiline customfield you'd like to export the resulting table into.
.EXAMPLE
    -ExportAll "ReplaceWithNameOfAMultiLineCustomField"

    Name        Installed Version   Definitions UpToDate CurrentlyRunning HasRunningService
    ----        --------- -------   ----------- -------- ---------------- -----------------
    SentinelOne Yes       21.7.1219 2023/04/27      True Yes              Yes

PARAMETER: -ExportDef "ReplaceWithNameOfAMultiLineCustomField"
    The name of a multiline customfield you'd like to export the definitions column into.

PARAMETER: -ExportDefStatus "ReplaceWithNameOfAMultiLineCustomField"
    The name of a multiline customfield you'd like to export the UpToDate column into.

PARAMETER: -ExportName "ReplaceWithNameOfAMultiLineCustomField"
    The name of a multiline customfield you'd like to export the Name column into.

PARAMETER: -ExportStatus "ReplaceWithNameOfAMultiLineCustomField"
    The name of a multiline customfield you'd like to export the Running column into.

PARAMETER: -ExportVersion "ReplaceWithNameOfAMultiLineCustomField"
    The name of a multiline customfield you'd like to export the Version column into.
.EXAMPLE
    ExportOptions: -ExportAll, -ExportDef, -ExportDefStatus (Whether or not definitions are up to date), 
    -ExportName, -ExportStatus (Whether or not its running), -ExportVersion

    -ExportAll "ReplaceWithNameOfAMultiLineCustomField" -DateFormat "yyyy/MM/dd"

    [Alert] The AV definitions are out of date!

    Name   Installed Definitions UpToDate Running Service Version                  
    ----   --------- ----------- -------- ------- ------- -------                  
    MDE    Yes       2023/03/02     False Yes     Active  4.18.2303.8              
    Sophos Yes       2023/04/26      True Yes     Active  {2022.4.3.1 Legacy,      
                                                      2.4.274.0}               

.OUTPUTS
    None
.NOTES
    Minimum OS Architecture Supported: Windows 10, Server 2012 R2
    Release Notes: Initial Release
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).
#>

[CmdletBinding()]
param (
    [Parameter()]
    [String]$ExcludeAV,
    [Parameter()]
    [String]$ExclusionsFromCustomField,
    [Parameter()]
    [String]$ExportAll,
    [Parameter()]
    [String]$ExportDef,
    [Parameter()]
    [String]$ExportDefStatus,
    [Parameter()]
    [String]$ExportName,
    [Parameter()]
    [String]$ExportStatus,
    [Parameter()]
    [String]$ExportVersion,
    [Parameter()]
    [String]$OutOfDate = "7",
    [Parameter()]
    [Switch]$ShowNotFound = [System.Convert]::ToBoolean($env:showNotFound)
)
begin {
    Write-Host "Supported AVs: BitDefender, Carbon Black, Crowdstrike, Cylance, ESET, Huntress, MalwareBytes, Windows Defender, SentinelOne, Sophos, Vipre and Webroot."

    # Grabbing the script variables
    if ($env:definitionsAgeLimitInDays -and $env:definitionsAgeLimitInDays -notlike "null") { $OutOfDate = $env:definitionsAgeLimitInDays }
    if ($env:excludeAntivirusProduct -and $env:excludeAntivirusProduct -notlike "null") { $ExcludeAV = $env:excludeAntivirusProduct }
    if ($env:retrieveExclusionFromCustomField -and $env:retrieveExclusionFromCustomField -notlike "null") { $ExclusionsFromCustomField = $env:retrieveExclusionFromCustomField }
    if ($env:allResultsCustomFieldName -and $env:allResultsCustomFieldName -notlike "null") { $ExportAll = $env:allResultsCustomFieldName }
    if ($env:definitionsDateCustomFieldName -and $env:definitionsDateCustomFieldName -notlike "null") { $ExportDef = $env:definitionsDateCustomFieldName }
    if ($env:definitionStatusCustomFieldName -and $env:definitionStatusCustomFieldName -notlike "null" ) { $ExportDefStatus = $env:definitionStatusCustomFieldName }
    if ($env:statusCustomFieldName -and $env:statusCustomFieldName -notlike "null") { $ExportStatus = $env:statusCustomFieldName }
    if ($env:antivirusNameCustomFieldName -and $env:antivirusNameCustomFieldName -notlike "null") { $ExportName = $env:antivirusNameCustomFieldName }
    if ($env:statusCustomFieldName -and $env:statusCustomFieldName -notlike "null") { $ExportStatus = $env:statusCustomFieldName }
    if ($env:antivirusVersionCustomFieldName -and $env:antivirusVersionCustomFieldName -notlike "null") { $ExportVersion = $env:antivirusVersionCustomFieldName }

    # This script should run with administrator or system permissions. 
    # Technically it'll work without these permissions, however some directories would be inaccessible which could lead to false negatives.
    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 (!(Test-IsElevated)) {
        Write-Host "[Error] Access Denied. Please run with Administrator privileges."
        exit 1
    }

    function Test-IsWorkstation {
        $OS = if ($PSVersionTable.PSVersion.Major -ge 5) {
            Get-CimInstance -Class Win32_OperatingSystem
        }
        else {
            Get-WmiObject -Class Win32_OperatingSystem
        }

        if ($OS.ProductType -eq "1") {
            return $True
        }
    }

    # This will go through the uninstall registry keys and look for the AV. On occasion, we don't want all the information so we have switch options for those cases.
    function Find-UninstallKey {
        [CmdletBinding()]
        param (
            [Parameter(ValueFromPipeline)]
            [String]$DisplayName,
            [Parameter()]
            [Switch]$Version,
            [Parameter()]
            [Switch]$UninstallString,
            [Parameter()]
            [Switch]$InstallPath
        )
        process {
            $UninstallList = New-Object System.Collections.Generic.List[Object]

            $Result = Get-ChildItem HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | 
                Where-Object { $_.DisplayName -like "*$DisplayName*" }

            if ($Result) { $UninstallList.Add($Result) }

            $Result = Get-ChildItem HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Get-ItemProperty | 
                Where-Object { $_.DisplayName -like "*$DisplayName*" }

            if ($Result) { $UninstallList.Add($Result) }

            # Programs don't always have an uninstall string listed here, so to account for that, I made this optional.
            if ($UninstallString) {
                $UninstallList | ForEach-Object { $_ | Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue }
            }

            if ($Version) {
                $UninstallList | ForEach-Object { ($_ | Select-Object -ExpandProperty DisplayVersion -ErrorAction SilentlyContinue) -replace '[^\u0020-\u007E\u00A0-\u00FF]', '' }
            }

            if ($InstallPath) {
                $UninstallList | ForEach-Object { $_ | Select-Object -ExpandProperty InstallLocation -ErrorAction SilentlyContinue }
            }

            if (!$Version -and !$UninstallString -and !$InstallPath) {
                $UninstallList
            }
        }
    }

    # This will find the last write time for a particular file. I made it a function in case I wanted to do something similar as the Uninstall-Key function.
    function Find-Definitions {
        [CmdletBinding()]
        param(
            [Parameter(ValueFromPipeline)]
            [String]$Path
        )
        process {
            Get-Item $Path -ErrorAction SilentlyContinue | Sort-Object LastWriteTime | Select-Object LastWriteTime -Last 1 | Get-Date
        }
    }

    # This will search the typical directories programs are installed in.
    function Find-Executable {
        [CmdletBinding()]
        param(
            [Parameter(ValueFromPipeline)]
            [String]$Path,
            [Parameter()]
            [Switch]$Special
        )
        process {
            if (!$Special) {
                if (Test-Path "$env:ProgramFiles\$Path") {
                    "$env:ProgramFiles\$Path"
                }
        
                if (Test-Path "${Env:ProgramFiles(x86)}\$Path") {
                    "${Env:ProgramFiles(x86)}\$Path"
                }
    
                if (Test-Path "$env:ProgramData\$Path") {
                    "$env:ProgramData\$Path"
                }
            }
            else {
                if (Test-Path $Path) {
                    $Path
                }
            }
        }
    }

    # This will check the running processes for our AV.
    function Find-Process {
        [CmdletBinding()]
        param(
            [Parameter(ValueFromPipeline)]
            [String]$Name
        )
        process {
            Get-Process | Where-Object { $_.ProcessName -like "*$Name*" } | Select-Object -ExpandProperty Name
        }
    }

    # This was moved outside the function so I don't overload WMI.
    $ServiceList = if ($PSVersionTable.PSVersion.Major -ge 5) {
        Get-CimInstance win32_service
    }
    else {
        Get-WmiObject win32_service
    }
    
    # Looks for a service based on the executable.
    function Find-Service {
        [CmdletBinding()]
        param(
            [Parameter(ValueFromPipeline)]
            [String]$Name
        )
        process {
            # Get-Service will display an error everytime it has an issue reading a service. Ignoring them as they're not relevant.
            $ServiceList | Where-Object { $_.State -notlike "Disabled" -and $_.State -notlike "Stopped" } | 
                Where-Object { $_.PathName -Like "*$Name.exe*" }
        }
    }

    function Set-NinjaProperty {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory = $True)]
            [String]$Name,
            [Parameter()]
            [String]$Type,
            [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
            $Value,
            [Parameter()]
            [String]$DocumentName
        )
    
        $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters
        if ($Characters -ge 10000) {
            throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.")
        }
        
        # If we're requested to set the field value for a Ninja document we'll specify it here.
        $DocumentationParams = @{}
        if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName }
        
        # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed.
        $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG"
        if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" }
        
        # The field below requires additional information to be set
        $NeedsOptions = "Dropdown"
        if ($DocumentName) {
            if ($NeedsOptions -contains $Type) {
                # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong.
                $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1
            }
        }
        else {
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1
            }
        }
        
        # If an error is received it will have an exception property, the function will exit with that error information.
        if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions }
        
        # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports.
        switch ($Type) {
            "Checkbox" {
                # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry.
                $NinjaValue = [System.Convert]::ToBoolean($Value)
            }
            "Date or Date Time" {
                # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID.
                $Date = (Get-Date $Value).ToUniversalTime()
                $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date
                $NinjaValue = $TimeSpan.TotalSeconds
            }
            "Dropdown" {
                # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid.
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID
        
                if (-not $Selection) {
                    throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown")
                }
        
                $NinjaValue = $Selection
            }
            default {
                # All the other types shouldn't require additional work on the input.
                $NinjaValue = $Value
            }
        }
        
        # We'll need to set the field differently depending on if its a field in a Ninja Document or not.
        if ($DocumentName) {
            $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1
        }
        else {
            $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1
        }
        
        if ($CustomField.Exception) {
            throw $CustomField
        }
    }

    function Get-NinjaProperty {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
            [String]$Name,
            [Parameter()]
            [String]$Type,
            [Parameter()]
            [String]$DocumentName
        )
    
        # If we're requested to get the field value from a Ninja document we'll specify it here.
        $DocumentationParams = @{}
        if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName }
    
        # These two types require more information to parse.
        $NeedsOptions = "DropDown", "MultiSelect"
    
        # Grabbing document values requires a slightly different command.
        if ($DocumentName) {
            # Secure fields are only readable when they're a device custom field
            if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality") }
    
            # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong.
            Write-Host "Retrieving value from Ninja Document..."
            $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1
    
            # Certain fields require more information to parse.
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1
            }
        }
        else {
            # We'll redirect error output to the success stream to make it easier to error out if nothing was found or something else went wrong.
            $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1
    
            # Certain fields require more information to parse.
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1
            }
        }
    
        # If we received some sort of error it should have an exception property and we'll exit the function with that error information.
        if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue }
        if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions }
    
        if (-not $NinjaPropertyValue) {
            throw [System.NullReferenceException]::New("The Custom Field '$Name' is empty!")
        }
    
        # This switch will compare the type given with the quoted string. If it matches, it'll parse it further; otherwise, the default option will be selected.
        switch ($Type) {
            "Dropdown" {
                # Drop-Down custom fields come in as a comma-separated list of GUIDs; we'll compare these with all the options and return just the option values selected instead of a GUID.
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name
            }
            "MultiSelect" {
                # Multi-Select custom fields come in as a comma-separated list of GUID's we'll compare these with all the options and return just the option values selected instead of a guid.
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Selection = ($NinjaPropertyValue -split ',').trim()
    
                foreach ($Item in $Selection) {
                    $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name
                }
            }
            default {
                # If no type was given or not one that matches the above types just output what we retrieved.
                $NinjaPropertyValue
            }
        }
    }

    # List of AV's and how to detect them.
    $AVList = @(
        [PSCustomObject]@{ Name = "Bitdefender Antivirus"; DisplayName = "Bitdefender Agent", "Bitdefender Endpoint Security Tools"; xmlPath = "$env:ProgramFiles\BitDefender\Endpoint Security\update_statistics.xml"; ExecutablePath = "Bitdefender\Endpoint Security\EPSecurityService.exe", "Bitdefender Agent\ProductAgentService.exe", "Bitdefender\Endpoint Security\EPProtectedService.exe"; ProcessName = "ProductAgentUi", "ProductAgentService", "EPProtectedService", "EPSecurityService" }
        [PSCustomObject]@{ Name = "Carbon Black"; DisplayName = "Carbon Black Cloud Sensor", "Carbon Black App Control Agent"; Definitions = "Confer\scanner\data_0\aevdf.dat"; ExecutablePath = "Confer\RepMgr64.exe", "Confer\RepWSC64.exe", "Confer\repwav.exe"; SpecialExecutablePath = "$env:SystemRoot\CarbonBlack\cb.exe"; ProcessName = "RepMgr64", "RepWSC64", "cb" }
        [PSCustomObject]@{ Name = "Crowdstrike"; DisplayName = "CrowdStrike Windows Sensor", "Falcon Agent"; Definitions = "$env:SystemRoot\system32\drivers\crowdstrike\*.sys"; ExecutablePath = "CrowdStrike\CSFalconService.exe"; ProcessName = "CSFalconService" }
        [PSCustomObject]@{ Name = "Cylance"; DisplayName = "Cylance OPTICS", "Cylance Smart Antivirus", "Cylance PROTECT"; Definitions = "$env:ProgramData\Cylance\Desktop\chp.db"; ExecutablePath = "Cylance\Desktop\CylanceSvc.exe", "Cylance\Optics\CyOptics.exe"; ProcessName = "cylancesvc", "cylancedrv", "CyOptics" }
        [PSCustomObject]@{ Name = "ESET Security"; DisplayName = "ESET Security", "ESET Endpoint Security", "ESET Management Agent", "ESET Server Security"; RegistryDefPath = "HKLM:\SOFTWARE\ESET\ESET Security\CurrentVersion\Info"; RegistryDefName = "ScannerVersion"; ExecutablePath = "ESET\RemoteAdministrator\Agent\ERAAgent.exe", "ESET\ESET Security\ekrn.exe"; ProcessName = "ERAAgent", "ekrn" }
        [PSCustomObject]@{ Name = "Huntress"; DisplayName = "Huntress Agent"; ExecutablePath = "Huntress\HuntressAgent.exe"; ProcessName = "HuntressAgent", "HuntressRio" }
        [PSCustomObject]@{ Name = "MalwareBytes"; DisplayName = "Malwarebytes"; Definitions = "$env:ProgramData\Malwarebytes\MBAMService\scan.mbdb"; ExecutablePath = "Malwarebytes\Anti-Malware\mbam.exe", "Malwarebytes\Anti-Malware\MBAMService.exe"; ProcessName = "MBAMService" }
        [PSCustomObject]@{ Name = "Windows Defender"; ProcessName = "MsMpEng" }
        [PSCustomObject]@{ Name = "Sentinel Agent"; DisplayName = "Sentinel Agent"; Definitions = "$env:ProgramFiles\SentinelOne\Sentinel Agent *\config\DecoyPersistentConfig.json"; ExecutablePath = "SentinelOne\Sentinel Agent *\SentinelAgent.exe"; ProcessName = "SentinelServiceHost", "SentinelStaticEngine", "SentinelStaticEngineScanner", "SentinelUI" }
        [PSCustomObject]@{ Name = "Sophos"; DisplayName = "Sophos Endpoint Agent"; RegistryDefPath = "HKLM:\SOFTWARE\Sophos\Sophos File Scanner\Application\Versions"; RegistryDefName = "VirusDataVersion"; ExecutablePath = "Sophos\Remote Management System\ManagementAgentNT.exe", "Sophos\Sophos Anti-Virus\SavService.exe", "Sophos\Endpoint Defense\SEDService.exe"; ProcessName = "ManagementAgentNT", "SAVService", "SEDService" }
        [PSCustomObject]@{ Name = "Vipre"; DisplayName = "VIPRE Business Agent"; Definitions = "$env:ProgramFiles\VIPRE Business Agent\Definitions\defver.txt"; ExecutablePath = "VIPRE\SBAMSvc.exe"; ProcessName = "SBAMSvc" }
        [PSCustomObject]@{ Name = "Webroot SecureAnywhere"; DisplayName = "Webroot SecureAnywhere"; Definitions = "$env:ProgramData\WRData\WRlog.log"; ExecutablePath = "Webroot\WRSA.exe"; ProcessName = "WRSA" }
    )

    $ExitCode = 0
}
process {

    # Let's see what tools we don't want to alert on.
    $ExcludedAVs = New-Object System.Collections.Generic.List[String]

    if ($ExcludeAV) {
        $ExcludeAV.split(',') | ForEach-Object {
            $ExcludedAVs.Add($_.Trim())
        }
    }

    # For this kind of alert it might be worth it to create a whole custom field of ignorables.
    if ($ExclusionsFromCustomField) {
        try {
            Write-Host "Retrieving exclusions from custom field '$ExclusionsFromCustomField'..."
            $Exclusions = Get-NinjaProperty -Name $ExclusionsFromCustomField
            Write-Host "Successfully retrieved $Exclusions."
        }
        catch {
            Write-Host "[Error] $($_.Message)"
            exit 1
        }
        
        if ($Exclusions) {
            $Exclusions.split(',') | ForEach-Object {
                $ExcludedAVs.Add($_.Trim())
            }
        }
    }

    # WMI Would have better AV coverage and would likely be more accurate. However Windows Server does not have the Security Center
    if (Test-IsWorkstation) {
        Write-Host "Desktop Windows Detected, Checking the Windows Security Center...."
        $AVinfo = if ($PSVersionTable.PSVersion.Major -ge 5) {
            Get-CimInstance -Namespace root/SecurityCenter2 -Class AntivirusProduct
        }
        else {
            Get-WmiObject -Namespace root/SecurityCenter2 -Class AntivirusProduct
        }
    }

    $AVs = New-Object System.Collections.Generic.List[Object]

    # This takes our list and begins searching by the 4 methods in the begin block.
    if ($AVInfo) {
        Write-Warning "Antivirus info received from the Windows Security Center, this may result in duplicate entries in the report due to their naming scheme."
        $AVinfo | ForEach-Object {
            $Executable = ($_.pathToSignedReportingExe -replace '%programfiles%', "$env:ProgramFiles" | Get-Item).BaseName
            $RunningStatus = Find-Process -Name $Executable

            $ConvertToHex = [Convert]::ToString($_.ProductState, 16).PadLeft(6, '0')
            $ProductStateHex = $ConvertToHex.Substring(2, 2)
            $DefinitionsHex = $ConvertToHex.Substring(4, 2)

            $ProductState = switch ($ProductStateHex) {
                "10" { "Active" }
                default { "Inactive" }
            }

            $UpToDateWMI = switch ($DefinitionsHex) {
                "00" { $True }
                default { $False }
            }

            if ($_.displayName -eq "Windows Defender" -and ((Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue).Count -ne 0)) {
                $Version = (Get-MpComputerStatus).AMProductVersion
                $Definitions = (Get-MpComputerStatus).AntivirusSignatureLastUpdated
            }
            else {
                $Definitions = Get-Date $_.timestamp -ErrorAction SilentlyContinue
            }

            $UpToDate = if ($Definitions -and $Definitions -gt (Get-Date).AddDays(-$OutOfDate) -and ($UpToDateWMI -like "True")) {
                $True
            }
            else {
                $False
            }

            if ($_.displayName -ne "Windows Defender") {
                $Version = Find-UninstallKey -Version -DisplayName "$($_.displayName)"
            }

            [PSCustomObject]@{
                Name        = $_.displayName
                Installed   = "Yes"
                Definitions = if ($Definitions) { Get-Date $Definitions -Format "yyyy/MM/dd" }else { $null }
                UpToDate    = $UpToDate
                Running     = if ($RunningStatus) { "Yes" }else { "No" }
                Service     = $ProductState
                Version     = if ($Version) { "$Version" }else { $null }
            } | Where-Object { $ExcludedAVs -notcontains $_.Name } | ForEach-Object { $AVs.Add($_) }
        } 
    }

    $AVList | Where-Object { $AVs.Name -notcontains $_.Name } | ForEach-Object {

        $UninstallKey = if ($_.DisplayName) {
            $_.DisplayName | Find-UninstallKey
        }
        
        $UninstallInfo = if ($_.DisplayName) {
            $_.DisplayName | Find-UninstallKey -Version
        }
        
        $RunningStatus = if ($_.ProcessName) {
            $_.ProcessName | Find-Process
        }

        $ServiceStatus = if ($_.ProcessName) {
            $_.ProcessName | Find-Service
        }

        # AV's don't really have a consistent way to check their definitions (unless it's desktop windows)
        $Definitions = if ($_.Definitions) {
            $_.Definitions | Find-Definitions
        }
        elseif ($_.Name -eq "BitDefender") {
            [xml]$xml = Get-Content $_.xmlPath -ErrorAction SilentlyContinue
            if ($xml) {
                [datetime]$origin = '1970-01-01 00:00:00'
                $ConvertFromUnix = $origin.AddSeconds($xml.UpdateStatistics.Antivirus.Check.updtime)
                Get-Date ($ConvertFromUnix.ToLocalTime()) -ErrorAction SilentlyContinue
            }
        }
        elseif ($_.Name -eq "Sophos") {
            $RegValue = (Get-ItemProperty -Path $_.RegistryDefPath -ErrorAction SilentlyContinue).($_.RegistryDefName)
            if ($RegValue) {
                Get-Date ([datetime]::ParseExact($RegValue.SubString(0, 8), 'yyyyMMdd', $null)) -ErrorAction SilentlyContinue
            }
        }
        elseif ($_.Name -eq "ESET") {
            $RegValue = (Get-ItemProperty -Path $_.RegistryDefPath -ErrorAction SilentlyContinue).($_.RegistryDefName)
            if ($RegValue) {
                $RegValue -match '(\d{8})' | Out-Null
                Get-Date ([datetime]::ParseExact($Matches[0], 'yyyyMMdd', $null)) -ErrorAction SilentlyContinue
            }
        }
        
        $InstallPath = if ($_.ExecutablePath) {
            $_.ExecutablePath | Find-Executable
        }
        elseif ($_.SpecialExecutablePath) {
            $_.SpecialExecutablePath | Find-Executable -Special
        }

        if ($UninstallKey -or $RunningStatus -or $InstallPath -or $ServiceStatus) {
            $Installed = "Yes"
        }
        else {
            $Installed = "No"
        }

        if ($_.Name -eq "Windows Defender" -and ((Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue).Count -ne 0)) {
            $UninstallInfo = (Get-MpComputerStatus).AMProductVersion
            $Definitions = (Get-MpComputerStatus).AntivirusSignatureLastUpdated
        }

        $UpToDate = if ($Definitions -and $Definitions -gt (Get-Date).AddDays(-$OutOfDate)) {
            $True
        }
        else {
            $False
        }

        [PSCustomObject]@{
            Name        = $_.Name
            Installed   = $Installed
            Definitions = if ($Definitions) { Get-Date $Definitions -Format "yyyy/MM/dd" }else { $Null }
            UpToDate    = $UpToDate
            Running     = if ($RunningStatus) { "Yes" }else { "No" }
            Service     = if ($ServiceStatus) { "Active" }else { "Inactive" }
            Version     = $UninstallInfo
        } | Where-Object { $ExcludedAVs -notcontains $_.Name } | ForEach-Object { $AVs.Add($_) }
    }

    $InstalledAVs = $AVs | Where-Object { $_.Installed -eq "Yes" }
    Write-Host ""

    if (!$InstalledAVs) {
        Write-Host "[Alert] It appears there's no installed antivirus? You may want to check the list of AV's this script supports."
        $ExitCode = 1
    }
    
    if ($InstalledAVs | Where-Object { $_.UpToDate -Like "False" }) {
        Write-Host "[Alert] The AV definitions are out of date!"
        $ExitCode = 1
    }

    if ($InstalledAVs | Where-Object { $_.HasRunningService -Like "False" }) {
        Write-Host "[Alert] The AV's service doesn't appear to be running, is the AV Updating or performing maintenance?"
        $ExitCode = 1
    }

    if ($InstalledAVs | Where-Object { $_.CurrentlyRunning -Like "False" }) {
        Write-Host "[Alert] The AV doesn't appear to have a running process, is the AV Updating or performing maintenance?"
        $ExitCode = 1
    }

    # If we found anything in the four checks, we're going to indicate it's installed, but we may also want to save our results to a custom field.
    if ($ShowNotFound) {
        $AVs | Format-Table -AutoSize -Wrap | Out-String | Write-Host
    }
    else {
        if ($InstalledAVs) {
            $InstalledAVs | Format-Table -AutoSize -Wrap | Out-String | Write-Host
        }
    }

    if ($ExportAll) {
        $ExportReport = $InstalledAVs | Format-Table -AutoSize -Wrap | Out-String
        try {
            Write-Host "Attempting to set Custom Field '$ExportAll'."
            Set-NinjaProperty -Name $ExportAll -Value $ExportReport
            Write-Host "Successfully set Custom Field '$ExportAll'!"
        }
        catch {
            Write-Host "[Error] $($_.Message)"
            $ExitCode = 1
        }
    }

    if ($ExportDef) {
        try {
            Write-Host "Attempting to set Custom Field '$ExportDef'."
            $Value = ($InstalledAVs | Select-Object -ExpandProperty Definitions) -join ', '
            Set-NinjaProperty -Name $ExportDef -Value ( $Value | Out-String )
            Write-Host "Successfully set Custom Field '$ExportDef'!"
        }
        catch {
            Write-Host "[Error] $($_.Message)"
            $ExitCode = 1
        }
    }

    if ($ExportDefStatus) {
        try {
            Write-Host "Attempting to set Custom Field '$ExportDefStatus'."
            $Value = ($InstalledAVs | Select-Object -ExpandProperty UpToDate) -join ', '
            Set-NinjaProperty -Name $ExportDefStatus -Value ( $Value | Out-String )
            Write-Host "Successfully set Custom Field '$ExportDefStatus'!"
        }
        catch {
            Write-Host "[Error] $($_.Message)"
            $ExitCode = 1
        }
    }

    if ($ExportName) {
        try {
            Write-Host "Attempting to set Custom Field '$ExportName'."
            $Value = ($InstalledAVs | Select-Object -ExpandProperty Name) -join ', '
            Set-NinjaProperty -Name $ExportName -Value ( $Value | Out-String )
            Write-Host "Successfully set Custom Field '$ExportName'!"
        }
        catch {
            Write-Host "[Error] $($_.Message)"
            $ExitCode = 1
        } 
    }

    if ($ExportStatus) {
        try {
            Write-Host "Attempting to set Custom Field '$ExportStatus'."
            $Value = ($InstalledAVs | Select-Object -ExpandProperty Running) -join ', '
            Set-NinjaProperty -Name $ExportStatus -Value ( $Value | Out-String )
            Write-Host "Successfully set Custom Field '$ExportStatus'!"
        }
        catch {
            Write-Host "[Error] $($_.Message)"
            $ExitCode = 1
        }
    }

    if ($ExportVersion) {
        try {
            Write-Host "Attempting to set Custom Field '$ExportVersion'."
            $Value = (($InstalledAVs | Select-Object -ExpandProperty Version | ForEach-Object { $_.Trim() }) -join ', ')
            Set-NinjaProperty -Name $ExportVersion -Value ($Value | Out-String)
            Write-Host "Successfully set Custom Field '$ExportVersion'!"
        }
        catch {
            Write-Host "[Error] $($_.Message)"
            $ExitCode = 1
        }
    }

    exit $ExitCode
}end {
    
    
    
}

 

Access over 300+ scripts in the NinjaOne Dojo

Get Access

Detailed Breakdown

This PowerShell script is designed to detect various antivirus programs installed on a Windows system, retrieve details about their definitions and status, and export the results. It supports detection of 11 popular antivirus solutions, including BitDefender, Carbon Black, Crowdstrike, Cylance, ESET, Huntress, MalwareBytes, Windows Defender, SentinelOne, Sophos, Vipre, and Webroot.

How It Works

1. Initialization and Parameter Handling: The script starts by defining various parameters that can be used to customize its execution. These include options for excluding specific antivirus ript verifies that it is running with administrator privileges, as this is necessary to access certain system information. If the script is not run as an administrator, it will terminate with an error message.

2. Checking for Administrator Privileges: The script verifies that it is running with administrator privileges, as this is necessary to access certain system information. If the script is not run as an administrator, it will terminate with an error message.

3. Retrieving Antivirus Information: The script uses multiple methods to detect installed antivirus software:

  • Registry Keys: It searches the Windows registry for entries related to antivirus software.
  • WMI (Windows Management Instrumentation): For desktop systems, it queries the Windows Security Center for antivirus details.
  • Processes and Services: It checks for running processes and services associated with known antivirus programs.

4. Collecting Definition Dates: For each detected antivirus program, the script retrieves the date of the last definition update. This is used to determine if the definitions are outdated based on the specified threshold.

5. Compiling Results: The script compiles the results into a table, including details such as the name of the antivirus software, whether it is installed, the date of the last definition update, its version, and its current running status.

6. Exporting Results: If specified, the script exports the results to custom fields, which can be useful for integration with IT management tools like NinjaOne.

Potential Use Cases

Consider an IT professional managing a large network of Windows systems. Regularly checking the antivirus status on each system manually is time-consuming and prone to errors. By using this script, the IT professional can automate the process, ensuring that all systems have up-to-date antivirus definitions and are actively running their antivirus software. If any issues are detected, such as outdated definitions or inactive antivirus programs, the script can alert the IT team, allowing for prompt remediation.

Alternative Methods

  • Manual Checking: Manually verifying antivirus status on each system is labor-intensive and inefficient, especially in large environments.
  • Third-Party Tools: While there are commercial tools available for monitoring antivirus status, they can be expensive and may require additional infrastructure.

This PowerShell script offers a cost-effective and flexible alternative, allowing IT professionals to customize it according to their specific needs.

FAQs

How do I run the script with administrator privileges?

Right-click on the PowerShell application and select “Run as administrator” before executing the script.

Can I add more antivirus programs to the detection list?

Yes, the script can be modified to include additional antivirus programs by adding their details to the $AVList array.

What should I do if the script reports outdated definitions?

Check the affected systems to ensure they are connected to the internet and configured to receive automatic updates. If necessary, manually update the antivirus definitions.

Implications

Regularly checking the status of antivirus software is crucial for maintaining a secure IT environment. This script helps ensure that antivirus programs are installed, up-to-date, and actively running, thereby reducing the risk of malware infections and other security threats. By automating these checks, IT departments can focus on other critical tasks, improving overall efficiency and security posture.

Recommendations

  • Schedule Regular Checks: Run the script on a regular basis (e.g., daily or weekly) to ensure continuous monitoring of antivirus status.
  • Customize Exclusions: Use the -ExcludeAV parameter to exclude any antivirus programs that are not relevant to your environment.
  • Review and Act on Alerts: Promptly address any alerts generated by the script, such as outdated definitions or inactive antivirus programs.

Final Thoughts

In the realm of IT security, staying vigilant is paramount. This PowerShell script provides a powerful tool for automating the monitoring of antivirus status, ensuring that systems are protected against emerging threats. By integrating this script with management tools like NinjaOne, IT professionals can further streamline their workflows and enhance their security posture.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service deliver tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Categories:

You might also like

×

See NinjaOne in action!

By submitting this form, I accept NinjaOne's privacy policy.

NinjaOne Terms & Conditions

By clicking the “I Accept” button below, you indicate your acceptance of the following legal terms as well as our 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 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).