Private/Entra/Checks/Invoke-EntraAuthChecks.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 Invoke-EntraAuthChecks {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$AuditData
    )

    $checkDefs = Get-AuditCategoryDefinitions -Category 'EntraAuthChecks'
    $findings = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($check in $checkDefs.checks) {
        $funcName = "Test-Infiltration$($check.id -replace '-', '')"
        if (Get-Command $funcName -ErrorAction SilentlyContinue) {
            try {
                $finding = & $funcName -AuditData $AuditData -CheckDefinition $check
                if ($finding) { $findings.Add($finding) }
            } catch {
                $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'ERROR' `
                    -CurrentValue "Check failed: $_"))
            }
        } else {
            $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'SKIP' `
                -CurrentValue 'Check not yet implemented'))
        }
    }

    return @($findings)
}

# ── EIDAUTH-001: Authentication Methods Policy Audit ─────────────────────
function Test-InfiltrationEIDAUTH001 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $policy = $AuditData.AuthMethods.AuthMethodsPolicy
    if (-not $policy) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Authentication methods policy not available'
    }

    $configs = $AuditData.AuthMethods.MethodConfigurations
    $enabledMethods = @($configs | Where-Object { $_.state -eq 'enabled' })
    $disabledMethods = @($configs | Where-Object { $_.state -eq 'disabled' })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "$($enabledMethods.Count) methods enabled, $($disabledMethods.Count) disabled" `
        -Details @{
            EnabledCount   = $enabledMethods.Count
            DisabledCount  = $disabledMethods.Count
            EnabledMethods = @($enabledMethods | ForEach-Object {
                @{ Id = $_.id; Type = $_.'@odata.type'; State = $_.state }
            })
        }
}

# ── EIDAUTH-002: MFA Registration Status ─────────────────────────────────
function Test-InfiltrationEIDAUTH002 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $registrationDetails = $AuditData.AuthMethods.UserRegistrationDetails
    if (-not $registrationDetails -or $registrationDetails.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User registration details not available'
    }

    $total = $registrationDetails.Count
    $mfaRegistered = @($registrationDetails | Where-Object { $_.isMfaRegistered -eq $true }).Count
    $mfaCapable = @($registrationDetails | Where-Object { $_.isMfaCapable -eq $true }).Count
    $notRegistered = $total - $mfaRegistered
    $percentage = if ($total -gt 0) { [Math]::Round(($mfaRegistered / $total) * 100, 1) } else { 0 }

    $status = if ($percentage -ge 99) { 'PASS' }
              elseif ($percentage -ge 90) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "MFA registered: $mfaRegistered / $total users ($percentage%)" `
        -Details @{
            TotalUsers      = $total
            MfaRegistered   = $mfaRegistered
            MfaCapable      = $mfaCapable
            NotRegistered   = $notRegistered
            Percentage      = $percentage
        }
}

# ── EIDAUTH-003: MFA Method Distribution ─────────────────────────────────
function Test-InfiltrationEIDAUTH003 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $registrationDetails = $AuditData.AuthMethods.UserRegistrationDetails
    if (-not $registrationDetails -or $registrationDetails.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User registration details not available'
    }

    $methodCounts = @{}
    foreach ($user in $registrationDetails) {
        foreach ($method in @($user.methodsRegistered)) {
            if ($method) {
                $methodCounts[$method] = ($methodCounts[$method] ?? 0) + 1
            }
        }
    }

    $distribution = @($methodCounts.GetEnumerator() | Sort-Object -Property Value -Descending | ForEach-Object {
        @{ Method = $_.Key; Count = $_.Value; Percentage = [Math]::Round(($_.Value / $registrationDetails.Count) * 100, 1) }
    })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "MFA methods in use: $($methodCounts.Count) distinct types" `
        -Details @{
            Distribution = $distribution
            TotalUsers   = $registrationDetails.Count
        }
}

# ── EIDAUTH-004: Users with Only SMS/Voice MFA ──────────────────────────
function Test-InfiltrationEIDAUTH004 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $registrationDetails = $AuditData.AuthMethods.UserRegistrationDetails
    if (-not $registrationDetails -or $registrationDetails.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User registration details not available'
    }

    $weakMethods = @('mobilePhone', 'officePhone', 'alternateMobilePhone', 'smsSignIn')
    $strongMethods = @('microsoftAuthenticatorPush', 'softwareOneTimePasscode',
        'hardwareOneTimePasscode', 'microsoftAuthenticatorPasswordless',
        'fido2', 'windowsHelloForBusiness', 'passKeyDeviceBound', 'passKeyDeviceBoundAuthenticator')

    $weakOnlyUsers = @($registrationDetails | Where-Object {
        $_.isMfaRegistered -eq $true -and
        $_.methodsRegistered -and
        ($_.methodsRegistered | Where-Object { $_ -in $strongMethods }).Count -eq 0 -and
        ($_.methodsRegistered | Where-Object { $_ -in $weakMethods }).Count -gt 0
    })

    $status = if ($weakOnlyUsers.Count -eq 0) { 'PASS' }
              elseif ($weakOnlyUsers.Count -le 5) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($weakOnlyUsers.Count) users have only weak MFA methods (SMS/voice)" `
        -Details @{
            WeakOnlyCount = $weakOnlyUsers.Count
            Users         = @($weakOnlyUsers | Select-Object -First 50 | ForEach-Object {
                @{ UserPrincipalName = $_.userPrincipalName; Methods = @($_.methodsRegistered) }
            })
        }
}

# ── EIDAUTH-005: Users with No MFA ──────────────────────────────────────
function Test-InfiltrationEIDAUTH005 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $registrationDetails = $AuditData.AuthMethods.UserRegistrationDetails
    if (-not $registrationDetails -or $registrationDetails.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User registration details not available'
    }

    $noMfa = @($registrationDetails | Where-Object { $_.isMfaRegistered -ne $true })
    $total = $registrationDetails.Count
    $percentage = if ($total -gt 0) { [Math]::Round(($noMfa.Count / $total) * 100, 1) } else { 0 }

    $status = if ($noMfa.Count -eq 0) { 'PASS' }
              elseif ($percentage -le 5) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($noMfa.Count) users ($percentage%) have no MFA methods registered" `
        -Details @{
            NoMfaCount = $noMfa.Count
            TotalUsers = $total
            Percentage = $percentage
            Users      = @($noMfa | Select-Object -First 50 | ForEach-Object {
                @{ UserPrincipalName = $_.userPrincipalName; Id = $_.id }
            })
        }
}

# ── EIDAUTH-006: FIDO2 Security Key Inventory ───────────────────────────
function Test-InfiltrationEIDAUTH006 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $registrationDetails = $AuditData.AuthMethods.UserRegistrationDetails
    if (-not $registrationDetails) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User registration details not available'
    }

    $fido2Users = @($registrationDetails | Where-Object {
        $_.methodsRegistered -contains 'fido2'
    })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "$($fido2Users.Count) users have FIDO2 security keys registered" `
        -Details @{
            Fido2UserCount = $fido2Users.Count
            TotalUsers     = $registrationDetails.Count
        }
}

# ── EIDAUTH-007: FIDO2 ROCA Vulnerability Check ─────────────────────────
function Test-InfiltrationEIDAUTH007 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # ROCA check requires detailed FIDO2 key data which may not be in registration details
    # This is a known-vulnerable AAGUID check
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
        -CurrentValue 'ROCA vulnerability check requires detailed FIDO2 key metadata (AAGUID analysis)'
}

# ── EIDAUTH-008: Passwordless Readiness ──────────────────────────────────
function Test-InfiltrationEIDAUTH008 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $registrationDetails = $AuditData.AuthMethods.UserRegistrationDetails
    if (-not $registrationDetails -or $registrationDetails.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User registration details not available'
    }

    $passwordlessMethods = @('fido2', 'windowsHelloForBusiness', 'microsoftAuthenticatorPasswordless',
        'passKeyDeviceBound', 'passKeyDeviceBoundAuthenticator')

    $passwordlessCapable = @($registrationDetails | Where-Object {
        $_.isPasswordlessCapable -eq $true -or
        ($_.methodsRegistered | Where-Object { $_ -in $passwordlessMethods }).Count -gt 0
    })

    $total = $registrationDetails.Count
    $percentage = if ($total -gt 0) { [Math]::Round(($passwordlessCapable.Count / $total) * 100, 1) } else { 0 }

    $status = if ($percentage -ge 80) { 'PASS' }
              elseif ($percentage -ge 30) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($passwordlessCapable.Count) users ($percentage%) are passwordless-capable" `
        -Details @{
            PasswordlessCapable = $passwordlessCapable.Count
            TotalUsers          = $total
            Percentage          = $percentage
        }
}

# ── EIDAUTH-009: Windows Hello for Business ──────────────────────────────
function Test-InfiltrationEIDAUTH009 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $configs = $AuditData.AuthMethods.MethodConfigurations
    $whfb = $configs | Where-Object { $_.id -eq 'windowsHelloForBusiness' -or $_.'@odata.type' -match 'windowsHelloForBusiness' } | Select-Object -First 1

    if (-not $whfb) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'Windows Hello for Business configuration not found'
    }

    $status = if ($whfb.state -eq 'enabled') { 'PASS' } else { 'WARN' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "Windows Hello for Business: $($whfb.state)" `
        -Details @{ State = $whfb.state; Configuration = $whfb }
}

# ── EIDAUTH-010: Temporary Access Pass Policy ────────────────────────────
function Test-InfiltrationEIDAUTH010 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $configs = $AuditData.AuthMethods.MethodConfigurations
    $tap = $configs | Where-Object { $_.id -eq 'TemporaryAccessPass' -or $_.'@odata.type' -match 'temporaryAccessPass' } | Select-Object -First 1

    if (-not $tap) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue 'Temporary Access Pass not configured'
    }

    $isOneTime = $tap.isUsableOnce
    $maxLifetime = $tap.maximumLifetimeInMinutes

    $status = if ($tap.state -ne 'enabled') { 'PASS' }
              elseif ($isOneTime -and $maxLifetime -le 60) { 'PASS' }
              elseif ($isOneTime) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "TAP: $($tap.state), one-time: $isOneTime, max lifetime: $maxLifetime min" `
        -Details @{
            State           = $tap.state
            IsUsableOnce    = $isOneTime
            MaxLifetime     = $maxLifetime
            DefaultLifetime = $tap.defaultLifetimeInMinutes
            DefaultLength   = $tap.defaultLength
        }
}

# ── EIDAUTH-011: SSPR Configuration ──────────────────────────────────────
function Test-InfiltrationEIDAUTH011 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $authzPolicy = $AuditData.AuthMethods.AuthorizationPolicy
    if (-not $authzPolicy) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Authorization policy not available'
    }

    $sspr = $authzPolicy.defaultUserRolePermissions
    $allowedToReset = $authzPolicy.allowedToUseSSPR ?? $false

    $status = if ($allowedToReset) { 'PASS' } else { 'WARN' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "SSPR enabled: $allowedToReset" `
        -Details @{
            SSPREnabled = $allowedToReset
        }
}

# ── EIDAUTH-012: SSPR Methods and Requirements ──────────────────────────
function Test-InfiltrationEIDAUTH012 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # SSPR method details require additional API calls or directory settings
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
        -CurrentValue 'SSPR method configuration details require additional data collection'
}

# ── EIDAUTH-013: Password Protection Configuration ──────────────────────
function Test-InfiltrationEIDAUTH013 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $settings = $AuditData.AuthMethods.DirectorySettings
    $passwordSettings = $settings | Where-Object {
        $_.displayName -match 'Password Rule Settings' -or
        $_.templateId -eq '5cf42378-d67d-4f36-ba46-e8b86229381d'
    } | Select-Object -First 1

    if (-not $passwordSettings) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'Password protection settings not found — Azure AD Password Protection may not be configured'
    }

    $bannedPasswordEnabled = ($passwordSettings.values | Where-Object { $_.name -eq 'BannedPasswordCheckOnPremisesMode' }).value
    $enableBannedPasswordCheck = ($passwordSettings.values | Where-Object { $_.name -eq 'EnableBannedPasswordCheck' }).value

    $status = if ($enableBannedPasswordCheck -eq 'True') { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "Banned password check: $enableBannedPasswordCheck, on-premises mode: $bannedPasswordEnabled" `
        -Details @{
            EnableBannedPasswordCheck       = $enableBannedPasswordCheck
            BannedPasswordCheckOnPremisesMode = $bannedPasswordEnabled
        }
}

# ── EIDAUTH-014: Custom Banned Password List ────────────────────────────
function Test-InfiltrationEIDAUTH014 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $settings = $AuditData.AuthMethods.DirectorySettings
    $passwordSettings = $settings | Where-Object {
        $_.displayName -match 'Password Rule Settings' -or
        $_.templateId -eq '5cf42378-d67d-4f36-ba46-e8b86229381d'
    } | Select-Object -First 1

    if (-not $passwordSettings) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'Password protection settings not found'
    }

    $bannedPasswordList = ($passwordSettings.values | Where-Object { $_.name -eq 'BannedPasswordList' }).value

    $hasCustomList = $bannedPasswordList -and $bannedPasswordList.Length -gt 0
    $status = if ($hasCustomList) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "Custom banned password list: $(if ($hasCustomList) { 'configured' } else { 'not configured' })" `
        -Details @{ HasCustomList = $hasCustomList }
}

# ── EIDAUTH-015: Legacy Authentication Protocol Usage ────────────────────
function Test-InfiltrationEIDAUTH015 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Legacy auth usage detection requires sign-in logs analysis
    # Check if CA policies block legacy auth as a proxy
    $caData = $AuditData.ConditionalAccess
    if (-not $caData -or -not $caData.Policies) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'CA policy data needed to assess legacy auth blocking'
    }

    $enabledPolicies = @($caData.Policies | Where-Object { $_.state -eq 'enabled' })
    $legacyBlockPolicies = @($enabledPolicies | Where-Object {
        ($_.conditions.clientAppTypes -contains 'exchangeActiveSync' -or
         $_.conditions.clientAppTypes -contains 'other') -and
        $_.grantControls.builtInControls -contains 'block'
    })

    $status = if ($legacyBlockPolicies.Count -gt 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "Legacy auth blocking policies: $($legacyBlockPolicies.Count)" `
        -Details @{
            BlockPolicyCount = $legacyBlockPolicies.Count
            Note             = 'Full legacy auth usage analysis requires sign-in log data'
        }
}

# ── EIDAUTH-016: ROPC Flow Enabled ──────────────────────────────────────
function Test-InfiltrationEIDAUTH016 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $apps = $AuditData.Applications.AppRegistrations
    if (-not $apps) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Application registration data not available'
    }

    $ropcApps = @($apps | Where-Object {
        $_.isFallbackPublicClient -eq $true -or $_.allowPublicClient -eq $true
    })

    $status = if ($ropcApps.Count -eq 0) { 'PASS' }
              elseif ($ropcApps.Count -le 3) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($ropcApps.Count) app registrations allow public client / ROPC flow" `
        -Details @{
            RopcAppCount = $ropcApps.Count
            Apps         = @($ropcApps | Select-Object -First 20 | ForEach-Object {
                @{ AppId = $_.appId; DisplayName = $_.displayName }
            })
        }
}

# ── EIDAUTH-017: Per-User MFA vs CA Conflict ────────────────────────────
function Test-InfiltrationEIDAUTH017 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Per-user MFA status requires legacy MFA management API
    # We can detect the conflict by checking if CA MFA policies exist
    $caData = $AuditData.ConditionalAccess
    $hasCaMfa = $false
    if ($caData -and $caData.Policies) {
        $mfaPolicies = @($caData.Policies | Where-Object {
            $_.state -eq 'enabled' -and
            $_.grantControls.builtInControls -contains 'mfa'
        })
        $hasCaMfa = $mfaPolicies.Count -gt 0
    }

    if (-not $hasCaMfa) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'No CA-based MFA policies found — verify if per-user MFA is being used instead' `
            -Details @{ HasCaMfa = $false }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'CA-based MFA detected. Verify per-user MFA is disabled to avoid conflicts (requires legacy MFA admin portal)' `
        -Details @{
            HasCaMfa = $true
            Note     = 'Per-user MFA status check requires legacy MFA management portal API'
        }
}