Private/Kinds/Network.ps1
|
# The Network Kind: how this machine reaches everything that is not on it. # # Nearly every application complaint that turns out not to be the application turns out to # be here, and the three things that cause it - a weak radio, a lossy link and slow name # resolution - are all invisible to a Technician looking at the machine. # How many pings each target gets. The gateway gets more because local loss is the reading # most often dismissed as noise, and a wider window is what makes it undeniable. $script:NetworkGatewayPingCount = 30 $script:NetworkInternetPingCount = 20 $script:NetworkServerPingCount = 20 # Where the Network Kind looks for the internet and for name resolution. Not Check # Definition parameters: a Customer may disagree about a threshold, not about whether # 1.1.1.1 is a reasonable thing to ping. $script:NetworkInternetTarget = '1.1.1.1' $script:NetworkDnsTarget = 'www.microsoft.com' $script:NetworkSmbPort = 445 function Get-NetworkData { [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) $adapters = @() try { $adapters = @(Get-NetAdapter -ErrorAction Stop | Where-Object { $_.Status -eq 'Up' } | ForEach-Object { [pscustomobject]@{ Name = $_.Name Description = "$($_.InterfaceDescription)" LinkSpeed = "$($_.LinkSpeed)" } }) } catch { } # The signal is parsed out of an external tool's output, which is the one genuinely # localisation-exposed read in Gutcheck. An unparseable answer leaves the percentage # absent, which the Judge treats as nothing to say rather than as a bad signal. $wifiText = '' $wifiSignal = $null try { $wifiText = (netsh wlan show interfaces 2>$null | Out-String) $match = [regex]::Match($wifiText, '(?m)^\s*Signal\s*:\s*(\d+)\s*%') if ($match.Success) { $wifiSignal = [int]$match.Groups[1].Value } } catch { } $gateway = $null try { $gateway = (Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop | Sort-Object RouteMetric | Select-Object -First 1).NextHop } catch { } if ($gateway -eq '0.0.0.0') { $gateway = $null } $gatewayLatency = $null if ($gateway) { $gatewayLatency = Measure-Latency -Target $gateway -Count $script:NetworkGatewayPingCount } $internetLatency = Measure-Latency -Target $script:NetworkInternetTarget -Count $script:NetworkInternetPingCount $resolution = Resolve-HostAddress -HostName $script:NetworkDnsTarget $mappings = @() try { $mappings = @(Get-SmbMapping -ErrorAction Stop | ForEach-Object { $server = ("$($_.RemotePath)" -split '\\')[2] [pscustomobject]@{ LocalPath = "$($_.LocalPath)" RemotePath = "$($_.RemotePath)" Status = "$($_.Status)" Server = $server ConnectMs = $(if ($server) { Measure-TcpConnect -HostName $server -Port $script:NetworkSmbPort } else { $null }) } }) } catch { } # Latency to every mapped file server, for the detail table: a share that feels slow # and a share that is slow are told apart here. $mappedLatency = @( $mappings | Where-Object { $_.Server } | Select-Object -ExpandProperty Server -Unique | ForEach-Object { Measure-Latency -Target $_ -Count $script:NetworkServerPingCount } ) [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Network' Adapters = $adapters WifiSignalPercent = $wifiSignal WifiInterfaceText = $wifiText GatewayAddress = $gateway GatewayLatency = $gatewayLatency InternetTarget = $script:NetworkInternetTarget InternetLatency = $internetLatency DnsTarget = $script:NetworkDnsTarget DnsResolved = $resolution.Resolved DnsMilliseconds = $resolution.Milliseconds SmbMappings = $mappings MappedLatency = $mappedLatency # Which rights the readings were taken with. Not a judgement, and not the same # question as Finding.Privilege: the Judge needs it because an elevated session # cannot see the Technician's drive mappings, so an empty list means two things. Elevated = (Get-CurrentPrivilege) -eq 'admin' } } function ConvertTo-NetworkFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) New-AdapterFinding -Data $Data -Parameters $Parameters New-WifiSignalFinding -Data $Data -Parameters $Parameters New-GatewayLatencyFinding -Data $Data -Parameters $Parameters New-InternetLatencyFinding -Data $Data -Parameters $Parameters New-DnsResolutionFinding -Data $Data -Parameters $Parameters New-MappedDriveFinding -Data $Data -Parameters $Parameters } function ConvertTo-NetworkSection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $latency = @( Get-DataProperty $Data 'GatewayLatency' Get-DataProperty $Data 'InternetLatency' Get-DataProperty $Data 'MappedLatency' ) | Where-Object { $_ } New-Section -Title (Get-Text 'Title.Network.LatencyTestsNetwork') -Row (@($latency) | ForEach-Object { $_ | Select-Object Target, Sent, Lost, LossPercent, AverageMs, MaximumMs }) $wifi = Get-DataProperty $Data 'WifiInterfaceText' if ($wifi) { New-Section -Title (Get-Text 'Title.Network.WiFiInterfaceNetsh') -Text $wifi } } function New-AdapterFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) # Which descriptions name a tunnel rather than a wire. A parameter because the estate # a Customer runs decides it, and because a VPN nobody knew was up explains a great # deal about an application that only feels slow at one site. $vpnPattern = Get-Parameter $Parameters 'VpnAdapterPattern' ` 'VPN|Fortinet|AnyConnect|GlobalProtect|Wintun|TAP-|WireGuard|Juniper|Check Point|Sophos' $adapters = (Get-DataCollection $Data 'Adapters') if (-not $adapters.Count) { # The script said nothing here, which reads in the Report exactly like a machine # that was never asked. A machine with every adapter down explains itself. return New-Finding -Category Network -Check (Get-Text 'Check.Network.NetworkAdapters') -Severity WARN ` -Value (Get-Text 'Value.Network.NoneUp') ` -Hint (Get-Text 'Hint.Network.NoNetworkAdapterIsUp') } foreach ($adapter in $adapters) { $hint = '' if ("$($adapter.Description)" -match $vpnPattern) { $hint = (Get-Text 'Hint.Network.VpnAdapterActive') } New-Finding -Category Network -Check ((Get-Text 'Check.Network.Adapter') -f $adapter.Name) -Severity INFO ` -Value ('{0} @ {1}' -f $adapter.Description, $adapter.LinkSpeed) -Hint $hint } } function New-WifiSignalFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warnBelow = Get-Parameter $Parameters 'WifiSignalWarnBelowPercent' 70 $failBelow = Get-Parameter $Parameters 'WifiSignalFailBelowPercent' 50 $signal = ConvertTo-Number (Get-DataProperty $Data 'WifiSignalPercent') # Nothing at all when there is no reading, which is the one place Gutcheck stays # silent on purpose. A machine with no wireless adapter and a machine whose wireless # output did not parse are indistinguishable here, and inventing a Finding for either # would be inventing a fact. See #1, Out of Scope. if ($null -eq $signal) { return } New-Finding -Category Network -Check (Get-Text 'Check.Network.WiFiSignal') ` -Severity (Get-SeverityBelow $signal $warnBelow $failBelow) -Value ('{0:N0} %' -f $signal) ` -Hint (Get-Text 'Hint.Network.WeakWiFiSlowSync') } function New-GatewayLatencyFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $lossWarn = Get-Parameter $Parameters 'GatewayLossWarnPercent' 0 $lossFail = Get-Parameter $Parameters 'GatewayLossFailPercent' 5 $msWarn = Get-Parameter $Parameters 'GatewayLatencyWarnMs' 10 $msFail = Get-Parameter $Parameters 'GatewayLatencyFailMs' 50 $address = Get-DataProperty $Data 'GatewayAddress' $latency = Get-DataProperty $Data 'GatewayLatency' if (-not $address -or -not $latency) { return New-Finding -Category Network -Check (Get-Text 'Check.Network.DefaultGateway') -Severity WARN -Value (Get-Text 'Value.Shared.None') ` -Hint (Get-Text 'Hint.Network.TheMachineHasNoDefault') } $loss = ConvertTo-Number (Get-DataProperty $latency 'LossPercent') $avg = ConvertTo-Number (Get-DataProperty $latency 'AverageMs') $max = ConvertTo-Number (Get-DataProperty $latency 'MaximumMs') if ($null -eq $avg) { # Every ping lost. On a gateway that is not a blocked ping, it is a broken link. return New-Finding -Category Network -Check (Get-Text 'Check.Network.GatewayLatency') -Severity FAIL ` -Value ((Get-Text 'Value.Network.NoReplyWithLoss') -f $address, $loss) ` -Hint (Get-Text 'Hint.Network.TheDefaultGatewayDidNot') } # Loss and latency are independent: whichever reads worse decides. $severity = Get-WorstSeverity (Get-Severity $loss $lossWarn $lossFail) (Get-Severity $avg $msWarn $msFail) New-Finding -Category Network -Check (Get-Text 'Check.Network.GatewayLatency') -Severity $severity ` -Value ((Get-Text 'Value.Network.PingResult') -f $address, $avg, $max, $loss) ` -Hint (Get-Text 'Hint.Network.UnstableLocalNetworkWiFi') } function New-InternetLatencyFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $target = Get-DataProperty $Data 'InternetTarget' $latency = Get-DataProperty $Data 'InternetLatency' if (-not $latency) { return } $loss = ConvertTo-Number (Get-DataProperty $latency 'LossPercent') $avg = ConvertTo-Number (Get-DataProperty $latency 'AverageMs') # Never graded. Plenty of Customer networks block ICMP to the internet entirely, and a # FAIL there would be a Finding about a firewall policy rather than about the machine. $value = $(if ($null -eq $avg) { "no reply (loss $loss %)" } else { (Get-Text 'Value.Network.LatencyWithLoss') -f $avg, $loss }) New-Finding -Category Network -Check ((Get-Text 'Check.Network.InternetLatency') -f $target) -Severity INFO -Value $value } function New-DnsResolutionFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warn = Get-Parameter $Parameters 'DnsResolutionWarnMs' 500 $fail = Get-Parameter $Parameters 'DnsResolutionFailMs' ([double]::MaxValue) $target = Get-DataProperty $Data 'DnsTarget' $check = 'DNS resolution ({0})' -f $target if (-not (Get-DataProperty $Data 'DnsResolved')) { return New-Finding -Category Network -Check $check -Severity FAIL -Value (Get-Text 'Value.Network.Failed') ` -Hint (Get-Text 'Hint.Network.NoInternetNameResolution') } $ms = ConvertTo-Number (Get-DataProperty $Data 'DnsMilliseconds') if ($null -eq $ms) { return New-UnavailableFinding -Category Network -Check $check ` -Hint (Get-Text 'Hint.Shared.NameResolvedNotTimed') } New-Finding -Category Network -Check $check -Severity (Get-Severity $ms $warn $fail) ` -Value ('{0:N0} ms' -f $ms) ` -Hint (Get-Text 'Hint.Network.SlowDNSDelaysEveryConnection') } function New-MappedDriveFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $mappings = (Get-DataCollection $Data 'SmbMappings') if (-not $mappings.Count) { # In the Technician's own session no mappings means no mappings, which is not # worth a line. Elevated it means something else entirely, and staying quiet would # let a Technician read an empty list as a machine with no network drives. if (Get-DataProperty $Data 'Elevated') { return New-Finding -Category Network -Check (Get-Text 'Check.Network.MappedDrives') -Severity INFO -Value (Get-Text 'Value.Network.NoneVisible') ` -Hint (Get-Text 'Hint.Network.ElevatedSessionsDoNotSee') } return } foreach ($mapping in $mappings) { $connect = ConvertTo-Number $mapping.ConnectMs $statusOk = "$($mapping.Status)" -eq 'OK' $severity = 'OK' if (-not $statusOk -or $null -eq $connect) { $severity = 'WARN' } New-Finding -Category Network -Check ((Get-Text 'Check.Network.MappedDrive') -f $mapping.LocalPath) -Severity $severity ` -Value ((Get-Text 'Value.Network.MappingStatus') -f $mapping.RemotePath, $mapping.Status, $(if ($null -eq $connect) { (Get-Text 'Value.Network.Failed') } else { '{0} ms' -f $connect })) ` -Hint (Get-Text 'Hint.Network.AppsStartedFromANetwork') } } |