PowerShell Function: Get-LegacyFWRule

English   |   Código fuente

This post started out to be about a custom DSC Resource I've written (LegacyFirewall), however before getting to that I thought I'd start with a Function I'm using within that Resource. I think this is probably a more useful starting point, as I'm using PowerShell to run a dos command, and write the output into a PowerShell object. I end up using a similar Function/technique in a number of my scripts.

Regarding Desired State Configuration Resources, Microsoft with their DSC Resource Kit's have done a great thing to expand the Resources available in DSC (I think more of these should be 'shipped' and supported though), and as you'd expect these are all written to take full advantage of Windows Server 2012 and 2012 R2 and the PowerShell cmdlets available in those Operating Systems. This is all well and good, until you're forced to work with something 'out of scope'.

In my case, that is Windows Server 2008 R2. Due to vendor requirements, I have application servers that are only supported on this Operating System, and I had planned to use DSC to automate as much of the installation process as possible, from the base virtual machine, adding to domain, base config and finally the installation and configuration of the vendors application. As part of this deployment, I'm using a mix of PowerShell Modules I've written to handle specifics that require user intervention, and DSC to handle some specifics of the VM baseline, including firewall rules.

Firewall Rules, Server 2008 R2 and DSC, now there's a bit of a tricky situation! As part of the DSC Resource Kit's, Microsoft have released the xNetworking Module, which includes the xFirewall Resource, which is great! ... Except it uses the PowerShell NetFirewall cmdlets found in Windows Server 2012 and 2012 R2. To be honest that is exactly what I want Microsoft to be doing, except for this specific task when I wanted the same functionality for 2008 R2. So I went ahead and performed what is essentially a re-write of the Microsoft xFirewall DSC Resource utilizing the netsh command to handle the getting and setting of the firewall rules within my DSC Resource.

I'll split the details for my DSC Resource into a number of posts. If you're wondering about the 'c' in my function name, I use a 'c' at the start of most of my PowerShell functions/modules to indicate it is 'custom', especially useful to find these cmdlets with the Get-Command cmdlet or when using ISE to tab through cmdlets.

The primary change to Microsoft's Resource was to handle the different inputs/outputs of netsh compared to the NetFirewall cmdlets, and creating custom object handling for the netsh output so it would work nicely with PowerShell loops to test existing firewall rules against each element of the Resource. To support the gathering of firewall rules I created a function, Get-LegacyFWRule. This function is inside the DSC Resource, and is not exported.

Function Get-LegacyFWRule {
    Param ([string]$DisplayName)

    @(netsh advfirewall firewall show rule name="$DisplayName" dir=in verbose) | `
        Where {$_ -match '^([^:]+):\s*(\S.*)$' } | `
        Foreach `
            -Begin {
                $FirstRun = $true
                $HashProps = @{}
            } `
            -Process {
                If (($Matches[1] -eq 'Rule Name') -and (!($FirstRun))) {
                    New-Object -TypeName PSCustomObject -Property $HashProps
                    $HashProps = @{}
                }
                $HashProps.$($Matches[1]) = $Matches[2]
                $FirstRun = $false
            } `
            -End {
                New-Object -TypeName PSCustomObject -Property $HashProps
            }
}

And now to step through the main sections of the function:

@(netsh advfirewall firewall show rule name="$DisplayName" dir=in verbose) | `

This line simply retrieves all firewall rules matching the "$DisplayName" and pipes it into the regex command.

Where {$_ -match '^([^:]+):\s*(\S.*)$' } | `

This line pipes those firewall rules into a regex match, before piping that output further. I am by no means a regex expert, in fact I'm a rank noob. This is where I turned to trusty old Google.

This regex essentially keeps each line returned by netsh as long as there is a ":" in that line. It then separates those lines into two sections, the text before the ":" and the text after it.

For example, imagine if this were the first four lines output by netsh:

Rule Name: Excel

Enabled: Yes Direction: In The three highlighted rows will be matched by the regex, with for example on row 1, "$Matches[1]" being "Rule Name" and "$Matches[2]" being "Excel". This allows each line of the rule to be added nicely to a hashtable. For more information about the $Matches variable, see the Microsoft TechNet page on 'about_Automatic_Variables'. Essentially "$Matches[0]" is the 'whole base match', followed by "$Matches[1]" being the first 'matched content', "$Matches[2]" being the second 'matched content', etc.

This output is then piped into a ForEach using these methods:

-Begin {
    $FirstRun = $true
    $HashProps = @{}
}

The "-Begin" method is here to support the 'first' rule returned by netsh, and to create a blank hashtable used to create the first PowerShell object.

-Process {
    If (($Matches[1] -eq 'Rule Name') -and (!($FirstRun))) {
        New-Object -TypeName PSCustomObject -Property $HashProps
        $HashProps = @{}
    }
    $HashProps.$($Matches[1]) = $Matches[2]
    $FirstRun = $false
}

The "-Process" method handles the creation of the Hashtable, with it's properties and values.

The "If" statement is used to identify when the line being processed has 'Rule Name', which indicates a new firewall rule is found. When this is the case, take the existing Hashtable and create a new PSCustomObject based from that Hashtable (3rd line). Then, clear the Hashtable (4th line).

The 6th line adds each rule property found with associated value to the Hashtable.

The 7th line ensures the If statement is parsed for all rules outside of the first. The reason the first rule is different is because a Hashtable with associated firewall rule properties has not been created, so we don't want to create a PSCustomObject. Granted, technically there is a Hashtable as we created this in the '-Begin' method, but for the first rule it is empty when the '-Process' method is first run.

-End {
    New-Object -TypeName PSCustomObject -Property $HashProps
}

Finally, the '-End' method is required to ensure an object is created for the last firewall rule returned.

To see visually where each section of code is run, I simply add some Write-Host commands into the function and run it to find an existing Firewall Rule. Try this below, simply change the $DisplayName variable for a rule you have (to see this better, try and find a DisplayName that has multiple entries, (eg try 'Bonjour Service' if you have that installed). You'll see the output starts with 'Begin Method', 'Process Method', 'Setting Hashtable' and continues with the 'Process Method' and 'Setting Hashtable' until a 'New Rule Match' is found. The last entry will be the 'End Method'

See the additional 'Write-Host' lines here:

Function Get-LegacyFWRule {
    Param ([string]$DisplayName)

    @(netsh advfirewall firewall show rule name="$DisplayName" dir=in verbose) | `
        Where {$_ -match '^([^:]+):\s*(\S.*)$' } | `
        Foreach `
            -Begin {
                Write-Host 'Begin Method'
                $FirstRun = $true
                $HashProps = @{}
            } `
            -Process {
                Write-Host 'Process Method'
                If (($Matches[1] -eq 'Rule Name') -and (!($FirstRun))) {
                    Write-Host 'New Rule Match'
                    New-Object -TypeName PSCustomObject -Property $HashProps
                    $HashProps = @{}
                }
                Write-Host 'Setting Hashtable'
                $HashProps.$($Matches[1]) = $Matches[2]
                $FirstRun = $false
            } `
            -End {
                Write-Host 'End Method'
                New-Object -TypeName PSCustomObject -Property $HashProps
            }
}

Lastly, the advantage of using PowerShell objects for the output is you can use the output however you like, eg:

$Rules = Get-LegacyFWRule -DisplayName 'Bonjour Service'

# Pipe the object for Sorting or Selecting
$Rules | Sort 'Rule Name',Protocol | Select 'Rule Name',Protocol

# Count the number of rules found
$Rules.Count

Most 'wrappers' I've created to allow object creation from the output of a dos command are much simpler than this, as most commands I use only have a single 'object' as their output. I'm using similar 'wrappers' all through my scripts when I find the need, not just within DSC Resources.

If anyone can do the above in a nicer way please let me know, I'm always keen to improve my code. Hopefully this can help you figure out how to 'wrap' a dos command in PowerShell.

Good luck!