Scripts/Get-NetworkDiagnostics.ps1

<#
.SYNOPSIS
    Collects a comprehensive network diagnostic snapshot following OSI-layer
    methodology for IT support escalation and ticket documentation.
 
.DESCRIPTION
    Gathers adapter status, IP configuration, DHCP/DNS state, gateway reachability,
    internet egress, and DNS resolution results into a single structured report.
    Mirrors the Layer 1-7 diagnostic sequence defined in
    networking/network-troubleshooting-guide.md so that script output maps
    directly to that documentation for any technician reading the ticket.
 
    This script is read-only by default. It makes no configuration changes.
    It is safe to run on any Windows 10/11 workstation without administrative
    privileges.
 
.NOTES
    Author: it-support-ops repository
    Run as: Standard user (no elevation required)
    Compatibility: Windows 10, Windows 11, PowerShell 5.1+
    Output: Console output + log file in user's Documents folder
 
.EXAMPLE
    .\Get-NetworkDiagnostics.ps1
 
    Runs the full network diagnostic sequence and saves output to:
    $env:USERPROFILE\Documents\IT-Diagnostics\NetworkDiagnostics_<timestamp>.txt
 
.EXAMPLE
    .\Get-NetworkDiagnostics.ps1 -TestHost "intranet.company.local"
 
    Runs the diagnostic sequence and additionally tests reachability and DNS
    resolution against a specified internal host.
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Interactive console tool by design: report content goes through Write-ReportLine (pipe-safe); Write-Host is used only for colored status messages meant for a human reading the console.')]
[CmdletBinding()]
param(
    # Optional custom output path. Defaults to Documents\IT-Diagnostics.
    [Parameter(Mandatory = $false)]
    [string]$OutputPath,

    # Optional additional internal hostname to test (e.g. an intranet site
    # or file server) alongside the standard external connectivity tests.
    [Parameter(Mandatory = $false)]
    [string]$TestHost
)

# ---------------------------------------------------------------------------
# SETUP
# ---------------------------------------------------------------------------

$ErrorActionPreference = "Continue"
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"

if (-not $OutputPath) {
    $diagnosticsFolder = Join-Path $env:USERPROFILE "Documents\IT-Diagnostics"
    if (-not (Test-Path $diagnosticsFolder)) {
        try {
            New-Item -ItemType Directory -Path $diagnosticsFolder -Force | Out-Null
        }
        catch {
            Write-Warning "Could not create diagnostics folder. Output will be console-only."
            $diagnosticsFolder = $null
        }
    }
    if ($diagnosticsFolder) {
        $OutputPath = Join-Path $diagnosticsFolder "NetworkDiagnostics_$timestamp.txt"
    }
}

$reportLines = New-Object System.Collections.Generic.List[string]

function Write-ReportLine {
    param([string]$Text = "")
    Write-Host $Text
    $reportLines.Add($Text)
}

function Write-SectionHeader {
    param([string]$Title)
    Write-ReportLine ""
    Write-ReportLine "=" * 70
    Write-ReportLine " $Title"
    Write-ReportLine "=" * 70
}

# Tracks the first layer at which a failure is detected, so the report can
# give a clear, single-line conclusion at the end rather than requiring the
# reader to infer it from scattered results.
$script:faultLayer = $null
# Note: PSScriptAnalyzer flags Set-FaultLayer under
# PSUseShouldProcessForStateChangingFunctions because of its "Set-" verb.
# This is a false positive: it only sets an internal script-scoped tracking
# variable, not real system state. Left unsuppressed (rather than adding a
# SuppressMessageAttribute) because that attribute is only valid on advanced
# functions ([CmdletBinding()]), and this is a plain helper function. The
# warning is non-blocking (Warning severity, not Error) and safe to ignore.
function Set-FaultLayer {
    param([string]$Layer)
    if (-not $script:faultLayer) {
        $script:faultLayer = $Layer
    }
}

# ---------------------------------------------------------------------------
# REPORT HEADER
# ---------------------------------------------------------------------------

Write-ReportLine "=" * 70
Write-ReportLine " IT SUPPORT - NETWORK DIAGNOSTIC REPORT"
Write-ReportLine " (Layer 1-7 sequence per networking/network-troubleshooting-guide.md)"
Write-ReportLine "=" * 70
Write-ReportLine "Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-ReportLine "Computer Name: $env:COMPUTERNAME"
Write-ReportLine "Current User: $env:USERNAME"

# ---------------------------------------------------------------------------
# LAYER 1/2: ADAPTER STATUS
# ---------------------------------------------------------------------------

Write-SectionHeader "LAYER 1-2: NETWORK ADAPTERS"

$activeAdapter = $null
try {
    $adapters = Get-NetAdapter -ErrorAction Stop
    if (-not $adapters) {
        Write-ReportLine "No network adapters found on this system."
        Set-FaultLayer "Layer 1 (Physical) - No adapters detected"
    }
    else {
        foreach ($adapter in $adapters) {
            Write-ReportLine ("Adapter: {0}" -f $adapter.Name)
            Write-ReportLine (" Status: {0}" -f $adapter.Status)
            Write-ReportLine (" Link Speed: {0}" -f $adapter.LinkSpeed)
            Write-ReportLine (" MAC Address: {0}" -f $adapter.MacAddress)
            Write-ReportLine (" Media Type: {0}" -f $adapter.MediaType)
            Write-ReportLine ""
        }

        # Identify the active adapter to use for subsequent IP-level tests
        $activeAdapter = $adapters | Where-Object { $_.Status -eq "Up" } | Select-Object -First 1

        if (-not $activeAdapter) {
            Write-ReportLine "FLAG: No adapter is currently in an 'Up' state."
            Set-FaultLayer "Layer 1 (Physical) - No active adapter"
        }
    }
}
catch {
    Write-ReportLine "ERROR: Could not retrieve adapter information - $($_.Exception.Message)"
    Set-FaultLayer "Layer 1 (Physical) - Adapter query failed"
}

# ---------------------------------------------------------------------------
# LAYER 3: IP CONFIGURATION
# ---------------------------------------------------------------------------

Write-SectionHeader "LAYER 3: IP CONFIGURATION"

$ipv4Address = $null
$gateway     = $null
$isApipa     = $false

try {
    if ($activeAdapter) {
        $ipConfig = Get-NetIPConfiguration -InterfaceIndex $activeAdapter.InterfaceIndex -ErrorAction Stop

        $ipv4Address = ($ipConfig.IPv4Address | Select-Object -First 1).IPAddress
        $gateway     = ($ipConfig.IPv4DefaultGateway | Select-Object -First 1).NextHop
        $dnsServers  = $ipConfig.DNSServer | Where-Object { $_.AddressFamily -eq 2 } |
                       Select-Object -ExpandProperty ServerAddresses

        Write-ReportLine "Interface: $($ipConfig.InterfaceAlias)"
        Write-ReportLine "IPv4 Address: $ipv4Address"
        Write-ReportLine "Default Gateway: $gateway"
        Write-ReportLine "DNS Servers: $($dnsServers -join ', ')"

        # DHCP lease details
        $netAdapterConfig = Get-CimInstance Win32_NetworkAdapterConfiguration -ErrorAction Stop |
            Where-Object { $_.InterfaceIndex -eq $activeAdapter.InterfaceIndex }

        if ($netAdapterConfig) {
            Write-ReportLine "DHCP Enabled: $($netAdapterConfig.DHCPEnabled)"
            if ($netAdapterConfig.DHCPEnabled -and $netAdapterConfig.DHCPServer) {
                Write-ReportLine "DHCP Server: $($netAdapterConfig.DHCPServer)"
            }
            if ($netAdapterConfig.DHCPLeaseObtained) {
                Write-ReportLine "Lease Obtained: $($netAdapterConfig.DHCPLeaseObtained)"
            }
            if ($netAdapterConfig.DHCPLeaseExpires) {
                Write-ReportLine "Lease Expires: $($netAdapterConfig.DHCPLeaseExpires)"
            }
        }

        # APIPA detection - this is one of the most common and most diagnostic
        # findings in a network ticket, so it is flagged explicitly rather than
        # left for the reader to notice in the raw IP address.
        if ($ipv4Address -and $ipv4Address -like "169.254.*") {
            $isApipa = $true
            Write-ReportLine ""
            Write-ReportLine "FLAG: APIPA address detected (169.254.x.x)."
            Write-ReportLine " This indicates DHCP failed to assign an address."
            Write-ReportLine " See networking/dns-dhcp-playbook.md Part 2."
            Set-FaultLayer "Layer 3 (Network) - APIPA / DHCP failure"
        }
        elseif (-not $ipv4Address -or $ipv4Address -eq "0.0.0.0") {
            Write-ReportLine ""
            Write-ReportLine "FLAG: No valid IPv4 address assigned."
            Set-FaultLayer "Layer 3 (Network) - No IP address"
        }

        if (-not $gateway) {
            Write-ReportLine "FLAG: No default gateway configured."
            Set-FaultLayer "Layer 3 (Network) - No default gateway"
        }
    }
    else {
        Write-ReportLine "Skipped - no active adapter identified in Layer 1-2 section."
    }
}
catch {
    Write-ReportLine "ERROR: Could not retrieve IP configuration - $($_.Exception.Message)"
    Set-FaultLayer "Layer 3 (Network) - IP configuration query failed"
}

# ---------------------------------------------------------------------------
# LAYER 3: GATEWAY REACHABILITY
# ---------------------------------------------------------------------------

Write-SectionHeader "LAYER 3: GATEWAY REACHABILITY"

if ($gateway -and -not $isApipa) {
    try {
        $gatewayPing = Test-Connection -ComputerName $gateway -Count 4 -ErrorAction Stop
        $avgLatency  = ($gatewayPing | Measure-Object -Property ResponseTime -Average).Average
        # $lossPercent = [math]::Round((4 - $gatewayPing.Count) / 4 * 100, 0)

        Write-ReportLine "Gateway: $gateway"
        Write-ReportLine "Replies Received: $($gatewayPing.Count) / 4"
        Write-ReportLine "Average Latency: $([math]::Round($avgLatency, 1)) ms"

        if ($gatewayPing.Count -eq 0) {
            Write-ReportLine "FLAG: Gateway is unreachable."
            Set-FaultLayer "Layer 3 (Network) - Gateway unreachable"
        }
        elseif ($avgLatency -gt 100) {
            Write-ReportLine "FLAG: High latency to gateway (>100ms)."
        }
    }
    catch {
        Write-ReportLine "FLAG: Gateway is unreachable - $($_.Exception.Message)"
        Set-FaultLayer "Layer 3 (Network) - Gateway unreachable"
    }
}
else {
    Write-ReportLine "Skipped - no valid gateway available to test (see Layer 3 IP section above)."
}

# ---------------------------------------------------------------------------
# LAYER 3/4: INTERNET EGRESS
# ---------------------------------------------------------------------------

Write-SectionHeader "INTERNET EGRESS (BYPASSING DNS)"

$internetReachable = $false
if ($gateway -and -not $isApipa) {
    $externalTargets = @("8.8.8.8", "1.1.1.1")
    foreach ($target in $externalTargets) {
        try {
            $result = Test-Connection -ComputerName $target -Count 2 -ErrorAction Stop
            if ($result.Count -gt 0) {
                $internetReachable = $true
                Write-ReportLine "$target : REACHABLE (avg $([math]::Round(($result | Measure-Object ResponseTime -Average).Average,1)) ms)"
            }
            else {
                Write-ReportLine "$target : UNREACHABLE"
            }
        }
        catch {
            Write-ReportLine "$target : UNREACHABLE"
        }
    }

    if (-not $internetReachable) {
        Write-ReportLine ""
        Write-ReportLine "FLAG: No external IP addresses reachable. Internet egress is blocked"
        Write-ReportLine " or there is an upstream routing fault."
        Set-FaultLayer "Layer 3/4 - Internet egress unreachable"
    }
}
else {
    Write-ReportLine "Skipped - gateway not reachable, internet test would not be informative."
}

# ---------------------------------------------------------------------------
# LAYER 7: DNS RESOLUTION
# ---------------------------------------------------------------------------

Write-SectionHeader "LAYER 7: DNS RESOLUTION"

if ($internetReachable) {
    $dnsTargets = @("google.com", "microsoft.com")
    if ($TestHost) { $dnsTargets += $TestHost }

    $dnsFailures = 0
    foreach ($target in $dnsTargets) {
        try {
            $dnsResult = Resolve-DnsName -Name $target -ErrorAction Stop |
                Where-Object { $_.Type -eq "A" } | Select-Object -First 1
            if ($dnsResult) {
                Write-ReportLine "$target : RESOLVED -> $($dnsResult.IPAddress)"
            }
            else {
                Write-ReportLine "$target : NO A RECORD RETURNED"
                $dnsFailures++
            }
        }
        catch {
            Write-ReportLine "$target : RESOLUTION FAILED - $($_.Exception.Message)"
            $dnsFailures++
        }
    }

    if ($dnsFailures -gt 0) {
        Write-ReportLine ""
        Write-ReportLine "FLAG: $dnsFailures of $($dnsTargets.Count) DNS lookups failed."
        Write-ReportLine " See networking/dns-dhcp-playbook.md Part 1."
        Set-FaultLayer "Layer 7 (Application) - DNS resolution failure"
    }
}
else {
    Write-ReportLine "Skipped - internet egress not confirmed, DNS test would not be informative."
}

# ---------------------------------------------------------------------------
# WIRELESS DETAIL (IF APPLICABLE)
# ---------------------------------------------------------------------------

Write-SectionHeader "WIRELESS DETAIL (IF APPLICABLE)"

try {
    $wifiAdapter = Get-NetAdapter -ErrorAction Stop |
        Where-Object { $_.PhysicalMediaType -like "*802.11*" -and $_.Status -eq "Up" }

    if ($wifiAdapter) {
        $wlanInfo = netsh wlan show interfaces
        $wlanInfo | Where-Object { $_ -match "SSID|Signal|Radio type|Channel|Receive rate|Transmit rate|Authentication" } |
            ForEach-Object { Write-ReportLine " $($_.Trim())" }
    }
    else {
        Write-ReportLine "No active wireless adapter detected (wired connection or Wi-Fi disabled)."
    }
}
catch {
    Write-ReportLine "Could not retrieve wireless details - $($_.Exception.Message)"
}

# ---------------------------------------------------------------------------
# SUMMARY AND CONCLUSION
# ---------------------------------------------------------------------------

Write-SectionHeader "DIAGNOSTIC SUMMARY"

if ($script:faultLayer) {
    Write-ReportLine "FAULT BOUNDARY IDENTIFIED: $script:faultLayer"
    Write-ReportLine ""
    Write-ReportLine "Refer to the corresponding section of networking/network-troubleshooting-guide.md"
    Write-ReportLine "or the relevant playbook for next steps."
}
else {
    Write-ReportLine "No fault detected in Layers 1-7 automated checks."
    Write-ReportLine "If the user is still reporting an issue, the fault is likely"
    Write-ReportLine "application-specific or intermittent. Refer to"
    Write-ReportLine "networking/connectivity-fault-isolation.md for extended diagnosis."
}

Write-ReportLine ""
Write-ReportLine "Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"

# ---------------------------------------------------------------------------
# FILE OUTPUT
# ---------------------------------------------------------------------------

if ($OutputPath) {
    try {
        $reportLines | Out-File -FilePath $OutputPath -Encoding UTF8 -ErrorAction Stop
        Write-Host ""
        Write-Host "Report saved to: $OutputPath" -ForegroundColor Green
        Write-Host "Attach this file to the ticket per diagnostic-report-template.md" -ForegroundColor Green
    }
    catch {
        Write-Warning "Could not save report to file: $($_.Exception.Message)"
        Write-Warning "Report was displayed in console only — copy manually if needed."
    }
}
else {
    Write-Host ""
    Write-Host "No output file path available — report displayed in console only." -ForegroundColor Yellow
}