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')
    }
}