From 7b82711ec70613af39b4e1fe244e368cd278caf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0t=C4=9Bp=C3=A1n?= Date: Mon, 27 Jul 2026 21:31:07 +0200 Subject: [PATCH] Add functions to check and repair Microsoft.Graph module environment for compatibility --- Get-UserMfaMethodReport.ps1 | 209 +++++++++++++++++++++++++++++++++--- 1 file changed, 196 insertions(+), 13 deletions(-) diff --git a/Get-UserMfaMethodReport.ps1 b/Get-UserMfaMethodReport.ps1 index 2726a6e..3c2f424 100644 --- a/Get-UserMfaMethodReport.ps1 +++ b/Get-UserMfaMethodReport.ps1 @@ -151,6 +151,172 @@ function Test-PremiumAuthReportLicense { return $false } +function Test-GraphModuleEnvironment { + <# + .SYNOPSIS + Checks the Microsoft.Graph modules this script depends on for the + most common cause of hard-to-diagnose runtime failures: multiple + mismatched versions of the same module installed side by side. + .DESCRIPTION + When several versions of a Microsoft.Graph module are installed at + once, PowerShell can end up loading one version's assembly into the + session and then, mid-script, trying to auto-load a different + module that depends on a different version - which fails with + "assembly already loaded" or "could not load file or assembly" + errors. This surfaces as scattered per-user warnings deep into a + run rather than one clear message, so it's worth catching upfront. + .PARAMETER RequiredModuleNames + Names of the Microsoft.Graph modules this script depends on. + .OUTPUTS + [bool] $true if the environment looks consistent, $false if a + problem was found (details and a fix are written via Write-Warning). + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]] + $RequiredModuleNames + ) + + $requiredModuleNames = $RequiredModuleNames + + # @(...) around each pipeline (not just an [array] cast on the variable) + # is required here: when a pipeline produces zero output, PowerShell + # assigns $null even to an [array]-typed variable, not an empty array - + # only wrapping the pipeline itself in @(...) guarantees a real array. + [array]$installed = @(Get-Module -ListAvailable -Name $requiredModuleNames -ErrorAction SilentlyContinue) + # Piped through Select-Object rather than direct dot-access ($installed.Name): + # member-enumeration on an empty array throws under StrictMode, and + # $installed IS legitimately empty on a machine with none of these + # modules installed at all. + [array]$installedNames = @($installed | Select-Object -ExpandProperty Name) + [array]$missingNames = @($requiredModuleNames | Where-Object { $_ -notin $installedNames }) + [array]$allVersions = @($installed | Select-Object -ExpandProperty Version -Unique) + $hasVersionMismatch = $allVersions.Count -gt 1 + + # Display-only reference: whichever version most of the required + # modules currently have installed. Not the authoritative "correct" + # version (Repair-GraphModuleEnvironment resolves that separately from + # the Beta module's manifest / the gallery) - just enough to flag which + # rows are the odd ones out when printing the table below. + $referenceVersion = $null + if ($installed.Count -gt 0) { + $referenceVersion = ($installed | Group-Object Version | Sort-Object Count -Descending | Select-Object -First 1).Name + } + + Write-Host 'Checking required Microsoft.Graph modules...' -ForegroundColor Cyan + $nameColumnWidth = 48 + foreach ($name in $requiredModuleNames) { + [array]$moduleVersions = @($installed | Where-Object { $_.Name -eq $name } | Select-Object -ExpandProperty Version | Sort-Object -Descending) + + $isOk = $false + if ($moduleVersions.Count -eq 1) { + if ($moduleVersions[0].ToString() -eq $referenceVersion) { + $isOk = $true + } + } + + if ($isOk) { + Write-Host (" [OK] {0,-$nameColumnWidth} {1}" -f $name, $moduleVersions[0]) -ForegroundColor Green + } + else { + $versionDisplay = if ($moduleVersions.Count -gt 0) { $moduleVersions -join ', ' } else { 'not installed' } + Write-Host (" [FAIL] {0,-$nameColumnWidth} {1}" -f $name, $versionDisplay) -ForegroundColor Red + } + } + + if ($missingNames.Count -eq 0 -and -not $hasVersionMismatch) { + return $true + } + + Write-Warning 'Microsoft.Graph module check found a problem (see [FAIL] rows above) likely to cause runtime failures (e.g. "assembly already loaded" errors partway through the report).' + return $false +} + +function Repair-GraphModuleEnvironment { + <# + .SYNOPSIS + Installs missing Microsoft.Graph modules and removes mismatched + versions of the ones this script depends on. + .DESCRIPTION + Determines the single version every required module must share - + the exact version Microsoft.Graph.Beta.Identity.SignIns' own + manifest pins for Microsoft.Graph.Authentication (a RequiredVersion, + not just a minimum), or, if that module isn't installed at all yet, + the latest version currently published on the gallery. For each + required module, it then installs that exact version if missing and + offers to remove any other installed version of it. + + Every install/uninstall changes state outside this repository (and + removing a module version can affect other scripts on the machine + that depend on it), so this supports -WhatIf/-Confirm and prompts + for confirmation by default (ConfirmImpact = 'High'). + .PARAMETER RequiredModuleNames + Names of the Microsoft.Graph modules this script depends on. + #> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]] + $RequiredModuleNames + ) + + $betaModuleName = 'Microsoft.Graph.Beta.Identity.SignIns' + $betaModule = Get-Module -ListAvailable -Name $betaModuleName -ErrorAction SilentlyContinue | + Sort-Object Version -Descending | Select-Object -First 1 + + if ($betaModule) { + $pin = $betaModule.RequiredModules | Where-Object { $_.Name -eq 'Microsoft.Graph.Authentication' } | Select-Object -First 1 + $targetVersion = if ($pin) { $pin.Version } else { $betaModule.Version } + } + else { + try { + Write-Host "Looking up the latest published version of $betaModuleName..." -ForegroundColor Cyan + $targetVersion = (Find-Module -Name $betaModuleName -ErrorAction Stop).Version + } + catch { + Write-Warning "Could not look up the latest version of $betaModuleName from the gallery: $_" + return + } + } + Write-Host "Target Microsoft.Graph module version: $targetVersion" -ForegroundColor Cyan + + foreach ($name in $RequiredModuleNames) { + [array]$copies = @(Get-Module -ListAvailable -Name $name -ErrorAction SilentlyContinue) + $hasTarget = $copies | Where-Object { $_.Version -eq $targetVersion } + + if (-not $hasTarget) { + if ($PSCmdlet.ShouldProcess("$name $targetVersion", 'Install-Module')) { + try { + Install-Module -Name $name -RequiredVersion $targetVersion -Scope CurrentUser -Force -Confirm:$false -AllowClobber -ErrorAction Stop + Write-Host "Installed $name $targetVersion" -ForegroundColor Green + } + catch { + Write-Warning "Could not install $name ${targetVersion}: $_" + } + } + else { + Write-Warning "$name $targetVersion was not installed - the script cannot continue without it." + } + } + + [array]$stale = @($copies | Where-Object { $_.Version -ne $targetVersion }) + foreach ($old in $stale) { + if ($PSCmdlet.ShouldProcess("$name $($old.Version)", 'Uninstall-Module')) { + try { + Uninstall-Module -Name $name -RequiredVersion $old.Version -Force -Confirm:$false -ErrorAction Stop + Write-Host "Removed $name $($old.Version)" -ForegroundColor Green + } + catch { + Write-Warning "Could not remove $name $($old.Version) - it may be installed system-wide (Scope AllUsers) and need an elevated PowerShell session to remove: $_" + } + } + } + } +} + function ConvertTo-FriendlyAuthMethod { <# .SYNOPSIS @@ -410,23 +576,34 @@ $($methodRows -join "`n") # --- Main -------------------------------------------------------------- try { - # Fail fast and loudly if a module this script depends on isn't - # installed OR can't actually be loaded (e.g. an assembly-version - # conflict with another installed Microsoft.Graph module version), - # rather than letting every per-user call into it fail silently later - # and produce an incomplete/misleading report. Microsoft.Graph.Beta. - # Identity.SignIns provides Get-MgBetaUserAuthenticationSignInPreference, - # which reads each user's actual default sign-in method - there is no - # v1.0/GA equivalent, this data is beta-only in Microsoft Graph. - $requiredBetaModule = 'Microsoft.Graph.Beta.Identity.SignIns' - if (-not (Get-Module -ListAvailable -Name $requiredBetaModule)) { - throw "Required module '$requiredBetaModule' is not installed. It's needed to read each user's actual default sign-in method. Install it with: Install-Module $requiredBetaModule -Scope CurrentUser" + # Fail fast and loudly if the Microsoft.Graph modules this script + # depends on aren't installed OR can't actually be loaded (e.g. an + # assembly-version conflict between mismatched module versions), + # rather than letting every per-user call into them fail silently later + # and produce an incomplete/misleading report. If a problem is found, + # offer to fix it (install what's missing, remove mismatched versions) + # rather than just describing the fix and stopping. + $requiredGraphModuleNames = @( + 'Microsoft.Graph.Authentication' + 'Microsoft.Graph.Beta.Identity.SignIns' + 'Microsoft.Graph.Identity.DirectoryManagement' + 'Microsoft.Graph.Identity.SignIns' + 'Microsoft.Graph.Reports' + 'Microsoft.Graph.Users' + ) + if (-not (Test-GraphModuleEnvironment -RequiredModuleNames $requiredGraphModuleNames)) { + Repair-GraphModuleEnvironment -RequiredModuleNames $requiredGraphModuleNames + if (-not (Test-GraphModuleEnvironment -RequiredModuleNames $requiredGraphModuleNames)) { + throw 'Microsoft.Graph module environment still has a problem after the repair attempt - see the warnings above. Close this terminal window, open a new one, and try again; if it still fails, some of the old versions may be installed system-wide (Scope AllUsers) and need to be removed from an elevated PowerShell session.' + } + Write-Host 'Microsoft.Graph module environment repaired.' -ForegroundColor Green } + $requiredBetaModule = 'Microsoft.Graph.Beta.Identity.SignIns' try { Import-Module -Name $requiredBetaModule -ErrorAction Stop } catch { - throw "Required module '$requiredBetaModule' is installed but failed to load - this is usually caused by multiple mismatched Microsoft.Graph module versions installed side by side (an assembly-version conflict), not a missing module. Run: Get-Module -ListAvailable 'Microsoft.Graph*' | Group-Object Name | Where-Object { (`$_.Group.Version | Sort-Object -Unique).Count -gt 1 } to find the mismatched ones, update them all to the same version, and remove the old ones. Underlying error: $_" + throw "Required module '$requiredBetaModule' failed to load even though the version check passed - see the underlying error for details: $_" } Write-Verbose 'Connecting to Microsoft Graph...' @@ -624,7 +801,13 @@ catch { exit 1 } finally { - Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null + # -ErrorAction SilentlyContinue does not suppress "command not found" - + # that's a terminating parse/discovery-time error, not a stream event - + # so guard explicitly for the case where the script failed before + # Microsoft.Graph.Authentication was ever loaded (e.g. missing modules). + if (Get-Command -Name Disconnect-MgGraph -ErrorAction SilentlyContinue) { + Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null + } } exit 0