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
}