@ -1,273 +1,164 @@
#!powershell
#
# (c) 2014, Timothy Vandenbrande <timothy.vandenbrande@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (c) 2017 Artem Zinenko <zinenkoartem@gmail.com>
# Copyright (c) 2014 Timothy Vandenbrande <timothy.vandenbrande@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# WANT_JSON
# POWERSHELL_COMMON
# TODO: Reimplement this using Powershell cmdlets
function Parse-ProtocolType {
param ( $protocol )
$ErrorActionPreference = " Stop "
$protocolNumber = $protocol -as [ int ]
if ( $protocolNumber -is [ int ] ) {
return $protocolNumber
}
function convertToNetmask($maskLength ) {
[ IPAddress ] $ip = 0
$ip . Address = ( [ UInt32 ] :: MaxValue ) -shl ( 32 - $maskLength ) -shr ( 32 - $maskLength )
return $ip . IPAddressToString
switch -wildcard ( $protocol ) {
" tcp " { return [ System.Net.Sockets.ProtocolType ] :: Tcp -as [ int ] }
" udp " { return [ System.Net.Sockets.ProtocolType ] :: Udp -as [ int ] }
" icmpv4* " { return [ System.Net.Sockets.ProtocolType ] :: Icmp -as [ int ] }
" icmpv6* " { return [ System.Net.Sockets.ProtocolType ] :: IcmpV6 -as [ int ] }
default { throw " Unknown protocol ' $protocol '. " }
}
}
function ConvertTo-TitleCase($string ) {
return ( Get-Culture ) . TextInfo . ToTitleCase ( $string . ToLower ( ) )
}
# See 'Direction' constants here: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364724(v=vs.85).aspx
function Parse-Direction {
param ( $directionStr )
function ConvertTo-SortedKV($object , $unsupported = @ ( ) ) {
$output = " "
foreach ( $item in $object . GetEnumerator ( ) | Sort -Property Name ) {
if ( ( $item . Name -notin $unsupported ) -and ( $item . Value -ne $null ) ) {
$output + = " $( $item . Name ) : $( $item . Value ) `n "
switch ( $directionStr ) {
" in " { return 1 }
" out " { return 2 }
default { throw " Unknown direction ' $directionStr '. " }
}
}
return $output
}
function preprocessAndCompare($key , $outputValue , $fwsettingValue ) {
if ( $key -eq 'RemoteIP' ) {
if ( $outputValue -eq $fwsettingValue ) {
return $true
}
if ( $outputValue -eq $fwsettingValue + '-' + $fwsettingValue ) {
return $true
}
# See 'Action' constants here: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364724(v=vs.85).aspx
function Parse-Action {
param ( $actionStr )
if ( ( $outputValue -eq $fwsettingValue + '/32' ) -or ( $outputValue -eq $fwsettingValue + '/255.255.255.255' ) ) {
return $true
switch ( $actionStr ) {
" block " { return 0 }
" allow " { return 1 }
default { throw " Unknown action ' $actionStr '. " }
}
}
if ( $outputValue -match '^([\d\.]+)\/(\d+)$' ) {
$netmask = convertToNetmask ( $Matches [ 2 ] )
if ( $fwsettingValue -eq $Matches [ 1 ] + " / " + $netmask ) {
return $true
}
}
# Profile enum values: https://msdn.microsoft.com/en-us/library/windows/desktop/aa366303(v=vs.85).aspx
function Parse-Profiles
{
param ( $profilesStr )
if ( $fwsettingValue -match '^([\d\.]+)\/(\d+)$' ) {
$netmask = convertToNetmask ( $Matches [ 2 ] )
if ( $outputValue -eq $Matches [ 1 ] + " / " + $netmask ) {
return $true
}
}
$profiles = ( $profilesStr . Split ( ',' ) | Select -uniq | ForEach {
switch ( $_ ) {
" domain " { return 1 }
" private " { return 2 }
" public " { return 4 }
default { throw " Unknown profile ' $_ '. " }
}
} | Measure-Object -Sum ) . Sum
return $false
if ( $profiles -eq 7 ) { return 0x7fffffff }
return $profiles
}
function getFirewallRule ( $fwsettings ) {
$diff = $false
$result = @ {
changed = $false
identical = $false
exists = $false
failed = $false
msg = @ ( )
multiple = $false
}
function Parse-InterfaceTypes
{
param ( $interfaceTypesStr )
try {
$command = " netsh advfirewall firewall show rule name= `" $( $fwsettings . 'Rule Name' ) `" verbose "
#$output = Get-NetFirewallRule -name $($fwsettings.'Rule Name')
$result . output = Invoke-Expression $command | Where { $_ }
$rc = $LASTEXITCODE
if ( $rc -eq 1 ) {
$result . msg + = @ ( " No rule ' $name ' could be found " )
} elseif ( $rc -eq 0 ) {
# Process command output
$result . output | Where { $_ -match '^([^:]+):\s*(\S.*)$' } | ForEach -Begin {
$FirstRun = $true
$HashProps = @ { }
} -Process {
if ( ( $Matches [ 1 ] -eq 'Rule Name' ) -and ( -not $FirstRun ) ) {
$output = $HashProps
$HashProps = @ { }
}
$HashProps . $ ( $Matches [ 1 ] ) = $Matches [ 2 ]
$FirstRun = $false
} -End {
$output = $HashProps
return ( $interfaceTypesStr . Split ( ',' ) | Select -uniq | ForEach {
switch ( $_ ) {
" wireless " { return " Wireless " }
" lan " { return " Lan " }
" ras " { return " RemoteAccess " }
default { throw " Unknown interface type ' $_ '. " }
}
if ( $ ( $output | measure ) . count -gt 0 ) {
$diff = $false
$result . exists = $true
#$result.msg += @("The rule '$($fwsettings.'Rule Name')' exists.")
if ( $ ( $output | measure ) . count -gt 1 ) {
$result . multiple = $true
$result . msg + = @ ( " The rule ' $( $fwsettings . 'Rule Name' ) ' has multiple entries. " )
$result . diff = @ { }
$result . diff . after = ConvertTo-SortedKV $fwsettings
$result . diff . before = ConvertTo-SortedKV $rule $unsupported
if ( $result . diff . after -ne $result . diff . before ) {
$diff = $true
}
} else {
if ( $diff_support ) {
$result . diff = @ { }
$result . diff . after = ConvertTo-SortedKV $fwsettings
$result . diff . before = ConvertTo-SortedKV $output $unsupported
}
ForEach ( $fwsetting in $fwsettings . GetEnumerator ( ) ) {
if ( $output . $ ( $fwsetting . Key ) -ne $fwsettings . $ ( $fwsetting . Key ) ) {
if ( ( preprocessAndCompare -key $fwsetting . Key -outputValue $output . $ ( $fwsetting . Key ) -fwsettingValue $fwsettings . $ ( $fwsetting . Key ) ) ) {
Continue
} elseif ( ( $fwsetting . Key -eq 'DisplayName' ) -and ( $output . " Rule Name " -eq $fwsettings . $ ( $fwsetting . Key ) ) ) {
Continue
} elseif ( ( $fwsetting . Key -eq 'Program' ) -and ( $output . $ ( $fwsetting . Key ) -eq ( Expand-Environment ( $fwsettings . $ ( $fwsetting . Key ) ) ) ) ) {
# Ignore difference caused by expanded environment variables
Continue
} else {
$diff = $true
Break
}
}
}
}
if ( -not $diff ) {
$result . identical = $true
}
if ( $result . identical ) {
$result . msg + = @ ( " The rule ' $name ' exists and is identical " )
} else {
$result . msg + = @ ( " The rule ' $name ' exists but has different values " )
}
}
} else {
$result . failed = $true
}
} catch [ Exception ] {
$result . failed = $true
$result . error = $_ . Exception . Message
}
return $result
} ) -Join " , "
}
function createFireWallRule ( $fwsettings ) {
$result = @ {
changed = $false
failed = $false
msg = @ ( )
}
$command = " netsh advfirewall firewall add rule "
ForEach ( $fwsetting in $fwsettings . GetEnumerator ( ) ) {
if ( $fwsetting . value -ne $null ) {
switch ( $fwsetting . key ) {
" Direction " { $option = " dir " }
" Rule Name " { $option = " name " }
" Enabled " { $option = " enable " }
" Profiles " { $option = " profile " }
" InterfaceTypes " { $option = " interfacetype " }
" Security " { $option = " security " }
" Edge traversal " { $option = " edge " }
default { $option = $ ( $fwsetting . key ) . ToLower ( ) }
}
$command + = " $option =' $( $fwsetting . value ) ' "
}
}
function Parse-EdgeTraversalOptions
{
param ( $edgeTraversalOptionsStr )
try {
$rc = 0
if ( -not $check_mode ) {
$result . output = Invoke-Expression $command | Where { $_ }
$rc = $LASTEXITCODE
}
if ( $rc -eq 0 ) {
if ( $diff_support ) {
$result . diff = @ { }
$result . diff . after = ConvertTo-SortedKV $fwsettings
$result . diff . before = " "
}
$result . changed = $true
$result . msg + = @ ( " Created firewall rule ' $name ' " )
} else {
$result . failed = $true
$result . msg + = @ ( " Create command ' $command ' failed with rc= $rc " )
}
} catch [ Exception ] {
$result . error = $_ . Exception . Message
$result . failed = $true
$result . msg = @ ( " Failed to create the rule ' $name ' " )
switch ( $edgeTraversalOptionsStr ) {
" yes " { return 1 }
" deferapp " { return 2 }
" deferuser " { return 3 }
default { throw " Unknown edge traversal options ' $edgeTraversalOptionsStr '. " }
}
return $result
}
function removeFireWallRule ( $fwsettings ) {
$result = @ {
changed = $false
failed = $false
msg = @ ( )
}
function Parse-SecureFlags
{
param ( $secureFlagsStr )
$command = " netsh advfirewall firewall delete rule name=' $( $fwsettings . 'Rule Name' ) ' "
try {
$rc = 0
if ( -not $check_mode ) {
$result . output = Invoke-Expression $command | Where { $_ }
$rc = $LASTEXITCODE
$result . output | Where { $_ -match '^([^:]+):\s*(\S.*)$' } | Foreach -Begin {
$FirstRun = $true
$HashProps = @ { }
} -Process {
if ( ( $Matches [ 1 ] -eq 'Rule Name' ) -and ( -not $FirstRun ) ) {
$result . output = $HashProps
$HashProps = @ { }
}
$HashProps . $ ( $Matches [ 1 ] ) = $Matches [ 2 ]
$FirstRun = $false
} -End {
$result . output = $HashProps
}
}
if ( $rc -eq 0 -or $rc -eq 1 ) {
if ( $diff_support ) {
$result . diff = @ { }
$result . diff . after = " "
$result . diff . before = ConvertTo-SortedKV $fwsettings
switch ( $secureFlagsStr ) {
" authnoencap " { return 1 }
" authenticate " { return 2 }
" authdynenc " { return 3 }
" authenc " { return 4 }
default { throw " Unknown secure flags ' $secureFlagsStr '. " }
}
$result . changed = $true
$result . msg + = @ ( " Removed the rule ' $name ' " )
} else {
$result . failed = $true
$result . msg + = @ ( " Remove command ' $command ' failed with rc= $rc " )
}
} catch [ Exception ] {
$result . error = $_ . Exception . Message
$result . failed = $true
$result . msg + = @ ( " Failed to remove the rule ' $name ' " )
}
return $result
}
# FIXME: Unsupported keys
#$unsupported = @("Grouping", "Rule source")
$unsupported = @ ( " Rule source " )
function New-FWRule
{
param (
[ string ] $name ,
[ string ] $description ,
[ string ] $applicationName ,
[ string ] $serviceName ,
[ string ] $protocol ,
[ string ] $localPorts ,
[ string ] $remotePorts ,
[ string ] $localAddresses ,
[ string ] $remoteAddresses ,
[ string ] $direction ,
[ string ] $action ,
[ bool ] $enabled ,
[ string ] $profiles ,
[ string ] $interfaceTypes ,
[ string ] $edgeTraversalOptions ,
[ string ] $secureFlags
)
# INetFwRule interface description: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365344(v=vs.85).aspx
$rule = New-Object -ComObject HNetCfg . FWRule
$rule . Name = $name
$rule . Enabled = $enabled
if ( $description ) { $rule . Description = $description }
if ( $applicationName ) { $rule . ApplicationName = $applicationName }
if ( $serviceName ) { $rule . ServiceName = $serviceName }
if ( $protocol -and $protocol -ne " any " ) { $rule . Protocol = Parse-ProtocolType -protocol $protocol }
if ( $localPorts -and $localPorts -ne " any " ) { $rule . LocalPorts = $localPorts }
if ( $remotePorts -and $remotePorts -ne " any " ) { $rule . RemotePorts = $remotePorts }
if ( $localAddresses -and $localAddresses -ne " any " ) { $rule . LocalAddresses = $localAddresses }
if ( $remoteAddresses -and $remoteAddresses -ne " any " ) { $rule . RemoteAddresses = $remoteAddresses }
if ( $direction ) { $rule . Direction = Parse-Direction -directionStr $direction }
if ( $action ) { $rule . Action = Parse-Action -actionStr $action }
if ( $profiles ) { $rule . Profiles = Parse-Profiles -profilesStr $profiles }
if ( $interfaceTypes -and $interfaceTypes -ne " any " ) { $rule . InterfaceTypes = Parse-InterfaceTypes -interfaceTypesStr $interfaceTypes }
if ( $edgeTraversalOptions -and $edgeTraversalOptions -ne " no " ) {
# EdgeTraversalOptions property exists only from Windows 7/Windows Server 2008 R2: https://msdn.microsoft.com/en-us/library/windows/desktop/dd607256(v=vs.85).aspx
if ( $rule | Get-Member -Name 'EdgeTraversalOptions' ) {
$rule . EdgeTraversalOptions = Parse-EdgeTraversalOptions -edgeTraversalOptionsStr $edgeTraversalOptions
}
}
if ( $secureFlags -and $secureFlags -ne " notrequired " ) {
# SecureFlags property exists only from Windows 8/Windows Server 2012: https://msdn.microsoft.com/en-us/library/windows/desktop/hh447465(v=vs.85).aspx
if ( $rule | Get-Member -Name 'SecureFlags' ) {
$rule . SecureFlags = Parse-SecureFlags -secureFlagsStr $secureFlags
}
}
return $rule
}
$ErrorActionPreference = " Stop "
$result = @ {
changed = $false
fwsettings = @ { }
msg = @ ( )
}
$params = Parse-Args $args -supports_check_mode $true
@ -287,167 +178,99 @@ $remoteip = Get-AnsibleParam -obj $params -name "remoteip" -type "str" -default
$localport = Get-AnsibleParam -obj $params -name " localport " -type " str "
$remoteport = Get-AnsibleParam -obj $params -name " remoteport " -type " str "
$protocol = Get-AnsibleParam -obj $params -name " protocol " -type " str " -default " any "
$edge = Get-AnsibleParam -obj $params -name " edge " -type " str " -default " no " -validateset " no " , " yes " , " deferapp " , " deferuser "
$interfacetypes = Get-AnsibleParam -obj $params -name " interfacetypes " -type " str " -default " any "
$security = Get-AnsibleParam -obj $params -name " security " -type " str " -default " notrequired "
$edge = Get-AnsibleParam -obj $params -name " edge " -type " str " -default " no " -validateset " no " , " yes " , " deferapp " , " deferuser "
$security = Get-AnsibleParam -obj $params -name " security " -type " str " -default " notrequired " -validateset " notrequired " , " authnoencap " , " authenticate " , " authdynenc " , " authenc "
$state = Get-AnsibleParam -obj $params -name " state " -type " str " -default " present " -validateset " present " , " absent "
$force = Get-AnsibleParam -obj $params -name " force " -type " bool " -default $false
# Check the arguments
if ( $enabled ) {
$result . fwsettings . Add ( " Enabled " , " Yes " )
} else {
$result . fwsettings . Add ( " Enabled " , " No " )
}
$result . fwsettings . Add ( " Rule Name " , $name )
#$result.fwsettings.Add("displayname", $name)
if ( $state -eq " present " ) {
$result . fwsettings . Add ( " Direction " , $ ( ConvertTo-TitleCase ( $direction ) ) )
$result . fwsettings . Add ( " Action " , $ ( ConvertTo-TitleCase $action ) )
}
if ( $description -ne $null ) {
$result . fwsettings . Add ( " Description " , $description )
}
if ( $program -ne $null ) {
$result . fwsettings . Add ( " Program " , $program )
}
$result . fwsettings . Add ( " LocalIP " , $localip )
$result . fwsettings . Add ( " RemoteIP " , $remoteip )
if ( $localport -ne $null ) {
$result . fwsettings . Add ( " LocalPort " , $localport )
}
if ( $remoteport -ne $null ) {
$result . fwsettings . Add ( " RemotePort " , $remoteport )
}
if ( $service -ne $null ) {
$result . fwsettings . Add ( " Service " , $ ( ConvertTo-TitleCase ( $service ) ) )
}
if ( $protocol -eq " Any " ) {
$result . fwsettings . Add ( " Protocol " , $protocol )
} else {
$result . fwsettings . Add ( " Protocol " , $protocol . toupper ( ) )
}
if ( $profiles -eq " Any " ) {
$result . fwsettings . Add ( " Profiles " , " Domain,Private,Public " )
} else {
$result . fwsettings . Add ( " Profiles " , $ ( ConvertTo-TitleCase ( $profiles ) ) )
}
$result . fwsettings . Add ( " Edge traversal " , $ ( ConvertTo-TitleCase ( $edge ) ) )
if ( $interfacetypes -ne $null ) {
$result . fwsettings . Add ( " InterfaceTypes " , $ ( ConvertTo-TitleCase ( $interfacetypes ) ) )
}
switch ( $security ) {
" Authenticate " { $security = " Authenticate " }
" AuthDynEnc " { $security = " AuthDynEnc " }
" AuthEnc " { $security = " AuthEnc " }
" AuthNoEncap " { $security = " AuthNoEncap " }
" NotRequired " { $security = " NotRequired " }
}
$result . fwsettings . Add ( " Security " , $security )
# FIXME: Define unsupported options
#$result.fwsettings.Add("Grouping", "")
#$result.fwsettings.Add("Rule source", "Local Setting")
$get = getFirewallRule ( $result . fwsettings )
$result . msg + = $get . msg
if ( $get . failed ) {
$result . error = $get . error
$result . output = $get . output
Fail-Json $result $result . msg
if ( $diff_support ) {
$result . diff = @ { }
$result . diff . prepared = " "
}
$result . diff = $get . diff
if ( $state -eq " present " ) {
if ( -not $get . exists ) {
$create = createFireWallRule ( $result . fwsettings )
$result . msg + = $create . msg
$result . diff = $create . diff
if ( $create . failed ) {
$result . error = $create . error
$result . output = $create . output
Fail-Json $result $result . msg
try {
$fw = New-Object -ComObject HNetCfg . FwPolicy2
$existingRule = $fw . Rules | Where { $_ . Name -eq $name }
if ( $existingRule -is [ System.Array ] ) {
Fail-Json $result " Multiple firewall rules with name ' $name ' found. "
}
$rule = New-FWRule -name $name `
-description $description `
-direction $direction `
-action $action `
-applicationName $program `
-serviceName $service `
-enabled $enabled `
-profiles $profiles `
-localAddresses $localip `
-remoteAddresses $remoteip `
-localPorts $localport `
-remotePorts $remoteport `
-protocol $protocol `
-interfaceTypes $interfacetypes `
-edgeTraversalOptions $edge `
-secureFlags $security
$fwPropertiesToCompare = @ ( 'Name' , 'Description' , 'Direction' , 'Action' , 'ApplicationName' , 'ServiceName' , 'Enabled' , 'Profiles' , 'LocalAddresses' , 'RemoteAddresses' , 'LocalPorts' , 'RemotePorts' , 'Protocol' , 'InterfaceTypes' , 'EdgeTraversalOptions' , 'SecureFlags' )
if ( $state -eq " absent " ) {
if ( $existingRule -eq $null ) {
$result . msg = " Firewall rule ' $name ' does not exist. "
} else {
if ( $diff_support ) {
foreach ( $prop in $fwPropertiesToCompare ) {
$result . diff . prepared + = " -[ $( $prop ) =' $( $existingRule . $prop ) '] `n "
}
$result . changed = $true
} elseif ( -not $get . identical ) {
# FIXME: This ought to use netsh advfirewall firewall set instead !
if ( $force ) {
$remove = removeFirewallRule ( $result . fwsettings )
# NOTE: We retain the diff output from $get.diff here
$result . msg + = $remove . msg
if ( $remove . failed ) {
$result . error = $remove . error
$result . output = $remove . output
Fail-Json $result $result . msg
}
$create = createFireWallRule ( $result . fwsettings )
# NOTE: We retain the diff output from $get.diff here
$result . msg + = $create . msg
if ( $create . failed ) {
$result . error = $create . error
$result . output = $create . output
Fail-Json $result $result . msg
if ( -not $check_mode ) {
$fw . Rules . Remove ( $existingRule . Name )
}
$result . changed = $true
$result . msg = " Firewall rule ' $name ' removed. "
}
} elseif ( $state -eq " present " ) {
if ( $existingRule -eq $null ) {
if ( $diff_support ) {
foreach ( $prop in $fwPropertiesToCompare ) {
$result . diff . prepared + = " +[ $( $prop ) =' $( $existingRule . $prop ) '] `n "
}
}
} else {
$result . msg + = @ ( " There was already a rule ' $name ' with different values, use the 'force' parameter to overwrite it " )
Fail-Json $result $result . msg
if ( -not $check_mode ) {
$fw . Rules . Add ( $rule )
}
$result . changed = $true
$result . msg = " Firewall rule ' $name ' created. "
} else {
$result . msg + = @ ( " Firewall rule ' $name ' was already created " )
foreach ( $prop in $fwPropertiesToCompare ) {
if ( $existingRule . $prop -ne $rule . $prop ) {
if ( $diff_support ) {
$result . diff . prepared + = " -[ $( $prop ) =' $( $existingRule . $prop ) '] `n "
$result . diff . prepared + = " +[ $( $prop ) =' $( $rule . $prop ) '] `n "
}
} elseif ( $state -eq " absent " ) {
if ( $get . exists ) {
$remove = removeFirewallRule ( $result . fwsettings )
$result . diff = $remove . diff
$result . msg + = $remove . msg
if ( $remove . failed ) {
$result . error = $remove . error
$result . output = $remove . output
Fail-Json $result $result . msg
if ( -not $check_mode ) {
$existingRule . $prop = $rule . $prop
}
$result . changed = $true
}
}
if ( $result . changed ) {
$result . msg = " Firewall rule ' $name ' changed. "
} else {
$result . msg + = @ ( " Firewall rule ' $name ' did not exist " )
$result . msg = " Firewall rule ' $name ' already exists. "
}
}
}
} catch [ Exception ] {
Fail-Json $result $_ . Exception . Message
}
Exit-Json $result