<< Back to Script Library

FSLogix Log Parser

Parses the Profile log for FSLogix event for a specific user and displays the results.
Version: 7.6.27
Created: 2019-12-03
Modified: 2023-10-26
Creator: Trentent Tye
Downloads: 2681
Tags: fslogix logon duration vdi
The Script Copy Script Copied to clipboard
<#
    .SYNOPSIS
        Finds all FSLogix Profile events for a user's logon

    .DESCRIPTION
        Finds all FSLogix Profile events for a user's logon for review and troubleshooting.

    .EXAMPLE
        . .\Get-FSLogixProfileLog.ps1 -User BOTTHEORY\amttye -SessionId 2
        Gets all events for the user "amttye" from domain "bottheory" on this machine

    .EXAMPLE
        . .\Get-FSLogixProfileLog.ps1 -User BOTTHEORY\amttye -SessionId 2
        Gets all events for the user "amttye" from domain "bottheory" on this machine

    .PARAMETER User
        samAccountName in the format DOMAIN\samAccountName
            
    .PARAMETER SessionId
        the session id

    .NOTES
        This script must be run on a machine where the user is currently logged on.
        CONTEXT : Session
        MODIFICATION_HISTORY:
        Created TTYE : 2019-11-19
        Edit: Ton de Vreede 2022-9-15 - small bugfix for error handling
        AUTHOR: Trentent Tye
#>
[CmdLetBinding()]
Param (
    [Parameter(Mandatory=$true,HelpMessage='Enter the samAccountName in the format DOMAIN\samAccountName')][ValidateNotNullOrEmpty()]       [string]$User,
    [Parameter(Mandatory=$true,HelpMessage='Enter the session ID')][ValidateNotNullOrEmpty()]                                   [int]$SessionId
)


$ErrorActionPreference = "Stop"
###$VerbosePreference = "continue"

[int]$outputWidth = 1500
# Altering the size of the PS Buffer
$PSWindow = (Get-Host).UI.RawUI
$WideDimensions = $PSWindow.BufferSize
$WideDimensions.Width = $outputWidth
$PSWindow.BufferSize = $WideDimensions

$userdomain = ($user -split "\\")[0]
$samAccountName = ($user -split "\\")[1]

function Process-FSLogixLogFile {
    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$true)][String]$LogFile
    )

    Write-Verbose "Found Log file: $($LogFile)"
    $FSLogixLog = Get-Content "$($LogFile)"
    $logfilePath = Get-Item $LogFile

    $CurrentTimezone = ((Get-TimeZone).baseUtcOffset)
    Write-Verbose "Current Timezone Offset: $($CurrentTimezone.TotalHours)"

    Write-Verbose "Searching for FSLogix Timezone offsets"
    [timespan]$UTCOffset = (($FSLogixLog -match "^UTC")[0]) -replace "^UTC\+{0,1}",''
    Write-Verbose "FSLogix Timezone Offset: $($UTCOffset.TotalHours)"
    $TimeZoneOffset = $CurrentTimezone.Subtract($UTCOffset)

    Write-Verbose "Timezone Offset for log file processing: $($TimeZoneOffset.TotalHours)"
    $FSLogixLogObject = New-Object System.Collections.ArrayList

    #Create powershell object out of the FSLogix Log.
    Foreach ($line in $FSLogixLog) {
        $line | Select-String -Pattern "\[(.*?)\]|.+" -AllMatches | ForEach-Object{
            if ( $_.Matches.count -eq 4) { #ignore all lines that don't conform to the grid table
                $MMddyyyy = $(($start).ToString("MM/dd/yyyy"))
                $time = $($_.Matches[0].Value -replace ("\[","") -replace ("\]",""))
                $FSLogixTime = ([datetime]"$MMddyyyy $time").AddMinutes($TimeZoneOffset.TotalMinutes) ## Adding by TotalMinutes for those tricky 30 min timezone offsets
                if ($FSLogixTime -ge $start) {
                    $obj = [PSCustomObject]@{
                        Time = $FSLogixTime
                        Source   = ($logfilePath.Name).split("-")[0]
                        ThreadId = $_.Matches[1].Value -replace ("\[","") -replace ("\]","")
                        LogLevel = $_.Matches[2].Value -replace ("\[","") -replace ("\]","")
                        Message  = $_.Matches[3].Value.Trim()
                    }

                    $FSLogixLogObject.Add($obj)|Out-Null
                }
            }
        }
    }


    $SessionEvents = $FSLogixLogObject | Where-Object {$_.Message -like "*$samaccountname*"}
    Write-Verbose "Number of SessionEvents: $($SessionEvents.Count)"
    Write-Verbose "SessionEvents:"
    #Write-Verbose "$($SessionEvents | Format-Table | Out-String)"

    ## Going to assume that the process and thread ID will be unique. This maybe a mistake ##TTYE
    if ($SessionEvents.count -gt 0) {
        ## Get unique thread/process id
        $ProcessThreadID = $SessionEvents.threadid | Sort-Object -Unique
        Write-Verbose "Number of unique thread id's : $($ProcessThreadID.Count)"
        Write-Verbose "$($ProcessThreadID)"
        ## Get all events with the process threadid
        $FSLogixEvents = New-Object System.Collections.ArrayList
        foreach ($threadId in $ProcessThreadID) {
            Write-Verbose "ThreadID: `"$threadId`""
            Write-Verbose "$($FSLogixLogObject | Where-Object {$_.ThreadId -like "*$threadId*"})"
            $FSLogixLogObject.ForEach({if ($_.ThreadId -eq $threadId) {$FSLogixEvents.Add($_)}})
        }
        return $FSLogixEvents
    } else {
        Write-Verbose "Found no events for $samAccountName in $file"
    }
}

function Get-FSLogixProfileEvents {
    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$true)]
    [DateTime]
    $Start,

    [Parameter(Mandatory=$true)]
    [String]
    $samAccountName
    )


    Write-Verbose "samAccountName: `"$($samAccountName)`""
    Write-Verbose "User Session StartTime: `"$($Start)`""

    #FSLogix Log path is here: C:\ProgramData\FSLogix\Logs\Profile
    #at the time of this testing version 2.9.7205.27375 of FSLogix provided all the necessary information

    Write-Verbose "Looking for file with `"$($($start).ToString("yyyyMMdd"))`" in the file name"
    try {
        $FSLogixLogDir = Get-ItemPropertyValue -Path HKLM:\SOFTWARE\FSLogix\Logging -Name Logdir
        Write-Verbose "LogDir value configured. LogDir set to $FSLogixLogDir"
        }
    Catch {
        #LogDir registry value not found. Set to default:
            Write-Verbose "LogDir value not set. Setting LogDir to default path"
            $FSLogixLogDir = "C:\ProgramData\FSLogix\Logs"
        }

    $logFiles = Get-ChildItem -Path "$FSLogixLogDir" -Recurse | Where-Object {$_.Name -like "*$($($start).ToString("yyyyMMdd"))*"}

    $AllEvents = New-Object System.Collections.ArrayList
    $SortedEvents = New-Object System.Collections.ArrayList
    foreach ($file in $LogFiles.FullName) {
        Write-Verbose "Checking for events in logfile: $file"
        try {
            Test-Path $file | out-null
        } catch {
            Write-Error "Unable to determine or find FSLogix profile log file. Failed with file $file"
            break
        }
        
        $AllEvents.Add($(Process-FSLogixLogFile -LogFile $($file)))|Out-Null
        foreach ($events in $allEvents) {
            foreach ($event in $events) {
                $SortedEvents.Add($event) | Out-Null
            }
        }
    }
    $SortedEvents | Select-Object -Property Time,ThreadId,Source,LogLevel,Message -Unique | Sort-Object -Property Time | Format-Table -AutoSize
}

#region Login Information Gathering
$LSADefinitions = @'
    [DllImport("secur32.dll", SetLastError = false)]
    public static extern uint LsaFreeReturnBuffer(IntPtr buffer);

    [DllImport("Secur32.dll", SetLastError = false)]
    public static extern uint LsaEnumerateLogonSessions
            (out UInt64 LogonSessionCount, out IntPtr LogonSessionList);

    [DllImport("Secur32.dll", SetLastError = false)]
    public static extern uint LsaGetLogonSessionData(IntPtr luid, 
        out IntPtr ppLogonSessionData);

    [StructLayout(LayoutKind.Sequential)]
    public struct LSA_UNICODE_STRING
    {
        public UInt16 Length;
        public UInt16 MaximumLength;
        public IntPtr buffer;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct LUID
    {
        public UInt32 LowPart;
        public UInt32 HighPart;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_LOGON_SESSION_DATA
    {
        public UInt32 Size;
        public LUID LoginID;
        public LSA_UNICODE_STRING samAccountName;
        public LSA_UNICODE_STRING LoginDomain;
        public LSA_UNICODE_STRING AuthenticationPackage;
        public UInt32 LogonType;
        public UInt32 Session;
        public IntPtr PSiD;
        public UInt64 LoginTime;
        public LSA_UNICODE_STRING LogonServer;
        public LSA_UNICODE_STRING DnsDomainName;
        public LSA_UNICODE_STRING Upn;
    }

    public enum SECURITY_LOGON_TYPE : uint
    {
        Interactive = 2,        //The security principal is logging on 
                                //interactively.
        Network,                //The security principal is logging using a 
                                //network.
        Batch,                  //The logon is for a batch process.
        Service,                //The logon is for a service account.
        Proxy,                  //Not supported.
        Unlock,                 //The logon is an attempt to unlock a workstation.
        NetworkCleartext,       //The logon is a network logon with cleartext 
                                //credentials.
        NewCredentials,         //Allows the caller to clone its current token and
                                //specify new credentials for outbound connections.
        RemoteInteractive,      //A terminal server session that is both remote 
                                //and interactive.
        CachedInteractive,      //Attempt to use the cached credentials without 
                                //going out across the network.
        CachedRemoteInteractive,// Same as RemoteInteractive, except used 
                                // internally for auditing purposes.
        CachedUnlock            // The logon is an attempt to unlock a workstation.
    }
'@

if( ! ( ([System.Management.Automation.PSTypeName]'Win32.Secure32').Type ) )
{
    Add-Type -MemberDefinition $LSADefinitions -Name 'Secure32' -Namespace 'Win32' -UsingNamespace System.Text -Debug:$false
}

$count = [UInt64]0
$luidPtr = [IntPtr]::Zero

[uint64]$ntStatus = [Win32.Secure32]::LsaEnumerateLogonSessions( [ref]$count , [ref]$luidPtr )

$count = [UInt64]0
$luidPtr = [IntPtr]::Zero

[uint64]$ntStatus = [Win32.Secure32]::LsaEnumerateLogonSessions( [ref]$count , [ref]$luidPtr )

if( $ntStatus )
{
    Write-Error "LsaEnumerateLogonSessions failed with error $ntStatus"
}
elseif( ! $count )
{
    Write-Error "No sessions returned by LsaEnumerateLogonSessions"
}
elseif( $luidPtr -eq [IntPtr]::Zero )
{
    Write-Error "No buffer returned by LsaEnumerateLogonSessions"
}
else
{   
    Write-Debug "$count sessions retrieved from LSASS"
    [IntPtr] $iter = $luidPtr
    $earliestSession = $null
    [array]$lsaSessions = @( For ([uint64]$i = 0; $i -lt $count; $i++)
    {
        $sessionData = [IntPtr]::Zero
        $ntStatus = [Win32.Secure32]::LsaGetLogonSessionData( $iter , [ref]$sessionData )

        if( ! $ntStatus -and $sessionData -ne [IntPtr]::Zero )
        {
            $data = [System.Runtime.InteropServices.Marshal]::PtrToStructure( $sessionData , [type][Win32.Secure32+SECURITY_LOGON_SESSION_DATA] )

            if ($data.PSiD -ne [IntPtr]::Zero)
            {
                $sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $Data.PSiD

                #extract some useful information from the session data struct
                [datetime]$loginTime = [datetime]::FromFileTime( $data.LoginTime )
                $thisUser = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.samAccountName.buffer) #get the account name
                $thisDomain = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.LoginDomain.buffer) #get the domain name
                try
                { 
                    $secType = [Win32.Secure32+SECURITY_LOGON_TYPE]$data.LogonType
                }
                catch
                {
                    $secType = 'Unknown'
                }

                if( ! $earliestSession -or $loginTime -lt $earliestSession )
                {
                    $earliestSession = $loginTime
                }
                if( $thisUser -eq $samAccountName -and $thisDomain -eq $UserDomain -and $secType -match 'Interactive' )
                {
                    $authPackage = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.AuthenticationPackage.buffer) #get the authentication package
                    $session = $data.Session # get the session number
                    if( $session -eq $SessionId )
                    {
                        $logonServer = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.LogonServer.buffer) #get the logon server
                        $DnsDomainName = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.DnsDomainName.buffer) #get the DNS Domain Name
                        $upn = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.upn.buffer) #get the User Principal Name

                        [pscustomobject]@{
                            'Sid' = $sid
                            'samAccountName' = $thisUser
                            'Domain' = $thisDomain
                            'Session' = $session
                            'LoginId' = [uint64]( $loginID = [Int64]("0x{0:x8}{1:x8}" -f $data.LoginID.HighPart , $data.LoginID.LowPart) )
                            'LogonServer' = $logonServer
                            'DnsDomainName' = $DnsDomainName
                            'UPN' = $upn
                            'AuthPackage' = $authPackage
                            'SecurityType' = $secType
                            'Type' = $data.LogonType
                            'LoginTime' = [datetime]$loginTime
                        }
                    }
                }
            }
            [void][Win32.Secure32]::LsaFreeReturnBuffer( $sessionData )
            $sessionData = [IntPtr]::Zero
        }
        $iter = $iter.ToInt64() + [System.Runtime.InteropServices.Marshal]::SizeOf([type][Win32.Secure32+LUID])  # move to next pointer
    }) | Sort-Object -Descending -Property 'LoginTime'

    [void]([Win32.Secure32]::LsaFreeReturnBuffer( $luidPtr ))
    $luidPtr = [IntPtr]::Zero

    Write-Debug "Found $(if( $lsaSessions ) { $lsaSessions.Count } else { 0 }) LSA sessions for $UserDomain\$samAccountName, earliest session $(if( $earliestSession ) { Get-Date $earliestSession -Format G } else { 'never' })"
}

if( $lsaSessions -and $lsaSessions.Count )
{
    ## get all logon ids for logons that happened at the same time
    [array]$loginIds = @( $lsaSessions | Where-Object { $_.LoginTime -eq $lsaSessions[0].LoginTime } | Select-Object -ExpandProperty LoginId )
    if( ! $loginIds -or ! $loginIds.Count )
    {
        Write-Error "Found no login ids for $samAccountName at $(Get-Date -Date $lsaSessions[0].LoginTime -Format G)"
    }
    $Logon = New-Object -TypeName psobject -Property @{
        LogonTime = $lsaSessions[0].LoginTime
        LogonTimeFileTime = $lsaSessions[0].LoginTime.ToFileTime()
        FormatTime = $lsaSessions[0].LoginTime.ToString( 'HH:mm:ss.fff' ) 
        LogonID = $loginIds
        UserSID = $lsaSessions[0].Sid
        Type = $lsaSessions[0].Type
        SamAccountName = $samAccountName
        UserDomain = $UserDomain
    }
}
else
{
    Throw "Failed to retrieve logon session for $UserDomain\$samAccountName from LSASS"
}

#endregion

Get-FSLogixProfileEvents -Start $logon.LogonTime -samAccountName $logon.samAccountName