# ============================================================================ # Entra Connect Readiness Check - CS-SERVER # ---------------------------------------------------------------------------- # Read-only diagnostic. No state changes. No service restarts. No registry # writes. Safe to run on a production DC at any time. Takes ~30-60 seconds. # # Output is structured text with section headers. Copy the full output back # to Howard for analysis. # # Prepared: 2026-04-22 # Target: CS-SERVER (cascades.local single DC) # Prerequisite check for: Entra Connect install per user-account-rollout-plan # ============================================================================ $ErrorActionPreference = 'Continue' $sep = '=' * 76 function Write-Section($title) { Write-Output '' Write-Output $sep Write-Output "== $title" Write-Output $sep } function Write-Check($label, $value, $status = '') { $marker = switch ($status) { 'PASS' { '[OK] ' } 'WARN' { '[WARN] ' } 'FAIL' { '[FAIL] ' } default { ' ' } } Write-Output ("{0}{1,-40}: {2}" -f $marker, $label, $value) } Write-Output "Entra Connect Readiness Check - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')" Write-Output "Host: $env:COMPUTERNAME" # ---------------------------------------------------------------------------- Write-Section '1. Operating System' # ---------------------------------------------------------------------------- $os = Get-CimInstance Win32_OperatingSystem Write-Check 'OS Caption' $os.Caption Write-Check 'OS Version' $os.Version Write-Check 'OS Build' $os.BuildNumber Write-Check 'Architecture' $os.OSArchitecture Write-Check 'Install Date' ($os.InstallDate) Write-Check 'Last Boot' ($os.LastBootUpTime) Write-Check 'Uptime (days)' ([math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 1)) $osMajor = [int]($os.Version -split '\.')[0] $osBuild = [int]$os.BuildNumber $osStatus = if ($osBuild -ge 14393) { 'PASS' } else { 'FAIL' } Write-Check 'Server 2016+ required' "$($os.Caption) -> $osStatus" $osStatus # ---------------------------------------------------------------------------- Write-Section '2. .NET Framework version' # ---------------------------------------------------------------------------- # Release key lookup: https://learn.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed $netKey = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -ErrorAction SilentlyContinue if ($netKey) { $release = $netKey.Release $netName = switch ($true) { ($release -ge 533320) { '4.8.1' } ($release -ge 528040) { '4.8' } ($release -ge 461808) { '4.7.2' } ($release -ge 461308) { '4.7.1' } ($release -ge 460798) { '4.7' } ($release -ge 394802) { '4.6.2' } default { "Older (release=$release)" } } $netStatus = if ($release -ge 461808) { 'PASS' } else { 'FAIL' } Write-Check '.NET Framework version' $netName Write-Check '.NET release key' $release Write-Check '.NET 4.7.2+ required' "$netName -> $netStatus" $netStatus } else { Write-Check '.NET Framework v4 Full' 'NOT INSTALLED' 'FAIL' } # ---------------------------------------------------------------------------- Write-Section '3. PowerShell' # ---------------------------------------------------------------------------- Write-Check 'PSVersion' $PSVersionTable.PSVersion Write-Check 'PSEdition' $PSVersionTable.PSEdition $psStatus = if ($PSVersionTable.PSVersion.Major -ge 5) { 'PASS' } else { 'FAIL' } Write-Check 'PS 5.0+ required' "$($PSVersionTable.PSVersion) -> $psStatus" $psStatus # ---------------------------------------------------------------------------- Write-Section '4. TLS 1.2 configuration' # ---------------------------------------------------------------------------- # Entra Connect requires TLS 1.2 enforced for outbound sync $net4 = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' -Name SchUseStrongCrypto,SystemDefaultTlsVersions -ErrorAction SilentlyContinue $net4w = Get-ItemProperty 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v4.0.30319' -Name SchUseStrongCrypto,SystemDefaultTlsVersions -ErrorAction SilentlyContinue Write-Check '.NET SchUseStrongCrypto (64-bit)' $net4.SchUseStrongCrypto Write-Check '.NET SchUseStrongCrypto (32-bit)' $net4w.SchUseStrongCrypto Write-Check '.NET SystemDefaultTlsVersions (64)' $net4.SystemDefaultTlsVersions Write-Check '.NET SystemDefaultTlsVersions (32)' $net4w.SystemDefaultTlsVersions foreach ($role in 'Client','Server') { foreach ($proto in 'TLS 1.0','TLS 1.1','TLS 1.2') { $k = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$proto\$role" $v = Get-ItemProperty $k -ErrorAction SilentlyContinue $enabled = if ($v) { "Enabled=$($v.Enabled) DisabledByDefault=$($v.DisabledByDefault)" } else { 'unset (OS default)' } Write-Check "SCHANNEL $proto $role" $enabled } } Write-Output '(Entra Connect requires TLS 1.2 client enabled; TLS 1.0/1.1 should be disabled per HIPAA best practice.)' # ---------------------------------------------------------------------------- Write-Section '5. Disk space' # ---------------------------------------------------------------------------- Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object { $free = [math]::Round($_.FreeSpace / 1GB, 1) $total = [math]::Round($_.Size / 1GB, 1) $pct = if ($_.Size) { [math]::Round(100 * $_.FreeSpace / $_.Size, 1) } else { 0 } Write-Check "Drive $($_.DeviceID)" "$free GB free of $total GB ($pct% free)" } Write-Output '(Entra Connect needs ~5-20 GB free on install drive, typically C:.)' # ---------------------------------------------------------------------------- Write-Section '6. AD Domain + FSMO roles' # ---------------------------------------------------------------------------- try { Import-Module ActiveDirectory -ErrorAction Stop $dom = Get-ADDomain $for = Get-ADForest Write-Check 'Domain' $dom.DNSRoot Write-Check 'NetBIOS name' $dom.NetBIOSName Write-Check 'Forest mode' $for.ForestMode Write-Check 'Domain mode' $dom.DomainMode Write-Check 'Schema master' $for.SchemaMaster Write-Check 'Domain naming master' $for.DomainNamingMaster Write-Check 'PDC emulator' $dom.PDCEmulator Write-Check 'RID master' $dom.RIDMaster Write-Check 'Infrastructure master' $dom.InfrastructureMaster $userCount = (Get-ADUser -Filter * -ResultSetSize $null | Measure-Object).Count $groupCount = (Get-ADGroup -Filter * -ResultSetSize $null | Measure-Object).Count $ouCount = (Get-ADOrganizationalUnit -Filter * | Measure-Object).Count Write-Check 'AD user count' $userCount Write-Check 'AD group count' $groupCount Write-Check 'AD OU count' $ouCount } catch { Write-Check 'AD module' "FAIL: $_" 'FAIL' } # ---------------------------------------------------------------------------- Write-Section '7. AD Schema version' # ---------------------------------------------------------------------------- try { $schema = Get-ADObject (Get-ADRootDSE).schemaNamingContext -Property objectVersion Write-Check 'Schema objectVersion' $schema.objectVersion $schemaName = switch ($schema.objectVersion) { 88 { 'Windows Server 2019' } 87 { 'Windows Server 2016' } 69 { 'Windows Server 2012 R2' } 56 { 'Windows Server 2012' } 47 { 'Windows Server 2008 R2' } default { "Unknown ($($schema.objectVersion))" } } Write-Check 'Schema version (mapped)' $schemaName } catch { Write-Check 'AD Schema' "FAIL: $_" 'FAIL' } # ---------------------------------------------------------------------------- Write-Section '8. Time sync' # ---------------------------------------------------------------------------- Write-Output '(Entra Connect requires clock within ~5 min of Microsoft time.)' $w32status = w32tm /query /status 2>&1 $w32status | Out-String | Write-Output $w32peers = w32tm /query /peers 2>&1 $w32peers | Out-String | Write-Output # ---------------------------------------------------------------------------- Write-Section '9. Internet connectivity to Microsoft sync endpoints' # ---------------------------------------------------------------------------- $endpoints = @( 'login.microsoftonline.com', 'login.windows.net', 'secure.aadcdn.microsoftonline-p.com', 'management.azure.com', 'graph.windows.net', 'adminwebservice.microsoftonline.com', 'provisioningapi.microsoftonline.com' ) foreach ($e in $endpoints) { $r = Test-NetConnection -ComputerName $e -Port 443 -InformationLevel Quiet -WarningAction SilentlyContinue $status = if ($r) { 'PASS' } else { 'FAIL' } Write-Check "HTTPS 443 -> $e" $r $status } # ---------------------------------------------------------------------------- Write-Section '10. WinHTTP proxy' # ---------------------------------------------------------------------------- netsh winhttp show proxy | Out-String | Write-Output # ---------------------------------------------------------------------------- Write-Section '11. Existing Entra Connect / AAD Sync installations' # ---------------------------------------------------------------------------- $svcs = Get-Service -Name 'ADSync','MIISERVER','Microsoft Azure AD Sync' -ErrorAction SilentlyContinue if ($svcs) { $svcs | ForEach-Object { Write-Check "Existing service $($_.Name)" "$($_.Status) - CONFLICT" 'WARN' } } else { Write-Check 'Entra Connect / AAD Sync service' 'Not found (expected)' 'PASS' } $aadApps = Get-CimInstance Win32_Product -Filter "Name LIKE '%Azure AD%' OR Name LIKE '%Entra%' OR Name LIKE '%AADSync%'" -ErrorAction SilentlyContinue | Select-Object Name, Version, InstallDate if ($aadApps) { $aadApps | Format-Table -AutoSize | Out-String | Write-Output } else { Write-Check 'Azure/Entra installed products' 'none' 'PASS' } # ---------------------------------------------------------------------------- Write-Section '12. SQL services (LocalDB conflict check)' # ---------------------------------------------------------------------------- # Entra Connect installs SQL Server 2019 LocalDB by default. Existing SQL # instances are not a conflict (different service name), but worth noting. $sql = Get-Service -Name 'MSSQL*' -ErrorAction SilentlyContinue if ($sql) { $sql | ForEach-Object { Write-Check "$($_.Name)" "$($_.Status) ($($_.DisplayName))" } } else { Write-Check 'Existing SQL services' 'none' } # ---------------------------------------------------------------------------- Write-Section '13. Key services running (potential conflicts / heavy load)' # ---------------------------------------------------------------------------- $watch = 'NTDS','DNS','DHCPServer','W3SVC','vmms','Spooler','SynoDriveClient','QBIDPService','QuickBooksDB34','DattoAV','ScreenConnect','SyncroDeviceAgent','Splashtop Streamer' Get-Service -Name $watch -ErrorAction SilentlyContinue | Sort-Object Name | ForEach-Object { Write-Check "Service $($_.Name)" "$($_.Status) - $($_.DisplayName)" } # ---------------------------------------------------------------------------- Write-Section '14. Memory + CPU pressure (current)' # ---------------------------------------------------------------------------- $mem = Get-CimInstance Win32_OperatingSystem $memTotal = [math]::Round($mem.TotalVisibleMemorySize / 1MB, 1) $memFree = [math]::Round($mem.FreePhysicalMemory / 1MB, 1) $memUsed = [math]::Round($memTotal - $memFree, 1) $memPct = [math]::Round(100 * $memUsed / $memTotal, 0) Write-Check 'RAM total (GB)' $memTotal Write-Check 'RAM in use (GB)' "$memUsed ($memPct%)" Write-Check 'RAM free (GB)' $memFree $cpu = Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average Write-Check 'CPU load (avg %)' ([math]::Round($cpu.Average,1)) # ---------------------------------------------------------------------------- Write-Section '15. Event log health (last 24h)' # ---------------------------------------------------------------------------- $since = (Get-Date).AddHours(-24) foreach ($log in 'System','Application') { $ev = Get-WinEvent -FilterHashtable @{LogName=$log; Level=1,2; StartTime=$since} -ErrorAction SilentlyContinue $crit = ($ev | Where-Object Level -eq 1 | Measure-Object).Count $err = ($ev | Where-Object Level -eq 2 | Measure-Object).Count Write-Check "$log log errors/critical" "$err errors, $crit critical" if ($ev) { $ev | Group-Object ProviderName | Sort-Object Count -Descending | Select-Object -First 5 Count, Name | Format-Table -AutoSize | Out-String | Write-Output } } # ---------------------------------------------------------------------------- Write-Section '16. DC health (dcdiag subset)' # ---------------------------------------------------------------------------- # Focused tests only - full dcdiag is ~2 minutes. Skip if too slow. Write-Output '--- dcdiag /test:connectivity,fsmocheck,services,advertising /v (trimmed) ---' $dc = dcdiag /test:connectivity /test:fsmocheck /test:services /test:advertising 2>&1 $dc | Where-Object { $_ -match 'Starting test|passed test|failed test|warning|error' } | Out-String | Write-Output # ---------------------------------------------------------------------------- Write-Section '17. Windows Features (Server Backup etc.)' # ---------------------------------------------------------------------------- $features = Get-WindowsFeature -Name 'Windows-Server-Backup','ADDS-AdminCenter','RSAT-AD-PowerShell','RSAT-AD-Tools' -ErrorAction SilentlyContinue $features | Format-Table Name, InstallState -AutoSize | Out-String | Write-Output # ---------------------------------------------------------------------------- Write-Section '18. Certificate expirations (local cert store, expires < 90d)' # ---------------------------------------------------------------------------- Get-ChildItem Cert:\LocalMachine\My, Cert:\LocalMachine\Root -ErrorAction SilentlyContinue | Where-Object { $_.NotAfter -lt (Get-Date).AddDays(90) -or $_.NotAfter -lt (Get-Date) } | Sort-Object NotAfter | Select-Object Subject, NotAfter, Thumbprint | Format-Table -AutoSize -Wrap | Out-String | Write-Output # ---------------------------------------------------------------------------- Write-Section 'Done' # ---------------------------------------------------------------------------- Write-Output "Completed at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')"