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