Checks/Azure/Test-EntraNonPrivilegedUserHasMfa.ps1

function Test-EntraNonPrivilegedUserHasMfa {
    <#
    .SYNOPSIS
        Tests if all non-privileged users have MFA enabled.

    .DESCRIPTION
        This check verifies that users who are not assigned to any directory roles
        (non-privileged users) have MFA registered. MFA provides additional security
        by requiring a second form of authentication.

    .PARAMETER CheckMetadata
        Hashtable containing check metadata including id and severity.

    .EXAMPLE
        Test-EntraNonPrivilegedUserHasMfa -CheckMetadata $metadata
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory)]
        [hashtable]$CheckMetadata
    )

    $ErrorActionPreference = 'Stop'

    # Check if required data is available
    if (-not $script:EntraService.Users) {
        $findingParams = @{
            CheckMetadata  = $CheckMetadata
            Status         = 'SKIPPED'
            StatusExtended = 'Unable to retrieve users - missing permissions'
            ResourceId     = 'N/A'
            ResourceName   = 'Users'
        }
        New-CIEMFinding @findingParams
    }
    elseif (-not $script:EntraService.UserMFAStatus) {
        $findingParams = @{
            CheckMetadata  = $CheckMetadata
            Status         = 'SKIPPED'
            StatusExtended = 'Unable to retrieve user MFA registration details - missing permissions'
            ResourceId     = 'N/A'
            ResourceName   = 'User MFA Status'
        }
        New-CIEMFinding @findingParams
    }
    else {
        # Build a set of privileged user IDs (users in any directory role)
        $privilegedUserIds = @{}
        if ($script:EntraService.DirectoryRoleMembers) {
            foreach ($roleId in $script:EntraService.DirectoryRoleMembers.Keys) {
                $members = $script:EntraService.DirectoryRoleMembers[$roleId]
                if ($members) {
                    foreach ($member in $members) {
                        if ($member.id) {
                            $privilegedUserIds[$member.id] = $true
                        }
                    }
                }
            }
        }

        # Build MFA status lookup
        $mfaStatusLookup = @{}
        foreach ($mfaStatus in $script:EntraService.UserMFAStatus) {
            $mfaStatusLookup[$mfaStatus.id] = $mfaStatus
        }

        # Check non-privileged users for MFA
        $nonPrivilegedUsers = $script:EntraService.Users | Where-Object {
            -not $privilegedUserIds.ContainsKey($_.id) -and $_.accountEnabled -eq $true -and $_.userType -eq 'Member'
        }

        $usersWithoutMfa = @()
        $usersWithMfa = @()

        foreach ($user in $nonPrivilegedUsers) {
            $mfaStatus = $mfaStatusLookup[$user.id]

            # Check if user has MFA methods registered
            $hasMfa = $false
            if ($mfaStatus) {
                # Check isMfaRegistered property or methodsRegistered array
                if ($mfaStatus.isMfaRegistered -eq $true) {
                    $hasMfa = $true
                }
                elseif ($mfaStatus.methodsRegistered -and $mfaStatus.methodsRegistered.Count -gt 0) {
                    # Check for actual MFA methods (not just password)
                    $mfaMethods = $mfaStatus.methodsRegistered | Where-Object { $_ -ne 'password' }
                    if ($mfaMethods.Count -gt 0) {
                        $hasMfa = $true
                    }
                }
            }

            if ($hasMfa) {
                $usersWithMfa += $user
            }
            else {
                $usersWithoutMfa += $user
            }
        }

        $totalNonPrivileged = $nonPrivilegedUsers.Count
        $withoutMfaCount = $usersWithoutMfa.Count

        if ($withoutMfaCount -eq 0 -and $totalNonPrivileged -gt 0) {
            $findingParams = @{
                CheckMetadata  = $CheckMetadata
                Status         = 'PASS'
                StatusExtended = "All $totalNonPrivileged non-privileged users have MFA enabled"
                ResourceId     = 'non-privileged-users'
                ResourceName   = 'Non-Privileged Users'
            }
            New-CIEMFinding @findingParams
        }
        elseif ($totalNonPrivileged -eq 0) {
            $findingParams = @{
                CheckMetadata  = $CheckMetadata
                Status         = 'PASS'
                StatusExtended = 'No non-privileged users found to check'
                ResourceId     = 'non-privileged-users'
                ResourceName   = 'Non-Privileged Users'
            }
            New-CIEMFinding @findingParams
        }
        else {
            # Report individual users without MFA (limit to first 10 for readability)
            $displayLimit = 10
            $usersToDisplay = $usersWithoutMfa | Select-Object -First $displayLimit

            foreach ($user in $usersToDisplay) {
                $findingParams = @{
                    CheckMetadata  = $CheckMetadata
                    Status         = 'FAIL'
                    StatusExtended = "Non-privileged user '$($user.displayName)' ($($user.userPrincipalName)) does not have MFA enabled"
                    ResourceId     = $user.id
                    ResourceName   = $user.displayName
                }
                New-CIEMFinding @findingParams
            }

            if ($withoutMfaCount -gt $displayLimit) {
                $findingParams = @{
                    CheckMetadata  = $CheckMetadata
                    Status         = 'FAIL'
                    StatusExtended = "... and $($withoutMfaCount - $displayLimit) additional non-privileged users without MFA (total: $withoutMfaCount out of $totalNonPrivileged)"
                    ResourceId     = 'non-privileged-users-summary'
                    ResourceName   = 'Non-Privileged Users Summary'
                }
                New-CIEMFinding @findingParams
            }
        }
    }
}