Public/Test-EnvironmentFacts.ps1

function Test-EnvironmentFacts {
    <#
    .SYNOPSIS
        Verifies each fact in the database against the live environment.
    .DESCRIPTION
        Reads the facts database and dispatches each claim to the appropriate
        verification method (AD, DNS, CIM, network, etc.). Updates the facts.json
        file in place with actual values and verification status.
    .PARAMETER FactsPath
        Path to the facts.json database file.
    .PARAMETER ComputerName
        Additional servers to check via CIM sessions.
    .PARAMETER Credential
        PSCredential for remote server access.
    .PARAMETER SkipUnreachable
        Don't retry servers that were unreachable in the last run.
    .EXAMPLE
        Test-EnvironmentFacts -FactsPath .\facts.json
    .EXAMPLE
        Test-EnvironmentFacts -FactsPath .\facts.json -Credential (Get-Credential) -SkipUnreachable
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateScript({ Test-Path $_ })]
        [string]$FactsPath,

        [Parameter()]
        [string[]]$ComputerName,

        [Parameter()]
        [PSCredential]$Credential,

        [Parameter()]
        [switch]$SkipUnreachable
    )

    # Load facts database
    try {
        $factsDatabase = Get-Content -Path $FactsPath -Raw -Encoding UTF8 | ConvertFrom-Json
    }
    catch {
        throw "Failed to load facts database from '$FactsPath': $($_.Exception.Message)"
    }

    if (-not $factsDatabase.facts -or $factsDatabase.facts.Count -eq 0) {
        Write-Warning "No facts found in database."
        return [PSCustomObject]@{
            TotalClaims  = 0
            Verified     = 0
            Drift        = 0
            Unreachable  = 0
            Unverifiable = 0
            Pending      = 0
        }
    }

    # Build set of previously unreachable servers (if SkipUnreachable)
    $unreachableServers = @{}
    if ($SkipUnreachable) {
        foreach ($fact in $factsDatabase.facts) {
            foreach ($claim in $fact.claims) {
                if ($claim.status -eq 'unreachable') {
                    $unreachableServers[$claim.subject] = $true
                }
            }
        }
        if ($unreachableServers.Count -gt 0) {
            Write-Verbose "Skipping $($unreachableServers.Count) previously unreachable servers."
        }
    }

    # Track verification statistics
    $stats = @{
        TotalClaims  = 0
        Verified     = 0
        Drift        = 0
        Unreachable  = 0
        Unverifiable = 0
        Pending      = 0
    }

    $verifyStart = Get-Date
    Write-Verbose "Starting verification of $($factsDatabase.facts.Count) facts..."

    # Process each fact and its claims
    for ($fi = 0; $fi -lt $factsDatabase.facts.Count; $fi++) {
        $fact = $factsDatabase.facts[$fi]
        Write-Verbose "Verifying fact: $($fact.id) - $($fact.source_text)"

        $factStatuses = @()

        for ($ci = 0; $ci -lt $fact.claims.Count; $ci++) {
            $claim = $fact.claims[$ci]
            $stats.TotalClaims++

            Write-Verbose " Checking claim: $($claim.subject) / $($claim.claim_type) via $($claim.verification_method)"

            # Run the verification
            $verifiedClaim = Test-SingleFact -Claim $claim -Credential $Credential -UnreachableServers $unreachableServers

            # Update the claim in the database
            $fact.claims[$ci].actual_value = $verifiedClaim.actual_value
            $fact.claims[$ci].status = $verifiedClaim.status
            $fact.claims[$ci].last_checked = $verifiedClaim.last_checked

            $factStatuses += $verifiedClaim.status

            # Update counters
            switch ($verifiedClaim.status) {
                'verified'     { $stats.Verified++ }
                'drift'        { $stats.Drift++ }
                'unreachable'  { $stats.Unreachable++ }
                'unverifiable' { $stats.Unverifiable++ }
                'pending'      { $stats.Pending++ }
            }

            Write-Verbose " Result: $($verifiedClaim.status) (actual: $($verifiedClaim.actual_value))"
        }

        # Determine overall fact status
        $fact.last_verified = (Get-Date).ToString('o')
        if ($factStatuses -contains 'drift') {
            $fact.overall_status = 'drift'
        }
        elseif ($factStatuses -contains 'unreachable') {
            $fact.overall_status = 'unreachable'
        }
        elseif ($factStatuses -contains 'unverifiable' -and $factStatuses -notcontains 'verified') {
            $fact.overall_status = 'unverifiable'
        }
        elseif ($factStatuses -contains 'verified') {
            $fact.overall_status = 'verified'
        }
        else {
            $fact.overall_status = 'pending'
        }

        # Update the database object
        $factsDatabase.facts[$fi] = $fact
    }

    # Update metadata
    $factsDatabase.metadata.last_verified = (Get-Date).ToString('o')
    $factsDatabase.metadata.total_facts = $factsDatabase.facts.Count
    $factsDatabase.metadata.verified = $stats.Verified
    $factsDatabase.metadata.drift_detected = $stats.Drift
    $factsDatabase.metadata.unverifiable = $stats.Unverifiable

    # Save updated facts database
    try {
        $factsDatabase | ConvertTo-Json -Depth 10 | Out-File -FilePath $FactsPath -Encoding UTF8 -Force
        Write-Verbose "Updated facts database saved to $FactsPath."
    }
    catch {
        Write-Warning "Could not save updated facts database: $($_.Exception.Message)"
    }

    $elapsed = (Get-Date) - $verifyStart
    Write-Verbose "Verification complete in $([Math]::Round($elapsed.TotalSeconds, 1)) seconds."

    # Return summary
    $summary = [PSCustomObject]@{
        TotalClaims  = $stats.TotalClaims
        Verified     = $stats.Verified
        Drift        = $stats.Drift
        Unreachable  = $stats.Unreachable
        Unverifiable = $stats.Unverifiable
        Pending      = $stats.Pending
        Duration     = $elapsed
        FactsPath    = $FactsPath
    }

    return $summary
}