Updated: May 15, 2015
When this article was originally published, two extra carriage returns were add causing the code to malfunction. The code below is correct.
My client for this week’s PowerShell class had a really interesting question. They needed to know where an account is being locked out at. OK, interesting. Apparently users hop around clients and forget to log off, leading to eventual lock out of their accounts. The accounts can be unlocked, but are then relocked after Active Directory replication.
This problem is solved in two parts. The first one is to modify the event auditing on the network. The second part is resolved with PowerShell.
The first part involves creating a group policy that will encompass your Domain Controllers. In this GPO, make these changes.
 
 
Once the above GPO has been applied, you will now begin to record lock outs. All accounts currently locked out will not have entries in the Security log until they report another lock out.
The second part is to use PowerShell to parse through all the Security logs on the domain controllers and tell you which client a user’s account was locked out on. This script is designed to be dot sourced or turned into a module. It is heavily commented so it can be used for instruction as well as an actually script in production.
When this article was originally published, two extra carriage returns were add causing the code to malfunction. The code below is correct.
My client for this week’s PowerShell class had a really interesting question. They needed to know where an account is being locked out at. OK, interesting. Apparently users hop around clients and forget to log off, leading to eventual lock out of their accounts. The accounts can be unlocked, but are then relocked after Active Directory replication.
This problem is solved in two parts. The first one is to modify the event auditing on the network. The second part is resolved with PowerShell.
The first part involves creating a group policy that will encompass your Domain Controllers. In this GPO, make these changes.
- Expand Computer Configuration \ Policies \ Windows Settings \ Security Settings \ Advanced Audit Policy Configuration \ Audit Policies \ Account Management
- Double click User Account Management
- Check Configure the following audit events.
- Check Success
Once the above GPO has been applied, you will now begin to record lock outs. All accounts currently locked out will not have entries in the Security log until they report another lock out.
The second part is to use PowerShell to parse through all the Security logs on the domain controllers and tell you which client a user’s account was locked out on. This script is designed to be dot sourced or turned into a module. It is heavily commented so it can be used for instruction as well as an actually script in production.
| 
Function Extract-UserName 
{ 
param ( 
   $StringData 
) 
   <# 
    Extract 
the username from the log file. 
     The 
following line of code performs several tasks.  
It receives 
     a multi 
line string that is the message data from the event.  This data 
     is saved 
in the variable $StringData.  Since this 
is a [STRING] object 
     we have 
access to the methods Split, Replace, and Trim. 
    - The 
first operation splits the string into an array of single 
lines. 
      This is 
accomplished with the "Split" method.  
The `n tells the 
      method 
to split on each new line. 
    - The next 
operation take the contents in array index [10] 
    - The 
"Replace" method is used to replace the part of the string that 
we 
      do not 
want.  In this case, we are replacing 
"Account Name:" with $NULL. 
    - The 
"Trim" method removes all leading and trailing spaces and 
leaves 
      us with 
just the username. 
   #> 
   ((($StringData.Split("`n"))[10]).Replace("Account Name:",$Null)).Trim() 
} 
Function Extract-ComputerName 
{ 
param ( 
   $StringData 
) 
   # Extract the computer name from the log 
file. 
   Write-Output ((($StringData.Split("`n"))[13]).Replace("Caller Computer Name:",$Null)).Trim() 
} 
<# 
.SYNOPSIS 
Discovers the 
client that a user account was locked out on. 
.DESCRIPTION 
Returns the 
client machine which has locked out a user account. 
See the NOTEs 
section for setup information. 
.PARAMETER 
$User 
The Name of the 
User Account that you want to discover the client  
that locked 
this account out. 
.PARAMETER 
$DaysToSearch 
This is the 
number of days to look back in the security logs. 
The default 
setting is 14 days. 
.EXAMPLE 
Find-LockingClients -user jyoder 
UserName                      LockingClient                
TimeLocked 
--------                      -------------                
---------- 
jyoder                        LON-CL1                      
8/27/2012 
7:02:06 
PM 
jyoder                        LON-CL1                      
8/28/2012 
8:53:42 
PM 
Returns the 
clients where the user account -jyoder was locked out. 
.EXAMPLE 
Find-LockingClients -User jyoder -DaysToSearch 
7 
Returns the 
clients where the user account -jyoder was locked out.  
This will 
search the security logs for the past 7 days.  
 
The default is 
14 days. 
.NOTES 
For the cmdlet 
to work, advanced auditing needs to be configured  
on the domain 
controllers. This can be configured locally or in  
Group 
Policy.  It must be done for all Domain 
Controllers. 
Category: 
Account Management 
Subcategory: 
User Account Management 
Audit for: 
Success 
#> 
Function Find-LockingClients 
{ 
Param( 
    [cmdletbinding()] 
    [Parameter(Mandatory=$True)][String]$User = $(Read-Host 
"Provide a user 
name"), 
    [int]$DaysToSearch = 
14 
) 
   # Get a list of all of the Domain 
Controllers 
   $DCs = Get-ADComputer -Filter * 
` 
    -SearchBase "OU=Domain Controllers,$((Get-ADDomain).distinguishedName)" 
   # Set up the number of days to search in 
the past. 
   $StartTime = (Get-Date).AddDays(-($DaysToSearch)) 
   # Create the hash for the event 
logs. 
   $LogHash = @{LogName = 
'Security';StartTime = $StartTime; ID=4740} 
   # Store each relevant event in the variable 
$Events. 
    ForEach($Item 
in $DCs) 
     { 
      # Error handling just in case on the Domain 
Controllers 
      # is 
offline. 
      Try 
       { 
          Write-Host "Gathering events from"$Item.Name-ForegroundColor Green 
` 
           -BackgroundColor 
DarkMagenta 
          $Events = Get-WinEvent -FilterHashtable $LogHash 
` 
          -ComputerName $Item.Name 
-ErrorAction Stop 
| 
          Sort-Object -Property 
TimeCreated 
       } 
      Catch 
       { 
           Write-host "Domain Controller"$Item.Name" if 
offline" ` 
           -ForegroundColor Red 
` 
           -BackgroundColor 
DarkRed 
       } 
      Finally 
       { 
            Write-Host $Item.Name"completed" -ForegroundColor Green 
` 
            -BackgroundColor 
DarkMagenta 
       } 
     } 
   # Set up the array to hold multiple sets of 
data from the event logs. 
   $EventArray = 
@() 
   ForEach ($Event 
in $Events) 
    { 
       # Send the event message data to the 
functions to extract 
       # Username and Computer 
Name. 
       $UserName = Extract-UserName $Event.Message 
       $ComputerName = Extract-ComputerName $Event.Message 
       # Write the data to the 
object. 
       $Obj = New-Object 
-TypeName PSObject 
       $Obj |Add-Member 
-MemberType NoteProperty -Name 
"UserName" -Value $UserName 
       $Obj |Add-Member 
-MemberType NoteProperty -Name 
"LockingClient" -Value 
$ComputerName 
       $Obj |Add-Member 
-MemberType NoteProperty -Name 
"TimeLocked" -Value $Event.TimeCreated 
       # Add the individual object to the array of 
objects. 
       $Obj | 
Where-Object {$_.Username -eq $User} 
    } 
   # Filter for the user specified in the 
parameter $User and 
   # Send the output into the 
Pipeline. 
  #$EventArray |Where-Object {$_.UserName -eq 
$User} 
} | 
Comments
I do not utilize the Quest cmdlets. If the object produced has a property called NAME that holds the computer name, then I do not see why not. Also, GET-ADDomainControler could work to extract the names of the domain controllers.
Jason
Thanks for the group policy thing. I was looking all over the place for this.
I do not have access to Apple Tech on my networks. If they are active directory integrated, go ahead and lock an account on one and see what is in the event log. I'm interested in hearing what you find.
Jason
The call to the remote computer is in the Get-WinEvent cmdlet. Doing a quick test on my computer to a non-existent server, and RPC error was returned.
Try this:
Enable Windows Remoting on your servers.
Change -------
$Events = Get-WinEvent -FilterHashtable $LogHash `
-ComputerName $Item.Name-ErrorAction Stop |
Sort-Object -Property TimeCreated
To -----------
$Events = Invoke-Command -ScriptBlock {
Param ($LogHash)
Get-WinEvent -FilterHashtable $LogHash `|
Sort-Object -Property TimeCreated } -ComputerName $Item.Name -EA Stop
I do not have a setup to test this. This should remove the RPC technology and utilize Windows PowerShell Remoting for the connection to the remote system. Search my blog of the post on enabling PowerShell Remoting with Group Policy.
What parameters do I need to supply?
I have tried:
Full name
"Full Name"
SamID
Domain\SamID
nothing
In all cases, I get the error:
[PS] E:\>Extract-UserName
You cannot call a method on a null-valued expression.
At E:\SCripts\PS\Functions\Extract-UserName.ps1:25 char:37
+ Write-Output ((($StringData.Split <<<< ("`n"))[10]).Replace("Account
+ CategoryInfo : InvalidOperation: (Split:String) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
What am I missing?
thanks in advance.
Extract-UserName is a supporting function. Take a look further down the code. You will see a cmdlet called Find-LockingClients. it also has a help file.
Jason
The Value parameter is required for a member of type "NoteProperty". Specify the Value parameter when adding members of
this type.
Value:
Thank you for sending the error. The source of the problem came from two extra carriage returns injected when I copied the code over. I've fixed the formatting in the blog to prevent that from occurring again. Please copy the above code and try again.
Jason
This creates a PowerShell Script Cmdlet in memory. just type Find-LockingClients and press Enter.
Remember, nothing will get reported if you just turned on the auditing. If auditing was not enabled until today, you will not receive information on events from yesterday.
Jason
There is a property named ReplacementStrings that will contain the username, computername, Domain name, and some related SIDS. Using this, you can get the names without needing to parse the message.
$a = get-eventlog -logname security -instanceid 4740 -newest 1
$a.replacementstrings[0] will be the username.
$a.replacementstrings[1] will be the computer that locked it out.
$a.replacementstrings[5] will be the domain the account was in that locked it out.
Thought you would find this interesting.
-Bill