289 lines
14 KiB
PowerShell
289 lines
14 KiB
PowerShell
# ============================================================================
|
|
# 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')"
|