Modules/Connection/Connection.psm1

function Connect-Tenant {
    <#
   .Description
   This function uses the various PowerShell modules to establish
   a connection to an M365 Tenant associated with provided
   credentials
   .Functionality
   Internal
   #>

   [CmdletBinding(DefaultParameterSetName='Manual')]
   param (
   [Parameter(ParameterSetName = 'Auto')]
   [Parameter(ParameterSetName = 'Manual')]
   [Parameter(Mandatory = $true)]
   [ValidateNotNullOrEmpty()]
   [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", IgnoreCase = $false)]
   [string[]]
   $ProductNames,

   [Parameter(ParameterSetName = 'Auto')]
   [Parameter(ParameterSetName = 'Manual')]
   [Parameter(Mandatory = $true)]
   [ValidateNotNullOrEmpty()]
   [ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
   [string]
   $M365Environment,

   [Parameter(ParameterSetName = 'Auto')]
   [Parameter(Mandatory = $false)]
   [AllowNull()]
   [hashtable]
   $ServicePrincipalParams
   )
   Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "ConnectHelpers.psm1")
   Import-Module -Name $PSScriptRoot/../Utility/Utility.psm1 -Function Invoke-GraphDirectly, ConvertFrom-GraphHashtable

   # Prevent duplicate sign ins
   $EXOAuthRequired = $true
   $SPOAuthRequired = $true
   $AADAuthRequired = $true

   $ProdAuthFailed = @()

   $N = 0
   $Len = $ProductNames.Length

   foreach ($Product in $ProductNames) {
       $N += 1
       $Percent = $N*100/$Len
       $ProgressParams = @{
           'Activity' = "Authenticating to each Product";
           'Status' = "Authenticating to $($Product); $($N) of $($Len) Products authenticated to.";
           'PercentComplete' = $Percent;
       }
       Write-Progress @ProgressParams
       try {
           switch ($Product) {
               "aad" {
                   $GraphScopes = Get-CyberAssessmentEntraMinimumPermissions

                   $GraphParams = @{
                       'M365Environment' = $M365Environment;
                       'Scopes' = $GraphScopes;
                   }
                   if($ServicePrincipalParams) {
                    $GraphParams += @{ServicePrincipalParams = $ServicePrincipalParams}
                   }
                   Connect-GraphHelper @GraphParams
                   $AADAuthRequired = $false
               }
               {($_ -eq "exo") -or ($_ -eq "defender")} {
                   if ($EXOAuthRequired) {
                       $EXOHelperParams = @{
                           M365Environment = $M365Environment;
                       }
                       if ($ServicePrincipalParams) {
                           $EXOHelperParams += @{ServicePrincipalParams = $ServicePrincipalParams}
                       }
                       Write-Verbose "Defender will require a sign in every single run regardless of what the LogIn parameter is set"
                       Connect-EXOHelper @EXOHelperParams
                       $EXOAuthRequired = $false
                   }
               }
               "powerplatform" {
                   $AddPowerAppsParams = @{
                       'ErrorAction' = 'Stop';
                   }
                   if ($ServicePrincipalParams.CertThumbprintParams) {
                       $AddPowerAppsParams += @{
                           CertificateThumbprint = $ServicePrincipalParams.CertThumbprintParams.CertificateThumbprint;
                           ApplicationId = $ServicePrincipalParams.CertThumbprintParams.AppID;
                           TenantID  = $ServicePrincipalParams.CertThumbprintParams.Organization; # Organization also works here
                       }
                   }
                   switch ($M365Environment) {
                       "commercial" {
                           $AddPowerAppsParams += @{'Endpoint'='prod';}
                       }
                       "gcc" {
                           $AddPowerAppsParams += @{'Endpoint'='usgov';}
                       }
                       "gcchigh" {
                           $AddPowerAppsParams += @{'Endpoint'='usgovhigh';}
                       }
                       "dod" {
                           $AddPowerAppsParams += @{'Endpoint'='dod';}
                       }
                   }
                   Add-PowerAppsAccount @AddPowerAppsParams | Out-Null

                   if ($AADAuthRequired) {
                        $LimitedGraphParams = @{
                            'M365Environment' = $M365Environment;
                            'ErrorAction' = 'Stop';
                        }
                        if ($ServicePrincipalParams) {
                            $LimitedGraphParams += @{ServicePrincipalParams = $ServicePrincipalParams }
                        }
                        Connect-GraphHelper @LimitedGraphParams
                        $AADAuthRequired = $false
                    }
               }
               "sharepoint" {
                   if ($AADAuthRequired) {
                       $LimitedGraphParams = @{
                           'M365Environment' = $M365Environment;
                           'ErrorAction' = 'Stop';
                       }
                       if ($ServicePrincipalParams) {
                           $LimitedGraphParams += @{ServicePrincipalParams = $ServicePrincipalParams }
                       }
                       Connect-GraphHelper @LimitedGraphParams
                       $AADAuthRequired = $false
                   }
                   if ($SPOAuthRequired) {
                       $InitialDomain = (Invoke-GraphDirectly -Commandlet "Get-MgBetaOrganization" -M365Environment $M365Environment).Value.VerifiedDomains | Where-Object {$_.isInitial}
                       $InitialDomainPrefix = $InitialDomain.Name.split(".")[0]
                       $SPOParams = @{
                           'ErrorAction' = 'Stop';
                       }
                       $PnPParams = @{
                           'ErrorAction' = 'Stop';
                       }

                       #pull api endpoint from json
                       $Url = Get-CyberAssessmentPermissions -Product "sharepoint" -Environment $M365Environment -Domain $InitialDomainPrefix -OutAs endpoint
                       $SPOParams += @{
                            'Url'= $Url;
                       }
                       $PnPParams += @{
                            'Url'= $Url;
                       }

                       #populate the rest of the parameters for splatting
                       switch ($M365Environment) {
                            "gcchigh" {
                                 $SPOParams += @{'Region' = "ITAR"; }
                                 $PnPParams += @{'AzureEnvironment' = 'USGovernmentHigh';}
                            }
                            "dod" {
                                 $SPOParams += @{'Region' = "ITAR"; }
                                 $PnPParams += @{'AzureEnvironment' = 'USGovernmentDoD';}
                            }

                       }
                       if ($ServicePrincipalParams.CertThumbprintParams) {
                           $PnPParams += @{
                               Thumbprint = $ServicePrincipalParams.CertThumbprintParams.CertificateThumbprint;
                               ClientId = $ServicePrincipalParams.CertThumbprintParams.AppID;
                               Tenant  = $ServicePrincipalParams.CertThumbprintParams.Organization; # Organization Domain is actually required here.
                           }
                           $env:PNPPOWERSHELL_UPDATECHECK = "false"  # disable PnP update banner
                           Connect-PnPOnline @PnPParams | Out-Null
                       }
                       else {
                           Connect-SPOService @SPOParams | Out-Null
                       }
                       $SPOAuthRequired = $false
                   }
               }
               "teams" {
                   $TeamsParams = @{'ErrorAction'= 'Stop'}
                   if ($ServicePrincipalParams.CertThumbprintParams) {
                       $TeamsConnectToTenant = @{
                           CertificateThumbprint = $ServicePrincipalParams.CertThumbprintParams.CertificateThumbprint;
                           ApplicationId = $ServicePrincipalParams.CertThumbprintParams.AppID;
                           TenantId  = $ServicePrincipalParams.CertThumbprintParams.Organization; # Organization Domain is actually required here.
                       }
                       $TeamsParams += $TeamsConnectToTenant
                   }
                   switch ($M365Environment) {
                       "gcchigh" {
                           $TeamsParams += @{'TeamsEnvironmentName'= 'TeamsGCCH';}
                       }
                       "dod" {
                           $TeamsParams += @{'TeamsEnvironmentName'= 'TeamsDOD';}
                       }
                   }
                   Connect-MicrosoftTeams @TeamsParams | Out-Null
               }
               default {
                   throw "Invalid ProductName argument"
               }
           }
       }
       catch {
           Write-Warning "Error establishing a connection with $($Product): $($_.Exception.Message)`n$($_.ScriptStackTrace)"
           $ProdAuthFailed += $Product
           Write-Warning "$($Product) will be omitted from the output because of failed authentication"
       }
   }
   Write-Progress -Activity "Authenticating to each service" -Status "Ready" -Completed
   $ProdAuthFailed
}

function Disconnect-CyberAssessmentTenant {
   <#
   .SYNOPSIS
       Disconnect all active M365 connection sessions made by CyberAssessment
   .DESCRIPTION
       Forces disconnect of all outstanding open sessions associated with
       M365 product APIs within the current PowerShell session.
       Best used after an CyberAssessment run to ensure a new tenant connection is
       used for future CyberAssessment runs.
   .Parameter ProductNames
   A list of one or more M365 shortened product names this function will disconnect from. By default this function will disconnect from all possible products CyberAssessment can run against.
   .EXAMPLE
   Disconnect-CyberAssessmentTenant
   .EXAMPLE
   Disconnect-CyberAssessmentTenant -ProductNames teams
   .EXAMPLE
   Disconnect-CyberAssessmentTenant -ProductNames aad, exo
   .Functionality
   Public
   #>

   [CmdletBinding()]
   param(
       [ValidateSet("aad", "defender", "exo","powerplatform", "sharepoint", "teams", IgnoreCase = $false)]
       [ValidateNotNullOrEmpty()]
       [string[]]
       $ProductNames = @("aad", "defender", "exo", "powerplatform", "sharepoint", "teams")
   )
   $ErrorActionPreference = "SilentlyContinue"

   try {
       $N = 0
       $Len = $ProductNames.Length

       foreach ($Product in $ProductNames) {
           $N += 1
           $Percent = $N*100/$Len
           Write-Progress -Activity "Disconnecting from each service" -Status "Disconnecting from $($Product); $($n) of $($Len) disconnected." -PercentComplete $Percent
           Write-Verbose "Disconnecting from $Product."
           if (($Product -eq "aad") -or ($Product -eq "sharepoint")) {
               Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null

               if($Product -eq "sharepoint") {
                   Disconnect-SPOService -ErrorAction SilentlyContinue
                   Disconnect-PnPOnline -ErrorAction SilentlyContinue
               }
           }
           elseif ($Product -eq "teams") {
               Disconnect-MicrosoftTeams -Confirm:$false -ErrorAction SilentlyContinue
           }
           elseif ($Product -eq "powerplatform") {
               Remove-PowerAppsAccount -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
           }
           elseif (($Product -eq "exo") -or ($Product -eq "defender")) {
               if($Product -eq "defender") {
                   Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
               }
               Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue -InformationAction SilentlyContinue | Out-Null
           }
           else {
               Write-Warning "Product $Product not recognized, skipping..."
           }
       }
       Write-Progress -Activity "Disconnecting from each service" -Status "Done" -Completed

   } catch [System.InvalidOperationException] {
       # Suppress error due to disconnect from service with no active connection
       continue
   } catch {
       Write-Warning "Could not disconnect from $Product`n: $($_.Exception.Message)`n$($_.ScriptStackTrace)"
   } finally {
       $ErrorActionPreference = "Continue"
   }

}

Export-ModuleMember -Function @(
   'Connect-Tenant',
   'Disconnect-CyberAssessmentTenant'
)