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