Private/Get-NSPConditionalAccessDashboardMenu.ps1
|
function Get-NSPConditionalAccessDashboardMenu { <# .SYNOPSIS The dashboard's All tools menu: every action by category. Each action receives the dashboard session (TenantId, ConfigRoot, State, Cache) and only calls the module's public commands, so the dashboard adds no write path of its own. .DESCRIPTION The main screen's tasks (Get-NSPConditionalAccessDashboardTask) cover the usual goals from start to finish; these are the individual steps, for when a tech wants one of them alone. Each item has a Help line shown under it. Legacy V2 menu intents and where they went: 2 Connect to a different tenant -> S (switch) and C (connection) 3/4 Search strings, group/breakglass -> Tenant profile > Build or rebuild the profile 5 Add/remove named locations -> tasks Add travel and Update trusted locations 6 Inspect current rules (detailed) -> Discover > Inspect a live policy 7 Add rules from the predefined list -> task Deploy policies from the templates 9/10 Export / import -> the saved inventory and tenant profile files #> [CmdletBinding()] param() [ordered]@{ '1' = @{ Title = 'Discover' Help = 'Read-only: inventory, preflight, and looking at live policies.' Items = @( @{ Id = 'Inventory'; Label = 'Take an inventory'; Help = 'A read-only snapshot of the tenant, saved locally. The dashboard also takes one on its own after changes.'; Action = { param($s) if (-not (Invoke-NSPConditionalAccessDashboardInventory -Session $s)) { return } $inventory = Get-Content -LiteralPath $s.State.Inventory.FullName -Raw | ConvertFrom-Json $mismatch = @($inventory.RoleTableCheck | Where-Object Status -ne 'Match') if ($mismatch.Count) { $mismatch | Format-Table -Wrap | Out-Host } } } @{ Id = 'Preflight'; Label = 'Preflight: lockout and breakglass checks'; Help = 'The raw findings table. The Check tenant health task explains each one.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -Inventory -TenantProfile Test-NSPConditionalAccessTenant -Inventory $s.State.Inventory.FullName -TenantProfile $s.State.ProfilePath | Format-Table Severity, Rule, Subject, Message -Wrap | Out-Host } } @{ Id = 'PolicyMap'; Label = 'Policy map: every template in plain words'; Help = 'Who, which apps, where, what it requires, and its status here.'; Action = { param($s) Show-NSPConditionalAccessGuide -Session $s -Page Map } } @{ Id = 'InspectPolicy'; Label = 'Inspect a live policy in detail'; Help = 'Any policy in the tenant, field by field, from the latest inventory.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -Inventory $inventory = Get-Content -LiteralPath $s.State.Inventory.FullName -Raw | ConvertFrom-Json $choices = @($inventory.Policies | Sort-Object displayName | ForEach-Object { [pscustomobject]@{ Label = "$($_.displayName) [$($_.state)]"; Value = $_ } }) if (-not $choices.Count) { Write-NSPConsoleLine ' The inventory has no policies.' -Role Muted; return } $policy = Read-NSPMenu -Prompt 'Policy' -CancelLabel 'Back' -Choices $choices if ($null -eq $policy) { return } $canonical = ConvertTo-NSPConditionalAccessCanonicalPolicy -Policy $policy Write-NSPConsoleLine " $($policy.displayName) [$($policy.state)] $($policy.id)" -Role Heading foreach ($key in $canonical.Keys) { Write-NSPConsoleSegment -Segment @(@((' {0,-40}' -f $key), 'Muted'), (@($canonical[$key]) -join ', ')) } } } @{ Id = 'BackendParity'; Label = 'Check that REST and the Graph cmdlets see the same policies'; Help = 'A diagnostic for the tool itself.'; Action = { param($s) $rows = @(Test-NSPConditionalAccessBackendParity -ConfigRoot $s.ConfigRoot | Where-Object { -not $_.Match }) if ($rows.Count) { $rows | Format-Table -Wrap | Out-Host } else { Write-NSPConsoleLine ' Both backends agree on every policy.' -Role Success } } } ) } '2' = @{ Title = 'Tenant profile' Help = 'How the templates are filled in for this client: accounts, groups, country, options.' Items = @( @{ Id = 'BuildProfile'; Label = 'Build or rebuild the profile'; Help = 'Asks for breakglass and service accounts, the groups, and the country locations. Travel and settings are kept.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -Inventory $force = $false if ($s.State.ProfilePath) { if (-not (Read-NSPConfirm -Prompt 'A profile exists. Replace it with a new one built from the latest inventory? (travel, options, and coverage settings are kept)')) { return } $force = $true } $built = New-NSPConditionalAccessTenantProfile -Inventory $s.State.Inventory.FullName -Interactive -Force:$force -ConfigRoot $s.ConfigRoot Write-NSPConsoleLine " Saved $($built.Path)." -Role Success } } @{ Id = 'ShowProfile'; Label = 'Show the profile bindings'; Help = 'What each placeholder in the templates means in this tenant.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile $p = $s.State.Profile $purposes = @{} foreach ($slot in Get-NSPConditionalAccessGroupSlot) { $purposes[$slot.Slot] = $slot.Purpose } Write-NSPConsoleSegment -Segment @(@(' Breakglass ', 'Muted'), (@($p.BreakGlass.Labels) -join ', ')) Write-NSPConsoleSegment -Segment @(@(' Service accounts ', 'Muted'), (@($p.ServiceAccounts.Labels) -join ', ')) foreach ($slot in $p.Groups.PSObject.Properties) { $value = if ($slot.Value.Missing) { @("missing (planned: $($slot.Value.DefaultName))", 'Warning') } else { @((@($slot.Value.Labels) -join ', '), 'Strong') } Write-NSPConsoleSegment -Segment @(@((' {0,-18}' -f $slot.Name), 'Key'), $value) if ($purposes.ContainsKey($slot.Name)) { Write-NSPConsoleLine " $($purposes[$slot.Name])" -Role Muted } } Write-NSPConsoleSegment -Segment @(@(' Home country ', 'Muted'), [string]$p.HomeCountry, @(" hybrid: $($p.Hybrid)", 'Muted')) $registration = if ($p.PSObject.Properties['Options'] -and $p.Options.Registration) { $p.Options.Registration } else { "$((Get-NSPConditionalAccessOption).Registration.Default) (default)" } Write-NSPConsoleSegment -Segment @(@(' MFA setup ', 'Muted'), $registration) } } @{ Id = 'Registration'; Label = 'Choose how MFA setup is protected'; Help = 'The same choice as the Protect MFA setup task, without planning.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile $mode = Read-NSPMenu -Prompt 'Registration' -CancelLabel 'Back' -Choices @( [pscustomobject]@{ Label = 'TrustedLocations'; Value = 'TrustedLocations'; Help = 'Registration blocked outside trusted locations (NSP-BASE-REG-01).' } [pscustomobject]@{ Label = 'TapOutsideTrusted'; Value = 'TapOutsideTrusted'; Help = 'A Temporary Access Pass to register outside trusted locations (NSP-BASE-REG-02).' } [pscustomobject]@{ Label = 'TapEverywhere'; Value = 'TapEverywhere'; Help = 'A Temporary Access Pass to register anywhere; no static office IP (NSP-BASE-REG-03).' } ) if (-not $mode) { return } Set-NSPConditionalAccessTenantOption -TenantProfile $s.State.ProfilePath -Registration $mode -Confirm:$false | Out-Null Write-NSPConsoleLine " Registration = $mode. Next: Deploy policies plans the matching policy." -Role Success } } @{ Id = 'TenantOptions'; Label = 'Tenant options: admin MFA, service accounts, risk policies'; Help = 'Choose between policy variants for this tenant. Deploy policies then plans what the choice selects.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile $table = Get-NSPConditionalAccessOption $options = Get-NSPConditionalAccessMapValue (Get-Content -LiteralPath $s.State.ProfilePath -Raw | ConvertFrom-Json) 'Options' $names = @($table.Keys | Where-Object { $_ -ne 'Registration' } | Sort-Object) $name = Read-NSPMenu -Prompt 'Which option?' -CancelLabel 'Back' -Choices @($names | ForEach-Object { $chosen = Get-NSPConditionalAccessMapValue $options $_ if (-not $chosen) { $chosen = "$($table[$_].Default) (default)" } [pscustomobject]@{ Label = "$_ = $chosen"; Value = $_; Help = $table[$_].Help } }) if (-not $name) { return } $value = Read-NSPMenu -Prompt $name -CancelLabel 'Back' -Choices @($table[$name].Values | ForEach-Object { $default = if ($_ -eq $table[$name].Default) { ' (default)' } else { '' } [pscustomobject]@{ Label = "$_$default"; Value = $_; Help = $table[$name].ValueHelp[$_] } }) if (-not $value) { return } $setting = @{ TenantProfile = $s.State.ProfilePath; Confirm = $false } $setting[$name] = $value Set-NSPConditionalAccessTenantOption @setting | Out-Null Write-NSPConsoleLine " $name = $value. Next: Deploy policies plans what it selects. A policy it no longer selects shows as Replaced; turn it off once its replacement is On." -Role Success } } ) } '3' = @{ Title = 'Deploy' Help = 'The full deployment plan: every policy, prerequisite, and cleanup item at once.' Items = @( @{ Id = 'Plan'; Label = 'Plan, review, and run everything'; Help = 'All items in one plan, including cleanup of duplicate locations.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -Inventory -TenantProfile $plan = New-NSPConditionalAccessDeploymentPlan -Inventory $s.State.Inventory.FullName -TenantProfile $s.State.ProfilePath -Save -ConfigRoot $s.ConfigRoot if ((Invoke-NSPConditionalAccessPlanReview -PlanPath $plan.Path -TenantProfile $s.State.ProfilePath) -and (Read-NSPConfirm -Prompt 'Run the approved items now?')) { Invoke-NSPConditionalAccessDashboardRun -PlanPath $plan.Path -ConfigRoot $s.ConfigRoot | Out-Null } } } @{ Id = 'Runs'; Label = 'Show run journals, and resume one'; Help = 'The last 15 runs and their status.'; Action = { param($s) if (-not @($s.State.Runs).Count) { Write-NSPConsoleLine ' No runs yet.' -Role Muted; return } $s.State.Runs | Select-Object -First 15 | Format-Table RunId, Status, Items, Updated | Out-Host $open = @($s.State.RunsNeedingAttention | Where-Object Status -in 'Failed', 'Paused', 'InProgress', 'Pending') if (-not $open.Count) { return } $run = Read-NSPMenu -Prompt 'Resume a run' -CancelLabel 'Back' -Choices @($open | ForEach-Object { [pscustomobject]@{ Label = "$($_.RunId) ($($_.Status))"; Value = $_.Path } }) if ($run) { Invoke-NSPConditionalAccessDashboardRun -RunPath $run -ConfigRoot $s.ConfigRoot | Out-Null } } } @{ Id = 'Undo'; Label = 'Undo a run'; Help = 'Reverses a run item by item, reviewed like any plan.'; Action = { param($s) if (-not @($s.State.Runs).Count) { Write-NSPConsoleLine ' No runs yet.' -Role Muted; return } $run = Read-NSPMenu -Prompt 'Run to undo' -CancelLabel 'Back' -Choices @($s.State.Runs | Select-Object -First 15 | ForEach-Object { [pscustomobject]@{ Label = "$($_.RunId) ($($_.Status), $($_.Items) item(s))"; Value = $_.Path } }) if (-not $run) { return } $plan = New-NSPConditionalAccessUndoPlan -Run $run -Save -ConfigRoot $s.ConfigRoot if ((Invoke-NSPConditionalAccessPlanReview -PlanPath $plan.Path -Title 'Undo' -TenantProfile $s.State.ProfilePath) -and (Read-NSPConfirm -Prompt 'Run the approved undo items now?')) { Invoke-NSPConditionalAccessDashboardRun -PlanPath $plan.Path -ConfigRoot $s.ConfigRoot | Out-Null } } } ) } '4' = @{ Title = 'Enforce' Help = 'State changes only: report-only to On, back again, or off.' Items = @( @{ Id = 'Impact'; Label = 'Read report-only sign-in results'; Help = 'Who each report-only policy would have blocked or prompted. Needs Entra ID P1.'; Action = { param($s) $days = [int](Read-NSPConditionalAccessText -Prompt 'Days of sign-ins to read' -Default '7') $impact = Get-NSPConditionalAccessReportOnlyImpact -Days $days -Save -ConfigRoot $s.ConfigRoot if (-not $impact.Available) { Write-NSPConsoleLine " $($impact.Reason)" -Role Warning; return } $impact.Policies | Format-Table DisplayName, WouldBlock, WouldInterrupt, Satisfied, UsersAffected -Wrap | Out-Host if ($impact.Truncated) { Write-NSPConsoleLine " Stopped after $($impact.SignInsRead) sign-ins; counts are partial." -Role Warning } } } @{ Id = 'Promote'; Label = 'Plan and run promotions to On'; Help = 'The same as the Turn policies on task, without reading sign-ins first.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -Inventory -TenantProfile $promoteArgs = @{ Inventory = $s.State.Inventory.FullName; TenantProfile = $s.State.ProfilePath; Save = $true; ConfigRoot = $s.ConfigRoot } if ($s.State.Impact) { $promoteArgs.Impact = $s.State.Impact.FullName } else { Write-NSPConsoleLine ' No sign-in results saved; the plan will warn. See Read report-only sign-in results.' -Role Warning } $plan = New-NSPConditionalAccessPromotionPlan @promoteArgs if ((Invoke-NSPConditionalAccessPlanReview -PlanPath $plan.Path -Title 'Turn policies on' -TenantProfile $s.State.ProfilePath) -and (Read-NSPConfirm -Prompt 'Run the approved promotions now?')) { Invoke-NSPConditionalAccessDashboardRun -PlanPath $plan.Path -ConfigRoot $s.ConfigRoot | Out-Null } } } @{ Id = 'Rollback'; Label = 'Roll back from On to report-only'; Help = 'Stops enforcing without deleting anything. No gates.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -Inventory -TenantProfile $plan = New-NSPConditionalAccessPromotionPlan -Inventory $s.State.Inventory.FullName -TenantProfile $s.State.ProfilePath -TargetState ReportOnly -Save -ConfigRoot $s.ConfigRoot if ((Invoke-NSPConditionalAccessPlanReview -PlanPath $plan.Path -Title 'Roll back to report-only' -TenantProfile $s.State.ProfilePath) -and (Read-NSPConfirm -Prompt 'Run the approved rollbacks now?')) { Invoke-NSPConditionalAccessDashboardRun -PlanPath $plan.Path -ConfigRoot $s.ConfigRoot | Out-Null } } } @{ Id = 'TurnOff'; Label = 'Turn off a replaced policy'; Help = 'For a policy another option replaced (shown as "Replaced"), once its replacement is On.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -Inventory -TenantProfile $plan = New-NSPConditionalAccessPromotionPlan -Inventory $s.State.Inventory.FullName -TenantProfile $s.State.ProfilePath -TargetState Disabled -Save -ConfigRoot $s.ConfigRoot if ((Invoke-NSPConditionalAccessPlanReview -PlanPath $plan.Path -Title 'Turn policies off' -TenantProfile $s.State.ProfilePath) -and (Read-NSPConfirm -Prompt 'Turn the approved policies off now?')) { Invoke-NSPConditionalAccessDashboardRun -PlanPath $plan.Path -ConfigRoot $s.ConfigRoot | Out-Null } } } ) } '5' = @{ Title = 'Travel' Help = 'Travel entries one step at a time. The Add travel task does all of it.' Items = @( @{ Id = 'ListTravel'; Label = 'List travel entries'; Help = 'Countries and trips, with their groups and status.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile $entries = @(Get-NSPConditionalAccessTravel -TenantProfile $s.State.ProfilePath) if ($entries.Count) { $entries | Format-Table -Wrap | Out-Host } else { Write-NSPConsoleLine ' No travel entries.' -Role Muted } } } @{ Id = 'TravelOverlap'; Label = 'Check for travelers in more than one travel group'; Help = 'Someone in two groups gets the looser of the two.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile $rows = @(Test-NSPConditionalAccessTravelOverlap -TenantProfile $s.State.ProfilePath -ConfigRoot $s.ConfigRoot) if ($rows.Count) { $rows | Format-Table UserPrincipalName, Entries, Message -Wrap | Out-Host } else { Write-NSPConsoleLine ' No traveler is in more than one travel group.' -Role Success } } } @{ Id = 'ImportTravel'; Label = 'Import existing per-country travel policies'; Help = 'Records them so the plan adopts them instead of creating new ones.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -Inventory -TenantProfile # A -WhatIf pass shows what would be recorded; nothing is saved until confirmed. $wanted = @(Import-NSPConditionalAccessTravel -Inventory $s.State.Inventory.FullName -TenantProfile $s.State.ProfilePath -WhatIf) if (-not $wanted.Count) { Write-NSPConsoleLine ' No travel policies found to import.' -Role Muted; return } $wanted | Format-Table Result, Policy, Key, Group, Notes -Wrap | Out-Host if (@($wanted | Where-Object Result -eq 'Imported').Count -and (Read-NSPConfirm -Prompt 'Record the imported entries in the tenant profile?')) { Import-NSPConditionalAccessTravel -Inventory $s.State.Inventory.FullName -TenantProfile $s.State.ProfilePath -Confirm:$false | Out-Null Write-NSPConsoleLine ' Recorded. Next: Add travel or Deploy plans adopting them.' -Role Success } } } @{ Id = 'AddCountry'; Label = 'Allow a country (record only)'; Help = 'Records a standing country entry; plan it with Deploy.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile $iso = @(Read-NSPConditionalAccessCountry -Prompt 'Country code') if ($iso.Count) { Add-NSPConditionalAccessTravel -TenantProfile $s.State.ProfilePath -Country $iso[0] -Confirm:$false | Format-Table Key, PolicyId, DefaultName | Out-Host } } } @{ Id = 'RetireTravel'; Label = 'Retire a travel entry early (record only)'; Help = 'Marks it for retirement; plan it twice with Deploy.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile $entries = @(Get-NSPConditionalAccessTravel -TenantProfile $s.State.ProfilePath | Where-Object Status -eq 'Active') if (-not $entries.Count) { Write-NSPConsoleLine ' No active travel entries.' -Role Muted; return } $key = Read-NSPMenu -Prompt 'Entry to retire' -CancelLabel 'Back' -Choices @($entries | ForEach-Object { [pscustomobject]@{ Label = "$($_.Key) $($_.Name) ($($_.Countries))"; Value = $_.Key } }) if ($key) { Remove-NSPConditionalAccessTravel -TenantProfile $s.State.ProfilePath -Key $key -Confirm:$false | Out-Null } Write-NSPConsoleLine ' Retirement requested. Plan and run twice: GEO-01 first, then the deletion.' -Role Muted } } ) } '6' = @{ Title = 'Coverage (MFACatchAll)' Help = 'Scheduled coverage: the unattended job that adds uncovered users, with rails.' Items = @( @{ Id = 'ScheduledStatus'; Label = 'Scheduled coverage: status and last run'; Help = 'Its settings, and what the last job did.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile (Get-NSPConditionalAccessCoverageSetting -TenantProfile $s.State.Profile) | Format-List | Out-Host $job = $s.State.LastCoverageJob if ($job) { Write-NSPConsoleLine " Last run $($job.FinishedAtUtc): $($job.Status). $(@($job.Messages) -join ' ') $(@($job.Warnings) -join ' ')" } } } @{ Id = 'InstallScheduled'; Label = 'Set up scheduled coverage'; Help = 'Its own app and certificate, then a scheduled task. Each step is planned first.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile $app = Register-NSPConditionalAccessCoverageApp -TenantId $s.TenantId -ConfigRoot $s.ConfigRoot $app | Format-List Status, AppName, Actions, CertificateThumbprint, CertificateNotAfter | Out-Host if ($app.Status -eq 'PlanOnly' -and (Read-NSPConfirm -Prompt 'Apply the coverage app registration?')) { Register-NSPConditionalAccessCoverageApp -TenantId $s.TenantId -ConfigRoot $s.ConfigRoot -Execute | Format-List Status | Out-Host } $task = Install-NSPConditionalAccessCoverageTask -TenantProfile $s.State.ProfilePath -ConfigRoot $s.ConfigRoot $task | Format-List Status, TaskName, RunAs, Sides, MaxAdds, FirstRun, EveryHours, Problems, Notes | Out-Host if ($task.Status -eq 'PlanOnly' -and (Read-NSPConfirm -Prompt 'Install the task and enable scheduled coverage?')) { Install-NSPConditionalAccessCoverageTask -TenantProfile $s.State.ProfilePath -ConfigRoot $s.ConfigRoot -Execute | Format-List Status | Out-Host } } } @{ Id = 'RepairCoverageApp'; Label = 'Repair or renew the coverage app'; Help = 'Adds permissions a newer version needs, and renews the certificate within 30 days of expiry. Run it on the job host, elevated.'; Action = { param($s) $recordPath = Join-Path (Get-NSPConditionalAccessLocalPath -Kind CoverageApps -ConfigRoot $s.ConfigRoot) "$($s.TenantId).json" if (-not (Test-Path -LiteralPath $recordPath)) { Write-NSPConsoleLine ' No coverage app is recorded on this computer. Repair it on the host that runs the scheduled job, or use "Set up scheduled coverage".' -Role Warning return } # The registration signs in as an administrator through Microsoft Graph # PowerShell's app; the tool's own session is restored afterwards. Write-NSPConsoleLine ' Checking the coverage app: sign in as an administrator in the browser window, then wait while Graph is read.' -Role Heading $app = Register-NSPConditionalAccessCoverageApp -TenantId $s.TenantId -ConfigRoot $s.ConfigRoot try { if ($app.Status -eq 'AlreadyRegistered') { Write-NSPConsoleLine " Nothing to do: every permission is granted, and the certificate is good until $($app.CertificateNotAfter)." -Role Success return } if ($app.Status -ne 'PlanOnly') { Write-NSPConsoleLine " $($app.Status): $($app.Message)" -Role Warning; return } Write-NSPConsoleLine ' To do:' -Role Heading foreach ($action in @($app.Actions)) { $text = switch ($action) { 'CreateCertificate' { 'Create a new certificate in this computer''s store (the old one keeps working until it expires)' } 'UploadCertificates' { 'Upload the certificate(s) to the app' } 'UpdateRequiredPermissions' { 'Add the missing permission(s) to the app' } 'AssignAppRoles' { "Grant admin consent: $(@($app.MissingPermissions) -join ', ')" } 'CreateServicePrincipal' { 'Recreate the app''s service principal' } default { $action } } Write-NSPConsoleLine " - $text" } $elevated = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $elevated -and @($app.Actions) -contains 'CreateCertificate') { Write-NSPConsoleLine ' ! A new certificate goes into the LocalMachine store, which needs an elevated session. Restart PowerShell as administrator first.' -Role Warning return } if (Read-NSPConfirm -Prompt 'Apply these changes to the coverage app?') { $done = Register-NSPConditionalAccessCoverageApp -TenantId $s.TenantId -ConfigRoot $s.ConfigRoot -Execute Write-NSPConsoleLine " $($done.Status). Certificate good until $($done.CertificateNotAfter)." -Role Success } } finally { try { Write-NSPConsoleLine ' Signing in again as the tool''s app: finish the sign-in in the browser window.' -Role Heading Connect-NSPConditionalAccessGraph -Tenant $s.TenantId -ConfigRoot $s.ConfigRoot | Out-Null } catch { Write-NSPConsoleLine " Sign-in as the tool's app did not complete: $_ Press C to sign in." -Role Warning } } } } @{ Id = 'MailSetup'; Label = 'E-mail scheduled reports'; Help = 'A scoped Exchange setup (one mailbox only), then the recipients.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile $from = Read-NSPConditionalAccessText -Prompt 'Sender mailbox (blank to turn e-mail off)' if (-not $from) { Set-NSPConditionalAccessCoverageSetting -TenantProfile $s.State.ProfilePath -MailFrom '' -Confirm:$false | Out-Null; Write-NSPConsoleLine ' Report e-mail is off.' -Role Muted; return } $setup = Get-NSPConditionalAccessCoverageMailSetup -TenantProfile $s.State.Profile -From $from -ConfigRoot $s.ConfigRoot Write-NSPConsoleLine ' An Exchange administrator runs these once (the app can then send only as this mailbox):' -Role Heading foreach ($command in $setup.Commands) { Write-NSPConsoleLine " $command" } $to = @((Read-NSPConditionalAccessText -Prompt 'Recipients, comma separated') -split '[,;\s]+' | Where-Object { $_ }) $when = Read-NSPMenu -Prompt 'Send when' -CancelLabel 'Back' -Choices @( [pscustomobject]@{ Label = 'Problems only (held, failed, operator action, warnings)'; Value = 'Problems' } [pscustomobject]@{ Label = 'Problems and any users added'; Value = 'Changes' } [pscustomobject]@{ Label = 'Every run'; Value = 'Always' } ) if ($to.Count -and $when) { Set-NSPConditionalAccessCoverageSetting -TenantProfile $s.State.ProfilePath -MailFrom $from -MailTo $to -MailWhen $when -Confirm:$false | Out-Null } } } @{ Id = 'ToggleScheduled'; Label = 'Switch scheduled coverage on or off'; Help = 'The task stays installed; the job checks this switch.'; Action = { param($s) Assert-NSPConditionalAccessDashboardReady $s -TenantProfile $enabled = [bool](Get-NSPConditionalAccessCoverageSetting -TenantProfile $s.State.Profile).Scheduled.Enabled if (Read-NSPConfirm -Prompt "Scheduled coverage is $(if ($enabled) { 'ON' } else { 'OFF' }). Switch it $(if ($enabled) { 'off' } else { 'on' })?") { Set-NSPConditionalAccessCoverageSetting -TenantProfile $s.State.ProfilePath -ScheduledEnabled (-not $enabled) -Confirm:$false | Out-Null } } } ) } } } |