DscResources/Carbon_FirewallRule/Carbon_FirewallRule.psm1

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

& (Join-Path -Path $PSScriptRoot -ChildPath '..\Initialize-CarbonDscResource.ps1' -Resolve)

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [bool]
        $Enabled = $true,

        [ValidateSet('In','Out')]
        [string]
        $Direction,

        [ValidateSet('Any','Domain','Private','Public')]
        [string[]]
        $Profile = @( 'Any' ),

        [string]
        $LocalIPAddress = 'Any',

        [string]
        $LocalPort,

        [string]
        $RemoteIPAddress = 'Any',

        [string]
        $RemotePort,

        [string]
        $Protocol = 'Any',

        [ValidateSet('Yes', 'No', 'DeferUser','DeferApp')]
        [string]
        $EdgeTraversalPolicy = 'No',

        [ValidateSet('Allow','Block','Bypass')]
        [string]
        $Action,

        [ValidateSet('Any','Wireless','LAN','RAS')]
        [string]
        $InterfaceType = 'Any',

        [ValidateSet('NotRequired','Authenticate','AuthEnc','AuthDynEnc','AuthNoEncap')]
        [string]
        $Security = 'NotRequired',

        [string]
        $Description,

        [string]
        $Program,

        [string]
        $Service,

        [ValidateSet('Present','Absent')]
        [string]
        $Ensure = 'Present'
    )

    Set-StrictMode -Version 'Latest'

    $rule = Get-CFirewallRule -LiteralName $Name
    if( $rule -is [object[]] )
    {
        Write-Error ('Found {0} firewall rules named ''{1}''.' -f $rule.Count,$Name)
        return
    }
    
    $resource = @{ 
                    'Action' = $Action;
                    'Description' = $Description;
                    'Direction' = $Direction;
                    'EdgeTraversalPolicy' = $EdgeTraversalPolicy
                    'Enabled' = $Enabled;
                    'Ensure' = 'Absent';
                    'InterfaceType' = $InterfaceType
                    'LocalIPAddress' = $LocalIPAddress;
                    'LocalPort' = $LocalPort;
                    'Name' = $Name;
                    'Profile' = $Profile;
                    'Program' = $Program;
                    'Protocol' = $Protocol;
                    'RemoteIPAddress' = $RemoteIPAddress;
                    'RemotePort' = $RemotePort;
                    'Security' = $Security;
                    'Service' = $Service;
                }

    if( $rule )
    {
        $propNames = $resource.Keys | ForEach-Object { $_ }
        $propNames | 
            Where-Object { $_ -ne 'Ensure' } |
            ForEach-Object { 
                $propName = $_
                switch( $propName )
                {
                    'Profile' { $value = $rule.Profile.ToString() -split ', ' }
                    'Enabled' { $value = $rule.Enabled }
                    default
                    {
                        $value = ($rule.$propName).ToString()
                    }
                }

                $resource[$propName] = $value
            }
        $resource.Ensure = 'Present'
    }

    return $resource
}
 
function Set-TargetResource
{
    <#
    .SYNOPSIS
    DSC resource for managing firewall rules.
 
    .DESCRIPTION
    The `Carbon_FirewallRule` resource manages firewall rules. It uses the `netsh advfirewall firewall` command. Please see [Netsh AdvFirewall Firewall Commands](http://technet.microsoft.com/en-us/library/dd734783.aspx) or run `netsh advfirewall firewall set rule` for documentation on how to configure the firewall.
 
    When modifying existing rules, only properties you pass are updated/changed. All other properties are left as-is.
 
    `Carbon_FirewallRule` is new in Carbon 2.0.
 
    .LINK
    Get-CFirewallRule
 
    .LINK
    http://technet.microsoft.com/en-us/library/dd734783.aspx
 
    .EXAMPLE
    >
    Demonstrates how to enable a firewall rule.
 
        Carbon_FirewallRule EnableHttpIn
        {
            Name = 'World Wide Web Services (HTTP Traffic-In)'
            Enabled = $true;
            Ensure = 'Present'
        }
 
    .EXAMPLE
    >
    Demonstrates how to delete a firewall rule.
 
        Carbon_FirewallRule DeleteMyRule
        {
            Name = 'MyCustomRule';
            Ensure = 'Absent';
        }
 
    There may be multiple rules with the same name, so we recommend disabling rules instead.
 
    .EXAMPLE
    >
    Demonstrates how to create/modify an incoming firewall rule.
 
        Carbon_FirewallRule MyAppPorts
        {
            Name = 'My App Ports';
            Action = 'Allow';
            Direction = 'In';
            Protocol = 'tcp';
            LocalPort = '8080,8180';
            Ensure = 'Present';
        }
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        # The name of the rule.
        $Name,

        [bool]
        # If `$true`, the rule is enabled. If `$false`, the rule is disabled.
        $Enabled = $true,

        [ValidateSet('In','Out')]
        [string]
        # If set to `In`, the rule applies to inbound network traffic. If set to `Out`, the rule applies to outbound traffic.
        $Direction,

        [ValidateSet('Any','Domain','Private','Public')]
        [string[]]
        # Specifies the profile(s) to which the firewall rule is assigned. The rule is active on the local computer only when the specified profile is currently active. Valid values are `Any`, `Domain`, `Public`, and `Private`.
        $Profile,

        [string]
        # The local IP addresses the rule applies to. Valid values are `any`, an exact IPv4 or IPv6 address, a subnet mask (e.g. 192.168.0.0/24), or a range. Separate each value with a comma; no spaces.
        $LocalIPAddress,

        [string]
        # The local port the rule applies to. Valid values are a specific port number, a range of port numbers (e.g. `5000-5010`), a comma-separate list of numbers and ranges, `any`, `rpc`, `rpc-epmap`, `Teredo`, and `iphttps`.
        $LocalPort,

        [string]
        # The remote IP addresses the rules applies to. Valid values are `any`, an exact IPv4 or IPv6 address, a subnet mask (e.g. 192.168.0.0/24), or a range. Separate each value with a comma; no spaces.
        $RemoteIPAddress,

        [string]
        # The remote port the rule applies to. Valid values are a specific port number, a range of port numbers (e.g. `5000-5010`), a comma-separate list of numbers and ranges, `any`, `rpc`, `rpc-epmap`, `Teredo`, and `iphttps`.
        $RemotePort,

        [string]
        # The protocol the rule applies to. Valid values are `any`, the protocol number, `icmpv4`, `icmpv6', `icmpv4:type,code`, `icmpv6:type,code`, `tcp`, or `udp`. Separate multiple values with a comma; no spaces.
        $Protocol,

        [ValidateSet('Yes', 'No', 'DeferUser','DeferApp')]
        [string]
        # For inbound rules, specifies that traffic that traverses an edge device, such as a Network Address Translation (NAT) enabled router, between the local and remote computer matches this rule. Valid values are `any`, `deferapp`, `deferuse`, or `no`.
        $EdgeTraversalPolicy,

        [ValidateSet('Allow','Block','Bypass')]
        [string]
        # Specifies what to do when packets match the rule. Valid values are `Allow`, `Block`, or `Bypass`.
        $Action,

        [ValidateSet('Any','Wireless','LAN','RAS')]
        [string]
        # Specifies that only network packets passing through the indicated interface types match this rule. Valid values are `Any`, `Wireless`, `LAN`, or `RAS`.
        $InterfaceType,

        [ValidateSet('NotRequired','Authenticate','AuthEnc','AuthDynEnc','AuthNoEncap')]
        [string]
        # Specifies that only network packets protected with the specified type of IPsec options match this rule. Valid values are `NotRequired`, `Authenticate`, `AuthEnc`, `AuthDynEnc`, or `AuthNoEncap`.
        $Security,

        [string]
        # A description of the rule.
        $Description,

        [string]
        # Specifies that network traffic generated by the identified executable program matches this rule.
        $Program,

        [string]
        # Specifies that traffic generated by the identified service matches this rule. The ServiceShortName for a service can be found in Services MMC snap-in, by right-clicking the service, selecting Properties, and examining Service Name.
        $Service,

        [ValidateSet('Present','Absent')]
        [string]
        # Set to `Present` to create the fireall rule. Set to `Absent` to delete it.
        $Ensure = 'Present'
    )

    Set-StrictMode -Version 'Latest'

    $resource = Get-TargetResource -Name $Name
    if( $Ensure -eq 'Absent' -and $resource.Ensure -eq 'Present' )
    {
        Write-Verbose ('Deleting firewall rule ''{0}''' -f $Name)
        $output = netsh advfirewall firewall delete rule name=$Name
        if( $LASTEXITCODE )
        {
            Write-Error ($output -join ([Environment]::NewLine))
            return
        }
        $output | Write-Verbose
        return
    }

    $cmd = 'add'
    $cmdDisplayName = 'Adding'
    $newArg = ''
    if( $Ensure -eq 'Present' -and $resource.Ensure -eq 'Present' )
    {
        $cmd = 'set'
        $cmdDisplayName = 'Setting'
        $newArg = 'new'
    }
    else
    {
        if( -not $Direction -and -not $Action )
        {
            Write-Error ('Parameters ''Direction'' and ''Action'' are required when adding a new firewall rule.')
            return
        }
        elseif( -not $Direction )
        {
            Write-Error ('Parameter ''Direction'' is required when adding a new firewall rule.')
            return
        }
        elseif( -not $Action )
        {
            Write-Error ('Parameter ''Action'' is required when adding a new firewall rule.')
            return
        }
    }

    $argMap = @{
                    'Direction' = 'dir';
                    'Enabled' = 'enable';
                    'LocalIPAddress' = 'localip';
                    'RemoteIPAddress' = 'remoteip';
                    'EdgeTraversalPolicy' = 'edge';
              }

    $netshArgs = New-Object 'Collections.Generic.List[string]'
    $resource.Keys |
        Where-Object { $_ -ne 'Ensure' -and $_ -ne 'Name' } |
        Where-Object { $PSBoundParameters.ContainsKey($_) } |
        ForEach-Object {
            $argName = $_.ToLowerInvariant()
            $argValue = $PSBoundParameters[$argName]
            if( $argValue -is [bool] )
            {
                $argValue = if( $argValue ) { 'yes' } else { 'no' }
            }
            if( $argMap.ContainsKey($argName) )
            {
                $argName = $argMap[$argName]
            }
            if( $argName -eq 'Profile' )
            {
                $argValue = $argValue -join ','
            }

            [void]$netshArgs.Add( ('{0}=' -f $argName) )
            [void]$netshArgs.Add( $argValue )
        }
    
    Write-Verbose ('{0} firewall rule ''{1}'': cmd= {2}; name= {3}; newArg: {4}; netshargs= {5}' -f $cmdDisplayName,$Name,$cmd,$Name,$newArg,($netshArgs -join ' '))
    Write-Debug -Message ('cmd= {0}; name= {1}; newArg: {2}; netshargs= {3}' -f $cmd,$Name,$newArg,($netshArgs -join ' '))
    $output = netsh advfirewall firewall $cmd rule name= $Name $newArg $netshArgs 
    if( $LASTEXITCODE )
    {
        Write-Error ($output -join ([Environment]::NewLine))
        return
    }
    $output | Write-Verbose
}

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [bool]
        $Enabled,

        [ValidateSet('In','Out')]
        [string]
        $Direction,

        [ValidateSet('Any','Domain','Private','Public')]
        [string[]]
        $Profile,

        [string]
        $LocalIPAddress,

        [string]
        $LocalPort,

        [string]
        $RemoteIPAddress,

        [string]
        $RemotePort,

        [string]
        $Protocol,

        [ValidateSet('Yes', 'No', 'DeferUser','DeferApp')]
        [string]
        $EdgeTraversalPolicy,

        [ValidateSet('Allow','Block','Bypass')]
        [string]
        $Action,

        [ValidateSet('Any','Wireless','LAN','RAS')]
        [string]
        $InterfaceType,

        [ValidateSet('NotRequired','Authenticate','AuthEnc','AuthDynEnc','AuthNoEncap')]
        [string]
        $Security,

        [string]
        $Description,

        [string]
        $Program,

        [string]
        $Service,

        [ValidateSet('Present','Absent')]
        [string]
        $Ensure = 'Present'
    )

    Set-StrictMode -Version 'Latest'

    $resource = Get-TargetResource @PSBoundParameters
    if( $Ensure -eq 'Absent' )
    {
        $result = ($resource.Ensure -eq 'Absent')
        if( $result )
        {
            Write-Verbose ('Firewall rule ''{0}'' not found.' -f $Name)
        }
        else
        {
            Write-Verbose ('Firewall rule ''{0}'' found.' -f $Name)
        }
        return $result
    }

    if( $Ensure -eq 'Present' -and $resource.Ensure -eq 'Absent' )
    {
        Write-Verbose ('Firewall rule ''{0}'' not found.' -f $Name)
        return $false
    }

    return Test-CDscTargetResource -TargetResource $resource -DesiredResource $PSBoundParameters -Target ('Firewall rule ''{0}''' -f $Name)
}