Public/Invoke-EFEndpointRemediation.ps1
|
function Invoke-EFEndpointRemediation { <# .SYNOPSIS Remediates supported noncompliant controls on the local Windows endpoint. .DESCRIPTION Evaluates controls, changes only those marked Remediable, and evaluates them again. The cmdlet supports WhatIf and Confirm, requires an elevated session when changes are needed, and returns a result for every selected control. It never enables BitLocker, Secure Boot, or a TPM automatically. .PARAMETER Baseline A built-in baseline name, a JSON file path, or a validated baseline object. .PARAMETER ControlId Limits remediation to specific control identifiers. .PARAMETER StopOnError Stops after the first remediation error. By default, remaining controls continue. .PARAMETER NoProgress Suppresses the progress display for non-interactive automation hosts. .EXAMPLE Invoke-EFEndpointRemediation -WhatIf .EXAMPLE Invoke-EFEndpointRemediation -ControlId EF-FW-DOMAIN,EF-UAC-ENABLED -Confirm:$false #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( [Parameter(Position = 0)] [AllowNull()] [object]$Baseline = 'EnterpriseRecommended', [string[]]$ControlId, [switch]$StopOnError, [switch]$NoProgress ) $null = Test-EFWindows -Throw $correlationId = [guid]::NewGuid().ToString() $resolvedBaseline = Resolve-EFBaseline -Baseline $Baseline $controls = @($resolvedBaseline.Controls) if ($PSBoundParameters.ContainsKey('ControlId') -and $null -ne $ControlId -and $ControlId.Length -gt 0) { $missingIds = @($ControlId | Where-Object { $_ -notin $controls.Id }) if ($missingIds.Count -gt 0) { throw [System.ArgumentException]::new("Unknown control Id(s): $($missingIds -join ', ')") } $controls = @($controls | Where-Object { $_.Id -in @($ControlId) }) } $beforeResults = @{} $evaluationIndex = 0 foreach ($control in $controls) { $evaluationIndex++ if (-not $NoProgress) { Write-Progress -Id 1103 -Activity 'EndpointForge remediation' ` -Status "Checking $($control.Id): $($control.Title)" ` -PercentComplete ([math]::Round(($evaluationIndex / $controls.Count) * 30)) } $beforeResults[[string]$control.Id] = Get-EFControlState -Control $control } $changeCandidates = @($controls | Where-Object { $beforeResults[[string]$_.Id].Status -eq 'NonCompliant' -and [bool]$_.Remediable }) if ($changeCandidates.Count -gt 0 -and -not $WhatIfPreference -and -not (Test-EFAdministrator)) { if (-not $NoProgress) { Write-Progress -Id 1103 -Activity 'EndpointForge remediation' -Completed } throw [System.UnauthorizedAccessException]::new( 'Endpoint remediation requires an elevated PowerShell session. Re-run as Administrator, or use -WhatIf to preview changes.' ) } Write-EFLog -Message "Remediation started for baseline '$($resolvedBaseline.Name)'." ` -CorrelationId $correlationId -Data @{ selected = $controls.Count; candidates = $changeCandidates.Count; whatIf = [bool]$WhatIfPreference } $rebootRequired = $false $stopRequested = $false $remediationIndex = 0 $results = @( foreach ($control in $controls) { $remediationIndex++ if (-not $NoProgress) { Write-Progress -Id 1103 -Activity 'EndpointForge remediation' ` -Status "Processing $($control.Id): $($control.Title)" ` -PercentComplete (30 + [math]::Round(($remediationIndex / $controls.Count) * 70)) } $before = $beforeResults[[string]$control.Id] $outcome = $null $after = $before $message = $before.Message if ($before.Status -eq 'Compliant') { $outcome = 'NotRequired' } elseif ($before.Status -eq 'NotApplicable') { $outcome = 'NotApplicable' } elseif ($before.Status -eq 'Error') { $outcome = 'EvaluationFailed' } elseif (-not [bool]$control.Remediable) { $outcome = 'NotRemediable' } else { $target = "$env:COMPUTERNAME/$($control.Id)" if ($PSCmdlet.ShouldProcess($target, "Remediate $($control.Title)")) { try { $change = Invoke-EFControlRemediation -Control $control $rebootRequired = $rebootRequired -or [bool]$change.RebootRequired $after = Get-EFControlState -Control $control if ($after.Status -eq 'Compliant') { $outcome = 'Changed' $message = 'The control was remediated and verified.' } else { $outcome = 'VerificationFailed' $message = "The change completed, but verification returned '$($after.Status)': $($after.Message)" } } catch { $outcome = 'Failed' $message = $_.Exception.Message Write-EFLog -Level Error -Message "Remediation failed for control '$($control.Id)'." ` -CorrelationId $correlationId -Data @{ error = $message } if ($StopOnError) { $stopRequested = $true } } } else { $outcome = if ($WhatIfPreference) { 'WhatIf' } else { 'Skipped' } $message = if ($WhatIfPreference) { 'The change was previewed; endpoint state was not modified.' } else { 'The change was declined.' } } } $recommendedAction = switch ($outcome) { 'NotRequired' { 'No action required.' } 'NotApplicable' { 'No action is required on this endpoint.' } 'Changed' { 'No further action is required unless RebootRequired is true.' } 'WhatIf' { 'Apply this control from an elevated session using the same Baseline and ControlId parameters when approved.' } 'Skipped' { 'Run Invoke-EFEndpointRemediation again with the same Baseline and ControlId when the change is approved.' } 'NotRemediable' { 'Remediate this control through your approved enterprise policy or management platform.' } 'EvaluationFailed' { $before.RecommendedAction } default { 'Review the failure details, correct the underlying issue, and try again.' } } $resultItem = [pscustomobject]@{ PSTypeName = 'EndpointForge.RemediationResult' ControlId = [string]$control.Id Title = [string]$control.Title Severity = [string]$control.Severity BeforeStatus = [string]$before.Status Outcome = $outcome AfterStatus = [string]$after.Status Message = $message RecommendedAction = $recommendedAction } $resultItem if ($stopRequested) { break } } ) if (-not $NoProgress) { Write-Progress -Id 1103 -Activity 'EndpointForge remediation' -Completed } $failureCount = @($results | Where-Object Outcome -in @('Failed', 'EvaluationFailed', 'VerificationFailed')).Count $remainingCount = @($results | Where-Object AfterStatus -eq 'NonCompliant').Count $changedCount = @($results | Where-Object Outcome -eq 'Changed').Count $previewCount = @($results | Where-Object Outcome -eq 'WhatIf').Count $unprocessedCount = $controls.Count - $results.Count $exitCode = if ($failureCount -gt 0) { 3 } elseif ($remainingCount -gt 0) { 2 } else { 0 } $summaryText = if ($previewCount -gt 0) { "$previewCount change(s) were previewed; endpoint state was not modified." } elseif ($failureCount -gt 0) { "$changedCount change(s) succeeded and $failureCount action(s) failed." } elseif ($remainingCount -gt 0) { "$changedCount change(s) succeeded; $remainingCount noncompliant control(s) remain." } else { "Remediation completed successfully with $changedCount verified change(s)." } $nextStep = if ($rebootRequired) { 'Schedule a restart through your approved change process; EndpointForge does not restart devices automatically.' } elseif ($exitCode -ne 0) { 'Review Results.RecommendedAction for unresolved controls.' } else { 'Run Get-EFComplianceReport to confirm the complete baseline state.' } $report = [pscustomobject]@{ PSTypeName = 'EndpointForge.RemediationReport' ComputerName = $env:COMPUTERNAME BaselineName = [string]$resolvedBaseline.Name BaselineVersion = [string]$resolvedBaseline.Version CorrelationId = $correlationId CompletedAtUtc = [DateTime]::UtcNow IsSuccessful = $exitCode -eq 0 ExitCode = $exitCode Summary = $summaryText NextStep = $nextStep ChangedCount = $changedCount CandidateCount = $changeCandidates.Count PreviewCount = $previewCount RemainingCount = $remainingCount FailureCount = $failureCount UnprocessedCount = $unprocessedCount RebootRequired = $rebootRequired Results = $results } Write-EFLog -Message "Remediation completed for baseline '$($resolvedBaseline.Name)'." ` -Level $(if ($exitCode -eq 0) { 'Information' } else { 'Warning' }) -CorrelationId $correlationId ` -Data @{ changed = $changedCount; remaining = $remainingCount; failures = $failureCount; exitCode = $exitCode } return $report } |