Modules/Providers/ExportAADProvider.psm1
# Many of the commandlets can be replaced with direct API access, but we are starting the transition with the ones # below because they have slow imports that affect performance more than the others. Some commandlets are fast # and there is no obvoius performance advantage to using the API beyond maybe batching. $GraphEndpoints = @{ "Get-MgBetaRoleManagementDirectoryRoleEligibilityScheduleInstance" = "/beta/roleManagement/directory/roleEligibilityScheduleInstances" "Get-MgBetaRoleManagementDirectoryRoleAssignmentScheduleInstance" = "/beta/roleManagement/directory/roleAssignmentScheduleInstances" "Get-MgBetaIdentityGovernancePrivilegedAccessGroupEligibilityScheduleInstance" = "/beta/identityGovernance/privilegedAccess/group/eligibilityScheduleInstances" "Get-MgBetaPrivilegedAccessResource" = "/beta/privilegedAccess/aadGroups/resources" } function Invoke-GraphDirectly { param ( [ValidateNotNullOrEmpty()] [string] $commandlet, [ValidateNotNullOrEmpty()] [string] $M365Environment, [System.Collections.Hashtable] $queryParams ) Write-Debug "Replacing Cmdlet: $commandlet" try { $endpoint = $GraphEndpoints[$commandlet] } catch { Write-Error "The commandlet $commandlet can't be used with the Invoke-GraphDirectly function yet." } if ($M365Environment -eq "gcchigh") { $endpoint = "https://graph.microsoft.us" + $endpoint } elseif ($M365Environment -eq "dod") { $endpoint = "https://dod-graph.microsoft.us" + $endpoint } else { $endpoint = "https://graph.microsoft.com" + $endpoint } if ($queryParams) { # If query params are passed in, we augment the endpoint URI to include the params. $q = [System.Web.HttpUtility]::ParseQueryString([string]::Empty) foreach ($item in $queryParams.GetEnumerator()) { $q.Add($item.Key, $item.Value) } $uri = [System.UriBuilder]::new("", "", 443, $endpoint) $uri.Query = $q.ToString() $endpoint = $uri.ToString() } Write-Debug "Graph Api direct: $endpoint" $resp = Invoke-MgGraphRequest -ErrorAction Stop -Uri $endpoint return $resp.Value } function Export-AADProvider { <# .Description Gets the Azure Active Directory (AAD) settings that are relevant to the SCuBA AAD baselines using a subset of the modules under the overall Microsoft Graph PowerShell Module .Functionality Internal #> [CmdletBinding()] param ( [ValidateNotNullOrEmpty()] [string] $M365Environment ) Import-Module $PSScriptRoot/ProviderHelpers/CommandTracker.psm1 $Tracker = Get-CommandTracker # The below cmdlet covers ~ 8 policy checks that inspect conditional access policies $AllPolicies = $Tracker.TryCommand("Get-MgBetaIdentityConditionalAccessPolicy") Import-Module $PSScriptRoot/ProviderHelpers/AADConditionalAccessHelper.psm1 $CapHelper = Get-CapTracker $CapTableData = $CapHelper.ExportCapPolicies($AllPolicies) # Used in generating the CAP html in the report if ($CapTableData -eq "") { # Quick sanity check, did ExportCapPolicies return something? Write-Warning "Error parsing CAP data, empty json returned from ExportCapPolicies." $CapTableData = "[]" } try { # Final sanity check, did ExportCapPolicies return valid json? ConvertFrom-Json $CapTableData -ErrorAction "Stop" | Out-Null } catch { Write-Warning "Error parsing CAP data, invalid json returned from ExportCapPolicies." $CapTableData = "[]" } $AllPolicies = ConvertTo-Json -Depth 10 @($AllPolicies) $SubscribedSku = $Tracker.TryCommand("Get-MgBetaSubscribedSku") # Get a list of the tenant's provisioned service plans - used to see if the tenant has AAD premium p2 license required for some checks # The Rego looks at the service_plans in the JSON $ServicePlans = $SubscribedSku.ServicePlans | Where-Object -Property ProvisioningStatus -eq -Value "Success" #Obtains license information for tenant and total number of active users $LicenseInfo = $SubscribedSku | Select-Object -Property Sku*, ConsumedUnits, PrepaidUnits | ConvertTo-Json -Depth 3 if ($ServicePlans) { # The RequiredServicePlan variable is used so that PIM Cmdlets are only executed if the tenant has the premium license $RequiredServicePlan = $ServicePlans | Where-Object -Property ServicePlanName -eq -Value "AAD_PREMIUM_P2" # Get-PrivilegedUser provides a list of privileged users and their role assignments. if ($RequiredServicePlan) { # If the tenant has the premium license then we also include calls to PIM APIs $PrivilegedUsers = $Tracker.TryCommand("Get-PrivilegedUser", @{"TenantHasPremiumLicense"=$true; "M365Environment"=$M365Environment}) } else{ $PrivilegedUsers = $Tracker.TryCommand("Get-PrivilegedUser", @{"TenantHasPremiumLicense"=$false; "M365Environment"=$M365Environment}) } # The Converto-Json call below doesn't need to have the input wrapped in an # array (e.g, "ConvertTo-Json (@PrivilegedUsers)") because $PrivilegedUsers is # a dictionary, not an array, and ConvertTo-Json doesn't mess up dictionaries like it does arrays $PrivilegedUsers = $PrivilegedUsers | ConvertTo-Json # While ConvertTo-Json won't mess up a dict as described in the above comment, # on error, $TryCommand returns an empty list, not a dictionary. $PrivilegedUsers = if ($null -eq $PrivilegedUsers) {"{}"} else {$PrivilegedUsers} # Get-PrivilegedRole provides a list of security configurations for each privileged role and information about Active user assignments if ($RequiredServicePlan){ # If the tenant has the premium license then we also include calls to PIM APIs $PrivilegedRoles = $Tracker.TryCommand("Get-PrivilegedRole", @{"TenantHasPremiumLicense"=$true; "M365Environment"=$M365Environment}) } else { $PrivilegedRoles = $Tracker.TryCommand("Get-PrivilegedRole", @{"TenantHasPremiumLicense"=$false; "M365Environment"=$M365Environment}) } $PrivilegedRoles = ConvertTo-Json -Depth 10 @($PrivilegedRoles) # Depth required to get policy rule object details } else { Write-Warning "Omitting calls to Get-PrivilegedRole and Get-PrivilegedUser." $PrivilegedUsers = ConvertTo-Json @() $PrivilegedRoles = ConvertTo-Json @() $Tracker.AddUnSuccessfulCommand("Get-PrivilegedRole") $Tracker.AddUnSuccessfulCommand("Get-PrivilegedUser") } $ServicePlans = ConvertTo-Json -Depth 3 @($ServicePlans) $UserCount = $Tracker.TryCommand("Get-MgBetaUserCount", @{"ConsistencyLevel"='eventual'}) # Ensure we successfully got a count of users if(-Not $UserCount -is [int]) { $UserCount = "NaN" } # Provides data for policies such as user consent and guest user access $AuthZPolicies = ConvertTo-Json @($Tracker.TryCommand("Get-MgBetaPolicyAuthorizationPolicy")) # Provides data for admin consent workflow $DirectorySettings = ConvertTo-Json -Depth 10 @($Tracker.TryCommand("Get-MgBetaDirectorySetting")) ##### This block supports policies that need data on the tenant's authentication methods $AuthenticationMethodPolicyRootObject = $Tracker.TryCommand("Get-MgBetaPolicyAuthenticationMethodPolicy") $AuthenticationMethodFeatureSettings = @($AuthenticationMethodPolicyRootObject.AuthenticationMethodConfigurations | Where-Object { $_.Id}) # Exclude the AuthenticationMethodConfigurations so we do not duplicate it in the JSON $AuthenticationMethodPolicy = $AuthenticationMethodPolicyRootObject | ForEach-Object { $_ | Select-Object * -ExcludeProperty AuthenticationMethodConfigurations } $AuthenticationMethodObjects = @{ authentication_method_policy = $AuthenticationMethodPolicy authentication_method_feature_settings = $AuthenticationMethodFeatureSettings } $AuthenticationMethod = ConvertTo-Json -Depth 10 @($AuthenticationMethodObjects) ##### End block # Provides data on the password expiration policy $DomainSettings = ConvertTo-Json @($Tracker.TryCommand("Get-MgBetaDomain")) $SuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands()) $UnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands()) # Note the spacing and the last comma in the json is important $json = @" "conditional_access_policies": $AllPolicies, "cap_table_data": $CapTableData, "authorization_policies": $AuthZPolicies, "privileged_users": $PrivilegedUsers, "privileged_roles": $PrivilegedRoles, "service_plans": $ServicePlans, "directory_settings": $DirectorySettings, "authentication_method": $AuthenticationMethod, "domain_settings": $DomainSettings, "license_information": $LicenseInfo, "total_user_count": $UserCount, "aad_successful_commands": $SuccessfulCommands, "aad_unsuccessful_commands": $UnSuccessfulCommands, "@ $json } function Get-AADTenantDetail { <# .Description Gets the tenant details using the Microsoft Graph PowerShell Module .Functionality Internal #> try { $OrgInfo = Get-MgBetaOrganization -ErrorAction "Stop" $InitialDomain = $OrgInfo.VerifiedDomains | Where-Object {$_.isInitial} if (-not $InitialDomain) { $InitialDomain = "AAD: Domain Unretrievable" } $AADTenantInfo = @{ "DisplayName" = $OrgInfo.DisplayName; "DomainName" = $InitialDomain.Name; "TenantId" = $OrgInfo.Id; "AADAdditionalData" = $OrgInfo; } $AADTenantInfo = ConvertTo-Json @($AADTenantInfo) -Depth 4 $AADTenantInfo } catch { Write-Warning "Error retrieving Tenant details using Get-AADTenantDetail $($_)" $AADTenantInfo = @{ "DisplayName" = "Error retrieving Display name"; "DomainName" = "Error retrieving Domain name"; "TenantId" = "Error retrieving Tenant ID"; "AADAdditionalData" = "Error retrieving additional data"; } $AADTenantInfo = ConvertTo-Json @($AADTenantInfo) -Depth 4 $AADTenantInfo } } function Get-PrivilegedUser { <# .Description Returns a hashtable of privileged users and their respective roles .Functionality Internal #> param ( [ValidateNotNullOrEmpty()] [bool] $TenantHasPremiumLicense, [ValidateNotNullOrEmpty()] [string] $M365Environment ) try { # A hashtable of privileged users $PrivilegedUsers = @{} $PrivilegedRoles = [ScubaConfig]::ScubaDefault('DefaultPrivilegedRoles') # Get a list of the Id values for the privileged roles in the list above. # The Id value is passed to other cmdlets to construct a list of users assigned to privileged roles. $AADRoles = Get-MgBetaDirectoryRole -All -ErrorAction Stop | Where-Object { $_.DisplayName -in $PrivilegedRoles } # Construct a list of privileged users based on the Active role assignments foreach ($Role in $AADRoles) { # Get a list of all the users and groups Actively assigned to this role $UsersAssignedRole = Get-MgBetaDirectoryRoleMember -All -ErrorAction Stop -DirectoryRoleId $Role.Id foreach ($User in $UsersAssignedRole) { $Objecttype = $User.AdditionalProperties."@odata.type" -replace "#microsoft.graph." if ($Objecttype -eq "user") { LoadObjectDataIntoPrivilegedUserHashtable -RoleName $Role.DisplayName -PrivilegedUsers $PrivilegedUsers -ObjectId $User.Id -TenantHasPremiumLicense $TenantHasPremiumLicense -M365Environment $M365Environment -Objecttype "user" } elseif ($Objecttype -eq "group") { # In this context $User.Id is a group identifier $GroupId = $User.Id # Process all of the group members that are transitively assigned to the current role as Active via group membership LoadObjectDataIntoPrivilegedUserHashtable -RoleName $Role.DisplayName -PrivilegedUsers $PrivilegedUsers -ObjectId $GroupId -TenantHasPremiumLicense $TenantHasPremiumLicense -M365Environment $M365Environment -Objecttype "group" } } } # Process the Eligible role assignments if the premium license for PIM is there if ($TenantHasPremiumLicense) { # Get a list of all the users and groups that have Eligible assignments $graphArgs = @{ "commandlet" = "Get-MgBetaRoleManagementDirectoryRoleEligibilityScheduleInstance" "M365Environment" = $M365Environment } $AllPIMRoleAssignments = Invoke-GraphDirectly @graphArgs # Add to the list of privileged users based on Eligible assignments foreach ($Role in $AADRoles) { $PrivRoleId = $Role.RoleTemplateId # Get a list of all the users and groups Eligible assigned to this role $PIMRoleAssignments = $AllPIMRoleAssignments | Where-Object { $_.RoleDefinitionId -eq $PrivRoleId } foreach ($PIMRoleAssignment in $PIMRoleAssignments) { $UserObjectId = $PIMRoleAssignment.PrincipalId LoadObjectDataIntoPrivilegedUserHashtable -RoleName $Role.DisplayName -PrivilegedUsers $PrivilegedUsers -ObjectId $UserObjectId -TenantHasPremiumLicense $TenantHasPremiumLicense -M365Environment $M365Environment } } } } catch { Write-Warning "An error occurred in Get-PrivilegedUser: $($_.Exception.Message)" Write-Warning "Stack trace: $($_.ScriptStackTrace)" throw $_ } $PrivilegedUsers } function LoadObjectDataIntoPrivilegedUserHashtable { <# .Description Takes an object Id (either a user or group) and loads metadata about the object in the provided privileged user hashtable. If the object is a group, this function will iterate the group members and load metadata about each member. .Functionality Internal #> param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$RoleName, [Parameter(Mandatory=$true)] [hashtable]$PrivilegedUsers, # The Entra Id unique identifiter for an object (either a user or a group) in the directory. # Metadata about this object will be loaded into the PrivilegedUsers hashtable which is passed as a parameter. [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$ObjectId, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [bool]$TenantHasPremiumLicense, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$M365Environment, # This describes the type of Entra Id object that the parameter ObjectId is referencing. # Valid values are "user", "group". If this is not passed, the function will call Graph to dynamically determine the object type. [Parameter()] [string]$Objecttype = "", [Parameter()] [int]$Recursioncount = 0 ) # Write-Warning "Recursion level: $recursioncount" # We support group nesting up to 2 levels deep (stops after processing levels 0 and 1). # Safeguard: Also protects against infinite loops if there is a circular group assignment in PIM. if ($recursioncount -ge 2) { return } # If the object type was not supplied we need to determine whether it is a user or a group. if ($Objecttype -eq "") { try { $DirectoryObject = Get-MgBetaDirectoryObject -ErrorAction Stop -DirectoryObjectId $ObjectId } catch { # If the object was probably recently deleted from the directory we ignore it. Otherwise an unhandled 404 causes the tool to crash. if ($_.Exception.Message -match "Request_ResourceNotFound") { Write-Warning "Processing privileged users. Resource $ObjectId may have been recently deleted from the directory because it was not found." return # Exit the function to ignore this resource and keep the flow going. } # If it is a different error, rethrow the error to let the calling function handle it. else { throw $_ } } # Extract what type of object this is. $Objecttype = $DirectoryObject.AdditionalProperties."@odata.type" -replace "#microsoft.graph." } if ($Objecttype -eq "user") { # If the user's data has not been fetched from graph, go get it and add it to the hashtable if (-Not $PrivilegedUsers.ContainsKey($ObjectId)) { $AADUser = Get-MgBetaUser -ErrorAction Stop -UserId $ObjectId $PrivilegedUsers[$ObjectId] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()} # Write-Warning "Processing role: $($RoleName) User: $($AADUser.DisplayName)" } # If the current role has not already been added to the user's roles array then add the role if ($PrivilegedUsers[$ObjectId].roles -notcontains $RoleName) { $PrivilegedUsers[$ObjectId].roles += $RoleName } } elseif ($Objecttype -eq "group") { # In this context $ObjectId is a group identifier so we need to iterate the group members $GroupId = $ObjectId # Get all of the group members that are transitively assigned to the current role via group membership $GroupMembers = Get-MgBetaGroupMember -All -ErrorAction Stop -GroupId $GroupId # Write-Warning "Processing role: $($RoleName) Group: $($GroupId)" foreach ($GroupMember in $GroupMembers) { $Membertype = $GroupMember.AdditionalProperties."@odata.type" -replace "#microsoft.graph." if ($Membertype -eq "user") { # If the user's data has not been fetched from graph, go get it and add it to the hashtable if (-Not $PrivilegedUsers.ContainsKey($GroupMember.Id)) { $AADUser = Get-MgBetaUser -ErrorAction Stop -UserId $GroupMember.Id $PrivilegedUsers[$GroupMember.Id] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()} } # If the current role has not already been added to the user's roles array then add the role if ($PrivilegedUsers[$GroupMember.Id].roles -notcontains $RoleName) { $PrivilegedUsers[$GroupMember.Id].roles += $RoleName } } } # Since this is a group, we need to also process assignments in PIM in case it is in PIM for Groups # If the premium license for PIM is there, process the users that are "member" of the PIM group as Eligible if ($TenantHasPremiumLicense) { # Get the users that are assigned to the PIM group as Eligible members $graphArgs = @{ "commandlet" = "Get-MgBetaIdentityGovernancePrivilegedAccessGroupEligibilityScheduleInstance" "queryParams" = @{'$filter' = "groupId eq '$GroupId'"} "M365Environment" = $M365Environment } $PIMGroupMembers = Invoke-GraphDirectly @graphArgs foreach ($GroupMember in $PIMGroupMembers) { # Write-Warning "Processing role: $($RoleName) PIM group Eligible member: $($GroupMember.PrincipalId)" # If the user is not a member of the PIM group (i.e. they are an owner) then skip them if ($GroupMember.AccessId -ne "member") { continue } $PIMEligibleUserId = $GroupMember.PrincipalId # Recursively call this function to process the group member that was found $LoopIterationRecursioncount = $Recursioncount + 1 LoadObjectDataIntoPrivilegedUserHashtable -RoleName $RoleName -PrivilegedUsers $PrivilegedUsers -ObjectId $PIMEligibleUserId -TenantHasPremiumLicense $TenantHasPremiumLicense -M365Environment $M365Environment -Recursioncount $LoopIterationRecursioncount } } } } function AddRuleSource{ <# .NOTES Internal helper function to add a source to policy rule for reporting purposes. Source should be either PIM Group Name or Role Name #> param( [ValidateNotNullOrEmpty()] [string] $Source, [ValidateNotNullOrEmpty()] [string] $SourceType = "Directory Role", [ValidateNotNullOrEmpty()] [array] $Rules ) foreach ($Rule in $Rules){ $Rule | Add-Member -Name "RuleSource" -Value $Source -MemberType NoteProperty $Rule | Add-Member -Name "RuleSourceType" -Value $SourceType -MemberType NoteProperty } } # This cache keeps track of PIM groups that we've already processed class GroupTypeCache{ static [hashtable]$CheckedGroups = @{} } function GetConfigurationsForPimGroups{ param ( [ValidateNotNullOrEmpty()] [array] $PrivilegedRoleArray, [ValidateNotNullOrEmpty()] [array] $AllRoleAssignments, [ValidateNotNullOrEmpty()] [string] $M365Environment ) # Get a list of the groups that are enrolled in PIM - we want to ignore the others $graphArgs = @{ "commandlet" = "Get-MgBetaPrivilegedAccessResource" "queryParams" = @{'$PrivilegedAccessId' = "aadGroups"} "M365Environment" = $M365Environment } $PIMGroups = Invoke-GraphDirectly @graphArgs foreach ($RoleAssignment in $AllRoleAssignments){ # Check if the assignment in current loop iteration is assigned to a privileged role $Role = $PrivilegedRoleArray | Where-Object RoleTemplateId -EQ $($RoleAssignment.RoleDefinitionId) # If this is a privileged role if ($Role){ # Store the Id of the object assigned to the role (could be user,group,service principal) $PrincipalId = $RoleAssignment.PrincipalId # If the current object is not a PIM group we skip it $FoundPIMGroup = $PIMGroups | Where-Object { $_.Id -eq $PrincipalId } if ($null -eq $FoundPIMGroup) { continue } # If we haven't processed the current group before, add it to the cache and proceed If ($null -eq [GroupTypeCache]::CheckedGroups[$PrincipalId]){ [GroupTypeCache]::CheckedGroups.Add($PrincipalId, $true) } # If we have processed it before, then skip it to avoid unnecessary cycles else { continue } # Get all the configuration rules for the current PIM group - get member not owner configs $PolicyAssignment = Get-MgBetaPolicyRoleManagementPolicyAssignment -All -ErrorAction Stop -Filter "scopeId eq '$PrincipalId' and scopeType eq 'Group' and roleDefinitionId eq 'member'" | Select-Object -Property PolicyId # Add each configuration rule to the array. There are usually about 17 configurations for a group. # Get the detailed configuration settings $MemberPolicyRules = Get-MgBetaPolicyRoleManagementPolicyRule -All -ErrorAction Stop -UnifiedRoleManagementPolicyId $PolicyAssignment.PolicyId # Filter for the PIM group so we can grab its name $PIMGroup = $PIMGroups | Where-Object {$_.Id -eq $PrincipalId} # $SourceGroup = Get-MgBetaGroup -Filter "id eq '$PrincipalId' " | Select-Object -Property DisplayName AddRuleSource -Source $PIMGroup.DisplayName -SourceType "PIM Group" -Rules $MemberPolicyRules $RoleRules = $Role.psobject.Properties | Where-Object {$_.Name -eq 'Rules'} if ($RoleRules){ # Appending rules $Role.Rules += $MemberPolicyRules } else { # Adding rules node if it is not already present $Role | Add-Member -Name "Rules" -Value $MemberPolicyRules -MemberType NoteProperty } } } } function GetConfigurationsForRoles{ param ( [ValidateNotNullOrEmpty()] [array] $PrivilegedRoleArray, [ValidateNotNullOrEmpty()] [array] $AllRoleAssignments ) # Get all the configuration settings (aka rules) for all the roles in the tenant $RolePolicyAssignments = Get-MgBetaPolicyRoleManagementPolicyAssignment -All -ErrorAction Stop -Filter "scopeId eq '/' and scopeType eq 'DirectoryRole'" foreach ($Role in $PrivilegedRoleArray) { $RolePolicies = @() $RoleTemplateId = $Role.RoleTemplateId # Get a list of the configuration rules assigned to this role $PolicyAssignment = $RolePolicyAssignments | Where-Object -Property RoleDefinitionId -eq -Value $RoleTemplateId # Get the detailed configuration settings $RolePolicies = Get-MgBetaPolicyRoleManagementPolicyRule -All -ErrorAction Stop -UnifiedRoleManagementPolicyId $PolicyAssignment.PolicyId # Get a list of the users / groups assigned to this role $RoleAssignments = @($AllRoleAssignments | Where-Object { $_.RoleDefinitionId -eq $RoleTemplateId }) # Store the data that we retrieved in the Role object which is part of the privileged role array $Role | Add-Member -Name "Assignments" -Value $RoleAssignments -MemberType NoteProperty $RoleRules = $Role.psobject.Properties | Where-Object {$_.Name -eq 'Rules'} AddRuleSource -Source $Role.DisplayName -SourceType "Directory Role" -Rules $RolePolicies if ($RoleRules){ $Role.Rules += $RolePolicies } else { $Role | Add-Member -Name "Rules" -Value $RolePolicies -MemberType NoteProperty } } } function Get-PrivilegedRole { <# .Description Returns an array of the highly privileged roles along with the users actively assigned to the role and the security configurations applied to the role .Functionality Internal #> param ( [ValidateNotNullOrEmpty()] [bool] $TenantHasPremiumLicense, [ValidateNotNullOrEmpty()] [string] $M365Environment ) try { $PrivilegedRoles = [ScubaConfig]::ScubaDefault('DefaultPrivilegedRoles') # Get a list of the RoleTemplateId values for the privileged roles in the list above. # The RoleTemplateId value is passed to other cmdlets to retrieve role/group security configuration rules and user/group assignments. $PrivilegedRoleArray = Get-MgBetaDirectoryRoleTemplate -All -ErrorAction Stop | Where-Object { $_.DisplayName -in $PrivilegedRoles } | Select-Object "DisplayName", @{Name='RoleTemplateId'; Expression={$_.Id}} # If the tenant has the premium license then you can access the PIM service to get the role configuration policies and the active role assigments if ($TenantHasPremiumLicense) { # Clear the cache of already processed PIM groups because this is a static variable [GroupTypeCache]::CheckedGroups.Clear() # Get ALL the roles and users actively assigned to them $graphArgs = @{ "commandlet" = "Get-MgBetaRoleManagementDirectoryRoleAssignmentScheduleInstance" "M365Environment" = $M365Environment } $AllRoleAssignments = Invoke-GraphDirectly @graphArgs # Each of the helper functions below add configuration settings (aka rules) to the role array. # Get the PIM configurations for the roles GetConfigurationsForRoles -PrivilegedRoleArray $PrivilegedRoleArray -AllRoleAssignments $AllRoleAssignments # Get the PIM configurations for the groups GetConfigurationsForPimGroups -PrivilegedRoleArray $PrivilegedRoleArray -AllRoleAssignments $AllRoleAssignments -M365Environment $M365Environment } } catch { Write-Warning "An error occurred in Get-PrivilegedRole: $($_.Exception.Message)" Write-Warning "Stack trace: $($_.ScriptStackTrace)" throw $_ } # Return the array $PrivilegedRoleArray } # SIG # Begin signature block # MIIuuQYJKoZIhvcNAQcCoIIuqjCCLqYCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDZsK0Raef2lP1r # 4ygaJaqG4eQMbMOpyfAs9ygc1jgT6KCCE6MwggWQMIIDeKADAgECAhAFmxtXno4h # MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV # BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z # ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB # AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z # G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ # anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s # Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL # 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb # BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3 # JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c # AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx # YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0 # viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL # T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud # EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf # Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk # aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS # PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK # 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB # cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp # 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg # dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri # RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7 # 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5 # nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3 # i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H # EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G # CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C # 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce # 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da # E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T # SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA # FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh # D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM # 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z # 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05 # huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY # mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP # /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN # BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry # sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL # IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf # Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh # OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh # dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV # 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j # wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH # Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC # XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l # /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW # eE4wggdXMIIFP6ADAgECAhANkQ8dPvvR0q3Ytt4H0T3aMA0GCSqGSIb3DQEBCwUA # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwHhcNMjQwMTMwMDAwMDAwWhcNMjUwMTI5MjM1OTU5WjBfMQsw # CQYDVQQGEwJVUzEdMBsGA1UECBMURGlzdHJpY3Qgb2YgQ29sdW1iaWExEzARBgNV # BAcTCldhc2hpbmd0b24xDTALBgNVBAoTBENJU0ExDTALBgNVBAMTBENJU0EwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCT1y7uJCQax8JfiDEYgpiU9URj # EXCTRqtZbDALM9rPUudiuM3mj6A1SUSAAWYv6DTsvGPvxyMI2Idg0mQunl4Ms9DJ # yVwe5k4+Anj/73Nx1AbOPYP8xRZcD10FkctKGhV0PzvrDcwU15hsQWtiepFgg+bX # fHkGMeu426oc69f43vKE43DiqKTf0/UBX/qgpj3JZvJ3zc1kilBOv4sBCksfCjbW # tLZD0tqAgBsNPo3Oy5mQG31E1eZdTNvrdTnEXacSwb3k615z7mHy7nqBUkOruZ9E # tnvC2qla+uL3ks91O/e/LnKzH9Lj1JmEBf6jwPN/MYR9Dymni4Mi3AQ8mpQMyFmi # XcSHymibSNbtTMavpdBWjFfrcvPETX7krROUOoLzMQmNgHArceSh55tgvDRdSU5c # WK3BTvK3l3mgCdgjre7XGYxV3W8apyxk5+RKfHdbv9cpRwpSuDnI8sHeqmB3fnfo # Cr1PPu4WhKegt20CobhDVybiBdhDVqUdR53ful4N/coQOEHDrIExB5nJf9Pvdrza # DyIGKAMIXD79ba5/rQEo+2cA66oJkPlvB5hEGI/jtDcYwDBgalbwB7Kc8zAAhl6+ # JvHfYpXOkppSfEQbaRXZI+LGXWQAFa5pJDfDEAyZSXprStgw594sWUOysp+UOxFe # kSA4mBr0o1jVpdaulwIDAQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8R # hvv+YXsIiGX0TkIwHQYDVR0OBBYEFAmyTB5bcWyA+8+rq540jPRLJ1nYMD4GA1Ud # IAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNl # cnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMw # gbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j # cmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0 # ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMIGUBggrBgEF # BQcBAQSBhzCBhDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t # MFwGCCsGAQUFBzAChlBodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNl # cnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJ # BgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQAh2Jnt9IPoBvOQlYQUlCP9iJ5y # XAvEWe1camOwedqMZsHEPpT2yd6+fMzPZmV3/bYJgaN2OrDS1snf62S7yc+AulVw # PAXSp1lSAiFEbZ6PFEdEBIag9B65Mp/cvRtJsIWQIc//jWqFMHpkU6r3MW9YARRu # vaIf5/0qlM4VEwn3lTf+jJdxhhyoOFTWWd3BrlMPcT06z6F6hFfyycQkZ3Y9wEJ3 # uOU9bCNLZL1HCjlKT+oI0WsgeRdbe2sYrnvv9NmDY9oEi8PEq+DGjiTgLbY5OcAX # uUogPPw6gbcuNn8Hq6FFKPIQxaksB8dF8Gw4m2lQoUWESPRF8Zaq9lmZN3+QzA79 # yskfJtAFqz3gUP5wJBdNfi/u1sGbLI0QnJQkIKfFuz7DfDPldw0gIl05BIYwZBmj # TpFRu1/+gIlP1Ul4L/wt9Lxk6pglObLsdxHP2UQrG30JaUN0gv3xZMBBByHGVVTe # cyU4qwJ0ulMdv/kjHwh+m58uOF8gHXLfyBmOjYpohN3+l0rS0qdArZMNSmLTA7N8 # n3V3AZLKB//1yhPt++gR4pCFdXmgwYDDLRxjlV0cMsG1UeSQUdI0aieh/grg5TQO # CergVXS5h3sz5U0ZQPWND41LJhA0gF2OGZNHdUc9+0dwTsfxAERrjaTdeZp0/rdZ # 9iGBoiRsS4U86S8xkDGCGmwwghpoAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNV # BAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0 # IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQDZEPHT770dKt # 2LbeB9E92jANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgACh # AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM # BgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCCPHKtHzNnANuy1/8WaQEodE59Q # UhYzpLwsVOn56BJW+jANBgkqhkiG9w0BAQEFAASCAgCTvJSh6cFfjmNcI+LIBzR3 # vfgb4zMt0eYkVHuqshzzkRLADeDy8NwF8VAonk44CjzLWdnMahzQgly8otfLESJg # umRVV96CmaoiMBtawbtu2jbmAV4BRVhuxWtYgrcco1x5WKe98oMqothF2+vscmzf # uAKYOSjBq9v97GsMUwGKkRV7I82APUHM+i1dHQ/IUepnOdgiYZ4qVJ2jABz0LzA7 # qX3pOj7v/XNuBQuIBmLXQJwRLOIfcVFW9TGn1zDOTncvZ6T72T2V/qA6yY2Q1UrU # XCYLNYaxhqato9DdV097WDYopJnqSODX87pMUPsTXe8Piy8L+T7tjlYXgkIjw7aV # ADUOhLpM+Ooy07eNZeM0nph4DKuV8te6FwY8/dgEW7ZaxDoQEqCEpOGL3SL8WxhZ # G/469loLJWAj3A2RiQryyRLritMbzThzXy6uCHAE9969KiYuq/8DvaTmZEj+RTHZ # D2KyyS1dWnNFdDBMcdDs2/bgHgBRayAwi8ApP8Yfs5tP+ZRJ7B4eoy9M26TRAlkE # jDpZ0jZjl2GN47ZyOztlS7//E3xk86bm9aeToigumRUuUhhZualghx8vGAjK/1aN # YC0YXrLVnkMNRzyVXIlCKW/6GMvdUL7bWnl+PBy4JLlgdpoCh+rKgABAiGqQrUO1 # uyupOQQINYNvvNBsTQ31x6GCFzkwghc1BgorBgEEAYI3AwMBMYIXJTCCFyEGCSqG # SIb3DQEHAqCCFxIwghcOAgEDMQ8wDQYJYIZIAWUDBAIBBQAwdwYLKoZIhvcNAQkQ # AQSgaARmMGQCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCDxxSaGBEAR # ifo6tIjdIAIyRT/OivXtDSmJi8pu8oaN4gIQSdf4S0HEJijTJ+E2bC01cBgPMjAy # NDEwMTAxOTMwMjRaoIITAzCCBrwwggSkoAMCAQICEAuuZrxaun+Vh8b56QTjMwQw # DQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0 # LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB # MjU2IFRpbWVTdGFtcGluZyBDQTAeFw0yNDA5MjYwMDAwMDBaFw0zNTExMjUyMzU5 # NTlaMEIxCzAJBgNVBAYTAlVTMREwDwYDVQQKEwhEaWdpQ2VydDEgMB4GA1UEAxMX # RGlnaUNlcnQgVGltZXN0YW1wIDIwMjQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw # ggIKAoICAQC+anOf9pUhq5Ywultt5lmjtej9kR8YxIg7apnjpcH9CjAgQxK+CMR0 # Rne/i+utMeV5bUlYYSuuM4vQngvQepVHVzNLO9RDnEXvPghCaft0djvKKO+hDu6O # bS7rJcXa/UKvNminKQPTv/1+kBPgHGlP28mgmoCw/xi6FG9+Un1h4eN6zh926SxM # e6We2r1Z6VFZj75MU/HNmtsgtFjKfITLutLWUdAoWle+jYZ49+wxGE1/UXjWfISD # mHuI5e/6+NfQrxGFSKx+rDdNMsePW6FLrphfYtk/FLihp/feun0eV+pIF496OVh4 # R1TvjQYpAztJpVIfdNsEvxHofBf1BWkadc+Up0Th8EifkEEWdX4rA/FE1Q0rqViT # bLVZIqi6viEk3RIySho1XyHLIAOJfXG5PEppc3XYeBH7xa6VTZ3rOHNeiYnY+V4j # 1XbJ+Z9dI8ZhqcaDHOoj5KGg4YuiYx3eYm33aebsyF6eD9MF5IDbPgjvwmnAalNE # eJPvIeoGJXaeBQjIK13SlnzODdLtuThALhGtyconcVuPI8AaiCaiJnfdzUcb3dWn # qUnjXkRFwLtsVAxFvGqsxUA2Jq/WTjbnNjIUzIs3ITVC6VBKAOlb2u29Vwgfta8b # 2ypi6n2PzP0nVepsFk8nlcuWfyZLzBaZ0MucEdeBiXL+nUOGhCjl+QIDAQABo4IB # izCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAww # CgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMB8G # A1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCPnshvMB0GA1UdDgQWBBSfVywDdw4o # FZBmpWNe7k+SH3agWzBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2VGltZVN0YW1w # aW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCBgDAkBggrBgEFBQcwAYYYaHR0cDov # L29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUFBzAChkxodHRwOi8vY2FjZXJ0cy5k # aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2VGltZVN0 # YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQA9rR4fdplb4ziEEkfZQ5H2 # EdubTggd0ShPz9Pce4FLJl6reNKLkZd5Y/vEIqFWKt4oKcKz7wZmXa5VgW9B76k9 # NJxUl4JlKwyjUkKhk3aYx7D8vi2mpU1tKlY71AYXB8wTLrQeh83pXnWwwsxc1Mt+ # FWqz57yFq6laICtKjPICYYf/qgxACHTvypGHrC8k1TqCeHk6u4I/VBQC9VK7iSpU # 5wlWjNlHlFFv/M93748YTeoXU/fFa9hWJQkuzG2+B7+bMDvmgF8VlJt1qQcl7YFU # MYgZU1WM6nyw23vT6QSgwX5Pq2m0xQ2V6FJHu8z4LXe/371k5QrN9FQBhLLISZi2 # yemW0P8ZZfx4zvSWzVXpAb9k4Hpvpi6bUe8iK6WonUSV6yPlMwerwJZP/Gtbu3CK # ldMnn+LmmRTkTXpFIEB06nXZrDwhCGED+8RsWQSIXZpuG4WLFQOhtloDRWGoCwwc # 6ZpPddOFkM2LlTbMcqFSzm4cd0boGhBq7vkqI1uHRz6Fq1IX7TaRQuR+0BGOzISk # cqwXu7nMpFu3mgrlgbAW+BzikRVQ3K2YHcGkiKjA4gi4OA/kz1YCsdhIBHXqBzR0 # /Zd2QwQ/l4Gxftt/8wY3grcc/nS//TVkej9nmUYu83BDtccHHXKibMs/yXHhDXNk # oPIdynhVAku7aRZOwqw6pDCCBq4wggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlsw # DQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0 # IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNl # cnQgVHJ1c3RlZCBSb290IEc0MB4XDTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1 # OVowYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYD # VQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFt # cGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9 # cklRVcclA8TykTepl1Gh1tKD0Z5Mom2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+d # H54PMx9QEwsmc5Zt+FeoAn39Q7SE2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+Qtxn # jupRPfDWVtTnKC3r07G1decfBmWNlCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9d # rMvohGS0UvJ2R/dhgxndX7RUCyFobjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02 # DVzV5huowWR0QKfAcsW6Th+xtVhNef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aP # TnYVVSZwmCZ/oBpHIEPjQ2OAe3VuJyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De # 4z6ic/rnH1pslPJSlRErWHRAKKtzQ87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPg # v/CiPMpC3BhIfxQ0z9JMq++bPf4OuGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIs # VzV5K6jzRWC8I41Y99xh3pP+OcD5sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7 # W4oiqMEmCPkUEBIDfV8ju2TjY+Cm4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTu # zuldyF4wEr1GnrXTdrnSDmuZDNIztM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8E # CDAGAQH/AgEAMB0GA1UdDgQWBBS6FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSME # GDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0l # BAwwCgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRw # Oi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8 # MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0 # ZWRSb290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATAN # BgkqhkiG9w0BAQsFAAOCAgEAfVmOwJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/ # GPvHUF3iSyn7cIoNqilp/GnBzx0H6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBM # Yh0MCIKoFr2pVs8Vc40BIiXOlWk/R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4s # nuCKrOX9jLxkJodskr2dfNBwCnzvqLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKj # I/rAJ4JErpknG6skHibBt94q6/aesXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HB # anHZxhOACcS2n82HhyS7T6NJuXdmkfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVj # mScsPT9rp/Fmw0HNT7ZAmyEhQNC3EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87 # eK1MrfvElXvtCl8zOYdBeHo46Zzh3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttv # FXseGYs2uJPU5vIXmVnKcPA3v5gA3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc6 # 1RWYMbRiCQ8KvYHZE/6/pNHzV9m8BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2 # QqYphwlHK+Z/GqSFD/yYlvZVVCsfgPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3W # fPwwggWNMIIEdaADAgECAhAOmxiO+dAt5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUA # MGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsT # EHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQg # Um9vdCBDQTAeFw0yMjA4MDEwMDAwMDBaFw0zMTExMDkyMzU5NTlaMGIxCzAJBgNV # BAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdp # Y2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIw # DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIwaTPswqcl # LskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLKEdLkX9YF # PFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4TmdDttceIt # DBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembud8hIqGZX # V59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1 # ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1XXhm2Tox # RJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVldQnaHiZdp # ekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF # 30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSmM9GJB+G9 # t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzTQRESW+UQ # UOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6KxfgommfXk # aS+YHS312amyHeUbAgMBAAGjggE6MIIBNjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud # DgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEt # UYunpyGd823IDzAOBgNVHQ8BAf8EBAMCAYYweQYIKwYBBQUHAQEEbTBrMCQGCCsG # AQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0 # dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RD # QS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29t # L0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAw # DQYJKoZIhvcNAQEMBQADggEBAHCgv0NcVec4X6CjdBs9thbX979XB72arKGHLOyF # XqkauyL4hxppVCLtpIh3bb0aFPQTSnovLbc47/T/gLn4offyct4kvFIDyE7QKt76 # LVbP+fT3rDB6mouyXtTP0UNEm0Mh65ZyoUi0mcudT6cGAxN3J0TU53/oWajwvy8L # punyNDzs9wPHh6jSTEAZNUZqaVSwuKFWjuyk1T3osdz9HNj0d1pcVIxv76FQPfx2 # CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPFmCLBsln1VWvPJ6tsds5vIy30fnFqI2si # /xK4VC0nftg62fC2h5b9W9FcrBjDTZ9ztwGpn1eqXijiuZQxggN2MIIDcgIBATB3 # MGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UE # AxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBp # bmcgQ0ECEAuuZrxaun+Vh8b56QTjMwQwDQYJYIZIAWUDBAIBBQCggdEwGgYJKoZI # hvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMTAxOTMw # MjRaMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYEFNvThe5i29I+e+T2cUhQhyTVhltF # MC8GCSqGSIb3DQEJBDEiBCB5kts74RM+G2vpzavl1GyeHdNnZJkS+ZmM6uJMEU8G # 5TA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCB2dp+o8mMvH0MLOiMwrtZWdf7Xc9sF # 1mW5BZOYQ4+a2zANBgkqhkiG9w0BAQEFAASCAgCaIf7u3bBj85yJPr0qrQqaz9WW # FhZr296iAFBZE0ZIjPGW1eqDYN7RsGWiGOHc/+iZmV7S6mwSTUivr2aO7L8Gkht5 # afjOpglWl5Z8rKlvDafEXAoL8GqZ6xrUlW3uTlpJlQ0wZbJmNAWoYwvqqQeW7k/n # jG151HzgkE+eXJcqDEKNCy3ghHhmxK3wLQjb7vEtHChSUXGxaEPCC2wIchx5ypVm # eyr2hMhfyR8CqPyX603Xjq0TBWHkygco1/vZek63pJuVzjL4mv3TmBIMXNu6Fx3x # 7IGpRg/m0V3xj7iLSFyzEIVsmWtHfxF4w2quRDWlRsVHDjfTdYeSFRtOezDH+mBl # /CiBeh1vc2D6nTawZVr7jXUAz8K7F+7OfSCrrtXvNQI6FlaNsWE0VZ2GnsU/pjua # 5OvuQABEFnFbxGl9VTJMsyUhKmoQjsBp7ypd2iJnH8SQKJryf6ZPFuOHE4xn10DJ # /m0OUnYsYRSY3ll0Bd8LKWSGjDUy/H7UpK40QvV4iXTWxqzRxpy9pe2cxrXAcpfP # KB1kxS9M1LBFhrmD1s7X1J01IqSqIf0knZiMXfJ82GchKBHKgqpJnQOaChjgz92P # Bn5+Rnofj8d0yvZhBfCC/T3u82hsYVHsUP2bIbZWxBGT/GLPTBHHzTLnkbZt2CQO # HImwT7UTn4n5ymSJsg== # SIG # End signature block |