Public/Get-TntSharedMailboxComplianceReport.ps1

function Get-TntSharedMailboxComplianceReport {
    <#
    .SYNOPSIS
        Reports on shared mailbox licensing compliance.

    .DESCRIPTION
        Retrieves all shared mailboxes and checks whether enabled accounts have an Exchange Online
        license assigned. Shared mailboxes with sign-in enabled but no Exchange Online license
        are flagged as noncompliant.

    .PARAMETER TenantId
        The Azure AD Tenant ID (GUID) to connect to.

    .PARAMETER ClientId
        The Application (Client) ID of the app registration created for security reporting.

    .PARAMETER ClientSecret
        The client secret for the app registration. Use this for automated scenarios.

    .PARAMETER CertificateThumbprint
        The thumbprint of the certificate to use for authentication instead of client secret.

    .EXAMPLE
        Get-TntSharedMailboxComplianceReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret

        Checks all shared mailboxes for licensing compliance.

    .INPUTS
        None. This function does not accept pipeline input.

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        Returns a structured object containing:
        - Summary: Total shared mailboxes, compliant/noncompliant counts
        - Mailboxes: Detailed per-mailbox compliance status

    .NOTES
        Author: Tom de Leeuw
        Website: https://systom.dev
        Module: TenantReports

        Required Permissions:
        - User.Read.All (Application)
        - Exchange Online app access

    .LINK
        https://systom.dev
    #>


    [CmdletBinding(DefaultParameterSetName = 'ClientSecret')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'Certificate')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Interactive')]
        [ValidateNotNullOrEmpty()]
        [Alias('Tenant')]
        [string]$TenantId,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = 'Certificate')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Interactive')]
        [ValidatePattern('^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$')]
        [Alias('ApplicationId')]
        [string]$ClientId,

        [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret', ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias('Secret', 'ApplicationSecret')]
        [SecureString]$ClientSecret,

        [Parameter(Mandatory = $true, ParameterSetName = 'Certificate', ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias('Thumbprint')]
        [string]$CertificateThumbprint,

        # Use interactive authentication (no app registration required).
        [Parameter(Mandatory = $true, ParameterSetName = 'Interactive')]
        [switch]$Interactive
    )

    begin {
        Write-Information 'Starting shared mailbox compliance analysis...' -InformationAction Continue
    }

    process {
        try {
            $ConnectionParams = Get-ConnectionParameters -BoundParameters $PSBoundParameters
            $ConnectionInfo = Connect-TntGraphSession @ConnectionParams

            # Connect to Exchange Online
            try {
                if ($PSCmdlet.ParameterSetName -eq 'ClientSecret') {
                    $TokenParams = @{
                        TenantId     = $TenantId
                        ClientId     = $ClientId
                        ClientSecret = $ClientSecret
                        Scope        = 'Exchange'
                    }
                    $ExchangeToken = Get-GraphToken @TokenParams
                    Connect-ExchangeOnline -Organization $TenantId -AccessToken $ExchangeToken.AccessToken -ShowBanner:$false -ErrorAction Stop
                } else {
                    Connect-ExchangeOnline -AppId $ClientId -CertificateThumbprint $CertificateThumbprint -Organization $TenantId -ShowBanner:$false -ErrorAction Stop
                }
                Write-Verbose 'Successfully connected to Exchange Online.'
            } catch {
                $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new(
                        [System.Exception]::new("Exchange Online connection required: $($_.Exception.Message)"),
                        'ExchangeConnectionError',
                        [System.Management.Automation.ErrorCategory]::ConnectionError,
                        $null
                    ))
            }

            $Mailboxes = [System.Collections.Generic.List[PSCustomObject]]::new()

            # Exchange Online service plan IDs
            $ExchangePlans = @(
                'EXCHANGE_S_ENTERPRISE'
                'EXCHANGE_S_STANDARD'
                'EXCHANGE_S_FOUNDATION'
                'EXCHANGE_S_DESKLESS'
                'EXCHANGE_S_ARCHIVE'
            )

            # Get all shared mailboxes
            Write-Verbose 'Retrieving shared mailboxes...'
            $SharedMailboxes = Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited -Properties UserPrincipalName, DisplayName, ExternalDirectoryObjectId

            Write-Verbose "Found $($SharedMailboxes.Count) shared mailboxes. Checking compliance..."

            foreach ($Mbx in $SharedMailboxes) {
                $UserId = $Mbx.ExternalDirectoryObjectId
                if (-not $UserId) {
                    $Mailboxes.Add([PSCustomObject]@{
                            DisplayName       = $Mbx.DisplayName
                            UserPrincipalName = $Mbx.UserPrincipalName
                            AccountEnabled    = 'Unknown'
                            HasExchangeLicense = $false
                            ComplianceStatus  = 'Unknown'
                            LicensePlans      = @()
                        })
                    continue
                }

                # Get user account status
                try {
                    $User = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/users/$UserId`?`$select=accountEnabled" -Method GET -ErrorAction Stop
                    $AccountEnabled = $User.accountEnabled
                } catch {
                    Write-Warning "Could not retrieve user info for $($Mbx.UserPrincipalName): $($_.Exception.Message)"
                    $AccountEnabled = $null
                }

                # Get license details
                $HasExchangeLicense = $false
                $AssignedPlans = @()
                try {
                    $LicenseDetails = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/users/$UserId/licenseDetails" -Method GET -ErrorAction Stop
                    foreach ($License in $LicenseDetails.value) {
                        $ExchangeServicePlans = $License.servicePlans | Where-Object {
                            $_.servicePlanName -in $ExchangePlans -and $_.provisioningStatus -eq 'Success'
                        }
                        if ($ExchangeServicePlans) {
                            $HasExchangeLicense = $true
                            $AssignedPlans += $ExchangeServicePlans.servicePlanName
                        }
                    }
                } catch {
                    Write-Warning "Could not retrieve license details for $($Mbx.UserPrincipalName): $($_.Exception.Message)"
                }

                # Determine compliance
                $ComplianceStatus = if ($null -eq $AccountEnabled) {
                    'Unknown'
                } elseif (-not $AccountEnabled) {
                    'Compliant'  # Disabled account - no license needed
                } elseif ($HasExchangeLicense) {
                    'Compliant'  # Enabled with license
                } else {
                    'NonCompliant'  # Enabled without Exchange license
                }

                $Mailboxes.Add([PSCustomObject]@{
                        DisplayName        = $Mbx.DisplayName
                        UserPrincipalName  = $Mbx.UserPrincipalName
                        AccountEnabled     = $AccountEnabled
                        HasExchangeLicense = $HasExchangeLicense
                        ComplianceStatus   = $ComplianceStatus
                        LicensePlans       = $AssignedPlans
                    })
            }

            # Build summary
            $Compliant = @($Mailboxes | Where-Object ComplianceStatus -EQ 'Compliant')
            $NonCompliant = @($Mailboxes | Where-Object ComplianceStatus -EQ 'NonCompliant')
            $Unknown = @($Mailboxes | Where-Object ComplianceStatus -EQ 'Unknown')

            $Summary = [PSCustomObject]@{
                TenantId              = $TenantId
                ReportGeneratedDate   = Get-Date
                TotalSharedMailboxes  = $Mailboxes.Count
                CompliantCount        = $Compliant.Count
                NonCompliantCount     = $NonCompliant.Count
                UnknownCount          = $Unknown.Count
            }

            Write-Information "Shared mailbox compliance analysis completed - $($NonCompliant.Count) noncompliant of $($Mailboxes.Count) total." -InformationAction Continue

            [PSCustomObject][Ordered]@{
                Summary   = $Summary
                Mailboxes = $Mailboxes.ToArray()
            }
        } catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                [System.Exception]::new("Get-TntSharedMailboxComplianceReport failed: $($_.Exception.Message)", $_.Exception),
                'GetTntSharedMailboxComplianceReportError',
                [System.Management.Automation.ErrorCategory]::OperationStopped,
                $TenantId
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        } finally {
            try {
                if ($ConnectionInfo.ShouldDisconnect) {
                    Disconnect-TntGraphSession -ConnectionState $ConnectionInfo
                }
            } catch {
                Write-Verbose "Could not disconnect from services: $($_.Exception.Message)"
            }
        }
    }
}