Public/Get-EntraConnectSyncStatus.ps1
|
function Get-EntraConnectSyncStatus { <# .SYNOPSIS Reports the health and sync status of an Entra Connect (Azure AD Connect) server. .DESCRIPTION Must be run locally on the Entra Connect server - it collects: - Last sync cycle time and result (delta / initial / export) - Connector export/import activity counts per connector (adds/updates/ deletes/no-change) - ADSync's connector statistics API does not expose a per-connector error count; check the Synchronization Service Manager UI or Get-ADSyncRunProfileResult for actual sync errors - Password hash sync status - Staging mode state - Auto-upgrade setting - Pending export objects (adds, updates, deletes) per connector Outputs a summary to the console and optionally exports a CSV. Requires the ADSync module installed on the Entra Connect server. Does not use PSRemoting/WinRM, which is assumed blocked in this environment. Remote execution against another server's ADSync state would require WinRM (Invoke-Command); that path has been intentionally removed. Run this function directly on the Entra Connect server itself (e.g. via a scheduled task or interactive session on that box). .PARAMETER ComputerName Informational only - used for report labelling. Must match the local machine name; this function cannot query a remote Entra Connect server. .PARAMETER ExportPath Optional path to export connector results as CSV. Example: "C:\temp\EntraConnectStatus.csv" .EXAMPLE Get-EntraConnectSyncStatus Runs against the local machine (must be executed on the Entra Connect server). .EXAMPLE Get-EntraConnectSyncStatus -ExportPath "C:\temp\sync.csv" Runs locally and exports connector stats to CSV. .NOTES Author: K Shankar R Karanth Website: https://karanth.ovh Version: 2.0 Requires: ADSync PowerShell module (installed with Entra Connect), run as local Administrator or ADSyncAdmins group member, locally on the Entra Connect server. WinRM/PSRemoting is not used or required. #> [CmdletBinding()] param ( [ValidateNotNullOrEmpty()] [string]$ComputerName = $env:COMPUTERNAME, [string]$ExportPath = "C:\ADOpsKit\Reports\Get-EntraConnectSyncStatus\$(Get-Date -Format 'yyyy-MM-dd')_EntraConnectStatus.csv" ) Set-StrictMode -Version Latest if ($ComputerName -ne $env:COMPUTERNAME) { throw "Get-EntraConnectSyncStatus must be run locally on the Entra Connect server. " + "-ComputerName '$ComputerName' does not match the local machine name '$env:COMPUTERNAME', " + "and remote execution via WinRM/PSRemoting is not supported in this environment. " + "Run this function directly on '$ComputerName' instead." } # ============ HELPERS ============ function Write-Header { param([string]$Title) Write-Host "`n===== $Title =====" -ForegroundColor Cyan } function Write-StatusLine { param([string]$Label, $Value, [string]$OkValue = "") $color = if ($OkValue -and $Value -ne $OkValue) { 'Yellow' } else { 'White' } Write-Host (" {0,-40} {1}" -f $Label, $Value) -ForegroundColor $color } # ============ LOCAL EXECUTION ============ # $ComputerName is validated above to match the local machine - no # WinRM/PSRemoting is used here, only local module calls. $syncScript = { $ErrorActionPreference = 'Stop' if (-not (Get-Module -ListAvailable -Name ADSync)) { throw "ADSync module not found. Run this script on the Entra Connect server." } Import-Module ADSync # -- Global sync state -- $scheduler = Get-ADSyncScheduler # Global settings has no product-version parameter (an earlier version # of this script looked up 'Microsoft.Synchronize.SynchronizationVersion', # which does not exist and always evaluated to $null). The real product # version is the file version of the sync engine binary. $version = 'Unknown' try { $miiserverPath = Join-Path $env:ProgramFiles 'Microsoft Azure AD Sync\Bin\miiserver.exe' if (Test-Path -LiteralPath $miiserverPath) { $version = (Get-Item -LiteralPath $miiserverPath).VersionInfo.ProductVersion } } catch { <# leave $version as 'Unknown' #> } # Scheduler has no "last successful sync" property - the closest real # signal is the most recent run's own result/timestamp from run history. $lastRunResult = $null try { $lastRunResult = Get-ADSyncRunProfileResult -NumberRequested 1 -ErrorAction Stop | Select-Object -First 1 } catch { <# run history unavailable - leave $lastRunResult as $null #> } # -- Connector summary -- # Microsoft.IdentityManagement.PowerShell.ObjectModel.ConnectorStatistics # (the real type Get-ADSyncConnectorStatistics returns) only exposes # ExportAdds/Updates/Deletes and ImportAdds/Updates/Deletes/NoChange - # there is no per-connector error-count property on this object. An # earlier version of this script read ExportErrors/ImportErrors/ # PendingExport* here, which don't exist on this type and always # silently evaluated to $null. $connectors = Get-ADSyncConnector | ForEach-Object { $stats = Get-ADSyncConnectorStatistics -ConnectorName $_.Name [PSCustomObject]@{ ConnectorName = $_.Name Type = $_.Type ExportAdds = $stats.ExportAdds ExportUpdates = $stats.ExportUpdates ExportDeletes = $stats.ExportDeletes ImportAdds = $stats.ImportAdds ImportUpdates = $stats.ImportUpdates ImportDeletes = $stats.ImportDeletes ImportNoChange = $stats.ImportNoChange } } # -- Password sync -- $pwdSync = $null try { $pwdSync = Get-ADSyncAADPasswordSyncConfiguration -SourceConnector ( $connectors | Where-Object Type -eq 'AD' | Select-Object -First 1 -ExpandProperty ConnectorName ) } catch { <# Password sync not configured - skip #> } [PSCustomObject]@{ Version = $version StagingModeEnabled = $scheduler.StagingModeEnabled # Get-ADSyncAutoUpgrade returns the state enum value directly # (AutoUpgradeConfigurationState), not an object with a .State # property - the earlier .State access always returned $null. AutoUpgradeState = [string](Get-ADSyncAutoUpgrade) # The real scheduler property is SyncCycleEnabled - an earlier # version of this script read SyncEnabled, which doesn't exist. SyncEnabled = $scheduler.SyncCycleEnabled NextSyncCycle = $scheduler.NextSyncCyclePolicyType SchedulerSuspended = $scheduler.SchedulerSuspended # LastSyncRunStartTime doesn't exist on the scheduler object - an # earlier version of this script read it there and always got # $null. Real run history comes from Get-ADSyncRunProfileResult. LastSuccessfulSync = if ($lastRunResult) { $lastRunResult.StartDate } else { $null } LastSyncResult = if ($lastRunResult) { $lastRunResult.Result } else { 'Unknown' } PasswordSyncEnabled = if ($pwdSync) { $pwdSync.Enabled } else { 'N/A' } Connectors = $connectors } } # ============ EXECUTE ============ try { $result = & $syncScript } catch { Write-Error "Failed to retrieve Entra Connect status: $_" return } # ============ OUTPUT ============ Write-Header "Entra Connect Global Status" Write-StatusLine "Server" $ComputerName Write-StatusLine "Entra Connect Version" $result.Version Write-StatusLine "Staging Mode" $result.StagingModeEnabled "False" Write-StatusLine "Sync Enabled" $result.SyncEnabled "True" Write-StatusLine "Scheduler Suspended" $result.SchedulerSuspended "False" Write-StatusLine "Auto-Upgrade" $result.AutoUpgradeState "Enabled" Write-StatusLine "Next Sync Policy" $result.NextSyncCycle Write-StatusLine "Last Sync Run" $result.LastSuccessfulSync Write-StatusLine "Last Sync Result" $result.LastSyncResult "success" Write-StatusLine "Password Sync Enabled" $result.PasswordSyncEnabled "True" Write-Header "Connector Summary" $result.Connectors | Format-Table -AutoSize ` ConnectorName, Type, ExportAdds, ExportUpdates, ExportDeletes, ImportAdds, ImportUpdates, ImportDeletes, ImportNoChange # ADSync's ConnectorStatistics object has no per-connector error-count # property, so pending export/import activity is used as the "needs # attention" signal instead of a genuine error count. For real sync # errors, check the Synchronization Service Manager UI or # Get-ADSyncRunProfileResult for the relevant run history. $hasPendingExports = $result.Connectors | Where-Object { $_.ExportAdds -gt 0 -or $_.ExportUpdates -gt 0 -or $_.ExportDeletes -gt 0 } if ($hasPendingExports) { Write-Host " ATTENTION: One or more connectors have pending export changes." -ForegroundColor Red Write-Warning "One or more Entra Connect connectors have pending export changes." $hasPendingExports | Format-Table -AutoSize ConnectorName, ExportAdds, ExportUpdates, ExportDeletes } else { Write-Host " All connectors have zero pending export changes." -ForegroundColor Green } if ($result.StagingModeEnabled -eq $true) { Write-Host "`n WARNING: Entra Connect is in STAGING MODE - no changes are being written to Azure AD." -ForegroundColor Yellow } # ============ EXPORT ============ if ($ExportPath -ne "") { $exportDir = Split-Path -LiteralPath $ExportPath if ($exportDir -and -not (Test-Path -LiteralPath $exportDir)) { New-Item -ItemType Directory -Path $exportDir -Force | Out-Null } $result.Connectors | Export-Csv -NoTypeInformation -LiteralPath $ExportPath Write-Host "`nConnector stats exported to $ExportPath" -ForegroundColor Green } } |