Report on changed Active Directory groups using PowerShell

Currently, I’m working for a customer on a new security model for their Active Directory, one of the things that we discussed was how to report on changes in certain administrative groups. I did this in the past using the Active Directory command-line tools (dsquery, dsget, etc.) but in this blog post, I will show you how to do this using PowerShell.

Requirements

The script should show the difference in members between the previous time it ran and the current situation, logs should be saved for reviewing purposes and an email should be sent when there is a change detected.

Running the script

The best way would be to add this as a scheduled task on an admin server or Domain Controller with the Active Directory module for PowerShell installed. The script can be run with “powershell.exe” as Program/Script, c:\scripts\AdminGroupChangeReport.ps1 as argument and c:\scripts as Start in. Configure a trigger with an interval of an hour and of course, use an account to run the script with enough credentials 🙂

Below is the output of the script when no change is detected, it shows the groups being checked and the members in it:

If it does find an Admin group that has been changed (Member added/removed), it will send an email with a subject of ‘Admin group changed detected’ with the following body: (Options are configurable in the script)

The logs from the current and previous runs are stored in the Logs folder that you have specified:

The time-stamped changes.txt contains the changes found, the previousmembers.csv is the one from the previous run and the currentmembers.csv and previousmembers.csv are the files from the current run.

The script

Below is the script, make sure the $logs variable in the script is configured to the path you store the script in, and configure the email options to the ones suited for your environment. The admingroups variable consists of all the groups that you want to monitor, these are just for example and add your own and use the Display Name value.

#Set Logs folder
$logs = 'c:\scripts\logs'

#Create Logs folder it it doesn't exist
if (-not (Test-Path -Path $logs -PathType Any)) {
    New-Item -Path $logs -ItemType Directory | Out-Null
}

#Start Transcript logging to $logs\run.log
Start-Transcript -Path "$($logs)\run.log" -Append

#Configure groups to monitor
$admingroups = @(
    "Account Operators",
    "Administrators",
    "Backup Operators",
    "Domain Admins",
    "DNSAdmins",
    "Enterprise Admins",
    "Group Policy Creator Owners",
    "Schema Admins",
    "Server Operators"
)

#rename previous currentmembers.csv to previousmembers.csv and rename the old
#previousmembers.csv to one with a time-stamp for archiving
if (Test-Path -Path "$($logs)\previousmembers.csv" -ErrorAction SilentlyContinue) {
    #Set date format variable
    $date = Get-Date -Format 'dd-MM-yyyy-HHMM'
    Write-Host ("- Renaming previousmembers.csv to {0}_previousmembers.csv" -f $date) -ForegroundColor Green
    Move-Item -Path "$($logs)\previousmembers.csv" -Destination "$($logs)\$($date)_previousmembers.csv" -Confirm:$false -Force:$true
}

if (Test-Path -Path "$($logs)\currentmembers.csv" -ErrorAction SilentlyContinue) {
    Write-Host ("- Renaming currentmembers.csv to previousmembers.csv") -ForegroundColor Green
    Move-Item -Path "$($logs)\currentmembers.csv" -Destination "$($logs)\previousmembers.csv" -Confirm:$false -Force:$true
}

#Retrieve all direct members of the admingroups,
#store them in the members variable and output
#them to currentmembers.csv
$members = foreach ($admingroup in $admingroups) {
    Write-Host ("- Checking {0}" -f $admingroup) -ForegroundColor Green
    try {
        $admingroupmembers = Get-ADGroupMember -Identity $admingroup -Recursive -ErrorAction Stop | Sort-Object SamAccountName
    }
    catch {
        Write-Warning ("Members of {0} can't be retrieved, skipping..." -f $admingroup)
        $admingroupmembers = $null
    }
    if ($null -ne $admingroupmembers) {
        foreach ($admingroupmember in $admingroupmembers) {
            Write-Host ("  - Adding {0} to list" -f $admingroupmember.SamAccountName) -ForegroundColor Green
            [PSCustomObject]@{
                Group  = $admingroup
                Member = $admingroupmember.SamAccountName
            }
        }
    }
}

#Save found members to currentmembers.csv and create previousmembers.csv if not present (First Run)
Write-Host ("- Exporting results to currentmembers.csv") -ForegroundColor Green
$members | export-csv -Path "$($logs)\currentmembers.csv" -NoTypeInformation -Encoding UTF8 -Delimiter ';'
if (-not (Test-Path "$($logs)\previousmembers.csv")) {
    $members | export-csv -Path "$($logs)\previousmembers.csv" -NoTypeInformation -Encoding UTF8 -Delimiter ';'
}

#Compare currentmembers.csv to the #previousmembers.csv
$CurrentMembers = Import-Csv -Path "$($logs)\currentmembers.csv" -Delimiter ';'
$PreviousMembers = Import-Csv -Path "$($logs)\previousmembers.csv" -Delimiter ';'
Write-Host ("- Comparing current members to the previous members") -ForegroundColor Green
$compare = Compare-Object -ReferenceObject $PreviousMembers -DifferenceObject $CurrentMembers -Property Group, Member
if ($null -ne $compare) {
    $differencetotal = foreach ($change in $compare) {
        if ($change.SideIndicator -match ">") {
            $action = 'Added'
        }
        if ($change.SideIndicator -match "<") {
            $action = 'Removed'
        }

        [PSCustomObject]@{
            Date   = $date
            Group  = $change.Group
            Action = $action
            Member = $change.Member
        }
    }

    #Save output to file
    $differencetotal | Sort-Object group | Out-File "$($logs)\$($date)_changes.txt"

    #Send email with changes to admin email address
    Write-Host ("- Emailing detected changes") -ForegroundColor Green
    $body = Get-Content "$($logs)\$($date)_changes.txt" | Out-String
    $options = @{
        Body        = $body
        Erroraction = 'Stop'
        From        = 'admin@powershellisfun.com'
        Priority    = 'High'
        Subject     = "Admin group change detected"
        SmtpServer  = 'emailserver.domain.local'
        To          = 'harm@powershellisfun.com'     
    }
    
    try {
        Send-MailMessage @options
    }
    catch {
        Write-Warning ("- Error sending email, please check the email options")
    }
}
else {
    Write-Host ("No changes detected") -ForegroundColor Green
}

Stop-Transcript

Download the script(s) from GitHub here

8 thoughts on “Report on changed Active Directory groups using PowerShell

  1. I like the script and have used something similar in the past for general reporting purposes. The issue is it doesn’t tell you the “who” or “when” of the change. When I want to know the details, I prefer to use Get-WinEvent or Get-EventLog commandlets. It takes a bit of effort in large environments, but if the script(s) are part of your DC build process, it’s fairly easy to manage. Just my two cents :-).

  2. You’re right, this only gives you the something changed but not the who changed it report 🙂 But good idea, will write a different blogpost on Account management events and reporting on that!

  3. Nice work! .csv output also means it can be normalized in a SIEM (Splunk, QRADAR, AlienVault) where you could correlate the added users. Taken a step further and paired with a SOAR you could actually revert the changes back automatically and sus out who did it later.

  4. Pingback: Retrieve Security events from Active Directory using PowerShell | PowerShell is fun :)

  5. Nice stuff, @Harm!
    Can also check out my Get-ADGroupChanges script, “pure” powershell cmdlet (no dependencies, no special permissions needed etc’) to retrieve change history in an AD group membership, or all groups, or per user. relies on object metadata rather than event logs. useful for DF/IR, tracking changes in groups etc’. Supports querying AD Metadata either from an Online Domain Controller, or from an offline system state backup / Snapshot:
    https://github.com/YossiSassi/Get-ADGroupChanges

Leave a Reply

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