1

I am trying to determine if specific Windows hotfixes are installed on our Windows servers. I am quite new to PowerShell scripting and this is what I have so far:

$servers = Get-ADComputer -Filter {(OperatingSystem -like "Windows Server 2019*") -and (enabled -ne $false)} -Property *
$result = @()
ForEach ($item in $servers) {
    $testhotfix = Get-HotFix -Id KB4534310,KB4534314,KB4534283,KB4534288,KB4534297,KB4534309,KB4534271,KB4534273 -ComputerName $item.Name | `
    select $item.Name,$item.CanonicalName,$item.OperatingSystem
    $result += $testhotfix
}
$result | Export-Csv -Path C:\Users\user1\Desktop\Servers.csv -NoTypeInformation

The CSV file that is created includes one line with the information I'm looking for, followed by several lines of commas, like so:

Script Output

"SERVER1","somedomain.com/Servers/Non-Prod/New_Server_Staging/SERVER1","Windows Server 2019 Standard" ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,, ,,

We have several servers with at least one of the hotfixes installed. How do I add each server to the $result array?

Thank you

dpsmith
  • 13
  • 2
  • As an aside: While seductively convenient, it's best to [avoid the use of script blocks (`{ ... }`) as `-Filter` arguments](https://stackoverflow.com/a/44184818/45375). – mklement0 Feb 05 '20 at 16:42

2 Answers2

0

Generally speaking:

select $item.Name,$item.CanonicalName,$item.OperatingSystem

should be:

select Name, CanonicalName, OperatingSystem

That is, you need to pass the property names (e.g., Name), not the current input object's property values (e.g., $item.Name) to select (the Select-Objectcmdlet).

The net effect is that Select-Object creates custom objects whose properties are (mistakenly) named for the property values and themselves have no value, given that the input objects have no such properties. This explains the output you saw.

However, the bigger problem is that even that won't work, given that the property names relate to the $item object, not to the objects output by Get-HotFix, which are the ones select operates on.

As it turns out, what you really need is to use the Get-HotFix call as a conditional, so as to only write a CSV row for the computer at hand if at least one of the specified hotfixes is installed:

$hotfixIds = 'KB4534310', 'KB4534314', 'KB4534283', 'KB4534288', 'KB4534297', 'KB4534309', 'KB4534271', 'KB4534273'

if (0 -ne (Get-HotFix -ErrorAction SilentlyContinue -Id $hotfixIds -ComputerName $item.Name).Count)  {
  $result += $item | select Name, CanonicalName, OperatingSystem
}

Note:

  • Note how it is now $item (the computer at hand) that is piped to select, to ensure that its properties are extracted (in the form of a custom object with these properties).

  • You could omit 0 -eq altogether and rely on PowerShell's implicit to-Boolean conversion, where any nonzero number evaluates to $true (see the bottom section of this answer for a summary of all rules.

    • If instead you want to test for all of the specified hotfixes being installed, replace 0 -ne with $hotfixIds.Count -eq.
  • -ErrorAction SilentlyContinue silences the errors from computers where none of the specified hotfixes are installed; you could examine the automatic $Error collection afterwards, or use -ErrorVariable err to collect all command-specific errors in variable $err.

Also, your overall command can be greatly streamlined - see the bottom section.


A solution for a different scenario, that may be of interest as well:

If you wanted to combine properties from the Get-HotFix output objects with properties from the $item objects (representing the computer at hand):

The following command:

  • selects all properties from the Get-HotFix output objects (-Property *)
  • adds the properties of interest from the current $item, using calculated properties
# Additional 'KB...' values omitted for brevity.
Get-HotFix -Id KB4534310, KB4534314 -ComputerName $item.Name | 
  Select-Object -Exclude Name -Property *,
                  @{ n = 'Name'; e = { $item.Name } },
                  @{ n = 'CanonicalName'; e = { $item.CanonicalName } },
                  @{ n = 'OperatingSystem'; e = { $item.OperatingSystem } }

Note that -Exclude Name excludes the Name property from the input objects (Get-HotFix output objects that have such a property, but it is empty), so that Name can be added as a property containing the computer name.


As for what you tried:

Aside from the Select-Object property-name problem mentioned above, your major problem was that you expected a pipeline segment as a conditional, which is not how pipelines work:

Get-HotFix ... | select ...

The above simply sends Get-HotFix's output objects to select (Select-Object), which then unconditionally processes them (and, as stated, looks for properties with the given names on these objects).

Now, if Get-HotFix produced no output, then conditional logic applies implicitly: the select command would then simply not be invoked.

Conversely, if Get-HotFix produces multiple outputs, select would be invoked on each.

That is, if we had naively tried to correct your command from:

Get-HotFix ... | select ...

to:

Get-HotFix ... | ForEach-Object { $item | select ... }

you would have potentially created multiple output objects per computer, namely whenever a given computer happens to have more than one among the given hotfixes installed.


A streamlined version of your (corrected) command:

Your command can be streamlined to use a single pipeline only, without the need for aux. variables:

Get-ADComputer -Filter '(OperatingSystem -like "Windows Server 2019*") -and (enabled -ne $false)' -Property * |
  ForEach-Object {
    if (0 -ne (Get-HotFix -ErrorAction SilentlyContinue -ComputerName $item.Name -Id KB4534310,KB4534314,KB4534283,KB4534288,KB4534297,KB4534309,KB4534271,KB4534273).Count) {
      $item | select Name, CanonicalName, OperatingSystem
    }
  } | Export-Csv -Path C:\Users\user1\Desktop\Servers.csv -NoTypeInformation

Note:

  • If you end a line with |, you do not need a trailing ` to signal line continuation.

    • PowerShell [Core] v7.0+ now also allows placing | at the start of the very next line.
  • A single-quoted string ('...') is used instead of a script block ({ ... }) to pass the -Filter argument, because tt's best to avoid the use of script blocks ({ ... }) as -Filter arguments.

  • The output custom object instances created with $item | select Name, CanonicalName, OperatingSystem are sent directly to the pipeline.

mklement0
  • 382,024
  • 64
  • 607
  • 775
0

I would use a PSCustomObject.

    $array = foreach($item in $obj)
    {
        [PSCustomObject]@{
            Name = $item.Name
            CanonicalName = $item.CanonicalName
            OS = $item.OperatingSystem
        }
    }
Alex_P
  • 2,580
  • 3
  • 22
  • 37
  • While that is a useful technique for more advanced scenarios, in your scenario `$obj | select Name, CanonicalName, OperatingSystem` is sufficient. (And neither form solves the OP's problem by itself.) – mklement0 Feb 05 '20 at 16:53