Public/Undo-TcRemediation.ps1
|
function Undo-TcRemediation { <# .SYNOPSIS Goes back on what a remediation run changed, from its journal, when that is possible. .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. Reads the operations.jsonl of one run folder and replays the undos, the most recent first. A folder that holds several runs is refused, and they are listed. An operation with no way back is listed with its reason, and stays to be handled by hand. So does an operation cut short, whose status nobody knows. Without -Apply it only shows what it would do, with no connection. With -Apply it asks what Invoke-TcRemediation asks: the word UNTESTED, which -AcknowledgeUntested replaces, the primary domain of each tenant an undo writes to, which nothing replaces, and the word REMOVE when an undo deletes an object, as undoing a creation does, which nothing replaces either. What was undone is marked in the journal of the run, so a second undo finds nothing to do. The undo keeps a journal and a summary of its own, in its own folder, shown as Run in the result. .PARAMETER Path The folder of one run, as shown at the end of Invoke-TcRemediation. .PARAMETER Project The project file, or a project as read from it. .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. .EXAMPLE Undo-TcRemediation -Path ./output/remediations/20270115-091500-ENT-MFA-Apply -Project ./project.json Shows what would be undone. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory, Position = 0)][string]$Path, [Parameter(Mandatory)][object]$Project, [switch]$Apply, [switch]$AcknowledgeUntested ) $folder = (Resolve-TcPath -Path $Path).TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) $projectData = if ($Project -is [string]) { Read-TcProjectFile -Path (Resolve-TcPath -Path $Project) } else { $Project } $journal = Join-Path $folder 'operations.jsonl' $nested = @(Get-ChildItem -LiteralPath $folder -Recurse -File -Filter 'operations.jsonl' -ErrorAction Stop | Where-Object { $_.FullName -ne $journal }) if ($nested.Count) { throw ('{0} is not one run folder: it holds {1} run(s). Give one of them: {2}' -f $folder, $nested.Count, (@($nested | ForEach-Object { $_.Directory.Name } | Sort-Object) -join ', ')) } $recorded = @(if (Test-Path -LiteralPath $journal) { Read-TcOperation -Path $journal }) # Cut short between two lines of the journal: the write may have gone through. $unknown = @($recorded | Where-Object { $_['status'] -in 'Running', 'Undoing' }) # A block that failed may have done part of its work: what it recorded in its undo is replayed too. $operations = @($recorded | Where-Object { $_['status'] -eq 'Applied' -or ($_['status'] -eq 'Failed' -and $_['type'] -eq 'Block' -and $_['undo']) }) $operations = @($operations | Sort-Object @{ Expression = { [datetimeoffset](Get-TcProperty $_ 'timestamp' '0001-01-01T00:00:00Z') }; Descending = $true }, @{ Expression = { $_['id'] }; Descending = $true }) $undoable = @($operations | Where-Object { $_['undo'] }) $noWayBack = @($operations | Where-Object { -not $_['undo'] }) $summaryFile = Join-Path $folder 'summary.json' $id = if (Test-Path -LiteralPath $summaryFile) { (Read-TcJsonFile -Path $summaryFile)['id'] } else { '' } $entry = if ($id -and (Get-TcRemediationCatalog).Contains($id)) { Get-TcRemediationEntry -Id $id } else { [ordered]@{ id = 'UNDO'; title = 'Undo'; permissions = @(); removesObjects = $false; service = 'Graph'; nature = 'Partial' } } $output = Resolve-TcPath -Path ([string](Get-TcProperty (Get-TcProperty $projectData 'output') 'path')) $run = New-TcRemediationRun -Entry ([ordered]@{ id = "UNDO-$($entry['id'])"; title = "Undo of $($entry['title'])"; permissions = @($entry['permissions']); removesObjects = $false; nature = 'Partial'; scope = 'Target'; ruleIds = @() }) -Project $projectData -Snapshot ([ordered]@{ tenants = [ordered]@{} }) -Plan ([ordered]@{ actions = @() }) -Apply $Apply.IsPresent -Acknowledged $AcknowledgeUntested.IsPresent -Output $output $script:TcRemediationRun = $run $context = Open-TcRemediation Write-TcMessage -Message ('{0} operation(s) to undo, {1} with no way back. Source: {2}' -f $undoable.Count, ($noWayBack.Count + $unknown.Count), $folder) foreach ($operation in $unknown) { Write-TcRemediationLine -Status Warning -Text ('{0}: {1}: status unknown, check the tenant' -f ((@(Get-TcActionTenant -Action $operation) | Select-Object -Unique) -join ', '), $operation['description']) -Step $operation['id'] } foreach ($operation in $noWayBack) { Write-TcRemediationLine -Status Info -Text ('{0}: {1}: {2}' -f ((@(Get-TcProperty $operation 'written' @()) + @($operation['tenant']) | Select-Object -Unique) -join ', '), $operation['description'], $(if ($operation['reason']) { $operation['reason'] } else { 'no undo recorded' })) -Step $operation['id'] } foreach ($operation in $undoable) { Write-TcRemediationLine -Status Planned -Text ('{0}: undo {1}' -f ((Get-TcUndoTenant -Operation $operation) -join ', '), $operation['description']) -Step $operation['id'] } $failed = 0 $done = 0 if ($context.Apply -and $undoable.Count) { $tenants = @($undoable | ForEach-Object { Get-TcUndoTenant -Operation $_ } | Select-Object -Unique) $removal = @($undoable | Where-Object { Test-TcRemovingUndo -Operation $_ }).Count -gt 0 if (Confirm-TcRemediationApply -Tenant $tenants -Removal $removal) { foreach ($operation in $undoable) { $shown = (Get-TcUndoTenant -Operation $operation) -join ', ' $status = $operation['status'] $operation['status'] = 'Undoing' $operation['undoRun'] = $run['Folder'] Write-TcOperation -Action $operation -Path $journal try { Invoke-TcUndoOperation -Undo $operation['undo'] -Tenant (Get-TcWrittenTenant -Operation $operation) -Service $operation['service'] $done++ $operation['status'] = 'Undone' Write-TcOperation -Action $operation -Path $journal Write-TcOperation -Action $operation Write-TcRemediationLine -Status Applied -Text ('{0}: undone: {1}' -f $shown, $operation['description']) -Step $operation['id'] } catch { $text = Get-TcErrorText -ErrorInput $_ $failed++ $operation['undoError'] = $text # The change is still in place: the journal of the run keeps it to undo, and this one says it failed. $operation['status'] = $status Write-TcOperation -Action $operation -Path $journal $operation['status'] = 'Undo failed' Write-TcOperation -Action $operation Write-TcRemediationLine -Status Failed -Text ('{0}: {1}: {2}' -f $shown, $operation['description'], $text) -Step $operation['id'] } } } else { $run['Stopped'] = $true Write-TcRemediationLine -Status Warning -Text 'Not confirmed. Stopped: nothing was changed.' -Step $run['Id'] } } Disconnect-TcRemediationService $code = if ($failed) { 2 } elseif ($run['Stopped']) { 3 } else { 0 } Write-TcJsonFile -Path (Join-Path $run['Folder'] 'summary.json') -InputObject ([ordered]@{ id = $run['Id']; title = $run['Entry']['title']; mode = $(if ($Apply) { 'Apply' } else { 'Preview' }); source = $folder; undoable = $undoable.Count; noWayBack = $noWayBack.Count + $unknown.Count undone = $done; failed = $failed; code = $code; folder = $run['Folder']; testedOnRealTenant = $false }) [pscustomobject]@{ Folder = $folder; Run = $run['Folder']; Undoable = $undoable.Count; NoWayBack = $noWayBack.Count + $unknown.Count; Undone = $done; Failed = $failed; Code = $code; Notice = $script:TcUntestedNotice } } |