PSWinVitals.psm1

# See the help for Set-StrictMode for the full details on what this enables.
Set-StrictMode -Version 2.0

Function Get-VitalInformation {
    [CmdletBinding(DefaultParameterSetName='All')]
    Param(
        [Parameter(ParameterSetName='Statistics')]
        [Switch]$ComponentStoreAnalysis,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$ComputerInfo,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$CrashDumps,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$DevicesNotPresent,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$DevicesWithBadStatus,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$EnvironmentVariables,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$HypervisorInfo,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$InstalledFeatures,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$InstalledPrograms,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$StorageVolumes,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$SysinternalsSuite,

        [Parameter(ParameterSetName='Statistics')]
        [Switch]$WindowsUpdates
    )

    if ($PSCmdlet.ParameterSetName -eq 'All') {
        $ComponentStoreAnalysis = $true
        $ComputerInfo = $true
        $CrashDumps = $true
        $DevicesNotPresent = $true
        $DevicesWithBadStatus = $true
        $EnvironmentVariables = $true
        $HypervisorInfo = $true
        $InstalledFeatures = $true
        $InstalledPrograms = $true
        $StorageVolumes = $true
        $SysinternalsSuite = $true
        $WindowsUpdates = $true
    }

    if ($ComponentStoreAnalysis) {
        if (!(Test-IsAdministrator)) {
            throw 'You must have administrator privileges to analyse the component store.'
        }
    }

    $VitalStatistics = [PSCustomObject]@{
        ComponentStoreAnalysis = $null
        ComputerInfo = $null
        CrashDumps = $null
        DevicesNotPresent = $null
        DevicesWithBadStatus = $null
        EnvironmentVariables = $null
        HypervisorInfo = $null
        InstalledFeatures = $null
        InstalledPrograms = $null
        StorageVolumes = $null
        SysinternalsSuite = $null
        WindowsUpdates = $null
    }

    if ($ComputerInfo) {
        if (Get-Command -Name Get-ComputerInfo -ErrorAction Ignore) {
            Write-Host -ForegroundColor Green -Object 'Retrieving computer info ...'
            $VitalStatistics.ComputerInfo = Get-ComputerInfo
        } else {
            Write-Warning -Message 'Unable to retrieve computer info as Get-ComputerInfo cmdlet not available.'
            $VitalStatistics.ComputerInfo = $false
        }
    }

    if ($HypervisorInfo) {
        Write-Host -ForegroundColor Green -Object 'Retrieving hypervisor info ...'
        $VitalStatistics.HypervisorInfo = Get-HypervisorInfo
    }

    if ($DevicesWithBadStatus) {
        if (Get-Module -Name PnpDevice -ListAvailable) {
            Write-Host -ForegroundColor Green -Object 'Retrieving problem devices ...'
            $VitalStatistics.DevicesWithBadStatus = Get-PnpDevice | Where-Object { $_.Status -in ('Degraded', 'Error') }
        } else {
            Write-Warning -Message 'Unable to retrieve problem devices as PnpDevice module not available.'
            $VitalStatistics.DevicesWithBadStatus = $false
        }
    }

    if ($DevicesNotPresent) {
        if (Get-Module -Name PnpDevice -ListAvailable) {
            Write-Host -ForegroundColor Green -Object 'Retrieving not present devices ...'
            $VitalStatistics.DevicesNotPresent = Get-PnpDevice | Where-Object { $_.Status -eq 'Unknown' }
        } else {
            Write-Warning -Message 'Unable to retrieve not present devices as PnpDevice module not available.'
            $VitalStatistics.DevicesNotPresent = $false
        }
    }

    if ($StorageVolumes) {
        Write-Host -ForegroundColor Green -Object 'Retrieving storage volumes summary ...'
        $VitalStatistics.StorageVolumes = Get-Volume | Where-Object { $_.DriveType -eq 'Fixed' }
    }

    if ($CrashDumps) {
        [PSCustomObject]$CrashDumps = [PSCustomObject]@{
            Kernel = $null
            Service = $null
        }

        Write-Host -ForegroundColor Green -Object 'Retrieving kernel crash dumps ...'
        $CrashDumps.Kernel = Get-KernelCrashDumps

        Write-Host -ForegroundColor Green -Object 'Retrieving service crash dumps ...'
        $CrashDumps.Service = Get-ServiceCrashDumps

        $VitalStatistics.CrashDumps = $CrashDumps
    }

    if ($ComponentStoreAnalysis) {
        Write-Host -ForegroundColor Green -Object 'Running component store analysis ...'
        $VitalStatistics.ComponentStoreAnalysis = Invoke-DISM -Operation AnalyzeComponentStore
    }

    if ($InstalledFeatures) {
        if (Get-Module -Name ServerManager -ListAvailable) {
            Write-Host -ForegroundColor Green -Object 'Retrieving installed features ...'
            $VitalStatistics.InstalledFeatures = Get-WindowsFeature | Where-Object { $_.Installed }
        } else {
            Write-Warning -Message 'Unable to retrieve installed features as ServerManager module not available.'
            $VitalStatistics.InstalledFeatures = $false
        }
    }

    if ($InstalledPrograms) {
        Write-Host -ForegroundColor Green -Object 'Retrieving installed programs ...'
        $VitalStatistics.InstalledPrograms = Get-InstalledPrograms
    }

    if ($EnvironmentVariables) {
        [PSCustomObject]$EnvironmentVariables = [PSCustomObject]@{
            Machine = $null
            User = $null
        }

        Write-Host -ForegroundColor Green -Object 'Retrieving system environment variables ...'
        $EnvironmentVariables.Machine = [Environment]::GetEnvironmentVariables([EnvironmentVariableTarget]::Machine)

        Write-Host -ForegroundColor Green -Object 'Retrieving user environment variables ...'
        $EnvironmentVariables.User = [Environment]::GetEnvironmentVariables([EnvironmentVariableTarget]::User)

        $VitalStatistics.EnvironmentVariables = $EnvironmentVariables
    }

    if ($WindowsUpdates) {
        if (Get-Module -Name PSWindowsUpdate -ListAvailable) {
            Write-Host -ForegroundColor Green -Object 'Retrieving available Windows updates ...'
            $VitalStatistics.WindowsUpdates = Get-WUList
        } else {
            Write-Warning -Message 'Unable to retrieve available Windows updates as PSWindowsUpdate module not available.'
            $VitalStatistics.WindowsUpdates = $false
        }
    }

    if ($SysinternalsSuite) {
        if (Test-IsWindows64bit) {
            $InstallDir = Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath 'Sysinternals'
        } else {
            $InstallDir = Join-Path -Path $env:ProgramFiles -ChildPath 'Sysinternals'
        }

        if (Test-Path -Path $InstallDir -PathType Container) {
            Write-Host -ForegroundColor Green -Object 'Retrieving Sysinternals Suite version ...'
            $Sysinternals = [PSCustomObject]@{
                Path = $null
                Version = $null
                Updated = $false
            }

            $Sysinternals.Path = $InstallDir
            $Sysinternals.Version = (Get-Item -Path $InstallDir).CreationTime.ToString('yyyyMMdd')
            $VitalStatistics.SysinternalsSuite = $Sysinternals
        } else {
            Write-Warning -Message 'Unable to retrieve Sysinternals Suite version as it does not appear to be installed.'
            $VitalStatistics.SysinternalsSuite = $false
        }
    }

    return $VitalStatistics
}

Function Invoke-VitalChecks {
    [CmdletBinding(DefaultParameterSetName='All')]
    Param(
        [Parameter(ParameterSetName='Checks')]
        [Switch]$ComponentStoreScan,

        [Parameter(ParameterSetName='Checks')]
        [Switch]$FileSystemScans,

        [Parameter(ParameterSetName='Checks')]
        [Switch]$SystemFileChecker,

        [Switch]$VerifyOnly
    )

    if ($PSCmdlet.ParameterSetName -eq 'All') {
        $ComponentStoreScan = $true
        $FileSystemScans = $true
        $SystemFileChecker = $true
    }

    if (!(Test-IsAdministrator)) {
        throw 'You must have administrator privileges to perform system checks.'
    }

    $VitalChecks = [PSCustomObject]@{
        ComponentStoreScan = $null
        FileSystemScans = $null
        SystemFileChecker = $null
    }

    if ($FileSystemScans) {
        Write-Host -ForegroundColor Green -Object 'Running file system scans ...'
        if ($VerifyOnly) {
            $VitalChecks.FileSystemScans = Invoke-CHKDSK -Operation Verify
        } else {
            $VitalChecks.FileSystemScans = Invoke-CHKDSK -Operation Scan
        }
    }

    if ($SystemFileChecker) {
        Write-Host -ForegroundColor Green -Object 'Running System File Checker ...'
        if ($VerifyOnly) {
            $VitalChecks.SystemFileChecker = Invoke-SFC -Operation Verify
        } else {
            $VitalChecks.SystemFileChecker = Invoke-SFC -Operation Scan
        }
    }

    if ($ComponentStoreScan) {
        Write-Host -ForegroundColor Green -Object 'Running component store scan ...'
        if ($VerifyOnly) {
            $VitalChecks.ComponentStoreScan = Invoke-DISM -Operation ScanHealth
        } else {
            $VitalChecks.ComponentStoreScan = Invoke-DISM -Operation RestoreHealth
        }
    }

    return $VitalChecks
}

Function Invoke-VitalMaintenance {
    [CmdletBinding(DefaultParameterSetName='All')]
    Param(
        [Parameter(ParameterSetName='Maintenance')]
        [Switch]$ComponentStoreCleanup,

        [Parameter(ParameterSetName='Maintenance')]
        [Switch]$ClearInternetExplorerCache,

        [Parameter(ParameterSetName='Maintenance')]
        [Switch]$DeleteErrorReports,

        [Parameter(ParameterSetName='Maintenance')]
        [Switch]$DeleteTemporaryFiles,

        [Parameter(ParameterSetName='Maintenance')]
        [Switch]$EmptyRecycleBin,

        [Parameter(ParameterSetName='Maintenance')]
        [Switch]$PowerShellHelp,

        [Parameter(ParameterSetName='Maintenance')]
        [Switch]$SysinternalsSuite,

        [Parameter(ParameterSetName='Maintenance')]
        [Switch]$WindowsUpdates
    )

    if ($PSCmdlet.ParameterSetName -eq 'All') {
        $ClearInternetExplorerCache = $true
        $ComponentStoreCleanup = $true
        $DeleteErrorReports = $true
        $DeleteTemporaryFiles = $true
        $EmptyRecycleBin = $true
        $PowerShellHelp = $true
        $SysinternalsSuite = $true
        $WindowsUpdates = $true
    }

    if (!(Test-IsAdministrator)) {
        throw 'You must have administrator privileges to perform system maintenance.'
    }

    $VitalMaintenance = [PSCustomObject]@{
        ClearInternetExplorerCache = $null
        ComponentStoreCleanup = $null
        DeleteErrorReports = $null
        DeleteTemporaryFiles = $null
        EmptyRecycleBin = $null
        PowerShellHelp = $null
        SysinternalsSuite = $null
        WindowsUpdates = $null
    }

    if ($WindowsUpdates) {
        if (Get-Module -Name PSWindowsUpdate -ListAvailable) {
            Write-Host -ForegroundColor Green -Object 'Installing available Windows updates ...'
            $VitalMaintenance.WindowsUpdates = Get-WUInstall -AcceptAll -IgnoreReboot
        } else {
            Write-Warning -Message 'Unable to install available Windows updates as PSWindowsUpdate module not available.'
            $VitalMaintenance.WindowsUpdates = $false
        }
    }

    if ($ComponentStoreCleanup) {
        Write-Host -ForegroundColor Green -Object 'Running component store clean-up ...'
        $VitalMaintenance.ComponentStoreCleanup = Invoke-DISM -Operation StartComponentCleanup
    }

    if ($PowerShellHelp) {
        Write-Host -ForegroundColor Green -Object 'Updating PowerShell help ...'
        try {
            Update-Help -Force -ErrorAction Stop
            $VitalMaintenance.PowerShellHelp = $true
        } catch {
            # Often we'll fail to update help data for a few modules because they haven't defined
            # the HelpInfoUri key in their manifest. There's nothing that can be done to fix this.
            $VitalMaintenance.PowerShellHelp = $_.Exception.Message
        }
    }

    if ($SysinternalsSuite) {
        Write-Host -ForegroundColor Green -Object 'Updating Sysinternals Suite ...'
        $VitalMaintenance.SysinternalsSuite = Update-Sysinternals
    }

    if ($ClearInternetExplorerCache) {
        if (Get-Command -Name inetcpl.cpl -ErrorAction Ignore) {
            Write-Host -ForegroundColor Green -Object 'Clearing Internet Explorer cache ...'
            # More details on the bitmask here: https://github.com/SeleniumHQ/selenium/blob/master/cpp/iedriver/BrowserFactory.cpp
            $RunDll32Path = Join-Path -Path $env:SystemRoot -ChildPath 'System32\rundll32.exe'
            & $RunDll32Path inetcpl.cpl,ClearMyTracksByProcess 9FF
            $VitalMaintenance.ClearInternetExplorerCache = $true
        } else {
            Write-Warning -Message 'Unable to clear Internet Explorer cache as Control Panel applet not available.'
            $VitalMaintenance.ClearInternetExplorerCache = $false
        }
    }

    if ($DeleteErrorReports) {
        Write-Host -ForegroundColor Green -Object 'Deleting system error reports ...'
        $SystemReports = Join-Path -Path $env:ProgramData -ChildPath 'Microsoft\Windows\WER'
        $SystemQueue = Join-Path -Path $SystemReports -ChildPath 'ReportQueue'
        $SystemArchive = Join-Path -Path $SystemReports -ChildPath 'ReportArchive'
        foreach ($Path in @($SystemQueue, $SystemArchive)) {
            if (Test-Path -Path $Path -PathType Container) {
                Remove-Item -Path "$Path\*" -Recurse -ErrorAction Ignore
            }
        }

        Write-Host -ForegroundColor Green -Object ('Deleting {0} error reports ...' -f $env:USERNAME)
        $UserReports = Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Microsoft\Windows\WER'
        $UserQueue = Join-Path -Path $UserReports -ChildPath 'ReportQueue'
        $UserArchive = Join-Path -Path $UserReports -ChildPath 'ReportArchive'
        foreach ($Path in @($UserQueue, $UserArchive)) {
            if (Test-Path -Path $Path -PathType Container) {
                Remove-Item -Path "$Path\*" -Recurse -ErrorAction Ignore
            }
        }

        $VitalMaintenance.DeleteErrorReports = $true
    }

    if ($DeleteTemporaryFiles) {
        Write-Host -ForegroundColor Green -Object 'Deleting system temporary files ...'
        $SystemTemp = [Environment]::GetEnvironmentVariable('Temp', [EnvironmentVariableTarget]::Machine)
        Remove-Item -Path "$SystemTemp\*" -Recurse -ErrorAction Ignore

        Write-Host -ForegroundColor Green -Object ('Deleting {0} temporary files ...' -f $env:USERNAME)
        $UserTemp = [Environment]::GetEnvironmentVariable('Temp', [EnvironmentVariableTarget]::User)
        Remove-Item -Path "$UserTemp\*" -Recurse -ErrorAction Ignore

        $VitalMaintenance.DeleteTemporaryFiles = $true
    }

    if ($EmptyRecycleBin) {
        if (Get-Command -Name Clear-RecycleBin -ErrorAction Ignore) {
            Write-Host -ForegroundColor Green -Object 'Emptying Recycle Bin ...'
            try {
                Clear-RecycleBin -Force -ErrorAction Stop
                $VitalMaintenance.EmptyRecycleBin = $true
            } catch [ComponentModel.Win32Exception] {
                # Sometimes clearing the Recycle Bin fails with an exception which seems to indicate
                # the Recycle Bin folder doesn't exist. If that happens we only get a generic E_FAIL
                # exception, so checking the actual exception message seems to be the best method.
                if ($_.Exception.Message -eq 'The system cannot find the path specified') {
                    $VitalMaintenance.EmptyRecycleBin = $true
                } else {
                    $VitalMaintenance.EmptyRecycleBin = $_.Exception.Message
                }
            }
        } else {
            Write-Warning -Message 'Unable to empty Recycle Bin as Clear-RecycleBin cmdlet not available.'
            $VitalMaintenance.EmptyRecycleBin = $false
        }
    }

    return $VitalMaintenance
}

Function Get-HypervisorInfo {
    [CmdletBinding()]
    Param()

    $LogPrefix = 'HypervisorInfo'
    $HypervisorInfo = [PSCustomObject]@{
        Vendor = $null
        Hypervisor = $null
        ToolsVersion = $null
    }

    $ComputerSystem = Get-WmiObject -Class Win32_ComputerSystem
    $Manufacturer = $ComputerSystem.Manufacturer
    $Model = $ComputerSystem.Model

    # Useful: http://git.annexia.org/?p=virt-what.git;a=blob_plain;f=virt-what.in;hb=HEAD
    if ($Manufacturer -eq 'Microsoft Corporation' -and $Model -eq 'Virtual Machine') {
        $HypervisorInfo.Vendor = 'Microsoft'
        $HypervisorInfo.Hypervisor = 'Hyper-V'

        $IntegrationServicesVersion = $false
        $VMInfoRegPath = 'HKLM:\Software\Microsoft\Virtual Machine\Auto'
        if (Test-Path -Path $VMInfoRegPath -PathType Container) {
            $VMInfo = Get-ItemProperty -Path $VMInfoRegPath
            if ($VMInfo.PSObject.Properties['IntegrationServicesVersion']) {
                $IntegrationServicesVersion = $VMInfo.IntegrationServicesVersion
            }
        }

        if ($IntegrationServicesVersion) {
            $HypervisorInfo.ToolsVersion = $VMinfo.IntegrationServicesVersion
        } else {
            Write-Warning -Message ('[{0}] Detected Microsoft Hyper-V but unable to determine Integration Services version.' -f $LogPrefix)
        }
    } elseif ($Manufacturer -eq 'VMware, Inc.' -and $Model -match '^VMware') {
        $HypervisorInfo.Vendor = 'VMware'
        $HypervisorInfo.Hypervisor = 'Unknown'

        $VMwareToolboxCmd = Join-Path -Path $env:ProgramFiles -ChildPath 'VMware\VMware Tools\VMwareToolboxCmd.exe'
        if (Test-Path -Path $VMwareToolboxCmd -PathType Leaf) {
            $HypervisorInfo.ToolsVersion = & $VMwareToolboxCmd -v
        } else {
            Write-Warning -Message ('[{0}] Detected a VMware hypervisor but unable to determine VMware Tools version.' -f $LogPrefix)
        }
    } else {
        Write-Verbose -Message ('[{0}] Either not running in a hypervisor or hypervisor not recognised.' -f $LogPrefix)
        return $false
    }

    return $HypervisorInfo
}

Function Get-InstalledPrograms {
    [CmdletBinding()]
    Param()

    $NativeRegPath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall'
    $Wow6432RegPath = 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'

    $UninstallKeys = Get-ChildItem -Path $NativeRegPath
    if (Test-Path -Path $Wow6432RegPath -PathType Container) {
        $UninstallKeys += Get-ChildItem -Path $Wow6432RegPath
    }

    $InstalledPrograms = @()
    foreach ($UninstallKey in $UninstallKeys) {
        $Program = Get-ItemProperty -Path $UninstallKey.PSPath
        if ($Program.PSObject.Properties['DisplayName'] -and
            !$Program.PSObject.Properties['SystemComponent'] -and
            !$Program.PSObject.Properties['ReleaseType'] -and
            !$Program.PSObject.Properties['ParentKeyName'] -and
            ($Program.PSObject.Properties['UninstallString'] -or
             $Program.PSObject.Properties['NoRemove'])) {
            $InstalledProgram = [PSCustomObject]@{
                Name = $Program.DisplayName
                Publisher = $null
                InstallDate = $null
                EstimatedSize = $null
                Version = $null
                Location = $null
                Uninstall = $null
            }

            if ($Program.PSObject.Properties['Publisher']) {
                $InstalledProgram.Publisher = $Program.Publisher
            }

            if ($Program.PSObject.Properties['InstallDate']) {
                $InstalledProgram.InstallDate = $Program.InstallDate
            }

            if ($Program.PSObject.Properties['EstimatedSize']) {
                $InstalledProgram.EstimatedSize = $Program.EstimatedSize
            }

            if ($Program.PSObject.Properties['DisplayVersion']) {
                $InstalledProgram.Version = $Program.DisplayVersion
            }

            if ($Program.PSObject.Properties['InstallLocation']) {
                $InstalledProgram.Location = $Program.InstallLocation
            }

            if ($Program.PSObject.Properties['UninstallString']) {
                $InstalledProgram.Uninstall = $Program.UninstallString
            }

            $InstalledPrograms += $InstalledProgram
        }
    }

    return $InstalledPrograms
}

Function Get-KernelCrashDumps {
    [CmdletBinding()]
    Param()

    $LogPrefix = 'KernelCrashDumps'
    $KernelCrashDumps = [PSCustomObject]@{
        MemoryDump = $null
        Minidumps = $null
    }

    $CrashControlRegPath = 'HKLM:\System\CurrentControlSet\Control\CrashControl'

    if (Test-Path -Path $CrashControlRegPath -PathType Container) {
        $CrashControl = Get-ItemProperty -Path $CrashControlRegPath

        if ($CrashControl.PSObject.Properties['DumpFile']) {
            $DumpFile = $CrashControl.DumpFile
        } else {
            $DumpFile = Join-Path -Path $env:SystemRoot -ChildPath 'MEMORY.DMP'
            Write-Warning -Message ("[{0}] The DumpFile value doesn't exist in CrashControl so we're guessing the location." -f $LogPrefix)
        }

        if ($CrashControl.PSObject.Properties['MinidumpDir']) {
            $MinidumpDir = $CrashControl.MinidumpDir
        } else {
            $DumpFile = Join-Path -Path $env:SystemRoot -ChildPath 'Minidump'
            Write-Warning -Message ("[{0}]The MinidumpDir value doesn't exist in CrashControl so we're guessing the location." -f $LogPrefix)
        }
    } else {
        Write-Warning -Message ("[{0}]The CrashControl key doesn't exist in the Registry so we're guessing dump locations." -f $LogPrefix)
    }

    if (Test-Path -Path $DumpFile -PathType Leaf) {
        $KernelCrashDumps.MemoryDump = Get-Item -Path $DumpFile
    }

    if (Test-Path -Path $MinidumpDir -PathType Container) {
        $KernelCrashDumps.Minidumps = Get-ChildItem -Path $MinidumpDir
    }

    return $KernelCrashDumps
}

Function Get-ServiceCrashDumps {
    [CmdletBinding()]
    Param()

    $LogPrefix = 'ServiceCrashDumps'
    $ServiceCrashDumps = [PSCustomObject]@{
        LocalSystem = $null
        LocalService = $null
        NetworkService = $null
    }

    $LocalSystemPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\Config\SystemProfile\AppData\Local\CrashDumps'
    $LocalServicePath = Join-Path -Path $env:SystemRoot -ChildPath 'ServiceProfiles\LocalService\AppData\Local\CrashDumps'
    $NetworkServicePath = Join-Path -Path $env:SystemRoot -ChildPath 'ServiceProfiles\NetworkService\AppData\Local\CrashDumps'

    if (Test-Path -Path $LocalSystemPath -PathType Container) {
        $ServiceCrashDumps.LocalSystem = Get-ChildItem -Path $LocalSystemPath
    } else {
        Write-Verbose -Message ("[{0}] The crash dumps path for the LocalSystem account doesn't exist." -f $LogPrefix)
    }

    if (Test-Path -Path $LocalServicePath -PathType Container) {
        $ServiceCrashDumps.LocalService = Get-ChildItem -Path $LocalServicePath
    } else {
        Write-Verbose -Message ("[{0}] The crash dumps path for the LocalService account doesn't exist." -f $LogPrefix)
    }

    if (Test-Path -Path $NetworkServicePath -PathType Container) {
        $ServiceCrashDumps.NetworkService = Get-ChildItem -Path $NetworkServicePath
    } else {
        Write-Verbose -Message ("[{0}] The crash dumps path for the NetworkService account doesn't exist." -f $LogPrefix)
    }

    return $ServiceCrashDumps
}

Function Invoke-CHKDSK {
    [CmdletBinding()]
    Param(
        [ValidateSet('Scan', 'Verify')]
        [String]$Operation = 'Scan'
    )

    # We could use the Repair-Volume cmdlet introduced in Windows 8/Server 2012, but it's just a
    # thin wrapper around CHKDSK and only exposes a small subset of its underlying functionality.
    $LogPrefix = 'CHKDSK'

    $SupportedFileSystems = @('FAT', 'FAT16', 'FAT32', 'NTFS', 'NTFS4', 'NTFS5')
    $Volumes = Get-Volume | Where-Object { $_.DriveType -eq 'Fixed' -and $_.FileSystem -in $SupportedFileSystems }

    [PSCustomObject[]]$Results = $null
    foreach ($Volume in $Volumes) {
        $VolumePath = $Volume.Path.TrimEnd('\')
        $CHKDSK = [PSCustomObject]@{
            Operation = $Operation
            VolumePath = $VolumePath
            Output = $null
            ExitCode = $null
        }

        Write-Verbose -Message ('[{0}] Running {1} operation on: {2}' -f $LogPrefix, $Operation.ToLower(), $VolumePath)
        $ChkDskPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\chkdsk.exe'
        if ($Operation -eq 'Scan') {
            $CHKDSK.Output += & $ChkDskPath "$VolumePath" /scan
        } else {
            $CHKDSK.Output += & $ChkDskPath "$VolumePath"
        }
        $CHKDSK.ExitCode = $LASTEXITCODE

        switch ($CHKDSK.ExitCode) {
            0           { continue }
            2           { Write-Warning -Message ('[{0}] Volume requires cleanup: {1}' -f $LogPrefix, $VolumePath) }
            3           { Write-Warning -Message ('[{0}] Volume contains errors: {1}' -f $LogPrefix, $VolumePath) }
            default     { Write-Error -Message ('[{0}] Unexpected exit code: {1}' -f $LogPrefix, $CHKDSK.ExitCode) }
        }

        $Results += $CHKDSK
    }

    return $Results
}

Function Invoke-DISM {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [ValidateSet('AnalyzeComponentStore', 'RestoreHealth', 'ScanHealth', 'StartComponentCleanup')]
        [String]$Operation
    )

    # The Dism PowerShell module doesn't appear to expose the /Cleanup-Image family of parameters
    # available in the underlying Dism.exe utility, so we have to fallback to invoking it directly.
    $LogPrefix = 'DISM'
    $DISM = [PSCustomObject]@{
        Operation = $Operation
        Output = $null
        ExitCode = $null
    }

    Write-Verbose -Message ('[{0}] Running {1} operation ...' -f $LogPrefix, $Operation)
    $DismPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\dism.exe'
    $DISM.Output = & $DismPath /Online /Cleanup-Image /$Operation
    $DISM.ExitCode = $LASTEXITCODE

    switch ($DISM.ExitCode) {
        0           { continue }
        -2146498554 { Write-Warning -Message ('[{0}] The operation could not be completed due to pending operations.' -f $LogPrefix, $DISM.ExitCode) }
        default     { Write-Error -Message ('[{0}] Returned non-zero exit code: {1}' -f $LogPrefix, $DISM.ExitCode) }
    }

    return $DISM
}

Function Invoke-SFC {
    [CmdletBinding()]
    Param(
        [ValidateSet('Scan', 'Verify')]
        [String]$Operation = 'Scan'
    )

    $LogPrefix = 'SFC'
    $SFC = [PSCustomObject]@{
        Operation = $Operation
        Output = $null
        ExitCode = $null
    }

    Write-Verbose -Message ('[{0}] Running {1} operation ...' -f $LogPrefix, $Operation.ToLower())
    $SfcPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\sfc.exe'
    if ($Operation -eq 'Scan') {
        $SFC.Output = & $SfcPath /SCANNOW
    } else {
        $SFC.Output = & $SfcPath /VERIFYONLY
    }
    $SFC.ExitCode = $LASTEXITCODE

    switch ($SFC.ExitCode) {
        0           { continue }
        default     { Write-Error -Message ('[{0}] Returned non-zero exit code: {1}' -f $LogPrefix, $SFC.ExitCode) }
    }

    return $SFC
}

Function Update-Sysinternals {
    [CmdletBinding()]
    Param(
        [ValidatePattern('^http[Ss]?://.*')]
        [String]$DownloadUrl = 'https://download.sysinternals.com/files/SysinternalsSuite.zip'
    )

    $LogPrefix = 'Sysinternals'
    $Sysinternals = [PSCustomObject]@{
        Path = $null
        Version = $null
        Updated = $false
    }

    $DownloadDir = $env:TEMP
    $DownloadFile = Split-Path -Path $DownloadUrl -Leaf
    $DownloadPath = Join-Path -Path $DownloadDir -ChildPath $DownloadFile

    if (Test-IsWindows64bit) {
        $InstallDir = Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath 'Sysinternals'
    } else {
        $InstallDir = Join-Path -Path $env:ProgramFiles -ChildPath 'Sysinternals'
    }
    $Sysinternals.Path = $InstallDir

    $ExistingVersion = $false
    if (Test-Path -Path $InstallDir -PathType Container) {
        $ExistingVersion = (Get-Item -Path $InstallDir).CreationTime.ToString('yyyyMMdd')
    }

    Write-Verbose -Message ('[{0}] Downloading latest version from: {1}' -f $LogPrefix, $DownloadUrl)
    $null = New-Item -Path $DownloadDir -ItemType Directory -ErrorAction Ignore
    $WebClient = New-Object -TypeName Net.WebClient
    $WebClient.DownloadFile($DownloadUrl, $DownloadPath)

    Write-Verbose -Message ('[{0}] Extracting archive to: {1}' -f $LogPrefix, $InstallDir)
    Remove-Item -Path $InstallDir -Recurse -ErrorAction Ignore
    Expand-ZipFile -FilePath $DownloadPath -DestinationPath $InstallDir
    Remove-Item -Path $DownloadPath

    $SystemPath = [Environment]::GetEnvironmentVariable('Path', [EnvironmentVariableTarget]::Machine)
    $RegEx = [Regex]::Escape($InstallDir)
    if (!($SystemPath -match "^;*$RegEx;" -or $SystemPath -match ";$RegEx;" -or $SystemPath -match ";$RegEx;*$")) {
        Write-Verbose -Message ('[{0}] Updating system path ...' -f $LogPrefix)
        if (!$SystemPath.EndsWith(';')) {
            $SystemPath += ';'
        }
        $SystemPath += $InstallDir
        [Environment]::SetEnvironmentVariable('Path', $SystemPath, [EnvironmentVariableTarget]::Machine)
    }

    $Sysinternals.Version = (Get-Item -Path $InstallDir).CreationTime.ToString('yyyyMMdd')
    if (!$ExistingVersion -or $Sysinternals.Version -ne $ExistingVersion) {
        $Sysinternals.Updated = $true
    }

    Write-Verbose -Message ('[{0}] Installed version: {1}' -f $LogPrefix, $Sysinternals.Version)
    return $Sysinternals
}

Function Expand-ZipFile {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [String]$FilePath,

        [Parameter(Mandatory)]
        [String]$DestinationPath
    )

    # The Expand-Archive cmdlet is only available from v5.0
    if ($PSVersionTable.PSVersion.Major -ge 5) {
        Expand-Archive -Path $FilePath -DestinationPath $DestinationPath
    } else {
        Add-Type -AssemblyName System.IO.Compression.FileSystem
        [IO.Compression.ZipFile]::ExtractToDirectory($FilePath, $DestinationPath)
    }
}

Function Test-IsAdministrator {
    [CmdletBinding()]
    Param()

    $User = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
    if ($User.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        return $true
    }
    return $false
}

Function Test-IsWindows64bit {
    [CmdletBinding()]
    Param()

    if ((Get-WmiObject -Class Win32_OperatingSystem).OSArchitecture -eq '64-bit') {
        return $true
    }
    return $false
}