Private/Audit/Get-NotAssessedFinding.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Get-NotAssessedFinding {
    <#
    .SYNOPSIS
        Returns a SKIP ("Not Assessed") finding when the data a check depends on
        failed to collect — so absence of evidence is never scored as PASS.
    .DESCRIPTION
        A check must call this BEFORE it returns a PASS-on-empty verdict. If a
        collector recorded a failure for any of the named source keys (i.e. the
        data was never successfully gathered), an empty result is indistinguishable
        from a genuinely clean tenant — and scoring it PASS manufactures false
        confidence. In that case this returns a SKIP finding, which the posture
        scorer (Get-AuditPostureScore) excludes from the score entirely rather than
        crediting as 100.

        Returns $null when none of the named sources failed — meaning the data WAS
        collected and an empty result is a legitimate PASS the caller can proceed to.

        Collector failures are recorded in a hashtable Errors map keyed by source
        name (e.g. $AuditData.Errors['TrustRelationships'], or a nested
        $AuditData.Federation.Errors['Domains']). Pass every Errors map that could
        carry the dependency. Non-hashtable Errors (a few AD sub-collectors use an
        array of strings for fine-grained partial failures) are ignored here.
    .EXAMPLE
        $na = Get-NotAssessedFinding -CheckDefinition $CheckDefinition `
            -ErrorMap $AuditData.Errors -SourceKey 'TrustRelationships' `
            -Subject 'trust relationships'
        if ($na) { return $na }
        # ... safe to treat an empty $AuditData.Trusts as a real PASS below ...
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$CheckDefinition,

        # One or more collector Errors maps to inspect. $null entries are ignored,
        # so callers can pass an optional nested map without guarding it first.
        [AllowNull()]
        [object[]]$ErrorMap,

        # The collector source key(s) this check depends on. If ANY is present in
        # ANY supplied Errors map, the dependency could not be assessed. May be empty
        # when only prefix matching is used.
        [string[]]$SourceKey = @(),

        # Prefix(es) for collectors that record per-entity error keys (e.g. Google
        # records 'GmailSettings:user@x' / 'DnsRecords:contoso.com'). Any error key
        # that starts with a supplied prefix trips the guard.
        [string[]]$SourceKeyPrefix = @(),

        # Human-readable subject for the message, e.g. 'trust relationships'.
        [Parameter(Mandatory)]
        [string]$Subject
    )

    foreach ($map in $ErrorMap) {
        if ($map -isnot [System.Collections.IDictionary]) { continue }

        # Exact source-key match
        foreach ($key in $SourceKey) {
            if ($map.Contains($key)) {
                return New-NotAssessedSkip -CheckDefinition $CheckDefinition -Subject $Subject `
                    -FailedSource $key -Reason $map[$key]
            }
        }

        # Prefix match for per-entity error keys
        foreach ($prefix in $SourceKeyPrefix) {
            foreach ($k in $map.Keys) {
                if ("$k".StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
                    return New-NotAssessedSkip -CheckDefinition $CheckDefinition -Subject $Subject `
                        -FailedSource "$k" -Reason $map[$k]
                }
            }
        }
    }

    return $null
}

function New-NotAssessedSkip {
    # Internal: builds the standard "Not Assessed" SKIP finding for Get-NotAssessedFinding.
    [CmdletBinding()]
    param([hashtable]$CheckDefinition, [string]$Subject, [string]$FailedSource, $Reason)

    New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
        -CurrentValue "Not Assessed — could not collect $Subject ($Reason). This control was not evaluated; absence of evidence is not compliance." `
        -Details @{
            NotAssessed     = $true
            FailedSource    = $FailedSource
            CollectionError = "$Reason"
        }
}