Framework/Core/SVT/ADO/ADO.Project.ps1
Set-StrictMode -Version Latest class Project: ADOSVTBase { [PSObject] $PipelineSettingsObj = $null hidden $PAMembers = @() hidden $Repos = $null Project([string] $organizationName, [SVTResource] $svtResource): Base($organizationName,$svtResource) { $this.Repos = $null $this.GetPipelineSettingsObj() } GetPipelineSettingsObj() { $apiURL = "https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1" -f $($this.OrganizationContext.OrganizationName); #TODO: testing adding below line commenting above line #$apiURL = "https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1" -f $($this.OrganizationContext.OrganizationName); $orgUrl = "https://dev.azure.com/{0}" -f $($this.OrganizationContext.OrganizationName); $projectName = $this.ResourceContext.ResourceName; #$inputbody = "{'contributionIds':['ms.vss-org-web.collection-admin-policy-data-provider'],'context':{'properties':{'sourcePage':{'url':'$orgUrl/_settings/policy','routeId':'ms.vss-admin-web.collection-admin-hub-route','routeValues':{'adminPivot':'policy','controller':'ContributedPage','action':'Execute'}}}}}" | ConvertFrom-Json $inputbody = "{'contributionIds':['ms.vss-build-web.pipelines-general-settings-data-provider'],'dataProviderContext':{'properties':{'sourcePage':{'url':'$orgUrl/$projectName/_settings/settings','routeId':'ms.vss-admin-web.project-admin-hub-route','routeValues':{'project':'$projectName','adminPivot':'settings','controller':'ContributedPage','action':'Execute'}}}}}" | ConvertFrom-Json $responseObj = $null try{ $responseObj = [WebRequestHelper]::InvokePostWebRequest($apiURL,$inputbody); } catch{ #Write-Host "Pipeline settings for the project [$projectName] can not be fetched." } if($responseObj){ if([Helpers]::CheckMember($responseObj,"dataProviders")) { try { if($responseObj.dataProviders.'ms.vss-build-web.pipelines-general-settings-data-provider'){ $this.PipelineSettingsObj = $responseObj.dataProviders.'ms.vss-build-web.pipelines-general-settings-data-provider' } } catch { #Write-Host "Pipeline settings for the project [$projectName] can not be fetched." } } } } hidden [ControlResult] CheckPublicProjects([ControlResult] $controlResult) { try { if([Helpers]::CheckMember($this.ResourceContext.ResourceDetails,"visibility")) { $visibility = $this.ResourceContext.ResourceDetails.visibility; if(($visibility -eq "private") -or ($visibility -eq "organization")) { $controlResult.AddMessage([VerificationResult]::Passed, "Project visibility is set to '$visibility'."); } else # For orgs with public projects allowed, this control needs to be attested by the project admins. { $controlResult.AddMessage([VerificationResult]::Failed, "Project visibility is set to '$visibility'."); } $controlResult.AdditionalInfo += "Project visibility is set to: " + $visibility; } else { $controlResult.AddMessage([VerificationResult]::Error,"Project visibility details could not be fetched."); } } catch { $controlResult.AddMessage([VerificationResult]::Error,"Project visibility details could not be fetched."); } return $controlResult; } hidden [ControlResult] CheckBadgeAnonAccess([ControlResult] $controlResult) { if($this.PipelineSettingsObj) { if($this.PipelineSettingsObj.statusBadgesArePrivate.enabled -eq $true ) { $controlResult.AddMessage([VerificationResult]::Passed, "Anonymous access to status badge API is disabled. It is set as '$($this.PipelineSettingsObj.statusBadgesArePrivate.orgEnabled)' at organization scope."); } else{ $controlResult.AddMessage([VerificationResult]::Failed, "Anonymous access to status badge API is enabled. It is set as '$($this.PipelineSettingsObj.statusBadgesArePrivate.orgEnabled)' at organization scope."); } } else{ $controlResult.AddMessage([VerificationResult]::Manual, "Pipeline settings could not be fetched due to insufficient permissions at project scope."); } return $controlResult } hidden [ControlResult] CheckSettableQueueTime([ControlResult] $controlResult) { if($this.PipelineSettingsObj) { if($this.PipelineSettingsObj.enforceSettableVar.enabled -eq $true ) { $controlResult.AddMessage([VerificationResult]::Passed, "Only limited variables can be set at queue time. It is set as '$($this.PipelineSettingsObj.enforceSettableVar.orgEnabled)' at organization scope."); } else{ $controlResult.AddMessage([VerificationResult]::Failed, "All variables can be set at queue time. It is set as '$($this.PipelineSettingsObj.enforceSettableVar.orgEnabled)' at organization scope."); } } else{ $controlResult.AddMessage([VerificationResult]::Manual, "Pipeline settings could not be fetched due to insufficient permissions at project scope."); } return $controlResult } hidden [ControlResult] CheckJobAuthZScope([ControlResult] $controlResult) { if($this.PipelineSettingsObj) { $orgLevelScope = $this.PipelineSettingsObj.enforceJobAuthScope.orgEnabled; $prjLevelScope = $this.PipelineSettingsObj.enforceJobAuthScope.enabled; if($prjLevelScope -eq $true ) { $controlResult.AddMessage([VerificationResult]::Passed, "Job authorization scope is limited to current project for non-release pipelines."); } else { $controlResult.AddMessage([VerificationResult]::Failed, "Job authorization scope is set to project collection for non-release pipelines."); } if($orgLevelScope -eq $true ) { $controlResult.AddMessage("This setting is enabled (limited to current project) at organization level."); } else { $controlResult.AddMessage("This setting is disabled (set to project collection) at organization level."); } } else { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch project pipeline settings."); } return $controlResult } hidden [ControlResult] CheckJobAuthZReleaseScope([ControlResult] $controlResult) { if($this.PipelineSettingsObj) { $orgLevelScope = $this.PipelineSettingsObj.enforceJobAuthScopeForReleases.orgEnabled; $prjLevelScope = $this.PipelineSettingsObj.enforceJobAuthScopeForReleases.enabled; if($prjLevelScope -eq $true ) { $controlResult.AddMessage([VerificationResult]::Passed, "Job authorization scope is limited to current project for release pipelines."); } else { $controlResult.AddMessage([VerificationResult]::Failed, "Job authorization scope is set to project collection for release pipelines."); } if($orgLevelScope -eq $true ) { $controlResult.AddMessage("This setting is enabled (limited to current project) at organization level."); } else { $controlResult.AddMessage("This setting is disabled (set to project collection) at organization level."); } } else { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch project pipeline settings."); } return $controlResult } hidden [ControlResult] CheckAuthZRepoScope([ControlResult] $controlResult) { if($this.PipelineSettingsObj) { $orgLevelScope = $this.PipelineSettingsObj.enforceReferencedRepoScopedToken.orgEnabled; $prjLevelScope = $this.PipelineSettingsObj.enforceReferencedRepoScopedToken.enabled; if($prjLevelScope -eq $true ) { $controlResult.AddMessage([VerificationResult]::Passed, "Job authorization scope of pipelines is limited to explicitly referenced Azure DevOps repositories."); } else { $controlResult.AddMessage([VerificationResult]::Failed, "Job authorization scope of pipelines is set to all Azure DevOps repositories in the authorized projects."); } if($orgLevelScope -eq $true ) { $controlResult.AddMessage("This setting is enabled (limited to explicitly referenced Azure DevOps repositories) at organization level."); } else { $controlResult.AddMessage("This setting is disabled (set to all Azure DevOps repositories in authorized projects) at organization level."); } } else { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch project pipeline settings."); } return $controlResult } hidden [ControlResult] CheckPublishMetadata([ControlResult] $controlResult) { if($this.PipelineSettingsObj) { if($this.PipelineSettingsObj.publishPipelineMetadata.enabled -eq $true ) { $controlResult.AddMessage([VerificationResult]::Passed, "Publishing metadata from pipeline is enabled. It is set as '$($this.PipelineSettingsObj.publishPipelineMetadata.orgEnabled)' at organization scope."); } else{ $controlResult.AddMessage([VerificationResult]::Failed, "Publishing metadata from pipeline is disabled. It is set as '$($this.PipelineSettingsObj.publishPipelineMetadata.orgEnabled)' at organization scope."); } } else{ $controlResult.AddMessage([VerificationResult]::Manual, "Pipeline settings could not be fetched due to insufficient permissions at project scope."); } return $controlResult } hidden [ControlResult] CheckRBACAccess([ControlResult] $controlResult) { $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName); $inputbody = '{"contributionIds":["ms.vss-admin-web.org-admin-groups-data-provider"],"dataProviderContext":{"properties":{"sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"permissions","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.sourcePage.url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ResourceContext.ResourceName)/_settings/permissions"; $inputbody.dataProviderContext.properties.sourcePage.routeValues.Project =$this.ResourceContext.ResourceName; $groupsObj = [WebRequestHelper]::InvokePostWebRequest($url,$inputbody); $Allgroups = @() $groupsObj.dataProviders."ms.vss-admin-web.org-admin-groups-data-provider".identities | ForEach-Object { $Allgroups += $_; } $descrurl ='https://vssps.dev.azure.com/{0}/_apis/graph/descriptors/{1}?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName), $this.ResourceContext.ResourceId.split('/')[-1]; $descr = [WebRequestHelper]::InvokeGetWebRequest($descrurl); $apiURL = "https://vssps.dev.azure.com/{0}/_apis/Graph/Users?scopeDescriptor={1}" -f $($this.OrganizationContext.OrganizationName), $descr[0]; $usersObj = [WebRequestHelper]::InvokeGetWebRequest($apiURL); <# $Users = @() $usersObj[0].items | ForEach-Object { $Users+= $_ } #> $groups = ($Allgroups | Select-Object -Property @{Name="Name"; Expression = {$_.displayName}},@{Name="Description"; Expression = {$_.description}}); $UsersNames = ($usersObj | Select-Object -Property @{Name="Name"; Expression = {$_.displayName}},@{Name="mailAddress"; Expression = {$_.mailAddress}}) if ( (($groups | Measure-Object).Count -gt 0) -or (($UsersNames | Measure-Object).Count -gt 0)) { $controlResult.AddMessage([VerificationResult]::Verify, "Verify users and groups present on project"); $controlResult.AddMessage("Verify groups has access on project", $groups); $controlResult.AddMessage("Verify users has access on project", $UsersNames); } else { $controlResult.AddMessage([VerificationResult]::Passed, "No users or groups found"); } return $controlResult } hidden [ControlResult] JustifyGroupMember([ControlResult] $controlResult) { $grpmember = @(); $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName); $inputbody = '{"contributionIds":["ms.vss-admin-web.org-admin-groups-data-provider"],"dataProviderContext":{"properties":{"sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"permissions","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.sourcePage.url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ResourceContext.ResourceName)/_settings/permissions"; $inputbody.dataProviderContext.properties.sourcePage.routeValues.Project =$this.ResourceContext.ResourceName; $groupsObj = [WebRequestHelper]::InvokePostWebRequest($url,$inputbody); $groups = @() $groupsObj.dataProviders."ms.vss-admin-web.org-admin-groups-data-provider".identities | ForEach-Object { $groups += $_; } $apiURL = "https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview" -f $($this.OrganizationContext.OrganizationName); $membercount =0; Foreach ($group in $groups){ $groupmember = @(); $descriptor = $group.descriptor; $inputbody = '{"contributionIds":["ms.vss-admin-web.org-admin-members-data-provider"],"dataProviderContext":{"properties":{"subjectDescriptor":"","sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"permissions","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.subjectDescriptor = $descriptor; $inputbody.dataProviderContext.properties.sourcePage.url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ResourceContext.ResourceName)/_settings/permissions?subjectDescriptor=$($descriptor)"; $inputbody.dataProviderContext.properties.sourcePage.routeValues.Project =$this.ResourceContext.ResourceName; $usersObj = [WebRequestHelper]::InvokePostWebRequest($apiURL,$inputbody); if([Helpers]::CheckMember($usersObj.dataProviders.'ms.vss-admin-web.org-admin-members-data-provider', "identities")) { $usersObj.dataProviders."ms.vss-admin-web.org-admin-members-data-provider".identities | ForEach-Object { $groupmember += $_; } } $grpmember = ($groupmember | Select-Object -Property @{Name="Name"; Expression = {$_.displayName}},@{Name="mailAddress"; Expression = {$_.mailAddress}}); if ($grpmember -ne $null) { $membercount= $membercount + 1 $controlResult.AddMessage("Verify below members of the group: '$($group.principalname)', Description: $($group.description)", $grpmember); } } if ( $membercount -gt 0) { $controlResult.AddMessage([VerificationResult]::Verify, "Verify members of groups present on project"); } else { $controlResult.AddMessage([VerificationResult]::Passed, "No users or groups found"); } return $controlResult } hidden [ControlResult] CheckMinPACount([ControlResult] $controlResult) { $TotalPAMembers = 0; if (($this.PAMembers | Measure-Object).Count -eq 0) { $this.PAMembers += [AdministratorHelper]::GetTotalPAMembers($this.OrganizationContext.OrganizationName,$this.ResourceContext.ResourceName) $this.PAMembers = $this.PAMembers | Select-Object displayName,mailAddress } $TotalPAMembers = ($this.PAMembers | Measure-Object).Count $controlResult.AddMessage("There are a total of $TotalPAMembers Project Administrators in your project.") if($TotalPAMembers -lt $this.ControlSettings.Project.MinPAMembersPermissible){ $controlResult.AddMessage([VerificationResult]::Failed,"Number of administrators configured are less than the minimum required administrators count: $($this.ControlSettings.Project.MinPAMembersPermissible)."); } else{ $controlResult.AddMessage([VerificationResult]::Passed,"Number of administrators configured are more than the minimum required administrators count: $($this.ControlSettings.Project.MinPAMembersPermissible)."); } if($TotalPAMembers -gt 0){ $controlResult.AddMessage("Verify the following Project Administrators: ",$this.PAMembers) $controlResult.SetStateData("List of Project Administrators: ",$this.PAMembers) $controlResult.AdditionalInfo += "Total number of Project Administrators: " + $TotalPAMembers; } return $controlResult } hidden [ControlResult] CheckMaxPACount([ControlResult] $controlResult) { $TotalPAMembers = 0; if (($this.PAMembers | Measure-Object).Count -eq 0) { $this.PAMembers += [AdministratorHelper]::GetTotalPAMembers($this.OrganizationContext.OrganizationName,$this.ResourceContext.ResourceName) $this.PAMembers = $this.PAMembers | Select-Object displayName,mailAddress } $TotalPAMembers = ($this.PAMembers | Measure-Object).Count $controlResult.AddMessage("There are a total of $TotalPAMembers Project Administrators in your project.") if($TotalPAMembers -gt $this.ControlSettings.Project.MaxPAMembersPermissible){ $controlResult.AddMessage([VerificationResult]::Failed,"Number of administrators configured are more than the approved limit: $($this.ControlSettings.Project.MaxPAMembersPermissible)."); } else{ $controlResult.AddMessage([VerificationResult]::Passed,"Number of administrators configured are within than the approved limit: $($this.ControlSettings.Project.MaxPAMembersPermissible)."); } if($TotalPAMembers -gt 0){ $controlResult.AddMessage("Verify the following Project Administrators: ",$this.PAMembers) $controlResult.SetStateData("List of Project Administrators: ",$this.PAMembers) $controlResult.AdditionalInfo += "Total number of Project Administrators: " + $TotalPAMembers; } return $controlResult } hidden [ControlResult] CheckSCALTForAdminMembers([ControlResult] $controlResult) { try { if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings, "Project.GroupsToCheckForSCAltMembers")) { $adminGroupNames = $this.ControlSettings.Project.GroupsToCheckForSCAltMembers; if (($adminGroupNames | Measure-Object).Count -gt 0) { #api call to get descriptor for organization groups. This will be used to fetch membership of individual groups later. $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName); $inputbody = '{"contributionIds":["ms.vss-admin-web.org-admin-groups-data-provider"],"dataProviderContext":{"properties":{"sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"permissions","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.sourcePage.url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ResourceContext.ResourceName)/_settings/permissions"; $inputbody.dataProviderContext.properties.sourcePage.routeValues.Project = $this.ResourceContext.ResourceName; $response = [WebRequestHelper]::InvokePostWebRequest($url, $inputbody); if ($response -and [Helpers]::CheckMember($response[0], "dataProviders") -and $response[0].dataProviders."ms.vss-admin-web.org-admin-groups-data-provider") { $adminGroups = @(); $adminGroups += $response.dataProviders."ms.vss-admin-web.org-admin-groups-data-provider".identities | where { $_.displayName -in $adminGroupNames } if(($adminGroups | Measure-Object).Count -gt 0) { #global variable to track admin members across all admin groups $allAdminMembers = @(); for ($i = 0; $i -lt $adminGroups.Count; $i++) { # [AdministratorHelper]::AllPAMembers is a static variable. Always needs ro be initialized. At the end of each iteration, it will be populated with members of that particular admin group. [AdministratorHelper]::AllPAMembers = @(); # Helper function to fetch flattened out list of group members. [AdministratorHelper]::FindPAMembers($adminGroups[$i].descriptor, $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceName) $groupMembers = @(); # Add the members of current group to this temp variable. $groupMembers += [AdministratorHelper]::AllPAMembers # Create a custom object to append members of current group with the group name. Each of these custom object is added to the global variable $allAdminMembers for further analysis of SC-Alt detection. $groupMembers | ForEach-Object {$allAdminMembers += @( [PSCustomObject] @{ name = $_.displayName; mailAddress = $_.mailAddress; id = $_.originId; groupName = $adminGroups[$i].displayName } )} } # clearing cached value in [AdministratorHelper]::AllPAMembers as it can be used in attestation later and might have incorrect group loaded. [AdministratorHelper]::AllPAMembers = @(); # Filtering out distinct entries. A user might be added directly to the admin group or might be a member of a child group of the admin group. $allAdminMembers = $allAdminMembers| Sort-Object -Property id -Unique if(($allAdminMembers | Measure-Object).Count -gt 0) { if([Helpers]::CheckMember($this.ControlSettings, "AlernateAccountRegularExpressionForOrg")){ $matchToSCAlt = $this.ControlSettings.AlernateAccountRegularExpressionForOrg #currently SC-ALT regex is a singleton expression. In case we have multiple regex - we need to make the controlsetting entry as an array and accordingly loop the regex here. if (-not [string]::IsNullOrEmpty($matchToSCAlt)) { $nonSCMembers = @(); $nonSCMembers += $allAdminMembers | Where-Object { $_.mailAddress -notmatch $matchToSCAlt } $nonSCCount = ($nonSCMembers | Measure-Object).Count $SCMembers = @(); $SCMembers += $allAdminMembers | Where-Object { $_.mailAddress -match $matchToSCAlt } $SCCount = ($SCMembers | Measure-Object).Count if ($nonSCCount -gt 0) { $nonSCMembers = $nonSCMembers | Select-Object name,mailAddress,groupName $stateData = @(); $stateData += $nonSCMembers $controlResult.AddMessage([VerificationResult]::Failed, "`nTotal number of non SC-ALT accounts with admin privileges: $nonSCCount"); $controlResult.AddMessage("Review the non SC-ALT accounts with admin privileges: ", $stateData); $controlResult.SetStateData("List of non SC-ALT accounts with admin privileges: ", $stateData); $controlResult.AdditionalInfo += "Total number of non SC-ALT accounts with admin privileges: " + $nonSCCount; } else { $controlResult.AddMessage([VerificationResult]::Passed, "No users have admin privileges with non SC-ALT accounts."); } if ($SCCount -gt 0) { $SCMembers = $SCMembers | Select-Object name,mailAddress,groupName $SCData = @(); $SCData += $SCMembers $controlResult.AddMessage("`nTotal number of SC-ALT accounts with admin privileges: $SCCount"); $controlResult.AdditionalInfo += "Total number of SC-ALT accounts with admin privileges: " + $SCCount; $controlResult.AddMessage("SC-ALT accounts with admin privileges: ", $SCData); } } else { $controlResult.AddMessage([VerificationResult]::Manual, "Regular expressions for detecting SC-ALT account is not defined in the organization."); } } else{ $controlResult.AddMessage([VerificationResult]::Error, "Regular expressions for detecting SC-ALT account is not defined in the organization. Please update your ControlSettings.json as per the latest AzSK.ADO PowerShell module."); } } else { #count is 0 then there is no members added in the admin groups $controlResult.AddMessage([VerificationResult]::Passed, "Admin groups does not have any members."); } } else { $controlResult.AddMessage([VerificationResult]::Error, "Could not find the list of administrator groups in the project."); } } else { $controlResult.AddMessage([VerificationResult]::Error, "Could not find the list of groups in the project."); } } else { $controlResult.AddMessage([VerificationResult]::Manual, "List of administrator groups for detecting non SC-ALT accounts is not defined in your project."); } } else { $controlResult.AddMessage([VerificationResult]::Error, "List of administrator groups for detecting non SC-ALT accounts is not defined in your project. Please update your ControlSettings.json as per the latest AzSK.ADO PowerShell module."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the list of groups in the project."); } return $controlResult } hidden [ControlResult] CheckFeedAccess([ControlResult] $controlResult) { try { $url = 'https://feeds.dev.azure.com/{0}/{1}/_apis/packaging/feeds?api-version=6.0-preview.1' -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceName; $feedsObj = [WebRequestHelper]::InvokeGetWebRequest($url); $feedsWithUploadPkgPermission = @(); $GroupsToCheckForFeedPermission = $null; if (($feedsObj | Measure-Object).Count -gt 0) { $controlResult.AddMessage("Total number of feeds found: $($feedsObj.count)") $controlResult.AdditionalInfo += "Total number of feeds found: " + $feedsObj.count; if ([Helpers]::CheckMember($this.ControlSettings.Project, "GroupsToCheckForFeedPermission")) { $GroupsToCheckForFeedPermission = $this.ControlSettings.Project.GroupsToCheckForFeedPermission } foreach ($feed in $feedsObj) { #GET https://feeds.dev.azure.com/{organization}/{project}/_apis/packaging/Feeds/{feedId}/permissions?api-version=6.0-preview.1 #Using visualstudio api because new api (dev.azure.com) is giving null in the displayName property. $url = 'https://{0}.feeds.visualstudio.com/{1}/_apis/Packaging/Feeds/{2}/Permissions?includeIds=true&excludeInheritedPermissions=false&includeDeletedFeeds=false' -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceName, $feed.Id; $feedPermissionObj = [WebRequestHelper]::InvokeGetWebRequest($url); $feedsPermission = ($feedPermissionObj | Where-Object {$_.role -eq "administrator" -or $_.role -eq "contributor"}) | Select-Object -Property @{Name="FeedName"; Expression = {$feed.name}},@{Name="Role"; Expression = {$_.role}},@{Name="DisplayName"; Expression = {$_.displayName}} ; $feedsWithUploadPkgPermission += $feedsPermission | Where-Object { $GroupsToCheckForFeedPermission -contains $_.DisplayName.split('\')[-1] } } } $feedCount = ($feedsWithUploadPkgPermission | Measure-Object).Count; if ($feedCount -gt 0) { $controlResult.AddMessage("`nNote: The following groups are considered as 'critical': [$GroupsToCheckForFeedPermission]"); $controlResult.AddMessage("`nTotal number of feeds that have contributor/administrator permission: $feedCount"); $controlResult.AddMessage([VerificationResult]::Failed, "List of feeds that have contributor/administrator permission: "); $controlResult.AdditionalInfo += "Total number of groups that are considered as 'critical': " + ($GroupsToCheckForFeedPermission | Measure-Object).Count; $controlResult.AdditionalInfo += "Total number of feeds that have contributor/administrator permission: " + $feedCount; $display = ($feedsWithUploadPkgPermission | FT FeedName, Role, DisplayName -AutoSize | Out-String -Width 512) $controlResult.AddMessage($display) } else { $controlResult.AddMessage([VerificationResult]::Passed, "No feeds found in the project."); } } catch { $controlResult.AddMessage([VerificationResult]::Passed, "Could not fetch project feed settings."); } return $controlResult } hidden [ControlResult] CheckEnviornmentAccess([ControlResult] $controlResult) { try { $apiURL = "https://dev.azure.com/{0}/{1}/_apis/distributedtask/environments?api-version=6.0-preview.1" -f $($this.OrganizationContext.OrganizationName), $($this.ResourceContext.ResourceName); $responseObj = [WebRequestHelper]::InvokeGetWebRequest($apiURL); # TODO: When there are no environments configured, CheckMember in the below condition returns false when checknull flag [third param in CheckMember] is not specified (default value is $true). Assiging it $false. Need to revisit. if(([Helpers]::CheckMember($responseObj[0],"count",$false)) -and ($responseObj[0].count -eq 0)) { $controlResult.AddMessage([VerificationResult]::Passed, "No environment has been configured in the project."); } # When environments are configured - the below condition will be true. elseif((-not ([Helpers]::CheckMember($responseObj[0],"count"))) -and ($responseObj.Count -gt 0)) { $environmentsWithOpenAccess = @(); foreach ($item in $responseObj) { $url = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/environment/{2}" -f $($this.OrganizationContext.OrganizationName), $($this.ResourceContext.ResourceDetails.id), $($item.id); $apiResponse = [WebRequestHelper]::InvokeGetWebRequest($url); if (([Helpers]::CheckMember($apiResponse,"allPipelines")) -and ($apiResponse.allPipelines.authorized -eq $true)) { $environmentsWithOpenAccess += $item | Select-Object id, name; } } $environmentsWithOpenAccessCount = ($environmentsWithOpenAccess | Measure-Object).Count; if($environmentsWithOpenAccessCount -gt 0) { $controlResult.AddMessage([VerificationResult]::Failed, "Total number of environments in the project that are accessible to all pipelines: $($environmentsWithOpenAccessCount)"); $controlResult.AddMessage("List of environments in the project that are accessible to all pipelines: ", $environmentsWithOpenAccess); $controlResult.AdditionalInfo += "Total number of environments in the project that are accessible to all pipelines: " + $environmentsWithOpenAccessCount; } else { $controlResult.AddMessage([VerificationResult]::Passed, "There are no environments that are accessible to all pipelines."); } } else { $controlResult.AddMessage([VerificationResult]::Passed, "No environments found in the project."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the list of environments in the project."); } return $controlResult } hidden [ControlResult] CheckSecureFilesPermission([ControlResult] $controlResult) { # getting the project ID $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] $url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($projectId)/_apis/distributedtask/securefiles?api-version=6.1-preview.1" try { $response = [WebRequestHelper]::InvokeGetWebRequest($url); # check on response object, if null -> no secure files present if(([Helpers]::CheckMember($response[0],"count",$false)) -and ($response[0].count -eq 0)) { $controlResult.AddMessage([VerificationResult]::Passed, "There are no secure files present."); } # else there are secure files present elseif((-not ([Helpers]::CheckMember($response[0],"count"))) -and ($response.Count -gt 0)) { # object to keep a track of authorized secure files and their count [Hashtable] $secFiles = @{ count = 0; names = @(); }; foreach ($secFile in $response) { $url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($projectId)/_apis/build/authorizedresources?type=securefile&id=$($secFile.id)" $resp = [WebRequestHelper]::InvokeGetWebRequest($url); # check if the secure file is authorized if((-not ([Helpers]::CheckMember($resp[0],"count"))) -and ($resp.Count -gt 0)) { if([Helpers]::CheckMember($resp, "authorized")) { if($resp.authorized) { $secFiles.count += 1; $secFiles.names += $secFile.name; } } } } # there are secure files present that are authorized if($secFiles.count -gt 0) { $controlResult.AddMessage([VerificationResult]::Failed, "Total number of secure files in the project that are authorized for use in all pipelines: $($secFiles.count)"); $controlResult.AddMessage("List of secure files in the project that are authorized for use in all pipelines: ", $secFiles.names); $controlResult.AdditionalInfo += "Total number of secure files in the project that are authorized for use in all pipelines: " + $secFiles.count; } # there are no secure files present that are authorized else { $controlResult.AddMessage([VerificationResult]::Passed, "There are no secure files in the project that are authorized for use in all pipelines."); } } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the list of secure files."); } return $controlResult } hidden [ControlResult] CheckAuthorEmailValidationPolicy([ControlResult] $controlResult) { # body for post request $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName); $inputbody = '{"contributionIds":["ms.vss-code-web.repository-policies-data-provider"],"dataProviderContext":{"properties":{"projectId": "","sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"repositories","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.projectId = "$($this.ResourceContext.ResourceDetails.id)" $inputbody.dataProviderContext.properties.sourcePage.routeValues.project = "$($this.ResourceContext.ResourceName)" $inputbody.dataProviderContext.properties.sourcePage.url = "https://$($this.OrganizationContext.OrganizationName).visualstudio.com/$($this.ResourceContext.ResourceName)/_settings/repositories?_a=policies" try { $response = [WebRequestHelper]::InvokePostWebRequest($url, $inputbody); if ([Helpers]::CheckMember($response, "dataProviders") -and $response.dataProviders.'ms.vss-code-web.repository-policies-data-provider' -and [Helpers]::CheckMember($response.dataProviders.'ms.vss-code-web.repository-policies-data-provider', "policyGroups")) { # fetching policy groups $policyGroups = $response.dataProviders."ms.vss-code-web.repository-policies-data-provider".policyGroups # fetching "Commit author email validation" $authorEmailPolicyId = $this.ControlSettings.Repo.AuthorEmailValidationPolicyID $commitAuthorEmailPattern = $this.ControlSettings.Repo.CommitAuthorEmailPattern if ([Helpers]::CheckMember($policyGroups, $authorEmailPolicyId)) { $currentScopePoliciesEmail = $policyGroups."$($authorEmailPolicyId)".currentScopePolicies $controlResult.AddMessage("`nNote: Commits from the following email ids are considered as 'trusted': `n`t[$($commitAuthorEmailPattern -join ', ')]"); # validating email patterns $flag = 0; $emailPatterns = $currentScopePoliciesEmail.settings.authorEmailPatterns $invalidPattern = @() if ($emailPatterns -eq $null) { $flag = 1; } else { foreach ($val in $emailPatterns) { if ($val -notin $commitAuthorEmailPattern -and (-not [string]::IsNullOrEmpty($val))) { $flag = 1; $invalidPattern += $val } } } if ($flag -eq 0) { $controlResult.AddMessage([VerificationResult]::Passed, "Commit author email validation is set as per the organizational requirements."); } else { $controlResult.AddMessage([VerificationResult]::Verify, "Commit author email validation is not set as per the organizational requirements."); if($invalidPattern.Count -gt 0) { $controlResult.AddMessage("List of commit author email patterns that are not trusted: $($invalidPattern)") } } } else { $controlResult.AddMessage([VerificationResult]::Failed, "'Commit author email validation' policy is disabled."); } } else { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch repository policies."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch repository policies $($_)."); } return $controlResult } hidden [ControlResult] CheckCredentialsAndSecretsPolicy([ControlResult] $controlResult) { # body for post request $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName); $inputbody = '{"contributionIds":["ms.vss-code-web.repository-policies-data-provider"],"dataProviderContext":{"properties":{"projectId": "","sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"repositories","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.projectId = "$($this.ResourceContext.ResourceDetails.id)" $inputbody.dataProviderContext.properties.sourcePage.routeValues.project = "$($this.ResourceContext.ResourceName)" $inputbody.dataProviderContext.properties.sourcePage.url = "https://$($this.OrganizationContext.OrganizationName).visualstudio.com/$($this.ResourceContext.ResourceName)/_settings/repositories?_a=policies" try { $response = [WebRequestHelper]::InvokePostWebRequest($url, $inputbody); if ([Helpers]::CheckMember($response, "dataProviders") -and $response.dataProviders.'ms.vss-code-web.repository-policies-data-provider' -and [Helpers]::CheckMember($response.dataProviders.'ms.vss-code-web.repository-policies-data-provider', "policyGroups")) { # fetching policy groups $policyGroups = $response.dataProviders."ms.vss-code-web.repository-policies-data-provider".policyGroups # fetching "Secrets scanning restriction" $credScanId = $this.ControlSettings.Repo.CredScanPolicyID if ([Helpers]::CheckMember($policyGroups, $credScanId)) { $currentScopePoliciesSecrets = $policyGroups."$($credScanId)".currentScopePolicies if ($currentScopePoliciesSecrets.isEnabled) { $controlResult.AddMessage([VerificationResult]::Passed, "Check for credentials and other secrets is enabled."); } else { $controlResult.AddMessage([VerificationResult]::Failed, "Check for credentials and other secrets is disabled."); } } else { $controlResult.AddMessage([VerificationResult]::Failed, "Check for credentials and other secrets is disabled."); } } else { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch repository policies."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch repository policies $($_)."); } return $controlResult } hidden [PSObject] FetchRepositoriesList() { if($null -eq $this.Repos) { # fetch repositories $repoDefnURL = ("https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ResourceContext.ResourceName)/_apis/git/repositories?api-version=6.0") try { $repoDefnsObj = [WebRequestHelper]::InvokeGetWebRequest($repoDefnURL); $this.Repos = $repoDefnsObj; } catch { $this.Repos = $null } } return $this.Repos } hidden [ControlResult] CheckInactiveRepo([ControlResult] $controlResult) { try { $repoDefnsObj = $this.FetchRepositoriesList() $inactiveRepos = @() $threshold = $this.ControlSettings.Repo.RepoHistoryPeriodInDays if (($repoDefnsObj | Measure-Object).Count -gt 0) { foreach ($repo in $repoDefnsObj) { $currentDate = Get-Date # check if repo has commits in past RepoHistoryPeriodInDays days $thresholdDate = $currentDate.AddDays(-$threshold); $url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ResourceContext.ResourceName)/_apis/git/repositories/$($repo.id)/commits?searchCriteria.fromDate=$($thresholdDate)&&api-version=6.0" try{ $res = [WebRequestHelper]::InvokeGetWebRequest($url); # When there are no commits, CheckMember in the below condition returns false when checknull flag [third param in CheckMember] is not specified (default value is $true). Assiging it $false. if (([Helpers]::CheckMember($res[0], "count", $false)) -and ($res[0].count -eq 0)) { $inactiveRepos += $repo.name } } catch{ $controlResult.AddMessage("Could not fetch the history of repository [$($repo.name)]."); } } $inactivecount = $inactiveRepos.Count if ($inactivecount -gt 0) { $inactiveRepos = $inactiveRepos | sort-object $controlResult.AddMessage([VerificationResult]::Failed, "Total number of inactive repositories that have no commits in last $($threshold) days: $($inactivecount) ", $inactiveRepos); } else { $controlResult.AddMessage([VerificationResult]::Passed, "There are no inactive repositories in the project."); } } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the list of repositories in the project.", $_); } return $controlResult } hidden [ControlResult] CheckRepoRBACAccess([ControlResult] $controlResult) { $accessList = @() #permissionSetId = '2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87' is the std. namespaceID. Refer: https://docs.microsoft.com/en-us/azure/devops/organizations/security/manage-tokens-namespaces?view=azure-devops#namespaces-and-their-ids try{ $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName); $refererUrl = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ResourceContext.ResourceName)/_settings/repositories?_a=permissions"; $inputbody = '{"contributionIds":["ms.vss-admin-web.security-view-members-data-provider"],"dataProviderContext":{"properties":{"permissionSetId": "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87","permissionSetToken":"","sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"repositories","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.sourcePage.url = $refererUrl $inputbody.dataProviderContext.properties.sourcePage.routeValues.Project = $this.ResourceContext.ResourceName; $inputbody.dataProviderContext.properties.permissionSetToken = "repoV2/$($this.ResourceContext.ResourceDetails.id)" # Get list of all users and groups granted permissions on all repositories $responseObj = [WebRequestHelper]::InvokePostWebRequest($url, $inputbody); # Iterate through each user/group to fetch detailed permissions list if([Helpers]::CheckMember($responseObj[0],"dataProviders") -and ($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider') -and ([Helpers]::CheckMember($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider',"identities"))) { $body = '{"contributionIds":["ms.vss-admin-web.security-view-permissions-data-provider"],"dataProviderContext":{"properties":{"subjectDescriptor":"","permissionSetId": "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87","permissionSetToken":"","accountName":"","sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"repositories","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $body.dataProviderContext.properties.sourcePage.url = $refererUrl $body.dataProviderContext.properties.sourcePage.routeValues.Project = $this.ResourceContext.ResourceName; $body.dataProviderContext.properties.permissionSetToken = "repoV2/$($this.ResourceContext.ResourceDetails.id)" $accessList += $responseObj.dataProviders."ms.vss-admin-web.security-view-members-data-provider".identities | Where-Object { $_.subjectKind -eq "group" } | ForEach-Object { $identity = $_ $body.dataProviderContext.properties.accountName = $_.principalName $body.dataProviderContext.properties.subjectDescriptor = $_.descriptor $identityPermissions = [WebRequestHelper]::InvokePostWebRequest($url, $body); $configuredPermissions = $identityPermissions.dataproviders."ms.vss-admin-web.security-view-permissions-data-provider".subjectPermissions | Where-Object {$_.permissionDisplayString -ne 'Not set'} return @{ IdentityName = $identity.DisplayName; IdentityType = $identity.subjectKind; Permissions = ($configuredPermissions | Select-Object @{Name="Name"; Expression = {$_.displayName}},@{Name="Permission"; Expression = {$_.permissionDisplayString}}) } } $accessList += $responseObj.dataProviders."ms.vss-admin-web.security-view-members-data-provider".identities | Where-Object { $_.subjectKind -eq "user" } | ForEach-Object { $identity = $_ $body.dataProviderContext.properties.subjectDescriptor = $_.descriptor $identityPermissions = [WebRequestHelper]::InvokePostWebRequest($url, $body); $configuredPermissions = $identityPermissions.dataproviders."ms.vss-admin-web.security-view-permissions-data-provider".subjectPermissions | Where-Object {$_.permissionDisplayString -ne 'Not set'} return @{ IdentityName = $identity.DisplayName; IdentityType = $identity.subjectKind; Permissions = ($configuredPermissions | Select-Object @{Name="Name"; Expression = {$_.displayName}},@{Name="Permission"; Expression = {$_.permissionDisplayString}}) } } } if(($accessList | Measure-Object).Count -ne 0) { $accessList= $accessList | Select-Object -Property @{Name="IdentityName"; Expression = {$_.IdentityName}},@{Name="IdentityType"; Expression = {$_.IdentityType}},@{Name="Permissions"; Expression = {$_.Permissions}} $controlResult.AddMessage([VerificationResult]::Verify,"Validate that the following identities have been provided with minimum RBAC access to repositories.", $accessList); $controlResult.SetStateData("List of identities having access to repositories: ", ($responseObj.dataProviders."ms.vss-admin-web.security-view-members-data-provider".identities | Select-Object -Property @{Name="IdentityName"; Expression = {$_.FriendlyDisplayName}},@{Name="IdentityType"; Expression = {$_.subjectKind}},@{Name="Scope"; Expression = {$_.Scope}})); } else { $controlResult.AddMessage([VerificationResult]::Passed,"No identities have been explicitly provided access to repositories."); } $responseObj = $null; } catch{ $controlResult.AddMessage([VerificationResult]::Manual,"Unable to fetch repositories permission details. $($_) Please verify from portal all teams/groups are granted minimum required permissions."); } return $controlResult } hidden [ControlResult] CheckInheritedPermissions([ControlResult] $controlResult) { $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] try { $repoDefnsObj = $this.FetchRepositoriesList() $failedRepos = @() $passedRepos = @() foreach ($repo in $repoDefnsObj) { $url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1" $body = "{ 'contributionIds': [ 'ms.vss-admin-web.security-view-data-provider' ], 'dataProviderContext': { 'properties': { 'permissionSetId': '2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87', 'permissionSetToken': '', 'sourcePage': { 'url': '', 'routeId': 'ms.vss-admin-web.project-admin-hub-route', 'routeValues': { 'project': '', 'adminPivot': 'repositories', 'controller': 'ContributedPage', 'action': 'Execute', 'serviceHost': '' } } } } }" | ConvertFrom-Json $body.dataProviderContext.properties.permissionSetToken = "repoV2/$($projectId)/$($repo.id)" $body.dataProviderContext.properties.sourcePage.url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$projectId/_settings/repositories?repo=/$($repo.id)&_a=permissionsMid"; $body.dataProviderContext.properties.sourcePage.routeValues.project = "$projectId"; $response = "" try { $response = [WebRequestHelper]::InvokePostWebRequest($url, $body); if ([Helpers]::CheckMember($response, "dataProviders") -and $response.dataProviders.'ms.vss-admin-web.security-view-data-provider' -and [Helpers]::CheckMember($response.dataProviders.'ms.vss-admin-web.security-view-data-provider', "permissionsContextJson")) { $permissionsContextJson = $response.dataProviders.'ms.vss-admin-web.security-view-data-provider'.permissionsContextJson $permissionsContextJson = $permissionsContextJson | ConvertFrom-Json if ($permissionsContextJson.inheritPermissions) { $failedRepos += $repo.name } else { $passedRepos += $repo.name } } else { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch details for repository $($repo.name). Please verify from portal whether inherited permissions is disabled."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch details for repository $($repo.name). Please verify from portal whether inherited permissions is disabled. $($_)."); } } $failedcount = $failedRepos.Count $passedcount = $passedRepos.Count $passedRepos = $passedRepos | sort-object if($failedcount -gt 0){ $failedRepos = $failedRepos | sort-object $controlResult.AddMessage([VerificationResult]::Failed, "Inherited permissions are enabled on the below $($failedcount) repositories:", $failedRepos); $controlResult.AddMessage("Inherited permissions are disabled on the below $($passedcount) repositories:", $passedRepos); } else { $controlResult.AddMessage("Inherited permissions are disabled on all repositories."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch list of repositories in the project. $($_)."); } return $controlResult } } # SIG # Begin signature block # MIIjoQYJKoZIhvcNAQcCoIIjkjCCI44CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAPBt3wxv5nHpLF # mXVVV7m9sO4KmEsyWDUaQU3S/0o9N6CCDYEwggX/MIID56ADAgECAhMzAAABh3IX # chVZQMcJAAAAAAGHMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjAwMzA0MTgzOTQ3WhcNMjEwMzAzMTgzOTQ3WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDOt8kLc7P3T7MKIhouYHewMFmnq8Ayu7FOhZCQabVwBp2VS4WyB2Qe4TQBT8aB # znANDEPjHKNdPT8Xz5cNali6XHefS8i/WXtF0vSsP8NEv6mBHuA2p1fw2wB/F0dH # sJ3GfZ5c0sPJjklsiYqPw59xJ54kM91IOgiO2OUzjNAljPibjCWfH7UzQ1TPHc4d # weils8GEIrbBRb7IWwiObL12jWT4Yh71NQgvJ9Fn6+UhD9x2uk3dLj84vwt1NuFQ # itKJxIV0fVsRNR3abQVOLqpDugbr0SzNL6o8xzOHL5OXiGGwg6ekiXA1/2XXY7yV # Fc39tledDtZjSjNbex1zzwSXAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUhov4ZyO96axkJdMjpzu2zVXOJcsw # UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 # ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDU4Mzg1MB8GA1UdIwQYMBaAFEhu # ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w # Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx # MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAixmy # S6E6vprWD9KFNIB9G5zyMuIjZAOuUJ1EK/Vlg6Fb3ZHXjjUwATKIcXbFuFC6Wr4K # NrU4DY/sBVqmab5AC/je3bpUpjtxpEyqUqtPc30wEg/rO9vmKmqKoLPT37svc2NV # BmGNl+85qO4fV/w7Cx7J0Bbqk19KcRNdjt6eKoTnTPHBHlVHQIHZpMxacbFOAkJr # qAVkYZdz7ikNXTxV+GRb36tC4ByMNxE2DF7vFdvaiZP0CVZ5ByJ2gAhXMdK9+usx # zVk913qKde1OAuWdv+rndqkAIm8fUlRnr4saSCg7cIbUwCCf116wUJ7EuJDg0vHe # yhnCeHnBbyH3RZkHEi2ofmfgnFISJZDdMAeVZGVOh20Jp50XBzqokpPzeZ6zc1/g # yILNyiVgE+RPkjnUQshd1f1PMgn3tns2Cz7bJiVUaqEO3n9qRFgy5JuLae6UweGf # AeOo3dgLZxikKzYs3hDMaEtJq8IP71cX7QXe6lnMmXU/Hdfz2p897Zd+kU+vZvKI # 3cwLfuVQgK2RZ2z+Kc3K3dRPz2rXycK5XCuRZmvGab/WbrZiC7wJQapgBodltMI5 # GMdFrBg9IeF7/rP4EqVQXeKtevTlZXjpuNhhjuR+2DMt/dWufjXpiW91bo3aH6Ea # jOALXmoxgltCp1K7hrS6gmsvj94cLRf50QQ4U8Qwggd6MIIFYqADAgECAgphDpDS # AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK # V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0 # ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla # MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT # H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG # OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S # 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz # y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7 # 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u # M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33 # X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl # XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP # 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB # l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF # RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM # CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ # BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud # DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO # 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0 # LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p # Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw # cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA # XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY # 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj # 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd # d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ # Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf # wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ # aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j # NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B # xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96 # eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7 # r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I # RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVdjCCFXICAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAYdyF3IVWUDHCQAAAAABhzAN # BglghkgBZQMEAgEFAKCBsDAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgLeaw0Ma3 # XjzaNwiP/uwwv4F5PpVFo8tmJCSjE2gMDbYwRAYKKwYBBAGCNwIBDDE2MDSgFIAS # AE0AaQBjAHIAbwBzAG8AZgB0oRyAGmh0dHBzOi8vd3d3Lm1pY3Jvc29mdC5jb20g # MA0GCSqGSIb3DQEBAQUABIIBAMaqb1zgsUBR2kxYbqaDSvtDWvyMb9mQmhD13aoT # HaQWvXO3OZLR772+rOY7fCcxi7T3LFjTUnN1lOYysKfroQN/EY9ROBrHdsx7hEXO # 4ZzZOS17++/AqZVAGK2skEiFqxmKKZqUG9NaOylA/zUXEeNw41eDVZwsD+Aevlwm # DfPAhVa5tBoOG960VlvQJnRxihk8vM4gDAzhXTH1P8kcKb7Kdtmo46gSC87wQTpQ # 6bA8LryTDz9N8iMa4RE0I+0kkEZWpE5q/kXDiHJ5OHuhhkZ7XwhJ+tYoGdSpjsJc # z16tOrwS52SCsJMQug9pVYZBRCXk9St9GADDigKxX+crI5+hghL+MIIS+gYKKwYB # BAGCNwMDATGCEuowghLmBgkqhkiG9w0BBwKgghLXMIIS0wIBAzEPMA0GCWCGSAFl # AwQCAQUAMIIBWQYLKoZIhvcNAQkQAQSgggFIBIIBRDCCAUACAQEGCisGAQQBhFkK # AwEwMTANBglghkgBZQMEAgEFAAQgZNxskKclOPo+aE6KIb8vsYnNHmlkO5JnzOBk # Hal3PvICBmAlqW1c4hgTMjAyMTAyMTUxMDAzMjEuMDk4WjAEgAIB9KCB2KSB1TCB # 0jELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMk # TWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1U # aGFsZXMgVFNTIEVTTjpGQzQxLTRCRDQtRDIyMDElMCMGA1UEAxMcTWljcm9zb2Z0 # IFRpbWUtU3RhbXAgU2VydmljZaCCDk0wggT5MIID4aADAgECAhMzAAABQCMZ1l7e # lSQxAAAAAAFAMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI # EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv # ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD # QSAyMDEwMB4XDTIwMTAxNTE3MjgyNloXDTIyMDExMjE3MjgyNlowgdIxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29m # dCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRT # UyBFU046RkM0MS00QkQ0LUQyMjAxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0 # YW1wIFNlcnZpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCufWsz # cerVL03TPxH5gqpm7bnKSTk6VPxOy7C10FbIMJEWgBKT18HqyIKiUWFcGHJ6Phzf # IjA3RTIlYE5MCMe144hiN8KnHnf2tuAEjn8FMe0L6pwFPt+0+SdO1Cfz2U05yk/v # R+5hVkuhCwOcuMbHG1b95V7BHlDQjWZZB8nLnE596WTk5aPPdhXgcq2rIhHMll39 # HNxjzDqqbOhI2xgh2+WJPZ55BlvJhN0lCxGjMgpMwsIlQF9WOjDZ8kwO3MMH1cQ5 # 1+E9bO9Q5p1iCqqHSWyUBHs1X3QUWZmBlYBGsbyPtmdWcLkw5c5L80jnxLjzJyy6 # DSk3Y0YsuTZhaPELAgMBAAGjggEbMIIBFzAdBgNVHQ4EFgQUNUMcLiZ3RiCOjNKq # dWz454QtDmcwHwYDVR0jBBgwFoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0f # BE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJv # ZHVjdHMvTWljVGltU3RhUENBXzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4w # TDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0 # cy9NaWNUaW1TdGFQQ0FfMjAxMC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNV # HSUEDDAKBggrBgEFBQcDCDANBgkqhkiG9w0BAQsFAAOCAQEAYwxSraBC4IL3Cvhi # EhJ8/Khto1hXc6/hjBaxJ8jP+PXFo31O8sAHYHE+LYK1FuBsFR/jyfTvJF5kifC7 # avy/Aug0bZO1jN7LTUNHKOOw2iIcX1S5EsXIpkKGQoLej2vQ7LbHRhiNSkPFUKFn # mrlwB/DzzjA/SJRxicooafx4nSfCmvvOv9OW74c6NcNP0LvnhpLgpQU2bwPuLC69 # ZbNI5WXtcxZ27zYGedOYHuzY5x/cjhp0bN2LFDlnHFrfM4C8rOtX7QdxVAhjdJAn # 0/OMNGXMK+IxOHEDwVQhEvcWdiq9yFaQShnjDxLsWwZY2VctZDt8cxveXiCO54fI # 7inq1TCCBnEwggRZoAMCAQICCmEJgSoAAAAAAAIwDQYJKoZIhvcNAQELBQAwgYgx # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1p # Y3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTEwMDcw # MTIxMzY1NVoXDTI1MDcwMTIxNDY1NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB # IDIwMTAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpHQ28dxGKOiDs # /BOX9fp/aZRrdFQQ1aUKAIKF++18aEssX8XD5WHCdrc+Zitb8BVTJwQxH0EbGpUd # zgkTjnxhMFmxMEQP8WCIhFRDDNdNuDgIs0Ldk6zWczBXJoKjRQ3Q6vVHgc2/JGAy # WGBG8lhHhjKEHnRhZ5FfgVSxz5NMksHEpl3RYRNuKMYa+YaAu99h/EbBJx0kZxJy # GiGKr0tkiVBisV39dx898Fd1rL2KQk1AUdEPnAY+Z3/1ZsADlkR+79BL/W7lmsqx # qPJ6Kgox8NpOBpG2iAg16HgcsOmZzTznL0S6p/TcZL2kAcEgCZN4zfy8wMlEXV4W # nAEFTyJNAgMBAAGjggHmMIIB4jAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU # 1WM6XIoxkPNDe3xGG8UzaFqFbVUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEw # CwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/o # olxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNy # b3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt # MjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5t # aWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5j # cnQwgaAGA1UdIAEB/wSBlTCBkjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIB # FjFodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQu # aHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8A # UwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAH5ohRDeLG # 4Jg/gXEDPZ2joSFvs+umzPUxvs8F4qn++ldtGTCzwsVmyWrf9efweL3HqJ4l4/m8 # 7WtUVwgrUYJEEvu5U4zM9GASinbMQEBBm9xcF/9c+V4XNZgkVkt070IQyK+/f8Z/ # 8jd9Wj8c8pl5SpFSAK84Dxf1L3mBZdmptWvkx872ynoAb0swRCQiPM/tA6WWj1kp # vLb9BOFwnzJKJ/1Vry/+tuWOM7tiX5rbV0Dp8c6ZZpCM/2pif93FSguRJuI57BlK # cWOdeyFtw5yjojz6f32WapB4pm3S4Zz5Hfw42JT0xqUKloakvZ4argRCg7i1gJsi # OCC1JeVk7Pf0v35jWSUPei45V3aicaoGig+JFrphpxHLmtgOR5qAxdDNp9DvfYPw # 4TtxCd9ddJgiCGHasFAeb73x4QDf5zEHpJM692VHeOj4qEir995yfmFrb3epgcun # Caw5u+zGy9iCtHLNHfS4hQEegPsbiSpUObJb2sgNVZl6h3M7COaYLeqN4DMuEin1 # wC9UJyH3yKxO2ii4sanblrKnQqLJzxlBTeCG+SqaoxFmMNO7dDJL32N79ZmKLxvH # Ia9Zta7cRDyXUHHXodLFVeNp3lfB0d4wwP3M5k37Db9dT+mdHhk4L7zPWAUu7w2g # UDXa7wknHNWzfjUeCLraNtvTX4/edIhJEqGCAtcwggJAAgEBMIIBAKGB2KSB1TCB # 0jELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMk # TWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1U # aGFsZXMgVFNTIEVTTjpGQzQxLTRCRDQtRDIyMDElMCMGA1UEAxMcTWljcm9zb2Z0 # IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUAQqXmHvITpjsyl+Yy # kRtDOQlyUVOggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAN # BgkqhkiG9w0BAQUFAAIFAOPUxMswIhgPMjAyMTAyMTUxODAwNDNaGA8yMDIxMDIx # NjE4MDA0M1owdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA49TEywIBADAKAgEAAgIj # awIB/zAHAgEAAgIR1zAKAgUA49YWSwIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgor # BgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUA # A4GBAAnBvwS6FV0HYJh0nXmBG339BekiNGOydnQ6n8FvolI0z0A0q8POGzi/tqEP # jod6w3XqZXzzMxuPtBrcAE5N38BC1NEjY4xQ6A0Z0a8MzjoCKyAPC45oOM949VSa # tu8j9OQ+Pkz922244gS59XzcpQivQZCfdJDcoWiKWvlna8DOMYIDDTCCAwkCAQEw # gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAFAIxnWXt6VJDEA # AAAAAUAwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B # CRABBDAvBgkqhkiG9w0BCQQxIgQgW5UDkkN6O8HAtG4mFF5zoVIyo3zzezP9KCHD # u3ZbcugwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCAvNrC16szSpFwk7/Ny # 8lPt2j/JynxFmxFJOqq2AgiXgzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwAhMzAAABQCMZ1l7elSQxAAAAAAFAMCIEIKk6tAM9WzjIXV3JBNF5 # Exte1ZkAemtAUtiQC+LIqE6EMA0GCSqGSIb3DQEBCwUABIIBAHrWxO4SHaB4fuNy # h5wzz34cSbTCz7fawD4OGzdaVbulC8/zBtp5plUvpWy8YwLr0AmvMV+bibqsQBBN # Qtq2XJcrcu8Hbhl8m/lxWbOekIygEdTrpDddrMPTQsK6iUF0YxZ2IkuvLlZTKxfN # huWkaZjHwmWxTPw7Sa92zt/zgGqXOOiX+pDwNyiulrmzDHCiJ4DESAazhxqV/R0v # JQu26pmikbttaJFggHqPQqhq2EMl49RIyiMA5dB72KUsrmvZH8ofAq0dPMBvCotd # za42nd4OpQnpSq3LL4kVFlyqoueSX4i/V0FLkn7EnwFFAmgfV5ts4Op/LGLCHFvU # vUjQcRU= # SIG # End signature block |