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 # MIInuwYJKoZIhvcNAQcCoIInrDCCJ6gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # 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/BvW1taslScxMNelDNMYIZkDCCGYwCAQEwgZUwfjELMAkG # 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/qhghcYMIIXFAYKKwYB # BAGCNwMDATGCFwQwghcABgkqhkiG9w0BBwKgghbxMIIW7QIBAzEPMA0GCWCGSAFl # AwQCAQUAMIIBWAYLKoZIhvcNAQkQAQSgggFHBIIBQzCCAT8CAQEGCisGAQQBhFkK # AwEwMTANBglghkgBZQMEAgEFAAQg53VYbuZHNurjaPx9pGxm4kmDrR0MKjx97ta2 # jLYjtewCBmH9VmkTShgSMjAyMjAyMTUxMDIxMDEuMThaMASAAgH0oIHYpIHVMIHS # MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk # bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRN # aWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRo # YWxlcyBUU1MgRVNOOkQwODItNEJGRC1FRUJBMSUwIwYDVQQDExxNaWNyb3NvZnQg # VGltZS1TdGFtcCBTZXJ2aWNloIIRaDCCBxQwggT8oAMCAQICEzMAAAGP81Go61py # 3cwAAQAAAY8wDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB # IDIwMTAwHhcNMjExMDI4MTkyNzQ2WhcNMjMwMTI2MTkyNzQ2WjCB0jELMAkGA1UE # BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc # BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0 # IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNT # IEVTTjpEMDgyLTRCRkQtRUVCQTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3Rh # bXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJlXPv61 # zxcehZOPgqS67mw6y02t0LgB0VV7svs7MV8JKIJE9Gvl1rgWm8B8qo/EUYmUEL3b # 2zquTURMTnh4mgrZFLENNhEgHvQs/paATbbHwqvOOrt6LVhwiZapLw60q+5jAasH # EWO3H4QBny75aTEqI6AJ5O0Xo/o3CZ2MSHjd+Bd4fScADWN+tKBmAiEu6SzqLFlf # m8boPbok2WBP13JcmDRel3c2f8w/+FOacU+DGUJswRrw7PvHA3QP7LWX4/68votF # 1GDRT4bqnPlCpMJv1zRwfgg7BkJfmUtBnG1FLp+FT04RyZupkQMC+cvM6bVhCKHG # 03kUk5mZ1GtomB9hDgVe3go8dEnW+pC3NcXRUXJA3kBkeCdchcsm7mbFD/MdHTrB # BKC0LjobipQy0BIOw+mcZmSZ0vAdN3sJP0qVS6rG+ulNqsheAcA7dbmZIxGe34py # KFIEs+Ae31i2CHjtjgmMSBNF78LFaKfT70102bRj885h1O+dxmqysrjOqGv6mk82 # L6wH1G+ymIb1UCsRlD5C/fniojOxtKnpyQha182T8EVqHHAEd9z4TRLrs8ymRSeA # 3mkwi4P/LitEOEIxUXn+Z+B/tikCBIm2e8yHgV944LKyAm880ptEF90kVZmR//wK # qfGMZMHKCNVggYs7/OM/XqsEQXUOB2HDW0DDAgMBAAGjggE2MIIBMjAdBgNVHQ4E # FgQU8wbmdGuuSc7ioc6Fm9uX+zcjcbwwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXS # ZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIw # MTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0 # YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAK # BggrBgEFBQcDCDANBgkqhkiG9w0BAQsFAAOCAgEAzX/TqPc8oQuZ9YwvIlOzSWN/ # RYs44cWcCWyWP2LcJ+t6ZTJU0mgaXq2p+eun7kaIxiUr9xMGbPka7jlNk/2UQ8eF # R3rCx7XJRPBpjDNakzGmTy/CNha0Zn+TqKeBqnMTXTRAgQpVWZp9CsxXTzKkWuf9 # EegpoKiYdJrryJopIB7m76IbGrzxMsh0GveBw+PyzSnf2CGgiij8/UgOXwGuKYUO # BL89mrmPUlJbiHeTCvR+XI1lcAcQr2AA/tQlvc+xrISZTY6gb1rSjuidAHpn4Os9 # vIO6nOxv7Qra5R+P8tu8vrLbsFlzb8HbEndZbweDOtwLjJfWKemrv1xZJxsyTxep # /7lkeiYUL84eNCC4Hk4S5mIX/M/mg2+K9jgSxtT9gemgk1tmEJLe06VJ8yNHChq9 # tdwmyrRpPzjiB0rAzsWrJnhifhYlCQoldecId2mU/1U/z5C/ROIQwQMBrePRvPIE # gjtMfY33Q2VnVhmxC15UpgNxD+Hk2Ku0a6JWNOBvHxrRdKc7mbuNwNvc2iPZSK+b # pSkc/BKEB1OnLtD8VMNAfR/HAJL0MWjLpkWf+Hwm6jW+E3D5D3FjiNuEGJb6W7U/ # ad9X5WBJZnOcIxqZQJMv55CXE9B2RDV3p8VrT77eIHKKqaXiTwN0v9Q+eyi9+uu3 # qSK9MldvdBNqLWWwFvAwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAV # MA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRo # b3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJ # BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv # c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A # MIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK # 4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLem # jkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+ # NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+y # OSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTes # y+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9z # fUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUD # o9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDq # hFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8w # dJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N # +VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOC # Ad0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5k # xJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBc # BgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYD # VR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYD # VR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxi # aNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3Nv # ZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMu # Y3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNy # b3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQw # DQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+Tkdk # eLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYe # eNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3Uk # V7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wX # sFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mj # dAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY # 3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmR # aw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyh # YWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+ # 57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7t # fqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOh # cGbyoYIC1zCCAkACAQEwggEAoYHYpIHVMIHSMQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVy # YXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkQwODItNEJG # RC1FRUJBMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMK # AQEwBwYFKw4DAhoDFQA+TS+CBHbnSAcHRqAmldFgW0GaaqCBgzCBgKR+MHwxCzAJ # BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv # c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBBQUAAgUA5bWsEDAi # GA8yMDIyMDIxNTEyMzUyOFoYDzIwMjIwMjE2MTIzNTI4WjB3MD0GCisGAQQBhFkK # BAExLzAtMAoCBQDltawQAgEAMAoCAQACAgofAgH/MAcCAQACAhFJMAoCBQDltv2Q # AgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSCh # CjAIAgEAAgMBhqAwDQYJKoZIhvcNAQEFBQADgYEAFuxFeQpg3ANQPQx1Bl6iXpOG # pCYEPOVeFCc1hR2z8NPYE2IOZ5oyzTFwR0/JAK+3K2uZeeeAqEkjij68A0ldL9ru # tIcFpJDqtE58GhEMG/pXxgzAwRnm9hdlUcf1UkFBiqZkGjPeETSxtHtSmE13NlU2 # x5m+vBWllXkyDF2IR2IxggQNMIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFt # cCBQQ0EgMjAxMAITMwAAAY/zUajrWnLdzAABAAABjzANBglghkgBZQMEAgEFAKCC # AUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCBP # dd8KYXzgmKMvGPxdz5aDSVPCNnsyk+xduFEJRF4fPDCB+gYLKoZIhvcNAQkQAi8x # geowgecwgeQwgb0EIJdyBU/ixsYLlY4ipttiLZjunRQ1lvJ0obujuoPMhccfMIGY # MIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV # BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG # A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAGP81Go61py # 3cwAAQAAAY8wIgQgRWi8ag7eEy3CT8WrhVesQhzlEyfViw1LieElUaD89dIwDQYJ # KoZIhvcNAQELBQAEggIACEdhzL1oVsXAxeJDf+dOG4cqb/rr7uhSUOiLrLgh9M7S # SMYye/qpJkuv2I5tO1KxEvZRisLAuHSlqOJ5dx2q9r3WechilbaxrJlaa4ymN93T # s7+ci4WCnvkodHdWPXsFGtFYscIcC0fvLQL1ydWTr03wc+dhHT3gEamaPcsCZYf8 # QUCi7cR4KVVLIucqHfQlhrarSlYgqlbA2m6vzM5M+aOaKjImhnkYrHbuQCFZARoA # wMh9rJ8ogNt11YoaC5VFcwYii5YZltXny19sfnCJkljFLMtzQ+pbUeM6Abxzvbw9 # nAE3TJftWpkmKtm1Ta+tqr9aNMZ+i12ehtevb5qzoLGi5pmpvRPlXcjj2cym5lsZ # VVZmp3QIy56apSeBKNKg+VqrkNW3dxf+WQGoihJCi7AB3aH8DGufXIsbS657zUNc # uoCRKfCIanpTkOx7uB0j3zMjgDyVMoOYTm30NxVtHoGdZtnMSl5Mvq60zuIVjGaw # O5gkKs9vUXl6nlDfq86G9FrMXg0WOEynimgn38zi+3S9rVUGXoor3H2SsLDbwc/4 # tyPzvs87aThAfO2UnKwXhA8t4Kfb5CGRPaa+IuL6bz8K6tLM3x8XxNKpIxMXljQD # Uz/R6LrqxNDflxMnylY+ISIqLKApKcAnCPFv7vO2nXq66xvfRUmz5qsprOaPUH0= # SIG # End signature block |