Private/Kinds/System.ps1

# The System Kind: what the machine is.
#
# Two Checks here are about age rather than a reading, so the Gatherer records when it
# looked. The Judge computes the age from that rather than from the clock, which keeps it
# pure and lets a fixture pin an age exactly.

function Get-SystemData {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([hashtable]$Parameters = @{})

    $computer = Get-CimInstance Win32_ComputerSystem  -ErrorAction SilentlyContinue
    $os       = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
    $bios     = Get-CimInstance Win32_BIOS            -ErrorAction SilentlyContinue
    $cpu      = @(Get-CimInstance Win32_Processor     -ErrorAction SilentlyContinue)[0]
    $current  = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction SilentlyContinue
    $power    = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power' -ErrorAction SilentlyContinue

    $modules = @(Get-CimInstance Win32_PhysicalMemory -ErrorAction SilentlyContinue | ForEach-Object {
        [pscustomobject]@{ CapacityBytes = $_.Capacity; ConfiguredClockSpeed = $_.ConfiguredClockSpeed }
    })

    # What the box at the top of the Report shows beyond the above: the board, the
    # graphics adapters with their driver, the drives, and every driver that is not
    # Microsoft's - the ones a vendor's update tool would replace.
    # What ipconfig /all says, of every adapter that has an address: Windows keeps it in
    # the adapter's configuration, and the name a person knows it by ("Ethernet", "WLAN")
    # and its speed in the adapter itself.
    $adapters = @{}
    foreach ($adapter in @(Get-CimInstance Win32_NetworkAdapter -ErrorAction SilentlyContinue)) { $adapters[[string]$adapter.Index] = $adapter }
    $network = @(Get-CimInstance Win32_NetworkAdapterConfiguration -Filter 'IPEnabled = TRUE' -ErrorAction SilentlyContinue | ForEach-Object {
        $adapter = $adapters[[string]$_.Index]
        [pscustomobject]@{
            Alias        = $adapter.NetConnectionID
            Description  = $_.Description
            MacAddress   = $_.MACAddress
            SpeedBps     = $adapter.Speed
            Addresses    = @($_.IPAddress)
            Subnets      = @($_.IPSubnet)
            Gateways     = @($_.DefaultIPGateway)
            DnsServers   = @($_.DNSServerSearchOrder)
            DnsSuffix    = $_.DNSDomain
            DhcpEnabled  = $_.DHCPEnabled
            DhcpServer   = $_.DHCPServer
            LeaseObtained = $_.DHCPLeaseObtained
            LeaseExpires  = $_.DHCPLeaseExpires
        }
    })

    $board = @(Get-CimInstance Win32_BaseBoard -ErrorAction SilentlyContinue)[0]
    $graphics = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue | ForEach-Object {
        [pscustomobject]@{
            Name = $_.Name; DriverVersion = $_.DriverVersion; DriverDate = $_.DriverDate
            PnpDeviceId = "$($_.PNPDeviceID)"
            Horizontal = $_.CurrentHorizontalResolution; Vertical = $_.CurrentVerticalResolution
        }
    })
    $disks = @(Get-PhysicalDisk -ErrorAction SilentlyContinue | ForEach-Object {
        [pscustomobject]@{ Name = $_.FriendlyName; SizeBytes = $_.Size; MediaType = "$($_.MediaType)"; BusType = "$($_.BusType)" }
    })
    $drivers = @(Get-CimInstance Win32_PnPSignedDriver -ErrorAction SilentlyContinue |
        Where-Object { $_.DeviceName -and $_.DriverProviderName -and $_.DriverProviderName -ne 'Microsoft' } |
        ForEach-Object {
            [pscustomobject]@{
                Class = $_.DeviceClass; Device = $_.DeviceName; Provider = $_.DriverProviderName
                Version = $_.DriverVersion; Date = $_.DriverDate
            }
        })

    [pscustomobject]@{
        PSTypeName               = 'Gutcheck.Data.System'
        GatheredAt               = Get-Date
        Manufacturer             = $computer.Manufacturer
        Model                    = $computer.Model
        SerialNumber             = $bios.SerialNumber
        OsCaption                = $os.Caption
        OsDisplayVersion         = $current.DisplayVersion
        OsBuildNumber            = $os.BuildNumber
        OsUpdateBuildRevision    = $current.UBR
        BiosVersion              = $bios.SMBIOSBIOSVersion
        BiosReleaseDate          = $bios.ReleaseDate
        ProcessorName            = $cpu.Name
        ProcessorCores           = $cpu.NumberOfCores
        ProcessorLogicalCount    = $cpu.NumberOfLogicalProcessors
        TotalPhysicalMemoryBytes = $computer.TotalPhysicalMemory
        MemoryModules            = $modules
        LastBootUpTime           = $os.LastBootUpTime
        FastStartupEnabled       = $power.HiberbootEnabled
        OsArchitecture           = $os.OSArchitecture
        OsInstallDate            = $os.InstallDate
        Domain                   = $computer.Domain
        PartOfDomain             = $computer.PartOfDomain
        BaseBoard                = ('{0} {1}' -f $board.Manufacturer, $board.Product).Trim()
        Graphics                 = $graphics
        Disks                    = $disks
        Drivers                  = $drivers
        HostName                 = $computer.DNSHostName
        NetworkAdapters          = $network
    }
}

function ConvertTo-SystemPrefixLength {
    <#
    .SYNOPSIS
        A subnet as the number after the slash: "255.255.255.0" is 24, and "64" is 64. Pure.
    .DESCRIPTION
        Windows gives an IPv4 subnet as a mask and an IPv6 one as the length already.
        Nothing for what is neither, and nothing for a mask that is not one.
    #>

    [CmdletBinding()]
    [OutputType([int])]
    param([AllowNull()][AllowEmptyString()][string]$Subnet)

    $text = "$Subnet".Trim()
    if ($text -match '^\d{1,3}$') { return [int]$text }
    if ($text -notmatch '^\d{1,3}(\.\d{1,3}){3}$') { return }
    $bits = ''
    foreach ($part in ($text -split '\.')) {
        if ([int]$part -gt 255) { return }
        $bits += [Convert]::ToString([int]$part, 2).PadLeft(8, '0')
    }
    if ($bits -notmatch '^1*0*$') { return }
    ($bits -replace '0', '').Length
}

function Get-SystemNetworkAddress {
    <#
    .SYNOPSIS
        An adapter's addresses as "address/length", IPv4 or IPv6 as asked. Pure.
    .DESCRIPTION
        A link-local IPv6 address (fe80::) is left out: every adapter has one, and it
        says nothing about where the machine is.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Adapter, [ValidateSet('IPv4', 'IPv6')][string]$Family = 'IPv4')

    $addresses = Get-DataCollection $Adapter 'Addresses'
    $subnets   = Get-DataCollection $Adapter 'Subnets'
    for ($i = 0; $i -lt $addresses.Count; $i++) {
        $address = "$($addresses[$i])".Trim()
        if (-not $address) { continue }
        $isV6 = $address.Contains(':')
        if ($isV6 -ne ($Family -eq 'IPv6')) { continue }
        if ($isV6 -and $address -match '^(?i)fe80:') { continue }
        $length = $null
        if ($i -lt $subnets.Count) { $length = ConvertTo-SystemPrefixLength -Subnet "$($subnets[$i])" }
        if ($null -ne $length) { '{0}/{1}' -f $address, $length } else { $address }
    }
}

function Format-SystemLinkSpeed {
    <#
    .SYNOPSIS
        A link speed as people say it: 1000000000 is "1 Gbit/s". Pure; nothing for no speed.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$BitsPerSecond)

    $speed = ConvertTo-Number $BitsPerSecond
    # An adapter without a link reports the largest number there is.
    if ($null -eq $speed -or $speed -le 0 -or $speed -gt 1e12) { return '' }
    if ($speed -ge 1e9) { return '{0:0.##} Gbit/s' -f ($speed / 1e9) }
    '{0:0.##} Mbit/s' -f ($speed / 1e6)
}

function Format-SystemDate {
    <#
    .SYNOPSIS
        A date as the box shows it, or nothing for what is no date. Pure.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Value, [string]$Format = 'dd.MM.yyyy')

    if ($null -eq $Value -or "$Value" -eq '') { return '' }
    try { ([datetime]$Value).ToString($Format) } catch { '' }
}

function Get-SystemInformationRow {
    <#
    .SYNOPSIS
        What the box at the top of the Report says about the machine: Group, Name, Value,
        in the order they are shown. Pure.
    .DESCRIPTION
        Only what the machine reported. A value it did not report is a row left out, not
        a row that says "unknown": the box is for reading at a glance, and the Findings
        already say what could not be read.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()]$Data)

    $rows = New-Object 'System.Collections.Generic.List[psobject]'
    $add = {
        param([string]$Group, [string]$Name, $Value)
        $text = "$Value".Trim()
        if ($text) { $rows.Add([pscustomobject]@{ Group = $Group; Name = $Name; Value = $text }) }
    }

    $device = Get-Text 'System.Info.Group.Device'
    & $add $device (Get-Text 'System.Info.Manufacturer') (Get-DataProperty $Data 'Manufacturer')
    & $add $device (Get-Text 'System.Info.Model')        (Get-DataProperty $Data 'Model')
    & $add $device (Get-Text 'System.Info.Serial')       (Get-DataProperty $Data 'SerialNumber')
    & $add $device (Get-Text 'System.Info.Board')        (Get-DataProperty $Data 'BaseBoard')
    $bios = "$(Get-DataProperty $Data 'BiosVersion')".Trim()
    $biosDate = Format-SystemDate (Get-DataProperty $Data 'BiosReleaseDate')
    if ($bios -and $biosDate) { $bios = (Get-Text 'System.Info.VersionOf') -f $bios, $biosDate }
    & $add $device (Get-Text 'System.Info.Bios') $bios

    $windows = Get-Text 'System.Info.Group.Windows'
    & $add $windows (Get-Text 'System.Info.Edition') (Get-DataProperty $Data 'OsCaption')
    & $add $windows (Get-Text 'System.Info.Version') (Get-DataProperty $Data 'OsDisplayVersion')
    $build = "$(Get-DataProperty $Data 'OsBuildNumber')"
    $revision = "$(Get-DataProperty $Data 'OsUpdateBuildRevision')"
    if ($build -and $revision) { $build = '{0}.{1}' -f $build, $revision }
    & $add $windows (Get-Text 'System.Info.Build')        $build
    & $add $windows (Get-Text 'System.Info.Architecture') (Get-DataProperty $Data 'OsArchitecture')
    & $add $windows (Get-Text 'System.Info.InstallDate')  (Format-SystemDate (Get-DataProperty $Data 'OsInstallDate'))
    & $add $windows (Get-Text 'System.Info.LastBoot')     (Format-SystemDate (Get-DataProperty $Data 'LastBootUpTime') 'dd.MM.yyyy HH:mm')
    $domainLabel = Get-Text 'System.Info.Workgroup'
    if ([bool](Get-DataProperty $Data 'PartOfDomain')) { $domainLabel = Get-Text 'System.Info.Domain' }
    & $add $windows $domainLabel (Get-DataProperty $Data 'Domain')

    $hardware = Get-Text 'System.Info.Group.Hardware'
    $cpu = "$(Get-DataProperty $Data 'ProcessorName')".Trim()
    if ($cpu) {
        & $add $hardware (Get-Text 'System.Info.Processor') ((Get-Text 'Value.System.Processor') -f
            $cpu, (Get-DataProperty $Data 'ProcessorCores'), (Get-DataProperty $Data 'ProcessorLogicalCount'))
    }
    $bytes = ConvertTo-Number (Get-DataProperty $Data 'TotalPhysicalMemoryBytes')
    if ($null -ne $bytes -and $bytes -gt 0) {
        $memory = '{0} GB' -f [math]::Round($bytes / 1GB, 1)
        $modules = Get-DataCollection $Data 'MemoryModules'
        if ($modules.Count) {
            $sizes = @($modules | ForEach-Object { ConvertTo-Number (Get-DataProperty $_ 'CapacityBytes') } | Where-Object { $_ } |
                ForEach-Object { '{0} GB' -f [math]::Round($_ / 1GB) })
            $memory = (Get-Text 'System.Info.MemoryModules') -f $memory, $modules.Count, ($sizes -join ' + ')
        }
        & $add $hardware (Get-Text 'System.Info.Memory') $memory
    }
    foreach ($adapter in (Get-DataCollection $Data 'Graphics')) {
        $name = "$(Get-DataProperty $adapter 'Name')".Trim()
        $h = ConvertTo-Number (Get-DataProperty $adapter 'Horizontal')
        $v = ConvertTo-Number (Get-DataProperty $adapter 'Vertical')
        if ($name -and $h -and $v) { $name = (Get-Text 'System.Info.Resolution') -f $name, $h, $v }
        & $add $hardware (Get-Text 'System.Info.Graphics') $name
    }
    foreach ($disk in (Get-DataCollection $Data 'Disks')) {
        $name = "$(Get-DataProperty $disk 'Name')".Trim()
        if (-not $name) { continue }
        $facts = @()
        $size = ConvertTo-Number (Get-DataProperty $disk 'SizeBytes')
        if ($size) { $facts += '{0:N0} GB' -f ($size / 1GB) }
        foreach ($fact in 'MediaType', 'BusType') {
            $text = "$(Get-DataProperty $disk $fact)".Trim()
            if ($text -and $text -ne 'Unspecified' -and $text -ne '0') { $facts += $text }
        }
        if ($facts.Count) { $name = '{0} - {1}' -f $name, ($facts -join ', ') }
        & $add $hardware (Get-Text 'System.Info.Disk') $name
    }

    # The network, as ipconfig /all would say it: one group per adapter that has an address.
    $hostName = "$(Get-DataProperty $Data 'HostName')".Trim()
    foreach ($adapter in (Get-DataCollection $Data 'NetworkAdapters')) {
        $alias = "$(Get-DataProperty $adapter 'Alias')".Trim()
        if (-not $alias) { $alias = "$(Get-DataProperty $adapter 'Description')".Trim() }
        $net = (Get-Text 'System.Info.Group.Network') -f $alias

        $what = "$(Get-DataProperty $adapter 'Description')".Trim()
        $speed = Format-SystemLinkSpeed (Get-DataProperty $adapter 'SpeedBps')
        if ($what -and $speed) { $what = '{0}, {1}' -f $what, $speed }
        & $add $net (Get-Text 'System.Info.Adapter') $what
        if ($hostName) {
            $suffix = "$(Get-DataProperty $adapter 'DnsSuffix')".Trim()
            $name = $hostName
            if ($suffix) { $name = '{0}.{1}' -f $hostName, $suffix }
            & $add $net (Get-Text 'System.Info.HostName') $name
        }
        & $add $net (Get-Text 'System.Info.IPv4') (@(Get-SystemNetworkAddress -Adapter $adapter -Family IPv4) -join ', ')
        & $add $net (Get-Text 'System.Info.IPv6') (@(Get-SystemNetworkAddress -Adapter $adapter -Family IPv6) -join ', ')
        & $add $net (Get-Text 'System.Info.Gateway')    ((Get-DataCollection $adapter 'Gateways') -join ', ')
        & $add $net (Get-Text 'System.Info.DnsServers') ((Get-DataCollection $adapter 'DnsServers') -join ', ')

        $dhcp = Get-Text 'System.Info.DhcpOff'
        if ([bool](Get-DataProperty $adapter 'DhcpEnabled')) {
            $dhcp = Get-Text 'System.Info.DhcpOn'
            $server = "$(Get-DataProperty $adapter 'DhcpServer')".Trim()
            if ($server) { $dhcp = (Get-Text 'System.Info.DhcpServer') -f $server }
            $until = Format-SystemDate (Get-DataProperty $adapter 'LeaseExpires') 'dd.MM.yyyy HH:mm'
            if ($until) { $dhcp = (Get-Text 'System.Info.DhcpLease') -f $dhcp, $until }
        }
        & $add $net (Get-Text 'System.Info.Dhcp') $dhcp
        & $add $net (Get-Text 'System.Info.Mac')  (Get-DataProperty $adapter 'MacAddress')
    }

    $driver = Get-Text 'System.Info.Group.Drivers'
    foreach ($adapter in (Get-DataCollection $Data 'Graphics')) {
        $version = "$(Get-DataProperty $adapter 'DriverVersion')".Trim()
        if (-not $version) { continue }
        $date = Format-SystemDate (Get-DataProperty $adapter 'DriverDate')
        if ($date) { $version = (Get-Text 'System.Info.VersionOf') -f $version, $date }
        & $add $driver (Get-Text 'System.Info.GraphicsDriver') ('{0}: {1}' -f "$(Get-DataProperty $adapter 'Name')".Trim(), $version)
    }
    $drivers = Get-DataCollection $Data 'Drivers'
    if ($drivers.Count) {
        # Named by its title, in quotation marks: the Report makes a link of that.
        & $add $driver (Get-Text 'System.Info.OtherDrivers') ((Get-Text 'System.Info.SeeDrivers') -f $drivers.Count, (Get-Text 'Title.System.Drivers'))
    }

    $rows
}

function ConvertTo-SystemSection {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()]$Data, [AllowNull()]$Situation)

    # What the Hints of this Report assumed about the machine stands before what the
    # machine is: it is what a reader has to know to read the rest.
    New-Section -Title (Get-Text 'Title.System.Information') -Placement Top -Row @(
        Get-SituationRow -Situation $Situation
        Get-SystemInformationRow -Data $Data)

    # Everything ipconfig /all says, as a table: the box shows the same, and a table can
    # be copied into a ticket.
    New-Section -Title (Get-Text 'Title.System.IpConfiguration') -Row @(
        foreach ($adapter in (Get-DataCollection $Data 'NetworkAdapters')) {
            $row = [ordered]@{}
            $row[(Get-Text 'Column.System.Adapter')]       = Get-DataProperty $adapter 'Alias'
            $row[(Get-Text 'Column.System.Description')]   = Get-DataProperty $adapter 'Description'
            $row[(Get-Text 'Column.System.Speed')]         = Format-SystemLinkSpeed (Get-DataProperty $adapter 'SpeedBps')
            $row[(Get-Text 'Column.System.MacAddress')]    = Get-DataProperty $adapter 'MacAddress'
            $row[(Get-Text 'Column.System.IPv4')]          = @(Get-SystemNetworkAddress -Adapter $adapter -Family IPv4) -join ', '
            $row[(Get-Text 'Column.System.IPv6')]          = @(Get-SystemNetworkAddress -Adapter $adapter -Family IPv6) -join ', '
            $row[(Get-Text 'Column.System.Gateway')]       = (Get-DataCollection $adapter 'Gateways') -join ', '
            $row[(Get-Text 'Column.System.DnsServers')]    = (Get-DataCollection $adapter 'DnsServers') -join ', '
            $row[(Get-Text 'Column.System.DnsSuffix')]     = Get-DataProperty $adapter 'DnsSuffix'
            $row[(Get-Text 'Column.System.Dhcp')]          = Get-TriStateText ([bool](Get-DataProperty $adapter 'DhcpEnabled'))
            $row[(Get-Text 'Column.System.DhcpServer')]    = Get-DataProperty $adapter 'DhcpServer'
            $row[(Get-Text 'Column.System.LeaseObtained')] = Format-SystemDate (Get-DataProperty $adapter 'LeaseObtained') 'yyyy-MM-dd HH:mm'
            $row[(Get-Text 'Column.System.LeaseExpires')]  = Format-SystemDate (Get-DataProperty $adapter 'LeaseExpires') 'yyyy-MM-dd HH:mm'
            [pscustomobject]$row
        }
    )

    # By class and then by device, so that the network adapters stand together.
    New-Section -Title (Get-Text 'Title.System.Drivers') -Row @(
        (Get-DataCollection $Data 'Drivers') | Sort-Object { "$(Get-DataProperty $_ 'Class')" }, { "$(Get-DataProperty $_ 'Device')" } | ForEach-Object {
            $row = [ordered]@{}
            # What Device Manager calls the class, and the name Windows keeps it under.
            $row[(Get-Text 'Column.System.DriverClass')]    = Get-SystemDriverClassText -Class (Get-DataProperty $_ 'Class')
            $row[(Get-Text 'Column.System.DriverDevice')]   = Get-DataProperty $_ 'Device'
            $row[(Get-Text 'Column.System.DriverProvider')] = Get-DataProperty $_ 'Provider'
            $row[(Get-Text 'Column.System.DriverVersion')]  = Get-DataProperty $_ 'Version'
            $row[(Get-Text 'Column.System.DriverDate')]     = Format-SystemDate (Get-DataProperty $_ 'Date') 'yyyy-MM-dd'
            [pscustomobject]$row
        }
    )
}

function Get-SystemDriverClassText {
    <#
    .SYNOPSIS
        A device setup class in the words of Device Manager, with the name Windows keeps
        it under behind it: "Grafikkarten (DISPLAY)". Pure.
    .DESCRIPTION
        The names are those of Microsoft's system-defined device setup classes. A class
        that is not among them - a vendor's own - is shown as it is.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyString()][string]$Class)

    $name = switch ("$Class".Trim().ToUpperInvariant()) {
        'AUDIOENDPOINT'     { Get-Text 'Value.System.DriverClass.AudioEndpoint' }
        'BATTERY'           { Get-Text 'Value.System.DriverClass.Battery' }
        'BIOMETRIC'         { Get-Text 'Value.System.DriverClass.Biometric' }
        'BLUETOOTH'         { Get-Text 'Value.System.DriverClass.Bluetooth' }
        'CAMERA'            { Get-Text 'Value.System.DriverClass.Camera' }
        'DISKDRIVE'         { Get-Text 'Value.System.DriverClass.DiskDrive' }
        'DISPLAY'           { Get-Text 'Value.System.DriverClass.Display' }
        'EXTENSION'         { Get-Text 'Value.System.DriverClass.Extension' }
        'FIRMWARE'          { Get-Text 'Value.System.DriverClass.Firmware' }
        'HDC'               { Get-Text 'Value.System.DriverClass.Hdc' }
        'HIDCLASS'          { Get-Text 'Value.System.DriverClass.Hid' }
        'IMAGE'             { Get-Text 'Value.System.DriverClass.Image' }
        'KEYBOARD'          { Get-Text 'Value.System.DriverClass.Keyboard' }
        'MEDIA'             { Get-Text 'Value.System.DriverClass.Media' }
        'MONITOR'           { Get-Text 'Value.System.DriverClass.Monitor' }
        'MOUSE'             { Get-Text 'Value.System.DriverClass.Mouse' }
        'NET'               { Get-Text 'Value.System.DriverClass.Net' }
        'PORTS'             { Get-Text 'Value.System.DriverClass.Ports' }
        'PRINTER'           { Get-Text 'Value.System.DriverClass.Printer' }
        'PRINTQUEUE'        { Get-Text 'Value.System.DriverClass.PrintQueue' }
        'PROCESSOR'         { Get-Text 'Value.System.DriverClass.Processor' }
        'SCSIADAPTER'       { Get-Text 'Value.System.DriverClass.ScsiAdapter' }
        'SECURITYDEVICES'   { Get-Text 'Value.System.DriverClass.SecurityDevices' }
        'SMARTCARDREADER'   { Get-Text 'Value.System.DriverClass.SmartCardReader' }
        'SOFTWARECOMPONENT' { Get-Text 'Value.System.DriverClass.SoftwareComponent' }
        'SOFTWAREDEVICE'    { Get-Text 'Value.System.DriverClass.SoftwareDevice' }
        'SYSTEM'            { Get-Text 'Value.System.DriverClass.System' }
        'USB'               { Get-Text 'Value.System.DriverClass.Usb' }
    }
    if (-not $name) { return "$Class" }
    (Get-Text 'Value.System.DriverClass') -f $name, $Class
}

function ConvertTo-SystemFinding {
    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [AllowNull()]$Data,
        [hashtable]$Parameters = @{}
    )

    New-ModelFinding           -Data $Data -Parameters $Parameters
    New-OperatingSystemFinding -Data $Data -Parameters $Parameters
    New-BiosFinding            -Data $Data -Parameters $Parameters
    New-ProcessorFinding       -Data $Data -Parameters $Parameters
    New-RamInstalledFinding    -Data $Data -Parameters $Parameters
    New-RamModuleFinding       -Data $Data -Parameters $Parameters
    New-UptimeFinding          -Data $Data -Parameters $Parameters
    New-GraphicsDriverFinding  -Data $Data -Parameters $Parameters
}

function New-GraphicsDriverFinding {
    <#
    .SYNOPSIS
        The graphics driver and its date, one Finding for each adapter. Pure.
    .DESCRIPTION
        INFO whatever its age: an old driver that works is no warning. What its age is
        for is to be read with what else was found - a display that hangs, a blue screen
        of the display driver - and that reading is a Lead's. An adapter that reports no
        date gets no Finding: nothing can be said of its age.
 
        Three years is this module's own rule of thumb, like the two years of the BIOS,
        and a Check Definition can say otherwise.
    #>

    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $noteDays = ConvertTo-Number (Get-Parameter $Parameters 'GraphicsDriverAgeNoteDays' 1095)
    $asOf = Get-DataProperty $Data 'GatheredAt'

    foreach ($adapter in (Get-DataCollection $Data 'Graphics')) {
        $date = (Get-DataProperty $adapter 'DriverDate') -as [datetime]
        if (-not $date -or -not $asOf) { continue }

        # A display adapter that is software - a remote desktop session's, a virtual
        # display's - is no graphics card and has no driver anybody could update.
        if ("$(Get-DataProperty $adapter 'PnpDeviceId')" -match '(?i)^(SWD|ROOT)\\') { continue }

        # Every driver that ships with Windows is dated 21 June 2006, whenever it was
        # written: Microsoft does that so that a manufacturer's driver always counts as
        # newer (Raymond Chen, The Old New Thing, "Why are all Windows drivers dated
        # June 21, 2006?", 2017). That date says nothing about the driver's age.
        $inbox = $date.Year -eq 2006 -and $date.Month -eq 6 -and $date.Day -in 20, 21, 22
        $old = -not $inbox -and ([datetime]$asOf - $date).TotalDays -gt $noteDays
        $hint = ''
        $signal = @()
        if ($old) {
            $hint   = (Get-Text 'Hint.System.GraphicsDriverOld') -f [math]::Round($noteDays / 365)
            $signal = @('graphics-driver-old')
        }
        New-Finding -Category System -Check (Get-Text 'Check.System.GraphicsDriver') -Severity INFO `
            -Value ((Get-Text 'Value.System.GraphicsDriver') -f "$(Get-DataProperty $adapter 'Name')".Trim(), "$(Get-DataProperty $adapter 'DriverVersion')".Trim(), $date) `
            -Hint $hint -Signal $signal -Subject "$(Get-DataProperty $adapter 'Name')".Trim()
    }
}

function New-ModelFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $manufacturer = Get-DataProperty $Data 'Manufacturer'
    $model        = Get-DataProperty $Data 'Model'
    if (-not $manufacturer -and -not $model) {
        return New-UnavailableFinding -Category System -Check (Get-Text 'Check.System.Model') `
            -Hint (Get-Text 'Hint.System.TheMachineDidNotReport4')
    }
    New-Finding -Category System -Check (Get-Text 'Check.System.Model') -Severity INFO `
        -Value ('{0} {1} (S/N {2})' -f $manufacturer, $model, (Get-DataProperty $Data 'SerialNumber'))
}

function New-OperatingSystemFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $caption = Get-DataProperty $Data 'OsCaption'
    if (-not $caption) {
        return New-UnavailableFinding -Category System -Check (Get-Text 'Check.System.OS') `
            -Hint (Get-Text 'Hint.System.TheOperatingSystemDidNot')
    }
    New-Finding -Category System -Check (Get-Text 'Check.System.OS') -Severity INFO -Value ((Get-Text 'Value.System.OperatingSystem') -f
        $caption, (Get-DataProperty $Data 'OsDisplayVersion'),
        (Get-DataProperty $Data 'OsBuildNumber'), (Get-DataProperty $Data 'OsUpdateBuildRevision'))
}

function New-BiosFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $warnDays = Get-Parameter $Parameters 'BiosAgeWarnDays' 730
    $failDays = Get-Parameter $Parameters 'BiosAgeFailDays' ([double]::MaxValue)

    $released = Get-DataProperty $Data 'BiosReleaseDate'
    if (-not $released) {
        return New-UnavailableFinding -Category System -Check (Get-Text 'Check.System.BIOS') `
            -Hint (Get-Text 'Hint.System.TheMachineDidNotReport')
    }

    $ageDays = ((Get-DataProperty $Data 'GatheredAt') - [datetime]$released).TotalDays
    New-Finding -Category System -Check (Get-Text 'Check.System.BIOS') -Severity (Get-Severity $ageDays $warnDays $failDays) `
        -Value ('{0} ({1:yyyy-MM-dd})' -f (Get-DataProperty $Data 'BiosVersion'), [datetime]$released) `
        -Hint (Get-Text 'Hint.System.BIOSOlderThan2Years')
}

function New-ProcessorFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $name = Get-DataProperty $Data 'ProcessorName'
    if (-not $name) {
        return New-UnavailableFinding -Category System -Check (Get-Text 'Check.System.CPU') `
            -Hint (Get-Text 'Hint.System.TheMachineDidNotReport5')
    }
    New-Finding -Category System -Check (Get-Text 'Check.System.CPU') -Severity INFO -Value ((Get-Text 'Value.System.Processor') -f
        ([string]$name).Trim(), (Get-DataProperty $Data 'ProcessorCores'), (Get-DataProperty $Data 'ProcessorLogicalCount'))
}

function New-RamInstalledFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    # Graded downwards: here it is the small number that is the problem.
    $warnBelow = Get-Parameter $Parameters 'RamInstalledWarnBelowGB' 15
    $failBelow = Get-Parameter $Parameters 'RamInstalledFailBelowGB' 7.5

    $bytes = ConvertTo-Number (Get-DataProperty $Data 'TotalPhysicalMemoryBytes')
    if ($null -eq $bytes -or $bytes -le 0) {
        return New-UnavailableFinding -Category System -Check (Get-Text 'Check.System.RAMInstalled') `
            -Hint (Get-Text 'Hint.System.TheMachineDidNotReport2')
    }

    $gb = [math]::Round($bytes / 1GB, 1)
    New-Finding -Category System -Check (Get-Text 'Check.System.RAMInstalled') -Severity (Get-SeverityBelow $gb $warnBelow $failBelow) `
        -Value "$gb GB" `
        -Hint (Get-Text 'Hint.System.OfficeTeamsBrowserBusinessApps')
}

function New-RamModuleFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $modules = (Get-DataCollection $Data 'MemoryModules')
    if (-not $modules.Count) {
        return New-UnavailableFinding -Category System -Check (Get-Text 'Check.System.RAMModules') `
            -Hint (Get-Text 'Hint.System.TheMachineDidNotReport3')
    }

    # A single module means single-channel, which is a real performance finding, but the
    # script reported it as INFO with a Hint rather than a WARN. Carried across unchanged.
    $hint = ''
    if ($modules.Count -eq 1) {
        $hint = (Get-Text 'Hint.System.SingleChannelMemory')
    }

    # Not a bare division: gathered data also reaches a Judge through a CliXML round trip,
    # where a capacity can arrive as text and dividing it would throw rather than report.
    $described = $modules | ForEach-Object {
        $capacity = ConvertTo-Number $_.CapacityBytes
        if ($null -eq $capacity) { '? GB @ {0} MHz' -f $_.ConfiguredClockSpeed }
        else { '{0} GB @ {1} MHz' -f ($capacity / 1GB), $_.ConfiguredClockSpeed }
    }
    New-Finding -Category System -Check (Get-Text 'Check.System.RAMModules') -Severity INFO -Value ($described -join ', ') -Hint $hint
}

function New-UptimeFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $warnDays = Get-Parameter $Parameters 'UptimeWarnDays' 7
    $failDays = Get-Parameter $Parameters 'UptimeFailDays' ([double]::MaxValue)

    $booted = Get-DataProperty $Data 'LastBootUpTime'
    if (-not $booted) {
        return New-UnavailableFinding -Category System -Check (Get-Text 'Check.System.Uptime') `
            -Hint (Get-Text 'Hint.System.TheOperatingSystemDidNot2')
    }

    $days = ((Get-DataProperty $Data 'GatheredAt') - [datetime]$booted).TotalDays
    $fastStartup = Get-Text 'Value.System.FastStartupOff'
    if ((ConvertTo-Number (Get-DataProperty $Data 'FastStartupEnabled')) -eq 1) { $fastStartup = Get-Text 'Value.System.FastStartupOn' }

    New-Finding -Category System -Check (Get-Text 'Check.System.Uptime') -Severity (Get-Severity $days $warnDays $failDays) `
        -Value ((Get-Text 'Value.System.Uptime') -f $days, $fastStartup) `
        -Hint (Get-Text 'Hint.System.ShutDownWithFastStartup')
}