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.
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:
$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.
+=
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.