Private/Rules/Get-TcConsolidationRule.ps1

function Get-TcConsolidationRule {
    <#
    .SYNOPSIS
    Returns the rules that look at every tenant at once, as script blocks.
 
    .DESCRIPTION
    The blocks work on the roles of the run, whatever their number: nothing here assumes that S2 or S3
    exist. A rule that needs two sources, like a site path shared by two sources, finds nothing with one.
 
    A total that misses a tenant is a minimum: storage, licenses and voice name what was not read and
    never conclude that there is enough. A shortage that is certain anyway is still reported.
    #>

    [CmdletBinding()]
    [OutputType([scriptblock])]
    param()

    { # People with an account in several tenants
        $people = @{}
        foreach ($role in $script:TcRuleRun['Roles']) {
            foreach ($user in Get-TcRuleRow $role 'users' | Where-Object { (Get-TcProperty $_ 'Type') -eq 'Member' -and (Test-TcRowTrue $_ 'Enabled') }) {
                $first = Get-TcProperty $user 'First name'
                $last = Get-TcProperty $user 'Last name'
                $key = if ($first -and $last) { ConvertTo-TcPersonKey "$first $last" } else { ConvertTo-TcPersonKey (Get-TcProperty $user 'Name') }
                if ($key -notmatch '\s' -or $key.Length -lt 5) { continue }
                if (-not $people.ContainsKey($key)) { $people[$key] = [System.Collections.Generic.List[object]]::new() }
                $people[$key].Add(@{ role = $role; upn = Get-TcProperty $user 'UPN'; name = Get-TcProperty $user 'Name' })
            }
        }
        $duplicates = @($people.Keys | Where-Object { @($people[$_] | ForEach-Object { $_['role'] } | Sort-Object -Unique).Count -ge 2 } | Sort-Object)
        if ($duplicates.Count) {
            Add-TcFinding 'CON-IDENTITIES' 'All' -Values @{ count = $duplicates.Count } -Items ($duplicates | ForEach-Object {
                    $accounts = @($people[$_])
                    '{0}: {1}' -f $accounts[0]['name'], (@($accounts | ForEach-Object { '{0} ({1})' -f (Get-TcRuleTenantName $_['role']), $_['upn'] }) -join ', ')
                })
        }
    }

    { # Generic mailboxes and aliases in several tenants
        $boxes = @{}
        $aliases = @{}
        foreach ($role in $script:TcRuleRun['Roles']) {
            foreach ($box in Get-TcRuleRow $role 'mailboxes' | Where-Object { (Get-TcProperty $_ 'Type') -in 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox' }) {
                $key = (Get-TcAddressPrefix ([string](Get-TcProperty $box 'Primary address'))).ToLowerInvariant()
                if (-not $boxes.ContainsKey($key)) { $boxes[$key] = [System.Collections.Generic.List[object]]::new() }
                $boxes[$key].Add(@{ role = $role; address = Get-TcProperty $box 'Primary address' })
            }
            foreach ($table in 'mailboxes', 'groups', 'microsoft365Groups', 'distributionGroups') {
                foreach ($row in Get-TcRuleRow $role $table) {
                    $key = ([string](Get-TcProperty $row 'Alias')).ToLowerInvariant()
                    if (-not $key) { continue }
                    if (-not $aliases.ContainsKey($key)) { $aliases[$key] = @{} }
                    $aliases[$key][$role] = 1
                }
            }
        }

        $sharedBoxes = @($boxes.Keys | Where-Object { @($boxes[$_] | ForEach-Object { $_['role'] } | Sort-Object -Unique).Count -ge 2 } | Sort-Object)
        if ($sharedBoxes.Count) {
            Add-TcFinding 'CON-SHARED-MAILBOXES' 'All' -Values @{ count = $sharedBoxes.Count } -Items ($sharedBoxes | ForEach-Object { '{0}@: {1}' -f $_, (@($boxes[$_] | ForEach-Object { $_['address'] }) -join ', ') })
        }

        $sharedAliases = @($aliases.Keys | Where-Object { $aliases[$_].Count -ge 2 } | Sort-Object)
        if ($sharedAliases.Count) {
            Add-TcFinding 'CON-ALIASES' 'All' -Values @{ count = $sharedAliases.Count } -Items ($sharedAliases | ForEach-Object { '{0}: {1}' -f $_, (@($aliases[$_].Keys | ForEach-Object { Get-TcRuleTenantName $_ }) -join ', ') })
        }
    }

    { # Site paths and team names in several tenants
        $paths = @{}
        $names = @{}
        foreach ($role in $script:TcRuleRun['Sources']) {
            foreach ($site in Get-TcRuleRow $role 'sites' | Where-Object { (Get-TcProperty $_ 'Path') -match '^/(sites|teams)/' }) {
                $key = [string](Get-TcProperty $site 'Path')
                if (-not $paths.ContainsKey($key)) { $paths[$key] = @{} }
                $paths[$key][$role] = Get-TcProperty $site 'Url'
            }
        }
        foreach ($role in $script:TcRuleRun['Roles']) {
            foreach ($team in Get-TcRuleRow $role 'teams') {
                $key = ([string](Get-TcProperty $team 'Name')).ToLowerInvariant()
                if (-not $names.ContainsKey($key)) { $names[$key] = @{} }
                $names[$key][$role] = Get-TcProperty $team 'Name'
            }
        }

        $sharedPaths = @($paths.Keys | Where-Object { $paths[$_].Count -ge 2 } | Sort-Object)
        if ($sharedPaths.Count) {
            Add-TcFinding 'CON-SITES' 'All' -Values @{ count = $sharedPaths.Count } -Items ($sharedPaths | ForEach-Object { '{0}: {1}' -f $_, (@($paths[$_].Keys | ForEach-Object { Get-TcRuleTenantName $_ }) -join ', ') })
        }

        $sharedNames = @($names.Keys | Where-Object { $names[$_].Count -ge 2 } | Sort-Object)
        if ($sharedNames.Count) {
            Add-TcFinding 'CON-TEAMS' 'All' -Values @{ count = $sharedNames.Count } -Items ($sharedNames | ForEach-Object { '{0}: {1}' -f @($names[$_].Values)[0], (@($names[$_].Keys | ForEach-Object { Get-TcRuleTenantName $_ }) -join ', ') })
        }
    }

    { # Storage, licenses and voice across the sources
        $sources = @($script:TcRuleRun['Sources'])
        if (-not $script:TcRuleRun['Tenants'].Contains('Target') -or $sources.Count -lt 2) { return }

        $quota = Get-TcRowNumber @{ q = Get-TcRuleSetting 'Target' 'sharePointSettings' 'StorageQuota' } 'q'
        $estimated = $false
        if (-not $quota) {
            $licensed = @(Get-TcRuleRow 'Target' 'users' | Where-Object { (Get-TcRowInteger $_ 'License count') -gt 0 }).Count
            if ($licensed) { $quota = (1024 * 1024) + (10 * 1024 * $licensed); $estimated = $true }
        }
        $volumes = [ordered]@{ Target = (@(Get-TcRuleRow 'Target' 'sites' | ForEach-Object { [double](Get-TcProperty $_ 'Storage MB') }) | Measure-Object -Sum).Sum }
        foreach ($source in $sources) { $volumes[$source] = (@(Get-TcRuleRow $source 'sites' | ForEach-Object { [double](Get-TcProperty $_ 'Storage MB') }) | Measure-Object -Sum).Sum }
        $total = (@($volumes.Values) | Measure-Object -Sum).Sum
        if ($quota) {
            $unread = @(Get-TcRuleUnreadRole @($volumes.Keys) 'sites')
            $margin = [double](Get-TcRuleThreshold 'storageMarginPercent')
            $severity = if ($total -gt $quota) { 'Critical' } elseif ($total -gt $quota * (1 - $margin / 100)) { 'High' } else { 'Info' }
            $values = @{
                total = Format-TcNumber ($total / 1024); quota = Format-TcNumber ($quota / 1024); margin = $margin
                estimated = $(if ($estimated) { Get-TcRulePart 'CON-CAPACITY' 'estimated' } else { '' })
                breakdown = @($volumes.Keys | ForEach-Object { if ($_ -in $unread) { '{0} not read' -f (Get-TcRuleTenantName $_) } else { '{0} {1} GB' -f (Get-TcRuleTenantName $_), (Format-TcNumber ($volumes[$_] / 1024)) } }) -join ' - '
                missing = Format-TcNumber (($total - $quota * (1 - $margin / 100)) / 1024)
                unread = @($unread | ForEach-Object { Get-TcRuleTenantName $_ }) -join ', '
            }
            if ($unread.Count -and $severity -eq 'Info') { Add-TcFinding 'CON-CAPACITY' 'All' -Variant 'unread' -Values $values }
            elseif ($unread.Count) { Add-TcFinding 'CON-CAPACITY' 'All' -Severity $severity -Variant 'unreadShort' -Values $values }
            else {
                $variant = if ($severity -eq 'Info') { '' } else { 'short' }
                Add-TcFinding 'CON-CAPACITY' 'All' -Severity $severity -Variant $variant -Values $values
            }
        }

        $licensesRead = -not @(Get-TcRuleUnreadRole 'Target' 'licenses').Count
        $sku = Get-TcRuleLicenseSku
        $needs = [ordered]@{}
        foreach ($source in $sources) { $needs[$source] = @(Get-TcRuleRow $source 'users' | Where-Object { (Get-TcProperty $_ 'Type') -eq 'Member' -and (Test-TcRowTrue $_ 'Enabled') -and (Get-TcRowInteger $_ 'License count') -gt 0 }).Count }
        $needed = (@($needs.Values) | Measure-Object -Sum).Sum
        $license = Get-TcRuleRow 'Target' 'licenses' | Where-Object { (Get-TcProperty $_ 'SKU') -eq $sku } | Select-Object -First 1
        $available = if ($license) { [int](Get-TcProperty $license 'Available') } else { 0 }
        $unreadSources = @(Get-TcRuleUnreadRole $sources 'users')
        $unread = @($unreadSources) + @(if (-not $licensesRead) { 'Target' })
        if ($needed -or $unread.Count) {
            $breakdown = @($needs.Keys | ForEach-Object { if ($_ -in $unreadSources) { '{0} not read' -f (Get-TcRuleTenantName $_) } else { '{0} {1}' -f (Get-TcRuleTenantName $_), $needs[$_] } }) -join ' - '
            $values = @{
                sku = $(if ($sku) { $sku } else { 'Expected' }); needed = $needed; available = $(if ($licensesRead) { $available } else { 'unknown' }); breakdown = $breakdown; missing = $needed - $available
                unread = @($unread | ForEach-Object { Get-TcRuleTenantName $_ }) -join ', '
            }
            if ($unread.Count) { Add-TcFinding 'CON-LICENSES' 'All' -Variant $(if ($licensesRead -and $available -lt $needed) { 'unreadShort' } else { 'unread' }) -Values $values }
            elseif ($available -lt $needed) { Add-TcFinding 'CON-LICENSES' 'All' -Variant 'short' -Values $values }
            else { Add-TcFinding 'CON-LICENSES' 'All' -Values $values }
        }

        $voice = [ordered]@{}
        $numbers = @{}
        foreach ($source in $sources) {
            $voice[$source] = @(Get-TcRuleRow $source 'teamsUsers' | Where-Object { Test-TcRowTrue $_ 'Enterprise Voice' }).Count
            foreach ($number in Get-TcRuleRow $source 'phoneNumbers') { $type = [string](Get-TcProperty $number 'Type'); $numbers[$type] = 1 + [int]$numbers[$type] }
        }
        $voiceUsers = (@($voice.Values) | Measure-Object -Sum).Sum
        $unreadSources = @(Get-TcRuleUnreadRole $sources 'teamsUsers', 'phoneNumbers')
        if ($voiceUsers -or $numbers.Count -or $unreadSources.Count) {
            $phone = (@(Get-TcRuleRow 'Target' 'licenses' | Where-Object { (Get-TcProperty $_ 'Plans') -match 'MCOEV' -and (Get-TcProperty $_ 'SKU') -notmatch 'VIRTUALUSER' } | ForEach-Object { [int](Get-TcProperty $_ 'Available') }) | Measure-Object -Sum).Sum
            $unread = @($unreadSources) + @(if (-not $licensesRead) { 'Target' })
            $short = $licensesRead -and $phone -lt $voiceUsers
            $values = @{
                users = $voiceUsers; numbers = (@($numbers.Values) | Measure-Object -Sum).Sum; available = $(if ($licensesRead) { [int]$phone } else { 'unknown' })
                breakdown = @($voice.Keys | ForEach-Object { if ($_ -in $unreadSources) { '{0} not read' -f (Get-TcRuleTenantName $_) } else { '{0} {1}' -f (Get-TcRuleTenantName $_), $voice[$_] } }) -join ' - '
                types = @($numbers.Keys | ForEach-Object { '{0} {1}' -f $_, $numbers[$_] }) -join ' - '
                unread = @($unread | ForEach-Object { Get-TcRuleTenantName $_ }) -join ', '
            }
            if ($unread.Count) { Add-TcFinding 'CON-VOICE' 'All' -Severity $(if ($short) { 'High' } else { '' }) -Variant 'unread' -Values $values }
            else { Add-TcFinding 'CON-VOICE' 'All' -Severity $(if ($short) { 'High' } else { 'Info' }) -Values $values }
        }
    }

    { # Third-party applications across the sources
        $apps = @{}
        foreach ($source in $script:TcRuleRun['Sources']) {
            foreach ($app in Get-TcRuleRow $source 'servicePrincipals' | Where-Object { (Get-TcProperty $_ 'Origin') -eq 'Third party' -and (Test-TcRowTrue $_ 'Enabled') }) {
                $key = ([string](Get-TcProperty $app 'Name')).ToLowerInvariant()
                if (-not $apps.ContainsKey($key)) { $apps[$key] = @{ name = Get-TcProperty $app 'Name'; roles = @{} } }
                $apps[$key]['roles'][$source] = 1
            }
        }
        if (-not $apps.Count) { return }
        $inTarget = @{}
        foreach ($app in Get-TcRuleRow 'Target' 'servicePrincipals') { $inTarget[([string](Get-TcProperty $app 'Name')).ToLowerInvariant()] = 1 }
        $present = @($apps.Keys | Where-Object { $inTarget.ContainsKey($_) }).Count
        Add-TcFinding 'CON-APPLICATIONS' 'All' -Values @{ count = $apps.Count; present = $present } -Items ($apps.Keys | Sort-Object | ForEach-Object {
                '{0} - {1}{2}' -f $apps[$_]['name'], (@($apps[$_]['roles'].Keys | ForEach-Object { Get-TcRuleTenantName $_ }) -join ', '), $(if ($inTarget.ContainsKey($_)) { ' - already in the target' })
            })
    }

    { # Domains to move
        $sources = @($script:TcRuleRun['Sources'])
        if (-not $sources.Count) { return }
        $items = foreach ($source in $sources) {
            $dns = @{}
            foreach ($row in Get-TcRuleRow $source 'dnsSummary') { $dns[([string](Get-TcProperty $row 'Domain')).ToLowerInvariant()] = $row }
            foreach ($domain in Get-TcSourceDomain $source) {
                $row = $dns[$domain]
                '{0} - {1} - {2}{3}' -f (Get-TcRuleTenantName $source), $domain, $(if ($row) { Get-TcProperty $row 'DNS host' } else { 'host to confirm' }), $(if ($row) { " - flow $(Get-TcProperty $row 'Inbound mail flow') - MX TTL $(Get-TcProperty $row 'MX TTL')" })
            }
        }
        Add-TcFinding 'CON-DOMAINS' 'All' -Values @{ count = @($items).Count; sources = $sources.Count } -Items @($items)
    }

    { # Teams policy types customized in several places
        $policies = @{}
        foreach ($source in $script:TcRuleRun['Sources']) {
            foreach ($policy in Get-TcRuleRow $source 'teamsPolicies' | Where-Object { Test-TcRowTrue $_ 'Custom' }) {
                $type = [string](Get-TcProperty $policy 'Type')
                if (-not $policies.ContainsKey($type)) { $policies[$type] = [System.Collections.Generic.List[string]]::new() }
                $policies[$type].Add(('{0} ({1})' -f (([string](Get-TcProperty $policy 'Identity')) -replace '^Tag:', ''), (Get-TcRuleTenantName $source)))
            }
        }
        $several = @($policies.Keys | Where-Object { @($policies[$_]).Count -ge 2 } | Sort-Object)
        if ($several.Count) { Add-TcFinding 'CON-TEAMS-POLICIES' 'All' -Values @{ count = $several.Count } -Items ($several | ForEach-Object { '{0}: {1}' -f $_, (@($policies[$_]) -join ', ') }) }
    }
}