Private/Report/Get-TcReportData.ps1

function Get-TcReportData {
    <#
    .SYNOPSIS
    Gathers what the report shows, from the project, the snapshot, the rules and the plan.
 
    .DESCRIPTION
    Returns Data, read when the page opens, and Tables, the rows of the snapshot, read at the first need.
    Everything is computed here and only shown in the page, so the numbers have one source. A role that the
    project does not have never appears. What was not read stays empty, with its state beside it.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'The data of the report is one thing.')]
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]$Project,
        [Parameter(Mandatory)][System.Collections.IDictionary]$Snapshot,
        [Parameter(Mandatory)][System.Collections.IDictionary]$Plan,
        [Parameter(Mandatory)][System.Collections.IDictionary]$RuleSet,
        [AllowEmptyCollection()][object[]]$Readiness = @(),
        [AllowEmptyCollection()][string[]]$Log = @(),
        [AllowEmptyString()][string]$TrackingText = '',
        [string]$TrackingName = 'team-tracking.csv',
        [AllowEmptyCollection()][string[]]$IgnoreNameSuffix = @()
    )

    $target = Get-TcProperty $Project 'target'
    $sources = @(@(Get-TcProperty $Project 'sources' @()) | Sort-Object { Get-TcProperty $_ 'role' })
    $sourceRoles = @($sources | ForEach-Object { [string](Get-TcProperty $_ 'role') })
    $roles = @('Target') + $sourceRoles
    $tenantData = Get-TcProperty $Snapshot 'tenants'
    $coverage = @(Get-TcProperty $Snapshot 'coverage' @())
    $meta = Get-TcProperty $Snapshot 'meta'
    $dictionary = Get-TcTableDictionary
    $catalog = Get-TcProductCatalog

    $names = @{ Target = [string](Get-TcProperty $target 'displayName') }
    foreach ($source in $sources) { $names[[string](Get-TcProperty $source 'role')] = [string](Get-TcProperty $source 'displayName') }

    $date = {
        param($Value)
        if ($Value -is [datetime]) { return $Value.ToString('yyyy-MM-dd HH:mm', [cultureinfo]::InvariantCulture) }
        $parsed = [datetime]::MinValue
        if ($Value -and [datetime]::TryParse([string]$Value, [cultureinfo]::InvariantCulture, [Globalization.DateTimeStyles]::None, [ref]$parsed)) { return $parsed.ToString('yyyy-MM-dd HH:mm', [cultureinfo]::InvariantCulture) }
        [string]$Value
    }

    # Get-TcTableState walks the coverage: give it only the lines of the tenant, as the tables number in the hundreds.
    $tableStates = [ordered]@{}
    foreach ($role in $roles) {
        $scoped = @{ tenants = $tenantData; coverage = @($coverage | Where-Object { (Get-TcProperty $_ 'role') -eq $role }) }
        $states = [ordered]@{}
        foreach ($table in $dictionary.Keys) {
            $state = Get-TcTableState -Snapshot $scoped -Role $role -Table $table
            $states[$table] = [ordered]@{ state = $state.State; status = $state.Status; rows = $state.Rows; message = $state.Message; remedy = $state.Remedy }
        }
        $tableStates[$role] = $states
    }

    $tenants = foreach ($role in $roles) {
        $definition = if ($role -eq 'Target') { $target } else { @($sources | Where-Object { (Get-TcProperty $_ 'role') -eq $role })[0] }
        $present = $null -ne $tenantData -and $tenantData.Contains($role)
        $lines = @($coverage | Where-Object { (Get-TcProperty $_ 'role') -eq $role })
        $worked = @($lines | Where-Object { (Get-TcProperty $_ 'status') -in 'OK', 'Partial' })
        $clean = @($lines | Where-Object { (Get-TcProperty $_ 'status') -eq 'OK' })
        $tables = Get-TcProperty (Get-TcProperty $tenantData $role) 'tables'
        $rows = 0
        if ($tables) { foreach ($table in $tables.Keys) { $rows += @($tables[$table]).Count } }

        $domains = @(@(Get-TcProperty $tables 'domains' @()) | Where-Object { -not ([string](Get-TcProperty $_ 'Domain')).EndsWith('.onmicrosoft.com') } | ForEach-Object { [string](Get-TcProperty $_ 'Domain') })
        $initial = @(@(Get-TcProperty $tables 'domains' @()) | Where-Object { Get-TcProperty $_ 'Initial' $false } | ForEach-Object { [string](Get-TcProperty $_ 'Domain') }) | Select-Object -First 1
        $primary = [string](Get-TcProperty $definition 'primaryDomain')

        [ordered]@{
            role = $role; kind = $(if ($role -eq 'Target') { 'Target' } else { 'Source' }); name = $names[$role]; primaryDomain = $primary
            wave = Get-TcProperty $definition 'wave'; domains = @(if ($domains.Count) { $domains } else { $primary }); initialDomain = [string]$initial
            collection = $(if (-not $present) { 'Absent' } elseif (-not $worked.Count -and -not $rows) { 'Nothing' } elseif ($clean.Count -lt $lines.Count) { 'Partial' } else { 'Full' })
            collectors = $lines.Count; collected = $clean.Count; rows = $rows
        }
    }

    $actions = @(Get-TcProperty $Plan 'actions' @())

    $identity = [ordered]@{}
    foreach ($role in $sourceRoles) {
        $action = @($actions | Where-Object { $_['id'] -eq "$role-IDENTITY-MATRIX" }) | Select-Object -First 1
        $state = $tableStates[$role]['users']
        $sourceRead = $state['state'] -notin 'NotCollected', 'NotFullyRead'
        $targetThere = $null -ne $tenantData -and $tenantData.Contains('Target')
        if (-not $sourceRead -or -not $targetThere -or -not (Get-TcProperty $action 'drafted' $true)) {
            $identity[$role] = [ordered]@{
                drafted = $false; rows = @(); counts = [ordered]@{}
                reason = $(if ($action) { [string]$action['finding'] } else { "The accounts could not be read ($($state['status']))." })
                remedy = $(if ($action) { [string]$action['action'] } else { [string]$state['remedy'] })
            }
            continue
        }
        $rows = @(ConvertTo-TcIdentityMatrix -Snapshot $Snapshot -SourceRole $role -IgnoreNameSuffix $IgnoreNameSuffix)
        $counts = [ordered]@{}
        foreach ($decision in 'Create', 'Duplicate', 'To review') { $counts[$decision] = @($rows | Where-Object { $_['Decision'] -eq $decision }).Count }
        $counts['Bookings'] = @($rows | Where-Object { $_['Notes'] -match 'Bookings' }).Count
        $identity[$role] = [ordered]@{ drafted = $true; rows = $rows; counts = $counts; reason = ''; remedy = '' }
    }

    $dns = [ordered]@{}
    foreach ($role in $sourceRoles) {
        $tables = Get-TcProperty (Get-TcProperty $tenantData $role) 'tables'
        $dns[$role] = [ordered]@{
            summaryState = $tableStates[$role]['dnsSummary']; recordState = $tableStates[$role]['publicDns']
            summary = @(Get-TcProperty $tables 'dnsSummary' @()); records = @(Get-TcProperty $tables 'publicDns' @())
        }
    }

    $measures = @(Get-TcProperty $RuleSet 'measures' @())
    $exposure = Get-TcReportExposure -Snapshot $Snapshot -Catalog $catalog -Source $sourceRoles -TableState $tableStates -Measure $measures -Identity $identity -Name $names
    $found = Get-TcProductTrace -Snapshot $Snapshot -Catalog $catalog -Role $roles -TableState $tableStates

    $products = foreach ($product in $catalog['products']) {
        $copy = [ordered]@{}
        foreach ($key in $product.Keys) { if ($key -ne 'detection') { $copy[$key] = $product[$key] } }
        $copy['searched'] = [bool]$product['detection']
        $copy['traces'] = if ($product['detection']) { $found['Traces'][$product['id']] } else { [ordered]@{} }
        $copy
    }

    $ewsState = [ordered]@{}
    foreach ($role in $roles) {
        $tables = Get-TcProperty (Get-TcProperty $tenantData $role) 'tables'
        $settingsRead = $tableStates[$role]['exchangeSettings']['state'] -notin 'NotCollected', 'NotFullyRead'
        $protocolsRead = $tableStates[$role]['mailboxProtocols']['state'] -notin 'NotCollected', 'NotFullyRead'
        $line = @(Get-TcProperty $tables 'exchangeSettings' @()) | Where-Object { (Get-TcProperty $_ 'Setting') -eq 'Organization.EwsEnabled' } | Select-Object -First 1
        $protocols = @(Get-TcProperty $tables 'mailboxProtocols' @())
        $ewsState[$role] = [ordered]@{
            read = $settingsRead; found = [bool]$line; enabled = $(if ($line) { [string](Get-TcProperty $line 'Value') } else { $null })
            mailboxes = $(if ($protocolsRead) { $protocols.Count } else { $null })
            usingEws = $(if ($protocolsRead) { @($protocols | Where-Object { Test-TcRowTrue $_ 'EWS' }).Count } else { $null })
            status = $tableStates[$role]['exchangeSettings']['status']
        }
    }

    $tableLabels = [ordered]@{}
    foreach ($table in $dictionary.Keys) { $tableLabels[$table] = [ordered]@{ label = $dictionary[$table]['label']; workload = $dictionary[$table]['workload'] } }

    $findings = @(Get-TcProperty $RuleSet 'findings' @())
    $settingGaps = @(Get-TcProperty $RuleSet 'settingGaps' @())
    $rowCount = 0
    $tableCount = 0
    $tables = [ordered]@{}
    foreach ($role in $roles) {
        $mine = Get-TcProperty (Get-TcProperty $tenantData $role) 'tables'
        $tables[$role] = if ($mine) { $mine } else { [ordered]@{} }
        if ($mine) { foreach ($table in $mine.Keys) { $tableCount++; $rowCount += @($mine[$table]).Count } }
    }

    $now = [datetime]::UtcNow
    $data = [ordered]@{
        report         = [ordered]@{
            generated = $now.ToLocalTime().ToString('yyyy-MM-dd HH:mm', [cultureinfo]::InvariantCulture); generatedIso = $now.ToString('yyyy-MM-ddTHH:mm:ss.fffZ', [cultureinfo]::InvariantCulture)
            moduleVersion = [string](Get-TcProperty $meta 'moduleVersion'); depth = [string](Get-TcProperty $meta 'depth')
            snapshotStarted = & $date (Get-TcProperty $meta 'started'); snapshotFinished = & $date (Get-TcProperty $meta 'finished')
        }
        project        = [ordered]@{
            name = [string](Get-TcProperty $Project 'name'); key = '{0}|{1}' -f (Get-TcProperty $Project 'name'), (Get-TcProperty $target 'primaryDomain')
            targetState = [string](Get-TcProperty $target 'state'); team = @(@(Get-TcProperty $Project 'team' @()) | Where-Object { $_ })
        }
        tenants        = @($tenants)
        waves          = @(Get-TcProperty $Plan 'waves' @())
        moments        = @(Get-TcProperty (Get-TcProperty $Plan 'meta') 'moments' @('Now', 'Preparation', 'Before cutover', 'Cutover', 'After cutover', 'Closure'))
        actions        = $actions
        tasks          = @(Get-TcProperty $Plan 'tasks' @())
        decisions      = @(Get-TcProperty $Plan 'decisions' @())
        configurations = @(Get-TcProperty $Plan 'targetConfigurations' @())
        procedures     = Get-TcProperty $Plan 'procedures' ([ordered]@{})
        remediations   = @(Get-TcProperty $Plan 'remediations' @())
        findings       = $findings
        measures       = $measures
        settingGaps    = $settingGaps
        unread         = @(Get-TcProperty $RuleSet 'unread' @())
        errors         = @(Get-TcProperty $RuleSet 'errors' @())
        coverage       = $coverage
        readiness      = @($Readiness)
        identity       = $identity
        dns            = $dns
        tableLabels    = $tableLabels
        tableStates    = $tableStates
        manualProducts = @(Get-TcProperty (Get-TcProperty (Get-TcProperty $tenantData 'Target') 'tables') 'manualProducts' @())
        summary        = [ordered]@{
            collectors = $coverage.Count; collected = @($coverage | Where-Object { (Get-TcProperty $_ 'status') -eq 'OK' }).Count
            partial = @($coverage | Where-Object { (Get-TcProperty $_ 'status') -eq 'Partial' }).Count
            missing = @($coverage | Where-Object { (Get-TcProperty $_ 'status') -notin 'OK', 'Partial' }).Count
            tables = $tableCount; rows = $rowCount; findings = $findings.Count; gaps = $settingGaps.Count
        }
        solutions      = [ordered]@{
            checkedOn = $catalog['checkedOn']; sources = $catalog['sources']; families = $catalog['families']; products = @($products)
            matrixStates = $catalog['matrixStates']; copyTools = $catalog['copyTools']; copyMatrix = $catalog['copyMatrix']; ews = $catalog['ews']
            ewsState = $ewsState; exposure = $exposure['Values']; exposureFacts = $exposure['Facts']; traceCoverage = $found['Coverage']
        }
        tracking       = [ordered]@{ fileName = $TrackingName; text = $TrackingText }
        log            = @($Log)
    }

    @{ Data = $data; Tables = $tables }
}