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. This is the code I am using:
can you edit your post and use the Preformatted Text option so that your code is formatted?
You may have to hit the gear icon and find āPreformatted Textā there.
Getting the content of a text file for the list of computers makes sense, but why is $FolderLocation a read-host prompt with prompt text of a fodler?
Then whatever the user inputs at that prompt gets added to the beginning of a filepath for $Path which effectively ruins that path:
Then it looks like your method for determining last logged on user may have come from here:
That is unfortunately not an accurate way to determine last logged on user. The userās profile folder does not update LastWriteTime every time they log in. For instance, on my work computer that āLastWriteTimeā is March sometime, but Iām logged in right now.
Itās an intersting problem though, and one I donāt have an immediate solution for. Searching around yields mixed results, but Event Viewer might hold more accurate info.
Iām going to play around with this for a couple minutes and see if I can come up with a better filter:
# 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 an XML filter for use with Get-WinEvent.
# this gets all events from the Security log of event ID 4672 (logon) and filters out hits for some built-in accounts (identified by SID) and the Windows Manager
$Query = @"
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">
*[System[(EventID=4672)]]
and
*[EventData[Data[@Name='SubjectUserSid'] !='S-1-5-18' and Data[@Name='SubjectUserSid'] !='S-1-5-19' and Data[@Name='SubjectUserSid'] !='S-1-5-20' and Data[@Name='SubjectDomainName'] != "Window Manager"]]
</Select>
</Query>
</QueryList>
"@
# 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.
$Results = Foreach ($Computer in $Computers) {
# try/catch block because doing anything remotely against a computer has the potential to fail
try {
# only getting a max of 5 events because there can be a LOT of logs that match this.
$Events = Get-WinEvent -ComputerName $Computer -FilterXml $Query -MaxEvents 5 -ErrorAction Stop
Foreach ($Event in $Events) {
[XML]$XMLEvent = $Event.ToXml()
[PSCustomObject]@{
TimeCreated = $Event.TimeCreated
UserName = $XMLEvent.Event.SelectSingleNode("//*[@Name='SubjectUserName']").'#text'
Domain = $XMLEvent.Event.SelectSingleNode("//*[@Name='SubjectDomainName']").'#text'
SID = $XMLEvent.Event.SelectSingleNode("//*[@Name='SubjectUserSid']").'#text'
}
}
} catch {
Write-Warning "Failed to get Windows Events for $Computer"
continue
}
}
# checking to make sure we actually have some results before outputting
if ($Results) {
# added the NoTypeInformation switch to the Export so our first row isn't information about the objects
$Results | Export-Csv $Path -NoTypeInformation
}
only tested against my current computer and one other computer. In both cases it does not reflect the last person to log in to the graphical desktop environment, but rather the last person to ālogonā which includes authenticating remotely for Powershell.
Yes, I grabbed this from the site. You would be surprised how little googling yielded on this exact process. Seems like it would be a pretty standard Script that many people would use/need.
Hmmm, I wonder if there is a way to parse the Powershell Authentication. I really appreciate you checking on this!
The values returned dont quit match what you see in Event Viewer which shows the user and domain. Not sure if you can get that but you could translate the SID to the username??
What Iāve seen people do thatās more reliable is leverage a Logon script to write to a file who the last user was to logon.
Since the logon script is ran by whoever the user is that logged on you can use $ENV:USERNAME to capture who that is and either write it to a file locally, or set up a network share for logs like this.
One example would be: have the logon script log to a file on a network share named for the user (i.e. āj.smith.logā). Have it write a row with a timestamp, computername, whatever else you want.
Then at the same time have the logon script log to a file named after the computer (i.e. āworkstation1504.logā) where it writes a row with the timestamp, username, and whatever else you want.
With this in place you could then read those text files at will and find out when a user logged in to a particular computer, or what computers a user has logged in to.
this function I found here is pretty nice:
Itās essentially looking at and filtering the Security Event log as well but the output data is much more enriched. The extra properties give you something to filter on:
running it against the same computer I tested my own code against I can see yesterday at 2:26pm when I Remote Desktopād into that machine there are an āInteractiveā and a āRemoteInteractiveā logintype recorded for that user account. If we pre-filtered out all the System stuff and window manager stuff, we could then just focus on interactive logins and it might be more accurate.
Well, I am doing this to figure out what workstation name each of my users is using. I just started here and they donāt have a real inventory, so I have been tapped to gather this information
Maybe I should just figure out a way to get the profile names on each pc instead?
fair warning, one of the forums i looked at stated that the Win32_UserProfile property for LastUseTime could get updated by other things other then a user actually logging in. It may not be 100% accurate.
Hereās a quick function that combines some of the filtering I was doing with that function I linked to earlier:
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 {
$Events = Get-WinEvent -ComputerName $ComputerName -FilterXml $Filter -MaxEvents 15 -ErrorAction Stop
Foreach ($Event in $Events) {
[XML]$XMLEvent = $Event.ToXml()
[PSCustomObject]@{
TimeCreated = $Event.TimeCreated
UserName = $XMLEvent.Event.SelectSingleNode("//*[@Name='TargetUserName']").'#text'
Domain = $XMLEvent.Event.SelectSingleNode("//*[@Name='TargetDomainName']").'#text'
SID = $XMLEvent.Event.SelectSingleNode("//*[@Name='TargetUserSid']").'#text'
}
}
} catch {
Write-Warning "Failed to get Windows Events for $ComputerName"
continue
}
}
When I run this against the remote PC I was using for testing I get this for output:
Itās not very fast, but that is and accurate result for that PC. And this is coming from the Security log on the machine.
Caveats to this: on my own machine there are no results returned and this is because the Event ID 4624 correlating to my login this morning is already gone due to log turnover. If you were inspecting a machine that last had a user login days ago, this might happen to use as well.
The Get-CimInstance example from @neemobeer when ran against my same test PC does not yield the same result. In fact, when ran against my local PC, none of the timestamps returned for LastUseTime are even today, which means itās not accurately correlating to the last logged in user. According to the LastUsetime field my last logged in user is c:\Users\Administrator, which is an account that doesnāt even exist on my machine.
I like this one a lot. But, I wonder if there is a way to deliver the workstation name instead of the SID? Translating each individual SID-to-name for a company user-to-workstation name inventory project may be a bit much. Any ideas?
the SID was just kind of āextraā. The function technically returns the correct domain name and username so you could just combine that with the Computer that you know you ran the function against and thereās your user/device affinity.
Otherwise @neemobeer is right that Get-ADUser can take the SID as an argument and return the user from Active Directory
Hereās an updated version of the function that may potentially translate the username from sid. It worked on my workgroup system but my limited testing in domain environment showed no matching events.