Using PowerShell to Audit Antivirus Updates on your Servers

How often do you check to make sure that things like antivirus has received the latest definition files on all of your servers? There's probably some centralized GUI interface somewhere that you could log into and check. The antivirus product itself may even have some sort of notification system that sends alerts if the updates fail. Both of those options provide data in a format that can't be worked with and what happens if something falls through the cracks? Are you willing to bet your job and possible the reputation of your company that some junior level engineer is monitoring those systems?

While each antivirus product is different, it's fairly simple to determine if the information for the antivirus product is stored somewhere such as in the registry where you can access it remotely with PowerShell. The following example was my first attempt at querying this information for ESET File Security:

 1function Get-MrEsetUpdateVersion {
 2    [CmdletBinding()]
 3    param (
 4        [string[]]$ComputerName,
 5        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
 6    )
 7
 8    $Params = @{}
 9    if ($PSBoundParameters.Credential){
10        $Params.Credential = $Credential
11    }
12
13    foreach ($Computer in $ComputerName){
14        $Results = Invoke-Command -ComputerName $Computer {
15            Get-ItemProperty -Path 'HKLM:\SOFTWARE\ESET\ESET Security\CurrentVersion\Info' -ErrorAction SilentlyContinue
16        } @Params
17
18        [pscustomobject]@{
19            ComputerName = $Computer
20            ProductName = $Results.ProductName
21            ScannerVersion = $Results.ScannerVersionId
22            LastUpdate = $Results.ScannerVersion -replace '^.*\(|\)'
23        }
24
25    }
26
27}

eset-version1b.png

Not bad, but the problem is that it queries each server individually (one at a time) which takes more time than necessary and the last update property is returned in a format that isn't in a date/time datatype.

The problem with trying to query all of the servers in parallel with Invoke-Command is keeping up with the computer name for the ones that don't have that particular registry key which means they don't have ESET File Security installed.

 1function Get-MrEsetUpdateVersion {
 2    [CmdletBinding()]
 3    param (
 4        [ValidateNotNullOrEmpty()]
 5        [string[]]$ComputerName = $env:COMPUTERNAME,
 6
 7        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
 8    )
 9
10    $Params = @{}
11    if ($PSBoundParameters.Credential){
12        $Params.Credential = $Credential
13    }
14
15    $Results = Invoke-Command -ComputerName $ComputerName {
16        Get-ItemProperty -Path 'HKLM:\SOFTWARE\ESET\ESET Security\CurrentVersion\Info' -ErrorAction SilentlyContinue
17    } @Params
18
19    foreach ($Result in $Results) {
20        [pscustomobject]@{
21            ComputerName = $Result.PSComputerName
22            ProductName = $Result.ProductName
23            ScannerVersion = $Result.ScannerVersionId
24            LastUpdate = if ($Result.ScannerVersion) {([datetime]::ParseExact($Result.ScannerVersion -replace '^.*\(|\)', 'yyyyMMdd', $null)).ToShortDateString()}
25        }
26    }
27
28
29}

eset-version2b.png

In the previous example, Srv02 actually returns an error because the registry key doesn't exist and that error is suppressed with -ErrorAction SilentlyContinue.

One of two things need to happen to make Srv02 return a result so the PSComputerName synthetic property can be used in the output. Either catch the error and generate something out of nothing that can be returned as shown in the following example:

 1function Get-MrEsetUpdateVersion {
 2    [CmdletBinding()]
 3    param (
 4        [ValidateNotNullOrEmpty()]
 5        [string[]]$ComputerName = $env:COMPUTERNAME,
 6
 7        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
 8    )
 9
10    $Params = @{}
11    if ($PSBoundParameters.Credential){
12        $Params.Credential = $Credential
13    }
14
15    $Results = Invoke-Command -ComputerName $ComputerName {
16    try {
17        Get-ItemProperty -Path 'HKLM:\SOFTWARE\ESET\ESET Security\CurrentVersion\Info' -ErrorAction Stop
18    }
19    catch {
20        Write-Output ''
21    }
22
23    } @Params
24
25    foreach ($Result in $Results) {
26        [pscustomobject]@{
27            ComputerName = $Result.PSComputerName
28            ProductName = $Result.ProductName
29            ScannerVersion = $Result.ScannerVersionId
30            LastUpdate = if ($Result.ScannerVersion) {([datetime]::ParseExact($Result.ScannerVersion -replace '^.*\(|\)', 'yyyyMMdd', $null)).ToShortDateString()}
31        }
32    }
33
34
35}

eset-version3b.png

Or return the error as part of the success output stream:

 1function Get-MrEsetUpdateVersion {
 2    [CmdletBinding()]
 3    param (
 4        [ValidateNotNullOrEmpty()]
 5        [string[]]$ComputerName = $env:COMPUTERNAME,
 6
 7        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
 8    )
 9
10    $Params = @{}
11    if ($PSBoundParameters.Credential){
12        $Params.Credential = $Credential
13    }
14
15    $Results = Invoke-Command -ComputerName $ComputerName {
16        Get-ItemProperty -Path 'HKLM:\SOFTWARE\ESET\ESET Security\CurrentVersion\Info' 2>&1
17    } @Params
18
19    foreach ($Result in $Results) {
20        [pscustomobject]@{
21            ComputerName = $Result.PSComputerName
22            ProductName = $Result.ProductName
23            ScannerVersion = $Result.ScannerVersionId
24            LastUpdate = if ($Result.ScannerVersion) {([datetime]::ParseExact($Result.ScannerVersion -replace '^.*\(|\)', 'yyyyMMdd', $null)).ToShortDateString()}
25        }
26    }
27
28
29}

eset-version3b.png

Notice that the date is now returned in a format that can be worked with. It can be used to return a list of all servers that haven't received updates in a certain number of days. The function shown in this blog article could also be used as one of the tests in your operational validation testing of your servers to verify that everything is configured and working properly as well as receiving proper updates for things like antivirus software.

The Get-MrEsetUpdateVersion function shown in this blog article can be downloaded from my PowerShell repository on GitHub.

µ