Private/Show-NSPConditionalAccessDashboard.ps1
|
function Show-NSPConditionalAccessDashboard { <# .SYNOPSIS Dashboard: draws the main screen. Banner, status tiles, every policy's status, and what needs attention. .DESCRIPTION From local files and the session only (Get-NSPConditionalAccessDashboardState and the cached Get-NSPConditionalAccessDashboardInsight); never a Graph call. Clears the screen first. #> [CmdletBinding()] param( [Parameter(Mandatory)]$Session ) function Format-Age { param([timespan]$Age) if ($Age.TotalMinutes -lt 1) { 'just now' } elseif ($Age.TotalMinutes -lt 60) { "$([int]$Age.TotalMinutes) min ago" } elseif ($Age.TotalHours -lt 24) { "$([int]$Age.TotalHours) h ago" } else { "$([int]$Age.TotalDays) day(s) ago" } } $state = $Session.State $width = [Math]::Min((Get-NSPConsoleWidth), 200) $insight = Get-NSPConditionalAccessDashboardInsight -State $state -Cache $Session.Cache -ConfigRoot $Session.ConfigRoot Clear-NSPConsole $subtitle = if ($state.TenantDomain) { $state.TenantDomain } elseif ($state.TenantId) { $state.TenantId } else { 'no tenant yet: press N' } Write-NSPConsoleHeader -Title 'NSP Conditional Access' -Subtitle $subtitle -Width $width # Status tiles, two to four per line by window width. $session = if ($state.Connected) { @([string]$state.Context.Account, 'Success') } elseif ($state.OtherApp) { @('signed in through another app (C to fix)', 'Warning') } elseif (-not $state.TenantId) { @('no tenant (N to set one up)', 'Warning') } elseif (-not $state.Registered) { @('tool not registered here (C)', 'Warning') } else { @('not signed in (C to sign in)', 'Warning') } $inventory = if (-not $state.Inventory) { @('none yet', 'Warning') } elseif ($state.InventoryAge.TotalHours -ge 24) { @((Format-Age $state.InventoryAge), 'Warning') } else { @((Format-Age $state.InventoryAge), 'Success') } $profileText = if (-not $state.ProfilePath) { @('none yet', 'Warning') } else { $kind = if ($state.Profile.Hybrid) { 'hybrid (AD sync)' } else { 'cloud only' } @("built, $kind, home $($state.Profile.HomeCountry)", 'Success') } $defaults = $null if ($insight.Inventory) { $defaults = Get-NSPConditionalAccessMapValue $insight.Inventory 'SecurityDefaults' } $defaultsText = if (-not $defaults -or -not (Get-NSPConditionalAccessMapValue $defaults 'Available')) { @('unknown (take an inventory)', 'Muted') } elseif (Get-NSPConditionalAccessMapValue $defaults 'IsEnabled') { @('ON (turned off when enforcing)', 'Warning') } else { @('off', 'Success') } $registration = (Get-NSPConditionalAccessOption).Registration.Default $options = Get-NSPConditionalAccessMapValue $state.Profile 'Options' if ($options -and (Get-NSPConditionalAccessMapValue $options 'Registration')) { $registration = Get-NSPConditionalAccessMapValue $options 'Registration' } $open = @($state.RunsNeedingAttention).Count $runsText = if ($open) { @("$open need attention", 'Warning') } else { @('none open', 'Muted') } $planText = if ($state.PendingDecisions) { @("$($state.PendingDecisions) undecided in the latest plan", 'Accent') } else { @('nothing undecided', 'Muted') } $coverage = 'off' $coverageRole = 'Muted' if ($state.Profile) { $scheduled = (Get-NSPConditionalAccessCoverageSetting -TenantProfile $state.Profile).Scheduled if ($scheduled -and $scheduled.Enabled) { $coverage = 'scheduled'; $coverageRole = 'Success' } if ($state.LastCoverageJob) { $coverage += ", last $($state.LastCoverageJob.Status)" if ($state.LastCoverageJob.Status -in 'Held', 'Failed') { $coverageRole = 'Warning' } } } $tiles = @( @('Session', $session), @('Inventory', $inventory) @('Profile', $profileText), @('Sec. Defaults', $defaultsText) @('MFA setup', @($registration, 'Normal')), @('Coverage', @($coverage, $coverageRole)) @('Plan', $planText), @('Runs', $runsText) ) $perLine = [Math]::Max(2, [Math]::Min(4, [int][Math]::Floor(($width - 2) / 46))) $tileWidth = [int][Math]::Floor(($width - 2) / $perLine) for ($index = 0; $index -lt $tiles.Count; $index += $perLine) { $segments = @() foreach ($tile in $tiles[$index..([Math]::Min($index + $perLine, $tiles.Count) - 1)]) { $segments += , @(" $($tile[0])", 'Muted', 17) $segments += , @($tile[1][0], $tile[1][1], ($tileWidth - 17)) } Write-NSPConsoleSegment -Segment $segments } # Policies, in columns: with each policy's name where the window is wide enough. Write-NSPConsoleLine '' Write-NSPConsoleRule -Title 'Policies' -Width $width if ($insight.Problem) { Write-NSPConsoleLine " Status not available: $($insight.Problem)" -Role Error } elseif (-not @($insight.Policies).Count) { Write-NSPConsoleLine ' Shown once there is an inventory and a tenant profile.' -Role Muted } else { $named = $width -ge 150 # Columns share the window; a named cell needs about 90 characters to be worth it. $columns = if ($named) { [Math]::Max(1, [Math]::Min(3, [int][Math]::Floor(($width - 2) / 90))) } else { [Math]::Max(1, [Math]::Min(4, [int][Math]::Floor(($width - 2) / 38))) } $cell = [int][Math]::Floor(($width - 2) / $columns) $rows = @($insight.Policies) for ($index = 0; $index -lt $rows.Count; $index += $columns) { $segments = @(' ') foreach ($row in $rows[$index..([Math]::Min($index + $columns, $rows.Count) - 1)]) { $segments += , @(($row.Id -replace '^NSP-', ''), 'Strong', 18) $segments += , @($row.Status, $row.Role, 20) if ($named) { $segments += , @($row.Name, 'Muted', ($cell - 41)); $segments += ' ' } } Write-NSPConsoleSegment -Segment $segments } } # Attention. $notes = [System.Collections.Generic.List[object]]::new() if ($Session.InventoryProblem) { $notes.Add(@("$($Session.InventoryProblem) Press I to try again.", 'Error')) } $errors = @($insight.Findings | Where-Object Severity -eq 'Error') $warnings = @($insight.Findings | Where-Object Severity -eq 'Warning') foreach ($finding in $errors | Select-Object -First 3) { $notes.Add(@("$($finding.Rule): $($finding.Subject)", 'Error')) } if ($errors.Count -gt 3) { $notes.Add(@("$($errors.Count - 3) more problem(s)", 'Error')) } if ($warnings.Count) { $notes.Add(@("$($warnings.Count) warning(s): $(@($warnings | ForEach-Object { $_.Rule } | Select-Object -Unique) -join ', ')", 'Warning')) } foreach ($run in @($state.RunsNeedingAttention) | Select-Object -First 2) { $what = if ($run.Status -eq 'Pending') { 'approved but never run' } else { $run.Status } $notes.Add(@("Run $($run.RunId): $what", 'Warning')) } $coverageApp = $state.PSObject.Properties['CoverageApp'] | ForEach-Object { $_.Value } if ($coverageApp -and @($coverageApp.MissingPermissions).Count) { $notes.Add(@("The coverage app lacks $(@($coverageApp.MissingPermissions) -join ', '), so scheduled runs fail. Repair it: A, then Coverage.", 'Warning')) } elseif ($coverageApp -and $coverageApp.ExpiresSoon) { $notes.Add(@("The coverage app's certificate expires $($coverageApp.CertificateNotAfter.ToLocalTime().ToString('yyyy-MM-dd')). Renew it: A, then Coverage.", 'Warning')) } if ($state.Inventory -and $state.InventoryAge.TotalHours -ge 24 -and -not $state.Connected) { $notes.Add(@('The inventory is old; sign in (C) and a fresh one is taken.', 'Warning')) } if ($notes.Count) { Write-NSPConsoleLine '' Write-NSPConsoleRule -Title 'Needs attention ("Check tenant health" explains each)' -Width $width -Role Warning foreach ($note in $notes) { Write-NSPConsoleSegment -Segment @(@(' ! ', $note[1]), $note[0]) } } Write-NSPConsoleRule -Width $width } |