systemchecks.psm1

#Requires -Version 5
<#
 .Synopsis
 Count the number of files in a directory.
 
 .Description
 Returns the count of files (not sub-directories) in the specified path.
 Optionally appends a date-based sub-folder to the base path using the
 AppendLeaf and LeafFormat parameters — handy for checking whether today's
 or yesterday's output files were created by a batch process.
 
 .Parameter FilePath
 Base directory path to check.
 
 .Parameter SystemName
 Friendly name for the system this check belongs to (used in reporting).
 
 .Parameter SystemDescription
 Short description of the system (used in reporting).
 
 .Parameter AppendLeaf
 When set, a date sub-folder is appended to FilePath. Accepted values:
 'Today' (current date) or 'Yesterday' (previous day).
 
 .Parameter LeafFormat
 The date format string passed to Get-Date when building the sub-folder name,
 e.g. 'yyyyMMdd'.
 
 .Example
Get-FileCount -FilePath "c:\my\folder"
 #>

function Get-FileCount {
    [CmdletBinding()]
    param (
        [string]$FilePath,
        [string]$SystemName,
        [string]$SystemDescription,
        [string]$AppendLeaf,
        [string]$LeafFormat
    )

    $HealthCheckType = 'FileCount'

    try {
        if (Test-Path -Path $FilePath) {

            $FolderName = Split-Path -Path $FilePath -Leaf

            if (-not [string]::IsNullOrEmpty($AppendLeaf)) {
                switch ($AppendLeaf) {
                    'Today' {
                        $ChildPath = Get-Date -Format $LeafFormat
                        $CheckPath = Join-Path -Path $FilePath -ChildPath $ChildPath
                        $FolderName += "\$ChildPath."
                    }
                    'Yesterday' {
                        $ChildPath = Get-Date -Date ((Get-Date).AddDays(-1)) -Format $LeafFormat
                        $CheckPath = Join-Path -Path $FilePath -ChildPath $ChildPath
                        $FolderName += "\$ChildPath."
                    }
                }
                Write-Verbose "AppendLeaf: $AppendLeaf"
                Write-Verbose "CheckPath: $CheckPath"
                Write-Verbose "FolderName: $FolderName"
            }
            else {
                $CheckPath = $FilePath
                Write-Verbose "Skipping - AppendLeaf"
            }

            if (Test-Path -Path $CheckPath) {
                $FileCount = Get-ChildItem -Path $CheckPath -File | Measure-Object | Select-Object -ExpandProperty Count
                $Comment = $CheckPath
                Write-Verbose "Good test path - value: $CheckPath"
            } else{
                $Exception = $Error[0].Exception.Message
                if ([string]::IsNullOrEmpty($Exception)) {
                    $Exception = "Path not found: $CheckPath"
                }
                Write-Verbose $Exception
                $FileCount = 0
                $Comment = $Exception
            }

            Write-Verbose "Comment: $Comment"

            return [PSCustomObject]@{
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
                Name              = $FolderName
                Type              = $HealthCheckType
                Status            = $FileCount
                LastUpdate        = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment           = $Comment
                ComputerName      = $ENV:COMPUTERNAME
            }
        }
        else {
            $Exception = $Error[0].Exception.Message
            if ([string]::IsNullOrEmpty($Exception)) {
                $Exception = "Path not found: $FilePath"
            }
            Write-Verbose $Exception

            return [PSCustomObject]@{
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
                Name              = $FilePath
                Type              = $HealthCheckType
                Status            = 'ERROR'
                LastUpdate        = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment           = $Exception
                ComputerName      = $ENV:COMPUTERNAME
            }
        }
    }
    catch {
        $Exception = $Error[0].Exception.Message
        if ([string]::IsNullOrEmpty($Exception)) {
            $Exception = "Path not found: $FilePath"
        }
        Write-Verbose $Exception

        return [PSCustomObject]@{
            SystemName        = $SystemName
            SystemDescription = $SystemDescription
            Name              = $FilePath
            Type              = $HealthCheckType
            Status            = 'ERROR'
            LastUpdate        = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
            Comment           = $Exception
            ComputerName      = $ENV:COMPUTERNAME
        }
    }
}

#Requires -Version 5
<#
 .Synopsis
 Run health checks against one or more systems defined in JSON config files.
 
 .Description
 Reads one or more JSON configuration files and runs the appropriate health checks
 (processes, services, files, shares, URIs, scheduled tasks, file counts) for each
 system defined. Results are collected into a flat list and written to an output
 JSON file under .\output_files\.
 
 .Parameter ConfigFileName
 One or more FileInfo or path objects pointing to the JSON configuration files to process.
 
 .Example
Get-SystemHealth -ConfigFileName ".\config_files\system1.json",".\config_files\system2.json"
 
 #>

function Get-SystemHealth {
    [CmdletBinding()]
    param (
        [System.Object[]]$ConfigFileName
    )

    $ConfigFileName | ForEach-Object {
        
        $ConfigFile = $_
        $SystemHealthData = @()

        $file = Get-Content -Path $ConfigFile.FullName | ConvertFrom-Json
        $SystemName = $file.systemName
        $SystemDescription = $file.description
        
        $file.Processes | ForEach-Object {
            $procSplat = @{
                ProcessName       = $_.name
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
            }
            $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-ProcessHealth @procSplat))
        }

        $file.Services | ForEach-Object {
            $serviceSplat = @{
                ServiceName       = $_.name
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
            }
            $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-ServiceHealth @serviceSplat))
        }

        $file.FilesExist | ForEach-Object {
            $checkfileSplat = @{
                FilePath          = $_.FilePath
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
            }
            $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-FileExists @checkfileSplat))
        }

        $file.SharesExist | ForEach-Object {
            $checkshareSplat = @{
                SharePath         = $_.SharePath
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
            }
            $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-ShareExists @checkshareSplat))
        }

        $File.URIs | ForEach-Object {
            $checkURISplat = @{
                URI                   = $_.URI
                SystemName            = $SystemName
                SystemDescription     = $SystemDescription
                UseBasicParsing       = $_.useBasicParsing
                UseDefaultCredentials = $_.useDefaultCredentials
            }
            $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-URIHealth @checkURISplat))
        }

        $file.ScheduledTasks | ForEach-Object {
            $schedtaskSplat = @{
                TaskPath          = $_.TaskPath
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
            }
            $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-ScheduledTask @schedtaskSplat))
        }

        $file.FileCount | ForEach-Object {
            $filecountSplat = @{
                FilePath          = $_.FilePath
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
                AppendLeaf        = $_.appendLeaf
                LeafFormat        = $_.leafFormat
            }
            $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Get-FileCount @filecountSplat))
        }

        $OutFileName = ".\output_files\healthcheck_$($ENV:COMPUTERNAME)_$($ConfigFile.Name)"
        $SystemHealthData | ConvertTo-Json | Out-File (New-Item -Path $OutFileName -Force)
        $SystemHealthData
    }
}
#Requires -Version 5
<#
 .Synopsis
 This function calls the error lookup tool to get detailed information about an error.
 
 .Description
 This function is intended to be used with Get-SystemHealth and relies on $ScriptDirectory for proper function.
 
 Requires the Microsoft Error Lookup Tool (err.exe), included in the project. For details
 on error lookup tool see article:
 https://www.microsoft.com/en-us/download/details.aspx?id=100432&msockid=2fd802363d216c82121f16d63c406d64
 
 The tool can also be installed by using winget (winget install Microsoft.err). This function expects
 the tool is NOT installed and runs it from the project folder.
 
 NOTE: By default, this function checks errors against the winerror.h file.
 
 .Parameter ErrorCode
 The numeric error code to look up. Accepts decimal or hex integers, e.g. 5 or 0x80070005.
 
 .Example
 Get-Win32Error 0x80070005 # Access Denied error
 #>

function Get-Win32Error {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int]$ErrorCode
    )

    # Path to the Error Lookup Tool executable
    $errExe = Join-Path -Path $ScriptDirectory -ChildPath "Includes\err.exe"

    Write-Verbose "Error Tool Path: {$errExe}"

    # Check if the executable exists
    if (!(Test-Path $errExe)) {
        Write-Error "Error Lookup Tool not found at $errExe"
        return
    }

    # Run the tool and capture output
    $output = & $errExe "/winerror.h" $ErrorCode

    # Parse the output and return the message
    # this is a rough parse
    if ($output -match "winerror.h") {
        $returnvalue = $output -join " "
        $returnvalue = $returnvalue -replace "#", '`r`n'
        return $returnvalue
    }
    else {
        return "Error code not found"
    }
}

#Requires -Version 5
<#
 .Synopsis
 Check whether a file or directory path exists.
 
 .Description
 Uses Test-Path to determine whether the supplied path is present on disk.
 Returns 'Exists' when the path is found, 'Not Found' when it is absent, or
 'ERROR' if an unexpected exception occurs (e.g. access denied, invalid path).
 
 .Parameter FilePath
 Full path to the file or directory to check.
 
 .Parameter SystemName
 Friendly name for the system this check belongs to (used in reporting).
 
 .Parameter SystemDescription
 Short description of the system (used in reporting).
 
 .Example
Test-FileExists -FilePath "c:\my\file"
 #>

function Test-FileExists {
    [CmdletBinding()]
    param (
        [string]$FilePath,
        [string]$SystemName,
        [string]$SystemDescription
    )

    $HealthCheckType = 'FileExists'

    $filename = Split-Path -Path $FilePath -Leaf

    try {
        if (Test-Path -Path $FilePath) {
            return [PSCustomObject]@{
                SystemName          = $SystemName
                SystemDescription   = $SystemDescription
                Name                = $filename
                Type                = $HealthCheckType
                Status              = 'Exists'
                LastUpdate          = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment             = $FilePath
                ComputerName        = $ENV:COMPUTERNAME
            }
        } else {
            return [PSCustomObject]@{
                SystemName          = $SystemName
                SystemDescription   = $SystemDescription
                Name                = $filename
                Type                = $HealthCheckType
                Status              = 'Not Found'
                LastUpdate          = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment             = $FilePath
                ComputerName        = $ENV:COMPUTERNAME
            }
        }
    } catch {
        return [PSCustomObject]@{
            SystemName          = $SystemName
            SystemDescription   = $SystemDescription
            Name                = $FilePath
            Type                = $HealthCheckType
            Status              = 'ERROR'
            LastUpdate          = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
            Comment             = $Error[0].Exception.Message
            ComputerName        = $ENV:COMPUTERNAME
        }
    }
}

#Requires -Version 5
<#
 .Synopsis
 Check if a process is running and responding.
 
 .Description
 Uses Get-Process to find the named process and checks the Responding flag.
 Returns 'Responding' if the process is found and not hung, or 'ERROR' if the
 process is not running or is unresponsive.
 
 .Parameter ProcessName
 The name of the process to check (without the .exe extension).
 
 .Parameter SystemName
 Friendly name for the system this check belongs to (used in reporting).
 
 .Parameter SystemDescription
 Short description of the system (used in reporting).
 
 .Example
Test-ProcessHealth -ProcessName "explorer"
 #>

 function Test-ProcessHealth {
    [CmdletBinding()]
    param (
        [string]$ProcessName,
        [string]$SystemName,
        [string]$SystemDescription
    )
    $HealthCheckType = 'Process'
    try {
        $process = Get-Process -Name $ProcessName -ErrorAction Stop
        if ($process.Responding -eq $true) {
            return [PSCustomObject]@{
                SystemName          = $SystemName
                SystemDescription   = $SystemDescription
                Name                = $ProcessName
                Type                = $HealthCheckType
                Status              = 'Responding'
                LastUpdate          = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment             = ""
                ComputerName        = $ENV:COMPUTERNAME
            }
        } else {
            return [PSCustomObject]@{
                SystemName          = $SystemName
                SystemDescription   = $SystemDescription
                Name                = $ProcessName
                Type                = $HealthCheckType
                Status              = 'ERROR'
                LastUpdate          = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment             = "Process not responding. Status: $($process.Responding)"
                ComputerName        = $ENV:COMPUTERNAME
            }
        }
    } catch {
        return [PSCustomObject]@{
            SystemName                  = $SystemName
            SystemDescription           = $SystemDescription
            Name                        = $ProcessName
            Type                        = $HealthCheckType
            Status                      = 'ERROR'
            LastUpdate                  = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
            Comment                     = $_.Exception.Message
            ComputerName                = $ENV:COMPUTERNAME
        }
    }
}

#Requires -Version 5
<#
 .Synopsis
 Get the last-run status of a scheduled task.
 
 .Description
 Retrieves task run information using Get-ScheduledTaskInfo and checks
 LastTaskResult. A result of 0 means the task completed successfully.
 Any other code is looked up via Get-Win32Error so you get a human-readable
 description rather than a raw hex value.
 
 .Parameter TaskPath
 Full task path including folder, e.g. '\Tasks\Send Email'.
 
 .Parameter SystemName
 Friendly name for the system this check belongs to (used in reporting).
 
 .Parameter SystemDescription
 Short description of the system (used in reporting).
 
 .Example
Test-ScheduledTask -TaskPath "\Tasks\Send Email"
 #>

function Test-ScheduledTask {
    [CmdletBinding()]
    param (
        [string]$TaskPath,
        [string]$SystemName,
        [string]$SystemDescription
    )

    $HealthCheckType = 'ScheduledTask'

    $task = Split-Path -Path $TaskPath -Leaf
    $path = Split-Path -Path $TaskPath -Parent

    try {
        $taskdetail = Get-ScheduledTaskInfo -TaskName $task -TaskPath $path -ErrorAction Stop
        if ($taskdetail -and $taskdetail.LastTaskResult -eq '0') {
            return [PSCustomObject]@{
                SystemName          = $SystemName
                SystemDescription   = $SystemDescription
                Name                = $TaskPath
                Type                = $HealthCheckType
                Status              = 'OK'
                LastUpdate          = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment             = "LastRunTime: $($taskdetail.LastRunTime) NextRunTime: $($taskdetail.NextRunTime) MissedRuns: $($taskdetail.NumberOfMissedRuns)"
                ComputerName        = $ENV:COMPUTERNAME
            }
        }
        else {
            if (!$null -eq $taskdetail) {
                $errorResult = Get-Win32Error -ErrorCode $taskdetail.LastTaskResult
                $parsedErrorMessage = $errorResult -split '`r`n'
    
                Write-Verbose "Parsed Error: {$parsedErrorMessage}"
    
                if (-not [string]::IsNullOrEmpty($parsedErrorMessage)) {
                    $errMessage = $parsedErrorMessage[2].Trim()
                    $errCodeConverted = $parsedErrorMessage[1].Trim() -replace '\s{2,}', ', '
                }
                else {
                    $errMessage = ""
                    $errCodeConverted = 0
                }
    
                return [PSCustomObject]@{
                    SystemName          = $SystemName
                    SystemDescription   = $SystemDescription
                    Name                = $TaskPath
                    Type                = $HealthCheckType
                    Status              = ("{0} - {1}" -f $taskdetail.LastTaskResult, $errCodeConverted)
                    LastUpdate          = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                    Comment             = $errMessage
                    ComputerName        = $ENV:COMPUTERNAME
                }
            }
            else {
                return [PSCustomObject]@{
                    SystemName          = $SystemName
                    SystemDescription   = $SystemDescription
                    Name                = $TaskPath
                    Type                = $HealthCheckType
                    Status              = 'ERROR'
                    LastUpdate          = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                    Comment             = "Task not found."
                    ComputerName        = $ENV:COMPUTERNAME
                }
            }
        }
    }
    catch {
        return [PSCustomObject]@{
            SystemName              = $SystemName
            SystemDescription       = $SystemDescription
            Name                    = $TaskPath
            Type                    = $HealthCheckType
            Status                  = 'ERROR'
            LastUpdate              = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
            Comment                 = "Task not found."
            ComputerName            = $ENV:COMPUTERNAME
        }
    }
}

#Requires -Version 5
<#
 .Synopsis
 Check the running status of a Windows service.
 
 .Description
 Retrieves the named service using Get-Service and checks whether it is in the
 Running state. Returns 'OK' if running, or 'ERROR' with the current status in
 the Comment field if stopped, paused, or not found.
 
 .Parameter ServiceName
 The short service name (not the display name) to check, e.g. 'w3svc'.
 
 .Parameter SystemName
 Friendly name for the system this check belongs to (used in reporting).
 
 .Parameter SystemDescription
 Short description of the system (used in reporting).
 
 .Example
Test-ServiceHealth -ServiceName 'w3svc'
 #>

function Test-ServiceHealth {
    [CmdletBinding()]
    param (
        [string]$ServiceName,
        [string]$SystemName,
        [string]$SystemDescription
    )

    $HealthCheckType = 'Service'

    try {
        $service = Get-Service -Name $ServiceName -ErrorAction Stop
        if ($service.Status -eq 'Running') {
            return [PSCustomObject]@{
                SystemName          = $SystemName
                SystemDescription   = $SystemDescription
                Name                = $ServiceName
                Type                = $HealthCheckType
                Status              = 'OK'
                LastUpdate          = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment             = ""
                ComputerName        = $ENV:COMPUTERNAME
            }
        } else {
            return [PSCustomObject]@{
                SystemName          = $SystemName
                SystemDescription   = $SystemDescription
                Name                = $ServiceName
                Type                = $HealthCheckType
                Status              = 'ERROR'
                LastUpdate          = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment             = $service.Status
                ComputerName        = $ENV:COMPUTERNAME
            }
        }
    } catch {
        return [PSCustomObject]@{
            SystemName              = $SystemName
            SystemDescription       = $SystemDescription
            Name                    = $ServiceName
            Type                    = $HealthCheckType
            Status                  = 'ERROR'
            LastUpdate              = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
            Comment                 = $_.Exception.Message
            ComputerName        = $ENV:COMPUTERNAME
        }
    }
}

#Requires -Version 5
<#
 .Synopsis
 Check whether a network share or local path is accessible.
 
 .Description
 Uses Test-Path to verify access to a UNC share, drive letter, or local path.
 Handles UNC paths (\\server\share), drive letters (C:), and plain paths, and
 extracts a meaningful share name for reporting in each case.
 Returns 'Exists' when the path is reachable, 'Not Found' when it is not, or
 'ERROR' if an exception is raised.
 
 .Parameter SharePath
 The path to test, e.g. '\\server\e$' or 'D:\Data'.
 
 .Parameter SystemName
 Friendly name for the system this check belongs to (used in reporting).
 
 .Parameter SystemDescription
 Short description of the system (used in reporting).
 
 .Example
Test-ShareExists -SharePath "\\server\e$"
 #>

function Test-ShareExists {
    [CmdletBinding()]
    param (
        [string]$SharePath,
        [string]$SystemName,
        [string]$SystemDescription
    )

    $HealthCheckType = 'ShareExists'

    # Extract share name - handle UNC paths, drive letters, and regular paths
    if ($SharePath -match '^\\\\[^\\]+\\([^\\]+)') {
        # UNC path like \\server\share
        $ShareName = $matches[1]
    }
    elseif ($SharePath -match '^([A-Z]:)') {
        # Drive letter like C:
        $ShareName = $matches[1]
    }
    else {
        # Fall back to GetFileName for other paths
        $ShareName = [System.IO.Path]::GetFileName($SharePath)
        if ([string]::IsNullOrEmpty($ShareName)) {
            $ShareName = $SharePath
        }
    }

    try {
        if (Test-Path -Path $SharePath) {
            return [PSCustomObject]@{
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
                Name              = $ShareName
                Type              = $HealthCheckType
                Status            = 'Exists'
                LastUpdate        = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment           = $SharePath
                ComputerName      = $ENV:COMPUTERNAME
            }
        }
        else {
            return [PSCustomObject]@{
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
                Name              = $ShareName
                Type              = $HealthCheckType
                Status            = 'Not Found'
                LastUpdate        = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment           = $SharePath
                ComputerName      = $ENV:COMPUTERNAME
            }
        }
    }
    catch {
        return [PSCustomObject]@{
            SystemName        = $SystemName
            SystemDescription = $SystemDescription
            Name              = $ShareName
            Type              = $HealthCheckType
            Status            = 'ERROR'
            LastUpdate        = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
            Comment           = $Error[0].Exception.Message
            ComputerName      = $ENV:COMPUTERNAME
        }
    }
}

#Requires -Version 5
<#
 .Synopsis
 Compare the current date/time on two remote systems.
 
 .Description
 Opens a temporary PSSession to each system, retrieves the current date/time,
 and calculates the difference as a TimeSpan. Useful for spotting NTP drift
 between servers that need to stay in sync (e.g. domain controllers or cluster
 nodes). If a session cannot be established, the affected system is reported
 as 'ERROR' and the difference is returned as 0.
 
 .Parameter System1Name
 Hostname or IP address of the first system.
 
 .Parameter System2Name
 Hostname or IP address of the second system.
 
 .Example
Test-TimeSync -System1Name "server1" -System2Name "server2" -Verbose
 #>

function Test-TimeSync {
    [CmdletBinding()]
    param (
        [string]$System1Name,
        [string]$System2Name
    )

    $HealthCheckType = 'TimeSync'

    $session1 = New-PSSession -ComputerName $System1Name -ErrorAction SilentlyContinue
    if ($session1) {
        Write-Verbose "Collecting current date/time from $System1Name"
        $timedate = Invoke-Command -Session $session1 -ScriptBlock { Get-Date }
        $System1DateTime = [PSCustomObject]@{
            SystemName     = $System1Name
            SystemDateTime = $timedate
            Status         = 'Success'
            Comment        = ''
        }
    }
    else {
        $System1DateTime = [PSCustomObject]@{
            SystemName     = $System1Name
            SystemDateTime = ''
            Status         = 'ERROR'
            Comment        = "Unable to establish a session with '$System1Name'"
        }
    }

    $session2 = New-PSSession -ComputerName $System2Name -ErrorAction SilentlyContinue
    if ($session2) {
        Write-Verbose "Collecting current date/time from $System2Name"
        $timedate = Invoke-Command -Session $session2 -ScriptBlock { Get-Date }
        $System2DateTime = [PSCustomObject]@{
            SystemName     = $System2Name
            SystemDateTime = $timedate
            Status         = 'Success'
            Comment        = ''
        }
    }
    else {
        $System2DateTime = [PSCustomObject]@{
            SystemName     = $System2Name
            SystemDateTime = ''
            Status         = 'ERROR'
            Comment        = "Unable to establish a session with '$System2Name'"
        }
    }
    if ($System1DateTime.Status -eq 'ERROR' -or $System2DateTime.Status -eq 'ERROR') {
        $DateTimeDifference = 0
        $Status = "ERROR"
    }
    else {
        $difference = New-TimeSpan -Start $System1DateTime.SystemDateTime -End $System2DateTime.SystemDateTime
        $DateTimeDifference = $difference
        $Status = 'Success'
    }
    $System1DateTime | Format-Table -AutoSize | Out-String | Write-Verbose
    $System2DateTime  | Format-Table -AutoSize | Out-String | Write-Verbose

    return [PSCustomObject]@{
        System1Name     = $System1Name
        System1DateTime = $System1DateTime.SystemDateTime
        System2Name     = $System2Name
        System2DateTime = $System2DateTime.SystemDateTime
        Type            = $HealthCheckType
        Status          = $Status
        Difference      = $DateTimeDifference
        LastUpdate      = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
        Comment         = @($System1DateTime.Comment,$System2DateTime.Comment)
    }
}

#Requires -Version 5
<#
 .Synopsis
 Test whether a web endpoint is reachable and returns HTTP 200.
 
 .Description
 Sends an HTTP request to the given URI using Invoke-WebRequest. Returns 'OK'
 when the server responds with a 200 status code, or an error/status description
 when the response indicates a problem. Exceptions (connection refused, DNS
 failure, etc.) are caught and returned as 'ERROR' results.
 
 .Parameter URI
 The full URI to request, e.g. 'http://server/health'.
 
 .Parameter SystemName
 Friendly name for the system this check belongs to (used in reporting).
 
 .Parameter SystemDescription
 Short description of the system (used in reporting).
 
 .Parameter UseBasicParsing
 Pass $true to use basic parsing (avoids IE engine dependency on servers without a GUI).
 
 .Parameter UseDefaultCredentials
 Pass $true to send the current user's Windows credentials with the request.
 
 .Example
Test-URIHealth -URI "http://server/health"
 #>

function Test-URIHealth {
    [CmdletBinding()]
    param (
        [string]$URI,
        [string]$SystemName,
        [string]$SystemDescription,
        [bool]$UseBasicParsing,
        [bool]$UseDefaultCredentials
    )

    $HealthCheckType = 'URI'

    try {
        $WebRequestSplat = @{
            URI                   = $URI
            UseBasicParsing       = $UseBasicParsing
            UseDefaultCredentials = $UseDefaultCredentials
        }
        $response = Invoke-WebRequest @WebRequestSplat
        if (!$null -eq $response) {
            if ($response.StatusCode -eq '200') {
                return [PSCustomObject]@{
                    SystemName        = $SystemName
                    SystemDescription = $SystemDescription
                    Name              = $URI
                    Type              = $HealthCheckType
                    Status            = 'OK'
                    LastUpdate        = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                    Comment           = ""
                    ComputerName      = $ENV:COMPUTERNAME
                }
            }
            else {
                return [PSCustomObject]@{
                    SystemName        = $SystemName
                    SystemDescription = $SystemDescription
                    Name              = $URI
                    Type              = $HealthCheckType
                    Status            = $response.StatusDescription
                    LastUpdate        = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                    Comment           = $response.StatusCode
                    ComputerName      = $ENV:COMPUTERNAME
                }
            }
        }
        else {
            return [PSCustomObject]@{
                SystemName        = $SystemName
                SystemDescription = $SystemDescription
                Name              = $URI
                Type              = $HealthCheckType
                Status            = 'ERROR'
                LastUpdate        = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                Comment           = $Error[0].Exception.Message
                ComputerName      = $ENV:COMPUTERNAME
            }
        }
    }
    catch {
        return [PSCustomObject]@{
            SystemName        = $SystemName
            SystemDescription = $SystemDescription
            Name              = $URI
            Type              = $HealthCheckType
            Status            = 'ERROR'
            LastUpdate        = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
            Comment           = $_.Exception.Message
            ComputerName      = $ENV:COMPUTERNAME
        }
    }
}