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 } } } |