Framework/Core/SVT/ADO/ADO.CommonSVTControls.ps1
Set-StrictMode -Version Latest class CommonSVTControls: ADOSVTBase { hidden [PSObject] $Repos; # This is used for fetching repo details #hidden [PSObject] $ProjectId; hidden [string] $checkInheritedPermissionsSecureFile = $false hidden [string] $checkInheritedPermissionsEnvironment = $false hidden [string] $checkInheritedPermissionsRepo = $false hidden [string] $checkInheritedPermissionsFeed = $false hidden [object] $repoInheritePermissions = @{}; hidden [PSObject] $excessivePermissionBitsForRepo = @(1) hidden [PSObject] $excessivePermissionsForRepoBranch = $null; hidden [string] $repoPermissionSetId = "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87"; CommonSVTControls([string] $organizationName, [SVTResource] $svtResource): Base($organizationName, $svtResource) { if ([Helpers]::CheckMember($this.ControlSettings, "SecureFile.CheckForInheritedPermissions") -and $this.ControlSettings.SecureFile.CheckForInheritedPermissions) { $this.checkInheritedPermissionsSecureFile = $true } if ([Helpers]::CheckMember($this.ControlSettings, "Environment.CheckForInheritedPermissions") -and $this.ControlSettings.Environment.CheckForInheritedPermissions) { $this.checkInheritedPermissionsEnvironment = $true } if ([Helpers]::CheckMember($this.ControlSettings, "Repo.CheckForInheritedPermissions") -and $this.ControlSettings.Build.CheckForInheritedPermissions) { #allow permission bit for inherited permission is '3' $this.checkInheritedPermissionsRepo = $true $this.excessivePermissionBitsForRepo = @(1,3) } if ([Helpers]::CheckMember($this.ControlSettings.Feed, "CheckForInheritedPermissions") -and $this.ControlSettings.Feed.CheckForInheritedPermissions) { $this.checkInheritedPermissionsFeed = $true } $this.excessivePermissionsForRepoBranch = $this.ControlSettings.Repo.ExcessivePermissionsForBranch } hidden [ControlResult] CheckInactiveRepo([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed try { $repoDefnsObj = $this.ResourceContext.ResourceDetails; $threshold = $this.ControlSettings.Repo.RepoHistoryPeriodInDays $currentDate = Get-Date # check if repo is disabled or not if ($repoDefnsObj.isDisabled) { $controlResult.AddMessage([VerificationResult]::Failed, "Repositories does not have any commits in last $($threshold) days. "); } else { # check if repo has commits in past RepoHistoryPeriodInDays days $thresholdDate = $currentDate.AddDays(-$threshold); $url = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ResourceContext.ResourceGroupName)/_apis/git/repositories/$($repoDefnsObj.id)/commits?searchCriteria.fromDate=$($thresholdDate)&&api-version=6.0" try { $repoCommitHistoryObj = @(); $repoCommitHistoryObj += @([WebRequestHelper]::InvokeGetWebRequest($url)) # When there are no commits, CheckMember in the below condition returns false when checknull flag [third param in CheckMember] is not specified (default value is $true). Assiging it $false. if (([Helpers]::CheckMember($repoCommitHistoryObj[0], "count", $false)) -and ($repoCommitHistoryObj[0].count -eq 0)) { $controlResult.AddMessage([VerificationResult]::Failed, "Repositories does not have any commits in last $($threshold) days. "); } else { $controlResult.AddMessage([VerificationResult]::Passed, "Repositories is in active state."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the history of repository [$($repoDefnsObj.name)]."); $controlResult.LogException($_) } } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch details of repository.", $_); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckRepositoryPipelinePermission([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed try { $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] $url = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/repository/{2}.{3}" -f $this.OrganizationContext.OrganizationName, $projectId, $projectId, $this.ResourceContext.ResourceDetails.Id; $repoPipelinePermissionObj = @([WebRequestHelper]::InvokeGetWebRequest($url)); if (([Helpers]::CheckMember($repoPipelinePermissionObj[0], "allPipelines")) -and ($repoPipelinePermissionObj[0].allPipelines.authorized -eq $true)) { $controlResult.AddMessage([VerificationResult]::Failed, "Repository is accessible to all YAML pipelines."); } else { $controlResult.AddMessage([VerificationResult]::Passed, "Repository is not accessible to all YAML pipelines."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch repository pipeline permission."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckBuildServiceAccessOnBranch([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed try { $isControlPassing = $true $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName); $inputbody = '{"contributionIds":["ms.vss-admin-web.security-view-members-data-provider"],"dataProviderContext":{"properties":{"permissionSetId": "","permissionSetToken":"","sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"repositories","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.sourcePage.routeValues.Project = $this.ResourceContext.ResourceGroupName; $inputbody.dataProviderContext.properties.permissionSetId = $this.repoPermissionSetId #first check permissions for all branches $inputbody.dataProviderContext.properties.permissionSetToken = "repoV2/$($projectId)/$($this.ResourceContext.ResourceDetails.id)/refs^heads/" #calling the common method to get permissions object on this level $autoFixStateData = @(); $excessivePermissionsListOnAllBranches = $this.CheckPermsOnBranch($url,$inputBody,$projectId,$null) if ($null -ne $excessivePermissionsListOnAllBranches -and $excessivePermissionsListOnAllBranches.count -gt 0) { $isControlPassing = $false $controlResult.AddMessage([VerificationResult]::Failed, "Build service groups have excessive permissions on 'All Branches' level of the repository."); $formattedGroupsData = $excessivePermissionsListOnAllBranches | Select @{l = 'Group'; e = { $_.Group} }, @{l = 'ExcessivePermissions'; e = { $_.ExcessivePermissions } } $formattedBuildServiceGrpTable = ($formattedGroupsData | Out-String -Width 512) $controlResult.AddMessage("`nList of groups : `n$formattedBuildServiceGrpTable"); $groups = $formattedGroupsData | ForEach-Object { $_.Group + ': ' + $_.ExcessivePermissions } $groups = $groups -join ' ; ' $controlResult.AdditionalInfo += "List of Build service groups with excessive permission on 'All Branches' level: $($groups); "; $controlResult.AdditionalInfoInCSV+= "'All Branches' level excessive permissions: $($groups); " if ($this.ControlFixBackupRequired) { $autoFixStateData +=$excessivePermissionsListOnAllBranches; #Data object that will be required to fix the control $controlResult.BackupControlState = $autoFixStateData; } } else { $controlResult.AddMessage("Build service groups do not have excessive permissions on 'All Branches' level of the repository."); } #get all eligible branches from settings and check for each of them $individualBranches = $this.ControlSettings.Repo.BranchesToCheckForExcessivePermissions $individualBranches | foreach { $inputbody.dataProviderContext.properties.permissionSetToken = "repoV2/$($projectId)/$($this.ResourceContext.ResourceDetails.id)/refs^heads^$($_)/" $excessivePermissionsListOnBranch = $this.CheckPermsOnBranch($url,$inputBody,$projectId,$_) if ($null -ne $excessivePermissionsListOnBranch -and $excessivePermissionsListOnBranch.count -gt 0) { if($isControlPassing){ $isControlPassing = $false $controlResult.AddMessage([VerificationResult]::Failed, "Build service groups have excessive permissions on '$($_)' branch of the repository."); } else{ $controlResult.AddMessage("Build service groups have excessive permissions on '$($_)' branch of the repository."); } $formattedGroupsData = $excessivePermissionsListOnBranch | Select @{l = 'Group'; e = { $_.Group} }, @{l = 'ExcessivePermissions'; e = { $_.ExcessivePermissions } } $formattedBuildServiceGrpTable = ($formattedGroupsData | Out-String -Width 512) $controlResult.AddMessage("`nList of groups : `n$formattedBuildServiceGrpTable"); $groups = $formattedGroupsData | ForEach-Object { $_.Group + ': ' + $_.ExcessivePermissions } $groups = $groups -join ' ; ' $controlResult.AdditionalInfo += "List of Build service groups with excessive permission on $($_) branch: $($groups); "; $controlResult.AdditionalInfoInCSV+= "$_ branch excessive permissions: $($groups); " if ($this.ControlFixBackupRequired) { $autoFixStateData +=$excessivePermissionsListOnBranch; #Data object that will be required to fix the control $controlResult.BackupControlState = $autoFixStateData; } } else { $controlResult.AddMessage("Build service groups do not have excessive permissions on '$($_)' branch of the repository."); } } #only when all the branches are passing, this controls will be passed if($isControlPassing){ $controlResult.AddMessage([VerificationResult]::Passed, "Build service groups do not have excessive permissions on either 'all branch' level or individual branches in the repository"); $controlResult.AdditionalInfoInCSV = 'NA' } $controlResult.AddMessage("`nFollowing permissions are considered 'excessive':`n$($this.excessivePermissionsForRepoBranch | FT | Out-String)"); } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch repository permissions."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckBuildServiceAccessOnBranchAutomatedFix([ControlResult] $controlResult) { try{ $RawDataObjForControlFix = @(); $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject $url = "https://dev.azure.com/{0}/_apis/AccessControlEntries/{1}?api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $this.repoPermissionSetId if (-not $this.UndoFix) { foreach ($identity in $RawDataObjForControlFix) { $excessivePermissions = $identity.ExcessivePermissions -split ";" $descriptor = $identity.Descriptor foreach ($excessivePermission in $excessivePermissions) { if ($excessivePermission.trim() -eq 'Force push (rewrite history, delete branches and tags)') { $roleId = [int][RepoPermissions] 'Forcepush' } elseif ($excessivePermission.trim() -eq "Remove others' locks") { $roleId = [int][RepoPermissions] 'Removeotherslocks' } else { $roleId = [int][RepoPermissions] $excessivePermission.Replace(" ","").trim(); } #need to invoke a post request which does not accept all permissions added in the body at once #hence need to call invoke seperately for each permission $body = "{ 'token': '$($identity.PermissionSetToken)', 'merge': true, 'accessControlEntries' : [{ 'descriptor' : '$descriptor', 'allow':0, 'deny':$($roleId) }] }" | ConvertFrom-Json $result = [WebRequestHelper]:: InvokePostWebRequest($url,$body) } $identity | Add-Member -NotePropertyName OldPermission -NotePropertyValue "Allow" $identity | Add-Member -NotePropertyName NewPermission -NotePropertyValue "Deny" } } else { foreach ($identity in $RawDataObjForControlFix) { $descriptor = $identity.Descriptor $excessivePermissions = $identity.ExcessivePermissions -split ";" foreach ($excessivePermission in $excessivePermissions) { if ($excessivePermission.trim() -eq 'Force push (rewrite history, delete branches and tags)') { $roleId = [int][RepoPermissions] 'Forcepush' } elseif ($excessivePermission.trim() -eq "Remove others' locks") { $roleId = [int][RepoPermissions] 'Removeotherslocks' } else { $roleId = [int][RepoPermissions] $excessivePermission.Replace(" ","").trim(); } $body = "{ 'token': '$($identity.PermissionSetToken)', 'merge': true, 'accessControlEntries' : [{ 'descriptor' : '$descriptor', 'allow':$($roleId), 'deny':0 }] }" | ConvertFrom-Json [WebRequestHelper]:: InvokePostWebRequest($url,$body) } $identity | Add-Member -NotePropertyName OldPermission -NotePropertyValue "Deny" $identity | Add-Member -NotePropertyName NewPermission -NotePropertyValue "Allow" } } $controlResult.AddMessage([VerificationResult]::Fixed, "Permissions for build service accounts on branches have been changed as below: "); $formattedGroupsData = $RawDataObjForControlFix | Select @{l = 'Group'; e = { $_.Group } }, @{l = 'ExcessivePermissions'; e = { $_.ExcessivePermissions }}, @{l = 'OldPermission'; e = { $_.OldPermission }}, @{l = 'NewPermission'; e = { $_.NewPermission } } $display = ($formattedGroupsData | FT -AutoSize | Out-String -Width 512) $controlResult.AddMessage("`n$display"); } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not apply fix."); $controlResult.LogException($_) } return $controlResult } #common method to get excessive permisions for every group for a branch hidden [Object] CheckPermsOnBranch($url,$inputBody,$projectId,$branch){ $responseObj = [WebRequestHelper]::InvokePostWebRequest($url, $inputbody); if([Helpers]::CheckMember($responseObj[0],"dataProviders") -and ($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider') -and ([Helpers]::CheckMember($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider',"identities"))) { $broaderGroupsList = @($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider'.identities | Where-Object { $_.subjectKind -eq 'group' -and $_.displayName -like "*Project Collection Build Service Accounts" }) $broaderGroupsList+=@($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider'.identities | Where-Object { $_.subjectKind -eq 'user' -and ($_.displayName -match "Project Collection Build Service ($($this.OrganizationContext.OrganizationName))" -or $_.displayName -like "*Build Service ($($this.OrganizationContext.OrganizationName))" )}) if ($broaderGroupsList.Count) { $groupsWithExcessivePermissionsList = @() foreach ($broaderGroup in $broaderGroupsList) { $broaderGroupInputbody = "{'contributionIds':['ms.vss-admin-web.security-view-permissions-data-provider'],'dataProviderContext':{'properties':{'subjectDescriptor':'','permissionSetId':'','permissionSetToken':'','accountName':'','sourcePage':{'routeId':'ms.vss-admin-web.project-admin-hub-route','routeValues':{'project':'PrivateProjectWithRepo','adminPivot':'repositories','controller':'ContributedPage','action':'Execute'}}}}}" | ConvertFrom-Json $broaderGroupInputbody.dataProviderContext.properties.sourcePage.routeValues.Project = $this.ResourceContext.ResourceGroupName; $broaderGroupInputbody.dataProviderContext.properties.permissionSetId = $this.repoPermissionSetId if($null -eq $branch){ $broaderGroupInputbody.dataProviderContext.properties.permissionSetToken = "repoV2/$($projectId)/$($this.ResourceContext.ResourceDetails.id)/refs^heads/" } else { $broaderGroupInputbody.dataProviderContext.properties.permissionSetToken = "repoV2/$($projectId)/$($this.ResourceContext.ResourceDetails.id)/refs^heads^$($branch)/" } $broaderGroupInputbody.dataProviderContext.properties.subjectDescriptor = $broaderGroup.descriptor $broaderGroupResponseObj = @([WebRequestHelper]::InvokePostWebRequest($url, $broaderGroupInputbody)); $broaderGroupRBACObj = @($broaderGroupResponseObj[0].dataProviders.'ms.vss-admin-web.security-view-permissions-data-provider'.subjectPermissions) $excessivePermissionList = $broaderGroupRBACObj | Where-Object { $_.displayName -in $this.excessivePermissionsForRepoBranch } $excessivePermissionsPerGroup = @() $excessivePermissionList | ForEach-Object { if ([Helpers]::CheckMember($_, "effectivePermissionValue")) { if ($this.excessivePermissionBitsForRepo -contains $_.effectivePermissionValue) { $excessivePermissionsPerGroup += $_ } } } if ($excessivePermissionsPerGroup.Count -gt 0) { $groupFoundWithExcessivePermissions = $true # For PCBSA, resolve the group and check if PBS, PCBS are part of it if ($broaderGroup.displayName -like '*Project Collection Build Service Accounts') { $groupFoundWithExcessivePermissions = $false $url="https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.1-preview" -f $($this.OrganizationContext.OrganizationName); $postbody="{'contributionIds':['ms.vss-admin-web.org-admin-members-data-provider'],'dataProviderContext':{'properties':{'subjectDescriptor':'','sourcePage':{'url':'','routeId':'ms.vss-admin-web.collection-admin-hub-route','routeValues':{'adminPivot':'groups','controller':'ContributedPage','action':'Execute'}}}}}" | ConvertFrom-Json $postbody.dataProviderContext.properties.subjectDescriptor = $broaderGroup.descriptor $bodyUrl = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/_settings/groups?subjectDescriptor=$($broaderGroup.descriptor)" $postbody.dataProviderContext.properties.sourcePage.url = $bodyUrl try { $response = [WebRequestHelper]::InvokePostWebRequest($url, $postbody) if([Helpers]::CheckMember($response.dataProviders.'ms.vss-admin-web.org-admin-members-data-provider', "identities")) { $buildServiceAccountIdentities = $response.dataProviders.'ms.vss-admin-web.org-admin-members-data-provider'.identities foreach ($eachIdentity in $buildServiceAccountIdentities) { if ($eachIdentity.displayName -like "*Project Collection Build Service ($($this.OrganizationContext.OrganizationName))" -or $eachIdentity.displayName -like "*Build Service ($($this.OrganizationContext.OrganizationName))") { $groupFoundWithExcessivePermissions = $true } } } } catch {} } if ($groupFoundWithExcessivePermissions -eq $true) { $excessivePermissionsGroupObj = @{} $excessivePermissionsGroupObj['Group'] = $broaderGroup.displayName $excessivePermissionsGroupObj['ExcessivePermissions'] = $($excessivePermissionsPerGroup.displayName -join '; ') $excessivePermissionsGroupObj['Descriptor'] = $broaderGroupResponseObj[0].dataProviders.'ms.vss-admin-web.security-view-permissions-data-provider'.identityDescriptor $excessivePermissionsGroupObj['PermissionSetToken'] = $excessivePermissionsPerGroup[0].token $groupsWithExcessivePermissionsList += $excessivePermissionsGroupObj } } } return $groupsWithExcessivePermissionsList; } else{ return $null; } } return $null; } hidden [ControlResult] CheckRepoRBACAccess([ControlResult] $controlResult) { #Control is dissabled mow <# { "ControlID": "ADO_Repository_AuthZ_Grant_Min_RBAC_Access", "Description": "All teams/groups must be granted minimum required permissions on repositories.", "Id": "Repository110", "ControlSeverity": "High", "Automated": "Yes", "MethodName": "CheckRepoRBACAccess", "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": "Go to Project Settings --> Repositories --> Permissions --> Validate whether each user/group is granted minimum required access to repositories.", "Tags": [ "SDL", "TCP", "Automated", "AuthZ", "RBAC" ], "Enabled": true }, #> $accessList = @() #permissionSetId = '2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87' is the std. namespaceID. Refer: https://docs.microsoft.com/en-us/azure/devops/organizations/security/manage-tokens-namespaces?view=azure-devops#namespaces-and-their-ids try{ $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName); $refererUrl = "https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ResourceContext.ResourceGroupName)/_settings/repositories?_a=permissions"; $inputbody = '{"contributionIds":["ms.vss-admin-web.security-view-members-data-provider"],"dataProviderContext":{"properties":{"permissionSetId": "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87","permissionSetToken":"","sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"repositories","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.sourcePage.url = $refererUrl $inputbody.dataProviderContext.properties.sourcePage.routeValues.Project = $this.ResourceContext.ResourceGroupName; $inputbody.dataProviderContext.properties.permissionSetToken = "repoV2/$($this.ResourceContext.ResourceDetails.id)" # Get list of all users and groups granted permissions on all repositories $responseObj = [WebRequestHelper]::InvokePostWebRequest($url, $inputbody); # Iterate through each user/group to fetch detailed permissions list if([Helpers]::CheckMember($responseObj[0],"dataProviders") -and ($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider') -and ([Helpers]::CheckMember($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider',"identities"))) { $body = '{"contributionIds":["ms.vss-admin-web.security-view-permissions-data-provider"],"dataProviderContext":{"properties":{"subjectDescriptor":"","permissionSetId": "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87","permissionSetToken":"","accountName":"","sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"repositories","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $body.dataProviderContext.properties.sourcePage.url = $refererUrl $body.dataProviderContext.properties.sourcePage.routeValues.Project = $this.ResourceContext.ResourceGroupName; $body.dataProviderContext.properties.permissionSetToken = "repoV2/$($this.ResourceContext.ResourceDetails.id)" $accessList += $responseObj.dataProviders."ms.vss-admin-web.security-view-members-data-provider".identities | Where-Object { $_.subjectKind -eq "group" } | ForEach-Object { $identity = $_ $body.dataProviderContext.properties.accountName = $_.principalName $body.dataProviderContext.properties.subjectDescriptor = $_.descriptor $identityPermissions = [WebRequestHelper]::InvokePostWebRequest($url, $body); $configuredPermissions = $identityPermissions.dataproviders."ms.vss-admin-web.security-view-permissions-data-provider".subjectPermissions | Where-Object {$_.permissionDisplayString -ne 'Not set'} return @{ IdentityName = $identity.DisplayName; IdentityType = $identity.subjectKind; Permissions = ($configuredPermissions | Select-Object @{Name="Name"; Expression = {$_.displayName}},@{Name="Permission"; Expression = {$_.permissionDisplayString}}) } } $accessList += $responseObj.dataProviders."ms.vss-admin-web.security-view-members-data-provider".identities | Where-Object { $_.subjectKind -eq "user" } | ForEach-Object { $identity = $_ $body.dataProviderContext.properties.subjectDescriptor = $_.descriptor $identityPermissions = [WebRequestHelper]::InvokePostWebRequest($url, $body); $configuredPermissions = $identityPermissions.dataproviders."ms.vss-admin-web.security-view-permissions-data-provider".subjectPermissions | Where-Object {$_.permissionDisplayString -ne 'Not set'} return @{ IdentityName = $identity.DisplayName; IdentityType = $identity.subjectKind; Permissions = ($configuredPermissions | Select-Object @{Name="Name"; Expression = {$_.displayName}},@{Name="Permission"; Expression = {$_.permissionDisplayString}}) } } } if(($accessList | Measure-Object).Count -ne 0) { $accessList= $accessList | Select-Object -Property @{Name="IdentityName"; Expression = {$_.IdentityName}},@{Name="IdentityType"; Expression = {$_.IdentityType}},@{Name="Permissions"; Expression = {$_.Permissions}} $controlResult.AddMessage([VerificationResult]::Verify,"Validate that the following identities have been provided with minimum RBAC access to repositories.", $accessList); $controlResult.SetStateData("List of identities having access to repositories: ", ($responseObj.dataProviders."ms.vss-admin-web.security-view-members-data-provider".identities | Select-Object -Property @{Name="IdentityName"; Expression = {$_.FriendlyDisplayName}},@{Name="IdentityType"; Expression = {$_.subjectKind}},@{Name="Scope"; Expression = {$_.Scope}})); } else { $controlResult.AddMessage([VerificationResult]::Passed,"No identities have been explicitly provided access to repositories."); } $responseObj = $null; } catch{ $controlResult.AddMessage([VerificationResult]::Manual,"Unable to fetch repositories permission details. $($_) Please verify from portal all teams/groups are granted minimum required permissions."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckInheritedPermissionsOnRepository([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] $projectName = $this.ResourceContext.ResourceGroupName; #permissionSetId = '2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87' is the std. namespaceID. Refer: https://docs.microsoft.com/en-us/azure/devops/organizations/security/manage-tokens-namespaces?view=azure-devops#namespaces-and-their-ids try { # Fetch the repo permissions only if not already fetch, for all the repositories in the organization if (!$this.repoInheritePermissions.ContainsKey($projectName)) { $repoPermissionUrl = 'https://dev.azure.com/{0}/_apis/accesscontrollists/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87?api-version=6.0' -f $this.OrganizationContext.OrganizationName; $responseObj = @([WebRequestHelper]::InvokeGetWebRequest($repoPermissionUrl)); $respoPermissionResponseObj = $responseObj | where-object {($_.token -match "^repoV2/$projectId\/.{36}$") -and $_.inheritPermissions -eq $true} $this.repoInheritePermissions.Add($projectName, $respoPermissionResponseObj); #Clearing local variable $responseObj = $null; $respoPermissionResponseObj = $null; } if ($this.repoInheritePermissions.ContainsKey($projectName)) { # Filter the inherited permissions specific to the given project $repoPermission = @($this.repoInheritePermissions."$projectName" | where-object { $_.token -eq "repoV2/$projectId/$($this.ResourceContext.ResourceDetails.Id)" }); if($repoPermission.Count -gt 0) { $controlResult.AddMessage([VerificationResult]::Failed, "Inherited permission is enabled on the repository."); } else { $controlResult.AddMessage([VerificationResult]::Passed, "Inherited permission is disabled on the repository."); } } else { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch the permission details for repositories."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch permission details for repositories. $($_)."); $controlResult.LogException($_) } return $controlResult } hidden [PSObject] FetchRepositoriesList() { if($null -eq $this.Repos) { # fetch repositories $repoDefnURL = ("https://dev.azure.com/$($this.OrganizationContext.OrganizationName)/$($this.ResourceContext.ResourceGroupName)/_apis/git/repositories?api-version=6.1-preview.1") try { $repoDefnsObj = [WebRequestHelper]::InvokeGetWebRequest($repoDefnURL); $this.Repos = $repoDefnsObj; } catch { $this.Repos = $null } } return $this.Repos } hidden [ControlResult] CheckBroaderGroupAccessOnFeeds([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed try { $restrictedBroaderGroups = @{} $restrictedBroaderGroupsForFeeds = $this.ControlSettings.Feed.RestrictedBroaderGroupsForFeeds if(@($restrictedBroaderGroupsForFeeds.psobject.properties).Count -gt 0){ $restrictedBroaderGroupsForFeeds.psobject.properties | foreach { $restrictedBroaderGroups[$_.Name] = $_.Value } #GET https://feeds.dev.azure.com/{organization}/{project}/_apis/packaging/Feeds/{feedId}/permissions?api-version=6.0-preview.1 #Using visualstudio api because new api (dev.azure.com) is giving null in the displayName property. #orgFeedURL will be used to identify if feed is org scoped or project scoped $scope = "Project" #Project property does not exist of org scoped feeds if ("Project" -notin $this.ResourceContext.ResourceDetails.PSobject.Properties.name){ $url = 'https://{0}.feeds.visualstudio.com/_apis/Packaging/Feeds/{1}/Permissions?includeIds=true&excludeInheritedPermissions=false&includeDeletedFeeds=false' -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id; $controlResult.AddMessage("`n***Organization scoped feed***") $scope = "Organization" } else { $url = 'https://{0}.feeds.visualstudio.com/{1}/_apis/Packaging/Feeds/{2}/Permissions?includeIds=true&excludeInheritedPermissions=false&includeDeletedFeeds=false' -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id; $controlResult.AddMessage("`n***Project scoped feed***") } $feedPermissionList = @([WebRequestHelper]::InvokeGetWebRequest($url)); if ($this.checkInheritedPermissionsFeed -eq $false) { $feedPermissionList = $feedPermissionList | where-object { $_.isInheritedRole -eq $false } } $excesiveFeedsPermissions = @($feedPermissionList | Where-Object { $restrictedBroaderGroups.keys -contains $_.displayName.split('\')[-1] -and ($_.role -in $restrictedBroaderGroups[$_.displayName.split('\')[-1]])}) $feedWithBroaderGroup = @($excesiveFeedsPermissions | Select-Object -Property @{Name="FeedName"; Expression = {$this.ResourceContext.ResourceName}},@{Name="Role"; Expression = {$_.role}},@{Name="Name"; Expression = {$_.displayName}}) ; if ($this.ControlSettings.CheckForBroadGroupMemberCount -and $feedWithBroaderGroup.Count -gt 0) { $broaderGroupsWithExcessiveMembers = @([ControlHelper]::FilterBroadGroupMembers($feedWithBroaderGroup, $true)) $feedWithBroaderGroup = @($feedWithBroaderGroup | Where-Object {$broaderGroupsWithExcessiveMembers -contains $_.Name}) } $feedWithBroaderGroupCount = $feedWithBroaderGroup.count; if ($feedWithBroaderGroupCount -gt 0) { $controlResult.AddMessage([VerificationResult]::Failed, "Count of broader groups that have administrator/contributor/collaborator access to feed: $($feedWithBroaderGroupCount)") $display = ($feedWithBroaderGroup | FT Name, Role -AutoSize | Out-String -Width 512) $controlResult.AddMessage("`nList of groups: ", $display) $controlResult.SetStateData("List of groups: ", $feedWithBroaderGroup); $groups = $feedWithBroaderGroup | ForEach-Object { $_.Name + ': ' + $_.Role } $controlResult.AdditionalInfoInCSV = $groups -join ' ; ' if ($this.ControlFixBackupRequired) { #Data object that will be required to fix the control $excesiveFeedsPermissions | ForEach-Object{ $_ | Add-Member -MemberType NoteProperty -Name "Scope" -Value $scope } $controlResult.BackupControlState = $excesiveFeedsPermissions; } } else { $controlResult.AddMessage([VerificationResult]::Passed, "Feed is not granted with administrator/contributor/collaborator permission to broad groups."); $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 'broader groups': `n$($displayObj | FT -AutoSize | out-string)"); } else { $controlResult.AddMessage([VerificationResult]::Error, "List of broader groups for feeds is not defined in control settings for your organization."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch feed permissions."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckBroaderGroupAccessOnFeedsAutomatedFix([ControlResult] $controlResult) { try{ $RawDataObjForControlFix = @(); $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject $scope = $RawDataObjForControlFix[0].Scope $body = "[" if (-not $this.UndoFix) { foreach ($identity in $RawDataObjForControlFix) { $roleId = [int][FeedPermissions] "Reader" if ($body.length -gt 1) {$body += ","} $body += @" { "displayName": "$($($identity.displayName).Replace('\','\\'))", "identityId": "$($identity.identityId)", "role": $roleId, "identityDescriptor": "$($($identity.identityDescriptor).Replace('\','\\'))", "isInheritedRole": false } "@; } $RawDataObjForControlFix | Add-Member -NotePropertyName NewRole -NotePropertyValue "Reader" $RawDataObjForControlFix = @($RawDataObjForControlFix | Select-Object @{Name="DisplayName"; Expression={$_.DisplayName}}, @{Name="OldRole"; Expression={$_.Role}},@{Name="NewRole"; Expression={$_.NewRole}}) } else { foreach ($identity in $RawDataObjForControlFix) { $roleId = [int][FeedPermissions] "$($identity.role)" if ($body.length -gt 1) {$body += ","} $body += @" { "displayName": "$($($identity.displayName).Replace('\','\\'))", "identityId": "$($identity.identityId)", "role": $roleId, "identityDescriptor": "$($($identity.identityDescriptor).Replace('\','\\'))", "isInheritedRole": false } "@; } $RawDataObjForControlFix | Add-Member -NotePropertyName OldRole -NotePropertyValue "Reader" $RawDataObjForControlFix = @($RawDataObjForControlFix | Select-Object @{Name="DisplayName"; Expression={$_.DisplayName}}, @{Name="OldRole"; Expression={$_.OldRole}},@{Name="NewRole"; Expression={$_.Role}}) } #Patch request $body += "]" if ($scope -eq "Organization") { $url = "https://feeds.dev.azure.com/{0}/_apis/packaging/Feeds/{1}/permissions?api-version=6.1-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id; } else { $url = "https://feeds.dev.azure.com/{0}/{1}/_apis/packaging/Feeds/{2}/permissions?api-version=6.1-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id; } $header = [WebRequestHelper]::GetAuthHeaderFromUriPatch($url) Invoke-RestMethod -Uri $url -Method Patch -ContentType "application/json" -Headers $header -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] CheckSecureFilesPermission([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed try { $url = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/securefile/{2}" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id; $secureFilePipelinePermObj = @([WebRequestHelper]::InvokeGetWebRequest($url)); if($secureFilePipelinePermObj.Count -gt 0 -and [Helpers]::CheckMember($secureFilePipelinePermObj,"allPipelines") -and $secureFilePipelinePermObj.allPipelines.authorized -eq $true) { $controlResult.AddMessage([VerificationResult]::Failed, "Secure file is accesible to all YAML pipelines."); if ($this.ControlFixBackupRequired){ $controlResult.BackupControlState = $secureFilePipelinePermObj; } } else { $controlResult.AddMessage([VerificationResult]::Passed, "Secure file is not accesible to all YAML pipelines."); try { $buildPipelineIds = @(); if ($secureFilePipelinePermObj.Count -gt 0 -and $secureFilePipelinePermObj.pipelines.Count -gt 0) { $buildPipelineIds = $secureFilePipelinePermObj.pipelines.id $buildDefnURL = "https://{0}.visualstudio.com/{1}/_apis/build/definitions?definitionIds={2}&api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $this.ResourceContext.ResourceGroupName, ($buildPipelineIds -join ","); $buildDefnsObj = [WebRequestHelper]::InvokeGetWebRequest($buildDefnURL); if (([Helpers]::CheckMember($buildDefnsObj,"name"))) { $controlResult.AdditionalInfoInCSV = "NumYAMLPipelineWithAccess: $($buildDefnsObj.Count)" $controlResult.AdditionalInfoInCSV = "List: " + ($buildDefnsObj.Name -join ",") } } } catch { } } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch authorization details of secure file."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckSecureFilesPermissionAutomatedFix([ControlResult] $controlResult) { try{ $this.PublishCustomMessage( "`nAfter applying this fix, any YAML pipelines using this secure file will lose access. You will have to explicitly add them.", [MessageType]::Warning); $RawDataObjForControlFix = @(); $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject if (-not $this.UndoFix) { $RawDataObjForControlFix.allPipelines.authorized = $false; $RawDataObjForControlFix.allPipelines.authorizedBy = $null; $RawDataObjForControlFix.allPipelines.authorizedOn = $null; $body = $RawDataObjForControlFix | ConvertTo-Json -Depth 10; $uri = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/securefile/{2}?api-version=5.1-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id; $header = [WebRequestHelper]::GetAuthHeaderFromUriPatch($uri) $Result = Invoke-RestMethod -Uri $uri -Method Patch -ContentType "application/json" -Headers $header -Body $body $controlResult.AddMessage([VerificationResult]::Fixed, "Secure file is not accessible to all YAML pipelines."); } else { $body = $RawDataObjForControlFix | ConvertTo-Json -Depth 10; $uri = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/securefile/{2}?api-version=5.1-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id; $header = [WebRequestHelper]::GetAuthHeaderFromUriPatch($uri) $Result = Invoke-RestMethod -Uri $uri -Method Patch -ContentType "application/json" -Headers $header -Body $body $controlResult.AddMessage([VerificationResult]::Fixed, "Secure file is accessible to all YAML pipelines."); } } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not apply fix."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckBroaderGroupAccessOnSecureFile([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed try { $restrictedBroaderGroups = @{} $restrictedBroaderGroupsForSecureFile = $this.ControlSettings.SecureFile.RestrictedBroaderGroupsForSecureFile if(@($restrictedBroaderGroupsForSecureFile.psobject.properties).Count -gt 0){ $restrictedBroaderGroupsForSecureFile.psobject.properties | foreach { $restrictedBroaderGroups[$_.Name] = $_.Value } $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] $url = 'https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.securefile/roleassignments/resources/{1}%24{2}' -f $this.OrganizationContext.OrganizationName, $projectId, $this.ResourceContext.ResourceDetails.Id; $secureFilePermissionList = @([WebRequestHelper]::InvokeGetWebRequest($url)); $roleAssignmentsToCheck = $secureFilePermissionList; if ($this.checkInheritedPermissionsSecureFile -eq $false) { $roleAssignmentsToCheck = $secureFilePermissionList | where-object { $_.access -ne "inherited" } } $excesiveSecureFilePermissions = @($roleAssignmentsToCheck | Where-Object { $restrictedBroaderGroups.keys -contains $_.identity.displayName.split('\')[-1] -and ($_.role.name -in $restrictedBroaderGroups[$_.identity.displayName.split('\')[-1]])}) $secureFileWithBroaderGroup = @($excesiveSecureFilePermissions | Select-Object -Property @{Name="SecureFileName"; Expression = {$this.ResourceContext.ResourceName}},@{Name="Role"; Expression = {$_.role.name}},@{Name="Name"; Expression = {$_.identity.displayName}}, @{Name="Id"; Expression = {$_.identity.id}}) ; if ($this.ControlSettings.CheckForBroadGroupMemberCount -and $secureFileWithBroaderGroup.Count -gt 0) { $broaderGroupsWithExcessiveMembers = @([ControlHelper]::FilterBroadGroupMembers($secureFileWithBroaderGroup, $true)) $secureFileWithBroaderGroup = @($secureFileWithBroaderGroup | Where-Object {$broaderGroupsWithExcessiveMembers -contains $_.Name}) } $secureFileWithBroaderGroupCount = $secureFileWithBroaderGroup.count; if ($secureFileWithBroaderGroupCount -gt 0) { $controlResult.AddMessage([VerificationResult]::Failed, "Count of broader groups that have user/administrator access to secure file: $($secureFileWithBroaderGroupCount)") $display = ($secureFileWithBroaderGroup | FT Name, Role -AutoSize | Out-String -Width 512) $controlResult.AddMessage("`nList of groups: ", $display) if ($this.ControlFixBackupRequired) { #Data object that will be required to fix the control $controlResult.BackupControlState = $secureFileWithBroaderGroup; } $groups = $secureFileWithBroaderGroup | ForEach-Object { $_.Name + ': ' + $_.Role } $controlResult.AdditionalInfoInCSV = $groups -join ' ; ' } else { $controlResult.AddMessage([VerificationResult]::Passed, "Secure file is not granted with user/administrator permission to broad groups."); } $displayObj = $restrictedBroaderGroups.Keys | Select-Object @{Name = "Broader Group"; Expression = {$_}}, @{Name = "Excessive Permissions"; Expression = {$restrictedBroaderGroups[$_] -join ', '}} $controlResult.AddMessage("`nNote: `nThe following groups are considered 'broader groups': `n$($displayObj | FT -AutoSize | out-string)"); } else { $controlResult.AddMessage([VerificationResult]::Error, "List of broader groups for secure file is not defined in control settings for your organization."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch secure file permissions."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckBroaderGroupAccessOnSecureFileAutomatedFix ([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 DisplayName, @{Name="OldRole"; Expression={$_.Role}}, 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 DisplayName, OldRole, @{Name="NewRole"; Expression={$_.Role}}) } $body += "]" #Put request $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] $url = "https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.securefile/roleassignments/resources/{1}%24{2}?api-version=6.1-preview.1" -f $($this.OrganizationContext.OrganizationName),$projectId,$($this.ResourceContext.ResourceDetails.Id); $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] CheckEnviornmentAccess([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed try { $url = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/environment/{2}" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id; $envPipelinePermissionObj = @([WebRequestHelper]::InvokeGetWebRequest($url)); if (($envPipelinePermissionObj.Count -gt 0) -and ([Helpers]::CheckMember($envPipelinePermissionObj[0],"allPipelines")) -and ($envPipelinePermissionObj[0].allPipelines.authorized -eq $true)) { $controlResult.AddMessage([VerificationResult]::Failed, "Environment is accessible to all YAML pipelines."); if ($this.ControlFixBackupRequired){ $controlResult.BackupControlState = $envPipelinePermissionObj; } } else { $controlResult.AddMessage([VerificationResult]::Passed, "Environment is not accessible to all YAML pipelines."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch environment's pipeline permission setting."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckEnviornmentAccessAutomatedFix([ControlResult] $controlResult) { try{ $this.PublishCustomMessage( "`nAfter applying this fix, any YAML pipelines using this Environment will lose access. You will have to explicitly add them.", [MessageType]::Warning); $RawDataObjForControlFix = @(); $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject if (-not $this.UndoFix) { $RawDataObjForControlFix.allPipelines.authorized = $false; $RawDataObjForControlFix.allPipelines.authorizedBy = $null; $RawDataObjForControlFix.allPipelines.authorizedOn = $null; $body = $RawDataObjForControlFix | ConvertTo-Json -Depth 10; $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] $uri = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/environment/{2}?api-version=5.1-preview.1" -f ($this.OrganizationContext.OrganizationName), $projectId, $($this.ResourceContext.ResourceDetails.id); $header = [WebRequestHelper]::GetAuthHeaderFromUriPatch($uri) $Result = Invoke-RestMethod -Uri $uri -Method Patch -ContentType "application/json" -Headers $header -Body $body $controlResult.AddMessage([VerificationResult]::Fixed, "Environment is not accessible to all YAML pipelines."); } else { $body = $RawDataObjForControlFix | ConvertTo-Json -Depth 10; $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] $uri = "https://dev.azure.com/{0}/{1}/_apis/pipelines/pipelinePermissions/environment/{2}?api-version=5.1-preview.1" -f ($this.OrganizationContext.OrganizationName), $projectId, $($this.ResourceContext.ResourceDetails.id); $header = [WebRequestHelper]::GetAuthHeaderFromUriPatch($uri) $Result = Invoke-RestMethod -Uri $uri -Method Patch -ContentType "application/json" -Headers $header -Body $body $controlResult.AddMessage([VerificationResult]::Fixed, "Environment is accessible to all YAML pipelines."); } } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not apply fix."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckPreDeploymentApprovalOnEnv([ControlResult] $controlResult){ $controlResult.VerificationResult = [VerificationResult]::Failed try{ $url = "https://dev.azure.com/{0}/{1}/_apis/pipelines/checks/configurations?resourceType=environment&resourceId={2}&api-version=6.1-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id; $response = [WebRequestHelper]::InvokeGetWebRequest($url); if([Helpers]::CheckMember($response, "count") -and $response[0].count -eq 0){ $controlResult.AddMessage([VerificationResult]::Failed, "No approvals and checks have been defined for the environment."); } else{ $approvals = @($response | Where-Object{$_.type.name -eq "Approval"}) if($approvals.Count -eq 0){ $controlResult.AddMessage([VerificationResult]::Failed, "No approvals have been defined for the environment."); } else{ $controlResult.AddMessage([VerificationResult]::Passed, "Approvals have been enabled for the environment."); } } } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch approvals and checks on the environment."); } return $controlResult } hidden [ControlResult] CheckPreDeploymentApproversOnEnv([ControlResult] $controlResult){ $controlResult.VerificationResult = [VerificationResult]::Failed try{ $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': 'environment'}]" $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 environment."); } else{ $approvals = @($response.value | Where-Object{$_.type.name -eq "Approval"}) if($approvals.Count -eq 0){ $controlResult.AddMessage([VerificationResult]::Failed, "No approvals have been defined for the environment."); } else{ $approvers = $approvals.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 environment $($approvers)."; $controlResult.AddMessage([VerificationResult]::Verify, "Validate users/groups added as approver within the environment."); } } } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch list of approvers on the environment."); } return $controlResult } hidden [ControlResult] CheckBroaderGroupAccessOnEnvironment([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed try { $broaderGroups = $this.ControlSettings.Environment.RestrictedBroaderGroupsForEnvironment if(@($broaderGroups.psobject.Properties).Count -gt 0) { $restrictedBroaderGroups = @{} $broaderGroups.psobject.properties | foreach { $restrictedBroaderGroups[$_.Name] = $_.Value } $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] $url = 'https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.environmentreferencerole/roleassignments/resources/{1}_{2}' -f $this.OrganizationContext.OrganizationName, $projectId, $this.ResourceContext.ResourceDetails.Id; $environmentPermissionList = @([WebRequestHelper]::InvokeGetWebRequest($url)); $roleAssignmentsToCheck = $environmentPermissionList; if ($this.checkInheritedPermissionsEnvironment -eq $false) { $roleAssignmentsToCheck = $environmentPermissionList | where-object { $_.access -ne "inherited" } } $excesiveEnvironmentPermissions = @($roleAssignmentsToCheck | Where-Object { $restrictedBroaderGroups.keys -contains $_.identity.displayName.split('\')[-1] -and ($_.role.name -in $restrictedBroaderGroups[$_.identity.displayName.split('\')[-1]])}) $environmentWithBroaderGroup = @($excesiveEnvironmentPermissions | Select-Object -Property @{Name="EnvironmentName"; Expression = {$this.ResourceContext.ResourceName}},@{Name="Role"; Expression = {$_.role.name}},@{Name="Name"; Expression = {$_.identity.displayName}},@{Name="Id"; Expression = {$_.identity.id}}) ; if ($this.ControlSettings.CheckForBroadGroupMemberCount -and $environmentWithBroaderGroup.Count -gt 0) { $broaderGroupsWithExcessiveMembers = @([ControlHelper]::FilterBroadGroupMembers($environmentWithBroaderGroup, $true)) $environmentWithBroaderGroup = @($environmentWithBroaderGroup | Where-Object {$broaderGroupsWithExcessiveMembers -contains $_.Name}) } $environmentWithBroaderGroupCount = $environmentWithBroaderGroup.count; if ($environmentWithBroaderGroupCount -gt 0) { $controlResult.AddMessage([VerificationResult]::Failed, "Count of broader groups that have user/administrator access to environment: $($environmentWithBroaderGroupCount)") $backupDataObject = $environmentWithBroaderGroup | Select @{l = 'Name'; e = { $_.Name} },@{l = 'Id'; e = { $_.Id} }, @{l = 'Role'; e = { $_.Role } } $display = ($environmentWithBroaderGroup | FT Name, Role -AutoSize | Out-String -Width 512) $controlResult.AddMessage("`nList of groups: ", $display) if ($this.ControlFixBackupRequired) { #Data object that will be required to fix the control $controlResult.BackupControlState = $backupDataObject; } } else { $controlResult.AddMessage([VerificationResult]::Passed, "Environment is not granted with user/administrator permission to broad groups."); } $displayObj = $restrictedBroaderGroups.Keys | Select-Object @{Name = "Broader Group"; Expression = {$_}}, @{Name = "Excessive Permissions"; Expression = {$restrictedBroaderGroups[$_] -join ', '}} $controlResult.AddMessage("`nNote: `nThe following groups are considered 'broader groups': `n$($displayObj | FT -AutoSize | out-string)"); } else { $controlResult.AddMessage([VerificationResult]::Error, "List of broader groups for environment is not defined in control settings for your organization."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch environment permissions."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckBroaderGroupAccessOnEnvironmentAutomatedFix ([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 $projectId = ($this.ResourceContext.ResourceId -split "project/")[-1].Split('/')[0] $url = 'https://dev.azure.com/{0}/_apis/securityroles/scopes/distributedtask.environmentreferencerole/roleassignments/resources/{1}_{2}?api-version=5.0-preview.1' -f $this.OrganizationContext.OrganizationName, $projectId, $this.ResourceContext.ResourceDetails.Id; $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] CheckBranchHygieneOnEnv([ControlResult] $controlResult){ try{ $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': 'environment'}]" $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 environment."); } else{ $branchControl = @() try{ $branchControl = @($response.value | Where-Object {$_.PSObject.Properties.Name -contains "settings"}) $branchControl = @($branchControl.settings | Where-Object {$_.PSObject.Properties.Name -contains "displayName" -and $_.displayName -eq "Branch Control"}) } catch{ $branchControl = @() } if($branchControl.Count -eq 0){ $controlResult.AddMessage([VerificationResult]::Failed, "Branch control has not been defined for the environment."); } else{ #response is a string of branches seperaed via comma $branches = ($branchControl.inputs.allowedBranches).Split(","); $nonPermissibleBranchesFound = $false foreach($branch in $branches){ try{ #allowed format is refs/heads/branch $branch = ($branch -split 'refs/heads/')[1] } catch{ #to catch branch names like *, refs/tags etc. $nonPermissibleBranchesFound = $true; break; } if($branch -notin $this.ControlSettings.Build.BranchesToCheckForYAMLScript){ $nonPermissibleBranchesFound = $true; break; } } if($nonPermissibleBranchesFound -eq $false){ $controlResult.AddMessage([VerificationResult]::Passed, "Deployments to the environment is allowed via standard branches only."); } else{ $controlResult.AddMessage([VerificationResult]::Verify, "Validate the branches approved for deployment to the environment."); } $branches = $branches | Select @{n='Branch name';e={$_}} $formattedBranchesTable = ($branches| FT -AutoSize | Out-String -width 512) $controlResult.AddMessage("`nList of branches : `n$formattedBranchesTable"); $controlResult.AdditionalInfo += "List of branches approved on environment $($branches)."; } } } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch branch control checks on the environment."); } return $controlResult } hidden [ControlResult] CheckBuildSvcAccAccessOnFeeds([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed try { #orgFeedURL will be used to identify if feed is org scoped or project scoped $scope = "Project" #Project property does not exist of org scoped feeds if ("Project" -notin $this.ResourceContext.ResourceDetails.PSobject.Properties.name){ $url = 'https://{0}.feeds.visualstudio.com/_apis/Packaging/Feeds/{1}/Permissions?includeIds=true&excludeInheritedPermissions=false&includeDeletedFeeds=false' -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id; $controlResult.AddMessage("`n***Organization scoped feed***") $scope = "Organization" } else { $url = 'https://{0}.feeds.visualstudio.com/{1}/_apis/Packaging/Feeds/{2}/Permissions?includeIds=true&excludeInheritedPermissions=false&includeDeletedFeeds=false' -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id; $controlResult.AddMessage("`n***Project scoped feed***") } $feedPermissionList = @([WebRequestHelper]::InvokeGetWebRequest($url)); $restrictedRolesForBroaderGroupsInFeeds = $this.ControlSettings.Feed.RestrictedRolesForBuildSvcAccountsInFeed; $excessiveBuildSvcAccFeedsPerm = @($feedPermissionList | Where-Object {($restrictedRolesForBroaderGroupsInFeeds -contains $_.role) -and ` (($_.DisplayName.split('\')[-1] -like "*Project Collection Build Service ($($this.OrganizationContext.OrganizationName))") -or ` ($_.DisplayName.split('\')[-1] -like "*Build Service ($($this.OrganizationContext.OrganizationName))" ))}) $feedWithBuildSvcAcc = @($excessiveBuildSvcAccFeedsPerm | Select-Object -Property @{Name="Role"; Expression = {$_.role}},@{Name="DisplayName"; Expression = {$_.displayName}}) ; $feedWithBuildSvcAccCount = $feedWithBuildSvcAcc.count; if ($feedWithBuildSvcAccCount -gt 0) { $controlResult.AddMessage([VerificationResult]::Failed, "Count of build service accounts that have administrator/contributor/collaborator access to feed: $($feedWithBuildSvcAccCount)") $display = ($feedWithBuildSvcAcc | FT Role, DisplayName -AutoSize | Out-String -Width 512) $controlResult.AddMessage("`nList of groups: ", $display) $controlResult.SetStateData("List of groups: ", $feedWithBuildSvcAcc); $groups = $feedWithBuildSvcAcc | ForEach-Object { $_.DisplayName + ': ' + $_.Role } $controlResult.AdditionalInfoInCSV = "$($groups -join ' ; ')" #Fetching identity used to publish last 10 packages $accUsedToPublishPackage = $this.ValidateBuildSvcAccInPackage($scope, $true); if ($accUsedToPublishPackage.packagesInfo.count -gt 0) { $controlResult.AddMessage("`nList of last 10 published packages and identity used to publish: ", ($accUsedToPublishPackage.packagesInfo | FT | Out-String -Width 512)) $uniqueIdentities = $accUsedToPublishPackage.packagesInfo | select-object -Property IdentityName -Unique $controlResult.AdditionalInfo += "List of identities used to publish last 10 packages: $($uniqueIdentities.IdentityName -join ', ')"; $controlResult.AdditionalInfoInCSV += "; Last 10 publishers: $($uniqueIdentities.IdentityName -join ', ')"; } else { $controlResult.AdditionalInfo += "No package found"; $controlResult.AdditionalInfoInCSV += "; No package found"; } if ($this.ControlFixBackupRequired) { #Data object that will be required to fix the control $excessiveBuildSvcAccFeedsPerm | ForEach-Object{ $_ | Add-Member -MemberType NoteProperty -Name "Scope" -Value $scope } $controlResult.BackupControlState = $excessiveBuildSvcAccFeedsPerm; } } else { $controlResult.AddMessage([VerificationResult]::Passed, "Feed is not granted with administrator/contributor/collaborator permission to build service accounts."); $controlResult.AdditionalInfoInCSV = "NA"; } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch feed permissions."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckBuildSvcAccAccessOnFeedsAutomatedFix([ControlResult] $controlResult) { try{ $RawDataObjForControlFix = @(); $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject $scope = $RawDataObjForControlFix[0].Scope $isBuildSVcAccUsedToPublishPackage = $false $body = "[" if (-not $this.UndoFix) { #If last 10 published packages are published via Build service accounts, user should provide -Force switch in the command if (-not $this.invocationContext.BoundParameters["Force"]) { $isBuildSVcAccUsedToPublishPackage = $this.ValidateBuildSvcAccInPackage($scope, $false); } if ($isBuildSVcAccUsedToPublishPackage.isBuildSvcAccUsed -eq $false) { foreach ($identity in $RawDataObjForControlFix) { $roleId = [int][FeedPermissions] "Reader" if ($body.length -gt 1) {$body += ","} $body += @" { "displayName": "$($($identity.displayName).Replace('\','\\'))", "identityId": "$($identity.identityId)", "role": $roleId, "identityDescriptor": "$($($identity.identityDescriptor).Replace('\','\\'))", "isInheritedRole": false } "@; } $RawDataObjForControlFix | Add-Member -NotePropertyName NewRole -NotePropertyValue "Reader" $RawDataObjForControlFix = @($RawDataObjForControlFix | Select-Object @{Name="DisplayName"; Expression={$_.DisplayName}}, @{Name="OldRole"; Expression={$_.Role}},@{Name="NewRole"; Expression={$_.NewRole}}) } else { $this.PublishCustomMessage("Build service accounts have been used recently to publish package. Please use -Force in the command to apply fix for such feeds.`n",[MessageType]::Warning); $controlResult.AddMessage([VerificationResult]::Verify, "Build service accounts have been used recently to publish package. Please use -Force in the command to apply fix for such feeds."); return $controlResult; } } else { foreach ($identity in $RawDataObjForControlFix) { $roleId = [int][FeedPermissions] "$($identity.role)" if ($body.length -gt 1) {$body += ","} $body += @" { "displayName": "$($($identity.displayName).Replace('\','\\'))", "identityId": "$($identity.identityId)", "role": $roleId, "identityDescriptor": "$($($identity.identityDescriptor).Replace('\','\\'))", "isInheritedRole": false } "@; } $RawDataObjForControlFix | Add-Member -NotePropertyName OldRole -NotePropertyValue "Reader" $RawDataObjForControlFix = @($RawDataObjForControlFix | Select-Object @{Name="DisplayName"; Expression={$_.DisplayName}}, @{Name="OldRole"; Expression={$_.OldRole}},@{Name="NewRole"; Expression={$_.Role}}) } #Patch request $body += "]" if ($scope -eq "Organization") { $url = "https://feeds.dev.azure.com/{0}/_apis/packaging/Feeds/{1}/permissions?api-version=6.1-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id; } else { $url = "https://feeds.dev.azure.com/{0}/{1}/_apis/packaging/Feeds/{2}/permissions?api-version=6.1-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id; } $header = [WebRequestHelper]::GetAuthHeaderFromUriPatch($url) Invoke-RestMethod -Uri $url -Method Patch -ContentType "application/json" -Headers $header -Body $body $controlResult.AddMessage([VerificationResult]::Fixed, "Permission for Build service accounts 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 [psobject] ValidateBuildSvcAccInPackage($scope, $detailedList) { $isBuildSvsAccUsed = $false $packagesInfo = @() try { if ($scope -eq "Organization") { #$top in this api returns data alphabetically. Also queryorder is not supported. $url = "https://feeds.dev.azure.com/{0}/_apis/packaging/Feeds/{1}/packages?api-version=6.0-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id; } else { $url = "https://feeds.dev.azure.com/{0}/{1}/_apis/packaging/Feeds/{2}/packages?api-version=6.0-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id; } $packageList = @([WebRequestHelper]::InvokeGetWebRequest($url)); if ( $packageList.Count -gt 0 -and [Helpers]::CheckMember($packageList[0],"Id")) { #Get top 10 published packages $recentPackages = $packageList | Sort-Object -Property @{Expression={$_.versions.publishdate}; Descending = $true } | Select-Object -First 10 foreach ($package in $recentPackages) { if ($scope -eq "Organization") { $provenanceURL = "https://feeds.dev.azure.com/{0}/_apis/packaging/Feeds/{1}/Packages/{2}/Versions/{3}/provenance?api-version=6.0-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id, $package.id, $package.versions.id ; } else { $provenanceURL = "https://feeds.dev.azure.com/{0}/{1}/_apis/packaging/Feeds/{2}/Packages/{3}/Versions/{4}/provenance?api-version=6.0-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceDetails.Id, $package.id, $package.versions.id ; } $provenanceDetails = @([WebRequestHelper]::InvokeGetWebRequest($provenanceURL)); $pkgDetails = New-Object -TypeName PSObject $pkgDetails | Add-Member -NotePropertyName PackageName -NotePropertyValue $package.name $pkgDetails | Add-Member -NotePropertyName IdentityName -NotePropertyValue $provenanceDetails.provenance.data.'Common.IdentityDisplayName' $packagesInfo += $pkgDetails if (-not $detailedList) { if ($provenanceDetails.provenance.data.'Common.IdentityDisplayName' -like "*Project Collection Build Service ($($this.OrganizationContext.OrganizationName))" -or $provenanceDetails.provenance.data.'Common.IdentityDisplayName' -like "*Build Service ($($this.OrganizationContext.OrganizationName))") { $isBuildSvsAccUsed = $true break; } } } } } catch { #eat exception } $returnObj = New-Object -TypeName PSObject $returnObj | Add-Member -NotePropertyName isBuildSvcAccUsed -NotePropertyValue $isBuildSvsAccUsed $returnObj | Add-Member -NotePropertyName packagesInfo -NotePropertyValue $packagesInfo return $returnObj } hidden [ControlResult] CheckBuildSvcAcctAccessOnRepository([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed $excessivePermissions = $this.ControlSettings.Repo.RestrictedRolesForBuildSvcAccountsInRepo try { # Fetching repository RBAC using portal api's because no documented api present for this purpose. $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName); $refererUrl = "https://dev.azure.com/{0}/{1}/_settings/repositories?repo={2}&_a=permissionsMid" -f $($this.OrganizationContext.OrganizationName), $($this.ResourceContext.ResourceGroupName), $($this.ResourceContext.ResourceDetails.id) $inputbody = '{"contributionIds":["ms.vss-admin-web.security-view-members-data-provider"],"dataProviderContext":{"properties":{"permissionSetId": "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87","permissionSetToken":"","sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"repositories","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.sourcePage.url = $refererUrl $inputbody.dataProviderContext.properties.sourcePage.routeValues.Project = $this.ResourceContext.ResourceGroupName; $inputbody.dataProviderContext.properties.permissionSetToken = "repoV2/$($this.ResourceContext.ResourceDetails.Project.id)/$($this.ResourceContext.ResourceDetails.id)" $responseObj = [WebRequestHelper]::InvokePostWebRequest($url, $inputbody); $repositoryIdentities = @(); if([Helpers]::CheckMember($responseObj[0],"dataProviders") -and ($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider') -and ([Helpers]::CheckMember($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider',"identities"))) { $repositoryIdentities = @($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-members-data-provider'.identities) } if($repositoryIdentities.Count -gt 0) { # fetch the groups that have access to the repo $groupPermissionsBody = '{"contributionIds":["ms.vss-admin-web.security-view-permissions-data-provider"],"dataProviderContext":{"properties":{"subjectDescriptor":"","permissionSetId":"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87","permissionSetToken":"","accountName":"","sourcePage":{"url":"","routeId":"ms.vss-admin-web.project-admin-hub-route","routeValues":{"project":"","adminPivot":"repositories","controller":"ContributedPage","action":"Execute"}}}}}' | ConvertFrom-Json $groupPermissionsBody.dataProviderContext.properties.sourcePage.url = $refererUrl $groupPermissionsBody.dataProviderContext.properties.sourcePage.routeValues.Project = $this.ResourceContext.ResourceGroupName; $groupPermissionsBody.dataProviderContext.properties.permissionSetToken = "repoV2/$($this.ResourceContext.ResourceDetails.Project.id)/$($this.ResourceContext.ResourceDetails.id)" $buildServieAccountOnRepo = @() $groupsWithExcessivePermissionsList = @() foreach ($identity in $repositoryIdentities) { if ($identity.displayName -like '*Project Collection Build Service Accounts' -or $identity.displayName -like "*Project Collection Build Service ($($this.OrganizationContext.OrganizationName))" -or $identity.displayName -like "*Build Service ($($this.OrganizationContext.OrganizationName))") { $groupPermissionsBody.dataProviderContext.properties.subjectDescriptor = $identity.descriptor $responseObj = [WebRequestHelper]::InvokePostWebRequest($url, $groupPermissionsBody); $buildServiceAccountRbacObj = @($responseObj[0].dataProviders.'ms.vss-admin-web.security-view-permissions-data-provider'.subjectPermissions) $excessivePermissionList = $buildServiceAccountRbacObj | Where-Object { $_.displayName -in $excessivePermissions } $excessivePermissionsPerGroup = @() $excessivePermissionList | ForEach-Object { #effectivePermissionValue equals to 1 implies edit build pipeline perms is set to 'Allow'. Its value is 3 if it is set to Allow (inherited). This param is not available if it is 'Not Set'. if ([Helpers]::CheckMember($_, "effectivePermissionValue")) { if ($this.excessivePermissionBitsForRepo -contains $_.effectivePermissionValue) { $excessivePermissionsPerGroup += $_ } } } if ($excessivePermissionsPerGroup.Count -gt 0) { $groupFoundWithExcessivePermissions = $true # For PCBSA, resolve the group and check if PBS, PCBS are part of it if ($identity.displayName -like '*Project Collection Build Service Accounts') { $groupFoundWithExcessivePermissions = $false $url="https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.1-preview" -f $($this.OrganizationContext.OrganizationName); $postbody=@' {"contributionIds":["ms.vss-admin-web.org-admin-members-data-provider"],"dataProviderContext":{"properties":{"subjectDescriptor":"{0}","sourcePage":{"url":"https://dev.azure.com/{2}/_settings/groups?subjectDescriptor={1}","routeId":"ms.vss-admin-web.collection-admin-hub-route","routeValues":{"adminPivot":"groups","controller":"ContributedPage","action":"Execute"}}}}} '@ $postbody=$postbody.Replace("{0}",$identity.descriptor ) $postbody=$postbody.Replace("{1}",$this.OrganizationContext.OrganizationName) $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) try { $response = Invoke-RestMethod -Uri $url -Method Post -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Body $postbody if([Helpers]::CheckMember($response.dataProviders.'ms.vss-admin-web.org-admin-members-data-provider', "identities")) { $buildServiceAccountIdentities = $response.dataProviders.'ms.vss-admin-web.org-admin-members-data-provider'.identities foreach ($eachIdentity in $buildServiceAccountIdentities) { if ($eachIdentity.displayName -like "*Project Collection Build Service ($($this.OrganizationContext.OrganizationName))" -or $eachIdentity.displayName -like "*Build Service ($($this.OrganizationContext.OrganizationName))") { $groupFoundWithExcessivePermissions = $true } } } } catch {} } if ($groupFoundWithExcessivePermissions -eq $true) { $excessivePermissionsGroupObj = @{} $excessivePermissionsGroupObj['Group'] = $identity.displayName $excessivePermissionsGroupObj['ExcessivePermissions'] = $($excessivePermissionsPerGroup.displayName -join '; ') $excessivePermissionsGroupObj['Descriptor'] = $responseObj[0].dataProviders.'ms.vss-admin-web.security-view-permissions-data-provider'.identityDescriptor $excessivePermissionsGroupObj['PermissionSetToken'] = $excessivePermissionsPerGroup[0].token $groupsWithExcessivePermissionsList += $excessivePermissionsGroupObj } } } } if ($groupsWithExcessivePermissionsList.count -gt 0) { #TODO: Do we need to put state object? $controlResult.AddMessage([VerificationResult]::Failed, "Count of restricted Build Service groups that have access to repository: $($groupsWithExcessivePermissionsList.count)"); $formattedGroupsData = $groupsWithExcessivePermissionsList | Select @{l = 'Group'; e = { $_.Group} }, @{l = 'ExcessivePermissions'; e = { $_.ExcessivePermissions } } $formattedBroaderGrpTable = ($formattedGroupsData | Out-String -Width 512 ) $controlResult.AddMessage("`nList of 'Build Service' Accounts: $formattedBroaderGrpTable"); $controlResult.SetStateData("List of 'Build Service' Accounts: ", $formattedGroupsData) $additionalInfoInCSV = $formattedGroupsData | ForEach-Object { $_.Group + ': ' + $_.ExcessivePermissions } $additionalInfoInCSV = $additionalInfoInCSV -join ' ; ' $controlResult.AdditionalInfo += "Count of restricted Build Service groups that have access to repository: $($groupsWithExcessivePermissionsList.Count)"; $controlResult.AdditionalInfoInCSV+= "'Repo' level excessive permissions: $($additionalInfoInCSV); " if ($this.ControlFixBackupRequired) { #Data object that will be required to fix the control $controlResult.BackupControlState = $groupsWithExcessivePermissionsList; } } else { $controlResult.AddMessage([VerificationResult]::Passed,"Build Service accounts are not granted access to the repository."); $controlResult.AdditionalInfoInCSV = "NA"; } } else{ $controlResult.AddMessage([VerificationResult]::Error,"Unable to fetch repository permission details."); } $controlResult.AddMessage("`nNote:`nFollowing permissions are considered 'excessive':`n$($excessivePermissions | FT -AutoSize | Out-String -Width 512)"); } catch { $controlResult.AddMessage([VerificationResult]::Error,"Unable to fetch repository permission details."); $controlResult.LogException($_) } return $controlResult; } hidden [ControlResult] CheckBuildSvcAcctAccessOnRepositoryAutomatedFix([ControlResult] $controlResult) { try{ $RawDataObjForControlFix = @(); $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject $url = "https://dev.azure.com/{0}/_apis/AccessControlEntries/{1}?api-version=6.0" -f $($this.OrganizationContext.OrganizationName), $this.repoPermissionSetId if (-not $this.UndoFix) { foreach ($identity in $RawDataObjForControlFix) { $excessivePermissions = $identity.ExcessivePermissions -split ";" $descriptor = $identity.Descriptor foreach ($excessivePermission in $excessivePermissions) { if ($excessivePermission.trim() -eq 'Force push (rewrite history, delete branches and tags)') { $roleId = [int][RepoPermissions] 'Forcepush' } elseif ($excessivePermission.trim() -eq "Remove others' locks") { $roleId = [int][RepoPermissions] 'Removeotherslocks' } else { $roleId = [int][RepoPermissions] $excessivePermission.Replace(" ","").trim(); } #need to invoke a post request which does not accept all permissions added in the body at once #hence need to call invoke seperately for each permission $body = "{ 'token': '$($identity.PermissionSetToken)', 'merge': true, 'accessControlEntries' : [{ 'descriptor' : '$descriptor', 'allow':0, 'deny':$($roleId) }] }" | ConvertFrom-Json $result = [WebRequestHelper]:: InvokePostWebRequest($url,$body) } $identity | Add-Member -NotePropertyName OldPermission -NotePropertyValue "Allow" $identity | Add-Member -NotePropertyName NewPermission -NotePropertyValue "Deny" } } else { foreach ($identity in $RawDataObjForControlFix) { $descriptor = $identity.Descriptor $excessivePermissions = $identity.ExcessivePermissions -split ";" foreach ($excessivePermission in $excessivePermissions) { if ($excessivePermission.trim() -eq 'Force push (rewrite history, delete branches and tags)') { $roleId = [int][RepoPermissions] 'Forcepush' } elseif ($excessivePermission.trim() -eq "Remove others' locks") { $roleId = [int][RepoPermissions] 'Removeotherslocks' } else { $roleId = [int][RepoPermissions] $excessivePermission.Replace(" ","").trim(); } $body = "{ 'token': '$($identity.PermissionSetToken)', 'merge': true, 'accessControlEntries' : [{ 'descriptor' : '$descriptor', 'allow':$($roleId), 'deny':0 }] }" | ConvertFrom-Json [WebRequestHelper]:: InvokePostWebRequest($url,$body) } $identity | Add-Member -NotePropertyName OldPermission -NotePropertyValue "Deny" $identity | Add-Member -NotePropertyName NewPermission -NotePropertyValue "Allow" } } $controlResult.AddMessage([VerificationResult]::Fixed, "Permissions for build service accounts have been changed as below: "); $formattedGroupsData = $RawDataObjForControlFix | Select @{l = 'Group'; e = { $_.Group } }, @{l = 'ExcessivePermissions'; e = { $_.ExcessivePermissions }}, @{l = 'OldPermission'; e = { $_.OldPermission }}, @{l = 'NewPermission'; e = { $_.NewPermission } } $display = ($formattedGroupsData | 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] CheckCredentialsAndSecretsPolicyOnRepository([ControlResult] $controlResult) { # body for post request $controlResult.VerificationResult = [VerificationResult]::Failed $url = 'https://dev.azure.com/{0}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1' -f $($this.OrganizationContext.OrganizationName); $inputbody = '{"contributionIds": ["ms.vss-code-web.repository-policies-data-provider"],"dataProviderContext": {"properties": {"projectId": "","repositoryId": "","sourcePage": {"url": "","routeId": "ms.vss-admin-web.project-admin-hub-route","routeValues": {"project": "","adminPivot": "repositories","controller": "ContributedPage","action": "Execute"}}}}}' | ConvertFrom-Json $inputbody.dataProviderContext.properties.projectId = "$($this.ResourceContext.ResourceDetails.project.id)" $inputbody.dataProviderContext.properties.repositoryId = "$($this.ResourceContext.ResourceDetails.id)" $inputbody.dataProviderContext.properties.sourcePage.routeValues.project = "$($this.ResourceContext.ResourceDetails.project.Name)" $inputbody.dataProviderContext.properties.sourcePage.url = "https://dev.azure.com/{0}/{1}/_settings/repositories?repo={2}&_a=policiesMid" -f $($this.OrganizationContext.OrganizationName),$($this.ResourceContext.ResourceGroupName),$($this.ResourceContext.ResourceDetails.id) try { $response = [WebRequestHelper]::InvokePostWebRequest($url, $inputbody); if ([Helpers]::CheckMember($response, "dataProviders") -and $response.dataProviders.'ms.vss-code-web.repository-policies-data-provider' -and [Helpers]::CheckMember($response.dataProviders.'ms.vss-code-web.repository-policies-data-provider', "policyGroups")) { # fetching policy groups $policyGroups = $response.dataProviders."ms.vss-code-web.repository-policies-data-provider".policyGroups # fetching "Secrets scanning restriction" $credScanId = $this.ControlSettings.Repo.CredScanPolicyID if ([Helpers]::CheckMember($policyGroups, $credScanId )) { $currentScopePoliciesSecrets = $policyGroups."$($credScanId)".currentScopePolicies if($null -eq $currentScopePoliciesSecrets){ $currentScopePoliciesSecrets = $policyGroups."$($credScanId)".inheritedPolicies } if ([Helpers]::CheckMember($currentScopePoliciesSecrets, "isEnabled") -and $currentScopePoliciesSecrets.isEnabled) { $controlResult.AddMessage([VerificationResult]::Passed, "Check for credentials and other secrets is enabled."); } else { $controlResult.AddMessage([VerificationResult]::Failed, "Check for credentials and other secrets is disabled."); } } else { $controlResult.AddMessage([VerificationResult]::Failed, "Policy to check for credentials and other secrets on the repository not found."); } } else { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch repository policies."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch repository policies."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckForInactiveFeeds([ControlResult] $controlResult) { $controlResult.VerificationResult = [VerificationResult]::Failed try { #orgFeedURL will be used to identify if feed is org scoped or project scoped $scope = "Project" #Project property does not exist of org scoped feeds if ("Project" -notin $this.ResourceContext.ResourceDetails.PSobject.Properties.name){ $scope = "Organization" } if ($scope -eq "Organization") { $url = "https://{0}.feeds.visualstudio.com/_apis/Packaging/Feeds/{1}/Packages?includeDescription=true&includeDeleted=false" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id; } else { $url = "https://{0}.feeds.visualstudio.com/{1}/_apis/Packaging/Feeds/{2}/Packages?includeDescription=true&includeDeleted=false" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.project.id, $this.ResourceContext.ResourceDetails.Id; } $packagesList = @([WebRequestHelper]::InvokeGetWebRequest($url)); $inactiveLimit = $this.ControlSettings.FeedsAndPackages.ThreshHoldDaysForFeedsAndPackagesInactivity; if ($packagesList.Count -gt 0 -and [Helpers]::CheckMember($packagesList[0],"id")) { $packagesList = $packagesList |Sort-Object -Property @{Expression={$_.versions[0].publishDate}} -Descending $latestPackage = $packagesList[0] | select-object name, @{l="publishedDate"; e = {([datetime] $_.versions[0].publishDate).ToString("d MMM yyyy")}}, @{l="version";e={$_.versions.version}}, protocolType $lastPublishDate = $latestPackage.publishedDate if ((((Get-Date) - [datetime]::Parse($lastPublishDate)).Days) -gt $inactiveLimit) { if ($scope -eq "Organization") { $packageUrl = "https://{0}.feeds.visualstudio.com/_apis/Packaging/Feeds/{1}/PackageMetricsBatch?api-version=5.1-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id; } else { $packageUrl = "https://{0}.feeds.visualstudio.com/{1}/_apis/Packaging/Feeds/{2}/PackageMetricsBatch?api-version=5.1-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.project.id, $this.ResourceContext.ResourceDetails.Id; } # the below API call will fetch the additional details for the feeds such as download count and last downlad date. $body = "{'packageIds':['$($packagesList.id -join "','")']}" $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $response = @(Invoke-RestMethod -Uri $packageUrl -Method Post -ContentType "application/json" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Body $body) if(-not[Helpers]::CheckMember($response[0].value,"lastDownloaded")){ $controlResult.AddMessage([VerificationResult]::Failed, "Feed package has never been downloaded."); } else{ $lastDownloadedPackage = $response.value | sort-object lastDownloaded -descending | Select-Object -first 1 $lastDownloadedPackage = $lastDownloadedPackage | select-object @{l="Name";e={$packagesList | Where-Object {$_.id -eq $lastDownloadedPackage.packageId}| Select-Object name}}, downloadCount, @{l="lastDownloaded"; e={([datetime] $_.lastDownloaded).ToString("d MMM yyyy")}} $lastDownloadedDate = $lastDownloadedPackage.lastDownloaded if ((((Get-Date) - [datetime]::Parse($lastDownloadedDate)).Days) -gt $inactiveLimit) { $controlResult.AddMessage([VerificationResult]::Failed, "Feed was inactive from last $((((Get-Date) - [datetime]::Parse($lastDownloadedDate)).Days)) days."); } else { $controlResult.AddMessage([VerificationResult]::Passed, "Feed package was last downloaded on $(([datetime] $lastDownloadedDate).ToString("d MMM yyyy"))."); } $lastDownloadedPackage = ($lastDownloadedPackage | FT -AutoSize | Out-String -Width 512) $controlResult.AddMessage("`nLatest downloaded package in the feed: ", $lastDownloadedPackage); } } else { $controlResult.AddMessage([VerificationResult]::Passed, "Feed package was last published on $(([datetime] $lastPublishDate).ToString("d MMM yyyy"))."); } $latestPackage = ($latestPackage | FT -AutoSize | Out-String -Width 512) $controlResult.AddMessage("`nLatest published package in the feed: ", $latestPackage); } else { $controlResult.AddMessage([VerificationResult]::Failed, "Feed does not contain any packages."); } } catch { $controlResult.AddMessage([VerificationResult]::Error, "Could not fetch feed activity details."); $controlResult.LogException($_) } if($controlResult.VerificationResult -eq [VerificationResult]::Failed -and $this.ControlFixBackupRequired){ $controlResult.BackupControlState = [PSCustomObject]@{ "Feed" = $this.ResourceContext.ResourceDetails.Id } } return $controlResult } hidden [ControlResult] CheckForInactiveFeedsAutomatedFix([ControlResult] $controlResult){ try{ $RawDataObjForControlFix = @(); $RawDataObjForControlFix = ([ControlHelper]::ControlFixBackup | where-object {$_.ResourceId -eq $this.ResourceId}).DataObject $scope = "Project" if ("Project" -notin $this.ResourceContext.ResourceDetails.PSobject.Properties.name){ $scope = "Organization" } $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) if(-not $this.UndoFix){ $this.PublishCustomMessage("Feeds deleted from automated fix will remain in 'soft state' for the next 30 days, during which you can restore them back. You cannot undo this operation after 30 days.", [MessageType]::Warning); if($scope -eq "Organization"){ $url = "https://feeds.dev.azure.com/{0}/_apis/packaging/feeds/{1}?api-version=6.0-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id } else{ $url = "https://feeds.dev.azure.com/{0}/{1}/_apis/packaging/feeds/{2}?api-version=6.0-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.project.id, $this.ResourceContext.ResourceDetails.Id } $controlResult.AddMessage([VerificationResult]::Fixed, "Feed has been deleted. It will remain in soft state for next 30 days after which it will be permanently deleted."); Invoke-RestMethod -Uri $url -Method Delete -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) } } else{ if($scope -eq "Organization"){ $url = "https://feeds.dev.azure.com/{0}/_apis/Packaging/FeedRecycleBin/{1}?api-version=6.0-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id } else{ $url = "https://feeds.dev.azure.com/{0}/{1}/_apis/Packaging/FeedRecycleBin/{2}?api-version=6.0-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.project.id, $this.ResourceContext.ResourceDetails.Id } $controlResult.AddMessage([VerificationResult]::Fixed, "Feed has been restored."); $body = '[{"path":"/isDeleted","op":"replace","value":false}]' $header = [WebRequestHelper]::GetAuthHeaderFromUriPatch($url) Invoke-RestMethod -Uri $url -Method Patch -ContentType "application/json-patch+json" -Headers $header -Body $body } } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not apply fix."); $controlResult.LogException($_) } return $controlResult } hidden [ControlResult] CheckForInactivePackages([ControlResult] $controlResult){ $controlResult.VerificationResult = [VerificationResult]::Failed try { $scope = "Project" if ("Project" -notin $this.ResourceContext.ResourceDetails.PSobject.Properties.name){ $scope = "Organization" } if($scope -eq "Organization"){ $url = "https://feeds.dev.azure.com/{0}/_apis/Packaging/Feeds/{1}/RetentionPolicies?api-version=6.0-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.Id } else{ $url = "https://feeds.dev.azure.com/{0}/{1}/_apis/Packaging/Feeds/{2}/RetentionPolicies?api-version=6.0-preview.1" -f $this.OrganizationContext.OrganizationName, $this.ResourceContext.ResourceDetails.project.id, $this.ResourceContext.ResourceDetails.Id } $rmContext = [ContextHelper]::GetCurrentContext(); $user = ""; $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$rmContext.AccessToken))) $retentionPolicies = Invoke-RestMethod -Uri $url -Method Get -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo) } if($retentionPolicies -eq "null"){ $controlResult.AddMessage("Settings to delete older packages has not been enabled for the feed.") } else{ if($retentionPolicies.daysToKeepRecentlyDownloadedPackages -gt $this.ControlSettings.FeedsAndPackages.ThreshHoldDaysForFeedsAndPackagesInactivity){ $controlResult.AddMessage("Settings to delete older packages is enabled for the feed. The maximum number of days to keep recently downloaded packages is greater than $($this.ControlSettings.FeedsAndPackages.ThreshHoldDaysForFeedsAndPackagesInactivity) days. Please keep it less than the threshold days ($($this.ControlSettings.FeedsAndPackages.ThreshHoldDaysForFeedsAndPackagesInactivity) days).") } else{ if($retentionPolicies.countLimit -gt $this.ControlSettings.FeedsAndPackages.ThresholdPackagesPerFeed){ $controlResult.AddMessage("Settings to delete older packages is enabled for the feed. The maximum number of days to keep recently downloaded packages is under $($this.ControlSettings.FeedsAndPackages.ThreshHoldDaysForFeedsAndPackagesInactivity) days, but maximum number of packages to keep per feed is more than $($this.ControlSettings.FeedsAndPackages.ThresholdPackagesPerFeed) packages. Please keep the number of packages to retain per feed less than the threshold ($($this.ControlSettings.FeedsAndPackages.ThresholdPackagesPerFeed)).") } else{ $controlResult.AddMessage([VerificationResult]::Passed,"Settings to delete older packages is enabled for the feed. The maximum number of days to keep recently downloaded packages is less than $($this.ControlSettings.FeedsAndPackages.ThreshHoldDaysForFeedsAndPackagesInactivity) days.") } } $controlResult.AddMessage("`n Current number of days to keep recently downloaded packages is $($retentionPolicies.daysToKeepRecentlyDownloadedPackages).") $controlResult.AddMessage("`n Maximum number of packages per feed is $($retentionPolicies.countLimit).") } } catch{ $controlResult.AddMessage([VerificationResult]::Error, "Could not retrieve feed settings."); } return $controlResult } } # SIG # Begin signature block # MIIjiAYJKoZIhvcNAQcCoIIjeTCCI3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBFMLAXmGtmMQ2r # Nb7lv7DfSIjOtDnbHjGaL7SZN6umtaCCDYEwggX/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/BvW1taslScxMNelDNMYIVXTCCFVkCAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN # BglghkgBZQMEAgEFAKCBsDAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgpPrrP/Rk # 73x86btom89POOWLoz9MGaBFq6JWDOkmyDEwRAYKKwYBBAGCNwIBDDE2MDSgFIAS # AE0AaQBjAHIAbwBzAG8AZgB0oRyAGmh0dHBzOi8vd3d3Lm1pY3Jvc29mdC5jb20g # MA0GCSqGSIb3DQEBAQUABIIBACmqCjqiwRKH11unqh8j7WH6zIXJGKKwkiRZ3RJP # E8a/o6iGFKQiedXeVHCbM+1yKDGAFLfy5ueh9jWuQuG7iF7L6k6WNA1jwIhx/ghb # 0DzH7ppfBUug36qCP5jsllIAU7PUytO+EP92E8SU8NyTBfGNpkgMG0E2hwB0/H/k # 7zbcCOSJjXQETqIJHeqSXJ3eyB10mXhCbGwT7jKUnyGyC/4jXfiHqzl4omGUv7No # 9dLWchEOnGsk4GEEolkV3TJ0RVTcN7MU/SHVfROkAt/WECvTts6QP5U09kF5OeGE # Yg2vi/JmW/ooOz0sxHdrWunUE9V6pV52BFBG6mmK3qrRm9ShghLlMIIS4QYKKwYB # BAGCNwMDATGCEtEwghLNBgkqhkiG9w0BBwKgghK+MIISugIBAzEPMA0GCWCGSAFl # AwQCAQUAMIIBUQYLKoZIhvcNAQkQAQSgggFABIIBPDCCATgCAQEGCisGAQQBhFkK # AwEwMTANBglghkgBZQMEAgEFAAQgOYLfAEJjlj87blTZACfrEXzj2eaEpv1X4V/2 # B/o8FzACBmGS1tcB4BgTMjAyMTEyMTQxMTU2NDEuMTA0WjAEgAIB9KCB0KSBzTCB # yjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMc # TWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRT # UyBFU046RUFDRS1FMzE2LUM5MUQxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0 # YW1wIFNlcnZpY2Wggg48MIIE8TCCA9mgAwIBAgITMwAAAUzFTMHQ228/sgAAAAAB # TDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAe # Fw0yMDExMTIxODI2MDBaFw0yMjAyMTExODI2MDBaMIHKMQswCQYDVQQGEwJVUzET # MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj # YSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpFQUNFLUUzMTYt # QzkxRDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCCASIw # DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMphYFHDrMe576NV7IEKD/jk37xP # iaTjee2zK3XP+qUJpBVMY2ICxaRRhy1Cnyf/5vWRpn33Bk9xbGegnpbkoL880bNp # SZ6uWcpzSgFBOdmNUrTBt96RWXaPY7ktUMBZEWviSf3yCV2IXgWYAQFuZ9ssQ9Yg # jpo1pvUrtaoUwAjiaM436UCU9fW1D+kcEH05m4hucWbE8JW+O9b3bletiv78n+fC # 6oKk6aSQRRFL4OJiovS+ib175G6pSf9wDRk9X3kO661OtCcrHZAfwe2MHXDP4eZf # GRksA/IvvrLFNcajI7It6Tx+onDyR5igRi+kCJoTG0YUGC1UMjCK05WtDrsCAwEA # AaOCARswggEXMB0GA1UdDgQWBBQBlh6nBApe5yeVQgGA9BBH3mb6fDAfBgNVHSME # GDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBHhkVodHRw # Oi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNUaW1TdGFQ # Q0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5o # dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0YVBDQV8y # MDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMI # MA0GCSqGSIb3DQEBCwUAA4IBAQBPBOSw99ZDrqiAYq9362Z3HYhBhoSXvMeICG9x # w7rlp8hAtmiSHPIAcM74xkfYZndBf1ZQ5unU5YmV+/PG/Qu7NX8ZKgkcsNW8UPAn # VbTpR+vNmf//kXdiDJP3b8U7nMzZ05peRKMV4vUOEYD6+ww8HNSSBEjRVfaESBLZ # 3opjPoxzayaop+WXU5ZWtloml3oLrnum1sicTVqw30mM2jY/wJJH/bK4bTRzzv7t # 7n18gB/+XC/YR/j2+tIuntj0xL0QUFG0XuBAL+6zLSCtJR36q0hP/77Zsk0txL95 # mNcrRfRQJy4xT5lkGIZXbAyEQg51BG5aomVO/1+05vrtz8prMIIGcTCCBFmgAwIB # AgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2Vy # dGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcNMjUwNzAx # MjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G # A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYw # JAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIwDQYJKoZI # hvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0VBDVpQoA # goX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEwRA/xYIiE # VEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQedGFnkV+B # VLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKxXf13Hz3w # V3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4GkbaICDXo # eByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEAAaOCAeYw # ggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7fEYbxTNo # WoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBW # BgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUH # AQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp # L2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0gAQH/BIGV # MIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93d3cubWlj # cm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYBBQUHAgIw # NB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUAbQBlAG4A # dAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOhIW+z66bM # 9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS+7lTjMz0 # YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlKkVIArzgP # F/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon/VWvL/62 # 5Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOiPPp/fZZq # kHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/fmNZJQ96 # LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCIIYdqwUB5v # vfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0cs0d9LiF # AR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7aKLixqduW # sqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQcdeh0sVV # 42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+NR4Iuto2 # 29Nfj950iEkSoYICzjCCAjcCAQEwgfihgdCkgc0wgcoxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNh # IE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkVBQ0UtRTMxNi1D # OTFEMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEw # BwYFKw4DAhoDFQA9mVtOCSgTYnYdGM1jKASXGuD3oKCBgzCBgKR+MHwxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m # dCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBBQUAAgUA5WLnMTAiGA8y # MDIxMTIxNDE3NDkzN1oYDzIwMjExMjE1MTc0OTM3WjB3MD0GCisGAQQBhFkKBAEx # LzAtMAoCBQDlYucxAgEAMAoCAQACAjWUAgH/MAcCAQACAhHwMAoCBQDlZDixAgEA # MDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAI # AgEAAgMBhqAwDQYJKoZIhvcNAQEFBQADgYEAcG+IQznF8ALzTwX47eQGzu2baVpT # 8zrM8aS5k0FfWsOpLkFXyustzGUxFdLDS5xibn7p/oX2KW7xBw2rN5/G31pxnIHc # 0ZLC92cZL+HE9/OdWivNA7tcwz94p3VJkyQVpQI9r5aO2vBomhx/cfR0Ax3w/ugH # L5qssuWvLEfpDjMxggMNMIIDCQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ # Q0EgMjAxMAITMwAAAUzFTMHQ228/sgAAAAABTDANBglghkgBZQMEAgEFAKCCAUow # GgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCAK8wXg # YUqLG8clgfAUc+gvV0nLvGUDJSSmpIJY1KwX6jCB+gYLKoZIhvcNAQkQAi8xgeow # gecwgeQwgb0EINvCpbu/UEsy0RBMIOH6TwsthlN90/tz2a8QYmfEr04lMIGYMIGA # pH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAFMxUzB0NtvP7IA # AAAAAUwwIgQgkAe5PTMLARMQ3z9JPYVCmx6f6CDSILpUVB89sDA8QfswDQYJKoZI # hvcNAQELBQAEggEAf4M01mO7JA1IWgBSk6kreoKet0R2VGLUmn+2tovZ8+slpYdZ # HXhx7qr4lnBtBR5Ah7SEHOx6srxk07iJETgnkDg2CZMFFJK2sHfyChhm0EuSg0NJ # Xf3j17k1O70l6oie5d75Deoii+XFw1TKYM/rQ7s0gHCo/BSy9SNxB8MMeCK0FtPb # 5oBc5ngpKkFOzJkaa0DNu9RZVtHg8vg6+psUzacQFioye8OBqEgcrjs+DQYvUCsV # AtogEgBM+P4NKEUmWqEGq1yI5nMsYUZ6GVsEHc87T6m/Sde03+2AGRYQsOQJUvpR # 9RO0+tX66q5GhtuXocksqlLpOGFLGl6frQI3Pg== # SIG # End signature block |