Get Group Membership Changes
Scenario:
Get Group Membership Changes
There are sometimes that we would like to know when a member has been removed or added in groups in Active Directory. The script that we will discuss below, is checking groups to find out if any member has been added or removed from groups based on the last time we run it. The script can be used to run it manually when ever you want or you can schedule it to run automatically based on a time interval that you like. After the script performs all changes, it will inform you who has been added or removed from the groups if there was any change. Let’s see below in more detail what the script does.
Collecting the information
When we run the script without any parameter, the script will run against the entire domain. The script will collect all groups and then it go through each group that has collected to perform checks for the changes on it membership.
OrganizationalUnit Parameter
You are also able to select a specific Organizational Unit in Active Directory to run the checks. You will just need to use -OrganizationalUnit
and the you will provide the DistinguishedName of the Organizational Unit. This will limit the search for groups only to the specific location.
Code:
param ( [string]$OrganizationalUnit = "" )
if ($OrganizationalUnit -eq ""){ $Groups = (Get-ADGroup -Filter *).Name } else{ try{$Groups = (Get-ADGroup -Filter * -SearchBase "$OrganizationalUnit").Name} catch{ $TimeStamp = Get-Date $LogDetails = "$TimeStamp" + " " + "$_" Write-Log $LogDetails Exit} }
[adinserter name=”In Article”]
Performing the checks
In order to keep track of the changes, the script is using csv files that are created by the script itself. If it is the first time that you run the script for the specific groups, then you will have no csv files to import the data from. So when you run the script for first time it will assume that all members of the group are new. Every time that you run the script, the csv files are replaced with the current members of each group. Each csv file represents a group. If the csv file exists, it will import the data from the respective file and keep it in memory in order to perform the comparison between the members. Based on the result, it will add the result in an email and a log file.
Using a switch statement we check for three conditions. The first one is that old members exist and we have a value for new members. When this is true then we perform the comparison mentioned above. The second case is when $oldmembers
variable is empty. In this case, it means that we do not have a csv file to import previous member in memory and we assume that all new members have been added to the group. This usually happens when you run the script against a group for a first time. This third condition is when we have $newmembers
variable empty. Then all new members will be considered that they have been removed. This case happens mostly when we have cleared all members from a group and the group has been left in the system.
Code:
Foreach ($Group in $Groups){ $oldmembers = $null $newmembers = $null $file = "$Path" + "$Group" + ".csv" try{$oldmembers = (Import-Csv $file).SamAccountName} catch{ $TimeStamp = Get-Date $LogDetails = "$TimeStamp" + " " + "$_" Write-Log $LogDetails} try{Get-ADGroupMember -Identity $Group | Select-Object SamAccountName | Export-Csv $file -NoTypeInformation} catch{ $TimeStamp = Get-Date $LogDetails = "$TimeStamp" + " " + "$_" Write-Log $LogDetails} $newmembers = (Import-Csv $file).SamAccountName switch -Regex ($oldmembers){ {($oldmembers -ne $null) -and ($newmembers -ne $null)}{ $Difference = Compare-Object $oldmembers $newmembers if ($Difference -ne ""){ Foreach ($DifferenceValue in $Difference){ $DifferenceValueIndicator = $DifferenceValue.SideIndicator switch -Regex ($DifferenceValueIndicator){ {$_ -eq "<="}{ $GroupMember = $DifferenceValue.InputObject $Action = "Removed" $EmailTemp = @" <tr> <td class="colorm">$GroupMember</td> <td>$Action</td> <td>$Group</td> </tr> "@ $EmailResult = $EmailResult + $EmailTemp $TimeStamp = Get-Date $LogDetails = "$TimeStamp $GroupMember has been $Action from $Group" Write-Log $LogDetails } {$_ -eq "=>"}{ $GroupMember = $DifferenceValue.InputObject $Action = "Added" $EmailTemp = @" <tr> <td class="colorm">$GroupMember</td> <td>$Action</td> <td>$Group</td> </tr> "@ $EmailResult = $EmailResult + $EmailTemp $TimeStamp = Get-Date $LogDetails = "$TimeStamp $GroupMember has been $Action to $Group" Write-Log $LogDetails } } } } Break } {($oldmembers -eq $null) -and ($newmembers -ne $null)}{ Foreach ($newmember in $newmembers){ $Action = "Added" $EmailTemp = @" <tr> <td class="colorm">$newmember</td> <td>$Action</td> <td>$Group</td> </tr> "@ $EmailResult = $EmailResult + $EmailTemp $TimeStamp = Get-Date $LogDetails = "$TimeStamp $newmember has been $Action to $Group" Write-Log $LogDetails } Break } {($oldmembers -ne $null) -and ($newmembers -eq $null)}{ Foreach ($oldmember in $oldmembers){ $Action = "Removed" $EmailTemp = @" <tr> <td class="colorm">$oldmember</td> <td>$Action</td> <td>$Group</td> </tr> "@ $EmailResult = $EmailResult + $EmailTemp $TimeStamp = Get-Date $LogDetails = "$TimeStamp $oldmember has been $Action from $Group" Write-Log $LogDetails } Break } } } $Email = $EmailUp + $EmailResult + $EmailDown
[adinserter name=”In Article”]
Logs
The script will also create few logs in a log file every time that we run it. Logs will be written when we will have:
- A member added to a group
- A member removed from a group
- Error
Code:
Function Write-Log{ Param ([string]$LogDetails) Add-content $Logfile -value $LogDetails }
Every time the script is running, it will create a new log file. The name of the file will be in the format of log-“current date/time”.log. The script will create the file only if there is something to be added in it. If there are no changes or errors, then no file will be created. The path that the log file will be created is the same with the csv files of the groups.
[adinserter name=”In Article”]
Error Control
In the script there are three error controls. The first one is when we will try to retrieve the groups from Active Directory. If the -OrganizationalUnit
parameter is in wrong format it will not be able to retrieve groups. A record will be added in the log file. The second one is when we are trying to import old members from the csv file. If the file does not exists, it will throw an error. The error is added in the log file. The third check is when we are trying export the csv for a group. In case of someone having the specific csv file open or another process is using it, the script will not be able to export the new file and replace the existing one.
You can download the script here or copy it from below. (Note that code within the script might not be copied correctly due to syntax highlighting.)
Hope you like it.
You feedback is appreciated.
If you have any questions or anything else please let me know in the comments below.
[adinserter name=”In Article”]
Related Links:
- PowerShell Scripts
- PowerShell Tutorials
- Import-Module – Microsoft Docs
- Get-Content – Microsoft Docs
- ConvertTo-SecureString – Microsoft Docs
- New-Object – Microsoft Docs
- Get-Date – Microsoft Docs
- Add-Content – Microsoft Docs
- about_Functions | Microsoft Docs
- about_Switch | Microsoft Docs
- Get-ADGroup – Microsoft Docs
- Import-Csv – Microsoft Docs
- Get-ADGroupMember – Microsoft Docs
- Select-Object – Microsoft Docs
- Export-Csv – Microsoft Docs
- Compare-Object – Microsoft Docs
- Send-MailMessage – Microsoft Docs
- about_If | Microsoft Docs
[adinserter name=”In Article”]
Solution / Script:
<# .SYNOPSIS Name: Get-GroupMembershipChanges.ps1 The purpose of this script is to monitor and inform you for membership changes on groups .DESCRIPTION This is a simple script that checks for changes of the members of groups. The script can run as one off or it can be configured to run a scheduled basis to monitor and inform you for group membership changes. .RELATED LINKSHome.PARAMETER OrganizationalUnit This is the only parameter for the script. It is used to define the Organizational Unit in Active Directory that you want to run the script. You need to provide the DistinguishedName of the Organizational Unit. The default value is "". .NOTES Release Date: 10-08-2018 Author: Stephanos Constantinou .EXAMPLE Run the Get-GroupMembershipChanges.ps1 script without any parameter to run it for the entire domain. Get-GroupMembershipChanges.ps1 .EXAMPLE Run the Get-GroupMembershipChanges.ps1 script with Organizational Unit parameter to run it on specific Organizational Unit in Active Directory. Get-GroupMembershipChanges.ps1 -OrganizationalUnit "OU=Groups,DC=Domain,DC=com" #> param ( [string]$OrganizationalUnit = "" ) Import-Module ActiveDirectory $PasswordFile = "C:\Scripts\Password.txt" $Key = (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32) $EmailUser = "Script-User@domain.com" $Password = Get-Content $PasswordFile | ConvertTo-SecureString -Key $Key $EmailCredentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $EmailUser,$Password $To = 'User1@domain.com','User2@domain.com' $From = 'Script-User@domain.com' $Path = "C:\Scripts\Files\" $Date = Get-Date -format dd-MM-yyyy-HH-mm-ss $LogFile = "C:\Scripts\Files\log-$Date .log" $EmailResult = "" $EmailUp = @" <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cstyle%3E%0D%0A%0D%0Abody%20%7B%20font-family%3ASegoe%2C%20%22Segoe%20UI%22%2C%20%22DejaVu%20Sans%22%2C%20%22Trebuchet%20MS%22%2C%20Verdana%2C%20sans-serif%20!important%3B%20color%3A%23434242%3B%7D%0D%0ATABLE%20%7B%20font-family%3ASegoe%2C%20%22Segoe%20UI%22%2C%20%22DejaVu%20Sans%22%2C%20%22Trebuchet%20MS%22%2C%20Verdana%2C%20sans-serif%20!important%3B%20border-width%3A%201px%3Bborder-style%3A%20solid%3Bborder-color%3A%20black%3Bborder-collapse%3A%20collapse%3B%7D%0D%0ATR%20%7Bborder-width%3A%201px%3Bpadding%3A%2010px%3Bborder-style%3A%20solid%3Bborder-color%3A%20white%3B%20%7D%0D%0ATD%20%7Bfont-family%3ASegoe%2C%20%22Segoe%20UI%22%2C%20%22DejaVu%20Sans%22%2C%20%22Trebuchet%20MS%22%2C%20Verdana%2C%20sans-serif%20!important%3B%20border-width%3A%201px%3Bpadding%3A%2010px%3Bborder-style%3A%20solid%3Bborder-color%3A%20white%3B%20background-color%3A%23C3DDDB%3B%7D%0D%0A.colorm%20%7Bbackground-color%3A%2358A09E%3B%20color%3Awhite%3B%7D%0D%0A.colort%7Bbackground-color%3A%2358A09E%3B%20padding%3A20px%3B%20color%3Awhite%3B%20font-weight%3Abold%3B%7D%0D%0A.colorn%7Bbackground-color%3Atransparent%3B%7D%0D%0A%3C%2Fstyle%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<style>" title="<style>" /> <body> <h3>Script has been completed successfully</h3> <h4>Changes applied:</h4> <table> <tr> <td class="colort">User</td> <td class="colort">Action</td> <td class="colort">Group</td> </tr> "@ $EmailDown = @" </table> </body> "@ Function Write-Log{ Param ([string]$LogDetails) Add-content $Logfile -value $LogDetails } if ($OrganizationalUnit -eq ""){ $Groups = (Get-ADGroup -Filter *).Name } else{ try{$Groups = (Get-ADGroup -Filter * -SearchBase "$OrganizationalUnit").Name} catch{ $TimeStamp = Get-Date $LogDetails = "$TimeStamp" + " " + "$_" Write-Log $LogDetails Exit} } Foreach ($Group in $Groups){ $oldmembers = $null $newmembers = $null $file = "$Path" + "$Group" + ".csv" try{$oldmembers = (Import-Csv $file).SamAccountName} catch{ $TimeStamp = Get-Date $LogDetails = "$TimeStamp" + " " + "$_" Write-Log $LogDetails} try{Get-ADGroupMember -Identity $Group | Select-Object SamAccountName | Export-Csv $file -NoTypeInformation} catch{ $TimeStamp = Get-Date $LogDetails = "$TimeStamp" + " " + "$_" Write-Log $LogDetails} $newmembers = (Import-Csv $file).SamAccountName switch -Regex ($oldmembers){ {($oldmembers -ne $null) -and ($newmembers -ne $null)}{ $Difference = Compare-Object $oldmembers $newmembers if ($Difference -ne ""){ Foreach ($DifferenceValue in $Difference){ $DifferenceValueIndicator = $DifferenceValue.SideIndicator switch -Regex ($DifferenceValueIndicator){ {$_ -eq "<="}{ $GroupMember = $DifferenceValue.InputObject $Action = "Removed" $EmailTemp = @" <tr> <td class="colorm">$GroupMember</td> <td>$Action</td> <td>$Group</td> </tr> "@ $EmailResult = $EmailResult + $EmailTemp $TimeStamp = Get-Date $LogDetails = "$TimeStamp $GroupMember has been $Action from $Group" Write-Log $LogDetails } {$_ -eq "=>"}{ $GroupMember = $DifferenceValue.InputObject $Action = "Added" $EmailTemp = @" <tr> <td class="colorm">$GroupMember</td> <td>$Action</td> <td>$Group</td> </tr> "@ $EmailResult = $EmailResult + $EmailTemp $TimeStamp = Get-Date $LogDetails = "$TimeStamp $GroupMember has been $Action to $Group" Write-Log $LogDetails } } } } Break } {($oldmembers -eq $null) -and ($newmembers -ne $null)}{ Foreach ($newmember in $newmembers){ $Action = "Added" $EmailTemp = @" <tr> <td class="colorm">$newmember</td> <td>$Action</td> <td>$Group</td> </tr> "@ $EmailResult = $EmailResult + $EmailTemp $TimeStamp = Get-Date $LogDetails = "$TimeStamp $newmember has been $Action to $Group" Write-Log $LogDetails } Break } {($oldmembers -ne $null) -and ($newmembers -eq $null)}{ Foreach ($oldmember in $oldmembers){ $Action = "Removed" $EmailTemp = @" <tr> <td class="colorm">$oldmember</td> <td>$Action</td> <td>$Group</td> </tr> "@ $EmailResult = $EmailResult + $EmailTemp $TimeStamp = Get-Date $LogDetails = "$TimeStamp $oldmember has been $Action from $Group" Write-Log $LogDetails } Break } } } $Email = $EmailUp + $EmailResult + $EmailDown if ($EmailResult -ne ""){ $EmailParameters = @{ To = $To Subject = "Group Membership Changes Report $(Get-Date -format dd/MM/yyyy)" Body = $Email BodyAsHtml = $True Priority = "High" UseSsl = $True Port = "587" SmtpServer = "smtp.office365.com" Credential = $EmailCredentials From = $From} send-mailmessage @EmailParameters }
[adinserter name=”Matched-Content”]


One note. You know you can export-clixml a credential object and import it back in with import-clixml file name and it is alrady incripted and you don’t have to make a key and create the credential.
Hello Stephen,
Thanks for the note. I know about Export-Clixml and it is really helpful on credentials. The problem is that you are not able to run the script with a different user and on a different computer/server using that method.
I use this method in order to allow me to run it with different user and on a different computer.
Thanks
Stephanos
Great script. I can use it on a daily base with scheduled task so once per day I get an overview. This is better then set triggers on event id’s and get constant messages. Is it possible to add who made the change?
Thanks in advance. Regards, Paul Konen
Hi Paul,
On the current state of the script we are not able to get the account that made the change. In order to get this information we need to check the events and a different approach is needed
Hi Stephanos,
Big fan. Thank you very much.
Possible to include a timestamp when a user was added/removed in the email report?
Thank you
Hi Stephanos,
thanks for the script.
In Italy, at the change of time, it happened that the e-mails arrived in which the total removal was reported and in reintegration of all the users contained in the groups under monitoring.
I’m afraid the script doesn’t take the change into account now and detects changes that haven’t actually happened.
Thanks in advance.
Regards
Salvatore