pdqcom / PowerShell-Scanners

A community repository of PowerShell Scanners for PDQ Inventory.
MIT License
179 stars 74 forks source link

Last Logged on User scanner doesn't always detect the last logged on user. #110

Open ccheath opened 3 months ago

ccheath commented 3 months ago

I've been noticing that a lot (about 80%) of our computers do not have a last logged on user even though the scan is completing every time it runs.

So I began to do some testing on these computers that were scanning successfully but returning no data.

At first I thought that it was because of the "-newest 200" flag in the Get-EventLog cmdlet. So I changed it to "-newest 2000" without any change in results. Then I removed the "-newest" flag altogether to get all the events possible, but still no results.

I then moved my attention to the if statement below looking at the $LogonType variable. This turned out to be the issue. There were no types matching "2", "10" or "11". All we were getting were "3" and "5" types, and none of those were for the logged on user. They were for the computer account, SYSTEM, and my remote PSSessions.

In a last-ditch effort to try and figure out a way to get some more data out of this scanner I turned to the currently logged in user. I modified the script to check for the currently logged in user and if that was different that any of the users that were collected by the for loop above in the $UserArray variable. If there is a currently logged in user and it is not in that array then add it to the PSCustomObject to be returned with a logon type of "Current User" and the current timestamp for the last logon field.

Originally I was doing this check with a Get-CIMInstance cmdlet but it seemed to be failing for RDP and VDI users. I then tried a few other options (WMI, owner of the explorer.exe process, etc) but they also did not work consistently. Eventually I landed on the quser command and parsing the output of that which seems to be working well for me.

Here is my modified script:

# This script requires that Audit Logon events are enabled in Group Policy and those events are kept for the amount of history preferred

[CmdletBinding()]
param (
    [Switch]$Lowercase
)

$UserArray = New-Object System.Collections.ArrayList
# Query all logon events with id 4624 
Get-EventLog -LogName "Security" -Newest 200 -InstanceId 4624 -ErrorAction "SilentlyContinue" | ForEach-Object {
    $EventMessage = $_
    $AccountName = $EventMessage.ReplacementStrings[5]
    $LogonType = $EventMessage.ReplacementStrings[8]
    if ( $Lowercase ) {
        # Make all usernames lowercase so they group properly in Inventory
        $AccountName = $AccountName.ToLower()
    }
    # Look for events that contain local or remote logon events, while ignoring Windows service accounts
    if ( ( $LogonType -in "2", "10", "11" ) -and ( $AccountName -notmatch "^(DWM|UMFD)-\d" -and ($AccountName -ne "") ) ) {
        # Skip duplicate names
        if ( $UserArray -notcontains $AccountName ) {
            $null = $UserArray.Add($AccountName)            
            # Translate the Logon Type
            if ( $LogonType -eq "2" ) {
                $LogonTypeName = "Local"
            }
            elseif ( $LogonType -eq "10" ) {
                $LogonTypeName = "Remote"
            }
            elseif ( $LogonType -eq "11" ) {                
                $LogonTypeName = "Cached"
            }
            $time = [DateTime]$EventMessage.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
            # Build an object containing the Username, Logon Type, and Last Logon time
            [PSCustomObject]@{
                Username  = $AccountName
                LogonType = $LogonTypeName
                LastLogon = $time
            }
        }
    }    
}
# Get the current logged in user in case nothing is returned by the above
#$userName = (Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object -ExpandProperty UserName).Split('\')[1]
$queryUser = quser
$userName = $null
if ($queryUser) {
    $userName = $queryUser -match ' (\S+)\s+\d+ '
    if ($matches) {
        $userName = $matches[1]
    }
}
# Return if no username found via quser
if ( $null -eq $userName ) {
    return
}
$userName = $userName.Substring(1)
$userName = $userName.Split(" ")[0]
if ( $Lowercase ) {
    # Make all usernames lowercase so they group properly in Inventory
    $userName = $userName.ToLower()
}
if ($null -ne $userName -and $UserArray -notcontains $userName) {
    [PSCustomObject]@{
        Username  = $userName
        LogonType = "Current User"
        LastLogon = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
}
ccheath commented 2 months ago

modified the script to handle when quser returns errors because no user is logged on or other error conditions

# This script requires that Audit Logon events are enabled in Group Policy and those events are kept for the amount of history preferred

[CmdletBinding()]
param (
    [Switch]$Lowercase
)

$UserArray = New-Object System.Collections.ArrayList
# Query all logon events with id 4624 
Get-EventLog -LogName "Security" -Newest 200 -InstanceId 4624 -ErrorAction "SilentlyContinue" | ForEach-Object {
    $EventMessage = $_
    $AccountName = $EventMessage.ReplacementStrings[5]
    $LogonType = $EventMessage.ReplacementStrings[8]
    if ( $Lowercase ) {
        # Make all usernames lowercase so they group properly in Inventory
        $AccountName = $AccountName.ToLower()
    }
    # Look for events that contain local or remote logon events, while ignoring Windows service accounts
    if ( ( $LogonType -in "2", "10", "11" ) -and ( $AccountName -notmatch "^(DWM|UMFD)-\d" -and ($AccountName -ne "") ) ) {
        # Skip duplicate names
        if ( $UserArray -notcontains $AccountName ) {
            $null = $UserArray.Add($AccountName)            
            # Translate the Logon Type
            if ( $LogonType -eq "2" ) {
                $LogonTypeName = "Local"
            }
            elseif ( $LogonType -eq "10" ) {
                $LogonTypeName = "Remote"
            }
            elseif ( $LogonType -eq "11" ) {                
                $LogonTypeName = "Cached"
            }
            $time = [DateTime]$EventMessage.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
            # Build an object containing the Username, Logon Type, and Last Logon time
            [PSCustomObject]@{
                Username  = $AccountName
                LogonType = $LogonTypeName
                LastLogon = $time
            }
        }
    }    
}
# Get the current logged in user in case nothing is returned by the above
#$userName = (Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object -ExpandProperty UserName).Split('\')[1]
$queryUser = quser 2>&1
If ($LASTEXITCODE -ne 0) {
    $ErrorDetail = $queryUser.Exception.Message[1]
    Switch -Wildcard ($queryUser) {
        '*[1722]*' { 
            $Status = 'Remote RPC not enabled'
        }
        '*[5]*' {
            $Status = 'Access denied'
        }
        'No User exists for*' {
            $Status = 'No logged on users found'
        }
        default {
            $Status = 'Error'
            $ErrorDetail = $queryUser.Exception.Message
        }
    }
    [pscustomobject]@{ErrorStatus = $Status;ErrorMessage = $ErrorDetail}
    $queryUser = $null
}
$userName = $null
if ($queryUser) {
    $userName = $queryUser -match ' (\S+)\s+\d+ '
    if ($matches) {
        $userName = $matches[1]
    }
}
# Return if no username found via quser
if ( $null -eq $userName ) {
    return
}
$userName = $userName.Substring(1)
$userName = $userName.Split(" ")[0]
if ( $Lowercase ) {
    # Make all usernames lowercase so they group properly in Inventory
    $userName = $userName.ToLower()
}
if ($null -ne $userName -and $UserArray -notcontains $userName) {
    [PSCustomObject]@{
        Username  = $userName
        LogonType = "Current User"
        LastLogon = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
}
ccheath commented 2 months ago

One final change... (I forgot to set the $queryUser variable to $null when no user is found)

# This script requires that Audit Logon events are enabled in Group Policy and those events are kept for the amount of history preferred

[CmdletBinding()]
param (
    [Switch]$Lowercase
)

$UserArray = New-Object System.Collections.ArrayList
# Query all logon events with id 4624 
Get-EventLog -LogName "Security" -Newest 200 -InstanceId 4624 -ErrorAction "SilentlyContinue" | ForEach-Object {
    $EventMessage = $_
    $AccountName = $EventMessage.ReplacementStrings[5]
    $LogonType = $EventMessage.ReplacementStrings[8]
    if ( $Lowercase ) {
        # Make all usernames lowercase so they group properly in Inventory
        $AccountName = $AccountName.ToLower()
    }
    # Look for events that contain local or remote logon events, while ignoring Windows service accounts
    if ( ( $LogonType -in "2", "10", "11" ) -and ( $AccountName -notmatch "^(DWM|UMFD)-\d" -and ($AccountName -ne "") ) ) {
        # Skip duplicate names
        if ( $UserArray -notcontains $AccountName ) {
            $null = $UserArray.Add($AccountName)            
            # Translate the Logon Type
            if ( $LogonType -eq "2" ) {
                $LogonTypeName = "Local"
            }
            elseif ( $LogonType -eq "10" ) {
                $LogonTypeName = "Remote"
            }
            elseif ( $LogonType -eq "11" ) {                
                $LogonTypeName = "Cached"
            }
            $time = [DateTime]$EventMessage.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
            # Build an object containing the Username, Logon Type, and Last Logon time
            [PSCustomObject]@{
                Username  = $AccountName
                LogonType = $LogonTypeName
                LastLogon = $time
            }
        }
    }    
}
# Get the current logged in user in case nothing is returned by the above
#$userName = (Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object -ExpandProperty UserName).Split('\')[1]
$queryUser = quser 2>&1
If ($LASTEXITCODE -ne 0) {
    $ErrorDetail = $queryUser.Exception.Message[1]
    Switch -Wildcard ($queryUser) {
        '*[1722]*' { 
            $Status = 'Remote RPC not enabled'
        }
        '*[5]*' {
            $Status = 'Access denied'
        }
        'No User exists for*' {
            $Status = 'No logged on users found'
        }
        default {
            $Status = 'Error'
            $ErrorDetail = $queryUser.Exception.Message
        }
    }
    [PSCustomObject]@{
        Username  = $Status
        LogonType = $ErrorDetail
        LastLogon = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
    $queryUser = $null
}
$userName = $null
if ($queryUser) {
    $userName = $queryUser -match ' (\S+)\s+\d+ '
    if ($matches) {
        $userName = $matches[1]
    }
}
# Return if no username found via quser
if ( $null -eq $userName ) {
    return
}
$userName = $userName.Substring(1)
$userName = $userName.Split(" ")[0]
if ( $Lowercase ) {
    # Make all usernames lowercase so they group properly in Inventory
    $userName = $userName.ToLower()
}
if ($null -ne $userName -and $UserArray -notcontains $userName) {
    [PSCustomObject]@{
        Username  = $userName
        LogonType = "Current User"
        LastLogon = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
}