PowerShell DSC: cLegacyFirewall Test/Set Functions

  |   Source

This is the (very slowly written) follow up to my cLegacyFirewall Get-TargetResource post. This will cover off the two remaining DSC 'TargetResource' requirements for my cLegacyFirewall Desired State Configuration (DSC) Resource, however a final post is coming with the remaining helper functions.


The Test-TargetResource will check whether the specified firewall rule currently exists on the machine, and is used by DSC to check whether the Set-TargetResource function is required. This function is very similar to the Get-TargetResource, with the primary difference being it checks the properties of existing firewall rules to ensure all properties line up with the rule DSC is trying to create (/remove).

Here's the code for Test-TargetResource:

Function Test-TargetResource
{
    [OutputType([Boolean])]
    param
    (
        # Localized, user-facing name of the Firewall Rule being created
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]$DisplayName,

        # Enable or disable the supplied configuration
        [ValidateSet("Yes", "No")]
        [String]$Enabled = "Yes",

        # Direction of the connection
        [ValidateSet("In", "Out")]
        [String]$Direction = "In",

        # Specifies one or more profiles to which the rule is assigned
        [ValidateSet("Any", "Public", "Private", "Domain")]
        [String]$Profiles = "Any",

        # Local IP used for the filter
        $LocalIP = "Any",

        # Remote IP used for the filter
        $RemoteIP = "Any",

        # Path and file name of the program for which the rule is applied
        [String]$Program,

        # Local Port used for the filter
        $LocalPort,

        # Remote Port used for the filter
        $RemotePort,

        # IP Protocol used for the filter
        $Protocol,

        # Permit or Block the supplied configuration 
        [ValidateSet("Bypass", "Allow", "Block")]
        [String]$Action,

        # Ensure the presence/absence of the resource
        [ValidateSet("Present", "Absent")]
        [String]$Ensure = "Present"
    )

    Write-Verbose "TEST: Find rules with specified parameters"
    $FirewallRules = Get-cLegacyFWRule -DisplayName $DisplayName

    # Note 1
    if (!($FirewallRules.'Rule Name'))
    {
        Write-Verbose "TEST: Firewall Rule does not exist"

        $ReturnValue = (($Ensure -eq "Present") -eq $false)

        return $ReturnValue
    }

    $exists = $true
    $valid = $true
    foreach ($FirewallRule in $FirewallRules)
    {
        Write-Verbose "TEST: Check each defined parameter against the existing firewall rule [$($firewallRule.'Rule Name')]"
        # Note 2
        if (Test-RuleHasProperties -FirewallRule $FirewallRule `
                                    -DisplayName $DisplayName `
                                    -Enabled $Enabled `
                                    -Direction $Direction `
                                    -Profiles $Profiles `
                                    -LocalIP $LocalIP `
                                    -RemoteIP $RemoteIP `
                                    -Program $Program `
                                    -LocalPort $LocalPort `
                                    -RemotePort $RemotePort `
                                    -Protocol $Protocol `
                                    -Action $Action )
        {
        }
        else
        {
            $valid = $false
        }
    }

    # Returns whether or not $exists complies with $Ensure
    $ReturnValue = ($valid -and $exists -eq ($Ensure -eq "Present"))

    Write-Verbose "TEST: Returning $ReturnValue"

    return $ReturnValue
}

The primary sections to note:

The IF statement at "# Note 1" is used if the calling Firewall Rule does not exist. It will return $true/$false depending on the $Ensure value. eg if $Ensure = Present, and the Firewall Rule doesn't exist, the function will return $false. DSC would interpret this to mean the Set-TargetResource function is required to create the Firewall Rule.

The code at "# Note 2" will test the found Firewall Rules against the calling DSC Rule to confirm whether the property values line up. This code sets the $valid variable depending whether the properties line up correctly or not.

This code at "# Returns whether or not $exists complies with $Ensure" will return a $true/$false dependent on the existence of Firewall Rules, the $valid state of the rule and whether $Ensure = Present/Absent.

The Set-TargetResource function will create or delete the Firewall Rule being called, dependent on the value of $Ensure. The code is below:

Function Set-TargetResource
{
    param
    (
        # Localized, user-facing name of the Firewall Rule being created
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]$DisplayName,

        # Enable or disable the supplied configuration
        [ValidateSet("Yes", "No")]
        [String]$Enabled = "Yes",

        # Direction of the connection
        [ValidateSet("In", "Out")]
        [String]$Direction = "In",

        # Specifies one or more profiles to which the rule is assigned
        [ValidateSet("Any", "Public", "Private", "Domain")]
        [String]$Profiles = "Any",

        # Local IP used for the filter
        $LocalIP = "Any",

        # Remote IP used for the filter
        $RemoteIP = "Any",

        # Path and file name of the program for which the rule is applied
        [String]$Program,

        # Local Port used for the filter
        $LocalPort,

        # Remote Port used for the filter
        $RemotePort,

        # IP Protocol used for the filter
        $Protocol,

        # Permit or Block the supplied configuration 
        [ValidateSet("Bypass", "Allow", "Block")]
        [String]$Action,

        # Ensure the presence/absence of the resource
        [ValidateSet("Present", "Absent")]
        [String]$Ensure = "Present"
    )

    If (!($Action)) {$Action = "Allow"}

    Write-Verbose "SET: Get Rules for the specified DisplayName [$DisplayName]"
    $FirewallRules = Get-cLegacyFWRule -DisplayName $DisplayName

    $exists = If ($FirewallRules.'Rule Name') {$true} Else {$false}

    if ($Ensure -eq "Present")
    {
        Write-Verbose "SET: We want the firewall rule to exist since Ensure is set to $Ensure"
        if ($exists)
        {
            # Ensure the existing rule matches what we want
            Write-Verbose "SET: Checking for multiple rules with the same Displayname [$DisplayName]"
            If ($FirewallRules.Count -gt 1) {
                Write-Verbose "SET: Multiple rules found. Removing all rules before creating a new rule"
                $DeleteCommand = @(netsh advfirewall firewall delete rule name=$DisplayName)
            }

            Write-Verbose "SET: Check firewall rule for valid properties"
            foreach ($FirewallRule in $FirewallRules)
            {
                Write-Verbose "SET: Check each defined parameter against the existing firewall rule [$($FirewallRule.'Rule Name')]"
                if (Test-RuleHasProperties -FirewallRule $FirewallRule `
                                           -DisplayName $DisplayName `
                                           -Enabled $Enabled `
                                           -Direction $Direction `
                                           -Profiles $Profiles `
                                           -LocalIP $LocalIP `
                                           -RemoteIP $RemoteIP `
                                           -Program $Program `
                                           -LocalPort $LocalPort `
                                           -RemotePort $RemotePort `
                                           -Protocol $Protocol `
                                           -Action $Action)
                {
                    # Do nothing, firewall rule is correct
                }
                else
                {
                    # Remove firewall rule and create new rule with correct parameters
                    Write-Verbose "SET: Removing existing firewall rule [$DisplayName] to recreate one based on desired configuration"
                    $DeleteCommand = @(netsh advfirewall firewall delete rule name=$DisplayName)

                    $AddCommand = Add-cLegacyFWRule
                    If ($AddCommand -like "Ok*") {Write-Verbose "SET: The firewall rule [$DisplayName] was added"}
                }
            }
        }
        else
        {
            # Create the rules due to '$Ensure -eq "Present"'
            Write-Verbose "SET: We want the firewall rule [$DisplayName] to exist, but it does not"

            $AddCommand = Add-cLegacyFWRule
            If ($AddCommand -like "Ok*") {Write-Verbose "SET: The firewall rule [$DisplayName] was added"}
        }
    }
    elseif ($Ensure -eq "Absent")
    {
        Write-Verbose "SET: We do not want the firewall rule to exist"
        if ($exists)
        {
            # Remove the existing rule due to '$Ensure -eq "Absent"'
            Write-Verbose "SET: We do not want the firewall rule to exist, but it does. Removing the Rule(s)"
            $DeleteCommand = @(netsh advfirewall firewall delete rule name=$DisplayName)
            If ($DeleteCommand -like "*Deleted*rule*") {Write-Verbose "SET: The firewall rule with Displayname [$Displayname] was deleted"}
        }
        else
        {
            # Do Nothing
            Write-Verbose "SET: We do not want the firewall rule to exist, and it does not"
        }
    }
}

I have used Test-RuleHasProperties in this function as well just to confirm the Firewall Rules before the creation or deletion. This was primarily during my testing for the function and can likely be cleaned out a bit.

The primary lines of this function are:

Line 91->94: These lines will remove any existing Firewall Rule with the same $DisplayName as being called, then create a new Firewall Rule using the netsh command. It will then check to ensure the new rule was created correctly by searching for 'Ok' in the netsh command's output.

Line 114->115: If ($Ensure -eq Absent) these lines will remove the Firewall Rule and then confirm the deletion was successful by searching for 'Deletedrule*' in the netsh command's output.

I haven't made this post quite as detailed as the last, primarily due to its length (and it's been a long time coming, so I wanted to get it published!). I will follow up with the last post on this DSC Resource to cover the helper functions. By then, I'm hoping a copy of this will also be available in the master branch of PowerShell.org's cNetworking Resource on github.

Until next time... automate!