Public/Core/Connect-EntraOps.ps1
|
<#
.SYNOPSIS Establishes connections to required PowerShell modules and requests EntraOps access tokens. .DESCRIPTION Connection to Azure Resource Management and Microsoft Graph API by using Connect-AzAccount and Connect-MgGraph. .PARAMETER AuthenticationType Type of authentication to be used for Azure and Microsoft Graph. Default is "AlreadyAuthenticated". .PARAMETER UseInvokeRestMethodOnly Use Invoke-RestMethod instead of the Microsoft Graph SDK (Invoke-MgGraphRequest) for all Invoke-EntraOps*Query cmdlets in this session. The Microsoft Graph SDK is neither required nor installed in this mode; tokens are provided via -MsGraphAccessToken or acquired with Get-AzAccessToken from the Az PowerShell context. Can also be set through the config file (top-level setting "UseInvokeRestMethodOnly"); an explicit parameter wins over the config file. Note: parallel object resolution requires the Graph SDK and falls back to sequential processing in this mode. Connect-EntraOps itself also skips Connect-MgGraph for service-principal/managed- identity AuthenticationType values (SystemAssignedMSI, UserAssignedMSI, FederatedCredentials, AlreadyAuthenticated) in this mode - their Graph tokens come from Get-AzAccessToken instead, since application permissions are fully contained in a client-credentials token without needing per-request scopes. UserInteractive and DeviceAuthentication still call Connect-MgGraph (and thus still require Microsoft.Graph.Authentication) even with this switch enabled, because only Connect-MgGraph -Scopes can trigger the interactive/incremental consent prompt that delegated Graph scopes require; a warning is emitted when this fallback applies. .EXAMPLE Using Interactive Sign-In of User with double authentication to Az PowerShell (Connect-AzAccount) and Microsoft Graph SDK (Connect-MgGraph) Connect-EntraOps -AuthenticationType "UserInteractive" -TenantName "contoso.onmicrosoft.com" .EXAMPLE Using authenticated session to Az PowerShell (Connect-AzAccount) in GitHub workflow or any other workload identity environment to request access token for Microsoft Graph SDK (by Get-AzAccessToken) without any further initial authentication. Connect-EntraOps -AuthenticationType "AlreadyAuthenticated" -TenantName "contoso.onmicrosoft.com" .EXAMPLE Using Managed Identity (User Assigned) to sign-in to Azure and Microsoft Graph Connect-EntraOps -AuthenticationType "UserAssignedMSI" -AccountId "00000000-0000-0000-0000-000000000000" -TenantName "contoso.onmicrosoft.com" #> function Connect-EntraOps { # -MsGraphAccessToken is a plaintext [string] by public contract (populated from workflow # secrets/federated tokens in CI pipelines); Connect-MgGraph -AccessToken requires a # SecureString, so the one-time in-memory conversion below is unavoidable without a breaking # change to the parameter type. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'MsGraphAccessToken is an existing public string parameter fed by external pipeline secrets; converting it in memory for Connect-MgGraph -AccessToken does not expose a new plaintext secret, and changing the parameter to SecureString would break the public API.')] [cmdletbinding()] param ( [Parameter(Mandatory = $False)] [ValidateSet('UserInteractive', 'SystemAssignedMSI', 'UserAssignedMSI', 'FederatedCredentials', 'AlreadyAuthenticated', 'DeviceAuthentication')] [System.String]$AuthenticationType = "AlreadyAuthenticated" , [Parameter(Mandatory = $False)] [ValidateSet("beta", "v1.0")] [System.String]$GraphApiVersion = "beta" , [Parameter(Mandatory = $False)] [System.String]$AccountId , [Parameter(Mandatory = $True)] [ValidatePattern('^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$')] [System.String]$TenantName , [Parameter(Mandatory = $False)] [ValidatePattern('^$|^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')] [System.String]$TenantId , [Parameter(Mandatory = $False)] [ValidatePattern('^$|^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')] [System.String]$ManagingTenantId , [Parameter(Mandatory = $False)] [System.String]$ManagingTenantName , [Parameter(Mandatory = $False)] [boolean]$MultiTenantRepo = $false , [Parameter(Mandatory = $False)] [System.String]$AzArmAccessToken , [Parameter(Mandatory = $False)] [System.String]$MsGraphAccessToken , [Parameter(Mandatory = $False)] [ValidateScript({ Test-Path $_ })] [System.String]$ConfigFilePath , [Parameter(Mandatory = $False)] [switch]$NoWelcome , [Parameter(Mandatory = $False)] [switch]$UseInvokeRestMethodOnly ) Process { $ErrorActionPreference = "Stop" # Display welcome banner unless suppressed if (-not $NoWelcome) { # Robustly resolve module manifest path $ManifestPath = if ($MyInvocation.MyCommand.Module.ModuleBase) { Join-Path $MyInvocation.MyCommand.Module.ModuleBase "EntraOps.psd1" } else { # Fallback to relative path if module context is missing (e.g. running script directly) "$PSScriptRoot/../../EntraOps.psd1" } if (Test-Path $ManifestPath) { $ModuleManifest = Import-PowerShellDataFile $ManifestPath $ModuleVersion = $($ModuleManifest.ModuleVersion) } else { $ModuleVersion = "Unknown" } $PSEnvironment = "PowerShell " + ($PSVersionTable.PSEdition) + " " + ($PSVersionTable.PSVersion.ToString()) $Splash = @" ______ _ ____ | ____| | | / __ \ | |__ _ __ | |_ _ __ __ _| | | |_ __ ___ | __| | '_ \| __| '__/ _` | | | | '_ \/ __| | |____| | | | |_| | | (_| | |__| | |_) \__ \ |______|_| |_|\__|_| \__,_|\____/| .__/|___/ | | |_| Version $($ModuleVersion) on $($PSEnvironment) Community Project by Thomas Naunheim - www.entraops.com "@ Write-Host $Splash -ForegroundColor Blue } #region Load tenant and managing tenant settings from config file if not provided as parameters if ($ConfigFilePath -and (Test-Path $ConfigFilePath)) { $EarlyConfig = Get-Content -Path $ConfigFilePath | ConvertFrom-Json if ([string]::IsNullOrEmpty($TenantId) -and -not [string]::IsNullOrEmpty($EarlyConfig.TenantId)) { $TenantId = $EarlyConfig.TenantId } if ([string]::IsNullOrEmpty($ManagingTenantId) -and -not [string]::IsNullOrEmpty($EarlyConfig.ManagingTenantId)) { $ManagingTenantId = $EarlyConfig.ManagingTenantId } if ([string]::IsNullOrEmpty($ManagingTenantName) -and -not [string]::IsNullOrEmpty($EarlyConfig.ManagingTenantName)) { $ManagingTenantName = $EarlyConfig.ManagingTenantName } } #endregion #region Switch between Microsoft Graph SDK (Invoke-MgGraphRequest) and Azure PowerShell only in combination with Invoke-RestMethod # Resolve the effective mode: explicit parameter > config file setting > default ($false) if (-not $PSBoundParameters.ContainsKey('UseInvokeRestMethodOnly') -and $null -ne $EarlyConfig.UseInvokeRestMethodOnly) { $UseInvokeRestMethodOnly = [bool]$EarlyConfig.UseInvokeRestMethodOnly Write-Verbose "UseInvokeRestMethodOnly set from config file: $UseInvokeRestMethodOnly" } # The module-private session store is the authoritative home of the mode - all # Invoke-EntraOps*Query cmdlets read it from there (a user-set global variable is # only honored as legacy fallback when the session store has no value) $__EntraOpsSession['UseInvokeRestMethodOnly'] = [bool]$UseInvokeRestMethodOnly # Clean up the flag persisted as global variable by previous module versions Remove-Variable -Name UseInvokeRestMethodOnly -Scope Global -Force -ErrorAction SilentlyContinue # Start each connection with fresh token state: cached tokens from a previous connection may # belong to another tenant and would otherwise be re-used until they expire. Tenant-keyed ARM # token entries stay valid; only the context-dependent 'default' entry must go. $__EntraOpsSession.MsGraphTokenCache.Clear() $__EntraOpsSession.ArmTokenCache.Remove('default') # Persist an explicitly provided Microsoft Graph access token in the module-private session # store so the Invoke-RestMethod path of Invoke-EntraOpsMsGraphQuery can use it (required for # token-based authentication where the Az context cannot mint new Graph tokens itself). # Deliberately not a global variable: module scope keeps the token out of the user-visible # session state (Get-Variable, transcripts, diagnostic dumps). if (-not [string]::IsNullOrEmpty($MsGraphAccessToken)) { # Read the real expiry from the JWT exp claim; fall back to a conservative default $ProvidedTokenExpiry = [DateTime]::UtcNow.AddMinutes(50) try { $PayloadPart = ($MsGraphAccessToken -split '\.')[1].Replace('-', '+').Replace('_', '/') switch ($PayloadPart.Length % 4) { 2 { $PayloadPart += '==' } 3 { $PayloadPart += '=' } } $TokenPayload = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($PayloadPart)) | ConvertFrom-Json if ($TokenPayload.exp) { $ProvidedTokenExpiry = [DateTimeOffset]::FromUnixTimeSeconds($TokenPayload.exp).UtcDateTime } } catch { Write-Verbose "Could not parse expiry from provided Microsoft Graph access token: $($_.Exception.Message)" } $__EntraOpsSession.MsGraphTokenCache['provided'] = @{ Token = $MsGraphAccessToken; Expiry = $ProvidedTokenExpiry } # Clean up a token persisted as global variable by previous module versions Remove-Variable -Name MsGraphAccessToken -Scope Global -Force -ErrorAction SilentlyContinue } # Azure authentication and ARM access are required in every connection mode. @( @{ ModuleName = 'Az.Accounts'; ModuleVersion = '2.19.0' } @{ ModuleName = 'Az.Resources'; ModuleVersion = '6.16.2' } ) | ForEach-Object { Install-EntraOpsRequiredModule -ModuleName $_.ModuleName -MinimalVersion $_.ModuleVersion } # The Microsoft Graph SDK is only required (and installed) when it is actually used if (-not $__EntraOpsSession['UseInvokeRestMethodOnly']) { $RequiredCoreModules = @{ ModuleName = 'Microsoft.Graph.Authentication' ModuleVersion = '2.0.0' } # Recommendation 1: Validate module availability before installation check $RequiredCoreModules | ForEach-Object { if (-not (Get-Module -Name $_.ModuleName)) { Install-EntraOpsRequiredModule -ModuleName $_.ModuleName -MinimalVersion $_.ModuleVersion } } $Scopes = @( "AdministrativeUnit.Read.All", "Application.Read.All", "CustomSecAttributeAssignment.Read.All", "DeviceManagementConfiguration.Read.All", "DeviceManagementManagedDevices.Read.All", "DeviceManagementRBAC.Read.All", "DeviceManagementServiceConfig.Read.All", "Directory.Read.All", "DirectoryRecommendations.Read.All", "EntitlementManagement.Read.All", "Group.Read.All", "PrivilegedAccess.Read.AzureADGroup", "PrivilegedEligibilitySchedule.Read.AzureADGroup", "Policy.Read.All", "RemoteTenantGroups.Read.All", "RoleManagement.Read.All", "TenantGovernance-Relationship.Read.All", "ThreatHunting.Read.All", "User.Read.All", "Zone.Read.All" ) } #endregion # In UseInvokeRestMethodOnly mode, Invoke-EntraOps*Query cmdlets acquire and cache their own # Microsoft Graph token via Get-AzAccessToken, so Connect-MgGraph is unnecessary for the # service-principal/managed-identity authentication types below: their Graph permissions are # Application permissions already admin-consented to the app, and a client-credentials token # always contains every granted app role (no per-request scope negotiation needed). # UserInteractive/DeviceAuthentication are the exception: Get-AzAccessToken cannot trigger the # interactive/incremental consent prompt that delegated Graph scopes require, so those two # authentication types still fall back to Connect-MgGraph (and therefore still require # Microsoft.Graph.Authentication to be installed) even when UseInvokeRestMethodOnly is enabled. $SkipGraphSdkAuth = [bool]$__EntraOpsSession['UseInvokeRestMethodOnly'] -and ($AuthenticationType -notin @('UserInteractive', 'DeviceAuthentication')) if ($__EntraOpsSession['UseInvokeRestMethodOnly'] -and $AuthenticationType -in @('UserInteractive', 'DeviceAuthentication')) { Write-Warning "UseInvokeRestMethodOnly is enabled, but AuthenticationType '$AuthenticationType' still requires Connect-MgGraph (Microsoft.Graph.Authentication module) as a fallback: Get-AzAccessToken cannot trigger the interactive/incremental admin-consent prompt needed for delegated Microsoft Graph scopes, only Connect-MgGraph -Scopes can. Use a service-principal-based AuthenticationType (SystemAssignedMSI, UserAssignedMSI, FederatedCredentials or AlreadyAuthenticated) to avoid this Graph SDK dependency, or provide an already-consented -MsGraphAccessToken." } #region Switch to choose authentication method for Azure and Microsoft Graph switch ( $AuthenticationType ) { UserInteractive { try { # Pre-authenticate to managing tenant for cross-tenant access if (-not [string]::IsNullOrEmpty($ManagingTenantId)) { Write-Output "Pre-authenticating to managing tenant (Azure)..." Connect-AzAccount -Tenant $ManagingTenantId -ErrorAction Stop | Out-Null Write-Output "Pre-authenticating to managing tenant (Microsoft Graph) using Az token..." $SecureManagingAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $ManagingTenantId -AsSecureString -ErrorAction Stop).Token Connect-MgGraph -AccessToken $SecureManagingAccessToken -NoWelcome -ErrorAction Stop Write-Output "Successfully pre-authenticated to managing tenant $ManagingTenantId" } Write-Output "Logging in to Azure..." Connect-AzAccount -Tenant $TenantName -ErrorAction Stop | Out-Null if ($TenantId -ne (Get-AzContext).Tenant.Id -or $null -eq $TenantId) { $TenantId = (Get-AzContext).Tenant.Id } Write-Output "Succesfully logged in to Azure" Write-Output "Logging in to Microsoft Graph..." Connect-MgGraph -TenantId $TenantId -NoWelcome -ErrorAction Stop -Scopes $Scopes Write-Output "Succesfully logged in to Microsoft Graph" } catch { Write-Error -Message $_.Exception throw $_.Exception } } DeviceAuthentication { try { # Pre-authenticate to managing tenant for cross-tenant access if (-not [string]::IsNullOrEmpty($ManagingTenantId)) { Write-Output "Pre-authenticating to managing tenant (Azure)..." Connect-AzAccount -Tenant $ManagingTenantId -ErrorAction Stop -UseDeviceAuthentication | Out-Null Write-Output "Pre-authenticating to managing tenant (Microsoft Graph) using Az token..." $SecureManagingAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $ManagingTenantId -AsSecureString -ErrorAction Stop).Token Connect-MgGraph -AccessToken $SecureManagingAccessToken -NoWelcome -ErrorAction Stop Write-Output "Successfully pre-authenticated to managing tenant $ManagingTenantId" } Write-Output "Logging in to Azure..." Connect-AzAccount -Tenant $TenantName -ErrorAction Stop -UseDeviceAuthentication | Out-Null if ($TenantId -ne (Get-AzContext).Tenant.Id -or $null -eq $TenantId) { $TenantId = (Get-AzContext).Tenant.Id } Write-Output "Succesfully logged in to Azure" Write-Output "Logging in to Microsoft Graph..." Connect-MgGraph -TenantId $TenantId -NoWelcome -ErrorAction Stop -Scopes $Scopes -UseDeviceAuthentication Write-Output "Succesfully logged in to Microsoft Graph" } catch { Write-Error -Message $_.Exception throw $_.Exception } } SystemAssignedMSI { try { Write-Output "Logging in to Azure..." Connect-AzAccount -Identity -ErrorAction Stop Write-Output "Succesfully logged in to Azure" if (-not $SkipGraphSdkAuth) { Write-Output "Logging in to Microsoft Graph..." Connect-MgGraph -Identity -ErrorAction Stop -NoWelcome Write-Output "Succesfully logged in to Microsoft Graph" } else { Write-Verbose "UseInvokeRestMethodOnly is enabled: skipping Connect-MgGraph, Microsoft Graph tokens will be acquired via Get-AzAccessToken when needed." } } catch { Write-Error -Message $_.Exception throw $_.Exception } } UserAssignedMSI { try { Write-Output "Logging in to Azure..." Connect-AzAccount -Identity -AccountId $AccountId -ErrorAction Stop Write-Output "Succesfully logged in to Azure" if (-not $SkipGraphSdkAuth) { Write-Output "Logging in to Microsoft Graph..." Connect-MgGraph -Identity -ClientId $AccountId -NoWelcome -ErrorAction Stop Write-Output "Succesfully logged in to Microsoft Graph" } else { Write-Verbose "UseInvokeRestMethodOnly is enabled: skipping Connect-MgGraph, Microsoft Graph tokens will be acquired via Get-AzAccessToken when needed." } } catch { Write-Error -Message $_.Exception throw $_.Exception } } FederatedCredentials { if ($Null -eq (Get-AzContext).Tenant.Id) { throw "Federated environment is not already authenticated" } try { # Pre-authenticate to managing tenant for cross-tenant access if (-not [string]::IsNullOrEmpty($ManagingTenantId)) { $SecureManagingAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $ManagingTenantId -AsSecureString).Token if (-not $SkipGraphSdkAuth) { Write-Output "Pre-authenticating to managing tenant (Microsoft Graph)..." Connect-MgGraph -AccessToken $SecureManagingAccessToken -ErrorAction Stop -NoWelcome Write-Output "Successfully pre-authenticated to managing tenant $ManagingTenantId" } } # Connect to target tenant if (-not [string]::IsNullOrEmpty($TenantId)) { $SecureAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $TenantId -AsSecureString).Token } else { $SecureAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -AsSecureString).Token } if (-not $SkipGraphSdkAuth) { Connect-MgGraph -AccessToken $SecureAccessToken -ErrorAction Stop -NoWelcome } else { Write-Verbose "UseInvokeRestMethodOnly is enabled: skipping Connect-MgGraph, Microsoft Graph tokens will be acquired via Get-AzAccessToken when needed." } # Pre-warm an ARM access token too (same pattern as above), while the GitHub OIDC # federated assertion behind this Az context is still fresh (it's only valid for a # few minutes). Az.Accounts caches acquired tokens per resource/tenant across the # job's remaining steps, so a later step's first-ever ARM request (e.g. Azure RBAC # collection) is served from this cached token instead of needing a new assertion # exchange once it has expired. Non-fatal: callers that never touch Azure RBAC # should not fail Connect-EntraOps over this optimization. try { if (-not [string]::IsNullOrEmpty($TenantId)) { $SecureArmAccessToken = (Get-AzAccessToken -ResourceUrl "https://management.azure.com/" -TenantId $TenantId -AsSecureString).Token } else { $SecureArmAccessToken = (Get-AzAccessToken -ResourceUrl "https://management.azure.com/" -AsSecureString).Token } } catch { Write-Verbose "Could not pre-warm an Azure Resource Manager access token: $($_.Exception.Message)" } } catch { throw $_.Exception } } AlreadyAuthenticated { # Recommendation 2: Optimize context retrieval to avoid redundant cmdlet calls $CurrentAzContext = Get-AzContext $CurrentMgContext = if (-not $SkipGraphSdkAuth) { Get-MgContext } else { $null } # Pre-authenticate to managing tenant for cross-tenant access if (-not [string]::IsNullOrEmpty($ManagingTenantId) -and $Null -ne $CurrentAzContext.Tenant.Id) { try { $SecureManagingAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $ManagingTenantId -AsSecureString).Token if (-not $SkipGraphSdkAuth) { Write-Output "Pre-authenticating to managing tenant (Microsoft Graph)..." Connect-MgGraph -AccessToken $SecureManagingAccessToken -ErrorAction Stop -NoWelcome Write-Output "Successfully pre-authenticated to managing tenant $ManagingTenantId" } } catch { Write-Warning "Failed to pre-authenticate to managing tenant: $($_.Exception.Message)" } } if ($AccountId -and $MsGraphAccessToken -and $AzArmAccessToken) { Connect-AzAccount -AccountId $AccountId -AccessToken $AzArmAccessToken -Tenant $TenantName if (-not $SkipGraphSdkAuth) { $SecureMsGraphAccessToken = $MsGraphAccessToken | ConvertTo-SecureString -AsPlainText -Force Connect-MgGraph -AccessToken $SecureMsGraphAccessToken -NoWelcome } } elseif ($Null -ne $CurrentAzContext.Tenant.Id -and $Null -ne $CurrentMgContext.TenantId) { try { $SecureAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -AsSecureString).Token Connect-MgGraph -AccessToken $SecureAccessToken -ErrorAction Stop -NoWelcome } catch { $ErrorMessage = if ($null -ne $_.Exception.Message) { $_.Exception.Message } else { $_.ToString() } throw "Failed to connect to Microsoft Graph using Azure access token: $ErrorMessage" } } elseif ($Null -ne $CurrentAzContext.Tenant.Id) { if ($SkipGraphSdkAuth) { Write-Verbose "UseInvokeRestMethodOnly is enabled: skipping Connect-MgGraph, Microsoft Graph tokens will be acquired via Get-AzAccessToken when needed." } else { try { $SecureAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -AsSecureString).Token Connect-MgGraph -AccessToken $SecureAccessToken -ErrorAction Stop -NoWelcome } catch { $ErrorMessage = if ($null -ne $_.Exception.Message) { $_.Exception.Message } else { $_.ToString() } throw "Failed to connect to Microsoft Graph using Azure access token: $ErrorMessage" } } } else { Write-Error -Message 'User or workload is not already authenticated. This authentication method is the default for EntraOps. Check "Get-Help Connect-EntraOps" to review the various options. Authenticated Azure PowerShell session is required for using "AlreadyAuthenticated" mode.' } } } #endregion #region Verify connection to target tenant if (-not [string]::IsNullOrEmpty($TenantId)) { $VerifyAzContext = Get-AzContext if ($VerifyAzContext -and $VerifyAzContext.Tenant.Id -ne $TenantId) { Write-Output "Azure context is on tenant $($VerifyAzContext.Tenant.Id), switching to target tenant $TenantId..." Set-AzContext -TenantId $TenantId -ErrorAction Stop | Out-Null Write-Output "Successfully switched Azure context to target tenant" } $VerifyMgContext = if (-not $SkipGraphSdkAuth) { Get-MgContext } else { $null } if ($VerifyMgContext -and $VerifyMgContext.TenantId -ne $TenantId) { Write-Output "Microsoft Graph is connected to $($VerifyMgContext.TenantId), reconnecting to target tenant $TenantId..." try { $SecureTargetAccessToken = (Get-AzAccessToken -ResourceTypeName "MSGraph" -TenantId $TenantId -AsSecureString).Token Connect-MgGraph -AccessToken $SecureTargetAccessToken -ErrorAction Stop -NoWelcome Write-Output "Successfully reconnected Microsoft Graph to target tenant" } catch { Write-Warning "Could not reconnect Microsoft Graph to target tenant ${TenantId}: $($_.Exception.Message)" } } } #endregion #region Summary of established connection to ARM and Microsoft Graph API # Recommendation 3: Optimize context retrieval and verbose output generation Write-Verbose -Message "Connected to Azure Management" $AzContextRaw = Get-AzContext $AzContext = $AzContextRaw | Select-Object Account, Tenant, TokenCache Write-Verbose ($AzContext | Out-String) # Retrieve MG context once (skipped when Connect-MgGraph was itself skipped in REST-only mode) $MgContextRaw = if (-not $SkipGraphSdkAuth) { Get-MgContext } else { $null } if ($MgContextRaw) { Write-Verbose -Message "Connected to Microsoft Graph" } else { Write-Verbose -Message "Not connected via Microsoft Graph SDK (UseInvokeRestMethodOnly mode: tokens are acquired via Get-AzAccessToken as needed)" } $MgContext = $MgContextRaw | Select-Object ClientId, TenantId, AppName, ContextScope Write-Verbose -Message ($MgContext | Out-String) Write-Verbose "Scoped permissions in Microsoft Graph" $MgScopes = $MgContextRaw.Scopes Write-Verbose -Message ($MgScopes | Out-String) #endregion #region Validate baseline Graph scopes for already-authenticated user sessions if ($AuthenticationType -eq 'AlreadyAuthenticated' -and $AzContextRaw.Account.Type -eq 'User' -and $MgContextRaw) { $MissingScopes = @($Scopes | Where-Object { $_ -notin $MgContextRaw.Scopes }) $TenantGovernanceRelationshipScope = 'TenantGovernance-Relationship.Read.All' $MissingBaselineScopes = @($MissingScopes | Where-Object { $_ -ne $TenantGovernanceRelationshipScope }) if ($MissingBaselineScopes.Count -gt 0) { Write-Warning "The already-authenticated user session is missing $($MissingBaselineScopes.Count) baseline EntraOps delegated scope(s): $($MissingBaselineScopes -join ', '). Dependent collection steps may fail with 403 Forbidden. Reconnect with the missing scopes consented." } if ($MissingScopes -contains $TenantGovernanceRelationshipScope) { $HasManagingTenantConfiguration = -not [string]::IsNullOrWhiteSpace($ManagingTenantId) -or -not [string]::IsNullOrWhiteSpace($ManagingTenantName) if ($HasManagingTenantConfiguration) { Write-Warning "The already-authenticated user session is missing '$TenantGovernanceRelationshipScope'. Tenant Governance delegated-administration relationships may not be discovered or analyzed. Reconnect with this delegated scope consented." } else { Write-Warning "The already-authenticated user session is missing '$TenantGovernanceRelationshipScope'. No ManagingTenantId or ManagingTenantName is configured, so this may be acceptable only if you have verified that no Tenant Governance delegated-administration relationships exist. Without this scope, unknown relationships cannot be discovered or analyzed." } } } #endregion #region Import Environment variables if exists $IncludeObjectDetails = $false if ($ConfigFilePath) { try { $EntraOpsConfig = Get-Content -Path $ConfigFilePath | ConvertFrom-Json -Depth 10 -AsHashtable } catch { Write-Error "Issue to import config file $($ConfigFilePath)! Check if the file exists and is in JSON format." } # Remove Ingest or Apply* parameters from config file and show configuration $EntraOpsConfig.AutomatedClassificationUpdate.Remove("ApplyAutomatedClassificationUpdate") $EntraOpsConfig.AutomatedControlPlaneScopeUpdate.Remove("ApplyAutomatedControlPlaneScopeUpdate") if ($EntraOpsConfig.LogAnalytics) { $EntraOpsConfig.LogAnalytics.Remove("IngestToLogAnalytics") } if ($EntraOpsConfig.SentinelWatchLists) { $EntraOpsConfig.SentinelWatchLists.Remove("IngestToWatchLists") } if ($EntraOpsConfig.AutomatedAdministrativeUnitManagement) { $EntraOpsConfig.AutomatedAdministrativeUnitManagement.Remove("ApplyAdministrativeUnitAssignments") } if ($EntraOpsConfig.AutomatedConditionalAccessTargetGroups) { $EntraOpsConfig.AutomatedConditionalAccessTargetGroups.Remove("ApplyConditionalAccessTargetGroups") } if ($EntraOpsConfig.AutomatedRmauAssignmentsForUnprotectedObjects) { $EntraOpsConfig.AutomatedRmauAssignmentsForUnprotectedObjects.Remove("ApplyRmauAssignmentsForUnprotectedObjects") } if ($EntraOpsConfig.AutomatedElmCatalogProtection) { $EntraOpsConfig.AutomatedElmCatalogProtection.Remove("ApplyPrivilegedElmCatalogProtection") } if ($EntraOpsConfig.ConsoleOutput -and $null -ne $EntraOpsConfig.ConsoleOutput.IncludeObjectDetails) { $IncludeObjectDetails = [bool]$EntraOpsConfig.ConsoleOutput.IncludeObjectDetails } New-Variable -Name EntraOpsConfig -Value $EntraOpsConfig -Scope Global -Force Write-Verbose -Message "Config file $($ConfigFilePath) imported" } #endregion #region Set global variables New-Variable -Name TenantIdContext -Value $TenantId -Scope Global -Force New-Variable -Name TenantNameContext -Value $TenantName -Scope Global -Force New-Variable -Name ManagingTenantIdContext -Value $ManagingTenantId -Scope Global -Force New-Variable -Name ManagingTenantNameContext -Value $ManagingTenantName -Scope Global -Force New-Variable -Name EntraOpsIncludeObjectDetails -Value $IncludeObjectDetails -Scope Global -Force New-Variable -Name XdrAvdHuntingAccess -Value (-not $SkipGraphSdkAuth -and (Get-MgContext).Scopes -contains "ThreatHunting.Read.All") -Scope Global -Force $__EntraOpsSession['AuthenticationType'] = $AuthenticationType # $DefaultFolderClassification is always the Classification/ root; consumers append # $TenantNameContext or Templates/ themselves (Resolve-EntraOpsClassificationPath, # Import-EntraOpsClassificationOverwrites, Update-EntraOpsClassificationControlPlaneScope, ...). New-Variable -Name DefaultFolderClassification -Value "$EntraOpsBaseFolder/Classification/" -Scope Global -Force if ($MultiTenantRepo -eq $true) { New-Variable -Name DefaultFolderClassifiedEam -Value "$EntraOpsBaseFolder/PrivilegedEAM/$($TenantName)/" -Scope Global -Force Write-Verbose -Message "Multi Tenant in Repository" } else { New-Variable -Name DefaultFolderClassifiedEam -Value "$EntraOpsBaseFolder/PrivilegedEAM/" -Scope Global -Force Write-Verbose -Message "Single Tenant in Repository" } #endregion if (-not $NoWelcome) { #region Display connection summary Write-Host "" Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " 🔐 Connection Summary" -ForegroundColor Cyan Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " Authentication Type : $AuthenticationType" -ForegroundColor White # Display managing tenant info if configured if (-not [string]::IsNullOrEmpty($ManagingTenantId)) { Write-Host " Managing Tenant ID : $ManagingTenantId" -ForegroundColor White Write-Host " Managing Tenant Name: $ManagingTenantName" -ForegroundColor White } # Get Azure context $AzContext = Get-AzContext -ErrorAction SilentlyContinue if ($AzContext) { Write-Host " Azure Account : $($AzContext.Account.Id)" -ForegroundColor Green Write-Host " Azure Tenant : $($AzContext.Tenant.Id)" -ForegroundColor White Write-Host " Azure Subscription : $($AzContext.Subscription.Name)" -ForegroundColor White } else { Write-Host " Azure Account : Not connected" -ForegroundColor Gray } # Get Microsoft Graph context $MgContext = if (-not $SkipGraphSdkAuth) { Get-MgContext } else { $null } if ($MgContext) { Write-Host " Graph Account : $($MgContext.Account)" -ForegroundColor Green Write-Host " Graph Tenant : $($MgContext.TenantId)" -ForegroundColor White Write-Host " Graph Auth Type : $($MgContext.AuthType)" -ForegroundColor White # Display scopes (truncate if too many) $Scopes = $MgContext.Scopes if ($Scopes.Count -gt 0) { $ScopeDisplay = if ($Scopes.Count -le 5) { $Scopes -join ", " } else { ($Scopes | Select-Object -First 5) -join ", " + " (+$($Scopes.Count - 5) more)" } Write-Host " Graph Scopes : $ScopeDisplay" -ForegroundColor White } } elseif ($SkipGraphSdkAuth) { Write-Host " Graph Account : Not connected (UseInvokeRestMethodOnly - tokens acquired via Get-AzAccessToken as needed)" -ForegroundColor Gray } else { Write-Host " Graph Account : Not connected" -ForegroundColor Gray } Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host "" #endregion #region Display cache information Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " 📦 Cache Configuration" -ForegroundColor Cyan Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan # Cache location Write-Host " Cache Location : $($__EntraOpsSession.PersistentCachePath)" -ForegroundColor White # Memory cache $MemoryCacheCount = $__EntraOpsSession.GraphCache.Count Write-Host " Memory Cache : $MemoryCacheCount entries" -ForegroundColor $(if ($MemoryCacheCount -gt 0) { "Green" } else { "Gray" }) # Persistent cache statistics if (Test-Path $__EntraOpsSession.PersistentCachePath) { $PersistentFiles = Get-ChildItem -Path $__EntraOpsSession.PersistentCachePath -Filter "*.json" -ErrorAction SilentlyContinue $PersistentCount = $PersistentFiles.Count if ($PersistentCount -gt 0) { $PersistentSizeMB = [Math]::Round(($PersistentFiles | Measure-Object -Property Length -Sum).Sum / 1MB, 2) Write-Host " Persistent Cache : $PersistentCount files ($PersistentSizeMB MB)" -ForegroundColor Green # Show newest cache file $NewestCache = $PersistentFiles | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($NewestCache) { $CacheAge = [Math]::Round(((Get-Date) - $NewestCache.LastWriteTime).TotalHours, 1) Write-Host " Latest Cache Update : $CacheAge hours ago" -ForegroundColor $(if ($CacheAge -lt 1) { "Green" } elseif ($CacheAge -lt 24) { "Yellow" } else { "Gray" }) } } else { Write-Host " Persistent Cache : Empty (will populate on first API call)" -ForegroundColor Gray } } else { Write-Host " Persistent Cache : Directory will be created on first use" -ForegroundColor Gray } # Cache TTL settings Write-Host " Default TTL : $([Math]::Round($__EntraOpsSession.DefaultCacheTTL / 3600, 1)) hours" -ForegroundColor White Write-Host " Static Data TTL : $([Math]::Round($__EntraOpsSession.StaticDataCacheTTL / 3600, 1)) hours" -ForegroundColor White Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host "" # Warning to disconnect when finished Write-Host "⚠️ REMINDER: Run 'Disconnect-EntraOps' when finished to:" -ForegroundColor Yellow Write-Host " • Clear persistent cache on disk with results from your session and free memory" -ForegroundColor Yellow Write-Host " • Disconnect from Azure Resource Management" -ForegroundColor Yellow Write-Host " • Disconnect from Microsoft Graph API" -ForegroundColor Yellow Write-Host "" #endregion } } } # SIG # Begin signature block # MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC+VTjlwmFwI5vq # QMfrjxsmiNCmDylMOggg6+T17Cyie6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j # AEkoya49fu0nMA0GCSqGSIb3DQEBDAUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK # ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy # dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l # dHdvcmsgQ0EwHhcNMjEwNTMxMDY0MzA2WhcNMjkwOTE3MDY0MzA2WjCBgDELMAkG # A1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl # BgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMb # Q2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMIICIjANBgkqhkiG9w0BAQEFAAOC # Ag8AMIICCgKCAgEAvfl4+ObVgAxknYYblmRnPyI6HnUBfe/7XGeMycxca6mR5rlC # 5SBLm9qbe7mZXdmbgEvXhEArJ9PoujC7Pgkap0mV7ytAJMKXx6fumyXvqAoAl4Va # qp3cKcniNQfrcE1K1sGzVrihQTib0fsxf4/gX+GxPw+OFklg1waNGPmqJhCrKtPQ # 0WeNG0a+RzDVLnLRxWPa52N5RH5LYySJhi40PylMUosqp8DikSiJucBb+R3Z5yet # /5oCl8HGUJKbAiy9qbk0WQq/hEr/3/6zn+vZnuCYI+yma3cWKtvMrTscpIfcRnNe # GWJoRVfkkIJCu0LW8GHgwaM9ZqNd9BjuiMmNF0UpmTJ1AjHuKSbIawLmtWJFfzcV # WiNoidQ+3k4nsPBADLxNF8tNorMe0AZa3faTz1d1mfX6hhpneLO/lv403L3nUlbl # s+V1e9dBkQXcXWnjlQ1DufyDljmVe2yAWk8TcsbXfSl6RLpSpCrVQUYJIP4ioLZb # MI28iQzV13D4h1L92u+sUS4Hs07+0AnacO+Y+lbmbdu1V0vc5SwlFcieLnhO+Nqc # noYsylfzGuXIkosagpZ6w7xQEmnYDlpGizrrJvojybawgb5CAKT41v4wLsfSRvbl # jnX98sy50IdbzAYQYLuDNbdeZ95H7JlI8aShFf6tjGKOOVVPORa5sWOd/7cCAwEA # AaOCAT4wggE6MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLahVDkCw6A/joq8 # +tT4HKbROg79MB8GA1UdIwQYMBaAFAh2zcsH/yT2xc3tu5C84oQ3RnX3MA4GA1Ud # DwEB/wQEAwIBBjAvBgNVHR8EKDAmMCSgIqAghh5odHRwOi8vY3JsLmNlcnR1bS5w # bC9jdG5jYS5jcmwwawYIKwYBBQUHAQEEXzBdMCgGCCsGAQUFBzABhhxodHRwOi8v # c3ViY2Eub2NzcC1jZXJ0dW0uY29tMDEGCCsGAQUFBzAChiVodHRwOi8vcmVwb3Np # dG9yeS5jZXJ0dW0ucGwvY3RuY2EuY2VyMDkGA1UdIAQyMDAwLgYEVR0gADAmMCQG # CCsGAQUFBwIBFhhodHRwOi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJKoZIhvcNAQEM # BQADggEBAFHCoVgWIhCL/IYx1MIy01z4S6Ivaj5N+KsIHu3V6PrnCA3st8YeDrJ1 # BXqxC/rXdGoABh+kzqrya33YEcARCNQOTWHFOqj6seHjmOriY/1B9ZN9DbxdkjuR # mmW60F9MvkyNaAMQFtXx0ASKhTP5N+dbLiZpQjy6zbzUeulNndrnQ/tjUoCFBMQl # lVXwfqefAcVbKPjgzoZwpic7Ofs4LphTZSJ1Ldf23SIikZbr3WjtP6MZl9M7JYjs # NhI9qX7OAo0FmpKnJ25FspxihjcNpDOO16hO0EoXQ0zF8ads0h5YbBRRfopUofbv # n3l6XYGaFpAP4bvxSgD5+d2+7arszgowggZrMIIEU6ADAgECAhBX5gfP8LabvnxC # XoItEBa/MA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB # c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp # Z25pbmcgMjAyMSBDQTAeFw0yNjEwMDYwODI4MjNaFw0yNzEwMDYwODI4MjJaMIGQ # MQswCQYDVQQGEwJERTEdMBsGA1UECAwUUmhpbmVsYW5kLVBhbGF0aW5hdGUxEjAQ # BgNVBAcMCUxhaG5zdGVpbjEeMBwGA1UECgwVT3BlbiBTb3VyY2UgRGV2ZWxvcGVy # MS4wLAYDVQQDDCVPcGVuIFNvdXJjZSBEZXZlbG9wZXIgVGhvbWFzIE5hdW5oZWlt # MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAphp2ljSmVsRB6gvkEq7A # DUMENpKZiDFoAfF1ktD1nCwaVyI+bJ0ROjHwAn4iJgS+vlBDPOUIw2GLHkiqUhOg # HSwzg6gj45GNmt6zxsKP0zPlhMB/fb0zmz7/F7e3yfKCXLfhbHuk0Es9gJ+8Wmet # 9StVwwSE/K0MpW06GKA9nPWyE4N/ADwigDIj9rsTKJR4cve9w8G1b3zohpUc/qMr # onab+HVw5r9b3sOBnnaXC6zvrX4fovRz1ztwfWjRjILLoFG6mW/TuK5F2ICD3Rhl # 9R2rJWWWhzTDMsQlzvX+/SaM74q3OFpIVNdhvtmWU9pKUBRAyYMijcDqKEFGSzrc # 8JFmGMpP5TuroWF+N21d3px2eARvW1mV0DCfFAH+Og20SH/gVW6/ltQQEkH/z/Zt # 49ARyJrgESLOQtlMCIYSoKISdkk4ml4VUS1E/PcbzXEYj3a7m6DOxcd3WuY03UT9 # cnk3p0LMN6LeLYAZgVY+HRloZlH+kW2SkOHNuWgtXvZhAgMBAAGjggF4MIIBdDAM # BgNVHRMBAf8EAjAAMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jY3NjYTIwMjEu # Y3JsLmNlcnR1bS5wbC9jY3NjYTIwMjEuY3JsMHMGCCsGAQUFBwEBBGcwZTAsBggr # BgEFBQcwAYYgaHR0cDovL2Njc2NhMjAyMS5vY3NwLWNlcnR1bS5jb20wNQYIKwYB # BQUHMAKGKWh0dHA6Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jY3NjYTIwMjEuY2Vy # MB8GA1UdIwQYMBaAFN10XUwA23ufoHTKsW73PMAywHDNMB0GA1UdDgQWBBTBvGyH # MXj1dMbVnxuof9oP4PY7cDBLBgNVHSAERDBCMAgGBmeBDAEEATA2BgsqhGgBhvZ3 # AgUBBDAnMCUGCCsGAQUFBwIBFhlodHRwczovL3d3dy5jZXJ0dW0ucGwvQ1BTMBMG # A1UdJQQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsF # AAOCAgEAkC165c7/r6y3VeEEpNXas9bQs6ZpKPdJffHkf9xoPi4kWvAIWGc4ocG1 # DJLb18RXiDYShDmT5sb/6I08FeipKi2DNJkuOq6yOTJ3S7+hNFIZCyd6i7WKO8ea # P5I+FAbyJP4+5coZoMg45bUa9JoocaJCm+5n7WiCfN7/EQpClW5Trm4+ZMbp7EMN # F1uLJgGCMnN0SsoUarp7PHlqupMxMC4Uhhe+lAP2ytcSIU81L97/qjrADbLWqICk # 9il5iGM+aZndguq/cmfH9rJj4GRZHWnG6WwnzYD9+KgYlRI6em+/9dq+IRiXe2Bs # thUuIaFW7U2gWw8tBTEYm1UBLHdJx+L1gjJCLs5KT/wy5wU32wZxNDqPUQejUC1x # GvU3cKqmLgRq+yNf3gFVfzqIoggyM3U9wQvnk7nBtCkpjXS2hdx4yxzcmrlWsDHr # z6+6/jeyQySzbYUlFmKnc4n9Mw2soHOajEua5poFARVRS5KgUBuiiQ21YFNYzWBo # qGuECaaAI/C5rGxapMJ3dAOBnHw6NLqBet+ZXBJEccX5y89MLlOQu+jDmJP8HieR # qDLlwfhVZnw4RY8Rus43NZBwhNlVuOUXqocHKmJhKNlekda3qsJj93eQBQOxKJcc # w5O08q9jukpH2+yzFAj+7ll33DuNUgWptogc4Awtbx3glzcEvJQwggaCMIIEaqAD # AgECAhAo8HfBHDa9/l90MkdwJy4DMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYT # AlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMT # G0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQTAeFw0yNjAzMTEwNzM0NTRaFw0z # NjAyMjcwNzM0NTRaMFAxCzAJBgNVBAYTAlBMMSEwHwYDVQQKDBhBc3NlY28gRGF0 # YSBTeXN0ZW1zIFMuQS4xHjAcBgNVBAMMFUNlcnR1bSBUaW1lc3RhbXAgMjAyNjCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALi9zfBw+xI12hi2F9uNQBNO # STkTIi13fuh3DiWybI5392bBfJw6tO7zDs7UqP/rejVf4xFTU3XNOPsuOFkP5SPF # snWwn3VJL+NhNhsuujvYsQtaQnvNo1wk46reTgws1L8OFEdKaxgyqT4zOQx81mLw # NZSqXNU9Rrb9oAJWJF4Mydz+C1ebu0D0+vgM3tqAQ8TU203fqgKCPqIdHloaFqH9 # eyR+8tnpmy2hnnN59ZOtmsuHeRwf5iTOhlhf0d/qqquLOSIGPgGVL1Hj5XBUox2O # E7tQRr+l/xxNbL6OuwBO4Aa2EbuxYg37cpXv5i2/lTL4NASB/duf9W4fjA3Ro+GP # +bC54n2OYiM3Cq8z92ejx8F5HWO2e4/75eC95B9tMw/eq7109GDyW2+2ut5Bn0o8 # MXJTERkyystWbqcnUkomA97qfiDCwvpWuOMdfTOksXe9S+xDxOJaCbiyTivgPulx # cT+tfQTRXBr5UwtPvc+INnafOEilzVHA11tv+pHcylryxBc08cUihf/ttqkZS0bm # x3VxQay8uohaTp0//ECA9vYGW51SKdJMJhXvKyIdBLy8WYNchTt8yRITv9Adxg9J # qGujh+TnP1bW3aQjFtlEl7SzgNeI8OVA/CJAWWUuRT3Au9ToOpHy/flrrknhN4he # Ew7/6ILMv4HSsj0YZ4otAgMBAAGjggFQMIIBTDB1BggrBgEFBQcBAQRpMGcwOwYI # KwYBBQUHMAKGL2h0dHA6Ly9zdWJjYS5yZXBvc2l0b3J5LmNlcnR1bS5wbC9jdHNj # YTIwMjEuY2VyMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u # Y29tMB8GA1UdIwQYMBaAFL5UAi+/QGxzQ86sCSVOnkNEGu7gMAwGA1UdEwEB/wQC # MAAwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL3N1YmNhLmNybC5jZXJ0dW0ucGwv # Y3RzY2EyMDIxLmNybDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8E # BAMCB4AwIgYDVR0gBBswGTAIBgZngQwBBAIwDQYLKoRoAYb2dwIFAQswHQYDVR0O # BBYEFCM5aiirmhKnpKfyH/lFmr+N/oIDMA0GCSqGSIb3DQEBDAUAA4ICAQBm/pOb # rAveTqYFPCSthxNVwBHTW4TfuBQY2KGCdMlH6ALjVPYf2XARNGMTXScR0JQ2/c2L # xLpoHHhRwxFf3d933DhFS165rYj6iPSQKm41rc9AMpTCjMdmai1aK9o870pOzTzL # RhWFVIxm+5Qf+t4V5z14tPoUsAWdfRKD1Zzvzfe5AHlE9rxOZcGROiULxs3Edf7Z # J8RZI0jzD9juj2oEX2XS7BVKuNCK1W0h1mjapv028yqo8UtZh5/1Ib48Yyp88BDY # Shh5PSGUUuS5tYXVfcduIPBe3PdNTYRYdT5VNxkwLgjMzIcr/6SH2fejFJDjnCxj # vXLuarErJg/IdI+hXdGIBDVOGv1zrkF4ktoQvFZIp5DIAYh0GwyUhc87qqn5UaYT # j2+iyiBDSNh7aGCrkuAnqg7Qh7frhX+QhMGeJ9SpLpnG76qN85hLyjvYR6gRYiLT # j8G4fxjqV400/buPw0rBo9yJZyq/8cGdCOLPOYf9qpefTcrZKeO9pg+UJHx8UABb # aTqizRxm8sUSEycccCCFhIit3AI/3wGuSOADeiA676NcQ7+dsP04YvzvdN7pTZNy # g672WpBDJdg0eItHl4yomvNeIOWh1qs4QqOW6TI8Ocsi4uSDTLorxLUDnBTwKQzz # pxd1YUctEgRh32R0J9FkFkeEruJU94RljFr1uDCCBrkwggShoAMCAQICEQCZo4AK # JlU7ZavcboSms+o5MA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAG # A1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVt # IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3Rl # ZCBOZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjE4WhcNMzYwNTE4MDUzMjE4WjBW # MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu # MSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEgQ0EwggIiMA0GCSqG # SIb3DQEBAQUAA4ICDwAwggIKAoICAQCdI88EMCM7wUYs5zNzPmNdenW6vlxNur3r # Lfi+5OZ+U3iZIB+AspO+CC/bj+taJUbMbFP1gQBJUzDUCPx7BNLgid1TyztVLn52 # NKgxxu8gpyTr6EjWyGzKU/gnIu+bHAse1LCitX3CaOE13rbuHbtrxF2tPU8f253Q # gX6eO8yTbGps1Mg+yda3DcTsOYOhSYNCJiL+5wnjZ9weoGRtvFgMHtJg6i671OPX # IciiHO4Lwo2p9xh/tnj+JmCQEn5QU0NxzrOiRna4kjFaA9ZcwSaG7WAxeC/xoZSx # F1oK1UPZtKVt+yrsGKqWONoK6f5EmBOAVEK2y4ATDSkb34UD7JA32f+Rm0wsr5aj # zftDhA5mBipVZDjHpwzv8bTKzCDUSUuUmPo1govD0RwFcTtMXcfJtm1i+P2UNXad # PyYVKRxKQATHN3imsfBiNRdN5kiVVeqP55piqgxOkyt+HkwIA4gbmSc3hD8ke66t # 9MjlcNg73rZZlrLHsAIV/nJ0mmgSjBI/TthoGJDydekOQ2tQD2Dup/+sKQptalDl # ui59SerVSJg8gAeV7N/ia4mrGoiez+SqV3olVfxyLFt3o/OQOnBmjhKUANoKLYlK # mUpKEFI0PfoT8Q1W/y6s9LTI6ekbi0igEbFUIBE8KDUGfIwnisEkBw5KcBZ3XwnH # mfznwlKo8QIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU # 3XRdTADbe5+gdMqxbvc8wDLAcM0wHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61Pgc # ptE6Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDAGA1Ud # HwQpMCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYI # KwYBBQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0 # dW0uY29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv # Y3RuY2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0 # cDovL3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQB1iFgP5Y9Q # KJpTnxDsQ/z0O23JmoZifZdEOEmQvo/79PQg9nLF/GJe6ZiUBEyDBHMtFRK0mXj3 # Qv3gL0sYXe+PPMfwmreJHvgFGWQ7XwnfMh2YIpBrkvJnjwh8gIlNlUl4KENTK5DL # qsYPEtRQCw7R6p4s2EtWyDDr/M58iY2UBEqfUU/ujR9NuPyKk0bEcEi62JGxauFY # zZ/yld13fHaZskIoq2XazjaD0pQkcQiIueL0HKiohS6XgZuUtCKA7S6CHttZEsOb # QJ1j2s0urIDdqF7xaXFVaTHKtAuMfwi0jXtF3JJphrJfc+FFILgCbX/uYBPBlbBI # P4Ht4xxk2GmfzMn7oxPITpigQFJFWuzTMUUgdRHTxaTSKRJ/6Uh7ki/pFjf9sUAS # WgxT69QF9Ki4JF5nBIujxZ2sOU9e1HSCJwOfK07t5nnzbs1LbHuAIGJsRJiQ6HX/ # DW1XFOlXY1rc9HufFhWU+7Uk+hFkJsfzqBz3pRO+5aI6u5abI4Qws4YaeJH7H7M8 # X/YNoaArZbV4Ql+jarKsE0+8XvC4DJB+IVcvC9Ydqahi09mjQse4fxfef0L7E3hh # o2O3bLDM6v60rIRUCi2fJT2/IRU5ohgyTch4GuYWefSBsp5NPJh4QRTP9DC3gc5Q # EKtbrTY0Ka87Web7/zScvLmvQBm8JDFpDjCCBrkwggShoAMCAQICEQDn/2nHOzXO # S5Em2HR8aKWHMA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UE # ChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl # cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBO # ZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjA3WhcNMzYwNTE4MDUzMjA3WjBWMQsw # CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQw # IgYDVQQDExtDZXJ0dW0gVGltZXN0YW1waW5nIDIwMjEgQ0EwggIiMA0GCSqGSIb3 # DQEBAQUAA4ICDwAwggIKAoICAQDpEh8ENe25XXrFppVBvoplf0530W0lddNmjtv4 # YSh/f7eDQKFaIqc7tHj7ox+u8vIsJZlroakUeMS3i3T8aJRC+eQs4FF0GqvkM6+W # ZO8kmzZfxmZaBYmMLs8FktgFYCzywmXeQ1fEExflee2OpbHVk665eXRHjH7MYZIz # Nnjl2m8Hy8ulB9mR8wL/W0v0pjKNT6G0sfrx1kk+3OGosFUb7yWNnVkWKU4qSxLv # 16kJ6oVJ4BSbZ4xMak6JLeB8szrK9vwGDpvGDnKCUMYL3NuviwH1x4gZG0JAXU3x # 2pOAz91JWKJSAmRy/l0s0l5bEYKolg+DMqVhlOANd8Yh5mkQWaMEvBRE/kAGzIqg # WhwzN2OsKIVtO8mf5sPWSrvyplSABAYa13rMYnzwfg08nljZHghquCJYCa/xHK9a # cev9UD7Y+usr15d7mrszzxhF1JOr1Mpup2chNSBlyOObhlSO16rwrffVrg/SzaKf # SndS5swRhr8bnDqNJY9TNyEYvBYpgF95K7p0g4LguR4A++Z1nFIHWVY5v0fNVZmg # zxD9uVo/gta3onGOQj3JCxgYx0KrCXu4yc9QiVwTFLWbNdHFSjBCt5/8Q9pLuRhV # ocdCunhcHudMS1CGQ/Rn0+7P+fzMgWdRKfEOh/hjLrnQ8BdJiYrZNxvIOhM2aa3z # EDHNwwIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvlQC # L79AbHNDzqwJJU6eQ0Qa7uAwHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61PgcptE6 # Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMDAGA1UdHwQp # MCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYIKwYB # BQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u # Y29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY3Ru # Y2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0cDov # L3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQC4k1l3yUwV/ZQH # CKCneqAs8EGTnwEUJLdDpokN/dMhKjK0rR5qX8nIIHzxpQR3TAw2IRw1Uxsr2Pli # G3bCFqSdQTUbfaTq6V3vBzEebDru9QFjqlKnxCF2h1jhLNFFplbPJiW+JSnJTh1f # KEqEdKdxgl9rVTvlxfEJ7exOn25MGbd/wGPwuSmMxRJVO0wnqgS7kmoJjNF9zqee # hFSDDP8ZVkWg4EZ2tIS0M3uZmByRr+1Lkwjjt8AtW83mVnZTyTsOb+FNfwJY7DS4 # FmWhkRbgcHRetreoTirPOr/ozyDKhT8MTSTf6Lttg6s6T/u08mDWw6HK04ZRDfQ9 # sb77QV8mKgO44WGP31vXnVKoWVJpFBjPvjL8/Zck/5wXX2iqjOaLStFOR/IQki+E # hn4zlcgVm22ZVCBPF+l8nAwUUShCtKuSU7GmZLKCmmxQMkSiWILTm8EtVD6AxnJh # oq8EnhjEEyUoflkeRF2WhFiVQOmWTwZRr44IxWGkNJC6tTorW5rl2Zl+2e9JLPYf # 3pStAPMDoPKIjVXd6NW2+fZrNUBeDo2eOa5Fn7Brs/HLQff5Xgris5MeUbdVgDrF # 8uxO6cLPvZPo63j62SsNg55pTWk9fUIF9iPoRbb4QurjoY/woI1RAOKtYtTic6aA # Jq3u83RIPpGXBSJKwx4KJAOZnCDCtTGCBqIwggaeAgEBMGowVjELMAkGA1UEBhMC # UEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMb # Q2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBAhBX5gfP8LabvnxCXoItEBa/MA0G # CWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZI # hvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC # ARUwLwYJKoZIhvcNAQkEMSIEIOYdCvUoV/7AyBaljYbw4srsq4i98keb5EQf7B9c # ALyDMA0GCSqGSIb3DQEBAQUABIIBgCrVsQvEguwq3/W2D26qkYINihJnaeVCE13D # gDMQ3LvY5SqVzvAPnTGLcKVsWQXI3SFXtWhLPpTYdU1JeqaqasRQtMyb7CLc+zRV # 2CM4TZFAllFdZrI0eIZPZtAnD77EtDtAOA9aNStioTx3eP050xzyaqZuvBXLyo95 # 6gGlyOyjiEQz+r6k+XRH/Tu7ooctKo0UJRd8ZhaUycN6Zc7bYA6IVYqD4Mtzoh2x # rpiRAdU+KTJx8K9Gn7skAu5jQXmpo9I0mTNnndlTK4ctlsGjzT0aKMHNZpTQnTco # +VQb3KRTBLkcfHqgbjKIenyE1oU+8aHtAP9JBEUcj6TvJ5mFKdyJ1QkBojSjeMn5 # egXAsWQdDG5XPIviL7csmcYiXiQxBQSxWUrPkEZZ/D5Hog5n+HP4mYvM2jmGm9kZ # aZM+8W4kfIDUGJcOBizHpAgI09glq6HlwSFtmgov1VIaeI5R++H8iSyuBQHMUSBF # KKT5PcVN2ZbrZgQXaL4+Q8ew+M/Fj6GCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID # 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0 # ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ # KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQwMzlaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDBETYGB2UNqj/Q/2+cqcjcSIPO5ivUkyCPj # WayMKgZSTU890EBpTnq0lXhO9Tso7p0wgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIAaXJ8v5xz5nlSDaU0gw6dXFsmrXX/+IL9LuK2DQgPVCewXs5x # EA2WeDSxVFYJ9HVEsdPU1ef70DcCTTpOEGeEHjo0HkJaFGC+EiWrGRFljCwmqrsl # 2xAAvD8X6zEwrlAmbTg53eVOT7bsmf189tKdVG3lNXBWnSiNFdWWqpDrQDwWaaBC # EF42dQVleqadUoy9Bkz/C8DzygPCfsNuHL2xyvvvOyY18j93kosL/g3G1IJpd2hj # 5y2IKTHtXMRzG7+zWQJwQ3WdFILkh3nOsh5BRaNsXw9afCU+oF8U0NVX7b28MWys # GQ5uuWYDCZVHr7Rqs/PZfZugyDkfrjlPHE6GnnV8N/MyEWMc+0nSV+p/6VxOQrlx # ivGqmAXoa3GyoSjWqfbQ8CRn3YWorYgLsgYlJTpmsexx1Rn5OaSn55mHuq8DVhlD # 2QRw+p887tLwrPO9T3ElMFywKYcQXQydWuVjCels1CmnrEkkPCQQlgtrNyj0bPYx # 0iHNPK/9CNve+fSpKTkxAjd69hEDgnhbkR/bkow1zVyikeKE3G3Uo1eunszQVvoK # RttoK79/g24Aa1XdTeydN09uQbV/XmhGuLYZt7N3GhJMe/yA6Vw2+EZg0d0+gbRk # hfYwH7oFjoMNIE59zXNQnRgsizDqt2nK/2utuStkBIYkIJ6Z+mLCyMtPrgA= # SIG # End signature block |