Private/Rules/Get-TcMeasure.ps1

function Get-TcMeasure {
    <#
    .SYNOPSIS
    Returns one line per indicator: the value of each tenant, the total of the sources and the target after.
 
    .DESCRIPTION
    A value is empty when its table was not collected, so a gap is not read as a zero. Sources and after
    stay empty for what cannot add up, like a Secure Score, and when target or sources are missing. A
    value that cannot be read stays empty too: it is reported in errors, and the other measures carry on.
    UnreadSources names the sources with no value: when it is not empty, sources and after are minimums.
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param()

    $count = { param($Role, $Table, [scriptblock]$Filter) @(Get-TcRuleRow $Role $Table | Where-Object $Filter).Count }
    $rows = { param($Role, $Table) @(Get-TcRuleRow $Role $Table).Count }
    $sum = { param($Role, $Table, $Column) (@(Get-TcRuleRow $Role $Table | ForEach-Object { [double](Get-TcProperty $_ $Column) }) | Measure-Object -Sum).Sum }

    # Workload, label, table that says whether it was collected, how to read it, whether it adds up.
    $definitions = @(
        @('Entra ID', 'Active member accounts', 'users', { param($R) & $count $R 'users' { (Get-TcProperty $_ 'Type') -eq 'Member' -and (Test-TcRowTrue $_ 'Enabled') } }, $true),
        @('Entra ID', 'Licensed accounts', 'users', { param($R) & $count $R 'users' { (Get-TcRowInteger $_ 'License count') -gt 0 } }, $true),
        @('Entra ID', 'Guest accounts', 'users', { param($R) & $count $R 'users' { (Get-TcProperty $_ 'Type') -eq 'Guest' } }, $false),
        @('Entra ID', 'Groups', 'groups', { param($R) & $rows $R 'groups' }, $true),
        @('Entra ID', 'Active Global Administrators', 'adminRoles', { param($R) & $count $R 'adminRoles' { (Get-TcProperty $_ 'Role') -eq 'Global Administrator' -and (Get-TcProperty $_ 'Assignment') -eq 'Active' } }, $false),
        @('Entra ID', 'Active Conditional Access policies', 'conditionalAccess', { param($R) & $count $R 'conditionalAccess' { (Get-TcProperty $_ 'State') -eq 'enabled' } }, $false),
        @('Entra ID', 'Third-party applications', 'servicePrincipals', { param($R) & $count $R 'servicePrincipals' { (Get-TcProperty $_ 'Origin') -eq 'Third party' } }, $false),
        @('Entra ID', 'Accounts on legacy authentication', 'legacySignIns', { param($R) @(Get-TcRuleRow $R 'legacySignIns' | ForEach-Object { Get-TcProperty $_ 'UPN' } | Sort-Object -Unique).Count }, $false),
        @('Entra ID', 'Risky accounts', 'riskyUsers', { param($R) & $rows $R 'riskyUsers' }, $false),
        @('Exchange Online', 'User mailboxes', 'mailboxes', { param($R) & $count $R 'mailboxes' { (Get-TcProperty $_ 'Type') -eq 'UserMailbox' } }, $true),
        @('Exchange Online', 'Shared mailboxes', 'mailboxes', { param($R) & $count $R 'mailboxes' { (Get-TcProperty $_ 'Type') -eq 'SharedMailbox' } }, $true),
        @('Exchange Online', 'Rooms and equipment', 'mailboxes', { param($R) & $count $R 'mailboxes' { (Get-TcProperty $_ 'Type') -in 'RoomMailbox', 'EquipmentMailbox' } }, $true),
        @('Exchange Online', 'Mailbox volume (GB)', 'mailboxUsage', { param($R) [math]::Round((@(Get-TcRuleRow $R 'mailboxUsage' | ForEach-Object { Get-TcRowNumber $_ 'Size GB' }) | Measure-Object -Sum).Sum) }, $true),
        @('Exchange Online', 'Active archives', 'mailboxes', { param($R) & $count $R 'mailboxes' { Test-TcRowTrue $_ 'Archive' } }, $true),
        @('Exchange Online', 'Distribution lists', 'distributionGroups', { param($R) & $rows $R 'distributionGroups' }, $true),
        @('Exchange Online', 'Mobile devices', 'mobileDevices', { param($R) & $rows $R 'mobileDevices' }, $true),
        @('Exchange Online', 'Public folders', 'publicFolders', { param($R) & $rows $R 'publicFolders' }, $true),
        @('Exchange Online', 'Transport rules', 'transportRules', { param($R) & $rows $R 'transportRules' }, $false),
        @('Domains and DNS', 'Custom domains', 'dnsSummary', { param($R) & $rows $R 'dnsSummary' }, $true),
        @('Domains and DNS', 'Domains behind a gateway', 'dnsSummary', { param($R) & $count $R 'dnsSummary' { (Get-TcProperty $_ 'Inbound mail flow') -eq 'Third-party gateway' } }, $true),
        @('SharePoint and OneDrive', 'Sites', 'sites', { param($R) & $rows $R 'sites' }, $true),
        @('SharePoint and OneDrive', 'Site volume (GB)', 'sites', { param($R) [math]::Round((& $sum $R 'sites' 'Storage MB') / 1024) }, $true),
        @('SharePoint and OneDrive', 'OneDrive accounts', 'oneDrive', { param($R) & $rows $R 'oneDrive' }, $true),
        @('SharePoint and OneDrive', 'OneDrive volume (GB)', 'oneDrive', { param($R) [math]::Round((& $sum $R 'oneDrive' 'Storage MB') / 1024) }, $true),
        @('SharePoint and OneDrive', 'External users', 'sharePointExternalUsers', { param($R) & $rows $R 'sharePointExternalUsers' }, $false),
        @('SharePoint and OneDrive', 'SPFx apps', 'sharePointCustomizations', { param($R) & $count $R 'sharePointCustomizations' { (Get-TcProperty $_ 'Type') -eq 'SPFx app' } }, $false),
        @('Teams', 'Teams', 'teams', { param($R) & $rows $R 'teams' }, $true),
        @('Teams', 'Private channels', 'teamComposition', { param($R) (@(Get-TcRuleRow $R 'teamComposition' | ForEach-Object { [int](Get-TcProperty $_ 'Private channels') }) | Measure-Object -Sum).Sum }, $true),
        @('Teams', 'Shared channels', 'teamComposition', { param($R) (@(Get-TcRuleRow $R 'teamComposition' | ForEach-Object { [int](Get-TcProperty $_ 'Shared channels') }) | Measure-Object -Sum).Sum }, $true),
        @('Teams', 'Custom policies', 'teamsPolicies', { param($R) & $count $R 'teamsPolicies' { Test-TcRowTrue $_ 'Custom' } }, $false),
        @('Teams Phone', 'Phone numbers', 'phoneNumbers', { param($R) & $rows $R 'phoneNumbers' }, $true),
        @('Teams Phone', 'Voice users', 'teamsUsers', { param($R) & $count $R 'teamsUsers' { Test-TcRowTrue $_ 'Enterprise Voice' } }, $true),
        @('Teams Phone', 'Auto attendants', 'autoAttendants', { param($R) & $rows $R 'autoAttendants' }, $true),
        @('Teams Phone', 'Call queues', 'callQueues', { param($R) & $rows $R 'callQueues' }, $true),
        @('Intune', 'Managed devices', 'managedDevices', { param($R) & $rows $R 'managedDevices' }, $true),
        @('Intune', 'Autopilot devices', 'autopilotDevices', { param($R) & $rows $R 'autopilotDevices' }, $true),
        @('Intune', 'Policies', 'intunePolicies', { param($R) & $rows $R 'intunePolicies' }, $false),
        @('Purview', 'Labels with encryption', 'sensitivityLabels', { param($R) & $count $R 'sensitivityLabels' { Test-TcRowTrue $_ 'Encryption' } }, $false),
        @('Purview', 'Retention policies', 'retentionPolicies', { param($R) & $rows $R 'retentionPolicies' }, $false),
        @('Power Platform', 'Apps and flows', 'powerFlows', { param($R) (& $rows $R 'powerApps') + (& $rows $R 'powerFlows') }, $true),
        @('Azure', 'Subscriptions', 'azureSubscriptions', { param($R) & $rows $R 'azureSubscriptions' }, $true),
        @('Other services', 'Secure Score (%)', 'secureScore', { param($R) Get-TcRuleSetting $R 'secureScore' 'Percentage' }, $false)
    )

    $roles = @($script:TcRuleRun['Roles'])
    foreach ($definition in $definitions) {
        $values = [ordered]@{}
        foreach ($role in $roles) {
            try { $values[$role] = if (Test-TcRuleTable $role $definition[2]) { & $definition[3] $role } else { $null } }
            catch {
                $values[$role] = $null
                Add-TcRuleError -Role $role -Workload $definition[0] -ErrorRecord $_
            }
        }
        $sources = @($roles | Where-Object { $_ -ne 'Target' -and $null -ne $values[$_] })
        $unreadSources = @($roles | Where-Object { $_ -ne 'Target' -and $null -eq $values[$_] })
        $total = if ($definition[4] -and $sources.Count) { (@($sources | ForEach-Object { [double]$values[$_] }) | Measure-Object -Sum).Sum } else { $null }
        $target = if ($values.Contains('Target')) { $values['Target'] } else { $null }
        [ordered]@{
            workload = $definition[0]; label = $definition[1]; table = $definition[2]; additive = [bool]$definition[4]; values = $values; sources = $total; unreadSources = $unreadSources
            after = $(if ($null -ne $total -and $null -ne $target) { [double]$target + $total } else { $null })
        }
    }
}