get-DLHierarchyFromExchangeOnline.ps1

#############################################################################################
# DISCLAIMER: #
# #
# THE SAMPLE SCRIPTS ARE NOT SUPPORTED UNDER ANY MICROSOFT STANDARD SUPPORT #
# PROGRAM OR SERVICE. THE SAMPLE SCRIPTS ARE PROVIDED AS IS WITHOUT WARRANTY #
# OF ANY KIND. MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING, WITHOUT #
# LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR A PARTICULAR #
# PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLE SCRIPTS #
# AND DOCUMENTATION REMAINS WITH YOU. IN NO EVENT SHALL MICROSOFT, ITS AUTHORS, OR #
# ANYONE ELSE INVOLVED IN THE CREATION, PRODUCTION, OR DELIVERY OF THE SCRIPTS BE LIABLE #
# FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS #
# PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) #
# ARISING OUT OF THE USE OF OR INABILITY TO USE THE SAMPLE SCRIPTS OR DOCUMENTATION, #
# EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES #
#############################################################################################


Function get-DLHierarchyFromExchangeOnline
{
    <#
    .SYNOPSIS
 
    This function utilizes Microsoft Exchange Online to generate a Tree view of DL membership.
 
    .DESCRIPTION
 
    This function utilizes Microsoft Exchange Online to generate a Tree view of DL membership.
 
    .PARAMETER GROUPOBJECTID
 
    *REQUIRED*
    This is the group object ID from Entra ID.
 
    .PARAMETER LOGFOLDERPATH
 
    *REQUIRED*
    This is the logging directory for storing the migration log and all backup XML files.
    If running multiple SINGLE instance migrations use different logging directories.
 
    .PARAMETER EXCHANGEONLINEENVIRONMENTNAME
 
    *OPTIONAL*
    This specifies the Exchange Online instance to log into.
    Values include China, Global, USGov, and USGovDOD.
 
    .PARAMETER EXCHANGEONLINEORGANIZTIONNAME
 
    *OPTIONAL*
    This specifies the organization name (.onmicrosoft.com) for the Exchange ONline TEnant..
 
    .PARAMETER EXCHANGEONLINECERTIFICATETHUMBPRINT
 
    *OPTIONAL*
    The certificate thumbprint assocaited with the app registration allowing non-interactive credentials.
 
    .PARAMETER EXCHANGEONLINEAPPID
 
    *OPTIONAL*
    This value specifies the application ID associated with the app registration allowing non-interactive credentials.
 
    .PARAMETER EXCHANGEONLINECREDENTIAL
 
    *OPTIONAL*
    Allows passing of a user name and password combination instead of certificate authentication.
 
    .PARAMETER ALLOWTELEMETRYCOLLECTION
 
    *OPTIONAL*
    Specifies if telemetry collection is allowed.
 
 
    .OUTPUTS
 
    Logs all activities and backs up all original data to the log folder directory.
    Outputs a text and HTML based view of the distribution group hierarchy.
 
    .NOTES
 
    The following blog posts maintain documentation regarding this module.
 
    https://timmcmic.wordpress.com.
 
    Refer to the first pinned blog post that is the table of contents.
 
     
    .EXAMPLE
 
    get-DLHierarchyFromExchangeOnline -groupObjectID XXXXX-XXX-XXXX-XXXXXXX -logFolderPath c:\temp -exchangeCredential $cred
 
    .EXAMPLE
 
    get-DLHierarchyFromExchangeOnline -groupObjectID XXXXX-XXX-XXXX-XXXXXXX -logFolderPath c:\temp -exchangeOrganizationName sometihng.onmicrosoft.com -exchangeCertificateThumbPrint ThumbPrint -ExchangeOnlineAppID APPID
 
    #>


    [cmdletbinding()]

    Param
    (
        [Parameter(Mandatory = $true)]
        [string]$groupObjectID,
        #Exchange Online Parameters
        [Parameter(Mandatory = $false)]
        [pscredential]$exchangeOnlineCredential=$NULL,
        [Parameter(Mandatory = $false)]
        [string]$exchangeOnlineCertificateThumbPrint="",
        [Parameter(Mandatory = $false)]
        [string]$exchangeOnlineOrganizationName="",
        [Parameter(Mandatory = $false)]
        [ValidateSet("O365Default","O365GermanyCloud","O365China","O365USGovGCCHigh","O365USGovDoD")]
        [string]$exchangeOnlineEnvironmentName="O365Default",
        [Parameter(Mandatory = $false)]
        [string]$exchangeOnlineAppID="",
        #Define other mandatory parameters
        [Parameter(Mandatory = $true)]
        [string]$logFolderPath,
        [Parameter(Mandatory =$FALSE)]
        [boolean]$allowTelemetryCollection=$TRUE,
        #Define other non-mandatory parameters.
        [Parameter(Mandatory =$FALSE)]
        [boolean]$expandGroupMembership=$TRUE,
        [Parameter(Mandatory =$FALSE)]
        [boolean]$expandDynamicGroupMembership=$TRUE,
        [Parameter(Mandatory =$FALSE)]
        [boolean]$enableTextOutput=$TRUE,
        [Parameter(Mandatory =$FALSE)]
        [boolean]$enableHTMLOutput=$TRUE,
        [Parameter(Mandatory =$FALSE)]
        [boolean]$reverseHierarchy=$FALSE,
        [Parameter(Mandatory =$FALSE)]
        [boolean]$isHealthCheck=$FALSE
    )

    #Define script based variables.

    #$logFileName = (Get-Date -Format FileDateTime) #Use random file date time for the log file name.
    $logFileName = $groupObjectID

    #Define the output file.

    [string]$global:outputFile=""

    #Initialize telemetry collection.

    $appInsightAPIKey = "63d673af-33f4-401c-931e-f0b64a218d89"
    $traceModuleName = "DLHierarchy"

    #Create telemetry values.

    $telemetryDLHierarchyVersion = $NULL
    $telemetryExchangeOnlineVersion = $NULL
    $telemetryOSVersion = (Get-CimInstance Win32_OperatingSystem).version
    $telemetryStartTime = get-universalDateTime
    $telemetryEndTime = $NULL
    [double]$telemetryElapsedSeconds = 0
    $telemetryEventName = "get-DLHierarchyFromExchangeOnline"
    [boolean]$telemetryError=$FALSE

    #Specify stub object types.

    $exchangeOnlineGroupType = "Group"
    $exchangeOnlineType = "ExchangeOnline"

    [int]$defaultIndent = 0

    $global:childCounter = 0

    $global:exchangeObjects =@()
    $global:groupCounter = @()
    $global:mailUniversalSecurityGroupCounter = @()
    $global:mailUniversalDistributionGroupCounter = @()
    $global:userMailboxCounter = @()
    $global:mailUserCounter = @()
    $global:guestMailUserCounter = @()
    $global:mailContactCounter = @()
    $global:groupMailboxCounter = @()
    $global:groupMailboxDynamicCounter = @()
    $global:dynamicGroupCounter = @()
    $global:userCounter = @()
    $global:equipmentMailboxCounter = @()
    $global:sharedMailboxCounter = @()
    $global:roomMailboxCounter = @()
    $totalObjectsProcessed = 0


    #Preare items for HTML Export.

    $global:HTMLSections = @()

    #Define windows title.

    $windowTitle = ("Get-DLHierarchyFromExchangeOnline "+$groupObjectID)
    $host.ui.RawUI.WindowTitle = $windowTitle

    #Define variables utilized in the core function that are not defined by parameters.

    $coreVariables = @{ 
        exchangeOnlinePowershellModuleName = @{ "Value" = "ExchangeOnlineManagement" ; "Description" = "Static Exchange Online powershell module name" }
        DLHierarchy = @{ "Value" = "DLHierarchy" ; "Description" = "Static dlConversionv2 powershell module name" }
    }

    $processedGroupIds = New-Object System.Collections.Generic.HashSet[string]

    #Create the log file.

    if ($isHealthCheck -eq $FALSE)
    {
        new-logfile -logFileName $logFileName -logFolderPath $logFolderPath
    }

    out-logfile -string "***********************************************************"
    out-logfile -string "Starting get-DLHierarchyFromExchangeOnline"
    out-logfile -string "***********************************************************"

    if ($allowTelemetryCollection -eq $TRUE)
    {
        start-telemetryConfiguration -allowTelemetryCollection $allowTelemetryCollection -appInsightAPIKey $appInsightAPIKey -traceModuleName $traceModuleName
    }

    out-logfile -string "Testing for supported version of Powershell engine."

    test-powershellVersion

    out-logfile -string "********************************************************************************"
    out-logfile -string "NOCTICE"
    out-logfile -string "Telemetry collection is now enabled by default."
    out-logfile -string "For information regarding telemetry collection see https://timmcmic.wordpress.com/2022/11/14/4288/"
    out-logfile -string "Administrators may opt out of telemetry collection by using -allowTelemetryCollection value FALSE"
    out-logfile -string "Telemetry collection is appreciated as it allows further development and script enhacement."
    out-logfile -string "********************************************************************************"

    #Output all parameters bound or unbound and their associated values.

    Out-LogFile -string "********************************************************************************"
    Out-LogFile -string "PARAMETERS"
    Out-LogFile -string "********************************************************************************"

    write-functionParameters -keyArray $MyInvocation.MyCommand.Parameters.Keys -parameterArray $PSBoundParameters -variableArray (Get-Variable -Scope Local -ErrorAction Ignore)

    out-logfile -string "Ensure that all strings specified have no leading or trailing spaces."

    #Perform cleanup of any strings so that no spaces existin trailing or leading.

    $groupObjectID = remove-stringSpace -stringToFix $groupObjectID
    $logFolderPath = remove-stringSpace -stringToFix $logFolderPath
    $exchangeOnlineCertificateThumbPrint=remove-stringSpace -stringToFix $exchangeOnlineCertificateThumbPrint  
    $exchangeOnlineEnvironmentName=remove-stringSpace -stringToFix $exchangeOnlineEnvironmentName
    $exchangeOnlineOrganizationName=remove-stringSpace -stringToFix $exchangeOnlineOrganizationName
    $exchangeOnlineAppID=remove-stringSpace -stringToFix $exchangeOnlineAppID  
    
    out-logfile -string "Testing to ensure group ID passed is a GUID format."

    if (test-isGUID -stringGUID $groupObjectID)
    {
        out-logfile -string "Group is vaild string format."
    }
    else 
    {
        Out-logfile -string "Identifier should be an acceptable GUID format. This incldues objectGUID, externalDirectoryObjectID, ExchangeObjectID"
    }


    Out-LogFile -string "Validating Exchange Online Credentials."

    start-parameterValidation -exchangeOnlineCredential $exchangeOnlineCredential -exchangeOnlineCertificateThumbprint $exchangeOnlineCertificateThumbprint

    #Validating that all portions for exchange certificate auth are present.

    out-logfile -string "Validating parameters for Exchange Online Certificate Authentication"

    start-parametervalidation -exchangeOnlineCertificateThumbPrint $exchangeOnlineCertificateThumbprint -exchangeOnlineOrganizationName $exchangeOnlineOrganizationName -exchangeOnlineAppID $exchangeOnlineAppID

    out-logfile -string "Calling Test-PowershellModule to validate the DL Conversion Module version installed."

    $telemetryDLHierarchyVersion = Test-PowershellModule -powershellModuleName $corevariables.DLHierarchy.value -powershellVersionTest:$TRUE

    Out-LogFile -string "Calling Test-PowerShellModule to validate the Exchange Module is installed."

    $telemetryExchangeOnlineVersion = Test-PowershellModule -powershellModuleName $corevariables.exchangeOnlinePowershellModuleName.value -powershellVersionTest:$TRUE

    Out-LogFile -string "Calling New-ExchangeOnlinePowershellSession to create session to office 365."

    if ($exchangeOnlineCertificateThumbPrint -eq "")
    {
        #User specified non-certifate authentication credentials.

            try {
                New-ExchangeOnlinePowershellSession -exchangeOnlineCredentials $exchangeOnlineCredential -exchangeOnlineEnvironmentName $exchangeOnlineEnvironmentName -debugLogPath $logFolderPath
            }
            catch {
                out-logfile -string "Unable to create the exchange online connection using credentials."
                out-logfile -string $_ -isError:$TRUE
            }
    }
    elseif ($exchangeOnlineCertificateThumbPrint -ne "")
    {
        #User specified thumbprint authentication.

            try {
                new-ExchangeOnlinePowershellSession -exchangeOnlineCertificateThumbPrint $exchangeOnlineCertificateThumbPrint -exchangeOnlineAppId $exchangeOnlineAppID -exchangeOnlineOrganizationName $exchangeOnlineOrganizationName -exchangeOnlineEnvironmentName $exchangeOnlineEnvironmentName -debugLogPath $logFolderPath
            }
            catch {
                out-logfile -string "Unable to create the exchange online connection using certificate."
                out-logfile -string $_ -isError:$TRUE
            }
    }

    out-logfile -string "Start building tree from group..."

    $tree = Get-GroupWithChildren -objectID $groupObjectID -processedGroupIds $processedGroupIds -objectType $exchangeOnlineGroupType -queryMethodExchangeOnline:$TRUE -expandGroupMembership $expandGroupMembership -expandDynamicGroupMembership $expandDynamicGroupMembership -reverseHierarchy $reverseHierarchy

    if ($enableTextOutput -eq $TRUE)
    {
        out-logfile -string "Set header in output file to group name."

        $global:outputFile += "Group Hierarchy for Group ID: "+$groupObjectID+"`n"
    
        out-logfile -string "Print hierarchy to log file."
    
        print-tree -node $tree -indent $defaultIndent -outputType $exchangeOnlineType -reverseHierarchy $reverseHierarchy
    
        out-logfile -string "Export hierarchy to file."
    
        out-HierarchyFile -outputFileName  ("Hierarchy-"+$logFileName) -logFolderPath $global:logFolderPath
    }
    else 
    {
        out-logfile -string "Text based output disabled."
    }

    out-logfile -string "Generate HTML File..."

    $global:GroupCounter = $global:GroupCounter | Sort-Object -unique
    $global:mailUniversalSecurityGroupCounter = $global:mailUniversalSecurityGroupCounter | Sort-Object -unique
    $global:mailUniversalDistributionGroupCounter = $global:mailUniversalDistributionGroupCounter | Sort-Object -unique
    $global:userMailboxCounter = $global:userMailboxCounter | Sort-Object -Unique
    $global:mailUserCounter = $global:mailUserCounter | Sort-Object -Unique
    $global:guestMailUserCounter = $global:guestMailUserCounter | Sort-Object -Unique
    $global:mailContactCounter = $global:mailContactCounter | Sort-Object -Unique
    $global:groupMailboxCounter = $global:groupMailboxCounter | Sort-Object -Unique
    $global:groupMailboxDynamicCounter = $global:groupMailboxDynamicCounter | Sort-Object -Unique
    $global:dynamicGroupCounter = $global:dynamicGroupCounter | Sort-Object -Unique
    $global:userCounter = $global:userCounter | Sort-Object -Unique
    $global:equipmentMailboxCounter = $global:equipmentMailboxCounter | Sort-Object -Unique
    $global:sharedMailboxCounter = $global:sharedMailboxCounter | Sort-Object -Unique
    $global:roomMailboxCounter = $global:roomMailboxCounter | Sort-Object -Unique

    if ($enableHTMLOutput -eq $TRUE)
    {
        start-HTMLOutput -node $tree -outputType $exchangeOnlineType -groupObjectID $groupObjectID -reverseHierarchy $reverseHierarchy -isHealthCheck $isHealthCheck
    }
    else 
    {
        out-logfile -string "HTML output disabled."
    }


    out-logfile -string ("Total groups processed: "+$global:groupCounter.count)
    out-logfile -string ("Total mail security groups processed: "+$global:mailUniversalSecurityGroupCounter.count)
    out-logfile -string ("Total mail distribution groups processed: "+$global:mailUniversalDistributionGroupCounter.count)
    out-logfile -string ("Total user mailbox processed: "+$global:userMailboxCounter.count)
    out-logfile -string ("Total mail user processed: "+$global:mailUserCounter.count)
    out-logfile -string ("Total guest mail user processed: "+$global:guestMailUserCounter.count)
    out-logfile -string ("Total mail contact processed: "+$global:mailContactCounter.count)
    out-logfile -string ("Total group mailbox processed: "+$global:groupMailboxDynamicCounter.count)
    out-logfile -string ("Total group mailbox dynamic processed: ")
    out-logfile -string ("Total dynamic groups processed: "+$global:dynamicGroupCounter.count)
    out-logfile -string ("Total user processed: "+$global:userCounter.count)
    out-logfile -string ("Total equipment mailbox processed: "+$global:equipmentMailboxCounter.count)
    out-logfile -string ("Total shared mailbox processed: "+$global:sharedMailboxCounter.count)
    out-logfile -string ("Total room mailbox processed: "+$global:roomMailboxCounter.count)

    $totalObjectsProcessed = $global:groupMailboxDynamicCounter.count+$global:groupCounter.count+$global:mailUniversalSecurityGroupCounter.count+$global:mailUniversalDistributionGroupCounter.count+$global:userMailboxCounter.count+$global:mailUserCounter.count+$global:guestMailUserCounter.count+$global:mailContactCounter.count+$global:groupMailboxCounter.count+$global:dynamicGroupCounter.count+$global:userCounter.count+$global:equipmentMailboxCounter.count+$global:sharedMailboxCounter.count+$global:roomMailboxCounter.count

    out-logfile -string ("Total objects processed: "+$totalObjectsProcessed)

    $telemetryEndTime = get-universalDateTime
    $telemetryElapsedSeconds = get-elapsedTime -startTime $telemetryStartTime -endTime $telemetryEndTime

    $telemetryEventProperties = @{
        DLConversionV2Command = $telemetryEventName
        DLHierarchyVersion = $telemetryDLHierarchyVersion
        ExchangeOnline = $telemetryExchangeOnlineVersion
        OSVersion = $telemetryOSVersion
        MigrationStartTimeUTC = $telemetryStartTime
        MigrationEndTimeUTC = $telemetryEndTime
        MigrationErrors = $telemetryError
        GroupsProcessed = $global:groupCounter.count
        MailSecurityGroupsProcessed = $global:mailUniversalSecurityGroupCounter.count
        MailDistributionGroupsProcessed = $global:mailUniversalDistributionGroupCounter.count
        UserMailboxProcessed = $global:userMailboxCounter.count
        MailUsersProcessed = $global:mailUserCounter.count
        GuestMailUserProcessed = $global:guestMailUserCounter.count
        MailContactProcessed = $global:mailContactCounter.count
        GroupMailboxProcessed = $global:groupMailboxCounter.count
        GroupMailboxProcessedDynamic = $global:groupMailboxDyanmicCounter.Count
        DynamicGroupsProcessed = $global:dynamicGroupCounter.count
        UsersProcessed = $global:userCounter.count
        RoomMailboxProcessed = $global:roomMailboxCounter.Count
        EquipmentMailboxProcessed = $global:equipmentMailboxCounter.Count
        SharedMailboxProcessed = $global:sharedMailboxCounter.Count
        TotalObjectsProcessed = $totalObjectsProcessed
    }

    $telemetryEventMetrics = @{
        MigrationElapsedSeconds = $telemetryElapsedSeconds
    }

    if ($allowTelemetryCollection -eq $TRUE)
    {
        out-logfile -string "Telemetry1"
        out-logfile -string $traceModuleName
        out-logfile -string "Telemetry2"
        out-logfile -string $telemetryEventName
        out-logfile -string "Telemetry3"
        out-logfile -string $telemetryEventMetrics
        out-logfile -string "Telemetry4"
        out-logfile -string $telemetryEventProperties
        send-TelemetryEvent -traceModuleName $traceModuleName -eventName $telemetryEventName -eventMetrics $telemetryEventMetrics -eventProperties $telemetryEventProperties
    }

    disable-allPowerShellSessions
}