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.
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:
First we need to collect the report data with the first half of the action above.
$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."
}