Files
claudetools/clients/cascades-tucson/docs/migration/scripts/entra-connect-readiness.ps1
Howard Enos 6bd416657c sync: auto-sync from HOWARD-HOME at 2026-04-22 17:39:56
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-04-22 17:39:56
2026-04-22 17:39:57 -07:00

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')"