Framework/Core/SVT/ADO/ADO.VariableGroup.ps1
Set-StrictMode -Version Latest class VariableGroup: ADOSVTBase { hidden [PSObject] $VarGrp; hidden [PSObject] $ProjectId; hidden [PSObject] $VarGrpId; hidden [string] $checkInheritedPermissionsPerVarGrp = $false hidden [PSObject] $variableGroupIdentities = $null; VariableGroup([string] $organizationName, [SVTResource] $svtResource): Base($organizationName,$svtResource) { $this.ProjectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0]; $this.VarGrpId = $this.ResourceContext.ResourceDetails.id $apiURL = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ProjectId)/_apis/distributedtask/variablegroups/$($this.VarGrpId)?api-version=6.1-preview.2" $this.VarGrp = [WebRequestHelper]::InvokeGetWebRequest($apiURL); if ([Helpers]::CheckMember($this.ControlSettings, "VariableGroup.CheckForInheritedPermissions") -and $this.ControlSettings.VariableGroup.CheckForInheritedPermissions) { $this.checkInheritedPermissionsPerVarGrp = $true } } hidden [ControlResult] CheckPipelineAccess([ControlResult] $controlResult) { try { $controlResult.VerificationResult = [VerificationResult]::Failed $url = 'https://dev.azure.com/{0}/{1}/_apis/build/authorizedresources?type=variablegroup&id={2}&api-version=6.0-preview.1' -f $($this.OrganizationContext.OrganizationName),$($this.ProjectId) ,$($this.VarGrpId); $responseObj = @([WebRequestHelper]::InvokeGetWebRequest($url)); # # When var grp is shared across all pipelines - the below condition will be true. if([Helpers]::CheckMember($responseObj[0],"authorized") -and $responseObj[0].authorized -eq $true ) { $isSecretFound = $false $secretVarList = @(); # Check if variable group has any secret or linked to KV if ($this.VarGrp.Type -eq 'AzureKeyVault') { $isSecretFound = $true } else { Get-Member -InputObject $this.VarGrp.variables -MemberType Properties | ForEach-Object { #no need to check if isSecret val is true, as it will always be true if isSecret is present if([Helpers]::CheckMember($this.VarGrp.variables.$($_.Name),"isSecret")) { $isSecretFound = $true $secretVarList += $_.Name } } } if ($isSecretFound -eq $true) { $controlResult.AddMessage([VerificationResult]::Failed, "Variable group contains secrets accessible to all YAML pipelines."); $controlResult.AdditionalInfoInCSV = "SecretVarsList: $($secretVarList -join '; ')"; if ($this.ControlFixBackupRequired) { #Data object that will be required to fix the control $controlResult.BackupControlState = $isSecretFound; } } else { $controlResult.AddMessage([VerificationResult]::Passed, "Variable group does not contain secret."); $controlResult.AdditionalInfoInCSV += "NA" } } else { $controlResult.AddMessage([VerificationResult]::Passed, "Variable group is not accessible to all YAML pipelines."); $controlResult.AdditionalInfoInCSV += "NA" } } catch { $controlResult.AddMessage([VerificationResult]::Error,"Could not fetch authorization details of variable group."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckPipelineAccessAutomatedFix ([ControlResult] $controlResult) { try { # Backup data object is not required in this scenario. #$RawDataObjForControlFix = @(); #$RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject $this.PublishCustomMessage("Note: After changing the pipeline permission, YAML pipelines that need access on variable group needs to be granted permission explicitly.`n",[MessageType]::Warning); $body = "" if (-not $this.UndoFix) { if ($body.length -gt 1) {$body += ","} $body += @" { "resource": { "type": "variablegroup", "id": "$($this.VarGrpId)" }, "allPipelines": { "authorized": false, "authorizedBy":null, "authorizedOn":null }, "pipelines":[] } "@; } else { if ($body.length -gt 1) {$body += ","} $body += @" { "resource": { "type": "variablegroup", "id": "$($this.VarGrpId)" }, "allPipelines": { "authorized": true, "authorizedBy":null, "authorizedOn":null }, "pipelines":[] } "@; } $url = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/variablegroup/{2}?api-version=5.1-preview.1" -f $($this.OrganizationContext.OrganizationName),$($this.projectId),$($this.VarGrpId); $header = [WebRequestHelper]::GetAuthHeaderFromUriPatch($url) $webRequestResult = Invoke-RestMethod -Uri $url -Method Patch -ContentType "application/json" -Headers $header -Body $body $controlResult.AddMessage([VerificationResult]::Fixed, "Pipeline permissions for variable group have been changed."); } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not apply fix."); $controlResult.LogException($_) } return $controlResult; } hidden [ControlResult] CheckInheritedPermissions([ControlResult] $controlResult) { $url = 'https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.variablegroup/roleassignments/resources/{1}%24{2}?api-version=6.1-preview.1' -f $($this.OrganizationContext.OrganizationName),$($this.ProjectId) ,$($this.VarGrpId); try { $responseObj = [WebRequestHelper]::InvokeGetWebRequest($url); $inheritedRoles = $responseObj | Where-Object {$_.access -eq "inherited"} if(($inheritedRoles | Measure-Object).Count -gt 0) { $roles = @(); $roles += ($inheritedRoles | Select-Object -Property @{Name="Name"; Expression = {$_.identity.displayName}}, @{Name="Role"; Expression = {$_.role.displayName}}); $controlResult.AddMessage("Total number of inherited role assignments on variable group: ", ($roles | Measure-Object).Count); $controlResult.AddMessage([VerificationResult]::Failed,"Review the list of inherited role assignments on variable group: ", $roles); $controlResult.SetStateData("List of inherited role assignments on variable group: ", $roles); $controlResult.AdditionalInfo += "Total number of inherited role assignments on variable group: " + ($roles | Measure-Object).Count; } else { $controlResult.AddMessage([VerificationResult]::Passed,"No inherited role assignments found on variable group.") } } catch { $controlResult.AddMessage([VerificationResult]::Error,"Could not fetch permission details of variable group."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckRBACAccess([ControlResult] $controlResult) { <# { "ControlID": "ADO_VariableGroup_AuthZ_Grant_Min_RBAC_Access", "Description": "All teams/groups must be granted minimum required permissions on variable group.", "Id": "VariableGroup110", "ControlSeverity": "High", "Automated": "Yes", "MethodName": "CheckRBACAccess", "Rationale": "Granting minimum access by leveraging RBAC feature ensures that users are granted just enough permissions to perform their tasks. This minimizes exposure of the resources in case of user/service account compromise.", "Recommendation": "Refer: https://docs.microsoft.com/en-us/azure/devops/pipelines/library/?view=azure-devops#security", "Tags": [ "SDL", "TCP", "Automated", "AuthZ", "RBAC" ], "Enabled": true } #> $url = 'https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.variablegroup/roleassignments/resources/{1}%24{2}?api-version=6.1-preview.1' -f $($this.OrganizationContext.OrganizationName), $($this.ProjectId), $($this.VarGrpId); try { $responseObj = [WebRequestHelper]::InvokeGetWebRequest($url); if(($responseObj | Measure-Object).Count -gt 0) { $roles = @(); $roles += ($responseObj | Select-Object -Property @{Name="Name"; Expression = {$_.identity.displayName}}, @{Name="Role"; Expression = {$_.role.displayName}}, @{Name="AccessType"; Expression = {$_.access}}); $controlResult.AddMessage("Total number of role assignments on variable group: ", ($roles | Measure-Object).Count); $controlResult.AddMessage([VerificationResult]::Verify,"Review the list of role assignments on variable group: ", $roles); $controlResult.SetStateData("List of role assignments on variable group: ", $roles); $controlResult.AdditionalInfo += "Total number of role assignments on variable group: " + ($roles | Measure-Object).Count; } else { $controlResult.AddMessage([VerificationResult]::Passed,"No role assignments found on variable group.") } } catch { $controlResult.AddMessage([VerificationResult]::Error,"Could not fetch RBAC details of variable group."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckCredInVarGrp([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed if([Helpers]::CheckMember([ConfigurationManager]::GetAzSKSettings(),"SecretsScanToolFolder")) { $ToolFolderPath = [ConfigurationManager]::GetAzSKSettings().SecretsScanToolFolder $SecretsScanToolName = [ConfigurationManager]::GetAzSKSettings().SecretsScanToolName if((-not [string]::IsNullOrEmpty($ToolFolderPath)) -and (Test-Path $ToolFolderPath) -and (-not [string]::IsNullOrEmpty($SecretsScanToolName))) { $ToolPath = Get-ChildItem -Path $ToolFolderPath -File -Filter $SecretsScanToolName -Recurse if($ToolPath) { if($this.VarGrp) { try { $varGrpDefFileName = $($this.ResourceContext.ResourceName).Replace(" ","") $varGrpDefPath = [Constants]::AzSKTempFolderPath + "\VarGrps\"+ $varGrpDefFileName + "\"; if(-not (Test-Path -Path $varGrpDefPath)) { New-Item -ItemType Directory -Path $varGrpDefPath -Force | Out-Null } $this.VarGrp | ConvertTo-Json -Depth 5 | Out-File "$varGrpDefPath\$varGrpDefFileName.json" $searcherPath = Get-ChildItem -Path $($ToolPath.Directory.FullName) -Include "buildsearchers.xml" -Recurse ."$($Toolpath.FullName)" -I $varGrpDefPath -S "$($searcherPath.FullName)" -f csv -Ve 1 -O "$varGrpDefPath\Scan" $scanResultPath = Get-ChildItem -Path $varGrpDefPath -File -Include "*.csv" if($scanResultPath -and (Test-Path $scanResultPath.FullName)) { $credList = Get-Content -Path $scanResultPath.FullName | ConvertFrom-Csv if(($credList | Measure-Object).Count -gt 0) { $controlResult.AddMessage("No. of credentials found:" + ($credList | Measure-Object).Count ) $controlResult.AddMessage([VerificationResult]::Failed,"Found credentials in variables.") $controlResult.AdditionalInfo += "No. of credentials found in variables: " + ($credList | Measure-Object).Count; } else { $controlResult.AddMessage([VerificationResult]::Passed,"No credentials found in variables.") } } } catch { #Publish Exception $this.PublishException($_); $controlResult.LogException($_) } finally { #Clean temp folders Remove-ITem -Path $varGrpDefPath -Recurse } } } } } else { try { if([Helpers]::CheckMember($this.VarGrp[0],"variables")) { $varList = @(); $variablesWithCreds=@{}; $noOfCredFound = 0; $patterns = @($this.ControlSettings.Patterns | where-object {$_.RegexCode -eq "SecretsInVariables"} | Select-Object -Property RegexList); $exclusions = $this.ControlSettings.Build.ExcludeFromSecretsCheck; $exclusions += $this.ControlSettings.Release.ExcludeFromSecretsCheck; $exclusions = @($exclusions | select-object -unique) if($patterns.Count -gt 0) { #Compare all non-secret variables with regex Get-Member -InputObject $this.VarGrp[0].variables -MemberType Properties | ForEach-Object { if([Helpers]::CheckMember($this.VarGrp[0].variables.$($_.Name),"value") -and (-not [Helpers]::CheckMember($this.VarGrp[0].variables.$($_.Name),"isSecret"))) { $varName = $_.Name $varValue = $this.VarGrp[0].variables.$varName.value <# helper code to build a list of vars and counts if ([Build]::BuildVarNames.Keys -contains $buildVarName) { [Build]::BuildVarNames.$buildVarName++ } else { [Build]::BuildVarNames.$buildVarName = 1 } #> if ($exclusions -notcontains $varName) { for ($i = 0; $i -lt $patterns.RegexList.Count; $i++) { #Note: We are using '-cmatch' here. #When we compile the regex, we don't specify ignoreCase flag. #If regex is in text form, the match will be case-sensitive. if ($varValue -cmatch $patterns.RegexList[$i]) { $noOfCredFound +=1 $varList += $varName; #if auto fix is required save the variable value after encrypting it, will be needed during undofix if($this.ControlFixBackupRequired){ $variablesWithCreds[$varName] = ($varValue | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString) } break } } } } } if($noOfCredFound -gt 0) { $varList = @($varList | Select-Object -Unique) if($this.ControlFixBackupRequired){ $controlResult.BackupControlState = $variablesWithCreds } $controlResult.AddMessage([VerificationResult]::Failed, "Found secrets in variable group.`nList of variables: ", $varList ); $controlResult.SetStateData("List of variable name containing secret: ", $varList); $controlResult.AdditionalInfo += "Count of variable(s) containing secret: " + $varList.Count; } else { $controlResult.AddMessage([VerificationResult]::Passed, "No credentials found in variable group."); } $patterns = $null; } else { $controlResult.AddMessage([VerificationResult]::Error, "Regular expressions for detecting credentials in variable groups are not defined in your organization."); } } else { $controlResult.AddMessage([VerificationResult]::Passed, "No variables found in variable group."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the variable group definition."); $controlResult.AddMessage($_); $controlResult.LogException($_) } } return $controlResult; } hidden [ControlResult] CheckCredInVarGrpAutomatedFix([ControlResult] $controlResult){ try{ $RawDataObjForControlFix = @(); $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject $varList = @(); if (-not $this.UndoFix) { $RawDataObjForControlFix.PSObject.Properties | foreach { #The api does not allow updating individual variables inside a var grp, all variables have to be a part of the body or else they will be removed from the grp. #Hence using the global var grp object to store all variables details inside the post body and updating only the required variable. $this.VarGrp.variables.($_.Name) | Add-Member NoteProperty -name "isSecret" -value $true $varList+=$_.Name; } $controlResult.AddMessage([VerificationResult]::Fixed, "Following variables have been marked as secret: "); } else { $RawDataObjForControlFix.PSObject.Properties | foreach { #The api does not allow updating individual variables inside a var grp, all variables have to be a part of the body or else they will be removed from the grp. #Hence using the global var grp object to store all variables details inside the post body and updating only the required variable. $this.VarGrp.variables.($_.Name).isSecret = $false #We do not get variable value in API response, if we do not set the value, the variable becomes null, thus decrypting the value from backup state $secureVariableValue = $_.Value | ConvertTo-SecureString $this.VarGrp.variables.($_.Name).value = [Helpers]::ConvertToPlainText($secureVariableValue); $varList+=$_.Name; } $controlResult.AddMessage([VerificationResult]::Fixed, "Following variables have been removed as secret: "); } $rmContext = [ContextHelper]::GetCurrentContext(); $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f "", $rmContext.AccessToken))) $apiURL = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ProjectId)/_apis/distributedtask/variablegroups/$($this.VarGrpId)?api-version=6.1-preview.2" $body = @($this.VarGrp) | ConvertTo-JSON -depth 99; Invoke-RestMethod -Method Put -Uri $apiURL -Body $body -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) }; $display = ($varList | FT -AutoSize | Out-String -Width 512); $controlResult.AddMessage("`n$display"); } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not apply fix."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckBroaderGroupAccess ([ControlResult] $controlResult) { try { $controlResult.VerificationResult = [VerificationResult]::Failed $restrictedBroaderGroups = @{} $restrictedBroaderGroupsForVarGrp = $this.ControlSettings.VariableGroup.RestrictedBroaderGroupsForVariableGroup; if(@($restrictedBroaderGroupsForVarGrp.psobject.Properties).Count -gt 0){ $restrictedBroaderGroupsForVarGrp.psobject.properties | foreach { $restrictedBroaderGroups[$_.Name] = $_.Value } #Fetch variable group RBAC $roleAssignments = @(); if ($null -eq $this.variableGroupIdentities) { $url = 'https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.variablegroup/roleassignments/resources/{1}%24{2}?api-version=6.1-preview.1' -f $($this.OrganizationContext.OrganizationName), $($this.ProjectId), $($this.VarGrpId); $this.variableGroupIdentities = @([WebRequestHelper]::InvokeGetWebRequest($url)); } if($this.variableGroupIdentities.Count -gt 0) { if ($this.checkInheritedPermissionsPerVarGrp -eq $false) { $roleAssignments = @($this.variableGroupIdentities | where-object { $_.access -ne "inherited" }) } $roleAssignments = @($roleAssignments | Select-Object -Property @{Name="Name"; Expression = {$_.identity.displayName}},@{Name="Id"; Expression = {$_.identity.id}}, @{Name="Role"; Expression = {$_.role.displayName}}); } # Checking whether the broader groups have User/Admin permissions $backupDataObject = @($roleAssignments | Where-Object { ($restrictedBroaderGroups.keys -contains $_.Name.split('\')[-1]) -and ($_.Role -in $restrictedBroaderGroups[$_.Name.split('\')[-1]])}) $restrictedGroups = @($backupDataObject | Select-Object Name,role) if ($this.ControlSettings.CheckForBroadGroupMemberCount -and $restrictedGroups.Count -gt 0) { $broaderGroupsWithExcessiveMembers = @([ControlHelper]::FilterBroadGroupMembers($restrictedGroups, $true)) $restrictedGroups = @($restrictedGroups | Where-Object {$broaderGroupsWithExcessiveMembers -contains $_.Name}) } $restrictedGroupsCount = $restrictedGroups.Count # fail the control if restricted group found on variable group if ($restrictedGroupsCount -gt 0) { $controlResult.AddMessage([VerificationResult]::Failed, "`nCount of broader groups that have excessive permissions on variable group: $($restrictedGroupsCount)"); $controlResult.AddMessage("`nList of groups: ") $controlResult.AddMessage(($restrictedGroups | FT Name,Role -AutoSize | Out-String -Width 512)); $controlResult.SetStateData("List of groups: ", $restrictedGroups) $controlResult.AdditionalInfo += "Count of broader groups that have excessive permissions on variable group: $($restrictedGroupsCount)"; if ($this.ControlFixBackupRequired) { #Data object that will be required to fix the control $controlResult.BackupControlState = $backupDataObject; } $formatedRestrictedGroups = $restrictedGroups | ForEach-Object { $_.Name + ': ' + $_.Role } $controlResult.AdditionalInfoInCSV = ($formatedRestrictedGroups -join '; ' ) } else { $controlResult.AddMessage([VerificationResult]::Passed, "No broader groups have excessive permissions on variable group."); $controlResult.AdditionalInfoInCSV += "NA" } $displayObj = $restrictedBroaderGroups.Keys | Select-Object @{Name = "Broader Group"; Expression = {$_}}, @{Name = "Excessive Permissions"; Expression = {$restrictedBroaderGroups[$_] -join ', '}} $controlResult.AddMessage("Note:`nThe following groups are considered 'broad' and should not have excessive permissions: `n$( $displayObj| FT | out-string)"); } else{ $controlResult.AddMessage([VerificationResult]::Error, "List of restricted broader groups and restricted roles for variable group is not defined in the control settings for your organization policy."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the variable group permissions."); $controlResult.LogException($_) } return $controlResult; } hidden [ControlResult] CheckBroaderGroupAccessAutomatedFix ([ControlResult] $controlResult) { try { $RawDataObjForControlFix = @(); $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject $body = "[" if (-not $this.UndoFix) { foreach ($identity in $RawDataObjForControlFix) { if ($body.length -gt 1) {$body += ","} $body += @" { "userId": "$($identity.id)", "roleName": "Reader" } "@; } $RawDataObjForControlFix | Add-Member -NotePropertyName NewRole -NotePropertyValue "Reader" $RawDataObjForControlFix = @($RawDataObjForControlFix | Select-Object @{Name="DisplayName"; Expression={$_.Name}}, @{Name="OldRole"; Expression={$_.Role}},@{Name="NewRole"; Expression={$_.NewRole}}) } else { foreach ($identity in $RawDataObjForControlFix) { if ($body.length -gt 1) {$body += ","} $body += @" { "userId": "$($identity.id)", "roleName": "$($identity.role)" } "@; } $RawDataObjForControlFix | Add-Member -NotePropertyName OldRole -NotePropertyValue "Reader" $RawDataObjForControlFix = @($RawDataObjForControlFix | Select-Object @{Name="DisplayName"; Expression={$_.Name}}, @{Name="OldRole"; Expression={$_.OldRole}},@{Name="NewRole"; Expression={$_.Role}}) } $body += "]" #Put request $url = "https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.variablegroup/roleassignments/resources/{1}%24{2}?api-version=6.1-preview.1" -f $($this.OrganizationContext.OrganizationName),$($this.ProjectId),$($this.VarGrpId); $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $webRequestResult = Invoke-RestMethod -Uri $url -Method Put -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) } -Body $body $controlResult.AddMessage([VerificationResult]::Fixed, "Permission for broader groups have been changed as below: "); $display = ($RawDataObjForControlFix | FT -AutoSize | Out-String -Width 512) $controlResult.AddMessage("`n$display"); } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not apply fix."); $controlResult.LogException($_) } return $controlResult; } hidden [ControlResult] CheckBroaderGroupAccessForVarGrpWithSecrets([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed; try { $restrictedBroaderGroups = @{} $restrictedBroaderGroupsForVarGrp = $this.ControlSettings.VariableGroup.RestrictedBroaderGroupsForVariableGroup; $restrictedBroaderGroupsForVarGrp.psobject.properties | foreach { $restrictedBroaderGroups[$_.Name] = $_.Value } if([Helpers]::CheckMember($this.VarGrp[0],"variables")) { $secretVarList = @(); $VGMembers = @(Get-Member -InputObject $this.VarGrp[0].variables -MemberType Properties) $patterns = @($this.ControlSettings.Patterns | Where-Object {$_.RegexCode -eq "SecretsInVariables"} | Select-Object -Property RegexList); $VGMembers | ForEach-Object { $varName = $_.Name if([Helpers]::CheckMember($this.VarGrp[0].variables.$varName,"value")) { $varValue = $this.VarGrp[0].variables.$varName.value for ($i = 0; $i -lt $patterns.RegexList.Count; $i++) { #Note: We are using '-cmatch' here. #When we compile the regex, we don't specify ignoreCase flag. #If regex is in text form, the match will be case-sensitive. if ($varValue -cmatch $patterns.RegexList[$i]) { $secretVarList += $varName break } } } elseif (([Helpers]::CheckMember($this.VarGrp[0].variables.$($_.Name),"isSecret"))) { $secretVarList += $varName } } if ($secretVarList.Count -gt 0) { #Fetch variable group RBAC $roleAssignments = @(); $url = 'https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.variablegroup/roleassignments/resources/{1}%24{2}?api-version=6.1-preview.1' -f $($this.OrganizationContext.OrganizationName), $($this.ProjectId), $($this.VarGrpId); $responseObj = @([WebRequestHelper]::InvokeGetWebRequest($url)); if($responseObj.Count -gt 0) { if ($this.checkInheritedPermissionsPerVarGrp -eq $false) { $responseObj = $responseObj | where-object { $_.access -ne "inherited" } } $roleAssignments += ($responseObj | Select-Object -Property @{Name="Name"; Expression = {$_.identity.displayName}}, @{Name="Role"; Expression = {$_.role.displayName}}, @{Name="Id"; Expression = {$_.identity.id}}); } # Checking whether the broader groups have User/Admin permissions $restrictedGroups = @($roleAssignments | Where-Object { ($restrictedBroaderGroups.keys -contains $_.Name.split('\')[-1]) -and ($_.Role -in $restrictedBroaderGroups[$_.Name.split('\')[-1]])}) if ($this.ControlSettings.CheckForBroadGroupMemberCount -and $restrictedGroups.Count -gt 0) { $broaderGroupsWithExcessiveMembers = @([ControlHelper]::FilterBroadGroupMembers($restrictedGroups, $true)) $restrictedGroups = @($restrictedGroups | Where-Object {$broaderGroupsWithExcessiveMembers -contains $_.Name}) } $restrictedGroupsCount = $restrictedGroups.Count # fail the control if restricted group found on variable group which contains secrets if ($restrictedGroupsCount -gt 0) { $controlResult.AddMessage([VerificationResult]::Failed, "Broader groups have excessive permissions on the variable group."); $controlResult.AddMessage("`nCount of broader groups that have excessive permissions on the variable group: $($restrictedGroupsCount)") $controlResult.AdditionalInfo += "Count of broader groups that have excessive permissions on the variable group: $($restrictedGroupsCount)"; $controlResult.AddMessage("`nList of broader groups: ",$($restrictedGroups | FT | Out-String)) $controlResult.AddMessage("`nList of variables with secret: ",$secretVarList) $controlResult.SetStateData("List of broader groups: ", $restrictedGroups) if ($this.ControlFixBackupRequired) { #Data object that will be required to fix the control $controlResult.BackupControlState = $restrictedGroups; } $groups = $restrictedGroups | ForEach-Object { $_.Name + ': ' + $_.Role } $controlResult.AdditionalInfoInCSV = $($groups -join '; ')+"; SecretVarsList: $($secretVarList -join '; ')"; } else { $controlResult.AddMessage([VerificationResult]::Passed, "No broader groups have excessive permissions on the variable group."); $controlResult.AdditionalInfoInCSV += "NA" } $displayObj = $restrictedBroaderGroups.Keys | Select-Object @{Name = "Broader Group"; Expression = {$_}}, @{Name = "Excessive Permissions"; Expression = {$restrictedBroaderGroups[$_] -join ', '}} $controlResult.AddMessage("`nNote:`nThe following groups are considered 'broad' and should not have excessive permissions: `n$( $displayObj| FT | out-string -Width 512)"); } else { $controlResult.AddMessage([VerificationResult]::Passed, "No secrets found in variable group."); $controlResult.AdditionalInfoInCSV += "NA" } } else { $controlResult.AddMessage([VerificationResult]::Passed, "No variables found in variable group."); $controlResult.AdditionalInfoInCSV += "NA" } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the variable group permissions."); $controlResult.LogException($_) } return $controlResult; } hidden [ControlResult] CheckBroaderGroupAccessForVarGrpWithSecretsAutomatedFix ([ControlResult] $controlResult) { try { $RawDataObjForControlFix = @(); $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject $body = "[" if (-not $this.UndoFix) { foreach ($identity in $RawDataObjForControlFix) { if ($body.length -gt 1) {$body += ","} $body += @" { "userId": "$($identity.id)", "roleName": "Reader" } "@; } $RawDataObjForControlFix | Add-Member -NotePropertyName NewRole -NotePropertyValue "Reader" $RawDataObjForControlFix = @($RawDataObjForControlFix | Select-Object @{Name="DisplayName"; Expression={$_.Name}}, @{Name="OldRole"; Expression={$_.Role}},@{Name="NewRole"; Expression={$_.NewRole}}) } else { foreach ($identity in $RawDataObjForControlFix) { if ($body.length -gt 1) {$body += ","} $body += @" { "userId": "$($identity.id)", "roleName": "$($identity.role)" } "@; } $RawDataObjForControlFix | Add-Member -NotePropertyName OldRole -NotePropertyValue "Reader" $RawDataObjForControlFix = @($RawDataObjForControlFix | Select-Object @{Name="DisplayName"; Expression={$_.Name}}, @{Name="OldRole"; Expression={$_.OldRole}},@{Name="NewRole"; Expression={$_.Role}}) } $body += "]" #Put request $url = "https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.variablegroup/roleassignments/resources/{1}%24{2}?api-version=6.1-preview.1" -f $($this.OrganizationContext.OrganizationName),$($this.ProjectId),$($this.VarGrpId); $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $webRequestResult = Invoke-RestMethod -Uri $url -Method Put -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) } -Body $body $controlResult.AddMessage([VerificationResult]::Fixed, "Permission for broader groups have been changed as below: "); $display = ($RawDataObjForControlFix | FT -AutoSize | Out-String -Width 512) $controlResult.AddMessage("`n$display"); } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not apply fix."); $controlResult.LogException($_) } return $controlResult; } } # SIG # Begin signature block # MIInvAYJKoZIhvcNAQcCoIInrTCCJ6kCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAenUiOdUfNmyLg # 7yj3+4aGdUdNVgiJ11z6hLA6ujoN5qCCDYEwggX/MIID56ADAgECAhMzAAACUosz # qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I # sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O # L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA # v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o # RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8 # q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw # UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 # ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu # ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w # Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx # MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3 # uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp # kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7 # l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u # TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1 # o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti # yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z # 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf # 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK # WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW # esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F # 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS # 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/BvW1taslScxMNelDNMYIZkTCCGY0CAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN # BglghkgBZQMEAgEFAKCBsDAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgcDxu5Wi0 # nClCS9DXBOcq4ykrgBHMCLLpnna1+LsIMuAwRAYKKwYBBAGCNwIBDDE2MDSgFIAS # AE0AaQBjAHIAbwBzAG8AZgB0oRyAGmh0dHBzOi8vd3d3Lm1pY3Jvc29mdC5jb20g # MA0GCSqGSIb3DQEBAQUABIIBABjmlGcMcyn6qSGBBsOZdzjUGt++OhYEk1fnrW4U # oWt8Kbvx4RNtCU2NyGAdRQrRLSDLqkI4c9cy/OPpSOF9tDA6sX/7x69Tlko9qlN6 # 8meC3oc3wC2K7MHCSvN5jPLY77IQCpbcqWyTmX2eAUNT/K28VecjIqpHZh4YkSX4 # 72xse0t3a3FiA0VztDCdzbSFV5s7oknuFCXXQZc1Yn65nlvszmojBYsH13b0R38W # O+++gkh07ZSTXI6Y/Sa9GID5aZc1qVxWyuSrAnRu6PntYBFGXQv3VOSTz8uC6xcf # fbZsdJN4zFwNsRzXUFLYL6EGohI5duCGs/pJTI+gj1L+Y5GhghcZMIIXFQYKKwYB # BAGCNwMDATGCFwUwghcBBgkqhkiG9w0BBwKgghbyMIIW7gIBAzEPMA0GCWCGSAFl # AwQCAQUAMIIBWQYLKoZIhvcNAQkQAQSgggFIBIIBRDCCAUACAQEGCisGAQQBhFkK # AwEwMTANBglghkgBZQMEAgEFAAQgGVBnajhiSjj63WFy0ONI6VBqAlPT1YU5shX7 # pgZg4eYCBmGVXmDPzRgTMjAyMTEyMTQxMTU2NDEuODQ4WjAEgAIB9KCB2KSB1TCB # 0jELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMk # TWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1U # aGFsZXMgVFNTIEVTTjo4RDQxLTRCRjctQjNCNzElMCMGA1UEAxMcTWljcm9zb2Z0 # IFRpbWUtU3RhbXAgU2VydmljZaCCEWgwggcUMIIE/KADAgECAhMzAAABiC7NxoFB # 4bwqAAEAAAGIMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI # EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv # ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD # QSAyMDEwMB4XDTIxMTAyODE5Mjc0MFoXDTIzMDEyNjE5Mjc0MFowgdIxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29m # dCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRT # UyBFU046OEQ0MS00QkY3LUIzQjcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0 # YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa5xAI # BCaRxcfIOtXhLzxV4mDZcao0pxamytqlEoVZyGGMux/8z1c31uDOcs3jwFc8C06X # Ch50HaQ3htl08/cp1E1tirW00VSHxSeaMIKv4KMuWuKAdyZLRH6uw3aAExeUsRmH # Zb8I64P1U4uxvY/aMOnjfdXitQABRbzYzuuDzV3c5xy077VdbWHcS1tC1LpASTDo # Ngi699fsDDyNcdmewy6A/xkDWi2mulM1SH/NFYLsInIHPKZAgNIJ1aFV8PiyHF75 # GzrVrF/bttODkf9X9KQ132HMzo2r/LY6MMqsu2432FLnfnr26FM1B4CEBUN94ekT # OUy+1c7JfoxOZ7eOcd0c+PoYtP0AxEisB/3qE9g6I8QG8e2uDoymIjf6Xo2VtI6z # Xr8VN6WNPX6x2xYa0VNm95r2kCpXVoHv3loOSZnqxGbmO12dVrN+hasd3e8N6Hfl # ZXTy9bhOU58RxXb4ptqKs/FoWQnj62Wwn4x+xU6JOv9mcOBoxoefPOiB6UjcCh8N # T0hNsyRO1PGss/KBNtF21um2ucvMGfaPNHhMl+RCj6HNa5oy7k60xmIpXYjkw7Sb # WYq5QCCir7jjYvDwJC6P0QLYXydNslvY1xQOD7vh2AmKz8/wFr86uXFb5OuBzpM8 # bEI61Pvf1Sp6yW9YPqs1DpQQ71/u9YOSF3a+2wIDAQABo4IBNjCCATIwHQYDVR0O # BBYEFGR5tVDEo7vOu736jbsaM+WMyUpKMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl # 0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0 # LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAy # MDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93 # d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1T # dGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAww # CgYIKwYBBQUHAwgwDQYJKoZIhvcNAQELBQADggIBAEAQWtv7WAgmy/8YotLbNq+b # Z6TXvuaTjK7oG5LpWIi4vR7bRg3Z11d6JSER2GTcVG2j8YP3eTlIjI0npf6ny5Aw # 7Ejbdg5J3ITMsnCHv5+27Qh/zLfHoAnRLV3XY5nt+xiqWMdR5xyd5L0NaqKkeTy4 # zybZlsGFGdQ3wziKqDiugkaZkpn0VzxntkcmAz3uLt8jID2EkfTXvPblasMmXFqk # Pl2YzI3LPN8BWpoHJ6YKgGfhWREIY0hLHTFGVxv3dboQ2EkXU0GMyXdwpUQdbh3x # jQ1mGl1cO14uT0eBsnJ4IjZ830YGsJLUHVqT7X3g8aJkovz6C0rs2isCgAxC8WRi # CsetYJh+NXo+i4Lc34DrA4GtyRU4dP09QgMrkAMIfhmtpCJ15L0sP+KYoczcjiJr # M+ShwdwUcH3Kjl32Uwln6mcABaCVBCMxaFSqcT+WUD4SqNs7SUDGWZS1WKhVSzCF # PekroOMVFcz8tTHBO225/PXMGMQuREhny4LLViQzF8EXASiz9AUiUNoVK9SfgiJZ # kDdUt8ASPLnWInAraNIgfD7VuMIj4UEdwJNEfak/f6HkOVDkBn929x82sBM/XDDP # bkivwqAo5sdEIhgfhUjZWuY5uhIcUbv0lsd2Q9VKN8vFO5OyiHkXOhTW3m6sbSvC # 6WhlkVnFOSvF/JOSG+aMMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJmQAAAAAA # FTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0 # aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1WjB8MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy # b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEBBQADggIP # ADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9s # SuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3 # po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJprx2rrPY2 # vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/dvI2k45GP # sjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka97aSueik3 # rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKRHh09/SDP # c31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9ituqBJR6L8F # A6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q # 6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItboKaDIV1f # MHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6bMURHXLv # jflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6tAgMBAAGj # ggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+ # ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIw # XAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMG # A1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsG # A1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJc # YmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9z # b2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz # LmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWlj # cm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0 # MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5H # ZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2 # HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVtI1TkeFN1 # JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis9/kpicO8 # F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTpkbKpW99J # o3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0sHrYUP4K # WN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138eW0QBjloZ # kWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJsWkBRH58 # oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w # /ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+ # 7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQtB1VM1iz # oXBm8qGCAtcwggJAAgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl # cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo4RDQxLTRC # RjctQjNCNzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIj # CgEBMAcGBSsOAwIaAxUA4TyKzHwgF5U9LB4PzTmXlB16DkKggYMwgYCkfjB8MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy # b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOViy4gw # IhgPMjAyMTEyMTQxNTUxMzZaGA8yMDIxMTIxNTE1NTEzNlowdzA9BgorBgEEAYRZ # CgQBMS8wLTAKAgUA5WLLiAIBADAKAgEAAgIOMgIB/zAHAgEAAgISqDAKAgUA5WQd # CAIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6Eg # oQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBABDB/jBSwhLtl4rO0zYWOn26 # zGmM97z7REv0xizasMadFZhB2yyV+5nCrvXLYHrFZH3W3JG+UUGm8dBmr6J8YiHu # mBjutQcoRN8U2S2TGckb2xisCvun+rhTwzKY5/t9rOSD2Rxg9G/V7O0crcWLUDLa # 4VZbM3JDWsJzlnYHK5GTMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3Rh # bXAgUENBIDIwMTACEzMAAAGILs3GgUHhvCoAAQAAAYgwDQYJYIZIAWUDBAIBBQCg # ggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQg # zNuhYVBR7pyovAKUXubqU7Nma7UOXbY3A1oSIVnKTx4wgfoGCyqGSIb3DQEJEAIv # MYHqMIHnMIHkMIG9BCBm6d7trAY3RoSC+M/snI7c0qXuGy1fwKGGsqZe0klApTCB # mDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD # VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAk # BgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABiC7NxoFB # 4bwqAAEAAAGIMCIEIE5h4EVwpqU5BYwseMOcXEa7hC0mpTohv4+JBHumVdnUMA0G # CSqGSIb3DQEBCwUABIICAErAZ6/JCTqLV7BZtIZEPWVWTl96lIlT08+cozKz3BWy # J8sWU5cn62jccVN6RXQ/dVPQtBGKX6Llr4cMOABDWMP5yQ192THv+FKYGPNy90YG # f6juSddX7Zf5BxsWHsIsT487hks1aCPJoZDrUm2j4xLXPqvuuYDe6V8E5FluUo75 # ZAuSiigeZL95cm4I1FMRt5MC3c1jfQr9jUXqwpkfNovzByAq3d7xDefOHGWRWhMq # SgwFxKve2LGpfbYOTNI3Lk7CxPy7H7sxsGREm3dVlxMxoIlyh7NBva6M8zOsjn8P # jWOEtdH3wL6+y3kmxPRAqciVbsXSgw1al1mzWgKIVSdwLqbwdIBt46K1lWA0HS/D # b+nHKbDJ2BlN/3TAl5tK8FlcwbbTT9sOrRK0TcEzW9TGw09QZ4Vsw1384Mdfsktr # dxVaVqbTbEK3Yw93fibM17AQo3WM5eErcN1NOfWdPvpQ6XgYpdQilFEsyYoPzqze # 1CWwJRN2VeyXChNixgx7e7cJVE7USh0xAJRqtGz8EVW0Gp8TJSEaRnhUIP5goVo4 # NYsw5av8h+PYM2Q5SH9VMvg6JPG72v4oEII+Exm+deEdrmPUike2RQbmL+lLyV/S # rGbJwmaF5W6WFH8grq+2bH+GXdkK6ldIwXrtcQVUPNU3c7v17igO6p2uzs0DZSZh # SIG # End signature block |