Thursday, November 26, 2015

PowerShell: Adding Elements to an Array

If you've been using PowerShell for a while, you may have stumbled across this syntax:
$Array += $Item
Or this:
$Array = $Array + $Item
This is inefficient and slow, and here is why. The += operator, instead of simple adding a new element to the array, creates a second copy of the array with the new element. To visualize:
$Array = 'A'
#New array = A
#Sum of array sizes = 1
$Array += 'B'
#Old array = A
#New array = A,B
#Sum of array sizes = 3
$Array += 'C'
#Old array = A,B
#New array = A,B,C
#Sum of array sizes = 5
...
$Array += 'Z'
#Old array = A,B,C,...,Y
#New array = A,B,C,...,Y,Z
#Sum of array sizes = 51
This may seem innocuous, but when the array is large, the issue is exacerbated. Say the array has millions of elements. Every time an element is added, another completely new array containing millions of items is created. So the memory usage is basically double (plus one) the size of the array.

The more efficient way to add to an array is to move it back a level. So instead of:
foreach ($Item in $Items) {
    $Array += $Item
}
Use this:
$Array = foreach ($Item in $Items) {
    $Item
}
This creates the array once. Much quicker, much more efficient. I'm not a mathematician, but I've run my own tests and have seen the difference.

This took 1.2 seconds:
$Array = 1..50000 | ForEach-Object { "$_" }
This took 143 seconds:
1..50000 | ForEach-Object { $Array += "$_" }
I didn't have the patience to test higher than 50000. Your mileage may vary, but += is not going to win this race.

PowerShell: Get Windows Firewall Status for Remote Systems

Recently I needed to check the status of Windows Firewall on several remote systems. No guarantee of PowerShell v4+, so I had to improvise a bit.

There are multiple methods of pulling the Windows Firewall status, some of them easier than others. I settled on using PowerShell remoting and the netsh command. Since I first learned about PowerShell remoting, I have fallen in love with it. Makes non-interactive (local or RDP logon) administration so much easier. With no guarantee of PowerShell 4 (or even 3) I chose to use netsh to actually pull the firewall status.

First, the netsh command:
netsh advfirewall show domain state
Since these systems are on a domain, I check the domain profile and don't bother checking the others. For systems not on a domain, I would recommend checking the standard and/or public profiles.

The netsh command will return something like this:

Domain Profile Settings:
----------------------------------------------------------------------
State                                 ON
Ok.

What I want is the state, and don't care about the other lines. The line with the firewall state is element number 3.
@(netsh advfirewall show domain state)[3]
I don't want the extra spaces, so I used regex with -replace, which worked very well.
@(netsh advfirewall show domain state)[3] -replace 'State' -replace '\s' 
Now I just have to execute that script block on each computer in our list (which I pulled with a filter through Get-ADComputer).
$Computer = Get-ADComputer -Identity "SampleServer1"
$ScriptBlock = { @(netsh advfirewall show domain state)[3] -replace 'State' -replace '\s' }
$Status = Invoke-Command -ComputerName $Computer.Name -ScriptBlock $ScriptBlock
If you have multiple systems you need to check, build a custom object to hold the array. This will also allow you to output to a CSV rather than a plain text file.
$Object = [PSCustomObject]@{
     Computer = $Computer.Name
     Status = $Status
}
Full code below. I've added error handling for the system being unreachable.
#############################################################
##
## PowerShell script - Benjamin Hubbard
##
## This script outputs to display and CSV the status of the 
## Windows Firewall on the inputted computer list.
##
#############################################################

$ComputerListFilter = "Name -like 'Serv*'"

$ComputerList = Get-ADComputer -Filter $ComputerListFilter
$ScriptBlock = { @(netsh advfirewall show domain state)[3] -replace 'State' -replace '\s' }

foreach ($Computer in $ComputerList) {
    if (Test-Connection -ComputerName $Computer.Name -Quiet -Count 1) {
        try {
            $Status = Invoke-Command -ComputerName $Computer.Name -ErrorAction Stop -ScriptBlock $ScriptBlock
        }
        catch {
            $Status = "Unable to retrieve firewall status"
        }
    }
    else {
        $Status = "Unreachable"
    }
    $Object = [PSCustomObject]@{
        Computer = $Computer.Name
        Status = $Status
    }

    Write-Output $Object
    $Object | Export-Csv -Path "C:\FirewallStatus.csv" -Append -NoTypeInformation

}