Skip to content

Conversation

@alexandair
Copy link
Collaborator

  • Updated RequiredModules in module manifest to include ExchangeOnlineManagement and adjust versions.
  • Refactored Invoke-ZtTests to separate sync and parallel tests for improved execution.
  • Expanded Connect-ZtAssessment to support multiple services and environments, including SharePoint Online.
  • Added Initialize-Dependencies.ps1 to manage DLL loading order and prevent conflicts.
  • Introduced Get-ModuleImportOrder function to evaluate module import order based on DLL versions.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the ZeroTrustAssessment module to support multiple Microsoft services and addresses DLL version conflicts between modules. The main objective is to enable connections to Exchange Online, Security & Compliance, and SharePoint Online in addition to the existing Microsoft Graph and Azure support, while preventing Microsoft.Identity.Client.dll conflicts through proper module loading order.

Key Changes:

  • Added comprehensive multi-service connection support (Exchange Online, Security & Compliance, SharePoint Online)
  • Implemented DLL conflict prevention through pre-loading strategy and module import order optimization
  • Separated synchronous and parallel test execution for improved reliability

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
ZeroTrustAssessment.psd1 Reordered RequiredModules to prioritize ExchangeOnlineManagement, downgraded Graph module versions to 2.0.0, and added Initialize-Dependencies.ps1 to ScriptsToProcess
Initialize-Dependencies.ps1 New script that pre-loads ExchangeOnlineManagement assemblies before module import to prevent DLL conflicts
Get-ModuleImportOrder.ps1 New utility function that determines optimal module import order based on Microsoft.Identity.Client.dll versions
Connect-ZtAssessment.ps1 Significantly expanded to support multiple services (Azure, Exchange Online, Security & Compliance, SharePoint Online) with dynamic module loading based on import order
Invoke-ZtTests.ps1 Refactored to separate synchronous tests (Compliance/Exchange) from parallel tests for better execution control

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


if (-not $ExoUPN) {
Write-Host "`nUnable to determine a UserPrincipalName for Security & Compliance. Please supply -UserPrincipalName or connect to Exchange Online first." -ForegroundColor Yellow
continue
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition uses 'continue' which is only valid inside loops. In the context of a switch statement at this level, 'continue' will skip to the next item in the switch, but since the switch is iterating over $OrderedImport.Name, this may not behave as intended. Consider using 'break' to exit the current switch case instead.

Copilot uses AI. Check for mistakes.
Comment on lines 264 to 283
# Get UPN from Exchange connection or Graph context
$ExoUPN = $UserPrincipalName

# Attempt to resolve UPN before any connection to avoid token acquisition failures without identity
$connectionInformation = $null
try {
$connectionInformation = Get-ConnectionInformation
}
catch {
# Intentionally swallow errors here; fall back to provided UPN if any
}

if (-not $ExoUPN) {
$ExoUPN = $connectionInformation | Where-Object { $_.IsEopSession -ne $true -and $_.State -eq 'Connected' } | Select-Object -ExpandProperty UserPrincipalName -First 1 -ErrorAction SilentlyContinue
}

if (-not $ExoUPN) {
Write-Host "`nUnable to determine a UserPrincipalName for Security & Compliance. Please supply -UserPrincipalName or connect to Exchange Online first." -ForegroundColor Yellow
continue
}
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for Security & Compliance requires a UserPrincipalName but attempts to retrieve it from Exchange connection information. If the user specifies 'SecurityCompliance' without 'ExchangeOnline' in the Service parameter, and without providing UserPrincipalName, the code will automatically add 'ExchangeOnline' (line 106). However, if the Exchange connection fails, the Get-ConnectionInformation call may not return valid data. Consider validating that the UserPrincipalName was successfully obtained before attempting the Security & Compliance connection.

Copilot uses AI. Check for mistakes.
$connectionInformation = Get-ConnectionInformation
}
catch {
# Intentionally swallow errors here; fall back to provided UPN if any
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block intentionally swallows all errors with only a comment explaining the intent. While this may be intentional for resilience, it could hide legitimate errors that would help diagnose connection issues. Consider logging these errors at a verbose or debug level so they can be reviewed if needed.

Suggested change
# Intentionally swallow errors here; fall back to provided UPN if any
# Intentionally swallow errors here; fall back to provided UPN if any
$connectionInfoError = $_
Write-Verbose "Get-ConnectionInformation failed; falling back to provided UserPrincipalName if available. Error: $($connectionInfoError.Exception.Message)"

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 87
# Initialize-Dependencies.ps1
# This script is run by the module manifest (ScriptsToProcess) before the module is imported.
# It ensures that dependencies are loaded in the correct order to avoid DLL conflicts.
# Specifically, ExchangeOnlineManagement and Az.Accounts/Graph both use Microsoft.Identity.Client.dll.
# We must ensure the oldest compatible version is loaded first, BEFORE any modules import.

Write-Host "=== Initialize-Dependencies.ps1 Starting ===" -ForegroundColor Cyan

try {
# Check if MSAL is already loaded
$loadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Microsoft.Identity.Client' }

if ($loadedAssemblies) {
Write-Host "MSAL assembly is already loaded in the current session:" -ForegroundColor Yellow
foreach ($asm in $loadedAssemblies) {
$loadedVersion = $asm.GetName().Version
Write-Host " Version $loadedVersion from $($asm.Location)" -ForegroundColor Yellow
}
Write-Host "" -ForegroundColor Yellow
Write-Host "This will cause DLL conflicts. To fix:" -ForegroundColor Red
Write-Host " 1. Close this PowerShell session" -ForegroundColor Cyan
Write-Host " 2. Open a NEW PowerShell session" -ForegroundColor Cyan
Write-Host " 3. Import ZeroTrustAssessment FIRST before any other modules" -ForegroundColor Cyan
Write-Host "" -ForegroundColor Yellow
} else {
Write-Host "MSAL not yet loaded - proceeding with pre-load" -ForegroundColor Green

# Dot-source the helper function to determine proper module import order
$helperPath = "$PSScriptRoot\private\utility\Get-ModuleImportOrder.ps1"
Write-Host "Loading helper from: $helperPath" -ForegroundColor Gray

if (-not (Test-Path $helperPath)) {
Write-Error "Cannot find Get-ModuleImportOrder.ps1 at $helperPath"
return
}

. $helperPath

# Define dependencies and their required versions
$dependencies = @('Az.Accounts', 'ExchangeOnlineManagement', 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Beta.Teams')
$requiredVersions = @{
'ExchangeOnlineManagement' = '3.8.0'
}

# CRITICAL: ExchangeOnlineManagement must be loaded FIRST due to its specific Broker DLL requirements
# Even if other modules have newer MSAL versions, Exchange needs its specific Broker extensions
Write-Host "Looking for ExchangeOnlineManagement module..." -ForegroundColor Gray
$exoModule = Get-ModuleImportOrder -Name 'ExchangeOnlineManagement' -RequiredVersions $requiredVersions

if ($exoModule) {
Write-Host "Loading MSAL and Broker from ExchangeOnlineManagement v$($exoModule.ModuleVersion)" -ForegroundColor Green

if ($exoModule.DLLPath -and (Test-Path $exoModule.DLLPath)) {
Write-Host "Explicitly loading MSAL assembly from: $($exoModule.DLLPath)" -ForegroundColor Cyan
$loadedAsm = [System.Reflection.Assembly]::LoadFrom($exoModule.DLLPath)
Write-Host "Successfully loaded MSAL DLL version $($loadedAsm.GetName().Version)" -ForegroundColor Green

# Load ALL Microsoft.Identity.Client related DLLs from Exchange's directory
$msalDirectory = Split-Path $exoModule.DLLPath
$relatedDlls = Get-ChildItem -Path $msalDirectory -Filter "Microsoft.Identity.Client*.dll" -File

foreach ($dll in $relatedDlls) {
if ($dll.Name -ne "Microsoft.Identity.Client.dll") {
Write-Host "Loading related assembly: $($dll.Name)" -ForegroundColor Gray
try {
[System.Reflection.Assembly]::LoadFrom($dll.FullName) | Out-Null
Write-Host " Successfully loaded $($dll.Name)" -ForegroundColor Green
} catch {
Write-Host " Failed to load $($dll.Name): $_" -ForegroundColor Yellow
}
}
}
} else {
Write-Warning "Could not find Microsoft.Identity.Client.dll for ExchangeOnlineManagement at $($exoModule.DLLPath)"
}
} else {
Write-Warning "ExchangeOnlineManagement 3.8.0 or higher not found. MSAL DLL conflict will occur."
}
}
Write-Host "=== Initialize-Dependencies.ps1 Complete ===" -ForegroundColor Cyan
}
catch {
Write-Host "=== Initialize-Dependencies.ps1 FAILED ===" -ForegroundColor Red
Write-Host "Error: $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Red
# Don't throw - let RequiredModules try to proceed
}
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Initialize-Dependencies.ps1 script will execute every time the module is imported, which could add noticeable overhead during module loading. The DLL conflict checks and assembly loading operations are relatively expensive. Consider caching the results or adding a mechanism to skip this initialization if it has already been performed in the current session.

Copilot uses AI. Check for mistakes.
Comment on lines 342 to 343
Write-Host "`nFailed to import SharePoint Online module: $_" -ForegroundColor Red
Write-PSFMessage "Failed to import SharePoint Online module: $_" -Level Error
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Import-Module with -UseWindowsPowerShell flag is used for PowerShell Core compatibility, but this requires that the module is installed in Windows PowerShell (5.1). The error message at line 342 doesn't provide guidance about this prerequisite. Consider enhancing the error message to inform users that the SharePoint module must be installed in Windows PowerShell 5.1 when running from PowerShell Core.

Suggested change
Write-Host "`nFailed to import SharePoint Online module: $_" -ForegroundColor Red
Write-PSFMessage "Failed to import SharePoint Online module: $_" -Level Error
# Provide clearer guidance when import fails, especially under PowerShell Core
if ($PSVersionTable.PSEdition -ne 'Desktop') {
$message = "Failed to import SharePoint Online module. When running in PowerShell Core, 'Microsoft.Online.SharePoint.PowerShell' must be installed in Windows PowerShell 5.1 (Desktop) for -UseWindowsPowerShell to work. Underlying error: $_"
}
else {
$message = "Failed to import SharePoint Online module: $_"
}
Write-Host "`n$message" -ForegroundColor Red
Write-PSFMessage $message -Level Error

Copilot uses AI. Check for mistakes.
Comment on lines 184 to 187
$azParams = @{
UseDeviceAuthentication = $UseDeviceCode
Environment = $azEnvironment
Tenant = $TenantId ? $TenantId : $contextTenantId
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable $contextTenantId is used on line 187 but may not be defined if the Graph service was not connected (e.g., if Service only contains 'Azure'). This will cause an error or unexpected behavior when attempting to connect to Azure without first connecting to Graph.

Suggested change
$azParams = @{
UseDeviceAuthentication = $UseDeviceCode
Environment = $azEnvironment
Tenant = $TenantId ? $TenantId : $contextTenantId
$tenantParam = $TenantId
if (-not $tenantParam) {
$tenantVar = Get-Variable -Name 'contextTenantId' -ErrorAction SilentlyContinue
if ($tenantVar) {
$tenantParam = $tenantVar.Value
}
}
$azParams = @{
UseDeviceAuthentication = $UseDeviceCode
Environment = $azEnvironment
Tenant = $tenantParam

Copilot uses AI. Check for mistakes.
$adminUrl = $SharePointAdminUrl
if (-not $adminUrl) {
# Try to infer from Graph context
if ($contextTenantId) {
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable $contextTenantId is referenced but may not be defined if the Graph connection was not established or skipped. This could lead to runtime errors when trying to infer the SharePoint Admin URL.

Suggested change
if ($contextTenantId) {
$contextTenantIdValue = Get-Variable -Name contextTenantId -ErrorAction SilentlyContinue -ValueOnly
if ($contextTenantIdValue) {

Copilot uses AI. Check for mistakes.
Comment on lines 64 to 68
# Separate Sync Tests (Compliance/Exchange) from Parallel Tests
$syncTestIds = @('35003', '35004', '35005', '35010')
$syncTests = $testsToRun | Where-Object { $_.TestId -in $syncTestIds }
$parallelTests = $testsToRun | Where-Object { $_.TestId -notin $syncTestIds }

Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded test IDs for sync tests may be fragile if test IDs change or new tests require synchronous execution. Consider using a more maintainable approach such as adding a property to the test definition to indicate whether it requires synchronous execution, or documenting why these specific test IDs must run synchronously.

Suggested change
# Separate Sync Tests (Compliance/Exchange) from Parallel Tests
$syncTestIds = @('35003', '35004', '35005', '35010')
$syncTests = $testsToRun | Where-Object { $_.TestId -in $syncTestIds }
$parallelTests = $testsToRun | Where-Object { $_.TestId -notin $syncTestIds }
# Separate Sync Tests (Compliance/Exchange) from Parallel Tests.
# Prefer using the test definition's RequiresSynchronousExecution property when available.
# The following IDs are kept as a legacy fallback for existing tests that are known to require
# synchronous execution due to dependencies on shared resources or ordering constraints.
$legacySyncTestIds = @('35003', '35004', '35005', '35010')
$syncTests = $testsToRun | Where-Object {
(
$_.PSObject.Properties.Match('RequiresSynchronousExecution').Count -gt 0 -and
$_.RequiresSynchronousExecution
) -or
$_.TestId -in $legacySyncTestIds
}
$parallelTests = $testsToRun | Where-Object {
-not (
(
$_.PSObject.Properties.Match('RequiresSynchronousExecution').Count -gt 0 -and
$_.RequiresSynchronousExecution
) -or
$_.TestId -in $legacySyncTestIds
)
}

Copilot uses AI. Check for mistakes.
Comment on lines 54 to 57
RequiredModules = @(@{ModuleName = 'ExchangeOnlineManagement'; GUID = 'b5eced50-afa4-455b-847a-d8fb64140a22'; ModuleVersion = '3.8.0'; },
@{ModuleName = 'Microsoft.Graph.Authentication'; GUID = '883916f2-9184-46ee-b1f8-b6a2fb784cee'; ModuleVersion = '2.0.0'; },
@{ModuleName = 'Microsoft.Graph.Beta.Teams'; GUID = 'e264919d-7ae2-4a89-ba8b-524bd93ddc08'; ModuleVersion = '2.0.0'; },
@{ModuleName = 'Az.Accounts'; GUID = '17a2feff-488b-47f9-8729-e2cec094624c'; ModuleVersion = '4.0.2'; },
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module reordering changes ExchangeOnlineManagement to be first in the RequiredModules list, but this may cause issues since the module manifest specifies that modules are loaded in the order they appear. The Initialize-Dependencies.ps1 script is meant to handle this, but if ScriptsToProcess fails or is skipped, having ExchangeOnlineManagement first could still cause the intended DLL loading order to be bypassed. Consider adding documentation or comments explaining this dependency.

Copilot uses AI. Check for mistakes.
Revert Microsoft.Graph.Authentication and Microsoft.Graph.Beta.Teams versions from 2.0.0 to 2.32.0

Co-authored-by: Copilot <[email protected]>
Copilot AI review requested due to automatic review settings December 28, 2025 21:43
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 64 to 65
# Separate Sync Tests (Compliance/Exchange) from Parallel Tests
$syncTestIds = @('35003', '35004', '35005', '35010')
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded test IDs make this code fragile and difficult to maintain. Consider using a more maintainable approach such as adding a 'RequiresSync' property to test definitions, or storing this configuration in a central location that can be easily updated.

Suggested change
# Separate Sync Tests (Compliance/Exchange) from Parallel Tests
$syncTestIds = @('35003', '35004', '35005', '35010')
function Get-SyncTestIds {
<#
.SYNOPSIS
Returns the list of test IDs that must run synchronously.
.DESCRIPTION
Attempts to load sync test IDs from a JSON configuration file
located alongside this script (SyncTests.json), falling back to
the default hard-coded IDs if the file or property is not present.
Expected JSON structure:
{
"SyncTestIds": [ "35003", "35004", "35005", "35010" ]
}
#>
# Default sync test IDs (current behavior)
$defaultSyncTestIds = @('35003', '35004', '35005', '35010')
# Attempt to load overrides from configuration file, if available
try {
$configPath = Join-Path -Path $PSScriptRoot -ChildPath 'SyncTests.json'
if (Test-Path -LiteralPath $configPath) {
$configContent = Get-Content -LiteralPath $configPath -Raw
if ($configContent) {
$config = $configContent | ConvertFrom-Json
if ($config.SyncTestIds -and $config.SyncTestIds.Count -gt 0) {
return [string[]]$config.SyncTestIds
}
}
}
}
catch {
# Swallow any errors and fall back to the default list
}
return $defaultSyncTestIds
}
# Separate Sync Tests (Compliance/Exchange) from Parallel Tests
$syncTestIds = Get-SyncTestIds

Copilot uses AI. Check for mistakes.
Comment on lines 128 to 129
Write-Host "`nThe Exchange Online module is not installed. Please install the module using the following command.`nFor more information see https://learn.microsoft.com/powershell/exchange/exchange-online-powershell-v2" -ForegroundColor Red
Write-Host "`nInstall-Module ExchangeOnlineManagement -Scope CurrentUser`n" -ForegroundColor Yellow
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message should clarify that ExchangeOnlineManagement version 3.8.0 or higher is specifically required, not just any version of the module. This matches the required version specified in the module manifest.

Suggested change
Write-Host "`nThe Exchange Online module is not installed. Please install the module using the following command.`nFor more information see https://learn.microsoft.com/powershell/exchange/exchange-online-powershell-v2" -ForegroundColor Red
Write-Host "`nInstall-Module ExchangeOnlineManagement -Scope CurrentUser`n" -ForegroundColor Yellow
Write-Host "`nThe Exchange Online module (ExchangeOnlineManagement 3.8.0 or higher) is not installed. Please install the required version using the following command.`nFor more information see https://learn.microsoft.com/powershell/exchange/exchange-online-powershell-v2" -ForegroundColor Red
Write-Host "`nInstall-Module ExchangeOnlineManagement -Scope CurrentUser -MinimumVersion 3.8.0`n" -ForegroundColor Yellow

Copilot uses AI. Check for mistakes.
$testsToRun = $testsToRun | Where-Object { $_.Pillar -in $stablePillars }
}

# Separate Sync Tests (Compliance/Exchange) from Parallel Tests
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment uses inconsistent capitalization for 'Compliance/Exchange'. In PowerShell comments, service names should be consistently capitalized. Consider using 'Exchange/Compliance' to match the order they appear in the test IDs or be consistent with how these services are named elsewhere in the codebase.

Suggested change
# Separate Sync Tests (Compliance/Exchange) from Parallel Tests
# Separate Sync Tests (Exchange/Compliance) from Parallel Tests

Copilot uses AI. Check for mistakes.
Comment on lines 138 to 347
switch ($OrderedImport.Name) {
'Microsoft.Graph.Authentication' {
if ($Service -contains 'Graph' -or $Service -contains 'All') {
Write-Host "`nConnecting to Microsoft Graph" -ForegroundColor Yellow
Write-PSFMessage 'Connecting to Microsoft Graph'

try {
Write-PSFMessage "Connecting to Microsoft Graph with params: $($params | Out-String)" -Level Verbose
Connect-MgGraph @params -ErrorAction Stop
$contextTenantId = (Get-MgContext).TenantId
}
catch [Management.Automation.CommandNotFoundException] {
Write-Host "`nThe Graph PowerShell module is not installed. Please install the module using the following command. For more information see https://learn.microsoft.com/powershell/microsoftgraph/installation" -ForegroundColor Red
Write-Host "`Install-Module Microsoft.Graph.Authentication -Scope CurrentUser`n" -ForegroundColor Yellow
}
catch {
Stop-PSFFunction -Message "Failed to authenticate to Graph" -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet
}

try {
Write-Verbose "Verifying Zero Trust context and permissions..."
Test-ZtContext
}
catch {
Stop-PSFFunction -Message "Authenticated to Graph, but the requirements for the ZeroTrustAssessment are not met by the established session:`n$_" -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet
}
}
}

'Az.Accounts' {
if ($SkipAzureConnection) {
continue
}

if ($Service -contains 'Azure' -or $Service -contains 'All') {
Write-Host "`nConnecting to Azure" -ForegroundColor Yellow
Write-PSFMessage 'Connecting to Azure'

$azEnvironment = 'AzureCloud'
if ($Environment -eq 'China') {
$azEnvironment = Get-AzEnvironment -Name AzureChinaCloud
}
elseif ($Environment -in 'USGov', 'USGovDoD') {
$azEnvironment = 'AzureUSGovernment'
}

$azParams = @{
UseDeviceAuthentication = $UseDeviceCode
Environment = $azEnvironment
Tenant = $TenantId ? $TenantId : $contextTenantId
}
if ($ClientId -and $Certificate) {
$azParams.ApplicationId = $ClientId
$azParams.CertificateThumbprint = $Certificate.Certificate.Thumbprint
}

try {
Connect-AzAccount @azParams -ErrorAction Stop
}
catch {
Stop-PSFFunction -Message "Failed to authenticate to Azure: $_" -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet
}
}
}

'ExchangeOnlineManagement' {
if ($Service -contains 'ExchangeOnline' -or $Service -contains 'All') {
Write-Verbose 'Connecting to Microsoft Exchange Online'
try {
if ($UseDeviceCode -and $PSVersionTable.PSEdition -eq 'Desktop') {
Write-Host 'The Exchange Online module in Windows PowerShell does not support device code flow authentication.' -ForegroundColor Red
Write-Host '💡Please use the Exchange Online module in PowerShell Core.' -ForegroundColor Yellow
}
elseif ($UseDeviceCode) {
Connect-ExchangeOnline -ShowBanner:$false -Device:$UseDeviceCode -ExchangeEnvironmentName $ExchangeEnvironmentName
}
else {
Connect-ExchangeOnline -ShowBanner:$false -ExchangeEnvironmentName $ExchangeEnvironmentName
}

# Fix for Get-Label visibility in other scopes
if (Get-Command Get-Label -ErrorAction SilentlyContinue) {
$module = Get-Command Get-Label | Select-Object -ExpandProperty Module
if ($module -and $module.Name -like 'tmp_*') {
Import-Module $module -Global -Force
}
}
}
catch {
Write-Host "`nFailed to connect to Exchange Online: $_" -ForegroundColor Red
}
}

if ($Service -contains 'SecurityCompliance' -or $Service -contains 'All') {
$Environments = @{
'O365China' = @{
ConnectionUri = 'https://ps.compliance.protection.partner.outlook.cn/powershell-liveid'
AuthZEndpointUri = 'https://login.chinacloudapi.cn/common'
}
'O365GermanyCloud' = @{
ConnectionUri = 'https://ps.compliance.protection.outlook.com/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.com/common'
}
'O365Default' = @{
ConnectionUri = 'https://ps.compliance.protection.outlook.com/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.com/common'
}
'O365USGovGCCHigh' = @{
ConnectionUri = 'https://ps.compliance.protection.office365.us/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.us/common'
}
'O365USGovDoD' = @{
ConnectionUri = 'https://l5.ps.compliance.protection.office365.us/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.us/common'
}
Default = @{
ConnectionUri = 'https://ps.compliance.protection.outlook.com/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.com/common'
}
}
Write-Verbose 'Connecting to Microsoft Security & Compliance PowerShell'

if ($UseDeviceCode) {
Write-Host "`nThe Security & Compliance module does not support device code flow authentication." -ForegroundColor Red
}
else {
# Get UPN from Exchange connection or Graph context
$ExoUPN = $UserPrincipalName

# Attempt to resolve UPN before any connection to avoid token acquisition failures without identity
$connectionInformation = $null
try {
$connectionInformation = Get-ConnectionInformation
}
catch {
# Intentionally swallow errors here; fall back to provided UPN if any
}

if (-not $ExoUPN) {
$ExoUPN = $connectionInformation | Where-Object { $_.IsEopSession -ne $true -and $_.State -eq 'Connected' } | Select-Object -ExpandProperty UserPrincipalName -First 1 -ErrorAction SilentlyContinue
}

if (-not $ExoUPN) {
Write-Host "`nUnable to determine a UserPrincipalName for Security & Compliance. Please supply -UserPrincipalName or connect to Exchange Online first." -ForegroundColor Yellow
continue
}

try {
$ippSessionParams = @{
BypassMailboxAnchoring = $true
UserPrincipalName = $ExoUPN
ShowBanner = $false
ErrorAction = 'Stop'
}

# Only override endpoints for non-default clouds to reduce token acquisition failures in Default
if ($ExchangeEnvironmentName -ne 'O365Default') {
$ippSessionParams.ConnectionUri = $Environments[$ExchangeEnvironmentName].ConnectionUri
$ippSessionParams.AzureADAuthorizationEndpointUri = $Environments[$ExchangeEnvironmentName].AuthZEndpointUri
}

Write-Verbose "Connecting to Security & Compliance with UPN: $ExoUPN"
Connect-IPPSSession @ippSessionParams
}
catch [Management.Automation.CommandNotFoundException] {
if (-not $ExchangeModuleNotInstalledWarningShown) {
Write-Host "`nThe Exchange Online module is not installed. Please install the module using the following command.`nFor more information see https://learn.microsoft.com/powershell/exchange/exchange-online-powershell-v2" -ForegroundColor Red
Write-Host "`nInstall-Module ExchangeOnlineManagement -Scope CurrentUser`n" -ForegroundColor Yellow
}
}
catch {
$exception = $_
if ($exception.Exception.InnerException.Message -like "*Method not found*Microsoft.Identity.Client*" -or $exception.Exception.Message -like "*Method not found*Microsoft.Identity.Client*") {
Write-Warning "DLL Conflict detected (Method not found in Microsoft.Identity.Client). This usually happens if Microsoft.Graph is loaded before ExchangeOnlineManagement."
Write-Warning "Please RESTART your PowerShell session and run Connect-ZtAssessment again."
}

Write-Host "`nFailed to connect to the Security & Compliance PowerShell: $exception" -ForegroundColor Red
}
}

# Fix for Get-Label visibility in other scopes
if (Get-Command Get-Label -ErrorAction SilentlyContinue) {
$module = Get-Command Get-Label | Select-Object -ExpandProperty Module
if ($module -and $module.Name -like 'tmp_*') {
Import-Module $module -Global -Force
}
}
}
}

'Microsoft.Online.SharePoint.PowerShell' {
if ($Service -contains 'SharePointOnline' -or $Service -contains 'All') {
try {
# Import module with compatibility if needed
if ($PSVersionTable.PSEdition -ne 'Desktop') {
# Assume module is installed in Windows PowerShell as per instructions
Import-Module Microsoft.Online.SharePoint.PowerShell -UseWindowsPowerShell -WarningAction SilentlyContinue -ErrorAction Stop -Global
}
else {
Import-Module Microsoft.Online.SharePoint.PowerShell -ErrorAction Stop -Global
}
}
catch {
Write-Host "`nFailed to import SharePoint Online module: $_" -ForegroundColor Red
Write-PSFMessage "Failed to import SharePoint Online module: $_" -Level Error
}
}
}
}
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch statement processes modules in the order returned by Get-ModuleImportOrder, but there's no guarantee about the order of execution for services that depend on each other. For example, if SharePoint connection needs Graph context but Graph comes after SharePoint in the import order, the code at line 357 will fail because $contextTenantId won't be set. Consider either ensuring Graph is always processed first when needed, or restructuring the logic to not depend on switch execution order.

Copilot uses AI. Check for mistakes.
$SkipAzureConnection
$SkipAzureConnection,

# The services to connect to such as Azure and EXO. Default is Graph.
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter description for $Service uses inconsistent terminology. It says "such as Azure and EXO" but the ValidateSet includes 'ExchangeOnline', not 'EXO'. The comment should use the full service name 'ExchangeOnline' to match the actual parameter values, or update all references to be consistent.

Suggested change
# The services to connect to such as Azure and EXO. Default is Graph.
# The services to connect to such as Azure and ExchangeOnline. Default is Graph.

Copilot uses AI. Check for mistakes.
}
catch {
$exception = $_
if ($exception.Exception.InnerException.Message -like "*Method not found*Microsoft.Identity.Client*" -or $exception.Exception.Message -like "*Method not found*Microsoft.Identity.Client*") {
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching a generic exception type and checking its message for specific strings is fragile and may not catch all DLL conflict scenarios. Consider using more specific exception types or creating a custom exception for DLL conflicts. Additionally, the check for InnerException and direct Exception could be consolidated.

Suggested change
if ($exception.Exception.InnerException.Message -like "*Method not found*Microsoft.Identity.Client*" -or $exception.Exception.Message -like "*Method not found*Microsoft.Identity.Client*") {
$methodNotFoundException = $null
# Detect DLL conflict via a specific MissingMethodException, preferring the inner exception when present
if ($exception.Exception.InnerException -is [System.MissingMethodException]) {
$methodNotFoundException = $exception.Exception.InnerException
}
elseif ($exception.Exception -is [System.MissingMethodException]) {
$methodNotFoundException = $exception.Exception
}
if ($methodNotFoundException -and $methodNotFoundException.Message -like "*Microsoft.Identity.Client*") {

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 1, 2026 21:30
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 15 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +54 to 58
RequiredModules = @(@{ModuleName = 'ExchangeOnlineManagement'; GUID = 'b5eced50-afa4-455b-847a-d8fb64140a22'; ModuleVersion = '3.8.0'; },
@{ModuleName = 'Microsoft.Graph.Authentication'; GUID = '883916f2-9184-46ee-b1f8-b6a2fb784cee'; ModuleVersion = '2.32.0'; },
@{ModuleName = 'Microsoft.Graph.Beta.Teams'; GUID = 'e264919d-7ae2-4a89-ba8b-524bd93ddc08'; ModuleVersion = '2.32.0'; },
@{ModuleName = 'Az.Accounts'; GUID = '17a2feff-488b-47f9-8729-e2cec094624c'; ModuleVersion = '4.0.2'; },
@{ModuleName = 'PSFramework'; GUID = '8028b914-132b-431f-baa9-94a6952f21ff'; ModuleVersion = '1.13.419'; })
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ordering of RequiredModules has changed to load ExchangeOnlineManagement first. However, when PowerShell processes RequiredModules, it loads them sequentially which can still cause DLL conflicts if Microsoft.Graph.Authentication (line 55) loads its MSAL version before ExchangeOnlineManagement's modules are fully initialized. The Initialize-Dependencies.ps1 script attempts to pre-load the DLLs, but there's a potential race condition. Consider documenting why this order is critical, or ensure the Initialize-Dependencies script is robust enough to handle cases where other modules might already be partially loaded.

Copilot uses AI. Check for mistakes.
Comment on lines 69 to 82
try {
$workflow = Start-ZtTestExecution -Tests $testsToRun -DbPath $Database.Database -ThrottleLimit $ThrottleLimit
Wait-ZtTest -Workflow $workflow
# Run Sync Tests in the main thread
foreach ($test in $syncTests) {
Invoke-ZtTest -Test $test -Database $Database
}

# Run Parallel Tests
if ($parallelTests) {
$workflow = Start-ZtTestExecution -Tests $parallelTests -DbPath $Database.Database -ThrottleLimit $ThrottleLimit
Wait-ZtTest -Workflow $workflow
}
}
finally {
if ($workflow) {
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'workflow' is referenced in the finally block on line 82, but it may not be defined if execution never enters the try block or if only syncTests are run without parallelTests. This will cause an error when checking 'if ($workflow)'. Initialize the variable before the try block (e.g., '$workflow = $null') to avoid potential undefined variable errors.

Copilot uses AI. Check for mistakes.
Comment on lines 133 to 172
$contextTenantId = (Get-MgContext).TenantId
}

catch {
Stop-PSFFunction -Message "Failed to authenticate to Graph" -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet
}

try {
Write-Verbose "Verifying Zero Trust context and permissions..."
Test-ZtContext
}
catch {
Stop-PSFFunction -Message "Authenticated to Graph, but the requirements for the ZeroTrustAssessment are not met by the established session:`n$_" -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet
}
}
}

'Az.Accounts' {
if ($SkipAzureConnection) {
continue
}

if ($Service -contains 'Azure' -or $Service -contains 'All') {
Write-Host "`nConnecting to Azure" -ForegroundColor Yellow
Write-PSFMessage 'Connecting to Azure'

$azEnvironment = 'AzureCloud'
if ($Environment -eq 'China') {
$azEnvironment = Get-AzEnvironment -Name AzureChinaCloud
}
elseif ($Environment -in 'USGov', 'USGovDoD') {
$azEnvironment = 'AzureUSGovernment'
}

$tenantParam = $TenantId
if (-not $tenantParam) {
$tenantVar = Get-Variable -Name 'contextTenantId' -ErrorAction SilentlyContinue
if ($tenantVar) {
$tenantParam = $tenantVar.Value
}
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'contextTenantId' is used on line 133, but it's only set when connecting to Graph. If the 'Graph' service is not in the Service array, this variable will be undefined when referenced in the 'Az.Accounts' switch case on line 169-172. This can cause errors when trying to connect to Azure without Graph. Consider checking if the variable exists before using it or ensuring Graph is connected first if Azure connection requires it.

Copilot uses AI. Check for mistakes.
Comment on lines 45 to 46
# CRITICAL: ExchangeOnlineManagement must be loaded FIRST due to its specific Broker DLL requirements
# Even if other modules have newer MSAL versions, Exchange needs its specific Broker extensions
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 45-46 states that ExchangeOnlineManagement must be loaded FIRST due to Broker DLL requirements, but the code then uses Get-ModuleImportOrder which may return modules in a different order based on DLL versions. This creates a contradiction: the function might determine that another module should be loaded first (based on MSAL version), but the comment says Exchange must always be first. Clarify the logic: should Exchange always be first, or should it be ordered by DLL version? The current implementation is ambiguous.

Suggested change
# CRITICAL: ExchangeOnlineManagement must be loaded FIRST due to its specific Broker DLL requirements
# Even if other modules have newer MSAL versions, Exchange needs its specific Broker extensions
# CRITICAL: ExchangeOnlineManagement is the FIRST module used to load MSAL/Broker DLLs in this script
# Get-ModuleImportOrder is only used here to locate the appropriate ExchangeOnlineManagement version/path,
# not to choose a different module to load MSAL from, even if other modules have newer MSAL versions.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +247
Default = @{
ConnectionUri = 'https://ps.compliance.protection.outlook.com/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.com/common'
}
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ExchangeEnvironmentName parameter defaults to 'O365Default', but the environment mapping for SecurityCompliance on line 244 uses 'Default' as a key which doesn't match any ValidateSet value. If someone passes an unexpected value or if there's a logic error, the hashtable lookup will fail silently and fall through to the Default key. This inconsistency should be resolved—either the Default key should be removed (since O365Default covers it), or the fallback logic should be made explicit.

Suggested change
Default = @{
ConnectionUri = 'https://ps.compliance.protection.outlook.com/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.com/common'
}

Copilot uses AI. Check for mistakes.

# Ensure ExchangeOnline is included if SecurityCompliance is requested
if ($Service -contains 'SecurityCompliance' -and $Service -notcontains 'ExchangeOnline' -and $Service -notcontains 'All') {
Write-Verbose "Adding ExchangeOnline to the list of services to connect to as it is required for SecurityCompliance."
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verbose message on line 103 will be hidden from users unless they use -Verbose. Since automatically adding ExchangeOnline when SecurityCompliance is requested is important behavior that affects which services are connected, consider using Write-Host or Write-PSFMessage at a higher level so users are aware of this automatic dependency resolution.

Suggested change
Write-Verbose "Adding ExchangeOnline to the list of services to connect to as it is required for SecurityCompliance."
Write-PSFMessage -Level Host -Message "Adding ExchangeOnline to the list of services to connect to as it is required for SecurityCompliance."

Copilot uses AI. Check for mistakes.
Comment on lines 124 to 352
switch ($OrderedImport.Name) {
'Microsoft.Graph.Authentication' {
if ($Service -contains 'Graph' -or $Service -contains 'All') {
Write-Host "`nConnecting to Microsoft Graph" -ForegroundColor Yellow
Write-PSFMessage 'Connecting to Microsoft Graph'

try {
Write-PSFMessage "Connecting to Microsoft Graph with params: $($params | Out-String)" -Level Verbose
Connect-MgGraph @params -ErrorAction Stop
$contextTenantId = (Get-MgContext).TenantId
}

catch {
Stop-PSFFunction -Message "Failed to authenticate to Graph" -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet
}

try {
Write-Verbose "Verifying Zero Trust context and permissions..."
Test-ZtContext
}
catch {
Stop-PSFFunction -Message "Authenticated to Graph, but the requirements for the ZeroTrustAssessment are not met by the established session:`n$_" -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet
}
}
}

'Az.Accounts' {
if ($SkipAzureConnection) {
continue
}

if ($Service -contains 'Azure' -or $Service -contains 'All') {
Write-Host "`nConnecting to Azure" -ForegroundColor Yellow
Write-PSFMessage 'Connecting to Azure'

$azEnvironment = 'AzureCloud'
if ($Environment -eq 'China') {
$azEnvironment = Get-AzEnvironment -Name AzureChinaCloud
}
elseif ($Environment -in 'USGov', 'USGovDoD') {
$azEnvironment = 'AzureUSGovernment'
}

$tenantParam = $TenantId
if (-not $tenantParam) {
$tenantVar = Get-Variable -Name 'contextTenantId' -ErrorAction SilentlyContinue
if ($tenantVar) {
$tenantParam = $tenantVar.Value
}
}

$azParams = @{
UseDeviceAuthentication = $UseDeviceCode
Environment = $azEnvironment
Tenant = $tenantParam
}
if ($ClientId -and $Certificate) {
$azParams.ApplicationId = $ClientId
$azParams.CertificateThumbprint = $Certificate.Certificate.Thumbprint
}

try {
Connect-AzAccount @azParams -ErrorAction Stop
}
catch {
Stop-PSFFunction -Message "Failed to authenticate to Azure: $_" -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet
}
}
}

'ExchangeOnlineManagement' {
if ($Service -contains 'ExchangeOnline' -or $Service -contains 'All') {
Write-Verbose 'Connecting to Microsoft Exchange Online'
try {
if ($UseDeviceCode -and $PSVersionTable.PSEdition -eq 'Desktop') {
Write-Host 'The Exchange Online module in Windows PowerShell does not support device code flow authentication.' -ForegroundColor Red
Write-Host '💡Please use the Exchange Online module in PowerShell Core.' -ForegroundColor Yellow
}
elseif ($UseDeviceCode) {
Connect-ExchangeOnline -ShowBanner:$false -Device:$UseDeviceCode -ExchangeEnvironmentName $ExchangeEnvironmentName
}
else {
Connect-ExchangeOnline -ShowBanner:$false -ExchangeEnvironmentName $ExchangeEnvironmentName
}

# Fix for Get-Label visibility in other scopes
if (Get-Command Get-Label -ErrorAction SilentlyContinue) {
$module = Get-Command Get-Label | Select-Object -ExpandProperty Module
if ($module -and $module.Name -like 'tmp_*') {
Import-Module $module -Global -Force
}
}
}
catch {
Write-Host "`nFailed to connect to Exchange Online: $_" -ForegroundColor Red
}
}

if ($Service -contains 'SecurityCompliance' -or $Service -contains 'All') {
$Environments = @{
'O365China' = @{
ConnectionUri = 'https://ps.compliance.protection.partner.outlook.cn/powershell-liveid'
AuthZEndpointUri = 'https://login.chinacloudapi.cn/common'
}
'O365GermanyCloud' = @{
ConnectionUri = 'https://ps.compliance.protection.outlook.com/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.com/common'
}
'O365Default' = @{
ConnectionUri = 'https://ps.compliance.protection.outlook.com/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.com/common'
}
'O365USGovGCCHigh' = @{
ConnectionUri = 'https://ps.compliance.protection.office365.us/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.us/common'
}
'O365USGovDoD' = @{
ConnectionUri = 'https://l5.ps.compliance.protection.office365.us/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.us/common'
}
Default = @{
ConnectionUri = 'https://ps.compliance.protection.outlook.com/powershell-liveid/'
AuthZEndpointUri = 'https://login.microsoftonline.com/common'
}
}
Write-Verbose 'Connecting to Microsoft Security & Compliance PowerShell'

if ($UseDeviceCode) {
Write-Host "`nThe Security & Compliance module does not support device code flow authentication." -ForegroundColor Red
}
else {
# Get UPN from Exchange connection or Graph context
$ExoUPN = $UserPrincipalName

# Attempt to resolve UPN before any connection to avoid token acquisition failures without identity
$connectionInformation = $null
try {
$connectionInformation = Get-ConnectionInformation
}
catch {
# Intentionally swallow errors here; fall back to provided UPN if any
$connectionInfoError = $_
Write-Verbose "Get-ConnectionInformation failed; falling back to provided UserPrincipalName if available. Error: $($connectionInfoError.Exception.Message)"
}

if (-not $ExoUPN) {
$ExoUPN = $connectionInformation | Where-Object { $_.IsEopSession -ne $true -and $_.State -eq 'Connected' } | Select-Object -ExpandProperty UserPrincipalName -First 1 -ErrorAction SilentlyContinue
}

if (-not $ExoUPN) {
Write-Host "`nUnable to determine a UserPrincipalName for Security & Compliance. Please supply -UserPrincipalName or connect to Exchange Online first." -ForegroundColor Yellow
continue
}

try {
$ippSessionParams = @{
BypassMailboxAnchoring = $true
UserPrincipalName = $ExoUPN
ShowBanner = $false
ErrorAction = 'Stop'
}

# Only override endpoints for non-default clouds to reduce token acquisition failures in Default
if ($ExchangeEnvironmentName -ne 'O365Default') {
$ippSessionParams.ConnectionUri = $Environments[$ExchangeEnvironmentName].ConnectionUri
$ippSessionParams.AzureADAuthorizationEndpointUri = $Environments[$ExchangeEnvironmentName].AuthZEndpointUri
}

Write-Verbose "Connecting to Security & Compliance with UPN: $ExoUPN"
Connect-IPPSSession @ippSessionParams
}

catch {
$exception = $_
$methodNotFoundException = $null

# Detect DLL conflict via a specific MissingMethodException, preferring the inner exception when present
if ($exception.Exception.InnerException -is [System.MissingMethodException]) {
$methodNotFoundException = $exception.Exception.InnerException
}
elseif ($exception.Exception -is [System.MissingMethodException]) {
$methodNotFoundException = $exception.Exception
}

if ($methodNotFoundException -and $methodNotFoundException.Message -like "*Microsoft.Identity.Client*") {
Write-Warning "DLL Conflict detected (Method not found in Microsoft.Identity.Client). This usually happens if Microsoft.Graph is loaded before ExchangeOnlineManagement."
Write-Warning "Please RESTART your PowerShell session and run Connect-ZtAssessment again."
}

Write-Host "`nFailed to connect to the Security & Compliance PowerShell: $exception" -ForegroundColor Red
}
}

# Fix for Get-Label visibility in other scopes
if (Get-Command Get-Label -ErrorAction SilentlyContinue) {
$module = Get-Command Get-Label | Select-Object -ExpandProperty Module
if ($module -and $module.Name -like 'tmp_*') {
Import-Module $module -Global -Force
}
}
}
}

'Microsoft.Online.SharePoint.PowerShell' {
if ($Service -contains 'SharePointOnline' -or $Service -contains 'All') {
try {
# Import module with compatibility if needed
if ($PSVersionTable.PSEdition -ne 'Desktop') {
# Assume module is installed in Windows PowerShell as per instructions
Import-Module Microsoft.Online.SharePoint.PowerShell -UseWindowsPowerShell -WarningAction SilentlyContinue -ErrorAction Stop -Global
}
else {
Import-Module Microsoft.Online.SharePoint.PowerShell -ErrorAction Stop -Global
}
}
catch {
# Provide clearer guidance when import fails, especially under PowerShell Core
if ($PSVersionTable.PSEdition -ne 'Desktop') {
$message = "Failed to import SharePoint Online module. When running in PowerShell Core, 'Microsoft.Online.SharePoint.PowerShell' must be installed in Windows PowerShell 5.1 (Desktop) for -UseWindowsPowerShell to work. Underlying error: $_"
}
else {
$message = "Failed to import SharePoint Online module: $_"
}
Write-Host "`n$message" -ForegroundColor Red
Write-PSFMessage $message -Level Error
}
}
}
}
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch statement on line 124 switches on module names from 'OrderedImport.Name', but the cases inside don't execute sequentially—they execute based on which module name matches. This means the "import order" established by Get-ModuleImportOrder isn't actually enforced for the connection sequence. The modules are connected in the order the cases are written in the switch, not the order returned by Get-ModuleImportOrder. If the intention is to connect services in a specific DLL-version order, the switch should be replaced with a foreach loop that iterates through OrderedImport in sequence.

Copilot uses AI. Check for mistakes.
Comment on lines 362 to 375
$contextTenantIdValue = Get-Variable -Name contextTenantId -ErrorAction SilentlyContinue -ValueOnly
if ($contextTenantIdValue) {
try {
$org = Invoke-ZtGraphRequest -RelativeUri 'organization'
$initialDomain = $org.verifiedDomains | Where-Object { $_.isInitial } | Select-Object -ExpandProperty name -First 1
if ($initialDomain) {
$tenantName = $initialDomain.Split('.')[0]
$adminUrl = "https://$tenantName-admin.sharepoint.com"
Write-Verbose "Inferred SharePoint Admin URL: $adminUrl"
}
}
catch {
Write-Verbose "Failed to infer SharePoint Admin URL from Graph: $_"
}
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'contextTenantIdValue' is retrieved on line 362 but is only used to check for its existence. Later on lines 365-370, the code attempts to use Graph API (Invoke-ZtGraphRequest) without verifying that the Graph connection was actually established. If Graph is not in the Service array, this will fail. Add a check to ensure Graph is connected before attempting to use Invoke-ZtGraphRequest.

Copilot uses AI. Check for mistakes.
Comment on lines 82 to 87
catch {
Write-Host "=== Initialize-Dependencies.ps1 FAILED ===" -ForegroundColor Red
Write-Host "Error: $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Red
# Don't throw - let RequiredModules try to proceed
}
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block on line 82 silently continues after a failure in Initialize-Dependencies.ps1 with only console output (Write-Host). This means errors won't be logged through the PSFramework logging system that the rest of the module uses. Additionally, the comment 'Don't throw - let RequiredModules try to proceed' suggests intentional failure tolerance, but this may hide critical initialization failures. Consider using Write-PSFMessage for consistency with the module's logging approach and clearly documenting the decision to allow failures.

Copilot uses AI. Check for mistakes.
Comment on lines 39 to 44
# Define dependencies and their required versions
$dependencies = @('Az.Accounts', 'ExchangeOnlineManagement', 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Beta.Teams')
$requiredVersions = @{
'ExchangeOnlineManagement' = '3.8.0'
}

Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variables $dependencies and $requiredVersions are defined but never used in the Initialize-Dependencies.ps1 script. The $dependencies variable is defined with multiple module names, but only 'ExchangeOnlineManagement' is actually processed. Either remove these unused definitions or implement the intended logic to process all dependencies.

Suggested change
# Define dependencies and their required versions
$dependencies = @('Az.Accounts', 'ExchangeOnlineManagement', 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Beta.Teams')
$requiredVersions = @{
'ExchangeOnlineManagement' = '3.8.0'
}
# Define required versions for dependencies
$requiredVersions = @{
'ExchangeOnlineManagement' = '3.8.0'
}

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 1, 2026 22:22
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 12 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

try {
Write-PSFMessage "Connecting to Microsoft Graph with params: $($params | Out-String)" -Level Verbose
Connect-MgGraph @params -ErrorAction Stop
$contextTenantId = (Get-MgContext).TenantId
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assignment of $contextTenantId happens inside a conditional block (lines 126-147) that only executes if 'Graph' or 'All' is in the Service array. However, this variable is used later at line 362 and line 169-172 without checking if it was actually set. If a user connects only to Azure or ExchangeOnline without Graph, $contextTenantId will be undefined, causing potential errors when referenced. Consider setting $contextTenantId to $null before the switch statement or adding proper null checks before using it.

Copilot uses AI. Check for mistakes.
Write-Warning "Please RESTART your PowerShell session and run Connect-ZtAssessment again."
}

Write-Host "`nFailed to connect to the Security & Compliance PowerShell: $exception" -ForegroundColor Red
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message includes the raw exception object using string interpolation. This could result in verbose and unhelpful output. Consider using $exception.Exception.Message or providing a more user-friendly error message that explains what went wrong and how to resolve it.

Suggested change
Write-Host "`nFailed to connect to the Security & Compliance PowerShell: $exception" -ForegroundColor Red
Write-Host "`nFailed to connect to the Security & Compliance PowerShell: $($exception.Exception.Message)" -ForegroundColor Red

Copilot uses AI. Check for mistakes.
Comment on lines +265 to +266
$connectionInfoError = $_
Write-Verbose "Get-ConnectionInformation failed; falling back to provided UserPrincipalName if available. Error: $($connectionInfoError.Exception.Message)"
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable $connectionInfoError is assigned at line 265 but never used afterwards. This creates dead code that adds no value. Either remove the variable assignment and just use $_ in the Write-Verbose message, or remove the variable entirely if it's not needed.

Suggested change
$connectionInfoError = $_
Write-Verbose "Get-ConnectionInformation failed; falling back to provided UserPrincipalName if available. Error: $($connectionInfoError.Exception.Message)"
Write-Verbose "Get-ConnectionInformation failed; falling back to provided UserPrincipalName if available. Error: $($_.Exception.Message)"

Copilot uses AI. Check for mistakes.
[Parameter(
Position = 0,
ValueFromPipelineByPropertyName,
HelpMessage = 'Enter a list of names to evaluate. Wildcards are allowed.'
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter help message says 'Enter a list of names to evaluate. Wildcards are allowed.' but the code doesn't actually process wildcards - it passes the names directly to Get-Module which does support wildcards, but there's no explicit wildcard expansion or validation in this function. The help message should clarify that wildcard support is provided by the underlying Get-Module cmdlet, or the function should explicitly handle and document wildcard behavior.

Copilot uses AI. Check for mistakes.
Comment on lines 167 to 178
$tenantParam = $TenantId
if (-not $tenantParam) {
$tenantVar = Get-Variable -Name 'contextTenantId' -ErrorAction SilentlyContinue
if ($tenantVar) {
$tenantParam = $tenantVar.Value
}
}

$azParams = @{
UseDeviceAuthentication = $UseDeviceCode
Environment = $azEnvironment
Tenant = $tenantParam
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable $tenantParam is used to construct $azParams but the logic for obtaining it from $contextTenantId may fail silently. If $TenantId is not provided and $contextTenantId variable doesn't exist (because Graph connection was skipped), $tenantParam will be $null. Then Connect-AzAccount will be called with Tenant = $null, which might cause unexpected behavior. Consider validating that $tenantParam has a value before calling Connect-AzAccount, or provide a more helpful error message if neither TenantId parameter nor contextTenantId variable is available.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +74
foreach ($test in $syncTests) {
Invoke-ZtTest -Test $test -Database $Database
}
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an exception occurs during the sync tests execution (lines 72-74), the parallel tests will never run and the error won't be explicitly handled. Consider wrapping the sync tests in a try-catch block to handle errors gracefully and ensure parallel tests can still execute even if some sync tests fail.

Copilot uses AI. Check for mistakes.
Comment on lines +279 to +284
$ippSessionParams = @{
BypassMailboxAnchoring = $true
UserPrincipalName = $ExoUPN
ShowBanner = $false
ErrorAction = 'Stop'
}
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name 'ippSessionParams' uses inconsistent casing (lowercase 'ipp'). PowerShell convention typically uses PascalCase for variable names in this context. Consider renaming to 'IppSessionParams' or 'IPPSSessionParams' for consistency with other variables in this function like '$azParams' and '$params'.

Copilot uses AI. Check for mistakes.
)

begin {
$ModulesWithVersionSortedIdentityClient = [System.Collections.Generic.List[PSCustomobject]]::new()
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses [System.Collections.Generic.List[PSCustomobject]] but 'PSCustomobject' uses incorrect casing. The correct type name in PowerShell is 'PSCustomObject' with a capital 'O'. While PowerShell may be case-insensitive in many contexts, using the proper casing improves code clarity and follows best practices.

Suggested change
$ModulesWithVersionSortedIdentityClient = [System.Collections.Generic.List[PSCustomobject]]::new()
$ModulesWithVersionSortedIdentityClient = [System.Collections.Generic.List[PSCustomObject]]::new()

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +48
try {
[System.Reflection.Assembly]::LoadFrom($_.FullName) | Out-Null
}
catch {
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty catch block at lines 48-50 silently swallows all exceptions without any logging or handling. While the comment indicates this is intentional, completely ignoring errors can hide bugs and make debugging difficult. Consider at minimum adding a Write-Verbose or Write-Debug statement to log that an error occurred during the DLL loading attempt.

Suggested change
try {
[System.Reflection.Assembly]::LoadFrom($_.FullName) | Out-Null
}
catch {
$dependentAssemblyPath = $_.FullName
try {
[System.Reflection.Assembly]::LoadFrom($dependentAssemblyPath) | Out-Null
}
catch {
Write-Verbose "Failed to load dependent MSAL assembly '$dependentAssemblyPath': $($_.Exception.Message)"

Copilot uses AI. Check for mistakes.
# Load Main DLL
[System.Reflection.Assembly]::LoadFrom($exoModule.DLLPath) | Out-Null

# Load related DLLs (Brokers, etc.)
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says 'Brokers, etc.' but the code specifically filters for files matching 'Microsoft.Identity.Client*.dll' excluding the main 'Microsoft.Identity.Client.dll'. This is more specific than the comment implies. Consider updating the comment to be more precise about what additional DLLs are being loaded, for example: 'Load related MSAL DLLs (e.g., Microsoft.Identity.Client.Broker.dll, Microsoft.Identity.Client.Extensions.dll)'.

Suggested change
# Load related DLLs (Brokers, etc.)
# Load related MSAL DLLs (e.g., Microsoft.Identity.Client.Broker.dll, Microsoft.Identity.Client.Extensions.dll)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant