Public/Set-NSPConditionalAccessPlanDecision.ps1

function Set-NSPConditionalAccessPlanDecision {
    <#
    .SYNOPSIS
        Records Approve or Skip on plan items. This is the only way an item becomes eligible to run.
    .DESCRIPTION
        -ItemId accepts exact IDs or wildcards. An exact ID for an item with nothing to decide
        (NoChange, BlockedPrerequisite, PendingPrerequisite, Conflict) is an error. A wildcard
        silently passes over such items.
 
        Decisions are stamped with the time and the Windows user, and the plan file is rewritten
        atomically (temp file, then replace), so an interrupted save cannot leave a half-written plan.
    .EXAMPLE
        Set-NSPConditionalAccessPlanDecision -Plan $plan.Path -ItemId 'group*' -Decision Approve
    .EXAMPLE
        Set-NSPConditionalAccessPlanDecision -Plan $plan.Path -ItemId 'NSP-BASE-MFA-01' -Decision Skip -Note 'Keep the portal-added session control for now.'
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Plan,
        [Parameter(Mandatory)][string[]]$ItemId,
        [Parameter(Mandatory)][ValidateSet('Approve', 'Skip')][string]$Decision,
        [string]$Note
    )

    $content = Get-Content -LiteralPath $Plan -Raw | ConvertFrom-Json
    if (-not $content.PlanId -or -not $content.Items) { throw "$Plan is not a deployment plan." }

    $decisionValue = if ($Decision -eq 'Approve') { 'Approved' } else { 'Skipped' }
    $changed = [System.Collections.Generic.List[object]]::new()
    foreach ($pattern in $ItemId) {
        $wildcard = [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($pattern)
        $matched = @($content.Items | Where-Object { $_.ItemId -like $pattern })
        if (-not $matched.Count) { throw "No plan item matches '$pattern'." }
        foreach ($item in $matched) {
            if ($item.Decision -eq 'NotActionable') {
                if ($wildcard) { continue }
                throw "$($item.ItemId) is $($item.Action); there is nothing to decide."
            }
            $item.Decision = $decisionValue
            $stamp = [pscustomobject]@{ At = (Get-Date).ToUniversalTime().ToString('o'); By = $env:USERNAME; Note = $Note }
            if ($item.PSObject.Properties['DecisionRecord']) { $item.DecisionRecord = $stamp } else { $item | Add-Member -NotePropertyName DecisionRecord -NotePropertyValue $stamp }
            $changed.Add($item)
        }
    }

    if ($changed.Count -and $PSCmdlet.ShouldProcess($Plan, "$Decision $($changed.Count) item(s): $(($changed | ForEach-Object ItemId) -join ', ')")) {
        $temp = "$Plan.tmp"
        ConvertTo-Json -InputObject $content -Depth 32 | Set-Content -LiteralPath $temp -Encoding UTF8
        Move-Item -LiteralPath $temp -Destination $Plan -Force
    }
    $changed | Select-Object ItemId, Action, Decision
}