1

In a modification of this, I'm doing this:

function Get-AntiMalwareStatus {
[CmdletBinding()]
param
(
[Parameter(Position=0,Helpmessage = 'Possible Values: AllServer')]
[ValidateSet('AllServer')]
$Scope
)
$result=@()
$ErrorActionPreference="SilentlyContinue"
switch ($Scope) {
$null {
Get-MpComputerStatus | Select-Object -Property Antivirusenabled,AMServiceEnabled,AntispywareEnabled,BehaviorMonitorEnabled,IoavProtectionEnabled,`
NISEnabled,OnAccessProtectionEnabled,RealTimeProtectionEnabled,AntivirusSignatureLastUpdated
}
AllServer {
$result=@() 
$server="server1","server2","server3"
foreach ($s in $server) {
$rs=Invoke-Command -ComputerName $s {Get-MpComputerStatus | Select-Object -Property Antivirusenabled,AMServiceEnabled,AntispywareEnabled,BehaviorMonitorEnabled,IoavProtectionEnabled,NISEnabled,OnAccessProtectionEnabled,RealTimeProtectionEnabled,AntivirusSignatureLastUpdated,AntispywareSignatureLastUpdated,NISSignatureLastUpdated}
If ($rs) {
$result+=New-Object -TypeName PSObject -Property ([ordered]@{
'Server'=$rs.PSComputername
'Anti-Virus'=$rs.AntivirusEnabled
'AV Update'=$rs.AntivirusSignatureLastUpdated
'Anti-Malware'=$rs.AMServiceEnabled
'Anti-Spyware'=$rs.AntispywareEnabled
'AS Update'=$rs.AntispywareSignatureLastUpdated
'Behavior Monitor'=$rs.BehaviorMonitorEnabled
'Office-Anti-Virus'=$rs.IoavProtectionEnabled
'NIS'=$rs.NISEnabled
'NIS Update'=$rs.NISSignatureLastUpdated
'Access Prot'=$rs.OnAccessProtectionEnabled
'R-T Prot'=$rs.RealTimeProtectionEnabled
})
}
}
}
}
Write-Output $result
}

WHich results in:

PS C:\WINDOWS\system32> Get-AntiMalwareStatus -Scope AllServer | Format-Table -AutoSize

Server          Anti-Virus AV Update             Anti-Malware Anti-Spyware AS Update            Behavior Monitor Office-Anti-Virus  NIS NIS Update          
------          ---------- ---------             ------------ ------------ ---------            ---------------- -----------------  --- ----------          
server1              False 12/31/1969 7:00:00 PM         True         True 8/10/2023 5:37:49 PM             True              True True 8/10/2023 5:37:16 PM
server2              False 12/31/1969 7:00:00 PM         True         True 8/9/2023 2:43:53 PM              True              True True 8/9/2023 2:46:39 PM 
server3              True 8/5/2023 9:44:58 PM           True         True 8/5/2023 9:44:59 PM              True              True True 8/5/2023 9:44:58 PM 

But when I modify line 20 to:

$rs=Invoke-Command -ComputerName $s {Get-MpComputerStatus | Select-Object -Property Antivirusenabled,AMServiceEnabled,AntispywareEnabled,BehaviorMonitorEnabled,IoavProtectionEnabled,NISEnabled,OnAccessProtectionEnabled,RealTimeProtectionEnabled,AntivirusSignatureLastUpdated,AntispywareSignatureLastUpdated,NISSignatureLastUpdated;Get-ComputerInfo | select WindowsProductName}

I get:

PS C:\WINDOWS\system32> Get-AntiMalwareStatus -Scope AllServer | Format-Table -AutoSize

Server                             Anti-Virus     AV Update                      Anti-Malware  Anti-Spyware  AS Update                     Behavior Monitor Office-Anti-Virus NIS           NIS Update                   
------                             ----------     ---------                      ------------  ------------  ---------                     ---------------- ----------------- ---           ----------                   
{server1, server1}                 {False, $null} {12/31/1969 7:00:00 PM, $null} {True, $null} {True, $null} {8/10/2023 5:37:49 PM, $null} {True, $null}    {True, $null}     {True, $null} {8/10/2023 5:37:16 PM, $null}
{server2, server2}                 {False, $null} {12/31/1969 7:00:00 PM, $null} {True, $null} {True, $null} {8/9/2023 2:43:53 PM, $null}  {True, $null}    {True, $null}     {True, $null} {8/9/2023 2:46:39 PM, $null} 
{server3, server3}                 {True, $null}  {8/5/2023 9:44:58 PM, $null}   {True, $null} {True, $null} {8/5/2023 9:44:59 PM, $null}  {True, $null}    {True, $null}     {True, $null} {8/5/2023 9:44:58 PM, $null} 

Also same result when I chain the Get-ComputerInfo | select WindowsProductName to the end of line 13 as well, or just at the end of line 13.

What I'm trying to do is get OS version and AV status together, as I have many 2019 and 2022 servers, and we have a different response for which OS version if AV is not auditing correctly.

Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
tpcolson
  • 716
  • 1
  • 11
  • 27

1 Answers1

2

With your modification, $rs receives two objects in each iteration, so that property access such as $rs.PSComputername - due to member-access enumeration - analogously yields two values.

Since the two objects on which the property access is made are of disparate types and don't share properties, the second value in each value pair is $null - this is what you're seeing in the formatted output.

A minimal repro:

# The .Foo property receives *two* values, the second one being $null,
# because the nested [pscustomobject] has no .Name property.
[pscustomobject] @{ 
  Foo = $(Get-Item /; [pscustomobject] @{ Unrelated=1 }).Name 
} | Format-Table

Output:

Foo
---
{/, $null}

As an - inconsequential - aside:

  • Member-access enumeration only includes the $null value because the second object happens to be a [pscustomobject] instance, such as created by Select-Object; for all other types, a $null value is simply omitted (e.g., if you replace [pscustomobject] @{ Unrelated=1 } with Get-Date, the $null disappears) - see GitHub issue #13752.

The solution is to capture these two objects in separate variables, and then combine their properties in the custom object that is constructed for output.

switch ($Scope) {
  $null {
    Get-MpComputerStatus | Select-Object -Property Antivirusenabled, AMServiceEnabled, AntispywareEnabled, BehaviorMonitorEnabled, IoavProtectionEnabled, `
      NISEnabled, OnAccessProtectionEnabled, RealTimeProtectionEnabled, AntivirusSignatureLastUpdated
  }
  AllServer {
    $server = 'server1', 'server2', 'server3'
    foreach ($s in $server) {
      # COLLECT THE TWO OUTPUT OBJECTS SEPARATELY
      $rs, $prodName = 
        Invoke-Command -ComputerName $s { 
          Get-MpComputerStatus | Select-Object -Property Antivirusenabled, AMServiceEnabled, AntispywareEnabled, BehaviorMonitorEnabled, IoavProtectionEnabled, NISEnabled, OnAccessProtectionEnabled, RealTimeProtectionEnabled, AntivirusSignatureLastUpdated, AntispywareSignatureLastUpdated, NISSignatureLastUpdated 
          Get-ComputerInfo | Select-Object -ExpandProperty WindowsProductName
        }
      If ($rs -and $prodName) {
        [pscustomobject] @{
          'Server'             = $rs.PSComputername
          'WindowsProductName' = $prodName # NEW PROPERTY with the Windows product name.
          'Anti-Virus'         = $rs.AntivirusEnabled
          'AV Update'          = $rs.AntivirusSignatureLastUpdated
          'Anti-Malware'       = $rs.AMServiceEnabled
          'Anti-Spyware'       = $rs.AntispywareEnabled
          'AS Update'          = $rs.AntispywareSignatureLastUpdated
          'Behavior Monitor'   = $rs.BehaviorMonitorEnabled
          'Office-Anti-Virus'  = $rs.IoavProtectionEnabled
          'NIS'                = $rs.NISEnabled
          'NIS Update'         = $rs.NISSignatureLastUpdated
          'Access Prot'        = $rs.OnAccessProtectionEnabled
          'R-T Prot'           = $rs.RealTimeProtectionEnabled
        }
      }
    }
  }
}
  • $rs, $prodName = ... is a multi-assignment that captures the two output objects in separate variables.

  • select ProductName was replaced with Select-Object -ExpandProperty ProductName to return just the property value.

  • If ($rs -and $prodName) ensures that output is only produced if both expected objects were returned.

  • 'WindowsProductName' = $prodName adds the product name as a property to the output object; adjust as needed.

  • Instead of the New-Object -TypeName PSObject call, the more efficient and convenient PSv3+ [pscustomobject] @{ ... } syntax is used to create the output [pscustomobject] (aka [psobject]) instances - see the conceptual about_PSCustomObject help topic.

  • Instead of using an intermediate $result array, implicit output is used; that is, the [pscustomobject] is both created and output.

    • Note that in cases where up-front collection in an array is necessary, the use of += to iteratively "extend" an array is best avoided, because a new array must be constructed every time, given that arrays are data structures of fixed size; PowerShell allows you to use entire language statements such as switch and foreach as expression, meaning that their output is automatically collected in an array when assigned to a variable (e.g., $output = switch ... ), with two or more output objects - see this answer for more information.
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    A slight addendum to the accepted answer, Format-Table defaults to 10 columns, need to use Format-Table -AutoSize * in this case, as described in https://stackoverflow.com/questions/24218482/whats-the-maximum-number-of-columns-for-format-table-cmdlet-in-powershell – tpcolson Aug 11 '23 at 22:47
  • 1
    @tpcolson, while it's true that `Format-Table` _without a `-Property` argument_ is limited to 10 columns, adding `-AutoSize` does _not_ change that. You need `-Property *` (or an explicit enumeration of more than 10 property names) in order to display additional columns, assuming they fit (which is where `-AutoSize` may help). Try with `[pscustomobject] @{ 'p1'=1; 'p2'=1; 'p3'=1; 'p4'=1; 'p5'=1; 'p6'=1; 'p7'=1; 'p8'=1; 'p9'=1; 'p10'=1; 'p11'=1; } | Format-Table -AutoSize`, which still only prints 10 columns; adding `-Property *` shows all 11. – mklement0 Aug 11 '23 at 23:06
  • 1
    Where I'm taking this, I built upon your answer to add ..."a few more things" like domain/pub/priv firewall status, etc...the column list is growing. As usual your answer is above board and answers far more than the original question, thanks again! – tpcolson Aug 11 '23 at 23:40
  • @tpcolson, I'm glad to hear it, and I appreciate the nice feedback. I've opened a GitHub issue to suggest lifting the arbitrary 10-column limit: https://github.com/PowerShell/PowerShell/issues/20108 – mklement0 Aug 11 '23 at 23:49