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 # Where the active power plan keeps USB selective suspend: the USB settings subgroup and the # setting in it. Verified with "powercfg /q" on Windows 11; the names powercfg prints beside # them are localised, the GUIDs are not. $script:NetworkUsbPowerSubgroup = '2a737441-1930-4402-8d77-b2bebba308a3' $script:NetworkUsbSuspendSetting = '48e6b7a6-50f5-4782-a5d4-53bb8f07e226' # Which of an adapter's advanced properties are gathered and shown, matched on the registry # keyword: the display names are localised and every vendor words them differently, the # keywords are what the driver's INF calls them. Wider than what counts as power saving, # because wake and interrupt moderation are what a second-level Technician asks about next. $script:NetworkEnergySettingPattern = 'EEE|Energy|Green|PowerSav|PowerDown|LowPower|ULP|Sips|SelectiveSuspend|SSIdle|DeviceSleep|GigaLite|AutoDisableGigabit|Wake|Wol|PME|Moderation|^ITR$' # Which of those save energy while the machine is running, and so can take the link down # under a program that is using it. The default of the PowerSavingKeywordPattern parameter. # A keyword that matches is counted only when it is a switch; see Test-AdapterSettingSwitch. $script:NetworkPowerSavingPattern = 'EEE|EnergyEfficient|Green|PowerSav|LowPower|ULPMode|SipsEnabled|SelectiveSuspend|DeviceSleep|GigaLite|AutoDisableGigabit' # A keyword that names a list of modes and not a switch, where the driver did not say which # values the setting can take. $script:NetworkModeKeywordPattern = '(?:Mode|Level)$' # What the SMB client configuration is asked for. The rest of it concerns signing and # guests, which is another Check's question. $script:NetworkSmbClientProperties = @( 'SessionTimeout', 'FileInfoCacheLifetime', 'DirectoryCacheLifetime', 'OplocksDisabled', 'EnableBandwidthThrottling', 'EnableMultiChannel' ) # The power management answers that say Windows may take the adapter away to save energy. $script:NetworkPowerManagementSavers = @('AllowComputerToTurnOffDevice', 'SelectiveSuspend', 'DeviceSleepOnDisconnect') $script:NetworkPowerManagementProperties = $script:NetworkPowerManagementSavers + @('WakeOnMagicPacket', 'WakeOnPattern', 'ArpOffload', 'NSOffload', 'D0PacketCoalescing', 'RsnRekeyOffload') # How the signal is read out of "netsh wlan show interfaces": the label is "Signal" on a # German and on an English Windows, and the value is a percentage. The Gatherer reads the # Finding's number by it and the Section its row, so the two cannot disagree. $script:NetworkWifiSignalPattern = '(?m)^\s*Signal\s*:\s*(\d+)\s*%' # The user's proxy settings, as Internet Options writes them. $script:NetworkUserProxyKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' function Get-NetworkData { <# .PARAMETER Observed What earlier Checks gathered, the Elevated Part's among them. The adapters' power management is closed to a Run without admin rights; the NetworkDisruption Check reads it with them and it is taken from there. See Get-NetworkAdapterFact. #> [CmdletBinding()] [OutputType([psobject])] param( [hashtable]$Parameters = @{}, [AllowNull()][hashtable]$Observed = @{} ) $elevatedPower = $null if ($Observed -and $Observed.ContainsKey('NetworkDisruption')) { $elevatedPower = Get-DataProperty $Observed['NetworkDisruption'] 'AdapterPowerManagement' } $adapterFacts = Get-NetworkAdapterFact -Parameters $Parameters -ElevatedPowerManagement $elevatedPower $adapters = $adapterFacts.Adapters # 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, $script:NetworkWifiSignalPattern) 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 } ) # The user's proxy (WinINET), read from the hive of whoever this process runs as. That # is the Technician's session only in the Main Part and unelevated; Elevated below is # what lets the Judge say so rather than present another account's settings as the # user's. $null when the key could not be read, which is not the same as no proxy. $userProxy = $null try { $settings = Get-ItemProperty -Path $script:NetworkUserProxyKey -ErrorAction Stop $userProxy = [pscustomobject]@{ ProxyEnable = $settings.ProxyEnable ProxyServer = $settings.ProxyServer ProxyOverride = $settings.ProxyOverride AutoConfigURL = $settings.AutoConfigURL } } catch { } # The system proxy (WinHTTP), which is what Office uses for its background requests. # Kept as netsh's own text and parsed by the Judge: the output is localised, and a # parse that goes wrong has to be something a fixture can catch. There are two views on # 64-bit Windows, and 32-bit Office reads the SysWOW64 one (KB 2847833). A 32-bit # PowerShell would be redirected to SysWOW64 by "System32", so it asks for Sysnative. $nativeNetsh = Join-Path $env:windir 'System32\netsh.exe' if ([Environment]::Is64BitOperatingSystem -and -not [Environment]::Is64BitProcess) { $nativeNetsh = Join-Path $env:windir 'Sysnative\netsh.exe' } $winHttp64 = '' try { $winHttp64 = (& $nativeNetsh winhttp show proxy 2>$null | Out-String) } catch { } # $null only where there is no 32-bit view at all; a view that would not answer is an # empty string, which the Judge reports as unreadable rather than as absent. $winHttp32 = $null $wowNetsh = Join-Path $env:windir 'SysWOW64\netsh.exe' if (Test-Path -LiteralPath $wowNetsh) { $winHttp32 = '' try { $winHttp32 = (& $wowNetsh winhttp show proxy 2>$null | Out-String) } catch { } } [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 UserProxy = $userProxy WinHttpProxy64Text = $winHttp64 WinHttpProxy32Text = $winHttp32 # USB selective suspend in the active power plan: $true, $false, or $null when # powercfg gave no answer. The text is what it printed, for the Section. UsbSelectiveSuspend = $adapterFacts.UsbSelectiveSuspend UsbSelectiveSuspendText = $adapterFacts.UsbSelectiveSuspendText SmbClientConfiguration = $adapterFacts.SmbClientConfiguration # Which keywords PowerSavingOn was reduced by, so that the Section can say of each # setting whether it was counted. Not a reading: the parameter one was reduced with. PowerSavingKeywordPattern = $adapterFacts.PowerSavingKeywordPattern # The sources that would not answer, by the name a Technician can type. Not read # is not the same as absent, and only the Gatherer can tell the two apart. Unread = $adapterFacts.Unread # 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 Get-NetworkAdapterPowerManagement { <# .SYNOPSIS Get-NetAdapterPowerManagement for every adapter, as rows of text, and whether it refused. Never throws, and writes no error into the transcript. .DESCRIPTION Asked for all adapters at once: a virtual adapter has no power management, and asked for by name it answers with an error that says nothing about this Run's rights. Nothing back at all, with an error, is the refusal - and without admin rights that is what comes back. Called twice in a Run that was given admin rights: by the Network Check, which runs with the user's rights and is refused, and by the NetworkDisruption Check in the Elevated Part, whose answer the Network Check then uses. #> [CmdletBinding()] [OutputType([psobject])] param() $rows = @() $refused = $true if (Get-Command -Name Get-NetAdapterPowerManagement -ErrorAction SilentlyContinue) { $problems = $null $power = @(Get-NetAdapterPowerManagement -ErrorAction SilentlyContinue -ErrorVariable problems) $refused = [bool](-not $power.Count -and @($problems).Count) $rows = @(foreach ($row in $power) { $out = [pscustomobject]@{ Name = "$($row.Name)" } foreach ($property in $script:NetworkPowerManagementProperties) { $out | Add-Member -NotePropertyName $property -NotePropertyValue "$($row.$property)" } $out }) } [pscustomobject]@{ Rows = $rows; Refused = $refused } } function Select-AdapterPowerManagement { <# .SYNOPSIS Which reading of the adapters' power management a Run uses: its own, or the Elevated Part's where its own was refused. Pure. .DESCRIPTION Seen on a real machine: a Run that was given admin rights still reported "nicht gelesen: Get-NetAdapterPowerManagement", because the Check that asks runs with the user's rights and nobody handed it what the Elevated Part could read. Unread only when neither has rows: refused here, and no Elevated Part, or one that was refused as well or came from a module that did not read it yet. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Own, [AllowNull()]$Elevated) if (-not [bool](Get-DataProperty $Own 'Refused')) { return [pscustomobject]@{ Rows = (Get-DataCollection $Own 'Rows'); Unread = $false; Source = 'Own' } } $rows = @((Get-DataCollection ([pscustomobject]@{ Rows = $Elevated }) 'Rows') | Where-Object { $_ }) [pscustomobject]@{ Rows = $rows; Unread = [bool](-not $rows.Count); Source = $(if ($rows.Count) { 'Elevated' } else { 'None' }) } } function Get-NetworkAdapterFact { <# .SYNOPSIS The adapters that are up with their driver, whether they are USB devices and what their power saving is set to, USB selective suspend and the SMB client configuration. .DESCRIPTION Every source is asked once and may refuse: Get-NetAdapterPowerManagement does so without admin rights. A refusal is an answer here, so nothing is asked with -ErrorAction Stop - that would write a TerminatingError line into the Run's transcript for a setting this Run was never going to read. What refused is named in Unread. #> [CmdletBinding()] [OutputType([psobject])] param( [hashtable]$Parameters = @{}, # What the Elevated Part read of the adapters' power management, when it ran. Used # where this Run's own rights were refused. [AllowNull()]$ElevatedPowerManagement ) $unread = [System.Collections.Generic.List[string]]::new() $up = @() if (Get-Command -Name Get-NetAdapter -ErrorAction SilentlyContinue) { $problems = $null $up = @(Get-NetAdapter -ErrorAction SilentlyContinue -ErrorVariable problems | Where-Object { $_.Status -eq 'Up' }) if (@($problems).Count) { $unread.Add('Get-NetAdapter') } } else { $unread.Add('Get-NetAdapter') } $power = @() $advanced = @() $sleep = @() if ($up.Count) { # Refused without admin rights. A Run that was given them read it in the Elevated # Part, and that answer is as good as one read here. $chosen = Select-AdapterPowerManagement -Own (Get-NetworkAdapterPowerManagement) -Elevated $ElevatedPowerManagement $power = @($chosen.Rows) if ($chosen.Unread) { $unread.Add('Get-NetAdapterPowerManagement') } if (Get-Command -Name Get-NetAdapterAdvancedProperty -ErrorAction SilentlyContinue) { $problems = $null $advanced = @(Get-NetAdapterAdvancedProperty -ErrorAction SilentlyContinue -ErrorVariable problems) if (-not $advanced.Count -and @($problems).Count) { $unread.Add('Get-NetAdapterAdvancedProperty') } } else { $unread.Add('Get-NetAdapterAdvancedProperty') } # "Allow the computer to turn off this device to save power" as Device Manager # shows it, one instance per device that has the tick box. Readable without admin # rights where Get-NetAdapterPowerManagement is not, which is why both are asked. $problems = $null $sleep = @(Get-CimInstance -Namespace 'root\wmi' -ClassName 'MSPower_DeviceEnable' -ErrorAction SilentlyContinue -ErrorVariable problems) if (-not $sleep.Count -and @($problems).Count) { $unread.Add('MSPower_DeviceEnable') } } $energyPattern = Get-Parameter $Parameters 'EnergySettingPattern' $script:NetworkEnergySettingPattern $savingPattern = Get-Parameter $Parameters 'PowerSavingKeywordPattern' $script:NetworkPowerSavingPattern $adapters = @($up | ForEach-Object { $name = "$($_.Name)" $pnp = "$($_.PnPDeviceID)" $management = $null # Read with Get-DataProperty: a row from the Elevated Part came through CliXML. $row = $power | Where-Object { "$(Get-DataProperty $_ 'Name')" -eq $name } | Select-Object -First 1 if ($row) { $management = [pscustomobject]@{} foreach ($property in $script:NetworkPowerManagementProperties) { $management | Add-Member -NotePropertyName $property -NotePropertyValue "$(Get-DataProperty $row $property)" } } # The instance is named after the device: its PnP device id and an index. $sleepAllowed = $null if ($pnp) { $instance = $sleep | Where-Object { "$($_.InstanceName)" -like ('{0}_*' -f [WildcardPattern]::Escape($pnp)) } | Select-Object -First 1 if ($instance) { $sleepAllowed = [bool]$instance.Enable } } # By keyword where the driver gave one; by display name only for the property # that has none. $settings = @($advanced | Where-Object { "$($_.Name)" -eq $name } | Where-Object { if ("$($_.RegistryKeyword)") { "$($_.RegistryKeyword)" -match $energyPattern } else { "$($_.DisplayName)" -match $energyPattern } } | ForEach-Object { [pscustomobject]@{ Keyword = "$($_.RegistryKeyword)" DisplayName = "$($_.DisplayName)" Value = "$($_.DisplayValue)" RegistryValue = (@($_.RegistryValue) -join ',') # What the driver says the setting can be, in its order. Empty for a # setting that takes a number from a range. It is what tells a switch # from a list of modes. ValidRegistryValues = @($_.ValidRegistryValues | Where-Object { $null -ne $_ } | ForEach-Object { "$_" }) } }) [pscustomobject]@{ Name = $name Description = "$($_.InterfaceDescription)" LinkSpeed = "$($_.LinkSpeed)" DriverVersion = "$($_.DriverVersion)" DriverDate = "$($_.DriverDate)" # A USB network adapter - in a dock, in a dongle, on a stick - is enumerated by # the USB bus, and its PnP device id says so. Kept beside the answer. PnpDeviceId = $pnp IsUsb = $pnp -like 'USB\*' PowerManagement = $management DeviceSleepAllowed = $sleepAllowed EnergySettings = $settings # A reduction of the three above and nothing more, so the Report can state one # answer per adapter; Get-AdapterPowerSaving is pure and says how. PowerSavingOn = Get-AdapterPowerSaving -PowerManagement $management -DeviceSleepAllowed $sleepAllowed ` -EnergySetting $settings -KeywordPattern $savingPattern } }) $suspendText = Get-PowercfgSettingText -Subgroup $script:NetworkUsbPowerSubgroup -Setting $script:NetworkUsbSuspendSetting $suspend = ConvertFrom-PowercfgIndexText -Text $suspendText if (-not $suspend.Recognised) { $unread.Add('powercfg') } $smb = $null if (Get-Command -Name Get-SmbClientConfiguration -ErrorAction SilentlyContinue) { $problems = $null $configuration = Get-SmbClientConfiguration -ErrorAction SilentlyContinue -ErrorVariable problems if ($configuration) { $smb = [pscustomobject]@{} foreach ($property in $script:NetworkSmbClientProperties) { $smb | Add-Member -NotePropertyName $property -NotePropertyValue $configuration.$property } } } if ($null -eq $smb) { $unread.Add('Get-SmbClientConfiguration') } [pscustomobject]@{ Adapters = $adapters UsbSelectiveSuspend = $suspend.On UsbSelectiveSuspendText = $suspendText SmbClientConfiguration = $smb PowerSavingKeywordPattern = "$savingPattern" Unread = @($unread) } } function Get-PowercfgSettingText { <# .SYNOPSIS What "powercfg /q" prints for one setting of the active power plan, or an empty string when it would not say. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)][string]$Subgroup, [Parameter(Mandatory)][string]$Setting ) if (-not (Get-Command -Name 'powercfg.exe' -CommandType Application -ErrorAction SilentlyContinue)) { return '' } # A power plan without the setting makes powercfg complain on stderr. Under the # module's Stop that complaint would be a terminating error, and a machine without the # setting is not one. $ErrorActionPreference = 'SilentlyContinue' $output = & powercfg.exe /q SCHEME_CURRENT $Subgroup $Setting 2>$null if ($LASTEXITCODE -ne 0) { return '' } ($output | Out-String) } function ConvertFrom-PowercfgIndexText { <# .SYNOPSIS Parses what "powercfg /q" printed for one setting into the index in effect on mains and the one on battery. Pure, and reaches nothing. .DESCRIPTION powercfg prints its labels in the UI language, so this reads the two values written as 0x and eight hex digits and nothing else: the possible settings above them are written as three plain digits. Mains comes first, battery second. Anything that does not hold both is not recognised, because an error read as "off" would be a false all-clear. On says whether the setting is switched on for either: a notebook on a dock is on mains at the desk and on battery in the meeting room, and the adapter goes with it. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyString()][string]$Text) $found = [regex]::Matches("$Text", '(?m):\s*0x([0-9a-fA-F]{8})\s*$') $ac = $null $dc = $null $on = $null if ($found.Count -ge 2) { $ac = [Convert]::ToInt32($found[0].Groups[1].Value, 16) $dc = [Convert]::ToInt32($found[1].Groups[1].Value, 16) $on = ($ac -ne 0) -or ($dc -ne 0) } [pscustomobject]@{ PSTypeName = 'Gutcheck.PowercfgIndex' Recognised = $found.Count -ge 2 AcIndex = $ac DcIndex = $dc On = $on } } function Test-AdapterSettingSwitch { <# .SYNOPSIS Whether an advanced property is a plain switch: 0 is off, 1 is on, and it can be nothing else. Pure. .DESCRIPTION Only a switch can be reduced to on or off without knowing the driver. A setting with more positions is a list of modes, and which of them saves energy is the vendor's to say: an Intel wireless adapter's MIMOPowerSaveMode is 3 when the saving is switched off, and reading every value but 0 as "on" called that power saving. The driver's own list of valid values decides, where it gave one: Intel's ULPMode is called a mode and lists 1 and 0. Data without the list - gathered before it was, or a setting that takes a number from a range - is judged by its name and its value: a keyword that ends in Mode or Level is taken for a list of modes, and anything else for a switch only while its value is 0 or 1. #> [CmdletBinding()] [OutputType([bool])] param([AllowNull()]$Setting) $valid = (Get-DataCollection $Setting 'ValidRegistryValues') if ($valid.Count) { foreach ($value in $valid) { $position = ConvertTo-Number $value if ($null -eq $position -or ($position -ne 0 -and $position -ne 1)) { return $false } } return $true } if ("$(Get-DataProperty $Setting 'Keyword')" -match $script:NetworkModeKeywordPattern) { return $false } $number = ConvertTo-Number (Get-DataProperty $Setting 'RegistryValue') ($number -eq 0) -or ($number -eq 1) } function Get-AdapterPowerSaving { <# .SYNOPSIS Whether an adapter's settings let it save energy while the machine runs: $true, $false, or $null when none of them gave an answer. Pure. .DESCRIPTION Three sources, any of which may be missing. One that says "on" decides it; "off" needs at least one that answered and none that said on. "Unsupported", an empty value and a setting that is not a number are not answers. Wake-on-LAN and interrupt moderation are gathered and shown but not counted: they do not take a link down under a program that is using it. Neither is a setting that is not a switch, whatever its keyword: Test-AdapterSettingSwitch says why. #> [CmdletBinding()] [OutputType([bool])] param( [AllowNull()]$PowerManagement, [AllowNull()]$DeviceSleepAllowed, [AllowNull()][AllowEmptyCollection()]$EnergySetting, [string]$KeywordPattern = $script:NetworkPowerSavingPattern ) $on = $false $off = $false if ($DeviceSleepAllowed -is [bool]) { if ($DeviceSleepAllowed) { $on = $true } else { $off = $true } } foreach ($name in $script:NetworkPowerManagementSavers) { $answer = "$(Get-DataProperty $PowerManagement $name)" if ($answer -eq 'Enabled') { $on = $true } elseif ($answer -eq 'Disabled') { $off = $true } } foreach ($setting in @($EnergySetting)) { if ($null -eq $setting) { continue } if ("$(Get-DataProperty $setting 'Keyword')" -notmatch $KeywordPattern) { continue } if (-not (Test-AdapterSettingSwitch -Setting $setting)) { continue } $number = ConvertTo-Number (Get-DataProperty $setting 'RegistryValue') if ($number -eq 1) { $on = $true } elseif ($number -eq 0) { $off = $true } } if ($on) { return $true } if ($off) { return $false } $null } function ConvertTo-NetworkFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) New-AdapterFinding -Data $Data -Parameters $Parameters New-AdapterSettingFinding -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 New-ProxyFinding -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') -Help (Get-Text 'Help.Network.LatencyTestsNetwork') -Row (@($latency) | ForEach-Object { $row = [ordered]@{} $row[(Get-Text 'Column.Network.Latency.Target')] = Get-DataProperty $_ 'Target' $row[(Get-Text 'Column.Network.Latency.Sent')] = Get-DataProperty $_ 'Sent' $row[(Get-Text 'Column.Network.Latency.Lost')] = Get-DataProperty $_ 'Lost' $row[(Get-Text 'Column.Network.Latency.LossPercent')] = Get-DataProperty $_ 'LossPercent' $row[(Get-Text 'Column.Network.Latency.AverageMs')] = Get-DataProperty $_ 'AverageMs' $row[(Get-Text 'Column.Network.Latency.MaximumMs')] = Get-DataProperty $_ 'MaximumMs' [pscustomobject]$row }) New-WifiSection -Data $Data New-AdapterSection -Data $Data New-ProxySection -Data $Data } function New-NetworkSettingRow { <# .SYNOPSIS One row of a table of settings: what it concerns, the setting, its value, what that value does and where it was read. Pure. .DESCRIPTION The settings of an adapter, of the SMB client and of the proxy are one kind of table: a name a Technician searches for with what it is behind it, a value as Windows gave it with what it means behind it, and the consequence in a sentence. The headings are German like the rest of the Report. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowEmptyString()][string]$Area, [AllowEmptyString()][string]$Setting, [AllowEmptyString()][string]$Value, [AllowEmptyString()][string]$Meaning, [AllowEmptyString()][string]$Source ) $row = [ordered]@{} $row[(Get-Text 'Column.Network.Setting.Area')] = $Area $row[(Get-Text 'Column.Network.Setting.Name')] = $Setting $row[(Get-Text 'Column.Network.Setting.Value')] = $Value $row[(Get-Text 'Column.Network.Setting.Meaning')] = $Meaning $row[(Get-Text 'Column.Network.Setting.Source')] = $Source [pscustomobject]$row } function Test-NetworkDataPropertyPresent { # Whether data carries a property at all. Absent is data from a module that did not # gather it yet; present and empty is an answer of nothing. [CmdletBinding()] [OutputType([bool])] param([AllowNull()]$InputObject, [Parameter(Mandatory)][string]$Name) if ($null -eq $InputObject) { return $false } if ($InputObject -is [System.Collections.IDictionary]) { return [bool]$InputObject.Contains($Name) } [bool]$InputObject.PSObject.Properties[$Name] } function ConvertFrom-WlanInterfaceText { <# .SYNOPSIS Parses what "netsh wlan show interfaces" printed into its interfaces, each a list of label and value in the order netsh gave them. Pure, and reaches nothing. .DESCRIPTION netsh prints its labels in the UI language, so this reads by structure: an interface is a run of indented "label : value" lines, and an empty line ends it. A line that is indented and has no colon continues the value above it - the radio status is printed over two lines. A line that is not indented is a heading or an error message and belongs to no interface. What each pair is, is ConvertTo-WlanInterfaceRow's to say. #> [CmdletBinding()] [OutputType([object[]])] param([AllowNull()][AllowEmptyString()][string]$Text) $interfaces = @() $pairs = @() $last = $null foreach ($line in ("$Text" -split '\r?\n') + @('')) { if (-not $line.Trim()) { if ($pairs.Count) { $interfaces += , @($pairs) } $pairs = @() $last = $null continue } if ($line -notmatch '^\s') { continue } # The first colon: no label has one, and the address of an adapter has five. $match = [regex]::Match($line, '^\s+([^:]+?)\s*:(.*)$') if ($match.Success) { $last = [pscustomobject]@{ Label = $match.Groups[1].Value.Trim(); Value = $match.Groups[2].Value.Trim() } $pairs += $last } elseif ($last) { $last.Value = (@($last.Value, $line.Trim()) | Where-Object { $_ }) -join ', ' } } , @($interfaces) } function Get-WlanFieldKind { <# .SYNOPSIS What one line of "netsh wlan show interfaces" states, or nothing for a line this does not know. Pure. .DESCRIPTION By the shape of the value wherever it has one that nothing else has - an address, a percentage, "802.11", "GHz" - and by the label only where the German and the English Windows print one that is told apart without an umlaut. The two rates are told apart by their order, receive first, as the WinHTTP proxy and its bypass list are: their labels are the ones with an umlaut in them. .PARAMETER RateSeen How many rates stand above this line. #> [CmdletBinding()] [OutputType([string])] param([AllowEmptyString()][string]$Label, [AllowEmptyString()][string]$Value, [int]$RateSeen = 0) $address = '^(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$' if ($Value -match $address) { if ($Label -match 'BSSID') { return 'Bssid' } return 'Mac' } if ((' {0} : {1}' -f $Label, $Value) -match $script:NetworkWifiSignalPattern) { return 'Signal' } if ($Value -match '^802\.11') { return 'Radio' } if ($Value -match '^\d+(?:[.,]\d+)?\s*GHz$') { return 'Band' } if ($Label -match '\((?:Mbps|MBit/s)\)') { if ($RateSeen -eq 0) { return 'Receive' } elseif ($RateSeen -eq 1) { return 'Transmit' } else { return '' } } if ($Label -match '^Name$') { return 'Name' } if ($Label -match '^(?:Beschreibung|Description)$') { return 'Description' } if ($Label -match '^(?:Status|State)$') { return 'State' } if ($Label -match '^SSID$') { return 'Ssid' } if ($Label -match '^(?:Kanal|Channel)$' -and $Value -match '^\d+$') { return 'Channel' } if ($Label -match '^Rssi$') { return 'Rssi' } if ($Label -match '^Authenti') { return 'Authentication' } if ($Label -match '^(?:Cipher|Verschl)') { return 'Cipher' } if ($Label -match '^Profile?$') { return 'Profile' } '' } function Get-WlanFieldText { # What a line is called in the Report, and what a Technician should know about it. [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)][string]$Kind) $name = '' $meaning = '' switch ($Kind) { 'Description' { $name = Get-Text 'Value.Network.Wifi.Description' } 'Mac' { $name = Get-Text 'Value.Network.Wifi.Mac' } 'State' { $name = Get-Text 'Value.Network.Wifi.State'; $meaning = Get-Text 'Value.Network.Wifi.StateMeaning' } 'Ssid' { $name = Get-Text 'Value.Network.Wifi.Ssid' } 'Bssid' { $name = Get-Text 'Value.Network.Wifi.Bssid'; $meaning = Get-Text 'Value.Network.Wifi.BssidMeaning' } 'Radio' { $name = Get-Text 'Value.Network.Wifi.Radio'; $meaning = Get-Text 'Value.Network.Wifi.RadioMeaning' } 'Band' { $name = Get-Text 'Value.Network.Wifi.Band'; $meaning = Get-Text 'Value.Network.Wifi.BandMeaning' } 'Channel' { $name = Get-Text 'Value.Network.Wifi.Channel' } 'Signal' { $name = Get-Text 'Value.Network.Wifi.Signal'; $meaning = (Get-Text 'Value.Network.Wifi.SignalMeaning') -f (Get-Text 'Check.Network.WiFiSignal') } 'Rssi' { $name = Get-Text 'Value.Network.Wifi.Rssi'; $meaning = Get-Text 'Value.Network.Wifi.RssiMeaning' } 'Receive' { $name = Get-Text 'Value.Network.Wifi.Receive'; $meaning = Get-Text 'Value.Network.Wifi.RateMeaning' } 'Transmit' { $name = Get-Text 'Value.Network.Wifi.Transmit'; $meaning = Get-Text 'Value.Network.Wifi.RateMeaning' } 'Authentication' { $name = Get-Text 'Value.Network.Wifi.Authentication'; $meaning = Get-Text 'Value.Network.Wifi.AuthenticationMeaning' } 'Cipher' { $name = Get-Text 'Value.Network.Wifi.Cipher' } 'Profile' { $name = Get-Text 'Value.Network.Wifi.Profile'; $meaning = Get-Text 'Value.Network.Wifi.ProfileMeaning' } } [pscustomobject]@{ Name = $name; Meaning = $meaning } } function Get-WlanRadioText { # The name the Wi-Fi Alliance gave a radio type, for the ones that have one. Pure. [CmdletBinding()] [OutputType([string])] param([AllowEmptyString()][string]$Radio) switch -Regex ("$Radio".Trim()) { '^802\.11n$' { return 'Wi-Fi 4' } '^802\.11ac$' { return 'Wi-Fi 5' } '^802\.11ax$' { return 'Wi-Fi 6' } '^802\.11be$' { return 'Wi-Fi 7' } } '' } function Get-WlanChannelBandText { <# .SYNOPSIS The band a channel lies in, where the channel alone says it. Pure. .DESCRIPTION Only for the radio types that have two bands: 1 to 14 is 2.4 GHz and everything from 32 on is 5 GHz. 802.11ax and 802.11be count their 6 GHz channels from 1 again, so there the number says nothing, and a Windows that knows those prints the band on a line of its own. #> [CmdletBinding()] [OutputType([string])] param([AllowEmptyString()][string]$Channel, [AllowEmptyString()][string]$Radio) if ("$Radio".Trim() -notmatch '^802\.11(?:a|b|g|n|ac)$') { return '' } $number = ConvertTo-Number $Channel if ($null -eq $number) { return '' } if ($number -ge 1 -and $number -le 14) { return (Get-Text 'Value.Network.Wifi.Band24') } if ($number -ge 32) { return (Get-Text 'Value.Network.Wifi.Band5') } '' } function ConvertTo-WlanInterfaceRow { <# .SYNOPSIS The rows of one wireless interface: what is known in a fixed order, each with what it is called here beside what netsh called it, and the rest in one last row. Pure. .DESCRIPTION Nothing netsh printed is dropped. A line this does not know, and a second line of a kind already shown, go into "weitere Angaben" as they came. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyCollection()]$Pair) $order = 'Description', 'Mac', 'State', 'Ssid', 'Bssid', 'Radio', 'Band', 'Channel', 'Signal', 'Rssi', 'Receive', 'Transmit', 'Authentication', 'Cipher', 'Profile' $known = @{} $other = @() $rates = 0 foreach ($pair in @($Pair | Where-Object { $_ })) { $kind = Get-WlanFieldKind -Label "$($pair.Label)" -Value "$($pair.Value)" -RateSeen $rates if ("$($pair.Label)" -match '\((?:Mbps|MBit/s)\)') { $rates++ } if ($kind -and -not $known.ContainsKey($kind)) { $known[$kind] = $pair } else { $other += '{0}: {1}' -f $pair.Label, $pair.Value } } $interface = '' if ($known.ContainsKey('Name')) { $interface = "$($known['Name'].Value)" } $radio = '' if ($known.ContainsKey('Radio')) { $radio = "$($known['Radio'].Value)" } foreach ($kind in $order) { if (-not $known.ContainsKey($kind)) { continue } $pair = $known[$kind] $text = Get-WlanFieldText -Kind $kind $value = "$($pair.Value)" if ($kind -eq 'Radio') { $value = Format-CodeWithMeaning -Code $value -Meaning (Get-WlanRadioText -Radio $value) } elseif ($kind -eq 'Channel') { $value = Format-CodeWithMeaning -Code $value -Meaning (Get-WlanChannelBandText -Channel $value -Radio $radio) } # What netsh called it stays, because that is what a Technician sees who asks # netsh again; said once where both say the same. $field = "$($pair.Label)" if ($field -ne $text.Name) { $field = Format-CodeWithMeaning -Code $field -Meaning $text.Name } $row = [ordered]@{} $row[(Get-Text 'Column.Network.Wifi.Interface')] = $interface $row[(Get-Text 'Column.Network.Wifi.Field')] = $field $row[(Get-Text 'Column.Network.Setting.Value')] = $value $row[(Get-Text 'Column.Network.Setting.Meaning')] = $text.Meaning [pscustomobject]$row } if ($other.Count) { $row = [ordered]@{} $row[(Get-Text 'Column.Network.Wifi.Interface')] = $interface $row[(Get-Text 'Column.Network.Wifi.Field')] = Get-Text 'Value.Network.Wifi.Other' $row[(Get-Text 'Column.Network.Setting.Value')] = $other -join '; ' $row[(Get-Text 'Column.Network.Setting.Meaning')] = Get-Text 'Value.Network.Wifi.OtherMeaning' [pscustomobject]$row } } function New-WifiSection { <# .SYNOPSIS What "netsh wlan show interfaces" said, as a table: a row for each statement. .DESCRIPTION An answer with no interface in it - a machine without the wireless service prints one sentence - is shown as it came, in a single row: it is still what netsh said. #> [CmdletBinding()] param([AllowNull()]$Data) $text = "$(Get-DataProperty $Data 'WifiInterfaceText')" if (-not $text.Trim()) { return } $rows = @(foreach ($interface in (ConvertFrom-WlanInterfaceText -Text $text)) { ConvertTo-WlanInterfaceRow -Pair $interface }) if (-not $rows.Count) { $row = [ordered]@{} $row[(Get-Text 'Column.Network.Wifi.Interface')] = '' $row[(Get-Text 'Column.Network.Wifi.Field')] = Get-Text 'Value.Network.Setting.NetshOutput' $row[(Get-Text 'Column.Network.Setting.Value')] = (@($text -split '\r?\n' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) -join ' / ') $row[(Get-Text 'Column.Network.Setting.Meaning')] = Get-Text 'Value.Network.Wifi.NoInterfaceMeaning' $rows = @([pscustomobject]$row) } New-Section -Title (Get-Text 'Title.Network.WiFiInterfaceNetsh') -Help (Get-Text 'Help.Network.WiFiInterfaceNetsh') -Row $rows } function New-ProxySection { <# .SYNOPSIS The proxy of the user and the two of the machine as a table: what each is set to, what comes of it, and what that means. .DESCRIPTION What comes of a source is worded by the functions the Judge words its Finding with, and whether a WinHTTP proxy differs from the user's is asked of the function the Judge asks, so the table and the Finding cannot disagree. Under each WinHTTP view stands what netsh printed: its text is localised, and second level should see what the reading was made of rather than trust it. #> [CmdletBinding()] param([AllowNull()]$Data) $notSet = Get-Text 'Value.Network.Setting.NotSet' $result = Get-Text 'Value.Network.Setting.Result' $rows = @() $user = Get-DataProperty $Data 'UserProxy' # Read from the hive of whoever the Run runs as, and with admin rights that may be # another account than the User's. The table says whose it is. $area = Get-Text 'Value.Network.Area.UserProxy' if (Get-DataProperty $Data 'Elevated') { $area = Get-Text 'Value.Network.Area.UserProxyElevated' } $source = $script:NetworkUserProxyKey -replace '^HKCU:', 'HKCU' if ($null -eq $user) { $rows += New-NetworkSettingRow -Area $area -Setting $result -Value (Get-UserProxyDescription $user) ` -Meaning (Get-Text 'Value.Network.Effect.UserProxyUnreadable') -Source $source } else { $rows += New-NetworkSettingRow -Area $area -Setting $result -Value (Get-UserProxyDescription $user) ` -Meaning (Get-Text 'Value.Network.Effect.UserProxy') -Source $source # ProxyEnable is a switch, and is said as one: 1 is the tick box "use a proxy # server" in Internet Options. The other three are what was typed in. $enable = "$(Get-DataProperty $user 'ProxyEnable')".Trim() $on = (ConvertTo-Number $enable) -eq 1 $switch = '' if ($on) { $switch = Get-Text 'Value.Network.Section.ProxyOn' } elseif ((ConvertTo-Number $enable) -eq 0) { $switch = Get-Text 'Value.Network.Section.ProxyOff' } $value = $notSet if ($enable) { $value = Format-CodeWithMeaning -Code $enable -Meaning $switch } $meaning = Get-Text 'Value.Network.Effect.ProxyEnableOff' if ($on) { $meaning = Get-Text 'Value.Network.Effect.ProxyEnableOn' } $rows += New-NetworkSettingRow -Area $area -Setting (Format-CodeWithMeaning -Code 'ProxyEnable' -Meaning (Get-Text 'Value.Network.Setting.ProxyEnable')) ` -Value $value -Meaning $meaning -Source $source $server = "$(Get-DataProperty $user 'ProxyServer')".Trim() $value = $notSet $meaning = '' if ($server) { $value = $server $meaning = Get-Text 'Value.Network.Effect.ProxyServerUnused' if ($on) { $meaning = Get-Text 'Value.Network.Effect.ProxyServerUsed' } } $rows += New-NetworkSettingRow -Area $area -Setting (Format-CodeWithMeaning -Code 'ProxyServer' -Meaning (Get-Text 'Value.Network.Setting.ProxyServer')) ` -Value $value -Meaning $meaning -Source $source $override = "$(Get-DataProperty $user 'ProxyOverride')".Trim() $value = $notSet $meaning = '' if ($override) { $value = $override; $meaning = Get-Text 'Value.Network.Effect.ProxyOverride' } $rows += New-NetworkSettingRow -Area $area -Setting (Format-CodeWithMeaning -Code 'ProxyOverride' -Meaning (Get-Text 'Value.Network.Setting.ProxyOverride')) ` -Value $value -Meaning $meaning -Source $source $pac = "$(Get-DataProperty $user 'AutoConfigURL')".Trim() $value = $notSet $meaning = '' if ($pac) { $value = $pac $meaning = Get-Text 'Value.Network.Effect.ProxyScript' # What the Judge fails. See New-ProxyFinding. if (Test-ProxyScriptFromFile -Url $pac) { $meaning = Get-Text 'Meaning.Network.ProxyScriptFromFile' } } $rows += New-NetworkSettingRow -Area $area -Setting (Format-CodeWithMeaning -Code 'AutoConfigURL' -Meaning (Get-Text 'Value.Network.Setting.AutoConfigUrl')) ` -Value $value -Meaning $meaning -Source $source } foreach ($view in (Get-WinHttpProxyView -Data $Data)) { $meaning = Get-Text 'Value.Network.Effect.WinHttpProxy' if ($null -eq $view.Parsed) { $meaning = Get-Text 'Value.Network.Effect.WinHttpAbsent' } elseif (-not $view.Parsed.Recognised) { $meaning = Get-Text 'Value.Network.Effect.WinHttpUnreadable' } elseif ($view.Parsed.Direct) { $meaning = Get-Text 'Value.Network.Effect.WinHttpDirect' } elseif (Test-WinHttpProxyDiffers -UserProxy $user -Parsed $view.Parsed) { $meaning = Get-Text 'Meaning.Network.WinHttpProxyDiffers' } $rows += New-NetworkSettingRow -Area $view.Area -Setting $result -Value (Get-WinHttpProxyDescription $view.Parsed) ` -Meaning $meaning -Source $view.Source $printed = @("$($view.Text)" -split '\r?\n' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) -join ' / ' if ($printed) { $rows += New-NetworkSettingRow -Area $view.Area -Setting (Get-Text 'Value.Network.Setting.NetshOutput') -Value $printed ` -Meaning (Get-Text 'Value.Network.Effect.NetshOutput') -Source $view.Source } } New-Section -Title (Get-Text 'Title.Network.ProxySettings') -Help (Get-Text 'Help.Network.ProxySettings') -Row $rows } function Get-TriStateText { # Yes, no, or not read: $null is the third answer and must not be shown as either. [CmdletBinding()] param([AllowNull()]$Value) if ($null -eq $Value) { return (Get-Text 'Value.Network.NotRead') } if ($Value) { return (Get-Text 'Value.Network.Yes') } Get-Text 'Value.Network.No' } function Format-SettingAnswer { <# .SYNOPSIS A setting's raw answer with what it means behind it, where it is a coded one: Enabled, Disabled, Unsupported, True, False. Anything else is shown as it is. Pure. .DESCRIPTION Enabled, Disabled and Unsupported are what Get-NetAdapterPowerManagement answers; True and False what Get-SmbClientConfiguration does. The answer stays, because it is what the cmdlet prints when a Technician asks it again. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Value) $meaning = switch ("$Value") { 'Enabled' { Get-Text 'Value.Network.Section.On' } 'Disabled' { Get-Text 'Value.Network.Section.Off' } 'Unsupported' { Get-Text 'Value.Network.Section.Unsupported' } 'True' { Get-Text 'Value.Network.Yes' } 'False' { Get-Text 'Value.Network.No' } default { '' } } if (-not "$Value".Trim()) { return "$Value" } Format-CodeWithMeaning -Code "$Value" -Meaning $meaning } function Get-SmbMappingStatusText { <# .SYNOPSIS What the status of a mapped drive means. Nothing for one not known here. Pure. .DESCRIPTION The seven values of Status that Microsoft documents for MSFT_SmbMapping, the class behind Get-SmbMapping. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyString()][string]$Status) switch ($Status) { 'OK' { return (Get-Text 'Value.Network.MappingState.Ok') } 'Paused' { return (Get-Text 'Value.Network.MappingState.Paused') } 'Disconnected' { return (Get-Text 'Value.Network.MappingState.Disconnected') } 'NetworkError' { return (Get-Text 'Value.Network.MappingState.NetworkError') } 'Connecting' { return (Get-Text 'Value.Network.MappingState.Connecting') } 'Reconnecting' { return (Get-Text 'Value.Network.MappingState.Reconnecting') } 'Unavailable' { return (Get-Text 'Value.Network.MappingState.Unavailable') } } '' } function New-AdapterSection { <# .SYNOPSIS The adapters as a table, and as a second one the settings each answer was reduced from, USB selective suspend and the SMB client configuration: a row for each, with what it is and what its value does. #> [CmdletBinding()] param([AllowNull()]$Data) $adapters = (Get-DataCollection $Data 'Adapters') # The headings are German like the rest of the Report, so the rows are built with the # names the table is to show. $name = Get-Text 'Column.Network.Adapter.Name' $description = Get-Text 'Column.Network.Adapter.Description' $linkSpeed = Get-Text 'Column.Network.Adapter.LinkSpeed' $driverVersion = Get-Text 'Column.Network.Adapter.DriverVersion' $driverDate = Get-Text 'Column.Network.Adapter.DriverDate' $usb = Get-Text 'Column.Network.Adapter.Usb' $powerSaving = Get-Text 'Column.Network.Adapter.PowerSaving' New-Section -Title (Get-Text 'Title.Network.Adapters') -Help (Get-Text 'Help.Network.Adapters') -Row ($adapters | ForEach-Object { $row = [ordered]@{} $row[$name] = Get-DataProperty $_ 'Name' $row[$description] = Get-DataProperty $_ 'Description' $row[$linkSpeed] = Get-DataProperty $_ 'LinkSpeed' $row[$driverVersion] = Get-DataProperty $_ 'DriverVersion' $row[$driverDate] = Get-DataProperty $_ 'DriverDate' $row[$usb] = Get-TriStateText (Get-DataProperty $_ 'IsUsb') $row[$powerSaving] = Get-TriStateText (Get-DataProperty $_ 'PowerSavingOn') [pscustomobject]$row }) $rows = @( foreach ($adapter in $adapters) { ConvertTo-AdapterSettingRow -Adapter $adapter -Data $Data } ConvertTo-UsbSuspendRow -Data $Data ConvertTo-SmbClientRow -Data $Data ) New-Section -Title (Get-Text 'Title.Network.PowerSavingAndSmb') -Help (Get-Text 'Help.Network.PowerSavingAndSmb') -Row $rows } function Get-NetworkAbsentText { <# .SYNOPSIS What the table says of a setting that has no value, and why: not read, or nothing to read. Pure. .DESCRIPTION Not read where the source is named in Unread - the Finding about the settings this Run could not read names the same ones - and where the data does not carry the property at all, which is data from a module that did not gather it yet. Anything else was asked and has no answer for this adapter: a virtual adapter has no power management and no tick box in Device Manager. .PARAMETER Present Whether the data carries the property. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data, [Parameter(Mandatory)][string]$Source, [bool]$Present = $true) if (-not $Present -or (Get-DataCollection $Data 'Unread') -contains $Source) { return [pscustomobject]@{ Value = (Get-Text 'Value.Cell.NotRead'); Meaning = (Get-Text 'Value.Network.Effect.NotRead') } } [pscustomobject]@{ Value = (Get-Text 'Value.Network.Setting.NoAnswer'); Meaning = (Get-Text 'Value.Network.Effect.NoAnswer') } } function Get-PowerManagementSettingText { # What an answer of Get-NetAdapterPowerManagement is about, in a Technician's words. [CmdletBinding()] [OutputType([string])] param([AllowEmptyString()][string]$Name) switch ($Name) { 'AllowComputerToTurnOffDevice' { return (Get-Text 'Value.Network.Setting.AllowComputerToTurnOffDevice') } 'SelectiveSuspend' { return (Get-Text 'Value.Network.Setting.SelectiveSuspend') } 'DeviceSleepOnDisconnect' { return (Get-Text 'Value.Network.Setting.DeviceSleepOnDisconnect') } 'WakeOnMagicPacket' { return (Get-Text 'Value.Network.Setting.WakeOnMagicPacket') } 'WakeOnPattern' { return (Get-Text 'Value.Network.Setting.WakeOnPattern') } 'ArpOffload' { return (Get-Text 'Value.Network.Setting.ArpOffload') } 'NSOffload' { return (Get-Text 'Value.Network.Setting.NSOffload') } 'D0PacketCoalescing' { return (Get-Text 'Value.Network.Setting.D0PacketCoalescing') } 'RsnRekeyOffload' { return (Get-Text 'Value.Network.Setting.RsnRekeyOffload') } } '' } function Get-PowerManagementEffectText { <# .SYNOPSIS What an answer of Get-NetAdapterPowerManagement does, and whether it is counted as power saving. Pure. .DESCRIPTION Counted are the ones Get-AdapterPowerSaving counts, by the same list. The others concern waking the machine and what the adapter does while it sleeps. #> [CmdletBinding()] [OutputType([string])] param([AllowEmptyString()][string]$Name, [AllowEmptyString()][string]$Answer) if ($Answer -eq 'Unsupported') { return (Get-Text 'Value.Network.Effect.Unsupported') } if ($Answer -ne 'Enabled' -and $Answer -ne 'Disabled') { return '' } if ($script:NetworkPowerManagementSavers -contains $Name) { if ($Answer -eq 'Disabled') { return (Get-Text 'Value.Network.Effect.SaverOff') } switch ($Name) { 'AllowComputerToTurnOffDevice' { return (Get-Text 'Value.Network.Effect.TurnOffOn') } 'SelectiveSuspend' { return (Get-Text 'Value.Network.Effect.SelectiveSuspendOn') } 'DeviceSleepOnDisconnect' { return (Get-Text 'Value.Network.Effect.SleepOnDisconnectOn') } } } if ($Name -eq 'D0PacketCoalescing') { return (Get-Text 'Value.Network.Effect.Coalescing') } Get-Text 'Value.Network.Effect.WakeOnly' } function ConvertTo-AdapterSettingRow { <# .SYNOPSIS The rows of one adapter: the answer it was reduced to, and every setting that answer was reduced from, each with what it does and whether it was counted. Pure. .DESCRIPTION What is a cmdlet, a WMI class or a registry keyword stays what a Technician types and searches for, and stands first; what it is, is German and stands behind it. Whether a setting was counted is asked of what Get-AdapterPowerSaving asks: the list of savers, Test-AdapterSettingSwitch and the keywords the Gatherer was given. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Adapter, [AllowNull()]$Data) $area = (Get-Text 'Value.Network.Area.Adapter') -f (Get-DataProperty $Adapter 'Name') $savingPattern = "$(Get-DataProperty $Data 'PowerSavingKeywordPattern')" if (-not $savingPattern) { $savingPattern = $script:NetworkPowerSavingPattern } # The answer first: it is the one the table of the adapters shows. $saving = Get-DataProperty $Adapter 'PowerSavingOn' $meaning = Get-Text 'Value.Network.Effect.SavingUnknown' if ($saving -eq $true) { $meaning = Get-Text 'Value.Network.Effect.SavingOn' } elseif ($saving -eq $false) { $meaning = Get-Text 'Value.Network.Effect.SavingOff' } New-NetworkSettingRow -Area $area -Setting (Get-Text 'Value.Network.Setting.Saving') -Value (Get-TriStateText $saving) -Meaning $meaning ` -Source (Get-Text 'Value.Network.Source.Derived') # A USB adapter is one by its PnP device id, which is kept beside the answer. $pnp = "$(Get-DataProperty $Adapter 'PnpDeviceId')" $value = $pnp $meaning = Get-Text 'Value.Network.Effect.NotUsb' if (Get-DataProperty $Adapter 'IsUsb') { $meaning = Get-Text 'Value.Network.Effect.Usb' } if (-not $pnp) { $absent = Get-NetworkAbsentText -Data $Data -Source 'Get-NetAdapter' -Present (Test-NetworkDataPropertyPresent $Adapter 'PnpDeviceId') $value = $absent.Value $meaning = $absent.Meaning } New-NetworkSettingRow -Area $area -Setting (Format-CodeWithMeaning -Code 'PnPDeviceID' -Meaning (Get-Text 'Value.Network.Setting.PnpDeviceId')) ` -Value $value -Meaning $meaning -Source 'Get-NetAdapter' $source = 'Get-NetAdapterPowerManagement' $management = Get-DataProperty $Adapter 'PowerManagement' if ($null -eq $management) { $absent = Get-NetworkAbsentText -Data $Data -Source $source -Present (Test-NetworkDataPropertyPresent $Adapter 'PowerManagement') New-NetworkSettingRow -Area $area -Setting (Format-CodeWithMeaning -Code $source -Meaning (Get-Text 'Value.Network.Setting.PowerManagement')) ` -Value $absent.Value -Meaning $absent.Meaning -Source $source } else { foreach ($name in $script:NetworkPowerManagementProperties) { $answer = "$(Get-DataProperty $management $name)".Trim() $value = Get-Text 'Value.Network.Setting.NoAnswer' if ($answer) { $value = Format-SettingAnswer $answer } New-NetworkSettingRow -Area $area -Setting (Format-CodeWithMeaning -Code $name -Meaning (Get-PowerManagementSettingText -Name $name)) ` -Value $value -Meaning (Get-PowerManagementEffectText -Name $name -Answer $answer) -Source $source } } # The tick box of Device Manager, which answers without admin rights. $source = 'MSPower_DeviceEnable (root\wmi)' $allowed = Get-DataProperty $Adapter 'DeviceSleepAllowed' if ($allowed -is [bool]) { $value = Format-SettingAnswer "$allowed" $meaning = Get-Text 'Value.Network.Effect.SaverOff' if ($allowed) { $meaning = Get-Text 'Value.Network.Effect.TurnOffOn' } } else { $absent = Get-NetworkAbsentText -Data $Data -Source 'MSPower_DeviceEnable' -Present (Test-NetworkDataPropertyPresent $Adapter 'DeviceSleepAllowed') $value = $absent.Value $meaning = $absent.Meaning } New-NetworkSettingRow -Area $area -Setting (Format-CodeWithMeaning -Code 'MSPower_DeviceEnable' -Meaning (Get-Text 'Value.Network.Setting.DeviceEnable')) ` -Value $value -Meaning $meaning -Source $source # Every setting of the driver, the ones that are not counted as power saving included, # with the values the driver says it can take: that list is why a mode is not counted. foreach ($setting in (Get-DataCollection $Adapter 'EnergySettings')) { $keyword = "$(Get-DataProperty $setting 'Keyword')" $display = "$(Get-DataProperty $setting 'DisplayName')" $registry = "$(Get-DataProperty $setting 'RegistryValue')" $shown = "$(Get-DataProperty $setting 'Value')" $name = $display if ($keyword) { $name = Format-CodeWithMeaning -Code $keyword -Meaning $display } $value = $shown if ($registry.Trim()) { $value = Format-CodeWithMeaning -Code $registry -Meaning $shown } $meaning = Get-Text 'Value.Network.Effect.DriverNotCounted' if ($keyword -match $savingPattern) { $number = ConvertTo-Number $registry if (-not (Test-AdapterSettingSwitch -Setting $setting)) { $meaning = Get-Text 'Value.Network.Effect.DriverMode' } elseif ($number -eq 1) { $meaning = Get-Text 'Value.Network.Effect.DriverSaverOn' } elseif ($number -eq 0) { $meaning = Get-Text 'Value.Network.Effect.DriverSaverOff' } } $valid = (Get-DataCollection $setting 'ValidRegistryValues') if ($valid.Count) { $meaning = '{0} ({1})' -f $meaning, ((Get-Text 'Value.Network.Section.ValidValues') -f ($valid -join ', ')) } New-NetworkSettingRow -Area $area -Setting $name -Value $value -Meaning $meaning -Source 'Get-NetAdapterAdvancedProperty' } } function ConvertTo-UsbSuspendRow { <# .SYNOPSIS USB selective suspend in the active power plan, as rows: the plan, and the setting on mains and on battery. Pure. .DESCRIPTION The two indexes are read out of what powercfg printed by the function the Gatherer reads them with. Where that text is not there or does not hold both - data from a fixture, or a powercfg that would not say - the one answer the Gatherer stored is shown, and where there is none either, that it was not read. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $area = Get-Text 'Value.Network.Area.Usb' $source = 'powercfg /q SCHEME_CURRENT {0} {1}' -f $script:NetworkUsbPowerSubgroup, $script:NetworkUsbSuspendSetting $on = Get-Text 'Value.Network.Section.On' $off = Get-Text 'Value.Network.Section.Off' $text = "$(Get-DataProperty $Data 'UsbSelectiveSuspendText')" $index = ConvertFrom-PowercfgIndexText -Text $text # The plan is the first line that names a GUID with a name in brackets behind it: # powercfg prints the scheme before the subgroup and the setting. $plan = [regex]::Match($text, '(?m)^[^:\r\n]+:\s*([0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})\s+\((.+?)\)\s*$') if ($index.Recognised -and $plan.Success) { New-NetworkSettingRow -Area $area -Setting (Get-Text 'Value.Network.Setting.PowerPlan') ` -Value (Format-CodeWithMeaning -Code $plan.Groups[1].Value -Meaning $plan.Groups[2].Value) ` -Meaning (Get-Text 'Value.Network.Effect.PowerPlan') -Source 'powercfg /q SCHEME_CURRENT' } if ($index.Recognised) { foreach ($supply in @(@((Get-Text 'Value.Network.Setting.UsbSuspendAc'), $index.AcIndex), @((Get-Text 'Value.Network.Setting.UsbSuspendDc'), $index.DcIndex))) { $position = $off $meaning = Get-Text 'Value.Network.Effect.UsbSuspendOff' if ($supply[1] -ne 0) { $position = $on; $meaning = Get-Text 'Value.Network.Effect.UsbSuspendOn' } New-NetworkSettingRow -Area $area -Setting $supply[0] ` -Value (Format-CodeWithMeaning -Code ('0x{0:x8}' -f $supply[1]) -Meaning $position) -Meaning $meaning -Source $source } return } $suspend = Get-DataProperty $Data 'UsbSelectiveSuspend' if ($null -eq $suspend) { $absent = Get-NetworkAbsentText -Data $Data -Source 'powercfg' -Present $false New-NetworkSettingRow -Area $area -Setting (Get-Text 'Value.Network.Setting.UsbSuspend') -Value $absent.Value -Meaning $absent.Meaning -Source $source return } $position = $off $meaning = Get-Text 'Value.Network.Effect.UsbSuspendOff' if ($suspend) { $position = $on; $meaning = Get-Text 'Value.Network.Effect.UsbSuspendOn' } New-NetworkSettingRow -Area $area -Setting (Get-Text 'Value.Network.Setting.UsbSuspend') -Value $position -Meaning $meaning -Source $source } function Get-SmbClientSettingText { # What a value of the SMB client configuration is about, in a Technician's words. [CmdletBinding()] [OutputType([string])] param([AllowEmptyString()][string]$Name) switch ($Name) { 'SessionTimeout' { return (Get-Text 'Value.Network.Setting.SessionTimeout') } 'FileInfoCacheLifetime' { return (Get-Text 'Value.Network.Setting.FileInfoCacheLifetime') } 'DirectoryCacheLifetime' { return (Get-Text 'Value.Network.Setting.DirectoryCacheLifetime') } 'OplocksDisabled' { return (Get-Text 'Value.Network.Setting.OplocksDisabled') } 'EnableBandwidthThrottling' { return (Get-Text 'Value.Network.Setting.EnableBandwidthThrottling') } 'EnableMultiChannel' { return (Get-Text 'Value.Network.Setting.EnableMultiChannel') } } '' } function Get-SmbClientEffectText { <# .SYNOPSIS What a value of the SMB client configuration does. Pure. .DESCRIPTION The three lifetimes are seconds, and what they do does not depend on the number but for 0, which switches a cache off. The three switches do one thing when they are True and another when they are False. #> [CmdletBinding()] [OutputType([string])] param([AllowEmptyString()][string]$Name, [AllowNull()]$Value) $answer = "$Value".Trim() if (-not $answer) { return '' } $number = ConvertTo-Number $answer switch ($Name) { 'SessionTimeout' { return (Get-Text 'Value.Network.Effect.SessionTimeout') } 'FileInfoCacheLifetime' { if ($number -eq 0) { return (Get-Text 'Value.Network.Effect.CacheOff') } return (Get-Text 'Value.Network.Effect.FileInfoCache') } 'DirectoryCacheLifetime' { if ($number -eq 0) { return (Get-Text 'Value.Network.Effect.CacheOff') } return (Get-Text 'Value.Network.Effect.DirectoryCache') } 'OplocksDisabled' { if ($answer -eq 'True') { return (Get-Text 'Value.Network.Effect.OplocksOff') } if ($answer -eq 'False') { return (Get-Text 'Value.Network.Effect.OplocksOn') } } 'EnableBandwidthThrottling' { if ($answer -eq 'True') { return (Get-Text 'Value.Network.Effect.ThrottlingOn') } if ($answer -eq 'False') { return (Get-Text 'Value.Network.Effect.ThrottlingOff') } } 'EnableMultiChannel' { if ($answer -eq 'True') { return (Get-Text 'Value.Network.Effect.MultiChannelOn') } if ($answer -eq 'False') { return (Get-Text 'Value.Network.Effect.MultiChannelOff') } } } '' } function ConvertTo-SmbClientRow { <# .SYNOPSIS The SMB client configuration as rows, each with what it is and what its value does. Pure. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $area = Get-Text 'Value.Network.Area.SmbClient' $source = 'Get-SmbClientConfiguration' $smb = Get-DataProperty $Data 'SmbClientConfiguration' if ($null -eq $smb) { $absent = Get-NetworkAbsentText -Data $Data -Source $source -Present $false New-NetworkSettingRow -Area $area -Setting (Format-CodeWithMeaning -Code $source -Meaning (Get-Text 'Value.Network.Setting.SmbClient')) ` -Value $absent.Value -Meaning $absent.Meaning -Source $source return } foreach ($name in $script:NetworkSmbClientProperties) { $answer = Get-DataProperty $smb $name $value = Get-Text 'Value.Network.Setting.NoAnswer' if ("$answer".Trim()) { # The switches are said as yes or no beside their answer; the three lifetimes # are seconds, and a number with its unit needs nothing beside it. if ($null -ne (ConvertTo-Number $answer)) { $value = (Get-Text 'Value.Network.Setting.Seconds') -f $answer } else { $value = Format-SettingAnswer $answer } } New-NetworkSettingRow -Area $area -Setting (Format-CodeWithMeaning -Code $name -Meaning (Get-SmbClientSettingText -Name $name)) ` -Value $value -Meaning (Get-SmbClientEffectText -Name $name -Value $answer) -Source $source } } function ConvertFrom-WinHttpProxyText { <# .SYNOPSIS Parses what "netsh winhttp show proxy" printed into direct, proxy server and bypass list. Pure, and reaches nothing. .DESCRIPTION netsh prints its labels in the UI language, so this reads by structure and never by label. The settings are the indented lines; the heading and every error message are not. A proxy is two "label : value" lines, server first and bypass list second. Direct is one indented line with no colon, which in German reads "DirectAccess (kein Proxyserver)." - not the "Direkter Zugriff" one might guess - and in English "Direct access (no proxy server).", so "direct" is the one token both share. Anything else is not recognised, because an error read as direct would be a false all-clear. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyString()][string]$Text) $body = @("$Text" -split '\r?\n' | Where-Object { $_ -match '^\s{2,}\S' } | ForEach-Object { $_.Trim() }) # " : " with the spaces, because a proxy value carries colons of its own (host:port). $values = @($body | Where-Object { $_ -match '^[^:]+?\s:\s' } | ForEach-Object { ($_ -split '\s:\s', 2)[1].Trim() }) $server = $null $bypass = $null $direct = $false if ($values.Count -ge 1 -and $values[0]) { $server = $values[0] # "(keine)" / "(none)": the placeholder for an empty list, recognised by its shape. if ($values.Count -ge 2 -and $values[1] -notmatch '^\(.*\)$') { $bypass = $values[1] } } elseif (@($body | Where-Object { $_ -notmatch ':' -and $_ -match '(?i)direct' }).Count) { $direct = $true } [pscustomobject]@{ PSTypeName = 'Gutcheck.WinHttpProxy' Recognised = $direct -or [bool]$server Direct = $direct ProxyServer = $server BypassList = $bypass } } function Get-UserProxyDescription { [CmdletBinding()] param([AllowNull()]$UserProxy) if ($null -eq $UserProxy) { return (Get-Text 'Value.Network.ProxyUnreadable') } $parts = @() $pac = "$(Get-DataProperty $UserProxy 'AutoConfigURL')".Trim() if ($pac) { $parts += (Get-Text 'Value.Network.ProxyScript') -f $pac } $server = Get-UserProxyServer $UserProxy if ($server) { $override = "$(Get-DataProperty $UserProxy 'ProxyOverride')".Trim() if ($override) { $parts += (Get-Text 'Value.Network.ProxyServerWithBypass') -f $server, $override } else { $parts += (Get-Text 'Value.Network.ProxyServer') -f $server } } if (-not $parts.Count) { return (Get-Text 'Value.Network.ProxyDirect') } $parts -join '; ' } function Get-UserProxyServer { # The server only while it is switched on: Internet Options keeps the last one typed in # after the box is unticked, and that one is used by nothing. [CmdletBinding()] param([AllowNull()]$UserProxy) if ((ConvertTo-Number (Get-DataProperty $UserProxy 'ProxyEnable')) -ne 1) { return $null } $server = "$(Get-DataProperty $UserProxy 'ProxyServer')".Trim() if ($server) { return $server } $null } function Get-WinHttpProxyDescription { [CmdletBinding()] param([AllowNull()]$Parsed) if ($null -eq $Parsed) { return (Get-Text 'Value.Network.ProxyViewAbsent') } if (-not $Parsed.Recognised) { return (Get-Text 'Value.Network.ProxyUnreadable') } if ($Parsed.Direct) { return (Get-Text 'Value.Network.ProxyDirect') } if ($Parsed.BypassList) { return (Get-Text 'Value.Network.ProxyServerWithBypass') -f $Parsed.ProxyServer, $Parsed.BypassList } (Get-Text 'Value.Network.ProxyServer') -f $Parsed.ProxyServer } function Get-WinHttpProxyView { <# .SYNOPSIS The two views of the system proxy, each with what was read of it: the Check it is judged under, what the Section calls it, how it was asked and what the answer parses to. Pure. .DESCRIPTION The 32-bit view is $null only on a Windows without SysWOW64; the 64-bit one always exists, so its absence is data gathered before the proxy was, and reads unreadable. Parsed is $null for a view that is not there, and nothing else. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $views = @( [pscustomobject]@{ Check = (Get-Text 'Check.Network.WinHttpProxy64'); Area = (Get-Text 'Value.Network.Area.WinHttp64') Source = 'netsh winhttp show proxy' Text = (Get-DataProperty $Data 'WinHttpProxy64Text'); Optional = $false } [pscustomobject]@{ Check = (Get-Text 'Check.Network.WinHttpProxy32'); Area = (Get-Text 'Value.Network.Area.WinHttp32') Source = '%windir%\SysWOW64\netsh.exe winhttp show proxy' Text = (Get-DataProperty $Data 'WinHttpProxy32Text'); Optional = $true } ) foreach ($view in $views) { $parsed = $null if ($null -ne $view.Text -or -not $view.Optional) { $parsed = ConvertFrom-WinHttpProxyText -Text $view.Text } $view | Add-Member -NotePropertyName Parsed -NotePropertyValue $parsed } $views } function Test-ProxyScriptFromFile { # KB 2332495: WinHTTP cannot load a PAC file from a file:// URL, so Outlook cannot either. [CmdletBinding()] [OutputType([bool])] param([AllowNull()][AllowEmptyString()][string]$Url) "$Url".Trim() -match '^(?i)file:' } function Test-WinHttpProxyDiffers { <# .SYNOPSIS Whether a view of the system proxy names a proxy the user does not have. Pure. .DESCRIPTION KB 2847833: a WinHTTP proxy that is not the user's breaks free/busy, out-of-office and MailTips. Direct WinHTTP does not differ, whatever the user has: the research grades it OK beside a PAC, and it is exactly what that KB's own fix, "netsh winhttp reset proxy", leaves behind beside a fixed proxy. Nothing differs where there is nothing to compare: a view that is absent or was not recognised, and a user whose settings could not be read. #> [CmdletBinding()] [OutputType([bool])] param([AllowNull()]$UserProxy, [AllowNull()]$Parsed) if ($null -eq $Parsed -or -not $Parsed.Recognised) { return $false } if ($null -eq $UserProxy -or $Parsed.Direct) { return $false } $userServer = Get-UserProxyServer $UserProxy if ($userServer -and $userServer -eq "$($Parsed.ProxyServer)".Trim()) { return $false } $true } function New-ProxyFinding { <# .SYNOPSIS Judges the user's proxy (WinINET) against the system proxy (WinHTTP) Office uses for its background requests. See docs/research/outlook-connectivity-checks.md, 6. #> [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $user = Get-DataProperty $Data 'UserProxy' $views = @(Get-WinHttpProxyView -Data $Data) # The effective configuration, always: a Technician asked about a proxy needs the # answer even when nothing is wrong with it. $hint = '' if ($null -eq $user) { $hint = (Get-Text 'Hint.Network.UserProxyUnreadable') } elseif (Get-DataProperty $Data 'Elevated') { $hint = (Get-Text 'Hint.Network.ProxyReadInElevatedSession') } New-Finding -Category Network -Check (Get-Text 'Check.Network.Proxy') -Severity INFO ` -Value ((Get-Text 'Value.Network.ProxySummary') -f (Get-UserProxyDescription $user), (Get-WinHttpProxyDescription $views[0].Parsed), (Get-WinHttpProxyDescription $views[1].Parsed)) ` -Hint $hint # KB 2332495: WinHTTP cannot load a PAC file from a file:// URL, so Outlook cannot either. $pac = "$(Get-DataProperty $user 'AutoConfigURL')".Trim() if (Test-ProxyScriptFromFile -Url $pac) { New-Finding -Category Network -Check (Get-Text 'Check.Network.ProxyScript') -Severity FAIL ` -Value $pac -Meaning (Get-Text 'Meaning.Network.ProxyScriptFromFile') ` -Hint (Get-Text 'Hint.Network.ProxyScriptFromFile') } foreach ($view in $views) { if ($null -eq $view.Parsed) { continue } if (-not $view.Parsed.Recognised) { New-UnavailableFinding -Category Network -Check $view.Check -Hint (Get-Text 'Hint.Network.WinHttpProxyUnreadable') continue } if (-not (Test-WinHttpProxyDiffers -UserProxy $user -Parsed $view.Parsed)) { continue } New-Finding -Category Network -Check $view.Check -Severity WARN ` -Value ((Get-Text 'Value.Network.WinHttpDiffers') -f $view.Parsed.ProxyServer, (Get-UserProxyDescription $user)) ` -Meaning (Get-Text 'Meaning.Network.WinHttpProxyDiffers') ` -Hint (Get-Text 'Hint.Network.WinHttpProxyDiffers') } } 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') } $value = '{0} @ {1}' -f $adapter.Description, $adapter.LinkSpeed if (Get-DataProperty $adapter 'IsUsb') { $value = (Get-Text 'Value.Network.AdapterUsb') -f $adapter.Description, $adapter.LinkSpeed } # Power saving that is on is how Windows sets up most adapters, and no fault by # itself: it is said, and declared, so that what reads Findings together can # take it for a sign of the connection beside a link that dropped. $signal = @() if ((Get-DataProperty $adapter 'PowerSavingOn') -eq $true) { $value = '{0} | {1}' -f $value, (Get-Text 'Value.Network.AdapterSaving') $signal = @('adapter-power-saving') } New-Finding -Category Network -Check ((Get-Text 'Check.Network.Adapter') -f $adapter.Name) -Severity INFO ` -Value $value -Hint $hint -Signal $signal } } function New-AdapterSettingFinding { <# .SYNOPSIS Names the settings this Run could not read, so that their absence from the Report is not taken for a machine without power saving. .DESCRIPTION Says nothing else. Whether a USB adapter with power saving matters depends on whether a program runs from a share, and this Check is performed before the one that finds that out; the NetworkDependency Kind weighs the two together. #> [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $unread = (Get-DataCollection $Data 'Unread') if (-not $unread.Count) { return } New-Finding -Category Network -Check (Get-Text 'Check.Network.AdapterSettings') -Severity INFO ` -Value ((Get-Text 'Value.Network.SettingsUnread') -f ($unread -join ', ')) ` -Hint (Get-Text 'Hint.Network.SettingsUnread') } 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) ` -Meaning (Get-Text 'Meaning.Network.WeakWiFi') ` -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') ` -Meaning (Get-Text 'Meaning.Network.NoDefaultGateway') ` -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) ` -Meaning (Get-Text 'Meaning.Network.UnstableLocalNetwork') ` -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 = Get-Text 'Check.Network.DnsResolution' $target if (-not (Get-DataProperty $Data 'DnsResolved')) { return New-Finding -Category Network -Check $check -Severity FAIL -Value (Get-Text 'Value.Network.Failed') ` -Meaning (Get-Text 'Meaning.Network.NoInternetNameResolution') ` -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) ` -Meaning (Get-Text 'Meaning.Network.SlowDns') ` -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, (Format-CodeWithMeaning -Code "$($mapping.Status)" -Meaning (Get-SmbMappingStatusText -Status "$($mapping.Status)")), $(if ($null -eq $connect) { (Get-Text 'Value.Network.Failed') } else { '{0} ms' -f $connect })) ` -Meaning (Get-Text 'Meaning.Network.MappedDriveNotConnected') ` -Hint (Get-Text 'Hint.Network.AppsStartedFromANetwork') } } |