Public/Get-UserM365Details.ps1

function Get-UserM365Details {
    <#
    .SYNOPSIS
        Retrieves Microsoft 365 details for a user via Microsoft Graph.
    .DESCRIPTION
        Queries Microsoft Graph for license assignments, mailbox statistics,
        OneDrive usage, Teams activity, MFA status, and Conditional Access
        policy assignments. Requires an active Microsoft.Graph connection with
        appropriate scopes (User.Read.All, MailboxSettings.Read, etc.).
    .PARAMETER Identity
        User Principal Name (UPN) of the target user.
    .OUTPUTS
        PSCustomObject with M365 details.
    .EXAMPLE
        Get-UserM365Details -Identity "john.smith@contoso.com"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Identity
    )

    process {
        # Verify Graph connection
        try {
            $context = Get-MgContext -ErrorAction Stop
            if (-not $context) {
                throw "Not connected to Microsoft Graph. Run Connect-MgGraph first."
            }
        }
        catch {
            throw "Microsoft Graph is not available: $($_.Exception.Message). Run Connect-MgGraph -Scopes 'User.Read.All','Mail.Read','Directory.Read.All' first."
        }

        Write-Verbose "Querying Microsoft Graph for user: $Identity"

        # ------------------------------------------------------------------
        # Get base user object
        # ------------------------------------------------------------------
        try {
            $mgUser = Get-MgUser -UserId $Identity -Property 'Id,DisplayName,UserPrincipalName,AssignedLicenses,AccountEnabled' -ErrorAction Stop
        }
        catch {
            throw "Failed to retrieve user '$Identity' from Microsoft Graph: $($_.Exception.Message)"
        }

        $userId = $mgUser.Id

        # ------------------------------------------------------------------
        # License assignments - resolve SKU IDs to friendly names
        # ------------------------------------------------------------------
        $licenseNames = @()
        if ($mgUser.AssignedLicenses) {
            try {
                $subscribedSkus = Get-MgSubscribedSku -ErrorAction Stop
                $skuLookup = @{}
                foreach ($sku in $subscribedSkus) {
                    $skuLookup[$sku.SkuId] = $sku.SkuPartNumber
                }

                $friendlyMap = @{
                    'ENTERPRISEPREMIUM'      = 'Office 365 E5'
                    'ENTERPRISEPACK'         = 'Office 365 E3'
                    'ENTERPRISEPACKWITHOUTPROPLUS' = 'Office 365 E3 (no ProPlus)'
                    'SPE_E5'                 = 'Microsoft 365 E5'
                    'SPE_E3'                 = 'Microsoft 365 E3'
                    'SPE_F1'                 = 'Microsoft 365 F1'
                    'FLOW_FREE'              = 'Power Automate Free'
                    'POWER_BI_STANDARD'      = 'Power BI Free'
                    'POWER_BI_PRO'           = 'Power BI Pro'
                    'TEAMS_EXPLORATORY'      = 'Teams Exploratory'
                    'STREAM'                 = 'Microsoft Stream'
                    'PROJECTPREMIUM'         = 'Project Plan 5'
                    'VISIOCLIENT'            = 'Visio Plan 2'
                    'EMS_E5'                 = 'Enterprise Mobility + Security E5'
                    'EMS_E3'                 = 'Enterprise Mobility + Security E3'
                    'AAD_PREMIUM'            = 'Azure AD Premium P1'
                    'AAD_PREMIUM_P2'         = 'Azure AD Premium P2'
                    'EXCHANGESTANDARD'       = 'Exchange Online Plan 1'
                    'EXCHANGEENTERPRISE'     = 'Exchange Online Plan 2'
                    'MCOSTANDARD'            = 'Skype for Business Plan 2'
                    'PHONESYSTEM_VIRTUALUSER'= 'Phone System Virtual User'
                    'MICROSOFT_BUSINESS_CENTER' = 'Microsoft Business Center'
                    'WIN10_PRO_ENT_SUB'      = 'Windows 10/11 Enterprise E3'
                    'WINDOWS_STORE'          = 'Windows Store for Business'
                    'DEFENDER_ENDPOINT_P1'   = 'Microsoft Defender for Endpoint P1'
                }

                foreach ($lic in $mgUser.AssignedLicenses) {
                    $partNumber = $skuLookup[$lic.SkuId]
                    if ($partNumber) {
                        $friendly = $friendlyMap[$partNumber]
                        $licenseNames += if ($friendly) { $friendly } else { $partNumber }
                    }
                    else {
                        $licenseNames += $lic.SkuId
                    }
                }
            }
            catch {
                Write-Warning "Could not resolve license names: $($_.Exception.Message)"
                $licenseNames = $mgUser.AssignedLicenses | ForEach-Object { $_.SkuId }
            }
        }

        # ------------------------------------------------------------------
        # Mailbox statistics
        # ------------------------------------------------------------------
        $mailboxSize      = 'N/A'
        $mailboxItemCount = 0
        try {
            $mailFolderStats = Get-MgUserMailFolder -UserId $userId -MailFolderId 'inbox' -ErrorAction Stop
            $mailboxItemCount = $mailFolderStats.TotalItemCount

            # Use reporting API for total size
            $uri = "https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail(period='D7')"
            try {
                $reportRaw = Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop
                $reportContent = if ($reportRaw -is [string]) { $reportRaw } else { [System.Text.Encoding]::UTF8.GetString($reportRaw) }
                $reportLines = $reportContent -split "`n" | Where-Object { $_ -match $Identity }
                if ($reportLines) {
                    $fields = ($reportLines | Select-Object -First 1) -split ','
                    # Storage Used (Byte) is typically field index 5 in the CSV
                    if ($fields.Count -ge 6 -and $fields[5] -match '^\d+$') {
                        $bytes = [long]$fields[5]
                        if ($bytes -ge 1GB) { $mailboxSize = '{0:N2} GB' -f ($bytes / 1GB) }
                        elseif ($bytes -ge 1MB) { $mailboxSize = '{0:N2} MB' -f ($bytes / 1MB) }
                        else { $mailboxSize = '{0:N0} KB' -f ($bytes / 1KB) }
                    }
                }
            }
            catch {
                Write-Verbose "Mailbox usage report not available: $($_.Exception.Message)"
            }
        }
        catch {
            Write-Verbose "Could not retrieve mailbox stats: $($_.Exception.Message)"
        }

        # ------------------------------------------------------------------
        # OneDrive usage
        # ------------------------------------------------------------------
        $oneDriveUsage = 'N/A'
        try {
            $drive = Get-MgUserDrive -UserId $userId -ErrorAction Stop
            if ($drive.Quota) {
                $used = $drive.Quota.Used
                if ($used -ge 1GB) { $oneDriveUsage = '{0:N2} GB' -f ($used / 1GB) }
                elseif ($used -ge 1MB) { $oneDriveUsage = '{0:N2} MB' -f ($used / 1MB) }
                elseif ($used) { $oneDriveUsage = '{0:N0} KB' -f ($used / 1KB) }
                else { $oneDriveUsage = '0 KB' }
            }
        }
        catch {
            Write-Verbose "Could not retrieve OneDrive usage: $($_.Exception.Message)"
        }

        # ------------------------------------------------------------------
        # Teams activity (last activity date)
        # ------------------------------------------------------------------
        $teamsActivity = 'N/A'
        try {
            $teamsUri = "https://graph.microsoft.com/v1.0/reports/getTeamsUserActivityUserDetail(period='D30')"
            $teamsRaw = Invoke-MgGraphRequest -Method GET -Uri $teamsUri -ErrorAction Stop
            $teamsContent = if ($teamsRaw -is [string]) { $teamsRaw } else { [System.Text.Encoding]::UTF8.GetString($teamsRaw) }
            $teamsLines = $teamsContent -split "`n" | Where-Object { $_ -match $Identity }
            if ($teamsLines) {
                $tFields = ($teamsLines | Select-Object -First 1) -split ','
                # Last Activity Date is typically field index 3
                if ($tFields.Count -ge 4 -and $tFields[3] -match '\d{4}-\d{2}-\d{2}') {
                    $teamsActivity = $tFields[3]
                }
            }
        }
        catch {
            Write-Verbose "Could not retrieve Teams activity: $($_.Exception.Message)"
        }

        # ------------------------------------------------------------------
        # SharePoint sites
        # ------------------------------------------------------------------
        $sharePointSites = 0
        try {
            $memberOf = Get-MgUserMemberOf -UserId $userId -All -ErrorAction Stop
            $sharePointSites = ($memberOf | Where-Object { $_.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group' -and $_.AdditionalProperties['groupTypes'] -contains 'Unified' }).Count
        }
        catch {
            Write-Verbose "Could not enumerate SharePoint sites: $($_.Exception.Message)"
        }

        # ------------------------------------------------------------------
        # Shared mailbox access
        # ------------------------------------------------------------------
        $sharedMailboxAccess = @()
        try {
            $permsUri = "https://graph.microsoft.com/v1.0/users/$userId/mailboxSettings"
            $mbxSettings = Invoke-MgGraphRequest -Method GET -Uri $permsUri -ErrorAction Stop
            # Shared mailbox access requires Exchange Online cmdlets typically;
            # Graph approach: search for mailboxes where user has delegate access
            # This is best-effort via Graph - full delegate enumeration needs EXO
            Write-Verbose "Shared mailbox enumeration is limited via Graph. For full results, use Exchange Online PowerShell."
        }
        catch {
            Write-Verbose "Could not retrieve shared mailbox access: $($_.Exception.Message)"
        }

        # ------------------------------------------------------------------
        # MFA status and methods
        # ------------------------------------------------------------------
        $mfaStatus  = 'Unknown'
        $mfaMethods = @()
        try {
            $authMethods = Get-MgUserAuthenticationMethod -UserId $userId -ErrorAction Stop
            $methodTypes = @()
            foreach ($method in $authMethods) {
                $odataType = $method.AdditionalProperties['@odata.type']
                switch -Wildcard ($odataType) {
                    '*microsoftAuthenticator*' { $methodTypes += 'Microsoft Authenticator' }
                    '*phoneAuthentication*'     { $methodTypes += 'Phone (SMS/Call)' }
                    '*fido2*'                   { $methodTypes += 'FIDO2 Security Key' }
                    '*windowsHelloForBusiness*' { $methodTypes += 'Windows Hello for Business' }
                    '*emailAuthentication*'     { $methodTypes += 'Email' }
                    '*temporaryAccessPass*'     { $methodTypes += 'Temporary Access Pass' }
                    '*softwareOath*'            { $methodTypes += 'Software OATH Token' }
                    '*passwordAuthentication*'  { <# password is not MFA #> }
                    default {
                        if ($odataType) { $methodTypes += $odataType -replace '.*\.', '' }
                    }
                }
            }
            $mfaMethods = $methodTypes | Select-Object -Unique

            # If there are methods beyond just password, MFA is enabled
            $nonPasswordMethods = $authMethods | Where-Object {
                $_.AdditionalProperties['@odata.type'] -notmatch 'passwordAuthentication'
            }
            $mfaStatus = if ($nonPasswordMethods.Count -gt 0) { 'Enabled' } else { 'Disabled' }
        }
        catch {
            Write-Warning "Could not retrieve MFA details: $($_.Exception.Message)"
        }

        # ------------------------------------------------------------------
        # Conditional Access policies that apply
        # ------------------------------------------------------------------
        $caPolicies = @()
        try {
            $allPolicies = Invoke-MgGraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies' -ErrorAction Stop
            foreach ($policy in $allPolicies.value) {
                if ($policy.state -ne 'enabled') { continue }

                $applies = $false
                $conditions = $policy.conditions

                # Check if user is directly included
                if ($conditions.users.includeUsers -contains 'All' -or $conditions.users.includeUsers -contains $userId) {
                    $applies = $true
                }

                # Check if user is in an included group
                if (-not $applies -and $conditions.users.includeGroups) {
                    $userGroupIds = ($memberOf | ForEach-Object { $_.Id })
                    foreach ($groupId in $conditions.users.includeGroups) {
                        if ($userGroupIds -contains $groupId) {
                            $applies = $true
                            break
                        }
                    }
                }

                # Check exclusions
                if ($applies) {
                    if ($conditions.users.excludeUsers -contains $userId) { $applies = $false }
                    if ($conditions.users.excludeGroups) {
                        $userGroupIds = if (-not $userGroupIds) { $memberOf | ForEach-Object { $_.Id } } else { $userGroupIds }
                        foreach ($groupId in $conditions.users.excludeGroups) {
                            if ($userGroupIds -contains $groupId) {
                                $applies = $false
                                break
                            }
                        }
                    }
                }

                if ($applies) {
                    $caPolicies += $policy.displayName
                }
            }
        }
        catch {
            Write-Verbose "Could not retrieve Conditional Access policies: $($_.Exception.Message)"
        }

        [PSCustomObject]@{
            LicenseAssignments       = $licenseNames
            MailboxSize              = $mailboxSize
            MailboxItemCount         = $mailboxItemCount
            OneDriveUsage            = $oneDriveUsage
            TeamsActivity            = $teamsActivity
            SharePointSites          = $sharePointSites
            SharedMailboxAccess      = $sharedMailboxAccess
            MFAStatus                = $mfaStatus
            MFAMethods               = $mfaMethods
            ConditionalAccessPolicies = $caPolicies
        }
    }
}