Private/ConvertTo-AACCosmosDBAssessment.ps1

function ConvertTo-AACCosmosDBAssessment {
    <#
    .SYNOPSIS
        Turns what Invoke-AACCosmosDBAssessment read into per-account
        availability, requests, throughput, storage, posture and resilience,
        per-container throughput, findings and a heat map.
    .DESCRIPTION
        No Azure calls. Per account (AAC.CosmosDBAssessment):
          health over the period: ServiceAvailability (average %),
                      Requests and their status codes - Throttled (429),
                      ServerErrors (5xx), ClientErrors (other 4xx but 404,
                      409 and 412, which are often normal) - ThrottleRate,
                      RequestUnits, NormalizedRuMax (the busiest partition's
                      share of its throughput), LatencyMs (server side)
          storage the latest value: DataGB, IndexGB, Documents
          throughput CapacityMode (Provisioned, Serverless), Databases,
                      Containers, ProvisionedRU (manual), AutoscaleMaxRU
          posture Api, Network (Private, Firewall, Public),
                      PrivateEndpoints, LocalAuth (keys allowed), MinTls,
                      Encryption, Defender, LogsTo
          resilience Regions, WriteRegions, AutomaticFailover,
                      MultiRegionWrites, ZoneRedundant, Consistency, Backup,
                      BackupRedundancy
        Per container (AAC.CosmosDBContainer): database, partition key,
        throughput mode (Autoscale, Manual, Shared database, Serverless) and
        RU/s, default TTL, analytical store, indexing mode.
        Findings (AAC.CosmosDBFinding: Severity, Account, Database,
        Container, Signal, Finding, Detail, Recommendation) - Signal is the
        heat map's column: Availability, Throttling, Errors, Security,
        Resilience, Cost.
        Returns @{ Accounts; Containers; Findings; HeatMap; StatusCodes; Stats }.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]] $Account,

        # Read-AACCosmosDBResource's result, by account ID.
        [hashtable] $Resource = @{},

        # Read-AACNetworkHealthMetric's result (Requests, Usage, Availability, Latency, Storage), by account ID.
        [hashtable] $Metric = @{},

        [AllowEmptyCollection()]
        [object[]] $Pricing = @(),

        [AllowEmptyCollection()]
        [object[]] $Alert = @(),

        # Workspace resource ID (lower case) -> name.
        [hashtable] $WorkspaceName = @{},

        [hashtable] $SubscriptionName = @{}
    )

    $at = {
        param($Row, [string[]] $Keys)
        $value = $Row
        foreach ($key in $Keys) {
            if ($value -isnot [System.Collections.IDictionary]) { return $null }
            if ($value.Contains($key)) { $value = $value[$key]; continue }
            $found = $null
            foreach ($name in $value.Keys) { if ($name -eq $key) { $found = $value[$name]; break } }
            $value = $found
        }
        $value
    }
    $sum = { param([object[]] $Points, [string] $Field) $t = 0.0; foreach ($p in $Points) { $v = & $at $p $Field; if ($null -ne $v) { $t += [double]$v } }; $t }
    $values = { param([object[]] $Points, [string] $Field) @(foreach ($p in $Points) { $v = & $at $p $Field; if ($null -ne $v) { [double]$v } }) }
    $mean = { param($Numbers) $n = @($Numbers | Where-Object { $null -ne $_ }); if ($n.Count) { ($n | Measure-Object -Average).Average } else { $null } }
    $peak = { param($Numbers) $n = @($Numbers | Where-Object { $null -ne $_ }); if ($n.Count) { ($n | Measure-Object -Maximum).Maximum } else { $null } }
    $latest = { param([object[]] $Points) $v = @(& $values $Points 'total'); if (-not $v.Count) { $v = @(& $values $Points 'maximum') }; if ($v.Count) { $v[-1] } else { $null } }
    # Measure-Object returns nothing for no input (and .Sum then fails under StrictMode): this returns 0.
    $sumOf = { param([object[]] $Rows, [string] $Property) $t = 0.0; foreach ($row in $Rows) { if ($null -ne $row.$Property) { $t += [double]$row.$Property } }; $t }
    $round = { param($Value, [int] $Digits = 1) if ($null -eq $Value) { $null } else { [Math]::Round([double]$Value, $Digits) } }
    $gb = { param($Bytes) if ($null -eq $Bytes) { $null } else { [Math]::Round([double]$Bytes / 1GB, 2) } }
    $subName = { param([string] $Id) if ($SubscriptionName.Contains($Id)) { $SubscriptionName[$Id] } else { $Id } }
    $call = { param([string] $Id, [string] $Key) if ($Metric.Contains($Id) -and $Metric[$Id].Contains($Key)) { $Metric[$Id][$Key] } else { $null } }
    $pointsOf = { param($Answer, [string] $Name) @(if ($Answer -and -not $Answer.Error) { foreach ($s in $Answer.Series) { if ($s.Name -eq $Name) { $s.Points } } }) }
    $itemsOf = { param($Answer) @(if ($Answer -and -not $Answer['Error']) { $Answer['Items'] | Where-Object { $_ } }) }
    # A throughputSettings answer: @{ Mode = Manual / Autoscale; RU } - or $null when none is set at that level.
    $throughputOf = {
        param($Answer)
        if (-not $Answer -or $Answer['Error'] -or [int]$Answer['Status'] -ne 200) { return $null }
        $res = & $at $Answer 'Body', 'properties', 'resource'
        $auto = & $at $res 'autoscaleSettings', 'maxThroughput'
        if ($auto) { return @{ Mode = 'Autoscale'; RU = [int]$auto } }
        $manual = & $at $res 'throughput'
        if ($manual) { return @{ Mode = 'Manual'; RU = [int]$manual } }
        $null
    }
    $defender = @{}
    foreach ($p in $Pricing) { $defender[[string]$p['subscriptionId']] = [string]$p['tier'] }
    $alerts = @{}
    foreach ($a in $Alert) { $id = [string]$a['resourceId']; $alerts[$id] = 1 + $(if ($alerts.Contains($id)) { $alerts[$id] } else { 0 }) }

    $findings = [System.Collections.Generic.List[object]]::new()
    $accountRows = [System.Collections.Generic.List[object]]::new()
    $containerRows = [System.Collections.Generic.List[object]]::new()
    $heatRows = [System.Collections.Generic.List[object]]::new()
    $signals = @('Availability', 'Throttling', 'Errors', 'Security', 'Resilience', 'Cost')
    $statusTotals = [ordered]@{ Success = 0.0; 'Throttled (429)' = 0.0; 'Server (5xx)' = 0.0; 'Client (4xx)' = 0.0; 'Not found, conflict (404, 409, 412)' = 0.0 }

    foreach ($c in @($Account | Sort-Object -Property { [string]$_['name'] })) {
        $id = [string]$c['id']
        $name = [string]$c['name']
        $p = $c['properties']
        $capabilities = @(@(& $at $p 'capabilities') | ForEach-Object { [string](& $at $_ 'name') })
        $apiName = if ([string]$c['kind'] -eq 'MongoDB' -or $capabilities -contains 'EnableMongo') { 'MongoDB' } elseif ($capabilities -contains 'EnableCassandra') { 'Cassandra' } elseif ($capabilities -contains 'EnableGremlin') { 'Gremlin' } elseif ($capabilities -contains 'EnableTable') { 'Table' } else { 'NoSQL' }
        $serverless = $capabilities -contains 'EnableServerless'
        $find = {
            param([string] $Severity, [string] $Signal, [string] $Title, [string] $Detail, [string] $Fix, [string] $Database = '', [string] $Container = '')
            $findings.Add([pscustomobject][ordered]@{ PSTypeName = 'AAC.CosmosDBFinding'; Severity = $Severity; Account = $name; Database = $Database; Container = $Container; Signal = $Signal; Finding = $Title; Detail = $Detail; Recommendation = $Fix; ResourceGroup = [string]$c['resourceGroup']; Subscription = & $subName ([string]$c['subscriptionId']); ResourceId = $id })
        }

        # --- Health: Azure Monitor over the period ------------------------------------------------------------------
        $requestsCall = & $call $id 'Requests'
        $metricsRead = [bool]($requestsCall -and -not $requestsCall.Error)
        $codes = @{ Success = 0.0; Throttled = 0.0; Server = 0.0; Client = 0.0; Benign = 0.0 }
        foreach ($series in @(if ($metricsRead) { $requestsCall.Series })) {
            $code = [string]$series.Dims['statuscode']
            $n = & $sum $series.Points 'count'
            if (-not $n) { $n = & $sum $series.Points 'total' }
            if (-not $n) { continue }
            $bucket = switch -Regex ($code) { '^429$' { 'Throttled'; break } '^5' { 'Server'; break } '^(404|409|412)$' { 'Benign'; break } '^4' { 'Client'; break } default { 'Success' } }
            $codes[$bucket] += $n
        }
        $statusTotals['Success'] += $codes.Success; $statusTotals['Throttled (429)'] += $codes.Throttled; $statusTotals['Server (5xx)'] += $codes.Server; $statusTotals['Client (4xx)'] += $codes.Client; $statusTotals['Not found, conflict (404, 409, 412)'] += $codes.Benign
        $requests = [long]($codes.Success + $codes.Throttled + $codes.Server + $codes.Client + $codes.Benign)
        $rateOf = { param([double] $Count) if ($requests) { 100.0 * $Count / $requests } else { 0 } }
        $usage = & $call $id 'Usage'
        $requestUnits = [long](& $sum (& $pointsOf $usage 'TotalRequestUnits') 'total')
        $normalized = & $round (& $peak (& $values (& $pointsOf $usage 'NormalizedRUConsumption') 'maximum'))
        $availability = & $round (& $mean (& $values (& $pointsOf (& $call $id 'Availability') 'ServiceAvailability') 'average')) 3
        $latency = & $round (& $mean (& $values (& $pointsOf (& $call $id 'Latency') 'ServerSideLatency') 'average')) 2
        $storage = & $call $id 'Storage'
        $documents = & $latest (& $pointsOf $storage 'DocumentCount')

        # --- Databases, containers and throughput -------------------------------------------------------------------
        $read = if ($Resource.Contains($id)) { $Resource[$id] } else { @{ Logs = $null; Error = 'Not read.'; Databases = @() } }
        $manualRu = 0; $autoscaleRu = 0; $containerCount = 0
        foreach ($db in @($read.Databases)) {
            $shared = if ($serverless) { $null } else { & $throughputOf $db.Throughput }
            if ($shared) { if ($shared.Mode -eq 'Autoscale') { $autoscaleRu += $shared.RU } else { $manualRu += $shared.RU } }
            $smallDedicated = 0
            foreach ($container in @($db.Containers)) {
                $containerCount++
                $own = if ($serverless) { $null } else { & $throughputOf $container.Throughput }
                if ($own) { if ($own.Mode -eq 'Autoscale') { $autoscaleRu += $own.RU } else { $manualRu += $own.RU; if ($own.RU -le 1000) { $smallDedicated++ } } }
                $res = & $at $container.Resource 'resource'
                $partitionKey = switch ($apiName) {
                    'MongoDB' { (@((& $at $res 'shardKey').Keys) -join ', ') }
                    'Cassandra' { (@(@(& $at $res 'schema', 'partitionKeys') | ForEach-Object { [string](& $at $_ 'name') }) -join ', ') }
                    'Table' { 'PartitionKey' }
                    default { (@(& $at $res 'partitionKey', 'paths') -join ', ') }
                }
                $containerRows.Add([pscustomobject][ordered]@{
                        PSTypeName      = 'AAC.CosmosDBContainer'
                        Account         = $name
                        Database        = $db.Name
                        Container       = $container.Name
                        Api             = $apiName
                        PartitionKey    = $partitionKey
                        ThroughputMode  = $(if ($serverless) { 'Serverless' } elseif ($own) { $own.Mode } elseif ($shared) { 'Shared database' } else { '' })
                        RU              = $(if ($own) { $own.RU } elseif ($shared) { $shared.RU } else { $null })
                        DefaultTtl      = & $at $res 'defaultTtl'
                        AnalyticalStore = $null -ne (& $at $res 'analyticalStorageTtl')
                        IndexingMode    = [string](& $at $res 'indexingPolicy', 'indexingMode')
                        ResourceGroup   = [string]$c['resourceGroup']
                        Subscription    = & $subName ([string]$c['subscriptionId'])
                    })
            }
            if (-not $shared -and $smallDedicated -ge 4) { & $find 'Low' 'Cost' 'Many small containers with their own throughput' "$smallDedicated containers in '$($db.Name)' each have 1,000 RU/s or less of their own - each billed at least 400 RU/s, used or not." 'Share throughput at the database level for small, quiet containers - they then draw from one pool.' $db.Name }
        }

        # --- Posture --------------------------------------------------------------------------------------------
        $endpoints = @(@(& $at $p 'privateEndpointConnections') | Where-Object { $_ -and [string](& $at $_ 'properties', 'privateLinkServiceConnectionState', 'status') -eq 'Approved' }).Count
        $ipRules = @(& $at $p 'ipRules' | Where-Object { $_ }).Count
        $vnetRules = if ([string](& $at $p 'isVirtualNetworkFilterEnabled') -eq 'True') { @(& $at $p 'virtualNetworkRules' | Where-Object { $_ }).Count } else { 0 }
        $network = if ([string](& $at $p 'publicNetworkAccess') -eq 'Disabled') { 'Private' } elseif ($ipRules -or $vnetRules) { 'Firewall' } else { 'Public' }
        $localAuth = [string](& $at $p 'disableLocalAuth') -ne 'True'
        $metadataByKey = [string](& $at $p 'disableKeyBasedMetadataWriteAccess') -ne 'True'
        $tls = [string](& $at $p 'minimalTlsVersion')
        $cmk = [bool](& $at $p 'keyVaultKeyUri')
        $plan = [string]$defender[[string]$c['subscriptionId']]
        $logsTo = $null
        foreach ($diagnostic in (& $itemsOf $read.Logs)) {
            $dp = & $at $diagnostic 'properties'
            $workspace = ([string](& $at $dp 'workspaceId')).ToLowerInvariant()
            if (-not $workspace) { continue }
            $on = @(@(& $at $dp 'logs') | Where-Object { $_ -and (& $at $_ 'enabled') -eq $true -and ([string](& $at $_ 'category') -match 'Requests$|^QueryRuntimeStatistics$' -or [string](& $at $_ 'categoryGroup') -in 'audit', 'allLogs') }).Count
            if ($on) { $logsTo = $workspace; break }
        }
        $logsRead = [bool]($read.Logs -and -not $read.Logs['Error'])

        # --- Resilience -------------------------------------------------------------------------------------------
        $locations = @(@(& $at $p 'locations') | Where-Object { $_ } | Sort-Object -Property { [int](& $at $_ 'failoverPriority') })
        $regions = @($locations | ForEach-Object { [string](& $at $_ 'locationName') })
        $multiWrite = [string](& $at $p 'enableMultipleWriteLocations') -eq 'True'
        $autoFailover = [string](& $at $p 'enableAutomaticFailover') -eq 'True'
        $zoneRedundant = @($locations | Where-Object { [string](& $at $_ 'isZoneRedundant') -eq 'True' }).Count -gt 0
        $consistency = [string](& $at $p 'consistencyPolicy', 'defaultConsistencyLevel')
        $backupType = [string](& $at $p 'backupPolicy', 'type')
        $backup = if ($backupType -eq 'Continuous') { "Continuous $([string](& $at $p 'backupPolicy', 'continuousModeProperties', 'tier') -replace 'Continuous', '')".Trim() } elseif ($backupType) { $minutes = & $at $p 'backupPolicy', 'periodicModeProperties', 'backupIntervalInMinutes'; "Periodic$(if ($minutes) { " every $([Math]::Round([double]$minutes / 60, 1)) h" })" } else { '' }
        $backupRedundancy = [string](& $at $p 'backupPolicy', 'periodicModeProperties', 'backupStorageRedundancy')

        # --- Findings ---------------------------------------------------------------------------------------------
        if ($null -ne $availability -and $requests -and $availability -lt 99.99) { & $find $(if ($availability -lt 99.9) { 'High' } else { 'Medium' }) 'Availability' 'Availability below 99.99%' "Service availability averaged $availability% over the period." 'Check Resource Health and the failed requests by status code; add a region with automatic failover, and retry transient errors in the SDK.' }
        if ($codes.Throttled -gt 0) {
            $r = & $rateOf $codes.Throttled
            & $find $(if ($r -ge 1) { 'High' } else { 'Medium' }) 'Throttling' 'Requests rate limited (429)' ('{0:N0} request(s) rate limited ({1:N2}% of requests){2}.' -f $codes.Throttled, $r, $(if ($null -ne $normalized) { "; the busiest partition reached $normalized% of its throughput" } else { '' })) $(if ($serverless) { 'Serverless accounts have a per-container ceiling: spread the load with a better partition key, or move to provisioned throughput.' } else { 'Raise the RU/s or use autoscale; spread hot partitions with a better partition key; the SDKs retry 429s - make sure their retry limits are high enough.' })
        }
        elseif ($null -ne $normalized -and $normalized -ge 90 -and -not $serverless) { & $find 'Low' 'Throttling' 'Running close to its throughput' "The busiest partition reached $normalized% of its throughput, without rate limiting yet." 'Watch for 429s; autoscale absorbs peaks without paying for them all the time.' }
        if ($codes.Server -gt 0 -and (& $rateOf $codes.Server) -ge 0.1) { & $find 'Medium' 'Errors' 'Server errors (5xx)' ('{0:N0} request(s) failed on the service side.' -f $codes.Server) 'Retry transient errors (the SDKs do); check Resource Health and the request timeouts (408) and service unavailable (503) in the logs.' }
        if ($null -ne $latency -and $latency -ge 10) { & $find 'Low' 'Errors' 'Slow server-side latency' "Server-side latency averaged $latency ms." 'Look at the slow queries (QueryRuntimeStatistics): cross-partition queries, missing indexes or large documents; point reads by id and partition key are fastest.' }
        if ($tls -and $tls -notin 'Tls12', 'Tls13') { & $find 'High' 'Security' 'TLS below 1.2 accepted' "Minimum TLS version: $tls." 'Set the minimum TLS version to 1.2.' }
        if ($network -eq 'Public' -and -not $endpoints) { & $find 'High' 'Security' 'Open to every network' 'Public network access is on, with no IP firewall, virtual network rule or private endpoint.' 'Use private endpoints and disable public network access - or allow only the networks that need it.' }
        if ($localAuth) { & $find 'Medium' 'Security' 'Account keys allowed (local authentication)' 'The primary and secondary keys give full access to every database and can''t be traced to a user or app.' 'Give apps Microsoft Entra ID identities with Cosmos DB data-plane roles, then turn off local authentication.' }
        if ($localAuth -and $metadataByKey) { & $find 'Low' 'Security' 'Keys can change databases and containers' 'Anyone with a key can create, change or delete databases and containers (and their throughput).' 'Turn on disableKeyBasedMetadataWriteAccess so those changes need Azure RBAC.' }
        if ($logsRead -and -not $logsTo) { & $find 'Medium' 'Security' 'No logs to Log Analytics' 'Data-plane and control-plane requests aren''t sent to a workspace: who read or changed data can''t be investigated.' 'Add a diagnostic setting sending the request categories (DataPlaneRequests, ControlPlaneRequests, QueryRuntimeStatistics) to a Log Analytics workspace, in resource-specific tables.' }
        if ($plan -ne 'Standard') { & $find 'Low' 'Security' 'Defender for Azure Cosmos DB off' 'SQL injection, unusual access and data exfiltration raise no alert.' 'Turn on Microsoft Defender for Azure Cosmos DB for the subscription.' }
        if ($alerts.Contains($id)) { & $find 'High' 'Security' 'Defender alerts in the last 30 days' "$($alerts[$id]) alert(s) on this account." 'Investigate them in Defender for Cloud > Security alerts.' }
        if ($regions.Count -eq 1) { & $find 'Medium' 'Resilience' 'Single region' "Only in $($regions[0]): a regional outage makes the data unavailable." 'Add a second region (reads stay local, and the account can fail over).' }
        elseif ($regions.Count -gt 1 -and -not $multiWrite -and -not $autoFailover) { & $find 'Medium' 'Resilience' 'No automatic failover' "$($regions.Count) regions, but if the write region goes down Azure won't fail over by itself." 'Turn on service-managed failover, and set the failover priorities.' }
        if ($regions.Count -and -not $zoneRedundant) { & $find 'Low' 'Resilience' 'No availability zones' 'No region uses availability zones: a zone outage can interrupt the account.' 'Turn on zone redundancy for the regions that support it (set when a region is added).' }
        if ($backupType -eq 'Periodic') { & $find 'Low' 'Resilience' 'Periodic backup only' "Backups $($backup.ToLowerInvariant()); restoring means a support request, to the last backup." 'Move to continuous backup: point-in-time restore to any second in the last 7 or 30 days, by yourself.' }
        if ($backupType -eq 'Periodic' -and $backupRedundancy -eq 'Local') { & $find 'Low' 'Resilience' 'Backups stored in one datacentre' 'Periodic backups use locally redundant storage.' 'Store backups geo-redundant (or zone-redundant), or move to continuous backup.' }
        if ($consistency -eq 'Strong' -and $regions.Count -gt 1) { & $find 'Low' 'Resilience' 'Strong consistency across regions' "Every write waits for $($regions.Count) regions: slower writes, and writes stop if a region can't be reached." 'Use Bounded staleness or Session unless the app really needs Strong.' }
        if (-not $serverless -and $manualRu -ge 1000 -and $null -ne $normalized -and $normalized -lt 20 -and $requests) { & $find 'Low' 'Cost' 'Throughput higher than needed' ('{0:N0} RU/s provisioned (manual), but the busiest partition peaked at {1}% of it.' -f $manualRu, $normalized) 'Lower the RU/s, or switch to autoscale (it scales down to 10% when idle) - or serverless for spiky, light use.' }
        if ($metricsRead -and $requests -eq 0) { & $find 'Low' 'Cost' 'No requests in the period' 'Nothing read or wrote the account.' 'Confirm it is still needed; provisioned throughput is billed whether used or not.' }

        # --- Heat map: each signal 0-100 ----------------------------------------------------------------------------
        $cell = { param($Score) if ($null -eq $Score) { $null } else { @{ Score = [Math]::Round([Math]::Max(0, [Math]::Min(100, [double]$Score)), 1) } } }
        $share = { param([object[]] $Checks) 100.0 * @($Checks | Where-Object { $_ }).Count / [Math]::Max(1, $Checks.Count) }
        $securityChecks = @(($tls -notin 'Tls', 'Tls11'), ($network -ne 'Public' -or $endpoints -gt 0), (-not $localAuth), ($plan -eq 'Standard'))
        if ($logsRead) { $securityChecks += [bool]$logsTo }
        $resilienceChecks = @(($regions.Count -gt 1), ($multiWrite -or $autoFailover -or $regions.Count -le 1), $zoneRedundant, ($backupType -eq 'Continuous'))
        $costChecks = @((-not ($metricsRead -and $requests -eq 0)), (-not ($manualRu -ge 1000 -and $null -ne $normalized -and $normalized -lt 20 -and $requests)), (@($findings | Where-Object { $_.ResourceId -eq $id -and $_.Finding -like 'Many small containers*' }).Count -eq 0))
        $scores = [ordered]@{
            Availability = $(if ($null -ne $availability -and $requests) { ($availability - 99) * 100 } else { $null })
            Throttling   = $(if ($requests) { 100 - (& $rateOf $codes.Throttled) * 20 } else { $null })
            Errors       = $(if ($requests) { 100 - (& $rateOf $codes.Server) * 10 - $(if ($null -ne $latency -and $latency -ge 10) { 10 } else { 0 }) } else { $null })
            Security     = & $share $securityChecks
            Resilience   = & $share $resilienceChecks
            Cost         = & $share $costChecks
        }
        $cells = @(foreach ($signal in $signals) { & $cell $scores[$signal] })
        $scored = @($cells | Where-Object { $_ })
        $overall = if ($scored.Count) { [Math]::Round((@($scored | ForEach-Object { $_.Score }) | Measure-Object -Average).Average, 1) } else { $null }
        $heatRows.Add(@{ Label = $name; Filter = $name; Cells = $cells; Total = $(if ($null -ne $overall) { @{ Score = $overall } } else { $null }); Overall = $overall })

        $accountRows.Add([pscustomobject][ordered]@{
                PSTypeName        = 'AAC.CosmosDBAssessment'
                Account           = $name
                Score             = $overall
                Api               = $apiName
                CapacityMode      = $(if ($serverless) { 'Serverless' } else { 'Provisioned' })
                Regions           = $regions -join ', '
                RegionCount       = $regions.Count
                WriteRegions      = $(if ($multiWrite) { $regions.Count } else { [Math]::Min(1, $regions.Count) })
                AutomaticFailover = $autoFailover
                MultiRegionWrites = $multiWrite
                ZoneRedundant     = $zoneRedundant
                Consistency       = $consistency
                Backup            = $backup
                BackupRedundancy  = $backupRedundancy
                Databases         = @($read.Databases | Where-Object { $_.Name }).Count
                Containers        = $containerCount
                ProvisionedRU     = $(if ($serverless) { $null } else { $manualRu })
                AutoscaleMaxRU    = $(if ($serverless) { $null } else { $autoscaleRu })
                Requests          = $requests
                Throttled         = [long]$codes.Throttled
                ThrottleRate      = $(if ($requests) { & $round (& $rateOf $codes.Throttled) 2 } else { $null })
                ServerErrors      = [long]$codes.Server
                ClientErrors      = [long]$codes.Client
                Availability      = $availability
                NormalizedRuMax   = $normalized
                RequestUnits      = $requestUnits
                LatencyMs         = $latency
                DataGB            = & $gb (& $latest (& $pointsOf $storage 'DataUsage'))
                IndexGB           = & $gb (& $latest (& $pointsOf $storage 'IndexUsage'))
                Documents         = $(if ($null -ne $documents) { [long]$documents } else { $null })
                Network           = $network
                PrivateEndpoints  = $endpoints
                LocalAuth         = $localAuth
                MinTls            = $tls
                Encryption        = $(if ($cmk) { 'Customer-managed key' } else { 'Microsoft-managed key' })
                Defender          = $(if ($plan -eq 'Standard') { 'On' } elseif ($plan) { 'Off' } else { 'Unknown' })
                LogsTo            = $(if ($logsTo) { if ($WorkspaceName.Contains($logsTo)) { $WorkspaceName[$logsTo] } else { ($logsTo -split '/')[-1] } } else { '' })
                FreeTier          = [string](& $at $p 'enableFreeTier') -eq 'True'
                MetricsError      = $(if ($requestsCall -and $requestsCall.Error) { [string]$requestsCall.Error } else { '' })
                ResourcesError    = [string]$read.Error
                Findings          = @($findings | Where-Object { $_.ResourceId -eq $id }).Count
                Location          = [string]$c['location']
                ResourceGroup     = [string]$c['resourceGroup']
                Subscription      = & $subName ([string]$c['subscriptionId'])
                ResourceId        = $id
            })
    }

    $rank = @{ High = 0; Medium = 1; Low = 2; Info = 3 }
    $worst = @($heatRows | Sort-Object -Property @{ Expression = { if ($null -eq $_.Overall) { 101 } else { $_.Overall } } }, Label | Select-Object -First 40)
    $total = { param([string] $Property) [long](& $sumOf $accountRows.ToArray() $Property) }
    @{
        Accounts    = $accountRows.ToArray()
        Containers  = $containerRows.ToArray()
        Findings    = @($findings | Sort-Object -Property @{ Expression = { $rank[$_.Severity] } }, Account, Database, Container, Finding)
        HeatMap     = @{ Title = 'Cosmos DB accounts by signal'; RowHeader = 'Account'; Columns = $signals; Rows = $worst; Table = 'findings'; RowKey = 'Account'; ColumnKey = 'Signal'; ConsoleColumns = 6
            Note = "0-100 per signal, worst accounts first$(if ($heatRows.Count -gt 40) { " (40 of $($heatRows.Count))" }): availability, throttling, errors and latency, security, resilience, cost. Click a cell for its findings." }
        StatusCodes = $statusTotals
        Stats       = @{
            Accounts    = $accountRows.Count
            Containers  = $containerRows.Count
            Requests    = & $total 'Requests'
            Throttled   = & $total 'Throttled'
            RU          = (& $total 'ProvisionedRU') + (& $total 'AutoscaleMaxRU')
            DataGB      = [Math]::Round((& $sumOf $accountRows.ToArray() 'DataGB'), 2)
            Public      = @($accountRows | Where-Object { $_.Network -eq 'Public' -and -not $_.PrivateEndpoints }).Count
            SingleRegion = @($accountRows | Where-Object { $_.RegionCount -eq 1 }).Count
            LocalAuth   = @($accountRows | Where-Object { $_.LocalAuth }).Count
            High        = @($findings | Where-Object { $_.Severity -eq 'High' }).Count
        }
    }
}