Use PowerShell to Find Last Logon Times for Virtual Workstations

Doctor Scripto

Summary: Learn how to Use Windows PowerShell to find the last logon times for virtual workstations.

Microsoft Scripting Guy, Ed Wilson, is here. Welcome back guest blogger, Brian Wilhite. Brian was our guest blogger yesterday when he wrote about detecting servers that will have a problem with an upcoming time change due to daylight savings time. Here is a little bit about Brian.

Brian Wilhite works as a Windows System Administrator for a large health-care provider in North Carolina. He has over 15 years of experience in IT. In his current capacity as a Windows SysAdmin, he leads a team of individuals that have responsibilities for Microsoft Exchange Server, Windows Server builds, and management and system performance. Brian also supports and participates in the Charlotte PowerShell Users Group.
Twitter: Brian Wilhite

Take it away, Brian…

Several weeks ago our virtual guy asked me if there was a way to determine which virtual workstations have been recently used. I started thinking, and of course, the first place I turned was to Windows PowerShell. I did some research and found the Win32_UserProfile WMI class. However, the “minimum supported client” is Windows Vista with SP1, and the majority of our virtual workstations are running Windows XP. So the dilemma was to create a function that would provide the same type of information for computers running Windows XP and later. I evaluated the information that was returned from the Win32_UserProfile class. As you see in the following image, I indexed into the third object of the Win32_UserProfile array for brevity, and this is the information that’s available.

Image of command output

I wanted to provide the following information:

  • The computer from which the function was run against
  • The user account that was logged on last (security identifier or SID)
  • The last use time (LastUseTime)
  • Is the user currently logged on? (Loaded)

I’m going to use two methods to gather these four pieces of information. First, I’m going to use WMI to collect the information on computers running Windows Vista with SP1 and later. For computers running Windows Vista and earlier, I’m going to use user profile file properties and registry information to collect the needed data. We will discuss the WMI method first.

I am using the Win32_OperatingSystem WMI class to collect the build number to determine which method to use.

$Win32OS = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $Computer

$Build = $Win32OS.BuildNumber

The “If ($Build -ge 6001)” is the first decision point. If the build number is 6001 and above, the script block will run.

If ($Build -ge 6001)

{

$Win32User = Get-WmiObject -Class Win32_UserProfile -ComputerName $Computer

I am using RegEx to filter the LocalService, NetworkService, and System profiles because they aren’t needed, and I am sorting by LastUseTime to pick the one most recently used.

$Win32User = $Win32User | Where-Object {($_.SID -notmatch “^S-1-5-\d[18|19|20]$”)}

$Win32User = $Win32User | Sort-Object -Property LastUseTime -Descending

$LastUser = $Win32User | Select-Object -First 1

The Win32_UserProfile Loaded property determines if the user was logged on at the time the query was run. I’m casting that value into a new variable ($Loaded). I will create a New-Object with that property and value later.

$Loaded = $LastUser.Loaded

So now we’re looking at the LastUseTime property—the value is a “System.String” (20120209035107.508000+000), but I need to convert it to a “System.DateTime” object, so it’s readable, I will use the WMI ConvertToDateTime method to accomplish this.

$Time = ([WMI] ”).ConvertToDateTime($LastUser.LastUseTime)

One of the things I need to do is take the SID that is collected via Win32_UserProfile and convert it to Domain\samAccountName format.

So I created a New-Object with the .NET Security Identifier Class Provider, and I specified the $LastUser.SID variable.

$UserSID = New-Object System.Security.Principal.SecurityIdentifier($LastUser.SID)

When New-Object is created with the SID value, there is a translate method that can be used to convert the SID to the Domain\samAccountName.

$User = $UserSID.Translate([System.Security.Principal.NTAccount])

Instead of using Write-Host or some string-type output, I prefer to use object-based output. The following code snippet shows the four pieces of information that I wanted to gather and return.

$UserProf = New-Object PSObject -Property @{

Computer=$Computer

User=$User

Time=$Time

CurrentlyLoggedOn=$Loaded

}

If you’ve ever created custom objects in Windows PowerShell, you know that without any special XML formatting, when you return the object, it will place the properties in an order that you may not like. To quickly remedy this, what I usually do is pipe my variable that contains the custom object to Select-Object and type the names of the properties in the order in which I want them returned.

$UserProf = $UserProf | Select-Object Computer, User, Time, CurrentlyLoggedOn

$UserProf

Now when $UserProf is returned, the following is displayed:

Image of command output

Now that we’ve taken care of any computer that has the Win32_UserProfile WMI class, beginning with Windows Vista with SP1, let’s take a look at those computers that do not have that WMI class. I started thinking about how to figure out the last person to log on, what time they logged on, and if they were currently logged on. I observed my profile as I logged on, and I noticed that the NTUSER.DAT.LOG file was immediately modified. This file is intermittently updated throughout the user’s session. The NTUSER.DAT.LOG is used for fault tolerance purposes if Windows can’t update the NTUSER.DAT file. Obviously, as soon as the user logs off, the file is no longer updated.

The If statement checks for the build number 6000 and below, meaning Windows Vista without SP1 and earlier.

If ($Build -le 6000)

{

To scan the user profile directories for the NTUSER.DAT.LOG, I am making the assumption that the Documents and Settings folder is residing on the system drive. I’m querying the system drive information from the Win32_OperatingSystem WMI class and isolating only the drive letter by using the Replace method. When we have that information, we can put the $Computer and system drive letter together and make a UNC path for scanning “Documents and Settings”.

$SysDrv = $Win32OS.SystemDrive

$SysDrv = $SysDrv.Replace(“:”,”$”)

$ProfDrv = “\\” + $Computer + “\” + $SysDrv

$ProfLoc = Join-Path -Path $ProfDrv -ChildPath “Documents and Settings”

$Profiles = Get-ChildItem -Path $ProfLoc

When we have all of the user profiles, we want to search for the NTUSER.DAT.LOG files. After we capture all the NTUSER.DAT.LOG in the $LastProf variable, we need to sort by the LastWriteTime property in descending order, and select the first one.

$LastProf = $Profiles | ForEach-Object -Process {$_.GetFiles(“ntuser.dat.LOG”)}

$LastProf = $LastProf | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1

We’ve isolated the most recent NTUSER.DAT.LOG, so I’m now making another assumption that the profile folder name will equal the UserName. By using the Replace method, I’m going to strip the “\\$Computer\<System Drive>$\Documents and Settings” off of the DirectoryName, which represents the full path of the user’s profile. I’m also going to grab the LastAccessTime and cast it to the $Time variable.

$UserName = $LastProf.DirectoryName.Replace(“$ProfLoc”,””).Trim(“\”).ToUpper()

$Time = $LastProf.LastAccessTime

We are going to use the following code to extract the user’s SID from the access control entry of the NTUSER.DAT.LOG file.

$Sddl = $LastProf.GetAccessControl().Sddl

$Sddl = $Sddl.split(“(“) | Select-String -Pattern “[0-9]\)$” | Select-Object -First 1

Here we are formatting the SID, and assuming the sixth entry will be the user’s SID.

$Sddl = $Sddl.ToString().Split(“;”)[5].Trim(“)”)

The following code is used to convert the $UserName variable to the SID to detect if the profile is loaded via the remote registry and to compare the SID queried from the NTUSER.DAT.LOG file.

$TranSID = New-Object System.Security.Principal.NTAccount($UserName)

$UserSID = $TranSID.Translate([System.Security.Principal.SecurityIdentifier])

I felt it was necessary to compare the SID queried from the NTUSER.DAT.LOG file and the UserName extracted from the profile path, to ensure that the correct information is being returned.

If ($Sddl -eq $UserSID)

{

If the SIDs are equal, I’m going to open the HK_USERS hive and set the $Loaded variable to True if SubKeys contains the SID and to False if it isn’t present. If the user’s SID is present in the HK_USERS hive, the user is currently logged on.

$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]”Users”,$Computer)

$Loaded = $Reg.GetSubKeyNames() -contains $UserSID.Value

Because I have the UserName and no DomainName, I’m going to convert the SID to Account so that it will return in the DOMAIN\USER format.

$UserSID = New-Object System.Security.Principal.SecurityIdentifier($UserSID)

$User = $UserSID.Translate([System.Security.Principal.NTAccount])

}#End If ($Sddl -eq $UserSID)

If the SIDs are not equal, I will set $User to the profile folder name and set $Loaded to “Unknown” because I could not determine if the SID was 100% accurate.

Else

{

$User = $UserName

$Loaded = “Unknown”

}#End Else

Here I am creating and formatting the custom object, like we discussed earlier for the Windows Vista with SP1 and later script block.

#Creating the PSObject UserProf

$UserProf = New-Object PSObject -Property @{

Computer=$Computer

User=$User

Time=$Time

CurrentlyLoggedOn=$Loaded

}

$UserProf = $UserProf | Select-Object Computer, User, Time, CurrentlyLoggedOn

$UserProf

I setup this function to accept piped input for the ComputerName parameter. It will also accept an array of ComputerNames. So when we run Get-Lastlogon, we’ll be able to determine what workstations haven’t been used in a while, as shown in the following image.

Image of command output

~Brian

Thank you Brian, this is a most useful and interesting script. The complete script can be found at the Script Center Repository.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

1 comment

Discussion is closed. Login to edit/delete existing comments.

  • Nasir Bashir 0

    Hi Brian,
    I am a newbie at scripting but when I run this command I just get blank output. Nothing happens. I am running Windows Server 2008 R2 with powershell versio. Can you please advise on this?
    Name : ConsoleHostVersion : 3.0InstanceId : 94c593c4-87bd-4821-b6a0-c1ec1ccd0553UI : System.Management.Automation.Internal.Host.InternalHostUserInterfaceCurrentCulture : en-USCurrentUICulture : en-USPrivateData : Microsoft.PowerShell.ConsoleHost+ConsoleColorProxyIsRunspacePushed : FalseRunspace : System.Management.Automation.Runspaces.LocalRunspace

    OutPut:
    PS C:\Users\administrator.PASYN\Downloads> .\Get-LastLogon.ps1 -ComputerName pasynvm-32
    Security warningRun only scripts that you trust. While scripts from the internet can be useful, this script can potentially harm yourcomputer. Do you want to run C:\Users\administrator.PASYN\Downloads\Get-LastLogon.ps1?[D] Do not run [R] Run once [S] Suspend [?] Help (default is “D”): rPS C:\Users\administrator.PASYN\Downloads>

Feedback usabilla icon