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