Public/Get-AACFirewallRule.ps1
|
function Get-AACFirewallRule { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Gets every Azure Firewall Policy rule - DNAT, network and application: a colour-coded Spectre.Console view at the prompt, PowerShell objects down a pipeline, and optional CSV, PDF and interactive HTML exports. .DESCRIPTION Reads the rules of every Azure Firewall Policy the signed-in account can see (or only those in -SubscriptionId / -FirewallPolicyName) from Azure Resource Graph, over REST with the Connect-AAC sign-in - no Az modules needed. One object is returned per rule, sorted as the Azure portal lists them: by policy, rule collection group priority, rule collection priority, then the rule's position in its collection. (Azure Firewall itself applies all DNAT rules first, then network, then application rules, each in that priority order, and a base policy's rules before the policy's own.) IP Groups used as a source or destination are resolved to their names and addresses, wherever in the tenant they live (one Resource Graph query, no per-group calls). Each rule also carries its policy's base (parent) policy and the firewalls the policy is attached to. What you get depends on where the command runs: at the prompt a Spectre.Console view: the account and scope, tiles with the number of rules, allow, deny and DNAT rules, policies and collections, a table of the policies, then one table per rule collection in priority order - Allow collections bordered in green, Deny in red, DNAT in orange - shown a screen at a time piped onward the AAC.FirewallRule objects, with no view -PassThru the view and the objects -NoDisplay the objects only (scripts, scheduled tasks) PowerShell can't tell "$r = Get-AACFirewallRule" from a plain call, so to keep the objects in a variable add -PassThru or -NoDisplay. In the view, IP Groups are shown by name with their addresses under them, and an Allow rule open to any source or destination has its '*' called out in yellow. When the view is longer than the terminal it is paged: press any key for the next page, or A for the rest. Exports: -CsvPath a CSV file written with Export-Csv (UTF-8, one row per rule, list values joined with ", ") -PdfPath a landscape A4 PDF: a summary of each policy, then every rule grouped by policy, rule collection group and rule collection, with Allow in green, Deny in red and DNAT in amber -HtmlPath a self-contained, interactive HTML report: clickable tiles and charts (by action, policy, rule collection, rule type) that filter a table of every rule, grouped by rule collection, with search, filters, sorting, Allow rules open to any address flagged, Azure portal links and a CSV download of what is shown When any of -CsvPath, -PdfPath or -HtmlPath is given, the console shows only the progress and the files written - the report is in the files. Add -PassThru to get the objects as well. Search: -SourceAddress, -DestinationAddress, -Port, -Protocol, -Fqdn, -Action and -RuleName narrow the rules to those that match - every filter given must match, and any one of a filter's values. Addresses and ports match by containment, so the search answers "which rules let this source reach that destination on this port?": 10.1.2.3 matches a rule for 10.1.0.0/16, for an IP Group holding it, or for '*'; a CIDR or 'a-b' range matches rules that overlap it; port 443 matches '443', '400-500' and '*'. Service tags (e.g. AzureCloud) aren't expanded, so they only match a search for them by -RuleName. Only Firewall Policy rules are read. Classic rules configured directly on a firewall (without a policy) are not included. Rules a policy inherits from its base policy appear under the base policy. PDF export needs Windows and PowerShell 7.4 or later; objects and CSV work everywhere. .PARAMETER SubscriptionId Only get policies in these subscriptions. Defaults to every subscription the signed-in account can see. IP Groups and base policies are resolved in every subscription either way. .PARAMETER FirewallPolicyName Only get policies whose name matches one of these wildcard patterns (case-insensitive), e.g. 'fwpol-hub-*'. .PARAMETER SourceAddress Only rules whose source covers or overlaps one of these addresses: IPs, CIDR prefixes or 'a-b' ranges, IPv4 or IPv6. The rule's IP Groups count, and a '*' source matches any address. .PARAMETER DestinationAddress Only rules whose destination covers or overlaps one of these addresses, as for -SourceAddress. .PARAMETER Port Only rules whose destination ports include one of these ports or overlap one of these ranges ('443', '8000-8080'). For application rules, the port of each protocol ('Https:443') counts. .PARAMETER Protocol Only rules for one of these protocols: TCP, UDP, ICMP (network and DNAT rules - a rule for 'Any' matches all), or Http, Https, Mssql (application rules). .PARAMETER Fqdn Only rules for one of these host names - '*.contoso.com' in a rule covers 'www.contoso.com' - or whose FQDNs match one of these wildcard patterns. .PARAMETER Action Only rules in collections with one of these actions: Allow, Deny, DNAT. .PARAMETER RuleName Only rules whose name matches one of these wildcard patterns. .PARAMETER CsvPath Also write the rules to this CSV file. An existing file is overwritten; missing folders are created. .PARAMETER PdfPath Also write the rules to this PDF file. An existing file is overwritten; missing folders are created. .PARAMETER HtmlPath Also write an interactive HTML report to this file. An existing file is overwritten; missing folders are created. .PARAMETER Title The PDF and HTML report's title. Defaults to 'Azure Firewall rules'. .PARAMETER PassThru Show the view and also return the rule objects. .PARAMETER NoDisplay Return the rule objects without showing the view. .PARAMETER NoPaging Show the whole view at once instead of a screen at a time. .EXAMPLE Connect-AAC Get-AACFirewallRule Shows every Firewall Policy rule you can see, policy by policy. .EXAMPLE Get-AACFirewallRule -CsvPath .\out\FirewallRules.csv -PdfPath .\out\FirewallRules.pdf Writes every rule to a CSV file and a PDF report; the console shows the progress and the files. .EXAMPLE Get-AACFirewallRule -SubscriptionId '00000000-0000-0000-0000-000000000000' | Where-Object { $_.Action -eq 'Allow' -and $_.SourceAddresses -match '(^|, )\*($|,)' } | Format-Table FirewallPolicy, RuleCollection, RuleName, DestinationPorts Lists allow rules open to any source in one subscription. .EXAMPLE Get-AACFirewallRule -FirewallPolicyName 'fwpol-hub-*' -PdfPath .\HubFirewall.pdf -Title 'Hub firewall rules' The hub firewall policies only, as a PDF. .EXAMPLE Get-AACFirewallRule -HtmlPath .\out\FirewallRules.html Every rule in an interactive HTML report to search, filter and share. .EXAMPLE Get-AACFirewallRule -SourceAddress 10.1.2.3 -DestinationAddress 10.0.0.4 -Port 53 -Protocol UDP Which rules let 10.1.2.3 reach 10.0.0.4 on UDP 53 - allow and deny - in priority order. .EXAMPLE Get-AACFirewallRule -Action Allow -SourceAddress 0.0.0.0/0 -Port 3389, 22 Allow rules for RDP or SSH from any address. .EXAMPLE Get-AACFirewallRule -Fqdn www.contoso.com -Protocol Https -CsvPath .\contoso.csv Application rules that cover www.contoso.com over HTTPS, also saved as a CSV file. .EXAMPLE $rules = Get-AACFirewallRule -NoDisplay Keeps the rule objects in a variable, without showing the view. .EXAMPLE Get-AACFirewallRule | Export-Csv -Path .\rules.csv -NoTypeInformation -Delimiter ';' Uses Export-Csv directly, for control over its options. .OUTPUTS AAC.FirewallRule (piped onward, or with -PassThru or -NoDisplay) #> [CmdletBinding()] [OutputType('AAC.FirewallRule')] param( [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string[]] $SubscriptionId, [SupportsWildcards()] [string[]] $FirewallPolicyName, [string[]] $SourceAddress, [string[]] $DestinationAddress, [string[]] $Port, [string[]] $Protocol, [SupportsWildcards()] [string[]] $Fqdn, [ValidateSet('Allow', 'Deny', 'DNAT')] [string[]] $Action, [SupportsWildcards()] [string[]] $RuleName, [string] $CsvPath, [string] $PdfPath, [string] $HtmlPath, [string] $Title = 'Azure Firewall rules', [switch] $PassThru, [switch] $NoDisplay, [switch] $NoPaging ) # Piped onward (| Where-Object, | Export-Csv ...) the objects are the # point, so no view is drawn over them. # An export means the report is in the files: the console shows only # the title, the progress and the files written. $pipedOnward = $MyInvocation.PipelinePosition -lt $MyInvocation.PipelineLength $interactive = -not $NoDisplay -and -not $pipedOnward $exporting = [bool]($CsvPath -or $PdfPath -or $HtmlPath) $showView = $interactive -and -not $exporting $returnObjects = $PassThru -or $NoDisplay -or $pipedOnward # Resolve paths now, relative to the caller's location, so a bad path # fails before any Azure call. $csvFullPath = if ($CsvPath) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($CsvPath) } $pdfFullPath = if ($PdfPath) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PdfPath) } $htmlFullPath = if ($HtmlPath) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($HtmlPath) } $headers = @{ Authorization = "Bearer $(Get-AACAccessToken)" } $ruleQuery = @' networkresources | where type =~ 'microsoft.network/firewallpolicies/rulecollectiongroups' | extend firewallPolicyId = tolower(substring(id, 0, indexof(tolower(id), '/rulecollectiongroups/'))) | mv-expand ruleCollection = properties.ruleCollections | mv-expand with_itemindex = ruleIndex rule = ruleCollection.rules | project subscriptionId, resourceGroup, location, firewallPolicyId, ruleCollectionGroupId = id, ruleCollectionGroup = name, ruleCollectionGroupPriority = toint(properties.priority), ruleCollection = tostring(ruleCollection.name), ruleCollectionPriority = toint(ruleCollection.priority), ruleCollectionType = tostring(ruleCollection.ruleCollectionType), action = tostring(ruleCollection.action.type), ruleIndex, rule '@ $policyQuery = @' resources | where type =~ 'microsoft.network/firewallpolicies' | project id = tolower(id), name, basePolicyId = tolower(tostring(properties.basePolicy.id)), firewalls = properties.firewalls, tier = tostring(properties.sku.tier) '@ $ipGroupQuery = @' resources | where type =~ 'microsoft.network/ipgroups' | project id = tolower(id), name, addresses = properties.ipAddresses '@ $subscriptionQuery = "resourcecontainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, name" # The title first, then a line per step - as every command shows them. if ($interactive) { Write-AACRule -Title 'Azure Admin Console :: Azure Firewall' -Color 'deepskyblue3_1' } $data = Invoke-AACProgress -ScriptBlock { Update-AACProgress -Id 'read' -Total 4 -Description 'Reading firewall policy rules from Azure Resource Graph' $ruleRows = @(Invoke-AACResourceGraphQuery -SubscriptionId $SubscriptionId -Headers $headers -Query $ruleQuery) # Base policies, IP Groups and subscription names can live in any # subscription, so these three are read tenant-wide. Update-AACProgress -Id 'read' -Increment 1 -Description 'Reading firewall policies' $policyRows = @(Invoke-AACResourceGraphQuery -Headers $headers -Query $policyQuery) Update-AACProgress -Id 'read' -Increment 1 -Description 'Reading IP Groups' $ipGroupRows = @(Invoke-AACResourceGraphQuery -Headers $headers -Query $ipGroupQuery) Update-AACProgress -Id 'read' -Increment 1 -Description 'Reading subscription names' $subscriptionRows = @(Invoke-AACResourceGraphQuery -Headers $headers -Query $subscriptionQuery) $policyCount = @($ruleRows | ForEach-Object { $_.firewallPolicyId } | Select-Object -Unique).Count Update-AACProgress -Id 'read' -Complete -Description ('Read {0:N0} firewall rule(s) in {1:N0} policy(ies), {2:N0} IP Group(s)' -f $ruleRows.Count, $policyCount, $ipGroupRows.Count) @{ Rules = $ruleRows; Policies = $policyRows; IpGroups = $ipGroupRows; Subscriptions = $subscriptionRows } } $lastSegment = { param([string] $Id) if ($Id) { $Id.TrimEnd('/').Split('/')[-1] } } $join = { param($Values) (@($Values) | Where-Object { $null -ne $_ -and "$_" -ne '' }) -join ', ' } $policies = @{} foreach ($policy in $data.Policies) { $policies[$policy.id] = $policy } $ipGroups = @{} foreach ($ipGroup in $data.IpGroups) { $ipGroups[$ipGroup.id] = $ipGroup } $subscriptionNames = @{} foreach ($subscription in $data.Subscriptions) { $subscriptionNames[$subscription.subscriptionId] = $subscription.name } # IP Group IDs -> "name" list and "name: address, address" list. An IP # Group the account can't read is shown by the name in its ID. $describeIpGroups = { param($Ids) $names = [System.Collections.Generic.List[string]]::new() $addresses = [System.Collections.Generic.List[string]]::new() foreach ($id in @($Ids | Where-Object { $_ })) { $ipGroup = $ipGroups[([string]$id).ToLowerInvariant()] if ($ipGroup) { $names.Add($ipGroup.name) $addresses.Add("$($ipGroup.name): $(& $join $ipGroup.addresses)") } else { $name = & $lastSegment $id $names.Add($name) $addresses.Add("${name}: (not readable)") } } @{ Names = $names -join ', '; Addresses = $addresses -join ' | ' } } $rows = @($data.Rules) if ($FirewallPolicyName) { $rows = @($rows | Where-Object { $policyName = & $lastSegment $_.firewallPolicyId @($FirewallPolicyName | Where-Object { $policyName -like $_ }).Count -gt 0 }) } $rules = foreach ($row in @($rows | Sort-Object -Property firewallPolicyId, ruleCollectionGroupPriority, ruleCollectionGroup, ruleCollectionPriority, ruleCollection, ruleIndex)) { $rule = $row.rule $get = { param([string] $Name) Get-AACPropertyValue -InputObject $rule -Name $Name } $policy = $policies[$row.firewallPolicyId] $ruleType = [string](& $get 'ruleType') $source = & $describeIpGroups (& $get 'sourceIpGroups') $destination = & $describeIpGroups (& $get 'destinationIpGroups') # Application rules list protocol:port pairs; network and DNAT rules # list IP protocols (TCP, UDP, ICMP, Any). $protocols = if ($ruleType -eq 'ApplicationRule') { & $join @(& $get 'protocols' | ForEach-Object { "$(Get-AACPropertyValue -InputObject $_ -Name 'protocolType'):$(Get-AACPropertyValue -InputObject $_ -Name 'port')" }) } else { & $join (& $get 'ipProtocols') } [pscustomobject]@{ PSTypeName = 'AAC.FirewallRule' SubscriptionName = $subscriptionNames[$row.subscriptionId] SubscriptionId = $row.subscriptionId ResourceGroup = $row.resourceGroup Location = $row.location FirewallPolicy = if ($policy) { $policy.name } else { & $lastSegment $row.firewallPolicyId } BasePolicy = if ($policy) { & $lastSegment $policy.basePolicyId } else { '' } Firewalls = if ($policy) { & $join @($policy.firewalls | ForEach-Object { & $lastSegment (Get-AACPropertyValue -InputObject $_ -Name 'id') }) } else { '' } RuleCollectionGroup = $row.ruleCollectionGroup RuleCollectionGroupPriority = $row.ruleCollectionGroupPriority RuleCollection = $row.ruleCollection RuleCollectionPriority = $row.ruleCollectionPriority RuleCollectionType = switch ($row.ruleCollectionType) { 'FirewallPolicyNatRuleCollection' { 'DNAT' } 'FirewallPolicyFilterRuleCollection' { 'Filter' } default { $row.ruleCollectionType } } Action = $row.action RuleName = [string](& $get 'name') RuleType = $ruleType Description = [string](& $get 'description') SourceAddresses = & $join (& $get 'sourceAddresses') SourceIpGroups = $source.Names SourceIpGroupAddresses = $source.Addresses DestinationAddresses = & $join (& $get 'destinationAddresses') DestinationIpGroups = $destination.Names DestinationIpGroupAddresses = $destination.Addresses DestinationFqdns = & $join (& $get 'destinationFqdns') TargetFqdns = & $join (& $get 'targetFqdns') TargetUrls = & $join (& $get 'targetUrls') FqdnTags = & $join (& $get 'fqdnTags') WebCategories = & $join (& $get 'webCategories') Protocols = $protocols DestinationPorts = & $join (& $get 'destinationPorts') TranslatedAddress = [string](& $get 'translatedAddress') TranslatedFqdn = [string](& $get 'translatedFqdn') TranslatedPort = [string](& $get 'translatedPort') TerminateTls = if ($ruleType -eq 'ApplicationRule') { [bool](& $get 'terminateTLS') } else { $null } FirewallPolicyId = $row.firewallPolicyId RuleCollectionGroupId = $row.ruleCollectionGroupId } } $rules = @($rules) # The search filters: all given must match, any value of each. $matchParameters = @{} foreach ($name in 'SourceAddress', 'DestinationAddress', 'Port', 'Protocol', 'Fqdn') { if ($PSBoundParameters.ContainsKey($name)) { $matchParameters[$name] = $PSBoundParameters[$name] } } if ($Action) { $rules = @($rules | Where-Object { $_.Action -in $Action }) } if ($RuleName) { $rules = @($rules | Where-Object { $name = $_.RuleName; @($RuleName | Where-Object { $name -like $_ }).Count -gt 0 }) } if ($matchParameters.Count -gt 0) { $rules = @($rules | Where-Object { Test-AACFirewallRuleMatch -Rule $_ @matchParameters }) } # Where the rules came from and what was searched for, for the view and the PDF. $scope = [ordered]@{ Subscriptions = if ($SubscriptionId) { $SubscriptionId -join ', ' } else { 'every subscription the account can see' } } if ($FirewallPolicyName) { $scope['Policy filter'] = $FirewallPolicyName -join ', ' } foreach ($filter in @( @('Source', $SourceAddress), @('Destination', $DestinationAddress), @('Port', $Port), @('Protocol', $Protocol), @('FQDN', $Fqdn), @('Action', $Action), @('Rule name', $RuleName))) { if ($filter[1]) { $scope[$filter[0]] = @($filter[1]) -join ', ' } } if ($rules.Count -eq 0 -and -not $showView) { Write-Warning 'No Firewall Policy rules were found for the signed-in account and the given filters.' } $null = Invoke-AACExport -CsvPath $csvFullPath -CsvObject $rules -Noun 'rule' -PdfPath $pdfFullPath -WritePdf { Write-AACFirewallRulePdf -Rule $rules -Path $pdfFullPath -Title $Title -Detail $scope } -HtmlPath $htmlFullPath -WriteHtml { Write-AACFirewallRuleHtml -Rule $rules -Path $htmlFullPath -Title $Title -Detail $scope } if ($showView) { Invoke-AACPagedOutput -NoPaging:$NoPaging -ScriptBlock { Show-AACFirewallRuleView -Rule $rules -Scope $scope -NoTitle [Spectre.Console.AnsiConsole]::WriteLine() if ($rules.Count -gt 0) { Write-AACMarkup '[grey42]Add -PassThru (or pipe the command) for the objects; -CsvPath, -PdfPath or -HtmlPath for a report.[/]' } } } if ($returnObjects) { $rules } } |