Have you ever had trouble getting lockout information for a single user? The DCs have the logs, but getting them can be tough. The core idea is to use the Get-Winevent command to gather the logs needed. Doing this from a remote computer is time-consuming. However, using the Invoke-Command tends to speed things up. Inside this example, I am going to use something called Splatting. Splatting allows you to drop everything a command needs into a command quickly and easily by creating a hash table. Now, not all commands like splatting, so be aware of that. Here is the heart of the code:

$parameters = @{
        ComputerName = $ComputerName
        ScriptBlock  = {
            Param ($param1)
            $FilterTable = @{
                'StartTime' = $param1
                'EndTime'   = (Get-date)
                'LogName'   = 'Security'
                'Id'        = 4740
            }
            $Events = Get-WinEvent -FilterHashtable $FilterTable
            foreach ($E in $Events) {
                [pscustomobject]@{
                    Time     = $E.TimeCreated
                    ID       = $E.ID 
                    DC       = $E.Properties[4].value
                    SID      = $E.Properties[2].value
                    Domain   = $E.Properties[5].Value
                    Username = $E.Properties[0].value
                    Computer = $E.Properties[1].value
                }
            }
        }
        ArgumentList = $time           
    }
Invoke-command @Parameters

The first thing we are doing is creating the parameters that will be splatted. Just like a hashtable, it’s something = something. So the computer name is the Array computername. The computer name inside the Invoke-command can handle arrays. Making this much easier as we want the DCs.

Next is the script block. This is where the invoke-command will be executing everything from. We start off with a Param () block. It will get it’s input from the Argumentlist later. The Argumentlist is the variables we will be dropping into the command. In this case, it will be time. As in this script you can build your time based on how far back you want to search your logs.

Next is the filter table. The Get-winevent command likes hashtables. So we start off with our starttime, basically how far back we are going to go. Then the end time which is the time of execution. The Logname is the type, there is an application, security, system, and more. Since the lockouts are security, we choose security. Finally, we want the ID. For account lockouts, the ID is 4740. There is an online Encyclopedia for windows logs. (Link)

The Get-WinEvent command is followed by the FilterHashtable flag. We dump this into the events variable for sort throu. Now we search each Event in Events. We express this with an E because $event is a system variable and we don’t want to cause any issues. We want the time it was created. The ID, what DC it came from. The SID of the user. The Domain. The username of the user, and finally we want the calling workstation. Once all that is put together we splat it into the command Invoke-Command.

That is the heart, but not the power. This script allows you to call back from the day, hour, and minute back. If you don’t put in a input, it makes it a day for you. It also can target a single user or different computers. Finally it gives you the ability to use different credentials. Lets take a look at the parameters.

param (
        [parameter(Helpmessage = "Username of target")][Alias('User', 'Samaccountname')][String[]]$Username,    
        [parameter(Helpmessage = "Days back")][int]$Day,
        [parameter(Helpmessage = "Days back")][int]$Hour,
        [parameter(Helpmessage = "Days back")][int]$Minute,
        [parameter(Helpmessage = "Computers to target, default is domain controllers.")][Alias('Computer', 'PC')][String[]]$ComputerName = (Get-ADDomain).ReplicaDirectoryServers,
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )

We first are looking for a list of usernames. Notice we don’t have to declare this variable. So, if we just want all the lockouts, we just don’t delcare it. This is done by using the PSBoundParameters.ContainsKey option. So, if the script sees the username is being used, it will follow this command.

if ($PSBoundParameters.ContainsKey('Username')) {
        foreach ($user in $Username) {
            $Return | Where-Object { $_.Username -like "$user" } | Sort-Object Time | Select-Object Username, Domain, SID, Time, Computer, PSComputerName
        }
    }

The next parameters are also optional. It is days, hour, and minutes back. So, you can state 1 day, 3 hours, and 2 minutes ago and it will find the times during starting at that moment forwarder. This is good if you plan to use this script as an auditor (what it was built for). How this is achived once again is the use of psboundparameters.containskey.

$time = Get-date
if ($PSBoundParameters.ContainsKey('Day')) { $time = ($time.AddDays( - ($day))) } 
    if ($PSBoundParameters.ContainsKey('Hour')) { $time = ($time.AddHours( - ($Hour))) } 
    if ($PSBoundParameters.ContainsKey('Minute')) { $time = ($time.AddMinutes( - ($Minute))) } 
    if (!($PSBoundParameters.ContainsKey('Day')) -and !($PSBoundParameters.ContainsKey('Hour')) -and !($PSBoundParameters.ContainsKey('Minute'))) {
        $time = $time.AddDays( - (1))
    }

We first declare the time with the current time/date. Then based on the input, we remove days, hours, or minutes from the current time. If there is no input, we tell time to remove 1 day. Depending on the size of your organization and how many logs you have, this can be useful. The $time will be used in the argumentlist.

Now we have the Computername parameter. If you have differentiating computers than your DCs that handles these logs, you can target them with this command. Otherwise, we grab the dc information with a single line of code.

(Get-ADDomain).ReplicaDirectoryServers

Finally we have the ability to add the credentials. Once you delcare the credentials, we add the credential flag to our splat. To do this we create a hashtable with a single item and add it to the parameter.

if ($PSBoundParameters.ContainsKey('Credential')) {
        $parameters += @{Credential = $Credential }
    }

Now, lets put all this together in a single script so you can copy it and move along with your day.

Function Get-SHDLockoutInfo {
    [cmdletbinding()]
    param (
        [parameter(Helpmessage = "Username of target")][Alias('User', 'Samaccountname')][String[]]$Username,    
        [parameter(Helpmessage = "Days back")][int]$Day,
        [parameter(Helpmessage = "Days back")][int]$Hour,
        [parameter(Helpmessage = "Days back")][int]$Minute,
        [parameter(Helpmessage = "Computers to target, default is domain controllers.")][Alias('Computer', 'PC')][String[]]$ComputerName = (Get-ADDomain).ReplicaDirectoryServers,
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )
    $time = Get-date
    if ($PSBoundParameters.ContainsKey('Day')) { $time = ($time.AddDays( - ($day))) } 
    if ($PSBoundParameters.ContainsKey('Hour')) { $time = ($time.AddHours( - ($Hour))) } 
    if ($PSBoundParameters.ContainsKey('Minute')) { $time = ($time.AddMinutes( - ($Minute))) } 
    if (!($PSBoundParameters.ContainsKey('Day')) -and !($PSBoundParameters.ContainsKey('Hour')) -and !($PSBoundParameters.ContainsKey('Minute'))) {
        $time = $time.AddDays( - (1))
    }
    $parameters = @{
        ComputerName = $ComputerName
        ScriptBlock  = {
            Param ($param1)
            $FilterTable = @{
                'StartTime' = $param1
                'EndTime'   = (Get-date)
                'LogName'   = 'Security'
                'Id'        = 4740
            }
            $Events = Get-WinEvent -FilterHashtable $FilterTable
            foreach ($E in $Events) {
                [pscustomobject]@{
                    Time     = $E.TimeCreated
                    ID       = $E.ID 
                    DC       = $E.Properties[4].value
                    SID      = $E.Properties[2].value
                    Domain   = $E.Properties[5].Value
                    Username = $E.Properties[0].value
                    Computer = $E.Properties[1].value
                }
            }
        }
        ArgumentList = $time           
    }
    if ($PSBoundParameters.ContainsKey('Credential')) {
        $parameters += @{Credential = $Credential }
    }
    $Return = Invoke-Command @parameters 
    if ($PSBoundParameters.ContainsKey('Username')) {
        foreach ($user in $Username) {
            $Return | Where-Object { $_.Username -like "$user" } | Sort-Object Time | Select-Object Username, Domain, SID, Time, Computer, PSComputerName
        }
    }
    else {
        $Return | Sort-Object UserName | Select-Object Username, Domain, SID, Time, Computer, PSComputerName
    }
} 

I hope you enjoy this little script and I hope it helps you grow your company. If you find any bugs, let me know. Thank you so much and have a great day!