Script Repository


Changes in group membership (including changes made by 3rd party tools)

January 11, 2017
1394

With the help of the script, you can get a report on changes in membership of an AD group, no matter whether the changes were made using Adaxes or any 3rd party tools, such as ADUC or Exchange.

To be able to identify which members were added to or removed from a group, the script saves GUIDs of the current members in a certain multi-valued attribute of the group. The saved GUIDs are used to compare the list of current members of the group with members on the previous run.

We suggest using an Adaxes custom attribute for storing member GUIDs, for example, adm-CustomAttributeTextMultiValue1. Such attributes are not stored in AD, but can be used the same as any other attribute of AD objects.

To use the script with Adaxes, create a Scheduled Task for Group objects that runs the script using the Run a program or PowerShell script action.

Parameters:

  • $savedMembersAttribute - specifies the LDAP display name of the attribute that will be used to store group member GUIDs;
  • $to - specifies the recipient of the email notification;
  • $subject - specifies the email message subject;
  • $reportHeader - specifies the report header;
  • $reportFooter - specifies the report footer;
  • $headerAddedMembers - specifies a header for the section with added members;
  • $headerRemovedMembers - specifies a header for the section with removed members.
Edit Remove
PowerShell
$savedMembersAttribute = "adm-CustomAttributeTextMultiValue1" # TODO: modify me

# E-mail message settings
$to = "recipient@domain.com" # TODO: modify me
$subject = "Changes in group membership for group '%name%'" # TODO: modify me
$reportHeader = "<h2><b>Changes in group membership for group '%name%'</b></h2><br/>" # TODO: modify me
$reportFooter = "<hr /><p><i>Please do not reply to this e-mail, it has been sent to you for notification purposes only.</i></p>" # TODO: modify me
$headerAddedMembers = "<b>Members added to the group</b><br />" # TODO: modify me
$headerRemovedMembers = "<b>Members removed from the group</b><br />" # TODO: modify me

function SaveCurrentMembers($guidsBytes, $savedMembersAttribute)
{
    if ($guidsBytes.Count -eq 0)
    {
        # All members were removed from the group
        $Context.TargetObject.PutEx("ADS_PROPERTY_UPDATE", $savedMembersAttribute, @("none"))
    }
    else
    {
        $values = @()
        foreach ($guidBytes in $guidsBytes)
        {
            $values += ([Guid]$guidBytes).ToString()
        }
        
        $Context.TargetObject.PutEx("ADS_PROPERTY_UPDATE", $savedMembersAttribute, $values)
    }
    
    # Save changes
    $Context.TargetObject.SetInfo()
}

# Get GUIDs of direct members of the group
try
{
    $currentMemberGuids = $Context.TargetObject.GetEx("adm-DirectMembersGuid")
}
catch
{
    $currentMemberGuids = @()
}

$memberGuids = New-Object "System.Collections.Generic.HashSet[System.Guid]"
foreach ($guidBytes in $currentMemberGuids)
{
    $guid = [Guid]$guidBytes
    $memberGuids.Add($guid)
}

# Get saved member GUIDs
try
{
    $savedMemberGuids = $Context.TargetObject.GetEx($savedMembersAttribute)
}
catch
{
    if ($memberGuids.Count -eq 0)
    {
        return # No current or saved members
    }
    
    # Save current members GUIDs and exit
    SaveCurrentMembers $currentMemberGuids $savedMembersAttribute
    return
}

if (($savedMemberGuids.Length -eq 1) -and ($savedMemberGuids[0] -ieq "none"))
{
    $savedMemberGuids = @() # All users were removed from the group previous time
}

# Find members that were removed from the group
$removedMemberGuids = @()
foreach ($savedMemberGuid in $savedMemberGuids)
{
    $guid = [Guid]$savedMemberGuid
    if ($memberGuids.Remove($guid))
    {
        continue
    }
    
    $removedMemberGuids += $guid
}

if (($removedMemberGuids.Length -eq 0) -and ($memberGuids.Count -eq 0))
{
    return # No changes
}

# Get the default Web Interface address
$webInterfaceAddress = "%adm-WebInterfaceUrl%"
if ([System.String]::IsNullOrEmpty($webInterfaceAddress))
{
    $Context.LogMessage("Default web interface address not set for Adaxes service. For details, see http://www.adaxes.com/help/?HowDoI.ManageService.RegisterWebInterface.html", "Warning")
}

if ($memberGuids.Count -ne 0)
{
    # Add new members to the report
    foreach ($newMemberGuid in $memberGuids)
    {
        # Bind to the member
        $path = "Adaxes://<GUID=$newMemberGuid>"
        
        # Get member name
        $memberName = [Softerra.Adaxes.Utils.ObjectNameHelper]::GetObjectName($path, 'IncludeParentPath')
        
        # Add to the report
        $addedMembersReport += "<li><a href='$webInterfaceAddress`ViewObject.aspx?guid=$newMemberGuid'>$memberName</a></li>"
    }
    $addedMembersReport += "</ol>"
    
    # Add to the report
    $reportHeader += $headerAddedMembers
    $reportHeader += $addedMembersReport
}

if ($removedMemberGuids.Length -ne 0)
{
    # Iterate through removed members
    foreach ($removedMemberGuid in $removedMemberGuids)
    {
        # Bind to the member
        $path = "Adaxes://<GUID=$removedMemberGuid>"
        
        # Get member name
        $memberName = [Softerra.Adaxes.Utils.ObjectNameHelper]::GetObjectName($path, 'IncludeParentPath')
        
        # Add to report
        $removedMembersReport += "<li><a href='$webInterfaceAddress`ViewObject.aspx?guid=$removedMemberGuid'>$memberName</a></li>"
    }
    $removedMembersReport += "</ol>"
    
    # Add to the report
    $reportHeader += $headerRemovedMembers
    $reportHeader += $removedMembersReport
}

# Send mail
$report = $reportHeader + $reportFooter
$Context.SendMail($to, $subject, $NULL, $report)
    
# Save current member GUIDs to custom attribute
SaveCurrentMembers $currentMemberGuids $savedMembersAttribute

This version of the script reports not only which members were added to or removed from a group, but also who did that, when and from which computer. Additional information is fetched from Event Logs on the domain controllers where the operation was performed

Note: The script must be used with big caution as it considerably impact the performance of Adaxes service.

Prerequisites:

  • All DCs that can be used for adding or removing members from the groups must be accessible to Adaxes service via RPC.
  • The Audit account management policy must be enabled on the domain controllers.

Parameters:

  • $days - specifies the maximum number of days that the script will poll Event Logs for;
  • $to - specifies a comma-separated list of recipients of the email notification;
  • $cc - specifies a comma-separated list of recipients of carbon copies;
  • $subject - specifies the email message subject;
  • $from - specifies the email message sender;
  • $smtpServer - specifies an SMTP Server that will be used to send the notifications;
  • $reportHeader - specifies the report header;
  • $reportFooter - specifies the report footer;
  • $htmlTable - specifies the header for lists of members who were added to or removed from a group;
  • $headerAddedMembers - specifies a header for the section with added members;
  • $headerRemovedMembers - specifies a header for the section with removed members.
Edit Remove
PowerShell
$days = 1 # TODO: modify me

# E-mail message settings
$to = "recipient1@domain.com", "recipient2@domain.com" # TODO: modify me
$cc = "recipient3@domain.com" # TODO: modify me
$subject = "Changes in group membership for group '%name%'" # TODO: modify me
$from = "noreply@domain.com" # TODO: modify me
$smtpServer = "mail.domain.com" # TODO: modify me
$reportHeader = "<h2><b>Changes in group membership for group '%name%'</b></h2><br/>" # TODO: modify me
$htmlTable = @"
<table border="1">
    <tr>
        <th>Member</th>
        <th>Initiator</th>
        <th>Time stamp</th>
        <th>Event ID</th>
        <th>Source</th>
    </tr>
"@ # TODO: modify me
$reportFooter = "<hr /><p><i>Please do not reply to this e-mail, it has been sent to you for notification purposes only.</i></p>" # TODO: modify me
$headerAddedMembers = "<b>Members added to the group</b><br />" # TODO: modify me
$headerRemovedMembers = "<b>Members removed from the group</b><br />" # TODO: modify me

# Event IDs for adding or removing a member from a group
$eventIDsAdded = @("4728","4732","4756", "4751", "4746", "4761") # TODO: modify me
$eventIDsRemoved = @("4729","4733","4757", "4752", "4747", "4762") # TODO: modify me

function GetEventLogs($days, $eventIDsAdded, $eventIDsRemoved)
{
    # Find domain controllers
    $domainName = $Context.GetObjectDomain("%distinguishedName%")
    $searcher = $Context.BindToObject("Adaxes://$domainName/rootDSE")
    $searcher.SearchFilter = "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192)(dNSHostName=*))"
    $searcher.SearchScope = "ADS_SCOPE_SUBTREE"
    $searcher.ReferralChasing = "ADS_CHASE_REFERRALS_NEVER"
    $searcher.SetPropertiesToLoad(@("Name", "dNSHostName"))
    
    try
    {
        $searchResultIterator = $searcher.ExecuteSearch()
        $domainControllers = $searchResultIterator.FetchAll()
        
        $startDate = (Get-Date).AddDays(-$days)
        $eventIDs = $eventIDsAdded + $eventIDsRemoved
        foreach ($dc in $domainControllers)
        {
            $dnsHostName = $dc.Properties["dNSHostName"].Value
            if (!(Test-Connection -ComputerName $dnsHostName -Quiet))
            {
                $Context.LogMessage("Domain Controller '$dnsHostName' is unavailable", "Warning")
                continue
            }
            
            # Get Event log
            try
            {
                [Object[]]$records = Get-WinEvent -FilterHashtable @{Logname='Security';ID=$eventIDs;StartTime=$startDate} -ComputerName $dnsHostName -ErrorAction Stop
            }
            catch
            {
                $Context.LogMessage("An error occurred while getting Event Log on the following domain controller: $dnsHostName", "Warning")
                $Context.LogMessage($_.Exception.Message, "Warning")
                continue
            }
            
            foreach ($record in $records)
            {
                $groupSid = $record.Properties[4].Value.Value
                if ($groupSid -ne "%objectSid%")
                {
                    continue
                }
                
                # Add information about the event
                $reportRecord = New-Object PSObject
                $reportRecord | Add-Member -MemberType NoteProperty -Name OperationTime -Value $record.TimeCreated
                $reportRecord | Add-Member -MemberType NoteProperty -Name InitiatorSid -Value $record.Properties[5].Value.Value
                $reportRecord | Add-Member -MemberType NoteProperty -Name EventId -Value $record.Id
                $reportRecord | Add-Member -MemberType NoteProperty -Name Source -Value $record.ProviderName

                # Check operation
                if ($eventIDsAdded -contains $record.Id)
                {
                    $operation = "Added"
                }
                else
                {
                    $operation = "Removed"
                }
                
                # Get memberDN
                $memberDN = $record.Properties[0].Value
                
                # Update the report
                if ($allEvents.ContainsKey($memberDN))
                {
                    $operations = $allEvents[$memberDN]
                    $operations[$operation] += $reportRecord
                }
                else
                {
                    $operations = @{}
                    [Void]$operations.Add("Added", @())
                    [Void]$operations.Add("Removed", @())
                    $operations[$operation] += $reportRecord
                    [Void]$allEvents.Add($memberDN, $operations)
                }
            }
        }
    }
    finally
    {
        # Release resources
        $searchResultIterator.Dispose()
    }
}

function BuildTableRecord ($memberDN, $operation, $allEvents)
{
    # Build member link
    try
    {
        $member = $Context.BindToObjectByDN($memberDN)
        $memberName = [Softerra.Adaxes.Utils.ObjectNameHelper]::GetObjectName($member.AdsPath, 'IncludeParentPath')
        $memberGuid = [Guid]$member.Get("ObjectGuid")
        $memberLink = "<a href='$webInterfaceAddress`ViewObject.aspx?guid=$memberGuid'>$memberName</a>"
    }
    catch
    {
        $memberLink = $memberDN
    }

    $operationsDetails = $allEvents[$memberDN][$operation]
    $tableRecords = ""
    foreach ($operation in $operationsDetails)
    {
        # Build initiator link
        $initiatorSid = $operation.InitiatorSid
        $initiatorPath = "Adaxes://<SID=$initiatorSid>"
        try
        {
            # Bind to the initiator
            $initiator = $Context.BindToObject($initiatorPath)
            
            # Get initiator GUID
            $initiatorGuid = [Guid]$initiator.Get("objectGuid")
            
            # Get initiator name
            $initiatorName = [Softerra.Adaxes.Utils.ObjectNameHelper]::GetObjectName($initiatorPath, 'IncludeParentPath')
            
            # Build link
            $initiatorLink = "<a href='$webInterfaceAddress`ViewObject.aspx?guid=$initiatorGuid'>$initiatorName</a>"    
        }
        catch
        {
            # Cannot bind to the initiator
            $initiatorLink = $initiatorSid
        }
        
        # Get other information
        $operationTime = $operation.OperationTime
        $eventId = $operation.EventId
        $source = $operation.Source
        
        $tableRecords += "<tr valign='top'><td>$memberLink</td><td>$initiatorLink</td><td>$operationTime</td><td>$eventId</td><td>$source</td></tr>"
    }
    
    return $tableRecords
}

# Get the default Web Interface address
$webInterfaceAddress = "%adm-WebInterfaceUrl%"
if ([System.String]::IsNullOrEmpty($webInterfaceAddress))
{
    $Context.LogMessage("Default web interface address not set for Adaxes service. For details, see http://www.adaxes.com/help/?HowDoI.ManageService.RegisterWebInterface.html", "Warning")
}

# Get event logs from all domain controllers in the domain of the group
$allEvents = @{}
GetEventLogs $days $eventIDsAdded $eventIDsRemoved

$addedMemberTableRecords = ""
$removedMemberTableRecords = ""
foreach ($memberDN in $allEvents.Keys)
{
    $addedMemberTableRecords += BuildTableRecord $memberDN "Added" $allEvents
    $removedMemberTableRecords += BuildTableRecord $memberDN "Removed" $allEvents
}

if ([System.String]::IsNullOrEmpty($addedMemberTableRecords) -and
    [System.String]::IsNullOrEmpty($removedMemberTableRecords))
{
    $reportHeader += "<b>Group membership was not updated</b><br/>"
}

if (-not([System.String]::IsNullOrEmpty($addedMemberTableRecords)))
{
    $reportHeader += $headerAddedMembers + $htmlTable + $addedMemberTableRecords + "</table>"
}

if (-not([System.String]::IsNullOrEmpty($removedMemberTableRecords)))
{
    $reportHeader += $headerRemovedMembers + $htmlTable + $removedMemberTableRecords + "</table>"
}

# Send mail
$html = $reportHeader + $reportFooter
Send-MailMessage -To $to -cc $cc -from $from -SmtpServer $smtpServer -Subject $subject -Body $html -BodyAsHtml

Comments ( 0 )
No results found.
Leave a comment