Modules/M365DSCReverse.psm1

<#
.SYNOPSIS
    Orchestrates a reverse extraction of Microsoft 365 tenant configuration.
 
.DESCRIPTION
    Coordinates the end-to-end reverse DSC export flow, including authentication validation, workload/resource selection, configuration data generation, and resource export execution.
    This function is the bridge between export entry points and the ReverseDSC extraction pipeline.
 
.PARAMETER Credential
    Specifies the credential used for delegated authentication.
 
.PARAMETER Components
    Specifies resource component names to export.
 
.PARAMETER ExcludeComponents
    Specifies resource component names to exclude from export.
 
.PARAMETER AllComponents
    Indicates that all components should be considered for export.
 
.PARAMETER Path
    Specifies the destination path for generated files.
 
.PARAMETER FileName
    Specifies the output file name for the generated configuration.
 
.PARAMETER ConfigurationName
    Specifies the DSC configuration name to generate.
 
.PARAMETER Workloads
    Specifies workload names used to derive exportable components.
 
.PARAMETER Mode
    Specifies the export mode.
 
.PARAMETER GenerateInfo
    Indicates whether additional resource information should be generated.
 
.PARAMETER Filters
    Specifies resource-level filters used to limit exported instances.
 
.PARAMETER ApplicationId
    Specifies the application id used for app-based authentication.
 
.PARAMETER ApplicationSecret
    Specifies the application secret used for app-based authentication.
 
.PARAMETER TenantId
    Specifies the tenant id or tenant domain used for authentication.
 
.PARAMETER CertificateThumbprint
    Specifies the certificate thumbprint used for app-based authentication.
 
.PARAMETER CertificatePath
    Specifies the certificate file path used for app-based authentication.
 
.PARAMETER CertificatePassword
    Specifies the certificate password used to read the certificate file.
 
.PARAMETER ManagedIdentity
    Indicates that managed identity authentication should be used.
 
.PARAMETER AccessTokens
    Specifies one or more pre-acquired access tokens to use.
 
.PARAMETER SubscriptionId
    Specifies the Azure subscription id used by Azure-backed resources.
 
.PARAMETER Validate
    Indicates whether extracted configuration should be validated.
 
.PARAMETER Parallel
    Indicates whether resource extraction should run in parallel.
 
.PARAMETER ResourceSettings
    Specifies resource settings metadata used during extraction.
 
.PARAMETER WithStatistics
    Indicates whether extraction statistics should be collected and emitted.
 
.PARAMETER IncludeDependencies
    Indicates whether dependency relationships should be included in output.
 
.FUNCTIONALITY
    Internal
 
.OUTPUTS
    System.Collections.Hashtable
#>

function Start-M365DSCConfigurationExtract
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Conversion for credential creation')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'GenerateInfo', Justification = 'Using statement not detected')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Filters', Justification = 'Using statement not detected')]
    param(
        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter()]
        [System.String[]]
        $Components,

        [Parameter()]
        [System.String[]]
        $ExcludeComponents,

        [Parameter()]
        [Switch]
        $AllComponents,

        [Parameter()]
        [System.String]
        $Path,

        [Parameter()]
        [System.String]
        $FileName,

        [Parameter()]
        [System.String]
        $ConfigurationName = 'M365TenantConfig',

        [Parameter()]
        [ValidateSet('AAD', 'ADO', 'AZURE', 'COMMERCE', 'DEFENDER', 'EXO', 'FABRIC', 'INTUNE', 'O365', 'OD', 'PLANNER', 'PP', 'SC', 'SENTINEL', 'SH', 'SPO', 'TEAMS', 'VIVA')]
        [System.String[]]
        $Workloads,

        [Parameter()]
        [ValidateSet('Default', 'Full')]
        [System.String]
        $Mode = 'Default',

        [Parameter()]
        [System.Boolean]
        $GenerateInfo = $false,

        [Parameter()]
        [System.Collections.Hashtable]
        $Filters,

        [Parameter()]
        [System.String]
        $ApplicationId,

        # TODO: Change to PSCredential during next breaking change
        [Parameter()]
        [System.String]
        $ApplicationSecret,

        [Parameter()]
        [System.String]
        $TenantId,

        [Parameter()]
        [System.String]
        $CertificateThumbprint,

        [Parameter()]
        [System.String]
        $CertificatePath,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $CertificatePassword,

        [Parameter()]
        [Switch]
        $ManagedIdentity,

        [Parameter()]
        [System.String[]]
        $AccessTokens,

        [Parameter()]
        [System.String]
        $SubscriptionId,

        [Parameter()]
        [Switch]
        $Validate,

        [Parameter()]
        [Switch]
        $Parallel,

        [Parameter()]
        [System.Collections.Generic.Dictionary[System.String, System.Object]]
        $ResourceSettings,

        [Parameter()]
        [Switch]
        $WithStatistics,

        [Parameter()]
        [Switch]
        $IncludeDependencies
    )

    # Start by checking to see if a new version of the tool is available in the PowerShell Gallery
    try
    {
        Write-Verbose -Message 'Testing Module Validity'
        Test-M365DSCModuleValidity
    }
    catch
    {
        Add-M365DSCEvent -Message $_ -Source 'M365DSCReverse::Test-M365DSCModuleValidity'
    }

    try
    {
        $shouldOpenOutputDirectory = $false
        #region Prompt the user for a location to save the extract and generate the files
        if ([System.String]::IsNullOrEmpty($Path))
        {
            $shouldOpenOutputDirectory = $true
            $OutputDSCPath = Read-Host "`r`nDestination Path"
        }
        else
        {
            $OutputDSCPath = $Path
        }

        if ([System.String]::IsNullOrEmpty($OutputDSCPath))
        {
            $OutputDSCPath = '.'
        }

        while ((Test-Path -Path $OutputDSCPath -PathType Container -ErrorAction SilentlyContinue) -eq $false)
        {
            try
            {
                Write-M365DSCHost -Message "Directory `"$OutputDSCPath`" doesn't exist; creating..."
                New-Item -Path $OutputDSCPath -ItemType Directory | Out-Null
                if ($?)
                {
                    break
                }
            }
            catch
            {
                Write-Warning "$($_.Exception.Message)"
                Write-Warning "Could not create folder $OutputDSCPath!"
            }
            $OutputDSCPath = Read-Host 'Please Provide Output Folder for DSC Configuration (will be created as necessary)'
        }
        <## Ensures the path we specify ends with a Slash, in order to make sure the resulting file path is properly structured. #>
        if (-not $OutputDSCPath.EndsWith('\') -and -not $OutputDSCPath.EndsWith('/'))
        {
            $OutputDSCPath += [System.IO.Path]::DirectorySeparatorChar
        }
        Push-Location -Path $OutputDSCPath
        #endregion

        $Global:PartialExportFileName = "$(New-Guid).partial.ps1"

        # Telemetry parameters initialization
        $Global:M365DSCExportResourceTypes = @()
        $Global:M365DSCExportResourceInstancesCount = 0

        $M365DSCExportStartTime = [System.DateTime]::Now

        [System.String[]]$Script:allM365DSCResources = Get-M365DSCAllResources

        if ($null -ne $Workloads)
        {
            Write-Verbose -Message 'Retrieving the resources to export by workloads'
            $Workloads = $Workloads | Select-Object -Unique
            $Components = Get-M365DSCResourcesByWorkloads -Workloads $Workloads `
                -Mode $Mode
        }

        $ComponentsToSkip = @()
        if ($Mode -eq 'Default' -and $null -eq $Components)
        {
            $ComponentsToSkip = Get-M365DSCResourcesByExportMode -Mode 'Full' -ExcludeConfigurationResources
        }

        if ($null -ne $ExcludeComponents)
        {
            $ComponentsToSkip += $ExcludeComponents
        }

        if ($null -ne $Components)
        {
            $newComponents = @()
            foreach ($component in $Components)
            {
                if ($component.Contains('*'))
                {
                    $matchingResources = $Script:allM365DSCResources -like $component
                    if ($matchingResources.Count -eq 0)
                    {
                        Write-Warning -Message "The component filter '$component' did not match any resources and will be ignored."
                    }
                    else
                    {
                        Write-Verbose -Message "The component filter '$component' matched the following resources: $($matchingResources -join ',')"
                        $newComponents += ($matchingResources | Where-Object { $ComponentsToSkip -notcontains $_ })
                    }
                }
                else
                {
                    $newComponents += $component
                }
            }
            $Components = $newComponents | Select-Object -Unique
            $resourcesInBothIncludeAndExclude = Compare-Object -ReferenceObject $Components `
                -DifferenceObject $ComponentsToSkip -ExcludeDifferent -IncludeEqual
        }

        if ($resourcesInBothIncludeAndExclude.Count -gt 0)
        {
            foreach ($resource in $resourcesInBothIncludeAndExclude)
            {
                Write-Warning -Message "The component '$($resource.InputObject)' was specified in both -Components and -ExcludeComponents parameters. It will be excluded from the export."
            }
        }

        # Check to validate that based on the received authentication parameters
        # we are allowed to export the selected components.
        $AuthMethods = @()

        Write-M365DSCHost -Message ' '
        Write-M365DSCHost -Message 'Authentication methods specified:'
        if ($null -ne $Credential -and `
                [System.String]::IsNullOrEmpty($ApplicationId) )
        {
            Write-M365DSCHost -Message '- Credentials'
            $AuthMethods += 'Credentials'
        }
        if ($null -ne $Credential -and `
                [System.String]::IsNullOrEmpty($ApplicationId) -and `
                -not [System.String]::IsNullOrEmpty($TenantId))
        {
            Write-M365DSCHost -Message '- Credentials with Tenant Id'
            $AuthMethods += 'CredentialsWithTenantId'
        }
        if ($null -ne $Credential -and `
                -not [System.String]::IsNullOrEmpty($ApplicationId))
        {
            Write-M365DSCHost -Message '- CredentialsWithApplicationId'
            $AuthMethods += 'CredentialsWithApplicationId'
        }
        if (-not [System.String]::IsNullOrEmpty($CertificateThumbprint))
        {
            Write-M365DSCHost -Message '- Service Principal with Certificate Thumbprint'
            $AuthMethods += 'CertificateThumbprint'
        }

        if (-not [System.String]::IsNullOrEmpty($CertificatePath))
        {
            Write-M365DSCHost -Message '- Service Principal with Certificate Path'
            $AuthMethods += 'CertificatePath'
        }

        if (-not [System.String]::IsNullOrEmpty($ApplicationSecret))
        {
            Write-M365DSCHost -Message '- Service Principal with Application Secret'
            $AuthMethods += 'ApplicationWithSecret'
        }

        if ($ManagedIdentity.IsPresent)
        {
            Write-M365DSCHost -Message '- Managed Identity'
            $AuthMethods += 'ManagedIdentity'
        }

        if ($null -ne $AccessTokens)
        {
            Write-M365DSCHost -Message '- Access Tokens'
            $AuthMethods += 'AccessTokens'
        }

        Write-M365DSCHost -Message ' '

        # If some resources are not supported based on the Authentication parameters
        # received, write a warning.
        $Components = $Components | Select-Object -Unique
        if ($Components.Length -eq 0)
        {
            Write-Verbose -Message 'Retrieving all resources'
            $selectedItems = Compare-Object -ReferenceObject $Script:allM365DSCResources `
                -DifferenceObject $ComponentsToSkip | Where-Object -Property SideIndicator -EQ '<='
            $selectedResources = @()
            foreach ($item in $selectedItems)
            {
                $selectedResources += $item.InputObject
            }
        }
        else
        {
            foreach ($component in $Components)
            {
                if ($Script:allM365DSCResources -notcontains $component)
                {
                    Write-Warning -Message "The component '$component' is not a valid Microsoft365DSC resource and will be ignored."
                    $ComponentsToSkip += $component
                }
            }
            $selectedResources = $Components
        }

        Write-Verbose -Message 'Based on provided parameters, retrieving the most secure authentication method to use.'
        $allSupportedResourcesWithMostSecureAuthMethod = Get-M365DSCComponentsWithMostSecureAuthenticationType -AuthenticationMethod $AuthMethods `
            -Resources $selectedResources

        try
        {
            if ($allSupportedResourcesWithMostSecureAuthMethod.Length -eq 0)
            {
                $allSupportedResourcesWithMostSecureAuthMethod = @()
            }
            if ($selectedResources.Length -eq 0)
            {
                $selectedResources = @()
            }
            # Filter null elements in case one resource was provided and is not supported with the provided authentication method
            # to avoid Compare-Object from throwing a ParameterArgumentValidationErrorNullNotAllowed error
            # allSupportedResourcesWithMostSecureAuthMethod is a List, so -ne $null evaluates each element in the list
            $allSupportedResourcesWithMostSecureAuthMethodArray = @()
            foreach ($resource in ($allSupportedResourcesWithMostSecureAuthMethod -ne $null))
            {
                $allSupportedResourcesWithMostSecureAuthMethodArray += $resource.Resource
            }
            [Array]$compareResourcesResult = Compare-Object -ReferenceObject $allSupportedResourcesWithMostSecureAuthMethodArray `
                -DifferenceObject $selectedResources | Where-Object -Property SideIndicator -EQ '=>'
        }
        catch
        {
            Write-Verbose -Message $_
        }

        if ($null -ne $compareResourcesResult)
        {
            # The client is trying to extract at least one resource which is not supported
            # using only the provided authentication parameters;
            $resourcesNotSupported = @()
            foreach ($resource in $compareResourcesResult)
            {
                $resourcesNotSupported += $resource.InputObject

                # Skip resources that are not supported;
                $ComponentsToSkip += $resource.InputObject
            }

            $warningMessage = 'Based on the provided Authentication parameters, the following resources cannot be extracted: '
            $warningMessage += $resourcesNotSupported -join ','
            Write-Warning -Message $warningMessage

            # If all selected resources are not valid based on the authentication method used, simply return.
            if ($ComponentsToSkip.Length -eq $selectedResources.Length)
            {
                return
            }
        }

        # Get Tenant Info
        $organization = ''
        if ($AuthMethods -contains 'CertificateThumbprint' -or `
                $AuthMethods -contains 'CertificatePath' -or `
                $AuthMethods -contains 'ApplicationWithSecret')
        {
            $AppSecretAsPSCredential = $null
            # TODO: Remove during next breaking change, when ApplicationSecret is changed to PSCredential
            if (-not [System.String]::IsNullOrEmpty($ApplicationSecret))
            {
                [SecureString]$secStringPassword = ConvertTo-SecureString $ApplicationSecret -AsPlainText -Force
                [PSCredential]$AppSecretAsPSCredential = New-Object System.Management.Automation.PSCredential ('ApplicationSecret', $secStringPassword)
            }

            $organization = $TenantId
        }
        elseif ($AuthMethods -contains 'Credentials' -or `
                $AuthMethods -contains 'CredentialsWithApplicationId')
        {
            if ($null -ne $Credential -and $Credential.UserName.Contains('@'))
            {
                Write-Verbose -Message 'Retrieving organization name based on provided credentials.'
                $organization = $Credential.UserName.Split('@')[1]
            }
        }
        elseif ($AuthMethods -contains 'ManagedIdentity')
        {
            # If tenantId comes in as a GUID then query to replace with string representation, else use what was provided
            if ($TenantId -match ('^(\{){0,1}[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}(\}){0,1}$'))
            {
                $null = New-M365DSCConnection -Workload 'MicrosoftGraph' -InboundParameters @{'ManagedIdentity' = $true; 'TenantId' = $TenantId }
                $organization = Get-M365DSCTenantDomain -TenantId $TenantId -ManagedIdentity
            }
            else
            {
                $organization = $TenantId
            }
        }

        $AzureAutomation = $false

        [array] $version = Get-Module 'Microsoft365DSC'
        $version = $version[0].Version
        $DSCContent = [System.Text.StringBuilder]::new()
        $DSCContent.Append("# Generated with Microsoft365DSC version $version`r`n") | Out-Null
        $DSCContent.Append("# For additional information on how to use Microsoft365DSC, please visit https://aka.ms/M365DSC`r`n") | Out-Null
        $DSCContent.Append("param (`r`n") | Out-Null

        # Add script parameters, only add PSCredential parameters. All other information
        # is placed in the Configuration Data file.
        $newline = $false
        switch ($AuthMethods)
        {
            'CertificatePath'
            {
                if ($newline)
                {
                    $DSCContent.Append("`r`n") | Out-Null
                }
                $DSCContent.Append(" [Parameter()]`r`n") | Out-Null
                $DSCContent.Append(" [System.Management.Automation.PSCredential]`r`n") | Out-Null
                $DSCContent.Append(" `$CertificatePassword`r`n") | Out-Null
                $newline = $true
            }
            { $_ -in 'Credentials', 'CredentialsWithApplicationId' }
            {
                if ($newline)
                {
                    $DSCContent.Append("`r`n") | Out-Null
                }
                $DSCContent.Append(" [Parameter()]`r`n") | Out-Null
                $DSCContent.Append(" [System.Management.Automation.PSCredential]`r`n") | Out-Null
                $DSCContent.Append(" `$Credential`r`n") | Out-Null
                $newline = $true
            }
        }

        $DSCContent.Append(")`r`n`r`n") | Out-Null

        # Create Configuration section
        if (-not [System.String]::IsNullOrEmpty($FileName))
        {
            $FileParts = $FileName.Split('.')

            if ([System.String]::IsNullOrEmpty($ConfigurationName))
            {
                $ConfigurationName = $FileName.Replace('.' + $FileParts[$FileParts.Length - 1], '').Replace(' ', '_')
            }
        }
        if ([System.String]::IsNullOrEmpty($ConfigurationName))
        {
            $ConfigurationName = 'M365TenantConfig'
        }
        $DSCContent.Append("Configuration $ConfigurationName`r`n{`r`n") | Out-Null

        # Adding Parameter section
        $DSCContent.Append(" param (`r`n") | Out-Null

        $newline = $false
        $postParamContent = [System.Text.StringBuilder]::new()
        switch ($AuthMethods)
        {
            { $_ -in 'CertificateThumbprint', 'CertificatePath', 'ApplicationWithSecret' }
            {
                $postParamContent.Append(" `$OrganizationName = `$ConfigurationData.NonNodeData.OrganizationName`r`n") | Out-Null

                Add-ConfigurationDataEntry -Node 'NonNodeData' `
                    -Key 'OrganizationName' `
                    -Value $organization `
                    -Description "Tenant's default verified domain name"
                Add-ConfigurationDataEntry -Node 'NonNodeData' `
                    -Key 'ApplicationId' `
                    -Value $ApplicationId `
                    -Description 'Azure AD Application Id for Authentication'
                Add-ConfigurationDataEntry -Node 'NonNodeData' `
                    -Key 'TenantId' `
                    -Value $TenantId `
                    -Description 'The Id or Name of the tenant to authenticate against'
            }
            'CertificateThumbprint'
            {
                Add-ConfigurationDataEntry -Node 'NonNodeData' `
                    -Key 'CertificateThumbprint' `
                    -Value $CertificateThumbprint `
                    -Description 'Thumbprint of the certificate to use for authentication'
            }
            'CertificatePath'
            {
                if ($newline)
                {
                    $DSCContent.Append("`r`n") | Out-Null
                }
                $DSCContent.Append(" [Parameter()]`r`n") | Out-Null
                $DSCContent.Append(" [System.Management.Automation.PSCredential]`r`n") | Out-Null
                $DSCContent.Append(" `$CertificatePassword`r`n") | Out-Null

                if ($newline)
                {
                    $postParamContent.Append("`r`n") | Out-Null
                }
                $postParamContent.Append(" if (`$null -eq `$CertificatePassword)`r`n") | Out-Null
                $postParamContent.Append(" {`r`n") | Out-Null
                $postParamContent.Append(" <# Credentials #>`r`n") | Out-Null
                $postParamContent.Append(" }`r`n") | Out-Null
                $postParamContent.Append(" else`r`n") | Out-Null
                $postParamContent.Append(" {`r`n") | Out-Null
                $postParamContent.Append(" `$CredsCertificatePassword = `$CertificatePassword`r`n") | Out-Null
                $postParamContent.Append(" }`r`n`r`n") | Out-Null

                Add-ConfigurationDataEntry -Node 'NonNodeData' `
                    -Key 'CertificatePath' `
                    -Value $CertificatePath `
                    -Description 'Local path to the .pfx certificate to use for authentication'

                $newline = $true
            }
            'ApplicationWithSecret'
            {
                Add-ConfigurationDataEntry -Node 'NonNodeData' `
                    -Key 'ApplicationSecret' `
                    -Value $ApplicationSecret `
                    -Description 'Azure AD Application Secret for Authentication'
            }
            'AccessTokens'
            {
                Add-ConfigurationDataEntry -Node 'NonNodeData' `
                    -Key 'AccessTokens' `
                    -Value $AccessTokens `
                    -Description 'Access tokens to use for authentication'

                Add-ConfigurationDataEntry -Node 'NonNodeData' `
                    -Key 'TenantId' `
                    -Value $TenantId `
                    -Description 'The Id or Name of the tenant to authenticate against'

                Set-M365DSCStringReplacementMap -Map @{ $TenantId = 'TenantId' }
            }
            { $_ -in 'Credentials', 'CredentialsWithApplicationId', 'CredentialsWithTenantId' }
            {
                if ($newline)
                {
                    $DSCContent.Append("`r`n") | Out-Null
                }
                $DSCContent.Append(" [Parameter()]`r`n") | Out-Null
                $DSCContent.Append(" [System.Management.Automation.PSCredential]`r`n") | Out-Null
                $DSCContent.Append(" `$Credential`r`n") | Out-Null

                if ($newline)
                {
                    $postParamContent.Append("`r`n") | Out-Null
                }
                $postParamContent.Append(" if (`$null -eq `$Credential)`r`n") | Out-Null
                $postParamContent.Append(" {`r`n") | Out-Null
                $postParamContent.Append(" <# Credentials #>`r`n") | Out-Null
                $postParamContent.Append(" }`r`n") | Out-Null
                $postParamContent.Append(" else`r`n") | Out-Null
                $postParamContent.Append(" {`r`n") | Out-Null
                $postParamContent.Append(" `$CredsCredential = `$Credential`r`n") | Out-Null
                $postParamContent.Append(" }`r`n`r`n") | Out-Null
                $postParamContent.Append(" `$OrganizationName = `$CredsCredential.UserName.Split('@')[1]`r`n") | Out-Null

                $newline = $true
            }
            'ManagedIdentity'
            {
                $postParamContent.Append(" `$OrganizationName = `$ConfigurationData.NonNodeData.OrganizationName`r`n") | Out-Null

                Add-ConfigurationDataEntry -Node 'NonNodeData' `
                    -Key 'OrganizationName' `
                    -Value $organization `
                    -Description "Tenant's default verified domain name"

                Add-ConfigurationDataEntry -Node 'NonNodeData' `
                    -Key 'TenantId' `
                    -Value $TenantId `
                    -Description 'The Id or Name of the tenant to authenticate against'

                Set-M365DSCStringReplacementMap -Map @{ $TenantId = 'TenantId' }
            }
        }

        $DSCContent.Append(" )`r`n`r`n") | Out-Null
        $DSCContent.Append($postParamContent.ToString()) | Out-Null
        $DSCContent.Append("`r`n") | Out-Null

        # Create Node section
        $DSCContent.Append(" Import-DscResource -ModuleName 'Microsoft365DSC' -ModuleVersion '$version'`r`n`r`n") | Out-Null
        $DSCContent.Append(" Node localhost`r`n") | Out-Null
        $DSCContent.Append(" {`r`n") | Out-Null

        Write-Verbose -Message 'Adding initial entry in the ConfigurationData file.'
        Add-ConfigurationDataEntry -Node 'localhost' `
            -Key 'ServerNumber' `
            -Value '0' `
            -Description 'Default Value Used to Ensure a Configuration Data File is Generated'

        $resourcesToExport = [System.Collections.Generic.List[Hashtable[]]]::new()
        $resourcesPath = [System.Collections.Generic.List[System.IO.FileInfo[]]]::new()
        $allResourcesPath = Get-M365DSCAllResourcesPath

        $containsConfigurationPolicies = $false
        $requestedConfigurationPolicyTemplateIds = @()
        # TODO: Update to -AsHashtable once PowerShell 7 is enforced
        $intuneTemplateRegistryObject = Get-Content -Path (Join-Path -Path $PSScriptRoot -ChildPath '../IntuneTemplateRegistry.json') -Raw | ConvertFrom-Json
        $intuneTemplateRegistry = @{}
        foreach ($property in $intuneTemplateRegistryObject.psobject.properties)
        {
            $intuneTemplateRegistry.Add($property.Name, $property.Value)
        }

        foreach ($resourceModule in $Script:allM365DSCResources)
        {
            try
            {
                if ((($Components -and ($Components -contains $resourceModule)) -or $AllComponents -or `
                        (-not $Components -and $null -eq $Workloads)) -and `
                    $ComponentsToSkip -notcontains $resourceModule -and `
                        $resourcesNotSupported -notcontains $resourceModule -and `
                        -not $resourceModule.StartsWith('M365DSC'))
                {
                    $authMethod = $allSupportedResourcesWithMostSecureAuthMethod | Where-Object -Property Resource -EQ $resourceModule
                    $resourceInfo = @{
                        Name                 = $resourceModule
                        AuthenticationMethod = $authMethod.AuthMethod
                    }
                    $resourcesToExport.Add($resourceInfo)
                    $resourcePath = $allResourcesPath | Where-Object -Property Name -EQ "MSFT_$resourceModule.psm1"
                    $resourcesPath.Add($resourcePath)

                    if ($intuneTemplateRegistry.ContainsKey($resourceModule))
                    {
                        $requestedConfigurationPolicyTemplateIds += $intuneTemplateRegistry.$resourceModule
                        $containsConfigurationPolicies = $true
                    }
                }
            }
            catch
            {
                New-M365DSCLogEntry -Message $resourceModule `
                    -Exception $_ `
                    -Source "[M365DSCReverse]$resourceModule"
            }
        }
        $resourcesPath = $resourcesPath | Sort-Object $_.Name

        # If the tenant id is not a GUID, retrieve it based on the organization name
        # Only implemented for public cloud tenants
        if (-not ($TenantId -match ('^(\{){0,1}[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}(\}){0,1}$')))
        {
            try
            {
                Write-Verbose -Message 'Retrieving Tenant Id based on provided organization name.'
                $tenantGuid = (Invoke-RestMethod -Uri "https://login.microsoftonline.com/$organization/.well-known/openid-configuration" -Method Get).authorization_endpoint.Split('/')[3]
                $currentStringReplacementMap = Get-M365DSCStringReplacementMap
                if (-not $currentStringReplacementMap.ContainsKey($tenantGuid))
                {
                    $currentStringReplacementMap.Add($tenantGuid, 'TenantGuid')
                    Set-M365DSCStringReplacementMap -Map $currentStringReplacementMap
                }
            }
            catch
            {
                Write-Warning -Message "Failed to resolve current tenant id from organization name '$organization'. Not replacing tenant id in exported configuration.
         If you want to have your tenant id replaced in the export, use the -TokenReplacement parameter of Export-M365DSCConfiguration."

            }
        }

        Confirm-M365DSCDependencies
        $partialExportName = $Global:PartialExportFileName
        $synchronizedHashtable = [System.Collections.Concurrent.ConcurrentDictionary[System.String, System.Object]]::new()
        [void]$synchronizedHashtable.TryAdd('ResourceCounter', 1)
        [void]$synchronizedHashtable.TryAdd('ResourcesResult', [System.Collections.Concurrent.ConcurrentDictionary[System.String, System.String]]::new())
        [void]$synchronizedHashtable.TryAdd('SuccessfulResources', 0)
        [void]$synchronizedHashtable.TryAdd('FailedResources', 0)
        $resourceDictionary = Get-M365DSCAllResourcesDictionary
        $M365DSCStringReplacementMap = Get-M365DSCStringReplacementMap
        $exportScriptBlock = {
            $Global:MaximumFunctionCount = 32768
            $Global:PartialExportFileName = $using:partialExportName
            $Global:M365DSCSkipDependenciesValidation = $true
            $Global:M365DSCStringReplacementMap = $using:M365DSCStringReplacementMap
            $resource = $_
            Set-M365DSCAllResourcesDictionary -DscResourceDictionary $using:resourceDictionary
            $resourceName = $resource.Name.Split('.')[0] -replace 'MSFT_', ''
            $mostSecureAuthMethod = ($using:allSupportedResourcesWithMostSecureAuthMethod | Where-Object -Property Resource -EQ $resourceName).AuthMethod

            $parameters = @{}
            switch ($mostSecureAuthMethod)
            {
                { $_ -in 'CertificateThumbprint', 'CertificatePath', 'ApplicationSecret' }
                {
                    $parameters.Add('ApplicationId', $using:ApplicationId)
                    $parameters.Add('TenantId', $using:TenantId)
                }
                'CertificateThumbprint'
                {
                    $parameters.Add('CertificateThumbprint', $using:CertificateThumbprint)
                }
                'CertificatePath'
                {
                    $parameters.Add('CertificatePath', $using:CertificatePath)
                    $parameters.Add('CertificatePassword', $using:CertificatePassword)
                }
                'ApplicationSecret'
                {
                    # TODO: Update during next breaking change, when ApplicationSecret is changed to PSCredential
                    $applicationSecretValue = New-Object System.Management.Automation.PSCredential ('ApplicationSecret', (ConvertTo-SecureString $using:ApplicationSecret -AsPlainText -Force))
                    $parameters.Add('ApplicationSecret', $applicationSecretValue)
                }
                { $_ -in 'Credentials', 'CredentialsWithApplicationId' }
                {
                    if ($using:AuthMethods -contains 'CredentialsWithApplicationId')
                    {
                        $parameters.Add('ApplicationId', $using:ApplicationId)
                    }
                    $parameters.Add('Credential', $using:Credential)
                }
                'CredentialsWithTenantId'
                {
                    $parameters.Add('Credential', $using:Credential)
                    $parameters.Add('TenantId', $using:TenantId)
                }
                'ManagedIdentity'
                {
                    $parameters.Add('ManagedIdentity', $using:ManagedIdentity)
                    $parameters.Add('TenantId', $using:TenantId)
                }
                'AccessTokens'
                {
                    $parameters.Add('AccessTokens', $using:AccessTokens)
                    $parameters.Add('TenantId', $using:TenantId)
                }
            }

            if ($using:ComponentsToSkip -notcontains $resourceName)
            {
                $counter = ($using:synchronizedHashtable).ResourceCounter++
                Write-M365DSCHost -Message "[$counter/$($using:resourcesToExport.Count)] Extracting [" -DeferWrite
                Write-M365DSCHost -Message $resourceName -ForegroundColor Green -DeferWrite
                Write-M365DSCHost -Message '] using {' -DeferWrite
                Write-M365DSCHost -Message $mostSecureAuthMethod -ForegroundColor Cyan -DeferWrite
                Write-M365DSCHost -Message '}...' -DeferWrite
                $exportString = [System.Text.StringBuilder]::new()
                if ($using:GenerateInfo)
                {
                    $exportString.Append("`r`n # For information on how to use this resource, please refer to:`r`n") | Out-Null
                    $exportString.Append(" # https://github.com/microsoft/Microsoft365DSC/wiki/$($resource.Name.Split('.')[0] -replace 'MSFT_', '')`r`n") | Out-Null
                }

                # Check if filters for the current resource were specified.
                $resourceFilter = $null

                Import-Module $resource.FullName -Force | Out-Null
                $filterExists = (Get-Command 'Export-TargetResource').Parameters.Keys.Contains('Filter')
                if ($filterExists -and $null -ne $using:Filters -and ($using:Filters).Keys.Contains($resourceName))
                {
                    $resourceFilter = ($using:Filters).$resourceName
                    if ($filterExists)
                    {
                        $parameters.Add('Filter', $resourceFilter)
                    }
                    elseif ($null -ne $resourceFilter)
                    {
                        Write-M365DSCHost -Message " `r`n$($Global:M365DSCEmojiYellowCircle) You specified a filter for resource {$resourceName} but it doesn't support filters. Filter will be ignored and all instances of the resource will be captured." -ForegroundColor DarkYellow -CommitWrite
                    }
                }

                # Check for Export-TargetResource parameters supports -SubscriptionId
                $functionParameters = (Get-Command 'Export-TargetResource').Parameters
                if ($functionParameters.Keys.Contains('SubscriptionId') -and -not [System.String]::IsNullOrEmpty($using:SubscriptionId))
                {
                    $parameters.Add('SubscriptionId', $using:SubscriptionId)
                }

                # Check for ErrorAction Preference
                $parameters.Add('ErrorAction', $using:ErrorActionPreference)
                $Global:M365DSCExportResourceTypes += $resourceName

                try
                {
                    $exportOutput = Export-TargetResource @parameters
                    $exportString.Append($exportOutput) | Out-Null
                    [void]($using:synchronizedHashtable).ResourcesResult.TryAdd($resourceName, $exportString.ToString())
                    ($using:synchronizedHashtable).SuccessfulResources++
                }
                catch
                {
                    Write-M365DSCHost -Message "$($Global:M365DSCEmojiRedX)`r`n An error occurred while exporting resource {$resourceName}: $($_.Exception.Message)" -ForegroundColor Red -CommitWrite
                    ($using:synchronizedHashtable).FailedResources++
                    if ($ErrorActionPreference -eq 'Stop')
                    {
                        throw $_
                    }
                }
            }
        }

        if ($containsConfigurationPolicies)
        {
            $null = New-M365DSCConnection -Workload MicrosoftGraph -InboundParameters @{
                Credential = $Credential
                ApplicationId = $ApplicationId
                ApplicationSecret = $ApplicationSecret
                CertificateThumbprint = $CertificateThumbprint
                CertificatePath = $CertificatePath
                CertificatePassword = $CertificatePassword
                TenantId = $TenantId
                ManagedIdentity = $ManagedIdentity
                AccessTokens = $AccessTokens
            }
            [array]$allRequestedConfigurationPolicies = Get-MgBetaDeviceManagementConfigurationPolicy -All | Where-Object { $_.templateReference.templateId -in $requestedConfigurationPolicyTemplateIds }
            $batchRequests = @()
            foreach ($policy in $allRequestedConfigurationPolicies)
            {
                $batchRequest = @{
                    Id = $policy.id
                    Method = 'GET'
                    Url = "/deviceManagement/configurationPolicies/$($policy.id)/settings?`$expand=settingDefinitions&`$top=1000"
                }
                $batchRequests += $batchRequest
            }
            $batchResponses = Invoke-M365DSCGraphBatchRequest -Requests $batchRequests
            foreach ($policySettings in $batchResponses)
            {
                $policy = $allRequestedConfigurationPolicies | Where-Object -Property Id -EQ $policySettings.id
                $policy.settings = $policySettings.body.value
            }
            [Microsoft365DSC.Intune.ConfigurationPolicyCache]::Populate($allRequestedConfigurationPolicies, [System.Func[System.Object, System.String]]{ param($policy) $policy.templateReference.templateId })
        }

        if ($Parallel)
        {
            if ($Workloads.Count -eq 0)
            {
                $Workloads = Get-M365DSCWorkloadForResource -ResourceName $resourcesToExport.Name
            }
            foreach ($workload in $Workloads)
            {
                Write-M365DSCHost -Message "Starting export in parallel mode for workload {$workload}. Initialization may take a while..."
                $requiredModules = [System.Collections.Generic.List[System.String]]::new(25)
                $currentWorkloadResources = $resourcesToExport | Where-Object -FilterScript { $_.Name -Like "$workload*" }
                foreach ($resource in $currentWorkloadResources)
                {
                    foreach ($module in $resourceSettings[$resource.Name].requiredModules)
                    {
                        if (-not $requiredModules.Contains($module))
                        {
                            $requiredModules.Add($module)
                        }
                    }
                }
                $arguments = @{
                    ScriptBlock = $exportScriptBlock
                }
                <# Removed due to collection enumeration error in parallel execution
                if ($requiredModules.Count -gt 0)
                {
                    $arguments.Add('ModuleName', $requiredModules)
                }
                #>

                $resourcesPath | Where-Object -FilterScript { $_.Name -Like "*MSFT_$workload*" } | Invoke-Parallel @arguments -Verbose
            }
        }
        else
        {
            Write-M365DSCHost -Message 'Starting export in sequential mode...'
            $exportScriptBlock = [ScriptBlock]::Create($exportScriptBlock.ToString().Replace('$using:', '$'))
            $resourcesPath | ForEach-Object -Process $exportScriptBlock
        }

        foreach ($resource in $($synchronizedHashtable.ResourcesResult.Keys | Sort-Object))
        {
            [void]$DSCContent.Append($synchronizedHashtable.ResourcesResult[$resource])
        }

        # Post-process: inject DependsOn declarations and generate stub blocks
        if ($IncludeDependencies.IsPresent)
        {
            $processedContent = Add-M365DSCExportDependsOn -DSCContent $DSCContent.ToString()
            $DSCContent = [System.Text.StringBuilder]::new()
            $DSCContent.Append($processedContent) | Out-Null
        }

        foreach ($pair in (Get-M365DSCStringReplacementMap).GetEnumerator())
        {
            Add-ConfigurationDataEntry -Node 'NonNodeData' `
                -Key $pair.Value `
                -Value $pair.Key `
                -Description "Placeholder for sensitive data - $($pair.Value)"
        }

        # Close the Node and Configuration declarations
        $DSCContent.Append(" }`r`n") | Out-Null
        $DSCContent.Append("}`r`n") | Out-Null

        # Azure Automation Check
        $AzureAutomation = $false
        if ($env:AZUREPS_HOST_ENVIRONMENT -like 'AzureAutomation*')
        {
            $AzureAutomation = $true
        }

        $launchCommand = "$ConfigurationName -ConfigurationData .\ConfigurationData.psd1"
        switch ($AuthMethods)
        {
            'CertificatePath'
            {
                $credsContent = ''
                $credsContent += ' $CredsCertificatePassword = Get-Credential -Message "Certificate Password"'
                $credsContent += "`r`n"
                $startPosition = $DSCContent.ToString().IndexOf('<# Credentials #>') + 19
                $DSCContent = $DSCContent.Insert($startPosition, $credsContent)
                $launchCommand += " -CertificatePassword `$CredsCertificatePassword"
            }
            { $_ -in 'Credentials', 'CredentialsWithApplicationId' }
            {
                #region Add the Prompt for Required Credentials at the top of the Configuration
                $credsContent = ''
                $credsContent += ' $CredsCredential ' + "= Get-Credential -Message `"Credentials`"`r`n"
                $credsContent += "`r`n"
                $startPosition = $DSCContent.ToString().IndexOf('<# Credentials #>') + 19
                $DSCContent = $DSCContent.Insert($startPosition, $credsContent)
                $launchCommand += " -Credential `$CredsCredential"
                #endregion
            }
        }

        $DSCContent.Append("`r`n") | Out-Null
        $DSCContent.Append($launchCommand) | Out-Null

        #region Benchmarks
        $M365DSCExportEndTime = [System.DateTime]::Now
        $timeTaken = New-TimeSpan -Start ($M365DSCExportStartTime.ToString()) `
            -End ($M365DSCExportEndTime.ToString())
        Write-M365DSCHost -Message "$($Global:M365DSCEmojiHourglass) Export took {" -DeferWrite
        Write-M365DSCHost -Message "$($timeTaken.TotalSeconds) seconds" -DeferWrite -ForegroundColor Cyan
        Write-M365DSCHost -Message '} for {' -DeferWrite
        Write-M365DSCHost -Message "$($Global:M365DSCExportResourceInstancesCount) instances" -DeferWrite -ForegroundColor Magenta
        Write-M365DSCHost -Message '}' -CommitWrite
        Write-M365DSCHost -Message "Successful exports: {$($synchronizedHashtable.SuccessfulResources)}"
        Write-M365DSCHost -Message "Failed exports: {$($synchronizedHashtable.FailedResources)}"
        if ($($synchronizedHashtable.FailedResources) -eq 0)
        {
            $Message = "$($Global:M365DSCEmojiGreenCheckmark) Export completed successfully."
            Write-M365DSCHost -Message $Message -ForegroundColor Green
        }
        else
        {
            $Message = "$($Global:M365DSCEmojiRedX) Export completed with errors."
            Write-M365DSCHost -Message $Message -ForegroundColor Red
        }
        Send-M365DSCPushNotification -Body $Message
        #endregion

        $sessions = Get-PSSession | Where-Object -FilterScript { $_.Name -like 'SfBPowerShellSessionViaTeamsModule_*' -or `
                $_.Name -like 'ExchangeOnlineInternalSession*' }
        foreach ($session in $sessions)
        {
            try
            {
                Write-Verbose -Message "Closing PSSession {$($session.Name)}"
                Remove-PSSession $session | Out-Null
            }
            catch
            {
                Write-Verbose -Message $_
            }
        }

        # Check if configuration validation needs to be performed
        if ($Validate.IsPresent)
        {
            Write-M365DSCHost -Message "$($Global:M365DSCMagnifyingGlass) Starting configuration validation..."
            [Array]$results = Get-M365DSCConfigurationConflict -ConfigurationContent $DSCContent.ToString()
            Write-M365DSCHost -Message 'Results:'
            if ($results.Count -gt 0)
            {
                $errorMessage = ''
                foreach ($issue in $results)
                {
                    $errorMessage += " - [$($issue.Reason)]: $($issue.InstanceName)`r`n"
                }
                Write-Error -Message $errorMessage -ErrorAction Continue
            }
            else
            {
                Write-M365DSCHost -Message 'No conflicts detected'
            }
        }

        #region Copy Downloaded files back into output folder
        if (($null -ne $Components -and $Components.Contains('SPOApp') -and -not $ComponentsToSkip.Contains('SPOApp')) -or $AllComponents)
        {
            if ($AuthMethods -contains 'Credentials')
            {
                $filesToDownload = Get-AllSPOPackages -Credential $Credential
            }
            else
            {
                $filesToDownload = Get-AllSPOPackages -ApplicationId $ApplicationId -CertificateThumbprint $CertificateThumbprint `
                    -CertificatePassword $CertificatePassword -TenantId $TenantId -CertificatePath $CertificatePath -ManagedIdentity:$ManagedIdentity.IsPresent
            }
            if ($filesToDownload.Count -gt 0)
            {
                foreach ($fileToCopy in $filesToDownload)
                {
                    if (-not [System.String]::IsNullOrEmpty($env:TEMP))
                    {
                        $filePath = Join-Path $env:TEMP $fileToCopy.Name -Resolve
                        $destPath = Join-Path $OutputDSCPath $fileToCopy.Name
                        Copy-Item -Path $filePath -Destination $destPath -Force
                    }
                }
            }
        }
        #endregion

        if (-not [System.String]::IsNullOrEmpty($FileName))
        {
            $outputDSCFile = $FileName
        }
        else
        {
            $outputDSCFile = 'M365TenantConfig.ps1'
        }

        # Clean empty lines with semi-colons, normally generated from CIMInstances convertions to String.
        $DSCContent = $DSCContent.Replace("`r`n;`r`n", ";`r`n")
        $DSCContent.ToString() | Out-File $outputDSCFile

        try
        {
            $Global:M365DSCExportContentSize = $DSCContent.Length
        }
        catch
        {
            Write-Verbose -Message $_
        }

        if (-not $AzureAutomation -and -not $ManagedIdentity.IsPresent)
        {
            try
            {
                if ($PSEdition -eq 'Desktop' -or $IsWindows -and ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
                {
                    $LCMConfig = Get-DscLocalConfigurationManager
                    if ($null -ne $LCMConfig.CertificateID)
                    {
                        try
                        {
                            # Export the certificate assigned to the LCM
                            $certPath = $OutputDSCPath + 'M365DSC.cer'
                            if (Test-Path $certPath)
                            {
                                Remove-Item $certPath -Force
                            }
                            Export-Certificate -FilePath $certPath `
                                -Cert "cert:\LocalMachine\my\$($LCMConfig.CertificateID)" `
                                -Type CERT `
                                -NoClobber | Out-Null
                        }
                        catch
                        {
                            New-M365DSCLogEntry -Message 'Error while exporting the DSC certificate:' `
                                -Exception $_ `
                                -Source $($MyInvocation.MyCommand.Source) `
                                -TenantId $TenantId `
                                -Credential $Credential
                        }

                        Add-ConfigurationDataEntry -Node 'localhost' `
                            -Key 'CertificateFile' `
                            -Value 'M365DSC.cer' `
                            -Description 'Path of the certificate used to encrypt credentials in the file.'
                    }
                }
                else
                {
                    Write-Verbose -Message "Cannot export Local Configuration Manager settings. This process isn't executed with Administrative Privileges."
                }
            }
            catch
            {
                Write-Verbose -Message 'Could not retrieve current Windows Principal. This may be due to the fact that the current OS is not Windows.'
            }
        }
        $outputConfigurationData = '.\ConfigurationData.psd1'
        New-ConfigurationDataDocument -Path $outputConfigurationData
        if ($shouldOpenOutputDirectory)
        {
            try
            {
                Invoke-Item -Path $OutputDSCPath
            }
            catch
            {
                Write-Verbose -Message $_
            }
        }

        # Close the partial export StreamWriter
        Close-M365DSCPartialExport

        # Remove Temp Partial Export File
        if (-not [System.String]::IsNullOrEmpty($env:TEMP))
        {
            $partialPath = Join-Path $env:TEMP -ChildPath "$($Global:PartialExportFileName)"
            if (Test-Path $partialPath)
            {
                Remove-Item -Path $partialPath -Force
            }
        }

        Pop-Location
        if ($WithStatistics)
        {
            @{
                ExportDurationInSeconds = $timeTaken.TotalSeconds
                ExportedResourceCount   = $synchronizedHashtable.SuccessfulResources
                FailedResourceCount     = $synchronizedHashtable.FailedResources
                TotalResourceCount      = $synchronizedHashtable.SuccessfulResources + $synchronizedHashtable.FailedResources
            }
        }
    }
    catch
    {
        # Close the partial export StreamWriter
        Close-M365DSCPartialExport

        if (-not [System.String]::IsNullOrEmpty($env:TEMP))
        {
            $partialPath = Join-Path $env:TEMP -ChildPath "$($Global:PartialExportFileName)"
            Write-M365DSCHost -Message "Partial Export file was saved at: $partialPath"
        }

        throw
    }
}

<#
.DESCRIPTION
    This function gets all resources for the specified workloads
 
.FUNCTIONALITY
    Internal, Hidden
#>

function Get-M365DSCResourcesByWorkloads
{
    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[System.String[]]])]
    param(
        [Parameter(Mandatory = $true)]
        [System.String[]]
        $Workloads,

        [Parameter()]
        [ValidateSet('Default', 'Full')]
        [System.String]
        $Mode = 'Default'
    )

    $components = [System.Collections.Generic.List[System.String[]]]::new()
    foreach ($Workload in $Workloads)
    {
        Write-M365DSCHost -Message "Finding all resources for workload {$Workload} and Mode {$Mode}" -ForegroundColor Gray

        $fullComponents = Get-M365DSCResourcesByExportMode -Mode 'Full' -ExcludeConfigurationResources
        foreach ($resource in $Script:allM365DSCResources)
        {
            if ($resource.StartsWith($Workload, 'CurrentCultureIgnoreCase') -and
                ($Mode -eq 'Full' -or `
                ($Mode -eq 'Default' -and -not $fullComponents.Contains($resource))))
            {
                $components.Add($resource)
            }
        }
    }

    return $components
}

Export-ModuleMember -Function @(
    'Start-M365DSCConfigurationExtract'
)