Private/Show-NSPConditionalAccessGuide.ps1

function Show-NSPConditionalAccessGuide {
    <#
    .SYNOPSIS
        Dashboard: lays out the pieces: how templates, the tenant profile, plans, and runs fit together, and a map of every policy.
    .DESCRIPTION
        For a tech who works with Conditional Access now and then. The overview says where each
        kind of change is made; the policy map shows each template by family, in plain words,
        with its status in this tenant and its file. -Page Map opens the map directly.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]$Session,
        [ValidateSet('Overview', 'Map')][string]$Page = 'Overview'
    )

    $families = [ordered]@{
        BASE = 'Baseline: what every client gets. MFA outside the office, sign-in only from the home country, office-only staff, how people set up MFA, no legacy or device-code sign-ins, MFA to add a device, and admin portals for admins only.'
        ADMIN = 'Administrators: MFA at every sign-in for anyone holding an admin role (phishing-resistant with the AdminMfa option), and shorter sessions.'
        GUEST = 'Guests and external users: MFA. Partner technicians (GDAP) are not included.'
        SVC = 'Service accounts: only from trusted locations, with the ServiceAccountLocations option.'
        RISK = 'Entra ID Protection (needs Entra ID P2), with the RiskPolicies option: MFA on a risky sign-in, a password change for a high-risk user.'
        INTUNE = 'Mobile and Intune: phones and tablets, and device enrollment. Only for clients that use Intune.'
        TRAVEL = 'Travel: generated per country or trip. Lets a travel group sign in from abroad, with MFA, while the baseline blocks everyone else.'
    }
    if ($Page -eq 'Overview') {
        Write-NSPConditionalAccessTaskIntro -Session $Session -Title 'How it fits together' -Text @(
            'Conditional Access policies decide, at each sign-in, who must do what (MFA, a compliant device) or who is blocked.'
            'This tool keeps a standard set of them per client. Four pieces:'
        )
        $pieces = @(
            ,@('1. Templates', 'Policies\*.psd1 in the module. One file per policy: who, which apps, where, and what it requires.', 'Same for every client. To change a policy for everyone, edit its file and raise its Version.')
            ,@('2. Tenant profile', 'Config\Local\TenantProfiles\<tenant>.json. Fills in each template for this client: breakglass and service accounts (always excluded), the groups, the home country, travel, options such as registration.', 'Changed through the tasks here (travel, registration, coverage) or by rebuilding the profile.')
            ,@('3. Plan and review', 'Compares the templates, filled in by the profile, with the tenant (the latest inventory) and lists what would change.', 'Nothing changes yet. You approve item by item.')
            ,@('4. Run', 'Does only what you approved. Re-reads the tenant first and stops if anything changed since the plan. Every run can be undone.', 'New policies start in report-only: they log what they would do and enforce nothing.')
        )
        foreach ($piece in $pieces) {
            Write-NSPConsoleLine " $($piece[0])" -Role Key
            Write-NSPConsoleLine " $($piece[1])"
            Write-NSPConsoleLine " $($piece[2])" -Role Muted
        }
        Write-NSPConsoleLine ''
        Write-NSPConsoleRule -Title 'A policy''s life'
        Write-NSPConsoleSegment -Segment @(' ', @('Not deployed', 'Muted'), ' -> deploy -> ', @('Report-only', 'Heading'), ' (about a week; read its sign-in results) -> turn on -> ', @('On', 'Success'))
        Write-NSPConsoleLine ' Security Defaults (Microsoft''s built-in MFA) must be off before any policy is On. The "Turn policies on" task handles that.' -Role Muted
        Write-NSPConsoleLine ''
        Write-NSPConsoleRule -Title 'Where to make a change'
        $where = @(
            ,@('Someone must or must not use MFA', 'Group membership (EnableMFA, RestrictExternal, MFACatchAll) in AD or Entra. No policy change.')
            ,@('Someone is travelling', 'Task: Add travel.')
            ,@('The office has a new public IP', 'Task: Update trusted locations.')
            ,@('How people set up MFA', 'Task: Protect MFA setup.')
            ,@('A user is in no MFA group', 'Task: Find users without MFA coverage.')
            ,@('Leave someone out of one policy, or bring them in', 'Task: Change who a policy applies to.')
            ,@('Admin MFA strength, service account locations, risk policies', 'All tools > Tenant profile > Tenant options.')
            ,@('A policy itself should behave differently', 'Edit its template (all clients), or ask before making a one-off.')
        )
        foreach ($row in $where) { Write-NSPConsoleSegment -Segment @(@((' {0,-44}' -f $row[0]), 'Strong'), $row[1]) }
        Write-NSPConsoleLine ''
        if (-not (Read-NSPConfirm -Prompt 'Show the policy map (every policy, in plain words)?' -DefaultYes)) { return }
    }

    $insight = Get-NSPConditionalAccessDashboardInsight -State $Session.State -Cache $Session.Cache -ConfigRoot $Session.ConfigRoot
    $status = @{}
    foreach ($row in @($insight.Policies)) { $status[$row.Id] = $row }
    $definitions = @(Get-NSPConditionalAccessDefinition)
    $selected = @{}
    foreach ($definition in $definitions) { $selected[$definition['Id']] = Test-NSPConditionalAccessDefinitionSelected -Definition $definition -TenantProfile $Session.State.Profile }

    foreach ($family in $families.Keys) {
        $members = @($definitions | Where-Object { $_['Id'] -like "NSP-$family-*" })
        # Travel policies are generated from the profile's travel entries, not shipped as files.
        $generated = @()
        if ($family -eq 'TRAVEL' -and $Session.State.Profile) { $generated = @(Get-NSPConditionalAccessTravelDefinition -TenantProfile $Session.State.Profile) }
        $known = @(@($definitions) + @($generated) | ForEach-Object { $_['Id'] })
        $travel = @($insight.Policies | Where-Object { $_.Family -eq $family -and $_.Id -notin $known })
        Clear-NSPConsole
        Write-NSPConsoleHeader -Title "Policy map: $family" -Subtitle ([string]$Session.State.TenantDomain)
        Write-NSPConsoleLine " $($families[$family])"
        Write-NSPConsoleLine ''
        foreach ($definition in $members) {
            $row = $status[$definition['Id']]
            $state = if (-not $selected[$definition['Id']]) { @('not used in this tenant (option)', 'Muted') } elseif ($row) { @($row.Status, $row.Role) } else { @('unknown (no inventory or profile)', 'Muted') }
            Write-NSPConsoleSegment -Segment @(@(" $($definition['Id']) ", 'Key'), @("$($definition['Name']) ", 'Strong'), @("[$($state[0])]", $state[1]))
            $summary = Get-NSPConditionalAccessDefinitionSummary -Definition $definition -TenantProfile $Session.State.Profile
            foreach ($key in $summary.Keys) { Write-NSPConsoleSegment -Segment @(@((' {0,-10}' -f $key), 'Muted'), [string]$summary[$key]) }
            Write-NSPConsoleLine " File Policies\$($definition['Id']).psd1 (version $($definition['Version']))" -Role Muted
        }
        if ($family -eq 'TRAVEL') {
            Write-NSPConsoleLine ' Each country or trip gets its own travel group and block policy. One session policy (NSP-TRAVEL-SESSION-01) covers every active travel group.' -Role Muted
            Write-NSPConsoleLine ''
        }
        foreach ($definition in $generated) {
            $row = $status[$definition['Id']]
            $state = if ($row) { @($row.Status, $row.Role) } else { @('unknown (no inventory)', 'Muted') }
            Write-NSPConsoleSegment -Segment @(@(" $($definition['Id']) ", 'Key'), @("$($definition['Name']) ", 'Strong'), @("[$($state[0])]", $state[1]))
            $summary = Get-NSPConditionalAccessDefinitionSummary -Definition $definition -TenantProfile $Session.State.Profile
            foreach ($key in $summary.Keys) { Write-NSPConsoleSegment -Segment @(@((' {0,-10}' -f $key), 'Muted'), [string]$summary[$key]) }
            Write-NSPConsoleLine ' Source a travel entry in the tenant profile (task: Add travel)' -Role Muted
        }
        foreach ($row in $travel) { Write-NSPConsoleSegment -Segment @(@(" $($row.Id) ", 'Key'), @("$($row.Name) ", 'Strong'), @("[$($row.Status)]", $row.Role)) }
        if ($family -eq 'TRAVEL' -and -not $travel.Count -and -not $generated.Count) { Write-NSPConsoleLine ' No travel entries in this tenant. Task: Add travel.' -Role Muted }
        Write-NSPConsoleLine ''
        $next = Read-NSPConditionalAccessText -Prompt 'Enter for the next family, Q to stop'
        if ($next -and $next -ieq 'Q') { return }
    }
}