Private/Entra/Core/Resolve-EidscaControl.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
#
# Pure evaluator for EIDSCA (Entra ID Security Config Analyzer) controls. Given a control accessor
# (source object + dotted property path + operator + expected value, from Data/AuditChecks/EidscaChecks.json)
# and the collected Graph policy objects, returns PASS / FAIL / SKIP. SKIP == "Not Assessed": the source
# object or property wasn't collected (e.g. scope/module not connected) — never scored as PASS.
# Offline-testable; no Graph calls. Property paths mirror the live Graph objects our collectors store raw.

function Get-EidscaPropertyValue {
    [CmdletBinding()]
    param($Object, [string]$Path)
    if ($null -eq $Object -or [string]::IsNullOrWhiteSpace($Path)) { return $null }
    $cur = $Object
    foreach ($seg in ($Path -split '\.')) {
        if ($null -eq $cur) { return $null }
        $cur = $cur.$seg
    }
    return $cur
}

function Resolve-EidscaControl {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Control,
        [Parameter(Mandatory)][hashtable]$Sources
        # Sources keys: AuthorizationPolicy, AuthMethodsPolicy, MethodConfigurations[], AdminConsentRequestPolicy, DirectorySettings[]
    )

    $op = "$($Control.op)"

    # ── Resolve the actual value from the right source object ──
    $parentPresent = $true
    $actual = $null
    switch ("$($Control.source)") {
        'directorySetting' {
            # find a value by name across all collected directory settings
            $found = $false
            foreach ($s in @($Sources.DirectorySettings)) {
                $v = @($s.values) | Where-Object { "$($_.name)" -eq "$($Control.path)" } | Select-Object -First 1
                if ($v) { $actual = $v.value; $found = $true; break }
            }
            if (-not $found) { return @{ Status = 'SKIP'; Actual = $null } }
        }
        'authMethodConfig' {
            $cfg = @($Sources.MethodConfigurations) | Where-Object {
                "$($_.id)" -eq "$($Control.configId)" -or "$($_.'@odata.type')" -match "$($Control.configId)"
            } | Select-Object -First 1
            if ($null -eq $cfg) { return @{ Status = 'SKIP'; Actual = $null } }
            $actual = Get-EidscaPropertyValue -Object $cfg -Path $Control.path
            $obj = $cfg
        }
        default {
            $obj = switch ("$($Control.source)") {
                'authorizationPolicy'       { $Sources.AuthorizationPolicy }
                'authMethodsPolicy'         { $Sources.AuthMethodsPolicy }
                'adminConsentRequestPolicy' { $Sources.AdminConsentRequestPolicy }
                default { $null }
            }
            if ($null -eq $obj) { return @{ Status = 'SKIP'; Actual = $null } }
            $actual = Get-EidscaPropertyValue -Object $obj -Path $Control.path
        }
    }

    # For simple comparisons, an unresolved property == Not Assessed. For the "presence" operators
    # (notempty / fido2) the parent object WAS present, so absence is a real FAIL, not a SKIP.
    if ($null -eq $actual -and $op -notin @('notempty', 'fido2-aaguid-enforced')) {
        return @{ Status = 'SKIP'; Actual = $null }
    }

    $exp = $Control.expected
    $pass = switch ($op) {
        'eq' { "$actual".ToLower() -eq "$exp".ToLower() }
        'ne' { "$actual".ToLower() -ne "$exp".ToLower() }
        'in' { @(@($exp) | ForEach-Object { "$_".ToLower() }) -contains "$actual".ToLower() }
        'ge' { $n = $actual -as [double]; if ($null -eq $n) { $null } else { $n -ge ([double]$exp) } }
        'le' { $n = $actual -as [double]; if ($null -eq $n) { $null } else { $n -le ([double]$exp) } }
        'clike-any' { (@($actual) | Where-Object { "$_" -like "$exp*" }).Count -gt 0 }
        'notempty'  { (@($actual) | Where-Object { $_ }).Count -gt 0 }
        'fido2-aaguid-enforced' {
            $aa = Get-EidscaPropertyValue -Object $obj -Path 'keyRestrictions.aaGuids'
            $et = Get-EidscaPropertyValue -Object $obj -Path 'keyRestrictions.enforcementType'
            ((@($aa) | Where-Object { $_ }).Count -gt 0) -and ("$et" -in @('allow', 'block'))
        }
        default { $null }
    }

    if ($null -eq $pass) { return @{ Status = 'SKIP'; Actual = $actual } }  # couldn't evaluate -> Not Assessed
    return @{ Status = ($(if ($pass) { 'PASS' } else { 'FAIL' })); Actual = $actual }
}