Forensics/Get-FileSystemJournal.ps1

function Get-FileSystemJournal {
    <#
    .SYNOPSIS
        Parses NTFS USN change journal and file-system metadata for forensic analysis
    .DESCRIPTION
        Reads the NTFS USN change journal via fsutil to surface file creation,
        deletion, and rename activity, summarizes MFT statistics from volume
        metadata, and scans targeted paths for timestomp indicators by comparing
        StandardInformation timestamps against journal creation records.
        Raw $MFT record carving is out of scope; this function operates at the
        summary level exposed by fsutil.
    .PARAMETER DriveLetter
        NTFS volume to analyze (default: C)
    .PARAMETER Mode
        Analysis mode: USNJournal, MFTSummary, or TimestompScan (default: USNJournal)
    .PARAMETER MaxRecords
        Maximum journal records to parse or files to scan (default: 10000)
    .PARAMETER ScanPaths
        Paths scanned in TimestompScan mode (default: user Desktop, Downloads, Temp)
    .PARAMETER OutputPath
        Optional CSV file path for exporting collected records
    .EXAMPLE
        Get-FileSystemJournal -DriveLetter C -Mode USNJournal -MaxRecords 5000
    .EXAMPLE
        Get-FileSystemJournal -Mode TimestompScan -ScanPaths "C:\Users\victim\Downloads" -OutputPath "C:\IR\timestomp.csv"
    .OUTPUTS
        PSCustomObject with Mode, Records, Summary, Notes, and Recommendations
    #>


    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$DriveLetter = 'C',

        [Parameter()]
        [ValidateSet('USNJournal', 'MFTSummary', 'TimestompScan')]
        [string]$Mode = 'USNJournal',

        [Parameter()]
        [ValidateRange(1, 1000000)]
        [int]$MaxRecords = 10000,

        [Parameter()]
        [string[]]$ScanPaths,

        [Parameter()]
        [string]$OutputPath
    )

    begin {
        Write-Host "========================================" -ForegroundColor Cyan
        Write-Host " FILE SYSTEM JOURNAL FORENSICS" -ForegroundColor Cyan
        Write-Host " USN Journal / MFT / Timestomp Analysis" -ForegroundColor Cyan
        Write-Host "========================================" -ForegroundColor Cyan
        Write-Host "Mode: $Mode" -ForegroundColor Yellow
        Write-Host "Drive: $DriveLetter" -ForegroundColor Yellow
        Write-Host "Max Records: $MaxRecords" -ForegroundColor Yellow
        Write-Host "Start Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Yellow

        $drive = $DriveLetter.Trim().TrimEnd(':', '\').ToUpper()

        $isWindowsPlatform = ($env:OS -eq 'Windows_NT')

        $isAdmin = $false
        if ($isWindowsPlatform) {
            $isAdmin = Test-JournalElevation
        }

        $result = [PSCustomObject]@{
            CollectionDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            ComputerName   = $env:COMPUTERNAME
            DriveLetter    = $drive
            Mode           = $Mode
            IsElevated     = $isAdmin
            Records        = @()
            Summary        = @{}
            Notes          = @()
            Recommendations = @()
        }

        if (-not $isWindowsPlatform) {
            Write-Warning "Get-FileSystemJournal requires Windows (NTFS volume + fsutil.exe). Current platform is not Windows."
        }
        elseif (-not $isAdmin) {
            Write-Warning "Administrator privileges are required to read the USN journal and volume metadata. Some sections will be skipped."
        }

        if ($Mode -eq 'TimestompScan' -and (-not $ScanPaths -or @($ScanPaths).Count -eq 0)) {
            $defaultPaths = @()
            if ($env:USERPROFILE) {
                $defaultPaths += (Join-Path $env:USERPROFILE 'Desktop')
                $defaultPaths += (Join-Path $env:USERPROFILE 'Downloads')
            }
            if ($env:TEMP) {
                $defaultPaths += $env:TEMP
            }
            $ScanPaths = $defaultPaths
        }
    }

    process {
        try {
            if (-not $isWindowsPlatform) {
                $result.Notes += "Platform check failed: NTFS journal and fsutil.exe are only available on Windows. No data was collected."
                $result.Recommendations += "Run this function on a Windows host against an NTFS volume."
            }
            else {
                switch ($Mode) {

                    'USNJournal' {
                        Write-Host "`n[*] Reading USN change journal on $drive`:..." -ForegroundColor Cyan

                        try {
                            $rawLines = @(Get-FsutilOutput -Arguments @('usn', 'readjournal', "$drive`:"))
                            $fsutilExitCode = $script:FsutilLastExitCode

                            if ($fsutilExitCode -ne 0) {
                                if ($fsutilExitCode -eq 1 -or $fsutilExitCode -eq 5) {
                                    Write-Warning "fsutil denied access to the USN journal (exit code $fsutilExitCode); fsutil requires elevation or access denied. Rerun from an elevated prompt."
                                    $result.Notes += "USN journal read failed: fsutil requires elevation or access denied."
                                }
                                else {
                                    Write-Warning "fsutil usn readjournal failed with exit code $fsutilExitCode."
                                    $result.Notes += "USN journal read failed: fsutil exited with code $fsutilExitCode."
                                }
                            }
                            else {
                                $journalRecords = @(ConvertFrom-UsnJournalOutput -Lines $rawLines -MaxRecords $MaxRecords)
                                Write-Host " Parsed $($journalRecords.Count) journal records" -ForegroundColor Green

                                $resolveCap = 250
                                $resolveCount = 0
                                $pathCache = @{}
                                foreach ($record in $journalRecords) {
                                    if ($null -eq $record.FileReferenceNumber) { continue }
                                    if ($pathCache.ContainsKey($record.FileReferenceNumber)) {
                                        $record.FullPath = $pathCache[$record.FileReferenceNumber]
                                        continue
                                    }
                                    if ($resolveCount -ge $resolveCap) { continue }
                                    $resolveCount++
                                    $resolvedPath = Resolve-UsnJournalPath -DriveLetter $drive -FileReferenceNumber $record.FileReferenceNumber
                                    $pathCache[$record.FileReferenceNumber] = $resolvedPath
                                    if ($resolvedPath) {
                                        $record.FullPath = $resolvedPath
                                    }
                                }
                                Write-Verbose "Resolved full paths for $resolveCount unique file reference numbers (cap $resolveCap)."
                                if ($journalRecords.Count -gt $resolveCap) {
                                    $result.Notes += "Full-path resolution was capped at $resolveCap unique file reference numbers; the USN journal stores names, not paths, so remaining records have no directory context."
                                }

                                $result.Records = $journalRecords

                                $createdRecords = @($journalRecords | Where-Object { $_.ReasonFlags -contains 'FILE_CREATE' })
                                $deletedCount = @($journalRecords | Where-Object { $_.ReasonFlags -contains 'FILE_DELETE' }).Count
                                $renamedCount = @($journalRecords | Where-Object { $_.ReasonFlags -contains 'RENAME_OLD_NAME' }).Count

                                $executableExtensions = @('.exe', '.dll', '.ps1', '.bat')
                                $executableCreations = @($createdRecords | Where-Object {
                                    $null -ne $_.FileName -and
                                    $executableExtensions -contains ([System.IO.Path]::GetExtension($_.FileName)).ToLower()
                                })

                                $topDirectories = @($journalRecords |
                                    Where-Object { $_.FullPath } |
                                    ForEach-Object {
                                        try { Split-Path -Path $_.FullPath -Parent -ErrorAction Stop } catch { $null }
                                    } |
                                    Where-Object { $_ } |
                                    Group-Object |
                                    Sort-Object Count -Descending |
                                    Select-Object -First 10 |
                                    ForEach-Object { [PSCustomObject]@{ Directory = $_.Name; ChangeCount = $_.Count } })

                                if ($topDirectories.Count -eq 0) {
                                    $result.Notes += "Top-directory ranking unavailable: journal records could not be resolved to full paths (deleted files and non-elevated runs commonly fail resolution)."
                                }

                                $result.Summary = @{
                                    TotalRecords        = $journalRecords.Count
                                    FilesCreated        = $createdRecords.Count
                                    FilesDeleted        = $deletedCount
                                    FilesRenamed        = $renamedCount
                                    ExecutableCreations = @($executableCreations | ForEach-Object {
                                        [PSCustomObject]@{
                                            FileName    = $_.FileName
                                            FullPath    = $_.FullPath
                                            TimeStamp   = if ($_.TimeStamp) { $_.TimeStamp.ToString('yyyy-MM-dd HH:mm:ss') } else { $_.TimeStampText }
                                            ReasonFlags = @($_.ReasonFlags) -join '|'
                                        }
                                    })
                                    TopDirectories      = $topDirectories
                                }

                                $result.Notes += "Journal output is text-parsed from 'fsutil usn readjournal'; fields are limited to what fsutil prints (file name, reason bitmask, time stamp, USN, file reference number)."
                                $result.Notes += "The journal is circular; records shown cover only the window fsutil returned and older history may already be overwritten."

                                $result.Recommendations += "Review executable creation events for unexpected binaries consistent with ingress tool transfer (MITRE ATT&CK T1105)."
                                $result.Recommendations += "Export and preserve the journal promptly during incident response before rollover destroys older records."
                                $result.Recommendations += "Correlate FILE_CREATE/RENAME sequences with process and network telemetry before attributing malicious intent."

                                Write-Host " Files created: $($createdRecords.Count), deleted: $deletedCount, renamed: $renamedCount" -ForegroundColor Gray
                                if ($executableCreations.Count -gt 0) {
                                    Write-Host "`n Executable creation events (T1105-style evidence):" -ForegroundColor Yellow
                                    foreach ($exe in $executableCreations) {
                                        $exeTime = if ($exe.TimeStamp) { $exe.TimeStamp.ToString('yyyy-MM-dd HH:mm:ss') } else { $exe.TimeStampText }
                                        $exePath = if ($exe.FullPath) { $exe.FullPath } else { $exe.FileName }
                                        Write-Host " [$exeTime] $exePath" -ForegroundColor Red
                                    }
                                }
                                if ($topDirectories.Count -gt 0) {
                                    Write-Host "`n Top directories by change count:" -ForegroundColor Gray
                                    foreach ($dir in $topDirectories) {
                                        Write-Host " $($dir.ChangeCount)x $($dir.Directory)" -ForegroundColor Gray
                                    }
                                }
                            }
                        }
                        catch {
                            Write-Warning "Failed to read or parse the USN journal - $_"
                            $result.Notes += "USN journal collection failed: $_"
                        }
                    }

                    'MFTSummary' {
                        Write-Host "`n[*] Collecting MFT summary for $drive`:..." -ForegroundColor Cyan

                        try {
                            $ntfsLines = @(Get-FsutilOutput -Arguments @('fsinfo', 'ntfsinfo', "$drive`:"))
                            $fsutilExitCode = $script:FsutilLastExitCode

                            if ($fsutilExitCode -ne 0) {
                                if ($fsutilExitCode -eq 1 -or $fsutilExitCode -eq 5) {
                                    Write-Warning "fsutil denied access to volume metadata (exit code $fsutilExitCode); fsutil requires elevation or access denied. Rerun from an elevated prompt."
                                    $result.Notes += "MFT summary collection failed: fsutil requires elevation or access denied."
                                }
                                else {
                                    Write-Warning "fsutil fsinfo ntfsinfo failed with exit code $fsutilExitCode."
                                    $result.Notes += "MFT summary collection failed: fsutil exited with code $fsutilExitCode."
                                }
                            }
                            else {
                                $volumeInfo = @{}
                                foreach ($line in $ntfsLines) {
                                    if ($null -eq $line -or $line -match '^\s*$') { continue }
                                    if ($line -match '^\s*(?<key>[^:=]+?)\s*:\s*(?<value>.*)$') {
                                        $volumeInfo[$Matches.key.Trim()] = $Matches.value.Trim()
                                    }
                                    else {
                                        Write-Verbose "Skipping unrecognized ntfsinfo line: $line"
                                    }
                                }

                                $mftValidBytes = $null
                                if ($volumeInfo.Count -gt 0) {
                                    $expectedFields = @('Mft Valid Data Length', 'Bytes Per FileRecord Segment', 'Mft Start Lcn', 'Mft2 Start Lcn', 'Mft Zone Start', 'Mft Zone End')
                                    $matchedFields = @($expectedFields | Where-Object { $volumeInfo.ContainsKey($_) })
                                    if ($matchedFields.Count -eq 0) {
                                        Write-Warning "fsutil ntfsinfo returned data but none of the expected volume fields were recognized; localized fsutil output cannot be mapped (English locale required). MFT summary values will be null."
                                        $result.Notes += "MFT summary values unavailable: localized fsutil ntfsinfo output cannot be parsed; English locale is required."
                                    }
                                }
                                if ($volumeInfo.ContainsKey('Mft Valid Data Length')) {
                                    $mftValidBytes = ConvertFrom-JournalNumericText -Text $volumeInfo['Mft Valid Data Length']
                                }
                                $recordSizeBytes = $null
                                if ($volumeInfo.ContainsKey('Bytes Per FileRecord Segment')) {
                                    $recordSizeBytes = ConvertFrom-JournalNumericText -Text $volumeInfo['Bytes Per FileRecord Segment']
                                }

                                $estimatedRecords = $null
                                if ($null -ne $mftValidBytes -and $null -ne $recordSizeBytes -and $recordSizeBytes -gt 0) {
                                    $estimatedRecords = [Math]::Floor($mftValidBytes / $recordSizeBytes)
                                }

                                $infoRecords = @($volumeInfo.GetEnumerator() | Sort-Object Name | ForEach-Object {
                                    [PSCustomObject]@{ Property = $_.Key; Value = $_.Value }
                                })
                                $result.Records = $infoRecords

                                $result.Summary = @{
                                    MftSizeBytes           = $mftValidBytes
                                    MftSizeMB              = if ($null -ne $mftValidBytes) { [Math]::Round($mftValidBytes / 1MB, 2) } else { $null }
                                    MftRecordSizeBytes     = $recordSizeBytes
                                    EstimatedMftRecords    = $estimatedRecords
                                    MftStartLcn            = if ($volumeInfo.ContainsKey('Mft Start Lcn')) { $volumeInfo['Mft Start Lcn'] } else { $null }
                                    Mft2StartLcn           = if ($volumeInfo.ContainsKey('Mft2 Start Lcn')) { $volumeInfo['Mft2 Start Lcn'] } else { $null }
                                    MftZoneStart           = if ($volumeInfo.ContainsKey('Mft Zone Start')) { $volumeInfo['Mft Zone Start'] } else { $null }
                                    MftZoneEnd             = if ($volumeInfo.ContainsKey('Mft Zone End')) { $volumeInfo['Mft Zone End'] } else { $null }
                                    PropertiesReported     = $infoRecords.Count
                                }

                                $result.Notes += "Summary-level view only: values come from 'fsutil fsinfo ntfsinfo' volume metadata."
                                $result.Notes += "Raw `$MFT record parsing (per-entry `$SI/`$FN attribute decoding) requires direct disk access or third-party libraries and is intentionally out of scope for this function."

                                $result.Recommendations += "For per-record `$MFT analysis, pair this summary with a dedicated parser (e.g., MFTECmd) run against an acquired `$MFT copy."
                                $result.Recommendations += "A large or rapidly growing MFT valid data length relative to expectations can indicate mass file creation (staging or encryption activity); baseline before alerting."

                                if ($null -ne $mftValidBytes) {
                                    Write-Host " MFT valid data: $([Math]::Round($mftValidBytes / 1MB, 2)) MB" -ForegroundColor Green
                                }
                                if ($null -ne $estimatedRecords) {
                                    Write-Host " Estimated MFT records: $estimatedRecords" -ForegroundColor Green
                                }
                            }
                        }
                        catch {
                            Write-Warning "Failed to collect MFT summary - $_"
                            $result.Notes += "MFT summary collection failed: $_"
                        }
                    }

                    'TimestompScan' {
                        Write-Host "`n[*] Scanning for timestomp indicators..." -ForegroundColor Cyan

                        $creationRecords = @()
                        if ($isAdmin) {
                            try {
                                Write-Host " Reading USN journal for creation-record comparison..." -ForegroundColor Gray
                                $rawLines = @(Get-FsutilOutput -Arguments @('usn', 'readjournal', "$drive`:"))
                                $fsutilExitCode = $script:FsutilLastExitCode
                                if ($fsutilExitCode -ne 0) {
                                    if ($fsutilExitCode -eq 1 -or $fsutilExitCode -eq 5) {
                                        Write-Warning "fsutil denied access to the USN journal (exit code $fsutilExitCode); fsutil requires elevation or access denied; journal comparison will be skipped."
                                        $result.Notes += "USN journal comparison skipped: fsutil requires elevation or access denied."
                                    }
                                    else {
                                        Write-Warning "fsutil usn readjournal failed with exit code $fsutilExitCode; journal comparison will be skipped."
                                        $result.Notes += "USN journal comparison skipped: fsutil exited with code $fsutilExitCode."
                                    }
                                }
                                else {
                                    $creationRecords = @(ConvertFrom-UsnJournalOutput -Lines $rawLines -MaxRecords $MaxRecords |
                                        Where-Object { $_.ReasonFlags -contains 'FILE_CREATE' -and $null -ne $_.TimeStamp })
                                    Write-Host " Loaded $($creationRecords.Count) journal creation records" -ForegroundColor Gray
                                }
                            }
                            catch {
                                Write-Warning "Could not read the USN journal for comparison - $_"
                                $result.Notes += "USN journal comparison skipped: $_"
                            }
                        }
                        else {
                            $result.Notes += "USN journal comparison skipped: elevation required. Only pre-2000/future timestamp anomalies are reported."
                        }

                        $filesScanned = 0
                        $suspicious = @()

                        foreach ($scanPath in @($ScanPaths)) {
                            if ($null -eq $scanPath) { continue }
                            if (-not (Test-Path $scanPath)) {
                                Write-Verbose "Scan path not found: $scanPath"
                                continue
                            }

                            try {
                                Write-Host " Scanning: $scanPath..." -ForegroundColor Gray

                                $files = @(Get-ChildItem -Path $scanPath -Recurse -File -ErrorAction SilentlyContinue |
                                    Select-Object -First $MaxRecords)

                                foreach ($file in $files) {
                                    if ($filesScanned -ge $MaxRecords) { break }
                                    $filesScanned++

                                    $evaluation = Test-TimestompSignature -FileInfo $file -CreationRecords $creationRecords

                                    if ($evaluation.Indicators.Count -gt 0) {
                                        $suspicious += [PSCustomObject]@{
                                            FileName            = $file.Name
                                            FullPath            = $file.FullName
                                            LastWriteTime       = $file.LastWriteTime
                                            CreationTime        = $file.CreationTime
                                            JournalCreationTime = $evaluation.JournalCreationTime
                                            Indicators          = $evaluation.Indicators
                                            Assessment          = 'Heuristic finding - validate with $MFT $FN timestamps before concluding timestomping'
                                        }
                                    }
                                }

                                Write-Host " Scanned $($files.Count) files" -ForegroundColor Gray
                            }
                            catch {
                                Write-Warning "Could not scan $scanPath - $_"
                            }
                        }

                        $result.Records = $suspicious

                        $backdatedCount = @($suspicious | Where-Object { $_.Indicators -contains 'BACKDATED_SI_TIMESTAMP' }).Count
                        $pre2000Count = @($suspicious | Where-Object { $_.Indicators -contains 'PRE_2000_TIMESTAMP' }).Count
                        $futureCount = @($suspicious | Where-Object { $_.Indicators -contains 'FUTURE_TIMESTAMP' }).Count

                        $result.Summary = @{
                            FilesScanned            = $filesScanned
                            SuspiciousFiles         = $suspicious.Count
                            BackdatedSiCount        = $backdatedCount
                            Pre2000TimestampCount   = $pre2000Count
                            FutureTimestampCount    = $futureCount
                            JournalCreationRecords  = $creationRecords.Count
                            ScanPaths               = @($ScanPaths)
                        }

                        $result.Notes += "Timestomp detection is a HEURISTIC: a `$SI LastWriteTime earlier than the file's USN journal creation record is a classic timestomp signature, but legitimate tools (archivers, installers, backup restore) also rewrite timestamps."
                        $result.Notes += "Journal records are matched to scanned files by full path when resolvable, otherwise by file name only; name-only matches can produce false positives for files sharing a name in different directories."

                        $result.Recommendations += "Validate flagged files against `$MFT `$FN attribute timestamps before concluding anti-forensic activity (MITRE ATT&CK T1070.006)."
                        $result.Recommendations += "Preserve flagged files and the volume journal before remediation; re-scanning after cleanup destroys evidence."

                        Write-Host "`n Files scanned: $filesScanned" -ForegroundColor Green
                        Write-Host " Suspicious files: $($suspicious.Count)" -ForegroundColor $(if ($suspicious.Count -gt 0) { 'Yellow' } else { 'Green' })

                        if ($suspicious.Count -gt 0) {
                            Write-Host "`n Timestomp indicators (heuristic):" -ForegroundColor Yellow
                            foreach ($hit in ($suspicious | Select-Object -First 25)) {
                                Write-Host " $($hit.FullPath)" -ForegroundColor Red
                                Write-Host " `$SI LastWriteTime: $($hit.LastWriteTime) | Journal creation: $($hit.JournalCreationTime) | $($hit.Indicators -join ', ')" -ForegroundColor Gray
                            }
                            if ($suspicious.Count -gt 25) {
                                Write-Host " ... and $($suspicious.Count - 25) more (see Records / CSV export)" -ForegroundColor Gray
                            }
                        }
                    }
                }

                if ($OutputPath) {
                    Write-Host "`n[*] Exporting records to CSV..." -ForegroundColor Cyan

                    try {
                        $csvPath = $OutputPath
                        if ($csvPath -notmatch '(?i)\.csv$') {
                            $csvPath = "$csvPath.csv"
                        }
                        $parentDir = Split-Path -Path $csvPath -Parent
                        if ($parentDir -and -not (Test-Path $parentDir)) {
                            New-Item -Path $parentDir -ItemType Directory -Force | Out-Null
                        }

                        $exportRows = @($result.Records | ForEach-Object {
                            $row = $_
                            $flattened = [ordered]@{}
                            foreach ($prop in $row.PSObject.Properties) {
                                $value = $prop.Value
                                if ($null -ne $value -and $value -is [System.Collections.IEnumerable] -and $value -isnot [string]) {
                                    $flattened[$prop.Name] = (@($value) -join '|')
                                }
                                elseif ($value -is [datetime]) {
                                    $flattened[$prop.Name] = $value.ToString('yyyy-MM-dd HH:mm:ss')
                                }
                                elseif ($value -is [string] -and $value -match '^[=+\-@\t\r\n]') {
                                    # Prefix formula-leading characters so attacker-controlled
                                    # file names cannot execute when the CSV is opened in a
                                    # spreadsheet application; in-memory records stay pristine
                                    $flattened[$prop.Name] = "'" + $value
                                }
                                else {
                                    $flattened[$prop.Name] = $value
                                }
                            }
                            [PSCustomObject]$flattened
                        })

                        @($exportRows) | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
                        Write-Host " CSV export saved: $csvPath" -ForegroundColor Green
                    }
                    catch {
                        Write-Warning "CSV export failed - $_"
                    }
                }
            }
        }
        catch {
            Write-Error "Error during file system journal analysis - $_"
            throw
        }
    }

    end {
        Write-Host "`n========================================" -ForegroundColor Green
        Write-Host " FILE SYSTEM JOURNAL ANALYSIS COMPLETE" -ForegroundColor Green
        Write-Host "========================================" -ForegroundColor Green

        Write-Host "`nMode: $($result.Mode)" -ForegroundColor Cyan
        Write-Host " Records collected: $(@($result.Records).Count)" -ForegroundColor White
        Write-Host " Elevated: $($result.IsElevated)" -ForegroundColor $(if ($result.IsElevated) { 'White' } else { 'Yellow' })

        if ($result.Notes.Count -gt 0) {
            Write-Host "`nCapability Notes:" -ForegroundColor Cyan
            foreach ($note in $result.Notes) {
                Write-Host " - $note" -ForegroundColor Gray
            }
        }

        if ($result.Recommendations.Count -gt 0) {
            Write-Host "`nRecommendations:" -ForegroundColor Yellow
            foreach ($recommendation in $result.Recommendations) {
                Write-Host " - $recommendation" -ForegroundColor Gray
            }
        }

        return $result
    }
}

function Get-FsutilOutput {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$Arguments
    )

    $output = @(& fsutil @Arguments 2>&1)
    $script:FsutilLastExitCode = $LASTEXITCODE
    return @($output | ForEach-Object { "$_" })
}

function Test-JournalElevation {
    [CmdletBinding()]
    param()

    try {
        return ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    }
    catch {
        return $false
    }
}

function ConvertFrom-UsnJournalOutput {
    [CmdletBinding()]
    param(
        [Parameter()]
        [AllowEmptyCollection()]
        [string[]]$Lines = @(),

        [Parameter()]
        [ValidateRange(1, 1000000)]
        [int]$MaxRecords = 10000
    )

    $records = @()
    $current = $null

    foreach ($line in $Lines) {
        if ($records.Count -ge $MaxRecords) { break }
        if ($null -eq $line -or $line -match '^\s*$') { continue }

        if ($line -match '(?i)USN_RECORD') {
            if ($null -ne $current) {
                $records += (New-UsnJournalRecord -Fields $current)
            }
            $current = @{}
            continue
        }

        if ($line -notmatch '^\s*(?<key>[^:=]+?)\s*:\s*(?<value>.*)$') {
            Write-Verbose "Skipping unrecognized journal line: $line"
            continue
        }

        $rawKey = $Matches.key.Trim()
        $value = $Matches.value.Trim()

        $normalizedKey = $null
        if ($rawKey -match '^(?i)file name$') { $normalizedKey = 'FileName' }
        elseif ($rawKey -match '^(?i)reason$') { $normalizedKey = 'Reason' }
        elseif ($rawKey -match '^(?i)time stamp$') { $normalizedKey = 'TimeStamp' }
        elseif ($rawKey -match '^(?i)usn$') { $normalizedKey = 'Usn' }
        elseif ($rawKey -match '^(?i)file reference number$') { $normalizedKey = 'FileReferenceNumber' }

        if ($null -eq $normalizedKey) {
            Write-Verbose "Skipping unrecognized journal field '$rawKey' in line: $line"
            continue
        }

        if ($null -eq $current) {
            $current = @{}
        }

        if ($current.ContainsKey($normalizedKey)) {
            $records += (New-UsnJournalRecord -Fields $current)
            if ($records.Count -ge $MaxRecords) { break }
            $current = @{}
        }

        $current[$normalizedKey] = $value
    }

    if ($null -ne $current -and $records.Count -lt $MaxRecords -and @($current.Keys).Count -gt 0) {
        $records += (New-UsnJournalRecord -Fields $current)
    }

    if ($records.Count -eq 0) {
        $contentLines = @($Lines | Where-Object { $null -ne $_ -and $_ -notmatch '^\s*$' })
        if ($contentLines.Count -gt 0) {
            Write-Warning "fsutil returned USN journal output but no records could be parsed; localized fsutil output is not supported (English locale required for USN journal parsing)."
        }
    }

    return @($records)
}

function New-UsnJournalRecord {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Fields
    )

    $fileName = if ($Fields.ContainsKey('FileName')) { $Fields['FileName'] } else { $null }
    $reasonText = if ($Fields.ContainsKey('Reason')) { $Fields['Reason'] } else { '' }
    $timeStampText = if ($Fields.ContainsKey('TimeStamp')) { $Fields['TimeStamp'] } else { $null }
    $usnText = if ($Fields.ContainsKey('Usn')) { $Fields['Usn'] } else { $null }
    $fileReference = if ($Fields.ContainsKey('FileReferenceNumber')) { $Fields['FileReferenceNumber'] } else { $null }

    $reasonMask = $null
    $maskMatch = [regex]::Match($reasonText, '0x(?<hex>[0-9A-Fa-f]+)')
    if ($maskMatch.Success) {
        try {
            $reasonMask = [Convert]::ToUInt32($maskMatch.Groups['hex'].Value, 16)
        }
        catch {
            $reasonMask = $null
        }
    }

    $reasonFlags = @()
    if ($null -ne $reasonMask) {
        $reasonFlags = @(Get-UsnReasonFlagNames -Mask $reasonMask)
    }
    if ($reasonFlags.Count -eq 0 -and $reasonText) {
        $reasonFlags = @(Get-UsnReasonFlagNames -ReasonText $reasonText)
    }

    $timeStamp = $null
    if ($timeStampText) {
        $parsedDate = [datetime]::MinValue
        if ([datetime]::TryParse($timeStampText, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None, [ref]$parsedDate)) {
            $timeStamp = $parsedDate
        }
        else {
            Write-Verbose "Could not parse journal time stamp '$timeStampText' with the invariant culture; localized fsutil timestamps require an English locale. TimeStamp is set to null."
        }
    }

    return [PSCustomObject]@{
        FileName            = $fileName
        FullPath            = $null
        Reason              = $reasonText
        ReasonMask          = $reasonMask
        ReasonFlags         = $reasonFlags
        TimeStamp           = $timeStamp
        TimeStampText       = $timeStampText
        Usn                 = $usnText
        FileReferenceNumber = $fileReference
    }
}

function Get-UsnReasonFlagNames {
    [CmdletBinding()]
    param(
        [Parameter()]
        [uint32]$Mask = 0,

        [Parameter()]
        [string]$ReasonText
    )

    $flagMap = [ordered]@{
        'DATA_OVERWRITE'        = [uint32]0x00000001
        'DATA_EXTEND'           = [uint32]0x00000002
        'DATA_TRUNCATION'       = [uint32]0x00000004
        'NAMED_DATA_OVERWRITE'  = [uint32]0x00000010
        'NAMED_DATA_EXTEND'     = [uint32]0x00000020
        'NAMED_DATA_TRUNCATION' = [uint32]0x00000040
        'FILE_CREATE'           = [uint32]0x00000100
        'FILE_DELETE'           = [uint32]0x00000200
        'EA_CHANGE'             = [uint32]0x00000400
        'SECURITY_CHANGE'       = [uint32]0x00000800
        'RENAME_OLD_NAME'       = [uint32]0x00001000
        'RENAME_NEW_NAME'       = [uint32]0x00002000
        'INDEXABLE_CHANGE'      = [uint32]0x00004000
        'BASIC_INFO_CHANGE'     = [uint32]0x00008000
        'HARD_LINK_CHANGE'      = [uint32]0x00010000
        'COMPRESSION_CHANGE'    = [uint32]0x00020000
        'ENCRYPTION_CHANGE'     = [uint32]0x00040000
        'OBJECT_ID_CHANGE'      = [uint32]0x00080000
        'REPARSE_POINT_CHANGE'  = [uint32]0x00100000
        'STREAM_CHANGE'         = [uint32]0x00200000
        'CLOSE'                 = [uint32]2147483648
    }

    $names = @()

    if ($Mask -ne 0) {
        foreach ($flagName in $flagMap.Keys) {
            if (($Mask -band $flagMap[$flagName]) -eq $flagMap[$flagName]) {
                $names += $flagName
            }
        }
    }
    elseif ($ReasonText) {
        $haystack = ($ReasonText.ToUpper() -replace '[^A-Z0-9]', '')
        foreach ($flagName in $flagMap.Keys) {
            $needle = $flagName -replace '_', ''
            if ($haystack.Contains($needle)) {
                $names += $flagName
            }
        }
    }

    return @($names)
}

function Resolve-UsnJournalPath {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$DriveLetter = 'C',

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$FileReferenceNumber
    )

    try {
        $drive = $DriveLetter.Trim().TrimEnd(':', '\').ToUpper()
        $output = @(Get-FsutilOutput -Arguments @('file', 'queryfilenamebyid', "$drive`:\", $FileReferenceNumber))

        foreach ($line in $output) {
            if ($null -ne $line -and $line -match '\\\\\?\\(?<path>.+?)\s*$') {
                return $Matches.path
            }
        }

        Write-Verbose "No path returned for file reference number $FileReferenceNumber (file may be deleted)."
    }
    catch {
        Write-Verbose "Path resolution failed for file reference number $FileReferenceNumber - $_"
    }

    return $null
}

function ConvertFrom-JournalNumericText {
    [CmdletBinding()]
    param(
        [Parameter()]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$Text
    )

    if ([string]::IsNullOrWhiteSpace($Text)) {
        return $null
    }

    $cleaned = $Text.Trim()
    try {
        if ($cleaned -match '0x(?<hex>[0-9A-Fa-f]+)') {
            return [Convert]::ToUInt64($Matches.hex, 16)
        }
        $digitsOnly = $cleaned -replace '[^0-9]', ''
        if ($digitsOnly) {
            return [Convert]::ToUInt64($digitsOnly, 10)
        }
    }
    catch {
        Write-Verbose "Could not parse numeric value: $Text"
    }

    return $null
}

function Test-TimestompSignature {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object]$FileInfo,

        [Parameter()]
        [AllowEmptyCollection()]
        [object[]]$CreationRecords = @()
    )

    $indicators = @()

    $lastWrite = $null
    if ($null -ne $FileInfo.PSObject.Properties['LastWriteTime'] -and $null -ne $FileInfo.LastWriteTime) {
        $lastWrite = $FileInfo.LastWriteTime
    }

    $fileName = $null
    if ($null -ne $FileInfo.PSObject.Properties['Name']) {
        $fileName = $FileInfo.Name
    }
    $fullName = $null
    if ($null -ne $FileInfo.PSObject.Properties['FullName']) {
        $fullName = $FileInfo.FullName
    }

    $journalCreationTime = $null
    foreach ($record in @($CreationRecords)) {
        if ($null -eq $record -or $null -eq $record.TimeStamp) { continue }

        $pathMatch = ($null -ne $record.FullPath -and $null -ne $fullName -and $record.FullPath -ieq $fullName)
        $nameMatch = ($null -eq $record.FullPath -and $null -ne $record.FileName -and $null -ne $fileName -and $record.FileName -ieq $fileName)

        if (($pathMatch -or $nameMatch) -and ($null -eq $journalCreationTime -or $record.TimeStamp -gt $journalCreationTime)) {
            $journalCreationTime = $record.TimeStamp
        }
    }

    if ($null -ne $lastWrite) {
        if ($null -ne $journalCreationTime -and $lastWrite -lt $journalCreationTime) {
            $indicators += 'BACKDATED_SI_TIMESTAMP'
        }
        if ($lastWrite -lt [datetime]'2000-01-01') {
            $indicators += 'PRE_2000_TIMESTAMP'
        }
        if ($lastWrite -gt (Get-Date)) {
            $indicators += 'FUTURE_TIMESTAMP'
        }
    }

    return [PSCustomObject]@{
        Indicators          = @($indicators)
        JournalCreationTime = $journalCreationTime
    }
}