Private/Get-MftOffline.ps1
|
<#
.SYNOPSIS Parses an acquired NTFS $MFT file or an injected offline record source (private). .DESCRIPTION Streams FILE records through the MftNative parser with mandatory fixup validation, separates not-in-use previous-existence evidence, resolves sequence-checked paths, and optionally writes a timestamp-sorted timeline CSV with bounded memory use. .NOTES This helper performs pure .NET IO and is available on Windows, macOS, and Linux. #> function ConvertTo-MftOfflineRecord { [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)] [object]$OfflineRecord ) $record = ConvertTo-MftNativeRecord -NativeRecord $OfflineRecord.Record $record.Source = 'OfflineMft' $record | Add-Member -MemberType NoteProperty -Name BestEffortPath -Value $OfflineRecord.BestEffortPath $record | Add-Member -MemberType NoteProperty -Name PathStatus -Value ([string]$OfflineRecord.PathStatus) $record | Add-Member -MemberType NoteProperty -Name EvidenceType -Value $(if ($OfflineRecord.EvidenceNote) { 'PreviousExistence' } else { 'CurrentRecord' }) $record | Add-Member -MemberType NoteProperty -Name EvidenceNote -Value $OfflineRecord.EvidenceNote $record | Add-Member -MemberType NoteProperty -Name RecordBytes -Value $OfflineRecord.RecordBytes return $record } function Get-MftOffline { [CmdletBinding(DefaultParameterSetName = 'File')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true, ParameterSetName = 'File')] [ValidateNotNullOrEmpty()] [string]$MftPath, [Parameter(Mandatory = $true, ParameterSetName = 'RecordSource')] [ValidateNotNull()] [scriptblock]$RecordSource, [Parameter(Mandatory = $true, ParameterSetName = 'RecordSource')] [ValidateScript({ $_ -ge 0 })] [long]$SourceLength, [Parameter(ParameterSetName = 'RecordSource')] [ValidateNotNull()] [scriptblock]$ElapsedClock = { [long]0 }, [Parameter()] [ValidateRange(0, 65536)] [int]$RecordSizeBytes = 0, [Parameter()] [ValidateRange(1, 65536)] [int]$BytesPerSector = 512, [Parameter()] [ValidateRange(1, 1000000)] [int]$MaxRecords = 50000, [Parameter()] [ValidateRange(1, 86400)] [int]$MaxSeconds = 300, [Parameter()] [switch]$IncludeDeleted = $true, [Parameter()] [switch]$IncludeAll, [Parameter()] [string]$TimelineCsvPath ) $nativeResult = $null $failureReason = 'InvocationFailed' $failureMessage = $null try { if (-not (Initialize-MftNativeType)) { $failureReason = 'TypeInitializationFailed' throw "Native MFT type initialization failed: $script:MftNativeTypeError" } if ($PSCmdlet.ParameterSetName -eq 'RecordSource') { $reader = [MftNative+MftOfflineRecordReader]$RecordSource $clock = [MftNative+MftElapsedClock]$ElapsedClock $nativeResult = [MftNative]::AnalyzeOffline( $SourceLength, $RecordSizeBytes, $BytesPerSector, $MaxRecords, $MaxSeconds, [bool]$IncludeDeleted, [bool]$IncludeAll, $TimelineCsvPath, $reader, $clock) } else { $nativeResult = [MftNative]::ReadOffline( $MftPath, $RecordSizeBytes, $BytesPerSector, $MaxRecords, $MaxSeconds, [bool]$IncludeDeleted, [bool]$IncludeAll, $TimelineCsvPath) } } catch { $failureMessage = "Offline MFT invocation failed: $($_.Exception.Message)" } if ($null -eq $nativeResult) { return [PSCustomObject]@{ Records = @() PreviousRecords = @() Success = $false Error = $failureMessage Source = 'OfflineMft' Notes = @($failureMessage) Incomplete = $false Truncated = $false StopReason = $failureReason RecordsScanned = [long]0 DeletedCount = 0 SuspectCount = 0 UnresolvedPathCount = 0 MalformedRecords = 0 AttributeListRecords = 0 MissingStandardInformation = 0 MissingFileName = 0 ExtensionRecordsSkipped = 0 EstimatedMftRecords = [long]0 SourceLength = if ($PSCmdlet.ParameterSetName -eq 'RecordSource') { $SourceLength } else { [long]0 } RecordSizeBytes = $RecordSizeBytes BytesPerFileRecord = $RecordSizeBytes BytesPerSector = $BytesPerSector PartialTrailingBytes = 0 TimelineCsvPath = $TimelineCsvPath TimelineRowCount = [long]0 MaxRecords = $MaxRecords MaxSeconds = $MaxSeconds IncludeDeleted = [bool]$IncludeDeleted IncludeAll = [bool]$IncludeAll ElapsedMilliseconds = [long]0 } } $records = @($nativeResult.Records | ForEach-Object { ConvertTo-MftOfflineRecord -OfflineRecord $_ }) $previousRecords = @($nativeResult.PreviousRecords | ForEach-Object { ConvertTo-MftOfflineRecord -OfflineRecord $_ }) return [PSCustomObject]@{ Records = $records PreviousRecords = $previousRecords Success = [bool]$nativeResult.Success Error = if ($null -ne $nativeResult.Error) { [string]$nativeResult.Error } else { $null } Source = 'OfflineMft' Notes = @($nativeResult.Notes | ForEach-Object { [string]$_ }) Incomplete = [bool]$nativeResult.Incomplete Truncated = [bool]$nativeResult.Truncated StopReason = [string]$nativeResult.StopReason RecordsScanned = [long]$nativeResult.RecordsScanned DeletedCount = [int]$nativeResult.DeletedCount SuspectCount = [int]$nativeResult.SuspectCount UnresolvedPathCount = [int]$nativeResult.UnresolvedPathCount MalformedRecords = [int]$nativeResult.MalformedRecords AttributeListRecords = [int]$nativeResult.AttributeListRecords MissingStandardInformation = [int]$nativeResult.MissingStandardInformation MissingFileName = [int]$nativeResult.MissingFileName ExtensionRecordsSkipped = [int]$nativeResult.ExtensionRecordsSkipped EstimatedMftRecords = [long]$nativeResult.EstimatedMftRecords SourceLength = [long]$nativeResult.SourceLength RecordSizeBytes = [int]$nativeResult.RecordSizeBytes BytesPerFileRecord = [int]$nativeResult.BytesPerFileRecord BytesPerSector = [int]$nativeResult.BytesPerSector PartialTrailingBytes = [int]$nativeResult.PartialTrailingBytes TimelineCsvPath = if ($null -ne $nativeResult.TimelineCsvPath) { [string]$nativeResult.TimelineCsvPath } else { $null } TimelineRowCount = [long]$nativeResult.TimelineRowCount MaxRecords = [int]$nativeResult.MaxRecords MaxSeconds = [int]$nativeResult.MaxSeconds IncludeDeleted = [bool]$nativeResult.IncludeDeleted IncludeAll = [bool]$nativeResult.IncludeAll ElapsedMilliseconds = [long]$nativeResult.ElapsedMilliseconds } } |