MSIX.AutoFixLoop.ps1
|
# ============================================================================= # Multi-pass remediation pipeline — Invoke-MsixAutoFixLoop # ----------------------------------------------------------------------------- # Automates the "fix → re-analyse → next fix" cycle. Many MSIX issues are # chained: fix working dir → reveals DLL issue → fix DLL → reveals updater. # The module already ships every building block; this stitches them into a # controlled loop: # # Pass N: # 1. Get-MsixCompatibilityReport (static + optional trace). # 2. Invoke-MsixAutoFixFromAnalysis -DryRun to produce a plan. # 3. Stop if the plan is empty (NoNewFixes). # 4. Apply the plan (no sign yet). # 5. If -CaptureTrace: run Invoke-MsixProcMonCapture and compare via # Compare-MsixTrace; stop if Introduced==0 (NoRegressions). # 6. Loop. # # After all passes: sign exactly once. # # Per-pass artefacts (report.json, plan.json, optional trace.pml, trace-delta.json) # are written under $env:TEMP\msix-autofix-loop-<guid>\ so operators can # post-mortem any pass. # ============================================================================= function Invoke-MsixAutoFixLoop { <# .SYNOPSIS Runs repeated static-analysis + auto-fix passes until the package is stable or the maximum pass count is reached. .DESCRIPTION Each pass runs Get-MsixCompatibilityReport against the current package state, plans the next round of fixes via Invoke-MsixAutoFixFromAnalysis, and applies them (unsigned). The loop continues until: NoNewFixes — the planner has nothing to do (stable). NoRegressions — Compare-MsixTrace shows no newly-introduced failures (only meaningful when -CaptureTrace is set; requires Procmon to be available via Initialize-MsixToolchain). Both stop conditions can be combined in -StopOn. The package is signed exactly once at the end of the last pass (unless -SkipSigning is set). Per-pass artefacts (report.json, plan.json, trace.pml, delta.json) are kept under: $env:TEMP\msix-autofix-loop-<runId>\pass-N\ so operators can post-mortem any pass without losing intermediate state. -WhatIf / -DryRun performs only the first pass's plan and exits without writing anything. .PARAMETER PackagePath .msix to act on. .PARAMETER MaxPasses Hard cap on pass count (default 5). Prevents runaway loops. .PARAMETER StopOn One or more stop conditions: NoNewFixes — stop when the planner has nothing new to apply. NoRegressions — stop when Compare-MsixTrace detects no introduced failures relative to the previous pass. Default: NoNewFixes. .PARAMETER MinConfidence Confidence floor forwarded to Invoke-MsixAutoFixFromAnalysis (default 0.85). .PARAMETER CaptureTrace When set, each pass installs the package in the MSIX Sandbox, captures a ProcMon trace, and feeds it into Compare-MsixTrace for the NoRegressions stop condition. Requires Hyper-V + Sandbox + ProcMon. .PARAMETER TraceDurationSeconds How long to capture the ProcMon trace per pass (default 30 seconds). .PARAMETER TraceLogPath Path to an existing TraceFixup .log/.txt to feed into each pass's compatibility report instead of running a live capture. Incompatible with -CaptureTrace. .PARAMETER OutputPath Write the final fixed package here. Defaults to overwriting PackagePath. .PARAMETER DryRun Run only the first pass planner and print the plan; do not write. .PARAMETER SkipSigning / NoSign / Pfx / PfxPassword Signing controls for the final sign-once call. # --- Auto-fix pass-through parameters (forwarded each pass) --- .PARAMETER VcRuntimeSourceFolder Forwarded to Invoke-MsixAutoFixFromAnalysis. .PARAMETER StartupTaskAppId / StartupTaskName / LoaderPaths Forwarded to Invoke-MsixAutoFixFromAnalysis. .PARAMETER IgnoreUpdaters / IgnorePluginDirectories / LegacyPluginFix .PARAMETER IgnoreNestedPackages / PreferManifestOverPsf Forwarded to Invoke-MsixAutoFixFromAnalysis. .OUTPUTS [pscustomobject] @{ Output [string] path to the final (signed) package Passes [object[]] per-pass summary objects FinalReport [pscustomobject] compatibility report from the last pass SignedOk [bool] RunDirectory[string] path to per-pass artefacts } .EXAMPLE Invoke-MsixAutoFixLoop -PackagePath app.msix -MaxPasses 5 ` -Pfx cert.pfx -PfxPassword $pw .EXAMPLE # With sandbox trace capture: Invoke-MsixAutoFixLoop -PackagePath app.msix ` -StopOn NoNewFixes,NoRegressions -CaptureTrace ` -TraceDurationSeconds 30 ` -Pfx cert.pfx -PfxPassword $pw -OutputPath app-fixed.msix .EXAMPLE # DryRun — only plan pass 1, do not write: Invoke-MsixAutoFixLoop -PackagePath app.msix -DryRun #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$PackagePath, [ValidateRange(1, 20)] [int]$MaxPasses = 5, [ValidateSet('NoNewFixes', 'NoRegressions')] [string[]]$StopOn = @('NoNewFixes'), [ValidateRange(0.0, 1.0)] [double]$MinConfidence = 0.85, # Trace / sandbox options [switch]$CaptureTrace, [int]$TraceDurationSeconds = 30, [string]$TraceLogPath, # Output [string]$OutputPath, [switch]$DryRun, [Alias('NoSign')] [switch]$SkipSigning, [string]$Pfx, [SecureString]$PfxPassword, # Pass-through to Invoke-MsixAutoFixFromAnalysis [string]$VcRuntimeSourceFolder, [string]$StartupTaskAppId, [string]$StartupTaskName, [string[]]$LoaderPaths, [switch]$IgnoreUpdaters, [switch]$IgnorePluginDirectories, [switch]$LegacyPluginFix, [switch]$IgnoreNestedPackages, [bool]$PreferManifestOverPsf = $true ) if ($CaptureTrace -and $TraceLogPath) { throw '-CaptureTrace and -TraceLogPath are mutually exclusive.' } # Per-run working directory $runId = [guid]::NewGuid().ToString('N').Substring(0, 12) $runDir = Join-Path $env:TEMP "msix-autofix-loop-$runId" $null = New-Item -ItemType Directory -Path $runDir -Force # Determine the working copy path. $targetPath = if ($OutputPath -and $OutputPath -ne $PackagePath) { if (-not $DryRun) { Copy-Item -LiteralPath $PackagePath -Destination $OutputPath -Force } $OutputPath } else { $PackagePath } Write-MsixLog Info ("AutoFixLoop started: runId={0} maxPasses={1} stopOn={2}" ` -f $runId, $MaxPasses, ($StopOn -join ',')) Write-MsixLog Info "AutoFixLoop artefacts: $runDir" # --- Build the static arg hashtable for Invoke-MsixAutoFixFromAnalysis --- $fixArgs = @{ MinConfidence = $MinConfidence PreferManifestOverPsf = $PreferManifestOverPsf SkipSigning = $true # sign once at end } if ($VcRuntimeSourceFolder) { $fixArgs['VcRuntimeSourceFolder'] = $VcRuntimeSourceFolder } if ($StartupTaskAppId) { $fixArgs['StartupTaskAppId'] = $StartupTaskAppId } if ($StartupTaskName) { $fixArgs['StartupTaskName'] = $StartupTaskName } if ($LoaderPaths) { $fixArgs['LoaderPaths'] = $LoaderPaths } if ($IgnoreUpdaters) { $fixArgs['IgnoreUpdaters'] = $true } if ($IgnorePluginDirectories){ $fixArgs['IgnorePluginDirectories'] = $true } if ($LegacyPluginFix) { $fixArgs['LegacyPluginFix'] = $true } if ($IgnoreNestedPackages) { $fixArgs['IgnoreNestedPackages'] = $true } $passSummaries = [System.Collections.Generic.List[object]]::new() $finalReport = $null $prevTracePath = $null $stopReason = $null for ($pass = 1; $pass -le $MaxPasses; $pass++) { Write-MsixLog Info "AutoFixLoop pass $pass / $MaxPasses" $passDir = Join-Path $runDir "pass-$pass" $null = New-Item -ItemType Directory -Path $passDir -Force # ── 1. Compatibility report ── $reportArgs = @{ PackagePath = $targetPath } if ($TraceLogPath) { $reportArgs['TraceLogPath'] = $TraceLogPath } $report = Get-MsixCompatibilityReport @reportArgs $finalReport = $report # Persist report for post-mortem $report | ConvertTo-Json -Depth 10 -Compress | Out-File (Join-Path $passDir 'report.json') -Encoding utf8 # ── 2. Plan ── $plan = Invoke-MsixAutoFixFromAnalysis -Report $report @fixArgs -DryRun $plan | ConvertTo-Json -Depth 10 -Compress | Out-File (Join-Path $passDir 'plan.json') -Encoding utf8 $passSummary = [pscustomobject]@{ Pass = $pass FindingCount = @($report.Findings).Count StageCount = if ($plan) { @($plan).Count } else { 0 } TraceDelta = $null StopReason = $null ArtifactPath = $passDir } # ── DryRun: only first pass plan, then stop ── if ($DryRun) { Write-MsixLog Info '[DryRun] Pass 1 plan produced - exiting without writing.' $passSummary.StopReason = 'DryRun' $passSummaries.Add($passSummary) break } # ── 3. Stop: NoNewFixes ── if ('NoNewFixes' -in $StopOn -and $passSummary.StageCount -eq 0) { Write-MsixLog Info "AutoFixLoop stopping: no new fixes planned (pass $pass)." $passSummary.StopReason = 'NoNewFixes' $stopReason = 'NoNewFixes' $passSummaries.Add($passSummary) break } # ── 4. Apply ── if ($PSCmdlet.ShouldProcess($targetPath, "AutoFix pass $pass")) { Invoke-MsixAutoFixFromAnalysis -Report $report -PackagePath $targetPath @fixArgs | Out-Null } Write-MsixLog Info "AutoFixLoop pass $pass applied." # ── 5. Optional trace capture + delta ── if ($CaptureTrace -and 'NoRegressions' -in $StopOn) { $tracePath = Join-Path $passDir 'trace.pml' Write-MsixLog Info "AutoFixLoop: capturing trace ($TraceDurationSeconds s)..." try { Invoke-MsixProcMonCapture -PackagePath $targetPath -OutputPml $tracePath ` -DurationSeconds $TraceDurationSeconds } catch { Write-MsixLog Warning "AutoFixLoop: trace capture failed on pass $pass - $_" } if ($prevTracePath -and (Test-Path $tracePath)) { $delta = Compare-MsixTrace -Baseline $prevTracePath -Candidate $tracePath $delta | ConvertTo-Json -Depth 10 -Compress | Out-File (Join-Path $passDir 'trace-delta.json') -Encoding utf8 $passSummary.TraceDelta = $delta.Summary if ('NoRegressions' -in $StopOn -and $delta.Summary.IntroducedCount -eq 0) { Write-MsixLog Info "AutoFixLoop stopping: no regressions introduced (pass $pass)." $passSummary.StopReason = 'NoRegressions' $stopReason = 'NoRegressions' $passSummaries.Add($passSummary) $prevTracePath = $tracePath break } } $prevTracePath = $tracePath } $passSummaries.Add($passSummary) if ($pass -eq $MaxPasses) { Write-MsixLog Warning "AutoFixLoop: reached MaxPasses ($MaxPasses) without a stop condition." $stopReason = 'MaxPasses' } } # ── 6. Sign once ── $signedOk = $false if (-not $DryRun) { if (-not $SkipSigning -and $Pfx) { try { if ($PSCmdlet.ShouldProcess($targetPath, 'Sign package')) { Invoke-MsixSigning -PackagePath $targetPath -Pfx $Pfx -PfxPassword $PfxPassword $signedOk = $true Write-MsixLog Info "AutoFixLoop: package signed - $targetPath" } } catch { Write-MsixLog Warning "AutoFixLoop: signing failed - $_" } } elseif (-not $SkipSigning -and -not $Pfx) { Write-MsixLog Warning 'AutoFixLoop: no -Pfx supplied - package left unsigned.' } else { Write-MsixLog Info 'AutoFixLoop: skipping signing (-SkipSigning).' } } $stopReasonStr = if ($null -ne $stopReason) { $stopReason } else { 'MaxPasses' } Write-MsixLog Info ("AutoFixLoop complete: passes={0} stopReason={1} output={2}" ` -f $passSummaries.Count, $stopReasonStr, $targetPath) return [pscustomobject]@{ Output = $targetPath Passes = [object[]]$passSummaries FinalReport = $finalReport SignedOk = $signedOk RunDirectory = $runDir StopReason = $stopReason } } |