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 '; ')"; $controlResult.AdditionalInfo += "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) { try { 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)); } $inheritedRoles = $this.variableGroupIdentities | 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 } #> try { 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) { $roles = @(); $roles += ($this.variableGroupIdentities | 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; $controlResult.AdditionalInfoInCSV += "List of variable name containing secret:" + $varList ; } 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 = @(); 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="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; } hidden [ControlResult] CheckBranchControlOnVariableGroup ([ControlResult] $controlResult) { try{ #check if resources is accessible even to a single pipeline $isRsrcAccessibleToAnyPipeline = $false; $apiURL = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/variablegroup/{2}?api-version=6.1-preview.1" -f $($this.OrganizationContext.OrganizationName),$($this.ProjectId),$($this.VarGrpId) $pipelinePermission = [WebRequestHelper]::InvokeGetWebRequest($apiURL); if([Helpers]::CheckMember($pipelinePermission,"allPipelines") -and $pipelinePermission.allPipelines.authorized){ $isRsrcAccessibleToAnyPipeline = $true; } if([Helpers]::CheckMember($pipelinePermission[0],"pipelines") -and $pipelinePermission[0].pipelines.Count -gt 0){ $isRsrcAccessibleToAnyPipeline = $true; } #if resource is not accessible to any YAML pipeline, there is no need to add any branch control, hence passing the control if($isRsrcAccessibleToAnyPipeline -eq $false){ $controlResult.AddMessage([VerificationResult]::Passed, "Variable group is not accessible to any YAML pipelines. Hence, branch control is not required."); return $controlResult; } $url = "https://dev.azure.com/{0}/{1}/_apis/pipelines/checks/queryconfigurations?`$expand=settings&api-version=6.1-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName; #using ps invoke web request instead of helper method, as post body (json array) not supported in helper method $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $body = "[{'name': '$($this.ResourceContext.ResourceDetails.Name)','id': '$($this.ResourceContext.ResourceDetails.Id)','type': 'variablegroup'}]" $response = @(Invoke-RestMethod -Uri $url -Method Post -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Body $body) if([Helpers]::CheckMember($response, "count") -and $response[0].count -eq 0){ $controlResult.AddMessage([VerificationResult]::Failed, "No approvals and checks have been defined for the variable group."); $controlResult.AdditionalInfo = "No approvals and checks have been defined for the variable group." $controlResult.AdditionalInfoInCsv = "No approvals and checks have been defined for the variable group." } else{ #we need to check only for two kinds of approvals and checks: manual approvals and branch controls, hence filtering these two out from the list $branchControl = @() $approvalControl = @() try{ $approvalAndChecks = @($response.value | Where-Object {$_.PSObject.Properties.Name -contains "settings"}) $branchControl = @($approvalAndChecks.settings | Where-Object {$_.PSObject.Properties.Name -contains "displayName" -and $_.displayName -eq "Branch Control"}) $approvalControl = @($approvalAndChecks | Where-Object {$_.PSObject.Properties.Name -contains "type" -and $_.type.name -eq "Approval"}) } catch{ $branchControl = @() } #if branch control is not enabled, but manual approvers are added pass this control if($branchControl.Count -eq 0){ if($approvalControl.Count -gt 0){ $controlResult.AddMessage([VerificationResult]::Passed, "Branch control has not been defined for the variable group. However, manual approvals have been added to the variable group."); $approvers = $approvalControl.settings.approvers | Select @{n='Approver name';e={$_.displayName}},@{n='Approver id';e = {$_.uniqueName}} $formattedApproversTable = ($approvers| FT -AutoSize | Out-String -width 512) $controlResult.AddMessage("`nList of approvers : `n$formattedApproversTable"); $controlResult.AdditionalInfo += "List of approvers on variable group $($approvers)."; } else{ $controlResult.AddMessage([VerificationResult]::Failed, "Branch control has not been defined for the variable group."); $controlResult.AdditionalInfo = "Branch control has not been defined for the variable group." } } else{ $branches = ($branchControl.inputs.allowedBranches).Split(","); $branchesWithNoProtectionCheck = @($branchControl.inputs | where-object {$_.ensureProtectionOfBranch -eq $false}) if("*" -in $branches){ $controlResult.AddMessage([VerificationResult]::Failed, "All branches have been given access to the variable group."); $controlResult.AdditionalInfo = "All branches have been given access to the variable group." } elseif ($branchesWithNoProtectionCheck.Count -gt 0) { #check if branch protection is enabled on all the found branches depending upon the org policy if($this.ControlSettings.VariableGroup.CheckForBranchProtection){ $controlResult.AddMessage([VerificationResult]::Failed, "Access to the variable group has not been granted to all branches. However, verification of branch protection has not been enabled for some branches."); $branchesWithNoProtectionCheck = @(($branchesWithNoProtectionCheck.allowedBranches).Split(",")); $controlResult.AddMessage("List of branches granted access to the variable group without verification of branch protection: ") $controlResult.AddMessage("$($branchesWithNoProtectionCheck | FT | Out-String)") $branchesWithProtection = @($branches | where {$branchesWithNoProtectionCheck -notcontains $_}) if($branchesWithProtection.Count -gt 0){ $controlResult.AddMessage("List of branches granted access to the variable group with verification of branch protection: "); $controlResult.AddMessage("$($branchesWithProtection | FT | Out-String)"); } $controlResult.AdditionalInfo = "List of branches granted access to the variable group without verification of branch protection: $($branchesWithNoProtectionCheck)" } else{ $controlResult.AddMessage([VerificationResult]::Passed, "Access to the variable group has not been granted to all branches."); $controlResult.AddMessage("List of branches granted access to the variable group: "); $controlResult.AddMessage("$($branches | FT | Out-String)"); } } else{ $controlResult.AddMessage([VerificationResult]::Passed, "Access to the variable group has not been granted to all branches. Verification of branch protection has been enabled for all allowed branches."); $controlResult.AddMessage("List of branches granted access to the variable group: "); $controlResult.AddMessage("$($branches | FT | Out-String)"); } } } } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch variable group details."); } return $controlResult; } } # SIG # Begin signature block # MIInoAYJKoZIhvcNAQcCoIInkTCCJ40CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCGmEfYffgyXjMY # C58EgP0qsAqfttMEtWzOC7vMsVdBOqCCDYEwggX/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/BvW1taslScxMNelDNMYIZdTCCGXECAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN # BglghkgBZQMEAgEFAKCBsDAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgPRuR+nFC # fF6ynuKHd2kbflrRD0oieO7u0ZO8iT+wxE8wRAYKKwYBBAGCNwIBDDE2MDSgFIAS # AE0AaQBjAHIAbwBzAG8AZgB0oRyAGmh0dHBzOi8vd3d3Lm1pY3Jvc29mdC5jb20g # MA0GCSqGSIb3DQEBAQUABIIBAIQ427GND2Xd+chD17BQ+r8PNhcaqgIt5dimfcmI # LfnzPQ6hGDpty3vvbCwkz+UmZ6hiwSOR5ThekEaj56eYTW2lUfDdU/xf6V4hk3mh # HT/RTosE5eY8SZ1e1VXYWK//WuGBg1VpOJoWGwlbAqOEsGsqQRtfr5KH+nR3ic+W # O3i3Zm0DpsmM2ef6Dih2r3Mq6RUv6JcK4yYjAZn33F6Tdrq8UgPeOVACz1Nve+nm # EpXevmqm8mcKC1AOAldJVuQSE4ZYe8sbVgo5ISXlfztoqfE+ejs/UNN9/1c8EOsF # G8GptvDySy7EvJ2BLEfIUEuUW7mWLyMNFNrGOZYut9/dS/qhghb9MIIW+QYKKwYB # BAGCNwMDATGCFukwghblBgkqhkiG9w0BBwKgghbWMIIW0gIBAzEPMA0GCWCGSAFl # AwQCAQUAMIIBUQYLKoZIhvcNAQkQAQSgggFABIIBPDCCATgCAQEGCisGAQQBhFkK # AwEwMTANBglghkgBZQMEAgEFAAQg53VYbuZHNurjaPx9pGxm4kmDrR0MKjx97ta2 # jLYjtewCBmH62gckahgTMjAyMjAyMTUwNzE2MzUuODIyWjAEgAIB9KCB0KSBzTCB # yjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMc # TWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRT # UyBFU046NDlCQy1FMzdBLTIzM0MxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0 # YW1wIFNlcnZpY2WgghFUMIIHDDCCBPSgAwIBAgITMwAAAZcDz1mca4l4PwABAAAB # lzANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAe # Fw0yMTEyMDIxOTA1MTRaFw0yMzAyMjgxOTA1MTRaMIHKMQswCQYDVQQGEwJVUzET # MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj # YSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo0OUJDLUUzN0Et # MjMzQzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCCAiIw # DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO0ASupKQU3z8J79yysdVZy/WJKM # 1MCs8oQyoo9ROlglfxMFmXzws2unDhsSk+KY74S1yWLlEGhkSJ6j5LNhSdvT6coR # ZoRiC5uCn4dMVmIPzkuV3uaTZeD3UowMTbIx44gfYMelOyfmnQt/QIOV88Tkc7Ck # /n++xTE14NYxbrOxK4pTr1ovs5zKTfpUzIIqMc0wvrtWZkwkE7ttfW9hVKE69Cpl # STEKkJHezObEdPT2zqeHAt40LPucydTs8SI0ZXFJi75XQROmkWkrtMdwZgAxrdJh # mNDEbIM5zsnbSQS53q3PkCtJHMbjuqxwN89iq/X5qR7HzXDf3kT7WRzi66R+fQJ4 # q0AO6bs+pGttEwPvDIWdfYW/JrK0aPS5oq4xcUmxn7B92TRGy495Ye1XPgxEITB9 # ivVz4lOSZLef+m8ev9vznd2jbwug8d7OTd1LFueJCiNbcFNgkuatR6L+fgEcrmZN # Pw27EbrOg/e3wdWaEJb/+LawXDFUc+zJDqx2vGz+Fqmw9Hmy2LYhMb8eB7hJ4ftK # d73jY4d4D9Puw5IlcCGHH1XJSIRRRrH50ohXsa7ruuOrlJWvlU1Lht246kuxYSN4 # Yekx6L//fF3x3FnjYb8QOSvn4vtQXEi4ECr6vx2I/8PzJH927u9zhEYrDWmnGmjg # kf0ydh937NQO5SBxAgMBAAGjggE2MIIBMjAdBgNVHQ4EFgQUbc8BzyjrMG6WVQvR # sb7dfr0VAGAwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYDVR0f # BFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwv # TWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwGCCsG # AQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAx # MCgxKS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkq # hkiG9w0BAQsFAAOCAgEABEjYYdIk7TMeeyFy34eC7h2w9TRvTJSwNcGEB0AZ0fVP # 64N6rdzcenu/kLhCjBISqnS394OUUeEf6GS+bxCVVlHyEh/6Elnc6q0oanJk+Dsc # 3NggbNFLZ8qKVQ5uFtgrpqN6rspGD6QaBnXoq7w3XdedwMLCZCDIJv/LMSSmyAXk # /NrZ61J4DZjaPLu5dbhNIbDAKtW4r0Ot30CJ6/lamCb2E9Fv9D1u6QN6oeKHDY4l # +mfHfZI8fC+7gTyPx7MYnwo//JhUb6VQWDsqj+2OXYuWQJ40w0hzGTVBTx7fp4RV # 1HB41z0URwjKqiYOA+t1+m9FdEfO0Pd4qcBiFwTMzjEDSLSTkXpB7R5S4t24Oi56 # Y7NGgqOf0ZRwozZEg4PsVe6mHmt+y/zikzY6ph96TQGtwbz/6w0IhhGL4AG1RxCE # M+1jFkmLFnlDxWSN+pgo4FGOled/ApELQ8DPAQ4gHMvqrjvHqcpIj9B99sqsA4fO # dgXlptXrRfj5MP7fFzt0PnYhbuxoIqo3Xpo+FX6UbJtrUzfR5wHsK629F8GPEBNr # adIUXTdm9FIksTJgeITciil1WgyzhQnYi57H6Q9K4zh/I2xAmTm2lli5/XhLw6/k # DUD70uK+Oy/QGvC69R6+O1cCeRNoAhJ72MCEQ86SYTEYtCoo2DeN8k+l0hkeDfYw # ggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUA # MIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQD # EylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0y # MTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0 # ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveV # U3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1hlDcwUTI # cVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62AW36M # EBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHI # NSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCGMFxP # LOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF803RKJ1v2l # IH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQcNIIP8BDy # t0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahhaYQFzymei # XtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1 # GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV2xo3xwgV # GD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIGCSsGAQQB # gjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTu # MB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsG # AQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYBBQUH # AwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud # EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQwVgYD # VR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwv # cHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEB # BE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9j # ZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcNAQELBQAD # ggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/ # 2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AFvono # aeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRW # qveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn8Atq # gcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7 # hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyxTkct # wRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu # +yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFmPWn9y8FB # SX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC4822rpM+Zv/ # Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ # 8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYICyzCCAjQCAQEw # gfihgdCkgc0wgcoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # JTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsT # HVRoYWxlcyBUU1MgRVNOOjQ5QkMtRTM3QS0yMzNDMSUwIwYDVQQDExxNaWNyb3Nv # ZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQBhQNKwjZhJNM1N # dg2DRjFNwdZj0aCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo # aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y # cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw # MA0GCSqGSIb3DQEBBQUAAgUA5bUp1TAiGA8yMDIyMDIxNTAzMTk0OVoYDzIwMjIw # MjE2MDMxOTQ5WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDltSnVAgEAMAcCAQAC # AgLlMAcCAQACAhFNMAoCBQDltntVAgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisG # AQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcNAQEFBQAD # gYEAF9jeP3q2yjamcMYzS6KDOqkVoFTmgEBbe9P4CtI9aZz9kJ/YW837faksdjfZ # kU+QRpeAGz2zd3nuMB0+pvvHhGmbKo1s421toI5WvuXlKRuuZ6RBPLi082I+blFW # YG454wws1D9UFegxGL996x8VmFcy/x1Hv57cue6g5OxQ95sxggQNMIIECQIBATCB # kzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAZcDz1mca4l4PwAB # AAABlzANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJ # EAEEMC8GCSqGSIb3DQEJBDEiBCC2nQIk4Or0caocgly1TCbnghlVj64RwFt/F4+X # eXZtWzCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIFt72hsQlXo4/gQMxUnz # MXx0Pm8cZKgsPC5DGP0GeKa/MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAg # UENBIDIwMTACEzMAAAGXA89ZnGuJeD8AAQAAAZcwIgQgUMlXe+ip8fG26laJUMPN # AVZMZ7UIn19XnJLrdwA7hI4wDQYJKoZIhvcNAQELBQAEggIAMheT1/TN5jyPW0ZT # NI8K4HHOeEg8f2/pTPWji7l1sHgUey8rdkDmjSHVdLese3MDbBTAIbz7g5xf3DNn # rzi296IkQdigAvQS454MLpDwOe43WQyPPznUIgLBiaxKEg1EfqgMoMI9FahYS8uF # 833Mu0SpHL2EnrdVFmnTBAsKILpSzAYrhnKxYlUoPMBvn19rxGReF+Zj8lt5jrey # c3Uk90kksuihPXVDjT1strR71OL1FvuUYYwKpU+X2mSjFrF8JssEOQYWlJAC+kVd # yMriL8aCCZlp4znM+Q26ybgVdd446K2DD/zQm5/ZYS3ZRE0QaIKINagF+eb71e5T # DgPltxutL+X+8/SNwC9Yqy2r13NEhJs6X2YpmHQRxhlEDuK3rH5ovEXrO0sUFj2t # WVtkQYM6okMv5VD3DJc+I2xLaLDrYgaOxjSFXX6U9aRj1QnCJVgi4N90/HfZa15A # 1v+2zEAgJ4uUgjAx1JOi92ol0gDTXlhlNOfWY15nh+BjzAWRQ3GctwSN68pkkrgK # v0QFoazXd89fidH/qGr3zlxRzNlxxcHIxBROXhlkB6Zt8iYzIkD6QBoMj0iMolcm # n9g+0Nf3mEiu5H1u1v4WPGgSmuwxGyBWgNlEfuFm3jdvWemjU5KGhOh0saN0GnY4 # oOrK28L4pPVK0riwOB3A5LQHOh8= # SIG # End signature block |