MSIX.Trace.ps1
|
# ============================================================================= # Trace Fixup output parser # ----------------------------------------------------------------------------- # Reads the OutputDebugString stream produced by PSF TraceFixup.dll, captured # either by DebugView (saved to a .log/.txt) or via an ETW session. # # Each captured line typically looks like: # # [hh:mm:ss.fff PID:TID] CreateFileW: \\?\C:\Program Files\WindowsApps\…\app.log -> ACCESS_DENIED # [00:00:01.234 8472:A1B] RegOpenKeyExW: HKLM\SOFTWARE\Vendor -> SUCCESS # # DebugView's "Save As" produces tab-separated lines with the timestamp, # process id and the message: # # <num>\t<elapsed>\t[PID:TID] <function>: <path> -> <result> # # This parser is permissive — it tries the structured form first, then falls # back to a regex over the message text. # ============================================================================= $script:_TraceLineRegex = [regex]'(?<func>[A-Za-z_][A-Za-z0-9_]+(?:[AW]|Ex[AW]?)?):\s*(?<path>.+?)\s*->\s*(?<result>[A-Z_][A-Z0-9_]+)' $script:_TraceHeadRegex = [regex]'\[(?<ts>[\d:.]+)?\s*(?<pid>\d+)?:?(?<tid>[0-9A-Fa-f]+)?\]' function ConvertFrom-MsixTraceLine { <# .SYNOPSIS Parses a single Trace Fixup log line into a structured object. .DESCRIPTION Accepts one line of OutputDebugString text emitted by PSF's TraceFixup.dll (typically captured via DebugView "Save As"). Two regexes run against the line: 1. The function/path/result triplet: 'Func: <path> -> RESULT'. 2. The leading '[hh:mm:ss.fff PID:TID]' header. If the first regex doesn't match, the line is silently skipped (returns nothing) so the parser can be used across mixed log files. On a match, the function name is mapped to a coarse category (filesystem / registry / module-load / other) which is convenient for downstream filtering. .PARAMETER Line A single text line from a DebugView capture. Empty strings are accepted (and produce no output). Pipeline input is supported so an entire file can be streamed via Get-Content | ConvertFrom-MsixTraceLine. .OUTPUTS [pscustomobject] with Timestamp, ProcessId, ThreadId, Function, Category, Path, Result, Raw. No output for lines that don't match. .EXAMPLE '[00:00:01.234 8472:A1B] CreateFileW: C:\Program Files\WindowsApps\app\log.txt -> ACCESS_DENIED' | ConvertFrom-MsixTraceLine .EXAMPLE Get-Content .\debugview.log | ConvertFrom-MsixTraceLine | Where-Object Category -eq 'filesystem' #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [AllowEmptyString()] [string]$Line ) PROCESS { if ([string]::IsNullOrWhiteSpace($Line)) { return } $m = $script:_TraceLineRegex.Match($Line) if (-not $m.Success) { return } $func = $m.Groups['func'].Value $path = $m.Groups['path'].Value.Trim() $result = $m.Groups['result'].Value $head = $script:_TraceHeadRegex.Match($Line) $ts = if ($head.Success) { $head.Groups['ts'].Value } else { $null } $procId = if ($head.Success -and $head.Groups['pid'].Value) { [int]$head.Groups['pid'].Value } else { $null } $tid = if ($head.Success) { $head.Groups['tid'].Value } else { $null } # Categorise by function-name prefix $category = switch -Regex ($func) { '^(CreateFile|ReadFile|WriteFile|Delete|MoveFile|CopyFile|FindFirstFile|GetFileAttributes|SetFileAttributes)' { 'filesystem' } '^(Reg)' { 'registry' } '^(LoadLibrary|GetModuleHandle|GetProcAddress)' { 'module-load' } default { 'other' } } return [pscustomobject]@{ Timestamp = $ts ProcessId = $procId ThreadId = $tid Function = $func Category = $category Path = $path Result = $result Raw = $Line } } } function Get-MsixTraceOutput { <# .SYNOPSIS Parses an entire DebugView log file (or any text file containing PSF TraceFixup output) into structured objects. .DESCRIPTION Streams the file through ConvertFrom-MsixTraceLine and applies the optional ProcessId / FunctionPattern filters. Lines that don't look like TraceFixup output (banners, blank lines, other process noise) are dropped. Use Get-MsixTraceFailure to narrow further to non-success rows. .PARAMETER Path Path to the saved log (DebugView "Save As" or any text dump that contains TraceFixup messages). .PARAMETER ProcessId Optional filter on process id (matches the PID in [PID:TID] header). .PARAMETER FunctionPattern Optional regex matched against Function (e.g. '^Reg' to keep registry only). .OUTPUTS [pscustomobject] one per parseable line. Same shape as ConvertFrom-MsixTraceLine. .EXAMPLE Get-MsixTraceOutput -Path C:\debug\app.log | Format-Table .EXAMPLE # Registry activity only, for a specific process Get-MsixTraceOutput -Path .\app.log -ProcessId 8472 -FunctionPattern '^Reg' #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [int]$ProcessId, [string]$FunctionPattern ) if (-not (Test-Path $Path)) { throw "Trace log not found: $Path" } Get-Content -LiteralPath $Path | ConvertFrom-MsixTraceLine | Where-Object { $_ } | Where-Object { (-not $ProcessId -or $_.ProcessId -eq $ProcessId) -and (-not $FunctionPattern -or $_.Function -match $FunctionPattern) } } function Get-MsixTraceFailure { <# .SYNOPSIS Filters Get-MsixTraceOutput to only the rows whose Result indicates a failure (anything other than SUCCESS / NO_ERROR / ERROR_SUCCESS). .DESCRIPTION Convenience wrapper around Get-MsixTraceOutput that drops successful operations, leaving the rows most useful for diagnosing fixup needs. Feed the output into ConvertFrom-MsixTraceToFinding to produce the same finding shape that Get-MsixStaticAnalysis emits. .PARAMETER Path Trace log path. .PARAMETER ProcessId Optional filter forwarded to Get-MsixTraceOutput. .PARAMETER FunctionPattern Optional regex forwarded to Get-MsixTraceOutput. .OUTPUTS [pscustomobject] one per failing trace row. .EXAMPLE Get-MsixTraceFailure -Path .\app.log | Format-Table Function, Path, Result .EXAMPLE # Identify only registry-side failures Get-MsixTraceFailure -Path .\app.log -FunctionPattern '^Reg' #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [int]$ProcessId, [string]$FunctionPattern ) Get-MsixTraceOutput -Path $Path -ProcessId $ProcessId -FunctionPattern $FunctionPattern | Where-Object { $_.Result -and $_.Result -ne 'SUCCESS' -and $_.Result -ne 'NO_ERROR' -and $_.Result -ne 'ERROR_SUCCESS' } } function ConvertFrom-MsixTraceToFinding { <# .SYNOPSIS Converts trace failures into the same finding shape that Get-MsixStaticAnalysis emits, so they can be merged into a compatibility report. .DESCRIPTION Maps observed paths/results to the appropriate fixup category: - Path under System32/SysWOW64 => WorkingDirectory - Path under WindowsApps + write/del => FileRedirectionFixup - Registry HKLM + access denied => RegLegacyFixups - LoadLibrary failure => DynamicLibraryFixup (manual) Findings are deduplicated by (Category + leaf path). Rows that don't fit any category are dropped. .PARAMETER Failures Output of Get-MsixTraceFailure. Accepts pipeline input. .OUTPUTS [pscustomobject] one per finding, with Severity, Category, Symptom, Recommendation, AppId, Evidence -- the same shape used elsewhere by Get-MsixStaticAnalysis / Invoke-MsixInvestigation. .EXAMPLE # Saved DebugView trace -> structured findings -> investigation report Get-MsixTraceFailure -Path .\app.log | ConvertFrom-MsixTraceToFinding | Invoke-MsixInvestigation -PackagePath .\app.msix .EXAMPLE Get-MsixTraceFailure -Path .\app.log | ConvertFrom-MsixTraceToFinding | Where-Object Category -eq 'FileRedirectionFixup' #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [psobject[]]$Failures ) BEGIN { $seen = @{} $out = New-Object System.Collections.Generic.List[object] } PROCESS { foreach ($f in $Failures) { if (-not $f) { continue } $category, $reason = switch -Regex ($f.Path) { '\\(System32|SysWOW64)\\' { 'WorkingDirectory', 'App reads files from CWD which defaults to System32; set workingDirectory.' } 'WindowsApps' { 'FileRedirectionFixup', 'App writes inside Program Files\WindowsApps (read-only).' } '^HK(LM|EY_LOCAL_MACHINE)\\' { 'RegLegacyFixups', 'App requests write/full access to HKLM keys.' } default { $null, $null } } if (-not $category -and $f.Function -match '^LoadLibrary' -and $f.Result -ne 'SUCCESS') { $category = 'DynamicLibraryFixup' $reason = 'Library could not be loaded; consider DynamicLibraryFixup.' } if (-not $category) { continue } $leaf = Split-Path $f.Path -Leaf -ErrorAction SilentlyContinue $key = "$category|$leaf" if ($seen.ContainsKey($key)) { continue } $seen[$key] = $true $out.Add([pscustomobject]@{ Severity = 'Error' Category = $category Symptom = "$($f.Function) on '$($f.Path)' returned $($f.Result)" Recommendation = $reason AppId = $null Evidence = "$($f.Function) [$($f.ProcessId):$($f.ThreadId)]" }) } } END { $out } } # Backward-compatible plural aliases Set-Alias Get-MsixTraceFailures Get-MsixTraceFailure Set-Alias ConvertFrom-MsixTraceToFindings ConvertFrom-MsixTraceToFinding |