<# .SYNOPSIS Query SMART and CIM storage reliability data for all local physical drives and flag predicted hardware failures before a drive actually dies. .DESCRIPTION Enumerates all local physical drives using MSFT_PhysicalDisk (Storage module) and correlates with MSFT_Disk and Win32_DiskDrive CIM data. For SATA/SAS/HDD drives, the MSFT_PhysicalDisk HealthStatus and OperationalStatus properties expose aggregated SMART predictions surfaced by the Windows Storage Stack. For NVMe drives (Windows 10 1903+), the same CIM namespace surfaces NVMe health log data through the same properties. Evaluation tiers: PASS -- Drive healthy, no predicted failure. WARN -- OperationalStatus contains a degraded/predictive-failure indicator but HealthStatus is not yet "Unhealthy". FAIL -- HealthStatus is "Unhealthy" OR OperationalStatus confirms a predicted failure. On any WARN or FAIL result the script writes an entry to the Windows Application Event Log (Source: DriveHealthCheck) for downstream alerting. Exit codes: 0 -- All drives passed. 1 -- One or more drives are WARN or FAIL (actionable finding). 2 -- Script could not complete (no drives found, insufficient privilege, etc.) .PARAMETER Detailed When specified, outputs per-drive extended CIM property breakdown in addition to the summary line. Equivalent to -Verbose output in non-[CmdletBinding] style. .EXAMPLE .\Get-DriveSmartHealth.ps1 Run silently with summary output only. .EXAMPLE .\Get-DriveSmartHealth.ps1 -Detailed Run with per-drive extended property breakdown. .NOTES v1.0 2026-06-26 -- Initial release. Covers SATA/HDD/SSD and NVMe via MSFT_PhysicalDisk (StorageWMI / ROOT\Microsoft\Windows\Storage). Requires Windows 8.1 / Server 2012 R2 or later. Must run as Administrator (SYSTEM is sufficient). #> [CmdletBinding()] param( [switch]$Detailed ) #region ======================================================= CONFIGURATION == $EventLogSource = 'DriveHealthCheck' $EventLogName = 'Application' $EventIdWarn = 1101 $EventIdFail = 1102 $EventIdInfo = 1100 #endregion #region =========================================================== FUNCTIONS == function Ensure-EventSource { <# .SYNOPSIS Create the event log source if it does not already exist. #> if (-not [System.Diagnostics.EventLog]::SourceExists($EventLogSource)) { try { New-EventLog -LogName $EventLogName -Source $EventLogSource -ErrorAction Stop } catch { Write-Output " [EventLog] Could not register source '$EventLogSource': $_" } } } function Write-DriveEvent { param( [string]$Message, [ValidateSet('Information','Warning','Error')] [string]$EntryType, [int]$EventId ) try { Write-EventLog -LogName $EventLogName -Source $EventLogSource ` -EntryType $EntryType -EventId $EventId -Message $Message ` -ErrorAction Stop } catch { Write-Output " [EventLog] Failed to write event: $_" } } function Get-HealthLabel { <# .SYNOPSIS Map MSFT_PhysicalDisk HealthStatus integer to a human-readable label. .NOTES 0 = Healthy | 1 = Warning | 2 = Unhealthy | 5 = Unknown #> param([uint16]$HealthStatus) switch ($HealthStatus) { 0 { 'Healthy' } 1 { 'Warning' } 2 { 'Unhealthy' } 5 { 'Unknown' } default { "Unknown($HealthStatus)" } } } function Get-OperationalLabel { <# .SYNOPSIS Map MSFT_PhysicalDisk OperationalStatus uint16 values to labels. .NOTES Common values: 0=Unknown 1=Other 2=OK 3=Degraded 4=Stressed 5=Predictive Failure 6=Error 7=Non-Recoverable Error 8=Starting 9=Stopping 10=Stopped 11=In Service 12=No Contact 13=Lost Communication 14=Aborted 15=Dormant 16=Supporting Entity In Error 17=Completed 0xD009=Failed Media 0xD00A=Split 0xD00B=Stale Metadata 0xD00C=IO Error 0xD00D=Unrecognized Metadata #> param([uint16[]]$StatusArray) $labels = foreach ($s in $StatusArray) { switch ($s) { 0 { 'Unknown' } 1 { 'Other' } 2 { 'OK' } 3 { 'Degraded' } 4 { 'Stressed' } 5 { 'Predictive Failure' } 6 { 'Error' } 7 { 'Non-Recoverable Error' } 8 { 'Starting' } 9 { 'Stopping' } 10 { 'Stopped' } 11 { 'In Service' } 14 { 'Aborted' } 53257 { 'Failed Media (0xD009)' } 53258 { 'Split (0xD00A)' } 53259 { 'Stale Metadata (0xD00B)' } 53260 { 'IO Error (0xD00C)' } 53261 { 'Unrecognized Metadata (0xD00D)' } default { "Status($s)" } } } $labels -join ', ' } function Get-DriveTier { <# .SYNOPSIS Derive PASS / WARN / FAIL from HealthStatus + OperationalStatus. #> param( [uint16] $HealthStatus, [uint16[]] $OperationalStatus ) # Explicit failure indicators $failOp = @(3, 5, 6, 7, 53257, 53260) # Degraded, Predictive Failure, Error, etc. $warnOp = @(4, 11, 53258, 53259, 53261) # Stressed, In Service, Split, Stale, etc. if ($HealthStatus -eq 2) { return 'FAIL' } foreach ($s in $OperationalStatus) { if ($s -in $failOp) { return 'FAIL' } } if ($HealthStatus -eq 1) { return 'WARN' } foreach ($s in $OperationalStatus) { if ($s -in $warnOp) { return 'WARN' } } return 'PASS' } #endregion #region ============================================================== MAIN === Write-Output "=== Get-DriveSmartHealth | $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ===" Write-Output "" # -------------------------------------------------------------- Admin check -- $isAdmin = ( [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { Write-Output "[ERROR] This script must run as Administrator or SYSTEM." Write-Output " CIM storage namespace requires elevated access." exit 2 } # -------------------------------------------------- Ensure EventLog source -- Ensure-EventSource # ---------------------------------------------------------- Enumerate drives -- try { $physicalDisks = Get-CimInstance -Namespace 'ROOT\Microsoft\Windows\Storage' ` -ClassName 'MSFT_PhysicalDisk' ` -ErrorAction Stop | Where-Object { $_.BusType -ne 7 } # Exclude USB (BusType 7) } catch { Write-Output "[ERROR] Failed to query MSFT_PhysicalDisk: $_" Write-Output " Requires Windows 8.1 / Server 2012 R2 or later." exit 2 } if (-not $physicalDisks) { Write-Output "[ERROR] No local physical drives found." exit 2 } # -------------------------------------------- BusType integer -> label map -- $busTypeMap = @{ 1 = 'SCSI' 2 = 'ATAPI' 3 = 'ATA' 4 = 'IEEE 1394' 5 = 'SSA' 6 = 'FC' 7 = 'USB' 8 = 'RAID' 9 = 'iSCSI' 10 = 'SAS' 11 = 'SATA' 12 = 'SD' 13 = 'MMC' 17 = 'NVMe' 0 = 'Unknown' } # ---------------------------------------------------- MediaType -> label map -- $mediaTypeMap = @{ 0 = 'Unspecified' 3 = 'HDD' 4 = 'SSD' 5 = 'SCM' } # -------------------------------------------------------------- Evaluate ----- $passCount = 0 $warnCount = 0 $failCount = 0 $findings = [System.Collections.Generic.List[string]]::new() foreach ($disk in ($physicalDisks | Sort-Object DeviceId)) { $busLabel = if ($busTypeMap.ContainsKey([int]$disk.BusType)) { $busTypeMap[[int]$disk.BusType] } else { "BusType($($disk.BusType))" } $mediaLabel = if ($mediaTypeMap.ContainsKey([int]$disk.MediaType)) { $mediaTypeMap[[int]$disk.MediaType] } else { "MediaType($($disk.MediaType))" } $healthLabel = Get-HealthLabel -HealthStatus $disk.HealthStatus $opLabel = Get-OperationalLabel -StatusArray $disk.OperationalStatus $tier = Get-DriveTier -HealthStatus $disk.HealthStatus -OperationalStatus $disk.OperationalStatus $sizeGB = if ($disk.Size -gt 0) { [math]::Round($disk.Size / 1GB, 1) } else { '?' } # Friendly drive ID (prefer FriendlyName, fall back to DeviceId) $displayName = if ($disk.FriendlyName) { $disk.FriendlyName } else { "Disk $($disk.DeviceId)" } $serial = if ($disk.SerialNumber) { $disk.SerialNumber.Trim() } else { 'N/A' } # Summary line $summaryLine = "[{0}] {1} | {2} {3} {4} GB | Health: {5} | OpStatus: {6}" -f ` $tier, $displayName, $busLabel, $mediaLabel, $sizeGB, $healthLabel, $opLabel Write-Output $summaryLine # Detailed breakdown (only with -Detailed) if ($Detailed) { Write-Output (" Serial : {0}" -f $serial) Write-Output (" FirmwareVersion : {0}" -f $disk.FirmwareVersion) Write-Output (" SpindleSpeed : {0} RPM" -f $(if ($disk.SpindleSpeed -eq 4294967295) { 'N/A (SSD/NVMe)' } else { $disk.SpindleSpeed })) Write-Output (" LogicalSectors : {0}" -f $disk.LogicalSectorSize) Write-Output (" PhysicalSectors : {0}" -f $disk.PhysicalSectorSize) # Wear / endurance for SSD/NVMe (property present on Win 10 1903+) if ($null -ne $disk.Usage) { Write-Output (" Usage (wear) : {0}%" -f $disk.Usage) } # Failure predicted flag (Win10 1903+ NVMe / SATA SMART rollup) if ($null -ne $disk.FailurePredicted) { Write-Output (" FailurePredicted: {0}" -f $disk.FailurePredicted) } Write-Output "" } # Tally + Event Log switch ($tier) { 'PASS' { $passCount++ } 'WARN' { $warnCount++ $msg = "SMART WARN on drive '{0}' (Serial: {1}). Health: {2} | OpStatus: {3}" -f ` $displayName, $serial, $healthLabel, $opLabel $findings.Add($summaryLine) Write-DriveEvent -Message $msg -EntryType Warning -EventId $EventIdWarn } 'FAIL' { $failCount++ $msg = "SMART FAIL on drive '{0}' (Serial: {1}). Health: {2} | OpStatus: {3}" -f ` $displayName, $serial, $healthLabel, $opLabel $findings.Add($summaryLine) Write-DriveEvent -Message $msg -EntryType Error -EventId $EventIdFail } } } # ----------------------------------------------------------------- Summary -- Write-Output "" Write-Output ("=== Summary: {0} PASS | {1} WARN | {2} FAIL | {3} drives evaluated ===" -f ` $passCount, $warnCount, $failCount, ($passCount + $warnCount + $failCount)) if ($findings.Count -gt 0) { Write-Output "" Write-Output "--- At-Risk Drives ---" foreach ($f in $findings) { Write-Output " $f" } # Single consolidated Event Log entry listing all flagged drives $consolidatedMsg = "Drive health check flagged {0} drive(s):`n`n{1}" -f ` $findings.Count, ($findings -join "`n") Write-DriveEvent -Message $consolidatedMsg -EntryType Warning -EventId $EventIdInfo } # ----------------------------------------------------------------- Exit ----- if ($failCount -gt 0 -or $warnCount -gt 0) { exit 1 } exit 0 #endregion