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