Advanced Windows PowerShell Scripting Video Training

Advanced Windows PowerShell Scripting Video Training
Advanced Windows PowerShell Scripting Video Training

Friday, August 31, 2012

Where did a User’s Account Get Locked Out?

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.
  • 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
This will allow the domain controllers to audit event 4740 which tells use that an account with locked out. Your users will see something like the image below when their account is locked out.
clip_image001
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}


}










17 comments:

Owen Martin said...

Can you use get-qadcomputer instead of get-adcomputer?

Jason Yoder, MCT said...

Owen,

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

Anonymous said...

Filtering the specific eventid in eventviewer would have been much easier, no? :)

Thanks for the group policy thing. I was looking all over the place for this.

Jason Yoder, MCT said...

It would be easier, if you only had one Domain Controller. What if you had 10, 20, or even 100 Domain Controllers? This method touches all of them by executing it only once.

Dennis Browning said...

What would be returned if you were to be locked by say a Mac, or iPhone or other tablet?

Jason Yoder, MCT said...

Dennis,

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

Dennis Browning said...

After a quick test, I tried to access wireless via a Mac and it showed up blank. I will test further tomorrow when in the office from a login window (via centrify) and an ipad. Thanks

Dennis Browning said...

How does this script decided whether a DC is online or offline? I keep getting messages that the DC is offline but I know for a fact it is online.

Jason Yoder, MCT said...

Dennis,

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.

Lars Panzerbjørn said...

Thanks for the post.

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.

Jason Yoder, MCT said...

Lars,

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

Ken Elmer said...

I have tweaked the powershell script to use Invoke-Command to run the Get-WinEvent part on each domain controller simultaneously and also run as a straight script instead of dot sourced. Much quicker with 8 DCs.

Bill Curtis said...

I keep getting this message when I run it:
The Value parameter is required for a member of type "NoteProperty". Specify the Value parameter when adding members of
this type.
Value:

Jason Yoder, MCT said...

Bill,

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

Anonymous said...

Awesome functions! Thanks for this!

Anthony Confalone said...

I know this is an old post, hoping someone still receives comments. I ran as admin, set execution policy, copy the script into power shell and it simply stares back at me with a blinking cursor after the last line. Sorry, newbie here, what am I doing wrong?

Jason Yoder, MCT said...

Anthony,

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