Public/Invoke-LocalRemediation.ps1
|
function Invoke-LocalRemediation { <# .SYNOPSIS Executes proactive endpoint remediation checks without Intune cloud licensing. .DESCRIPTION Implements the Microsoft Proactive Remediation standard locally on any Windows machine or homelab hypervisor VM. Runs detection scripts using standard exit codes (0 = Compliant, 1 = Non-Compliant), triggers auto-remediation when specified, and verifies post-remediation compliance drift. .PARAMETER Name Friendly name or identifier for this remediation policy. .PARAMETER DetectionScriptPath Path to the PowerShell detection script (.ps1). .PARAMETER DetectionScript ScriptBlock containing the detection logic. .PARAMETER RemediationScriptPath Path to the PowerShell remediation script (.ps1). .PARAMETER RemediationScript ScriptBlock containing the remediation logic. .PARAMETER Remediate If specified and detection exits with 1 (Non-Compliant), triggers remediation execution. .PARAMETER Verify Re-runs detection immediately following remediation to confirm drift was resolved. .EXAMPLE Invoke-LocalRemediation -Name "Ensure-DNS" -DetectionScript { if ((Get-DnsClientServerAddress -InterfaceAlias 'Ethernet').ServerAddresses -contains '1.1.1.1') { exit 0 } else { exit 1 } } .EXAMPLE Invoke-LocalRemediation -Name "Clear-Dumps" -DetectionScriptPath ".\Detect-CrashDumps.ps1" -RemediationScriptPath ".\Clear-CrashDumps.ps1" -Remediate -Verify #> [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')] param ( [Parameter(Position = 0)] [string]$Name = 'LocalRemediation', [Parameter(ParameterSetName = 'FilePath')] [string]$DetectionScriptPath, [Parameter(ParameterSetName = 'FilePath')] [string]$RemediationScriptPath, [Parameter(ParameterSetName = 'ScriptBlock')] [scriptblock]$DetectionScript, [Parameter(ParameterSetName = 'ScriptBlock')] [scriptblock]$RemediationScript, [Parameter()] [switch]$Remediate, [Parameter()] [switch]$Verify ) $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") function Run-PowerShellWorker { param ( [string]$Path, [scriptblock]$Block ) if ($Path) { if (-not (Test-Path -Path $Path)) { throw "Script file not found: '$Path'" } $command = "& '$Path'" } else { $command = $Block.ToString() } # Run in isolated child process to reliably trap standard exit code $encoded = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command)) $pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = (Get-Process -Id $PID).Path $pinfo.Arguments = "-NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded" $pinfo.RedirectStandardOutput = $true $pinfo.RedirectStandardError = $true $pinfo.UseShellExecute = $false $pinfo.CreateNoWindow = $true $proc = [System.Diagnostics.Process]::Start($pinfo) $stdout = $proc.StandardOutput.ReadToEnd() $stderr = $proc.StandardError.ReadToEnd() $proc.WaitForExit() $combinedOutput = $stdout if ($stderr) { $combinedOutput += "`n[STDERR]: $stderr" } return [PSCustomObject]@{ ExitCode = $proc.ExitCode Output = $combinedOutput.Trim() } } Write-Verbose "[$Name] Executing detection pass..." $detectResult = Run-PowerShellWorker -Path $DetectionScriptPath -Block $DetectionScript $remediationExecuted = $false $remedResult = $null $finalResult = $null $status = if ($detectResult.ExitCode -eq 0) { 'Compliant' } else { 'NonCompliant' } if ($detectResult.ExitCode -ne 0 -and $Remediate) { Write-Verbose "[$Name] Machine is Non-Compliant. Triggering local remediation..." $remediationExecuted = $true $remedResult = Run-PowerShellWorker -Path $RemediationScriptPath -Block $RemediationScript if ($Verify) { Write-Verbose "[$Name] Verifying compliance post-remediation..." $finalResult = Run-PowerShellWorker -Path $DetectionScriptPath -Block $DetectionScript $status = if ($finalResult.ExitCode -eq 0) { 'Remediated' } else { 'RemediationFailed' } } else { $status = if ($remedResult.ExitCode -eq 0) { 'Remediated' } else { 'RemediationFailed' } } } $stopwatch.Stop() return [PSCustomObject]@{ Name = $Name Status = $status Compliant = ($status -in @('Compliant', 'Remediated')) RemediationExecuted = $remediationExecuted InitialExitCode = $detectResult.ExitCode InitialOutput = $detectResult.Output RemediationExitCode = if ($remedResult) { $remedResult.ExitCode } else { $null } RemediationOutput = if ($remedResult) { $remedResult.Output } else { $null } FinalExitCode = if ($finalResult) { $finalResult.ExitCode } else { $detectResult.ExitCode } DurationMs = [math]::Round($stopwatch.ElapsedMilliseconds, 2) Timestamp = $timestamp } } |