Modules/M365DSCModuleMgmt.psm1

$Script:IsPowerShellCore = $PSVersionTable.PSEdition -eq 'Core'
$Script:IsPsResourceGetAvailable = $null -ne (Get-Module -Name Microsoft.PowerShell.PSResourceGet -ListAvailable)
$Script:M365DSCDependenciesValidated = $false
$Script:M365DSCGraphShimLoaded = $false

<#
.DESCRIPTION
    This function performs the one-time initialization of the M365DSC dependency and resource-settings
    metadata used throughout this module. It is wrapped in a function (rather than run as top-level module
    code) so that the scratch/intermediate variables it uses are released once it returns, instead of being
    pinned for the lifetime of the session as module-scope variables.
 
.FUNCTIONALITY
    Internal
#>

function Initialize-M365DSCModuleMgmt
{
    [CmdletBinding()]
    param()

    if ($null -eq $Script:M365DSCDependencies)
    {
        $Script:M365DSCDependencies = [System.Collections.Generic.Dictionary[System.String, System.Object]]::new([System.StringComparer]::OrdinalIgnoreCase)
        $Script:M365DSCDevDependencies = [System.Collections.Generic.Dictionary[System.String, System.Object]]::new([System.StringComparer]::OrdinalIgnoreCase)
        $dependencies = (Import-PowerShellDataFile "$PSScriptRoot/../Dependencies/Manifest.psd1").Dependencies
        $devDependencies = (Import-PowerShellDataFile "$PSScriptRoot/../Dependencies/DevManifest.psd1").Dependencies
        foreach ($dependency in $dependencies)
        {
            # TODO: Review again once ModuleFast can work with additional properties
            # https://github.com/microsoft/Microsoft365DSC/pull/6726
            # https://github.com/ykuijs/M365DSC_CICD/issues/53
            if ($dependency.ModuleName -eq 'PnP.PowerShell')
            {
                $dependency.DependsOn = @('Microsoft.Graph.Authentication')
            }
            $Script:M365DSCDependencies.Add($dependency.ModuleName, $dependency)
        }

        foreach ($devDependency in $devDependencies)
        {
            $Script:M365DSCDevDependencies.Add($devDependency.ModuleName, $devDependency)
        }

        $commandToModuleMap = @{}
        $Script:M365DSCResourceSettings = [System.Collections.Generic.Dictionary[System.String, System.Object]]::new([System.StringComparer]::OrdinalIgnoreCase)
        foreach ($file in (Get-ChildItem -Path "$PSScriptRoot/../DscResources" -Filter 'settings.json' -Recurse -File)) {
            Write-Verbose -Message "Processing settings.json file at path: $($file.FullName)"
            $jsonContent = [System.IO.File]::ReadAllText($file.FullName) | ConvertFrom-Json
            foreach ($commandMap in ($jsonContent.commands | Where-Object -Property module -NotIn $Script:M365DSCDevDependencies.Keys)) {
                $commandToModuleMap[$commandMap.module] += @($commandMap.cmdlets)
            }
            $directoryName = (Split-Path -Path $file.DirectoryName -Leaf).Replace('MSFT_', '')
            $Script:M365DSCResourceSettings.Add($directoryName, @{
                requiredModules = $jsonContent.requiredModules | Where-Object { $_ -notin $Script:M365DSCDevDependencies.Keys }
                mode = $jsonContent.mode
            })
        }

        Write-Verbose -Message "Loading current configuration from config.json"
        $Script:M365DSCValidatedDependencies = [System.Collections.Generic.List[System.String]]::new($Script:M365DSCDependencies.Count + $Script:M365DSCDevDependencies.Count)
        $configAsPsCustomObject = Get-Content -Path "$PSScriptRoot/../config.json" | ConvertFrom-Json
        $configAsHashtable = @{}
        foreach ($property in $configAsPsCustomObject.PSObject.Properties)
        {
            $configAsHashtable.Add($property.Name, $property.Value)
        }
        $Script:CurrentConfiguration = $configAsHashtable
        $globalRequiredModules = $Script:CurrentConfiguration.requiredModules
        foreach ($entry in $commandToModuleMap.GetEnumerator())
        {
            $sortedFunctions = @($globalRequiredModules.$($entry.Key)) + @($entry.Value) | Sort-Object -Unique
            $Script:M365DSCDependencies[$entry.Key].Commands = $sortedFunctions
        }
        $Script:M365DSCRequiredModules = @($globalRequiredModules.psobject.Properties.Name)
        $Script:M365DSCRequiredModulesLoaded = $false
    }
}
Initialize-M365DSCModuleMgmt

<#
.SYNOPSIS
    Returns resource settings metadata loaded by module management.
 
.DESCRIPTION
    Returns the in-memory dictionary of resource settings built from each resource settings.json file.
#>

function Get-M365DSCResourceSettings
{
    [CmdletBinding()]
    param()

    return $Script:M365DSCResourceSettings
}

<#
.SYNOPSIS
    Returns globally required module names for Microsoft365DSC.
 
.DESCRIPTION
    Returns the module names defined as required in the module configuration.
#>

function Get-M365DSCRequiredModules
{
    [CmdletBinding()]
    param()

    return $Script:M365DSCRequiredModules
}

<#
.SYNOPSIS
    Sets the required-modules-loaded state flag.
 
.DESCRIPTION
    Updates the module-scope flag used to track whether required modules were loaded in the current session.
 
.PARAMETER Value
    Specifies the new loaded state value.
#>

function Set-M365DSCRequiredModulesLoaded
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Boolean]$Value
    )

    $Script:M365DSCRequiredModulesLoaded = $Value
}

<#
.SYNOPSIS
    Returns whether required modules are marked as loaded.
 
.DESCRIPTION
    Returns the module-scope boolean state indicating whether required modules were loaded.
#>

function Test-IsM365DSCRequiredModulesLoaded
{
    [CmdletBinding()]
    param()

    return $Script:M365DSCRequiredModulesLoaded
}

<#
.SYNOPSIS
    Returns the current Microsoft365DSC module configuration.
 
.DESCRIPTION
    Returns a cloned hashtable of the loaded module configuration values.
 
.OUTPUTS
    System.Collections.Hashtable
#>

function Get-M365DSCModuleConfiguration
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param()

    return $Script:CurrentConfiguration.Clone()
}

<#
.SYNOPSIS
    Updates a single Microsoft365DSC module configuration value.
 
.DESCRIPTION
    Sets a configuration entry in the current module configuration hashtable.
 
.PARAMETER Key
    Specifies the configuration key to update.
 
.PARAMETER Value
    Specifies the value to assign to the configuration key.
#>

function Set-M365DSCModuleConfiguration
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Key,

        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [AllowNull()]
        [System.Object]
        $Value
    )

    $Script:CurrentConfiguration.$Key = $Value
}

<#
.SYNOPSIS
    Validates that required Microsoft365DSC dependencies are installed.
 
.DESCRIPTION
    Checks dependency health and throws when required module versions are missing.
    Validation can be skipped by session flags already used by Microsoft365DSC.
 
.FUNCTIONALITY
    Internal
#>

function Confirm-M365DSCDependencies
{
    [CmdletBinding()]
    param()

    if (-not $Script:M365DSCDependenciesValidated -and ($null -eq $Global:M365DSCSkipDependenciesValidation -or -not $Global:M365DSCSkipDependenciesValidation))
    {
        Write-Verbose -Message 'Dependencies were not already validated.'

        Test-CodePage
        $result = Update-M365DSCDependencies -ValidateOnly

        if ($result.Length -gt 0)
        {
            $ErrorMessage = "The following dependencies need updating:`r`n"
            foreach ($invalidDependency in $result)
            {
                $ErrorMessage += ' * ' + $invalidDependency.ModuleName + "`r`n"
            }
            $ErrorMessage += 'Please run Update-M365DSCDependencies as Administrator. '
            $Script:M365DSCDependenciesValidated = $false
            Add-M365DSCEvent -Message $ErrorMessage -EntryType 'Error' `
                -EventID 1 -Source $($MyInvocation.MyCommand.Source) `
                -TenantId $tenantIdValue
            throw $ErrorMessage
        }
        else
        {
            Write-Verbose -Message 'Dependencies were all successfully validated.'
            $Script:M365DSCDependenciesValidated = $true
        }
    }
    else
    {
        Write-Verbose -Message 'Dependencies were already successfully validated.'
    }
}

<#
.SYNOPSIS
    Ensures a dependency module is loaded at the required version.
 
.DESCRIPTION
    Loads dependency modules on demand, validates versions, and recursively validates dependency chains.
    For Graph typed modules, it applies the Microsoft365DSC Graph shim behavior.
 
.PARAMETER ModuleName
    Specifies the dependency module name to validate and load.
 
.EXAMPLE
    PS> Confirm-M365DSCLoadedModule -ModuleName 'Microsoft.Graph.Authentication'
 
.FUNCTIONALITY
    Internal
#>

function Confirm-M365DSCLoadedModule
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.String]
        $ModuleName
    )

    if ($Script:M365DSCValidatedDependencies.Contains($ModuleName))
    {
        Write-Verbose -Message "Module '$ModuleName' has already been validated."
        return
    }

    # Graph Shim intercept: Replace typed Graph SDK
    # sub-modules with the lightweight M365DSCGraphShim module that wraps
    # Invoke-MgGraphRequest. Microsoft.Graph.Authentication is always loaded
    # natively because it provides Connect-MgGraph and the underlying HTTP client.
    if ($ModuleName -like 'Microsoft.Graph.*' -and
        $ModuleName -ne 'Microsoft.Graph.Authentication')
    {
        if (-not $Script:M365DSCGraphShimLoaded)
        {
            Write-Verbose -Message "Graph Shim enabled: importing M365DSCGraphShim instead of '$ModuleName'."
            # Ensure Microsoft.Graph.Authentication is loaded first
            Confirm-M365DSCLoadedModule -ModuleName 'Microsoft.Graph.Authentication'

            Import-Module -Name "$PSScriptRoot/M365DSCGraphShim.psd1" -Global -Force -DisableNameChecking -Function * -Cmdlet @() -Variable @() -Alias @()
            $Script:M365DSCGraphShimLoaded = $true
        }
        else
        {
            Write-Verbose -Message "Graph Shim already loaded, skipping import for '$ModuleName'."
        }

        $Script:M365DSCValidatedDependencies.Add($ModuleName)
        return
    }

    $manifestModule = $Script:M365DSCDependencies[$ModuleName]

    if ($null -ne $manifestModule.DependsOn -and $manifestModule.DependsOn.Count -gt 0)
    {
        foreach ($dependency in $manifestModule.DependsOn)
        {
            Write-Verbose -Message "Validating dependency '$dependency' for module '$ModuleName'."
            Confirm-M365DSCLoadedModule -ModuleName $dependency
        }
    }

    $loadedModule = Get-Module -Name $ModuleName
    if ($null -eq $loadedModule)
    {
        Write-Verbose -Message "Module '$ModuleName' is not loaded. Importing it now."
        $importModuleSplat = @{
            Name             = $ModuleName
            RequiredVersion  = $manifestModule.RequiredVersion
            Global           = $true
            Alias            = @()
            Cmdlet           = @()
            Variable         = @()
            DisableNameChecking = $true
        }
        if ($manifestModule.Commands.Count -gt 0)
        {
            $importModuleSplat.Add('Function', $manifestModule.Commands)
            $importModuleSplat.Cmdlet = $manifestModule.Commands
        }
        if ($ModuleName -eq 'PnP.PowerShell' -and $manifestModule.RequiredVersion -eq '1.12.0' -and $Script:IsPowerShellCore)
        {
            $importModuleSplat.Add('UseWindowsPowerShell', $true)
            if ($importModuleSplat.ContainsKey('Function'))
            {
                $importModuleSplat.Remove('Function')
            }
            if ($importModuleSplat.ContainsKey('Cmdlet'))
            {
                $importModuleSplat.Remove('Cmdlet')
            }
            if ($importModuleSplat.ContainsKey('Alias'))
            {
                $importModuleSplat.Remove('Alias')
            }
            if ($importModuleSplat.ContainsKey('Variable'))
            {
                $importModuleSplat.Remove('Variable')
            }
        }
        Import-Module @importModuleSplat
        Write-Verbose -Message "Module '$ModuleName' with version '$($manifestModule.RequiredVersion)' has been imported."
    }
    elseif ($loadedModule.Version -ne $manifestModule.RequiredVersion)
    {
        Write-Verbose -Message "Module '$ModuleName' is loaded but the version '$($loadedModule.Version)' does not match the required version '$($manifestModule.RequiredVersion)'."
        Remove-Module -Name $ModuleName -Force -ErrorAction SilentlyContinue
        Write-Verbose -Message "Unloaded module '$ModuleName' with version '$($loadedModule.Version)'."
        Import-Module -Name $ModuleName -RequiredVersion $manifestModule.RequiredVersion -Global -Alias @() -Cmdlet @() -Variable @() -DisableNameChecking
        Write-Verbose -Message "Re-imported module '$ModuleName' with version '$($manifestModule.RequiredVersion)'."
    }
    else
    {
        Write-Verbose -Message "Module '$ModuleName' is already loaded."
    }

    if (-not $Script:M365DSCValidatedDependencies.Contains($ModuleName))
    {
        $Script:M365DSCValidatedDependencies.Add($ModuleName)
    }
}

<#
.SYNOPSIS
    Validates dependencies required by a DSC resource module.
 
.DESCRIPTION
    Resolves required modules from resource settings and validates each dependency before resource execution.
 
.PARAMETER ModuleName
    Specifies the DSC resource module name whose dependencies should be validated.
 
.EXAMPLE
    PS> Confirm-M365DSCModuleDependency -ModuleName 'MSFT_AADApplication'
 
.FUNCTIONALITY
    Internal
#>

function Confirm-M365DSCModuleDependency
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.String]
        $ModuleName
    )

    $Global:MaximumFunctionCount = 32767

    if ($Global:IsTestEnvironment -or (Get-M365DSCModuleConfiguration).skipModuleDependencyValidation)
    {
        Write-Verbose -Message "Skipping module dependency validation in test environment for module '$ModuleName'."
        return
    }

    $modulesToCheck = $Script:M365DSCResourceSettings[$ModuleName.Replace('MSFT_', '')].requiredModules
    foreach ($module in $modulesToCheck)
    {
        Write-Verbose -Message "Validating module dependency: $($module)"
        Confirm-M365DSCLoadedModule -ModuleName $module
    }
    Write-Verbose -Message "All dependencies for module '$ModuleName' have been validated."
}

<#
.SYNOPSIS
    Checks whether newer versions exist for configured dependencies.
 
.DESCRIPTION
    Queries the gallery for each configured dependency and reports modules with newer available versions.
 
.EXAMPLE
    PS> Test-M365DSCDependenciesForNewVersions
 
.FUNCTIONALITY
    Public
#>

function Test-M365DSCDependenciesForNewVersions
{
    [CmdletBinding()]
    param ()

    $i = 1
    Import-Module PowerShellGet -Force

    foreach ($dependency in $Script:M365DSCDependencies.Values.GetEnumerator())
    {
        Write-Progress -Activity 'Scanning Dependencies' -PercentComplete ($i / $Script:M365DSCDependencies.Count * 100)
        try
        {
            $moduleInGallery = Find-Module $dependency.ModuleName
            [array]$moduleInstalled = Get-Module $dependency.ModuleName -ListAvailable | Select-Object Version
            if ($moduleInstalled)
            {
                $modules = $moduleInstalled | Sort-Object Version -Descending
            }
            $moduleInstalled = $modules[0]
            if (-not $modules -or [Version]($moduleInGallery.Version) -gt [Version]($moduleInstalled[0].Version))
            {
                Write-Host "New version of {$($dependency.ModuleName)} is available {$($moduleInGallery.Version)}"
            }
        }
        catch
        {
            Write-Host $_
            Write-Host "New version of {$($dependency.ModuleName)} is available"
        }
        $i++
    }

    # The progress bar seems to hang sometimes. Make sure it is no longer displayed.
    Write-Progress -Activity 'Scanning Dependencies' -Completed
}

<#
.SYNOPSIS
    Validates the installed Microsoft365DSC module version.
 
.DESCRIPTION
    Compares local and gallery module versions and reports when a newer Microsoft365DSC module is available.
 
 
.EXAMPLE
    PS> Test-M365DSCModuleValidity
 
.FUNCTIONALITY
    Public
#>

function Test-M365DSCModuleValidity
{
    [CmdletBinding()]
    param()

    if ($Script:IsM365DSCModuleValidated)
    {
        Write-Verbose -Message 'The Microsoft365DSC module has already been validated in this session.'
        Write-Verbose -Message 'If you have updated the module, please restart your PowerShell session to re-validate.'
        return
    }

    if ($env:AZUREPS_HOST_ENVIRONMENT -like 'AzureAutomation*')
    {
        $message = 'Skipping check for newer version of Microsoft365DSC due to Azure Automation Environment restrictions.'
        Write-Verbose -Message $message
        return
    }

    # Validate if only one installation of the module is present and that it's the latest version available
    if ($Script:IsPsResourceGetAvailable)
    {
        $latestVersion = (Find-PSResource -Name 'Microsoft365DSC' -Repository 'PSGallery').Version | Sort-Object -Descending | Select-Object -First 1
    }
    else
    {
        $latestVersion = (Find-Module -Name 'Microsoft365DSC' -Includes 'DSCResource').Version
    }
    $localVersion = (Get-Module -Name 'Microsoft365DSC').Version

    if ($latestVersion -gt $localVersion)
    {
        Write-Host "There is a newer version of the 'Microsoft365DSC' module available on the gallery."
        Write-Host "To update the module and it's dependencies, run the following command:"
        Write-Host 'Update-M365DSCModule' -ForegroundColor Blue
    }

    $Script:IsM365DSCModuleValidated = $true
}

<#
.SYNOPSIS
    Removes outdated Microsoft365DSC module and dependency versions.
 
.DESCRIPTION
    Scans installed module versions and removes outdated Microsoft365DSC and dependency versions while preserving required versions.
 
.EXAMPLE
    PS> Uninstall-M365DSCOutdatedDependencies
 
.FUNCTIONALITY
    Public
#>

function Uninstall-M365DSCOutdatedDependencies
{
    [CmdletBinding()]
    param()

    try
    {
        $InformationPreference = 'Continue'

        [array]$microsoft365DscModules = Get-Module Microsoft365DSC -ListAvailable
        $outdatedMicrosoft365DscModules = $microsoft365DscModules | Sort-Object -Property Version | Select-Object -SkipLast 1

        foreach ($module in $outdatedMicrosoft365DscModules)
        {
            try
            {
                Write-Information -MessageData "Uninstalling $($module.Name) Version {$($module.Version)}"
                if (Test-Path -Path $($module.Path))
                {
                    Remove-Item $($module.ModuleBase) -Force -Recurse -ErrorAction Stop
                }
            }
            catch
            {
                $message = "Could not uninstall $($module.Name) Version $($module.Version)"
                if ($_.Exception.Message -like "*Access to the path* is denied*" -and ($Scope -eq "AllUsers") -and ($PSEdition -eq 'Desktop' -or $IsWindows) -and -not
                    ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
                {
                    $message += ' You need to run this command as a local administrator.'
                }
                New-M365DSCLogEntry -Message $message `
                    -Exception $_ `
                    -Source $($MyInvocation.MyCommand.Source)
                Write-Error -Message $message -ErrorAction Continue
            }
        }

        $allDependenciesExceptAuth = $Script:M365DSCDependencies.Values.GetEnumerator().Where({ $_.ModuleName -ne 'Microsoft.Graph.Authentication' })

        $i = 1
        foreach ($dependency in $allDependenciesExceptAuth)
        {
            Write-Progress -Activity 'Scanning Dependencies' -PercentComplete ($i / $allDependenciesExceptAuth.Count * 100)
            try
            {
                if ($dependency.PowerShellCore -and -not $Script:IsPowerShellCore)
                {
                    Write-Verbose -Message "Skipping module {$($dependency.ModuleName)} as it is managed by PowerShell Core."
                    continue
                }
                elseif ($dependency.PowerShellCore -eq $false -and $Script:IsPowerShellCore)
                {
                    Write-Verbose -Message "Skipping module {$($dependency.ModuleName)} as it is managed by Windows PowerShell."
                    continue
                }
                $found = Get-Module $dependency.ModuleName -ListAvailable | Where-Object -Property Version -NE $dependency.RequiredVersion
                foreach ($foundModule in $found)
                {
                    try
                    {
                        Write-Information -MessageData "Uninstalling $($foundModule.Name) Version {$($foundModule.Version)}"
                        if (Test-Path -Path $($foundModule.Path))
                        {
                            Remove-Item $($foundModule.ModuleBase) -Force -Recurse -ErrorAction Stop
                        }
                    }
                    catch
                    {
                        $message = "Could not uninstall $($foundModule.Name) Version $($foundModule.Version)"
                        if ($_.Exception.Message -like "*Access to the path* is denied*" -and ($PSEdition -eq 'Desktop' -or $IsWindows) -and
                            ($Scope -eq "AllUsers") -and -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
                        {
                            $message += ' You need to run this command as a local administrator.'
                        }
                        New-M365DSCLogEntry -Message $message `
                            -Exception $_ `
                            -Source $($MyInvocation.MyCommand.Source)
                        Write-Error -Message $message -ErrorAction Continue
                    }
                }
            }
            catch
            {
                Write-Error -Message "Could not uninstall {$($dependency.ModuleName)}" -ErrorAction Continue
            }
            $i++
        }
    }
    catch
    {
        New-M365DSCLogEntry -Message 'Error uninstalling outdated dependencies:' `
            -Exception $_ `
            -Source $($MyInvocation.MyCommand.Source)
        Write-Error $_
    }

    $authModule = $Script:M365DSCDependencies['Microsoft.Graph.Authentication']
    try
    {
        Write-Information -MessageData 'Checking Microsoft.Graph.Authentication'
        $found = Get-Module $authModule.ModuleName -ListAvailable | Where-Object -Property Version -NE $authModule.RequiredVersion
        foreach ($foundModule in $found)
        {
            try
            {
                Write-Information -MessageData "Uninstalling $($foundModule.Name) version {$($foundModule.Version)}"
                if (Test-Path -Path $($foundModule.Path))
                {
                    Remove-Item $($foundModule.ModuleBase) -Force -Recurse -ErrorAction Stop
                }
            }
            catch
            {
                $message = "Could not uninstall $($foundModule.Name) Version $($foundModule.Version)"
                if ($_.Exception.Message -like "*Access to the path* is denied*" -and ($PSEdition -eq 'Desktop' -or $IsWindows) -and
                    ($Scope -eq "AllUsers") -and -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
                {
                    $message += ' You need to run this command as a local administrator.'
                }
                New-M365DSCLogEntry -Message $message `
                    -Exception $_ `
                    -Source $($MyInvocation.MyCommand.Source)
                Write-Error -Message $message -ErrorAction Continue
            }
        }
    }
    catch
    {
        Write-Error -Message "Could not uninstall {$($dependency.ModuleName)}" -ErrorAction Continue
    }
}

<#
.SYNOPSIS
    Installs or validates Microsoft365DSC dependencies.
 
.DESCRIPTION
    Validates, installs, or force-refreshes dependency modules according to manifest requirements and selected installation options.
 
.PARAMETER Force
    Indicates that dependencies should be reinstalled even when required versions are present.
 
.PARAMETER ValidateOnly
    Indicates that only validation should run and missing dependencies should be returned.
 
.PARAMETER Scope
    Specifies the installation scope for dependency modules.
 
.PARAMETER Proxy
    Specifies the proxy server used for module installation requests.
 
.PARAMETER Repository
    Specifies the repository used to install dependencies.
 
.PARAMETER UsePowerShellGet
    Indicates that Install-Module should be used instead of Install-PSResource.
 
.PARAMETER Development
    Indicates that development dependencies should also be processed.
 
.EXAMPLE
    PS> Update-M365DSCDependencies
 
.EXAMPLE
    PS> Update-M365DSCDependencies -Force
 
.EXAMPLE
    PS> Update-M365DSCDependencies -Scope CurrentUser
 
.FUNCTIONALITY
    Public
#>

function Update-M365DSCDependencies
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [Switch]
        $Force,

        [Parameter()]
        [Switch]
        $ValidateOnly,

        [Parameter()]
        [ValidateSet("CurrentUser", "AllUsers")]
        $Scope = "AllUsers",

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

        [Parameter()]
        [System.String]
        $Repository = 'PSGallery',

        [Parameter()]
        [switch]
        $UsePowerShellGet,

        [Parameter()]
        [Switch]
        $Development
    )

    try
    {
        $InformationPreference = 'Continue'
        $i = 1

        $returnValue = @()

        $params = @{}
        if (-not [System.String]::IsNullOrEmpty($Proxy))
        {
            $params.Add('Proxy', $Proxy)
        }

        # Check if PSResourceGet is installed or not
        if (-not $Script:IsPsResourceGetAvailable)
        {
            Write-Warning -Message 'Microsoft.PowerShell.PSResourceGet is not installed, installing it now...'
            try
            {
                Install-Module -Name Microsoft.PowerShell.PSResourceGet -Scope $Scope -AllowClobber @params -Force -ErrorAction Stop -Repository PSGallery
                $Script:IsPsResourceGetAvailable = $true
            }
            catch
            {
                Write-Warning -Message "Failed to install Microsoft.PowerShell.PSResourceGet, continuing without it..."
            }
        }

        $scopedIsPsResourceGetAvailable = $Script:IsPsResourceGetAvailable
        if ($params.ContainsKey('Proxy'))
        {
            Write-Information -MessageData "Falling back to Install-Module because Install-PSResource does not support a proxy"
            $scopedIsPsResourceGetAvailable = $false
        }

        $dependencies = [System.Object[]]::new($Script:M365DSCDependencies.Count + $Script:M365DSCDevDependencies.Count)
        $Script:M365DSCDependencies.Values.CopyTo($dependencies, 0)
        if ($Development)
        {
            $Script:M365DSCDevDependencies.Values.CopyTo($dependencies, $Script:M365DSCDependencies.Count)
        }

        # $null comparison is correct in that way because the left-hand side is always an array and the right-hand side is a single value
        foreach ($dependency in ($dependencies -ne $null))
        {
            Write-Progress -Activity 'Scanning dependencies' -PercentComplete ($i / $dependencies.Count * 100)
            try
            {
                if (-not $Force)
                {
                    if ($dependency.PowerShellCore -and -not $Script:IsPowerShellCore)
                    {
                        Write-Verbose -Message "The dependency {$($dependency.ModuleName)} requires PowerShell Core. Skipping."
                        continue
                    }
                    elseif ($dependency.PowerShellCore -eq $false -and $Script:IsPowerShellCore -and $IsWindows)
                    {
                        Write-Verbose -Message "The dependency {$($dependency.ModuleName)} requires Windows PowerShell. Skipping."
                        continue
                    }
                    $found = Get-Module $dependency.ModuleName -ListAvailable | Where-Object -Property Version -EQ $dependency.RequiredVersion
                }

                if ((-not $found -or $Force) -and -not $ValidateOnly)
                {
                    $errorFound = $false
                    try
                    {
                        if (($PSEdition -eq 'Desktop' -or $IsWindows) -and (-not(([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) -and ($Scope -eq "AllUsers"))
                        {
                            Write-Error 'Cannot update the dependencies for Microsoft365DSC. You need to run this command as a local administrator.'
                            $errorFound = $true
                        }
                    }
                    catch
                    {
                        Write-Verbose -Message "Couldn't retrieve Windows Principal. One possible cause is that the current environment is not a Windows OS."
                    }
                    if (-not $errorFound)
                    {
                        if (-not $dependency.PowerShellCore -and $Script:IsPowerShellCore -and $IsWindows)
                        {
                            Write-Warning "The dependency {$($dependency.ModuleName)} does not support PowerShell Core. Please run Update-M365DSCDependencies in Windows PowerShell."
                            continue
                        }
                        elseif ($dependency.PowerShellCore -and -not $Script:IsPowerShellCore)
                        {
                            Write-Warning "The dependency {$($dependency.ModuleName)} requires PowerShell Core. Please run Update-M365DSCDependencies in PowerShell Core."
                            continue
                        }

                        Remove-Module $dependency.ModuleName -Force -ErrorAction SilentlyContinue
                        if ($dependency.ModuleName -like 'Microsoft.Graph*')
                        {
                            Remove-Module 'Microsoft.Graph.Authentication' -Force -ErrorAction SilentlyContinue
                        }
                        Remove-Module $dependency.ModuleName -Force -ErrorAction SilentlyContinue

                        if ($scopedIsPsResourceGetAvailable -and -not $UsePowerShellGet)
                        {
                            Write-Information -MessageData "Using Install-PSResource to install $($dependency.ModuleName) with version {$($dependency.RequiredVersion)}"
                            Install-PSResource -Name $dependency.ModuleName -Version $dependency.RequiredVersion -Scope $Scope -AcceptLicense -SkipDependencyCheck -TrustRepository -Repository $Repository
                        }
                        else
                        {
                            Write-Information -MessageData "Using Install-Module to install $($dependency.ModuleName) with version {$($dependency.RequiredVersion)}"
                            Install-Module $dependency.ModuleName -RequiredVersion $dependency.RequiredVersion -AllowClobber -Force -Scope "$Scope" @Params -Repository $Repository
                        }
                    }
                }

                if ($dependency.ExplicitLoading)
                {
                    Remove-Module $dependency.ModuleName -Force -ErrorAction SilentlyContinue
                    if ($dependency.Prefix)
                    {
                        Import-Module $dependency.ModuleName -Global -Prefix $dependency.Prefix -Force -DisableNameChecking
                    }
                    else
                    {
                        Import-Module $dependency.ModuleName -Global -Force -Alias @() -Cmdlet @() -Variable @() -DisableNameChecking
                    }
                }

                if (-not $found -and $validateOnly)
                {
                    $returnValue += $dependency
                }
            }
            catch
            {
                Write-Error -Message "Could not update or import {$($dependency.ModuleName)}: $($_.Exception.Message)" -ErrorAction Continue
            }

            $i++
        }

        # The progress bar seems to hang sometimes. Make sure it is no longer displayed.
        Write-Progress -Activity 'Scanning dependencies' -Completed

        if ($ValidateOnly)
        {
            return $returnValue
        }
    }
    catch
    {
        New-M365DSCLogEntry -Message 'Error updating dependencies:' `
            -Exception $_ `
            -Source $($MyInvocation.MyCommand.Source)
        Write-Error $_ -ErrorAction Continue
    }
}

<#
.SYNOPSIS
    Updates Microsoft365DSC and refreshes dependency state.
 
.DESCRIPTION
    Updates the Microsoft365DSC module, reloads the latest installed version, updates dependencies, and optionally removes outdated versions.
 
.PARAMETER Scope
    Specifies the installation scope used for update operations.
 
.PARAMETER Proxy
    Specifies the proxy server used for update operations.
 
.PARAMETER BaseRepository
    Specifies the repository used to update the Microsoft365DSC module.
 
.PARAMETER DependencyRepository
    Specifies the repository used to update dependencies.
 
.PARAMETER NoUninstall
    Indicates that outdated module and dependency versions should not be removed.
 
.EXAMPLE
    PS> Update-M365DSCModule
 
.EXAMPLE
    PS> Update-M365DSCModule -Scope CurrentUser
 
.EXAMPLE
    PS> Update-M365DSCModule -Scope AllUsers
 
.FUNCTIONALITY
    Public
#>

function Update-M365DSCModule
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet("CurrentUser", "AllUsers")]
        $Scope = "AllUsers",

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

        [Parameter()]
        [System.String]
        $BaseRepository = 'PSGallery',

        [Parameter()]
        [System.String]
        $DependencyRepository = 'PSGallery',

        [Parameter()]
        [switch]
        $NoUninstall
    )

    $params = @{}
    $unloadModule = $true

    if (-not [System.String]::IsNullOrEmpty($proxy))
    {
        $params.Add('Proxy', $Proxy)
    }
    try
    {
        Update-Module -Name 'Microsoft365DSC' @Params -ErrorAction Stop
    }
    catch
    {
        if ($_.Exception.Message -like "*Module 'Microsoft365DSC' was not installed by using Install-Module*")
        {
            Write-Verbose -Message "The Microsoft365DSC module might have been installed with Install-PSResource"
            if ($Script:IsPsResourceGetAvailable)
            {
                Write-Verbose -Message "Updating the Microsoft365DSC module using Update-PSResource..."
                try
                {
                    Update-PSResource -Name 'Microsoft365DSC' -Scope $Scope `
                        -TrustRepository -AcceptLicense -SkipDependencyCheck `
                        -Repository $BaseRepository -ErrorAction Stop
                }
                catch
                {
                    if ($_.Exception.Message -like "*No installed packages*")
                    {
                        Write-Verbose -Message "Microsoft365DSC was neither installed using Install-Module nor Install-PSResource. Trying to install it now..."
                        Install-PSResource -Name 'Microsoft365DSC' -Scope $Scope `
                            -TrustRepository -AcceptLicense -SkipDependencyCheck `
                            -Repository $BaseRepository -ErrorAction Stop
                    }
                    else
                    {
                        New-M365DSCLogEntry -Message 'Error Updating Module:' `
                            -Exception $_ `
                            -Source $($MyInvocation.MyCommand.Source)
                        throw
                    }
                }
            }
        }
        elseif ($_.Exception.Message -like "*was not updated because no valid module was found*")
        {
            Write-Verbose -Message "No valid module was found to update."
            $unloadModule = $false
        }
    }
    try
    {
        if ($unloadModule)
        {
            Write-Verbose -Message "Unloading all instances of the Microsoft365DSC module from the current PowerShell session."
            Remove-Module Microsoft365DSC -Force
        }

        Write-Verbose -Message "Retrieving all versions of the Microsoft365DSC installed on the machine."
        [Array]$instances = Get-Module Microsoft365DSC -ListAvailable | Sort-Object -Property Version -Descending
        if ($instances.Length -gt 0)
        {
            Write-Verbose -Message "Loading version {$($instances[0].Version.ToString())} of the Microsoft365DSC module from {$($instances[0].ModuleBase)}"
            Import-Module Microsoft365DSC -RequiredVersion $instances[0].Version.ToString() -Force
        }
    }
    catch
    {
        New-M365DSCLogEntry -Message 'Error Updating Module:' `
            -Exception $_ `
            -Source $($MyInvocation.MyCommand.Source)
        throw $_
    }

    Update-M365DSCDependencies -Scope $Scope -Proxy $Proxy -Repository $DependencyRepository

    if (-not $NoUninstall)
    {
        Uninstall-M365DSCOutdatedDependencies
    }
}

Export-ModuleMember -Function @(
    'Confirm-M365DSCDependencies',
    'Confirm-M365DSCLoadedModule',
    'Confirm-M365DSCModuleDependency',
    'Get-M365DSCModuleConfiguration',
    'Get-M365DSCRequiredModules',
    'Get-M365DSCResourceSettings',
    'Set-M365DSCModuleConfiguration',
    'Set-M365DSCRequiredModulesLoaded',
    'Test-IsM365DSCRequiredModulesLoaded',
    'Test-M365DSCDependenciesForNewVersions',
    'Test-M365DSCModuleValidity',
    'Uninstall-M365DSCOutdatedDependencies',
    'Update-M365DSCDependencies',
    'Update-M365DSCModule'
)