Powershell : Tracking Entra Connect OU Selection Updating



If you are using Entra Connect or AD-Connect as it used to be called, and you have a selected synchronisation for certain OU's to sync from on-premises to Entra then this maybe a handy guide for you if you wish to track changes.

Note : Microsoft no longer actively encourage or support selective OU synchronisation so you should not really be using it, but if you are then remember that is not something that can easily supported officially by Microsoft.

This is the option I am talking about, when you select or unselect containers in the options as below, this is know as a selective sync.


If you make changes to this panel and you unsync on OU that is required those accounts will be delete in Entra after the next sync run, which is every 30 minutes, which can be disastrous for your organisation, so lets get scripting to monitor for this and alert you, the flow is like this:
  1. Remotely run the command : Get-ADSyncServerConfiguration -Path "C:\temp\ADSyncConfig"
  2. Navigate to the folder C:\temp\ADSyncConfig\Connectors\Connector
  3. Extract the file called Connector_{<tenant_id>}.xml
  4. Read the XML file
  5. Extract the values between <inclusion> and </inclusion> which will be the OU's synced
  6. Create a folder to EntraData if it does not exist
  7. Save the results to a file called "${timestamp}_inclusion_values.txt"
  8. When one file exists no comparison can be done
  9. When you have more than two files in this folder move to Step 10
  10. Read the two latest inclusions_values.txt file 
  11. Compare these files for differences
  12. If no changes are detected return to 9
  13. If changes are detected then detect the OU changes
  14. Query who is logged into the server using query session /server:<server-id>
  15. Add the changes and the remote desktop sessions to a file called updates.txt
  16. Only allow one update to this file
  17. Archive the other changes to archive_updates.txt
  18. Rename the older file from .txt to .temp - so it is not used again
  19. Send an e-mail to the named person on what has been updated with the active RDP sessions
Note : You are not able to add or remove OUs remotely without logging into the server via remote desktop, the images extracted below are during my testing as an example.

This is the removal email you will get:
This is the added you will get:


Collecting the report

First we need to collect the report data with the first half of the action above.

Script : 1-EntraExtract.ps1

# Server configuration
$ServerName = "<fqdn_server_name>"

# Create timestamp for filename
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"

# Create directories in script's location if they don't exist
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$entraDataPath = Join-Path $scriptPath "EntraData"
$tempLocalPath = Join-Path $scriptPath "temp"

if (-not (Test-Path $entraDataPath)) {
    New-Item -ItemType Directory -Path $entraDataPath
}
if (-not (Test-Path $tempLocalPath)) {
    New-Item -ItemType Directory -Path $tempLocalPath
}

# Remote script block to execute on target server
$remoteScript = {
    # Export AD Sync configuration - errors will be non-terminating
    Get-ADSyncServerConfiguration -Path "C:\temp\ADSyncConfig" -ErrorAction SilentlyContinue
    
    # Check if the file was created despite the error
    if (Test-Path "C:\temp\ADSyncConfig\Connectors\Connector_{be12e375-214f-4a86-b311-af5984a445d1}.xml") {
        Write-Host "Configuration file created successfully"
        return $true
    } else {
        Write-Host "Configuration file was not created"
        return $false
    }
}

try {
    Write-Host "Connecting to remote server and exporting configuration..."
    $result = Invoke-Command -ComputerName $ServerName -ScriptBlock $remoteScript
    
    if ($result) {
        # Copy the file from remote server to local temp directory
        Write-Host "Copying configuration file to local machine..."
        $remotePath = "\\$ServerName\c$\temp\ADSyncConfig\Connectors\Connector_{be12e375-214f-4a86-b311-af5984a445d1}.xml"
        $localXmlPath = Join-Path $tempLocalPath "Connector_{be12e375-214f-4a86-b311-af5984a445d1}.xml"
        Copy-Item -Path $remotePath -Destination $localXmlPath -Force

        # Clean up remote server
        Invoke-Command -ComputerName $ServerName -ScriptBlock {
            Remove-Item "C:\temp\ADSyncConfig" -Recurse -Force
        }

        # Process the file locally
        Write-Host "Processing configuration file..."
        if (Test-Path $localXmlPath) {
            $content = Get-Content $localXmlPath -Raw
            
            # Extract values between <inclusion> and </inclusion>
            $pattern = '(?<=<inclusion>)(.*?)(?=</inclusion>)'
            $matches = [regex]::Matches($content, $pattern)
            
            # Save results to EntraData directory with timestamp
            $outputFile = Join-Path $entraDataPath "${timestamp}_inclusion_values.txt"
            $matches | ForEach-Object { $_.Value } | Out-File $outputFile

            Write-Host "Extraction complete. Results saved to: $outputFile"
        }
        else {
            Write-Error "Connector file not found at $localXmlPath"
        }

        # Clean up local temp directory
        Remove-Item $tempLocalPath -Recurse -Force
    }
    else {
        Write-Error "Failed to create configuration file on remote server"
    }
}
catch {
    Write-Error "An error occurred: $_"
}

Processing the data (with housekeeping)

Now we need to process the data, send the email and do the housekeeping tasks with the script below:

Script : 2-EntraCompare.ps1

# Create path variables
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$entraDataPath = Join-Path $scriptPath "EntraData"

# Function to get current timestamp
function Get-TimeStamp {
    return Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}

# Function to create HTML formatted email body
function Get-FormattedEmailBody {
    param(
        [string[]]$updateContent
    )
    
    $htmlHeader = @"
    <html>
    <head>
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            .container { max-width: 800px; margin: 0 auto; }
            .header { background-color: #0056b3; color: white; padding: 15px; border-radius: 5px; }
            .content { margin: 20px 0; padding: 15px; background-color: #f8f9fa; border-radius: 5px; }
            .timestamp { color: #666; font-size: 14px; }
            .changes { margin: 15px 0; }
            .added { color: #28a745; }
            .removed { color: #dc3545; }
            .sessions { background-color: #e9ecef; padding: 10px; border-radius: 5px; margin: 10px 0; }
            .active-users { margin: 5px 0; padding-left: 20px; }
            .no-users { color: #666; font-style: italic; margin: 5px 0; padding-left: 20px; }
        </style>
    </head>
    <body>
        <div class="container">
"@

    $htmlFooter = @"
        </div>
    </body>
    </html>
"@

    # Process the update content into HTML
    $htmlBody = ""
    $inChanges = $false
    $inSessions = $false
    $hasActiveUsers = $false
    
    foreach ($line in $updateContent) {
        if ($line -match "^={10,}$") {
            continue  # Skip separator lines
        }
        elseif ($line -match "Update detected at: (.*)") {
            $htmlBody += "<div class='header'>"
            $htmlBody += "<h2>Entra Connect OU Update Detection Report</h2>"
            $htmlBody += "<div class='timestamp'>$($matches[1])</div>"
            $htmlBody += "</div><div class='content'>"
        }
        elseif ($line -match "Terminal Sessions:") {
            $htmlBody += "<div class='sessions'><h3>Users active on that server</h3>"
            $inSessions = $true
            $sessionContent = ""
        }
        elseif ($line -match "Changes:") {
            if ($inSessions) {
                if (-not $hasActiveUsers) {
                    $htmlBody += "<p class='no-users'>No users active on server</p>"
                }
                $htmlBody += "</div>"
                $inSessions = $false
            }
            $htmlBody += "<div class='changes'><h3>Changes Detected</h3>"
            $inChanges = $true
        }
        elseif ($inChanges -and $line -match "- (.*)") {
            if ($line -match "- Added: (.*)") {
                $htmlBody += "<p class='added'>Added: $($matches[1])</p>"
            }
            elseif ($line -match "- Removed: (.*)") {
                $htmlBody += "<p class='removed'>Removed: $($matches[1])</p>"
            }
        }
        elseif ($inSessions -and $line -match "rdp-tcp.*Active$") {
            $hasActiveUsers = $true
            $htmlBody += "<p class='active-users'>$line</p>"
        }
    }
    
    # Handle case where Changes section never came (end of content)
    if ($inSessions) {
        if (-not $hasActiveUsers) {
            $htmlBody += "<p class='no-users'>No users active on server</p>"
        }
        $htmlBody += "</div>"
    }
    
    $htmlBody += "</div>"
    
    return $htmlHeader + $htmlBody + $htmlFooter
}


# Function to test and create file with proper permissions
function Initialize-File {
    param(
        [string]$FilePath
    )
    
    if (-not (Test-Path $FilePath)) {
        try {
            $null = New-Item -Path $FilePath -ItemType File -Force
            # Set permissions to allow modification
            $acl = Get-Acl $FilePath
            $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
            $fileSystemRights = [System.Security.AccessControl.FileSystemRights]::FullControl
            $type = [System.Security.AccessControl.AccessControlType]::Allow
            $fileSystemAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, $fileSystemRights, $type)
            $acl.AddAccessRule($fileSystemAccessRule)
            Set-Acl -Path $FilePath -AclObject $acl
        }
        catch {
            Write-Host "Error creating/setting permissions for $FilePath. Run as administrator or check folder permissions." -ForegroundColor Red
            Write-Host "Error: $_"
            exit 1
        }
    }
}

# Function to send email
function Send-UpdateEmail {
    param(
        [string[]]$updateContent
    )
    
    $emailParams = @{
        From = "entra.connect@croucher.cloud"
        To = "lee@croucher.cloud"
        Subject = "Entra OU Updates Report - $(Get-TimeStamp)"
        Body = Get-FormattedEmailBody -updateContent $updateContent
        SmtpServer = "smtp.bear.local"
        Port = 25
        BodyAsHtml = $true
    }
    
    Try {
        Send-MailMessage @emailParams
        Write-Host "Email notification sent successfully" -ForegroundColor Green
    }
    Catch {
        Write-Host "Failed to send email notification: $_" -ForegroundColor Red
        Write-Host "Please configure the SMTP settings in the script." -ForegroundColor Yellow
    }
}

# Ensure required folders exist with proper permissions
if (-not (Test-Path $entraDataPath)) {
    try {
        $null = New-Item -Path $entraDataPath -ItemType Directory -Force
    }
    catch {
        Write-Host "Error creating EntraData directory. Run as administrator or check permissions." -ForegroundColor Red
        exit 1
    }
}

# Initialize files with proper permissions
$updatesFile = Join-Path $entraDataPath "Updates.txt"
$archiveFile = Join-Path $entraDataPath "archive_updates.txt"
Initialize-File -FilePath $updatesFile
Initialize-File -FilePath $archiveFile

# Find the most recent txt files that don't include 'temp' in the name
$files = Get-ChildItem -Path $entraDataPath -Filter "*_inclusion_values.txt" | 
         Where-Object { $_.Name -notlike "*temp*" } |
         Sort-Object LastWriteTime -Descending

if ($files.Count -ge 2) {
    $currentFile = $files[0]
    $previousFile = $files[1]
    
    Write-Host "Comparing files:"
    Write-Host "Current : $($currentFile.Name)"
    Write-Host "Previous: $($previousFile.Name)"
    
    try {
        $currentContent = Get-Content $currentFile.FullName
        $previousContent = Get-Content $previousFile.FullName
        
        # Compare contents
        $comparison = Compare-Object -ReferenceObject $previousContent -DifferenceObject $currentContent
        
        if ($comparison) {
            Write-Host "`nChanges detected:" -ForegroundColor Yellow
            
            # Get terminal session information
            try {
                $sessionInfo = query session /server:st1w4155.stwater.intra
            }
            catch {
                $sessionInfo = "Unable to retrieve session information"
                Write-Host "Warning: Could not get session information" -ForegroundColor Yellow
            }
            
            # Prepare update information
            $updateInfo = @()
            $updateInfo += "=================="
            $updateInfo += "Update detected at: $(Get-TimeStamp)"
            $updateInfo += "Terminal Sessions:"
            $updateInfo += $sessionInfo
            $updateInfo += "`nChanges:"
            
            # Process and log changes
            foreach ($change in $comparison) {
                if ($change.SideIndicator -eq "<=") {
                    $message = "Removed: $($change.InputObject)"
                    Write-Host $message -ForegroundColor Red
                    $updateInfo += "- $message"
                } else {
                    $message = "Added: $($change.InputObject)"
                    Write-Host $message -ForegroundColor Green
                    $updateInfo += "- $message"
                }
            }
            $updateInfo += "==================`n"
            
            # Archive existing updates if Updates.txt exists and has content
            try {
                $existingContent = Get-Content $updatesFile
                if ($existingContent) {
                    Add-Content -Path $archiveFile -Value $existingContent -ErrorAction Stop
                    Write-Host "Previous updates archived to archive_updates.txt" -ForegroundColor Cyan
                    Set-Content $updatesFile $updateInfo -ErrorAction Stop
                } else {
                    Set-Content $updatesFile $updateInfo -ErrorAction Stop
                }
                
                # Send email notification
                Send-UpdateEmail -updateContent $updateInfo
                
                # Rename the older file to include 'temp'
                $newName = $previousFile.Name -replace ".txt", "_temp"
                Rename-Item -Path $previousFile.FullName -NewName $newName
                Write-Host "`nPrevious file renamed to: $newName"
                Write-Host "Update information has been logged to Updates.txt"
            }
            catch {
                Write-Host "Error managing update files: $_" -ForegroundColor Red
                Write-Host "Please check file permissions or run as administrator" -ForegroundColor Yellow
                exit 1
            }
        } else {
            Write-Host "`nNo changes detected between versions" -ForegroundColor Yellow
        }
    }
    catch {
        Write-Host "Error reading comparison files: $_" -ForegroundColor Red
        exit 1
    }
} elseif ($files.Count -eq 1) {
    Write-Host "Only one file found. No comparison possible."
    Write-Host "File: $($files[0].Name)"
} else {
    Write-Host "No files found for comparison."
}

This is a visual of the housekeeping, here you can see the rename and the archive of old changes/updates:

Previous Post Next Post

نموذج الاتصال