Public/Start-NSPConditionalAccess.ps1
|
function Start-NSPConditionalAccess { <# .SYNOPSIS Opens the guided Conditional Access dashboard for one tenant. .DESCRIPTION The main screen is a dashboard: the tenant, the session, the inventory's age, Security Defaults, every policy's status, and what needs attention. Under it, "What do you want to do?" lists tasks (deploy policies, turn them on, add travel, update trusted locations, protect MFA setup, coverage, tenant health, undo). Each task explains itself and walks from the questions through the plan, the review, and the run. Keys: N sets up a new tenant (register, sign in, inventory, profile), S switches tenant, C signs in or repairs the app registration, I takes an inventory now, H explains how the pieces fit together (with a map of every policy), A opens All tools (every individual step by category). The dashboard takes a fresh inventory (read-only) when it opens signed in, after signing in, and after any run, so what it shows and plans is the tenant as it is now. It adds no write path: every change is a plan reviewed item by item, then a run that confirms again, exactly as from the command line. One tenant at a time. .EXAMPLE Start-NSPConditionalAccess .EXAMPLE Start-NSPConditionalAccess -Tenant 'contoso.onmicrosoft.com' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Interactive dashboard. It writes only through commands that confirm themselves.')] [CmdletBinding()] param( [string]$Tenant, [string]$ConfigRoot ) Import-NSPToolkitModule -Name 'NSP.Console' -MinimumVersion '0.1.1' -Reload Import-NSPM365Graph # The dashboard is laid out for a large window; maximize it (best effort, never fails). Set-NSPConsoleMaximized function Get-KnownTenant { $folder = Get-NSPConditionalAccessLocalPath -Kind AppRegistrations -ConfigRoot $ConfigRoot if (-not (Test-Path -LiteralPath $folder)) { return } Get-ChildItem -LiteralPath $folder -Filter '*.json' -File | ForEach-Object { $record = Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json [pscustomobject]@{ TenantId = $record.TenantId; TenantDomain = $record.TenantDomain } } } function Select-Tenant { # Returns a tenant ID, '<new>' to set one up, or $null to quit. param([string]$Wanted, [switch]$Always) $known = @(Get-KnownTenant) if ($Wanted) { $match = @($known | Where-Object { $_.TenantId -eq $Wanted -or $_.TenantDomain -eq $Wanted }) if ($match.Count -ne 1) { throw "No single registered tenant matches '$Wanted'. Start without -Tenant and press N to set it up." } return $match[0].TenantId } if ($known.Count -eq 1 -and -not $Always) { return $known[0].TenantId } Clear-NSPConsole Write-NSPConsoleHeader -Title 'NSP Conditional Access' -Subtitle 'choose a tenant' if (-not $known.Count) { Write-NSPConsoleLine ' No tenant is set up on this computer yet. N walks through it: register the tool''s app, sign in, take an inventory, and build the profile.' Write-NSPConsoleLine '' } # Domains padded to the longest, so the tenant IDs line up in a column. $domainWidth = (@($known | ForEach-Object { ([string]$_.TenantDomain).Length }) + 10 | Measure-Object -Maximum).Maximum + 6 $choices = @($known | Sort-Object TenantDomain | ForEach-Object { [pscustomobject]@{ Label = ([string]$_.TenantDomain).PadRight($domainWidth) + "Tenant ID: $($_.TenantId)"; Value = $_.TenantId } }) + [pscustomobject]@{ Key = 'N'; Label = 'New tenant (set the tool up in another client)'; Value = '<new>' } Read-NSPMenu -Prompt 'Tenant' -Choices $choices -CancelLabel 'Quit' } function Wait-Operator { Read-NSPConditionalAccessText -Prompt 'Press Enter to return' | Out-Null } function Invoke-Waiting { # Sign-ins and registration wait on the operator in a browser window or on a device code; # say so on screen and on the progress bar until they return. param([string]$Status, [scriptblock]$Work) Write-NSPConsoleLine " $Status" -Role Heading Write-NSPConditionalAccessProgress -Activity SignIn -Status $Status try { & $Work } finally { Write-NSPConditionalAccessProgress -Activity SignIn -Completed } } function Connect-Tenant { param([string]$TenantId) $target = $TenantId $deviceCode = Read-NSPConfirm -Prompt 'Use a device code instead of the sign-in window?' Invoke-Waiting -Status 'Signing in as the tool''s app: finish the sign-in in the browser window (or with the device code below).' -Work { Connect-NSPConditionalAccessGraph -Tenant $target -UseDeviceCode:$deviceCode -ConfigRoot $ConfigRoot | Out-Null } Write-NSPConsoleLine ' Signed in.' -Role Success $session.InventoryWanted = $true } function Register-Tenant { # Registration signs in through Microsoft Graph PowerShell's app (as an administrator who # can consent); afterwards the session must be the tool's own app again. $planned = Invoke-Waiting -Status 'Checking the app registration: sign in as an administrator in the browser window, then wait while Graph is read.' -Work { Register-NSPConditionalAccessAppRegistration -ConfigRoot $ConfigRoot } $planned | Format-List Status, TenantDomain, AppName, ClientId, Actions, Message | Out-Host if ($planned.Status -eq 'PlanOnly' -and (Read-NSPConfirm -Prompt 'Apply these actions?')) { $done = Invoke-Waiting -Status 'Registering the tool''s app (writing the app registration and its consent through Graph).' -Work { Register-NSPConditionalAccessAppRegistration -TenantId $planned.TenantId -Execute -ConfigRoot $ConfigRoot } Write-NSPConsoleLine " $($done.Status)." -Role Success try { Invoke-Waiting -Status 'Signing in again as the tool''s app: finish the sign-in in the browser window.' -Work { Connect-NSPConditionalAccessGraph -Tenant $planned.TenantId -ConfigRoot $ConfigRoot | Out-Null } Write-NSPConsoleLine ' Signed in.' -Role Success $session.InventoryWanted = $true } catch { Write-NSPConsoleLine " Sign-in as the tool's app did not complete: $_ Press C to sign in." -Role Warning } } if ($planned.TenantId) { $session.TenantId = $planned.TenantId; $session.Cache.Clear() } $planned } function Start-NewTenant { Write-NSPConditionalAccessTaskIntro -Session $session -Title 'Set up a new tenant' -Text @( 'Four steps, each confirmed:' '- Register the tool''s own app in the client''s tenant. You sign in as an administrator who can grant consent (Global Administrator).' '- Sign in again, as yourself, through that app. The tool always works through its own app, so its permissions are fixed and visible.' '- Take an inventory: a read-only snapshot of the tenant''s policies, locations, groups, and accounts.' '- Build the tenant profile: which accounts are breakglass and service accounts, which groups mean what, the home country.' '! Nothing in the tenant''s policies changes. Deploying comes after, from the main screen.' ) if (-not (Read-NSPConfirm -Prompt 'Start?' -DefaultYes)) { return } $planned = Register-Tenant if (-not $planned.TenantId) { return } if (-not (Invoke-NSPConditionalAccessDashboardInventory -Session $session)) { return } if ($session.State.ProfilePath) { Write-NSPConsoleLine ' This tenant already has a profile.' -Role Muted; return } if (Read-NSPConfirm -Prompt 'Build the tenant profile now?' -DefaultYes) { $built = New-NSPConditionalAccessTenantProfile -Inventory $session.State.Inventory.FullName -Interactive -ConfigRoot $ConfigRoot Write-NSPConsoleLine " Saved $($built.Path)." -Role Success Write-NSPConsoleLine ' Next: "Deploy policies from the templates", then "Bulk-add users to an MFA group" to put everyone in EnableMFA.' -Role Heading } } function Show-Connection { Write-NSPConditionalAccessTaskIntro -Session $session -Title 'Connection' -Text @( 'The tool works through its own app registration in each tenant. Register it once per tenant (or repair it after the tool needs a new permission); after that, sign in.' ) $connect = [pscustomobject]@{ Label = 'Sign in to this tenant as the tool''s app'; Value = 'Connect'; Help = 'Your usual sign-in; the tool''s permissions only.' } $register = [pscustomobject]@{ Label = $(if ($session.State.Registered) { 'Repair the tool''s app registration (plan first)' } else { 'Register the tool''s app in this tenant (first time)' }); Value = 'Register'; Help = 'Signs in as an administrator to create or fix the app, then signs in through it.' } $choices = if ($session.State.Registered) { @($connect, $register) } else { @($register, $connect) } $choices += [pscustomobject]@{ Label = 'Fix repeated sign-in prompts (sign in again in the browser)'; Value = 'Reconnect'; Help = 'For prompts that keep coming back, or "insufficient_claims" / continuous access evaluation errors. Turns off the Windows sign-in broker for Microsoft Graph PowerShell (your Windows account only, every Graph tool), signs out, and signs in again.' } $choices += [pscustomobject]@{ Label = 'Sign out'; Value = 'Disconnect' } $action = Read-NSPMenu -Prompt 'Connection' -CancelLabel 'Back' -Choices $choices switch ($action) { 'Connect' { if (-not $session.TenantId) { throw 'No tenant yet. Press N to set one up.' } Connect-Tenant -TenantId $session.TenantId } 'Register' { Register-Tenant | Out-Null } 'Reconnect' { if (-not $session.TenantId) { throw 'No tenant yet. Press N to set one up.' } # The Windows broker (WAM) can't renew a token silently for a client admin account # that isn't the Windows sign-in, so Graph prompts on every request. The browser can. $option = Get-Command Set-MgGraphOption -ErrorAction SilentlyContinue if ($option -and $option.Parameters.ContainsKey('DisableLoginByWAM')) { Set-MgGraphOption -DisableLoginByWAM $true Write-NSPConsoleLine ' Microsoft Graph PowerShell now signs in through the browser.' -Role Muted } elseif ($option -and $option.Parameters.ContainsKey('EnableLoginByWAM')) { Set-MgGraphOption -EnableLoginByWAM $false Write-NSPConsoleLine ' Microsoft Graph PowerShell now signs in through the browser.' -Role Muted } if (Get-Command Disconnect-MgGraph -ErrorAction SilentlyContinue) { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } Write-NSPConsoleLine ' Signed out. Sign in again with the account you use for this tenant.' -Role Muted Connect-Tenant -TenantId $session.TenantId Write-NSPConsoleLine ' If a run stopped on this, resume it: "Undo or resume a run".' -Role Muted } 'Disconnect' { if (Get-Command Disconnect-MgGraph -ErrorAction SilentlyContinue) { Disconnect-MgGraph | Out-Null }; Write-NSPConsoleLine ' Signed out.' -Role Muted } } [bool]$action } function Show-AllTools { while ($true) { Clear-NSPConsole Write-NSPConsoleHeader -Title 'All tools' -Subtitle ([string]$session.State.TenantDomain) Write-NSPConsoleLine ' Every step on its own, by category. The main screen''s tasks chain these together.' -Role Muted $category = Read-NSPMenu -Prompt 'Category' -CancelLabel 'Back' -Choices @($menu.Keys | ForEach-Object { [pscustomobject]@{ Label = $menu[$_].Title; Value = $_; Help = $menu[$_].Help } }) if ($null -eq $category) { return } $items = $menu[$category].Items while ($true) { Clear-NSPConsole Write-NSPConsoleHeader -Title "All tools: $($menu[$category].Title)" -Subtitle ([string]$session.State.TenantDomain) Write-NSPConsoleLine " $($menu[$category].Help)" -Role Muted $picked = Read-NSPMenu -Prompt $menu[$category].Title -CancelLabel 'Back' -Choices @($items | ForEach-Object { [pscustomobject]@{ Label = $_.Label; Value = $_.Id; Help = $_.Help } }) if ($null -eq $picked) { break } $item = @($items | Where-Object { $_.Id -eq $picked })[0] Write-NSPConsoleRule -Title $item.Label try { & $item.Action $session } catch { Write-NSPConsoleLine " $_" -Role Error } Wait-Operator $session.State = Get-NSPConditionalAccessDashboardState -TenantId $session.TenantId -ConfigRoot $ConfigRoot } } } $session = @{ TenantId = $null; ConfigRoot = $ConfigRoot; State = $null; Cache = @{}; InventoryWanted = $true; RunsMarker = $null } $tasks = @(Get-NSPConditionalAccessDashboardTask) $menu = Get-NSPConditionalAccessDashboardMenu $picked = Select-Tenant -Wanted $Tenant if ($null -eq $picked) { return } $session.State = Get-NSPConditionalAccessDashboardState -TenantId $null -ConfigRoot $ConfigRoot if ($picked -eq '<new>') { try { Start-NewTenant } catch { Write-NSPConsoleLine " $_" -Role Error; Wait-Operator } } else { $session.TenantId = $picked } while ($true) { $session.State = Get-NSPConditionalAccessDashboardState -TenantId $session.TenantId -ConfigRoot $ConfigRoot # Anything that ran against the tenant (a task, All tools, a resumed run) changes the run # journals; the next inventory then shows the tenant as it is now. if ($null -ne $session.RunsMarker -and $session.State.RunsMarker -ne $session.RunsMarker) { $session.InventoryWanted = $true } $session.RunsMarker = $session.State.RunsMarker if ($session.InventoryWanted -and $session.State.Connected) { Invoke-NSPConditionalAccessDashboardInventory -Session $session -Screen | Out-Null } Show-NSPConditionalAccessDashboard -Session $session $choices = @($tasks | ForEach-Object { [pscustomobject]@{ Label = $_.Label; Value = $_.Id } }) $choices[0] | Add-Member -NotePropertyName Section -NotePropertyValue 'What do you want to do?' $choices += @( [pscustomobject]@{ Key = 'N'; Label = 'New tenant'; Value = '<new>'; Section = 'Tenant and tool' } [pscustomobject]@{ Key = 'S'; Label = 'Switch tenant'; Value = '<switch>' } [pscustomobject]@{ Key = 'C'; Label = 'Connection'; Value = '<connection>' } [pscustomobject]@{ Key = 'I'; Label = 'Inventory now'; Value = '<inventory>' } [pscustomobject]@{ Key = 'H'; Label = 'How it fits together'; Value = '<guide>' } [pscustomobject]@{ Key = 'A'; Label = 'All tools'; Value = '<all>' } ) $choice = Read-NSPMenu -Prompt 'Choose' -Choices $choices -Inline -CancelLabel 'Quit' if ($null -eq $choice) { break } try { switch ($choice) { '<new>' { Start-NewTenant; Wait-Operator } '<switch>' { $next = Select-Tenant -Always if ($next -eq '<new>') { Start-NewTenant; Wait-Operator } elseif ($next) { $session.TenantId = $next; $session.Cache.Clear(); $session.InventoryWanted = $true; $session.RunsMarker = $null } } '<connection>' { if (Show-Connection) { Wait-Operator } } '<inventory>' { $session.InventoryWanted = $true; if (-not $session.State.Connected) { Write-NSPConsoleLine " Not signed in to this tenant as the tool's app. Press C first." -Role Warning; Wait-Operator } } '<guide>' { Show-NSPConditionalAccessGuide -Session $session } '<all>' { Show-AllTools } default { $task = @($tasks | Where-Object { $_.Id -eq $choice })[0] if (-not $task) { continue } & $task.Action $session Wait-Operator } } } catch { Write-NSPConsoleLine " $_" -Role Error Wait-Operator } } } |