AzNetworkDiagram.psm1

#Requires -Version 7.0
#Requires -Modules Az.Accounts, Az.Network, Az.Compute, Az.KeyVault, Az.Storage, Az.MySql, Az.PostgreSql, Az.CosmosDB, Az.RedisCache, Az.Sql, Az.EventHub, Az.Websites, Az.ApiManagement, Az.ContainerRegistry, Az.ManagedServiceIdentity, Az.Resources

<#
  .SYNOPSIS
  Creates a Network Diagram of your Azure networking infrastructure.
 
  .DESCRIPTION
  The Get-AzNetworkDiagram (Powershell)Cmdlet visualizes Azure networking utilizing Graphviz and the "DOT", diagram-as-code language to export a PDF and PNG with a network digram containing:
  - VNets, including:
    - VNet peerings
    - Subnets
        - Special subnet: AzureBastionSubnet and associated Azure Bastion resource
        - Special subnet: GatewaySubnet and associated resources, incl. Network Gateways, Local Network Gateways and connections with the static defined remote subnets. But excluding Express Route Cirtcuits.
        - Special subnet: AzureFirewallSubnet and associated Azure Firewall Policy
        - Associated Route Tables
        - A * will be added to the subnet name, if a subnet is delegated. Commonly used delegations will be given a proper icon
        - A # will be added to the subnet name, in case an NSG is associated
 
  IMPORTANT:
  Icons in the .\icons\ folder is necessary in order to generate the diagram. If not present, they will be downloaded to the output directory during runtime.
   
  .PARAMETER OutputPath
  -OutputPath specifies the path for the DOT-based output file. If unset - current working directory will be used.
 
  .PARAMETER Tenant
  -Tenant "tenantId" Specifies the tenant Id to be used in all subscription authentication. Handy when you have multiple tenants to work with. Default: current tenant
 
  .PARAMETER Subscriptions
  -Subscriptions "subid1","subid2","..."** - a list of subscriptions in scope for the digram. Default is all available subscriptions.
 
  .PARAMETER EnableRanking
  -EnableRanking $true ($true/$false) - enable ranking (equal hight in the output) of certain resource types. For larger networks, this might be worth a shot. **Default: $true**
 
  .PARAMETER Sanitize
  -Sanitize $bool ($true/$false) - Sanitizes all names, locations, IP addresses and CIDR blocks. Default: $false
 
  .PARAMETER Prefix
  -Prefix "string" - Adds a prefix to the output file name. For example is cases where you want to do multiple automated runs then the file names will have the prefix per run that you specify. Default: No Prefix
 
  .PARAMETER OnlyCoreNetwork
  -OnlyCoreNetwork ($true/$false) - if $true/enabled, only cores network resources are processed - ie. non-network resources are skipped for a cleaner diagram.
 
  .INPUTS
  None. It will however require previous authentication to Azure
 
  .OUTPUTS
  None. .\Get-AzNetworkDiagram.psm1 doesn't generate any output (Powershell-wise). File based output will be save in the OutputPath, if set - otherwise to current working directory
 
  .EXAMPLE
  PS> Get-AzNetworkDiagram [-Tenant tenantId] [-Subscriptions "subid1","subid2","..."] [-OutputPath C:\temp\] [-EnableRanking $true] [-OnlyCoreNetwork $true] [-Sanitize $true] [-Prefix prefixstring]
  PS> .\Get-AzNetworkDiagram
 
  .LINK
   https://github.com/dan-madsen/AzNetworkDiagram
#>


# Change Execution Policy for current process, if prohibited by policy
# Set-ExecutionPolicy -scope process -ExecutionPolicy bypass

# Action preferences
$ErrorActionPreference = 'Stop'
$WarningPreference = 'Continue'
$InformationPreference = 'Continue'

function SanitizeLocation {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Location
    )
    # Array of known planets, major moons, stars, and star systems (all lowercase, spaces replaced with dash)
    $celestialBodies = @(
        # Planets (Solar System)
        "mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune",

        # Dwarf Planets (Solar System)
        "pluto", "eris", "haumea", "makemake", "ceres",

        # Major Moons (Solar System)
        "moon", "phobos", "deimos", "io", "europa", "ganymede", "callisto",
        "amalthea", "himalia", "elara", "pasiphae", "sinope", "lysithea", "carme", "ananke", "leda",
        "mimas", "enceladus", "tethys", "dione", "rhea", "titan", "hyperion", "iapetus", "phoebe",
        "miranda", "ariel", "umbriel", "titania", "oberon", "triton", "nereid", "charon", "hydra", "nix", "kerberos", "styx",

        # Notable Stars
        "sun", "sirius", "canopus", "arcturus", "alpha-centauri", "vega", "capella", "rigel", "procyon",
        "achernar", "betelgeuse", "hadar", "altair", "aldebaran", "antares", "spica", "pollux", "fomalhaut",
        "deneb", "mimosa", "regulus", "adhara", "castor", "gacrux", "shaula", "bellatrix", "elnath", "miaplacidus",

        # Notable Star Systems
        "alpha-centauri", "proxima-centauri", "barnard's-star", "luyten's-star", "wolf-359", "lalande-21185",
        "sirius-system", "epsilon-eridani", "tau-ceti", "61-cygni", "altair-system", "vega-system", "trappist-1",

        # Famous Exoplanets (selected)
        "proxima-centauri-b", "kepler-22b", "kepler-452b", "hd-209458-b", "51-pegasi-b", "gliese-581g", "trappist-1e", "trappist-1f", "trappist-1g"
    )
    return $script:DoSanitize ? ($celestialBodies | Get-Random) : $Location
}

# Example usage:
# $text = 'label = "my-label"; [label = "should-not-change"];'
# $newText = Replace-LabelEqualsWithRandomWord $text
# Write-Output $newText

function SanitizeString {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$InputString
    )
    $Ignore = @("true", "false", "enabled", "disabled", "yes", "no", "on", "off")

    # Example usage:
    # $randomIP = Get-RandomPrivateIPAddress
    # Write-Output $randomIP
    function Get-RandomPrivateIPAddress {
        # Define private IP ranges
        $privateRanges = @(
            @{ Base = "10"; Min2 = 0; Max2 = 255; Min3 = 0; Max3 = 255; Min4 = 1; Max4 = 254 },
            @{ Base = "172"; Min2 = 16; Max2 = 31; Min3 = 0; Max3 = 255; Min4 = 1; Max4 = 254 },
            @{ Base = "192.168"; Min2 = 0; Max2 = 0; Min3 = 1; Max3 = 254; Min4 = 1; Max4 = 254 }
        )

        $range = Get-Random -InputObject $privateRanges
        if ($range.Base -eq "10") {
            return "$($range.Base).$((Get-Random -Min $range.Min2 -Max ($range.Max2+1))).$((Get-Random -Min $range.Min3 -Max ($range.Max3+1))).$((Get-Random -Min $range.Min4 -Max ($range.Max4+1)))"
        }
        elseif ($range.Base -eq "172") {
            return "$($range.Base).$((Get-Random -Min $range.Min2 -Max ($range.Max2+1))).$((Get-Random -Min $range.Min3 -Max ($range.Max3+1))).$((Get-Random -Min $range.Min4 -Max ($range.Max4+1)))"
        }
        else {
            return "$($range.Base).$((Get-Random -Min 1 -Max 255)).$((Get-Random -Min 1 -Max 255))"
        }
    }

    if ($null -eq $InputString) {
        return $null
    }
    elseif ($InputString -eq "") {
        return $InputString
    }   
    elseif (-not $script:DoSanitize -or ($Ignore -contains $InputString.ToLower())) {
        return $InputString
    }
    # Regex: match 'label =' not preceded by '[' and followed by quoted string
    # Check for IPv4 address
    elseif ($InputString -match '^(?:\d{1,3}\.){3}\d{1,3}$') {
        return Get-RandomPrivateIPAddress
    }
    # Check for CIDR notation
    elseif ($InputString -match '^(?:\d{1,3}\.){3}\d{1,3}/\d{1,2}$') {
        # Extract mask
        $mask = $InputString.Split('/')[1]
        $ip = Get-RandomPrivateIPAddress
        return "$ip/$mask"
    }
    # Test if a string is only digits
    elseif ($InputString -match '^\d+$') {
        $length = $InputString.Length
        return -join (1..$length | ForEach-Object { Get-Random -Minimum 0 -Maximum 10 })

    }    # Check for dashes and dots
    elseif ($InputString -match '[-.]') {
        # List of 3-letter lowercase words
        $shortwords = @(
            'cat', 'dog', 'sun', 'sky', 'red', 'fox', 'owl', 'bee', 'ant', 'bat', 'cow', 'pig', 'rat', 'hen', 'elk', 'ape', 'yak', 'emu', 'gnu', 'eel', 'ram', 'cod', 'jay', 'kit', 'lob', 'man', 'nut', 'owl', 'pan', 'qua', 'rob', 'sow', 'tan', 'urn', 'vet', 'was', 'yak', 'zip'
        )

        # Split the string by dashes and dots
        $parts = $InputString -split '[-.]'
        if ($parts.Count -le 2) {
            $first = ($shortwords | Get-Random)
            $last = ($shortwords | Get-Random)
        }
        else {
            $first = $parts[0]
            $last = $parts[-1]
        }
        $middleCount = $parts.Count - 2
        $middle = @()
        for ($i = 0; $i -lt $middleCount; $i++) {
            $middle += ($shortwords | Get-Random)
        }
        return ($first + '-' + ($middle -join '-') + '-' + $last)
    }
    # Check for alphanumerical only (no spaces, no dashes, no special chars)
    elseif ($InputString -match '^[a-zA-Z0-9]+$') {
        # Array of known car brands (major global brands, all lowercase, spaces replaced with dash)
        $carBrands = @(
            "acura", "alfa-romeo", "aston-martin", "audi", "bentley", "bmw", "bugatti", "buick", "cadillac", "chevrolet",
            "chrysler", "citroen", "dacia", "daewoo", "daihatsu", "dodge", "ds-automobiles", "ferrari", "fiat", "fisker",
            "ford", "genesis", "gmc", "great-wall", "haval", "holden", "honda", "hummer", "hyundai", "infiniti", "isuzu",
            "jaguar", "jeep", "kia", "koenigsegg", "lada", "lamborghini", "lancia", "land-rover", "lexus", "lincoln",
            "lotus", "lucid", "maserati", "mazda", "mclaren", "mercedes-benz", "mercury", "mini", "mitsubishi", "nissan",
            "opel", "pagani", "peugeot", "polestar", "pontiac", "porsche", "proton", "ram", "renault", "rivian", "rolls-royce",
            "saab", "saturn", "scion", "seat", "skoda", "smart", "ssangyong", "subaru", "suzuki", "tata", "tesla", "toyota",
            "vauxhall", "volkswagen", "volvo", "wuling", "zotye"
        )        
        return $carBrands | Get-Random
    }
    else {
        # List of random words to choose from
        $fruits = @(
            "apple", "apricot", "avocado", "banana", "blackberry", "blueberry", "cantaloupe", "cherry", "coconut", "cranberry",
            "currant", "date", "dragonfruit", "durian", "elderberry", "fig", "gooseberry", "grape", "grapefruit", "guava",
            "honeydew", "jackfruit", "kiwi", "kumquat", "lemon", "lime", "lychee", "mango", "melon", "mulberry",
            "nectarine", "orange", "papaya", "passionfruit", "peach", "pear", "persimmon", "pineapple", "plum", "pomegranate",
            "quince", "raspberry", "starfruit", "strawberry", "tangerine", "watermelon"
        )
        $words = @('alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', 'golf', 'hotel', 'india', 'juliet', 'kilo', 'lima', 'mike', 'november', 'oscar', 'papa', 'quebec', 'romeo', 'sierra', 'tango', 'uniform', 'victor', 'whiskey', 'xray', 'yankee', 'zulu')
        return ($fruits + $words) | Get-Random
    }
}

##### Functions for standard definitions #####
<#
.SYNOPSIS
Exports the DOT file header for the network diagram.
.DESCRIPTION
This function writes the initial DOT syntax and global graph settings for the Azure network diagram.
.PARAMETER None
This function does not take any parameters.
.EXAMPLE
Export-dotHeader
#>

function Export-dotHeader {
    [CmdletBinding()]

    $Data = "digraph G {
    fontname=`"Arial,sans-serif`"
    node [fontname=`"Arial,sans-serif`"]
    edge [fontname=`"Arial,sans-serif`"]
     
    # Ability for peerings arrows/connections to end at border
    compound = true;
    #concentrate = true;
    clusterrank = local;
     
    # Rank (height in picture) support
    newrank = true;
    rankdir = TB;
    nodesep=`"1.0`"
    "

    Export-CreateFile -Data $Data
}

<#
.SYNOPSIS
Exports the DOT file footer with resource ranking for the network diagram.
.DESCRIPTION
This function writes the closing DOT syntax and resource ranking information for the Azure network diagram.
.PARAMETER None
This function does not take any parameters.
.EXAMPLE
Export-dotFooterRanking
#>

function Export-dotFooterRanking {
    Export-AddToFile -Data "`n ##########################################################################################################"
    Export-AddToFile -Data " ##### RANKS"
    Export-AddToFile -Data " ##########################################################################################################`n"
    Export-AddToFile -Data " ### AddressSpace ranks"
    Export-AddToFile " { rank=min; $($script:rankvnetaddressspaces -join '; ') }`n "
    Export-AddToFile -Data "`n ### Subnets ranks"
    Export-AddToFile " { rank=same; $($script:ranksubnets -join '; ') }`n "
    Export-AddToFile -Data "`n ### Virtual Network Gateways ranks"
    Export-AddToFile " { rank=same; $($script:rankvgws -join '; ') }`n "
    Export-AddToFile -Data "`n ### Route table ranks"
    Export-AddToFile " { rank=same; $($script:rankrts -join '; ') }`n "
    Export-AddToFile -Data "`n ### vWAN ranks"
    Export-AddToFile " { rank=same; $($script:rankvwans -join '; ') }`n "
    Export-AddToFile -Data "`n ### vWAN Hub ranks"
    Export-AddToFile " { rank=same; $($script:rankvwanhubs -join '; ') }`n "
    Export-AddToFile -Data "`n ### ER Circuit ranks"
    Export-AddToFile " { rank=same; $($script:rankercircuits -join '; ') }`n "
    Export-AddToFile -Data "`n ### VPN Site ranks"
    Export-AddToFile " { rank=same; $($script:rankvpnsites -join '; ') }`n "        
    Export-AddToFile -Data "`n ### IP Groups ranks"
    Export-AddToFile " { rank=max; $($script:rankipgroups -join '; ') }`n "        
}

<#
.SYNOPSIS
Exports the DOT file footer for the network diagram.
.DESCRIPTION
This function writes the closing DOT syntax for the Azure network diagram.
.PARAMETER None
This function does not take any parameters.
.EXAMPLE
Export-dotFooter
#>

function Export-dotFooter {
    Export-AddToFile -Data "}" #EOF
}

<#
.SYNOPSIS
Creates a new file for outputting the network diagram data.
.DESCRIPTION
This function creates a new output file for the Azure network diagram, overwriting any existing file with the same name.
.PARAMETER None
This function does not take any parameters.
.EXAMPLE
Export-CreateFile
#>

function Export-CreateFile {
    [CmdletBinding()]
    param([string]$Data)

    $Data | Out-File -Encoding ASCII $OutputPath\AzNetworkDiagram.dot
}

<#
.SYNOPSIS
Appends data to the output file for the network diagram.
.DESCRIPTION
This function appends a string of data to the output file for the Azure network diagram.
.PARAMETER Data
The string data to append to the output file.
.EXAMPLE
Export-AddToFile -Data $data
#>

function Export-AddToFile {
    [CmdletBinding()]
    param([string]$Data)

    $Data | Out-File -Encoding ASCII -Append $OutputPath\AzNetworkDiagram.dot
}

<#
.SYNOPSIS
Exports details of an AKS Cluster for inclusion in a network diagram.
.DESCRIPTION
This function processes an AKS Cluster object and formats its details for the Azure network diagram.
.PARAMETER Aks
The AKS Cluster object to process.
.EXAMPLE
Export-AKSCluster -Aks $Aks
#>

function Export-AKSCluster {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$Aks
    )

    try {
        # Check if ACR integration is enabled and which ACRs are attached
        #$Aks.IdentityProfile.kubeletidentity.ClientId
        $roleAssignments = Get-AzRoleAssignment -ObjectId $Aks.IdentityProfile.kubeletidentity.ObjectId -ErrorAction Stop

        # Filter for ACR-related role assignments
        $acrRoleAssignments = $roleAssignments | Where-Object { 
            $_.Scope -like "*/Microsoft.ContainerRegistry/registries/*" -and 
            ($_.RoleDefinitionName -eq "AcrPull" -or $_.RoleDefinitionName -eq "AcrPush")
        }

        # Display the linked ACRs
        if ($null -ne $acrRoleAssignments) {
            $aksacr = $acrRoleAssignments.Scope.split("/")[-1] 
            $aksacrid = $acrRoleAssignments.Scope.replace("-", "").replace("/", "").replace(".", "").ToLower()
        }
        else {
            $aksacr = "None"
            $aksacrid = ""
        }

        $aksid = $Aks.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Name = SanitizeString $Aks.Name
        $data = "
        # $Name - $aksid
        subgraph cluster_$aksid {
            style = solid;
            color = black;
            node [color = white;];
        "

        $ServiceCidr = $Aks.NetworkProfile.ServiceCidr ? $(SanitizeString $Aks.NetworkProfile.ServiceCidr) : "None"
        $PodCidr = $Aks.NetworkProfile.PodCidr ? $(SanitizeString $Aks.NetworkProfile.PodCidr) : "None"
        $Location = SanitizeLocation $Aks.Location
        $data += " $aksid [label = `"\nLocation: $Location\nVersion: $($Aks.KubernetesVersion)\nSKU Tier: $($Aks.Sku.Tier)\nPrivate Cluster: $($Aks.ApiServerAccessProfile.EnablePrivateCluster)\nDNS Service IP: $($Aks.DnsServiceIP)\nMax Agent Pools: $($Aks.MaxAgentPools)\nContainer Registry: $aksacr\nPod CIDR: $PodCidr\nService CIDR: $ServiceCidr\n`" ; color = lightgray;image = `"$OutputPath\icons\aks-service.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.0;];"
        
        #$Aks.PrivateLinkResources.PrivateLinkServiceId

        foreach ($agentpool in $Aks.AgentPoolProfiles) {
            $agentpoolid = $aksid + $agentpool.Name.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $($agentpoolid) [label = `"\nName: $($agentpool.Name ? (SanitizeString $agentpool.Name) : '')\nMode: $($agentpool.Mode)\nZones: $($agentpool.AvailabilityZones)\nVM Size: $($agentpool.VmSize)\nMax Pods: $($agentpool.MaxPods)\nOS SKU: $($agentpool.OsSKU)\nAgent Pools: $($agentpool.MinCount) >= Pod Count <= $($agentpool.MaxCount)\nEnable AutoScaling: $($agentpool.EnableAutoScaling)\nPublic IP: $($agentpool.EnableNodePublicIP ? (SanitizeString $agentpool.EnableNodePublicIP) : '')\n`" ; color = lightgray;image = `"$OutputPath\icons\aks-node-pool.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.0;];`n" 
            $data += " $aksid -> $agentpoolid [label = `"Node Pool`"];`n"
            if ($agentpool.VnetSubnetId) {
                $agentpoolsubnetid = $agentpool.VnetSubnetId.replace("-", "").replace("/", "").replace(".", "").ToLower()
                $data += " $agentpoolid -> $agentpoolsubnetid;`n"
            }
        }

        if ($aksacr -ne "None") {
            $data += " $aksid -> $aksacrid [label = `"Container Registry`"];`n"
        }   
        $sshid = (Get-AzSshKey | Where-Object { $_.publickey -eq $Aks.LinuxProfile.Ssh.Publickeys.Keydata }).Id
        if ($sshid) {
            $sshid = $sshid.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $aksid -> $sshid;`n"
        }
        # Check for User Assign Identity
        if ($aks.Identity.UserAssignedIdentities.Keys) {
            foreach ($identity in $aks.Identity.UserAssignedIdentities.Keys) { 
                $managedIdentityId = $identity.replace("-", "").replace("/", "").replace(".", "").ToLower() 
                $data += " $aksid -> $managedIdentityId;`n"
            } 
        }
        # Check for Private Endpoints
        $pvtendpoints = get-azprivateEndpointConnection -PrivateLinkResourceId $aks.id -ErrorAction SilentlyContinue
        if ($pvtendpoints) {
            foreach ($pe in $($pvtendpoints.PrivateEndpoint)) {
                $peid = $pe.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
                $data += " $aksid -> $peid [label = `"Private Endpoint`"; ];`n"
            }
        }
        # Match VMSS to node pools
        $vmssResources = Get-AzVmss 
        
        if ($vmssResources) {
            foreach ($vmss in $vmssResources) {
                # Extract node pool name from the VMSS name/tags
                
                # Method 1: Check in VMSS tags
                if ($vmss.Tags -and $vmss.Tags.ContainsKey("aks-managed-poolName")) {
                    $nodePoolName = $vmss.Tags["aks-managed-poolName"]
                }
                # Method 2: Extract from VMSS name (aks-[poolname]-[random])
                elseif ($vmss.Name -match "^aks-(.+?)-\d+-vmss$") {
                    $nodePoolName = $matches[1]
                }
                else {
                    # This VMSS is not an AKS VMSS
                    $nodePoolName = $null
                }
                if ($null -ne $nodePoolName) {
                    # Try to find matching node pool in the AKS cluster
                    $matchingPool = $aks.AgentPoolProfiles | Where-Object { $_.Name -eq $nodePoolName }
                    $agentpoolid = $aksid + $nodePoolName.replace("-", "").replace("/", "").replace(".", "").ToLower()
                    $vmssid = $vmss.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()

                    $data += " $agentpoolid -> $vmssid [label = `"VM Scale Set`"];`n"
                }
            }
        }
        $data += " label = `"$Name`";
                }`n"

        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export AKS Cluster: $($Aks.Name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of an Azure Application Gateway for inclusion in a network diagram.
 
.DESCRIPTION
The `Export-ApplicationGateway` function processes a specified Azure Application Gateway object, retrieves its details, and formats the data for inclusion in a network diagram. It visualizes the gateway's name, SKU, zones, SSL certificates, frontend IP configurations, and associated firewall policies.
 
.PARAMETER agw
Specifies the Azure Application Gateway object to be processed.
 
.EXAMPLE
PS> Export-ApplicationGateway -agw $applicationGateway
 
This example processes the specified Azure Application Gateway and exports its details for inclusion in a network diagram.
 
#>

function Export-ApplicationGateway {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$agw 
    )   
    
    try {
        $agwid = $agw.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $agwSubnetId = $agw.GatewayIPConfigurations.Subnet.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Name = SanitizeString $agw.Name
        $Location = SanitizeLocation $agw.Location
        $data = "
        # $Name - $agwid
        subgraph cluster_$agwid {
            style = solid;
            color = black;
            node [color = white;];
        "


        $skuname = $agw.Sku.Name
        if ($agw.SslCertificates) {
            $sslcerts = ($agw.SslCertificates.Name | ForEach-Object { SanitizeString   $_ }) -join ", "
        }
        else {
            $sslcerts = "None"
        }
        if ($agw.FrontendIPConfigurations) {
            $pvtips = ""
            foreach ($ipconfig in $agw.FrontendIPConfigurations) {
                if ($pvtips -ne "") {
                    $pvtips += ", "
                }
                if ($ipconfig.PrivateIPAllocationMethod -eq "Dynamic") {
                    if ($ipconfig.PublicIPAddress.Id) {
                        $pip = Get-AzPublicIpAddress -ResourceGroupName $agw.ResourceGroupName -Name $ipconfig.PublicIPAddress.Id.split("/")[-1] -ErrorAction SilentlyContinue
                        $pvtips += $(SanitizeString $pip.IPAddress) + " (Public)"
                    }
                }
                elseif ($ipconfig.PrivateIPAllocationMethod -eq "Static") {
                    $pvtips += $(SanitizeString $ipconfig.PrivateIPAddress) + " (Private)"
                }
            }
        }
        else {
            $pvtips = "None"
        }
        if ($agw.FirewallPolicy.Id) {
            $polname = SanitizeString $agw.FirewallPolicy.Id.split("/")[-1]
        }
        else {
            $polname = "None"
        }
        if ($agw.Zones) {
            $zones = $agw.Zones -join ","
        }
        else {
            $zones = "None"
        }
        if ($agw.FrontendPorts) {
            $feports = $agw.FrontendPorts.Port -join ", "
        }
        else {
            $feports = "None"
        }

        $data += " $agwid [label = `"\nLocation: $Location\nPolicy name: $polname\nIPs: $pvtips\nSKU: $skuname\nZones: $zones\nSSL Certificates: $sslcerts\nFrontend ports: $feports\n`" ; color = lightgray;image = `"$OutputPath\icons\agw.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.0;];"
        $data += "`n"
        $data += " $agwid -> $agwSubnetId;`n"

        if ($agw.Identity.UserAssignedIdentities.Keys) {
            foreach ($identity in $agw.Identity.UserAssignedIdentities.Keys) { 
                $managedIdentityId = $identity.replace("-", "").replace("/", "").replace(".", "").ToLower()
                $data += " $agwid -> $managedIdentityId;`n"
            }
        }
        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile $data

    }
    catch {
        Write-Host "Can't export Application Gateway: $($agw.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a managed identity for inclusion in a network diagram.
.DESCRIPTION
This function processes a managed identity object and formats its details for the Azure network diagram.
.PARAMETER managedIdentity
The managed identity object to process.
.EXAMPLE
Export-ManagedIdentity -managedIdentity $identity
#>

function Export-ManagedIdentity {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$managedIdentity
    )   
    
    try {
        $id = $managedIdentity.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $managedIdentity.Location
        $Name = SanitizeString $managedIdentity.Name
        $data = "
        # $Name - $managedIdentityId
        subgraph cluster_$id {
            style = solid;
            color = black;
            node [color = white;];
 
            $id [label = `"\n$Name\nLocation: $Location`" ; color = lightgray;image = `"$OutputPath\icons\managed-identity.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];
            label = `"$Name`";
        }
        "

        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export Managed Identity: $($managedIdentity.Name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a Network Security Group (NSG) for inclusion in a network diagram.
.DESCRIPTION
This function processes a Network Security Group object and formats its details for the Azure network diagram.
.PARAMETER nsg
The Network Security Group object to process.
.EXAMPLE
Export-NSG -nsg $nsg
#>

function Export-NSG {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$nsg
    )   
    
    try {
        $id = $nsg.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $nsg.Location
        $Name = SanitizeString $nsg.Name
        $data = "
        # $($nsg.Name) - $id
        subgraph cluster_$id {
            style = solid;
            color = black;
            node [color = white;];
 
            $id [label = `"\n$Name\nLocation: $Location`" ; color = lightgray;image = `"$OutputPath\icons\nsg.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];
            label = `"$Name`";
        }
        "

        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export NSG: $($nsg.Name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of an SSH key for inclusion in a network diagram.
.DESCRIPTION
This function processes an SSH key object and formats its details for the Azure network diagram.
.PARAMETER sshKey
The SSH key object to process.
.EXAMPLE
Export-SSHKey -sshKey $sshKey
#>

function Export-SSHKey {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$sshkey
    )   
    
    try {
        $id = $sshkey.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $sshkey.Location
        $Name = SanitizeString $sshkey.Name
        $data = "
        # $Name - $id
        subgraph cluster_$id {
            style = solid;
            color = black;
            node [color = white;];
 
            $id [label = `"\n$Name\nLocation: $Location`" ; color = lightgray;image = `"$OutputPath\icons\ssh-key.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];
            label = `"$Name`";
        }
        "

        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export SSH Key: $($sshkey.Name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

function Export-ComputeGallery {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$computeGallery
    )   
    
    try {
        $id = $computeGallery.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $sharing = $computeGallery.SharingProfile.Permissions ? "Shared" : "Private"
        $Location = SanitizeLocation $computeGallery.Location
        $Name = SanitizeString $computeGallery.Name
        $data = "
        # $Name - $id
        subgraph cluster_$id {
            style = solid;
            color = black;
            node [color = white;];
 
            $id [label = `"\nName: $Name\nLocation: $Location\nSharing Profile: $sharing`" ; color = lightgray;image = `"$OutputPath\icons\computegalleries.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.0;];`n"

        
        # Get all image definitions in the gallery
        $imageDefinitions = Get-AzGalleryImageDefinition -ResourceGroupName $computeGallery.ResourceGroupName -GalleryName $computeGallery.Name -ErrorAction Stop
        foreach ($imageDef in $imageDefinitions) {
            # Get all image versions for the image definition
            $imageVersions = Get-AzGalleryImageVersion -ResourceGroupName $computeGallery.ResourceGroupName -GalleryName $computeGallery.Name -GalleryImageDefinitionName $imageDef.Name -ErrorAction Stop
            $versions = $imageVersions | Select-Object @{Name = "Version"; Expression = { $_.Name } }, @{Name = "TargetRegions"; Expression = { $_.PublishingProfile.TargetRegions.Name -join ", " } } | Format-Table -AutoSize | Out-String

            $imageDefId = $imageDef.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $imageDefId [label = <
                                        <TABLE border=`"0`" style=`"rounded`">
                                        <TR><TD align=`"left`">Name</TD><TD align=`"left`">$($imageDef.Name)</TD></TR>
                                        <TR><TD align=`"left`">OS Type</TD><TD align=`"left`">$($imageDef.OsType)</TD></TR>
                                        <TR><TD align=`"left`">OS State</TD><TD align=`"left`">$($imageDef.OsState)</TD></TR>
                                        <TR><TD align=`"left`">VM Generation</TD><TD align=`"left`">$($imageDef.HyperVGeneration)</TD></TR>
                                        <TR><TD><BR/><BR/></TD></TR>
                                        <TR><TD><B>Version</B></TD><TD><B>Target Regions</B></TD></TR>
                                        "

            foreach ($imageVersion in $imageVersions) {
                $version = $imageVersion.Name
                $targetRegions = $imageVersion.PublishingProfile.TargetRegions.Name -join ", "
                $data += "<TR><TD>$version</TD><TD>$targetRegions</TD></TR>`n"
            }                                        
            $data += " </TABLE>>; color = lightgray;image = `"$OutputPath\icons\imagedef.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.5;];`n"
            $data += " $id -> $imageDefId;`n"
        }
        $data += "`n
            label = `"$Name`";
        }
        "

    
        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export Compute Gallery: $($computeGallery.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }

}
<#
.SYNOPSIS
Exports details of an Azure Key Vault for inclusion in a network diagram.
.DESCRIPTION
This function processes a Key Vault object and formats its details for the Azure network diagram.
.PARAMETER keyvault
The Key Vault object to process.
.EXAMPLE
Export-Keyvault -keyvault $keyvault
#>

function Export-Keyvault {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$keyvault
    )   
    
    try {
        $properties = Get-AzResource -ResourceId $keyvault.ResourceId -ErrorAction Stop
        $Location = SanitizeLocation $keyvault.Location
        $id = $keyvault.ResourceId.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Name = SanitizeString $keyvault.VaultName
        $data = "
        # $Name - $id
        subgraph cluster_$id {
            style = solid;
            color = black;
            node [color = white;];
 
            $id [label = `"\nLocation: $Location\nSKU: $($properties.Properties.Sku.Name)\nSoft Delete Enabled: $($properties.Properties.enableSoftDelete)\nRBAC Authorization Enabled: $($properties.Properties.enableRbacAuthorization)\nPublic Network Access: $($properties.Properties.publicNetworkAccess)\nPurge Protection Enabled: $($properties.Properties.enablePurgeProtection)`" ; color = lightgray;image = `"$OutputPath\icons\keyvault.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];
        "

        if ($properties.Properties.privateEndpointConnections.properties.PrivateEndpoint.Id) {
            $peid = $properties.Properties.privateEndpointConnections.properties.PrivateEndpoint.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $id -> $peid [label = `"Private Endpoint`"; ];`n"
        }
        $data += "
            label = `"$Name`";
        }
        "

        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export Key Vault: $($keyvault.VaultName) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a Virtual Machine Scale Set (VMSS) for inclusion in a network diagram.
.DESCRIPTION
This function processes a VMSS object and formats its details for the Azure network diagram.
.PARAMETER vmss
The Virtual Machine Scale Set object to process.
.EXAMPLE
Export-VMSS -vmss $vmss
#>

function Export-VMSS {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$vmss
    )   
    
    try {
        $vmssid = $vmss.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $vmss.Location
        $Name = SanitizeString $vmss.Name
        $data = "
        # $Name - $vmssid
        subgraph cluster_$vmssid {
            style = solid;
            color = black;
            node [color = white;];
        "

        $extensions = $vmss.VirtualMachineProfile.ExtensionProfile.Extensions | ForEach-Object { $_.Name } | Join-String -Separator ", "
        
        $data += " $vmssid [label = `"\nLocation: $Location\nSKU: $($vmss.Sku.Name)\nCapacity: $($vmss.Sku.Capacity)\nZones: $($vmss.Zones)\nOS Type: $($vmss.StorageProfile.OsDisk.OsType)\nOrchestration Mode: $($vmss.OrchestrationMode)\nUpgrade Policy: $($vmss.UpgradePolicy)\nExtensions: $extensions`" ; color = lightgray;image = `"$OutputPath\icons\vmss.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.0;];"
        $data += "`n"

        $sshid = (Get-AzSshKey | Where-Object { $_.publickey -eq $vmss.VirtualMachineProfile.OsProfile.LinuxConfiguration.Ssh.PublicKeys.KeyData }).Id
        if ($sshid) {
            $sshid = $sshid.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $vmssid -> $sshid;`n"
        }
        if ($vmss.Identity.UserAssignedIdentities.Keys) {
            foreach ($identity in $vmss.Identity.UserAssignedIdentities.Keys) { 
                $managedIdentityId = $identity.replace("-", "").replace("/", "").replace(".", "").ToLower() 
                $data += " $vmssid -> $managedIdentityId;`n"
            } 
        }
        if ($vmss.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations.IpConfigurations.Subnet.Id) {
            $subnetid = $vmss.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations.IpConfigurations.Subnet.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $vmssid -> $subnetid;`n"
        }
        if ($vmss.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations.NetworkSecurityGroup.Id) {
            $nsgid = $vmss.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations.NetworkSecurityGroup.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $vmssid -> $nsgid;`n"
        }
        $data += " label = `"$Name`";
        }`n"


        Export-AddToFile -Data $data

    }
    catch {
        Write-Host "Can't export VMSS: $($vmss.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a Virtual Machine (VM) for inclusion in a network diagram.
.DESCRIPTION
This function processes a VM object and formats its details for the Azure network diagram.
.PARAMETER vm
The Virtual Machine object to process.
.EXAMPLE
Export-VM -vm $vm
#>

function Export-VM {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$vm
    )   
    
    try {
        $vmid = $vm.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $vm.Location
        $Name = SanitizeString $vm.Name
        $data = "
        # $Name - $vmid
        subgraph cluster_$vmid {
            style = solid;
            color = black;
            node [color = white;];
        "

        $extensions = $vm.Extensions | ForEach-Object { $_.Id.split("/")[-1] } | Join-String -Separator ", "
        $nic = Get-AzNetworkInterface -ResourceId $vm.NetworkProfile.NetworkInterfaces[0].Id -ErrorAction Stop
        $PublicIpAddress = $nic.IpConfigurations[0].PublicIpAddress ? $(SanitizeString $nic.IpConfigurations[0].PublicIpAddress) : ""
        $PrivateIpAddress = $nic.IpConfigurations[0].PrivateIpAddress ? $(SanitizeString $nic.IpConfigurations[0].PrivateIpAddress) : ""
        $data += " $vmid [label = `"\nLocation: $Location\nSKU: $($vm.HardwareProfile.VmSize)\nZones: $($vm.Zones)\nOS Type: $($vm.StorageProfile.OsDisk.OsType)\nPublic IP: $PublicIpAddress\nPrivate IP Address: $PrivateIpAddress\nExtensions: $extensions`" ; color = lightgray;image = `"$OutputPath\icons\vm.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.0;];"
        $data += "`n"
        $subnetid = $nic.IpConfigurations[0].Subnet.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $data += " $vmid -> $subnetid;`n"
        if ($vm.Identity.UserAssignedIdentities.Keys) {
            foreach ($identity in $vm.Identity.UserAssignedIdentities.Keys) { 
                $managedIdentityId = $identity.replace("-", "").replace("/", "").replace(".", "").ToLower()
                $data += " $vmid -> $managedIdentityId;`n"
            }
        }
        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export VM: $($vm.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a MySQL Flexible Server for inclusion in a network diagram.
.DESCRIPTION
This function processes a MySQL Flexible Server object and formats its details for the Azure network diagram.
.PARAMETER mysql
The MySQL Flexible Server object to process.
.EXAMPLE
Export-MySQLServer -mysql $mysql
#>

function Export-MySQLServer {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$mysql
    )   
    
    try {
        # Get Entra ID Admin
        $subid = $mysql.id.split("/")[2]
        $resourceGroupName = $mysql.id.split("/")[4]
        $uri = "https://management.azure.com/subscriptions/$subid/resourceGroups/$resourceGroupName/providers/Microsoft.DBforMySQL/flexibleServers/$($mysql.Name)/administrators?api-version=2023-06-01-preview"
        $token = (Get-AzAccessToken -ResourceUrl 'https://management.azure.com').Token
        $headers = @{
            Accept        = '*/*'
            Authorization = "bearer $token"
        }

        $response = Invoke-RestMethod -ContentType "application/json" -Method Get -Uri $uri -Headers $headers -ErrorAction SilentlyContinue
        $sqladmins = $response.value.properties.login

        # Get other server properties
        $mysqlid = $mysql.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $properties = Get-AzResource -ResourceId $mysql.id -ErrorAction Stop      
        $Name = SanitizeString $mysql.Name
        $Location = SanitizeLocation $mysql.Location
        $data = "
        # $Name - $mysqlid
        subgraph cluster_$mysqlid {
            style = solid;
            color = black;
            node [color = white;];
        "


        $data += " $mysqlid [label = `"\n\n\nLocation: $Location\nSKU: $($mysql.SkuName)\nTier: $($mysql.SkuTier.ToString())\nVersion: $($mysql.Version)\nLogin Admins:$(SanitizeString $sqladmins)\nVM Size: $($properties.Sku.Name)\nAvailability Zone: $($mysql.AvailabilityZone)\nStandby Zone: $($mysql.HighAvailabilityStandbyAvailabilityZone)\nPublic Network Access: $($mysql.NetworkPublicNetworkAccess)`" ; color = lightgray;image = `"$OutputPath\icons\mysql.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.5;];"
        $data += "`n"
        
        $dbs = Get-AzMySqlFlexibleServerDatabase -ResourceGroupName $mysql.id.split("/")[4] -ServerName $mysql.Name -ErrorAction Stop
        foreach ($db in $dbs) {
            $dbid = $db.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $($dbid) [label = `"\n\nName: $(SanitizeString $db.Name)\n`" ; color = lightgray;image = `"$OutputPath\icons\db.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];`n" 
            $data += " $mysqlid -> $($dbid);`n"
        }

        if ($properties.properties.network.delegatedSubnetResourceId  ) {
            $mysqlsubnetid = $properties.properties.network.delegatedSubnetResourceId.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $mysqlid -> $($mysqlsubnetid);`n"
        }
        if ($properties.Identity.UserAssignedIdentities.Keys) {
            foreach ($identity in $properties.Identity.UserAssignedIdentities.Keys) { 
                $managedIdentityId = $identity.replace("-", "").replace("/", "").replace(".", "").ToLower() 
                $data += " $mysqlid -> $managedIdentityId;`n"
            }
        }
        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile -Data $data

    }
    catch {
        Write-Host "Can't export MySQL Server: $($mysql.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

function Invoke-TableWriter {
    # [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [scriptblock]$GetDatabases,
        [Parameter(Mandatory = $true)]
        [scriptblock]$GetDBThroughput,
        [Parameter(Mandatory = $true)]
        [scriptblock]$GetCollections,
        [Parameter(Mandatory = $true)]
        [scriptblock]$GetColThrouput,
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$cosmosdbact,
        [Parameter(Mandatory = $true)]
        [string]$TypeName,
        [Parameter(Mandatory = $true)]
        [string]$iconname
    )
    $resourceGroupName = $cosmosdbact.Id.split("/")[4]
    $data = ""
    $dbs = & $GetDatabases -ResourceGroupName $resourceGroupName -AccountName $cosmosdbact.Name -ErrorAction Stop
    foreach ($db in $dbs) {
        $dbthroughput = & $GetDBThroughput -ResourceGroupName $resourceGroupName -AccountName $cosmosdbact.Name -Name $db.Name -ErrorAction SilentlyContinue
        if ($null -eq $dbthroughput) {
            $dbthroughput = "Unknown"
        }   
        else {
            $dbthroughput = $dbthroughput.Throughput
        }
        $table = "<TABLE border=`"0`" style=`"rounded`">`n"
        $table += "<TR><TD><BR/><BR/></TD></TR>`n"
        $table += "<TR><TD align=`"left`">Name</TD><TD align=`"left`">$(SanitizeString $db.Name)</TD></TR>`n"
        $table += "<TR><TD align=`"left`">Database Throughput</TD><TD align=`"left`">$dbthroughput</TD></TR>`n"
        $table += "<TR><TD><BR/><BR/></TD></TR>`n"
        $table += "<TR><TD align=`"left`"><B>$TypeName</B></TD><TD align=`"left`"><B>RU</B></TD></TR><HR/>`n"
        $collection = & $GetCollections -ResourceGroupName $resourceGroupName -AccountName $cosmosdbact.Name -DatabaseName $db.Name -ErrorAction SilentlyContinue
        $colthroughputs = $collection | ForEach-Object {
            $collection = SanitizeString $_.Name
            $RU = (& $GetColThrouput  `
                    -ResourceGroupName $resourceGroupName `
                    -AccountName      $cosmosdbact.Name `
                    -DatabaseName     $db.Name `
                    -Name             $_.Name `
                    -ErrorAction      SilentlyContinue
            ).Throughput
            $table += "<TR><TD align=`"left`">$collection</TD><TD align=`"left`">$RU</TD></TR>`n"
        }
        $table += "</TABLE>`n"
        $dbid = $db.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $data += " $dbid [label = < $table > ; color = lightgray;image = `"$OutputPath\icons\$iconname.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.0;];`n"
        $data += " $cosmosdbactid -> $($dbid);`n"
    }
    return $data
}

<#
.SYNOPSIS
Exports details of a Cosmos DB account for inclusion in a network diagram.
.DESCRIPTION
This function processes a Cosmos DB account object and formats its details for the Azure network diagram.
.PARAMETER cosmosdbact
The Cosmos DB account object to process.
.EXAMPLE
Export-CosmosDBAccount -cosmosdbact $cosmosdbact
#>

function Export-CosmosDBAccount {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$cosmosdbact
    )   
    
    try {
        $cosmosdbactid = $cosmosdbact.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Locations = ($cosmosdbact.Locations.LocationName | ForEach-Object { SanitizeLocation $_ }) -join ", "
        $Name = SanitizeString $cosmosdbact.Name
        $data = "
        # $Name - $cosmosdbactid
        subgraph cluster_$cosmosdbactid {
            style = solid;
            color = black;
            node [color = white;];
        "


        $data += " $cosmosdbactid [label = `"Version: $($cosmosdbact.ApiProperties.ServerVersion)\nLocations: $Locations\nDefault Consistency Level: $($cosmosdbact.ConsistencyPolicy.DefaultConsistencyLevel)\nKind: $($cosmosdbact.Kind)\nDatabase Account Offer Type: $($cosmosdbact.DatabaseAccountOfferType)\nEnable Analytical Storage: $($cosmosdbact.EnableAnalyticalStorage)\nVirtual Network Filter Enabled: $($cosmosdbact.IsVirtualNetworkFilterEnabled)`" ; color = lightgray;image = `"$OutputPath\icons\cosmosdb.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.0;];"
        $data += "`n"
        $resourceGroupName = $cosmosdbact.Id.split("/")[4]
        switch ($cosmosdbact.Kind) {
            #MongoDB
            "MongoDB" {  
                $data += Invoke-TableWriter `
                    -GetDatabases { param($ResourceGroupName, $AccountName) Get-AzCosmosDBMongoDBDatabase -ResourceGroupName $ResourceGroupName -AccountName $AccountName -ErrorAction Stop } `
                    -GetDBThroughput { param($ResourceGroupName, $AccountName, $Name) Get-AzCosmosDBMongoDBDatabaseThroughput -ResourceGroupName $ResourceGroupName -AccountName $AccountName -Name $Name -ErrorAction SilentlyContinue } `
                    -GetCollections { param($ResourceGroupName, $AccountName, $DatabaseName) Get-AzCosmosDBMongoDBCollection -ResourceGroupName $ResourceGroupName -AccountName $AccountName -DatabaseName $DatabaseName -ErrorAction SilentlyContinue } `
                    -GetColThrouput { param($ResourceGroupName, $AccountName, $DatabaseName, $Name) Get-AzCosmosDBMongoDBCollectionThroughput -ResourceGroupName $ResourceGroupName -AccountName $AccountName -DatabaseName $DatabaseName -Name $Name -ErrorAction SilentlyContinue } `
                    -CosmosDbAct $cosmosdbact `
                    -TypeName "Collection" `
                    -IconName "mongodb"
            }
            # NoSQL
            "GlobalDocumentDB" { 
                $data += Invoke-TableWriter `
                    -GetDatabases { param($ResourceGroupName, $AccountName) Get-AzCosmosDBSqlDatabase -ResourceGroupName $ResourceGroupName -AccountName $AccountName -ErrorAction Stop } `
                    -GetDBThroughput { param($ResourceGroupName, $AccountName, $Name) Get-AzCosmosDBSqlDatabaseThroughput -ResourceGroupName $ResourceGroupName -AccountName $AccountName -Name $Name -ErrorAction SilentlyContinue } `
                    -GetCollections { param($ResourceGroupName, $AccountName, $DatabaseName) Get-AzCosmosDBSqlContainer -ResourceGroupName $ResourceGroupName -AccountName $AccountName -DatabaseName $DatabaseName -ErrorAction SilentlyContinue } `
                    -GetColThrouput { param($ResourceGroupName, $AccountName, $DatabaseName, $Name) Get-AzCosmosDBSqlContainerThroughput -ResourceGroupName $ResourceGroupName -AccountName $AccountName -DatabaseName $DatabaseName -Name $Name -ErrorAction SilentlyContinue } `
                    -CosmosDbAct $cosmosdbact `
                    -TypeName "Container" `
                    -IconName "documentdb"
            }
            #Gremlin
            "Gremlin" {  
                $data += Invoke-TableWriter `
                    -GetDatabases { param($ResourceGroupName, $AccountName) Get-AzCosmosDBGremlinDatabase -ResourceGroupName $ResourceGroupName -AccountName $AccountName -ErrorAction Stop } `
                    -GetDBThroughput { param($ResourceGroupName, $AccountName, $Name) Get-AzCosmosDBGremlinDatabaseThroughput -ResourceGroupName $ResourceGroupName -AccountName $AccountName -Name $Name -ErrorAction SilentlyContinue } `
                    -GetCollections { param($ResourceGroupName, $AccountName, $DatabaseName) Get-AzCosmosDBGremlinGraph -ResourceGroupName $ResourceGroupName -AccountName $AccountName -DatabaseName $DatabaseName -ErrorAction SilentlyContinue } `
                    -GetColThrouput { param($ResourceGroupName, $AccountName, $DatabaseName, $Name) Get-AzCosmosDBGremlinGraphThroughput -ResourceGroupName $ResourceGroupName -AccountName $AccountName -DatabaseName $DatabaseName -Name $Name -ErrorAction SilentlyContinue } `
                    -CosmosDbAct $cosmosdbact `
                    -TypeName "Graph" `
                    -IconName "gremlin"
            }
            #Table
            "Table" {  
                $dbs = Get-AzCosmosDBTable -ResourceGroupName $$resourceGroupName -AccountName $cosmosdbact.Name -ErrorAction Stop
                $iconname = "table"
                foreach ($db in $dbs) {
                    $throughput = Get-AzCosmosDBTableThroughput -ResourceGroupName $resourceGroupName -AccountName $cosmosdbact.Name -Name $db.Name -ErrorAction SilentlyContinue
                    if ($null -eq $dbthroughput) {
                        $dbthroughput = "Unknown"
                    }   
                    else {
                        $dbthroughput = $dbthroughput.Throughput
                    }

                    $dbid = $db.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
                    $data += " $($dbid) [label = `"\n\nName: $(SanitizeString $db.Name)\nTable Throughput: $dbthroughput\n`" ; color = lightgray;image = `"$OutputPath\icons\$iconname.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.0;];`n" 
                    $data += " $cosmosdbactid -> $($dbid);`n"
                }
            }   
            #Cassandra
            "Cassandra" { 
                $data += Invoke-TableWriter `
                    -GetDatabases { param($ResourceGroupName, $AccountName) Get-AzCosmosDBCassandraKeyspace -ResourceGroupName $ResourceGroupName -AccountName $AccountName -ErrorAction Stop } `
                    -GetDBThroughput { param($ResourceGroupName, $AccountName, $Name) Get-AzCosmosDBCassandraKeyspaceThroughput -ResourceGroupName $ResourceGroupName -AccountName $AccountName -Name $Name -ErrorAction SilentlyContinue } `
                    -GetCollections { param($ResourceGroupName, $AccountName, $DatabaseName) Get-AzCosmosDBCassandraTable -ResourceGroupName $ResourceGroupName -AccountName $AccountName -DatabaseName $DatabaseName -ErrorAction SilentlyContinue } `
                    -GetColThrouput { param($ResourceGroupName, $AccountName, $DatabaseName, $Name) Get-AzCosmosDBCassandraTableThroughput -ResourceGroupName $ResourceGroupName -AccountName $AccountName -DatabaseName $DatabaseName -Name $Name -ErrorAction SilentlyContinue } `
                    -CosmosDbAct $cosmosdbact `
                    -TypeName "Table" `
                    -IconName "cassandra"
            }

            default { 
                Write-Output "Unknown CosmosDB type: $($cosmosdbact.Kind)" 
                $iconname = $null
                $dbs = $null
            }
        }   
        # Add links to Virtual Network Rules, Private Endpoints, and Managed Identities
        foreach ($vnRule in $cosmosdbact.VirtualNetworkRules) {
            $vnRuleid = $vnRule.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $cosmosdbactid -> $($vnRuleid) [label = `"Virtual Network Rule`"; ];`n"
        }
        if ($cosmosdbact.PrivateEndpointConnections.PrivateEndpoint.Id) {
            $peid = $cosmosdbact.PrivateEndpointConnections.PrivateEndpoint.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $cosmosdbactid -> $peid [label = `"Private Endpoint`"; ];`n"
        }
        if ($cosmosdbact.Identity.UserAssignedIdentities.Keys) {
            foreach ($identity in $cosmosdbact.Identity.UserAssignedIdentities.Keys) { 
                $managedIdentityId = $identity.replace("-", "").replace("/", "").replace(".", "").ToLower() 
                $data += " $cosmosdbactid -> $managedIdentityId;`n"
            } 
        }
        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export Cosmos DB Account: $($cosmosdbact.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a PostgreSQL Flexible Server for inclusion in a network diagram.
.DESCRIPTION
This function processes a PostgreSQL Flexible Server object and formats its details for the Azure network diagram.
.PARAMETER postgresql
The PostgreSQL Flexible Server object to process.
.EXAMPLE
Export-PostgreSQLServer -postgresql $postgresql
#>

function Export-PostgreSQLServer {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$postgresql
    )
    try {
        $postgresqlid = $postgresql.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Name = SanitizeString $postgresql.Name
        $data = "
        # $Name - $postgresqlid
        subgraph cluster_$postgresqlid {
            style = solid;
            color = black;
            node [color = white;];
        "


        $resource = Get-AzResource -ResourceId $postgresql.Id -ErrorAction Stop
        # General Purpose, D4ds_v5 (SkuName), 4 vCores, 16 GiB RAM, 128 GiB storage $postgresql.StorageSizeGb
        $Location = SanitizeLocation (Get-AzLocation | Where-Object DisplayName -eq $postgresql.Location).Location 
        $SkuCaps = Get-AzComputeResourceSku -Location $postgresql.Location | Where-Object { $_.Name -eq $skuName }
        $iops = ($SkuCaps.Capabilities | Where-Object Name -eq "UncachedDiskIOPS").Value
        $vCPUs = ($SkuCaps.Capabilities | Where-Object Name -eq "vCPUs").Value
        $MemoryGB = ($SkuCaps.Capabilities | Where-Object Name -eq "MemoryGB").Value
        $config = $postgresql.SkuTier.ToString() + ", " + $postgresql.SkuName + ", " + $vCPUs + " vCores, " + $MemoryGB + " GiB RAM, " + $postgresql.StorageSizeGb + " GiB storage"

        $data += " $postgresqlid [label = `"\nLocation: $Location\nVersion: $($postgresql.Version.ToString()).$($postgresql.MinorVersion)\nAvailability Zone: $($postgresql.AvailabilityZone)\nConfiguration: $config\nMax IOPS: $iops\nPublic Network Access: $($postgresql.NetworkPublicNetworkAccess.ToString())`" ; color = lightgray;image = `"$OutputPath\icons\postgresql.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];"
        $data += "`n"

        $dbs = Get-AzPostgreSqlFlexibleServerDatabase -ResourceGroupName $postgresqlserver.id.split("/")[4] -ServerName $postgresqlserver.Name -ErrorAction Stop
        foreach ($db in $dbs) {
            $dbid = $db.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $($dbid) [label = `"\n\nName: $(SanitizeString $db.Name)\n`" ; color = lightgray;image = `"$OutputPath\icons\db.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];`n" 
            $data += " $postgresqlid -> $($dbid);`n"
        }
        if ($postgresql.NetworkDelegatedSubnetResourceId) {
            $postgresqlsubnetid = $postgresql.NetworkDelegatedSubnetResourceId.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $postgresqlid -> $($postgresqlsubnetid);`n"
        }
        if ($resource.Identity.UserAssignedIdentities.Keys) {
            foreach ($identity in $resource.Identity.UserAssignedIdentities.Keys) { 
                $managedIdentityId = $identity.replace("-", "").replace("/", "").replace(".", "").ToLower() 
                $data += " $postgresqlid -> $managedIdentityId;`n"
            } 
        }
        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile -Data $data

    }
    catch {
        Write-Host "Can't export PostgreSQL Server: $($postgresql.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a Redis server for inclusion in a network diagram.
.DESCRIPTION
This function processes a Redis server object and formats its details for the Azure network diagram.
.PARAMETER redis
The Redis server object to process.
.EXAMPLE
Export-RedisServer -redis $redis
#>

function Export-RedisServer {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$redis
    )
    try {
        $redisid = $redis.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $redis.Location
        $Name = SanitizeString $redis.Name
        $data = "
        # $Name - $redisid
        subgraph cluster_$redisid {
            style = solid;
            color = black;
            node [color = white;];
        "


        $data += " $redisid [label = `"\nLocation: $Location\nSKU: $($redis.Sku)\nRedis Version: $($redis.RedisVersion)\nZones: $($redis.Zone -join ", ")\nShard Count: $($redis.ShardCount)\n`" ; color = lightgray;image = `"$OutputPath\icons\redis.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];"
        $data += "`n"
        if ($redis.PrivateEndpointConnection.PrivateEndpoint.Id) {
            $peid = $redis.PrivateEndpointConnection.PrivateEndpoint.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $redisid -> $peid [label = `"Private Endpoint`"; ];`n"
        }
        if ($redis.Identity.UserAssignedIdentities.Keys) {
            foreach ($identity in $redis.Identity.UserAssignedIdentities.Keys) { 
                $managedIdentityId = $identity.replace("-", "").replace("/", "").replace(".", "").ToLower() 
                $data += " $redisid -> $managedIdentityId;`n"
            } 
        }
        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export Redis Cache: $($redis.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a SQL Managed Instance for inclusion in a network diagram.
.DESCRIPTION
This function processes a SQL Managed Instance object and formats its details for the Azure network diagram.
.PARAMETER sqlmi
The SQL Managed Instance object to process.
.EXAMPLE
Export-SQLManagedInstance -sqlmi $sqlmi
#>

function Export-SQLManagedInstance {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$sqlmi
    )
    try {
        $sqlmiid = $sqlmi.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $sqlmi.Location
        $Name = SanitizeString $sqlmi.ManagedInstanceName
        $data = "
        # $Name - $sqlmiid
        subgraph cluster_$sqlmiid {
            style = solid;
            color = black;
            node [color = white;];
        "


        $data += " $sqlmiid [label = `"\n\nLocation: $Location\nSKU: $($sqlmi.Sku.Tier) $($sqlmi.Sku.Family)\nVersion: $($sqlmi.DatabaseFormat)\nEntra Id Admin: $(SanitizeString $sqlmi.Administrators.Login)\nvCores: $($sqlmi.VCores)\nStorage Size: $($sqlmi.StorageSizeInGB) GB\nZone Redundant: $($sqlmi.ZoneRedundant)\nPublic endpoint (data): $($sqlmi.PublicDataEndpointEnabled)`" ; color = lightgray;image = `"$OutputPath\icons\sqlmi.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.5;];"
        $data += "`n"

        Get-AzSqlInstanceDatabase -InstanceResourceId $sqlmi.Id -ErrorAction SilentlyContinue |
        ForEach-Object {
            $db = $_
            $dbid = $_.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $Location = SanitizeLocation $db.Location
            $retention = Get-AzSqlInstanceDatabaseBackupShortTermRetentionPolicy -ResourceGroupName $db.ResourceGroupName -InstanceName $db.ManagedInstanceName -DatabaseName $db.Name -ErrorAction SilentlyContinue
            $data += " $($dbid) [label = `"\n\nLocation: $Location\nName: $($db.DatabaseName)\nBackup retention: $($retention.RetentionDays) Days`" ; color = lightgray;image = `"$OutputPath\icons\sqlmidb.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.0;];`n" 
            $data += " $sqlmiid -> $($dbid);`n"
        }

        if ($sqlmi.SubnetId) {
            $sqlmisubnetid = $sqlmi.SubnetId.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $sqlmiid -> $($sqlmisubnetid);`n"
        }
        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export SQL Managed Instance: $($sqlmi.ManagedInstanceName) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a SQL Server for inclusion in a network diagram.
.DESCRIPTION
This function processes a SQL Server object and formats its details for the Azure network diagram.
.PARAMETER sqlserver
The SQL Server object to process.
.EXAMPLE
Export-SQLServer -sqlserver $sqlserver
#>

function Export-SQLServer {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$sqlserver
    )
    try {
        $sqlserverid = $sqlserver.ResourceId.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $sqlserver.Location
        $Name = SanitizeString $sqlserver.ServerName
        $data = "
        # $Name - $sqlserverid
        subgraph cluster_$sqlserverid {
            style = solid;
            color = black;
            node [color = white;];
        "


        $data += " $sqlserverid [label = `"\nLocation: $Location\nVersion: $($sqlserver.ServerVersion)\nEntra ID Admin: $(SanitizeString $sqlserver.Administrators.Login)`" ; color = lightgray;image = `"$OutputPath\icons\sqlserver.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.0;];"
        $data += "`n"

        # Iterate through all SQL databases hosted on that server
        Get-AzSqlDatabase -ServerName $sqlserver.ServerName -ResourceGroupName $sqlserver.ResourceGroupName -ErrorAction SilentlyContinue |
        ForEach-Object {
            $db = $_
            $dbid = $_.ResourceId.replace("-", "").replace("/", "").replace(".", "").ToLower()

            if ($db.Edition -ne "System" -and $db.SkuName -ne "System") {
                # Master databases
                # pricing tier , vCore-based DBs expose Family
                if ($db.Family) {
                    $pricingTier = $db.Edition + " " + $db.Family + " " + $db.Capacity + " vCores"
                }
                else {
                    $pricingTier = $db.Edition + " " + $db.ServiceObjectiveName + " " + $db.Capacity + " DTUs"
                }

                #Max storage size
                $gb = [math]::Round($db.MaxSizeBytes / 1GB, 2)   # 1 GB = 1 073 741 824 bytes
                $Location = SanitizeLocation $db.Location
                $data += " $($dbid) [label = `"\n\nLocation: $Location\nName: $(SanitizeString $db.DatabaseName)\nPricing Tier: $pricingTier\nMax Size: $gb GB\nZone Redundant: $($db.ZoneRedundant)\nElastic Pool Name: $($db.ElasticPoolName ? (SanitizeString $db.ElasticPoolName) : '')`" ; color = lightgray;image = `"$OutputPath\icons\sqldb.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.0;];`n" 
                $data += " $sqlserverid -> $($dbid);`n"
            }
        }

        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export SQL Server: $($sqlserver.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of an Event Hub namespace for inclusion in a network diagram.
.DESCRIPTION
This function processes an Event Hub namespace object and formats its details for the Azure network diagram.
.PARAMETER namespace
The Event Hub namespace object to process.
.EXAMPLE
Export-EventHub -namespace $namespace
#>

function Export-EventHub {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$namespace
    )
    try {
        $namespaceid = $namespace.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $namespace.Location
        $Name = SanitizeString $namespace.Name
        $data = "
        # $Name - $namespaceid
        subgraph cluster_$namespaceid {
            style = solid;
            color = black;
            node [color = white;];
        "


        $data += " $namespaceid [label = `"\nLocation: $Location\nSKU: $($namespace.SkuName)\nTier: $($namespace.SkuTier)\nCapacity: $($namespace.SkuCapacity)\nZone Redundant: $($namespace.ZoneRedundant)`" ; color = lightgray;image = `"$OutputPath\icons\eventhub.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];"
        $data += "`n"

        # iterate through all event hubs hosted on that namespace
        Get-AzEventHub -NamespaceName $namespace.Name -ResourceGroupName $namespace.ResourceGroupName -ErrorAction SilentlyContinue |
        ForEach-Object {
            $eventhub = $_
            $eventhubid = $_.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $Location = SanitizeLocation $eventhub.Location
            $data += " $($eventhubid) [label = `"\n\nLocation: $Location\nName: $(SanitizeString $eventhub.Name)\nMessage Retention: $($eventhub.MessageRetentionInDays)\nPartition Count: $($eventhub.PartitionCount)\n`" ; color = lightgray;image = `"$OutputPath\icons\eventhub.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.0;];`n" 
            $data += " $namespaceid -> $eventhubid;`n"
        }
        if ($namespace.PrivateEndpointConnection.PrivateEndpointId) {
            $peid = $namespace.PrivateEndpointConnection.PrivateEndpointId.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $namespaceid -> $peid [label = `"Private Endpoint`"; ];`n"
        }
        $data += " label = `"$Name`";
                }`n"
    
        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export Event Hub Namespace: $($namespace.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of an App Service Plan for inclusion in a network diagram.
.DESCRIPTION
This function processes an App Service Plan object and formats its details for the Azure network diagram.
.PARAMETER plan
The App Service Plan object to process.
.EXAMPLE
Export-AppServicePlan -plan $plan
#>

function Export-AppServicePlan {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$plan
    )

    try {
        $resourceGroupName = $plan.Id.split("/")[4]
        $planid = $plan.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $plan.Location
        $Name = SanitizeString $plan.Name
        $data = "
        # $Name - $planid
        subgraph cluster_$planid {
            style = solid;
            color = black;
            node [color = white;];
        "


        $data += " $planid [label = `"\nLocation: $Location\nSKU: $($plan.Sku.Name)\nTier: $($plan.Sku.Tier)\nKind: $($plan.Kind)\nCapacity: $($plan.Sku.Capacity)\nNumber of Apps: $($plan.NumberOfSites)\n`" ; color = lightgray;image = `"$OutputPath\icons\appplan.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];"
        $data += "`n"

        # iterate through all web apps hosted on that plan
        Get-AzWebApp -ResourceGroupName $resourceGroupName -ErrorAction SilentlyContinue |
        Where-Object { $_.ServerFarmId -eq $plan.Id } |
        ForEach-Object {
            $app = $_
            $appid = $_.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $Location = SanitizeLocation $app.Location

            $data += " $($appid) [label = `"\n\nLocation: $Location\nName: $(SanitizeString $app.Name)\nKind: $($app.Kind)\nHost Name: $(SanitizeString $app.DefaultHostName)\n`" ; color = lightgray;image = `"$OutputPath\icons\appservices.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.0;];`n" 
            $data += " $planid -> $appid;`n"

            # Add links to Private Endpoints and Managed Identities
            if ($app.Identity.UserAssignedIdentities.Keys) {
                foreach ($identity in $app.Identity.UserAssignedIdentities.Keys) { 
                    $managedIdentityId = $identity.replace("-", "").replace("/", "").replace(".", "").ToLower() 
                    $data += " $appid -> $managedIdentityId;`n"
                } 
            }
        }
        $data += " label = `"$Name`";
                }`n"

        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export AppService Plan: $($plan.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }

}

<#
.SYNOPSIS
Exports details of an API Management (APIM) instance for inclusion in a network diagram.
.DESCRIPTION
This function processes an APIM object and formats its details for the Azure network diagram.
.PARAMETER apim
The API Management object to process.
.EXAMPLE
Export-APIM -apim $apim
#>

function Export-APIM {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$apim
    )
    try {
        $apimid = $apim.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $apim.Location
        $Name = SanitizeString $apim.Name
        $data = "
        # $Name - $apimid
        subgraph cluster_$apimid {
            style = solid;
            color = black;
            node [color = white;];
        "

        $apimCtx = New-AzApiManagementContext -ResourceGroupName $apim.ResourceGroupName -ServiceName $apim.name
        $prodCount = (Get-AzApiManagementProduct -Context $apimCtx -ErrorAction SilentlyContinue).Count
        $apiCount = (Get-AzApiManagementApi     -Context $apimCtx -ErrorAction SilentlyContinue).Count
        $PublicIPAddresses = ($apim.PublicIPAddresses | Foreach-Object { SanitizeString $_ }) -join ", "
        $PrivateIPAddresses = ($apim.PrivateIPAddresses | Foreach-Object { SanitizeString $_ }) -join ", "

        $data += " $apimid [label = `"\nLocation: $Location\nSKU: $($apim.Sku)\nPlatform Version: $($apim.PlatformVersion)\nPublic IP Addresses: $PublicIPAddresses\nPrivate IP Addresses: $PrivateIPAddresses\nCapacity: $($apim.Capacity)\nZone: $($apim.Zone)\nPublic Network Access: $($apim.PublicNetworkAccess)\nProducts: $prodCount\nAPI's: $apiCount\nVirtual Network: $($apim.VpnType)`" ; color = lightgray;image = `"$OutputPath\icons\apim.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];"
        $data += "`n"
        if ($apim.VirtualNetwork.SubnetResourceId) {
            $subnetid = $apim.VirtualNetwork.SubnetResourceId.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $apimid -> $subnetid;`n"
        }
        if ($apim.PrivateEndpointConnections.Id) {
            $peid = $apim.PrivateEndpointConnections.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $apimid -> $peid [label = `"Private Endpoint`"; ];`n"
        }
        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile -Data $data
    }
    catch {
        Write-Host "Can't export APIM: $($apim.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of an Azure Container Registry (ACR) for inclusion in a network diagram.
.DESCRIPTION
This function processes an Azure Container Registry object and formats its details for the Azure network diagram.
.PARAMETER acr
The Azure Container Registry object to process.
.EXAMPLE
Export-ACR -acr $acr
#>

function Export-ACR {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$acr
    )   
    
    try {
        $acrid = $acr.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $acr.Location
        $Name = SanitizeString $acr.Name
        $data = "
        # $Name - $acrid
        subgraph cluster_$acrid {
            style = solid;
            color = black;
            node [color = white;];
        "



        $data += " $acrid [label = `"\nACR Name: $Name\nLocation: $Location\nSKU: $($acr.SkuName.ToString())\nZone Redundancy: $($acr.ZoneRedundancy.ToString())\nPublic Network Access: $($acr.PublicNetworkAccess.ToString())\n`" ; color = lightgray;image = `"$OutputPath\icons\acr.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];"
        $data += "`n"
        if ($acr.PrivateEndpointConnection.PrivateEndpointId) {
            $acrpeid = $acr.PrivateEndpointConnection.PrivateEndpointId.ToString().replace("-", "").replace("/", "").replace(".", "").ToLower()
            $data += " $acrid -> $($acrpeid) [label = `"Private Endpoint`"; ];`n"
        }
        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile $data

    }
    catch {
        Write-Host "Can't export ACR: $($acr.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a Storage Account for inclusion in a network diagram.
.DESCRIPTION
This function processes a Storage Account object and formats its details for the Azure network diagram.
.PARAMETER storageaccount
The Storage Account object to process.
.EXAMPLE
Export-StorageAccount -storageaccount $storageaccount
#>

function Export-StorageAccount {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$storageaccount
    )   
    
    try {
        $staid = $storageaccount.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $Location = SanitizeLocation $storageaccount.Location
        $Name = SanitizeString $storageaccount.StorageAccountName
        $data = "
        # $Name - $staid
        subgraph cluster_$staid {
            style = solid;
            color = black;
            node [color = white;];
        "

        if ($storageaccount.PublicNetworkAccess -eq "Disabled") {
            $PublicNetworkAccess = "Disabled"
        }   
        elseif ($storageaccount.NetworkRuleSet.DefaultAction -eq "Allow") {
            $PublicNetworkAccess = "Enabled from all networks"
        }
        else {
            $PublicNetworkAccess = "Enabled from selected virtual`nnetworks and IP addresses"
        }
        $HierarchicalNamespace = $storageaccount.EnableHierarchicalNamespace ? "Enabled" : "Disabled"
        $data += " $staid [label = `"\n\nLocation: $Location\nSKU: $($storageaccount.Sku.Name)\nKind: $($storageaccount.Kind)\nPublic Network Access: $PublicNetworkAccess\nAccess Tier: $($storageaccount.AccessTier)\nHierarchical Namespace: $HierarchicalNamespace\n`" ; color = lightgray;image = `"$OutputPath\icons\storage-account.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.0;];"
        $data += "`n"
        $peids = Get-AzPrivateEndpointConnection -PrivateLinkResourceId $storageaccount.Id -ErrorAction Stop
        
        if ($peids) {
            foreach ($peid in $peids) {
                $stapeid = $peid.PrivateEndpoint.Id.ToString().replace("-", "").replace("/", "").replace(".", "").ToLower()
                $data += " $staid -> $($stapeid) [label = `"Private Endpoint`"; ];`n"
            }
        }
        $data += " label = `"$Name`";
                }`n"


        Export-AddToFile $data

    }
    catch {
        Write-Host "Can't export Storage Account: $($storageaccount.StorageAccountName) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of an Azure Firewall and its associated policies for inclusion in a network diagram.
 
.DESCRIPTION
The `Export-AzureFirewall` function processes a specified Azure Firewall object, retrieves its details, and formats the data for inclusion in a network diagram. It visualizes the firewall's name, private and public IP addresses, SKU tier, zones, and associated firewall policies, including DNS settings and IP groups.
 
.PARAMETER FirewallId
Specifies the unique identifier of the Azure Firewall to be processed.
 
.PARAMETER ResourceGroupName
Specifies the resource group of the Azure Firewall.
 
.EXAMPLE
PS> Export-AzureFirewall -FirewallId "/subscriptions/xxxx/resourceGroups/rg1/providers/Microsoft.Network/azureFirewalls/fw1" -ResourceGroupName "rg1"
 
This example processes the specified Azure Firewall and exports its details for inclusion in a network diagram.
 
#>

function Export-AzureFirewall {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$FirewallId,
        [Parameter(Mandatory = $true)]
        [string]$ResourceGroupName
    )

    try {
        $azFWId = $FirewallId.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $azFWName = $FirewallId.split("/")[-1]
        $azFW = Get-AzFirewall -ResourceGroupName $ResourceGroupName -Name $azFWName -ErrorAction Stop

        if ($azFW.IpConfigurations.count -gt 0) {
            # Standalone Azure Firewall
            $PrivateIPAddress = $azFW.IpConfigurations.PrivateIPAddress -join ""
            $ipConfigs = $azFW.IpConfigurations
            $PublicIPs = @()
            if ($ipConfigs) {
                foreach ($ipConfig in $ipConfigs) {
                    $publicIpId = $ipConfig.PublicIpAddress.Id
                    $publicIpName = $publicIpId.Split('/')[-1]
                    $publicIpRG = $publicIpId.Split('/')[4]
                    
                    $PublicIps += SanitizeString (Get-AzPublicIpAddress -ResourceGroupName $publicIpRG -Name $publicIpName -ErrorAction Stop).IpAddress
                }
            }
        }
        else {
            # Hub Integrated Azure Firewall
            $PrivateIPAddress = $azFW.HubIPAddresses.PrivateIPAddress
            $PublicIPs = ""
            foreach ($publicIP in $azFW.HubIPAddresses.PublicIPs.Addresses) { $PublicIPs += ((SanitizeString $publicIP.Address) + "\n") }
        }
        $data = "`n"
        $data += " $azFWId [label = `"\n\n$(SanitizeString $azFWName)\nPrivate IP Address: $(SanitizeString $PrivateIPAddress)\nSKU Tier: $($azfw.Sku.Tier)\nZones: $($azfw.zones -join "," )\nPublic IP(s):\n$($PublicIPs -join "\n")`" ; color = lightgray;image = `"$OutputPath\icons\afw.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];" 

        # Get the Azure Firewall policy
        $firewallPolicyName = $azfw.FirewallPolicy.id.split("/")[-1]
        $firewallPolicy = Get-AzFirewallPolicy -ResourceGroupName $ResourceGroupName -Name $firewallPolicyName -ErrorAction Stop
        $fwpolid = $firewallPolicy.Id.replace("-", "").replace("/", "").replace(".", "").ToLower()

        $data += "`n"
        $data += " $fwpolid [label = `"\n\n$(SanitizeString $firewallPolicyName)\nSKU Tier: $($firewallPolicy.sku.tier)\nThreat Intel Mode: $($firewallPolicy.ThreatIntelMode)\nDNS Servers: $(($firewallPolicy.DnsSettings.Servers|ForEach-Object {SanitizeString $_}) -join '; ')\nProxy Enabled: $($firewallPolicy.DnsSettings.EnableProxy)`" ; color = lightgray;image = `"$OutputPath\icons\firewallpolicy.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];" 
        $data += "`n $azFWId -> $fwpolid;"

        for ($i = 0; $i -lt $firewallPolicy.DnsSettings.Servers.Count; $i++) {
            $index = [array]::indexof($script:PDNSREpIp, $firewallPolicy.DnsSettings.Servers[$i])
            if ($index -ge 0) {
                $data += " $fwpolid -> $($script:PDNSRId[$index]) [label = `"DNS Query`"; ];`n" 
            }
        }
       
        # Initialize an array to store IP Group names
        $ipGroupIds = @()

        foreach ($ruleCollectionGroupId in $firewallPolicy.RuleCollectionGroups.Id) {
            $rcgName = $ruleCollectionGroupId.split("/")[-1]
            $rcg = Get-AzFirewallPolicyRuleCollectionGroup -Name $rcgName -AzureFirewallPolicy $firewallPolicy -ErrorAction Stop
            $ipGroupIds += $rcg.Properties.RuleCollection.rules.SourceIpGroups 
            $ipGroupIds += $rcg.Properties.RuleCollection.rules.DestinationIpGroups
        }

        # Remove duplicates and display the IP Group names
        $ipGroupIds = $ipGroupIds | Sort-Object -Unique
        $ipGroupIds = $ipGroupIds.replace("-", "").replace("/", "").replace(".", "").ToLower()
        foreach ($ipGroupId in $ipGroupIds) {
            $data += "`n $fwpolid -> $ipGroupId;"
        }
        return $data
    }
    catch {
        Write-Host "Can't export Azure Firewall: $($azFWName) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a Virtual WAN Hub for inclusion in a network diagram.
 
.DESCRIPTION
The `Export-Hub` function processes a specified Virtual WAN Hub object, retrieves its details, and formats the data for inclusion in a network diagram. It visualizes the hub's name, location, SKU, address prefix, routing preference, and associated resources such as VPN gateways, ExpressRoute gateways, and Azure Firewalls.
 
.PARAMETER hub
Specifies the Virtual WAN Hub object to be processed.
 
.EXAMPLE
PS> Export-Hub -hub $vwanHub
 
This example processes the specified Virtual WAN Hub and exports its details for inclusion in a network diagram.
 
#>

function Export-Hub {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject[]]$hub
    )
    $hubname = $hub.Name
    $hubrgname = $hub.ResourceGroupName
    $Name = SanitizeString $hubname
    $id = $hub.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
    $location = SanitizeLocation $hub.Location
    $sku = $hub.Sku
    $AddressPrefix = $hub.AddressPrefix
    $HubRoutingPreference = $hub.HubRoutingPreference

    try {
        Write-Host "Exporting vWAN Hub: $hubname"
        # DOT
        # Hub details
        $data = "
            # $Name - $id
            subgraph cluster_$id {
                style = solid;
                color = black;
                node [color = white;];
            "


        # Find out the Hub's own vNet
        if ($null -ne $hub.VirtualNetworkConnections) {
            $vnetname = ($hub.VirtualNetworkConnections[0].RemoteVirtualNetwork.id).Split("/")[-1]
            $vnetrg = ($hub.VirtualNetworkConnections[0].RemoteVirtualNetwork.id).Split("/")[4]
            $vnet = Get-AzVirtualNetwork -name $vnetname -ResourceGroupName $vnetrg -ErrorAction Stop
            $HubvNetID = $vnet.VirtualNetworkPeerings.RemoteVirtualNetwork.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $headid = $HubvNetID
            $script:AllInScopevNetIds += $vnet.VirtualNetworkPeerings.RemoteVirtualNetwork.id

            $data += " $HubvNetID [label = `"\n\n$Name\nLocation: $location\nSKU: $sku\nAddress Prefix: $(SanitizeString $AddressPrefix)\nHub Routing Preference: $HubRoutingPreference`" ; color = lightgray;image = `"$OutputPath\icons\vWAN-Hub.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];"
        }
        else {
            $data += " $id [label = `"\n$Name\nLocation: $location\nSKU: $sku\nAddress Prefix: $(SanitizeString $AddressPrefix)\nHub Routing Preference: $HubRoutingPreference`" ; color = lightgray;image = `"$OutputPath\icons\vWAN-Hub.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];"
            $headid = $id
        }
        $script:rankvwanhubs += $headid

        # Hub Items
        if ($null -ne $hub.VpnGateway) {
            $vgwId = $hub.VpnGateway.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $vgwName = $hub.VpnGateway.id.split("/")[-1]
            $vgwNameShort = $vgwName.split("-")[1, 2, 3] -join ("-")
            $vpngw = Get-AzVpnGateway -ResourceGroupName $hub.ResourceGroupName -Name $vgwName -ErrorAction Stop
            
            $data += "`n"
            $data += " $vgwId [label = `"\n\n$(SanitizeString $vgwNameShort)\nScale Units: $($vpngw.VpnGatewayScaleUnit)\nPublic IP(s):\n$(($vpngw.IpConfigurations.PublicIpAddress | ForEach-Object {SanitizeString $_}) -join ",")\n`" ; color = lightgray;image = `"$OutputPath\icons\vgw.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];" 
            $data += "`n $headid -> $vgwId;"

            # Connections
            $VpnSites = Get-AzVPNSite -ResourceGroupName $hub.ResourceGroupName  -ErrorAction Stop | Where-Object { $_.VirtualWan.id -eq $hub.virtualwan.id }
            # Get the VPN connections from this gateway
            $vpnConnections = $vpngw.Connections

            #foreach ($VpnSite in $VpnSites) {
            foreach ($connection in $vpnConnections) {
                # Find which VPN site this connection is linked to
                $siteId = $connection.RemoteVpnSite.Id
                $vpnSite = $VpnSites | Where-Object { $_.Id -eq $siteId }
            
                if ($vpnSite) {
                    $vpnsiteId = $siteId.replace("-", "").replace("/", "").replace(".", "").ToLower()
                    $script:rankvpnsites += $vpnsiteId
                    $vpnsiteName = SanitizeString $VpnSite.Name
                    $peerip = $vpnSite.VpnSiteLinks.IpAddress
                    $data += "`n"
                    $data += " $vpnsiteId [label = `"\n\n$(SanitizeString $vpnsiteName)\nDevice Vendor: $($VpnSite.DeviceProperties.DeviceVendor)\nLink Speed: $($VpnSite.VpnSiteLinks.LinkProperties.LinkSpeedInMbps) Mbps\nLinks: $($VpnSite.VpnSiteLinks.count)\n\nPeer IP: $(SanitizeString $peerip)\nAddressPrefixes: $(($VpnSite.AddressSpace.AddressPrefixes | ForEach-Object {SanitizeString $_}) -join ",")\n`" ; color = lightgray;image = `"$OutputPath\icons\VPN-Site.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];" 
                    $data += "`n $vgwId -> $vpnsiteId;"
                }
            }
        }

        if ($null -ne $hub.ExpressRouteGateway) {
            $ergwId = $hub.ExpressRouteGateway.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $ergwName = $hub.ExpressRouteGateway.id.split("/")[-1]
            $ergw = Get-AzExpressRouteGateway -ResourceGroupName $hub.ResourceGroupName -Name $ergwName -ErrorAction Stop
            $data += "`n"
            $data += " $ergwId [label = `"\n\n\n$(SanitizeString $ergwName)\nAuto Scale Configuration: $($ergw.AutoScaleConfiguration.Bounds.min)-$($ergw.AutoScaleConfiguration.Bounds.max)`" ; color = lightgray;image = `"$OutputPath\icons\ergw.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];" 
            $data += "`n $headid -> $ergwId;"
            $peerings = $ergw.ExpressRouteConnections.ExpressRouteCircuitPeering.id
            foreach ($peering in $peerings) {
                $peeringId = $peering.replace("-", "").replace("/", "").replace(".", "").replace("peeringsAzurePrivatePeering", "").ToLower()
                $data += "`n $ergwId -> $peeringId ;"
            }
        }
        if ($null -ne $hub.P2SVpnGateway) {
            $p2sgwId = $hub.P2SVpnGateway.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $p2sgwName = $hub.P2SVpnGateway.id.split("/")[-1]
            $p2sgwNameShort = $p2sgwName.split("-")[1, 2, 3] -join ("-")
            $p2sgw = Get-AzP2sVpnGateway -ResourceName $p2sgwName
            $cidr = $p2sgw.P2SConnectionConfigurations.VpnClientAddressPool.AddressPrefixes

            $configname = $p2sgw.P2SConnectionConfigurations.Name
            $configid = $p2sgw.P2SConnectionConfigurations.Id
            
            $vpnserverconfigs = Get-AzVpnServerConfiguration -ResourceGroupName $hubrgname
            $vpnserverconfig = ''
            foreach ( $config in $vpnserverconfigs ) { 
                if ( $config.P2SVpnGateways.id -eq $p2sgw.id ) { $vpnserverconfig = $config }
            }
                     
            $protocol = $vpnserverconfig.VpnProtocols
            $auth = $vpnserverconfig.VpnAuthenticationTypes
            
            $data += "`n"
            $data += " $p2sgwId [label = `"\n\n$(SanitizeString $p2sgwNameShort)\nProtocol: $protocol, Auth: $auth\nP2S Address Prefixes: $(($cidr | ForEach-Object {SanitizeString $_}) -join ", ")`" ; color = lightgray;image = `"$OutputPath\icons\VPN-User.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];" 
            $data += "`n $headid -> $p2sgwId;"
        }
        if ($null -ne $hub.AzureFirewall) {
            $data += Export-AzureFirewall -FirewallId $hub.AzureFirewall.id -ResourceGroupName $hub.ResourceGroupName
            $azFWId = $hub.AzureFirewall.id.replace("-", "").replace("/", "").replace(".", "").ToLower()

            $data += "`n $headid -> $azFWId [label = `"Secure Hub`"];"
        }
        $vWANId = $hub.VirtualWAN.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $data += "`n $vWANId -> $headid [label = `"vWAN Hub`"];"
        $footer = "
        label = `"$Name`";
        }
        "

        $data += $footer

        return $data
    }
    catch {
        Write-Error "Can't export Hub: $($hub.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
        return $null
    }
}

<#
.SYNOPSIS
Exports details of a Virtual Network Gateway for inclusion in a network diagram.
 
.DESCRIPTION
The `Export-VirtualGateway` function processes a specified Virtual Network Gateway object, retrieves its details, and formats the data for inclusion in a network diagram. It visualizes the gateway's name, type (VPN or ExpressRoute), and associated public IP addresses.
 
.PARAMETER GatewayName
Specifies the name of the Virtual Network Gateway to be processed.
 
.PARAMETER ResourceGroupName
Specifies the resource group of the Virtual Network Gateway.
 
.PARAMETER GatewayId
Specifies the unique identifier of the Virtual Network Gateway.
 
.PARAMETER HeadId
Specifies the identifier of the parent resource to which the gateway is connected.
 
.EXAMPLE
PS> Export-VirtualGateway -GatewayName "MyGateway" -ResourceGroupName "MyResourceGroup" -GatewayId "gateway123" -HeadId "vnet123"
 
This example processes the specified Virtual Network Gateway and exports its details for inclusion in a network diagram.
 
#>

function Export-VirtualGateway {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$GatewayName, 
        [Parameter(Mandatory = $true)]
        [string]$ResourceGroupName, 
        [Parameter(Mandatory = $true)]
        [string]$GatewayId, 
        [Parameter(Mandatory = $true)]
        [string]$HeadId
    )   
    
    $gw = Get-AzVirtualNetworkGateway -ResourceGroupName $ResourceGroupName -ResourceName $GatewayName -ErrorAction Stop
    $gwtype = $gw.Gatewaytype

    $script:rankvgws += $GatewayId

    # ER vs VPN GWs are handled differently
    if ($gwtype -eq "Vpn" ) {
        $gwipobjetcs = $gw.IpConfigurations.PublicIpAddress
        $gwips = ""
        $gwipobjetcs.id | ForEach-Object {
            $rgname = $_.split("/")[4]
            $ipname = $_.split("/")[8]
            $publicip = SanitizeString (Get-AzPublicIpAddress -ResourceName $ipname -ResourceGroupName $rgname -ErrorAction Stop).IpAddress
            $gwips += "$(SanitizeString $ipname) : $publicip \n"
        
        }
        $data += " $GatewayId [color = lightgray;label = `"\n\nName: $(SanitizeString $GatewayName)`\n\nPublic IP(s):\n$gwips`";image = `"$OutputPath\icons\vgw.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];"

        # Get P2S conf, if configured
        $protocol = $gw.VpnClientConfiguration.VpnClientProtocols
        $cidr = $gw.VpnClientConfiguration.VpnClientAddressPool.AddressPrefixes
        $auth = $gw.VpnClientConfiguration.VpnAuthenticationTypes
        $customroutes = $gw.CustomRoutes.AddressPrefixes

        if ($null -ne $auth) {
            #P2S config present
            $data += " ${GatewayId}P2S [color = lightgray;label = `"\n\nProtocol: $protocol, Auth: $auth\nP2S Address Prefix: $(SanitizeString $cidr)\nCustom routes: $(($customroutes | ForEach-Object {SanitizeString $_}) -join ",")`"; image = `"$OutputPath\icons\VPN-User.png`"; imagepos = `"tc`"; labelloc = `"b`"; height = 1.5; ]; "
            $data += " $GatewayId -> ${GatewayId}P2S"
        } 
    }
    elseif ($gwtype -eq "ExpressRoute") {
        $data += " $GatewayId [color = lightgray; label = `"\nName: $(SanitizeString $GatewayName)`"; image = `"$OutputPath\icons\ergw.png`"; imagepos = `"tc`"; labelloc = `"b`"; height = 1.5; ]; "
    }
    $data += "`n"
    $data += " $HeadId -> $GatewayId"
    $data += "`n"

    return $data

}

<#
.SYNOPSIS
Exports details of a subnet configuration for inclusion in a network diagram.
 
.DESCRIPTION
The `Export-SubnetConfig` function processes a list of subnet objects, retrieves their details, and formats the data for inclusion in a network diagram. It visualizes subnet properties such as name, address prefix, associated NSGs, route tables, NAT gateways, and special configurations like Azure Firewall, Bastion, and Gateway subnets.
 
.PARAMETER subnets
Specifies the list of subnet objects to be processed.
 
.EXAMPLE
PS> Export-SubnetConfig -subnets $subnetList
 
This example processes the specified list of subnets and exports their details for inclusion in a network diagram.
 
#>

function Export-SubnetConfig {
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [PSCustomObject[]] $subnets
    )

    try {
        $data = ""

        #Loop over subnets
        foreach ($subnet in $subnets) {
            $id = $subnet.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $name = $subnet.Name
            $AddressPrefix = SanitizeString $subnet.AddressPrefix
            $script:ranksubnets += $id

            # vNet
            $vnetid = $subnet.id
            $vnetid = $vnetid -split "/subnets/"
            $vnetid = $vnetid[0].replace("-", "").replace("/", "").replace(".", "").ToLower()
            $nsgid = $null

            ##########################################
            ##### Special subnet characteristics #####
            ##########################################
                    
            ### NSG ###
            if ($null -ne $subnet.NetworkSecurityGroup) {
                $nsgid = $subnet.NetworkSecurityGroup.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
                if ($nsgid -ne "null") { 
                    $data += "`n $id -> $nsgid`n"
                }
            }

            ### Route Table ###
            $routetableid = $subnet.RouteTableText.ToLower()
            if ($routetableid -ne "null" ) { $routetableid = (($subnet.RouteTableText | ConvertFrom-Json).id).replace("-", "").replace("/", "").replace(".", "").ToLower() }
            if ($routetableid -ne "null" ) { $data += " $id -> $routetableid" + "`n" }

            ### Private subnet - ie. no default outbound internet access ###
            $subnetDefaultOutBoundAccess = $subnet.DefaultOutboundAccess #(false if activated)
            if ($subnetDefaultOutBoundAccess -eq $false ) { $name += " *" }


            ##############################################
            ##### Special subnet characteristics END #####
            ##############################################
            
            # Support for different types of subnets (AzFW, Bastion etc.)
            # DOT
            switch ($name) {
                "AzureFirewallSubnet" { 
                    if ($subnet.IpConfigurations.Id) {
                        $AzFWid = $subnet.IpConfigurations.Id.ToLower().split("/azurefirewallipconfigurations/ipconfig1")[0]
                        $AzFWrg = $subnet.IpConfigurations.id.split("/")[4]

                        $data += " $id [label = `"\n\n$name\n$AddressPrefix`" ; color = lightgray; image = `"$OutputPath\icons\afw.png`"; imagepos = `"tc`"; labelloc = `"b`"; height = 1.5; ]; " 

                        $data += Export-AzureFirewall -FirewallId $AzFWid -ResourceGroupName $AzFWrg
                        $AzFWDotId = $AzFWid.replace("-", "").replace("/", "").replace(".", "").ToLower()
                        $data += "`n $id -> $azFWDotId"
                    }
                }
                "AzureBastionSubnet" { 
                    if ($subnet.IpConfigurations.Id) { 
                        $AzBastionName = SanitizeString $subnet.IpConfigurations.Id.split("/")[8].ToLower()
                    
                        $data += " $id [label = `"\n\n$name\n$AddressPrefix\nName: $AzBastionName`" ; color = lightgray; image = `"$OutputPath\icons\bas.png`"; imagepos = `"tc`"; labelloc = `"b`"; height = 1.5; ]; " 
                    }
                }
                "AppGatewaySubnet" { 
                    if ($subnet.IpConfigurations.Id) { 
                        $AppGatewayName = SanitizeString $subnet.IpConfigurations.Id.split("/")[8].ToLower()
                    
                        $data += " $id [label = `"\n\n$name\n$AddressPrefix\nName: $AppGatewayName`" ; color = lightgray; image = `"$OutputPath\icons\agw.png`"; imagepos = `"tc`"; labelloc = `"b`"; height = 1.5; ]; " 
                    }
                }
                "GatewaySubnet" { 
                    $data += " $id [label = `"\n\n$name\n$AddressPrefix`" ; color = lightgray; image = `"$OutputPath\icons\vgw.png`"; imagepos = `"tc`"; labelloc = `"b`"; height = 1.5; ]; " 
                    $data += "`n"
                    
                    #GW DOT
                    if ($subnet.IpConfigurations.Id) {
                        foreach ($subnet in $subnet.IpConfigurations.Id) {
                            $gwid = $subnet.split("/ipConfigurations/")[0].replace("-", "").replace("/", "").replace(".", "").ToLower()
                            $gwname = $subnet.split("/")[8].ToLower()
                            $gwrg = $subnet.split("/")[4].ToLower()
                            $data += Export-VirtualGateway -GatewayName $gwname -ResourceGroupName $gwrg -GatewayId $gwid -HeadId $id
                        }
                    }
                }
                default { 
                    ##### Subnet delegations #####
                    $subnetDelegationName = $subnet.Delegations.Name
                    
                    if ( $null -ne $subnetDelegationName ) {
                        # Delegated
                        $iconname = ""
                        switch ($subnetDelegationName) {
                            "Microsoft.Web/serverFarms" { $iconname = "appplan" }
                            "Microsoft.Sql/managedInstances" { $iconname = "sqlmi" } 
                            "Microsoft.Network/dnsResolvers" { $iconname = "dnspr" }
                            Default { $iconname = "snet" }
                        }
                        $data = $data + " $id [label = `"\n\n$(SanitizeString $name)\n$AddressPrefix\n\nDelegated to:\n$subnetDelegationName`" ; color = lightgray; image = `"$OutputPath\icons\$iconname.png`"; imagepos = `"tc`"; labelloc = `"b`"; height = 1.5; ]; " 
                    }
                    else {
                        # No Delegation
                        $data = $data + " $id [label = `"\n\n$(SanitizeString $name)\n$AddressPrefix`" ; color = lightgray; image = `"$OutputPath\icons\snet.png`"; imagepos = `"tc`"; labelloc = `"b`"; height = 1.5; ]; " 
                    }
                    $data += "`n"
                    foreach ($pe in $subnet.PrivateEndpoints) {
                        $peid = $pe.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
                        $data += " $id -> $peid [label = `"Private Endpoint`"; ] ; `n"
                    }
                }
            }
            $data += "`n"
            
            # DOT VNET->Subnet
            $data = $data + " $vnetid -> $id"
            $data += "`n"
        
            #NATGW
            if ($subnet.NatGateway.count -gt 0 ) {
                #Define NAT GW
                $NATGWID = $subnet.NatGateway.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
                
                $name = $subnet.NatGateway.id.split("/")[8]
                $rg = $subnet.NatGateway.id.split("/")[4]
                $NATGWobject = Get-AzNatGateway -Name $name -ResourceGroupName $rg -ErrorAction Stop
                
                #Public IPs associated
                $ips = $NATGWobject.PublicIpAddresses
                $ipsstring = ""
                if ($ips.id) {
                    $ips.id | ForEach-Object {
                        $rgname = $_.split("/")[4]
                        $ipname = $_.split("/")[8]
                        $publicip = SanitizeString (Get-AzPublicIpAddress -ResourceName $ipname -ResourceGroupName $rgname -ErrorAction Stop).IpAddress
                        $ipsstring += "$ipname : $publicip \n"
                    }
                }
                #Public IP prefixes associated
                $ipprefixes = $NATGWobject.PublicIpPrefixes | ForEach-Object { SanitizeString $_ }
                $ipprefixesstring = ""
                if ($ipprefixes.id) {
                    $ipprefixes.id | ForEach-Object {
                        $rgname = $_.split("/")[4]
                        $ipname = $_.split("/")[8]
                        $ipprefixesstring += "$ipname : $ipprefixes \n"
                    }
                }   
                $data += " $NATGWID [color = lightgrey; label = `"\n\nName: $(SanitizeString $name)\n\nPublic IP(s):\n$ipsstring\nPublic IP Prefix(es):\n$ipprefixesstring`"; image = `"$OutputPath\icons\ng.png`"; imagepos = `"tc`"; labelloc = `"b`"; height = 1.5; ]; "
                $data += " $id -> $NATGWID" + "`n"

            }
        }
    }
    catch {
        Write-Host "Can't export Subnet: $($subnet.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
    return $data
}

<#
.SYNOPSIS
Exports details of a virtual network (VNet) for inclusion in a network diagram.
 
.DESCRIPTION
The `Export-vnet` function processes a specified virtual network object, retrieves its details, and formats the data for inclusion in a network diagram. It visualizes the VNet's name, address spaces, subnets, associated private DNS resolvers, and other configurations.
 
.PARAMETER vnet
Specifies the virtual network object to be processed.
 
.EXAMPLE
PS> Export-vnet -vnet $vnet
 
This example processes the specified virtual network and exports its details for inclusion in a network diagram.
 
#>

function Export-vnet {
    [CmdletBinding()]
    param ([PSCustomObject[]]$vnet)

    try {
        $vnetname = SanitizeString $vnet.Name
        $Location = SanitizeLocation $vnet.Location
        $id = $vnet.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $vnetAddressSpaces = $vnet.AddressSpace.AddressPrefixes
        $script:rankvnetaddressspaces += $id

        $header = "
        # $vnetname - $id
        subgraph cluster_$id {
            style = solid;
            color = black;
            node [color = white;];
        "


        # Convert addressSpace prefixes from array to string
        $vnetAddressSpacesString = ""
        $vnetAddressSpaces | ForEach-Object {
            $vnetAddressSpacesString += $(SanitizeString $_) + "\n"
        }

        $vnetdata = " $id [color = lightgray;label = `"\nLocation: $Location\nAddress Space(s):\n$vnetAddressSpacesString`";image = `"$OutputPath\icons\vnet.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];`n"

        # Subnets
        if ($vnet.Subnets) {
            $subnetdata = Export-SubnetConfig $vnet.Subnets
        }
        # Retrieve all Private DNS Resolvers in a specific resource group
        $dnsResolvers = Get-AzDnsResolver | Where-Object { $_.VirtualNetworkId -eq $vnet.id } -ErrorAction Stop
        $dnsprdata = ""
        if ($dnsResolvers) {
            # Display details of each Private DNS Resolver
            foreach ($resolver in $dnsResolvers) {
                $resolverName = $resolver.Name
                $Location = SanitizeLocation $resolver.Location
                $inboundEp = (Get-AzDnsResolverInboundEndpoint -DnsResolverName $resolverName -ResourceGroupName $vnet.resourceGroupName -ErrorAction Stop)
                $outboundEp = (Get-AzDnsResolverOutboundEndpoint -DnsResolverName $resolverName -ResourceGroupName $vnet.resourceGroupName -ErrorAction Stop)
                $inboundEpIp = $inboundEp.IPConfiguration.PrivateIPAddress 
                $pdnsrId = $resolver.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
                $dnsFrs = Get-AzDnsForwardingRuleset -ResourceGroupName $vnet.ResourceGroupName -ErrorAction Stop | Where-Object { ($_.DnsResolverOutboundEndpoint).id -eq $outboundEp.id }
                
                if ($dnsFrs) {
                    # Retrieve and display Forwarding Rulesets associated with the resolver
                    $dnsFrsId = $dnsFrs.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
                    $frsRules = Get-AzDnsForwardingRulesetForwardingRule -DnsForwardingRulesetName $dnsFrs.name -ResourceGroupName $vnet.resourceGroupName -ErrorAction Stop

                    # DOT
                    $dnsprdata += "`n subgraph cluster_$pdnsrId {
                        style = solid;
                        color = black;
                             
                        $pdnsrId [label = <
                                        <TABLE border=`"0`" style=`"rounded`" align=`"left`">
                                        <TR><TD align=`"left`">Name</TD><TD align=`"left`">$(SanitizeString $resolverName)</TD></TR>
                                        <TR><TD align=`"left`">Location</TD><TD align=`"left`">$(SanitizeString $Location)</TD></TR>
                                        <TR><TD align=`"left`">Inbound IP Address</TD><TD align=`"left`">$(SanitizeString $inboundEpIp)</TD></TR>
                                        <TR><TD><BR/><BR/></TD></TR>
                                        <TR><TD colspan=`"3`" border=`"0`"><B>$(SanitizeString $dnsFrs.Name)</B></TD></TR>
                                        <TR><TD align=`"left`">Name</TD><TD align=`"left`">Domain Name</TD><TD align=`"left`">Target DNS</TD></TR>
                    "


                    foreach ($rule in $frsRules) {
                        $dnsprdata += " <TR><TD align=`"left`">$(SanitizeString $rule.Name)</TD><TD align=`"left`">$(SanitizeString $rule.DomainName)</TD><TD align=`"left`">$(($rule.TargetDnsServer.IPAddress | ForEach-Object {SanitizeString $_}) -join ', ')</TD></TR>`n"                    
                    }
                    # End table $pdnsrId -> $dnsFrsId;

                    $dnsprdata += "</TABLE>>; color = lightgray;image = `"$OutputPath\icons\dnspr.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.0;];
                        label = `"$(SanitizeString $resolverName)`";
                    }
                    "

                    $script:PDNSRepIP += $inboundEpIp
                    $script:PDNSRId += $pdnsrId
                }
            }
        }                            

        $footer = "
            label = `"$vnetname`";
        }
        "

        $alldata = $header + $vnetdata + $subnetdata + $footer + $dnsprdata
        Export-AddToFile -Data $alldata
    }
    catch {
        Write-Error "Can't export VNet: $($vnet.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of a Virtual WAN (vWAN) for inclusion in a network diagram.
 
.DESCRIPTION
The `Export-vWAN` function processes a specified Virtual WAN object, retrieves its details, and formats the data for inclusion in a network diagram. It visualizes the vWAN's name, type, location, and associated hubs, along with their configurations.
 
.PARAMETER vwan
Specifies the Virtual WAN object to be processed.
 
.EXAMPLE
PS> Export-vWAN -vwan $vWAN
 
This example processes the specified Virtual WAN and exports its details for inclusion in a network diagram.
 
#>

function Export-vWAN {
    [CmdletBinding()]
    param ([PSCustomObject[]]$vwan)

    $vwanname = $vwan.Name
    $Name = SanitizeString $vwanname
    $id = $vwan.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
    $VirtualWANType = $vwan.VirtualWANType
    $ResourceGroupName = $vwan.ResourceGroupName
    $AllowVnetToVnetTraffic = $vwan.AllowVnetToVnetTraffic
    $AllowBranchToBranchTraffic = $vwan.AllowBranchToBranchTraffic
    $Location = SanitizeLocation $vwan.Location

    try {
        Write-Host "Exporting vWAN: $vwanname"
        $script:rankvwans += $id
        $hubs = Get-AzVirtualHub -ResourceGroupName $ResourceGroupName -ErrorAction Stop | Where-Object { $($_.VirtualWAN.id) -eq $($vwan.id) }
        if ($null -ne $hubs) {
            $header = "
            # $Name - $id
            subgraph cluster_$id {
                style = solid;
                color = black;
                node [color = white;];
            "

        
            # Convert addressSpace prefixes from array to string
            $vWANDetails = "Virtual WAN Type: $VirtualWANType\nLocation: $Location\nAllow Vnet to Vnet Traffic: $AllowVnetToVnetTraffic\nAllow Branch to Branch Traffic: $AllowBranchToBranchTraffic"
            
            $vwandata = " $id [color = lightgray;label = `"\n$vWANDetails`";image = `"$OutputPath\icons\vwan.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.0;];`n"
            $footer = "
                label = `"$Name`";
            }
            "

            $alldata = $header + $vwandata + $footer
        
            # Hubs
            $hubdata = ""
            foreach ($hub in $hubs) {
                $hubdata += Export-Hub -Hub $hub
            }
        
            Export-AddToFile -Data $alldata
            Export-AddToFile -Data $hubdata

        }            
    }
    catch {
        Write-Error "Can't export Hub: $($hub.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of an ExpressRoute Circuit for inclusion in a network diagram.
 
.DESCRIPTION
The `Export-ExpressRouteCircuit` function processes a specified ExpressRoute Circuit object, retrieves its details, and formats the data for inclusion in a network diagram. It visualizes the circuit's name, SKU, bandwidth, provider, peering details, and associated ExpressRoute Direct ports if applicable.
 
.PARAMETER er
Specifies the ExpressRoute Circuit object to be processed.
 
.EXAMPLE
PS> Export-ExpressRouteCircuit -er $expressRouteCircuit
 
This example processes the specified ExpressRoute Circuit and exports its details for inclusion in a network diagram.
 
#>

function Export-ExpressRouteCircuit {
    [CmdletBinding()]
    param ([PSCustomObject[]]$er)

    $ername = SanitizeString $er.Name
    $id = $er.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
    if ($er.ServiceProviderProperties) {
        $ServiceProviderName = $er.ServiceProviderProperties.ServiceProviderName
        $Peeringlocation = SanitizeLocation $er.ServiceProviderProperties.PeeringLocation
        $Bandwidth = $er.ServiceProviderProperties.BandwidthInMbps.ToString() + " Mbps"
        $BillingType = "N/A"
        $Encapsulation = "N/A"
    }
    else {
        # ExpressRoute Direct
        $erport = Get-AzExpressRoutePort -ResourceId $er.ExpressRoutePort.Id -ErrorAction Stop
        $erportid = $erport.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $erportname = SanitizeString $erport.Name.ToLower()
        $ServiceProviderName = "N/A"
        $Peeringlocation = SanitizeLocation $erport.PeeringLocation
        $Bandwidth = $erport.ProvisionedBandwidthInGbps.ToString() + " Gbps"
        $BillingType = $erport.BillingType
        $Encapsulation = $erport.Encapsulation
        $Location = SanitizeLocation $erport.Location

        $erportdata = "
        # $erportname - $erportid
        subgraph cluster_$erportid {
            style = solid;
            color = black;
            node [color = white;];
     
            $erportid [label = `"\nName: $erportname\nLocation: $Location\n`" ; color = lightgray;image = `"$OutputPath\icons\erport.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];
        "

        foreach ($link in $erport.Links) { 
            $linkid = $link.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
            $linkname = $link.Name.ToLower()
            if ($link.MacSecConfig.SciState -eq "Enabled") {
                $macsec = "Enabled"
            }
            else {
                $macsec = "Disabled"
            }

            $erportdata += "
                            $linkid [shape = none;label = <
                                <TABLE border=`"0`" style=`"rounded`">
                                <TR><TD colspan=`"2`" border=`"0`"><B>$linkname</B></TD></TR>
                                <HR/><TR><TD align=`"left`">Router Name</TD><TD align=`"left`">$($link.RouterName)</TD></TR>
                                <TR><TD align=`"left`">Interface Name</TD><TD align=`"left`">$($link.InterfaceName)</TD></TR>
                                <TR><TD align=`"left`">Patch Panel Id</TD><TD align=`"left`">$($link.PatchPanelId)</TD></TR>
                                <TR><TD align=`"left`">Rack Id</TD><TD align=`"left`">$($link.RackId)</TD></TR>
                                <TR><TD align=`"left`">Connector Type</TD><TD align=`"left`">$($link.ConnectorType)</TD></TR>
                                <TR><TD align=`"left`">Encapsulation</TD><TD align=`"left`">$Encapsulation</TD></TR>
                                <TR><TD align=`"left`">MACSEC</TD><TD align=`"left`">$macsec</TD></TR>
                                </TABLE>>;
                                ];
                            $erportid -> $linkid;
            "

        }
        $erportdata += "
            label = `"$erportname`";
            }
            $id -> $erportid;
        "

        Export-AddToFile -Data $erportdata
    }
    $skuTier = $er.sku.tier
    $skuFamily = $er.sku.family
    $Location = SanitizeLocation $er.Location

    $header = "
    # $ername - $id
    subgraph cluster_$id {
        style = solid;
        color = black;
        node [color = white;];
 
        $id [label = `"\nName: $ername\nLocation: $Location`" ; color = lightgray;image = `"$OutputPath\icons\ercircuit.png`";imagepos = `"tc`";labelloc = `"b`";height = 3.5;];
        $id [shape = none;label = <
            <TABLE border=`"1`" style=`"rounded`">
            <TR><TD>SKU Tier</TD><TD>$skuTier</TD></TR>
            <TR><TD>SKU Family</TD><TD>$skuFamily</TD></TR>
            <TR><TD>Billing Type</TD><TD>$BillingType</TD></TR>
            <TR><TD>Provider</TD><TD>$ServiceProviderName</TD></TR>
            <TR><TD>Location</TD><TD>$Peeringlocation</TD></TR>
            <TR><TD>Bandwidth</TD><TD>$Bandwidth</TD></TR>
    "

    $script:rankercircuits += $id
    # End table
    $header = $header + "</TABLE>>;
            ];
            label = `"$ername`";
        }
    "


    # Express Route Circuit Peerings
    $PeeringData = ""
    $erPeerings = $er.Peerings
    foreach ($peering in $erPeerings) {
        $peeringName = SanitizeString $peering.Name
        $peeringId = $peering.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $peeringType = $peering.PeeringType
        $AzureASN = SanitizeString $peering.AzureASN
        $PeerASN = SanitizeString $peering.PeerASN
        $PrimaryPeerAddressPrefix = SanitizeString $peering.PrimaryPeerAddressPrefix
        $SecondaryPeerAddressPrefix = SanitizeString $peering.SecondaryPeerAddressPrefix
        $VlanId = SanitizeString $peering.VlanId

        # DOT
        $PeeringData = $PeeringData + "
            # $peeringName - $peeringId
            subgraph cluster_$peeringId {
                style = solid;
                color = black;
                node [color = white;];
         
                $peeringId [label = `"\n$peeringName`" ; color = lightgray;image = `"$OutputPath\icons\peerings.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];
                $peeringId [shape = none;label = <
                    <TABLE border=`"1`" style=`"rounded`" align=`"left`">
                    <TR><TD>Peering Type</TD><TD COLSPAN=`"2`">$peeringType</TD></TR>
                    <TR><TD>Address Prefixes</TD><TD>$PrimaryPeerAddressPrefix</TD><TD>$SecondaryPeerAddressPrefix</TD></TR>
                    <TR><TD>ASN Azure/Peer</TD><TD>$AzureASN</TD><TD>$PeerASN</TD></TR>
                    <TR><TD>VlanId</TD><TD colspan=`"2`">$VlanId</TD></TR>
                    </TABLE>>;
                    ];
 
                $id -> $peeringId [ltail = cluster_$id; lhead = cluster_$peeringId;];
 
                label = `"$peeringName`";
            }
            "

    }
    $footer = ""
    $alldata = $header + $PeeringData + $footer
    Export-AddToFile -Data $alldata
}

<#
.SYNOPSIS
Exports details of a route table for inclusion in a network diagram.
 
.DESCRIPTION
The `Export-RouteTable` function processes a specified route table object, retrieves its routes, and formats the data for inclusion in a network diagram. It visualizes the route table name, address prefixes, next hop types, and next hop IP addresses.
 
.PARAMETER routetable
Specifies the route table object to be processed.
 
.EXAMPLE
PS> Export-RouteTable -routetable $routeTable
 
This example processes the specified route table and exports its details for inclusion in a network diagram.
 
#>

function Export-RouteTable {
    [CmdletBinding()]
    param ([PSCustomObject[]]$routetable)

    try {
        $routetableName = SanitizeString $routetable.Name
        $Location = SanitizeLocation $routetable.Location
        $id = $routetable.id.replace("-", "").replace("/", "").replace(".", "").ToLower()

        $script:rankrts += $id

        $header = "
        subgraph cluster_$id {
            style = solid;
            color = black;
             
            $id [label = <
                <TABLE border=`"0`" style=`"rounded`">
                <TR><TD colspan=`"3`" border=`"0`"><B>$routetableName</B></TD></TR>
                <TR><TD colspan=`"3`" border=`"0`">Location: $Location</TD></TR>
                <TR><TD><B>Route</B></TD><TD><B>NextHopType</B></TD><TD><B>NextHopIpAddress</B></TD></TR>
                <HR/>"

        
        # Individual Routes
        $data = ""
        ForEach ($route in $routetable.Routes ) {
            if ($route.AddressPrefix -match '^[a-zA-Z]+$') {
                # Only letters, not IP address or CIDR
                $addressprefix = $route.AddressPrefix
            }
            else {
                $addressprefix = $route.AddressPrefix ? $(SanitizeString $route.AddressPrefix) : ""
            }
            $nexthoptype = $route.NextHopType
            $nexthopip = $route.NextHopIpAddress ? $(SanitizeString $route.NextHopIpAddress) : ""
            $data = $data + "<TR><TD align=`"left`">$addressprefix</TD><TD align=`"left`">$nexthoptype</TD><TD align=`"left`">$nexthopip</TD></TR>"
        }
        if ($data -eq "") {
            $data = "<TR><TD align=`"left`">No routes found</TD><TD align=`"left`">N/A</TD><TD align=`"left`">N/A</TD></TR>"
        }
        # End table
        $footer = "
                </TABLE>>;
                image = `"$OutputPath\icons\RouteTable.png`";imagepos = `"tc`";imagepos = `"tc`";labelloc = `"b`";height = 2.5;];
        }
                "

        $alldata = $header + $data + $footer
        Export-AddToFile -Data $alldata
    }
    catch {   
        Write-Host "Can't export Route Table: $($routetable.name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Exports details of an IP Group for inclusion in a network diagram.
 
.DESCRIPTION
The `Export-IpGroup` function processes a specified IP Group object, retrieves its details, and formats the data for inclusion in a network diagram. It visualizes the IP Group name and associated IP addresses.
 
.PARAMETER IpGroup
Specifies the IP Group object to be processed.
 
.EXAMPLE
PS> Export-IpGroup -IpGroup $ipGroup
 
This example processes the specified IP Group and exports its details for inclusion in a network diagram.
 
#>

function Export-IpGroup {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject[]]$IpGroup
    )

    $id = $ipGroup.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
    $Location = SanitizeLocation $ipGroup.Location
    $script:rankipgroups += $id
    if ($ipGroup.IpAddresses) {
        $IpAddresses = ($ipGroup.IpAddresses | ForEach-Object { SanitizeString $_ }) -join "\n"
    }
    else {
        $IpAddresses = "None"
    }

    $alldata = "
    subgraph cluster_$id {
        style = solid;
        color = black;
         
        $id [label = `"\nName: $(SanitizeString $ipGroup.Name)\nLocation: $Location\n$IpAddresses`" ; color = lightgray;image = `"$OutputPath\icons\ipgroup.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];
    }
    "

    Export-AddToFile -Data $alldata
}

<#
.SYNOPSIS
Exports details of a VPN connection and its associated gateways.
 
.DESCRIPTION
The `Export-Connection` function processes a specified VPN/ER connection object, retrieves details about the associated virtual network gateway or local network gateway, and formats the data for inclusion in a network diagram. It visualizes the connection type, peer information, and static remote subnets if applicable.
 
.PARAMETER connection
Specifies the VPN/ER connection object to be processed.
 
.EXAMPLE
PS> Export-Connection -connection $vpnConnection
 
This example processes the specified VPN connection and exports its details for inclusion in a network diagram.
 
#>

function Export-Connection {
    [CmdletBinding()]
    param ([PSCustomObject[]]$connection)

    $name = $connection.Name
    $lgwconnectionname = $name
    $lgconnectionType = $connection.ConnectionType

    ### Logic description
    # VirtualNetworkGateway1 - always set
    # VirtualNetworkGateway2 - if set = VNET2VNET
    # $connection.LocalNetworkGateway2 - if set = Site2Site VPN
    # $connection.Peer - if set = ER Circuit connection

    if ($connection.VirtualNetworkGateway2) { $VNET2VNET=$true }
    if ($connection.LocalNetworkGateway2) { $S2S=$true }
    if ($connection.Peer) { $ER=$true }

    # VPN GW 1, connection source endpoint, always set - Not added to DOT, as it is defined in VNet definition
    if ($connection.VirtualNetworkGateway1) {
        $lgwname = $connection.VirtualNetworkGateway1.id.split("/")[-1]
        $vpngwid = $connection.VirtualNetworkGateway1.id.replace("-", "").replace("/", "").replace(".", "").replace("`"", "").ToLower()
        #$data = " $vpngwid [color = lightgrey;label = `"\n\nLocal GW: $(SanitizeString $lgwname)\nConnection Name: $(SanitizeString $lgwconnectionname)\nConnection Type: $lgconnectionType\n`""
        $lgwid = 0
    }
    else {
        $vpngwid = 0
    }
    
    # LGW set - S2S
    if ($S2S) {
        $lgwid = $connection.LocalNetworkGateway2.id.replace("-", "").replace("/", "").replace(".", "").replace("`"", "").ToLower()
        $lgwname = $connection.LocalNetworkGateway2.id.split("/")[-1]
        $lgwrg = $connection.LocalNetworkGateway2.id.split("/")[4]
        $lgwobject = (Get-AzLocalNetworkGateway -ResourceGroupName $lgwrg -name $lgwname -ErrorAction Stop)
        $lgwip = $lgwobject.GatewayIpAddress
        $lgwFQDN = $lgwobject.Fqdn

        $lgwPeerInfo = ''
        if ( $null -eq $lgwip ) { $lgwPeerInfo = $lgwFQDN } else { $lgwPeerInfo = $lgwip }

        $lgwsubnetsarray = $lgwobject.addressSpaceText | ConvertFrom-Json
        $lgwsubnets = ""
        $lgwsubnetsarray.AddressPrefixes | ForEach-Object {
            $prefix = SanitizeString $_
            $lgwsubnets += "$prefix \n"
        }
        $data = " $lgwid [color = lightgrey;label = `"\n\nGateway: $(SanitizeString $lgwname)\nConnection Name: $(SanitizeString $lgwconnectionname)\nConnection Type: $lgconnectionType\n"
        $data += "Peer : $(SanitizeString $lgwPeerInfo)\n\nStatic remote subnet(s):\n$lgwsubnets"
        $data += "`";image = `"$OutputPath\icons\VPN-Site.png`";imagepos = `"tc`";labelloc = `"b`";height = 2.0;];"
    } 
    elseif ($VNET2VNET) {
        $lgwid = $connection.VirtualNetworkGateway2.id.replace("-", "").replace("/", "").replace("`"", "").replace(".", "").ToLower()
        $lgwname = $connection.VirtualNetworkGateway2.id.split("/")[-1]
    }
    else {
        #ER
        $lgwid = 0
    }
  
    # ER (Peer set = ER Circuit - circuit defined seperately)
    if ($ER -and $vpngwid -ne 0) {
        $peerid = $connection.Peer.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        $data += "`n $vpngwid -> $peerid`n"
    }
    # VPN or VNet2VNet
    elseif ($lgwid -ne 0 -and $vpngwid -ne 0) {
        $data += "`n $vpngwid -> $lgwid`n"
    }
    else {
        $data += "`n"
    }
    Export-AddToFile -Data $data
}

<#
.SYNOPSIS
Exports details of a Private Endpoint and its associated Private Link Service connections.
 
.DESCRIPTION
The `Export-PrivateEndpoint` function retrieves information about a specified Private Endpoint, including its name and associated Private Link Service connections. It formats the data for inclusion in a network diagram, displaying the Private Endpoint's details and connections visually.
 
.PARAMETER pe
Specifies the Private Endpoint object to be processed.
 
.EXAMPLE
PS> Export-PrivateEndpoint -pe $privateEndpoint
 
This example processes the specified Private Endpoint and exports its details for inclusion in a network diagram.
 
#>

function Export-PrivateEndpoint {
    [CmdletBinding()]
    param ([PSCustomObject]$pe)

    try {
        # Get the private link service connection information
        $connections = @()
        $peid = $pe.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
        
        # Check for standard service connections
        if ($pe.PrivateLinkServiceConnections) {
            $connections += $pe.PrivateLinkServiceConnections
        }
        
        # Check for manual service connections
        if ($pe.ManualPrivateLinkServiceConnections) {
            $connections += $pe.ManualPrivateLinkServiceConnections
        }

        $pedetails = $(SanitizeString $pe.name) + "\n"
        # Process each connection for this private endpoint
        foreach ($connection in $connections) {
            if ($connection.PrivateLinkServiceId) {
                $pedetails += $(SanitizeString $connection.PrivateLinkServiceId.Split('/')[-1]) + "\n"
            }
        }
        
        $data = "`n $peid [label = `"\n$pedetails`" ; color = lightgray;image = `"$OutputPath\icons\private-endpoint.png`";imagepos = `"tc`";labelloc = `"b`";height = 1.5;];" 
        Export-AddToFile -Data $data
    }
    catch {
        Write-Error "Can't export Private Endpoint: $($pe.Name) at line $($_.InvocationInfo.ScriptLineNumber) " $_.Exception.Message
    }
}

<#
.SYNOPSIS
Confirms that all prerequisites are met for generating the Azure network diagram.
 
.DESCRIPTION
The `Confirm-Prerequisites` function ensures that all required tools, modules, and configurations are in place before generating the Azure network diagram. It verifies the presence of Graphviz (`dot.exe`), required PowerShell modules (`Az.Network` and `Az.Accounts`), Azure authentication, and necessary icons for the diagram. If any prerequisites are missing, it provides guidance for resolving the issues.
 
#>

function Confirm-Prerequisites {
    [CmdletBinding()]
    $ErrorActionPreference = 'Stop'

    if (! (Test-Path $OutputPath)) {}

    # dot.exe executable
    try {
        $dot = (get-command dot.exe).Path
        if ($null -eq $dot) {
            Write-Error "dot.exe executable not found - please install Graphiz (https://graphviz.org), and/or ensure `"dot.exe`" is in `"`$PATH`" !"
        }
    }
    catch {
        Write-Error "dot.exe executable not found - please install Graphiz (https://graphviz.org), and/or ensure `"dot.exe`" is in `"`$PATH`" !"
    }
    
    # Load Powershell modules
    # This block is probably not neccesary anymore, as all modules are stated as required
    try {
        import-module az.network -DisableNameChecking
        import-module az.accounts
    }
    catch {
        Write-Output "Please install the following PowerShell modules, using install-module: Az.Network + Az.Accounts"
        Write-Output ""
        Write-Output "Ie:"
        Write-Output "Install-Module Az.Accounts"
        Write-Error "Install-Module Az.Network"
    }


    # Azure authentication verification
    $context = Get-AzContext  -ErrorAction Stop
    if ($null -eq $context) { 
        Write-Output "Please make sure you are logged in to Azure using Login-AzAccount, and that permissions are granted to resources within scope."
        Write-Output "A login window should appear - hint: it may hide behind active windows!"
        Login-AzAccount
    }

    # Icons available?
    if (! (Test-Path "$OutputPath\icons") ) { Write-Output "Downloading icons to $OutputPath\icons\ ... " ; New-Item -Path "$OutputPath" -Name "icons" -ItemType "directory" | Out-null }
    $icons = @(
        "LICENSE",
        "acr.png",
        "afw.png",
        "agw.png",
        "aks-node-pool.png",
        "aks-service.png",
        "apim.png",
        "appplan.png",
        #"appserviceplan.png",
        "appservices.png",
        #"azuresql.png",
        "bas.png",
        "cassandra.png",
        "computegalleries.png",
        #"Connections.png",
        "cosmosdb.png",
        "db.png",
        #"DNSforwardingruleset.png",
        "dnspr.png",
        "documentdb.png",
        "ercircuit.png",
        "ergw.png",
        "erport.png",
        "eventhub.png",
        "firewallpolicy.png",
        "gremlin.png",
        "imagedef.png",
        #"imagedefversions.png",
        "ipgroup.png",
        "keyvault.png",
        #"lgw.png",
        "managed-identity.png",
        #"mariadb.png",
        "mongodb.png",
        "mysql.png",
        "ng.png",
        "nsg.png",
        "peerings.png",
        "postgresql.png",
        "private-endpoint.png",
        #"privatednszone.png",
        "redis.png",
        "RouteTable.png",
        #"rsv.png",
        "snet.png",
        "sqldb.png",
        "sqlmi.png",
        "sqlmidb.png",
        "sqlserver.png",
        "ssh-key.png",
        "storage-account.png",
        "table.png",
        "vWAN-Hub.png",
        "vWAN.png",
        "vgw.png",
        "vm.png",
        "vmss.png",
        "vnet.png",
        "VPN-Site.png",
        "VPN-User.png"
    )
    
    $icons | ForEach-Object {
        if (! (Test-Path "$OutputPath\icons\$_") ) { Invoke-WebRequest "https://github.com/dan-madsen/AzNetworkDiagram/raw/refs/heads/main/icons/$_" -OutFile "$OutputPath\icons\$_" }
    }
}

<#
.SYNOPSIS
Generates a detailed network diagram of Azure resources for specified subscriptions.
 
.DESCRIPTION
The `Get-AzNetworkDiagram` function collects and visualizes Azure networking resources, including VNets, subnets, firewalls, gateways, Virtual WANs, ExpressRoute circuits, private endpoints, and more. It uses Graphviz to create a DOT-based diagram and outputs it in PDF, PNG, and SVG formats. The diagram includes relationships and dependencies between resources, providing a comprehensive view of the Azure network infrastructure.
 
.PARAMETER OutputPath
Specifies the directory where the output files (DOT, PDF, PNG, SVG) will be saved. Defaults to the current working directory.
 
.PARAMETER Subscriptions
A list of Azure subscription IDs to include in the diagram. If not specified, all accessible subscriptions are used.
 
.PARAMETER EnableRanking
Enables ranking of certain resource types in the diagram for better visualization. Defaults to `$true`.
 
.PARAMETER TenantId
Specifies the Azure tenant ID to scope the subscriptions. If not provided, the default tenant is used.
 
.EXAMPLE
PS> Get-AzNetworkDiagram -Subscriptions "subid1","subid2" -OutputPath "C:\Diagrams" -EnableRanking $true
 
#>

function Get-AzNetworkDiagram {
    [CmdletBinding()]
    # Parameters
    param (
        [Parameter(Mandatory = $false)]
        [string]$OutputPath = $pwd,
        [Parameter(Mandatory = $false)]
        [string[]]$Subscriptions,
        [Parameter(Mandatory = $false)]
        [bool]$EnableRanking = $true,
        [Parameter(Mandatory = $false)]
        [string]$TenantId = $null,
        [Parameter(Mandatory = $false)]
        [string]$Prefix = $null,
        [Parameter(Mandatory = $false)]
        [bool]$Sanitize = $false,
        [Parameter(Mandatory = $false)] [bool]$OnlyCoreNetwork = $false
    )

    # Remove trailing "\" from path
    $OutputPath = $OutputPath.TrimEnd('\')

    Write-Output "Checking prerequisites ..."
    Confirm-Prerequisites

    ##### Global runtime vars #####
    #Rank (visual) in diagram
    $script:rankrts = @()
    $script:ranksubnets = @()
    $script:rankvgws = @()
    $script:rankvnetaddressspaces = @()
    $script:rankvwans = @()
    $script:rankvwanhubs = @()
    $script:rankercircuits = @()
    $script:rankvpnsites = @()
    $script:rankipgroups = @()
    $script:PDNSREpIp = @()
    $script:PDNSRId = @()
    $script:AllInScopevNetIds = @()
    $script:DoSanitize = $Sanitize

    ##### Data collection / Execution #####

    # Run program and collect data through powershell commands
    Export-dotHeader

    # Set subscriptions to every accessible subscription, if unset
    try {
        if ($TenantId) {
            if ( $null -eq $Subscriptions ) { $Subscriptions = (Get-AzSubscription -TenantId $TenantId -ErrorAction Stop | Where-Object -Property State -eq "Enabled").Id }
        }
        else {
            if ( $null -eq $Subscriptions ) { $Subscriptions = (Get-AzSubscription -ErrorAction Stop | Where-Object -Property State -eq "Enabled").Id }
        }
    }
    catch {
        Write-Error "No available subscriptions within active AzContext - missing permissions? " $_.Exception.Message
        return
    } 
    
    Write-Output "Gathering information ..."
    Update-AzConfig -DisplaySecretsWarning $false -Scope process | Out-Null
    Update-AzConfig -DisplayBreakingChangeWarning $false -Scope process | Out-Null

    # No subscriptions available?
    if ( $null -eq $Subscriptions ) {
        throw  "No available subscriptions within active AzContext - missing permissions?"
    }

    try {
        # Collect all vNet ID's in scope otherwise we can end up with 1 vNet peered to 1000 other vNets which are not in scope
        # Errors will appear like: dot: graph is too large for cairo-renderer bitmaps. Scaling by 0.324583 to fit
        
        #$AzureRegions = Get-AzLocation | Select-Object DisplayName, Location | Sort-Object DisplayName

        $Subscriptions | ForEach-Object {
            # Set Context
            if ($TenantId) {
                $context = Set-AzContext -Subscription $_ -Tenant $TenantId -ErrorAction Stop
            }
            else {
                $context = Set-AzContext -Subscription $_ -ErrorAction Stop
            }
            $subid = $context.Subscription.Id
            $subname = $context.Subscription.Name
            
            Write-Output "`nCollecting data from subscription: $subname ($subid)"
            Export-AddToFile "`n ##########################################################################################################"
            Export-AddToFile " ##### $subname "
            Export-AddToFile " ##########################################################################################################`n"

            ### RTs
            Write-Output "Collecting Route Tables..."
            Export-AddToFile " ##### $subname - Route Tables #####"
            $routetables = Get-AzRouteTable -ErrorAction Stop 
            $routetables | ForEach-Object {
                $routetable = $_
                Export-RouteTable $routetable
            }

            ### Ip Groups
            Write-Output "Collecting IP Groups..."
            Export-AddToFile " ##### $subname - IP Groups #####"
            $ipGroups = Get-AzIpGroup -ErrorAction Stop
            if ($null -ne $ipGroups) {
                $cluster = "subgraph cluster_ipgroups {
                    style = solid;
                    color = black;
                "

                Export-AddToFile -Data $cluster
                $ipGroups | ForEach-Object {
                    $ipGroup = $_
                    Export-IpGroup -IpGroup $ipGroup
                }
                $footer = "
                    label = `"IP Groups`";
                }"

                Export-AddToFile -Data $footer
            }

            ### vNets (incl. subnets)
            Write-Output "Collecting vNets, and associated informations..."
            Export-AddToFile " ##### $subname - Virtual Networks #####"
            $vnets = Get-AzVirtualNetwork -ErrorAction Stop
            if ($null -ne $vnets.id) {
                $script:AllInScopevNetIds += $vnets.id

                $vnets | ForEach-Object {
                    $vnet = $_
                    Export-vnet $vnet
                }
            }

            #NSGs
            Write-Output "Collecting NSG's..."
            Export-AddToFile " ##### $subname - NSG's #####"
            $nsgs = Get-AzNetworkSecurityGroup -ErrorAction Stop
            foreach ($nsg in $nsgs) {
                Export-NSG $nsg
            }

            #VPN Connections
            Write-Output "Collecting VPN/ER Connections..."
            Export-AddToFile " ##### $subname - VPN/ER Connections #####"
            $VPNConnections = Get-AzResource | Where-Object { $_.ResourceType -eq "Microsoft.Network/connections" }
            $VPNConnections | ForEach-Object {
                $connection = $_
                $resname = $connection.Name
                $rgname = $connection.ResourceGroupName
                $connection = Get-AzVirtualNetworkGatewayConnection -name $resname -ResourceGroupName $rgname -ErrorAction Stop
                Export-Connection $connection
            }

            #Express Route Circuits
            Write-Output "Collecting Express Route Circuits..."
            Export-AddToFile " ##### $subname - Express Route Circuits #####"
            $er = Get-AzExpressRouteCircuit -ErrorAction Stop
            $er | ForEach-Object {
                $er = $_
                Export-ExpressRouteCircuit $er
            }

            #Virtual WANs
            Write-Output "Collecting vWANs..."
            Export-AddToFile " ##### $subname - Virtual WANs #####"
            $vWANs = Get-AzVirtualWan -ErrorAction Stop
            $vWANs | ForEach-Object {
                $vWAN = $_
                Export-vWAN $vWAN
            }
            
            # Skip the rest of the resource types, if -OnlyCoreNetwork was set to true, at runtime
            if ( -not $OnlyCoreNetwork ) {
                ### VMs
                Write-Output "Collecting VMs..."
                Export-AddToFile " ##### $subname - VMs #####"
                $VMs = Get-AzVM -ErrorAction Stop
                foreach ($vm in $VMs) {
                    Export-VM $VM
                }

                ### Keyvaults
                Write-Output "Collecting Keyvaults..."
                Export-AddToFile " ##### $subname - Keyvaults #####"
                $Keyvaults = Get-AzKeyVault -ErrorAction Stop
                foreach ($keyvault in $Keyvaults) {
                    Export-Keyvault $Keyvault
                }

                ### Storage Accounts
                Write-Output "Collecting Storage Accounts..."
                Export-AddToFile " ##### $subname - Storage Accounts #####"
                $storageaccounts = Get-AzStorageAccount -ErrorAction Stop
                foreach ($storageaccount in $storageaccounts) {
                    Export-StorageAccount $storageaccount
                }
            
                ### Private Endpoints
                Write-Output "Collecting Private Endpoints..."
                Export-AddToFile " ##### $subname - Private Endpoints #####"
                $privateEndpoints = Get-AzPrivateEndpoint -ErrorAction Stop
                foreach ($pe in $privateEndpoints) {
                    Export-PrivateEndpoint $pe
                }

                # Application Gateways
                Write-Output "Collecting Application Gateways..."
                Export-AddToFile " ##### $subname - Application Gateways #####"
                $agws = Get-AzApplicationGateway -ErrorAction Stop
                foreach ($agw in $agws) {
                    Export-ApplicationGateway $agw
                }
        
                #MySQL Servers
                Write-Output "Collecting MySQL Flexible Servers..."
                Export-AddToFile " ##### $subname - MySQL Flexible Servers #####"
                $mysqlservers = Get-AzMySqlFlexibleServer -ErrorAction Stop
                foreach ($mysqlserver in $mysqlservers) {
                    Export-MySQLServer $mysqlserver 
                }

                #PostgreSQL Servers
                Write-Output "Collecting PostgreSQL Servers..."
                Export-AddToFile " ##### $subname - PostgreSQL Servers #####"
                $postgresqlservers = Get-AzPostgreSqlFlexibleServer -ErrorAction Stop
                foreach ($postgresqlserver in $postgresqlservers) {
                    Export-PostgreSQLServer $postgresqlserver 
                }

                #CosmosDB Servers
                Write-Output "Collecting CosmosDB Servers..."
                Export-AddToFile " ##### $subname - CosmosDB Servers #####"
                $resourceGroups = Get-AzResourceGroup -ErrorAction Stop
                foreach ($rg in $resourceGroups) {
                    $dbaccts = Get-AzCosmosDBAccount -ResourceGroupName $rg.ResourceGroupName -ErrorAction Stop
                    foreach ($dbaact in $dbaccts) {
                        Export-CosmosDBAccount $dbaact
                    }
                }

                #Redis Servers
                Write-Output "Collecting Redis Servers..."
                Export-AddToFile " ##### $subname - Redis Servers #####"
                $redisservers = Get-AzRedisCache -ErrorAction Stop
                foreach ($redisserver in $redisservers) {
                    Export-RedisServer $redisserver 
                }

                #SQL Managed Instances
                Write-Output "Collecting SQL Managed Instances..."
                Export-AddToFile " ##### $subname - SQL Managed Instances #####"
                $sqlmanagedinstances = Get-AzSqlInstance -ErrorAction Stop
                foreach ($sqlmanagedinstance in $sqlmanagedinstances) {
                    Export-SQLManagedInstance $sqlmanagedinstance 
                }

                #Azure SQL logical servers
                Write-Output "Collecting SQL Servers..."
                Export-AddToFile " ##### $subname - SQL Servers #####"
                $sqlservers = Get-AzSqlServer -ErrorAction Stop
                foreach ($sqlserver in $sqlservers) {
                    Export-SQLServer $sqlserver 
                }

                #EventHubs
                Write-Output "Collecting Event Hubs..."
                Export-AddToFile " ##### $subname - Event Hubs #####"
                $namespaces = Get-AzEventHubNamespace -ErrorAction Stop
                foreach ($namespace in $namespaces) {
                    Export-EventHub $namespace 
                }

                #App Service Plans
                Write-Output "Collecting App Service Plans..."
                Export-AddToFile " ##### $subname - App Service Plans #####"
                $appserviceplans = Get-AzAppServicePlan -ErrorAction Stop   
                foreach ($appserviceplan in $appserviceplans) {
                    Export-AppServicePlan $appserviceplan 
                }

                #APIMs
                Write-Output "Collecting API Management Services..."
                Export-AddToFile " ##### $subname - API Management Services #####"
                $apims = Get-AzApiManagement -ErrorAction Stop
                foreach ($apim in $apims) {
                    Export-APIM $apim 
                }

                #AKS
                Write-Output "Collecting AKS Clusters..."
                Export-AddToFile " ##### $subname - AKS Clusters #####"
                $aksclusters = Get-AzAksCluster -ErrorAction Stop
                foreach ($akscluster in $aksclusters) {
                    Export-AKSCluster $akscluster
                }   

                #Compute Galleries
                Write-Output "Collecting Compute Galleries..."
                Export-AddToFile " ##### $subname - Compute Galleries #####"
                $computeGalleries = Get-AzGallery -ErrorAction Stop
                foreach ($computeGallery in $computeGalleries) {
                    Export-ComputeGallery $computeGallery
                }

                #VMSSs
                Write-Output "Collecting VMSS..."
                Export-AddToFile " ##### $subname - VMSS #####"
                $VMSSs = Get-AzVMSS -ErrorAction Stop
                foreach ($vmss in $VMSSs) {
                    Export-VMSS $vmss
                }

                #Managed Identities
                Write-Output "Collecting Managed Identities..."
                Export-AddToFile " ##### $subname - Managed Identities #####"
                $managedIdentities = Get-AzUserAssignedIdentity -ErrorAction Stop
                foreach ($managedIdentity in $managedIdentities) {
                    Export-ManagedIdentity $managedIdentity
                }

                #ACRs
                Write-Output "Collecting Azure Container Registries..."
                Export-AddToFile " ##### $subname - Azure Container Registries #####"
                $acrs = Get-AzContainerRegistry -ErrorAction Stop
                foreach ($acr in $acrs) {
                    Export-ACR $acr
                }   

                #SSH Keys
                Write-Output "Collecting SSH Keys..."
                Export-AddToFile " ##### $subname - SSH Keys #####"
                $sshkeys = Get-AzSshKey -ErrorAction Stop
                foreach ($sshkey in $sshkeys) {
                    Export-SSHKey $sshkey
                }
            }

            Export-AddToFile "`n ##########################################################################################################"
            Export-AddToFile " ##### $subname "
            Export-AddToFile " ##### END"
            Export-AddToFile " ##########################################################################################################`n"
        }
        
        # vNet Peerings
        Write-Output "`nConnecting in-scope peered vNets..."
        foreach ($InScopevNetId in $script:AllInScopevNetIds) {
            $vnetname = $InScopevNetId.split("/")[-1]
            $vnetsub = $InScopevNetId.split("/")[2]
            $vnetrg = $InScopevNetId.split("/")[4]
            #
            # The Hub is in another "managed" subscription, so we cannot use the context of that subscription
            # So we're filtering it out here. We do't have access to it.
            #
            if ($Subscriptions.IndexOf($vnetsub) -ge 0) {
                if ($TenantId) {
                    $context = Set-AzContext -Subscription $vnetsub -Tenant $TenantId -ErrorAction Stop
                }
                else {
                    $context = Set-AzContext -Subscription $vnetsub -ErrorAction Stop
                }
                $vnet = Get-AzVirtualNetwork -name $vnetname -ResourceGroupName $vnetrg -ErrorAction Stop
                $vnetId = $vnet.id.replace("-", "").replace("/", "").replace(".", "").ToLower()
                $vnetPeerings = $vnet.VirtualNetworkPeerings.RemoteVirtualNetwork.id
                foreach ($peering in $vnetPeerings) {
                    if ($script:AllInScopevNetIds.IndexOf($peering) -ge 0) {
                        $peeringId = $peering.replace("-", "").replace("/", "").replace(".", "").ToLower()
                        # DOT
                        $data = " $vnetId -> $peeringId [label = `"Peered to`"; ltail = cluster_$vnetId; lhead = cluster_$peeringId; weight = 10;];"

                        Export-AddToFile -Data $data
                    }
                }
            }
        }
    }
    catch {
        Write-Error "Error while collecting data from subscription: $subid" $_.Exception.Message
        return
    }
    
    if ( $EnableRanking ) { Export-dotFooterRanking }
    Export-dotFooter

    ##### Generate diagram #####
    # Generate diagram using Graphviz
    $OutputFileName = $OutputPath + "\"
    if ($Prefix) {
        $OutputFileName += $Prefix + "-"
    }
    $OutputFileName += "AzNetworkDiagram"
    Write-Output "`nGenerating $OutputFileName.pdf ..."
    dot -q1 -Tpdf $OutputPath\AzNetworkDiagram.dot -o "$OutputFileName.pdf"
    Write-Output "Generating $OutputFileName.png ..."
    dot -q1 -Tpng $OutputPath\AzNetworkDiagram.dot -o "$OutputFileName.png"
    Write-Output "Generating $OutputFileName.svg ..."
    dot -q1 -Tsvg $OutputPath\AzNetworkDiagram.dot -o "$OutputFileName.svg"
} 

Export-ModuleMember -Function Get-AzNetworkDiagram