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