PsModuleManagement.psm1
|
Function InstalledModuleInfoPsModuleManagement { [CmdletBinding()] param( [Parameter()] [string]$MainModule, [Parameter()] [string]$AuthModule, [Parameter()] [array]$MaintenancePowershellServices, [Parameter()] [array]$MaintenancePowershellProcesses, [Parameter()] [switch]$GetOldVersions, [Parameter()] [switch]$CheckInstallation, [Parameter()] [AllowEmptyString()] [AllowNull()] [string]$ModuleRequiredVersion, [AllowEmptyString()] [AllowNull()] [string]$AuthModuleRequiredVersion, [AllowEmptyString()] [AllowNull()] [array]$RequiredModules ) <# $MainModule = "Microsoft.Graph" $AuthModule = "Microsoft.Graph.Authentication" $ModuleRequiredVersion = $null $MaintenancePowershellServices = @("VisualCron") $MaintenancePowershellProcesses = @("powershell","powershell_ise","VisualCronClient") $GetOldVersions = $true #> If ($CheckInstallation) { # Checking if Main module is installed ... Otherwise it will install it write-host "" write-host "Installation: Checking installation of main module $($MainModule) ... Please Wait !" $Global:InstalledVersionMainModule = Get-installedmodule $MainModule -ErrorAction SilentlyContinue # Installing Main module if not found ! If (!($Global:InstalledVersionMainModule)) { # Stopping all services PowershellServiceProcessMaintenance -Services $MaintenancePowershellServices -Processes $MaintenancePowershellProcesses -Action STOP # install module write-host "" write-host "Installing module $($MainModule) as it wasn't found ... Please Wait !" Try { If ($ModuleRequiredVersion) { install-module $MainModule -force -Scope AllUsers -RequiredVersion $ModuleRequiredVersion -AllowClobber -ErrorAction Stop } Else { install-module $MainModule -force -Scope AllUsers -AllowClobber -ErrorAction Stop } } Catch { Try { If ($ModuleRequiredVersion) { install-module $MainModule -force -Scope AllUsers -RequiredVersion $ModuleRequiredVersion -ErrorAction Stop } Else { install-module $MainModule -force -Scope AllUsers -ErrorAction Stop } } Catch { write-host "" write-host "Errors occured .... terminating as modules are locked in memory !!" write-host "Close down the current Powershell session and re-run this script !" Exit 1 } } } # Verify critical Auth component exists ! If not, then install component If ($AuthModule) { #-------------------------------------------------------------------------------------------- $AuthModules = Get-module $AuthModule -ErrorAction SilentlyContinue #-------------------------------------------------------------------------------------------- write-host "" write-host "Installation: Checking installation of authentication module $($AuthModule) ... Please Wait !" $AuthModuleInfo = Get-installedmodule $AuthModule -ErrorAction SilentlyContinue write-host "$($AuthModule) -> $($AuthModuleInfo.version)" If (!($AuthModuleInfo)) { write-host "Re-install $($MainModule) (version: $($ModuleRequiredVersion)) as authentication module was not found !" install-module $MainModule -force -Scope AllUsers -RequiredVersion $ModuleRequiredVersion -AllowClobber } # Remove auth. modul if newer ! If ( ($global:AuthModuleRequiredVersion) -and ([version]$AuthModuleInfo.Version -gt [version]$global:AuthModuleRequiredVersion ) ) { write-host "Downgrade manually to $($global:AuthModuleRequiredVersion) ... Please Wait !!" write-host "" # Force auth removal Remove-Module $AuthModule -Force -ErrorAction SilentlyContinue Uninstall-Module $AuthModule -AllVersions -Force -ErrorAction SilentlyContinue # Force remove file in use $graphPaths = Get-Module $AuthModule -ListAvailable | Select-Object -ExpandProperty Path $versionFolders = $graphPaths | ForEach-Object { Split-Path $_ -Parent } | Sort-Object -Unique $versionFolders | ForEach-Object { $folder = $_ $folderVersion = Split-Path $folder -Leaf if ($folderVersion -eq $global:AuthModuleRequiredVersion) { Write-Host "Skipping required version folder: $folder" return } Write-Host "Removing: $folder" Remove-Item $folder -Recurse -Force -ErrorAction SilentlyContinue } write-host "Installing $($global:AuthModuleRequiredVersion)" install-Module $AuthModule -RequiredVersion $global:AuthModuleRequiredVersion -Force } } } # If ($CheckInstallation) # RequiredModules If ($RequiredModules) { $ForceMainModuleRepair = $false ForEach ($Module in $RequiredModules) { write-host "" write-host "Validating critical module exist: $($Module)" $ModuleChkAuthModuleInfo = Get-installedmodule $Module -ErrorAction SilentlyContinue If (!($ModuleChkAuthModuleInfo)) { write-host "$($Module) -> NOT FOUND - REPAIR REQUIRED!" $ForceMainModuleRepair = $true } Else { write-host "$($Module) -> $($ModuleChkAuthModuleInfo.version)" } } If ($ForceMainModuleRepair) { write-host "" write-host "Re-install $($MainModule) (version: $($ModuleRequiredVersion)) as required files were not detected !" install-module $MainModule -force -Scope AllUsers -RequiredVersion $ModuleRequiredVersion -AllowClobber } } # Get info about current version of Main Module write-host "" write-host "Getting info about current version of $($MainModule) ... Please Wait !" $Global:InstalledVersionMainModule = Get-installedmodule $MainModule If ($Global:InstalledVersionMainModule) { write-host "" write-host "Installed: Version of $($MainModule) found on system: $($Global:InstalledVersionMainModule.Version)" } Else { write-host "" write-host "Could not detect $($MainModule) on system .... exiting !" Exit 1 } $Global:CurrentInstalledVersion = $InstalledVersionMainModule.Version # Getting information about Sub modules write-host "" write-host "Getting info about sub modules of $($MainModule) ... Please Wait !" $Global:InstalledVersionSubModules = Get-installedmodule "$($MainModule).*" -ErrorAction SilentlyContinue # Getting information about Auth Module If ($AuthModule) { $AuthModuleInfo = Get-installedmodule $AuthModule -ErrorAction SilentlyContinue $Global:AuthModuleRequiredVersion = $AuthModuleInfo.Version If ($Global:AuthModuleRequiredVersion) { write-host "" write-host "Installed: Version of $($AuthModule) found on system: $($Global:AuthModuleRequiredVersion)" } Else { write-host "" write-host "Could not detect $($AuthModule) on system .... exiting !" Exit 1 } } If ($GetOldVersions) { # Main Module - Getting information latest versions of main module write-host "" write-host "Getting info about all versions of main module $($MainModule) on local system ... Please Wait !" $InstalledAllMain = Get-module $MainModule -ListAvailable -ErrorAction SilentlyContinue # Sub modules - Getting information about latest installed version of sub-modules module write-host "" write-host "Getting info about sub modules of $($MainModule) ... Please Wait !" $InstalledAllSub = Get-module "$($MainModule).*" -ListAvailable -ErrorAction SilentlyContinue # Build $InstalledAllVersions array $InstalledAllVersions = @() If ($InstalledAllMain) { ForEach ($Entry in $InstalledAllMain) { $Object = New-Object PSObject $Object | Add-Member -MemberType NoteProperty -Name "Name" -Value $Entry.Name $Object | Add-Member -MemberType NoteProperty -Name "Version" -Value $Entry.Version $InstalledAllVersions += $Object } } If ($InstalledAllSub) { ForEach ($Entry in $InstalledAllSub) { $Object = New-Object PSObject $Object | Add-Member -MemberType NoteProperty -Name "Name" -Value $Entry.Name $Object | Add-Member -MemberType NoteProperty -Name "Version" -Value $Entry.Version $InstalledAllVersions += $Object } } If ($global:ModuleRequiredVersion) { write-host "" write-host "Getting latest versions incl. sub-modules ... Please Wait (slow) !" $LatestVersions = Find-Module -Name $MainModule -Repository PSGallery -IncludeDependencies -RequiredVersion $global:ModuleRequiredVersion } Else { write-host "" write-host "Getting latest versions incl. sub-modules ... Please Wait (slow) !" $LatestVersions = Find-Module -Name $MainModule -Repository PSGallery -IncludeDependencies } write-host "" write-host "Building overview of old installed modules of $($MainModule) ... Please Wait !" $Global:OldInstalledVersionsModules = @() ForEach ($Module in $LatestVersions) { $Global:OldInstalledVersionsModules += $InstalledAllVersions | Where-Object { ([version]$_.Version -ne [version]$Module.Version) -and ($_.Name -eq $Module.Name) } } } } Function PostActionsPsModuleManagement { [CmdletBinding()] param( [Parameter(mandatory)] [string]$FileName, [Parameter(mandatory)] [string]$GitHubUri ) write-host "" write-host "Known Mitigations are in progress .... Please Wait !" write-host "" $TargetFile = $env:windir + "\temp\" + $PostMitigationScriptKnownIssues Remove-Item $TargetFile -ErrorAction SilentlyContinue $ScriptFromGitHub = Invoke-WebRequest "$($GitHubUri)/$($PostMitigationScriptKnownIssues)" -OutFile $TargetFile & $TargetFile Return $global:TerminateSession } Function PowershellServiceProcessMaintenance { [CmdletBinding()] param( [Parameter()] [array]$Services, [Parameter()] [array]$Processes, [Parameter(mandatory)] [ValidateSet("STOP","START")] $Action ) If ($Action -eq "STOP") { Write-host "" Write-host "Stopping all sessions locking Powershell modules ... Please Wait !" ForEach ($Service in $Services) { write-host "Stopping service $($Service)" Stop-Service $Service -ErrorAction SilentlyContinue } # Get process id of the current process, as it should not be terminated ! $CurrentProcessID = [System.Diagnostics.Process]::GetCurrentProcess() | Select-Object -ExpandProperty 'ID' $Processes = Get-Process -Name $Processes -ErrorAction SilentlyContinue ForEach ($Process in $Processes) { If ($Process.id -eq $CurrentProcessID) { Write-host "Skipping process $($CurrentProcessID) as it is the current process" } Else { Write-host "Terminating process $($Process.ProcessName) ($($Process.Id))" Stop-Process -Id $Process.Id -Force } } } ElseIf ($Action -eq "START") { Write-host "" Write-host "Starting all sessions locking Powershell modules ... Please Wait !" ForEach ($Service in $Services) { write-host "Starting service $($Service)" Start-Service $Service -ErrorAction SilentlyContinue } } } Function SendMailNotificationsPsModuleManagement { [CmdletBinding()] param( [Parameter(mandatory)] [boolean]$SendMailAlerts, [Parameter(mandatory)] [string]$SMTP_Host, [Parameter(mandatory)] [AllowEmptyString()] [AllowNull()] [string]$SMTP_UserId, [Parameter(mandatory)] [AllowEmptyString()] [AllowNull()] [string]$SMTP_Password, [Parameter(mandatory)] [string]$SMTP_Port, [Parameter(mandatory)] [string]$SMTP_From, [Parameter(mandatory)] [array]$SMTP_To, [Parameter(mandatory)] [string]$SMTP_Subject, [Parameter(mandatory)] [string]$SMTP_Body, [Parameter(mandatory)] [string]$Description, [Parameter()] [AllowEmptyString()] [AllowNull()] [boolean]$UseSSL = $false ) If ($SendMailAlerts) { $SMTP_Body += "<br>" $SMTP_Body += "Mail sent from $($Description) using SMTP Host: $($SMTP_Host)<br>" If ( ($SMTP_UserId -eq "") -or ($SMTP_UserId -eq $null) ) { $SMTP_Body += "SMTP Authentication: Anonymous" If ($UseSSL) { Write-host "Sending mail to $($SMTP_To) with subject '$($SMTP_Subject)' (anonymous)" Send-MailMessage -SmtpServer $SMTP_Host -To $SMTP_To -From $SMTP_From -Subject $SMTP_Subject -Body $SMTP_Body -Encoding UTF8 -BodyAsHtml -Priority high -port $SMTP_Port -UseSsl } Else { Write-host "Sending mail to $($SMTP_To) with subject '$($SMTP_Subject)' (anonymous)" Send-MailMessage -SmtpServer $SMTP_Host -To $SMTP_To -From $SMTP_From -Subject $SMTP_Subject -Body $SMTP_Body -Encoding UTF8 -BodyAsHtml -Priority high -port $SMTP_Port } } Else { $SMTP_Body += "SMTP Authentication: Userid/password" $SecureCredentialsSMTP = New-Object System.Management.Automation.PSCredential($SMTP_UserId,(ConvertTo-SecureString $SMTP_Password -AsPlainText -Force)) If ($UseSSL) { Write-host "Sending mail to $($SMTP_To) with subject '$($SMTP_Subject)' (secure)" Send-MailMessage -SmtpServer $SMTP_Host -To $SMTP_To -From $SMTP_From -Subject $SMTP_Subject -Body $SMTP_Body -Encoding UTF8 -BodyAsHtml -Priority high -port $SMTP_Port -Credential $SecureCredentialsSMTP -UseSsl } Else { Write-host "Sending mail to $($SMTP_To) with subject '$($SMTP_Subject)' (secure)" Send-MailMessage -SmtpServer $SMTP_Host -To $SMTP_To -From $SMTP_From -Subject $SMTP_Subject -Body $SMTP_Body -Encoding UTF8 -BodyAsHtml -Priority high -port $SMTP_Port -Credential $SecureCredentialsSMTP } } } } # Warn when a credential expires within this many days $global:CredentialExpiryWarningDays = 30 Function Get-LocalCertificateInfo { <# Looks the thumbprint up in both machine and user stores and reports usability. SYSTEM (scheduled task) reads LocalMachine\My; an interactive user usually has CurrentUser\My - which is why a manual test can succeed while the scheduled run fails. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Thumbprint ) ForEach ($StorePath in @("Cert:\LocalMachine\My", "Cert:\CurrentUser\My")) { $Cert = Get-ChildItem -Path $StorePath -ErrorAction SilentlyContinue | Where-Object Thumbprint -eq $Thumbprint If ($Cert) { Return [PSCustomObject]@{ Found = $true Store = $StorePath Subject = $Cert.Subject NotAfter = $Cert.NotAfter DaysLeft = [math]::Round(($Cert.NotAfter - (Get-Date)).TotalDays) HasPrivateKey = $Cert.HasPrivateKey Expired = ($Cert.NotAfter -lt (Get-Date)) Usable = ( ($Cert.NotAfter -gt (Get-Date)) -and ($Cert.HasPrivateKey) ) } } } Return [PSCustomObject]@{ Found = $false Store = $null Subject = $null NotAfter = $null DaysLeft = $null HasPrivateKey = $false Expired = $false Usable = $false } } Function Test-EntraAppCredentialExpiry { <# Reports expiry of EVERY credential on the app registration - certificates and secrets alike. Requires an active Graph connection and Application.Read.All on the app. This is the part that gives warning BEFORE things break: a secret has no local footprint, so the only way to know it expires next week is to ask Entra. Returns $true if anything is expired or expiring within $CredentialExpiryWarningDays. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Entra_App_ApplicationID, [Parameter()] [int]$WarningDays = 30 ) $ProblemFound = $false Try { $App = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/applications?`$filter=appId eq '$($Entra_App_ApplicationID)'" -ErrorAction Stop).value | Select-Object -First 1 If (!($App)) { Write-host "Could not read app registration $($Entra_App_ApplicationID) - skipping credential expiry check" -ForegroundColor Yellow Return $false } Write-host "" Write-host "Credentials on app registration '$($App.displayName)':" $Credentials = @() ForEach ($Key in $App.keyCredentials) { $Credentials += [PSCustomObject]@{ Type = "Certificate"; Name = $Key.displayName; EndDateTime = $Key.endDateTime } } ForEach ($Pwd in $App.passwordCredentials) { $Credentials += [PSCustomObject]@{ Type = "Secret"; Name = $Pwd.displayName; EndDateTime = $Pwd.endDateTime } } If (!($Credentials)) { Write-host " NO CREDENTIALS ON THE APP REGISTRATION" -ForegroundColor Red Return $true } ForEach ($Credential in $Credentials) { $EndDate = [datetime]$Credential.EndDateTime $DaysLeft = [math]::Round(($EndDate - (Get-Date)).TotalDays) If ($DaysLeft -lt 0) { Write-host " EXPIRED $($Credential.Type.PadRight(12)) '$($Credential.Name)' expired $([math]::Abs($DaysLeft)) day(s) ago ($($EndDate.ToString('yyyy-MM-dd')))" -ForegroundColor Red $ProblemFound = $true } ElseIf ($DaysLeft -le $WarningDays) { Write-host " EXPIRING $($Credential.Type.PadRight(12)) '$($Credential.Name)' expires in $($DaysLeft) day(s) ($($EndDate.ToString('yyyy-MM-dd')))" -ForegroundColor Yellow $ProblemFound = $true } Else { Write-host " OK $($Credential.Type.PadRight(12)) '$($Credential.Name)' expires in $($DaysLeft) day(s) ($($EndDate.ToString('yyyy-MM-dd')))" -ForegroundColor Green } } } Catch { # Missing Application.Read.All is not a connectivity failure - report and move on Write-host "Could not check credential expiry (Application.Read.All required?): $($_.Exception.Message)" -ForegroundColor Yellow } Return $ProblemFound } Function TestConnectivityPsModuleManagement { [CmdletBinding()] param( [Parameter(mandatory)] [string]$Entra_App_ApplicationID, [Parameter()] [string]$Entra_App_Secret, [Parameter(mandatory)] [string]$Entra_App_TenantID, [Parameter(mandatory)] [string]$Entra_TenantName, [Parameter()] [string]$Entra_App_CertificateThumbprint, [Parameter(mandatory)] [string]$MainModule, [Parameter()] [AllowEmptyString()] [AllowNull()] [string]$AuthModule, [Parameter()] [AllowEmptyString()] [AllowNull()] [string]$AuthModuleRequiredVersion, [Parameter()] [string]$AzSubscriptionId ) # Default $ErrorsDetected = $False # Signals to the caller that the failure is a CREDENTIAL problem, not a broken module. # Re-installing a module never fixes an expired certificate or secret. $global:CredentialErrorDetected = $False write-host "" write-host "Testing connectivity with $($MainModule)" If ($AuthModule) { write-host "" write-host "Auth Module version: $($AuthModuleRequiredVersion)" # -ErrorAction Stop: a missing Auth version leaves Connect-MgGraph undefined, which then # looks exactly like a connectivity failure and sends you hunting the wrong problem Try { import-module $AuthModule -RequiredVersion $AuthModuleRequiredVersion -ErrorAction Stop } Catch { write-host "COULD NOT IMPORT $($AuthModule) $($AuthModuleRequiredVersion)" -ForegroundColor Red write-host ($_ | Out-String) -ForegroundColor Red Return $True } } #------------------------------------------------------------------------------------------------ # Certificate pre-flight - do this BEFORE trying to connect, so an expired or key-less # certificate is reported as exactly that instead of a generic authentication failure #------------------------------------------------------------------------------------------------ $CertUsable = $false If ($Entra_App_CertificateThumbprint) { $CertInfo = Get-LocalCertificateInfo -Thumbprint $Entra_App_CertificateThumbprint If (!($CertInfo.Found)) { write-host "CERTIFICATE $($Entra_App_CertificateThumbprint) NOT FOUND in LocalMachine\My or CurrentUser\My" -ForegroundColor Red } ElseIf ($CertInfo.Expired) { write-host "CERTIFICATE EXPIRED on $($CertInfo.NotAfter.ToString('yyyy-MM-dd')) ($([math]::Abs($CertInfo.DaysLeft)) day(s) ago) - $($CertInfo.Subject)" -ForegroundColor Red } ElseIf (!($CertInfo.HasPrivateKey)) { write-host "CERTIFICATE HAS NO PRIVATE KEY in $($CertInfo.Store) - cannot be used to authenticate" -ForegroundColor Red } Else { $CertUsable = $true $ExpiryColour = If ($CertInfo.DaysLeft -le $global:CredentialExpiryWarningDays) { "Yellow" } Else { "Green" } write-host "Certificate OK in $($CertInfo.Store) - expires $($CertInfo.NotAfter.ToString('yyyy-MM-dd')) ($($CertInfo.DaysLeft) day(s) left)" -ForegroundColor $ExpiryColour } } #------------------------------------------------------------------------------------------------ If ( ($MainModule -eq "Microsoft.Graph") -or ($MainModule -eq "Microsoft.Graph.Beta") ) { $Connected = $false # 1) Certificate first If ($CertUsable) { Try { $Disconnect = Disconnect-MgGraph -ErrorAction SilentlyContinue Connect-MgGraph -CertificateThumbprint $Entra_App_CertificateThumbprint -ClientId $Entra_App_ApplicationID -TenantId $Entra_App_TenantID -NoWelcome -ErrorAction Stop $Connected = $true write-host "Connected using CERTIFICATE" -ForegroundColor Green } Catch { write-host "Certificate authentication failed" -ForegroundColor Yellow write-host ($_ | Out-String) -ForegroundColor Yellow } } # 2) Secret as fallback If ( (!($Connected)) -and ($Entra_App_Secret) ) { write-host "Falling back to CLIENT SECRET" -ForegroundColor Yellow Try { $Disconnect = Disconnect-MgGraph -ErrorAction SilentlyContinue $ClientSecretCredential = New-Object System.Management.Automation.PSCredential ($Entra_App_ApplicationID, (ConvertTo-SecureString $Entra_App_Secret -AsPlainText -Force)) Connect-MgGraph -TenantId $Entra_App_TenantID -ClientSecretCredential $ClientSecretCredential -NoWelcome -ErrorAction Stop $Connected = $true write-host "Connected using CLIENT SECRET" -ForegroundColor Green } Catch { write-host "Client secret authentication failed" -ForegroundColor Red write-host ($_ | Out-String) -ForegroundColor Red } } If (!($Connected)) { $ErrorsDetected = $True write-host "CONNECTIVITY ERRORS DETECTED - no authentication method succeeded" -ForegroundColor Yellow If (!($CertUsable)) { $global:CredentialErrorDetected = $True write-host "This is a CREDENTIAL problem, not a module problem - the module will NOT be re-installed" -ForegroundColor Yellow } } Else { # Now that we are connected, report expiry of every credential on the app. # This is what turns an outage into a warning weeks in advance. $Expiring = Test-EntraAppCredentialExpiry -Entra_App_ApplicationID $Entra_App_ApplicationID -WarningDays $global:CredentialExpiryWarningDays If ($Expiring) { write-host "" write-host "ACTION NEEDED: one or more app credentials are expired or expiring soon (see above)" -ForegroundColor Yellow } } } #------------------------------------------------------------------------------------------------ ElseIf ($MainModule -eq "Azure") { $Connected = $false # 1) Certificate first If ($CertUsable) { Try { $Disconnect = Disconnect-AzAccount -ErrorAction SilentlyContinue Connect-AzAccount -CertificateThumbprint $Entra_App_CertificateThumbprint -TenantId $Entra_App_TenantID -Application $Entra_App_ApplicationID -SkipContextPopulation -Force -ErrorAction Stop Set-AzContext -Subscription $AzSubscriptionId -ErrorAction Stop $Connected = $true write-host "Connected using CERTIFICATE" -ForegroundColor Green } Catch { write-host "Certificate authentication failed" -ForegroundColor Yellow write-host ($_ | Out-String) -ForegroundColor Yellow } } # 2) Secret as fallback If ( (!($Connected)) -and ($Entra_App_Secret) ) { write-host "Falling back to CLIENT SECRET" -ForegroundColor Yellow Try { $Disconnect = Disconnect-AzAccount -ErrorAction SilentlyContinue $ClientSecretCredential = New-Object System.Management.Automation.PSCredential ($Entra_App_ApplicationID, (ConvertTo-SecureString $Entra_App_Secret -AsPlainText -Force)) Connect-AzAccount -ServicePrincipal -TenantId $Entra_App_TenantID -Credential $ClientSecretCredential -SkipContextPopulation -Force -ErrorAction Stop Set-AzContext -Subscription $AzSubscriptionId -ErrorAction Stop $Connected = $true write-host "Connected using CLIENT SECRET" -ForegroundColor Green } Catch { write-host "Client secret authentication failed" -ForegroundColor Red write-host ($_ | Out-String) -ForegroundColor Red } } If (!($Connected)) { $ErrorsDetected = $True write-host "$($MainModule) CONNECTIVITY FAILED - no authentication method succeeded" -ForegroundColor Yellow If (!($CertUsable)) { $global:CredentialErrorDetected = $True write-host "This is a CREDENTIAL problem, not a module problem - the module will NOT be re-installed" -ForegroundColor Yellow } } } #------------------------------------------------------------------------------------------------ ElseIf ($MainModule -eq "ExchangeOnlineManagement") { # Exchange Online app-only supports certificate ONLY - there is no secret fallback here If (!($CertUsable)) { $ErrorsDetected = $True $global:CredentialErrorDetected = $True write-host "$($MainModule) CONNECTIVITY FAILED - a usable certificate is required (Exchange app-only does not support client secrets)" -ForegroundColor Yellow write-host "This is a CREDENTIAL problem, not a module problem - the module will NOT be re-installed" -ForegroundColor Yellow } Else { Try { Connect-ExchangeOnline -CertificateThumbprint $Entra_App_CertificateThumbprint -AppId $Entra_App_ApplicationID -Organization $Entra_TenantName -ShowProgress $false -ErrorAction Stop write-host "Connected using CERTIFICATE" -ForegroundColor Green } Catch { $ErrorsDetected = $True write-host "$($MainModule) CONNECTIVITY FAILED" -ForegroundColor Yellow write-host ($_ | Out-String) -ForegroundColor Red } } } #------------------------------------------------------------------------------------------------ If ($ErrorsDetected) { write-host "$($MainModule) CONNECTIVITY FAILED" -ForegroundColor Yellow write-host "" } ElseIf (!($ErrorsDetected)) { write-host "$($MainModule) CONNECTIVITY SUCCESS" -ForegroundColor Green write-host "" } Return $ErrorsDetected } |