Orchestrator/Test-ModuleCompatibility.ps1
|
function Get-CompatibleExoModule { <# .SYNOPSIS Returns the newest installed ExchangeOnlineManagement version that is compatible with the Graph SDK in the same session, or $null. .DESCRIPTION EXO 3.8.0+ bundles an MSAL (Microsoft.Identity.Client) that conflicts with Graph SDK 2.x when both load in one PowerShell session — tracked upstream (msgraph-sdk-powershell#3576, still unfixed as of EXO 3.10.0) and locally as #231. Versions below 3.8.0 can be installed side-by-side with newer ones; this helper picks the newest compatible install so the connector can pin its import instead of forcing an uninstall. .EXAMPLE $exo = Get-CompatibleExoModule if ($exo) { Import-Module ExchangeOnlineManagement -RequiredVersion $exo.Version } #> [CmdletBinding()] [OutputType([object])] param() Get-Module -Name ExchangeOnlineManagement -ListAvailable -ErrorAction SilentlyContinue | Where-Object { $_.Version -lt [version]'3.8.0' } | Sort-Object -Property Version -Descending | Select-Object -First 1 } function Test-ModuleCompatibility { [CmdletBinding()] param( [string[]]$Section, [hashtable]$SectionServiceMap, [switch]$NonInteractive, [switch]$SkipDLP ) $repairActions = [System.Collections.Generic.List[PSCustomObject]]::new() # Determine which modules the selected sections actually require (BEFORE checking modules) $needsGraph = $false $needsExo = $false $needsPowerBI = $false foreach ($s in $Section) { $svcList = $sectionServiceMap[$s] if ($svcList -contains 'Graph') { $needsGraph = $true } if ($svcList -contains 'ExchangeOnline' -or (-not $SkipDLP -and $svcList -contains 'Purview')) { $needsExo = $true } if ($s -eq 'PowerBI') { $needsPowerBI = $true } } # Detect installed module versions $exoModule = Get-Module -Name ExchangeOnlineManagement -ListAvailable -ErrorAction SilentlyContinue | Sort-Object -Property Version -Descending | Select-Object -First 1 $exoCompatible = Get-CompatibleExoModule $graphModule = Get-Module -Name Microsoft.Graph.Authentication -ListAvailable -ErrorAction SilentlyContinue | Sort-Object -Property Version -Descending | Select-Object -First 1 # EXO 3.8.0+ MSAL conflict (only if EXO is needed). A compatible (< 3.8.0) # version installed side-by-side satisfies the requirement: the connector # pins its import to it, and newer versions stay installed for other # tooling (#231). Only when NO compatible version exists do we ask for a # side-by-side 3.7.1 install — never an uninstall. if ($needsExo -and $exoModule -and $exoModule.Version -ge [version]'3.8.0') { if ($exoCompatible) { Write-AssessmentLog -Level INFO -Message "ExchangeOnlineManagement $($exoModule.Version) is MSAL-conflicting; session pins $($exoCompatible.Version) installed side-by-side" -Section 'Setup' Write-Host " i ExchangeOnlineManagement $($exoCompatible.Version) will be used this session ($($exoModule.Version) stays installed for other tooling)" -ForegroundColor DarkGray } else { $repairActions.Add([PSCustomObject]@{ Module = 'ExchangeOnlineManagement' Issue = "Version $($exoModule.Version) has MSAL conflicts (need <= 3.7.1 installed side-by-side)" Severity = 'Required' Tier = 'Downgrade' RequiredVersion = '3.7.1' InstallCmd = 'Install-Module ExchangeOnlineManagement -RequiredVersion 3.7.1 -Scope CurrentUser -Force' Description = "ExchangeOnlineManagement $($exoModule.Version) ΓÇö MSAL conflict (3.7.1 will be installed side-by-side)" }) # msalruntime.dll ΓÇö Windows only, EXO 3.8.0+ (only relevant while a # conflicting version is the sole install) if ($IsWindows -or $null -eq $IsWindows) { $exoNetCorePath = Join-Path -Path $exoModule.ModuleBase -ChildPath 'netCore' $msalDllDirect = Join-Path -Path $exoNetCorePath -ChildPath 'msalruntime.dll' $msalDllNested = Join-Path -Path $exoNetCorePath -ChildPath 'runtimes\win-x64\native\msalruntime.dll' if (-not (Test-Path -Path $msalDllDirect) -and (Test-Path -Path $msalDllNested)) { $repairActions.Add([PSCustomObject]@{ Module = 'ExchangeOnlineManagement' Issue = 'msalruntime.dll missing from load path' Severity = 'Required' Tier = 'FileCopy' RequiredVersion = $null InstallCmd = "Copy-Item '$msalDllNested' '$msalDllDirect'" Description = 'msalruntime.dll ΓÇö missing from EXO module load path' SourcePath = $msalDllNested DestPath = $msalDllDirect }) } } } } # Required modules ΓÇö fatal if missing if ($needsGraph -and -not $graphModule) { $repairActions.Add([PSCustomObject]@{ Module = 'Microsoft.Graph.Authentication' Issue = 'Not installed' Severity = 'Required' Tier = 'Install' RequiredVersion = $null InstallCmd = 'Install-Module -Name Microsoft.Graph.Authentication -Scope CurrentUser -Force' Description = 'Microsoft.Graph.Authentication ΓÇö not installed' }) } if ($needsExo -and -not $exoModule) { $repairActions.Add([PSCustomObject]@{ Module = 'ExchangeOnlineManagement' Issue = 'Not installed' Severity = 'Required' Tier = 'Install' RequiredVersion = '3.7.1' InstallCmd = 'Install-Module -Name ExchangeOnlineManagement -RequiredVersion 3.7.1 -Scope CurrentUser -Force' Description = 'ExchangeOnlineManagement ΓÇö not installed' }) } # Recommended modules -- core assessment features, default-install if ($needsPowerBI -and -not (Get-Module -Name MicrosoftPowerBIMgmt -ListAvailable -ErrorAction SilentlyContinue)) { $repairActions.Add([PSCustomObject]@{ Module = 'MicrosoftPowerBIMgmt' Issue = 'Not installed' Severity = 'Recommended' Tier = 'Install' RequiredVersion = $null InstallCmd = 'Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser -Force' Description = 'MicrosoftPowerBIMgmt -- enables Power BI security checks' }) } # ImportExcel -- needed for XLSX compliance matrix export if (-not (Get-Module -Name ImportExcel -ListAvailable -ErrorAction SilentlyContinue)) { $repairActions.Add([PSCustomObject]@{ Module = 'ImportExcel' Issue = 'Not installed' Severity = 'Recommended' Tier = 'Install' RequiredVersion = $null InstallCmd = 'Install-Module -Name ImportExcel -Scope CurrentUser -Force' Description = 'ImportExcel -- enables XLSX compliance matrix export' }) } # --- No issues? Continue --- if ($repairActions.Count -eq 0) { Write-AssessmentLog -Level INFO -Message 'Module compatibility check passed' -Section 'Setup' } else { # --- Present summary --- Write-Host '' Write-Host ' ΓòöΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòù' -ForegroundColor Magenta Write-Host ' Γòæ Module Issues Detected Γòæ' -ForegroundColor Magenta Write-Host ' ΓòÜΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓò¥' -ForegroundColor Magenta foreach ($action in $repairActions) { if ($action.Severity -eq 'Required') { Write-Host " Γ£ù $($action.Description)" -ForegroundColor Red } else { Write-Host " ΓÜá $($action.Description)" -ForegroundColor Yellow } } Write-Host '' $requiredIssues = @($repairActions | Where-Object { $_.Severity -eq 'Required' }) $recommendedIssues = @($repairActions | Where-Object { $_.Severity -eq 'Recommended' }) if ($NonInteractive -or -not [Environment]::UserInteractive) { # --- Headless: log and exit/skip --- if ($requiredIssues.Count -gt 0) { foreach ($action in $requiredIssues) { Write-AssessmentLog -Level ERROR -Message "Module issue: $($action.Description). Fix: $($action.InstallCmd)" } Write-Host ' Known compatible combo: Graph SDK 2.35.x + EXO 3.7.1' -ForegroundColor DarkGray Write-Host '' Write-Error "Required modules are missing or incompatible. See assessment log for install commands." return } # Auto-install recommended modules in NonInteractive mode foreach ($action in $recommendedIssues) { try { Write-Host " Installing $($action.Module)..." -ForegroundColor Cyan $installParams = @{ Name = $action.Module Scope = 'CurrentUser' Force = $true ErrorAction = 'Stop' } if ($action.RequiredVersion) { $installParams['RequiredVersion'] = $action.RequiredVersion } Install-Module @installParams Write-AssessmentLog -Level INFO -Message "Auto-installed recommended module: $($action.Module)" Write-Host " $([char]0x2714) $($action.Module) installed" -ForegroundColor Green } catch { Write-AssessmentLog -Level WARN -Message "Failed to auto-install $($action.Module): $_" if ($action.Module -eq 'MicrosoftPowerBIMgmt') { $Section = @($Section | Where-Object { $_ -ne 'PowerBI' }) } } } } else { # --- Interactive: offer repairs --- $failedRepairs = [System.Collections.Generic.List[PSCustomObject]]::new() # Step 1: Auto-fix FileCopy (no prompt) $fileCopyActions = @($repairActions | Where-Object { $_.Tier -eq 'FileCopy' }) foreach ($action in $fileCopyActions) { try { Copy-Item -Path $action.SourcePath -Destination $action.DestPath -Force -ErrorAction Stop Write-Host " Γ£ô Copied msalruntime.dll to EXO module load path" -ForegroundColor Green } catch { Write-Host " Γ£ù msalruntime.dll copy failed: $_" -ForegroundColor Red $failedRepairs.Add($action) } } # Step 2: Tier 1 ΓÇö Install missing modules $installActions = @($repairActions | Where-Object { $_.Tier -eq 'Install' -and $_.Severity -eq 'Required' }) if ($installActions.Count -gt 0) { $response = Read-Host ' Install missing modules to CurrentUser scope? [Y/n]' if ($response -match '^[Yy]?$') { foreach ($action in $installActions) { try { Write-Host " Installing $($action.Module)..." -ForegroundColor Cyan $installParams = @{ Name = $action.Module Scope = 'CurrentUser' Force = $true ErrorAction = 'Stop' } if ($action.RequiredVersion) { $installParams['RequiredVersion'] = $action.RequiredVersion } Install-Module @installParams Write-Host " Γ£ô $($action.Module) installed" -ForegroundColor Green } catch { Write-Host " Γ£ù $($action.Module) failed: $_" -ForegroundColor Red $failedRepairs.Add($action) } } } } # Step 3: Tier 2 ΓÇö EXO compatible-version install (separate confirmation). # Side-by-side: installs 3.7.1 WITHOUT uninstalling newer versions, so # other tooling that needs EXO 3.8+ keeps working (#231). $downgradeActions = @($repairActions | Where-Object { $_.Tier -eq 'Downgrade' }) foreach ($action in $downgradeActions) { Write-Host '' Write-Host " ΓÜá $($action.Module) $($action.Issue)" -ForegroundColor Yellow Write-Host " This installs $($action.RequiredVersion) side-by-side; newer versions stay installed." -ForegroundColor Yellow $response = Read-Host " Install $($action.Module) $($action.RequiredVersion) alongside? [Y/n]" if ($response -match '^[Yy]?$') { try { Write-Host " Installing $($action.Module) $($action.RequiredVersion)..." -ForegroundColor Cyan Install-Module -Name $action.Module -RequiredVersion $action.RequiredVersion -Scope CurrentUser -Force -ErrorAction Stop Write-Host " Γ£ô $($action.Module) $($action.RequiredVersion) installed (side-by-side)" -ForegroundColor Green } catch { Write-Host " Γ£ù EXO $($action.RequiredVersion) install failed: $_" -ForegroundColor Red $failedRepairs.Add($action) } } } # Recommended modules -- prompt individually with [Y/n] default $recInstallActions = @($repairActions | Where-Object { $_.Tier -eq 'Install' -and $_.Severity -eq 'Recommended' }) if ($recInstallActions.Count -gt 0) { $skippedNames = ($recInstallActions | ForEach-Object { $_.Module }) -join ', ' $response = Read-Host " Install recommended modules? ($skippedNames) [Y/n]" if ($response -match '^[Yy]?$') { foreach ($action in $recInstallActions) { try { Write-Host " Installing $($action.Module)..." -ForegroundColor Cyan $installParams = @{ Name = $action.Module Scope = 'CurrentUser' Force = $true ErrorAction = 'Stop' } if ($action.RequiredVersion) { $installParams['RequiredVersion'] = $action.RequiredVersion } Install-Module @installParams Write-Host " Γ£ô $($action.Module) installed" -ForegroundColor Green } catch { Write-Host " Γ£ù $($action.Module) install failed: $_" -ForegroundColor Red } } } else { # User declined -- skip affected sections/features foreach ($action in $recInstallActions) { if ($action.Module -eq 'MicrosoftPowerBIMgmt') { $Section = @($Section | Where-Object { $_ -ne 'PowerBI' }) Write-AssessmentLog -Level WARN -Message "Recommended module declined: $($action.Description). Section skipped." } elseif ($action.Module -eq 'ImportExcel') { Write-AssessmentLog -Level WARN -Message "Recommended module declined: $($action.Description). XLSX export will be skipped." } } } } # Step 4: Re-validate after repairs Write-Host '' Write-Host ' Re-validating module compatibility...' -ForegroundColor Cyan # Re-detect modules $exoModule = Get-Module -Name ExchangeOnlineManagement -ListAvailable -ErrorAction SilentlyContinue | Sort-Object -Property Version -Descending | Select-Object -First 1 $exoCompatible = Get-CompatibleExoModule $graphModule = Get-Module -Name Microsoft.Graph.Authentication -ListAvailable -ErrorAction SilentlyContinue | Sort-Object -Property Version -Descending | Select-Object -First 1 $stillBroken = @() if ($needsGraph -and -not $graphModule) { $stillBroken += 'Install-Module -Name Microsoft.Graph.Authentication -Scope CurrentUser -Force' } if ($needsExo -and -not $exoCompatible) { # Covers both "not installed at all" and "only MSAL-conflicting # versions installed" — the fix is the same side-by-side install. $stillBroken += 'Install-Module -Name ExchangeOnlineManagement -RequiredVersion 3.7.1 -Scope CurrentUser -Force' } # Re-check msalruntime.dll ΓÇö only relevant while a conflicting EXO # version remains the sole install if ($needsExo -and -not $exoCompatible -and $exoModule -and $exoModule.Version -ge [version]'3.8.0' -and ($IsWindows -or $null -eq $IsWindows)) { $exoNetCorePath = Join-Path -Path $exoModule.ModuleBase -ChildPath 'netCore' $msalDllDirect = Join-Path -Path $exoNetCorePath -ChildPath 'msalruntime.dll' $msalDllNested = Join-Path -Path $exoNetCorePath -ChildPath 'runtimes\win-x64\native\msalruntime.dll' if (-not (Test-Path -Path $msalDllDirect) -and (Test-Path -Path $msalDllNested)) { $stillBroken += "Copy-Item '$msalDllNested' '$msalDllDirect'" } } if ($stillBroken.Count -gt 0) { Write-Host '' Write-Host ' ΓòöΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòù' -ForegroundColor Magenta Write-Host ' Γòæ Unable to resolve all module issues Γòæ' -ForegroundColor Magenta Write-Host ' ΓòÜΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓò¥' -ForegroundColor Magenta Write-Host ' Manual steps needed:' -ForegroundColor Red foreach ($cmd in $stillBroken) { Write-Host " ΓÇó $cmd" -ForegroundColor Red } Write-Host '' Write-Host ' Run these commands and try again.' -ForegroundColor DarkGray Write-Host ' Known compatible combo: Graph SDK 2.35.x + EXO 3.7.1' -ForegroundColor DarkGray Write-Host '' Write-AssessmentLog -Level ERROR -Message "Module repair incomplete: $($stillBroken -join '; ')" Write-Error "Required modules are still missing or incompatible. See above for manual steps." return } Write-Host ' Γ£ô All module issues resolved' -ForegroundColor Green # Show installed module versions $versionTable = @() $modChecks = @('Microsoft.Graph.Authentication', 'ExchangeOnlineManagement', 'MicrosoftPowerBIMgmt', 'ImportExcel') foreach ($modName in $modChecks) { # EXO reports the version the session will actually pin, not the # highest installed (newer MSAL-conflicting versions may coexist) $mod = if ($modName -eq 'ExchangeOnlineManagement') { Get-CompatibleExoModule } else { Get-Module -Name $modName -ListAvailable -ErrorAction SilentlyContinue | Sort-Object -Property Version -Descending | Select-Object -First 1 } $versionTable += [PSCustomObject]@{ Module = $modName Version = if ($mod) { $mod.Version.ToString() } else { '(not installed)' } } } $versionTable | Format-Table -AutoSize | Out-String | ForEach-Object { Write-Host $_.TrimEnd() } Write-Host '' } } return @{ Passed = $true; Section = $Section } } |