Private/Invoke-NSPConditionalAccessGuidedDeployment.ps1

function Invoke-NSPConditionalAccessGuidedDeployment {
    <#
    .SYNOPSIS
        Dashboard: one task's deployment from start to finish: plan, review only that task's items, run, re-inventory, repeat.
    .DESCRIPTION
        -Focus picks the task's items from a full deployment plan (a scriptblock given each item;
        $true keeps it). Anything those items depend on is added. Other items in the plan are left
        undecided.
 
        Some items wait for others: a policy that needs a new group is planned only once the group
        exists. So after a run, the dashboard takes a fresh inventory and plans again, and offers
        the items that have now become possible (up to -MaxPasses). It stops when nothing in focus
        is left to decide, the operator approves nothing, or it is not signed in to re-inventory.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]$Session,
        [Parameter(Mandatory)][scriptblock]$Focus,
        [string]$Title = 'Review the plan',
        [ValidateRange(1, 5)][int]$MaxPasses = 3,
        # Extra arguments for New-NSPConditionalAccessDeploymentPlan, such as @{ GroupSlot = 'MFAExempt' }.
        [hashtable]$PlanArguments = @{}
    )

    for ($pass = 1; $pass -le $MaxPasses; $pass++) {
        $state = $Session.State
        if (-not $state.Inventory) { throw 'No saved inventory yet. Sign in (C), and the dashboard takes one.' }
        if (-not $state.ProfilePath) { throw 'No tenant profile yet. Use All tools > Tenant profile > Build the profile, or N for a new tenant.' }
        Write-NSPConditionalAccessProgress -Activity Dashboard -Status 'Planning: comparing the templates with the inventory'
        $plan = New-NSPConditionalAccessDeploymentPlan -Inventory $state.Inventory.FullName -TenantProfile $state.ProfilePath -Save -ConfigRoot $Session.ConfigRoot @PlanArguments
        Write-NSPConditionalAccessProgress -Activity Dashboard -Completed

        $byId = @{}
        foreach ($item in $plan.Items) { $byId[$item.ItemId] = $item }
        $wanted = [System.Collections.Generic.HashSet[string]]::new()
        foreach ($item in $plan.Items) { if (& $Focus $item) { [void]$wanted.Add($item.ItemId) } }
        $queue = [System.Collections.Generic.Queue[string]]::new()
        foreach ($id in @($wanted)) { $queue.Enqueue($id) }
        while ($queue.Count) {
            $id = $queue.Dequeue()
            foreach ($dependency in @($byId[$id].DependsOn | Where-Object { $_ })) {
                if ($byId.ContainsKey($dependency) -and $wanted.Add($dependency)) { $queue.Enqueue($dependency) }
            }
        }
        $ids = @($plan.Items | Where-Object { $wanted.Contains($_.ItemId) } | ForEach-Object { $_.ItemId })
        if (-not $ids.Count) {
            Write-NSPConsoleLine ' This plan has nothing for this task.' -Role Muted
            return
        }
        $passTitle = if ($pass -gt 1) { "$Title (step $pass)" } else { $Title }
        $approved = Invoke-NSPConditionalAccessPlanReview -PlanPath $plan.Path -Focus $ids -Title $passTitle -TenantProfile $state.ProfilePath
        if (-not $approved) {
            Write-NSPConsoleLine ' Nothing approved, so nothing runs. The plan is saved; come back to it any time.' -Role Muted
            return
        }
        $waiting = @($plan.Items | Where-Object { $wanted.Contains($_.ItemId) -and $_.Action -eq 'PendingPrerequisite' })
        # A bound synced group changes the policies that use it, which only the next plan shows.
        $bound = @((Get-Content -LiteralPath $plan.Path -Raw | ConvertFrom-Json).Items | Where-Object { $_.Decision -eq 'Approved' -and $_.Action -eq 'BindSyncedGroup' })
        $ran = Invoke-NSPConditionalAccessDashboardRun -PlanPath $plan.Path -TenantProfile $state.ProfilePath -ConfigRoot $Session.ConfigRoot
        if (-not $ran) { return }
        $Session.InventoryWanted = $true
        if ((-not $waiting.Count -and -not $bound.Count) -or $pass -eq $MaxPasses) { return }

        Write-NSPConsoleLine ''
        if ($waiting.Count) { Write-NSPConsoleLine " $($waiting.Count) item(s) were waiting for what just ran: $(@($waiting | ForEach-Object { $_.ItemId }) -join ', ')." -Role Heading }
        if ($bound.Count) { Write-NSPConsoleLine " Bound $(@($bound | ForEach-Object { $_.DisplayName }) -join ', '). The policies that use it are updated in the next step." -Role Heading }
        if (-not (Read-NSPConfirm -Prompt 'Take a fresh inventory and plan them now?' -DefaultYes)) { return }
        if (-not (Invoke-NSPConditionalAccessDashboardInventory -Session $Session -Reason 'Re-reading the tenant for the next step')) { return }
    }
}