Src/Public/Invoke-AsBuiltReport.Microsoft.Azure.ps1

function Invoke-AsBuiltReport.Microsoft.Azure {
    <#
    .SYNOPSIS
        PowerShell script to document the configuration of Microsoft Azure in Word/HTML/Text formats
    .DESCRIPTION
        Documents the configuration of Microsoft Azure in Word/HTML/Text formats using PScribo.
    .NOTES
        Version: 0.2.0
        Author: Tim Carman
        Twitter: @tpcarman
        Github: @tpcarman
        Credits: Iain Brighton (@iainbrighton) - PScribo module
 
    .EXAMPLE
        PS C:\> New-AsBuiltReport -Report Microsoft.Azure -Target 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' -Credential $Credential -Format Html -OutputFolderPath 'C:\Reports'
 
        Generates an Azure report in HTML format for the specified tenant using credentials.
 
    .EXAMPLE
        PS C:\> New-AsBuiltReport -Report Microsoft.Azure -Target 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' -UseInteractiveAuth -Format Word -OutputFolderPath 'C:\Reports'
 
        Generates an Azure report in Word format for the specified tenant using interactive authentication (MFA).
 
    .EXAMPLE
        PS C:\> New-AsBuiltReport -Report Microsoft.Azure -Target 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' -UseInteractiveAuth -Format Html,Word -OutputFolderPath 'C:\Reports' -ReportConfigFilePath 'C:\Config\AsBuiltReport.Microsoft.Azure.json'
 
        Generates an Azure report in both HTML and Word formats using a custom configuration file.
 
    .EXAMPLE
        PS C:\> $Token = (Get-AzAccessToken).Token
        PS C:\> New-AsBuiltReport -Report Microsoft.Azure -Target 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' -Token $Token -TokenParameters @{AccountId='user@domain.com'} -Format Html -OutputFolderPath 'C:\Reports'
 
        Generates an Azure report using token-based authentication.
 
    .LINK
        https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.Azure
    #>


    [CmdletBinding()]
    # Do not remove or add to these parameters
    param (
        [String[]] $Target,
        [PSCredential] $Credential,
        [Switch] $UseInteractiveAuth,
        [String] $Token,
        [String] $AccountId  # Passed via TokenParameters hashtable from Core
    )

    # Check for required modules
    Get-RequiredModule -Name 'Az' -Version '15.3.0'

    # Display report module information using Core function
    Write-ReportModuleInfo -ModuleName 'Microsoft.Azure'

    # Import Report Configuration
    $Report = $ReportConfig.Report
    $Filter = $ReportConfig.Filter
    $InfoLevel = $ReportConfig.InfoLevel
    $Options = $ReportConfig.Options
    $SectionOrder = $Options.SectionOrder
    $LocalizedData = $reportTranslate.InvokeAsBuiltReportMicrosoftAzure

    # Used to set values to TitleCase where required
    $TextInfo = (Get-Culture).TextInfo

    # Define default section order if not specified in config
    $DefaultSectionOrder = @(
        "StorageAccount",
        "KeyVault",
        "LogAnalyticsWorkspace",
        "LoadBalancer",
        "ExpressRoute",
        "VirtualNetwork",
        "NetworkSecurityGroup",
        "PrivateEndpoint",
        "IpGroup",
        "DnsPrivateResolver",
        "Bastion",
        "Policy",
        "Firewall",
        "FirewallPolicy",
        "RouteTable",
        "VirtualMachine",
        "AvailabilitySet",
        "RecoveryServicesVault",
        "SiteRecovery",
        "DesktopVirtualization"
    )

    # Use custom section order if provided, otherwise use default
    if (-not $SectionOrder -or $SectionOrder.Count -eq 0) {
        Write-PScriboMessage -Plugin "Module" -Message $LocalizedData.DefaultOrder
        $SectionOrder = $DefaultSectionOrder
    } else {
        Write-PScriboMessage -Plugin "Module" -Message $LocalizedData.CustomOrder
    }

    # Function mapping for section names to function names
    $SectionFunctionMap = @{
        "AvailabilitySet" = "Get-AbrAzAvailabilitySet"
        "Bastion" = "Get-AbrAzBastion"
        "DnsPrivateResolver" = "Get-AbrAzDnsPrivateResolver"
        "ExpressRouteCircuit" = "Get-AbrAzExpressRouteCircuit"
        "ExpressRoute" = "Get-AbrAzExpressRouteCircuit"  # Alias for backward compatibility
        "Firewall" = "Get-AbrAzFirewall"
        "FirewallPolicy" = "Get-AbrAzFirewallPolicy"
        "IpGroup" = "Get-AbrAzIpGroup"
        "KeyVault" = "Get-AbrAzKeyVault"
        "LogAnalyticsWorkspace" = "Get-AbrAzLogAnalyticsWorkspace"
        "LoadBalancer" = "Get-AbrAzLoadBalancer"
        "VirtualNetwork" = "Get-AbrAzVirtualNetwork"
        "NetworkSecurityGroup" = "Get-AbrAzNetworkSecurityGroup"
        "Policy" = "Get-AbrAzPolicy"
        "RouteTable" = "Get-AbrAzRouteTable"
        "VirtualMachine" = "Get-AbrAzVirtualMachine"
        "RecoveryServicesVault" = "Get-AbrAzRecoveryServicesVault"
        "SiteRecovery" = "Get-AbrAsrProtectedItems"
        "StorageAccount" = "Get-AbrAzStorageAccount"
        "PrivateEndpoint" = "Get-AbrAzPrivateEndpoint"
        "DesktopVirtualization" = "Get-AbrAzDesktopVirtualization"
    }

    #region foreach loop
    foreach ($TenantId in $Target) {
        try {
            Write-PScriboMessage -Plugin "Module" -Message ($LocalizedData.Connecting -f $TenantId)
            if ($UseInteractiveAuth -or (-not $Token -and -not $Credential)) {
                # Use interactive auth if explicitly requested OR if no other auth method provided
                Clear-AzContext -Scope Process -Force -ErrorAction SilentlyContinue
                $AzAccount = Connect-AzAccount -TenantId $TenantId -ErrorAction Stop
            } elseif ($Token) {
                # Validate AccountId is provided via TokenParameters
                if (-not $AccountId) {
                    Write-Error ($LocalizedData.TokenAccountIdRequired -f 'New-AsBuiltReport', 'TokenParameters')
                    throw "Azure token authentication requires AccountId. Please use: -TokenParameters @{AccountId='user@domain.com'}"
                }

                Write-PScriboMessage -Plugin "Module" -Message ($LocalizedData.ConnectingWithToken -f $AccountId, $TenantId)
                $AzAccount = Connect-AzAccount -TenantId $TenantId -AccessToken $Token -AccountId $AccountId -ErrorAction Stop
            } else {
                Clear-AzContext -Scope Process -Force -ErrorAction SilentlyContinue
                $AzAccount = Connect-AzAccount -Credential $Credential -TenantId $TenantId -ErrorAction Stop
            }
        } catch {
            Write-Error $_
        }

        if ($AzAccount) {
            $AzTenant = Get-AzTenant -TenantId $TenantId
            $AzLocations = Get-AzLocation
            $AzLocationLookup = @{}
            foreach ($AzLocation in $AzLocations) {
                $AzLocationLookup.($AzLocation.Location) = $AzLocation.DisplayName
            }
            if ($AzTenant) {
                # Create a Lookup Hashtable for all Azure Subscriptions
                $AzSubscriptions = Get-AzSubscription -TenantId $TenantId | Sort-Object Name
                $AzSubscriptionLookup = @{}
                foreach ($AzSubscription in $AzSubscriptions) {
                    $AzSubscriptionLookup.($AzSubscription.SubscriptionId) = $AzSubscription.Name
                }

                # Filter Subscriptions
                if ($Filter.Subscription -ne "*") {
                    $AzSubscriptions = foreach ($AzSubscription in $Filter.Subscription) {
                        Get-AzSubscription -TenantId $TenantId -SubscriptionId $AzSubscription | Sort-Object Name
                    }
                }

                Section -Style Heading1 $($AzTenant.Name) {
                    Get-AbrAzTenant
                    Section -Style Heading2 $LocalizedData.Subscriptions {
                        Get-AbrAzSubscription

                        foreach ($AzSubscription in $AzSubscriptions) {
                            Section -Style Heading3 $($AzSubscription.Name) {
                                Write-PScriboMessage ($LocalizedData.SubscriptionID -f $($AzSubscription.Id))
                                $AzContext = Set-AzContext -Subscription $AzSubscription.Id -Tenant $TenantId

                                # Process sections in the order specified by SectionOrder
                                foreach ($SectionName in $SectionOrder) {
                                    try {
                                        # Get the info level for this section
                                        $level = if ($InfoLevel.PSObject.Properties.Name -contains $SectionName) {
                                            $InfoLevel.$SectionName
                                        } elseif ($InfoLevel.ContainsKey($SectionName)) {
                                            $InfoLevel[$SectionName]
                                        } else {
                                            Write-PScriboMessage ($LocalizedData.InfoLevelNotFound -f $SectionName)
                                            continue
                                        }

                                        # Determine if section is enabled and execute function
                                        $enabled = switch ($level) {
                                            { $_ -is [hashtable] -or $_ -is [PSCustomObject] } {
                                                # For complex objects, sum all property values
                                                $sum = if ($_ -is [hashtable]) {
                                                    ($_.Values | Measure-Object -Sum).Sum
                                                } else {
                                                    ($_.PSObject.Properties.Value | ForEach-Object { [int]$_ } | Measure-Object -Sum).Sum
                                                }
                                                $sum -gt 0
                                            }
                                            default {
                                                # For simple types (int, int64, string), convert to int and check if > 0
                                                try { [int]$_ -gt 0 } catch { $false }
                                            }
                                        }

                                        if ($enabled) {
                                            $functionName = $SectionFunctionMap[$SectionName]
                                            if ($functionName -and (Get-Command $functionName -ErrorAction SilentlyContinue)) {
                                                & $functionName
                                            } else {
                                                Write-PScriboMessage ($LocalizedData.FunctionNotFound -f $functionName, $SectionName)
                                            }
                                        }
                                    } catch {
                                        Write-PScriboMessage ($LocalizedData.ErrorProcessing -f $SectionName, $_)
                                    }
                                }
                            }
                        }
                    }
                }
            } else {
                Write-PScriboMessage ($LocalizedData.TenantNotFound -f $TenantId)
            }
            Disconnect-AzAccount $AzAccount | Out-Null
        }
    }
    #endregion foreach loop
}