Collectors/DiagnosticSettings.ps1

function Get-AerDiagnosticSettings {
    [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 SubName($id) { if (-not $id) { return 'Unknown' }; $n = $subLookup[$id.ToLowerInvariant()]; if ($n) { $n } else { $id } }
    function Pctg($part, $total) { if ($total -gt 0) { [int][math]::Round($part / $total * 100) } else { $null } }
    function Leaf($id) { if ($id) { ($id -split '/')[-1] } else { '' } }
    function NsOf($id) {
        if (-not $id) { return '' }
        $parts = $id -split '/'; $i = [array]::IndexOf($parts, 'namespaces')
        if ($i -ge 0 -and ($i + 1) -lt $parts.Count) { $parts[$i + 1] } else { '' }
    }
    function ArgRows($query) { Expand-AerRows (Invoke-AerArgQuery -SubscriptionIds $SubscriptionIds -Query $query) }

    # Curated set of resource types that support diagnostic settings and where
    # log/metric forwarding is meaningful. VMs/disks/NICs are excluded (they use
    # the agent model, not diagnosticSettings).
    $diagTypes = @(
        'microsoft.keyvault/vaults','microsoft.storage/storageaccounts',
        'microsoft.web/sites','microsoft.web/serverfarms',
        'microsoft.sql/servers/databases','microsoft.sql/managedinstances',
        'microsoft.documentdb/databaseaccounts',
        'microsoft.dbforpostgresql/flexibleservers','microsoft.dbformysql/flexibleservers',
        'microsoft.network/applicationgateways','microsoft.network/loadbalancers',
        'microsoft.network/publicipaddresses','microsoft.network/networksecuritygroups',
        'microsoft.network/azurefirewalls','microsoft.network/frontdoors',
        'microsoft.cdn/profiles','microsoft.network/virtualnetworkgateways',
        'microsoft.network/expressroutecircuits','microsoft.network/bastionhosts',
        'microsoft.apimanagement/service','microsoft.servicebus/namespaces',
        'microsoft.eventhub/namespaces','microsoft.eventgrid/topics',
        'microsoft.eventgrid/systemtopics','microsoft.cache/redis',
        'microsoft.containerservice/managedclusters','microsoft.automation/automationaccounts',
        'microsoft.logic/workflows','microsoft.datafactory/factories',
        'microsoft.operationalinsights/workspaces','microsoft.recoveryservices/vaults',
        'microsoft.search/searchservices','microsoft.signalrservice/signalr',
        'microsoft.cognitiveservices/accounts','microsoft.streamanalytics/streamingjobs',
        'microsoft.batch/batchaccounts','microsoft.machinelearningservices/workspaces'
    )

    $totalAll = 0
    try { $totalAll = (@(ArgRows "resources | project id = tolower(id)")).Count } catch {}

    $resources = [System.Collections.Generic.List[object]]::new()
    $evaluated = 0; $enabled = 0
    $bySub  = @{}   # subId -> @{ Total; Covered }
    $noDiag = @{}   # type -> count without diag settings

    try {
        $diagList = "'" + ($diagTypes -join "','") + "'"
        $targets = @(ArgRows "resources | where type in~ ($diagList) | project id, name, type = tolower(type), subscriptionId, resourceGroup, location, tags")

        # Index targets by request name so batch responses can be matched back.
        $reqs = [System.Collections.Generic.List[object]]::new()
        $meta = [System.Collections.Generic.List[object]]::new()
        foreach ($t in $targets) {
            if (-not $t.id) { continue }
            $meta.Add($t)
            $reqs.Add([ordered]@{
                httpMethod = 'GET'
                name       = ($meta.Count - 1).ToString()
                url        = "$($t.id)/providers/microsoft.insights/diagnosticSettings?api-version=2021-05-01-preview"
            })
        }

        $responses = if ($reqs.Count) { Invoke-AerArmBatch $reqs } else { @{} }

        foreach ($key in $responses.Keys) {
            $rr = $responses[$key]; $t = $meta[[int]$key]
            if ($rr.httpStatusCode -lt 200 -or $rr.httpStatusCode -ge 300) { continue }   # type unsupported / no permission
            $evaluated++
            $subId = ("$($t.subscriptionId)").ToLowerInvariant()
            if (-not $bySub.ContainsKey($subId)) { $bySub[$subId] = @{ Total = 0; Covered = 0 } }
            $bySub[$subId].Total++

            $settingsRaw = @($rr.content.value)
            $isOn = $settingsRaw.Count -gt 0
            $destSet = [ordered]@{}
            $settings = foreach ($s in $settingsRaw) {
                $p = $s.properties
                $la = if ($p.workspaceId) { Leaf $p.workspaceId } else { '' }
                $sa = if ($p.storageAccountId) { Leaf $p.storageAccountId } else { '' }
                $eh = if ($p.eventHubAuthorizationRuleId) { $n = NsOf $p.eventHubAuthorizationRuleId; if ($p.eventHubName) { "$n / $($p.eventHubName)" } else { $n } } else { '' }
                $tp = if ($p.marketplacePartnerId) { Leaf $p.marketplacePartnerId } else { '' }
                if ($la) { $destSet['Log Analytics'] = $true }
                if ($sa) { $destSet['Storage'] = $true }
                if ($eh) { $destSet['Event Hub'] = $true }
                if ($tp) { $destSet['Third-party'] = $true }

                $logsOn = @(); $logsOff = @()
                foreach ($l in @($p.logs)) {
                    $cat = if ($l.category) { "$($l.category)" } elseif ($l.categoryGroup) { "$($l.categoryGroup)" } else { 'log' }
                    if ($l.enabled) { $logsOn += $cat } else { $logsOff += $cat }
                }
                $metOn = @(); $metOff = @()
                foreach ($m in @($p.metrics)) {
                    $cat = if ($m.category) { "$($m.category)" } else { 'AllMetrics' }
                    if ($m.enabled) { $metOn += $cat } else { $metOff += $cat }
                }
                [pscustomobject]@{
                    Name           = "$($s.name)"
                    LogAnalytics   = $la
                    Storage        = $sa
                    EventHub       = $eh
                    ThirdParty     = $tp
                    LogsEnabled    = @($logsOn)
                    LogsDisabled   = @($logsOff)
                    MetricsEnabled = @($metOn)
                    MetricsDisabled= @($metOff)
                }
            }

            if ($isOn) { $enabled++; $bySub[$subId].Covered++ }
            else { $noDiag[$t.type] = ([int]($noDiag[$t.type] ?? 0)) + 1 }

            $resources.Add([pscustomobject]@{
                Name             = "$($t.name)"
                Type             = ($t.type -replace '^microsoft\.', '')
                SubscriptionName = (SubName $t.subscriptionId)
                ResourceGroup    = "$($t.resourceGroup)"
                Location         = "$($t.location)"
                Id               = "$($t.id)"
                Tags             = $t.tags
                Enabled          = $isOn
                Destinations     = @($destSet.Keys)
                Settings         = @($settings)
            })
        }
    } catch { Write-Warning "[DiagnosticSettings] $($_.Exception.Message)" }

    $coverageBySub = $bySub.GetEnumerator() | ForEach-Object {
        [pscustomobject]@{ Subscription = (SubName $_.Key); Total = $_.Value.Total; Covered = $_.Value.Covered; Percent = (Pctg $_.Value.Covered $_.Value.Total) }
    } | Sort-Object Total -Descending

    $topNoDiag = $noDiag.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 8 | ForEach-Object {
        [pscustomobject]@{ Type = ($_.Key -replace '^microsoft\.', ''); Count = $_.Value }
    }

    return [pscustomobject]@{
        Summary = [pscustomobject]@{
            TotalResources = $totalAll
            Evaluated      = $evaluated
            Enabled        = $enabled
            Percent        = (Pctg $enabled $evaluated)
        }
        CoverageBySubscription = @($coverageBySub)
        TopTypesWithoutDiag    = @($topNoDiag)
        Resources              = @($resources)
    }
}