Private/Test-SingleFact.ps1

function Test-SingleFact {
    <#
    .SYNOPSIS
        Dispatches verification for a single claim based on its verification_method.
    .DESCRIPTION
        Big switch on verification_method. Each case runs the appropriate PowerShell
        command, compares expected vs actual, and returns the updated claim with
        actual_value, status, last_checked, and detail.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject]$Claim,

        [Parameter()]
        [PSCredential]$Credential,

        [Parameter()]
        [hashtable]$UnreachableServers = @{}
    )

    $result = $Claim.PSObject.Copy()
    $result.last_checked = (Get-Date).ToString('o')
    $subject = $Claim.subject
    $expected = $Claim.expected_value
    $method = $Claim.verification_method

    # Check if the target server is already known unreachable
    if ($UnreachableServers.ContainsKey($subject)) {
        $result.status = 'unreachable'
        $result.actual_value = 'Server previously marked unreachable this run'
        return $result
    }

    try {
        switch ($method) {
            'ad_computer' {
                try {
                    $computer = Get-ADComputer -Identity $subject -Properties OperatingSystem, IPv4Address, Enabled -ErrorAction Stop
                    switch ($Claim.claim_type) {
                        'server_exists' {
                            $result.actual_value = if ($computer) { 'exists' } else { 'not_found' }
                            $result.status = if ($computer) { 'verified' } else { 'drift' }
                        }
                        'server_os' {
                            $result.actual_value = $computer.OperatingSystem
                            $result.status = if ($computer.OperatingSystem -like "*$expected*") { 'verified' } else { 'drift' }
                        }
                        'server_enabled' {
                            $result.actual_value = $computer.Enabled.ToString()
                            $result.status = if ($computer.Enabled.ToString() -eq $expected) { 'verified' } else { 'drift' }
                        }
                        default {
                            $result.actual_value = $computer | Select-Object Name, OperatingSystem, IPv4Address, Enabled | ConvertTo-Json -Compress
                            $result.status = 'verified'
                        }
                    }
                }
                catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
                    $result.actual_value = 'not_found'
                    $result.status = 'drift'
                }
                catch {
                    if ($_.Exception.Message -match 'Unable to contact the server|is not recognized|not loaded') {
                        $result.actual_value = 'AD module not available'
                        $result.status = 'unverifiable'
                    }
                    else {
                        throw
                    }
                }
            }

            'ad_user' {
                try {
                    $user = Get-ADUser -Identity $subject -Properties MemberOf, Enabled, Description -ErrorAction Stop
                    switch ($Claim.claim_type) {
                        'user_exists' {
                            $result.actual_value = if ($user) { 'exists' } else { 'not_found' }
                            $result.status = if ($user) { 'verified' } else { 'drift' }
                        }
                        'user_enabled' {
                            $result.actual_value = $user.Enabled.ToString()
                            $result.status = if ($user.Enabled.ToString() -eq $expected) { 'verified' } else { 'drift' }
                        }
                        default {
                            $result.actual_value = $user.Name
                            $result.status = 'verified'
                        }
                    }
                }
                catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
                    $result.actual_value = 'not_found'
                    $result.status = 'drift'
                }
                catch {
                    if ($_.Exception.Message -match 'Unable to contact the server|is not recognized|not loaded') {
                        $result.actual_value = 'AD module not available'
                        $result.status = 'unverifiable'
                    }
                    else {
                        throw
                    }
                }
            }

            'ad_group' {
                try {
                    $group = Get-ADGroup -Identity $subject -ErrorAction Stop
                    switch ($Claim.claim_type) {
                        'group_exists' {
                            $result.actual_value = if ($group) { 'exists' } else { 'not_found' }
                            $result.status = if ($group) { 'verified' } else { 'drift' }
                        }
                        'group_members' {
                            $members = Get-ADGroupMember -Identity $subject -ErrorAction Stop | Select-Object -ExpandProperty SamAccountName
                            $result.actual_value = ($members -join ', ')
                            # Check if expected member is in the group
                            $result.status = if ($members -contains $expected) { 'verified' } else { 'drift' }
                        }
                        default {
                            $result.actual_value = $group.Name
                            $result.status = 'verified'
                        }
                    }
                }
                catch {
                    if ($_.Exception.Message -match 'Unable to contact the server|is not recognized|not loaded') {
                        $result.actual_value = 'AD module not available'
                        $result.status = 'unverifiable'
                    }
                    else {
                        $result.actual_value = "Error: $($_.Exception.Message)"
                        $result.status = 'drift'
                    }
                }
            }

            'dns_resolve' {
                try {
                    $dns = Resolve-DnsName -Name $subject -Type A -ErrorAction Stop
                    $resolvedIPs = ($dns | Where-Object { $_.QueryType -eq 'A' } | Select-Object -ExpandProperty IPAddress) -join ', '
                    $result.actual_value = $resolvedIPs
                    $result.status = if ($resolvedIPs -match [regex]::Escape($expected)) { 'verified' } else { 'drift' }
                }
                catch {
                    if ($_.Exception.Message -match 'DNS name does not exist|Non-existent domain') {
                        $result.actual_value = 'DNS record not found'
                        $result.status = 'drift'
                    }
                    else {
                        $result.actual_value = "DNS error: $($_.Exception.Message)"
                        $result.status = 'unreachable'
                    }
                }
            }

            'dns_record' {
                try {
                    $record = Get-DnsServerResourceRecord -ZoneName $subject -ErrorAction Stop
                    $result.actual_value = ($record | Select-Object -First 5 | ForEach-Object { "$($_.HostName) $($_.RecordType)" }) -join '; '
                    $result.status = if ($result.actual_value -match [regex]::Escape($expected)) { 'verified' } else { 'drift' }
                }
                catch {
                    if ($_.Exception.Message -match 'is not recognized|not loaded') {
                        $result.actual_value = 'DnsServer module not available'
                        $result.status = 'unverifiable'
                    }
                    else {
                        $result.actual_value = "Error: $($_.Exception.Message)"
                        $result.status = 'unreachable'
                    }
                }
            }

            'cim_os' {
                try {
                    $cimParams = @{ ClassName = 'Win32_OperatingSystem'; ErrorAction = 'Stop' }
                    if ($subject -ne $env:COMPUTERNAME -and $subject -ne 'localhost') {
                        $sessionParams = @{ ComputerName = $subject; ErrorAction = 'Stop' }
                        if ($Credential) { $sessionParams['Credential'] = $Credential }
                        $session = New-CimSession @sessionParams
                        $cimParams['CimSession'] = $session
                    }

                    $os = Get-CimInstance @cimParams
                    $result.actual_value = "$($os.Caption) $($os.Version)"
                    $result.status = if ($result.actual_value -like "*$expected*" -or $expected -like "*$($os.Caption)*") { 'verified' } else { 'drift' }

                    if ($session) { Remove-CimSession $session -ErrorAction SilentlyContinue }
                }
                catch {
                    if ($_.Exception.Message -match 'WinRM|access is denied|network path|RPC server') {
                        $result.actual_value = "Unreachable: $($_.Exception.Message)"
                        $result.status = 'unreachable'
                        $UnreachableServers[$subject] = $true
                    }
                    else {
                        $result.actual_value = "Error: $($_.Exception.Message)"
                        $result.status = 'unverifiable'
                    }
                }
            }

            'cim_service' {
                try {
                    $serviceName = $expected -replace '\s.*', ''  # Extract likely service name
                    $filter = "Name LIKE '%$($subject.ToLower() -replace '[^a-z0-9]', '%')%'"

                    $cimParams = @{ ClassName = 'Win32_Service'; Filter = $filter; ErrorAction = 'Stop' }
                    if ($subject -ne $env:COMPUTERNAME -and $subject -ne 'localhost') {
                        # Subject might be the server, not the service. Check claim for server context.
                        $targetServer = $subject
                        $sessionParams = @{ ComputerName = $targetServer; ErrorAction = 'Stop' }
                        if ($Credential) { $sessionParams['Credential'] = $Credential }
                        $session = New-CimSession @sessionParams
                        $cimParams['CimSession'] = $session

                        # Re-build filter for service on remote machine
                        $searchTerms = $expected -split '\s+' | Where-Object { $_.Length -gt 2 } | Select-Object -First 2
                        $filterParts = $searchTerms | ForEach-Object { "Name LIKE '%$_%' OR DisplayName LIKE '%$_%'" }
                        $cimParams['Filter'] = $filterParts -join ' OR '
                    }

                    $services = Get-CimInstance @cimParams
                    if ($services) {
                        $result.actual_value = ($services | ForEach-Object { "$($_.DisplayName) ($($_.State))" }) -join '; '
                        $result.status = 'verified'
                    }
                    else {
                        $result.actual_value = 'Service not found'
                        $result.status = 'drift'
                    }

                    if ($session) { Remove-CimSession $session -ErrorAction SilentlyContinue }
                }
                catch {
                    if ($_.Exception.Message -match 'WinRM|access is denied|network path|RPC server') {
                        $result.actual_value = "Unreachable: $($_.Exception.Message)"
                        $result.status = 'unreachable'
                        $UnreachableServers[$subject] = $true
                    }
                    else {
                        $result.actual_value = "Error: $($_.Exception.Message)"
                        $result.status = 'unverifiable'
                    }
                }
            }

            'cim_disk' {
                try {
                    $cimParams = @{ ClassName = 'Win32_LogicalDisk'; ErrorAction = 'Stop' }
                    if ($subject -ne $env:COMPUTERNAME -and $subject -ne 'localhost') {
                        $sessionParams = @{ ComputerName = $subject; ErrorAction = 'Stop' }
                        if ($Credential) { $sessionParams['Credential'] = $Credential }
                        $session = New-CimSession @sessionParams
                        $cimParams['CimSession'] = $session
                    }

                    $disks = Get-CimInstance @cimParams
                    $result.actual_value = ($disks | ForEach-Object {
                        "$($_.DeviceID) $([Math]::Round($_.Size / 1GB, 1))GB (Free: $([Math]::Round($_.FreeSpace / 1GB, 1))GB)"
                    }) -join '; '
                    $result.status = if ($result.actual_value -like "*$expected*") { 'verified' } else { 'drift' }

                    if ($session) { Remove-CimSession $session -ErrorAction SilentlyContinue }
                }
                catch {
                    if ($_.Exception.Message -match 'WinRM|access is denied|network path|RPC server') {
                        $result.actual_value = "Unreachable: $($_.Exception.Message)"
                        $result.status = 'unreachable'
                        $UnreachableServers[$subject] = $true
                    }
                    else {
                        $result.actual_value = "Error: $($_.Exception.Message)"
                        $result.status = 'unverifiable'
                    }
                }
            }

            'network_test' {
                try {
                    # Parse expected for port number
                    $port = $null
                    if ($expected -match '(\d+)') {
                        $port = [int]$Matches[1]
                    }

                    $testParams = @{ ComputerName = $subject; WarningAction = 'SilentlyContinue' }
                    if ($port) { $testParams['Port'] = $port }

                    $test = Test-NetConnection @testParams
                    if ($port) {
                        $result.actual_value = "Port ${port}: $($test.TcpTestSucceeded)"
                        $result.status = if ($test.TcpTestSucceeded) { 'verified' } else { 'drift' }
                    }
                    else {
                        $result.actual_value = "Ping: $($test.PingSucceeded), RemoteAddress: $($test.RemoteAddress)"
                        $result.status = if ($test.PingSucceeded) { 'verified' } else { 'unreachable' }
                    }
                }
                catch {
                    $result.actual_value = "Network test failed: $($_.Exception.Message)"
                    $result.status = 'unreachable'
                }
            }

            'file_share' {
                try {
                    $uncPath = if ($expected -match '^\\\\') { $expected } else { "\\$subject\$expected" }
                    $exists = Test-Path $uncPath -ErrorAction Stop
                    $result.actual_value = if ($exists) { 'accessible' } else { 'not_accessible' }
                    $result.status = if ($exists) { 'verified' } else { 'drift' }
                }
                catch {
                    $result.actual_value = "Share test failed: $($_.Exception.Message)"
                    $result.status = 'unreachable'
                }
            }

            'certificate' {
                try {
                    if ($subject -eq $env:COMPUTERNAME -or $subject -eq 'localhost') {
                        $certs = Get-ChildItem -Path 'Cert:\LocalMachine\My' -ErrorAction Stop
                    }
                    else {
                        $invokeParams = @{
                            ComputerName = $subject
                            ScriptBlock  = { Get-ChildItem -Path 'Cert:\LocalMachine\My' }
                            ErrorAction  = 'Stop'
                        }
                        if ($Credential) { $invokeParams['Credential'] = $Credential }
                        $certs = Invoke-Command @invokeParams
                    }

                    $matchingCerts = $certs | Where-Object { $_.Subject -like "*$expected*" -or $_.DnsNameList.Unicode -contains $expected }
                    if ($matchingCerts) {
                        $cert = $matchingCerts | Select-Object -First 1
                        $expired = $cert.NotAfter -lt (Get-Date)
                        $result.actual_value = "Found: $($cert.Subject), Expires: $($cert.NotAfter.ToString('yyyy-MM-dd')), Expired: $expired"
                        $result.status = if ($expired) { 'drift' } else { 'verified' }
                    }
                    else {
                        $result.actual_value = 'Certificate not found'
                        $result.status = 'drift'
                    }
                }
                catch {
                    if ($_.Exception.Message -match 'WinRM|access is denied|network path') {
                        $result.actual_value = "Unreachable: $($_.Exception.Message)"
                        $result.status = 'unreachable'
                    }
                    else {
                        $result.actual_value = "Error: $($_.Exception.Message)"
                        $result.status = 'unverifiable'
                    }
                }
            }

            'gpo' {
                try {
                    $gpo = Get-GPO -Name $expected -ErrorAction Stop
                    $result.actual_value = "$($gpo.DisplayName) (Status: $($gpo.GpoStatus), Modified: $($gpo.ModificationTime))"
                    $result.status = 'verified'
                }
                catch {
                    if ($_.Exception.Message -match 'is not recognized|not loaded') {
                        $result.actual_value = 'GroupPolicy module not available'
                        $result.status = 'unverifiable'
                    }
                    elseif ($_.Exception.Message -match 'was not found') {
                        $result.actual_value = 'GPO not found'
                        $result.status = 'drift'
                    }
                    else {
                        $result.actual_value = "Error: $($_.Exception.Message)"
                        $result.status = 'unreachable'
                    }
                }
            }

            'dhcp_scope' {
                try {
                    $scopes = Get-DhcpServerv4Scope -ErrorAction Stop
                    $matchingScope = $scopes | Where-Object { $_.ScopeId -eq $expected -or $_.Name -like "*$expected*" }
                    if ($matchingScope) {
                        $result.actual_value = "$($matchingScope.ScopeId) ($($matchingScope.Name)) - $($matchingScope.State)"
                        $result.status = 'verified'
                    }
                    else {
                        $result.actual_value = 'DHCP scope not found'
                        $result.status = 'drift'
                    }
                }
                catch {
                    if ($_.Exception.Message -match 'is not recognized|not loaded') {
                        $result.actual_value = 'DhcpServer module not available'
                        $result.status = 'unverifiable'
                    }
                    else {
                        $result.actual_value = "Error: $($_.Exception.Message)"
                        $result.status = 'unreachable'
                    }
                }
            }

            'ad_domain' {
                try {
                    $domain = Get-ADDomain -ErrorAction Stop
                    $result.actual_value = "$($domain.DNSRoot) (Forest: $($domain.Forest), Mode: $($domain.DomainMode))"
                    $result.status = if ($result.actual_value -like "*$expected*") { 'verified' } else { 'drift' }
                }
                catch {
                    if ($_.Exception.Message -match 'Unable to contact the server|is not recognized|not loaded') {
                        $result.actual_value = 'AD module not available'
                        $result.status = 'unverifiable'
                    }
                    else {
                        $result.actual_value = "Error: $($_.Exception.Message)"
                        $result.status = 'unreachable'
                    }
                }
            }

            'registry' {
                try {
                    if ($subject -eq $env:COMPUTERNAME -or $subject -eq 'localhost') {
                        $regValue = Get-ItemProperty -Path $expected -ErrorAction Stop
                        $result.actual_value = $regValue | ConvertTo-Json -Compress
                        $result.status = 'verified'
                    }
                    else {
                        $invokeParams = @{
                            ComputerName = $subject
                            ScriptBlock  = { param($path) Get-ItemProperty -Path $path -ErrorAction Stop }
                            ArgumentList = @($expected)
                            ErrorAction  = 'Stop'
                        }
                        if ($Credential) { $invokeParams['Credential'] = $Credential }
                        $regValue = Invoke-Command @invokeParams
                        $result.actual_value = $regValue | ConvertTo-Json -Compress
                        $result.status = 'verified'
                    }
                }
                catch {
                    if ($_.Exception.Message -match 'WinRM|access is denied|network path') {
                        $result.actual_value = "Unreachable: $($_.Exception.Message)"
                        $result.status = 'unreachable'
                        $UnreachableServers[$subject] = $true
                    }
                    elseif ($_.Exception.Message -match 'does not exist|Cannot find path') {
                        $result.actual_value = 'Registry key not found'
                        $result.status = 'drift'
                    }
                    else {
                        $result.actual_value = "Error: $($_.Exception.Message)"
                        $result.status = 'unverifiable'
                    }
                }
            }

            'unverifiable' {
                $result.actual_value = 'No automated verification method available'
                $result.status = 'unverifiable'
            }

            default {
                $result.actual_value = "Unknown verification method: $method"
                $result.status = 'unverifiable'
            }
        }
    }
    catch {
        # Catch-all for unexpected errors
        $result.actual_value = "Unexpected error: $($_.Exception.Message)"
        $result.status = 'unreachable'
        Write-Warning "Verification failed for $subject ($method): $($_.Exception.Message)"
    }

    return $result
}