Public/ConvertFrom-NSPProcmonLog.ps1

function ConvertFrom-NSPProcmonLog {
    <#
    .SYNOPSIS
        Reads a Process Monitor capture as events: a .pml is exported to CSV with ProcMon first
        (opens and closes of the files that had a sharing violation, unless -FilterPath says
        otherwise), a .csv is read as is.
 
    .DESCRIPTION
        Each event has Sequence (row order), TimeOfDay (a TimeSpan, 100 ns precision), Time (the
        text), ProcessName, PID, Operation, Path, Result and Detail - what Find-NSPMDECollision
        needs. The CSV must have ProcMon's default columns (Time of Day, Process Name, PID,
        Operation, Path, Result, Detail); reset ProcMon's columns if one is missing.
 
        Exporting runs: Procmon /OpenLog <pml> /LoadConfig <filter> /SaveApplyFilter /SaveAs <csv>,
        twice by default: first the sharing violations (<name>.violations.csv), then CreateFile
        and CloseFile on just the files that had one (<name>.csv) - a capture with no violations
        yields no events. More than 253 such files falls back to every open and close. The CSVs
        are written next to the .pml and reused if already there.
 
    .PARAMETER Path
        A .pml backing file or a .csv ProcMon export.
 
    .PARAMETER ProcmonPath
        Process Monitor executable (only for a .pml).
 
    .PARAMETER FilterPath
        Export once with this filter instead (New-NSPProcmonFilter), e.g. -Kind FileOpens for
        every open and close.
 
    .PARAMETER Force
        Export again even if the CSV exists.
 
    .EXAMPLE
        ConvertFrom-NSPProcmonLog -Path C:\Captures\capture.pml -ProcmonPath (Find-NSPProcmon) | Find-NSPMDECollision
 
    .EXAMPLE
        ConvertFrom-NSPProcmonLog -Path C:\Captures\Logfile.CSV
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path,
        [string]$ProcmonPath,
        [string]$FilterPath,
        [switch]$Force
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Not found: $Path" }
    $Path = (Resolve-Path -LiteralPath $Path).ProviderPath
    $csv = $Path
    if ([IO.Path]::GetExtension($Path) -ieq '.pml') {
        $csv = [IO.Path]::ChangeExtension($Path, '.csv')
        if ($Force -or -not (Test-Path -LiteralPath $csv)) {
            if (-not $ProcmonPath) { throw 'Exporting a .pml needs -ProcmonPath (Find-NSPProcmon).' }
            if (-not (Test-NSPProcmonSignature -Path $ProcmonPath)) { throw "$ProcmonPath is not validly signed by Microsoft - not running it." }
            $temp = Join-Path ([IO.Path]::GetTempPath()) ("NSP_{0}" -f [guid]::NewGuid().ToString('N'))
            $null = New-Item -ItemType Directory -Path $temp -Force
            try {
                if ($FilterPath) {
                    Export-NSPProcmonCsv -ProcmonPath $ProcmonPath -Path $Path -FilterPath $FilterPath -CsvPath $csv
                } else {
                    # Stage 1: the (few) violations, to learn which files matter. Stage 2: opens and
                    # closes on exactly those files - a busy minute is hundreds of thousands of
                    # opens, and reading them all back costs minutes.
                    $violationsCsv = [IO.Path]::ChangeExtension($Path, '.violations.csv')
                    $violations = (New-NSPProcmonFilter -Kind Violations -OutputPath (Join-Path $temp 'v.pmc') -WhatIf:$false).FullName
                    Export-NSPProcmonCsv -ProcmonPath $ProcmonPath -Path $Path -FilterPath $violations -CsvPath $violationsCsv
                    $paths = @(Import-Csv -LiteralPath $violationsCsv | ForEach-Object Path | Where-Object { $_ } | Sort-Object -Unique)
                    Write-Verbose "$($paths.Count) file(s) had a sharing violation."
                    if (-not $paths) {
                        Copy-Item -LiteralPath $violationsCsv -Destination $csv -Force
                    } else {
                        $kind = 'FileOpens'; $rules = @()
                        if ($paths.Count -le 253) {
                            $kind = 'Custom'
                            $rules = @(
                                [pscustomobject]@{ Column = 'Operation'; Relation = 'Is'; Value = 'CreateFile'; Action = 'Include' }
                                [pscustomobject]@{ Column = 'Operation'; Relation = 'Is'; Value = 'CloseFile'; Action = 'Include' }
                            ) + @($paths | ForEach-Object { [pscustomobject]@{ Column = 'Path'; Relation = 'Is'; Value = $_; Action = 'Include' } })
                        }
                        $opens = (New-NSPProcmonFilter -Kind $kind -Rule $rules -OutputPath (Join-Path $temp 'o.pmc') -WhatIf:$false).FullName
                        Export-NSPProcmonCsv -ProcmonPath $ProcmonPath -Path $Path -FilterPath $opens -CsvPath $csv
                    }
                }
            } finally {
                Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue
            }
        }
    }

    $required = 'Time of Day', 'Process Name', 'PID', 'Operation', 'Path', 'Result', 'Detail'
    $header = @((Get-Content -LiteralPath $csv -TotalCount 1) -split '","' | ForEach-Object { $_.Trim().Trim('"').TrimStart([char]0xFEFF) })
    $missing = @($required | Where-Object { $header -notcontains $_ })
    if ($missing) { throw "$csv is missing ProcMon column(s): $($missing -join ', '). Reset ProcMon's columns (Options > Select Columns) and export again." }

    $sequence = 0
    Import-Csv -LiteralPath $csv | ForEach-Object {
        $sequence++
        [pscustomobject]@{
            PSTypeName  = 'NSP.MDE.ProcmonEvent'
            Sequence    = $sequence
            TimeOfDay   = ConvertFrom-NSPProcmonTimeOfDay $_.'Time of Day'
            Time        = $_.'Time of Day'
            ProcessName = $_.'Process Name'
            PID         = $_.PID
            Operation   = $_.Operation
            Path        = $_.Path
            Result      = $_.Result
            Detail      = $_.Detail
        }
    }
}