Script repository

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

Updated on: Jan 18, 2026, Views: 7258

Group membership

The script sends a report on changes of an AD group membership, no matter whether the changes were made using Adaxes or any third party tools, such as ADUC or Exchange.

To identify the members added or removed from a group, on each run the script preserves GUIDs of the current members in a binary attribute (e.g. adm-CustomAttributeBinary1) of the group. On each subsequent run, the saved GUIDs are used to compare the list of current members of the group with the members preserved in the binary attribute.

To execute the script, create a scheduled task configured for the Group object type.

Parameters

  • $savedMembersAttribute - the name of the binary attribute to preserve group member GUIDs in.
  • $to - the address of the email notification recipient.
  • $subject - the email notification subject.
  • $reportHeader - the report header.
  • $reportFooter - the report footer.
  • $headerAddedMembers - the header for the section with added members.
  • $headerRemovedMembers - the header for the section with removed members.
$savedMembersAttribute = "adm-CustomAttributeBinary1" # TODO: modify me

# Email 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.Put($savedMembersAttribute, [Guid]::Empty.ToByteArray())
    }
    else
    {
        $totalBytes = $guidsBytes.Count * 16
        $result = New-Object 'System.Collections.Generic.List[System.Byte]' $totalBytes
        
        foreach ($guidBytes in $guidsBytes)
        {
            $result.AddRange($guidBytes)
        }
        
        $Context.TargetObject.Put($savedMembersAttribute, $result.ToArray())
    }
    
    # Save changes
    $Context.TargetObject.SetInfo()
}

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

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

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

if (($savedMemberGuidsBytes.Length -eq 16) -and ([Guid]$savedMemberGuidsBytes -eq [Guid]::Empty))
{
    $savedMemberGuidsBytes = @() # All users were removed from the group previous time.
}

$savedMemberGuids = New-Object "System.Collections.Generic.HashSet[System.Guid]"
if ($savedMemberGuidsBytes.Length -ne 0)
{
    # Calculate the number of GUIDs.
    $totalBytes = $savedMemberGuidsBytes.Length
    
    # Make sure that the total number of  bytes is a divisible by 16.
    $remainder = 0
    [System.Math]::DivRem($totalBytes, 16, [ref]$remainder)
    if ($remainder -ne 0)
    {
        $Context.Cancel("Unexpected data length! Exiting.")
        return
    }
    
    for ($i = 0; $i -lt ($totalBytes / 16); $i++)
    {
        $bytes = [System.Guid]::Empty.ToByteArray()
        [System.Array]::Copy($savedMemberGuidsBytes, $i * 16, $bytes, 0, 16)
        $guid = [Guid]$bytes
        [void]$savedMemberGuids.Add($guid)
    }
}

# Find members that were removed from the group.
$removedMemberGuids = New-Object "System.Collections.Generic.HashSet[System.Guid]"
foreach ($guid in $savedMemberGuids)
{
    if ($addedMemberGuids.Remove($guid))
    {
        continue
    }
    
    $removedMemberGuids.Add($guid)
}

if (($removedMemberGuids.Count -eq 0) -and ($addedMemberGuids.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 ($addedMemberGuids.Count -ne 0)
{
    # Add new members to the report.
    foreach ($newMemberGuid in $addedMemberGuids)
    {
        # Bind to member
        $path = "Adaxes://<GUID=$newMemberGuid>"
        
        # Get member name.
        $memberName = $Context.GetDisplayNameFromAdsPath($path, $True)
        $memberName = [System.Web.HttpUtility]::HtmlEncode($memberName)
        
        # Add to the report
        $addedMembersReport += "<li><a href='$webInterfaceAddress`#/Browse/$newMemberGuid'>$memberName</a></li>"
    }
    $addedMembersReport += "</ul>"
    
    # Add to report
    $reportHeader += $headerAddedMembers
    $reportHeader += $addedMembersReport
}

if ($removedMemberGuids.Count -ne 0)
{
    # Iterate through removed members.
    foreach ($removedMemberGuid in $removedMemberGuids)
    {
        # Bind to member
        $path = "Adaxes://<GUID=$removedMemberGuid>"
        
        # Get member name.
        $memberName = $Context.GetDisplayNameFromAdsPath($path, $True)
        $memberName = [System.Web.HttpUtility]::HtmlEncode($memberName)
        
        # Add to report
        $removedMembersReport += "<li><a href='$webInterfaceAddress`#/Browse/$removedMemberGuid'>$memberName</a></li>"
    }
    $removedMembersReport += "</ul>"
    
    # Add to report
    $reportHeader += $headerRemovedMembers
    $reportHeader += $removedMembersReport
}

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

Comments 6

You must be signed in to comment.

  • Jeremy Altman

    Jeremy Altman

    This script used to work great, but after upgrading to Adaxes 2023 (3.15.20916.0), the report is generated and emailed but the "Members added to/removed from the group" section is blank. It only shows a bullet point, but the members are no longer listed.

    • Support

      Support

      Hello Jeremy,

      There were some changes in Adaxes 2023 that might result in such a behaviour. Please, try the below updated script:

      $savedMembersAttribute = "adm-CustomAttributeBinary1" # 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.Put($savedMembersAttribute, [Guid]::Empty.ToByteArray())
          }
          else
          {
              $totalBytes = $guidsBytes.Count * 16
              $result = New-Object 'System.Collections.Generic.List[System.Byte]' $totalBytes
              
              foreach ($guidBytes in $guidsBytes)
              {
                  $result.AddRange($guidBytes)
              }
              
              $Context.TargetObject.Put($savedMembersAttribute, $result.ToArray())
          }
          
          # Save changes
          $Context.TargetObject.SetInfo()
      }
      
      # Get GUIDs of direct members of the group
      try
      {
          $currentMemberGuidsBytes = $Context.TargetObject.GetEx("adm-DirectMembersGuid")
      }
      catch
      {
          $currentMemberGuidsBytes = @()
      }
      
      $addedMemberGuids = New-Object "System.Collections.Generic.HashSet[System.Guid]"
      foreach ($guidBytes in $currentMemberGuidsBytes)
      {
          $guid = [Guid]$guidBytes
          $addedMemberGuids.Add($guid)
      }
      
      # Get saved member GUIDs
      try
      {
          $savedMemberGuidsBytes = $Context.TargetObject.Get($savedMembersAttribute)
      }
      catch
      {
          if ($addedMemberGuids.Count -eq 0)
          {
              return # No current or saved members
          }
          
          # Save current members GUIDs and exit
          SaveCurrentMembers $currentMemberGuidsBytes $savedMembersAttribute
          return
      }
      
      if (($savedMemberGuidsBytes.Length -eq 16) -and ([Guid]$savedMemberGuidsBytes -eq [Guid]::Empty))
      {
          $savedMemberGuidsBytes = @() # All users were removed from the group previous time
      }
      
      $savedMemberGuids = New-Object "System.Collections.Generic.HashSet[System.Guid]"
      if ($savedMemberGuidsBytes.Length -ne 0)
      {
          # Calculate the number of GUIDs
          $totalBytes = $savedMemberGuidsBytes.Length
          
          # Make sure that the total number of  bytes is a divisible of 16
          $remainder = 0
          [System.Math]::DivRem($totalBytes, 16, [ref]$remainder)
          if ($remainder -ne 0)
          {
              $Context.Cancel("Unexpected data length! Exiting.")
              return
          }
          
          for ($i = 0; $i -lt ($totalBytes / 16); $i++)
          {
              $bytes = [System.Guid]::Empty.ToByteArray()
              [System.Array]::Copy($savedMemberGuidsBytes, $i * 16, $bytes, 0, 16)
              $guid = [Guid]$bytes
              [void]$savedMemberGuids.Add($guid)
          }
      }
      
      # Find members that were removed from the group
      $removedMemberGuids = New-Object "System.Collections.Generic.HashSet[System.Guid]"
      foreach ($guid in $savedMemberGuids)
      {
          if ($addedMemberGuids.Remove($guid))
          {
              continue
          }
          
          $removedMemberGuids.Add($guid)
      }
      
      if (($removedMemberGuids.Count -eq 0) -and ($addedMemberGuids.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 ($addedMemberGuids.Count -ne 0)
      {
          # Add new members to the report
          foreach ($newMemberGuid in $addedMemberGuids)
          {
              # Bind to the member
              $path = "Adaxes://<GUID=$newMemberGuid>"
              
              # Get member name
              $memberName = $Context.GetDisplayNameFromAdsPath($path, $True)
              $memberName = [System.Web.HttpUtility]::HtmlEncode($memberName)
              
              # Add to the report
              $addedMembersReport += "<li><a href='$webInterfaceAddress`ViewObject.aspx?guid=$newMemberGuid'>$memberName</a></li>"
          }
          $addedMembersReport += "</ol><br/>"
          
          # Add to the report
          $reportHeader += $headerAddedMembers
          $reportHeader += $addedMembersReport
      }
      
      if ($removedMemberGuids.Count -ne 0)
      {
          # Iterate through removed members
          foreach ($removedMemberGuid in $removedMemberGuids)
          {
              # Bind to the member
              $path = "Adaxes://<GUID=$removedMemberGuid>"
              
              # Get member name
              $memberName = $Context.GetDisplayNameFromAdsPath($path, $True)
              $memberName = [System.Web.HttpUtility]::HtmlEncode($memberName)
              
              # 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 $currentMemberGuidsBytes $savedMembersAttribute
      
      • Charles Wilson

        Charles Wilson

        Could this script be modified to pull the AD email address field and company field also? Rather than list the information out in the email message body, can the information be placed in a pdf attachment and then emailed?

        Thanks!

        • Support

          Support

          Hello Charles,

          Unfortunately, there is no easy way to do that and requires significantly updating the script.

          • Charles Wilson

            Charles Wilson

            I understand. Are you aware of another script in the repository to accomplish something similar?

            • Support

              Support

              Hello Charles,

              No, there is no such script.

Got questions?

Support Questions & Answers

We use cookies to improve your experience.
By your continued use of this site you accept such use.
For more details please see our privacy policy and cookies policy.