Private/Kinds/DiskHealth.ps1

# The DiskHealth Kind: what the drive itself says about how much life it has left.
#
# The Storage Kind reports that a disk is Healthy, which is Windows repeating what the
# driver told it. This reads the reliability counters underneath: wear, uncorrected read
# errors, temperature and hours powered on. A solid-state drive at 85 % wear reports
# Healthy right up until it does not, and this is the Check that gives a Technician the
# weeks of notice that makes the difference between a replacement and a data recovery.

# Declared for the Kind vocabulary: the reliability counters are not readable without
# admin rights, so this Check belongs to the Elevated Part. See Private/Kind.ps1.
$script:DiskHealthNeedsAdmin = $true

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

    $disks = @()
    try {
        $disks = @(Get-PhysicalDisk -ErrorAction Stop | ForEach-Object {
            $device = $_
            $counter = $null
            $readable = $true
            try { $counter = $device | Get-StorageReliabilityCounter -ErrorAction Stop }
            catch {
                # Plenty of drives and controllers simply do not expose these counters, and
                # a Run without admin rights cannot read them at all. Which of the two this
                # was is the Judge's call, from the Privilege the Part carries.
                $readable = $false
            }

            [pscustomobject]@{
                DeviceId              = $device.DeviceId
                FriendlyName          = "$($device.FriendlyName)"
                MediaType             = "$($device.MediaType)"
                CountersReadable      = $readable
                Wear                  = $counter.Wear
                TemperatureC          = $counter.Temperature
                ReadErrorsUncorrected = $counter.ReadErrorsUncorrected
                PowerOnHours          = $counter.PowerOnHours
            }
        })
    }
    catch { }

    [pscustomobject]@{
        PSTypeName    = 'Gutcheck.Data.DiskHealth'
        PhysicalDisks = $disks
        # Which rights the readings were taken with: the difference between "this drive
        # does not report wear" and "nobody was allowed to ask".
        Elevated      = (Get-CurrentPrivilege) -eq 'admin'
    }
}

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

    $wearWarn = Get-Parameter $Parameters 'DiskWearWarnAbovePercent' 49
    $wearFail = Get-Parameter $Parameters 'DiskWearFailAbovePercent' 79
    $tempWarn = Get-Parameter $Parameters 'DiskTemperatureWarnAboveC' 60
    $tempFail = Get-Parameter $Parameters 'DiskTemperatureFailAboveC' 70

    $disks = Get-DataCollection $Data 'PhysicalDisks'
    if (-not $disks.Count) {
        return New-UnavailableFinding -Category Storage -Check (Get-Text 'Check.DiskHealth.DiskHealth') `
            -Hint (Get-Text 'Hint.DiskHealth.NoPhysicalDiskCouldBe')
    }

    $elevated = [bool](Get-DataProperty $Data 'Elevated')

    foreach ($disk in $disks) {
        $check = (Get-Text 'Check.DiskHealth.DiskWearErrors') -f $disk.DeviceId

        if (-not $disk.CountersReadable) {
            $why = $(if ($elevated) { 'not supported by disk/driver' } else { (Get-Text 'Value.Shared.NeedsAdmin') })
            New-Finding -Category Storage -Check $check -Severity INFO -Value $why `
                -Hint $(if ($elevated) {
                    'This drive or its controller does not expose reliability counters; use the vendor''s own tool'
                } else {
                    'Rerun and allow the admin prompt: without it this drive''s remaining life is unknown, not good'
                })
            continue
        }

        $wear   = ConvertTo-Number $disk.Wear
        $errors = ConvertTo-Number $disk.ReadErrorsUncorrected
        $temp   = ConvertTo-Number $disk.TemperatureC

        # Wear and uncorrected read errors both end in the same place. A single
        # uncorrected read error is data the drive could not give back, so it fails on its
        # own however young the drive is.
        $wearSeverity = 'OK'
        if ($null -ne $wear) { $wearSeverity = Get-Severity $wear $wearWarn $wearFail }

        $errorSeverity = 'OK'
        if ($null -ne $errors -and $errors -gt 0) { $errorSeverity = 'FAIL' }

        $tempSeverity = 'OK'
        if ($null -ne $temp -and $temp -gt 0) { $tempSeverity = Get-Severity $temp $tempWarn $tempFail }

        New-Finding -Category Storage -Check $check `
            -Severity (Get-WorstSeverity $wearSeverity $errorSeverity $tempSeverity) `
            -Value ((Get-Text 'Value.DiskHealth.WearTempErrors') -f
                    $disk.Wear, $disk.TemperatureC, $disk.ReadErrorsUncorrected, $disk.PowerOnHours) `
            -Hint (Get-Text 'Hint.DiskHealth.SSDWornOutOrReporting')
    }
}