Public/Invoke-NSPConditionalAccessDeploymentRun.ps1
|
function Invoke-NSPConditionalAccessDeploymentRun { <# .SYNOPSIS Performs a run journal's approved items against the tenant, one item and one stage at a time. .DESCRIPTION Without -Execute, lists what would run next and changes nothing. With -Execute: - the Graph session must be this tool's app, signed in to the run's tenant; - each item asks for confirmation (ConfirmImpact High) naming the tenant, action, and target. Declining pauses the run there; later items are never run out of order; - every stage transition is saved to the journal immediately; - the first failure stops the run with the item and stage named. Fix the cause and run again: it resumes at the failed stage; - items already Succeeded, Skipped, NeedsReview, or OperatorActionRequired are not repeated. Skipped means a re-check just before writing found nothing to do (for example, a user since disabled or covered). Run status afterwards: Completed, CompletedWithReview (Graph stored something other than planned; see the item Notes), OperatorActionRequired (an on-premises script to run), Paused, or Failed. .EXAMPLE Invoke-NSPConditionalAccessDeploymentRun -Run $run.Path .EXAMPLE Invoke-NSPConditionalAccessDeploymentRun -Run $run.Path -Execute #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( [Parameter(Mandatory)][string]$Run, [switch]$Execute, [string]$ConfigRoot ) $journal = Get-Content -LiteralPath $Run -Raw | ConvertFrom-Json if (-not $journal.RunId) { throw "$Run is not a run journal." } $journal.Path = (Resolve-Path -LiteralPath $Run).Path $done = @('Succeeded', 'Skipped', 'NeedsReview', 'OperatorActionRequired') if (-not $Execute) { return $journal.Items | Where-Object { $_.Result -notin $done } | ForEach-Object { [pscustomobject]@{ ItemId = $_.ItemId Action = $_.Action Target = $_.DisplayName NextStage = @($_.Stages | Where-Object Status -ne 'Succeeded')[0].Name Result = $_.Result } } } if ($script:UnattendedCoverage -and @($journal.Items | Where-Object Kind -ne 'Coverage').Count) { throw 'An unattended session runs coverage items only. This run contains other items.' } Import-NSPM365Graph $context = Assert-NSPConditionalAccessSession -ConfigRoot $ConfigRoot if ($context.TenantId -ne $journal.TenantId) { throw "The Graph session is for tenant $($context.TenantId), but this run is for $($journal.TenantId) ($($journal.TenantDomain))." } function Save-Journal { Save-NSPConditionalAccessJson -InputObject $journal -Path $journal.Path } function Get-Now { (Get-Date).ToUniversalTime().ToString('o') } $journal.Status = 'InProgress' Save-Journal $paused = $false $position = 0 $total = @($journal.Items).Count # Plain words for the progress bar; anything not listed shows its stage name. $stageWords = @{ VerifyTarget = 're-reading the tenant to check nothing changed since the plan' VerifyState = 're-reading the policy to check nothing changed since the plan' CreatePolicy = 'creating the policy (report-only)'; PatchPolicy = 'updating the policy'; PatchState = 'changing the policy state' ConfirmApplied = 'reading back what Graph stored'; ConfirmState = 'reading back the state' RecordOwnership = 'recording ownership'; BindProfile = 'recording it in the tenant profile' CreateGroup = 'creating the group'; CreateNamedLocation = 'creating the named location'; DeleteNamedLocation = 'deleting the named location' PatchNamedLocation = 'updating the named location'; VerifyTrustedLocation = 're-reading the location'; ConfirmTrustedLocation = 'reading back the location' VerifySecurityDefaults = 'reading Security Defaults'; PatchSecurityDefaults = 'changing Security Defaults'; ConfirmSecurityDefaults = 'reading back Security Defaults' AddMember = 'adding the user'; VerifyMember = 'checking the user'; DeletePolicy = 'deleting the policy'; WriteOnPremScript = 'writing the on-premises script' } foreach ($item in $journal.Items) { $position++ if ($item.Result -in $done) { continue } if (-not $PSCmdlet.ShouldProcess("tenant $($journal.TenantDomain) ($($journal.TenantId))", "$($item.Action): $($item.DisplayName)")) { $paused = $true break } foreach ($stage in $item.Stages) { if ($stage.Status -eq 'Succeeded') { continue } $words = if ($stageWords.ContainsKey($stage.Name)) { $stageWords[$stage.Name] } else { $stage.Name } Write-NSPConditionalAccessProgress -Activity Run -Status "$($item.ItemId): $words" -Step $position -Of $total $stage.Status = 'Running'; $stage.StartedAtUtc = Get-Now; $stage.Message = $null Save-Journal try { $message = Invoke-NSPConditionalAccessRunStage -Run $journal -Item $item -Stage $stage.Name -ConfigRoot $ConfigRoot $stage.Status = 'Succeeded'; $stage.Message = $message; $stage.FinishedAtUtc = Get-Now Save-Journal } catch { $stage.Status = 'Failed'; $stage.Message = "$_"; $stage.FinishedAtUtc = Get-Now $item.Result = 'Failed' $journal.Status = 'Failed' Save-Journal Write-NSPConditionalAccessProgress -Activity Run -Completed throw "Run stopped: $($item.ItemId) failed at $($stage.Name): $_ Fix the cause and run again to resume from this stage." } } if ($item.Result -notin 'Skipped', 'NeedsReview', 'OperatorActionRequired') { $item.Result = 'Succeeded' } Save-Journal } Write-NSPConditionalAccessProgress -Activity Run -Completed $journal.Status = if ($paused) { 'Paused' } elseif (@($journal.Items | Where-Object Result -eq 'NeedsReview').Count) { 'CompletedWithReview' } elseif (@($journal.Items | Where-Object Result -eq 'OperatorActionRequired').Count) { 'OperatorActionRequired' } else { 'Completed' } Save-Journal Get-NSPConditionalAccessRunSummary -Run $journal.Path } |