# ============================================================================== # smart.ps1 v3.0 - disk health (SMART) report + short system inventory # ============================================================================== # Run from the internet (main way): # irm https://share.anykey54.ru/smart.ps1 | iex # # Run locally (works on any Windows locale / codepage): # powershell -NoProfile -ExecutionPolicy Bypass -File .\smart.ps1 # # Self-check without admin rights (environment, mode, WMI, smartctl): # powershell -NoProfile -ExecutionPolicy Bypass -File .\smart.ps1 -SelfTest # # Automation (the same settings can be passed as environment variables): # $env:SMART_COMMENT='PC-123'; $env:SMART_PASSPHRASE='key'; irm ... | iex # # Parameters (env equivalents in brackets): # -Passphrase key used to decrypt the mail password [SMART_PASSPHRASE] # -Comment comment for the report [SMART_COMMENT] # -NoMail do not send e-mail [SMART_NO_MAIL=1] # -NoElevate do not request UAC elevation [SMART_NO_ELEVATE=1] # -SkipDisks system info only, no disks [SMART_SKIP_DISKS=1] # -SelfTest check environment and exit [SMART_SELFTEST=1] # -Refresh re-download smartctl.exe / drivedb.h [SMART_REFRESH=1] # -NoPause do not wait for a key at the end [SMART_NO_PAUSE=1] # -HardExit terminate the process with exit code [SMART_HARD_EXIT=1] # # What it does: # 1) picks a mode: Modern (Windows 10/11 with PowerShell 5.1) or Legacy # (Windows 7/8/8.1 with PowerShell 2.0-4.0); # 2) raises UAC itself when started without admin rights; # 3) takes smartctl.exe from disk / cache / server (with SHA256 check); # 4) collects OS, CPU, RAM, user, network, platform info; # 5) queries every disk: SMART health, attributes, wear, temperature, logs; # 6) writes a report into %ProgramData%\SmartCheck\reports (txt + csv) # and sends it by e-mail. # # Exit codes: 0 - all good, 1 - problems found, 2 - error (no admin / no smartctl). # # IMPORTANT - THIS FILE MUST STAY PURE ASCII. # Russian report texts live in texts.txt and are embedded below as base64. # Reason: Windows PowerShell 5.1 decodes BOM-less files with the ANSI codepage, # and Cyrillic bytes then produce characters the parser treats as line breaks # ("-File" breaks). A UTF-8 BOM in turn breaks "irm | iex", because the script # text may not start with U+FEFF. Pure ASCII source works both ways, on any # locale, over any HTTP charset. To change texts: edit texts.txt (UTF-8) and run # powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\build-texts.ps1 # ============================================================================== $ErrorActionPreference = 'Continue' $script:Version = '3.0' # How we were started: from a file (exit is safe) or through iex (exit would # close the user's PowerShell window). $script:SelfPath = '' try { if ($MyInvocation.MyCommand.Path) { $script:SelfPath = [string]$MyInvocation.MyCommand.Path } } catch { } $script:FromFile = -not [string]::IsNullOrEmpty($script:SelfPath) # ============================================================================== # SETTINGS - the only block to edit # ============================================================================== $Cfg = @{ # --- where the script and the tools come from ----------------------------- ScriptUrl = 'https://share.anykey54.ru/smart.ps1' # this file on the server ToolBaseUrl = 'https://share.anykey54.ru' # base for the tools SmartctlUrl = '' # default: ToolBaseUrl/smartctl.exe DrivedbUrl = '' # default: ToolBaseUrl/drivedb.h (optional) SmartctlSha256 = 'B5DB94E5082C042BE44994B7A4FA8F7B5C8E713B2AB1C9A560D8F7A7995EA27D' ExtraSearch = @('C:\dst\smartctl.exe', 'C:\Program Files\smartmontools\bin\smartctl.exe') # --- mail ----------------------------------------------------------------- SmtpServer = 'mail.hosting.reg.ru' SmtpPort = 587 SmtpUseSsl = $true SmtpUser = 'sysdata@sys.anykey54.ru' EncryptedSmtpPassword = '76492d1116743f0423413b16050a5345MgB8AE0ASgBvAGgAVwBqAEQAYgBsAG8AaAB1ADkAbAA1AEEAZABRAEwAUgBmAFEAPQA9AHwANgA1ADUAYgBlADYAMAA1ADgAMgA0ADAAZgAyADIAZgBhAGQAMABmADcAZABmADYAYwBjAGUAOQAzAGMAOQBhAGMAOABlADkANgA2AGUAOQBlAGEANQBmADEAZQBhAGMAYwBlADkANwA4ADcAYQAzADEAMwA3ADIAMwBkAGQAYgA2ADAANAA1AGMANABlADkAMQBlAGQAOAA5AGUAZQAxADkAMgBjAGEAMwAxAGIAMABmADQAZAA0AGYAYQBlADMA' MailFrom = 'sysdata@sys.anykey54.ru' MailTo = 'sysdata@sys.anykey54.ru' # several: 'a@x, b@y' MailAlways = $true # $false = write only when there are problems MailSubjectPrefix = '' # subject prefix, e.g. '[IT] ' # --- SMTP TLS certificate ------------------------------------------------- # By default the server certificate is fully validated (recommended). # Shared hosting often presents a certificate for the cluster name (for # example mail.hosting.reg.ru) while the client connects to its own domain. # Three ways to handle that, in order of preference: # 1) SmtpCertExpectedName - accept the name mismatch, but only for a # certificate that is really issued for this name and has a trusted # chain. Survives certificate renewal (recommended for shared hosting). # 2) SmtpCertThumbprint - accept exactly one certificate. Secure, but the # thumbprint must be updated after every renewal. # 3) SmtpAllowUntrustedCert - accept any certificate (insecure). SmtpCertExpectedName = '' # e.g. 'mail.hosting.reg.ru' SmtpCertThumbprint = '' # e.g. 'A1B2C3...' (colons and spaces ignored) SmtpAllowUntrustedCert = $false # $true = accept any SMTP server certificate # --- optional upload to a server ------------------------------------------ UploadUrl = '' # e.g. 'https://share.anykey54.ru/api/disk-report' UploadToken = '' # sent as the X-Auth-Token header # --- where data is kept --------------------------------------------------- DataDir = '' # default %ProgramData%\SmartCheck ReportKeep = 30 # how many recent reports to keep DrivedbRetryHours = 24 # hours between drivedb.h download retries # --- query options and thresholds ----------------------------------------- SmartctlArgs = @('-T', 'permissive') Fallbacks = @('sat', 'nvme', 'usbjmicron', 'usbprolific', 'usbsunplus') Th = @{ HddReallocBad = 100 # [05] reallocated sectors above this -> BAD HddPendingBad = 10 # [C5] pending sectors above this -> BAD WearBad = 10 # SSD/NVMe remaining life, % -> BAD WearWarn = 20 # SSD/NVMe remaining life, % -> WARNING SpareBad = 10 # NVMe available spare, % -> BAD SpareWarn = 20 TempWarn = 55 # HDD/SSD temperature, C TempBad = 65 TempNvmeWarn = 70 # NVMe temperature, C TempNvmeBad = 80 } PauseOnFinish = $false # wait for a key (true for the UAC-relaunched window) } # Environment overrides (needed for iex and for automation) if ($env:SMART_SMARTCTL_URL) { $Cfg.SmartctlUrl = $env:SMART_SMARTCTL_URL } if ($env:SMART_SMARTCTL_SHA256) { $Cfg.SmartctlSha256 = $env:SMART_SMARTCTL_SHA256 } if ($env:SMART_SCRIPT_URL) { $Cfg.ScriptUrl = $env:SMART_SCRIPT_URL } if ($env:SMART_SMTP_PASSWORD) { $Cfg.SmtpPassword = $env:SMART_SMTP_PASSWORD } if ($env:SMART_SMTP_SERVER) { $Cfg.SmtpServer = [string]$env:SMART_SMTP_SERVER } if ($env:SMART_SMTP_PORT) { $Cfg.SmtpPort = [int]$env:SMART_SMTP_PORT } if ($env:SMART_MAIL_TO) { $Cfg.MailTo = [string]$env:SMART_MAIL_TO } if ($env:SMART_MAIL_FROM) { $Cfg.MailFrom = [string]$env:SMART_MAIL_FROM } if ($env:SMART_SMTP_CERT_THUMBPRINT) { $Cfg.SmtpCertThumbprint = [string]$env:SMART_SMTP_CERT_THUMBPRINT } if ($env:SMART_SMTP_CERT_NAME) { $Cfg.SmtpCertExpectedName = [string]$env:SMART_SMTP_CERT_NAME } if ($env:SMART_SMTP_ALLOW_UNTRUSTED -eq '1') { $Cfg.SmtpAllowUntrustedCert = $true } if ($env:SMART_UPLOAD_URL) { $Cfg.UploadUrl = $env:SMART_UPLOAD_URL } if ($env:SMART_UPLOAD_TOKEN) { $Cfg.UploadToken = $env:SMART_UPLOAD_TOKEN } if ($env:SMART_DATA_DIR) { $Cfg.DataDir = $env:SMART_DATA_DIR } if ($env:SMART_PAUSE -eq '1') { $Cfg.PauseOnFinish = $true } if (-not $Cfg.SmartctlUrl) { $Cfg.SmartctlUrl = $Cfg.ToolBaseUrl.TrimEnd('/') + '/smartctl.exe' } if (-not $Cfg.DrivedbUrl) { $Cfg.DrivedbUrl = $Cfg.ToolBaseUrl.TrimEnd('/') + '/drivedb.h' } # ============================================================================== # COMMAND LINE # There is no param() block on purpose: a script text that starts with U+FEFF # (a BOM that survived the HTTP trip) breaks param() binding, because param() # must be the first statement. Plain $args parsing is immune to that. # ============================================================================== $OptPassphrase = '' $OptComment = '' $OptNoMail = $false $OptNoElevate = $false $OptSkipDisks = $false $OptSelfTest = $false $OptRefresh = $false $OptNoPause = $false $OptHardExit = $false $Argv = @($args) for ($i = 0; $i -lt $Argv.Count; $i++) { $a = [string]$Argv[$i] if ($a.Length -lt 2 -or $a[0] -ne '-') { continue } $key = $a.TrimStart('-').ToLower() $val = '' if (($i + 1) -lt $Argv.Count) { $nxt = [string]$Argv[$i + 1] if ($nxt.Length -gt 0 -and $nxt[0] -ne '-') { $val = $nxt } } switch ($key) { 'passphrase' { $OptPassphrase = $val; if ($val -ne '') { $i++ } } 'comment' { $OptComment = $val; if ($val -ne '') { $i++ } } 'nomail' { $OptNoMail = $true } 'noelevate' { $OptNoElevate = $true } 'skipdisks' { $OptSkipDisks = $true } 'selftest' { $OptSelfTest = $true } 'refresh' { $OptRefresh = $true } 'nopause' { $OptNoPause = $true } 'hardexit' { $OptHardExit = $true } } } $Passphrase = $OptPassphrase if ($Passphrase -eq '') { $Passphrase = [string]$env:SMART_PASSPHRASE } $Comment = $OptComment if ($Comment -eq '') { $Comment = [string]$env:SMART_COMMENT } $NoMail = $OptNoMail -or ($env:SMART_NO_MAIL -eq '1') $NoElevate = $OptNoElevate -or ($env:SMART_NO_ELEVATE -eq '1') $SkipDisks = $OptSkipDisks -or ($env:SMART_SKIP_DISKS -eq '1') $SelfTest = $OptSelfTest -or ($env:SMART_SELFTEST -eq '1') $Refresh = $OptRefresh -or ($env:SMART_REFRESH -eq '1') $NoPause = $OptNoPause -or ($env:SMART_NO_PAUSE -eq '1') $HardExit = $OptHardExit -or ($env:SMART_HARD_EXIT -eq '1') # ============================================================================== # REPORT TEXTS (generated, ASCII only) # Russian strings are stored as base64(UTF-8) so that the script source can be # pure ASCII. Rebuild after editing texts.txt: # powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\build-texts.ps1 # ============================================================================== # --- BEGIN GENERATED TEXT TABLE --- $TxtB64 = @{ 'Title' = '0J7QotCn0IHQoiDQniDQodCe0KHQotCe0K/QndCY0Jgg0JTQmNCh0JrQntCSINCYINCh0JjQodCi0JXQnNCr' 'RunInfo' = '0J/QkNCg0JDQnNCV0KLQoNCrINCX0JDQn9Cj0KHQmtCQ' 'SecSystem' = '0KHQmNCh0KLQldCc0JA=' 'SecDisks' = '0JTQmNCh0JrQmA==' 'SecNet' = '0KHQldCi0Kw=' 'SecResult' = '0JjQotCe0JM=' 'SecAppendix' = '0J/QoNCY0JvQntCW0JXQndCY0JU6INCf0J7QlNCg0J7QkdCd0KvQlSDQlNCQ0J3QndCr0JUgU01BUlQ=' 'LComment' = '0JrQvtC80LzQtdC90YLQsNGA0LjQuQ==' 'LComputer' = '0JrQvtC80L/RjNGO0YLQtdGA' 'LUser' = '0J/QvtC70YzQt9C+0LLQsNGC0LXQu9GM' 'LDate' = '0JTQsNGC0LA=' 'LMode' = '0KDQtdC20LjQvA==' 'LScript' = '0KHQutGA0LjQv9GC' 'LSmartctl' = 'c21hcnRjdGw=' 'LAdmin' = '0J/RgNCw0LLQsCDQsNC00LzQuNC90LjRgdGC0YDQsNGC0L7RgNCw' 'LYes' = '0LTQsA==' 'LNo' = '0L3QtdGC' 'LOS' = '0J7QoQ==' 'LBoot' = '0J/QvtGB0LvQtdC00L3Rj9GPINC30LDQs9GA0YPQt9C60LA=' 'LUptime' = '0JDQv9GC0LDQudC8' 'LPlatform' = '0J/Qu9Cw0YLRhNC+0YDQvNCw' 'LBios' = 'QklPUw==' 'LBiosDate' = '0LTQsNGC0LAgQklPUw==' 'LSerial' = '0KHQtdGA0LjQudC90YvQuSDQvdC+0LzQtdGA' 'LCPU' = '0J/RgNC+0YbQtdGB0YHQvtGA' 'LCMHz' = '0JzQk9GG' 'LCores' = '0Y/QtNC10YAv0L/QvtGC0L7QutC+0LI=' 'LRam' = '0J7Ql9Cj' 'LRamModules' = '0LzQvtC00YPQu9C10Lk=' 'LRamFree' = '0J7Ql9CjINGB0LLQvtCx0L7QtNC90L4=' 'LActivation' = '0JDQutGC0LjQstCw0YbQuNGP' 'LSecureBoot' = 'U2VjdXJlIEJvb3Q=' 'LTpm' = 'VFBN' 'LHost' = '0JjQvNGP' 'LDomain' = '0JTQvtC80LXQvQ==' 'LIP' = 'SVA=' 'LMAC' = 'TUFD' 'LGw' = '0KjQu9GO0Lc=' 'LModel' = '0JzQvtC00LXQu9GM' 'LFirmware' = '0J/RgNC+0YjQuNCy0LrQsA==' 'LInterface' = '0JjQvdGC0LXRgNGE0LXQudGB' 'LVolumes' = '0KLQvtC80LA=' 'LFreeOf' = '0YHQstC+0LHQvtC00L3QviDQuNC3' 'LType' = '0KLQuNC/' 'LCapacity' = '0IHQvNC60L7RgdGC0Yw=' 'LSmart' = 'U01BUlQ=' 'LStatus' = '0KHRgtCw0YLRg9GB' 'LPowerOn' = '0J3QsNGA0LDQsdC+0YLQutCw' 'LHours' = '0Yc=' 'LTemp' = '0KLQtdC80L/QtdGA0LDRgtGD0YDQsA==' 'LWear' = '0J7RgdGC0LDRgtC+0Log0YDQtdGB0YPRgNGB0LA=' 'LUsed' = '0JjRgdC/0L7Qu9GM0LfQvtCy0LDQvdC+INGA0LXRgdGD0YDRgdCw' 'LWritten' = '0JfQsNC/0LjRgdCw0L3Qvg==' 'LRead' = '0J/RgNC+0YfQuNGC0LDQvdC+' 'LSpare' = 'QXZhaWxhYmxlIFNwYXJl' 'LPercUsed' = 'UGVyY2VudGFnZSBVc2Vk' 'LCritWarn' = 'Q3JpdGljYWwgV2FybmluZw==' 'LAtaErrors' = '0JbRg9GA0L3QsNC7INC+0YjQuNCx0L7QuiBBVEE=' 'LSelfTests' = '0JbRg9GA0L3QsNC7INGB0LDQvNC+0YLQtdGB0YLQvtCy' 'LLastSelfTest' = '0J/QvtGB0LvQtdC00L3QuNC5INGB0LDQvNC+0YLQtdGB0YI=' 'LNote' = '0J/RgNC40LzQtdGH0LDQvdC40LU=' 'LPowerCycles' = '0KbQuNC60LvQvtCyINCy0LrQu9GO0YfQtdC90LjRjw==' 'LFreePct' = 'JSDRgdCy0L7QsdC+0LTQvdC+' 'LSockets' = '0YHQvtC60LXRgtC+0LI=' 'LProblems' = '0J/QoNCe0JHQm9CV0JzQqw==' 'LReasons' = '0J/QoNCY0KfQmNCd0Ksg0KHQotCQ0KLQo9Ch0JA=' 'LNoProblems' = '0J/RgNC+0LHQu9C10Lwg0L3QtSDQvtCx0L3QsNGA0YPQttC10L3Qvg==' 'LDevices' = '0JTQuNGB0LrQvtCyINC/0YDQvtCy0LXRgNC10L3Qvg==' 'LBadDevices' = '0J/RgNC+0LHQu9C10LzQvdGL0YUg0LTQuNGB0LrQvtCy' 'LOverall' = '0J7QsdGJ0LjQuSDRgdGC0LDRgtGD0YE=' 'LNone' = '0L3QtdGCINC00LDQvdC90YvRhQ==' 'LDash' = 'LQ==' 'VOk' = '0KXQntCg0J7QqNCe' 'VWarn' = '0JLQndCY0JzQkNCd0JjQlQ==' 'VBad' = '0J/Qm9Ce0KXQng==' 'VUnknown' = '0J3QldCY0JfQktCV0KHQotCd0J4=' 'VNoAccess' = '0J3QldCiINCU0J7QodCi0KPQn9CQ' 'KindHdd' = 'SERE' 'KindSsd' = 'U1NE' 'KindNvme' = 'TlZNZQ==' 'KindUnknown' = '0J3QldCY0JfQktCV0KHQotCd0J4=' 'MAdminOk' = '0J/RgNCw0LLQsCDQsNC00LzQuNC90LjRgdGC0YDQsNGC0L7RgNCwOiDQv9C+0LTRgtCy0LXRgNC20LTQtdC90Ys=' 'MAdminNeed' = '0J3Rg9C20L3RiyDQv9GA0LDQstCwINCw0LTQvNC40L3QuNGB0YLRgNCw0YLQvtGA0LAg4oCUINC/0YDQvtCx0YPRjiDQv9C10YDQtdC30LDQv9GD0YHRgtC40YLRjCDRh9C10YDQtdC3IFVBQy4uLg==' 'MAdminFail' = '0J/QvtCy0YvRiNC10L3QuNC1INC/0YDQsNCyINC+0YLQutC70L7QvdC10L3QviDQuNC70Lgg0L3QtdC00L7RgdGC0YPQv9C90L4uINCU0LjRgdC60Lgg0LHRg9C00YPRgiDQvdC10LTQvtGB0YLRg9C/0L3RiyDQtNC70Y8g0YfRgtC10L3QuNGPLg==' 'MAdminHow' = '0JfQsNC/0YPRgdGC0LjRgtC1IFBvd2VyU2hlbGwg0L7RgiDQuNC80LXQvdC4INCw0LTQvNC40L3QuNGB0YLRgNCw0YLQvtGA0LAg0Lgg0L/QvtCy0YLQvtGA0LjRgtC1INC60L7QvNCw0L3QtNGDLg==' 'MElevated' = '0J/RgNC+0LTQvtC70LbQtdC90LjQtSDRgNCw0LHQvtGC0Ysg0LIg0L7RgtC00LXQu9GM0L3QvtC8INC+0LrQvdC1INGBINC/0YDQsNCy0LDQvNC4INCw0LTQvNC40L3QuNGB0YLRgNCw0YLQvtGA0LAu' 'MElevatedEnd' = '0J7QutC90L4g0YEg0L/RgNCw0LLQsNC80Lgg0LDQtNC80LjQvdC40YHRgtGA0LDRgtC+0YDQsCDQt9Cw0LrRgNGL0YLQvi4=' 'MToolLocal' = '0JjRgdC/0L7Qu9GM0LfRg9GOINC70L7QutCw0LvRjNC90YvQuSBzbWFydGN0bA==' 'MToolCache' = '0JjRgdC/0L7Qu9GM0LfRg9GOIHNtYXJ0Y3RsINC40Lcg0LrRjdGI0LA=' 'MToolDown' = '0KHQutCw0YfQuNCy0LDRjiBzbWFydGN0bA==' 'MToolOk' = 'c21hcnRjdGwg0L/QvtC70YPRh9C10L0=' 'MToolHash' = 'U0hBMjU2INGB0L7QstC/0LDQtNCw0LXRgg==' 'MToolHashBad' = 'U0hBMjU2INCd0JUg0YHQvtCy0L/QsNC00LDQtdGCISDQntC20LjQtNCw0LvQvtGB0Yw=' 'MToolHashSkip' = '0J/RgNC+0LLQtdGA0LrQsCBTSEEyNTYg0L7RgtC60LvRjtGH0LXQvdCw' 'MToolFail' = '0J3QtSDRg9C00LDQu9C+0YHRjCDQv9C+0LvRg9GH0LjRgtGMIHNtYXJ0Y3RsLiDQn9GA0L7QstC10YDRjNGC0LUg0LTQvtGB0YLRg9C/0L3QvtGB0YLRjA==' 'MToolHint' = '0J/QvtC00YHQutCw0LfQutCwOiDQt9Cw0LTQsNC50YLQtSBTTUFSVF9TTUFSVENUTF9VUkwg0LjQu9C4INC/0L7Qu9C+0LbQuNGC0LUgc21hcnRjdGwuZXhlINCyIEM6XGRzdC4=' 'MDrivedb' = '0J7QsdC90L7QstC70Y/RjiDQsdCw0LfRgyDQvNC+0LTQtdC70LXQuSDQtNC40YHQutC+0LIgKGRyaXZlZGIuaCk=' 'MTlsWarn' = '0J3QtSDRg9C00LDQu9C+0YHRjCDQstC60LvRjtGH0LjRgtGMIFRMUyAxLjIg4oCUINCy0L7Qt9C80L7QttC90Ysg0L/RgNC+0LHQu9C10LzRiyDRgdC+INGB0LrQsNGH0LjQstCw0L3QuNC10Lwg0L3QsCDRgdGC0LDRgNGL0YUg0YHQuNGB0YLQtdC80LDRhQ==' 'MScanNone' = '0KPRgdGC0YDQvtC50YHRgtCy0LAg0L3QtSDQvdCw0LnQtNC10L3RiyDRh9C10YDQtdC3IHNtYXJ0Y3RsIC0tc2Nhbg==' 'MScanOpen' = '0J/QvtCy0YLQvtGA0L3Ri9C5INC/0L7QuNGB0Log0LTQuNGB0LrQvtCyICgtLXNjYW4tb3Blbik=' 'MNoAccess' = '0KPRgdGC0YDQvtC50YHRgtCy0L4g0L3QtSDQvtGC0LrRgNGL0LvQvtGB0Yw6INC90YPQttC90Ysg0L/RgNCw0LLQsCDQsNC00LzQuNC90LjRgdGC0YDQsNGC0L7RgNCw' 'MNoSmart' = 'U01BUlQg0L3QtdC00L7RgdGC0YPQv9C10L0gKFJBSUQt0LrQvtC90YLRgNC+0LvQu9C10YAsIFVTQi3QvNC+0YHRgiDQuNC70Lgg0LLQuNGA0YLRg9Cw0LvRjNC90YvQuSDQtNC40YHQuik=' 'MReportSaved' = '0J7RgtGH0ZHRgiDRgdC+0YXRgNCw0L3RkdC9' 'MReportCsv' = '0KLQsNCx0LvQuNGG0LAgQ1NWINGB0L7RhdGA0LDQvdC10L3QsA==' 'MMailSending' = '0J7RgtC/0YDQsNCy0LvRj9GOINC/0LjRgdGM0LzQviDQvdCw' 'MMailOk' = '0J/QuNGB0YzQvNC+INC+0YLQv9GA0LDQstC70LXQvdC+' 'MMailFail' = '0J7RiNC40LHQutCwINC+0YLQv9GA0LDQstC60Lgg0L/QuNGB0YzQvNCw' 'MMailSkip' = '0J7RgtC/0YDQsNCy0LrQsCDQv9C40YHRjNC80LAg0L/RgNC+0L/Rg9GJ0LXQvdCwICjQvdC10YIg0L/QsNGA0L7Qu9GPKQ==' 'MMailSkipNo' = '0J7RgtC/0YDQsNCy0LrQsCDQv9C40YHRjNC80LAg0L/RgNC+0L/Rg9GJ0LXQvdCwICgtLU5vTWFpbCk=' 'MMailNoCred' = '0J/QsNGA0L7Qu9GMINC/0L7Rh9GC0Ysg0L3QtSDQt9Cw0LTQsNC9OiDQstCy0LXQtNC40YLQtSDQutC70Y7Rhywg0LfQsNC00LDQudGC0LUgU01BUlRfU01UUF9QQVNTV09SRCDQuNC70Lgg0LfQsNC/0L7Qu9C90LjRgtC1IEVuY3J5cHRlZFNtdHBQYXNzd29yZA==' 'MUploadOk' = '0J7RgtGH0ZHRgiDQstGL0LPRgNGD0LbQtdC9INC90LAg0YHQtdGA0LLQtdGA' 'MUploadFail' = '0J7RiNC40LHQutCwINCy0YvQs9GA0YPQt9C60Lgg0L7RgtGH0ZHRgtCwINC90LAg0YHQtdGA0LLQtdGA' 'MSaveFail' = '0J3QtSDRg9C00LDQu9C+0YHRjCDRgdC+0YXRgNCw0L3QuNGC0Ywg0L7RgtGH0ZHRgg==' 'MNoRecipient' = '0L3QtSDQt9Cw0LTQsNC9INC/0L7Qu9GD0YfQsNGC0LXQu9GMINC/0LjRgdGM0LzQsA==' 'MHashHint' = '0J7QsdC90L7QstC40YLQtSBTbWFydGN0bFNoYTI1NiDQsiDQvdCw0YHRgtGA0L7QudC60LDRhSDRgdC60YDQuNC/0YLQsCDQuNC70Lgg0LfQsNC00LDQudGC0LUgU01BUlRfU01BUlRDVExfU0hBMjU2INC/0YPRgdGC0YvQvA==' 'MHashGot' = 'LCDQv9C+0LvRg9GH0LXQvdC+' 'MSmartUnknown' = 'U01BUlQgaGVhbHRoIHN0YXR1cyDQvdC1INGB0L7QvtCx0YnRkdC9INGD0YHRgtGA0L7QudGB0YLQstC+0Lw=' 'MSpareUnknown' = 'QXZhaWxhYmxlIFNwYXJlINC90LUg0YHQvtC+0LHRidGR0L0g0YPRgdGC0YDQvtC50YHRgtCy0L7QvA==' 'MHasRecords' = '0LXRgdGC0Ywg0LfQsNC/0LjRgdC4' 'MWmiFallback' = 'V01JINC90LXQtNC+0YHRgtGD0L/QtdC9IOKAlCDQtNCw0L3QvdGL0LUg0YHQvtCx0YDQsNC90Ysg0YDQtdC30LXRgNCy0L3Ri9C8INGB0L/QvtGB0L7QsdC+0Lw=' 'MPassPrompt' = '0JrQu9GO0Ycg0LTQu9GPINGA0LDRgdGI0LjRhNGA0L7QstC60Lgg0L/QsNGA0L7Qu9GPINC/0L7Rh9GC0YsgKEVudGVyIOKAlCDQsdC10Lcg0L/QuNGB0YzQvNCwKQ==' 'PassKey' = '0JrQu9GO0Yc=' 'MPassBad' = '0J3QtSDRg9C00LDQu9C+0YHRjCDRgNCw0YHRiNC40YTRgNC+0LLQsNGC0Ywg0L/QsNGA0L7Qu9GMINC/0L7Rh9GC0Ys6INC/0YDQvtCy0LXRgNGM0YLQtSDQutC70Y7Rhw==' 'MCommentPrompt' = '0JrQvtC80LzQtdC90YLQsNGA0LjQuSDQuiDQvtGC0YfRkdGC0YMgKEVudGVyIOKAlCDQv9GA0L7Qv9GD0YHRgtC40YLRjCk=' 'MDone' = '0JPQvtGC0L7QstC+' 'MExitCode' = '0JrQvtC0INCy0L7Qt9Cy0YDQsNGC0LA=' 'MSelfTest' = '0KDQldCW0JjQnCDQodCQ0JzQntCf0KDQntCS0JXQoNCa0JggKNCx0LXQtyDQvtC/0YDQvtGB0LAg0LTQuNGB0LrQvtCyKQ==' 'MSelfOk' = '0KHQsNC80L7Qv9GA0L7QstC10YDQutCwINC/0YDQvtC50LTQtdC90LA=' 'MSelfFail' = '0KHQsNC80L7Qv9GA0L7QstC10YDQutCwINC90LDRiNC70LAg0L/RgNC+0LHQu9C10LzRiw==' 'MEncoding' = '0JrQvtC00LjRgNC+0LLQutCwINGC0LXQutGB0YLQsA==' 'MEncodingBad' = '0KLQtdC60YHRgiDQv9C+0LvRg9GH0LXQvSDQsiDQvdC10LjQt9Cy0LXRgdGC0L3QvtC5INC60L7QtNC40YDQvtCy0LrQtSDigJQg0YfQsNGB0YLRjCDQvdCw0LTQv9C40YHQtdC5INC80L7QttC10YIg0LHRi9GC0Ywg0L3QtdGH0LjRgtCw0LXQvNC+0Lk=' 'MPressKey' = '0J3QsNC20LzQuNGC0LUg0LvRjtCx0YPRjiDQutC70LDQstC40YjRgyDQtNC70Y8g0LLRi9GF0L7QtNCwLi4u' 'STCap' = '0JLQvtC30LzQvtC20L3QvtGB0YLQuCDRgdC40YHRgtC10LzRiw==' 'STWmi' = '0JTQsNC90L3Ri9C1IFdNSQ==' 'STScan' = '0J/QvtC40YHQuiDRg9GB0YLRgNC+0LnRgdGC0LI=' 'STYes' = '0LXRgdGC0Yw=' 'STNo' = '0L3QtdGC' 'STDataDir' = '0JrQsNGC0LDQu9C+0LMg0LTQsNC90L3Ri9GF' 'STFromFile' = '0JfQsNC/0YPRgdC6INC40Lcg0YTQsNC50LvQsA==' 'STFromIex' = '0L3QtdGCIChpZXgp' 'STWmiOs' = 'V01JINC90LUg0LLQtdGA0L3Rg9C7INC00LDQvdC90YvQtSDQvtCxINC+0L/QtdGA0LDRhtC40L7QvdC90L7QuSDRgdC40YHRgtC10LzQtQ==' 'STRam' = '0J3QtSDQvtC/0YDQtdC00LXQu9GR0L0g0L7QsdGK0ZHQvCDQntCX0KM=' 'STNic' = '0J3QtSDQvdCw0LnQtNC10L3QviDQsNC60YLQuNCy0L3Ri9GFINGB0LXRgtC10LLRi9GFINCw0LTQsNC/0YLQtdGA0L7Qsg==' 'STAdminNote' = '0JHQtdC3INC/0YDQsNCyINCw0LTQvNC40L3QuNGB0YLRgNCw0YLQvtGA0LAgU01BUlQt0LDRgtGA0LjQsdGD0YLRiyDQvdC10LTQvtGB0YLRg9C/0L3RiyDigJQg0LIg0L7RgtGH0ZHRgtC1INCx0YPQtNC10YIgItCd0JXQoiDQlNCe0KHQotCj0J/QkCI=' 'STEncFixed' = '0LLQvtGB0YHRgtCw0L3QvtCy0LvQtdC90LAg0LjQtyBjcA==' 'STEncOk' = '0LrQvtGA0YDQtdC60YLQvdCw0Y8gKFVURi04KQ==' 'FUptime' = 'ezB9INC0IHsxfSDRhyB7Mn0g0LzQuNC9' 'FActNo' = '0J3QtSDQsNC60YLQuNCy0LjRgNC+0LLQsNC90LA=' 'FActYes' = '0JDQutGC0LjQstC40YDQvtCy0LDQvdCw' 'FActGrace' = '0JvRjNCz0L7RgtC90YvQuSDQv9C10YDQuNC+0LQ=' 'FActGrace2' = '0JTQvtC/0L7Qu9C90LjRgtC10LvRjNC90YvQuSDQu9GM0LPQvtGC0L3Ri9C5INC/0LXRgNC40L7QtA==' 'FActTimer' = '0J3QtSDQsNC60YLQuNCy0LjRgNC+0LLQsNC90LAgKNGC0LDQudC80LXRgCk=' 'FActNotify' = '0KPQstC10LTQvtC80LvQtdC90LjQtQ==' 'FActGrace3' = '0KDQsNGB0YjQuNGA0LXQvdC90YvQuSDQu9GM0LPQvtGC0L3Ri9C5INC/0LXRgNC40L7QtA==' 'FActStatus' = '0YHRgtCw0YLRg9GB' 'FSecOn' = '0LLQutC70Y7Rh9GR0L0=' 'FSecOff' = '0LLRi9C60LvRjtGH0LXQvQ==' 'FTpmPresent' = '0L/RgNC40YHRg9GC0YHRgtCy0YPQtdGCLCDQstC10YDRgdC40Y8=' 'FTpmOn' = 'LCDQstC60LvRjtGH0ZHQvQ==' 'FTpmOff' = 'LCDQstGL0LrQu9GO0YfQtdC9' 'R_Realloc' = '0J/QtdGA0LXRgNCw0YHQv9GA0LXQtNC10LvRkdC90L3Ri9C1INGB0LXQutGC0L7RgNCwIFswNV06IHswfQ==' 'R_ReallocEv' = '0KHQvtCx0YvRgtC40Y8g0L/QtdGA0LXRgNCw0YHQv9GA0LXQtNC10LvQtdC90LjRjyBbQzRdOiB7MH0=' 'R_Pending' = '0JrQsNC90LTQuNC00LDRgtC90YvQtSAo0L3QtdGB0YLQsNCx0LjQu9GM0L3Ri9C1KSDRgdC10LrRgtC+0YDQsCBbQzVdOiB7MH0=' 'R_Uncorr' = '0J3QtdC40YHQv9GA0LDQstC40LzRi9C1INGB0LXQutGC0L7RgNCwIFtDNl06IHswfQ==' 'R_Crc' = '0J7RiNC40LHQutC4INC40L3RgtC10YDRhNC10LnRgdCwIENSQyBbQzddOiB7MH0=' 'R_Spin' = '0J/QvtCy0YLQvtGA0Ysg0YDQsNGB0LrRgNGD0YLQutC4INGI0L/QuNC90LTQtdC70Y8gWzBBXTogezB9' 'R_Wear' = '0KDQtdGB0YPRgNGBINC90LDQutC+0L/QuNGC0LXQu9GPINC/0L7Rh9GC0Lgg0LjRgdGH0LXRgNC/0LDQvSwg0L7RgdGC0LDQu9C+0YHRjCB7MH0l' 'R_WearWarn' = '0KHQvdC40LbQtdC90LjQtSDRgNC10YHRg9GA0YHQsCDQvdCw0LrQvtC/0LjRgtC10LvRjywg0L7RgdGC0LDQu9C+0YHRjCB7MH0l' 'R_TempWarn' = '0J/QvtCy0YvRiNC10L3QvdCw0Y8g0YLQtdC80L/QtdGA0LDRgtGD0YDQsDogezB9IEM=' 'R_TempBad' = '0JrRgNC40YLQuNGH0LXRgdC60LDRjyDRgtC10LzQv9C10YDQsNGC0YPRgNCwOiB7MH0gQw==' 'R_Spare' = '0KDQtdC30LXRgNCy0L3Ri9C1INCx0LvQvtC60LggTlZNZSAoQXZhaWxhYmxlIFNwYXJlKTogezB9JQ==' 'R_CritWarn' = 'Q3JpdGljYWwgV2FybmluZyBOVk1lOiB7MH0=' 'R_Integrity' = '0J7RiNC40LHQutC4INGG0LXQu9C+0YHRgtC90L7RgdGC0Lgg0LTQsNC90L3Ri9GFIE5WTWU6IHswfQ==' 'R_SelfTest' = '0J7RiNC40LHQutC4INCyINC20YPRgNC90LDQu9C1INGB0LDQvNC+0YLQtdGB0YLQuNGA0L7QstCw0L3QuNGPOiB7MH0=' 'R_AtaErr' = '0JfQsNC/0LjRgdC4INCyINC20YPRgNC90LDQu9C1INC+0YjQuNCx0L7QuiBBVEE6IHswfQ==' 'R_Grown' = '0JTQtdGE0LXQutGC0L7QsiDQsiDRgdC/0LjRgdC60LUg0YDQvtGB0YLQsCAoU0NTSSk6IHswfQ==' 'R_SmartFail' = 'U01BUlQ6IEZBSUxFRA==' 'R_UsedRes' = '0JjRgdC/0L7Qu9GM0LfQvtCy0LDQvdC+INGA0LXRgdGD0YDRgdCwOiB7MH0l' 'R_Unsafe' = '0J3QtdCx0LXQt9C+0L/QsNGB0L3Ri9GFINCy0YvQutC70Y7Rh9C10L3QuNC5OiB7MH0=' 'R_MediaErr' = '0J7RiNC40LHQvtC6INC80LXQtNC40LAv0YbQtdC70L7RgdGC0L3QvtGB0YLQuDogezB9' 'T_MailHead' = '0J7RgtGH0ZHRgiDQviDRgdC+0YHRgtC+0Y/QvdC40Lgg0LTQuNGB0LrQvtCy' 'T_Attach' = '0J/QvtC70L3Ri9C5INC+0YLRh9GR0YIg0LLQviDQstC70L7QttC10L3QuNC4Lg==' 'T_Subj' = '0J7RgtGH0ZHRgiDQviDQtNC40YHQutCw0YU=' 'T_Problems' = '0JLQndCY0JzQkNCd0JjQlTog0L7QsdC90LDRgNGD0LbQtdC90Ysg0L/RgNC+0LHQu9C10LzRiw==' 'T_AllOk' = '0JLRgdC1INC00LjRgdC60Lgg0LIg0L3QvtGA0LzQtQ==' 'T_Time' = '0JLRgNC10LzRjyDRgdCx0L7RgNCw' 'T_Sec' = '0YE=' 'STTexts' = '0KLQtdC60YHRgtGLINC+0YLRh9GR0YLQsA==' 'STTextsOk' = '0LTQtdC60L7QtNC40YDQvtCy0LDQvdGLINC40LcgYmFzZTY0IChVVEYtOCk=' 'STTextsBad' = '0L/RgNC+0LHQu9C10LzQvdGL0YUg0LfQsNC/0LjRgdC10Lk=' 'MCertInvalid' = '0KHQtdGA0YLQuNGE0LjQutCw0YIgU01UUC3RgdC10YDQstC10YDQsCDQvdC1INC/0YDQvtGI0ZHQuyDQv9GA0L7QstC10YDQutGD' 'MCertNone' = '0KHQtdGA0YLQuNGE0LjQutCw0YIgU01UUC3RgdC10YDQstC10YDQsCDQvdC1INC/0L7Qu9GD0YfQtdC9' 'MCertPinned' = '0KHQtdGA0YLQuNGE0LjQutCw0YIgU01UUC3RgdC10YDQstC10YDQsCDQv9GA0LjQvdGP0YIg0L/QviDQvtGC0L/QtdGH0LDRgtC60YM=' 'MCertPinMismatch' = '0J7RgtC/0LXRh9Cw0YLQvtC6INGB0LXRgNGC0LjRhNC40LrQsNGC0LAg0L3QtSDRgdC+0LLQv9Cw0LTQsNC10YIg0YEg0LfQsNC00LDQvdC90YvQvA==' 'MCertAccepted' = '0KHQtdGA0YLQuNGE0LjQutCw0YIgU01UUC3RgdC10YDQstC10YDQsCDQv9GA0LjQvdGP0YIg0LHQtdC3INC/0YDQvtCy0LXRgNC60LggKFNtdHBBbGxvd1VudHJ1c3RlZENlcnQp' 'MCertSubject' = '0LLQu9Cw0LTQtdC70LXRhg==' 'MCertIssuer' = '0LjQt9C00LDRgtC10LvRjA==' 'MCertExpires' = '0LTQtdC50YHRgtCy0LjRgtC10LvQtdC9INC00L4=' 'MCertThumb' = '0L7RgtC/0LXRh9Cw0YLQvtC6IFNIQTE=' 'MCertHint' = '0JLQvdGD0YLRgNC10L3QvdC40Lkg0YHQtdGA0LLQtdGAPyDQo9C60LDQttC40YLQtSDQvtGC0L/QtdGH0LDRgtC+0Log0LIgU210cENlcnRUaHVtYnByaW50ICjQuNC70LggU01BUlRfU01UUF9DRVJUX1RIVU1CUFJJTlQpLiDQndCw0LTRkdC20L3QtdC1IOKAlCDRg9GB0YLQsNC90L7QstC40YLRjCDQutC+0YDQvdC10LLQvtC5INGB0LXRgNGC0LjRhNC40LrQsNGCINCj0KYg0LIg0YXRgNCw0L3QuNC70LjRidC1IMKr0JTQvtCy0LXRgNC10L3QvdGL0LUg0LrQvtGA0L3QtdCy0YvQtSDRhtC10L3RgtGA0Ysg0YHQtdGA0YLQuNGE0LjQutCw0YbQuNC4wrsu' 'MCertNames' = '0LjQvNC10L3QsCAoQ04vU0FOKQ==' 'MCertNameOk' = '0KHQtdGA0YLQuNGE0LjQutCw0YIg0L/RgNC40L3Rj9GCOiDQvtC9INCy0YvQv9C40YHQsNC9INC90LAgezB9LCDQsCDQv9C+0LTQutC70Y7Rh9C10L3QuNC1INC40LTRkdGCINC6IHsxfQ==' 'MCertNameBad' = '0KHQtdGA0YLQuNGE0LjQutCw0YIg0LLRi9C/0LjRgdCw0L0g0L3QtSDQvdCwINC+0LbQuNC00LDQtdC80L7QtSDQuNC80Y8gezB9' 'MCertNameHint' = '0JTQu9GPINGI0LDRgNC10LQt0YXQvtGB0YLQuNC90LPQsCDRg9C60LDQttC40YLQtSDQuNC80Y8g0LjQtyDRgdC10YDRgtC40YTQuNC60LDRgtCwOiBTbXRwQ2VydEV4cGVjdGVkTmFtZSDQuNC70LggU01BUlRfU01UUF9DRVJUX05BTUU=' } # --- END GENERATED TEXT TABLE --- $Txt = @{} foreach ($k in @($TxtB64.Keys)) { try { $Txt[$k] = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String([string]$TxtB64[$k])) } catch { $Txt[$k] = '' } } # ============================================================================== # BASIC HELPERS # ============================================================================== function Write-Line { param([string]$Text = '', [string]$Color = '') if ($Color) { Write-Host $Text -ForegroundColor $Color } else { Write-Host $Text } } function Write-Step([string]$Text) { Write-Line (" " + $Text) 'Gray' } function Write-Ok([string]$Text) { Write-Line (" [ OK ] " + $Text) 'Green' } function Write-Warn2([string]$Text) { Write-Line (" [WARN] " + $Text) 'Yellow' } function Write-Err([string]$Text) { Write-Line (" [FAIL] " + $Text) 'Red' } function Write-Info([string]$Text) { Write-Line (" [ -- ] " + $Text) 'DarkGray' } function Initialize-Tls { # TLS 1.2 is off by default on older systems - enable what we can. $ok = $false try { $types = [Net.SecurityProtocolType] $value = 0 foreach ($name in @('Tls12', 'Tls11', 'Tls')) { try { $f = $types.GetField($name) if ($f) { $value = $value -bor [int]$f.GetValue($null) } } catch { } } if ($value -gt 0) { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]$value $ok = $true } } catch { } return $ok } function Test-Admin { try { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object Security.Principal.WindowsPrincipal -ArgumentList $id return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } catch { return $false } } function Format-Size([double]$bytes) { $ci = [Globalization.CultureInfo]::InvariantCulture if ($bytes -le 0) { return '-' } if ($bytes -ge 1TB) { return (($bytes / 1TB).ToString('0.00', $ci) + ' TB') } if ($bytes -ge 1GB) { return (($bytes / 1GB).ToString('0.0', $ci) + ' GB') } if ($bytes -ge 1MB) { return (($bytes / 1MB).ToString('0.0', $ci) + ' MB') } return (($bytes / 1KB).ToString('0.0', $ci) + ' KB') } function Format-Num($n) { if ($null -eq $n -or "$n" -eq '') { return '-' } return ([string]$n) } function Trim-Str([string]$s, [int]$max) { if ([string]::IsNullOrEmpty($s)) { return '' } $s = $s.Trim() if ($s.Length -gt $max) { return $s.Substring(0, $max) } return $s } function Test-Blank([string]$s) { return [string]::IsNullOrEmpty($s) -or ($s.Trim().Length -eq 0) } function Get-WmiSafe { # Returns WMI objects through CIM when available, otherwise Get-WmiObject. param([string]$Class, [string]$Filter = '', [string]$Namespace = 'root\cimv2') $res = @() try { if ($script:HasCim) { $p = @{ ClassName = $Class; Namespace = $Namespace; ErrorAction = 'Stop' } if ($Filter) { $p['Filter'] = $Filter } $res = @(Get-CimInstance @p) } else { $p = @{ Class = $Class; Namespace = $Namespace; ErrorAction = 'Stop' } if ($Filter) { $p['Filter'] = $Filter } $res = @(Get-WmiObject @p) } } catch { $res = @() } return $res } function Convert-WmiDate($value) { if ($null -eq $value) { return $null } try { return [Management.ManagementDateTimeConverter]::ToDateTime($value) } catch { } try { return [datetime]$value } catch { } return $null } function Get-MemoryInfo { # Fallback RAM info when WMI is not available (GlobalMemoryStatusEx). $res = @{ Total = [double]0; Free = [double]0 } try { if (-not ('SmartCheck.Mem' -as [type])) { Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; namespace SmartCheck { public static class Mem { [StructLayout(LayoutKind.Sequential)] public struct MEMORYSTATUSEX { public uint dwLength; public uint dwMemoryLoad; public ulong ullTotalPhys; public ulong ullAvailPhys; public ulong ullTotalPageFile; public ulong ullAvailPageFile; public ulong ullTotalVirtual; public ulong ullAvailVirtual; public ulong ullAvailExtendedVirtual; } [DllImport("kernel32.dll", SetLastError = true)] public static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); } } '@ -ErrorAction Stop } $st = New-Object 'SmartCheck.Mem+MEMORYSTATUSEX' $st.dwLength = [uint32][Runtime.InteropServices.Marshal]::SizeOf($st) if ([SmartCheck.Mem]::GlobalMemoryStatusEx([ref]$st)) { $res.Total = [double]$st.ullTotalPhys $res.Free = [double]$st.ullAvailPhys } } catch { } return $res } function Get-Path-Safe([string]$path) { if (Test-Blank $path) { return '' } try { return [IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($path)) } catch { return $path } } function Resolve-DataDir { $dir = Get-Path-Safe $Cfg.DataDir if (Test-Blank $dir) { if ($env:ProgramData) { $dir = Join-Path $env:ProgramData 'SmartCheck' } elseif ($env:TEMP) { $dir = Join-Path $env:TEMP 'SmartCheck' } else { $dir = Join-Path (Get-Location).Path 'SmartCheck' } } foreach ($sub in @('', 'tools', 'reports')) { $p = if ($sub) { Join-Path $dir $sub } else { $dir } if (-not (Test-Path -LiteralPath $p)) { try { New-Item -ItemType Directory -Path $p -Force -ErrorAction SilentlyContinue | Out-Null } catch { } } } # if the directory is not writable, fall back to TEMP try { $probe = Join-Path $dir '.writetest' Set-Content -LiteralPath $probe -Value 'x' -Encoding ASCII -ErrorAction Stop Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue } catch { $dir = Join-Path $env:TEMP 'SmartCheck' foreach ($sub in @('tools', 'reports')) { $p = Join-Path $dir $sub if (-not (Test-Path -LiteralPath $p)) { New-Item -ItemType Directory -Path $p -Force -ErrorAction SilentlyContinue | Out-Null } } } return $dir } function Get-Sha256([string]$path) { try { $sha = [Security.Cryptography.SHA256]::Create() $fs = [IO.File]::OpenRead($path) try { $hash = $sha.ComputeHash($fs) } finally { $fs.Close(); $sha.Clear() } $sb = New-Object Text.StringBuilder foreach ($b in $hash) { [void]$sb.Append($b.ToString('X2')) } return $sb.ToString() } catch { return '' } } function Download-File([string]$url, [string]$target) { # WebClient exists on every supported PowerShell version. try { $dir = Split-Path -Parent $target if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force -ErrorAction SilentlyContinue | Out-Null } if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Force -ErrorAction SilentlyContinue } $wc = New-Object Net.WebClient try { $wc.Headers.Add('User-Agent', 'SmartCheck/' + $script:Version) } catch { } $wc.DownloadFile($url, $target) if (Test-Path -LiteralPath $target) { if ((Get-Item -LiteralPath $target).Length -gt 0) { return $true } } return $false } catch { return $false } } function Get-ExitCode { return [int]$script:ExitCode } function Set-ExitCode([int]$code) { if ($code -gt [int]$script:ExitCode) { $script:ExitCode = $code } } # ============================================================================== # ELEVATION # ============================================================================== function Start-Elevated { if (-not (Test-Blank $env:SMART_ELEVATED)) { return $false } # do not loop $env:SMART_ELEVATED = '1' $env:SMART_PAUSE = '1' # keep the UAC window open $psExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' if (-not (Test-Path -LiteralPath $psExe)) { $psExe = 'powershell.exe' } $psArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass') if ($script:FromFile) { $psArgs += @('-File', $script:SelfPath) } else { # Started through iex: download the script again in the new window. $loader = "try{[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12}catch{}; " + "iex ((New-Object Net.WebClient).DownloadString('" + $Cfg.ScriptUrl + "'))" $bytes = [Text.Encoding]::Unicode.GetBytes($loader) $psArgs += @('-EncodedCommand', [Convert]::ToBase64String($bytes)) } try { $p = Start-Process -FilePath $psExe -ArgumentList $psArgs -Verb RunAs -PassThru -ErrorAction Stop if ($p) { try { $p.WaitForExit() } catch { } } return $true } catch { return $false } } # ============================================================================== # SMARTCTL # ============================================================================== function Resolve-Smartctl { $target = Join-Path (Join-Path $script:DataDir 'tools') 'smartctl.exe' $candidates = @() if ($env:SMART_SMARTCTL_PATH) { $candidates += $env:SMART_SMARTCTL_PATH } if ($script:SelfPath) { $candidates += (Join-Path (Split-Path -Parent $script:SelfPath) 'smartctl.exe') } $candidates += $Cfg.ExtraSearch $candidates += $target foreach ($c in $candidates) { if (-not (Test-Blank $c) -and (Test-Path -LiteralPath $c)) { $script:SmartctlPath = $c if ($c -eq $target) { Write-Step ($Txt.MToolCache + ': ' + $c) } else { Write-Step ($Txt.MToolLocal + ': ' + $c) } Set-DriveDb return $true } } Write-Step ($Txt.MToolDown + ': ' + $Cfg.SmartctlUrl) $tmp = $target + '.part' if (-not (Download-File $Cfg.SmartctlUrl $tmp)) { Write-Err ($Txt.MToolFail + ' ' + $Cfg.SmartctlUrl) Write-Info $Txt.MToolHint return $false } if (-not (Test-Blank $Cfg.SmartctlSha256)) { $got = Get-Sha256 $tmp if ($got.ToUpper() -ne $Cfg.SmartctlSha256.ToUpper()) { Write-Err ($Txt.MToolHashBad + ' ' + $Cfg.SmartctlSha256 + $Txt.MHashGot + ' ' + $got) Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue Write-Info $Txt.MHashHint return $false } Write-Step $Txt.MToolHash } else { Write-Warn2 $Txt.MToolHashSkip } try { Move-Item -LiteralPath $tmp -Destination $target -Force } catch { $target = Join-Path (Join-Path $env:TEMP 'SmartCheck') 'smartctl.exe' try { Move-Item -LiteralPath $tmp -Destination $target -Force } catch { Write-Err ($Txt.MToolFail + ': ' + $target); return $false } } $script:SmartctlPath = $target Write-Ok ($Txt.MToolOk + ': ' + $target) Set-DriveDb return $true } function Set-DriveDb { # drivedb.h is optional: it only improves model recognition. $dir = Split-Path -Parent $script:SmartctlPath $db = Join-Path $dir 'drivedb.h' if ((Test-Path -LiteralPath $db) -and (-not $Refresh)) { return } if (Test-Blank $Cfg.DrivedbUrl) { return } $marker = Join-Path $dir 'drivedb.lastcheck' $retry = 0 try { $retry = [double]$Cfg.DrivedbRetryHours } catch { } if ($retry -gt 0 -and (Test-Path -LiteralPath $marker)) { $age = (Get-Date) - (Get-Item -LiteralPath $marker).LastWriteTime if ($age.TotalHours -lt $retry) { return } } Write-Step $Txt.MDrivedb if (Download-File $Cfg.DrivedbUrl $db) { Remove-Item -LiteralPath $marker -Force -ErrorAction SilentlyContinue } else { try { Set-Content -LiteralPath $marker -Value (Get-Date -Format 's') -Encoding ASCII } catch { } } } function Invoke-Smartctl { # Note: the parameter must not be called $Args - that clashes with the # PowerShell automatic variable and silently breaks argument binding. param([string[]]$ScArgs = @(), [string]$Device = '', [string[]]$DevArgs = @()) $all = @() $all += $Cfg.SmartctlArgs if ($DevArgs -and $DevArgs.Count -gt 0) { $all += $DevArgs } if ($ScArgs -and $ScArgs.Count -gt 0) { $all += $ScArgs } if (-not (Test-Blank $Device)) { $all += $Device } $lines = @() $script:ScRc = 0 try { $lines = @(& $script:SmartctlPath $all 2>&1 | ForEach-Object { [string]$_ }) $script:ScRc = [int]$LASTEXITCODE } catch { $script:ScRc = -1 $lines = @() } return $lines } function Get-ScanDevices { $devs = @() foreach ($pass in @('--scan', '--scan-open')) { $lines = @(Invoke-Smartctl -ScArgs @($pass)) foreach ($l in $lines) { if (Test-Blank $l) { continue } if ($l.StartsWith('#')) { continue } if ($l -match '^/dev/') { $parts = @($l -split '\s+') $path = $parts[0] $devArgs = @() for ($i = 0; $i -lt $parts.Count; $i++) { if ($parts[$i] -eq '-d' -and ($i + 1) -lt $parts.Count) { $devArgs = @('-d', $parts[$i + 1]); break } } $exists = $false foreach ($d in $devs) { if ($d.Path -eq $path) { $exists = $true; break } } if (-not $exists) { $devs += @{ Path = $path; DevArgs = $devArgs } } } } if ($devs.Count -gt 0) { break } Write-Warn2 $Txt.MScanNone if ($pass -eq '--scan') { Write-Step $Txt.MScanOpen } } return ,$devs } function Get-DiskIndexFromPath([string]$path) { if ($path -match '/dev/sd([a-z])') { return ([int][char]$Matches[1] - [int][char]'a') } if ($path -match '/dev/pd(\d+)') { return [int]$Matches[1] } if ($path -match 'PhysicalDrive(\d+)') { return [int]$Matches[1] } if ($path -match '/dev/nvme(\d+)') { return [int]$Matches[1] } return -1 } function Normalize-Serial([string]$s) { if (Test-Blank $s) { return '' } return (($s.ToUpper() -replace '[^A-Z0-9]', '')).Trim() } # ============================================================================== # SYSTEM INVENTORY # ============================================================================== function Get-SystemInventory { $sys = @{} $os = @(Get-WmiSafe 'Win32_OperatingSystem') $o = if ($os.Count -gt 0) { $os[0] } else { $null } $cs = @(Get-WmiSafe 'Win32_ComputerSystem') $c = if ($cs.Count -gt 0) { $cs[0] } else { $null } $bi = @(Get-WmiSafe 'Win32_BIOS') $b = if ($bi.Count -gt 0) { $bi[0] } else { $null } $cp = @(Get-WmiSafe 'Win32_ComputerSystemProduct') $pr = if ($cp.Count -gt 0) { $cp[0] } else { $null } $mem = @(Get-WmiSafe 'Win32_PhysicalMemory') $cpus = @(Get-WmiSafe 'Win32_Processor') # --- OS ------------------------------------------------------------------- if ($o) { $sys['OSCaption'] = Trim-Str ([string]$o.Caption) 80 $sys['OSVersion'] = [string]$o.Version $sys['OSBuild'] = [string]$o.BuildNumber $sys['OSArch'] = [string]$o.OSArchitecture $boot = Convert-WmiDate $o.LastBootUpTime $sys['BootTime'] = $boot if ($boot) { $up = (Get-Date) - $boot $sys['Uptime'] = ($Txt.FUptime -f [int]$up.Days, [int]$up.Hours, [int]$up.Minutes) } $sys['RamTotal'] = [double]$o.TotalVisibleMemorySize * 1KB $sys['RamFree'] = [double]$o.FreePhysicalMemory * 1KB } $sys['PSVersion'] = $PSVersionTable.PSVersion.ToString() $sys['Mode'] = $script:Mode # --- fallbacks when WMI is blocked or broken ------------------------------ if ((Test-Blank ([string]$sys['OSCaption'])) -or (Test-Blank ([string]$sys['OSVersion'])) -or (-not $sys['RamTotal'])) { $sys['WmiNote'] = $Txt.MWmiFallback } if ((Test-Blank ([string]$sys['OSCaption'])) -or (Test-Blank ([string]$sys['OSVersion']))) { try { $rk = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop $ver = [Environment]::OSVersion.Version $cap = [string]$rk.ProductName $bld = 0 if ($rk.CurrentBuildNumber) { try { $bld = [int]$rk.CurrentBuildNumber } catch { } } if ($bld -le 0) { $bld = $ver.Build } if ($bld -ge 22000 -and $cap -match 'Windows 10') { $cap = $cap -replace 'Windows 10', 'Windows 11' } if (Test-Blank ([string]$sys['OSCaption'])) { $sys['OSCaption'] = Trim-Str $cap 80 } if (Test-Blank ([string]$sys['OSVersion'])) { $sys['OSVersion'] = ('{0}.{1}.{2}' -f $ver.Major, $ver.Minor, $ver.Build) } if (Test-Blank ([string]$sys['OSBuild'])) { $sys['OSBuild'] = [string]$bld } } catch { } try { if (Test-Blank ([string]$sys['OSArch'])) { $sys['OSArch'] = if ([Environment]::Is64BitOperatingSystem) { '64-bit' } else { '32-bit' } } } catch { } } if (-not $sys['RamTotal']) { $mi = Get-MemoryInfo if ($mi.Total -gt 0) { $sys['RamTotal'] = $mi.Total $sys['RamFree'] = $mi.Free } } if (Test-Blank ([string]$sys['CpuName'])) { if ($env:PROCESSOR_IDENTIFIER) { $sys['CpuName'] = Trim-Str ([string]$env:PROCESSOR_IDENTIFIER) 70 } if ($env:NUMBER_OF_PROCESSORS) { $sys['CpuThreads'] = [int]$env:NUMBER_OF_PROCESSORS } if ($env:PROCESSOR_ARCHITECTURE -and (Test-Blank ([string]$sys['OSArch']))) { $sys['OSArch'] = [string]$env:PROCESSOR_ARCHITECTURE } } # --- platform ------------------------------------------------------------- if ($c) { $sys['Manufacturer'] = Trim-Str ([string]$c.Manufacturer) 40 $sys['Model'] = Trim-Str ([string]$c.Model) 40 $sys['Domain'] = [string]$c.Domain $sys['InDomain'] = [bool]$c.PartOfDomain $sys['CsUser'] = [string]$c.UserName if (-not $sys['RamTotal']) { $sys['RamTotal'] = [double]$c.TotalPhysicalMemory } } if ($b) { $sys['BiosVersion'] = Trim-Str ([string]$b.SMBIOSBIOSVersion) 40 $sys['BiosDate'] = [string]$b.ReleaseDate $sys['BiosSerial'] = Trim-Str ([string]$b.SerialNumber) 40 } if ($pr) { $sys['UUID'] = Trim-Str ([string]$pr.UUID) 40 } # --- CPU ------------------------------------------------------------------ if ($cpus.Count -gt 0) { $sys['CpuName'] = Trim-Str ([string]$cpus[0].Name) 70 $cores = 0; $logical = 0; $mhz = 0 foreach ($cpu in $cpus) { $cores += [int]$cpu.NumberOfCores $logical += [int]$cpu.NumberOfLogicalProcessors if ([int]$cpu.MaxClockSpeed -gt $mhz) { $mhz = [int]$cpu.MaxClockSpeed } } $sys['CpuCores'] = $cores $sys['CpuThreads'] = $logical $sys['CpuMHz'] = $mhz if ($cpus.Count -gt 1) { $sys['CpuSockets'] = $cpus.Count } } # --- RAM ------------------------------------------------------------------ $sys['RamModules'] = $mem.Count if ($mem.Count -gt 0) { $mods = @() foreach ($m in $mem) { $one = Format-Size ([double]$m.Capacity) if ($m.Speed) { $one += ' ' + [string]$m.Speed + ' ' + $Txt.LCMHz } $mfg = Trim-Str ([string]$m.Manufacturer) 16 $pn = Trim-Str ([string]$m.PartNumber) 24 if ($mfg) { $one += ' ' + $mfg } if ($pn) { $one += ' ' + $pn } $mods += $one.Trim() } $sys['RamDetail'] = ($mods -join ' | ') } # --- Windows activation (Win7+) ------------------------------------------- try { $lic = @(Get-WmiSafe 'SoftwareLicensingProduct' 'PartialProductKey IS NOT NULL') if ($lic.Count -gt 0) { $st = [int]$lic[0].LicenseStatus $map = @{ 0 = $Txt.FActNo; 1 = $Txt.FActYes; 2 = $Txt.FActGrace; 3 = $Txt.FActGrace2; 4 = $Txt.FActTimer; 5 = $Txt.FActNotify; 6 = $Txt.FActGrace3 } if ($map.ContainsKey($st)) { $sys['Activation'] = $map[$st] } else { $sys['Activation'] = $Txt.FActStatus + ' ' + $st } } } catch { } # --- Modern only: Secure Boot, TPM, physical disks ------------------------ if ($script:Mode -eq 'Modern') { try { $sb = Confirm-SecureBootUEFI -ErrorAction Stop $sys['SecureBoot'] = if ($sb) { $Txt.FSecOn } else { $Txt.FSecOff } } catch { } try { $tpm = Get-CimInstance -Namespace 'root\cimv2\security\microsofttpm' -ClassName Win32_Tpm -ErrorAction Stop if ($tpm) { $sys['Tpm'] = $Txt.FTpmPresent + ' ' + (Trim-Str ([string]$tpm.SpecVersion) 12) + $(if ($tpm.IsEnabled_InitialValue) { $Txt.FTpmOn } else { $Txt.FTpmOff }) } } catch { } try { $pd = @(Get-PhysicalDisk -ErrorAction Stop) if ($pd.Count -gt 0) { $kinds = @() foreach ($d in $pd) { $kinds += ([string]$d.MediaType + '/' + [string]$d.BusType) } $sys['PhysicalKinds'] = ($kinds -join ', ') } } catch { } } # --- network -------------------------------------------------------------- $nic = @(Get-WmiSafe 'Win32_NetworkAdapterConfiguration' 'IPEnabled=True') $sys['Nics'] = @() foreach ($n in $nic) { $ip = '' try { if ($n.IPAddress) { foreach ($a in @($n.IPAddress)) { if ($a -notmatch ':' -and $a -notmatch '^169\.254\.') { $ip = [string]$a; break } } if (Test-Blank $ip) { $ip = [string]@($n.IPAddress)[0] } } } catch { } $gw = '' try { if ($n.DefaultIPGateway) { $gw = [string]@($n.DefaultIPGateway)[0] } } catch { } $sys['Nics'] += @{ Desc = Trim-Str ([string]$n.Description) 60 IP = $ip MAC = [string]$n.MACAddress Gw = $gw } } return $sys } # ============================================================================== # DISK INVENTORY # ============================================================================== function Get-VolumeMap { # drive letter -> size / free space (used in the report) $map = @{} foreach ($v in @(Get-WmiSafe 'Win32_LogicalDisk' 'DriveType=3')) { $letter = ([string]$v.DeviceID).Trim().ToUpper() if (Test-Blank $letter) { continue } $map[$letter] = @{ Size = [double]$v.Size; Free = [double]$v.FreeSpace } } return $map } function Get-PhysicalDiskMap { # Maps a Windows disk to /dev/sdX by serial number, then by disk index. $items = @() $drives = @(Get-WmiSafe 'Win32_DiskDrive') foreach ($d in $drives) { $idx = -1 try { $idx = [int]$d.Index } catch { } $letters = @() if ($script:HasPartition) { try { $parts = @(Get-Partition -DiskNumber $idx -ErrorAction SilentlyContinue) foreach ($p in $parts) { if ($p.DriveLetter) { $letters += ([string]$p.DriveLetter + ':') } } } catch { } } if ($letters.Count -eq 0) { try { $parts = @(Get-WmiSafe 'Win32_DiskPartition' ("DiskIndex=$idx")) foreach ($part in $parts) { $did = [string]$part.DeviceID if (Test-Blank $did) { continue } $q = "ASSOCIATORS OF {Win32_DiskPartition.DeviceID='$did'} WHERE AssocClass=Win32_LogicalDiskToPartition" try { if ($script:HasCim) { $a = @(Get-CimInstance -Query $q -ErrorAction SilentlyContinue) } else { $a = @(Get-WmiObject -Query $q -ErrorAction SilentlyContinue) } foreach ($x in $a) { if ($x.DeviceID) { $letters += [string]$x.DeviceID } } } catch { } } } catch { } } $items += @{ Index = $idx Serial = Normalize-Serial ([string]$d.SerialNumber) Model = Trim-Str ([string]$d.Model) 60 Size = [double]$d.Size Bus = [string]$d.InterfaceType Letters = (@($letters | Sort-Object -Unique) -join ' ') } } return ,$items } function Find-PhysicalDisk($items, [string]$serial, [int]$index) { $ns = Normalize-Serial $serial if (-not (Test-Blank $ns)) { foreach ($it in $items) { if (Test-Blank $it.Serial) { continue } if ($it.Serial -eq $ns) { return $it } if ($it.Serial.Length -ge 8 -and $ns.Length -ge 8) { if ($it.Serial.Contains($ns) -or $ns.Contains($it.Serial)) { return $it } } } } if ($index -ge 0) { foreach ($it in $items) { if ($it.Index -eq $index) { return $it } } } return $null } function Get-AtaAttributes { # Parses the ATA table: ID NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW param([string[]]$Lines) $attrs = @{} foreach ($line in $Lines) { if ($line -notmatch '^\s*(\d{1,3})\s') { continue } $parts = @($line.Trim() -split '\s+') if ($parts.Count -lt 10) { continue } try { $id = [int]$parts[0] $val = [int]$parts[3] $raw = ($parts[9..($parts.Count - 1)] -join ' ') $attrs[$id] = @{ Id = $id; Value = $val; Raw = $raw.Trim() } } catch { continue } } return $attrs } function Get-RawNumber([string]$raw) { if (Test-Blank $raw) { return 0 } $t = ($raw -replace '[()]', ' ' -replace '\s+', ' ').Trim() $first = ($t -split ' ')[0] $digits = ($first -replace '[^0-9]', '') if (Test-Blank $digits) { return 0 } try { return [double]$digits } catch { return 0 } } function Get-DiskInventory { $devs = Get-ScanDevices $pdItems = Get-PhysicalDiskMap $script:Volumes = Get-VolumeMap $diskList = @() $n = 0 foreach ($dev in $devs) { $n++ $path = [string]$dev.Path $devArgs = @($dev.DevArgs) Write-Step ("[{0}/{1}] {2}" -f $n, $devs.Count, $path) $info = @(Invoke-Smartctl -ScArgs @('-i') -Device $path -DevArgs $devArgs) $rcInfo = $script:ScRc # if the device did not open, try alternative protocols $opened = $false foreach ($l in $info) { if ($l -match 'Device Model:|Model Number:|Product:|Vendor:|Model Family:') { $opened = $true; break } } if (-not $opened) { foreach ($proto in $Cfg.Fallbacks) { $alt = @('-d', $proto) $tryLines = @(Invoke-Smartctl -ScArgs @('-i') -Device $path -DevArgs $alt) $ok = $false foreach ($l in $tryLines) { if ($l -match 'Device Model:|Model Number:|Product:|Vendor:|Model Family:') { $ok = $true; break } } if ($ok) { $info = $tryLines; $devArgs = $alt; $rcInfo = $script:ScRc; $opened = $true; break } } } $d = @{ Path = $path; DevArgs = $devArgs; RcInfo = $rcInfo Model = ''; Serial = ''; Firmware = ''; Kind = $Txt.KindUnknown Capacity = 0; CapacityText = $Txt.LDash Letters = ''; Bus = ''; SmartStatus = $Txt.VUnknown Status = $Txt.VUnknown; Bad = @(); Warn = @(); Info = @() PowerOnHours = $null; PowerCycles = $null; Temperature = $null Wear = $null; UsedResource = $null; Spare = $null; PercUsed = $null; CritWarn = '' Written = ''; Read = ''; AtaErrors = $null; SelfTestErrors = $null; LastSelfTest = '' RawInfo = $info; RawHealth = @(); RawAttr = @(); RawErr = @(); RawSelf = @() Opened = $opened; Note = '' } if (-not $opened) { if (($rcInfo -band 2) -ne 0 -or $rcInfo -eq -1) { $d.Status = $Txt.VNoAccess; $d.Note = $Txt.MNoAccess } else { $d.Note = $Txt.MNoSmart } Write-Warn2 ($path + ': ' + $d.Note) $diskList += $d continue } # --- identity --------------------------------------------------------- $isNvme = $false foreach ($l in $info) { $cl = $l -replace '[\u00A0\u202F\u2009]', ' ' if ($cl -match 'NVMe Version|Model Number:') { $isNvme = $true; break } } foreach ($l in $info) { $cl = $l -replace '[\u00A0\u202F\u2009]', ' ' if ($cl -match 'Device Model:\s*(.+)') { if (Test-Blank $d.Model) { $d.Model = Trim-Str $Matches[1].Trim() 60 } } if ($cl -match 'Model Number:\s*(.+)') { if (Test-Blank $d.Model) { $d.Model = Trim-Str $Matches[1].Trim() 60 } } if ($cl -match 'Product:\s*(.+)') { if (Test-Blank $d.Model) { $d.Model = Trim-Str $Matches[1].Trim() 60 } } if ($cl -match 'Model Family:\s*(.+)') { if (Test-Blank $d.Model) { $d.Model = Trim-Str $Matches[1].Trim() 60 } } if ($cl -match 'Serial Number:\s*(.+)') { if (Test-Blank $d.Serial) { $d.Serial = Trim-Str $Matches[1].Trim() 40 } } if ($cl -match 'Firmware Version:\s*(.+)') { $d.Firmware = Trim-Str $Matches[1].Trim() 20 } if ($cl -match 'Revision:\s*(.+)') { if (Test-Blank $d.Firmware) { $d.Firmware = Trim-Str $Matches[1].Trim() 20 } } if ($cl -match 'Rotation Rate:\s*(.+)') { if ($Matches[1] -match 'Solid') { $d.Kind = $Txt.KindSsd } } if ($cl -match 'Solid State Device') { $d.Kind = $Txt.KindSsd } } if ($isNvme) { $d.Kind = $Txt.KindNvme } elseif ($d.Kind -eq $Txt.KindUnknown) { $d.Kind = $Txt.KindHdd } foreach ($l in $info) { $cl = $l -replace '[\u00A0\u202F\u2009]', ' ' if ($cl -match 'User Capacity:\s*([\d,]+)\s*(?:bytes)?\s*\[([^\]]+)\]') { try { $d.Capacity = [double]($Matches[1] -replace ',', '') } catch { } $d.CapacityText = $Matches[2].Trim() } elseif ($cl -match 'Total NVM Capacity:\s*([\d,]+)\s*(?:bytes)?\s*\[([^\]]+)\]') { try { $d.Capacity = [double]($Matches[1] -replace ',', '') } catch { } $d.CapacityText = $Matches[2].Trim() } elseif ($cl -match 'Namespace \d+ Size/Capacity:\s*([\d,]+)\s*(?:bytes)?\s*\[([^\]]+)\]') { if ($d.Capacity -le 0) { try { $d.Capacity = [double]($Matches[1] -replace ',', '') } catch { } $d.CapacityText = $Matches[2].Trim() } } elseif ($cl -match 'User Capacity:.*\[([^\]]+)\]') { $d.CapacityText = $Matches[1].Trim() } } # --- link to Windows -------------------------------------------------- $idx = Get-DiskIndexFromPath $path $pd = Find-PhysicalDisk $pdItems $d.Serial $idx if ($pd) { $d.Letters = $pd.Letters $d.Bus = $pd.Bus if ($d.CapacityText -eq $Txt.LDash -and $pd.Size -gt 0) { $d.CapacityText = Format-Size $pd.Size; $d.Capacity = $pd.Size } if (Test-Blank $d.Model -and -not (Test-Blank $pd.Model)) { $d.Model = $pd.Model } } # --- health ----------------------------------------------------------- $health = @(Invoke-Smartctl -ScArgs @('-H') -Device $path -DevArgs $devArgs) $rcH = $script:ScRc $d.RawHealth = $health $d.SmartStatus = $Txt.VUnknown foreach ($l in $health) { if ($l -match 'SMART overall-health self-assessment test result:\s*(.+)') { $d.SmartStatus = Trim-Str $Matches[1].Trim() 30 } elseif ($l -match 'SMART Health Status:\s*(.+)') { $d.SmartStatus = Trim-Str $Matches[1].Trim() 30 } } if ($d.SmartStatus -eq $Txt.VUnknown) { if (($rcH -band 8) -ne 0) { $d.SmartStatus = 'FAILED' } elseif (($rcH -band 4) -eq 0 -and ($rcH -band 2) -eq 0) { $d.SmartStatus = 'PASSED' } } $failed = ($d.SmartStatus -match 'FAILED') -or (($rcH -band 8) -ne 0) # --- attributes ------------------------------------------------------- $attr = @(Invoke-Smartctl -ScArgs @('-A') -Device $path -DevArgs $devArgs) $d.RawAttr = $attr if ($d.Kind -eq $Txt.KindNvme -or $isNvme) { $d.Kind = $Txt.KindNvme foreach ($l in $attr) { $cl = $l -replace '[\u00A0\u202F\u2009]', ' ' if ($cl -match '^Temperature:\s*(\d+)') { $d.Temperature = [int]$Matches[1] } elseif ($cl -match 'Temperature Sensor \d+:\s*(\d+)') { if ($null -eq $d.Temperature) { $d.Temperature = [int]$Matches[1] } } if ($cl -match 'Power On Hours:\s*([\d,\s]+)') { $d.PowerOnHours = [double](($Matches[1] -replace '[^\d]', '')) } if ($cl -match 'Power Cycles:\s*([\d,\s]+)') { $d.PowerCycles = [double](($Matches[1] -replace '[^\d]', '')) } if ($cl -match 'Percentage Used:\s*(\d+)\s*%') { $d.PercUsed = [int]$Matches[1]; $d.Wear = 100 - [int]$Matches[1]; $d.UsedResource = [int]$Matches[1] } if ($cl -match 'Available Spare:\s*(\d+)\s*%') { $d.Spare = [int]$Matches[1] } if ($cl -match 'Critical Warning:\s*(0x[0-9a-fA-F]+)') { $d.CritWarn = $Matches[1] } if ($cl -match 'Media and Data Integrity Errors:\s*([\d,]+)') { $d.MediaErrors = [double](($Matches[1] -replace '[^\d]', '')) } if ($cl -match 'Unsafe Shutdowns:\s*([\d,]+)') { $d.UnsafeShutdowns = [double](($Matches[1] -replace '[^\d]', '')) } if ($cl -match 'Data Units Written:\s*(.+?)\[([^\]]+)\]') { $d.Written = $Matches[2].Trim() } if ($cl -match 'Data Units Read:\s*(.+?)\[([^\]]+)\]') { $d.Read = $Matches[2].Trim() } } } else { $attrs = Get-AtaAttributes $attr # SCSI/SAS devices have no ATA attribute table: use grown defects and counters foreach ($l in $attr) { if ($l -match 'Elements in grown defect list:\s*([\d,]+)') { $d.GrownDefects = [double](($Matches[1] -replace '[^\d]', '')) } if ($l -match 'Non-medium error count:\s*([\d,]+)') { $d.NonMedium = [double](($Matches[1] -replace '[^\d]', '')) } } if ($attrs.ContainsKey(5)) { $d.Reallocated = Get-RawNumber $attrs[5].Raw } if ($attrs.ContainsKey(196)) { $d.ReallocEvent = Get-RawNumber $attrs[196].Raw } if ($attrs.ContainsKey(197)) { $d.Pending = Get-RawNumber $attrs[197].Raw } if ($attrs.ContainsKey(198)) { $d.Uncorrectable = Get-RawNumber $attrs[198].Raw } if ($attrs.ContainsKey(199)) { $d.CrcErrors = Get-RawNumber $attrs[199].Raw } if ($attrs.ContainsKey(10)) { $d.SpinRetry = Get-RawNumber $attrs[10].Raw } if ($attrs.ContainsKey(9)) { $d.PowerOnHours = Get-RawNumber $attrs[9].Raw } if ($attrs.ContainsKey(12)) { $d.PowerCycles = Get-RawNumber $attrs[12].Raw } if ($attrs.ContainsKey(194)) { $d.Temperature = [int](Get-RawNumber $attrs[194].Raw) } elseif ($attrs.ContainsKey(190)) { $d.Temperature = [int](Get-RawNumber $attrs[190].Raw) } if ($attrs.ContainsKey(241)) { $raw = [string]$attrs[241].Raw if ($raw -match '\[([^\]]+)\]') { $d.Written = $Matches[1].Trim() } else { $lba = Get-RawNumber $raw if ($lba -gt 0) { $d.Written = '~' + (Format-Size ($lba * 512)) } } } if ($attrs.ContainsKey(242)) { $raw = [string]$attrs[242].Raw if ($raw -match '\[([^\]]+)\]') { $d.Read = $Matches[1].Trim() } else { $lba = Get-RawNumber $raw if ($lba -gt 0) { $d.Read = '~' + (Format-Size ($lba * 512)) } } } if ($d.Kind -eq $Txt.KindSsd) { foreach ($id in @(177, 202, 231, 233)) { if ($attrs.ContainsKey($id)) { $d.Wear = [int]$attrs[$id].Value; break } } if ($null -ne $d.Wear) { $d.UsedResource = 100 - $d.Wear } } } # --- logs ------------------------------------------------------------- $errLog = @(Invoke-Smartctl -ScArgs @('-l', 'error') -Device $path -DevArgs $devArgs) $rcE = $script:ScRc $d.RawErr = $errLog foreach ($l in $errLog) { if ($l -match 'ATA Error Count:\s*(\d+)') { $d.AtaErrors = [double]$Matches[1] } } if ($null -eq $d.AtaErrors -and (($rcE -band 64) -ne 0)) { $d.AtaErrors = -1 } $selfLog = @(Invoke-Smartctl -ScArgs @('-l', 'selftest') -Device $path -DevArgs $devArgs) $rcS = $script:ScRc $d.RawSelf = $selfLog $d.SelfTestErrors = 0 foreach ($l in $selfLog) { if ($l -match 'was completed without error') { if (Test-Blank $d.LastSelfTest) { $d.LastSelfTest = Trim-Str $l.Trim() 70 } } if ($l -match 'read failure|write failure|electrical failure|Servo|Unknown|Interrupted') { $d.SelfTestErrors = [double]$d.SelfTestErrors + 1 } } if (($rcS -band 128) -ne 0 -and $d.SelfTestErrors -eq 0) { $d.SelfTestErrors = -1 } # --- verdict ---------------------------------------------------------- $Set = $Cfg.Th if ($failed) { $d.Bad += $Txt.R_SmartFail } if ($d.SmartStatus -eq $Txt.VUnknown -and -not $failed) { $d.Info += $Txt.MSmartUnknown } if ($d.Kind -eq $Txt.KindHdd) { if ($null -ne $d.Reallocated) { if ($d.Reallocated -gt 0) { $d.Warn += ($Txt.R_Realloc -f (Format-Num $d.Reallocated)) } if ($d.Reallocated -gt $Set.HddReallocBad) { $d.Bad += ($Txt.R_Realloc -f (Format-Num $d.Reallocated)) } } if ($null -ne $d.ReallocEvent -and $d.ReallocEvent -gt 0) { $d.Warn += ($Txt.R_ReallocEv -f (Format-Num $d.ReallocEvent)) } if ($null -ne $d.Pending) { if ($d.Pending -gt 0) { $d.Warn += ($Txt.R_Pending -f (Format-Num $d.Pending)) } if ($d.Pending -gt $Set.HddPendingBad) { $d.Bad += ($Txt.R_Pending -f (Format-Num $d.Pending)) } } if ($null -ne $d.Uncorrectable -and $d.Uncorrectable -gt 0) { $d.Bad += ($Txt.R_Uncorr -f (Format-Num $d.Uncorrectable)) } if ($null -ne $d.CrcErrors -and $d.CrcErrors -gt 0) { $d.Warn += ($Txt.R_Crc -f (Format-Num $d.CrcErrors)) } if ($null -ne $d.SpinRetry -and $d.SpinRetry -gt 0) { $d.Warn += ($Txt.R_Spin -f (Format-Num $d.SpinRetry)) } if ($null -ne $d.GrownDefects -and $d.GrownDefects -gt 0) { $d.Warn += ($Txt.R_Grown -f (Format-Num $d.GrownDefects)) } if ($null -ne $d.Temperature -and $d.Temperature -gt 0) { if ($d.Temperature -ge $Set.TempBad) { $d.Bad += ($Txt.R_TempBad -f $d.Temperature) } elseif ($d.Temperature -ge $Set.TempWarn) { $d.Warn += ($Txt.R_TempWarn -f $d.Temperature) } } } elseif ($d.Kind -eq $Txt.KindNvme -or $d.Kind -eq $Txt.KindSsd) { $isN = ($d.Kind -eq $Txt.KindNvme) if ($null -ne $d.Wear) { if ($d.Wear -lt $Set.WearBad) { $d.Bad += ($Txt.R_Wear -f $d.Wear) } elseif ($d.Wear -lt $Set.WearWarn) { $d.Warn += ($Txt.R_WearWarn -f $d.Wear) } elseif ($null -ne $d.UsedResource) { $d.Info += ($Txt.R_UsedRes -f $d.UsedResource) } } if ($null -ne $d.Temperature -and $d.Temperature -gt 0) { $tw = if ($isN) { $Set.TempNvmeWarn } else { $Set.TempWarn } $tb = if ($isN) { $Set.TempNvmeBad } else { $Set.TempBad } if ($d.Temperature -ge $tb) { $d.Bad += ($Txt.R_TempBad -f $d.Temperature) } elseif ($d.Temperature -ge $tw) { $d.Warn += ($Txt.R_TempWarn -f $d.Temperature) } } if ($isN) { if ($null -ne $d.Spare) { if ($d.Spare -lt $Set.SpareBad) { $d.Bad += ($Txt.R_Spare -f $d.Spare) } elseif ($d.Spare -lt $Set.SpareWarn) { $d.Warn += ($Txt.R_Spare -f $d.Spare) } } elseif ($d.Opened) { $d.Info += $Txt.MSpareUnknown } if (-not (Test-Blank $d.CritWarn) -and $d.CritWarn -notmatch '^0x0+$') { $d.Bad += ($Txt.R_CritWarn -f $d.CritWarn) } if ($null -ne $d.MediaErrors -and $d.MediaErrors -gt 0) { $d.Bad += ($Txt.R_MediaErr -f (Format-Num $d.MediaErrors)) } if ($null -ne $d.UnsafeShutdowns -and $d.UnsafeShutdowns -gt 0) { $d.Info += ($Txt.R_Unsafe -f (Format-Num $d.UnsafeShutdowns)) } } } if ($null -ne $d.AtaErrors) { if ($d.AtaErrors -gt 0) { $d.Warn += ($Txt.R_AtaErr -f (Format-Num $d.AtaErrors)) } elseif ($d.AtaErrors -lt 0) { $d.Warn += ($Txt.R_AtaErr -f $Txt.MHasRecords) } } if ($null -ne $d.SelfTestErrors) { if ($d.SelfTestErrors -gt 0) { $d.Warn += ($Txt.R_SelfTest -f (Format-Num $d.SelfTestErrors)) } elseif ($d.SelfTestErrors -lt 0) { $d.Warn += ($Txt.R_SelfTest -f $Txt.MHasRecords) } } # unique reasons, BAD has priority over WARNING if ($d.Bad.Count -gt 0) { $d.Warn = @($d.Warn | Where-Object { $d.Bad -notcontains $_ }) $d.Status = $Txt.VBad } elseif ($d.Warn.Count -gt 0) { $d.Status = $Txt.VWarn } elseif ($d.Opened) { $d.Status = $Txt.VOk } $d.Info = @($d.Info | Where-Object { $d.Bad -notcontains $_ -and $d.Warn -notcontains $_ }) $diskList += $d Write-Info ($path + ': ' + $d.Model + ' | ' + $d.Kind + ' | ' + $d.Status) } return ,$diskList } # ============================================================================== # REPORT # ============================================================================== function Get-VolumeText($letters) { # "C: E:" -> "C: E: (C: 120.5 GB free of 465.8 GB)" if (Test-Blank $letters) { return $Txt.LDash } $text = [string]$letters if (-not $script:Volumes) { return $text } $extra = @() foreach ($lt in ($text -split '\s+')) { $key = $lt.Trim().ToUpper() if (Test-Blank $key) { continue } if ($script:Volumes.ContainsKey($key)) { $v = $script:Volumes[$key] $extra += ($key + ' ' + (Format-Size $v.Free) + ' ' + $Txt.LFreeOf + ' ' + (Format-Size $v.Size)) } } if ($extra.Count -gt 0) { return ($text + ' (' + ($extra -join '; ') + ')') } return $text } function New-Report { param($Sys, $Disks, [double]$Elapsed, [bool]$IsAdmin) if ($null -eq $Disks) { $Disks = @() } $sb = New-Object Text.StringBuilder $line = '=' * 78 function Add([string]$s) { [void]$sb.AppendLine($s) } Add $line Add (' ' + $Txt.Title) Add (' ' + $Txt.LScript + ': v' + $script:Version + ' | ' + $Txt.LMode + ': ' + $script:Mode + ' | PowerShell ' + $Sys['PSVersion']) Add $line # --- run parameters ------------------------------------------------------- if (-not (Test-Blank $Comment)) { Add ($Txt.LComment.PadRight(18) + ': ' + $Comment) } Add ($Txt.LComputer.PadRight(18) + ': ' + $env:COMPUTERNAME) $userStr = $env:USERDOMAIN + '\' + $env:USERNAME if (-not (Test-Blank $Sys['CsUser'])) { $userStr = $Sys['CsUser'] } Add ($Txt.LUser.PadRight(18) + ': ' + $userStr + ' (' + $Txt.LAdmin + ': ' + $(if ($IsAdmin) { $Txt.LYes } else { $Txt.LNo }) + ')') Add ($Txt.LDate.PadRight(18) + ': ' + (Get-Date -Format 'dd.MM.yyyy HH:mm:ss') + ' (' + $Txt.T_Time + ' ' + $Elapsed.ToString('0.0', [Globalization.CultureInfo]::InvariantCulture) + ' ' + $Txt.T_Sec + ')') if (-not (Test-Blank $script:SmartctlPath)) { $scVer = '' try { $v = @(Invoke-Smartctl -ScArgs @('--version')) foreach ($l in $v) { if ($l -match '^smartctl\s+([\d\.]+)') { $scVer = $Matches[1]; break } } } catch { } Add ($Txt.LSmartctl.PadRight(18) + ': ' + $scVer + ' (' + $script:SmartctlPath + ')') } Add '' # --- system --------------------------------------------------------------- Add ('--- ' + $Txt.SecSystem + ' ' + ('-' * (74 - $Txt.SecSystem.Length))) $osStr = [string]$Sys['OSCaption'] if (-not (Test-Blank $Sys['OSVersion'])) { $verStr = [string]$Sys['OSVersion'] if (-not (Test-Blank $Sys['OSBuild'])) { if ($verStr -notmatch ('\.' + [regex]::Escape([string]$Sys['OSBuild']) + '$')) { $verStr = $verStr + '.' + $Sys['OSBuild'] } } $osStr += ' (v' + $verStr + ', ' + $Sys['OSArch'] + ')' } Add ($Txt.LOS.PadRight(18) + ': ' + $osStr) if (-not (Test-Blank $Sys['WmiNote'])) { Add (' '.PadRight(20) + $Sys['WmiNote']) } if ($Sys['BootTime']) { Add ($Txt.LBoot.PadRight(18) + ': ' + $Sys['BootTime'].ToString('dd.MM.yyyy HH:mm') + ' (' + $Txt.LUptime + ' ' + $Sys['Uptime'] + ')') } $plat = (([string]$Sys['Manufacturer']) + ' ' + ([string]$Sys['Model'])).Trim() if (-not (Test-Blank $plat)) { Add ($Txt.LPlatform.PadRight(18) + ': ' + $plat) } if (-not (Test-Blank $Sys['BiosVersion'])) { Add ($Txt.LBios.PadRight(18) + ': ' + $Sys['BiosVersion'] + ' (' + [string]$Sys['BiosDate'] + ')') } if (-not (Test-Blank $Sys['CpuName'])) { $cpu = [string]$Sys['CpuName'] if ($Sys['CpuCores'] -and $Sys['CpuThreads']) { $cpu += ' - ' + [string]$Sys['CpuCores'] + '/' + [string]$Sys['CpuThreads'] + ' ' + $Txt.LCores } elseif ($Sys['CpuThreads']) { $cpu += ' - ' + $Txt.LCores + ': ' + [string]$Sys['CpuThreads'] } elseif ($Sys['CpuCores']) { $cpu += ' - ' + $Txt.LCores + ': ' + [string]$Sys['CpuCores'] } if ($Sys['CpuMHz']) { $cpu += ', ' + [string]$Sys['CpuMHz'] + ' ' + $Txt.LCMHz } if ($Sys['CpuSockets']) { $cpu += ', ' + $Txt.LSockets + ': ' + [string]$Sys['CpuSockets'] } Add ($Txt.LCPU.PadRight(18) + ': ' + $cpu) } if ($Sys['RamTotal']) { $ram = Format-Size $Sys['RamTotal'] if ($Sys['RamModules']) { $ram += ' (' + [string]$Sys['RamModules'] + ' ' + $Txt.LRamModules + ')' } Add ($Txt.LRam.PadRight(18) + ': ' + $ram) if (-not (Test-Blank $Sys['RamDetail'])) { Add (' '.PadRight(20) + $Sys['RamDetail']) } if ($Sys['RamFree']) { $pct = 0 if ($Sys['RamTotal'] -gt 0) { $pct = [int][math]::Round(100 * $Sys['RamFree'] / $Sys['RamTotal']) } Add ($Txt.LRamFree.PadRight(18) + ': ' + (Format-Size $Sys['RamFree']) + ' (' + [string]$pct + $Txt.LFreePct + ')') } } if (-not (Test-Blank $Sys['Activation'])) { Add ($Txt.LActivation.PadRight(18) + ': ' + $Sys['Activation']) } if (-not (Test-Blank $Sys['SecureBoot'])) { Add ($Txt.LSecureBoot.PadRight(18) + ': ' + $Sys['SecureBoot']) } if (-not (Test-Blank $Sys['Tpm'])) { Add ($Txt.LTpm.PadRight(18) + ': ' + $Sys['Tpm']) } if ($Sys['Nics'] -and @($Sys['Nics']).Count -gt 0) { Add '' Add ('--- ' + $Txt.SecNet + ' ' + ('-' * (74 - $Txt.SecNet.Length))) Add ($Txt.LHost.PadRight(18) + ': ' + $env:COMPUTERNAME + $(if (-not (Test-Blank $Sys['Domain'])) { ' (' + $Txt.LDomain + ': ' + $Sys['Domain'] + ')' } else { '' })) foreach ($nic in @($Sys['Nics'])) { Add ($Txt.LIP.PadRight(18) + ': ' + (Format-Num $nic.IP) + ' ' + $Txt.LMAC + ': ' + (Format-Num $nic.MAC) + $(if (-not (Test-Blank $nic.Gw)) { ' ' + $Txt.LGw + ': ' + $nic.Gw } else { '' })) Add (' '.PadRight(20) + $nic.Desc) } } Add '' # --- disks ---------------------------------------------------------------- Add ('--- ' + $Txt.SecDisks + ' ' + ('-' * (74 - $Txt.SecDisks.Length))) if ($Disks.Count -eq 0) { Add (' ' + $Txt.LNone) } $i = 0 foreach ($d in $Disks) { $i++ $kindStr = '' if ([string]$d.Kind -ne $Txt.KindUnknown) { $kindStr = ' ' + [string]$d.Kind } Add ('[' + $i + '] ' + $d.Path + $kindStr + ' ' + $d.Status) Add (' ' + $Txt.LModel.PadRight(16) + ': ' + (Format-Num $d.Model)) Add (' ' + $Txt.LSerial.PadRight(16) + ': ' + (Format-Num $d.Serial) + $(if (-not (Test-Blank $d.Firmware)) { ' ' + $Txt.LFirmware + ': ' + $d.Firmware } else { '' })) Add (' ' + $Txt.LInterface.PadRight(16) + ': ' + (Format-Num $d.Bus) + ' ' + $Txt.LCapacity + ': ' + $d.CapacityText) Add (' ' + $Txt.LVolumes.PadRight(16) + ': ' + (Get-VolumeText $d.Letters)) Add (' ' + $Txt.LSmart.PadRight(16) + ': ' + $d.SmartStatus + ' ' + $Txt.LStatus + ': ' + $d.Status) if ($null -ne $d.PowerOnHours) { Add (' ' + $Txt.LPowerOn.PadRight(16) + ': ' + (Format-Num ([int]$d.PowerOnHours)) + ' ' + $Txt.LHours) } if ($null -ne $d.PowerCycles) { Add (' ' + $Txt.LPowerCycles.PadRight(16) + ': ' + (Format-Num ([int]$d.PowerCycles))) } if ($null -ne $d.Temperature) { Add (' ' + $Txt.LTemp.PadRight(16) + ': ' + [string]$d.Temperature + ' C') } if ($null -ne $d.Wear) { Add (' ' + $Txt.LWear.PadRight(16) + ': ' + [string]$d.Wear + '%') } if ($null -ne $d.PercUsed) { Add (' ' + $Txt.LPercUsed.PadRight(16) + ': ' + [string]$d.PercUsed + '%') } if (-not (Test-Blank $d.Written)) { Add (' ' + $Txt.LWritten.PadRight(16) + ': ' + $d.Written) } if (-not (Test-Blank $d.Read)) { Add (' ' + $Txt.LRead.PadRight(16) + ': ' + $d.Read) } if (-not (Test-Blank $d.LastSelfTest)) { Add (' ' + $Txt.LLastSelfTest.PadRight(16) + ': ' + $d.LastSelfTest) } if (-not (Test-Blank $d.Note)) { Add (' ' + $Txt.LNote + ': ' + $d.Note) } if ($d.Bad.Count -gt 0) { Add (' ' + $Txt.LReasons + ' "' + $Txt.VBad + '":') foreach ($r in $d.Bad) { Add (' [FAIL] ' + $r) } } if ($d.Warn.Count -gt 0) { Add (' ' + $Txt.LProblems + ':') foreach ($r in $d.Warn) { Add (' [WARN] ' + $r) } } if ($d.Info.Count -gt 0) { foreach ($r in $d.Info) { Add (' [ -- ] ' + $r) } } Add '' } # --- summary -------------------------------------------------------------- $bad = 0; $warn = 0; $ok = 0 foreach ($d in $Disks) { if ($d.Status -eq $Txt.VBad) { $bad++ } elseif ($d.Status -eq $Txt.VWarn) { $warn++ } elseif ($d.Status -eq $Txt.VOk) { $ok++ } } $overall = $Txt.VOk if ($bad -gt 0) { $overall = $Txt.VBad } elseif ($warn -gt 0) { $overall = $Txt.VWarn } if ($Disks.Count -eq 0) { $overall = $Txt.VUnknown } Add ('--- ' + $Txt.SecResult + ' ' + ('-' * (74 - $Txt.SecResult.Length))) Add ($Txt.LOverall.PadRight(18) + ': ' + $overall) Add ($Txt.LDevices.PadRight(18) + ': ' + [string]$Disks.Count + ' (OK: ' + [string]$ok + ', ' + $Txt.VWarn + ': ' + [string]$warn + ', ' + $Txt.VBad + ': ' + [string]$bad + ')') if ($bad -eq 0 -and $warn -eq 0) { Add (' ' + $Txt.T_AllOk) } else { Add (' ' + $Txt.T_Problems) } Add $line return @{ Text = $sb.ToString(); Overall = $overall; Bad = $bad; Warn = $warn } } function New-Appendix { param($Disks) if ($null -eq $Disks) { $Disks = @() } $sb = New-Object Text.StringBuilder [void]$sb.AppendLine('') [void]$sb.AppendLine('=' * 78) [void]$sb.AppendLine(' ' + $Txt.SecAppendix) [void]$sb.AppendLine('=' * 78) foreach ($d in $Disks) { if ($d.Bad.Count -eq 0 -and $d.Warn.Count -eq 0) { continue } [void]$sb.AppendLine('') [void]$sb.AppendLine('### ' + $d.Path + ' ' + $d.Model + ' [' + $d.Status + ']') foreach ($block in @(@{T = 'smartctl -i'; L = $d.RawInfo}, @{T = 'smartctl -H'; L = $d.RawHealth}, @{T = 'smartctl -A'; L = $d.RawAttr}, @{T = 'smartctl -l error'; L = $d.RawErr}, @{T = 'smartctl -l selftest'; L = $d.RawSelf})) { [void]$sb.AppendLine('--- ' + $block.T + ' ---') foreach ($l in @($block.L)) { if (Test-Blank $l) { continue } if ($l -match 'START OF|Copyright|^smartctl \d|smartmontools home page|^===') { continue } [void]$sb.AppendLine($l) } } } return $sb.ToString() } function Save-Report { param([string]$Text, $Disks, [string]$Overall) if ($null -eq $Disks) { $Disks = @() } $stamp = Get-Date -Format 'yyyyMMdd_HHmmss' $hostSafe = ($env:COMPUTERNAME -replace '[^A-Za-z0-9\-_]', '_') $base = 'disk_report_' + $hostSafe + '_' + $stamp $dir = Join-Path $script:DataDir 'reports' $txt = Join-Path $dir ($base + '.txt') $csv = Join-Path $dir ($base + '.csv') try { Set-Content -LiteralPath $txt -Value $Text -Encoding UTF8 Write-Ok ($Txt.MReportSaved + ': ' + $txt) } catch { Write-Warn2 ($Txt.MSaveFail + ': ' + $_.Exception.Message) } try { $rows = @() foreach ($d in $Disks) { $rows += New-Object PSObject -Property @{ Disk = $d.Path; Model = $d.Model; Serial = $d.Serial; Kind = $d.Kind Capacity = $d.CapacityText; Letters = $d.Letters; Bus = $d.Bus Smart = $d.SmartStatus; Status = $d.Status PowerOnHours = $d.PowerOnHours; Temperature = $d.Temperature Wear = $d.Wear; Written = $d.Written Reallocated = $d.Reallocated; Pending = $d.Pending; Uncorrectable = $d.Uncorrectable CrcErrors = $d.CrcErrors; Spare = $d.Spare Bad = ($d.Bad -join '; '); Warn = ($d.Warn -join '; ') } } if ($rows.Count -gt 0) { $rows | Select-Object Disk, Model, Serial, Kind, Capacity, Letters, Bus, Smart, Status, PowerOnHours, Temperature, Wear, Written, Reallocated, Pending, Uncorrectable, CrcErrors, Spare, Bad, Warn | Export-Csv -Path $csv -NoTypeInformation -Encoding UTF8 Write-Ok ($Txt.MReportCsv + ': ' + $csv) } } catch { } # prune old reports (txt+csv pairs) try { $keep = [int]$Cfg.ReportKeep if ($keep -gt 0) { $files = @(Get-ChildItem -LiteralPath $dir -Filter 'disk_report_*' | Sort-Object LastWriteTime -Descending) if ($files.Count -gt ($keep * 2)) { $files[($keep * 2)..($files.Count - 1)] | ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue } } } } catch { } return $txt } # ============================================================================== # DELIVERY # ============================================================================== function Get-SmtpPassword { if (-not (Test-Blank $Cfg.SmtpPassword)) { return $Cfg.SmtpPassword } if (-not (Test-Blank $Passphrase) -and -not (Test-Blank $Cfg.EncryptedSmtpPassword)) { try { $sha = [Security.Cryptography.SHA256]::Create() $key = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Passphrase)) $sec = ConvertTo-SecureString $Cfg.EncryptedSmtpPassword -Key $key $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec) try { $plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } $sec = $null; $key = $null; $sha = $null return $plain } catch { Write-Err $Txt.MPassBad return '' } } if ($script:Interactive) { Write-Line '' Write-Line (' ' + $Txt.MPassPrompt) 'Yellow' try { $in = Read-Host (' ' + $Txt.PassKey) -AsSecureString $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($in) try { $plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } $in = $null if (-not (Test-Blank $plain) -and -not (Test-Blank $Cfg.EncryptedSmtpPassword)) { $sha = [Security.Cryptography.SHA256]::Create() $key = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($plain)) try { $sec = ConvertTo-SecureString $Cfg.EncryptedSmtpPassword -Key $key $b2 = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec) try { $res = [Runtime.InteropServices.Marshal]::PtrToStringAuto($b2) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($b2) } return $res } catch { Write-Err $Txt.MPassBad; return '' } } if (-not (Test-Blank $Cfg.EncryptedSmtpPassword)) { return '' } return $plain } catch { return '' } } return '' } function Get-ErrorText($err) { # Unwrap nested exceptions: SMTP failures hide the real reason in InnerException. $msg = '' $cur = $err $depth = 0 while ($cur -and $depth -lt 6) { $m = [string]$cur.Message if (-not (Test-Blank $m) -and $msg.IndexOf($m) -lt 0) { if ($msg -eq '') { $msg = $m } else { $msg = $msg + ' <- ' + $m } } $cur = $cur.InnerException $depth++ } if ($msg -eq '') { $msg = 'unknown error' } return $msg } function Convert-CertThumbprint([string]$s) { # 'aa:bb cc' -> 'AABBCC' if (Test-Blank $s) { return '' } return (($s -replace '[^0-9A-Fa-f]', '')).ToUpper() } function Get-CertNames($cert) { # Certificate subject plus the DNS names from the SAN extension. $names = '' try { $names = [string]$cert.Subject } catch { } try { foreach ($ext in $cert.Extensions) { try { if ($ext.Oid -and $ext.Oid.Value -eq '2.5.29.17') { $s = '' try { $s = [string]$ext.Format($false) } catch { } if ($s -ne '') { $names = $names + ', ' + $s } } } catch { } } } catch { } return $names } function Test-CertNameMatch([string]$certNames, [string]$expected) { # Case-insensitive match against CN/SAN, single-label wildcards supported. if ((Test-Blank $expected) -or (Test-Blank $certNames)) { return $false } $e = $expected.Trim().ToLower() if ($certNames.ToLower().IndexOf($e) -ge 0) { return $true } foreach ($part in ($certNames -split ',')) { $p = $part.Trim() if ($p.Length -gt 9 -and $p.Substring(0, 9) -eq 'DNS Name=') { $p = $p.Substring(9).Trim() } $p = $p.ToLower() if ($p -eq $e) { return $true } if ($p.StartsWith('*.')) { $suffix = $p.Substring(1) if ($e.EndsWith($suffix)) { $prefix = $e.Substring(0, $e.Length - $suffix.Length) if ($prefix.Length -gt 0 -and $prefix.IndexOf('.') -lt 0) { return $true } } } } return $false } function Write-CertInfo($cert) { if ($null -eq $cert) { Write-Err $Txt.MCertNone; return } try { Write-Info (' ' + $Txt.MCertSubject + ': ' + $cert.Subject) } catch { } try { Write-Info (' ' + $Txt.MCertIssuer + ': ' + $cert.Issuer) } catch { } try { Write-Info (' ' + $Txt.MCertExpires + ': ' + $cert.GetExpirationDateString()) } catch { } try { Write-Info (' ' + $Txt.MCertNames + ': ' + (Get-CertNames $cert)) } catch { } try { Write-Info (' ' + $Txt.MCertThumb + ': ' + $cert.GetCertHashString()) } catch { } Write-Info (' ' + $Txt.MCertHint) } function New-SmtpCertCallback { # Returns $null when the default (strict) validation must be kept. # Otherwise returns a callback that accepts the pinned certificate, or a # certificate issued for the expected name with a trusted chain (shared # hosting), or - if explicitly allowed - any certificate (insecure). # Values are passed through global variables because a scriptblock converted # to a delegate does not see the local scope of this function. $pin = Convert-CertThumbprint $Cfg.SmtpCertThumbprint $name = '' if (-not (Test-Blank $Cfg.SmtpCertExpectedName)) { $name = ([string]$Cfg.SmtpCertExpectedName).Trim() } $allowAny = [bool]$Cfg.SmtpAllowUntrustedCert if ($pin -eq '' -and $name -eq '' -and -not $allowAny) { return $null } $global:SmartSmtpPin = $pin $global:SmartSmtpName = $name $global:SmartSmtpAllowAny = $allowAny $global:SmartSmtpHost = [string]$Cfg.SmtpServer try { return [Net.Security.RemoteCertificateValidationCallback] { param($sender, $cert, $chain, $errors) if ($errors -eq [Net.Security.SslPolicyErrors]::None) { return $true } Write-Warn2 ($Txt.MCertInvalid + ': ' + [string]$errors) Write-CertInfo $cert if ($null -eq $cert) { return $false } if ($global:SmartSmtpPin -ne '') { $thumb = '' try { $thumb = ([Security.Cryptography.X509Certificates.X509Certificate2]$cert).GetCertHashString().ToUpper() } catch { } if ($thumb -eq $global:SmartSmtpPin) { Write-Ok $Txt.MCertPinned; return $true } Write-Err ($Txt.MCertPinMismatch + ': ' + $thumb) return $false } if ($global:SmartSmtpName -ne '') { # Only the name may differ; the chain must still be trusted. $chainErrors = ($errors -band [Net.Security.SslPolicyErrors]::RemoteCertificateChainErrors) $noCert = ($errors -band [Net.Security.SslPolicyErrors]::RemoteCertificateNotAvailable) $names = Get-CertNames $cert if ($chainErrors -eq 0 -and $noCert -eq 0 -and (Test-CertNameMatch $names $global:SmartSmtpName)) { Write-Ok ($Txt.MCertNameOk -f $global:SmartSmtpName, $global:SmartSmtpHost) return $true } Write-Err ($Txt.MCertNameBad -f $global:SmartSmtpName) return $false } if ($global:SmartSmtpAllowAny) { Write-Warn2 $Txt.MCertAccepted; return $true } return $false } } catch { Write-Warn2 ('SMTP certificate pinning is not supported by this PowerShell: ' + (Get-ErrorText $_.Exception)) return $null } } function Send-ReportMail { param([string]$Subject, [string]$Body, [string]$AttachPath) if ($NoMail) { Write-Info $Txt.MMailSkipNo; return $false } $pass = Get-SmtpPassword if (Test-Blank $pass) { Write-Warn2 $Txt.MMailSkip; Write-Info $Txt.MMailNoCred; return $false } $cred = $null $certCb = $null try { $sec = ConvertTo-SecureString $pass -AsPlainText -Force $cred = New-Object System.Management.Automation.PSCredential -ArgumentList $Cfg.SmtpUser, $sec $pass = $null; $sec = $null [GC]::Collect() # Certificate handling is applied only for this SMTP connection and is # reset in the finally block, so HTTPS requests keep strict validation. $certCb = New-SmtpCertCallback if ($certCb) { try { [Net.ServicePointManager]::ServerCertificateValidationCallback = $certCb } catch { Write-Warn2 ('SMTP certificate callback was not applied: ' + (Get-ErrorText $_.Exception)); $certCb = $null } } $toList = @() foreach ($a in ($Cfg.MailTo -split '[,;]')) { if (-not (Test-Blank $a)) { $toList += $a.Trim() } } if ($toList.Count -eq 0) { Write-Warn2 ($Txt.MMailFail + ': ' + $Txt.MNoRecipient); return $false } $mail = New-Object System.Net.Mail.MailMessage $mail.From = New-Object System.Net.Mail.MailAddress -ArgumentList $Cfg.MailFrom foreach ($a in $toList) { $mail.To.Add($a) } $mail.Subject = $Subject $mail.Body = $Body $mail.SubjectEncoding = [Text.Encoding]::UTF8 $mail.BodyEncoding = [Text.Encoding]::UTF8 $mail.IsBodyHtml = $false if (-not (Test-Blank $AttachPath) -and (Test-Path -LiteralPath $AttachPath)) { $att = New-Object System.Net.Mail.Attachment -ArgumentList $AttachPath $mail.Attachments.Add($att) | Out-Null } # Do not use inline casts like "[int]$x" inside -ArgumentList: in argument # mode PowerShell passes such a token as a literal string, not as a value. $smtpHost = [string]$Cfg.SmtpServer $smtpPort = [int]$Cfg.SmtpPort $smtp = New-Object System.Net.Mail.SmtpClient -ArgumentList @($smtpHost, $smtpPort) $smtp.EnableSsl = [bool]$Cfg.SmtpUseSsl $smtp.UseDefaultCredentials = $false $smtp.Credentials = $cred $smtp.Timeout = 120000 Write-Step ($Txt.MMailSending + ' ' + ($toList -join ', ')) $smtp.Send($mail) $smtp.Dispose(); $mail.Dispose() Write-Ok $Txt.MMailOk return $true } catch { Write-Err ($Txt.MMailFail + ': ' + (Get-ErrorText $_.Exception)) return $false } finally { if ($certCb) { try { [Net.ServicePointManager]::ServerCertificateValidationCallback = $null } catch { } } $global:SmartSmtpPin = '' $global:SmartSmtpAllowAny = $false $cred = $null [GC]::Collect() } } function Send-ReportUpload { param([string]$Text, [string]$Overall) if (Test-Blank $Cfg.UploadUrl) { return $false } try { $wc = New-Object Net.WebClient $wc.Encoding = [Text.Encoding]::UTF8 $wc.Headers.Add('Content-Type', 'text/plain; charset=utf-8') if (-not (Test-Blank $Cfg.UploadToken)) { $wc.Headers.Add('X-Auth-Token', $Cfg.UploadToken) } $wc.Headers.Add('X-Computer', $env:COMPUTERNAME) $wc.Headers.Add('X-Overall', $Overall) [void]$wc.UploadString($Cfg.UploadUrl, 'POST', $Text) Write-Ok $Txt.MUploadOk return $true } catch { Write-Warn2 ($Txt.MUploadFail + ': ' + (Get-ErrorText $_.Exception)) return $false } } # ============================================================================== # SELF CHECK # ============================================================================== function Test-TextTable { # The text table is embedded as base64: verify that it decoded correctly. $bad = 0 foreach ($k in @($Txt.Keys)) { $v = [string]$Txt[$k] if ($v.Length -eq 0) { $bad++ } elseif ($v.IndexOf([char]0xFFFD) -ge 0) { $bad++ } } return @{ Count = @($Txt.Keys).Count; Bad = $bad } } function Invoke-SelfTest { Write-Line '' Write-Line ('=== ' + $Txt.MSelfTest + ' ===') 'Cyan' $problems = 0 Write-Line '' Write-Line (' ' + $Txt.LMode + ': ' + $script:Mode) Write-Line (' PowerShell: ' + $PSVersionTable.PSVersion.ToString() + ' CLR: ' + $PSVersionTable.CLRVersion) Write-Line (' ' + $Txt.STDataDir + ': ' + $script:DataDir) Write-Line (' ' + $Txt.STFromFile + ': ' + $(if ($script:FromFile) { $Txt.LYes + ' (' + $script:SelfPath + ')' } else { $Txt.STFromIex })) $tt = Test-TextTable if ($tt.Bad -eq 0) { Write-Ok ($Txt.STTexts + ': ' + $tt.Count + ' - ' + $Txt.STTextsOk) } else { Write-Err ($Txt.STTexts + ': ' + $tt.Bad + ' ' + $Txt.STTextsBad); $problems++ } Write-Line '' Write-Line (' ' + $Txt.STCap + ':') 'Gray' foreach ($pair in @(@('Get-CimInstance', $script:HasCim), @('Get-Partition', $script:HasPartition), @('Get-PhysicalDisk', $script:HasPhysicalDisk), @('TLS 1.2', $script:TlsOk))) { Write-Line (' ' + $pair[0].PadRight(20) + $(if ($pair[1]) { $Txt.STYes } else { $Txt.STNo })) } Write-Line '' Write-Line (' ' + $Txt.STWmi + ':') 'Gray' $sys = Get-SystemInventory foreach ($k in @('OSCaption', 'OSVersion', 'OSBuild', 'OSArch', 'CpuName', 'CpuThreads', 'RamTotal', 'RamFree', 'Domain', 'Activation', 'SecureBoot', 'Tpm')) { if (-not (Test-Blank ([string]$sys[$k]))) { $v = $sys[$k] if ($k -eq 'RamTotal' -or $k -eq 'RamFree') { $v = Format-Size ([double]$v) } Write-Line (' ' + $k.PadRight(20) + $v) } } if (Test-Blank ([string]$sys['OSCaption'])) { Write-Warn2 $Txt.STWmiOs; $problems++ } if (-not (Test-Blank $sys['WmiNote'])) { Write-Warn2 $sys['WmiNote'] } if (-not $sys['RamTotal']) { Write-Warn2 $Txt.STRam; $problems++ } if (-not $sys['Nics'] -or @($sys['Nics']).Count -eq 0) { Write-Warn2 $Txt.STNic } Write-Line '' Write-Line ' smartctl:' 'Gray' if (Resolve-Smartctl) { $v = @(Invoke-Smartctl -ScArgs @('--version')) foreach ($l in $v) { if ($l -match '^smartctl\s+(.+)') { Write-Ok ($Matches[1]); break } } Write-Line (' ' + $Txt.STScan + ':') 'Gray' $devs = Get-ScanDevices if ($devs.Count -eq 0) { Write-Warn2 $Txt.MScanNone } else { foreach ($dev in $devs) { Write-Line (' ' + $dev.Path + ' ' + ($dev.DevArgs -join ' ')) } } Write-Line '' Write-Line (' ' + $Txt.LAdmin + ': ' + $(if (Test-Admin) { $Txt.LYes } else { $Txt.LNo })) 'Gray' if (-not (Test-Admin)) { Write-Info $Txt.STAdminNote } } else { $problems++ } Write-Line '' if ($problems -eq 0) { Write-Ok $Txt.MSelfOk } else { Write-Warn2 ($Txt.MSelfFail + ': ' + $problems) } Write-Line '' Set-ExitCode $(if ($problems -eq 0) { 0 } else { 2 }) } # ============================================================================== # MAIN # ============================================================================== function Invoke-SmartCheck { $sw = [Diagnostics.Stopwatch]::StartNew() $script:ExitCode = 0 $script:Interactive = $false try { $script:Interactive = [Environment]::UserInteractive -and ($Host.Name -eq 'ConsoleHost') } catch { } # 1. environment $script:TlsOk = Initialize-Tls try { [Console]::OutputEncoding = New-Object Text.UTF8Encoding -ArgumentList $false } catch { } $script:HasCim = [bool](Get-Command Get-CimInstance -ErrorAction SilentlyContinue) $script:HasPartition = [bool](Get-Command Get-Partition -ErrorAction SilentlyContinue) $script:HasPhysicalDisk = [bool](Get-Command Get-PhysicalDisk -ErrorAction SilentlyContinue) $osVer = [version]'0.0' foreach ($o in @(Get-WmiSafe 'Win32_OperatingSystem')) { try { $osVer = [version]([string]$o.Version) } catch { } break } if ($osVer -le [version]'0.0') { try { $osVer = [Environment]::OSVersion.Version } catch { } } $psMajor = $PSVersionTable.PSVersion.Major if ($psMajor -ge 5 -and $osVer -ge [version]'10.0') { $script:Mode = 'Modern' } else { $script:Mode = 'Legacy' } $script:DataDir = Resolve-DataDir # 2. banner Write-Line '' Write-Line ('=' * 78) 'Cyan' Write-Line (' ' + $Txt.Title) 'Cyan' Write-Line (' v' + $script:Version + ' | ' + $Txt.LMode + ': ' + $script:Mode + ' | ' + (Get-Date -Format 'dd.MM.yyyy HH:mm')) 'Cyan' Write-Line ('=' * 78) 'Cyan' if (-not $script:TlsOk) { Write-Warn2 $Txt.MTlsWarn } if ($SelfTest) { Invoke-SelfTest; return } # 3. admin rights $isAdmin = Test-Admin if ($isAdmin) { Write-Ok $Txt.MAdminOk } else { if (-not $NoElevate -and $script:Interactive) { Write-Warn2 $Txt.MAdminNeed if (Start-Elevated) { Write-Ok $Txt.MElevated Write-Info $Txt.MElevatedEnd return } Write-Err $Txt.MAdminFail } else { Write-Err $Txt.MAdminFail } Write-Info $Txt.MAdminHow Set-ExitCode 2 } # 4. comment if ((Test-Blank $Comment) -and $script:Interactive) { try { $c = Read-Host (' ' + $Txt.MCommentPrompt) if (-not (Test-Blank $c)) { $Comment = Trim-Str $c 80 } } catch { } } $Comment = ($Comment -replace '[\r\n\t]', ' ').Trim() # 5. tool if (-not $SkipDisks) { if (-not (Resolve-Smartctl)) { Set-ExitCode 2 } } # 6. collection Write-Line '' Write-Line ('--- ' + $Txt.SecSystem) 'Cyan' $sys = Get-SystemInventory Write-Ok ((Format-Num $sys['OSCaption']) + ' | ' + (Format-Num $sys['CpuName'])) if ($sys['RamTotal']) { Write-Ok ($Txt.LRam + ': ' + (Format-Size $sys['RamTotal'])) } $disks = @() if (-not $SkipDisks -and $script:SmartctlPath) { Write-Line '' Write-Line ('--- ' + $Txt.SecDisks) 'Cyan' $disks = Get-DiskInventory if ($null -eq $disks) { $disks = @() } } # 7. report $sw.Stop() $elapsed = $sw.Elapsed.TotalSeconds $rep = New-Report $sys $disks $elapsed $isAdmin $appendix = New-Appendix $disks $full = $rep.Text + $appendix Write-Line '' Write-Line $rep.Text if ($rep.Bad -gt 0 -or $rep.Warn -gt 0) { Set-ExitCode 1 } $txtPath = Save-Report $full $disks $rep.Overall # 8. delivery if ($Cfg.MailAlways -or $rep.Bad -gt 0 -or $rep.Warn -gt 0) { Write-Line '' $prefix = Trim-Str ($Cfg.MailSubjectPrefix + $Comment) 60 $subj = ($prefix + ' ' + $env:COMPUTERNAME + ' ' + $Txt.T_Subj + ' - ' + $rep.Overall + ' - ' + (Get-Date -Format 'dd.MM.yyyy HH:mm')).Trim() $body = New-Object Text.StringBuilder [void]$body.AppendLine($Txt.T_MailHead) [void]$body.AppendLine('=' * 60) [void]$body.AppendLine($Txt.LComputer + ': ' + $env:COMPUTERNAME + ' | ' + $Txt.LUser + ': ' + $env:USERNAME) if (-not (Test-Blank $Comment)) { [void]$body.AppendLine($Txt.LComment + ': ' + $Comment) } if (-not (Test-Blank ([string]$sys['OSCaption']))) { $osLine = [string]$sys['OSCaption'] if (-not (Test-Blank $sys['OSVersion'])) { $osLine += ' (v' + $sys['OSVersion'] + ')' } [void]$body.AppendLine($Txt.LOS + ': ' + $osLine) } if (-not (Test-Blank ([string]$sys['CpuName']))) { [void]$body.AppendLine($Txt.LCPU + ': ' + $sys['CpuName']) } if ($sys['RamTotal']) { [void]$body.AppendLine($Txt.LRam + ': ' + (Format-Size $sys['RamTotal'])) } [void]$body.AppendLine($Txt.LOverall + ': ' + $rep.Overall + ' | ' + $Txt.LDevices + ': ' + $disks.Count) [void]$body.AppendLine('') foreach ($d in $disks) { [void]$body.AppendLine('[' + $d.Status + '] ' + $d.Path + ' ' + $d.Model + ' ' + $d.Kind + ' ' + $d.CapacityText + ' ' + (Get-VolumeText $d.Letters)) foreach ($r in $d.Bad) { [void]$body.AppendLine(' [FAIL] ' + $r) } foreach ($r in $d.Warn) { [void]$body.AppendLine(' [WARN] ' + $r) } } [void]$body.AppendLine('') [void]$body.AppendLine($Txt.T_Attach) [void](Send-ReportMail $subj $body.ToString() $txtPath) } else { Write-Info $Txt.MMailSkipNo } [void](Send-ReportUpload $full $rep.Overall) # 9. finish Write-Line '' Write-Line ($Txt.MDone + '. ' + $Txt.MExitCode + ': ' + (Get-ExitCode)) 'Cyan' if ($Cfg.PauseOnFinish -and -not $NoPause -and $script:Interactive) { Write-Line $Txt.MPressKey 'Gray' try { [void]$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') } catch { } } } Invoke-SmartCheck # exit only when started from a file or explicitly asked for: # inside iex an exit call would close the user's PowerShell window if ($script:FromFile -or $HardExit) { exit (Get-ExitCode) }