# ParseLogons.ps1 # # Copyright (c) 2015 Richard L. Mueller # Version 1.0 - December 6, 2015 # # ---------------------------------------------------------------------- # You have a royalty-free right to use, modify, reproduce, and # distribute this script file in any way you find useful, provided that # you agree that the copyright owner above has no warranty, obligations, # or liability for such use. # PowerShell version 1 script to parse a log file that documents logon and logoff # events. From the log file the script outputs user sessions. Each session is # defined by the name of the computer and the name of the user, so a user can have # more than one session at a time on different computers. The script outputs the # session (computername\username), the logon datetime, the logoff datetime, and the # duration of the logon session in days.hours:minutes:seconds. # The log file is created by logon and logoff scripts configured in Group Policy. # Each of these scripts appends a line to a shared log file. The logon script can be # as simple as the following batch file: # # @echo off # echo Logon,%date%,%time%,%computername%,%username%>> \\Server\Share\Events.log # # The logoff script can be similar to the following batch file: # # @echo off # echo Logoff,%date%,%time%,%computername%,%username%>> \\Server\Share\Events.log # # This PowerShell script assumes that the fields in the resulting log file are comma # delimited. There can be more than 5 fields, but the first 5 should be: # "logon" or "logoff",date,time,computer name, user name # The script will add a header line to the beginning of the log file if there # is no header line, so there is no need for you to do this. The script needs write # access to the log file to add the header line. # For example, two lines of the log file could be similar to: # # Logon,Mon 11/23/2015,14:34:57.66,WKSTA03,jsmith # Logoff,Wed 11/25/2015, 9:21:44.60,WKSTA03,jsmith # # The output from the script for these two events would be similar to: # # WKSTA03\jsmith,11/23/2015 14:34:57,11/25/2015 09:21:44,1.18:46:47 # # This script accepts a log file name (and optional path) as a parameter, or the # script will prompt for the log file name. The script output is displayed at # the console in comma delimited format. The output can be redirected to a text file, # which can then be opened in Excel for analysis. Errors and warning messages are # written to the console, but will not be redirected to a text file. Trap { If (("$_".StartsWith("Cannot convert value")) ` -or ("$_".StartsWith("A parameter cannot be found that matches parameter name"))) { # Datetime not recognized by the Get-Date cmdlet. # The first error message is generated by PowerShell V2, the second by PowerShell V1. $Err = $_.ToString() Write-Host "$Err. Line skipped" ` -ForegroundColor red -BackgroundColor black # This line is skipped but the script can continue reading events. $Script:Skip = $True $Script:NumBadLines = $Script:NumBadLines + 1 Continue } } Function ReadLog($Log, $Start, $Count) { # Function to read each line of the log file and parse for sessions. # When this function is called, we know the file has a header line and at least one # event. A function is used because if an error is raised, especially retrieving the # datetime from the Date and Time fields of the log file, the script will immediately # exit whatever loop, function, For, or ForEach the script was in when the error was # raised. We want to be able to skip the line with the bad date and/or time, and begin # again on the next line of the log file. The variable $Script:Skip indicates if a bad # line was skipped, and $Script:SkipLine indicates which line was skipped. This allows us # to call the function again after the error and read the remaining lines of the log file. # In this case, we only call the function a maximum of 3 times, so we continue if there # are no more than 2 bad dates in the log file. Any more and we abort the script. For ($j = $Start; $j -le ($Count - 1); $j = $j + 1) { $Script:SkipLine = $j $Script:Skip = $False # If there is only one event in the log file, then $Log is not an array. If ($Count -eq 1) {$Line = $Log} Else {$Line = $Log[$j]} $Event = $Line.Event $Date = $Line.Date $Time = $Line.Time $Computer = $Line.Computer $User = $Line.User $Script:NumLines = $Script:NumLines + 1 # If Get-Date raises an error, the Trap above sets $Skip to $True and this function # is aborted. $CurrentTime = Get-Date("$Date $Time") -ErrorAction SilentlyContinue # Check that all fields are present in the line. If ((-Not $Event) -Or (-Not $Date) -Or (-Not $Time) -Or (-Not $Computer) ` -Or (-Not $User)) { # Line of log file not recognized. Write-Host "Line skipped (not recognized): $Line" ` -ForegroundColor red -BackgroundColor black $Script:NumBadLines = $Script:NumBadLines + 1 If ($Script:NumBadLines -gt 5) { Write-Host "6 lines of the log file not recognized. Program aborted." ` -ForegroundColor red -BackgroundColor black $Script:Stop = $True Break } } Else { # We know these fields are not Null, so we can Trim. $Event = $Event.Trim() $Date = $Date.Trim() $Time = $Time.Trim() $Computer = $Computer.Trim() $User = $User.Trim() $Session = "$Computer\$User" # Check if this is a logon or logoff event. If ($Event.ToLower() -eq "logon") { # Check if the last event for this session was a logon. If ($LogonSessions.ContainsKey($Session)) { # Logoff event missing for previous logon event. # Computer may have crashed or the user logged off when the log file # was unavailable or could not be reached. $PreviousTime = $LogonSessions[$Session] "$Session,$PreviousTime,(unknown),(unknown)" $Script:NumSessions = $Script:NumSessions + 1 $Script:NumNoLogoffs = $Script:NumNoLogoffs + 1 # Remove previous session from the hash table. # Otherwise, we will have a duplicate session. $LogonSessions.Remove($Session) } # Add this session to the hash table. $LogonSessions.Add($Session, $CurrentTime) } If ($Event.ToLower() -eq "logoff") { # Check if the last event for this session was a logon. If ($LogonSessions.ContainsKey($Session)) { # Calculate timespan user was logged on. $PreviousTime = $LogonSessions[$Session] $TS = [Timespan]($CurrentTime - $PreviousTime) $Days = $TS.Days # Format hours, minutes, and seconds as two digit numbers with # no fractional seconds. $Hrs = $("0" + $TS.Hours).ToString() If ($Hrs.Length -eq 3) {$Hrs = $Hrs.Substring(1)} $Mins = $("0" + $TS.Minutes).ToString() If ($Mins.Length -eq 3) {$Mins = $Mins.Substring(1)} # Round the seconds plus milliseconds to the nearest second. # [Math]::Truncate and [Math]::Round are not supported on Windows RT 8.1. $Secs = "0" + ($TS.Seconds + ($TS.Milliseconds/1000) + 0.5).ToString() $Dot = $Secs.IndexOf(".") If ($Dot -gt 0) {$Secs = $Secs.SubString(0, $Dot)} If ($Secs.Length -eq 3) {$Secs = $Secs.SubString(1)} # The ":" character does not work in PowerShell V1, even if escaped. $Duration = $("$Days.$Hrs/$Mins/$Secs").Replace("/", ":") "$Session,$PreviousTime,$CurrentTime,$Duration" $Script:NumSessions = $Script:NumSessions + 1 # Remove this session from the hash table. $LogonSessions.Remove($Session) } Else { # Previous logon event missing. "$Session,(unknown),$CurrentTime,(unknown)" $Script:NumSessions = $Script:NumSessions + 1 $Script:NumNoLogons = $Script:NumNoLogons + 1 } } If (($Event.ToLower() -ne "logon") -and ($Event.ToLower() -ne "logoff")) { Write-Host "Event in line $Line of log file not recognized. Line Skipped" ` -ForegroundColor red -BackgroundColor black $Script:NumBadLines = $Script:NumBadLines + 1 If ($Script:NumBadLines -gt 5) { Write-Host "6 lines of the log file not recognized. Program aborted." ` -ForegroundColor red -BackgroundColor black $Script:Stop = $True Break } } } } } # Initialize counters. $NumLines = 0 $NumSessions = 0 $NumWarnings = 0 $NumBadLines = 0 $NumNoLogoffs = 0 $NumNoLogons = 0 $NumStillLoggedOn = 0 # Check for parameter identifying the log file. If ($Args.Count -eq 1) { $FileName = $Args[0] } Else { # Prompt for the log file. $FileName = Read-Host "Enter log file to be processed" } # Test for existence of the log file. If (-Not (Test-Path -Path $FileName)) { Write-Host "The log file $FileName not found. Program aborted." ` -ForegroundColor red -BackgroundColor black Break } # Check if the log file is empty or has one line. PowerShell Version 1 does not # support the -Header parameter, so the file must have a header line. This script # will add a header line if required. $LogFile = Import-Csv -Path $FileName If (-Not $LogFile) { # The log file is either empty or has one line. # Import-Csv without the -Header parameter always assumes the first line is a header. $Contents = Get-Content -Path $FileName If (-Not $Contents) { # The log file is empty. Write-Host "The log file $FileName is empty. Program aborted." ` -ForegroundColor yellow -BackgroundColor black Break } # The log file has one line. Check if the line is an event or a header line. # At this point we assume the header fields are in the order specified. If ($Contents.Replace(" ", "").ToLower() -Like "event,date,time,computer,user*") { # The log file has just a header line. Write-Host "The log file $FileName has no events. Program aborted." ` -ForegroundColor yellow -BackgroundColor black Break } If (($Contents.ToLower() -Like "logon,*") -Or ($Contents.ToLower() -Like "logoff,*")) { # Log file appears to have one event, but no header line. Write-Host "The log file $FileName has one event, but no header line." ` -ForegroundColor yellow -BackgroundColor black $NumWarnings = $NumWarnings + 1 # Insert a header line at the beginning of the log file. Set-Content -Path $FileName -Value "Event,Date,Time,Computer,User" Add-Content -Path $FileName -Value $Contents Write-Host "A header line has been added at the beginning of the file." ` -ForegroundColor green -BackgroundColor black # Read the log file again, this time with the header line added. # The script can continue. $LogFile = Import-Csv -Path $FileName } Else { # The contents of the log file are not recognized. Write-Host "The contents of log file $FileName are not recognized. Program aborted." ` -ForegroundColor red -BackgroundColor black Break } } # Read only the first event of the log file to check the header line. # At this point we know the file has a header line and at least one event. # If the file has more than one event, $LogFile is an array. # If the file has just one event, it is not. In that case $NumberOfLines is Null. $NumberOfLines = $LogFile.Count If (-Not $NumberOfLines) { $NumberOfLines = 1 $Line = $LogFile } Else { $Line = $LogFile[0] } $Event = $Line.Event $Date = $Line.Date $Time = $Line.Time $Computer = $Line.Computer $User = $Line.User # Check the header line. The script checked the header line when the log file had just # one line. Now the script must check log files with more than one line. If ((-Not $Event) -Or (-Not $Date) -Or (-Not $Time) -Or (-Not $Computer) -Or (-Not $User)) { # The header line is not recognized. Check if the first line is an event. $Contents = Get-Content -Path $FileName -TotalCount 1 If (($Contents.ToLower() -Like "logon,*") -Or ($Contents.ToLower() -Like "logoff,*")) { # Log file appears to have events, but no header line. Write-Host "The log file $FileName has events, but no header line." ` -ForegroundColor yellow -BackgroundColor black $NumWarnings = $NumWarnings + 1 # Insert a header line at the beginning of the log file. $Contents = Get-Content -Path $FileName Set-Content -Path $FileName -Value "Event,Date,Time,Computer,User" Add-Content -Path $FileName -Value $Contents Write-Host "A header line has been added at the beginning of the file." ` -ForegroundColor green -BackgroundColor black # Read the log file again, this time with the header line added. # The script can continue. $LogFile = Import-Csv -Path $FileName } Else { # The contents of the log file are not recognized. $Ns = $Line | Get-Member -MemberType NoteProperty | Select Name $Y = "Header: " ForEach ($N In $Ns) { $Y = $Y + $N.Name + "," } # Strip off trailing comma. $Y = $Y.Substring(0, $Y.Length - 1) Write-Host "$Y not in correct format. Program aborted." ` -ForegroundColor red -BackgroundColor black Break } } # Hash table of user sessions. The key will be the session, in the form ComputerName\UserName. # The value will be the logon datetime. Only logons are maintained in the hash table. When # the corresponding logoff event is detected in the log file, the session information is # output and the session is deleted from the hash table. $LogonSessions = @{} # Read each event in the log file. # At this point we know the log file has a header and at least one event. # The script must determine $NumberOfLines again, because a header line may have been added. $NumberOfLines = $LogFile.Count If (-Not $NumberOfLines) {$NumberOfLines = 1} $Initial = 0 $Stop = $False # Call the function to read the LogFile and parse the lines. ReadLog $LogFile $Initial $NumberOfLines # If a line with a bad datetime was skipped, read the rest of the log file. If (($Skip -eq $True) -and ($Stop -eq $False)) { $Skip = $False $Initial = $SkipLine + 1 # Continue with the next line in the log file. ReadLog $LogFile $Initial $NumberOfLines } # If another line with a bad datetime was skipped, read the rest of the log file. If (($Skip -eq $True) -and ($Stop -eq $False)) { $Skip = $False $Initial = $SkipLine + 1 # Continue with the next line in the log file. ReadLog $LogFile $Initial $NumberOfLines } # If a third line with a bad datetime was skipped, read no more lines of the log file. If (($Skip -eq $True) -and ($Stop -eq $False)) { Write-Host "More than two lines of the log file have bad dates. No more lines processed." ` -ForegroundColor red -BackgroundColor black $NLines = '{0:n0}' -f $NumLines Write-Host "ParseLogons.ps1" -ForegroundColor green -BackgroundColor black Write-Host "Date: $(Get-Date)" -ForegroundColor green -BackgroundColor black Write-Host "Log File: $FileName" -ForegroundColor green -BackgroundColor black Write-Host "Program halted after reading $NLines lines of the log file." ` -ForegroundColor green -BackgroundColor black Break } If ($Stop -eq $True) { $NLines = '{0:n0}' -f $NumLines Write-Host "ParseLogons.ps1" -ForegroundColor green -BackgroundColor black Write-Host "Date: $(Get-Date)" -ForegroundColor green -BackgroundColor black Write-Host "Log File: $FileName" -ForegroundColor green -BackgroundColor black Write-Host "Program halted after reading $NLines lines of the log file." ` -ForegroundColor green -BackgroundColor black Break } # Loop through the sessions to find users still logged on. ForEach ($Entry In $LogonSessions.Keys) { $PreviousTime = $LogonSessions[$Entry] "$Entry,$PreviousTime,(still logged on),(unknown)" $NumSessions = $NumSessions + 1 $NumStillLoggedOn = $NumStillLoggedOn + 1 } # Format the totals. $NLines = '{0:n0}' -f $NumLines $Max = $NLines.Length $NSessions = ('{0:n0}' -f $NumSessions).PadLeft($Max, " ") $NWarnings = ('{0:n0}' -f $NumWarnings).PadLeft($Max, " ") $NBadLines = ('{0:n0}' -f $NumBadLines).PadLeft($Max, " ") $NNoLogoffs = ('{0:n0}' -f $NumNoLogoffs).PadLeft($Max, " ") $NNoLogons = ('{0:n0}' -f $NumNoLogons).PadLeft($Max, " ") $NStillLoggedOn = ('{0:n0}' -f $NumStillLoggedOn).PadLeft($Max, " ") # Display totals. Write-Host "ParseLogons.ps1" -ForegroundColor green -BackgroundColor black Write-Host "Date: $(Get-Date)" -ForegroundColor green -BackgroundColor black Write-Host "Log File: $FileName" -ForegroundColor green -BackgroundColor black Write-Host "Totals:" -ForegroundColor green -BackgroundColor black Write-Host " Lines read in the log file: $NLines" ` -ForegroundColor green -BackgroundColor black Write-Host " Bad lines skipped: $NBadLines" ` -ForegroundColor green -BackgroundColor black Write-Host " Warnings: $NWarnings" ` -ForegroundColor green -BackgroundColor black Write-Host " Sessions with no Logoff : $NNoLogoffs" ` -ForegroundColor green -BackgroundColor black Write-Host " Sessions with no logon: $NNoLogons" ` -ForegroundColor green -BackgroundColor black Write-Host " Sessions still logged on: $NStillLoggedOn" ` -ForegroundColor green -BackgroundColor black Write-Host " Total Sessions: $NSessions" ` -ForegroundColor green -BackgroundColor black