Retrieve Security events from Active Directory using PowerShell

In a previous blog post (here), I wrote about how to get a list of changes in Active Directory administrative groups. I got a question about that on Facebook… The question was: Nice to get a list of changed groups and what the change was, but what account made that change? This blog post shows you a way to get all the security events from the Domain Controller security logs 🙂

Requirements

The script should get a list of all events regarding users added/deleted/changed, but also for group membership changes. It should output them in a .csv file, and can email those changes if found. Also, it should be able to run at an interval, and you should be able to specify a time frame in which the events are retrieved.

Challenges

Getting events from a Windows Security eventlog is easy, but it’s not that fast. This is where Get-WinEvent comes together with the -FilterHashtable parameter 🙂 (More about that here). But there are limits to using that. I collected around 60 eventid’s, indicating a change in user, groups, or computer objects… You can only use about 20 of those in one FilterHashTable. More about that here. Luckily PowerShell is very flexible, created multiple filters, and looped through them in one foreach loop.

Running the script

The Get-SecurityEvents function has a few parameters:

  • Hours, the number of hours back in time the script has to search for Security events
  • Outputfolder, the location where the events should be stored in CSV format with a time/date stamp
  • To_emailaddress, the email address (when specified) where the CSV file should be sent to
  • From_emailaddress, the email address that should be used as the sender
  • Smtpserver, the relay server which sends the email message together with the CSV file

In the example below, I specified c:\scripts as the output folder to search for Security events from 1 hour ago until now:

The contents of the output CSV file look like this:

You can also specify the email parameters. In this example, I specified the To and From address and a relay server for sending the email:

The email looks like this:

After opening the attached CSV file (and some Text to Columns if needed), it looks like this: (Masked some things here, of course 🙂 ) :

The script

Below is the script/function. It searches and connects to your Domain Controller, which holds the PDC Emulator FSMO role. The first version connected all Domain Controllers, but the events are duplicated on them, so that didn’t make any sense 😉 You must run it as an Administrator, and you can schedule or call it from any solution to output it to a CSV file. That CSV file can be imported into another solution to report on it or to archive events. You can also adjust it to only search for specific Event IDs and send an email if events were found. Have fun 🙂
Note: The script needs the ActiveDirectory module to be installed

function Get-SecurityEvents {
      param (
            [Parameter(Mandatory = $true, HelpMessage = "Number of hours to search back", Position = 1)][string]$hours,
            [Parameter(Mandatory = $true, HelpMessage = "Folder for storing found events", Position = 2)][string]$outputfolder,
            [Parameter(Mandatory = $False, HelpMessage = "Enter email-address to send the logs to", Position = 3)][string]$to_emailaddress,
            [Parameter(Mandatory = $False, HelpMessage = "Enter the From Address", Position = 4)][string]$from_emailaddress,
            [Parameter(Mandatory = $False, HelpMessage = "Enter the SMTP server to use", Position = 5)][string]$smtpserver
      )
      
      # Test admin privileges without using -Requires RunAsAdministrator,
      # which causes a nasty error message, if trying to load the function within a PS profile but without admin privileges
      if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]"Administrator")) {
            Write-Warning ("Function {0} needs admin privileges, aborting..." -f $MyInvocation.MyCommand)
            break
      }

      #Get Domain Controller with PDC FSMO Role to get events from
      try {
            $domaincontroller = (Get-ADDomain).PDCEmulator
      }
      catch {
            Write-Warning ("Unable to get Domain information, check ActiveDirectory module installation. Aborting...")
      }

      #Event id's from https://www.ultimatewindowssecurity.com/securitylog/book/page.aspx?spid=chapter8
      $useraccountmanagementeventids = 
      4720, #A user account was created
      4722, #A user account was enabled
      4723, #An attempt was made to change an account's password
      4724, #An attempt was made to reset an accounts password
      4725, #A user account was disabled
      4726, #A user account was deleted
      4738, #A user account was changed
      4740, #A user account was locked out
      4767, #A user account was unlocked
      4780, #The ACL was set on accounts which are members of administrators groups
      4781, #The name of an account was changed
      4794, #n attempt was made to set the Directory Services Restore Mode administrator password
      5376, #Credential Manager credentials were backed up
      5377  #redential Manager credentials were restored from a backup

      $computeraccountmanagementeventids = 
      4741, #A computer account was created
      4742, #A computer account was changed
      4743  #A computer account was deleted
      
      $securitygroupmanagementeventids =
      4727, #A security-enabled global group was created
      4728, #A member was added to a security-enabled global group
      4729, #A member was removed from a security-enabled global group
      4730, #A security-enabled global group was deleted
      4731, #A security-enabled local group was created
      4732, #A member was added to a security-enabled local group
      4733, #A member was removed from a security-enabled local group
      4734, #A security-enabled local group was deleted
      4735, #A security-enabled local group was changed
      4737, #A security-enabled global group was changed
      4754, #A security-enabled universal group was created
      4755, #A security-enabled universal group was changed
      4756, #A member was added to a security-enabled universal group
      4757, #A member was removed from a security-enabled universal group
      4758, #A security-enabled universal group was deleted
      4764 #A groups type was changed
      
      $distributiongroupmanagementeventids =
      4744, #A security-disabled local group was created
      4745, #A security-disabled local group was changed
      4746, #A member was added to a security-disabled local group
      4747, #A member was removed from a security-disabled local group
      4748, #A security-disabled local group was deleted
      4749, #A security-disabled global group was created
      4750, #A security-disabled global group was changed
      4751, #A member was added to a security-disabled global group
      4752, #A member was removed from a security-disabled global group
      4753, #A security-disabled global group was deleted
      4759, #A security-disabled universal group was created
      4760, #A security-disabled universal group was changed
      4761, #A member was added to a security-disabled universal group
      4762, #A member was removed from a security-disabled universal group
      4763  #A security-disabled universal group was deleted

      $applicationgroupmanagementeventids =
      4783, #A basic application group was created
      4784, #A basic application group was changed
      4785, #A member was added to a basic application group
      4786, #A member was removed from a basic application group
      4787, #A non-member was added to a basic application group
      4788, #A non-member was removed from a basic application group
      4789, #A basic application group was deleted
      4790, #An LDAP query group was created
      4791, #A basic application group was changed
      4792  #An LDAP query group was deleted

      $otheraccountmanagementeventids =
      4739, #Domain Policy was changed
      4793  #The Password Policy Checking API was called


      #Set empty collection variable, date and eventids
      $collection = @()
      $date = (Get-Date).AddHours( - $($hours))
            
      $filteruseraccountmanagement = @{
            Logname   = 'Security'
            ID        = $useraccountmanagementeventids
            StartTime = $date
            EndTime   = [datetime]::Now
      }

      $filtercomputeraccountmanagement = @{
            Logname   = 'Security'
            ID        = $computeraccountmanagementeventids
            StartTime = $date
            EndTime   = [datetime]::Now
      }

      $filtersecuritygroupmanagement = @{
            Logname   = 'Security'
            ID        = $securitygroupmanagementeventids
            StartTime = $date
            EndTime   = [datetime]::Now
      }

      $filterdistributiongroupmanagement = @{
            Logname   = 'Security'
            ID        = $distributiongroupmanagementeventids
            StartTime = $date
            EndTime   = [datetime]::Now
      }

      $filterapplicationgroupmanagement = @{
            Logname   = 'Security'
            ID        = $applicationgroupmanagementeventids
            StartTime = $date
            EndTime   = [datetime]::Now
      }

      $filterotheraccountmanagement = @{
            Logname   = 'Security'
            ID        = $otheraccountmanagementeventids
            StartTime = $date
            EndTime   = [datetime]::Now
      }

      #Retrieve events
      Write-Host ("Retrieving Security events from {0}..." -f $domaincontroller) -ForegroundColor Green
      foreach ($eventids in `
                  $filteruseraccountmanagement, `
                  $filtercomputeraccountmanagement, `
                  $filtersecuritygroupmanagement, `
                  $filterdistributiongroupmanagement, `
                  $filterapplicationgroupmanagement, `
                  $filterotheraccountmanagement ) {
            $events = Get-WinEvent -FilterHashtable $eventids -ComputerName $domaincontroller -ErrorAction SilentlyContinue 
            foreach ($event in $events) {
                  Write-Host ("- Found EventID {0} on {1} and adding to list..." -f $event.id, $event.TimeCreated) -ForegroundColor Green
                  $eventfound = [PSCustomObject]@{
                        DomainController = $domaincontroller
                        Timestamp        = $event.TimeCreated
                        LevelDisplayName = $event.LevelDisplayName
                        EventId          = $event.Id
                        Message          = $event.message -replace '\s+', " "
                  }
                  $collection += $eventfound
            }
      }

      if ($null -ne $collection) {
            $filenametimestamp = Get-Date -Format 'dd-MM-yyyy-HHmm'
            Write-Host ("- Saving the {0} events found to {1}..." -f $collection.count, "$($outputfolder)\events_$($filenametimestamp).csv") -ForegroundColor Green
            $collection | Sort-Object TimeStamp, DomainController, EventId | Export-Csv -Delimiter ';' -NoTypeInformation -Path "$($outputfolder)\events_$($filenametimestamp).csv"
      
            if ($to_emailaddress) {
                  $emailoptions = @{
                        Attachments = "$($outputfolder)\events_$($filenametimestamp).csv"
                        Body        = "See Attached CSV file"
                        ErrorAction = "Stop"
                        From        = $from_emailaddress
                        Priority    = "High" 
                        SmtpServer  = $smtpserver
                        Subject     = "Security event found"
                        To          = $to_emailaddress
                  }
                  Write-Host ("- Emailing the {0} events found to {1}..." -f $collection.count, $to_emailaddress) -ForegroundColor Green
                  try {
                        Send-MailMessage @emailoptions 
                  }
                  catch {
                        Write-Warning ("Unable to email results, please check the email settings...")
                  }
            }
      }
}

Download the script(s) from GitHub here

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.