Public/Test-StagedNetwork.ps1

<#
.SYNOPSIS
    Executes a 7-stage network, cloud connectivity, and HTTPS time synchronization diagnostic probe.
.DESCRIPTION
    Performs staged verification across physical link, gateway, DNS, TCP socket, TLS handshake,
    HTTPS time synchronization over port 443 with 5-minute Kerberos tolerance, and Autopilot endpoints.
#>

function Test-StagedNetwork {
    [CmdletBinding()]
    param(
        [Parameter()]
        [int]$TimeoutSeconds = 5,

        [Parameter()]
        [switch]$AutoSyncClock = $true,

        [Parameter()]
        [int]$MaxTimeSkewSeconds = 300
    )

    $stages = [System.Collections.Generic.List[PSCustomObject]]::new()
    $allPassed = $true

    # Stage 1: Network Interface Check
    $adapters = @(Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "IPEnabled = TRUE" -ErrorAction SilentlyContinue)
    $interfaceUp = ($adapters.Count -gt 0)
    $stages.Add([PSCustomObject]@{
        Stage       = 1
        Name        = 'Network Interface'
        Success     = $interfaceUp
        Description = if ($interfaceUp) { "Active adapter found (IP: $($adapters[0].IPAddress[0]))" } else { "No active IP-enabled network adapter" }
    })
    if (-not $interfaceUp) { $allPassed = $false }

    # Stage 2: Gateway Check
    $gatewayOk = $false
    if ($interfaceUp -and $adapters[0].DefaultIPGateway) {
        $gatewayOk = $true
    }
    $stages.Add([PSCustomObject]@{
        Stage       = 2
        Name        = 'Default Gateway'
        Success     = $gatewayOk
        Description = if ($gatewayOk) { "Gateway configured: $($adapters[0].DefaultIPGateway[0])" } else { "No default gateway assigned" }
    })

    # Stage 3: DNS Resolution
    $endpoints = @('login.microsoftonline.com', 'graph.microsoft.com', 'ztd.dds.microsoft.com')
    $dnsOk = $true
    foreach ($ep in $endpoints) {
        try {
            $ips = [System.Net.Dns]::GetHostAddresses($ep)
            if ($ips.Count -eq 0) { $dnsOk = $false }
        }
        catch { $dnsOk = $false }
    }

    $stages.Add([PSCustomObject]@{
        Stage       = 3
        Name        = 'DNS Resolution'
        Success     = $dnsOk
        Description = if ($dnsOk) { "Cloud hostnames resolved successfully" } else { "DNS resolution failed for one or more endpoints" }
    })
    if (-not $dnsOk) { $allPassed = $false }

    # Stage 4: TCP 443 Socket Connection
    $tcpOk = $false
    $latencyMs = 0
    if ($dnsOk) {
        try {
            $client = New-Object System.Net.Sockets.TcpClient
            $sw = [System.Diagnostics.Stopwatch]::StartNew()
            $iar = $client.BeginConnect('login.microsoftonline.com', 443, $null, $null)
            $wh = $iar.AsyncWaitHandle
            if ($wh.WaitOne($TimeoutSeconds * 1000, $false)) {
                $client.EndConnect($iar)
                $sw.Stop()
                $latencyMs = $sw.ElapsedMilliseconds
                $tcpOk = $true
            }
            $client.Close()
        } catch { }
    }

    $stages.Add([PSCustomObject]@{
        Stage       = 4
        Name        = 'TCP Port 443 Connect'
        Success     = $tcpOk
        Description = if ($tcpOk) { "TCP connection established in $latencyMs ms" } else { "Port 443 socket connection failed or timed out" }
    })
    if (-not $tcpOk) { $allPassed = $false }

    # Stage 5: HTTPS Clock Sync (5-Minute Kerberos/TLS Tolerance Threshold)
    $tlsAndTimeOk = $false
    $timeSkewSec = 0
    if ($tcpOk) {
        try {
            $req = [System.Net.HttpWebRequest]::Create('https://login.microsoftonline.com')
            $req.Method = 'HEAD'
            $req.Timeout = $TimeoutSeconds * 1000
            $resp = $req.GetResponse()
            
            if ($resp.Headers['Date']) {
                $cloudTimeUtc = [DateTime]::Parse($resp.Headers['Date']).ToUniversalTime()
                $localTimeUtc = (Get-Date).ToUniversalTime()
                $timeSkewSec = [Math]::Round([Math]::Abs(($localTimeUtc - $cloudTimeUtc).TotalSeconds), 1)

                if ($timeSkewSec -gt $MaxTimeSkewSeconds -and $AutoSyncClock) {
                    Write-Warning "Detected $timeSkewSec sec clock skew (> $MaxTimeSkewSeconds sec threshold). Resynchronizing system clock..."
                    try {
                        [Microsoft.VisualBasic.DateAndTime]::TimeString = $cloudTimeUtc.ToLocalTime().ToString('HH:mm:ss')
                    } catch { }
                }
                $tlsAndTimeOk = $true
            }
            $resp.Close()
        }
        catch {
            $tlsAndTimeOk = $false
        }
    }

    $stages.Add([PSCustomObject]@{
        Stage       = 5
        Name        = 'HTTPS Time & TLS Sync'
        Success     = $tlsAndTimeOk
        Description = if ($tlsAndTimeOk) { "TLS verified (Clock Skew: $timeSkewSec sec within $MaxTimeSkewSeconds sec tolerance)" } else { "TLS handshake or HTTPS Date header probe failed" }
    })
    if (-not $tlsAndTimeOk) { $allPassed = $false }

    # Stage 6: Captive Portal Check
    $captivePortal = $false
    try {
        $ncsiReq = [System.Net.HttpWebRequest]::Create('http://www.msftconnecttest.com/connecttest.txt')
        $ncsiReq.Timeout = $TimeoutSeconds * 1000
        $ncsiReq.AllowAutoRedirect = $false
        $ncsiResp = $ncsiReq.GetResponse()
        
        if ([int]$ncsiResp.StatusCode -eq 302 -or [int]$ncsiResp.StatusCode -eq 301) {
            $captivePortal = $true
        } else {
            $stream = $ncsiResp.GetResponseStream()
            $reader = New-Object System.IO.StreamReader($stream)
            $text = $reader.ReadToEnd().Trim()
            if ($text -ne 'Microsoft Connect Test') { $captivePortal = $true }
        }
        $ncsiResp.Close()
    } catch { }

    $stages.Add([PSCustomObject]@{
        Stage       = 6
        Name        = 'Captive Portal Check'
        Success     = (-not $captivePortal)
        Description = if (-not $captivePortal) { "No captive portal or HTTP interception detected" } else { "Captive portal detected (HTTP 302 redirect / body mismatch)" }
    })
    if ($captivePortal) { $allPassed = $false }

    # Stage 7: Microsoft Autopilot Endpoint Probe
    $autopilotEpOk = $false
    try {
        $req = [System.Net.HttpWebRequest]::Create('https://ztd.dds.microsoft.com')
        $req.Timeout = $TimeoutSeconds * 1000
        $resp = $req.GetResponse()
        $resp.Close()
        $autopilotEpOk = $true
    } catch {
        if ($_.Exception.Response) { $autopilotEpOk = $true }
    }

    $stages.Add([PSCustomObject]@{
        Stage       = 7
        Name        = 'Autopilot Endpoint'
        Success     = $autopilotEpOk
        Description = if ($autopilotEpOk) { "ztd.dds.microsoft.com reachable" } else { "Autopilot enrollment endpoint unreachable" }
    })
    if (-not $autopilotEpOk) { $allPassed = $false }

    $passedCount = @($stages | Where-Object { $_.Success }).Count

    return [PSCustomObject]@{
        IsFullyReady = ($allPassed -and $passedCount -eq 7)
        StagesPassed = $passedCount
        TotalStages  = 7
        Details      = $stages
    }
}