Private/Get-NSPConditionalAccessDashboardState.ps1

function Get-NSPConditionalAccessDashboardState {
    <#
    .SYNOPSIS
        What the dashboard header shows, from local files and the current session only (never a Graph call).
    .DESCRIPTION
        RunsMarker changes whenever a run journal is executed (not merely created), so the
        dashboard can tell that an action wrote to the tenant and a fresh inventory is due.
        RunsNeedingAttention includes journals approved but never run.
    #>

    [CmdletBinding()]
    param(
        [string]$TenantId,
        [string]$ConfigRoot
    )

    function Get-Latest {
        param([string]$Kind, [string]$Filter = '*.json')
        $folder = Get-NSPConditionalAccessLocalPath -Kind $Kind -ConfigRoot $ConfigRoot
        if ($TenantId) { $folder = Join-Path $folder $TenantId }
        if (-not (Test-Path -LiteralPath $folder)) { return $null }
        Get-ChildItem -LiteralPath $folder -Filter $Filter -File | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1
    }

    $context = $null
    if (Get-Command Get-MgContext -ErrorAction SilentlyContinue) { $context = Get-MgContext }
    # Registration signs in through Microsoft Graph PowerShell's app, so a session in the right
    # tenant can still be the wrong app. Only the tool's recorded app counts as connected.
    $registration = $null
    $registrations = Get-NSPConditionalAccessLocalPath -Kind AppRegistrations -ConfigRoot $ConfigRoot
    if ($TenantId -and (Test-Path -LiteralPath $registrations)) {
        $registration = @(Get-ChildItem -LiteralPath $registrations -Filter '*.json' -File | ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json } |
                Where-Object { $_.TenantId -eq $TenantId })[0]
    }
    $clientId = if ($registration) { $registration.ClientId } else { $null }
    $inTenant = [bool]($context -and $context.TenantId -eq $TenantId)
    $otherApp = [bool]($inTenant -and $clientId -and $context.ClientId -ne $clientId)
    $profilePath = if ($TenantId) { Join-Path (Get-NSPConditionalAccessLocalPath -Kind TenantProfiles -ConfigRoot $ConfigRoot) "$TenantId.json" } else { $null }
    $tenantProfile = if ($profilePath -and (Test-Path -LiteralPath $profilePath)) { Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json } else { $null }

    $runs = @()
    $runFolder = Join-Path (Get-NSPConditionalAccessLocalPath -Kind Runs -ConfigRoot $ConfigRoot) ([string]$TenantId)
    if ($TenantId -and (Test-Path -LiteralPath $runFolder)) {
        $runs = @(Get-ChildItem -LiteralPath $runFolder -Filter '*.json' -File | Sort-Object LastWriteTimeUtc -Descending | ForEach-Object {
                $run = Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json
                [pscustomobject]@{ Path = $_.FullName; RunId = $run.RunId; Status = $run.Status; Items = @($run.Items).Count; Updated = $_.LastWriteTime; UpdatedUtc = $_.LastWriteTimeUtc }
            })
    }
    $plan = Get-Latest -Kind Plans
    $pending = 0
    if ($plan) { $pending = @((Get-Content -LiteralPath $plan.FullName -Raw | ConvertFrom-Json).Items | Where-Object Decision -eq 'Pending').Count }
    $job = Get-Latest -Kind Coverage -Filter 'job-*.json'
    $inventory = Get-Latest -Kind Inventory
    # The coverage app recorded on this computer: what it was granted, against what the job needs now.
    $coverageApp = $null
    $coverageRecord = if ($TenantId) { Join-Path (Get-NSPConditionalAccessLocalPath -Kind CoverageApps -ConfigRoot $ConfigRoot) "$TenantId.json" } else { $null }
    if ($coverageRecord -and (Test-Path -LiteralPath $coverageRecord)) {
        $record = Get-Content -LiteralPath $coverageRecord -Raw | ConvertFrom-Json
        $notAfter = if ($record.CertificateNotAfter) { [datetime]::Parse($record.CertificateNotAfter, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::RoundtripKind) } else { $null }
        $coverageApp = [pscustomobject]@{
            MissingPermissions = @(Get-NSPConditionalAccessGraphScopes -Purpose Coverage | Where-Object { $_ -notin @($record.ApplicationPermissions) })
            CertificateNotAfter = $notAfter
            ExpiresSoon = [bool]($notAfter -and $notAfter.ToUniversalTime() -lt (Get-Date).ToUniversalTime().AddDays(30))
        }
    }
    $domain = if ($tenantProfile) { $tenantProfile.TenantDomain } elseif ($registration) { $registration.TenantDomain } else { $null }

    [pscustomobject]@{
        TenantId = $TenantId
        TenantDomain = $domain
        Registered = [bool]$registration
        ProfilePath = if ($tenantProfile) { $profilePath } else { $null }
        Profile = $tenantProfile
        Context = $context
        Connected = $inTenant -and -not $otherApp
        OtherApp = $otherApp
        Inventory = $inventory
        InventoryAge = if ($inventory) { (Get-Date).ToUniversalTime() - $inventory.LastWriteTimeUtc } else { $null }
        Plan = $plan
        PendingDecisions = $pending
        Runs = $runs
        RunsMarker = (@($runs | Where-Object Status -ne 'Pending' | ForEach-Object { "$($_.RunId)=$($_.UpdatedUtc.Ticks)" }) -join '|')
        RunsNeedingAttention = @($runs | Where-Object Status -in 'Failed', 'Paused', 'OperatorActionRequired', 'CompletedWithReview', 'InProgress', 'Pending')
        Impact = Get-Latest -Kind Impact
        CoverageApp = $coverageApp
        LastCoverageJob = if ($job) { Get-Content -LiteralPath $job.FullName -Raw | ConvertFrom-Json } else { $null }
    }
}