Collectors/Vnets.ps1

function Get-AerVnets {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string[]] $SubscriptionIds,
        [Parameter(Mandatory)]            $SubscriptionMap
    )

    $subLookup = @{}
    if ($SubscriptionMap -is [hashtable]) {
        $subLookup = $SubscriptionMap
    } elseif ($SubscriptionMap) {
        $SubscriptionMap.PSObject.Properties | ForEach-Object { $subLookup[$_.Name] = $_.Value }
    }
    function Leaf($id) { if ($id) { ($id -split '/')[-1] } else { '' } }

    # ── Route tables → compact route list (for UDR per subnet) ───────────────
    $rtMap = @{}
    try {
        foreach ($rt in (Invoke-AerArgQuery -SubscriptionIds $SubscriptionIds -Query "resources | where type =~ 'microsoft.network/routetables' | project id, name, routes = properties.routes")) {
            $routes = foreach ($r in @($rt.routes)) {
                $nh = "$($r.properties.nextHopType)"
                if ($nh -eq 'VirtualAppliance' -and $r.properties.nextHopIpAddress) { $nh = "VirtualAppliance ($($r.properties.nextHopIpAddress))" }
                [pscustomobject]@{ Prefix = "$($r.properties.addressPrefix)"; NextHop = $nh }
            }
            if ($rt.id) { $rtMap[$rt.id.ToLowerInvariant()] = [pscustomobject]@{ Name = $rt.name; Routes = @($routes) } }
        }
    } catch { Write-Warning "[Vnets.routeTables] $($_.Exception.Message)" }

    # ── Virtual networks (subnets + peerings inline) ─────────────────────────
    $vnets = [System.Collections.Generic.List[object]]::new()
    $nodes = [System.Collections.Generic.List[object]]::new()
    $edges = [System.Collections.Generic.List[object]]::new()
    try {
        foreach ($v in (Invoke-AerArgQuery -SubscriptionIds $SubscriptionIds -Query "resources | where type =~ 'microsoft.network/virtualnetworks' | project id, name, subscriptionId, resourceGroup, location, addressPrefixes = properties.addressSpace.addressPrefixes, subnets = properties.subnets, peerings = properties.virtualNetworkPeerings, dnsServers = properties.dhcpOptions.dnsServers, tags")) {
            $vid = if ($v.id) { $v.id.ToLowerInvariant() } else { '' }

            $subnets = foreach ($s in @($v.subnets)) {
                $rtId = $s.properties.routeTable.id
                $rt = if ($rtId) { $rtMap[$rtId.ToLowerInvariant()] } else { $null }
                $prefix = if ($s.properties.addressPrefix) { $s.properties.addressPrefix }
                          elseif ($s.properties.addressPrefixes) { @($s.properties.addressPrefixes) -join ', ' }
                          else { '' }
                [pscustomobject]@{
                    SubnetName = $s.name
                    Prefix     = $prefix
                    RouteTable = if ($rt) { $rt.Name } else { '' }
                    Routes     = if ($rt) { @($rt.Routes) } else { @() }
                    Nsg        = (Leaf $s.properties.networkSecurityGroup.id)
                }
            }

            $peerings = foreach ($p in @($v.peerings)) {
                $remoteId = $p.properties.remoteVirtualNetwork.id
                if ($remoteId) {
                    $edges.Add([pscustomobject]@{
                        From = $vid; To = $remoteId.ToLowerInvariant()
                        GatewayTransit = [bool]$p.properties.allowGatewayTransit
                        UseRemoteGateways = [bool]$p.properties.useRemoteGateways
                        State = "$($p.properties.peeringState)"
                    })
                }
                [pscustomobject]@{
                    PeerName              = $p.name
                    RemoteVnet            = (Leaf $remoteId)
                    State                 = "$($p.properties.peeringState)"
                    AllowGatewayTransit   = [bool]$p.properties.allowGatewayTransit
                    UseRemoteGateways     = [bool]$p.properties.useRemoteGateways
                    AllowForwardedTraffic = [bool]$p.properties.allowForwardedTraffic
                    AllowVnetAccess       = [bool]$p.properties.allowVirtualNetworkAccess
                }
            }

            $subName = if ($v.subscriptionId) { $subLookup[$v.subscriptionId.ToLowerInvariant()] } else { $null }
            $vnets.Add([pscustomobject]@{
                Id               = $v.id
                Name             = $v.name
                SubscriptionId   = $v.subscriptionId
                SubscriptionName = $subName ?? $v.subscriptionId
                ResourceGroup    = $v.resourceGroup
                Location         = $v.location
                AddressSpace     = (@($v.addressPrefixes) -join ', ')
                DnsServers       = (@($v.dnsServers) -join ', ')
                Tags             = $v.tags
                Subnets          = @($subnets)
                Peerings         = @($peerings)
            })
            $nodes.Add([pscustomobject]@{ id = $vid; name = $v.name; sub = ($subName ?? $v.subscriptionId); rg = $v.resourceGroup })
        }
    } catch { Write-Warning "[Vnets.vnets] $($_.Exception.Message)" }

    # Keep only intra-estate peering edges (both ends scanned), de-duplicated per pair
    $nodeSet = @{}; foreach ($n in $nodes) { $nodeSet[$n.id] = $true }
    $seen = @{}; $cleanEdges = [System.Collections.Generic.List[object]]::new()
    foreach ($e in $edges) {
        if (-not $nodeSet[$e.To]) { continue }
        $key = (@($e.From, $e.To) | Sort-Object) -join '|'
        if ($seen[$key]) { continue }
        $seen[$key] = $true
        $cleanEdges.Add($e)
    }

    return [pscustomobject]@{
        Vnets = @($vnets)
        Graph = [pscustomobject]@{ Nodes = @($nodes); Edges = @($cleanEdges) }
    }
}