Private/Test-InfraPulseConfigurationData.ps1

function Test-InfraPulseConfigurationData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Configuration
    )

    $errors = New-Object System.Collections.Generic.List[string]
    $warnings = New-Object System.Collections.Generic.List[string]
    $catalog = @(Get-InfraPulseCheckCatalog)
    $validChecks = @($catalog.Name)

    if (-not $Configuration.Contains('SchemaVersion')) {
        [void]$errors.Add('SchemaVersion is required.')
    }
    elseif ([string]$Configuration.SchemaVersion -ne '1.0') {
        [void]$errors.Add("Unsupported SchemaVersion '$($Configuration.SchemaVersion)'. Supported version: 1.0.")
    }

    if (-not $Configuration.Contains('General') -or -not ($Configuration.General -is [System.Collections.IDictionary])) {
        [void]$errors.Add('General must be a hashtable.')
    }
    else {
        $general = $Configuration.General
        if (-not $general.Contains('DefaultChecks')) {
            [void]$errors.Add('General.DefaultChecks is required.')
        }
        else {
            foreach ($check in @($general.DefaultChecks)) {
                if ([string]$check -notin $validChecks) {
                    [void]$errors.Add("General.DefaultChecks contains unknown check '$check'.")
                }
            }
        }

        if (-not $general.Contains('ConnectionTimeoutSeconds') -or -not (Test-InfraPulseNumber -Value $general.ConnectionTimeoutSeconds -Minimum 1 -Maximum 300)) {
            [void]$errors.Add('General.ConnectionTimeoutSeconds must be between 1 and 300.')
        }

        foreach ($booleanName in @('ContinueOnError', 'IncludeInventory')) {
            if (-not $general.Contains($booleanName) -or -not ($general[$booleanName] -is [bool])) {
                [void]$errors.Add("General.$booleanName must be Boolean.")
            }
        }

        if (-not $general.Contains('EnvironmentName') -or -not ($general['EnvironmentName'] -is [string])) {
            [void]$errors.Add('General.EnvironmentName must be a string.')
        }
    }

    if (-not $Configuration.Contains('Checks') -or -not ($Configuration.Checks -is [System.Collections.IDictionary])) {
        [void]$errors.Add('Checks must be a hashtable.')
    }
    else {
        $checks = $Configuration.Checks
        foreach ($key in $checks.Keys) {
            if ([string]$key -notin $validChecks) {
                [void]$warnings.Add("Checks contains unknown section '$key'; it will not be executed.")
            }
        }

        foreach ($checkName in $validChecks) {
            if (-not $checks.Contains($checkName) -or -not ($checks[$checkName] -is [System.Collections.IDictionary])) {
                [void]$errors.Add("Checks.$checkName must be a hashtable.")
                continue
            }

            if (-not $checks[$checkName].Contains('Enabled') -or -not ($checks[$checkName].Enabled -is [bool])) {
                [void]$errors.Add("Checks.$checkName.Enabled must be Boolean.")
            }
        }

        if ($checks.Contains('Disk')) {
            $disk = $checks.Disk
            foreach ($name in @('WarningFreePercent', 'CriticalFreePercent')) {
                if (-not $disk.Contains($name) -or -not (Test-InfraPulseNumber -Value $disk[$name] -Minimum 0 -Maximum 100)) {
                    [void]$errors.Add("Checks.Disk.$name must be between 0 and 100.")
                }
            }
            foreach ($name in @('WarningFreeGB', 'CriticalFreeGB')) {
                if (-not $disk.Contains($name) -or -not (Test-InfraPulseNumber -Value $disk[$name] -Minimum 0)) {
                    [void]$errors.Add("Checks.Disk.$name must be zero or greater.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $disk.WarningFreePercent) -and
                (Test-InfraPulseNumber -Value $disk.CriticalFreePercent) -and
                [double]$disk.CriticalFreePercent -gt [double]$disk.WarningFreePercent
            ) {
                [void]$errors.Add('Checks.Disk.CriticalFreePercent must be less than or equal to WarningFreePercent.')
            }
            if (
                (Test-InfraPulseNumber -Value $disk.WarningFreeGB) -and
                (Test-InfraPulseNumber -Value $disk.CriticalFreeGB) -and
                [double]$disk.CriticalFreeGB -gt [double]$disk.WarningFreeGB
            ) {
                [void]$errors.Add('Checks.Disk.CriticalFreeGB must be less than or equal to WarningFreeGB.')
            }

            $volumeIndex = 0
            $validVolumeKeys = @('DeviceId', 'WarningFreePercent', 'CriticalFreePercent', 'WarningFreeGB', 'CriticalFreeGB')
            foreach ($volume in @($disk['Volumes'])) {
                if (-not ($volume -is [System.Collections.IDictionary])) {
                    [void]$errors.Add("Checks.Disk.Volumes[$volumeIndex] must be a hashtable.")
                }
                else {
                    if (-not $volume.Contains('DeviceId') -or [string]::IsNullOrWhiteSpace([string]$volume.DeviceId)) {
                        [void]$errors.Add("Checks.Disk.Volumes[$volumeIndex].DeviceId is required.")
                    }
                    foreach ($key in $volume.Keys) {
                        if ([string]$key -notin $validVolumeKeys) {
                            [void]$errors.Add("Checks.Disk.Volumes[$volumeIndex] contains unsupported key '$key'. Supported keys: $($validVolumeKeys -join ', ').")
                        }
                    }
                    foreach ($name in @('WarningFreePercent', 'CriticalFreePercent')) {
                        if ($volume.Contains($name) -and -not (Test-InfraPulseNumber -Value $volume[$name] -Minimum 0 -Maximum 100)) {
                            [void]$errors.Add("Checks.Disk.Volumes[$volumeIndex].$name must be between 0 and 100.")
                        }
                    }
                    foreach ($name in @('WarningFreeGB', 'CriticalFreeGB')) {
                        if ($volume.Contains($name) -and -not (Test-InfraPulseNumber -Value $volume[$name] -Minimum 0)) {
                            [void]$errors.Add("Checks.Disk.Volumes[$volumeIndex].$name must be zero or greater.")
                        }
                    }
                    if (
                        $volume.Contains('WarningFreePercent') -and $volume.Contains('CriticalFreePercent') -and
                        (Test-InfraPulseNumber -Value $volume.WarningFreePercent) -and
                        (Test-InfraPulseNumber -Value $volume.CriticalFreePercent) -and
                        [double]$volume.CriticalFreePercent -gt [double]$volume.WarningFreePercent
                    ) {
                        [void]$errors.Add("Checks.Disk.Volumes[$volumeIndex].CriticalFreePercent must be less than or equal to WarningFreePercent.")
                    }
                    if (
                        $volume.Contains('WarningFreeGB') -and $volume.Contains('CriticalFreeGB') -and
                        (Test-InfraPulseNumber -Value $volume.WarningFreeGB) -and
                        (Test-InfraPulseNumber -Value $volume.CriticalFreeGB) -and
                        [double]$volume.CriticalFreeGB -gt [double]$volume.WarningFreeGB
                    ) {
                        [void]$errors.Add("Checks.Disk.Volumes[$volumeIndex].CriticalFreeGB must be less than or equal to WarningFreeGB.")
                    }
                }
                $volumeIndex++
            }
        }

        if ($checks.Contains('Memory')) {
            $memory = $checks.Memory
            foreach ($name in @('WarningAvailablePercent', 'CriticalAvailablePercent', 'WarningCommitPercent', 'CriticalCommitPercent')) {
                if (-not $memory.Contains($name) -or -not (Test-InfraPulseNumber -Value $memory[$name] -Minimum 0 -Maximum 100)) {
                    [void]$errors.Add("Checks.Memory.$name must be between 0 and 100.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $memory.WarningAvailablePercent) -and
                (Test-InfraPulseNumber -Value $memory.CriticalAvailablePercent) -and
                [double]$memory.CriticalAvailablePercent -gt [double]$memory.WarningAvailablePercent
            ) {
                [void]$errors.Add('Checks.Memory.CriticalAvailablePercent must be less than or equal to WarningAvailablePercent.')
            }
            if (
                (Test-InfraPulseNumber -Value $memory.WarningCommitPercent) -and
                (Test-InfraPulseNumber -Value $memory.CriticalCommitPercent) -and
                [double]$memory.CriticalCommitPercent -lt [double]$memory.WarningCommitPercent
            ) {
                [void]$errors.Add('Checks.Memory.CriticalCommitPercent must be greater than or equal to WarningCommitPercent.')
            }
        }

        if ($checks.Contains('Cpu')) {
            $cpu = $checks.Cpu
            if (-not $cpu.Contains('SampleCount') -or -not (Test-InfraPulseNumber -Value $cpu.SampleCount -Minimum 1 -Maximum 10)) {
                [void]$errors.Add('Checks.Cpu.SampleCount must be between 1 and 10.')
            }
            if (-not $cpu.Contains('SampleIntervalSeconds') -or -not (Test-InfraPulseNumber -Value $cpu.SampleIntervalSeconds -Minimum 1 -Maximum 30)) {
                [void]$errors.Add('Checks.Cpu.SampleIntervalSeconds must be between 1 and 30.')
            }
            foreach ($name in @('WarningPercent', 'CriticalPercent')) {
                if (-not $cpu.Contains($name) -or -not (Test-InfraPulseNumber -Value $cpu[$name] -Minimum 0 -Maximum 100)) {
                    [void]$errors.Add("Checks.Cpu.$name must be between 0 and 100.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $cpu.WarningPercent) -and
                (Test-InfraPulseNumber -Value $cpu.CriticalPercent) -and
                [double]$cpu.CriticalPercent -lt [double]$cpu.WarningPercent
            ) {
                [void]$errors.Add('Checks.Cpu.CriticalPercent must be greater than or equal to WarningPercent.')
            }
        }

        if ($checks.Contains('ScheduledTasks')) {
            $scheduledTasks = $checks.ScheduledTasks
            foreach ($patternName in @('IncludePaths', 'ExcludePaths', 'ExcludeTasks')) {
                foreach ($pattern in @($scheduledTasks[$patternName])) {
                    if ([string]::IsNullOrWhiteSpace([string]$pattern)) {
                        [void]$errors.Add("Checks.ScheduledTasks.$patternName cannot contain an empty pattern.")
                    }
                }
            }
            foreach ($excludeResult in @($scheduledTasks['ExcludeResults'])) {
                if (-not (Test-InfraPulseNumber -Value $excludeResult -Minimum 0)) {
                    [void]$errors.Add("Checks.ScheduledTasks.ExcludeResults contains invalid result code '$excludeResult'.")
                }
            }
            foreach ($name in @('WarningCount', 'CriticalCount')) {
                if (-not $scheduledTasks.Contains($name) -or -not (Test-InfraPulseNumber -Value $scheduledTasks[$name] -Minimum 0)) {
                    [void]$errors.Add("Checks.ScheduledTasks.$name must be zero or greater.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $scheduledTasks.WarningCount) -and
                (Test-InfraPulseNumber -Value $scheduledTasks.CriticalCount) -and
                [double]$scheduledTasks.CriticalCount -lt [double]$scheduledTasks.WarningCount
            ) {
                [void]$errors.Add('Checks.ScheduledTasks.CriticalCount must be greater than or equal to WarningCount.')
            }
        }

        if ($checks.Contains('Defender')) {
            $defender = $checks.Defender
            foreach ($name in @('SignatureWarningDays', 'SignatureCriticalDays')) {
                if (-not $defender.Contains($name) -or -not (Test-InfraPulseNumber -Value $defender[$name] -Minimum 0)) {
                    [void]$errors.Add("Checks.Defender.$name must be zero or greater.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $defender.SignatureWarningDays) -and
                (Test-InfraPulseNumber -Value $defender.SignatureCriticalDays) -and
                [double]$defender.SignatureCriticalDays -lt [double]$defender.SignatureWarningDays
            ) {
                [void]$errors.Add('Checks.Defender.SignatureCriticalDays must be greater than or equal to SignatureWarningDays.')
            }
        }

        if ($checks.Contains('Stability')) {
            $stability = $checks.Stability
            if (-not $stability.Contains('LookbackDays') -or -not (Test-InfraPulseNumber -Value $stability.LookbackDays -Minimum 1 -Maximum 365)) {
                [void]$errors.Add('Checks.Stability.LookbackDays must be between 1 and 365.')
            }
            foreach ($name in @('WarningCount', 'CriticalCount')) {
                if (-not $stability.Contains($name) -or -not (Test-InfraPulseNumber -Value $stability[$name] -Minimum 0)) {
                    [void]$errors.Add("Checks.Stability.$name must be zero or greater.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $stability.WarningCount) -and
                (Test-InfraPulseNumber -Value $stability.CriticalCount) -and
                [double]$stability.CriticalCount -lt [double]$stability.WarningCount
            ) {
                [void]$errors.Add('Checks.Stability.CriticalCount must be greater than or equal to WarningCount.')
            }
        }

        if ($checks.Contains('Uptime')) {
            $uptime = $checks.Uptime
            foreach ($name in @('WarningDays', 'CriticalDays')) {
                if (-not $uptime.Contains($name) -or -not (Test-InfraPulseNumber -Value $uptime[$name] -Minimum 0)) {
                    [void]$errors.Add("Checks.Uptime.$name must be zero or greater.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $uptime.WarningDays) -and
                (Test-InfraPulseNumber -Value $uptime.CriticalDays) -and
                [double]$uptime.CriticalDays -lt [double]$uptime.WarningDays
            ) {
                [void]$errors.Add('Checks.Uptime.CriticalDays must be greater than or equal to WarningDays.')
            }
        }

        if ($checks.Contains('PendingReboot')) {
            $pending = $checks.PendingReboot
            if (-not $pending.Contains('PendingStatus') -or [string]$pending.PendingStatus -notin @('Warning', 'Critical')) {
                [void]$errors.Add('Checks.PendingReboot.PendingStatus must be Warning or Critical.')
            }
            foreach ($pattern in @($pending['ExcludeReasons'])) {
                if ([string]::IsNullOrWhiteSpace([string]$pattern)) {
                    [void]$errors.Add('Checks.PendingReboot.ExcludeReasons cannot contain an empty pattern.')
                }
            }
        }

        if ($checks.Contains('PatchAge')) {
            $patchAge = $checks.PatchAge
            foreach ($name in @('WarningDays', 'CriticalDays')) {
                if (-not $patchAge.Contains($name) -or -not (Test-InfraPulseNumber -Value $patchAge[$name] -Minimum 0)) {
                    [void]$errors.Add("Checks.PatchAge.$name must be zero or greater.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $patchAge.WarningDays) -and
                (Test-InfraPulseNumber -Value $patchAge.CriticalDays) -and
                [double]$patchAge.CriticalDays -lt [double]$patchAge.WarningDays
            ) {
                [void]$errors.Add('Checks.PatchAge.CriticalDays must be greater than or equal to WarningDays.')
            }
        }

        if ($checks.Contains('Services')) {
            $services = $checks.Services
            if (-not $services.Contains('Required')) {
                [void]$errors.Add('Checks.Services.Required is required.')
            }
            else {
                $index = 0
                foreach ($service in @($services.Required)) {
                    if (-not ($service -is [System.Collections.IDictionary])) {
                        [void]$errors.Add("Checks.Services.Required[$index] must be a hashtable.")
                    }
                    else {
                        if (-not $service.Contains('Name') -or [string]::IsNullOrWhiteSpace([string]$service.Name)) {
                            [void]$errors.Add("Checks.Services.Required[$index].Name is required.")
                        }
                        if (-not $service.Contains('ExpectedStatus') -or [string]$service.ExpectedStatus -notin @('Running', 'Stopped', 'Paused')) {
                            [void]$errors.Add("Checks.Services.Required[$index].ExpectedStatus must be Running, Stopped, or Paused.")
                        }
                        if (-not $service.Contains('Severity') -or [string]$service.Severity -notin @('Warning', 'Critical')) {
                            [void]$errors.Add("Checks.Services.Required[$index].Severity must be Warning or Critical.")
                        }
                    }
                    $index++
                }
            }
        }

        if ($checks.Contains('Certificates')) {
            $certificates = $checks.Certificates
            foreach ($name in @('WarningDays', 'CriticalDays')) {
                if (-not $certificates.Contains($name) -or -not (Test-InfraPulseNumber -Value $certificates[$name] -Minimum 0)) {
                    [void]$errors.Add("Checks.Certificates.$name must be zero or greater.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $certificates.WarningDays) -and
                (Test-InfraPulseNumber -Value $certificates.CriticalDays) -and
                [double]$certificates.CriticalDays -gt [double]$certificates.WarningDays
            ) {
                [void]$errors.Add('Checks.Certificates.CriticalDays must be less than or equal to WarningDays.')
            }
            foreach ($storePath in @($certificates.StorePaths)) {
                if ([string]$storePath -notlike 'Cert:\*') {
                    [void]$errors.Add("Checks.Certificates.StorePaths contains invalid certificate provider path '$storePath'.")
                }
            }
            foreach ($patternName in @('SubjectExcludePatterns', 'IssuerExcludePatterns')) {
                foreach ($pattern in @($certificates[$patternName])) {
                    if ([string]::IsNullOrWhiteSpace([string]$pattern)) {
                        [void]$errors.Add("Checks.Certificates.$patternName cannot contain an empty pattern.")
                    }
                }
            }
            foreach ($booleanName in @('RequirePrivateKey', 'TreatShortLivedAsRotating')) {
                if (-not $certificates.Contains($booleanName) -or -not ($certificates[$booleanName] -is [bool])) {
                    [void]$errors.Add("Checks.Certificates.$booleanName must be Boolean.")
                }
            }
            if (-not $certificates.Contains('MinTotalLifetimeDays') -or -not (Test-InfraPulseNumber -Value $certificates.MinTotalLifetimeDays -Minimum 0)) {
                [void]$errors.Add('Checks.Certificates.MinTotalLifetimeDays must be zero or greater.')
            }
        }

        if ($checks.Contains('EventLog')) {
            $eventLog = $checks.EventLog
            if (-not $eventLog.Contains('LookbackHours') -or -not (Test-InfraPulseNumber -Value $eventLog.LookbackHours -Minimum 0.01 -Maximum 8760)) {
                [void]$errors.Add('Checks.EventLog.LookbackHours must be greater than zero and no greater than 8760.')
            }
            foreach ($name in @('WarningCount', 'CriticalCount')) {
                if (-not $eventLog.Contains($name) -or -not (Test-InfraPulseNumber -Value $eventLog[$name] -Minimum 0)) {
                    [void]$errors.Add("Checks.EventLog.$name must be zero or greater.")
                }
            }
            if (-not $eventLog.Contains('MaxEvents') -or -not (Test-InfraPulseNumber -Value $eventLog.MaxEvents -Minimum 1 -Maximum 50000)) {
                [void]$errors.Add('Checks.EventLog.MaxEvents must be between 1 and 50000.')
            }
            foreach ($level in @($eventLog.Levels)) {
                if (-not (Test-InfraPulseNumber -Value $level -Minimum 1 -Maximum 5)) {
                    [void]$errors.Add("Checks.EventLog.Levels contains invalid level '$level'; valid levels are 1 through 5.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $eventLog.WarningCount) -and
                (Test-InfraPulseNumber -Value $eventLog.CriticalCount) -and
                [double]$eventLog.CriticalCount -lt [double]$eventLog.WarningCount
            ) {
                [void]$errors.Add('Checks.EventLog.CriticalCount must be greater than or equal to WarningCount.')
            }
            if (
                (Test-InfraPulseNumber -Value $eventLog.MaxEvents) -and
                (Test-InfraPulseNumber -Value $eventLog.CriticalCount) -and
                [double]$eventLog.MaxEvents -lt [double]$eventLog.CriticalCount
            ) {
                [void]$warnings.Add('Checks.EventLog.MaxEvents is lower than CriticalCount; a truncated query may understate severity.')
            }
            if (-not $eventLog.Contains('IncludeMessages') -or -not ($eventLog.IncludeMessages -is [bool])) {
                [void]$errors.Add('Checks.EventLog.IncludeMessages must be Boolean.')
            }
        }

        if ($checks.Contains('Dns')) {
            $dns = $checks.Dns
            if (-not $dns.Contains('QueryType') -or [string]$dns.QueryType -notin @('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT')) {
                [void]$errors.Add('Checks.Dns.QueryType must be A, AAAA, CNAME, MX, NS, PTR, SRV, or TXT.')
            }
            foreach ($target in @($dns.Targets)) {
                if ($target -is [string]) {
                    if ([string]::IsNullOrWhiteSpace($target)) {
                        [void]$errors.Add('Checks.Dns.Targets cannot contain an empty string.')
                    }
                }
                elseif ($target -is [System.Collections.IDictionary]) {
                    if (-not $target.Contains('Name') -or [string]::IsNullOrWhiteSpace([string]$target.Name)) {
                        [void]$errors.Add('Each Checks.Dns.Targets hashtable requires Name.')
                    }
                    if ($target.Contains('Type') -and [string]$target.Type -notin @('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT')) {
                        [void]$errors.Add("Checks.Dns.Targets contains invalid query type '$($target.Type)'.")
                    }
                }
                else {
                    [void]$errors.Add('Checks.Dns.Targets entries must be strings or hashtables.')
                }
            }
        }

        if ($checks.Contains('Tcp')) {
            $tcp = $checks.Tcp
            if (-not $tcp.Contains('TimeoutMilliseconds') -or -not (Test-InfraPulseNumber -Value $tcp.TimeoutMilliseconds -Minimum 100 -Maximum 60000)) {
                [void]$errors.Add('Checks.Tcp.TimeoutMilliseconds must be between 100 and 60000.')
            }
            $index = 0
            foreach ($endpoint in @($tcp.Endpoints)) {
                if (-not ($endpoint -is [System.Collections.IDictionary])) {
                    [void]$errors.Add("Checks.Tcp.Endpoints[$index] must be a hashtable.")
                }
                else {
                    if (-not $endpoint.Contains('Host') -or [string]::IsNullOrWhiteSpace([string]$endpoint.Host)) {
                        [void]$errors.Add("Checks.Tcp.Endpoints[$index].Host is required.")
                    }
                    if (-not $endpoint.Contains('Port') -or -not (Test-InfraPulseNumber -Value $endpoint.Port -Minimum 1 -Maximum 65535)) {
                        [void]$errors.Add("Checks.Tcp.Endpoints[$index].Port must be between 1 and 65535.")
                    }
                    if ($endpoint.Contains('TimeoutMilliseconds') -and -not (Test-InfraPulseNumber -Value $endpoint.TimeoutMilliseconds -Minimum 100 -Maximum 60000)) {
                        [void]$errors.Add("Checks.Tcp.Endpoints[$index].TimeoutMilliseconds must be between 100 and 60000.")
                    }
                }
                $index++
            }
        }

        if ($checks.Contains('Tls')) {
            $tls = $checks.Tls
            if (-not $tls.Contains('TimeoutMilliseconds') -or -not (Test-InfraPulseNumber -Value $tls.TimeoutMilliseconds -Minimum 100 -Maximum 60000)) {
                [void]$errors.Add('Checks.Tls.TimeoutMilliseconds must be between 100 and 60000.')
            }
            foreach ($name in @('WarningDays', 'CriticalDays')) {
                if (-not $tls.Contains($name) -or -not (Test-InfraPulseNumber -Value $tls[$name] -Minimum 0)) {
                    [void]$errors.Add("Checks.Tls.$name must be zero or greater.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $tls.WarningDays) -and
                (Test-InfraPulseNumber -Value $tls.CriticalDays) -and
                [double]$tls.CriticalDays -gt [double]$tls.WarningDays
            ) {
                [void]$errors.Add('Checks.Tls.CriticalDays must be less than or equal to WarningDays.')
            }
            foreach ($booleanName in @('RequireTrustedChain', 'RequireNameMatch')) {
                if (-not $tls.Contains($booleanName) -or -not ($tls[$booleanName] -is [bool])) {
                    [void]$errors.Add("Checks.Tls.$booleanName must be Boolean.")
                }
            }
            $index = 0
            foreach ($endpoint in @($tls.Endpoints)) {
                if (-not ($endpoint -is [System.Collections.IDictionary])) {
                    [void]$errors.Add("Checks.Tls.Endpoints[$index] must be a hashtable.")
                }
                else {
                    if (-not $endpoint.Contains('Host') -or [string]::IsNullOrWhiteSpace([string]$endpoint.Host)) {
                        [void]$errors.Add("Checks.Tls.Endpoints[$index].Host is required.")
                    }
                    if ($endpoint.Contains('Port') -and -not (Test-InfraPulseNumber -Value $endpoint.Port -Minimum 1 -Maximum 65535)) {
                        [void]$errors.Add("Checks.Tls.Endpoints[$index].Port must be between 1 and 65535.")
                    }
                    if ($endpoint.Contains('Sni') -and [string]::IsNullOrWhiteSpace([string]$endpoint.Sni)) {
                        [void]$errors.Add("Checks.Tls.Endpoints[$index].Sni cannot be empty.")
                    }
                    if ($endpoint.Contains('TimeoutMilliseconds') -and -not (Test-InfraPulseNumber -Value $endpoint.TimeoutMilliseconds -Minimum 100 -Maximum 60000)) {
                        [void]$errors.Add("Checks.Tls.Endpoints[$index].TimeoutMilliseconds must be between 100 and 60000.")
                    }
                }
                $index++
            }
        }

        if ($checks.Contains('TimeSync')) {
            $timeSync = $checks.TimeSync
            if (-not $timeSync.Contains('TimeoutMilliseconds') -or -not (Test-InfraPulseNumber -Value $timeSync.TimeoutMilliseconds -Minimum 100 -Maximum 60000)) {
                [void]$errors.Add('Checks.TimeSync.TimeoutMilliseconds must be between 100 and 60000.')
            }
            foreach ($name in @('WarningOffsetSeconds', 'CriticalOffsetSeconds')) {
                if (-not $timeSync.Contains($name) -or -not (Test-InfraPulseNumber -Value $timeSync[$name] -Minimum 0)) {
                    [void]$errors.Add("Checks.TimeSync.$name must be zero or greater.")
                }
            }
            if (
                (Test-InfraPulseNumber -Value $timeSync.WarningOffsetSeconds) -and
                (Test-InfraPulseNumber -Value $timeSync.CriticalOffsetSeconds) -and
                [double]$timeSync.CriticalOffsetSeconds -lt [double]$timeSync.WarningOffsetSeconds
            ) {
                [void]$errors.Add('Checks.TimeSync.CriticalOffsetSeconds must be greater than or equal to WarningOffsetSeconds.')
            }
            foreach ($server in @($timeSync.Servers)) {
                if ([string]::IsNullOrWhiteSpace([string]$server)) {
                    [void]$errors.Add('Checks.TimeSync.Servers cannot contain an empty value.')
                }
            }
        }
    }

    [pscustomobject]@{
        IsValid  = $errors.Count -eq 0
        Errors   = @($errors)
        Warnings = @($warnings)
    }
}