Modules/Providers/ProviderHelpers/AADRiskyPermissionsHelper.psm1
|
Import-Module -Name $PSScriptRoot/../../Utility/Utility.psm1 -Function Invoke-GraphDirectly, ConvertFrom-GraphHashtable, Invoke-GraphBatchRequest # Module-scoped cache for RiskyAppPermissions.json - loaded once, reused across all function calls $script:CachedRiskyAppPermissionsJson = $null $script:CachedPermissionLookup = $null function Get-ResourcePermissions { param( [ValidateNotNullOrEmpty()] [string] $M365Environment, [hashtable] $ResourcePermissionCache, [string] $ResourceAppId ) try { if ($null -eq $ResourcePermissionCache) { $ResourcePermissionCache = @{} } if (-not $ResourcePermissionCache.ContainsKey($ResourceAppId)) { # v1.0 Graph endpoint is used here because it contains the oauth2PermissionScopes property $result = ( Invoke-GraphDirectly ` -Commandlet "Get-MgServicePrincipal" ` -M365Environment $M365Environment ` -QueryParams @{ '$filter' = "appId eq '$ResourceAppId'" '$select' = "appRoles,oauth2PermissionScopes" } ).Value $ResourcePermissionCache[$ResourceAppId] = $result } return $ResourcePermissionCache[$ResourceAppId] } catch { Write-Warning "An error occurred in Get-ResourcePermissions: $($_.Exception.Message)" Write-Warning "Stack trace: $($_.ScriptStackTrace)" throw $_ } } function Get-RiskyAppPermissionsJson { <# .Description Returns the parsed RiskyAppPermissions.json data. Uses a module-scoped cache to avoid redundant file reads and JSON parsing on subsequent calls. .Functionality Internal #> process { if ($null -eq $script:CachedRiskyAppPermissionsJson) { try { $SchemasPath = Join-Path -Path ((Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.FullName) -ChildPath "schemas" $script:CachedRiskyAppPermissionsJson = Get-Content -Path ( Join-Path -Path (Get-Item -Path $SchemasPath) -ChildPath "RiskyAppPermissions.json" ) -Raw | ConvertFrom-Json # Build the hashtable lookup on first load $script:CachedPermissionLookup = New-PermissionLookup -Json $script:CachedRiskyAppPermissionsJson } catch { Write-Warning "An error occurred in Get-RiskyAppPermissionsJson: $($_.Exception.Message)" Write-Warning "Stack trace: $($_.ScriptStackTrace)" throw $_ } } return $script:CachedRiskyAppPermissionsJson } } function New-PermissionLookup { <# .Description Builds a nested hashtable from the RiskyAppPermissions.json PSObject for O(1) permission lookups. Structure: $Lookup[$ResourceDisplayName][$RoleType][$Guid] = @{ Name; RiskLevel } .Functionality Internal #> param ( [ValidateNotNullOrEmpty()] [PSCustomObject] $Json ) $Lookup = @{} foreach ($Resource in $Json.permissions.PSObject.Properties) { $ResourceName = $Resource.Name $Lookup[$ResourceName] = @{} foreach ($RoleType in $Resource.Value.PSObject.Properties) { # Skip internal keys like _excludedDelegated if ($RoleType.Name.StartsWith("_")) { continue } $RoleTypeName = $RoleType.Name $Lookup[$ResourceName][$RoleTypeName] = @{} foreach ($Perm in $RoleType.Value.PSObject.Properties) { $Lookup[$ResourceName][$RoleTypeName][$Perm.Name] = @{ Name = $Perm.Value.Name RiskLevel = $Perm.Value.RiskLevel } } } } return $Lookup } function Get-PermissionLookup { <# .Description Returns the cached hashtable lookup for risky permissions. Builds it if not yet initialized. .Functionality Internal #> param ( [PSCustomObject] $RiskyAppPermissionsJson ) if ($null -ne $script:CachedPermissionLookup) { return $script:CachedPermissionLookup } if ($null -eq $RiskyAppPermissionsJson) { $RiskyAppPermissionsJson = Get-RiskyAppPermissionsJson } $script:CachedPermissionLookup = New-PermissionLookup -Json $RiskyAppPermissionsJson return $script:CachedPermissionLookup } function New-RiskyAppResourceLookup { <# .Description Builds a pair of hashtable lookups for risky resource mappings. .Functionality Internal #> param ( [ValidateNotNullOrEmpty()] [PSCustomObject] $RiskyAppPermissionsJson ) $Lookup = @{ AppIdToName = @{} NameToAppId = @{} } foreach ($Property in $RiskyAppPermissionsJson.resources.PSObject.Properties) { $Lookup.AppIdToName[$Property.Name] = $Property.Value $Lookup.NameToAppId[$Property.Value] = $Property.Name } return $Lookup } function Get-PermissionTypeDetails { <# .Description Resolves role type, display name, and consent requirements using cached per-resource lookups. .Functionality Internal #> param ( [ValidateNotNullOrEmpty()] [object] $ResourceAppPermissions, [ValidateNotNull()] [hashtable] $ResourcePermissionTypeCache, [ValidateNotNullOrEmpty()] [string] $ResourceAppId, [ValidateNotNullOrEmpty()] [string] $RoleId, [string] $DeclaredRoleType ) if (-not $ResourcePermissionTypeCache.ContainsKey($ResourceAppId)) { $RoleLookup = @{} foreach ($Role in @($ResourceAppPermissions.appRoles)) { if ($null -ne $Role -and $null -ne $Role.id) { $RoleLookup[[string]$Role.id] = $Role } } $ScopeLookup = @{} foreach ($Scope in @($ResourceAppPermissions.oauth2PermissionScopes)) { if ($null -ne $Scope -and $null -ne $Scope.id) { $ScopeLookup[[string]$Scope.id] = $Scope } } $ResourcePermissionTypeCache[$ResourceAppId] = @{ AppRoles = $RoleLookup Scopes = $ScopeLookup } } $PermissionTypeLookup = $ResourcePermissionTypeCache[$ResourceAppId] $Role = $PermissionTypeLookup.AppRoles[$RoleId] if ($null -ne $Role) { return @{ ReadableRoleType = "Application" RoleDisplayName = $Role.value RequiresAdminConsent = $true } } $Scope = $PermissionTypeLookup.Scopes[$RoleId] if ($null -ne $Scope) { return @{ ReadableRoleType = "Delegated" RoleDisplayName = $Scope.value RequiresAdminConsent = $Scope.type -eq "Admin" } } # Preserve role type semantics when the caller explicitly declared delegated/role and Graph object is missing. if ($DeclaredRoleType -eq "Role") { return @{ ReadableRoleType = "Application" RoleDisplayName = $null RequiresAdminConsent = $true } } return @{ ReadableRoleType = "Delegated" RoleDisplayName = $null RequiresAdminConsent = $false } } function Format-Permission { <# .Description Returns an API permission from either application/service principal which maps to the list of permissions declared in RiskyAppPermissions.json .Functionality #Internal ##> param ( [ValidateNotNullOrEmpty()] [PSCustomObject] $Json, [ValidateNotNullOrEmpty()] [string] $AppDisplayName, [ValidateNotNullOrEmpty()] [string] $Id, [string] $RoleType, [string] $RoleDisplayName, [ValidateNotNullOrEmpty()] [boolean] $IsAdminConsented, [ValidateNotNullOrEmpty()] [boolean] $RequiresAdminConsent ) $Map = @() if ($null -ne $RoleType) { $Lookup = Get-PermissionLookup -RiskyAppPermissionsJson $Json $IsRisky = $false $RiskLevel = $null if ($Lookup.ContainsKey($AppDisplayName) -and $Lookup[$AppDisplayName].ContainsKey($RoleType) -and $Lookup[$AppDisplayName][$RoleType].ContainsKey($Id)) { $IsRisky = $true $RiskLevel = $Lookup[$AppDisplayName][$RoleType][$Id].RiskLevel } $Map += [PSCustomObject]@{ RoleId = $Id RoleType = if ($null -ne $RoleType) { $RoleType } else { $null } RoleDisplayName = if ($null -ne $RoleDisplayName) { $RoleDisplayName } else { $null } ApplicationDisplayName = $AppDisplayName IsAdminConsented = $IsAdminConsented RequiresAdminConsent = $RequiresAdminConsent IsRisky = $IsRisky RiskLevel = $RiskLevel } } return $Map } function Format-Credentials { <# .Description Returns an array of valid/expired credentials .Functionality #Internal ##> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( "PSReviewUnusedParameter", "IsFromApplication", Justification = "False positive due to variable scoping" )] param ( [Object[]] $AccessKeys, [ValidateNotNullOrEmpty()] [boolean] $IsFromApplication, [switch] $IsFederated ) process { $ValidCredentials = @() if ($IsFederated) { $RequiredKeys = @("Id", "Name", "Description", "Issuer", "Subject", "Audiences") } else { $RequiredKeys = @("KeyId", "DisplayName", "StartDateTime", "EndDateTime") } foreach ($Credential in $AccessKeys) { # Only format credentials with the correct keys $MissingKeys = $RequiredKeys | Where-Object { -not ($Credential.PSObject.Properties.Name -contains $_) } if ($MissingKeys.Count -eq 0) { if ($IsFederated) { # $Credential is of type PSCredential which is immutable, create a copy $CredentialCopy = $Credential | Select-Object -Property ` Id, Name, Description, Issuer, Subject, Audiences,` @{ Name = "IsFromApplication"; Expression = { $IsFromApplication }} } else { $CredentialCopy = $Credential | Select-Object -Property ` KeyId, DisplayName, StartDateTime, EndDateTime, ` @{ Name = "IsFromApplication"; Expression = { $IsFromApplication }} } $ValidCredentials += $CredentialCopy } } if ($null -eq $AccessKeys -or $AccessKeys.Count -eq 0 -or $ValidCredentials.Count -eq 0) { return $null } return $ValidCredentials } } function Merge-Credentials { <# .Description Merge credentials from multiple resources into a single resource .Functionality #Internal ##> param ( [Object[]] $ApplicationAccessKeys, [Object[]] $ServicePrincipalAccessKeys ) # Both application/sp objects have key and federated credentials. # Conditionally merge the two together, select only application/service principal creds, or none. $MergedCredentials = @() if ($null -ne $ServicePrincipalAccessKeys -and $null -ne $ApplicationAccessKeys) { # Both objects valid $MergedCredentials = @($ServicePrincipalAccessKeys) + @($ApplicationAccessKeys) } elseif ($null -eq $ServicePrincipalAccessKeys -and $null -ne $ApplicationAccessKeys) { # Only application credentials valid $MergedCredentials = @($ApplicationAccessKeys) } elseif ($null -ne $ServicePrincipalAccessKeys -and $null -eq $ApplicationAccessKeys) { # Only service principal credentials valid $MergedCredentials = @($ServicePrincipalAccessKeys) } else { # Neither credentials are valid $MergedCredentials = $null } return $MergedCredentials } function Get-ApplicationsWithRiskyPermissions { <# .Description Returns an array of applications where each item contains its Object ID, App ID, Display Name, Key/Password/Federated Credentials, and risky API permissions. .Functionality #Internal ##> param ( [ValidateNotNullOrEmpty()] [string] $M365Environment, [hashtable] $ResourcePermissionCache, [PSCustomObject] $RiskyAppPermissionsJson ) process { try { if ($null -eq $RiskyAppPermissionsJson) { $RiskyAppPermissionsJson = Get-RiskyAppPermissionsJson } $ResourceLookup = New-RiskyAppResourceLookup -RiskyAppPermissionsJson $RiskyAppPermissionsJson $ResourcePermissionTypeCache = @{} # Get all applications in the tenant with only required fields to reduce payload size. $Applications = ( Invoke-GraphDirectly ` -commandlet "Get-MgBetaApplication" ` -M365Environment $M365Environment ` -QueryParams @{ '$select' = "id,appId,displayName,signInAudience,requiredResourceAccess,keyCredentials,passwordCredentials" } ).Value $ApplicationResults = [System.Collections.Generic.List[object]]::new() foreach ($App in $Applications) { # `AzureADMyOrg` = single tenant; `AzureADMultipleOrgs` = multi tenant $IsMultiTenantEnabled = $false if ($App.SignInAudience -eq "AzureADMultipleOrgs") { $IsMultiTenantEnabled = $true } # Map application permissions against RiskyAppPermissions.json $MappedPermissions = [System.Collections.Generic.List[object]]::new() foreach ($Resource in $App.RequiredResourceAccess) { # Returns both application and delegated permissions $Roles = $Resource.ResourceAccess $ResourceAppId = $Resource.ResourceAppId if (-not $ResourceLookup.AppIdToName.ContainsKey($ResourceAppId)) { continue } $ResourceDisplayName = $ResourceLookup.AppIdToName[$ResourceAppId] $ResourceAppPermissions = Get-ResourcePermissions ` -M365Environment $M365Environment ` -ResourcePermissionCache $ResourcePermissionCache ` -ResourceAppId $ResourceAppId if ($null -eq $ResourceAppPermissions) { Write-Warning "No permissions found for resource app ID: $ResourceAppId" continue } # Additional processing is required to determine if a permission is admin consented. # Initially assume admin consent is false since we reference the application's manifest, # then update the value later when its compared to service principal permissions. $IsAdminConsented = $false foreach ($Role in $Roles) { $RoleId = [string]$Role.Id $PermissionTypeDetails = Get-PermissionTypeDetails ` -ResourceAppPermissions $ResourceAppPermissions ` -ResourcePermissionTypeCache $ResourcePermissionTypeCache ` -ResourceAppId $ResourceAppId ` -RoleId $RoleId ` -DeclaredRoleType $Role.Type [void]$MappedPermissions.AddRange(@( Format-Permission ` -Json $RiskyAppPermissionsJson ` -AppDisplayName $ResourceDisplayName ` -Id $RoleId ` -RoleType $PermissionTypeDetails.ReadableRoleType ` -RoleDisplayName $PermissionTypeDetails.RoleDisplayName ` -IsAdminConsented $IsAdminConsented ` -RequiresAdminConsent $PermissionTypeDetails.RequiresAdminConsent )) } } $RiskyPermissions = @($MappedPermissions | Where-Object { $_.IsRisky -eq $true }) # Exclude applications without risky permissions if ($RiskyPermissions.Count -gt 0) { # Fetch federated credentials only for applications that are confirmed risky. $FederatedCredentials = (Invoke-GraphDirectly -commandlet "Get-MgBetaApplicationFederatedIdentityCredential" -M365Environment $M365Environment -Id $App.Id).Value $FederatedCredentialsResults = @() if ($FederatedCredentials -is [System.Collections.IEnumerable] -and $FederatedCredentials.Count -gt 0) { foreach ($FederatedCredential in $FederatedCredentials) { $FederatedCredentialsResults += [PSCustomObject]@{ Id = $FederatedCredential.Id Name = $FederatedCredential.Name Description = $FederatedCredential.Description Issuer = $FederatedCredential.Issuer Subject = $FederatedCredential.Subject Audiences = $FederatedCredential.Audiences | Out-String } } } else { $FederatedCredentialsResults = $null } [void]$ApplicationResults.Add([PSCustomObject]@{ ObjectId = $App.Id AppId = $App.AppId DisplayName = $App.DisplayName IsMultiTenantEnabled = $IsMultiTenantEnabled # Credentials from application and service principal objects may get merged in other cmdlets. # Differentiate between the two by setting IsFromApplication=$true KeyCredentials = Format-Credentials -AccessKeys $App.KeyCredentials -IsFromApplication $true PasswordCredentials = Format-Credentials -AccessKeys $App.PasswordCredentials -IsFromApplication $true FederatedCredentials = Format-Credentials -AccessKeys $FederatedCredentialsResults -IsFromApplication $true -IsFederated Permissions = $MappedPermissions.ToArray() }) } } } catch { Write-Warning "An error occurred in Get-ApplicationsWithRiskyPermissions: $($_.Exception.Message)" Write-Warning "Stack trace: $($_.ScriptStackTrace)" throw $_ } return $ApplicationResults.ToArray() } } function Get-ServicePrincipalsWithRiskyPermissions { <# .Description Returns an array of service principals where each item contains its Object ID, App ID, Display Name, Key/Password Credentials, and risky API permissions. .Functionality #Internal ##> param ( [ValidateNotNullOrEmpty()] [string] $M365Environment, [hashtable] $ResourcePermissionCache, [PSCustomObject] $RiskyAppPermissionsJson ) process { try { if ($null -eq $RiskyAppPermissionsJson) { $RiskyAppPermissionsJson = Get-RiskyAppPermissionsJson } $ServicePrincipalResults = [System.Collections.Generic.List[object]]::new() $ResourceLookup = New-RiskyAppResourceLookup -RiskyAppPermissionsJson $RiskyAppPermissionsJson $ResourcePermissionTypeCache = @{} # Get all service principals with only required fields to reduce payload size. $ServicePrincipals = ( Invoke-GraphDirectly ` -commandlet "Get-MgBetaServicePrincipal" ` -M365Environment $M365Environment ` -QueryParams @{ '$select' = "id,appId,displayName,signInAudience,keyCredentials,passwordCredentials,federatedIdentityCredentials,appOwnerOrganizationId" } ).Value $ServicePrincipalById = @{} foreach ($ServicePrincipal in @($ServicePrincipals)) { $ServicePrincipalById[[string]$ServicePrincipal.Id] = $ServicePrincipal } $BatchRequests = @( foreach ($ServicePrincipalId in @($ServicePrincipals.Id)) { @{ id = [string]$ServicePrincipalId method = "GET" url = "/servicePrincipals/$ServicePrincipalId/appRoleAssignments?`$select=appRoleId,resourceDisplayName" } } ) $BatchResponses = Invoke-GraphBatchRequest -Requests $BatchRequests -M365Environment $M365Environment -ApiVersion "beta" ` -RetriableStatusCodes @(429, 500, 502, 503, 504) -UseExponentialBackoffFallback -MaxRetries 6 -FallbackBaseDelaySeconds 1 foreach ($ServicePrincipalId in @($ServicePrincipals.Id)) { $Result = $BatchResponses[[string]$ServicePrincipalId] $ServicePrincipal = $ServicePrincipalById[[string]$ServicePrincipalId] if ($null -eq $ServicePrincipal) { continue } $MappedPermissions = [System.Collections.Generic.List[object]]::new() if ($null -ne $Result -and [int]$Result.status -eq 200 -and $null -ne $Result.body) { $AppRoleAssignments = @($Result.body.value) foreach ($Role in $AppRoleAssignments) { $ResourceDisplayName = [string]$Role.ResourceDisplayName $RoleId = [string]$Role.AppRoleId if (-not $ResourceLookup.NameToAppId.ContainsKey($ResourceDisplayName)) { continue } # Default to true, # `Get-MgBetaServicePrincipalAppRoleAssignment` only returns admin consented permissions $IsAdminConsented = $true $ResourceAppId = $ResourceLookup.NameToAppId[$ResourceDisplayName] $ResourceAppPermissions = Get-ResourcePermissions ` -M365Environment $M365Environment ` -ResourcePermissionCache $ResourcePermissionCache ` -ResourceAppId $ResourceAppId if ($null -eq $ResourceAppPermissions) { Write-Warning "No permissions found for resource app ID: $ResourceAppId" continue } $PermissionTypeDetails = Get-PermissionTypeDetails ` -ResourceAppPermissions $ResourceAppPermissions ` -ResourcePermissionTypeCache $ResourcePermissionTypeCache ` -ResourceAppId $ResourceAppId ` -RoleId $RoleId [void]$MappedPermissions.AddRange(@( Format-Permission ` -Json $RiskyAppPermissionsJson ` -AppDisplayName $ResourceDisplayName ` -Id $RoleId ` -RoleType $PermissionTypeDetails.ReadableRoleType ` -RoleDisplayName $PermissionTypeDetails.RoleDisplayName ` -IsAdminConsented $IsAdminConsented ` -RequiresAdminConsent $PermissionTypeDetails.RequiresAdminConsent )) } } elseif ($null -ne $Result) { Write-Warning "Error for service principal ${ServicePrincipalId}: $($Result.status)" } $RiskyPermissions = @($MappedPermissions | Where-Object { $_.IsRisky -eq $true }) # Exclude service principals without risky permissions if ($RiskyPermissions.Count -gt 0) { [void]$ServicePrincipalResults.Add([PSCustomObject]@{ ObjectId = $ServicePrincipal.Id AppId = $ServicePrincipal.AppId DisplayName = $ServicePrincipal.DisplayName SignInAudience = $ServicePrincipal.SignInAudience # Credentials from application and service principal objects may get merged in other cmdlets. # Differentiate between the two by setting IsFromApplication=$false KeyCredentials = Format-Credentials -AccessKeys $ServicePrincipal.KeyCredentials -IsFromApplication $false PasswordCredentials = Format-Credentials -AccessKeys $ServicePrincipal.PasswordCredentials -IsFromApplication $false FederatedCredentials = Format-Credentials -AccessKeys $ServicePrincipal.FederatedIdentityCredentials -IsFromApplication $false -IsFederated Permissions = $MappedPermissions.ToArray() AppOwnerOrganizationId = $ServicePrincipal.AppOwnerOrganizationId }) } } } catch { Write-Warning "An error occurred in Get-ServicePrincipalsWithRiskyPermissions: $($_.Exception.Message)" Write-Warning "Stack trace: $($_.ScriptStackTrace)" throw $_ } return $ServicePrincipalResults.ToArray() } } function Get-ServicePrincipalsWithRiskyDelegatedPermissionClassifications { <# .Description Returns an array of service principals where each item contains its Object ID, App ID, Display Name, Key/Password Credentials, and risky API permissions. .Functionality #Internal ##> param ( [ValidateNotNullOrEmpty()] [string] $M365Environment, [PSCustomObject] $RiskyAppPermissionsJson ) process { try { if ($null -eq $RiskyAppPermissionsJson) { $RiskyAppPermissionsJson = Get-RiskyAppPermissionsJson } $Resources = $RiskyAppPermissionsJson.resources.PSObject.Properties $ResourceIds = @($Resources | ForEach-Object { [string]$_.Name }) # Resolve all risky resource service principals in one filtered query instead of per-resource lookups. $ServicePrincipalByAppId = @{} if ($ResourceIds.Count -gt 0) { $FilterValues = @($ResourceIds | ForEach-Object { "appId eq '$_'" }) $FilterClause = $FilterValues -join " or " $ResolvedServicePrincipals = ( Invoke-GraphDirectly ` -Commandlet "Get-MgServicePrincipal" ` -M365Environment $M365Environment ` -QueryParams @{ '$filter' = $FilterClause '$select' = "id,appId,displayName" } ).Value foreach ($ResolvedServicePrincipal in @($ResolvedServicePrincipals)) { $ServicePrincipalByAppId[[string]$ResolvedServicePrincipal.appId] = $ResolvedServicePrincipal } } $ResourceByServicePrincipalId = @{} foreach ($Resource in $Resources) { $ResourceId = $Resource.Name $ResourceName = $Resource.Value $ServicePrincipal = $ServicePrincipalByAppId[$ResourceId] if ($null -eq $ServicePrincipal -or $null -eq $ServicePrincipal.id) { continue } $ResourceByServicePrincipalId[[string]$ServicePrincipal.id] = [PSCustomObject]@{ ResourceId = $ResourceId ResourceName = $ResourceName ServicePrincipal = $ServicePrincipal } } $BatchRequests = @( foreach ($ServicePrincipalId in @($ResourceByServicePrincipalId.Keys)) { @{ id = [string]$ServicePrincipalId method = "GET" url = "/servicePrincipals/$ServicePrincipalId/delegatedPermissionClassifications?`$select=id,permissionId,permissionName,classification" } } ) $BatchResponses = Invoke-GraphBatchRequest -Requests $BatchRequests -M365Environment $M365Environment -ApiVersion "beta" ` -RetriableStatusCodes @(429, 500, 502, 503, 504) -UseExponentialBackoffFallback -MaxRetries 6 -FallbackBaseDelaySeconds 1 $RiskyDelegatedPermissionClassificationResults = @() foreach ($ServicePrincipalId in @($ResourceByServicePrincipalId.Keys)) { $ResourceContext = $ResourceByServicePrincipalId[$ServicePrincipalId] $ResourceId = $ResourceContext.ResourceId $ResourceName = $ResourceContext.ResourceName $ServicePrincipal = $ResourceContext.ServicePrincipal $RiskyDelegatedPermissions = $RiskyAppPermissionsJson.permissions.$ResourceName.Delegated.PSObject.Properties $Result = $BatchResponses[[string]$ServicePrincipalId] if ($null -eq $Result -or [int]$Result.status -ne 200 -or $null -eq $Result.body) { if ($null -ne $Result) { Write-Warning "Error for service principal ${ServicePrincipalId}: $($Result.status)" } continue } $PermClassifications = @($Result.body.value) $RiskyPermClassifications = @() foreach ($PermClassification in $PermClassifications) { if ($PermClassification.Classification -eq "low" -and $RiskyDelegatedPermissions.Name -contains $PermClassification.PermissionId) { $RiskyPermClassifications += [PSCustomObject]@{ id = $PermClassification.id permissionId = $PermClassification.permissionId permissionName = $PermClassification.permissionName classification = $PermClassification.classification } } } if ($RiskyPermClassifications.Count -gt 0) { $RiskyDelegatedPermissionClassificationResults += [PSCustomObject]@{ ObjectId = $ServicePrincipalId AppId = $ResourceId DisplayName = $ServicePrincipal.DisplayName RiskyPermClassifications = $RiskyPermClassifications.permissionName } } } return $RiskyDelegatedPermissionClassificationResults } catch { Write-Warning "An error occurred in Get-ServicePrincipalsWithRiskyDelegatedPermissionClassifications: $($_.Exception.Message)" Write-Warning "Stack trace: $($_.ScriptStackTrace)" throw $_ } } } function Format-RiskyApplications { <# .Description Returns an aggregated JSON dataset of application objects, combining data from both applications and service principal objects. Key/Password/Federated credentials are combined into a single array, and admin consent is reflected in each object's list of associated risky permissions. .Functionality #Internal ##> param ( [ValidateNotNullOrEmpty()] [Object[]] $RiskyApps, [ValidateNotNullOrEmpty()] [Object[]] $RiskySPs ) process { try { $Applications = @() foreach ($App in $RiskyApps) { $MatchedServicePrincipal = $RiskySPs | Where-Object { $_.AppId -eq $App.AppId } # Merge objects if an application and service principal exist with the same AppId $MergedObject = @{} if ($MatchedServicePrincipal) { $ServicePrincipalRoleIds = @($MatchedServicePrincipal.Permissions | Select-Object -ExpandProperty RoleId) # Determine if each risky permission was admin consented or not foreach ($Permission in $App.Permissions) { if ($ServicePrincipalRoleIds -contains $Permission.RoleId) { $Permission.IsAdminConsented = $true } } $ObjectIds = [PSCustomObject]@{ Application = $App.ObjectId ServicePrincipal = $MatchedServicePrincipal.ObjectId } $MergedKeyCredentials = Merge-Credentials ` -ApplicationAccessKeys $App.KeyCredentials ` -ServicePrincipalAccessKeys $MatchedServicePrincipal.KeyCredentials $MergedPasswordCredentials = Merge-Credentials ` -ApplicationAccessKeys $App.PasswordCredentials ` -ServicePrincipalAccessKeys $MatchedServicePrincipal.PasswordCredentials $MergedFederatedCredentials = Merge-Credentials ` -ApplicationAccessKeys $App.FederatedCredentials ` -ServicePrincipalAccessKeys $MatchedServicePrincipal.FederatedCredentials $MergedObject = [PSCustomObject]@{ ObjectId = $ObjectIds AppId = $App.AppId DisplayName = $App.DisplayName IsMultiTenantEnabled = $App.IsMultiTenantEnabled KeyCredentials = $MergedKeyCredentials PasswordCredentials = $MergedPasswordCredentials FederatedCredentials = $MergedFederatedCredentials Permissions = $App.Permissions } } else { $MergedObject = $App } # Calculate severity score after admin consent for permissions has been determined $SeverityInfo = Set-SeverityScore -Object $MergedObject # Add severity info to the merged object $MergedObject | Add-Member -MemberType NoteProperty -Name "SeverityScore" -Value $SeverityInfo.SeverityScore $MergedObject | Add-Member -MemberType NoteProperty -Name "ScoreBreakdown" -Value $SeverityInfo.ScoreBreakdown $Applications += $MergedObject } } catch { Write-Warning "An error occurred in Format-RiskyApplications: $($_.Exception.Message)" Write-Warning "Stack trace: $($_.ScriptStackTrace)" throw $_ } return $Applications } } function Format-RiskyThirdPartyServicePrincipals { <# .Description Returns a JSON dataset of service principal objects owned by external organizations. .Functionality #Internal ##> param ( [ValidateNotNullOrEmpty()] [Object[]] $RiskySPs, [ValidateNotNullOrEmpty()] [string] $M365Environment, # Raw hashtable containing privileged service principals which is keyed by ServicePrincipalId (object id) [hashtable] $PrivilegedServicePrincipals = @{} ) process { try { $ServicePrincipals = @() $OrgInfo = (Invoke-GraphDirectly -Commandlet "Get-MgBetaOrganization" -M365Environment $M365Environment).Value foreach ($ServicePrincipal in $RiskySPs) { if ($null -eq $ServicePrincipal) { continue } # A null value indicates the owner organization is unknown (e.g., agent service principal) # and should not be treated as a third-party service principal. if ($null -eq $ServicePrincipal.AppOwnerOrganizationId) { Write-Warning "Service principal $($ServicePrincipal.DisplayName) with AppId $($ServicePrincipal.AppId) does not have an AppOwnerOrganizationId. Skipping." continue } # If the service principal's owner id is not the same as this tenant then it is a 3rd party principal if ($ServicePrincipal.AppOwnerOrganizationId -ne $OrgInfo.Id) { $PrivilegedRoles = @() if ($PrivilegedServicePrincipals.ContainsKey($ServicePrincipal.ObjectId)) { $PrivilegedRoles = $PrivilegedServicePrincipals[$ServicePrincipal.ObjectId].roles } # Calculate severity score after admin consent for permissions has been determined $SeverityInfo = Set-SeverityScore ` -Object $ServicePrincipal ` -IsThirdPartyServicePrincipal ` -PrivilegedRoles $PrivilegedRoles # Add severity info to the merged object $ServicePrincipal | Add-Member -MemberType NoteProperty -Name "SeverityScore" -Value $SeverityInfo.SeverityScore $ServicePrincipal | Add-Member -MemberType NoteProperty -Name "ScoreBreakdown" -Value $SeverityInfo.ScoreBreakdown $ServicePrincipal | Add-Member -MemberType NoteProperty -Name "PrivilegedRoles" -Value $PrivilegedRoles $ServicePrincipals += $ServicePrincipal } } } catch { Write-Warning "An error occurred in Format-RiskyThirdPartyServicePrincipals: $($_.Exception.Message)" Write-Warning "Stack trace: $($_.ScriptStackTrace)" throw $_ } return $ServicePrincipals } } function Get-SeverityScoreWeights { <# .Description Returns the weight factors used in severity score calculation. The priority score is determined by the sum of all weight factors. A higher value indicates higher risk. Weight Factor Notes ------------------------------------------------------------------------------------------------------------------------------------------- Permission risk level weights | Each risky permission adds its RiskLevel weight: critical = 50, high = 15, medium = 5, low = 2 ------------------------------------------------------------------------------------------------------------------------------------------- Permission volume | +1 per 10 total permissions (both risky and non-risky) ------------------------------------------------------------------------------------------------------------------------------------------- Multi-tenant | +10 for applications with multi-tenant enabled ------------------------------------------------------------------------------------------------------------------------------------------- Third-party service principal | +20 for externally-owned service principals ------------------------------------------------------------------------------------------------------------------------------------------- Privileged roles | +8 per privileged role assigned to a service principal ------------------------------------------------------------------------------------------------------------------------------------------- Credential context weights | Base points are added to a credential based on the highest level permission assigned to the application. | +50/cred for critical, +35/cred for high, +15/cred for medium, +5/cred for low ------------------------------------------------------------------------------------------------------------------------------------------- Credential type discounts | Key credentials are discounted by 50% and federated credentials are discounted by 75% ------------------------------------------------------------------------------------------------------------------------------------------- Credential lifetime tiers | Bonus points for credentials with long lifetimes (excludes federated credentials): | +5 points for password creds valid for 2+ years, +3 points for 1-2 years, +2 points for 6 months - 1 year | +5 points for key creds valid for 3+ years, +3 points for 2-3 years, +2 points for 1-2 years ------------------------------------------------------------------------------------------------------------------------------------------- .Functionality #Internal #> return [PSCustomObject]@{ PermissionRiskLevelWeights = @{ Critical = 50 High = 15 Medium = 5 Low = 2 Description = "Risk level weights are assigned based on the level of access granted by each permission." } PermissionVolume = @{ PointsPer10Permissions = 1 Description = "Over-permissioned applications/service principals represent an increased attack surface regardless of individual permission risk level." } MultiTenant = @{ Points = 10 Description = "Multi-tenant applications can be used across multiple organizations, increasing their attack surface." } ThirdPartyServicePrincipal = @{ Points = 20 Description = "Third-party service principals are owned by external organizations and do not fall under the same security policies as internal service principals." } PrivilegedRoles = @{ PointsPerRole = 8 Description = "Service principals with privileged roles (e.g., Global Administrator) have elevated permissions and pose a higher risk." } CredentialContextWeights = @{ Critical = 50 High = 35 Medium = 15 Low = 5 Description = "Credential base points dynamically scale by the highest risk level permission on the app/SP." } # Discount applied to credential base points. # Key and federated credentials are discounted since key (certificate) credentials are more difficult to steal, and federated credentials contain no shared secret. CredentialTypeDiscounts = @{ Password = 1.0 Key = 0.5 Federated = 0.25 Description = "Multiplier applied to credential and base points by credential type. Passwords hold the highest risk, then certificates, and federated creds with the least." } PasswordCredentialLifetimeTiers = @( @{ MinDays = 730; Points = 5 } # 2+ years @{ MinDays = 365; Points = 3 } # 1 - 2 years @{ MinDays = 180; Points = 2 } # 6 months - 1 year # <= 180 days is valid, no bonus points ) KeyCredentialLifetimeTiers = @( @{ MinDays = 1095; Points = 5 } # 3+ years @{ MinDays = 730; Points = 3 } # 2 - 3 years @{ MinDays = 365; Points = 2 } # 1 - 2 years # <= 365 days is valid, no bonus points ) CredentialVolume = @{ PointsPerCredentialAfterFirst = 5 Description = "Multiple active credentials increase the authentication attack surface. Each active credential beyond the first adds bonus points." } # Used in Entra ID HTML report to generate risk indicators. CredentialRiskIndicatorTiers = @{ Critical = 0.75 High = 0.50 Medium = 0.25 # Below 0.25 is considered low risk } } } function ConvertFrom-DotNetDate { param( [string] $DateString ) if ([string]::IsNullOrEmpty($DateString)) { return $null } # Dates are returned from Graph as .NET JSON dates: /Date(1675800895000)/ if ($DateString -match '\\?/Date\((\d+)\)\\?/') { $EpochMs = $Matches[1] return [System.DateTimeOffset]::FromUnixTimeMilliseconds($EpochMs).UtcDateTime } return [Datetime]::Parse($DateString) } function Set-CredentialScore { <# .Description Calculates the severity score for credentials; handles password, key, and federated credentials. .Functionality #Internal #> param ( [Object[]] $AccessKeys, # Base points per credential are derived by: # - multiplying the credential context weight (determined by the app/SP's highest risk level permission) # - credential type discount (password credentials have no discount, key credentials have a 50% discount, # and federated credentials have a 75% discount) [ValidateNotNullOrEmpty()] [int] $BasePointsPerCredential, # Bonus poinst are added to a credential's score if the credential's duration exceeds a certain time-bound threshold. [array] $LifetimeTiers, # Only check lifetime for password/key credentials, not required for federated credentials. [switch] $CheckLifetime ) $CredentialPoints = 0 $CredentialCount = 0 $LongLivedCredentialCount = 0 if ($null -eq $AccessKeys -or @($AccessKeys).Count -eq 0) { return @{ CredentialCount = $CredentialCount LongLivedCredentialCount = $LongLivedCredentialCount TotalPoints = $CredentialPoints } } foreach ($Credential in $AccessKeys) { $CurrentCredentialPoints = 0 # Skip expired credentials since they can't be used for authentication if ($CheckLifetime -and $null -ne $Credential.EndDateTime) { $End = ConvertFrom-DotNetDate -DateString $Credential.EndDateTime if ($null -ne $End -and $End -lt (Get-Date)) { continue } } $CredentialCount++ # Base points are determined by the app/SP's highest permission risk level $CurrentCredentialPoints += $BasePointsPerCredential # Add additional points for long-lived credentials (excludes federated) if ($CheckLifetime -and $null -ne $Credential.StartDateTime -and $null -ne $Credential.EndDateTime) { $Start = ConvertFrom-DotNetDate -DateString $Credential.StartDateTime $End = ConvertFrom-DotNetDate -DateString $Credential.EndDateTime $Duration = (New-TimeSpan -Start $Start -End $End).Days if ($LifetimeTiers) { foreach ($Tier in $LifetimeTiers) { if ($Duration -gt $Tier.MinDays) { $CurrentCredentialPoints += $Tier.Points $LongLivedCredentialCount++ break } } } } $CredentialPoints += $CurrentCredentialPoints } return @{ CredentialCount = $CredentialCount LongLivedCredentialCount = $LongLivedCredentialCount TotalPoints = $CredentialPoints } } function Set-SeverityScore { <# .Description Calculates a severity score for each risky application/service principal based on multiple risk factors: - Number of admin consented risky permissions - Number of non-admin consented risky permissions - Multi-tenant enabled/disabled - Third-party service principal (owned externally) - Privileged roles assigned to risky service principals - Existence of password/key/federated credentials (considers Long-lived credentials) The total severity score is normalized to 100 to factor in different weight distributions for each of the above risk factors. .Functionality #Internal #> param ( [ValidateNotNullOrEmpty()] [Object[]] $Object, [switch] $IsThirdPartyServicePrincipal, [string[]] $PrivilegedRoles = @() ) try { $Weights = Get-SeverityScoreWeights $Score = 0 $ScoreBreakdown = @{} # 1. Determine admin consented risky permission weight factor $AdminConsentedRiskyPermissions = @($Object.Permissions | Where-Object { $_.IsRisky -eq $true -and $_.IsAdminConsented -eq $true }) $AdminConsentedPoints = ($AdminConsentedRiskyPermissions | ForEach-Object { $Weights.PermissionRiskLevelWeights[$_.RiskLevel] } | Measure-Object -Sum).Sum $Score += $AdminConsentedPoints $ScoreBreakdown.AdminConsentedRiskyPermissions = [PSCustomObject]@{ PermissionCount = $AdminConsentedRiskyPermissions.Count TotalPoints = $AdminConsentedPoints } # 2. Determine non-admin consented risky permission weight factor $NonAdminConsentedRiskyPermissions = @($Object.Permissions | Where-Object { $_.IsRisky -eq $true -and $_.IsAdminConsented -eq $false }) $NonAdminConsentedPoints = ($NonAdminConsentedRiskyPermissions | ForEach-Object { $Weights.PermissionRiskLevelWeights[$_.RiskLevel] } | Measure-Object -Sum).Sum $Score += $NonAdminConsentedPoints $ScoreBreakdown.NonAdminConsentedRiskyPermissions = [PSCustomObject]@{ PermissionCount = $NonAdminConsentedRiskyPermissions.Count TotalPoints = $NonAdminConsentedPoints } # 3. Determine privileged roles weight factor (used only for service principals) $PrivilegedRolesPoints = 0 if ($PrivilegedRoles.Count -gt 0) { $PrivilegedRolesPoints = $PrivilegedRoles.Count * $Weights.PrivilegedRoles.PointsPerRole $Score += $PrivilegedRolesPoints $ScoreBreakdown.PrivilegedRoles = [PSCustomObject]@{ RoleCount = $PrivilegedRoles.Count TotalPoints = $PrivilegedRolesPoints Roles = $PrivilegedRoles } } # 4. Determine multi-tenant weight factor (used only for applications) $MultiTenantPoints = 0 if ($Object.IsMultiTenantEnabled -eq $true) { $MultiTenantPoints = $Weights.MultiTenant.Points $Score += $MultiTenantPoints $ScoreBreakdown.MultiTenant = [PSCustomObject]@{ IsMultiTenantEnabled = $Object.IsMultiTenantEnabled TotalPoints = $MultiTenantPoints } } # 5. Determine third-party service principal weight factor (used only for service principals) $ThirdPartyServicePrincipalPoints = 0 if ($IsThirdPartyServicePrincipal -eq $true) { $ThirdPartyServicePrincipalPoints = $Weights.ThirdPartyServicePrincipal.Points $Score += $ThirdPartyServicePrincipalPoints $ScoreBreakdown.ThirdPartyServicePrincipal = [PSCustomObject]@{ IsThirdPartyServicePrincipal = $true TotalPoints = $ThirdPartyServicePrincipalPoints } } # 6. Determine the credential base points by the highest risk level permission on the app/SP $AllRiskyPermissions = @($Object.Permissions | Where-Object { $_.IsRisky -eq $true }) $RiskLevelPriority = @{ Critical = 4; High = 3; Medium = 2; Low = 1 } $HighestRiskLevel = "None" $HighestPriority = 0 foreach ($Permission in $AllRiskyPermissions) { $Priority = $RiskLevelPriority[$Permission.RiskLevel] if ($null -ne $Priority -and $Priority -gt $HighestPriority) { $HighestPriority = $Priority $HighestRiskLevel = $Permission.RiskLevel } } $CredentialBasePoints = if ($HighestRiskLevel -ne "None") { $Weights.CredentialContextWeights[$HighestRiskLevel] } else { 0 } $ScoreBreakdown.HighestRiskLevel = $HighestRiskLevel # 7. Calculate password credential weight factor $PasswordBasePoints = [Math]::Ceiling($CredentialBasePoints * $Weights.CredentialTypeDiscounts.Password) $AllPasswordCredentials = @($Object.PasswordCredentials | Where-Object { $null -ne $_ }) $PasswordScore = Set-CredentialScore ` -AccessKeys $AllPasswordCredentials ` -BasePointsPerCredential $PasswordBasePoints ` -LifetimeTiers $Weights.PasswordCredentialLifetimeTiers ` -CheckLifetime $Score += $PasswordScore.TotalPoints $ScoreBreakdown.PasswordCredentials = [PSCustomObject]@{ CredentialCount = $PasswordScore.CredentialCount LongLivedCredentialCount = $PasswordScore.LongLivedCredentialCount TotalPoints = $PasswordScore.TotalPoints } # 8. Calculate key credential weight factor $KeyBasePoints = [Math]::Ceiling($CredentialBasePoints * $Weights.CredentialTypeDiscounts.Key) $AllKeyCredentials = @($Object.KeyCredentials | Where-Object { $null -ne $_}) $KeyScore = Set-CredentialScore ` -AccessKeys $AllKeyCredentials ` -BasePointsPerCredential $KeyBasePoints ` -LifetimeTiers $Weights.KeyCredentialLifetimeTiers ` -CheckLifetime $Score += $KeyScore.TotalPoints $ScoreBreakdown.KeyCredentials = [PSCustomObject]@{ CredentialCount = $KeyScore.CredentialCount LongLivedCredentialCount = $KeyScore.LongLivedCredentialCount TotalPoints = $KeyScore.TotalPoints } # 9. Calculate federated credential weight factor $FederatedBasePoints = [Math]::Ceiling($CredentialBasePoints * $Weights.CredentialTypeDiscounts.Federated) $AllFederatedCredentials = @($Object.FederatedCredentials | Where-Object { $null -ne $_}) $FederatedScore = Set-CredentialScore ` -AccessKeys $AllFederatedCredentials ` -BasePointsPerCredential $FederatedBasePoints ` $Score += $FederatedScore.TotalPoints $ScoreBreakdown.FederatedCredentials = [PSCustomObject]@{ CredentialCount = $FederatedScore.CredentialCount TotalPoints = $FederatedScore.TotalPoints } # 10. Credential volume factor $TotalActiveCredentials = $PasswordScore.CredentialCount + $KeyScore.CredentialCount + $FederatedScore.CredentialCount $CredentialVolumePoints = 0 if ($TotalActiveCredentials -gt 1) { # Subtract 1 because we're already taking into account the first credential. $CredentialVolumePoints = ($TotalActiveCredentials - 1) * $Weights.CredentialVolume.PointsPerCredentialAfterFirst $Score += $CredentialVolumePoints } $ScoreBreakdown.CredentialVolume = [PSCustomObject]@{ TotalActiveCredentials = $TotalActiveCredentials TotalPoints = $CredentialVolumePoints } # 11. Permission volume factor $TotalPermissionCount = @($Object.Permissions).Count # Use Math.floor() so the integer division truncates rounds down. # For example: # - 5 permissions / 10 = 0.5 x 1 (0 points) # - 15 permissions / 10 = 1.5 x 1 (1 point) $PermissionVolumePoints = [Math]::Floor($TotalPermissionCount / 10) * $Weights.PermissionVolume.PointsPer10Permissions $Score += $PermissionVolumePoints $ScoreBreakdown.PermissionVolume = [PSCustomObject]@{ TotalPermissions = $TotalPermissionCount TotalPoints = $PermissionVolumePoints } return [PSCustomObject]@{ SeverityScore = $Score ScoreBreakdown = $ScoreBreakdown } } catch { Write-Warning "An error occurred in Set-SeverityScore: $($_.Exception.Message)" Write-Warning "Stack trace: $($_.ScriptStackTrace)" throw $_ } } # Keep Get-RiskyAppPermissionsJson exported for cross-module usage in AADHybridExchangeHelper. Export-ModuleMember -Function @( "Get-RiskyAppPermissionsJson", "Format-Credentials", "Get-ApplicationsWithRiskyPermissions", "Get-ServicePrincipalsWithRiskyPermissions", "Format-RiskyApplications", "Format-RiskyThirdPartyServicePrincipals", "Get-SeverityScoreWeights", "Get-ServicePrincipalsWithRiskyDelegatedPermissionClassifications" ) # SIG # Begin signature block # MIIu9wYJKoZIhvcNAQcCoIIu6DCCLuQCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB2f1pF1xFyfkLu # hXbAq0F0XpwFHY7EFNHv+hJ+meacD6CCE6MwggWQMIIDeKADAgECAhAFmxtXno4h # 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 # eE4wggdXMIIFP6ADAgECAhAMM6tnPejLgA9WVhXroQvSMA0GCSqGSIb3DQEBCwUA # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwHhcNMjYwMTE0MDAwMDAwWhcNMjcwMTEzMjM1OTU5WjBfMQsw # CQYDVQQGEwJVUzEdMBsGA1UECBMURGlzdHJpY3Qgb2YgQ29sdW1iaWExEzARBgNV # BAcTCldhc2hpbmd0b24xDTALBgNVBAoTBENJU0ExDTALBgNVBAMTBENJU0EwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCuXYolNHqlh6smLTE592waXheZ # 8VHzxeds4pMaepGuwmjf8d1jG9wUNuJX9/qb0a1dgGz5D/EAz5NRTIin4SZYQEE8 # qvdl2yQJ5uWxXIjsFbrOyc1fWscUXw0Kt7OPLOafcEkdDoe8K0tO4h2GL3RWRzjp # uLfQhhnAmD6NT1l+ughnfmarV/ODgIn/RFR4YORlu4YP2xQX6KRxeTDslg7F+z6X # +t87/U8m8gQ9XTm5kBmteP4GcE/ytnyI+ScIxNRybzGomWIBm848XDE5yYhlYQ2R # SnCoo6M4CRqp9WFGVyoLkoPP0OlxzryKWaE1/nuPbYG/kf/rUB1OhqxvSSGwmNhs # vkkjsC0Z9H5Jy6heFdoxOu/+ZQksKoP/fMvHxuCCtkIJbV8tk0oT6MQ8EJbgsWDZ # TKhui1wxW6JIZyBOMPWoZUOouOzo2h5Cz7LBPKME5FkcUzcs47lpRlDkJco4PLcj # wJSo4XPnx3G/2DIjNEFNyfKCWfH8uW6nJjmDBiveFZ2j0YvgdQ+7MOjQnw7R/MAD # DTagrKl3rLV60+X2TY6/onKhCUuU3pMAjVbOwZ3PkzDLZnsEGRfm6hgp6014aXml # t8h4nu+uC41U8vUSGHl0vqKuzvmShLmnI+Iv0l95pmnqomuCZzRDrjEoaLPx7OxL # Dy/Id9E7yQDip4jBdQIDAQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8R # hvv+YXsIiGX0TkIwHQYDVR0OBBYEFN30sVU+fpfQQfPQWMhkO1qd+MxoMD4GA1Ud # IAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNl # cnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMw # gbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j # cmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0 # ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMIGUBggrBgEF # BQcBAQSBhzCBhDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t # MFwGCCsGAQUFBzAChlBodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNl # cnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJ # BgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQA77T42YiOx5wWPItgo+kvB+Gzb # ZFCRHRFfTAZZNQt9o/0CqNmyA2xFklX4t5Z9VNdiIOx14AnmJdQkcRdk3vsU5gby # jKEup7LTWtvcWrl6hQwGNt3l892BgUbPKsPBE+AriktVqn5yMSXVVzeboqsqAG8e # Syei0B54/QdgR4whfHvQ/qpCACsJTlJgAykXVgDPJNKnQ7wc17loLQutqF1JUcbO # XKWt1AA9Zas5q30LDZzZeK4B56yojK68CTQXN7toSRFIuZDMKKDfIZpCX8cmbaaO # DFOOu44/QWMv+Xc6+ISYGkrTTzqWhOqiXgLVBeXGn/WrOJJ8R29mZMneCpBesCLs # YII1gCFOo7Vt6mvOKxAPQ3KhJYBFEHkp+GI65koaQkO2xv50iLS0+/j2YC66uviU # MFe0JEOdXuE7Rn/OmWNSzQ+6kPNYDJQASQ974C3wUejJoMtGZEzoTbly/HufQTrd # rhcL2aC69CxSN+idTXPLC9UT3xo4sFdOw+hXkmbXtoB1GDsd5p1TWFgRbnTXDkbM # YMBWYVB6/Tk1bzwj4iTp4g0YrtB628FXPX/ko+JWZlv0Ea865S23w1uGlnDNVxIi # +8oi74G5DM66Q6ENt6+3WoRGRrdoyE6uCh1haY+oPYSgumb0ozzrp8tw89TRKVrK # KSXGBxlExEvjQ6dYwjGCGqowghqmAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNV # BAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0 # IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQDDOrZz3oy4AP # VlYV66EL0jANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgACh # AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM # BgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCBGPoptD3ZuCOrdi3h59NGFYm0D # CGUCS8IoY4smUesixjANBgkqhkiG9w0BAQEFAASCAgBTlHEqHD7C/B+ImulhLfMA # WsX+LhVy9VBzP8Z1w3mqsSfh9WuBC5oTrz7lpM46HJC9p94K5AJFFjEN3VJ51Ts5 # qRx8I3E30sSEnMbbc8+JfEfe8CxvXW6jfYNdTj3Ey4nv3pvHU2frAkZmQm5QGfBB # 3VNYQeW5zlPstVGpxgA+fo1WEl5O6jzJAlPN+7XxkWYn+CyRAoAo6zf48fu54oLt # 1egUhePeSu/a2726i4y7ia5xj9mXyoyp8mnFpQV5d5+yjWyterDLwBR0WPp9/lyL # QHr9r7O8tHck41WPxDAaKbyTdAUtgMNiwlJ4h9qt0BQYaC85tU+Rgx+XFY0FfBfh # ZyLLftSScy6sS8/Fi6/3LgGygzEVp+LOyZsMecY9d7f+NULkK1K6ShOKe7paqTp4 # Sr6E0a/TtHw2tV/l9dfg4iwMgS9B7n6ITGx6A0rzJGvEn1BL6tl9TreyjqI5X3eO # Hq/4qv1ZC4XMZ2yksOp7ZI6f+xEzCW+DvitSpPpUGG4A30Jz/XQIIc3auOGeQMta # KaFWWLOP1Fa2xzXn2UurKKk9NSqkAuYf/V7Q+mQv+7QYcL309Tahd19KQ13BcXak # q/X0k5a6x3dcwIJUpd2ex3NpP8DCIJVfP0wTbz17Jj2xuVo4/C1DYUHGotk6HW3N # CXWFigrLTc7ooM74HBnNRqGCF3cwghdzBgorBgEEAYI3AwMBMYIXYzCCF18GCSqG # SIb3DQEHAqCCF1AwghdMAgEDMQ8wDQYJYIZIAWUDBAIBBQAweAYLKoZIhvcNAQkQ # AQSgaQRnMGUCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCDS21a+SQ8b # mx7u+5lZrcWpLHsgGHkH8FT8ysHrxz3ksQIRAK6wfa/pYsjxbj+KtscntnkYDzIw # MjYxMDA4MjMyOTQ4WqCCEzowggbtMIIE1aADAgECAhAIT9wzT35FTtvDD4/5khg1 # MA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy # dCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBp # bmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwHhcNMjYwODA1MDAwMDAwWhcNMzcx # MTA0MjM1OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIElu # Yy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFNIQTI1NiBSU0E0MDk2IFRpbWVzdGFtcCBS # ZXNwb25kZXIgMjAyNiAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA # tnum8sn+zUr41JtMZbP9OMYw+HwJDpG5xkIu/lqcfNYmMX81YmsUiHLbh9ykpeWB # GKTLhYBrAN9Tdg/QEzG32XcObmgIblnr0CoQ3WSAeDZ6nH6X6VkFyYkJw3QBJREw # vm4UhLzSxmwPA7cFKRTEOMsmEEj6qJk/dqLEAL+oQYuOwE2UuiX1Vnul8YReIyWd # 4kgLn9gq6LNXM0UplkR6jL/QHxmb6fMoGBJYbnaUI7XD6cKDpekK2SVMld4iDbze # HDtOaaxldH5IxuNusQ69nd8/ZXEiB5Hbxj3RlK13cX1W4DlFXKdv/CEhM8Cj1vvl # mvhNroyPdRGbbpBlgyf8Wdu5N6ByhFwURn0U6ozlPoxN22v+fviUhP+6DR547OZn # pBMWDfei1f5sVGwiiW/KQTWOK97g+4RJpPzPNV4VYMAwO2jM2Aty2QYPVmOQTJm0 # msuXnJrSbl2gf9JylpkJlWXqk1Q4LJsxz+TELoQCZIljbgvTJgoPU2R12ydv8i1U # qL/adelA0y7U9Pmmtbze9Xx3rtajC5SzQd1jgfwAwsa90v9YcSPdmeoyoBBA/27c # CL237l5DTYYPDLQ4ON3OLTGWnvRb6jDrf/T75gMRfUzSLCBQfBusm9+mSWRlC/Df # 6S/e9Q8i13CuhzOT2Jx+V/nlbXM4QoBwlUAhelwwJT0CAwEAAaOCAZUwggGRMAwG # A1UdEwEB/wQCMAAwHQYDVR0OBBYEFBTJY4owLtRK+26U8+bjQH717M3iMB8GA1Ud # IwQYMBaAFO9vU0rp5AZ8esrikFb2L9RJ7MtOMA4GA1UdDwEB/wQEAwIHgDAWBgNV # HSUBAf8EDDAKBggrBgEFBQcDCDCBlQYIKwYBBQUHAQEEgYgwgYUwJAYIKwYBBQUH # MAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBdBggrBgEFBQcwAoZRaHR0cDov # L2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0YW1w # aW5nUlNBNDA5NlNIQTI1NjIwMjVDQTEuY3J0MF8GA1UdHwRYMFYwVKBSoFCGTmh0 # dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFt # cGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNybDAgBgNVHSAEGTAXMAgGBmeBDAEE # AjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIBAI3FOmEenVIK35msCYB+ # fShAsWvSYvLBItoNdAgQ2jIqrGsVsluXMJU/+mRebBc52s6lbKAvOVPXaizmKkML # LflEEKDZQx4CkS2t8aHPjkXha3hYZ010htFa3dhNgmalH5vuWvh3tTCf4frTS7gP # tGc4Z/xaPhQ2AB1mR8eEe/WbH0RWHvVIl6VwQ3+g5FKNfN2N/DWJkf13w2H+2Gfq # Efbd35Ww8CvoYBjLNIDTadcPWdgsjsiOaK/7EsKJgLjUNIVgvcaFOLLQ/GlrA+0Z # HJoFUbOr5SJN8zykPspXIXlpDJY/gqFUZRROeab9GVgmhbdOJcD/63RhxPahFUGb # ckRONqMe6DYAv6/mOG0pWd3cPStsdcS7buj5DyniwRY8yooMH6ptx5vpP/pZzBPB # eZD2U4IsthyxB5Jaa8qrOkB5z160TXiM5ADMspZ0TfD9MJoq0tFpFPssKRFhWeED # YPvcUuN7U7lvcdHl4ezQ3NT/7Ffs1sR1yh/LRbdZ3B3Vc6q2WmD8mDC0p9kzl2o7 # 3iVtS946IkEj7FkRsZGww1teYxERROC745xrtjvcw9ZyyUjHZWGRIpJeMNsPquCD # f0fkyHtB+J4AiNZqCQk23rxh+KbpyMTNVKItJ5l92Svl20U9NbqMBOVYl1h54NEY # LJq1/xHWFKPNK903zJZA9P2DMIIGtDCCBJygAwIBAgIQDcesVwX/IZkuQEMiDDpJ # hjANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNl # cnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdp # Q2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjUwNTA3MDAwMDAwWhcNMzgwMTE0MjM1 # OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/ # BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg # U0hBMjU2IDIwMjUgQ0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA # tHgx0wqYQXK+PEbAHKx126NGaHS0URedTa2NDZS1mZaDLFTtQ2oRjzUXMmxCqvkb # sDpz4aH+qbxeLho8I6jY3xL1IusLopuW2qftJYJaDNs1+JH7Z+QdSKWM06qchUP+ # AbdJgMQB3h2DZ0Mal5kYp77jYMVQXSZH++0trj6Ao+xh/AS7sQRuQL37QXbDhAkt # VJMQbzIBHYJBYgzWIjk8eDrYhXDEpKk7RdoX0M980EpLtlrNyHw0Xm+nt5pnYJU3 # Gmq6bNMI1I7Gb5IBZK4ivbVCiZv7PNBYqHEpNVWC2ZQ8BbfnFRQVESYOszFI2Wv8 # 2wnJRfN20VRS3hpLgIR4hjzL0hpoYGk81coWJ+KdPvMvaB0WkE/2qHxJ0ucS638Z # xqU14lDnki7CcoKCz6eum5A19WZQHkqUJfdkDjHkccpL6uoG8pbF0LJAQQZxst7V # vwDDjAmSFTUms+wV/FbWBqi7fTJnjq3hj0XbQcd8hjj/q8d6ylgxCZSKi17yVp2N # L+cnT6Toy+rN+nM8M7LnLqCrO2JP3oW//1sfuZDKiDEb1AQ8es9Xr/u6bDTnYCTK # IsDq1BtmXUqEG1NqzJKS4kOmxkYp2WyODi7vQTCBZtVFJfVZ3j7OgWmnhFr4yUoz # ZtqgPrHRVHhGNKlYzyjlroPxul+bgIspzOwbtmsgY1MCAwEAAaOCAV0wggFZMBIG # A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO9vU0rp5AZ8esrikFb2L9RJ7MtO # MB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIB # hjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUH # MAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDov # L2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQw # QwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lD # ZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZI # AYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQAXzvsWgBz+Bz0RdnEwvb4LyLU0pn/N # 0IfFiBowf0/Dm1wGc/Do7oVMY2mhXZXjDNJQa8j00DNqhCT3t+s8G0iP5kvN2n7J # d2E4/iEIUBO41P5F448rSYJ59Ib61eoalhnd6ywFLerycvZTAz40y8S4F3/a+Z1j # EMK/DMm/axFSgoR8n6c3nuZB9BfBwAQYK9FHaoq2e26MHvVY9gCDA/JYsq7pGdog # P8HRtrYfctSLANEBfHU16r3J05qX3kId+ZOczgj5kjatVB+NdADVZKON/gnZruMv # NYY2o1f4MXRJDMdTSlOLh0HCn2cQLwQCqjFbqrXuvTPSegOOzr4EWj7PtspIHBld # NE2K9i697cvaiIo2p61Ed2p8xMJb82Yosn0z4y25xUbI7GIN/TpVfHIqQ6Ku/qjT # Y6hc3hsXMrS+U0yy+GWqAXam4ToWd2UQ1KYT70kZjE4YtL8Pbzg0c1ugMZyZZd/B # dHLiRu7hAWE6bTEm4XYRkA6Tl4KSFLFk43esaUeqGkH/wyW4N7OigizwJWeukcyI # PbAvjSabnf7+Pu0VrFgoiovRDiyx3zEdmcif/sYQsfch28bZeUz2rtY/9TCA6TD8 # dC3JE3rYkrhLULy7Dc90G6e8BlqmyIjlgp2+VqsS9/wQD7yFylIz0scmbKvFoW2j # NrbM1pD2T7m3XDCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI # hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ # MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz # dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow # YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290 # IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww # IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5 # 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH # hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6 # Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ # ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b # A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9 # WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU # tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo # ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J # vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP # orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB # Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr # oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt # MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF # BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw # BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH # vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8 # UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn # f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU # jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j # LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDGCA3ww # ggN4AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu # MUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0 # MDk2IFNIQTI1NiAyMDI1IENBMQIQCE/cM09+RU7bww+P+ZIYNTANBglghkgBZQME # AgEFAKCB0TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkF # MQ8XDTI2MTAwODIzMjk0OFowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQUUdmr2gNJ # c9hPQmaspIJI5rNpxDkwLwYJKoZIhvcNAQkEMSIEICC4sagP6cV+vW5jj+W7Yp1Q # 36//lY1N7ZVpB5fHxXkHMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIC2gnaf0Ex+f # 5y22xebpyWVnVa8EPx6nQswNISDhQev8MA0GCSqGSIb3DQEBAQUABIICAC8tinRV # ZObp70yge5RKMWFy0HpdU6SNHBFms7Q/SpsUulzvyTL8YX2Sv0UmHiy1CVbH+dHX # 8IirooaUgUPmtyahN9xToidHyJFZiO6a3Vx4ytnp+DMEfmYXEEyuBEHHOaPMrJO4 # w21WnYsIEOdZGochNmQUFKV+XUBKfhwQQbpPnWYEXIIMg7Vw/V8GS2x4DEtfM6kz # XZIsJSPuzS2CIatMH34VJ1z1hLhqWwy+aDawGu1upGdppaTUHtFTJpIpevBjYPUE # BSRSOPePIUwyJWs4F+tK2Z/BiKK3mzKoJYiiKHpFMfGE87IA2KO9mnn2e6aoO/aW # sa8I09h8OHBAWaEcu4H+x33F9fsVqxRAAV4E85uCAAz9kwSweIWa2lBimihDEMUr # j5l6XpL1rpxQVUgz6PbtHjFANa0tF3SO+dWtlbsq/bEMqIleNTQ/hIxf+3Ro9IMl # a96cRtqhWctYrAS9/pAec53M7SspjsCzHTr6mM9s3Ic4ic56ZY12nTNjRHPYmTLQ # qJavFBWFhcTVYM2MHlVGa0rIHeGp23ldYp6dlUU3gBSp1NF/OSS78lGW56gzyonX # 4BHdMJVuFGd285NHC9qF7eO20Q3BK/3O1Q/C6gOiCkhFW08TLtSOJzZZlwfI3O/6 # ocliytvvxsa8lVszcBLxM1IPJnZ88HDSPh+V # SIG # End signature block |