Collectors/ApplicationServices.ps1

function Get-AerApplicationServices {
    [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 Resolve-SubName($sid) { if ($sid) { $subLookup[$sid.ToLowerInvariant()] ?? $sid } else { '' } }
    function YesNo($b) { if ($b -eq $true) { 'Yes' } elseif ($b -eq $false) { 'No' } else { '' } }
    function Leaf($id) { if ($id) { ($id -split '/')[-1] } else { '' } }
    $arg = { param($q) try { Invoke-AerArgQuery -SubscriptionIds $SubscriptionIds -Query $q } catch { Write-Warning "[ApplicationServices] $($_.Exception.Message)"; @() } }
    function New-Service($type, $r, $details) {
        [pscustomobject]@{
            Type = $type; Name = $r.name; Id = $r.id
            SubscriptionId = $r.subscriptionId; SubscriptionName = (Resolve-SubName $r.subscriptionId)
            ResourceGroup = $r.resourceGroup; Location = $r.location; Status = $r.status; Tags = $r.tags
            Details = @($details)
        }
    }

    $web = [System.Collections.Generic.List[object]]::new()
    $func = [System.Collections.Generic.List[object]]::new()
    $cont = [System.Collections.Generic.List[object]]::new()
    $integ = [System.Collections.Generic.List[object]]::new()
    $aks = [System.Collections.Generic.List[object]]::new()

    # ── microsoft.web/sites (Web App / Function App / Container / Logic Std) ──
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.web/sites'
| project id, name, subscriptionId, resourceGroup, location,
          acctKind = tostring(kind),
          status = tostring(properties.state),
          serverFarmId = tostring(properties.serverFarmId),
          linuxFx = tostring(properties.siteConfig.linuxFxVersion),
          defaultHost = tostring(properties.defaultHostName),
          httpsOnly = tobool(properties.httpsOnly),
          vnetSubnetId = tostring(properties.virtualNetworkSubnetId),
          tags
'@
)) {
        $k = "$($r.acctKind)".ToLower()
        $plan = (Leaf $r.serverFarmId)
        $webVnet = (Get-AerSubnetLabel $r.vnetSubnetId)
        if ($k -match 'workflowapp') {
            $func.Add((New-Service 'Logic App (Standard)' $r @(
                [pscustomobject]@{ label = 'Status'; value = $r.status }
                [pscustomobject]@{ label = 'Plan'; value = $plan }
                [pscustomobject]@{ label = 'Default host'; value = $r.defaultHost }
                [pscustomobject]@{ label = 'VNet integration'; value = $webVnet }
            )))
        } elseif ($k -match 'functionapp') {
            $func.Add((New-Service 'Function App' $r @(
                [pscustomobject]@{ label = 'Status'; value = $r.status }
                [pscustomobject]@{ label = 'Plan'; value = $plan }
                [pscustomobject]@{ label = 'Runtime'; value = $r.linuxFx }
                [pscustomobject]@{ label = 'Default host'; value = $r.defaultHost }
                [pscustomobject]@{ label = 'HTTPS only'; value = (YesNo $r.httpsOnly) }
                [pscustomobject]@{ label = 'VNet integration'; value = $webVnet }
            )))
        } elseif ($k -match 'container') {
            $web.Add((New-Service 'Web App for Containers' $r @(
                [pscustomobject]@{ label = 'Status'; value = $r.status }
                [pscustomobject]@{ label = 'Plan'; value = $plan }
                [pscustomobject]@{ label = 'Container image'; value = $r.linuxFx }
                [pscustomobject]@{ label = 'Default host'; value = $r.defaultHost }
                [pscustomobject]@{ label = 'HTTPS only'; value = (YesNo $r.httpsOnly) }
                [pscustomobject]@{ label = 'VNet integration'; value = $webVnet }
            )))
        } else {
            $runtime = if ($r.linuxFx) { $r.linuxFx } else { 'Windows' }
            $web.Add((New-Service 'Web App' $r @(
                [pscustomobject]@{ label = 'Status'; value = $r.status }
                [pscustomobject]@{ label = 'Plan'; value = $plan }
                [pscustomobject]@{ label = 'Runtime'; value = $runtime }
                [pscustomobject]@{ label = 'Default host'; value = $r.defaultHost }
                [pscustomobject]@{ label = 'HTTPS only'; value = (YesNo $r.httpsOnly) }
                [pscustomobject]@{ label = 'VNet integration'; value = $webVnet }
            )))
        }
    }

    # ── App Service Plans ────────────────────────────────────────────────────
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.web/serverfarms'
| project id, name, subscriptionId, resourceGroup, location,
          acctKind = tostring(kind),
          status = tostring(properties.provisioningState),
          skuName = tostring(sku.name), skuTier = tostring(sku.tier), skuCapacity = toint(sku.capacity),
          tags
'@
)) {
        $os = if ("$($r.acctKind)".ToLower() -match 'linux') { 'Linux' } else { 'Windows' }
        $web.Add((New-Service 'App Service Plan' $r @(
            [pscustomobject]@{ label = 'SKU'; value = $r.skuName }
            [pscustomobject]@{ label = 'Tier'; value = $r.skuTier }
            [pscustomobject]@{ label = 'Instances'; value = $r.skuCapacity }
            [pscustomobject]@{ label = 'OS'; value = $os }
        )))
    }

    # ── App Service Environments ─────────────────────────────────────────────
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.web/hostingenvironments'
| project id, name, subscriptionId, resourceGroup, location,
          acctKind = tostring(kind),
          status = tostring(properties.provisioningState),
          internalLb = tostring(properties.internalLoadBalancingMode),
          tags
'@
)) {
        $web.Add((New-Service 'App Service Environment' $r @(
            [pscustomobject]@{ label = 'Status'; value = $r.status }
            [pscustomobject]@{ label = 'Kind'; value = $r.acctKind }
            [pscustomobject]@{ label = 'Internal LB'; value = $r.internalLb }
        )))
    }

    # ── Static Web Apps ──────────────────────────────────────────────────────
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.web/staticsites'
| project id, name, subscriptionId, resourceGroup, location,
          status = tostring(properties.provisioningState),
          skuName = tostring(sku.name),
          defaultHost = tostring(properties.defaultHostname),
          repoUrl = tostring(properties.repositoryUrl),
          tags
'@
)) {
        $web.Add((New-Service 'Static Web App' $r @(
            [pscustomobject]@{ label = 'SKU'; value = $r.skuName }
            [pscustomobject]@{ label = 'Default host'; value = $r.defaultHost }
            [pscustomobject]@{ label = 'Repository'; value = $r.repoUrl }
        )))
    }

    # ── Logic Apps (Consumption) ─────────────────────────────────────────────
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.logic/workflows'
| project id, name, subscriptionId, resourceGroup, location,
          status = tostring(properties.state), tags
'@
)) {
        $func.Add((New-Service 'Logic App (Consumption)' $r @(
            [pscustomobject]@{ label = 'State'; value = $r.status }
        )))
    }

    # ── Container Apps ───────────────────────────────────────────────────────
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.app/containerapps'
| project id, name, subscriptionId, resourceGroup, location,
          status = tostring(properties.provisioningState),
          env = tostring(properties.managedEnvironmentId),
          fqdn = tostring(properties.configuration.ingress.fqdn),
          tags
'@
)) {
        $cont.Add((New-Service 'Container Apps' $r @(
            [pscustomobject]@{ label = 'Status'; value = $r.status }
            [pscustomobject]@{ label = 'Environment'; value = (Leaf $r.env) }
            [pscustomobject]@{ label = 'FQDN'; value = $r.fqdn }
        )))
    }
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.app/managedenvironments'
| project id, name, subscriptionId, resourceGroup, location,
          status = tostring(properties.provisioningState), tags
'@
)) {
        $cont.Add((New-Service 'Container Apps Environment' $r @(
            [pscustomobject]@{ label = 'Status'; value = $r.status }
        )))
    }

    # ── Container Instances ──────────────────────────────────────────────────
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.containerinstance/containergroups'
| project id, name, subscriptionId, resourceGroup, location,
          status = tostring(properties.provisioningState),
          osType = tostring(properties.osType),
          ip = tostring(properties.ipAddress.ip),
          containerCount = toint(array_length(properties.containers)),
          tags
'@
)) {
        $cont.Add((New-Service 'Container Instances' $r @(
            [pscustomobject]@{ label = 'Status'; value = $r.status }
            [pscustomobject]@{ label = 'OS'; value = $r.osType }
            [pscustomobject]@{ label = 'Containers'; value = $r.containerCount }
            [pscustomobject]@{ label = 'IP'; value = $r.ip }
        )))
    }

    # ── Container Registries ─────────────────────────────────────────────────
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.containerregistry/registries'
| project id, name, subscriptionId, resourceGroup, location,
          status = tostring(properties.provisioningState),
          skuName = tostring(sku.name),
          loginServer = tostring(properties.loginServer),
          adminEnabled = tobool(properties.adminUserEnabled),
          publicAccess = tostring(properties.publicNetworkAccess),
          tags
'@
)) {
        $cont.Add((New-Service 'Container Registry' $r @(
            [pscustomobject]@{ label = 'SKU'; value = $r.skuName }
            [pscustomobject]@{ label = 'Login server'; value = $r.loginServer }
            [pscustomobject]@{ label = 'Admin user'; value = (YesNo $r.adminEnabled) }
            [pscustomobject]@{ label = 'Public access'; value = $r.publicAccess }
        )))
    }

    # ── Service Fabric ───────────────────────────────────────────────────────
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.servicefabric/clusters'
| project id, name, subscriptionId, resourceGroup, location,
          status = tostring(properties.clusterState),
          version = tostring(properties.clusterCodeVersion),
          tags
'@
)) {
        $cont.Add((New-Service 'Service Fabric' $r @(
            [pscustomobject]@{ label = 'Status'; value = $r.status }
            [pscustomobject]@{ label = 'Version'; value = $r.version }
        )))
    }

    # ── API Management ───────────────────────────────────────────────────────
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.apimanagement/service'
| project id, name, subscriptionId, resourceGroup, location,
          status = tostring(properties.provisioningState),
          skuName = tostring(sku.name), skuCapacity = toint(sku.capacity),
          gateway = tostring(properties.gatewayUrl),
          vnetType = tostring(properties.virtualNetworkType),
          tags
'@
)) {
        $integ.Add((New-Service 'API Management' $r @(
            [pscustomobject]@{ label = 'SKU'; value = $r.skuName }
            [pscustomobject]@{ label = 'Capacity'; value = $r.skuCapacity }
            [pscustomobject]@{ label = 'Gateway'; value = $r.gateway }
            [pscustomobject]@{ label = 'VNet type'; value = $r.vnetType }
            [pscustomobject]@{ label = 'Status'; value = $r.status }
        )))
    }

    # ── AKS clusters (with node pools) ───────────────────────────────────────
    foreach ($r in (& $arg @'
resources
| where type =~ 'microsoft.containerservice/managedclusters'
| project id, name, subscriptionId, resourceGroup, location,
          status = tostring(properties.provisioningState),
          powerState = tostring(properties.powerState.code),
          kubeVersion = tostring(properties.kubernetesVersion),
          skuTier = tostring(sku.tier),
          networkPlugin = tostring(properties.networkProfile.networkPlugin),
          rbac = tobool(properties.enableRBAC),
          privateCluster = tobool(properties.apiServerAccessProfile.enablePrivateCluster),
          nodeRg = tostring(properties.nodeResourceGroup),
          agentPools = properties.agentPoolProfiles,
          tags
'@
)) {
        $pools = @($r.agentPools)
        $totalNodes = ($pools | Measure-Object -Property count -Sum).Sum ?? 0
        $aksPower = if ($r.powerState) { ($r.powerState -split '/')[-1] } else { '' }
        $details = [System.Collections.Generic.List[object]]::new()
        $details.Add([pscustomobject]@{ label = 'Status'; value = $r.status })
        $details.Add([pscustomobject]@{ label = 'Power state'; value = $aksPower })
        $details.Add([pscustomobject]@{ label = 'Kubernetes version'; value = $r.kubeVersion })
        $details.Add([pscustomobject]@{ label = 'SKU tier'; value = $r.skuTier })
        $details.Add([pscustomobject]@{ label = 'Network plugin'; value = $r.networkPlugin })
        $details.Add([pscustomobject]@{ label = 'RBAC'; value = (YesNo $r.rbac) })
        $details.Add([pscustomobject]@{ label = 'Private cluster'; value = (YesNo $r.privateCluster) })
        $details.Add([pscustomobject]@{ label = 'Node pools'; value = $pools.Count })
        $details.Add([pscustomobject]@{ label = 'Total nodes'; value = [int]$totalNodes })
        $details.Add([pscustomobject]@{ label = 'Node resource group'; value = $r.nodeRg })
        $aksVnet = (@($pools | ForEach-Object { Get-AerSubnetLabel ([string]$_.vnetSubnetID) } | Where-Object { $_ } | Select-Object -Unique) -join ', ')
        $details.Add([pscustomobject]@{ label = 'VNet integration'; value = $aksVnet })
        foreach ($p in $pools) {
            $details.Add([pscustomobject]@{ label = "Pool · $($p.name)"; value = "$($p.count) × $($p.vmSize) ($($p.mode))" })
        }
        $aks.Add((New-Service 'Azure Kubernetes Service' $r @($details)))
    }

    $webA = @($web); $funcA = @($func); $contA = @($cont); $integA = @($integ); $aksA = @($aks)
    $cnt = { param($list, $type) @($list | Where-Object { $_.Type -eq $type }).Count }

    return [pscustomobject]@{
        Web = [pscustomobject]@{
            Total = $webA.Count
            Counts = [pscustomobject]@{
                WebApp           = (& $cnt $webA 'Web App')
                WebAppContainers = (& $cnt $webA 'Web App for Containers')
                AppServicePlan   = (& $cnt $webA 'App Service Plan')
                Ase              = (& $cnt $webA 'App Service Environment')
                StaticWebApp     = (& $cnt $webA 'Static Web App')
            }
            Services = $webA
        }
        Functions = [pscustomobject]@{
            Total = $funcA.Count
            Counts = [pscustomobject]@{
                FunctionApp      = (& $cnt $funcA 'Function App')
                LogicStandard    = (& $cnt $funcA 'Logic App (Standard)')
                LogicConsumption = (& $cnt $funcA 'Logic App (Consumption)')
            }
            Services = $funcA
        }
        Containers = [pscustomobject]@{
            Total = $contA.Count
            Counts = [pscustomobject]@{
                ContainerApps      = (& $cnt $contA 'Container Apps')
                ContainerAppsEnv   = (& $cnt $contA 'Container Apps Environment')
                ContainerInstances = (& $cnt $contA 'Container Instances')
                ContainerRegistry  = (& $cnt $contA 'Container Registry')
                ServiceFabric      = (& $cnt $contA 'Service Fabric')
            }
            Services = $contA
        }
        Integration = [pscustomobject]@{
            Total = $integA.Count
            Counts = [pscustomobject]@{ ApiManagement = (& $cnt $integA 'API Management') }
            Services = $integA
        }
        Aks = [pscustomobject]@{
            Total = $aksA.Count
            Services = $aksA
        }
    }
}