Private/Invoke-NSPConditionalAccessPlanReview.ps1

function Invoke-NSPConditionalAccessPlanReview {
    <#
    .SYNOPSIS
        Dashboard: walks the operator through a saved plan's decisions, explaining each item in plain words.
    .DESCRIPTION
        Lists the decidable items with what each does. Picking one shows, for a policy, what the
        policy does (who, which apps, where, what it requires) from its definition, then the
        plan's reason, field differences, and warnings, and asks Approve, Skip, or Back. A
        approves every listed Pending item at once, after one confirmation. Decisions go through
        Set-NSPConditionalAccessPlanDecision, exactly as from the command line.
 
        -Focus limits the review to those item IDs (a task's own items); the rest of the plan stays
        as it is and is counted, not shown. Returns the number of approved items in the review.
    #>

    [CmdletBinding()]
    [OutputType([int])]
    param(
        [Parameter(Mandatory)][string]$PlanPath,
        [string[]]$Focus,
        [string]$Title = 'Review the plan',
        [string]$TenantProfile
    )

    $definitions = @{}
    foreach ($definition in Get-NSPConditionalAccessDefinition) { $definitions[$definition['Id']] = $definition }
    $actionWords = @{
        Create = 'create it (report-only)'; AdoptOrReview = 'take it over and align it'; UpdateInPlace = 'update it to the template'
        Drifted = 'put it back to the template'; CreateGroup = 'create the group'; CreateOnPremGroup = 'write a script to create the AD group'
        CreateNamedLocation = 'create the country location'; DeleteNamedLocation = 'delete an unused duplicate location'
        CreateAuthenticationStrength = 'create the authentication strength'; BindAuthenticationStrength = 'use the existing strength'
        BindSyncedGroup = 'use the synced AD group'; Retire = 'delete the retired travel policy'; PromoteState = 'change its state'
        DisableSecurityDefaults = 'turn Security Defaults off'; EnableSecurityDefaults = 'turn Security Defaults back on'
        CreateTrustedLocation = 'create the trusted location'; UpdateTrustedLocation = 'change the trusted ranges'
        UndoUpdateTrustedLocation = 'restore the earlier ranges'; UndoCreateTrustedLocation = 'delete the location'
        AddCloudMember = 'add to the group'; AddOnPremMember = 'add to the AD group'
    }
    # The profile names the tenant's groups in each policy's summary. Plans live under
    # <root>\.nsp-conditionalaccess\plans\<tenant>, so the profile can also be found from there.
    $profilePath = $TenantProfile

    while ($true) {
        $plan = Get-Content -LiteralPath $PlanPath -Raw | ConvertFrom-Json
        if (-not $profilePath -and $plan.TenantId) {
            $candidate = Join-Path (Get-NSPConditionalAccessLocalPath -Kind TenantProfiles -ConfigRoot (Split-Path (Split-Path (Split-Path (Split-Path $PlanPath)))) ) "$($plan.TenantId).json"
            if (Test-Path -LiteralPath $candidate) { $profilePath = $candidate }
        }
        $boundProfile = if ($profilePath) { Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json } else { $null }
        $shown = @($plan.Items | Where-Object { -not $Focus -or $_.ItemId -in $Focus })
        $decidable = @($shown | Where-Object Decision -ne 'NotActionable')
        $others = @($plan.Items | Where-Object { $Focus -and $_.ItemId -notin $Focus -and $_.Decision -ne 'NotActionable' }).Count

        Clear-NSPConsole
        Write-NSPConsoleHeader -Title $Title -Subtitle ([string]$plan.TenantDomain)
        Write-NSPConsoleLine ' Nothing has changed in the tenant yet. Approve what should happen; the next step runs only what you approved.' -Role Muted
        foreach ($item in @($shown | Where-Object Decision -eq 'NotActionable')) {
            $why = if ($item.Action -eq 'NoChange') { 'already as it should be' } elseif ($item.Action -eq 'PendingPrerequisite') { "waits for $(@($item.DependsOn) -join ', '); it comes in the next pass" } else { $item.Reason }
            Write-NSPConsoleSegment -Segment @(@(' - ', 'Muted'), @("$($item.ItemId) ", 'Muted'), @($why, 'Muted'))
        }
        if ($others) { Write-NSPConsoleLine " $others other item(s) in this plan belong to other tasks and are not shown; they stay undecided." -Role Muted }
        Write-NSPConsoleLine ''
        if (-not $decidable.Count) {
            Write-NSPConsoleLine ' Nothing here needs a decision.' -Role Success
            return 0
        }

        $choices = @($decidable | ForEach-Object {
                $item = $_
                $what = if ($actionWords.ContainsKey($item.Action)) { $actionWords[$item.Action] } else { $item.Action }
                $name = if ($item.DefinitionId -and $definitions.ContainsKey($item.DefinitionId)) { $definitions[$item.DefinitionId]['Name'] } else { $item.DisplayName }
                $flag = if (@($item.Warnings | Where-Object { $_ }).Count) { ' (!)' } else { '' }
                [pscustomobject]@{ Label = "[$($item.Decision)] $($item.ItemId): $what$flag"; Value = $item.ItemId; Help = $name }
            })
        $choices += [pscustomobject]@{ Key = 'A'; Label = 'Approve every Pending item listed'; Value = '<all>' }
        $picked = Read-NSPMenu -Prompt 'Pick an item to read and decide (Q when done)' -Choices $choices -CancelLabel 'Done reviewing'
        if ($null -eq $picked -or $picked -eq '<done>') { break }
        if ($picked -eq '<all>') {
            $pendingIds = @($decidable | Where-Object Decision -eq 'Pending' | ForEach-Object { $_.ItemId })
            if ($pendingIds.Count -and (Read-NSPConfirm -Prompt "Approve all $($pendingIds.Count) Pending item(s) without reading each one?")) {
                Set-NSPConditionalAccessPlanDecision -Plan $PlanPath -ItemId $pendingIds -Decision Approve -Confirm:$false | Out-Null
            }
            continue
        }

        $item = @($plan.Items | Where-Object ItemId -eq $picked)[0]
        Clear-NSPConsole
        Write-NSPConsoleHeader -Title "$($item.ItemId): $($item.Action)" -Subtitle ([string]$plan.TenantDomain)
        if ($item.DefinitionId -and $definitions.ContainsKey($item.DefinitionId)) {
            $definition = $definitions[$item.DefinitionId]
            Write-NSPConsoleLine " $($definition['Name'])" -Role Strong
            Write-NSPConsoleLine " $($definition['Description'])"
            $summary = Get-NSPConditionalAccessDefinitionSummary -Definition $definition -TenantProfile $boundProfile
            foreach ($key in $summary.Keys) { Write-NSPConsoleSegment -Segment @(@((' {0,-10}' -f $key), 'Muted'), [string]$summary[$key]) }
            Write-NSPConsoleLine ''
        }
        Write-NSPConsoleRule -Title 'What approving does'
        Write-NSPConsoleLine " $($item.Reason)"
        foreach ($difference in @($item.Differences)) {
            if ($difference) {
                Write-NSPConsoleSegment -Segment @(@(" $($difference.Field): ", 'Muted'), @((@($difference.Live) -join ', '), 'Error'), @(' -> ', 'Muted'), @((@($difference.Desired) -join ', '), 'Success'))
            }
        }
        foreach ($warning in @($item.Warnings)) { if ($warning) { Write-NSPConsoleLine " Warning: $warning" -Role Warning } }
        if ($item.Impact) { Write-NSPConsoleLine " $($item.Impact)" -Role Muted }
        if (@($item.DependsOn | Where-Object { $_ }).Count) { Write-NSPConsoleLine " Needs, approved in the same plan: $(@($item.DependsOn) -join ', ')" -Role Muted }
        Write-NSPConsoleLine ''
        $decision = Read-NSPMenu -Prompt 'Decision' -CancelLabel 'Back' -Choices @(
            [pscustomobject]@{ Key = 'A'; Label = 'Approve'; Value = 'Approve' }
            [pscustomobject]@{ Key = 'S'; Label = 'Skip (leave it as it is)'; Value = 'Skip' }
        )
        if ($decision -in 'Approve', 'Skip') {
            $note = if ($decision -eq 'Skip') { Read-NSPConditionalAccessText -Prompt 'Note (optional)' } else { $null }
            Set-NSPConditionalAccessPlanDecision -Plan $PlanPath -ItemId $item.ItemId -Decision $decision -Note $note -Confirm:$false | Out-Null
        }
    }
    @((Get-Content -LiteralPath $PlanPath -Raw | ConvertFrom-Json).Items | Where-Object { $_.Decision -eq 'Approved' -and (-not $Focus -or $_.ItemId -in $Focus) }).Count
}