3737 Import-Module - Name $PSScriptRoot / ../ Providers/ ProviderHelpers/ PowerPlatformRestHelper.psm1 - Function Get-PowerPlatformBaseUrl , Get-PowerPlatformScope
3838 Import-Module - Name $PSScriptRoot / ../ Providers/ ProviderHelpers/ SPORestHelper.psm1 - Function Get-SPOAdminUrl
3939 Import-Module - Name $PSScriptRoot / ../ Providers/ ProviderHelpers/ PowerBIRestHelper.psm1 - Function Get-PowerBIBaseUrl , Get-PowerBIScope
40+ Import-Module - Name $PSScriptRoot / ../ Providers/ ProviderHelpers/ EXORestHelper.psm1 - Function Get-ExchangeOnlineScope , Get-ExchangeOnlineApiEndpoint
4041
4142 # Prevent duplicate sign ins
4243 $EXOAuthRequired = $true
5556
5657 # Token data for REST-based products (populated during connection)
5758 $TokenData = @ {
58- SPOAccessToken = $null
59- SPOAdminUrl = $null
60- PPAccessToken = $null
61- PPBaseUrl = $null
62- PBIAccessToken = $null
63- PBIBaseUrl = $null
59+ SPOAccessToken = $null
60+ SPOAdminUrl = $null
61+ PPAccessToken = $null
62+ PPBaseUrl = $null
63+ PBIAccessToken = $null
64+ PBIBaseUrl = $null
65+ EXOAccessToken = $null
66+ EXOApiEndpoint = $null
6467 }
6568
6669 $N = 0
9598 }
9699 {($_ -eq " exo" ) -or ($_ -eq " securitysuite" )} {
97100 if ($EXOAuthRequired ) {
98- $EXOHelperParams = @ {
99- M365Environment = $M365Environment ;
101+ if ($AADAuthRequired ) {
102+ $LimitedGraphParams = @ {
103+ ' M365Environment' = $M365Environment ;
104+ ' ErrorAction' = ' Stop' ;
105+ }
106+ if ($ServicePrincipalParams ) {
107+ $LimitedGraphParams += @ {ServicePrincipalParams = $ServicePrincipalParams }
108+ }
109+ Connect-GraphHelper @LimitedGraphParams
110+ $AADAuthRequired = $false
100111 }
101- if ($ServicePrincipalParams ) {
102- $EXOHelperParams += @ {ServicePrincipalParams = $ServicePrincipalParams }
112+
113+ # Resolve tenant info if not already cached
114+ if ([string ]::IsNullOrEmpty($TenantName )) {
115+ $OrgDetails = (Invoke-GraphDirectly - Commandlet Get-MgBetaOrganization - M365Environment $M365Environment ).Value
116+ $InitialDomain = $OrgDetails.VerifiedDomains | Where-Object { $_.isInitial }
117+ $TenantName = $InitialDomain.Name
118+ $InitialDomainPrefix = $TenantName.split (" ." )[0 ]
119+ }
120+
121+ # Acquire Exchange Online access token
122+ $EXOScope = Get-ExchangeOnlineScope - M365Environment $M365Environment
123+
124+ if ($ServicePrincipalParams.CertThumbprintParams ) {
125+ $TokenData.EXOAccessToken = Get-MsalAccessToken `
126+ - Scope $EXOScope `
127+ - CertificateThumbprint $ServicePrincipalParams.CertThumbprintParams.CertificateThumbprint `
128+ - AppID $ServicePrincipalParams.CertThumbprintParams.AppID `
129+ - Tenant $ServicePrincipalParams.CertThumbprintParams.Organization `
130+ - M365Environment $M365Environment
131+ }
132+ else {
133+ # Microsoft Exchange Online Remote PowerShell well-known client ID
134+ $EXOClientId = " fb78d390-0c51-40cd-8e17-fdbfab77341b"
135+ $TokenData.EXOAccessToken = Get-MsalAccessToken `
136+ - Scope $EXOScope `
137+ - ClientId $EXOClientId `
138+ - Tenant $TenantName `
139+ - M365Environment $M365Environment
140+ }
141+
142+ # Resolve the EXO API endpoint (handles redirects)
143+ $TenantId = (Invoke-GraphDirectly - Commandlet Get-MgBetaOrganization - M365Environment $M365Environment ).Value.Id
144+ $TokenData.EXOApiEndpoint = Get-ExchangeOnlineApiEndpoint `
145+ - TenantId $TenantId `
146+ - TenantDomain $TenantName `
147+ - M365Environment $M365Environment `
148+ - AccessToken $TokenData.EXOAccessToken
149+
150+ # EXO product checks use REST; only establish an EXO module session
151+ # when Security Suite is part of the requested product list.
152+ if ($ProductNames -contains " securitysuite" ) {
153+ $EXOHelperParams = @ { M365Environment = $M365Environment }
154+ if ($ServicePrincipalParams.CertThumbprintParams ) {
155+ $EXOHelperParams += @ { ServicePrincipalParams = $ServicePrincipalParams }
156+ }
157+ Connect-EXOHelper @EXOHelperParams
103158 }
104- Write-Verbose " For the Security Suite baseline, Defender will require a sign in every single run regardless of what the LogIn parameter is set "
105- Connect-EXOHelper @EXOHelperParams
159+
160+ Write-Verbose " Exchange Online token and endpoint acquired successfully "
106161 $EXOAuthRequired = $false
107162 }
108163 }
348403 PPBaseUrl = $TokenData.PPBaseUrl
349404 PBIAccessToken = $TokenData.PBIAccessToken
350405 PBIBaseUrl = $TokenData.PBIBaseUrl
406+ EXOAccessToken = $TokenData.EXOAccessToken
407+ EXOApiEndpoint = $TokenData.EXOApiEndpoint
351408 }
352409}
353410
@@ -408,7 +465,8 @@ function Disconnect-SCuBATenant {
408465 if ($Product -eq " securitysuite" ) {
409466 Disconnect-MgGraph - ErrorAction SilentlyContinue | Out-Null
410467 }
411- Disconnect-ExchangeOnline - Confirm:$false - ErrorAction SilentlyContinue - InformationAction SilentlyContinue | Out-Null
468+ # EXO now uses REST API with on-demand token - no persistent connection to disconnect
469+ Disconnect-MgGraph - ErrorAction SilentlyContinue | Out-Null
412470 }
413471 else {
414472 Write-Warning " Product $Product not recognized, skipping..."
@@ -427,7 +485,86 @@ function Disconnect-SCuBATenant {
427485
428486}
429487
488+ function Get-ServicePrincipalParams {
489+ <#
490+ . Description
491+ Returns a valid a hastable of parameters for authentication via
492+ Service Principal. Throws an error if there are none.
493+ . Functionality
494+ Internal
495+ #>
496+ [CmdletBinding ()]
497+ param (
498+ [Parameter (Mandatory = $true )]
499+ [ValidateNotNullOrEmpty ()]
500+ [object ]
501+ $ScubaConfig
502+ )
503+
504+ $ServicePrincipalParams = @ {}
505+
506+ $CheckThumbprintParams = ($ScubaConfig.CertificateThumbprint ) `
507+ -and ($ScubaConfig.AppID ) -and ($ScubaConfig.Organization )
508+
509+ if ($CheckThumbprintParams ) {
510+ $CertThumbprintParams = @ {
511+ CertificateThumbprint = $ScubaConfig.CertificateThumbprint ;
512+ AppID = $ScubaConfig.AppID ;
513+ Organization = $ScubaConfig.Organization ;
514+ }
515+ $ServicePrincipalParams += @ {CertThumbprintParams = $CertThumbprintParams }
516+ }
517+ else {
518+ throw " When authenticating with Service Principal authentication, the following command line parameters must be provided: -AppID, -CertificateThumbprint and -Organization."
519+ }
520+ $ServicePrincipalParams
521+ }
522+
523+ function Get-M365EnvironmentByDomain {
524+ <#
525+ . SYNOPSIS
526+ Determines the M365 environment based on the tenant domain.
527+
528+ . DESCRIPTION
529+ Determines the M365 environment based on the tenant domain.
530+
531+ . PARAMETER TenantDomain
532+ The domain of the tenant for which to determine the environment.
533+
534+ . EXAMPLE
535+ $M365Environment = Get-M365EnvironmentByDomain -TenantDomain "contoso.onmicrosoft.com"
536+
537+ . FUNCTIONALITY
538+ Internal
539+ #>
540+ [CmdletBinding (DefaultParameterSetName = ' Interactive' )]
541+ param (
542+ [Parameter (Mandatory = $true )]
543+ [string ]$TenantDomain
544+ )
545+
546+ $MetadataUri = " https://login.microsoftonline.com/$TenantDomain /.well-known/openid-configuration"
547+
548+ $Metadata = Invoke-RestMethod - Uri $MetadataUri - Method Get - ErrorAction Stop
549+
550+ $TenantRegionSubScope = $Metadata.tenant_region_sub_scope
551+
552+ $M365Environment = switch ($TenantRegionSubScope ) {
553+ " DODCON" { " gcchigh" }
554+ " GCC" { " gcc" }
555+ " DOD" { " dod" }
556+ $null { " commercial" }
557+ default {
558+ throw " Unknown tenant_region_sub_scope value: '$TenantRegionSubScope '"
559+ }
560+ }
561+
562+ return $M365Environment
563+ }
564+
430565Export-ModuleMember - Function @ (
431- ' Connect-Tenant' ,
432- ' Disconnect-SCuBATenant'
566+ ' Connect-Tenant' ,
567+ ' Disconnect-SCuBATenant' ,
568+ ' Get-ServicePrincipalParams' ,
569+ ' Get-M365EnvironmentByDomain'
433570)
0 commit comments