Public/Invoke-TcRemediation.ps1

function Invoke-TcRemediation {
    <#
    .SYNOPSIS
    Runs one remediation script: a preview by default, a write only with -Apply.
 
    .DESCRIPTION
    UNTESTED: this script has never been run on a real tenant. Read it, try it on a test tenant first, and use it at your own risk.
 
    A preview connects to nothing and writes nothing to a tenant. It reads the snapshot and the plan, and
    keeps <output>\decisions\<id>.csv up to date with what a person has to settle. A script acts only on the
    rows of that file that carry a decision. With no file, or an empty one, it changes nothing and says so.
 
    With -Apply, the script first asks for the word UNTESTED, which -AcknowledgeUntested replaces. It then
    shows its plan: some scripts sign a person in at this step, to read what the plan needs, and write
    nothing. Before its first write, it asks for the primary domain of each tenant it touches, which
    nothing replaces, and, when it removes objects, for the word REMOVE, which nothing replaces either.
    Any other answer stops it, with nothing changed. Every sign-in carries only the permissions of the
    script. The read-only application of the assessment is never used. Each action is tested on the
    tenant just before it runs and written to operations.jsonl, so Undo-TcRemediation can go back.
 
    Each run has its own folder, <output>\remediations\<date>-<id>-<mode>.
 
    Start-TenantConverge never runs a remediation.
 
    .PARAMETER Id
    The identifier, from Get-TcRemediation.
 
    .PARAMETER Project
    The project file, or a project as read from it.
 
    .PARAMETER Snapshot
    The snapshot, as a path or as read from snapshot.json. Without it, the latest of the output folder.
 
    .PARAMETER Plan
    The result of New-TcPlan on this snapshot. Without it, the plan is computed.
 
    .PARAMETER Tenant
    Limits the run to these roles: Target, S1, S2 or S3.
 
    .PARAMETER Apply
    Writes to the tenants, after the confirmations. Without it, nothing is written.
 
    .PARAMETER AcknowledgeUntested
    Replaces the word UNTESTED, for a run from a script. It replaces nothing else.
 
    .PARAMETER Setting
    Replaces remediation settings for this run, such as @{ ObjectPrefix = 'Contoso' }. A file
    <output>\decisions\settings.json holds the settings that stay from one run to the next.
 
    .PARAMETER OutputPath
    The output folder. Without it, the one of the project.
 
    .EXAMPLE
    Invoke-TcRemediation -Id ENT-MFA -Project ./project.json
 
    Shows what ENT-MFA would do, and writes the proposals to the decisions file.
 
    .EXAMPLE
    Invoke-TcRemediation -Id ENT-MFA -Project ./project.json -Tenant Target -Apply
 
    Applies what was decided in the decisions file, after the confirmations.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory, Position = 0)][string]$Id,
        [Parameter(Mandatory)][object]$Project,
        [object]$Snapshot,
        [System.Collections.IDictionary]$Plan,
        [string[]]$Tenant,
        [switch]$Apply,
        [switch]$AcknowledgeUntested,
        [hashtable]$Setting = @{},
        [string]$OutputPath
    )

    $entry = Get-TcRemediationEntry -Id $Id
    $projectData = if ($Project -is [string]) { Read-TcProjectFile -Path (Resolve-TcPath -Path $Project) } else { $Project }
    $output = if ($OutputPath) { Resolve-TcPath -Path $OutputPath } else { Resolve-TcPath -Path ([string](Get-TcProperty (Get-TcProperty $projectData 'output') 'path')) }
    $snapshotData = Get-TcRemediationSnapshot -Snapshot $Snapshot -Output $output
    if (-not $Plan) { $Plan = New-TcPlan -Project $projectData -Snapshot $snapshotData }

    $script:TcRemediationRun = New-TcRemediationRun -Entry $entry -Project $projectData -Snapshot $snapshotData -Plan $Plan -Tenant $Tenant -Apply $Apply.IsPresent -Acknowledged $AcknowledgeUntested.IsPresent -Setting $Setting -Output $output
    if ($Apply) { Assert-TcSnapshotAge -Snapshot $snapshotData -Limit (Get-TcRemediationOption 'MaxSnapshotAgeDays') }
    Assert-TcModuleFamily -Service ([string]$entry['service'])

    $failure = ''
    try { Invoke-TcRemediationScript -Path (Join-Path $script:TcRemediationRoot $entry['path']) }
    catch {
        if (-not $script:TcRemediationRun['Refused']) {
            $failure = Get-TcErrorText -ErrorInput $_
            $script:TcRemediationRun['Blocking']++
            Write-TcLog -Message ('Stopped: {0}. {1}' -f $failure, ((($_.ScriptStackTrace -split "`n") | Select-Object -First 1) -replace '\s+', ' ')) -Level ERROR
            Write-TcMessage -Message "Stopped: $failure" -Level Warning
        }
        Close-TcRemediation
    }

    Get-TcRemediationOutcome -Failure $failure
}