Workloads/Get-UserData.ps1

# Get-UserData.ps1
# Collects users, guests, hybrid identity, global admins, MFA status,
# enterprise applications, app registrations, and groups.
# Part of the M365-QuickAssess module -- not exported.

function Get-UserData
{
    param
    (
        $Assessment
    )

    # -------------------------------------------------------------------
    # Users
    # -------------------------------------------------------------------
    try
    {
        Write-Log "Collecting user data"

        $users       = Get-MgUser -All -Property UserType,OnPremisesSyncEnabled -ErrorAction Stop
        $totalUsers  = $users.Count
        $guestUsers  = ( $users | Where-Object { $_.UserType -eq "Guest" } ).Count
        $syncedUsers = ( $users | Where-Object { $_.OnPremisesSyncEnabled -eq $true } ).Count

        $Assessment.Summary.UserCount        = $totalUsers
        $Assessment.Summary.GuestUserCount   = $guestUsers
        $Assessment.Summary.IsHybridIdentity = ( $syncedUsers -gt 0 )
        $Assessment.Summary.HasDirectorySync = ( $syncedUsers -gt 0 )

        Write-Log "Users: Total=$totalUsers Guests=$guestUsers Synced=$syncedUsers"

        # -------------------------------------------------------------------
        # Finding: High guest user count
        # -------------------------------------------------------------------
        if ( $totalUsers -gt 0 )
        {
            $guestPercent = [math]::Round( ( $guestUsers / $totalUsers ) * 100, 0 )

            if ( $guestPercent -ge 20 )
            {
                $Assessment.Findings += New-Finding `
                    -Type           "HighGuestUserCount" `
                    -Summary        "$guestUsers guest users detected ($guestPercent% of tenant)" `
                    -Category       "Identity" `
                    -Severity       "Medium" `
                    -Impact         "High guest populations require careful handling during migration -- guest accounts do not migrate with standard tooling." `
                    -Recommendation "Review guest accounts and determine which require access in the target tenant."
            }
        }
    }
    catch
    {
        Write-Log "User data collection failed: $( $_.Exception.Message )" "ERROR"
    }

    # -------------------------------------------------------------------
    # Global Admins
    # -------------------------------------------------------------------
    try
    {
        Write-Log "Collecting global admin count"

        $roles           = Get-MgDirectoryRole -All -ErrorAction Stop
        $globalAdminRole = $roles | Where-Object { $_.DisplayName -eq "Global Administrator" }

        if ( $globalAdminRole )
        {
            $globalAdmins = Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id -All
            $adminCount   = ( $globalAdmins | Measure-Object ).Count

            $Assessment.Summary.GlobalAdminCount = $adminCount

            Write-Log "Global Admins: $adminCount"

            # -------------------------------------------------------------------
            # Finding: Too many global admins
            # -------------------------------------------------------------------
            if ( $adminCount -gt 5 )
            {
                $Assessment.Findings += New-Finding `
                    -Type           "ExcessiveGlobalAdmins" `
                    -Summary        "$adminCount Global Administrators detected" `
                    -Category       "Security" `
                    -Severity       "High" `
                    -Impact         "Excessive Global Admin accounts increase the attack surface and risk of privilege abuse." `
                    -Recommendation "Reduce Global Admin count to 2-4 accounts. Use role-specific admin roles where possible."
            }
        }
        else
        {
            $Assessment.Summary.GlobalAdminCount = 0
        }
    }
    catch
    {
        Write-Log "Global admin count failed: $( $_.Exception.Message )" "WARN"
    }

    # -------------------------------------------------------------------
    # MFA Registration
    # -------------------------------------------------------------------
    try
    {
        Write-Log "Collecting MFA registration data"

        $mfa      = Get-MgReportAuthenticationMethodUserRegistrationDetail -All -ErrorAction Stop
        $mfaUsers = ( $mfa | Where-Object { $_.IsMfaRegistered -eq $true } ).Count

        if ( $totalUsers -gt 0 )
        {
            $mfaPercent = [math]::Round( ( $mfaUsers / $totalUsers ) * 100, 0 )
            $Assessment.Summary.MFAEnabledPercent = $mfaPercent

            Write-Log "MFA: $mfaUsers / $totalUsers registered ($mfaPercent%)"

            # -------------------------------------------------------------------
            # Finding: Low MFA adoption
            # -------------------------------------------------------------------
            if ( $mfaPercent -lt 50 )
            {
                $Assessment.Findings += New-Finding `
                    -Type           "LowMFAAdoption" `
                    -Summary        "Only $mfaPercent% of users have MFA registered" `
                    -Category       "Security" `
                    -Severity       "High" `
                    -Impact         "Low MFA adoption significantly increases account compromise risk during and after migration." `
                    -Recommendation "Enforce MFA registration for all users before migration begins."
            }
            elseif ( $mfaPercent -lt 80 )
            {
                $Assessment.Findings += New-Finding `
                    -Type           "ModerateMFAAdoption" `
                    -Summary        "$mfaPercent% of users have MFA registered" `
                    -Category       "Security" `
                    -Severity       "Medium" `
                    -Impact         "A significant portion of users are not MFA registered." `
                    -Recommendation "Review users without MFA and enforce registration prior to migration."
            }
        }
    }
    catch
    {
        if ( $_.Exception.Message -match 'NonPremiumTenant|PremiumLicense|Authentication_RequestFromNonPremiumTenant' )
        {
            Write-Log "MFA registration details unavailable -- tenant does not have Entra ID P1/P2" "WARN"

            $Assessment.Summary.MFAEnabledPercent = $null

            $Assessment.Findings += New-Finding `
                -Type     "MFADataUnavailable" `
                -Summary  "MFA registration data could not be collected -- Entra ID P1/P2 license required" `
                -Category "Security" `
                -Severity "Info" `
                -Impact   "MFA registration status cannot be assessed without an Entra ID P1 or P2 license." `
                -Recommendation "Manually verify MFA adoption in the source tenant before migration."
        }
        else
        {
            Write-Log "MFA registration data failed: $( $_.Exception.Message )" "WARN"
        }
    }

    # -------------------------------------------------------------------
    # Enterprise Applications
    # -------------------------------------------------------------------
    try
    {
        Write-Log "Collecting application data"

        $servicePrincipals = Get-MgServicePrincipal -All -ErrorAction Stop

        # -------------------------------------------------------------------
        # Custom apps -- owned by this tenant
        # -------------------------------------------------------------------
        $customerApps  = $servicePrincipals | Where-Object {
            $_.AccountEnabled -eq $true -and
            $_.ServicePrincipalType -eq "Application" -and
            $_.AppOwnerOrganizationId -eq $script:Context.TenantId
        }
        $customerCount = ( $customerApps | Measure-Object ).Count

        # -------------------------------------------------------------------
        # Managed Identities -- tenant-specific, good Azure signal
        # -------------------------------------------------------------------
        $managedIdentities     = $servicePrincipals | Where-Object {
            $_.ServicePrincipalType -eq "ManagedIdentity"
        }
        $managedIdentityCount  = ( $managedIdentities | Measure-Object ).Count

        # -------------------------------------------------------------------
        # App Registrations
        # -------------------------------------------------------------------
        $applications = Get-MgApplication -All -ErrorAction Stop
        $appCount     = ( $applications | Measure-Object ).Count

        # -------------------------------------------------------------------
        # Populate Schema
        # -------------------------------------------------------------------
        $Assessment.Applications.HasEnterpriseApplications = ( $customerCount -gt 0 )
        $Assessment.Applications.CustomerOwnedApps         = $customerCount
        $Assessment.Applications.ManagedIdentities         = $managedIdentityCount
        $Assessment.Applications.HasAppRegistrations       = ( $appCount -gt 0 )
        $Assessment.Applications.AppRegistrations          = $appCount

        Write-Log "Applications: CustomerOwned=$customerCount ManagedIdentities=$managedIdentityCount AppRegistrations=$appCount"

        # -------------------------------------------------------------------
        # Findings
        # -------------------------------------------------------------------
        $Assessment.Findings += New-Finding `
            -Type     "EnterpriseApplicationsSummary" `
            -Summary  "$customerCount custom applications detected in tenant" `
            -Category "Configuration" `
            -Severity "Info"

        if ( $customerCount -gt 0 )
        {
            $Assessment.Findings += New-Finding `
                -Type           "CustomerApplications" `
                -Summary        "$customerCount customer-owned applications detected" `
                -Category       "Configuration" `
                -Severity       "Medium" `
                -Details        ( $customerApps | Select-Object -ExpandProperty DisplayName ) `
                -Impact         "Custom applications will require re-registration and reconfiguration in the target tenant." `
                -Recommendation "Inventory all custom app registrations, their permissions, and any dependent services before migration."
        }

        if ( $managedIdentityCount -gt 0 )
        {
            $Assessment.Findings += New-Finding `
                -Type           "ManagedIdentitiesDetected" `
                -Summary        "$managedIdentityCount managed identities detected" `
                -Category       "Configuration" `
                -Severity       "Info" `
                -Impact         "Managed identities are tied to Azure resources and will need to be recreated in the target tenant." `
                -Recommendation "Review managed identities and associated Azure resources as part of migration planning."
        }
    }
    catch
    {
        Write-Log "Application data collection failed: $( $_.Exception.Message )" "ERROR"
    }

    # -------------------------------------------------------------------
    # Groups
    # -------------------------------------------------------------------
    try
    {
        Write-Log "Collecting group data"

        $groups                    = Get-MgGroup -All -Property GroupTypes,SecurityEnabled,MailEnabled,MembershipRule,DisplayName -ErrorAction Stop
        $totalGroups               = $groups.Count
        $m365Groups                = ( $groups | Where-Object { $_.GroupTypes -contains "Unified" } ).Count
        $securityGroups            = ( $groups | Where-Object { $_.SecurityEnabled -eq $true -and $_.MailEnabled -eq $false } ).Count
        $mailEnabledSecurityGroups = ( $groups | Where-Object { $_.SecurityEnabled -eq $true -and $_.MailEnabled -eq $true } ).Count
        $distributionLists         = ( $groups | Where-Object { $_.SecurityEnabled -eq $false -and $_.MailEnabled -eq $true } ).Count
        $dynamicGroups             = $groups | Where-Object { -not [string]::IsNullOrEmpty( $_.MembershipRule ) }
        $dynamicCount              = $dynamicGroups.Count
        $dynamicM365               = ( $dynamicGroups | Where-Object { $_.GroupTypes -contains "Unified" } ).Count
        $dynamicSecurity           = ( $dynamicGroups | Where-Object { $_.SecurityEnabled -eq $true } ).Count

        $Assessment.Groups.TotalGroups               = $totalGroups
        $Assessment.Groups.M365Groups                = $m365Groups
        $Assessment.Groups.SecurityGroups            = $securityGroups
        $Assessment.Groups.MailEnabledSecurityGroups = $mailEnabledSecurityGroups
        $Assessment.Groups.DistributionLists         = $distributionLists
        $Assessment.Groups.DynamicGroupsTotal        = $dynamicCount
        $Assessment.Groups.DynamicM365Groups         = $dynamicM365
        $Assessment.Groups.DynamicSecurityGroups     = $dynamicSecurity

        Write-Log "Groups: Total=$totalGroups M365=$m365Groups Security=$securityGroups DL=$distributionLists Dynamic=$dynamicCount"

        # -------------------------------------------------------------------
        # Finding: Dynamic Groups
        # -------------------------------------------------------------------
        if ( $dynamicCount -gt 0 )
        {
            $dynamicDetails = $dynamicGroups | ForEach-Object {
                "$( $_.DisplayName ) | Rule: $( $_.MembershipRule )"
            }

            $Assessment.Findings += New-Finding `
                -Type           "DynamicGroups" `
                -Summary        "$dynamicCount dynamic groups detected -- manual recreation required" `
                -Category       "Identity" `
                -Severity       "High" `
                -Details        $dynamicDetails `
                -Impact         "Dynamic group memberships are not directly migrated and must be recreated manually in the target tenant." `
                -Recommendation "Document all dynamic group membership rules and plan recreation in the target tenant."
        }
    }
    catch
    {
        Write-Log "Group data collection failed: $( $_.Exception.Message )" "ERROR"
    }
}