________  ________  ________  ________  ________  ________  ________ 
 ╱        ╲╱        ╲╱    ╱   ╲╱        ╲╱    ╱   ╲╱        ╲╱    ╱   ╲
╱        _╱    ╱    ╱         ╱         ╱         ╱    ╱    ╱         ╱
╱       ╱╱        _╱         ╱       --╱╲__      ╱         ╱         ╱ 
╲_____╱╱ ╲____╱___╱╲________╱╲________╱   ╲_____╱╲___╱____╱╲__╱_____╱  

Script – Powershell Prep for Azure & Graph

The following script prepares the environment with the latest Graph API and relevant Az.* modules. System level modules will be removed to prevent conflicts and to contain versioning within the user profile. This script should be run within a fresh PS session and contains $requiredModules fit for managing and deploying Azure Virtual Desktop resources.

Note: Module “ActiveDirectory” is provided through RSAT which can be obtained from Microsoft on Windows 10 or enabled through Apps & Features on Windows 11.

# Clear output
Clear-Host

# Color formatting
function formatInfo { process { Write-Host $_ -ForegroundColor cyan } }
function formatWarning { process { Write-Host $_ -ForegroundColor yellow } }
function formatSuccess { process { Write-Host $_ -ForegroundColor green } }
function formatError { process { Write-Host $_ -ForegroundColor red } }
function formatPrompt { process { Write-Host $_ -ForegroundColor gray } }
function formatBreak { process { Write-Host $_ -ForegroundColor white -BackgroundColor black } }

# Timestamp formatting
filter tsInfo {"[$(Get-Date -Format 'MM/dd/yyyy HH:mm:ss')] [?] $_" | formatInfo}
filter tsSuccess {"[$(Get-Date -Format 'MM/dd/yyyy HH:mm:ss')] [+] $_" | formatSuccess}
filter tsWarning {"[$(Get-Date -Format 'MM/dd/yyyy HH:mm:ss')] [#] $_" | formatWarning}
filter tsError {"[$(Get-Date -Format 'MM/dd/yyyy HH:mm:ss')] [-] $_" | formatError}
filter tsPrompt {"[$(Get-Date -Format 'MM/dd/yyyy HH:mm:ss')] [!] $_" | formatPrompt}

# Check context
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    Write-Output "You are running PowerShell as Local Administrator. Please close and run as your Named Account!" | formatError
    Write-Output "----------------------------------------------------------------------------------------------"
    Write-Output ""
    read-host Press ENTER to continue...
    $host.Exit()
}

# MOTD
Write-Output "#####################################" | formatBreak
Write-Output "## Az / Graph Module Update Script ##" | formatBreak
Write-Output "##---------------------------------##" | formatBreak
Write-Output "##         ~ Version 1.0 ~         ##" | formatBreak
Write-Output "#####################################`n" | formatBreak

# Change the execution policy
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser -Force

# Suppress environment warnings
$ProgressPreference = "SilentlyContinue"
$WarningPreference = 'SilentlyContinue'
Set-Item -Path Env:\SuppressAzurePowerShellBreakingChangeWarnings -Value $true
Set-Item -Path Env:\SuppressAzureRmModulesRetiringWarning -Value $true

# Fix PS 5.1 Limitation
$maximumfunctioncount = '32768'

##########################
## Session Dependencies ##
##########################

Write-Output "Verifying Installed Modules" | tsInfo

# Install Provider for package lookup
$currentProviders = get-packageprovider
$requiredProviders = @('NuGet')
foreach ($provider in $requiredProviders) {
    if ($provider -notin $currentProviders.Name) {
        Install-PackageProvider -Name $provider -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force | Out-Null
        Write-Output "Package provider $($provider) installed" | tsSuccess
    } else {
        Write-Output "Package provider $($provider) exists" | tsSuccess
    }
}

# Install Modules
Import-Module 'ActiveDirectory' -DisableNameChecking -ErrorAction Stop -Force 3>$null
Write-Output "Module ActiveDirectory exists" | tsSuccess

$installedModules = Get-Module -ListAvailable
$currentModules = Get-Module

# Delete conflicting system level modules
# -- All modules should be installed at the currentUser scope
if (Get-ChildItem 'C:\Program Files\WindowsPowerShell\Modules\' | Where-Object { ($_.PSIsContainer) -and (($_.Name -like "Microsoft.Graph*") -or ($_.Name -like "Az.*"))})
{
    Start-Process powershell.exe -Verb Runas -ArgumentList "-Command Remove-Item 'C:\Program Files\WindowsPowerShell\Modules\Az.*' -Force -Recurse; Remove-Item 'C:\Program Files\WindowsPowerShell\Modules\Microsoft.Graph.*' -Force -Recurse" -WindowStyle hidden -Wait
    Write-Output "Removed Module Az.* and Microsoft.Graph* Forcefully" | tsPrompt
}

[Version]$latestGraphVersion = (Find-Module -Name 'Microsoft.Graph' | Sort-Object Version -Descending  | Select-Object Version -First 1).Version

# If an older version of Graph Authentication exists, ALL other graph modules must be removed before the Auth module can be removed (Regardless of versions)
if (Get-Module Microsoft.Graph.Authentication -ListAvailable | Where-Object {($_.Version -ne $latestGraphVersion)}) 
{
    $Modules = Get-Module Microsoft.Graph* -ListAvailable | Where {$_.Name -ne "Microsoft.Graph.Authentication"} | Select-Object Name -Unique
    Foreach ($Module in $Modules)
    {
        $ModuleName = $Module.Name
        $Versions = Get-Module $ModuleName -ListAvailable
        Foreach ($Version in $Versions)
        {
            $ModuleVersion = $Version.Version
            Uninstall-Module $ModuleName -RequiredVersion $ModuleVersion
            Write-Output "Removed Module ${ModuleName} [${ModuleVersion}]" | tsPrompt
        }
    }

    # Sanity Check Manual Deletion
    $psPath = "$($env:USERPROFILE)\Documents\WindowsPowerShell\Modules"
    $modules = Get-ChildItem $psPath | Where-Object { ($_.PSIsContainer) -and ($_.Name -like "Microsoft.Graph*") -and ($_.Name -ne "Microsoft.Graph.Authentication")}
    Foreach ($module in $modules)
    {
        $module | Remove-Item
    }

    # Remove Graph Authentication Module
    $ModuleName = "Microsoft.Graph.Authentication"
    $Versions = Get-Module $ModuleName -ListAvailable
    Foreach ($Version in $Versions)
    {
        $ModuleVersion = $Version.Version
        Uninstall-Module $ModuleName -RequiredVersion $ModuleVersion
        Write-Output "Removed Module ${ModuleName} [${ModuleVersion}]" | tsPrompt
    }
}

# Remove legacy Azure modules
$legacyAzModules = @('AzFilesHybrid','AzureAD','AzureADPreview')

if (Compare-Object -Referenceobject $installedModules -DifferenceObject $legacyAzModules -IncludeEqual | Where-Object{$_.sideIndicator -eq "=="}) {
    Foreach ($legacyAzModule in $legacyAzModules) {
        if ($installedModules.Name -contains $legacyAzModule) {
            if ($legacyAzModule -eq "AzFilesHybrid") {
                Remove-Item "$($env:USERPROFILE)\Documents\WindowsPowerShell\Modules\AzFilesHybrid" -Force -Recurse | Out-Null
                Write-Output "Removed Module ${legacyAzModule} Forcefully" | tsPrompt
            } else {
                $versions = Get-Module $legacyAzModule -ListAvailable
                Foreach ($version in $versions)
                {
                    $moduleVersion = $version.Version
                    Uninstall-Module $legacyAzModule -RequiredVersion $moduleVersion
                    Write-Output "Removed Module ${legacyAzModule} [${moduleVersion}]" | tsPrompt
                }
            }
        }
    }
}

# Remove all Az.* modules that do not match the latest version
$requiredModules = @('Az.Accounts','Az.Billing','Az.DesktopVirtualization','Az.Network','Az.Resources','Az.Storage','Az.Subscription','Az.PrivateDns')

Foreach ($requiredModule in $requiredModules) {
    [Version]$latestVersion = (Find-Module -Name $requiredModule | Sort-Object Version -Descending  | Select-Object Version -First 1).Version
    $staleVersions = Get-Module $requiredModule -ListAvailable | where {$_.Version -lt $latestVersion}

    if ($staleVersions) {
        Foreach ($staleVersion in $staleVersions) {
            $version = $staleVersion.Version
            Uninstall-Module $requiredModule -RequiredVersion $version
            Write-Output "Removed Module ${requiredModule} [${version}]" | tsPrompt
        }
    }
}

# Populate required list with all available graph modules
$graphModules = Find-Module -Name 'Microsoft.Graph*' | where {($_.Version -eq $latestGraphVersion) -and ($_.Name -ne "Microsoft.Graph") -and ($_.Name -notmatch "Microsoft.Graph.Beta*")}
Foreach ($graphObj in $graphModules) {
    $requiredModules += $graphObj.Name
}

# Sort alphabetically
$requiredModules = $requiredModules | Sort-Object

#Refresh installed module list
$installedModules = Get-Module -ListAvailable

foreach ($module in $requiredModules) {
    if ($module -notin $installedModules.Name) {
        Install-Module $module -Scope CurrentUser -Repository PSGallery -AllowClobber -Force 3>$null
        Import-Module $module -DisableNameChecking 3>$null
        $version = (Get-Module $module).Version
        Write-Output "Installed Module ${module} [${version}]" | tsSuccess
    }
}

# Set Config -- This must be done last to avoid importing stale modules prior to updates
Update-AzConfig -DisplaySurveyMessage $false | Out-Null
Update-AzConfig -DisplayBreakingChangeWarning $false | Out-Null
Update-AzConfig -DisableInstanceDiscovery $true | Out-Null
Update-AzConfig -EnableLoginByWam $false | Out-Null
Update-AzConfig -LoginExperienceV2 Off | Out-Null