Public/psf-fwmgr.ps1
function ConvertTo-FalconFirewallRule { <# .SYNOPSIS Convert firewall rules to be compatible with Falcon Firewall Management .DESCRIPTION Ensures that an object (either from the pipeline, or via CSV import) has the required properties to be accepted as a valid Falcon Firewall Management rule. Rules that contain both IPv4 and IPv6 addresses will generate errors, along with any rules that are missing the required properties defined by the 'Map' parameter. Converted rules used with 'New-FalconFirewallGroup' to create groups containing newly converted rules. .PARAMETER Map A hashtable containing the following keys with the corresponding CSV column or rule property as the value Required: action, description, direction, enabled, local_address, local_port, name, remote_address, remote_port Optional: image_name, network_location, service_name .PARAMETER Path Path to a CSV file containing rules to convert .PARAMETER Object An existing rule object to convert .LINK https://github.com/crowdstrike/psfalcon/wiki/ConvertTo-FalconFirewallRule #> [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [Parameter(ParameterSetName='Pipeline',Mandatory,Position=1)] [Parameter(ParameterSetName='CSV',Mandatory,Position=1)] [ValidateScript({ foreach ($Key in @('action','description','direction','enabled','local_address','local_port','name', 'remote_address','remote_port')) { if ($_.Keys -notcontains $Key) { throw "Missing required '$Key' property." } else { $true } } })] [hashtable]$Map, [Parameter(ParameterSetName='CSV',Mandatory,Position=2)] [ValidateScript({ if (Test-Path $_ -PathType Leaf) { $true } else { throw "Cannot find path '$_' because it does not exist or is a directory." } })] [Alias('FullName')] [string]$Path, [Parameter(ParameterSetName='Pipeline',Mandatory,ValueFromPipeline)] [object]$Object ) begin { function Get-RuleAction ([object]$Rule,[hashtable]$Map) { if ($Rule.($Map.action) -eq 'BLOCK') { 'DENY' } else { $Rule.($Map.action).ToUpper() } } function Get-RuleDirection ([object]$Rule,[hashtable]$Map) { try { [regex]::Match($Rule.($Map.direction),'^(in|out|both)',1).Value.ToUpper() } catch {} } function Get-RuleFamily ([object]$Rule,[hashtable]$Map,[string]$Protocol,[string[]]$TypeList) { if ($Protocol -eq '1') { # Force 'IP4' when protocol is 'ICMPv4' 'IP4' } elseif ($Protocol -eq '58') { # Force 'IP6' when protocol is 'ICMPv6' 'IP6' } else { # Use unique value from 'TypeList' and default to 'IP4' when 'TypeList' is 'ANY' [string]$Output = (($TypeList | Select-Object -Unique) -replace 'v',$null -replace 'ANY', $null).ToUpper() if ($Output) { ($Output).Trim() } else { 'IP4' } } } function Get-RuleProtocol ([object]$Rule,[hashtable]$Map) { if ($Rule.($Map.protocol) -match '^(any|\*)$') { # Use asterisk for 'any' '*' } elseif ($Rule.($Map.protocol) -as [int] -is [int]) { # Use existing integer value $Rule.($Map.protocol) } else { switch ($Rule.($Map.protocol)) { # Convert expected protocol names to their numerical value 'icmpv4' { '1' } 'tcp' { '6' } 'udp' { '17' } 'icmpv6' { '58' } } } } function New-RuleAddress ([string]$String,[hashtable]$Map,[string]$Join,[string]$RuleName) { foreach ($Address in ($String -split $Join)) { # Remove excess spaces [string]$Address = $Address.Trim() if ($Address -match '^(any|\*)$') { # Output 'any' address and netmask [PSCustomObject]@{ address = '*'; netmask = 0 } } else { # Check whether address matches ipv4 or ipv6 [string]$Type = Test-RegexValue ($Address -replace '/\d+$',$null) [int]$Integer = if ($Address -match '/') { # Collect netmask from CIDR notation ($Address -split '/',2)[-1] $Address = $Address -replace '/\d+$' } elseif ($Type -eq 'ipv6') { # Use default for ipv6 address 128 } elseif ($Type -eq 'ipv4') { # Use default for ipv4 address 32 } else { throw "Rule '$RuleName' contains an address that does not match IPv4 or IPv6 pattern. ['$( $Address)']" } # Output object with address and netmask if ($Address -and $Integer) { [PSCustomObject]@{ address = $Address; netmask = $Integer }} } } } function New-RuleField ([object]$Rule,[hashtable]$Map,[string]$Join) { # Create default 'fields' array [string[]]$Location = if ($Rule.($Map.network_location) -and ($Rule.($Map.network_location) -notmatch '^(any|\*)$')) { # Add 'network_location' values @($Rule.($Map.network_location) -split $Join).foreach{ $_ } } else { 'ANY' } [PSCustomObject[]]([PSCustomObject]@{ name = 'network_location'; type = 'set'; values = $Location }) } function New-RulePort ([string]$String,[string]$Join) { if ($String -notmatch '^(any|\*)$') { # Create 'port' objects @($String -split $Join).foreach{ if ($_ -match '-') { # Split ranges into 'start' and 'end' [int[]]$Range = $_ -split '-',2 [PSCustomObject]@{ start = $Range[0]; end = $Range[1] } } else { # Create separate objects for each value when multiple are provided [PSCustomObject]@{ start = [int]$_; end = 0 } } } } } function Convert-RuleObject ([object]$Rule,[hashtable]$Map) { # Set RegEx pattern to split port/address strings and create object string for error messaging [string]$Join = '[;,-]' try { [string]$Protocol = Get-RuleProtocol $Rule $Map if (!$Protocol) { throw "Rule '$($Rule.($Map.name))' contains unexpected protocol '$($Rule.($Map.protocol))'." } [string[]]$TypeList = foreach ($Type in ('local_address','remote_address')) { @($Rule.($Map.$Type) -split $Join).foreach{ if ($_.Trim() -match '^(any|\*)$') { 'ANY' } else { # Error when 'local_address' or 'remote_address' does not match ipv4/ipv6 [string]$Trim = ($_.Trim() -replace '/\d+$',$null) if (!$Trim) { throw "Rule '$($Rule.($Map.name))' missing value for required property '$Type'." } [string]$Test = Test-RegexValue $Trim if ($Test -match '^ipv(4|6)$') { [string]($Test -replace 'v',$null).ToUpper() } else { throw "Rule '$($Rule.($Map.name))' contains unexpected $Type '$Trim'." } } } } if ($TypeList -contains 'IP4' -and $TypeList -contains 'IP6') { # Error when rules contain both ipv4 and ipv6 addresses throw "Rule '$($Rule.($Map.name))' contains both ipv4 and ipv6 addresses." } else { foreach ($Name in ('action','address_family','direction')) { # Set 'action', 'family' and 'direction' $Value = if ($Name -eq 'address_family') { Get-RuleFamily $Rule $Map $Protocol $TypeList } else { & "Get-Rule$Name" $Rule $Map } if ($Name -eq 'address_family' -and $Value -cnotmatch '^IP[4|6]$') { # Error when unexpected value is provided throw "Unable to determine $Name for rule '$($Rule.($Map.name))'." } elseif (($Name -eq 'action' -and $Value -cnotmatch '^(ALLOW|DENY)$') -or ($Name -eq 'direction' -and $Value -cnotmatch '^(BOTH|IN|OUT)$')) { throw "Rule '$($Rule.($Map.name))' contains unexpected $Name '$( $Rule.($Map.$Name))'." } else { New-Variable -Name $Name -Value $Value } } @('local_address','remote_address').foreach{ # Create 'local_address' and 'remote_address' objects New-Variable -Name $_ -Value ([PSCustomObject[]]( New-RuleAddress $Rule.($Map.$_) $Map $Join $Rule.($Map.name))) } # Construct default 'fields' [PSCustomObject[]]$Field = New-RuleField $Rule $Map foreach ($Name in ('image_name','service_name')) { # Add 'image_name' and 'service_name' to 'fields', when present if ($Rule.($Map.$Name) -and $Rule.($Map.$Name) -notmatch '^(any|\*)$') { [string]$Value = if ($Name -eq 'image_name' -and $Rule.($Map.$Name) -notmatch '\.\w+$') { # Convert directory paths to glob syntax with a single asterisk [string]$Glob = $Rule.($Map.$Name) -replace '^\w:\\',$null if ($Glob -match '\\$') { $Glob,'*' -join $null } else { $Glob,'*' -join '\' } } else { $Rule.($Map.$Name) } $Field += [PSCustomObject]@{ name = $Name type = if ($_ -eq 'image_name') { 'windows_path' } else { 'string' } value = $Value } } } # Create rule object $Output = [PSCustomObject]@{ action = $action address_family = $address_family description = $Rule.($Map.description) direction = $direction enabled = if ($Rule.($Map.enabled) -match '$?true') { $true } else { $false } fields = $Field name = $Rule.($Map.name) protocol = $Protocol local_address = $local_address remote_address = $remote_address } @('local_port','remote_port').foreach{ # Add 'local_port' and 'remote_port' New-Variable -Name $_ -Value ([PSCustomObject[]](New-RulePort $Rule.($Map.$_) $Join)) if ((Get-Variable -Name $_).Value) { $Output.PSObject.Properties.Add((New-Object PSNoteProperty($_, (Get-Variable -Name $_).Value))) } } $Output } } catch { throw $_ } } } process { if (!$Path) { # Convert pipeline object into formatted rule Convert-RuleObject ([PSCustomObject]$Object | Select-Object @($Map.Values)) $Map } } end { if ($Path) { # Import CSV and convert rules to expected format Import-Csv $Path | & $MyInvocation.MyCommand.Name -Map $Map } } } |