Export-AzureFirewallRule.ps1


<#PSScriptInfo
 
.VERSION 1.0.1
 
.GUID e7affeab-f70a-45e6-8d40-3d1e4f750812
 
.AUTHOR Chendrayan Venkatesan
 
.COMPANYNAME Freelancer
 
.COPYRIGHT
 
.TAGS AzureFirewallRule ExportAzureFirewallRule ExportAzureFirewallRuleCSV
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
 
.PRIVATEDATA
 
#>


<#
 
.DESCRIPTION
 PowerShell Script to export Azure Firewall rules in CSV format
 
#>
 
param(
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]$OutputCsvPath,

    [string[]]$SubscriptionIds,

    [ValidateRange(1, 1000)]
    [int]$PageSize = 1000
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Test-RequiredModules {
    if (-not (Get-Module -ListAvailable -Name Az.Accounts)) {
        throw "Az.Accounts is not installed. Install-Module Az.Accounts -Scope CurrentUser"
    }

    if (-not (Get-Module -ListAvailable -Name Az.ResourceGraph)) {
        throw "Az.ResourceGraph is not installed. Install-Module Az.ResourceGraph -Scope CurrentUser"
    }

    if (-not (Get-Module -ListAvailable -Name Az.Network)) {
        throw "Az.Network is not installed. Install-Module Az.Network -Scope CurrentUser"
    }

    Import-Module Az.Accounts -ErrorAction Stop
    Import-Module Az.ResourceGraph -ErrorAction Stop
    Import-Module Az.Network -ErrorAction Stop
}

function Test-AzLogin {
    try {
        $null = Get-AzContext -ErrorAction Stop
    }
    catch {
        throw "No Azure login found. Run Connect-AzAccount first."
    }
}

function Test-OutputDirectory {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $parent = Split-Path -Path $Path -Parent
    if (-not [string]::IsNullOrWhiteSpace($parent) -and -not (Test-Path -LiteralPath $parent)) {
        New-Item -Path $parent -ItemType Directory -Force | Out-Null
    }
}

function Invoke-AzGraphPagedQuery {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Query,

        [string[]]$SubscriptionIds,

        [ValidateRange(1, 1000)]
        [int]$PageSize = 1000
    )

    $rows = [System.Collections.Generic.List[object]]::new()
    $skipToken = $null
    $page = 0

    do {
        $page++
        $params = @{
            Query = $Query
            First = $PageSize
        }

        if ($SubscriptionIds -and $SubscriptionIds.Count -gt 0) {
            $params.Subscription = $SubscriptionIds
        }

        if (-not [string]::IsNullOrWhiteSpace($skipToken)) {
            $params.SkipToken = $skipToken
        }

        Write-Host ("Fetching Resource Graph page {0}..." -f $page) -ForegroundColor Cyan
        $response = Search-AzGraph @params -ErrorAction Stop

        if ($response.Data) {
            $rows.AddRange($response.Data)
            Write-Host (" Retrieved {0} rows" -f $response.Data.Count) -ForegroundColor Green
        }

        $skipToken = $response.SkipToken
    }
    while (-not [string]::IsNullOrWhiteSpace($skipToken))

    return $rows
}

function ConvertFrom-AzureResourceId {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ResourceId
    )

    $parts = $ResourceId.Trim('/') -split '/'

    $result = [ordered]@{
        SubscriptionId    = $null
        ResourceGroupName = $null
        ProviderNamespace = $null
        ResourceType      = $null
        Name              = $null
    }

    for ($i = 0; $i -lt $parts.Length; $i++) {
        switch -Regex ($parts[$i].ToLowerInvariant()) {
            '^subscriptions$' {
                if ($i + 1 -lt $parts.Length) { $result.SubscriptionId = $parts[$i + 1] }
            }
            '^resourcegroups$' {
                if ($i + 1 -lt $parts.Length) { $result.ResourceGroupName = $parts[$i + 1] }
            }
            '^providers$' {
                if ($i + 1 -lt $parts.Length) { $result.ProviderNamespace = $parts[$i + 1] }
                if ($i + 2 -lt $parts.Length) { $result.ResourceType = $parts[$i + 2] }
                if ($i + 3 -lt $parts.Length) { $result.Name = $parts[$i + 3] }
            }
        }
    }

    [pscustomobject]$result
}

function Resolve-SourceIpGroupDetails {
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$IpGroupIds
    )

    $cache = @{}

    foreach ($ipGroupId in ($IpGroupIds | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique)) {
        try {
            $parsed = ConvertFrom-AzureResourceId -ResourceId $ipGroupId

            if (-not $parsed.SubscriptionId -or -not $parsed.ResourceGroupName -or -not $parsed.Name) {
                Write-Warning "Could not parse IP Group resource ID: $ipGroupId"
                $cache[$ipGroupId.ToLowerInvariant()] = [pscustomobject]@{
                    SourceIpGroupName      = $null
                    SourceIpGroupAddresses = $null
                }
                continue
            }

            $currentContext = Get-AzContext
            if (-not $currentContext -or $currentContext.Subscription.Id -ne $parsed.SubscriptionId) {
                Set-AzContext -SubscriptionId $parsed.SubscriptionId -ErrorAction Stop | Out-Null
            }

            $ipGroup = Get-AzIpGroup -ResourceGroupName $parsed.ResourceGroupName -Name $parsed.Name -ErrorAction Stop

            $addresses = @()
            if ($ipGroup.IpAddresses) {
                $addresses = @($ipGroup.IpAddresses)
            }

            $cache[$ipGroupId.ToLowerInvariant()] = [pscustomobject]@{
                SourceIpGroupName      = $ipGroup.Name
                SourceIpGroupAddresses = ($addresses -join ', ')
            }
        }
        catch {
            Write-Warning ("Failed to resolve IP Group '{0}': {1}" -f $ipGroupId, $_.Exception.Message)
            $cache[$ipGroupId.ToLowerInvariant()] = [pscustomobject]@{
                SourceIpGroupName      = $null
                SourceIpGroupAddresses = $null
            }
        }
    }

    return $cache
}

try {
    Test-RequiredModules
    Test-AzLogin
    Test-OutputDirectory -Path $OutputCsvPath

    $query = @"
networkresources
| where type =~ 'microsoft.network/firewallpolicies/rulecollectiongroups'
| extend
    firewallPolicyId = tostring(split(id, '/ruleCollectionGroups/')[0]),
    ruleCollectionGroupName = name,
    ruleCollectionGroupPriority = toint(properties.priority),
    ruleCollections = properties.ruleCollections
| mv-expand ruleCollection = ruleCollections
| extend
    ruleCollectionName = tostring(ruleCollection.name),
    ruleCollectionPriority = toint(ruleCollection.priority),
    ruleCollectionType = tostring(ruleCollection.ruleCollectionType),
    actionType = tostring(ruleCollection.action.type),
    rules = ruleCollection.rules
| mv-expand rule = rules
| project
    subscriptionId,
    resourceGroup,
    location,
    firewallPolicyId,
    ruleCollectionGroupName,
    ruleCollectionGroupPriority,
    ruleCollectionName,
    ruleCollectionPriority,
    ruleCollectionType,
    actionType,
    ruleName = tostring(rule.name),
    ruleType = tostring(rule.ruleType),
    sourceAddresses = strcat_array(coalesce(rule.sourceAddresses, dynamic([])), ', '),
    sourceIpGroups = strcat_array(coalesce(rule.sourceIpGroups, dynamic([])), ', '),
    sourceIpGroupCount = array_length(coalesce(rule.sourceIpGroups, dynamic([]))),
    destinationAddresses = strcat_array(coalesce(rule.destinationAddresses, dynamic([])), ', '),
    destinationIpGroups = strcat_array(coalesce(rule.destinationIpGroups, dynamic([])), ', '),
    destinationFqdns = strcat_array(coalesce(rule.destinationFqdns, dynamic([])), ', '),
    targetFqdns = strcat_array(coalesce(rule.targetFqdns, dynamic([])), ', '),
    fqdnTags = strcat_array(coalesce(rule.fqdnTags, dynamic([])), ', '),
    ipProtocols = strcat_array(coalesce(rule.ipProtocols, dynamic([])), ', '),
    protocols = tostring(rule.protocols),
    destinationPorts = strcat_array(coalesce(rule.destinationPorts, dynamic([])), ', '),
    translatedAddress = tostring(rule.translatedAddress),
    translatedPort = tostring(rule.translatedPort),
    webCategories = strcat_array(coalesce(rule.webCategories, dynamic([])), ', '),
    terminateTls = tostring(rule.terminateTLS),
    id
| order by firewallPolicyId asc, ruleCollectionGroupPriority asc, ruleCollectionPriority asc, ruleName asc
"@


    $results = Invoke-AzGraphPagedQuery -Query $query -SubscriptionIds $SubscriptionIds -PageSize $PageSize

    if (-not $results -or $results.Count -eq 0) {
        throw "Query completed but returned no rows."
    }

    $allIpGroupIds = New-Object System.Collections.Generic.List[string]

    foreach ($row in $results) {
        if (-not [string]::IsNullOrWhiteSpace($row.sourceIpGroups)) {
            $ids = $row.sourceIpGroups -split '\s*,\s*' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
            foreach ($id in $ids) {
                $allIpGroupIds.Add($id)
            }
        }
    }

    Write-Host ("Resolving {0} unique source IP Group IDs..." -f (($allIpGroupIds | Sort-Object -Unique).Count)) -ForegroundColor Cyan
    $ipGroupLookup = Resolve-SourceIpGroupDetails -IpGroupIds $allIpGroupIds

    $final = foreach ($row in $results) {
        $resolvedNames = New-Object System.Collections.Generic.List[string]
        $resolvedAddresses = New-Object System.Collections.Generic.List[string]

        if (-not [string]::IsNullOrWhiteSpace($row.sourceIpGroups)) {
            $ids = $row.sourceIpGroups -split '\s*,\s*' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }

            foreach ($id in $ids) {
                $key = $id.ToLowerInvariant()
                if ($ipGroupLookup.ContainsKey($key)) {
                    $entry = $ipGroupLookup[$key]

                    if (-not [string]::IsNullOrWhiteSpace($entry.SourceIpGroupName)) {
                        $resolvedNames.Add($entry.SourceIpGroupName)
                    }

                    if (-not [string]::IsNullOrWhiteSpace($entry.SourceIpGroupAddresses)) {
                        $resolvedAddresses.Add($entry.SourceIpGroupAddresses)
                    }
                }
            }
        }

        [pscustomobject]@{
            subscriptionId              = $row.subscriptionId
            resourceGroup               = $row.resourceGroup
            location                    = $row.location
            firewallPolicyId            = $row.firewallPolicyId
            ruleCollectionGroupName     = $row.ruleCollectionGroupName
            ruleCollectionGroupPriority = $row.ruleCollectionGroupPriority
            ruleCollectionName          = $row.ruleCollectionName
            ruleCollectionPriority      = $row.ruleCollectionPriority
            ruleCollectionType          = $row.ruleCollectionType
            actionType                  = $row.actionType
            ruleName                    = $row.ruleName
            ruleType                    = $row.ruleType
            sourceAddresses             = $row.sourceAddresses
            sourceIpGroups              = $row.sourceIpGroups
            sourceIpGroupNames          = (($resolvedNames | Sort-Object -Unique) -join ', ')
            sourceIpGroupAddresses      = (($resolvedAddresses | Sort-Object -Unique) -join ' | ')
            sourceIpGroupCount          = $row.sourceIpGroupCount
            destinationAddresses        = $row.destinationAddresses
            destinationIpGroups         = $row.destinationIpGroups
            destinationFqdns            = $row.destinationFqdns
            targetFqdns                 = $row.targetFqdns
            fqdnTags                    = $row.fqdnTags
            ipProtocols                 = $row.ipProtocols
            # protocols = $row.protocols
            protocols                   = $(if (-not [string]::IsNullOrWhiteSpace($row.protocols)) {
                    ($row.protocols | ConvertFrom-Json | ForEach-Object {
                        "{0} ({1})" -f $_.protocolType, $_.port
                    }) -join ', '
                }
                else {
                    $null
                })
            destinationPorts            = $row.destinationPorts
            translatedAddress           = $row.translatedAddress
            translatedPort              = $row.translatedPort
            webCategories               = $row.webCategories
            terminateTls                = $row.terminateTls
            id                          = $row.id
        }
    }

    $final | Export-Csv -LiteralPath $OutputCsvPath -NoTypeInformation -Encoding UTF8 -Force
    Write-Host ("Exported {0} rows to {1}" -f $final.Count, $OutputCsvPath) -ForegroundColor Green
}
catch {
    Write-Error ("Script failed: {0}" -f $_.Exception.Message)
    exit 1
}