Script to track AD group changes and email

Rising Flight 6,096 Reputation points
2025-11-22T11:35:00.27+00:00

Hi all,

I’m trying to set up a scheduled PowerShell script that:

  1. Monitors an on-prem AD security group (example: SG_App_Access)
  2. Runs every 4 hours via Task Scheduler
  3. Compares current membership to the last run
  4. Detects which users were Added or Removed
  5. Sends an email report (HTML + CSV attachment) via Office 365 SMTP using a service account (e.g. svc_alerts(at)contoso.com)
  6. Service Account has the required access.
  7. I’ve replaced @ with (at) in email addresses in the script i have encrypted the password
       $SecurePwd = Read-Host "Enter SMTP mailbox password" -AsSecureString
       $SecurePwd | ConvertFrom-SecureString | Set-Content "C:\GroupMonitor\svc_alerts_pwd.txt"
    
    Can anyone validate the below script as i dont have any test environment
       # AD group to monitor
       $GroupName          = "SG_App_Access"
       # Folder to store logs/snapshots
       $BasePath           = "C:\GroupMonitor"
       $SnapshotFile       = Join-Path $BasePath "SG_App_Access_Members_Last.csv"
       $ChangeReportCsv    = Join-Path $BasePath "SG_App_Access_Changes.csv"
       $ChangeReportHtml   = Join-Path $BasePath "SG_App_Access_Changes.html"
       # Transcript log file (optional, for troubleshooting)
       $TranscriptFile     = "C:\GroupMonitor\GroupMonitorTranscript.txt"
       # ==========================
       # Config: SMTP / Email
       # ==========================
       $SmtpServer         = "smtp.office365.com"
       $SmtpPort           = 587
       $SmtpUseSsl         = $true
       $SenderUpn          = "svc_alerts(at)contoso.com"
       $SmtpPasswordFile   = "C:\GroupMonitor\svc_alerts_pwd.txt"
       # Recipients
       $EmailRecipients = @(
           "DL1(at)contoso.com",
           "user1(at)contoso.com"
       )
       # Ensure base folder exists
       if (-not (Test-Path $BasePath)) {
           New-Item -Path $BasePath -ItemType Directory -Force | Out-Null
       }
       # Start transcript (append to keep history)
       Start-Transcript -Path $TranscriptFile -Append
       Write-Host "===== $GroupName membership check started: $(Get-Date) ====="
       # Import AD module
       try {
           Import-Module ActiveDirectory -ErrorAction Stop
       }
       catch {
           Write-Error "Failed to import ActiveDirectory module: $_"
           Stop-Transcript
           exit 1
       }
       # Build SMTP credential
       if (-not (Test-Path $SmtpPasswordFile)) {
           Write-Error "SMTP password file not found: $SmtpPasswordFile"
           Stop-Transcript
           exit 1
       }
       try {
           $SecurePassword   = Get-Content $SmtpPasswordFile | ConvertTo-SecureString
           $SmtpCredential   = New-Object System.Management.Automation.PSCredential($SenderUpn, $SecurePassword)
       }
       catch {
           Write-Error "Failed to build SMTP credential: $_"
           Stop-Transcript
           exit 1
       }
       # ==========================
       # Get current group members
       # ==========================
       try {
           $group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
       }
       catch {
           Write-Error "Failed to find AD group '$GroupName': $_"
           Stop-Transcript
           exit 1
       }
       try {
           $CurrentMembers = Get-ADGroupMember -Identity $group.DistinguishedName -Recursive |
               ForEach-Object {
                   if ($_.ObjectClass -eq "user") {
                       Get-ADUser $_.DistinguishedName -Properties DisplayName, Mail, UserPrincipalName, SamAccountName
                   }
               } |
               Where-Object { $_ -ne $null } |
               Select-Object `
                   @{Name = "SamAccountName";    Expression = { $_.SamAccountName }},
                   @{Name = "DisplayName";      Expression = { $_.DisplayName }},
                   @{Name = "UserPrincipalName";Expression = { $_.UserPrincipalName }},
                   @{Name = "Mail";             Expression = { $_.Mail }},
                   @{Name = "DistinguishedName";Expression = { $_.DistinguishedName }}
       }
       catch {
           Write-Error "Failed to get members for group '$GroupName': $_"
           Stop-Transcript
           exit 1
       }
       Write-Host "Current member count: $($CurrentMembers.Count)"
       # ==========================
       # Load previous snapshot
       # ==========================
       $PreviousMembers = @()
       if (Test-Path $SnapshotFile) {
           try {
               $PreviousMembers = Import-Csv -Path $SnapshotFile
               Write-Host "Loaded previous snapshot with $($PreviousMembers.Count) members."
           }
           catch {
               Write-Warning "Failed to read snapshot file $SnapshotFile : $_"
           }
       }
       else {
           Write-Host "No previous snapshot found. This is likely the first run."
       }
       # Normalize objects
       $CurrentKeyed  = $CurrentMembers | Select-Object SamAccountName, DisplayName, UserPrincipalName, Mail, DistinguishedName
       $PreviousKeyed = $PreviousMembers | Select-Object SamAccountName, DisplayName, UserPrincipalName, Mail, DistinguishedName
       # ==========================
       # Compare (by SamAccountName)
       # ==========================
       $Added   = @()
       $Removed = @()
       if ($PreviousKeyed.Count -gt 0) {
           $Added = Compare-Object -ReferenceObject $PreviousKeyed -DifferenceObject $CurrentKeyed -Property SamAccountName -PassThru |
                    Where-Object { $_.SideIndicator -eq "=>" }
           $Removed = Compare-Object -ReferenceObject $PreviousKeyed -DifferenceObject $CurrentKeyed -Property SamAccountName -PassThru |
                      Where-Object { $_.SideIndicator -eq "<=" }
       }
       Write-Host "Added:   $($Added.Count)"
       Write-Host "Removed: $($Removed.Count)"
       $HasChanges = ($Added.Count -gt 0 -or $Removed.Count -gt 0 -or $PreviousKeyed.Count -eq 0)
       # ==========================
       # Build change report (if changes)
       # ==========================
       if ($HasChanges) {
           Write-Host "Changes detected, generating report..."
           $Now = Get-Date
           $ReportRows = @()
           foreach ($user in $Added) {
               $ReportRows += [PSCustomObject]@{
                   ChangeType        = "Added"
                   SamAccountName    = $user.SamAccountName
                   DisplayName       = $user.DisplayName
                   UserPrincipalName = $user.UserPrincipalName
                   Mail              = $user.Mail
                   WhenDetected      = $Now
               }
           }
           foreach ($user in $Removed) {
               $ReportRows += [PSCustomObject]@{
                   ChangeType        = "Removed"
                   SamAccountName    = $user.SamAccountName
                   DisplayName       = $user.DisplayName
                   UserPrincipalName = $user.UserPrincipalName
                   Mail              = $user.Mail
                   WhenDetected      = $Now
               }
           }
           # First run: treat all as InitialSnapshot
           if ($PreviousKeyed.Count -eq 0 -and $CurrentKeyed.Count -gt 0) {
               Write-Host "First run: treating all members as InitialSnapshot for reporting purposes."
               $ReportRows = @()
               foreach ($user in $CurrentKeyed) {
                   $ReportRows += [PSCustomObject]@{
                       ChangeType        = "InitialSnapshot"
                       SamAccountName    = $user.SamAccountName
                       DisplayName       = $user.DisplayName
                       UserPrincipalName = $user.UserPrincipalName
                       Mail              = $user.Mail
                       WhenDetected      = $Now
                   }
               }
           }
           # Export CSV
           $ReportRows | Export-Csv -Path $ChangeReportCsv -NoTypeInformation -Encoding UTF8
           # Build HTML body
           $HtmlBody = $ReportRows |
               Select-Object ChangeType, SamAccountName, DisplayName, UserPrincipalName, Mail, WhenDetected |
               ConvertTo-Html -Title "Membership changes for $GroupName" -PreContent "<h2>Membership changes for $GroupName</h2><p>Detected at $Now</p>" |
               Out-String
           $HtmlBody | Out-File -FilePath $ChangeReportHtml -Encoding UTF8
           # ==========================
           # Send email via SMTP
           # ==========================
           try {
               $Subject = "[$GroupName] Membership changes detected - $($Now.ToString("yyyy-MM-dd HH:mm"))"
               $AttachmentFiles = @()
               if (Test-Path $ChangeReportCsv) { $AttachmentFiles += $ChangeReportCsv }
               if (Test-Path $TranscriptFile)  { $AttachmentFiles += $TranscriptFile }
               Send-MailMessage `
                   -To $EmailRecipients `
                   -From $SenderUpn `
                   -Subject $Subject `
                   -Body $HtmlBody `
                   -BodyAsHtml `
                   -SmtpServer $SmtpServer `
                   -Port $SmtpPort `
                   -UseSsl:$SmtpUseSsl `
                   -Credential $SmtpCredential `
                   -Attachments $AttachmentFiles
               Write-Host "Change report email sent successfully via SMTP."
           }
           catch {
               Write-Error "Failed to send email via SMTP: $_"
           }
       }
       else {
           Write-Host "No membership changes detected. No email will be sent."
       }
       # ==========================
       # Save new snapshot
       # ==========================
       try {
           $CurrentKeyed | Export-Csv -Path $SnapshotFile -NoTypeInformation -Encoding UTF8
           Write-Host "Snapshot updated: $SnapshotFile"
       }
       catch {
           Write-Error "Failed to write snapshot file: $_"
       }
       Write-Host "===== $GroupName membership check completed: $(Get-Date) ====="
       Stop-Transcript
    
Developer technologies | .NET | .NET Runtime
{count} votes

Answer accepted by question author
  1. Susmitha T (INFOSYS LIMITED) 1,630 Reputation points Microsoft External Staff
    2025-11-24T11:48:39.9333333+00:00

    Thanks for reaching out!

     

    It sounds like you're trying to create a PowerShell script to monitor Active Directory (AD) group membership changes and email a report, here are a few things to keep in mind and suggestions for validating it:

    1. Test Execution: You can validate your script by performing the following steps:
      • Run the script for the first time to ensure it captures the current state of the group.
        • Inspect the resulting CSV files to confirm that they are formatted correctly.
          • Simulate changes by manually modifying the SG_App_Access_Members_Last.csv file to see if the script detects them.
            • Have an AD admin add and then remove a test account to the group to verify that the script can track real membership changes.
            1. Multiple Groups: If you plan on monitoring more than one group in the future, consider adding functionality to handle multiple groups efficiently.
            2. Error Handling: Ensure that your script includes robust error handling, as you’ve started to implement. It’s crucial for troubleshooting issues that may arise during execution.

    Here’s a basic reference for your documentation needs:

    Let me know if you need any further help with this. I will be happy to assist.

              If you find this helpful, Kindly mark the provided solution as "Accept Answer", so that others in the community facing similar issues can easily find the solution. Your contribution is highly appreciated.

    0 comments No comments

1 additional answer

Sort by: Most helpful
  1. Q&A Assist
    2025-11-22T11:35:11.29+00:00
    AI generated content. This question contains an answer created with AI from Q&A Assist. Learn more

    Your PowerShell script for monitoring Active Directory group membership changes looks well-structured and covers the required functionality. Here are some points to consider for validation:

    1. Error Handling: You have included error handling for various operations, which is good practice. Ensure that the error messages are clear and provide enough context for troubleshooting.
    2. SMTP Configuration: Make sure that the SMTP settings (server, port, SSL) are correct and that the service account has the necessary permissions to send emails through Office 365.
    3. CSV and HTML Reports: The script generates both CSV and HTML reports, which is useful for different reporting needs. Ensure that the paths for these files are accessible and that the script has permission to write to these locations.
    4. Testing: Since you mentioned not having a test environment, consider running the script in a controlled manner, perhaps during off-peak hours, to observe its behavior without impacting production.
    5. Scheduled Task: When setting up the scheduled task, ensure that it runs with the correct user context that has permissions to access Active Directory and send emails.
    6. Logging: The transcript logging is a great addition for troubleshooting. Make sure to monitor this log for any unexpected behavior during the initial runs.

    Overall, the script appears to be well thought out and should function as intended if all configurations are correct. Just ensure to monitor its first few runs closely to catch any potential issues early on.

    0 comments No comments

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.