Getting Last Logged on User from a List of Workstations

This worked on my domain-joined machine against another domain machine that I specified by short netbios name
image
Though now i’m not sure why I ever included the SID. The Username property and SID property contain the same value (now that the SID is being translated).

You guys kick ass, and I am going to ask something huge. You guys have been killin it, but I am still trying to fulfill my original goal.

I am trying to create a PowerShell script that will read a text file of workstations on my domain, get the last logged on user and write it to a CSV file.

What I chose to do was use WMI against the remote machine just in case the login was a local user. Adapted for Dougs function, I think this might work:

$User = ([wmi]"\\$ComputerName\root\cimv2:Win32_SID.SID='$($XMLEvent.Event.SelectSingleNode("//*[@Name='TargetUserSid']").InnerXml)'").AccountName

i think most of the “bones” of that are in this thread. My question would be; how do you want to deal with the username results?
What do you want to do if no username is returned?
What do you want to do if multiple usernames are returned?
What format do you want the username in? (i.e. Domain\User, User etc)

So, you will probably be internet famous after this. Seriously, I have this same request in multiple PowerShell groups and no great solution as of yet

I am wanting this output:
Username Workstation Time

That is it. Crazy that this can’t be generated easily. I have a text file with every workstation name as my source. And I want a csv as my output

I’m not sure what you mean. Which part can’t be generated easily?

The reason is, no one on the internet wants to write code for free. There’s a kind of a consistent experience through all the different discussion platforms out there where users will ask a question or request something be done and the impression from the user-base is that this person did not put any effort in to this at all. It creates a kind of sense of resentment.
The community in this forum is by far the best I’ve seen on the net for Powershell discussion and in part it’s because of their general attitude, and their commitment to holding the user base to a higher stander.
People put effort in, they get effort back.

For your post, I’ve been happy to go down this rabbit hole with you because I will use some of this code at work, so you see I’m getting something out of it too.

# get our list of computers and specify where to save the output
$computers = Get-Content 'C:\Scripts\ADscripts\pcList.txt'
$path = 'C:\Scripts\ADScripts\computers.csv'
# define our  function we're going to use
Function Get-LastLoggedInUser {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$false)]
        [string]$ComputerName = $env:COMPUTERNAME
    )

    $Filter = @"
<QueryList>
    <Query Id="0" Path="Security">
        <Select Path="Security">
        *[System[(EventID=4624)]]
        and
            *[EventData[
            Data[@Name='TargetUserName'] != 'SYSTEM' and
            Data[@Name='TargetUserName'] != '$($ComputerName)$' and
            Data[@Name='TargetDomainName'] != "Window Manager" and
            Data[@Name='TargetDomainName'] != "Font Driver Host" and
            (Data[@Name='LogonType'] = 2 or Data[@Name='LogonType'] = 11) and
            Data[@Name='SubjectDomainName'] != "Window Manager"
        ]
    ]
        </Select>
    </Query>
</QueryList>
"@

    try {
        $Results = Invoke-Command -computername $Computername -scriptblock {
            $Events = Get-WinEvent -FilterXml $using:Filter -MaxEvents 5 -ErrorAction Stop
            Foreach ($Event in $Events) {
                [XML]$XMLEvent = $Event.ToXml()
                [PSCustomObject]@{
                    Computer    = $ENV:COMPUTERNAME
                    TimeStamp = $Event.TimeCreated
                    UserName    = $XMLEvent.Event.SelectSingleNode("//*[@Name='TargetUserName']").'#text'
                    Domain      = $XMLEvent.Event.SelectSingleNode("//*[@Name='TargetDomainName']").'#text'
                }
            }
        } 
    } catch {
        Write-Warning "Failed to get Windows Events for $ComputerName"
        continue
    }
    if ($Results) {
        $Results | Select-Object -First 1 -Property Computer, TimeStamp,@{Name="User";Expression={'{0}\{1}' -f $_.Domain,$_.UserName}}
    }
}


# instead of creating an array and using the += syntax to tear down the array and rebuild it with each new object, we're just spitting out our objects right in to the $Results variable which becomes an array.
$LoginRecords = Foreach ($Computer in $Computers) {
# try/catch block because doing anything remotely against a computer has the potential to fail
    try {
        Get-LastLoggedInUser -ComputerName $Computer
    } catch {
        Write-Warning "Failed to get Windows Events for $Computer"
        continue
    }
}
# checking to make sure we actually have some results before outputting
if ($LoginRecords) {
# added the NoTypeInformation switch to the Export so our first row isn't information about the objects
    $LoginRecords | Export-Csv $Path -NoTypeInformation
}

Here is code you could paste in to a script. You’ll want to verify that the $Computers and $path variables are defined to your requirements.
This is using all the code we’ve discussed in this thread. It ingests a list of computer names from a text file, defines a function we’re going to use to determine last logged on user, spits out objects with Computername,timestamp and username, and exports the results to a CSV file.
I modified the function we last used after some more testing.
Levering the built-in -ComputerName parameter on Get-WinEvent was taking on average 45 seconds to run against a remote computer. Invoke-Command by comparison, which uses remote PS, takes about 5 seconds on average (for me). I also changed the output to combine the Domain name and Username properties in to one property so the output would look like this:

Computer     TimeStamp           User       
--------     ---------           ----
inspiron001 7/2/2024 6:37:19 AM CONTOSO\j.smith

Here is another approach. You will of course need to enhance to your liking, ping test etc …

$ErrorActionPreference = 'Stop'
$Computers = Get-Content -Path '.\computers.txt'
$ResultsFile = '.\computers.csv'
$logfile = 'Microsoft-Windows-Winlogon/Operational'
$aryLoginInfo = New-Object System.Collections.Generic.List[System.Object]

foreach($Computer in $Computers) {
    try {
        $UserInfo = Get-WinEvent -ComputerName $Computer -LogName $logFile | Where-Object{$_.ID -eq '812' -And $_.Message -Match 'SessionEnv'} | Sort-Object -Property 'TimeCreated' | Select-Object -Last 1
        $UserName = ([wmi]"\\$Computer\root\cimv2:Win32_SID.SID='$($UserInfo.UserId)'").AccountName
        $LastLoggedUser = [PSCustomObject][Ordered] @{
            'User Name' = $UserName
            'Workstation' = $UserInfo.MachineName
            'Login Time' = $UserInfo.TimeCreated
        }
        $aryLoginInfo.Add($LastLoggedUser)
    }
    catch {
        Write-Output "Unable to gather info for $Computer, $($_.Exception.Message)"
    }
}
$aryLoginInfo | Export-Csv $ResultsFile -NoTypeInformation

I totally get that. I have done some digging on my own and have tried and tested so many different solutions. But, I have failed in my efforts. I am skilled in many different areas, but Powershell may as well be Russian for me. I can put something together but I am always missing some kind of syntax.

I highly appreciate your efforts and am glad it is mutually beneficial. Going to try this one today and see how it goes. Thanks again for everything