Skip to content

Commit 607a8be

Browse files
authored
Group export and basic rules (#87)
* Add Azure DevOps groups export * Add 3 rules for groups
1 parent d475e9e commit 607a8be

13 files changed

Lines changed: 707 additions & 30 deletions

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ in building the ruleset for this module.
180180

181181
### Implemented rules
182182

183+
- [Azure.DevOps.Groups.ProjectAdmins.MinMembers](./src/PSRule.Rules.AzureDevOps/en/Azure.DevOps.Groups.ProjectAdmins.MinMembers.md)
184+
- [Azure.DevOps.Groups.ProjectAdmins.MaxMembers](./src/PSRule.Rules.AzureDevOps/en/Azure.DevOps.Groups.ProjectAdmins.MaxMembers.md)
185+
- [Azure.DevOps.Groups.ProjectValidUsers.DoNotAssignMemberOfOtherGroups](./src/PSRule.Rules.AzureDevOps/en/Azure.DevOps.Groups.ProjectValidUsers.DoNotAssignMemberOfOtherGroups.md)
183186
- [Azure.DevOps.Pipelines.Core.InheritedPermissions](./src/PSRule.Rules.AzureDevOps/en/Azure.DevOps.Pipelines.Core.InheritedPermissions.md)
184187
- [Azure.DevOps.Pipelines.Core.NoPlainTextSecrets](./src/PSRule.Rules.AzureDevOps/en/Azure.DevOps.Pipelines.Core.NoPlainTextSecrets.md)
185188
- [Azure.DevOps.Pipelines.Core.UseYamlDefinition](./src/PSRule.Rules.AzureDevOps/en/Azure.DevOps.Pipelines.Core.UseYamlDefinition.md)

docs/token-permissions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ The following table lists the token scopes that are required for each command.
8181

8282
| Rule Function | API(s) | Version | Method | Scope(s) | PAT Category | PAT Name |
8383
|-------------------------------------------|------------------------------------------|---------------|--------|-------------------------|---------------------|-----------------|
84-
| Get-AzDevOpsProjects | projects | 6.0 | GET | vso.profile | User Profile | Read |
84+
| Get-AzDevOpsProject | projects | 6.0 | GET | vso.profile | User Profile | Read |
8585
| | | | | vso.project | Project and Team | Read |
8686
| Get-AzDevOpsPipelines | pipelines | 6.0-preview.1 | GET | vso.build | Build | Read |
8787
| | pipelines/{pipelineid} | 6.0-preview.1 | GET | vso.build | Build | Read |

example/best-practice/ps-rule.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
execution:
2+
inconclusiveWarning: false
3+
notProcessedWarning: false
4+
suppressedRuleWarning: false
5+
suppressionGroupExpired: Error
6+
7+
configuration:
8+
ProjectAdminsMinMembers: 2
9+
ProjectAdminsMaxMembers: 4
10+
releaseMinimumProductionApproverCount: 1
11+
branchMinimumApproverCount: 1
12+
ghasEnabled: true
13+
ghasBlockPushesEnabled: true

src/PSRule.Rules.AzureDevOps/Functions/Common.ps1

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Function Connect-AzDevOps {
5858
[Parameter(ParameterSetName = 'ServicePrincipal', Mandatory=$true)]
5959
[string]
6060
$TenantId,
61+
[Parameter()]
6162
[ValidateSet('PAT', 'ServicePrincipal', 'ManagedIdentity')]
6263
[string]
6364
$AuthType = 'PAT',
@@ -96,6 +97,7 @@ Function Disconnect-AzDevOps {
9697
[CmdletBinding()]
9798
param ()
9899
Clear-Variable connection -Scope Script -ErrorAction SilentlyContinue
100+
Remove-Variable connection -Scope Script -ErrorAction SilentlyContinue
99101
$script:connection = ""
100102
$script:connection = $null
101103
}
@@ -109,19 +111,26 @@ Function Disconnect-AzDevOps {
109111
Get all Azure DevOps projects for an organization using Azure DevOps Rest API
110112
111113
.EXAMPLE
112-
Get-AzDevOpsProjects
114+
Get-AzDevOpsProject
113115
#>
114-
function Get-AzDevOpsProjects {
116+
function Get-AzDevOpsProject {
115117
[CmdletBinding()]
116-
[OutputType([System.Object[]])]
117-
param ()
118+
param (
119+
[Parameter()]
120+
[string]
121+
$Project = ""
122+
)
118123
if ($null -eq $script:connection) {
119124
throw "Not connected to Azure DevOps. Run Connect-AzDevOps first"
120125
}
121126
$header = $script:connection.GetHeader()
122127
$Organization = $script:connection.Organization
123128
Write-Verbose "Getting projects for organization $Organization"
124-
$uri = "https://dev.azure.com/$Organization/_apis/projects?api-version=6.0"
129+
if([string]::IsNullOrEmpty($Project) -eq $false) {
130+
$uri = "https://dev.azure.com/$Organization/_apis/projects/$($Project)?api-version=7.2-preview.4"
131+
} else {
132+
$uri = "https://dev.azure.com/$Organization/_apis/projects?api-version=7.2-preview.4"
133+
}
125134
Write-Verbose "URI: $uri"
126135
try {
127136
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $header
@@ -131,9 +140,12 @@ function Get-AzDevOpsProjects {
131140
}
132141
}
133142
catch {
134-
Write-Error "Failed to get projects from Azure DevOps"
143+
throw "Failed to get projects from Azure DevOps"
144+
}
145+
if($response.value) {
146+
return $response.value
147+
} else {
148+
return $response
135149
}
136-
$projects = $response.value
137-
return @($projects)
138150
}
139-
# End of Function Get-AzDevOpsProjects
151+
# End of Function Get-AzDevOpsProject
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<#
2+
.SYNOPSIS
3+
Get a list of groups from Azure DevOps
4+
5+
.DESCRIPTION
6+
Get a list of groups from Azure DevOps for the specified organization and project using the Azure DevOps Rest API
7+
8+
.PARAMETER Project
9+
The name of the project to get groups for
10+
11+
.EXAMPLE
12+
Get-AzDevOpsGroups -Project 'MyProject'
13+
14+
.NOTES
15+
This function requires a connection to Azure DevOps. See Connect-AzDevOps for more information.
16+
#>
17+
Function Get-AzDevOpsGroups {
18+
[CmdletBinding()]
19+
param (
20+
[Parameter(Mandatory=$true)]
21+
[string]
22+
$Project
23+
)
24+
if($null -eq $script:connection) {
25+
throw 'Not connected to Azure DevOps. Run Connect-AzDevOps first.'
26+
}
27+
# Get project id
28+
try {
29+
$projectResult = Get-AzDevOpsProject -Project $Project
30+
}
31+
catch {
32+
throw "Failed to get project details from Azure DevOps"
33+
}
34+
$header = $script:connection.GetHeader()
35+
$Organization = $script:connection.Organization
36+
# Set the scope descriptor REST API endpoint
37+
$uri = "https://vssps.dev.azure.com/$($Organization)/_apis/graph/descriptors/$($projectResult.id)?api-version=7.2-preview.1"
38+
$scopeDescriptor = (Invoke-RestMethod -Uri $uri -Method Get -Headers $header).value
39+
$uri = "https://vssps.dev.azure.com/$($Organization)/_apis/graph/groups?scopeDescriptor=$scopeDescriptor&api-version=7.2-preview.1"
40+
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $header
41+
return $response.value
42+
}
43+
Export-ModuleMember -Function Get-AzDevOpsGroups
44+
# End of Function Get-AzDevOpsGroups
45+
46+
<#
47+
.SYNOPSIS
48+
Get details for a group from Azure DevOps
49+
50+
.DESCRIPTION
51+
Get details for a group from Azure DevOps for the specified group object using the Azure DevOps Rest API
52+
53+
.PARAMETER Group
54+
The group object to get details for
55+
56+
.EXAMPLE
57+
$group = (Get-AzDevOpsGroups -Project 'MyProject')[0]
58+
Get-AzDevOpsGroupDetails -Group $group
59+
60+
.NOTES
61+
This function requires a connection to Azure DevOps. See Connect-AzDevOps for more information.
62+
#>
63+
Function Get-AzDevOpsGroupDetails {
64+
[CmdletBinding()]
65+
param (
66+
[Parameter(Mandatory=$true)]
67+
[object]
68+
$Group
69+
)
70+
if($null -eq $script:connection) {
71+
throw 'Not connected to Azure DevOps. Run Connect-AzDevOps first.'
72+
}
73+
$header = $script:connection.GetHeader()
74+
$result = $Group
75+
76+
# Get detail for which group this group is a member of
77+
$memberShipUri = $Group._links.memberships.href
78+
try {
79+
$membershipResult = (Invoke-RestMethod -Uri $memberShipUri -Method Get -Headers $header).value
80+
if($membershipResult -is [string] -or $null -eq $membershipResult) {
81+
throw "Authentication failed or organization not found"
82+
}
83+
}
84+
catch {
85+
throw "Failed to get group memberOf details from Azure DevOps"
86+
}
87+
# Get the self information for each membership
88+
$memberships = @()
89+
foreach($item in $membershipResult) {
90+
$itemUri = $item._links.container.href
91+
$itemResult = (Invoke-RestMethod -Uri $itemUri -Method Get -Headers $header)
92+
$memberships += $itemResult
93+
}
94+
$result | Add-Member -MemberType NoteProperty -Name MemberOf -Value $memberships -Force
95+
96+
# Get all members of this group
97+
$memberUri = "$($Group._links.memberships.href)?direction=down&api-version=7.2-preview.1"
98+
$memberResult = (Invoke-RestMethod -Uri $memberUri -Method Get -Headers $header).value
99+
100+
# Get the self information for each member
101+
$members = @()
102+
foreach($item in $memberResult) {
103+
$itemUri = $item._links.member.href
104+
$itemResult = (Invoke-RestMethod -Uri $itemUri -Method Get -Headers $header)
105+
$members += $itemResult
106+
}
107+
$result | Add-Member -MemberType NoteProperty -Name Members -Value $members -Force
108+
return $result
109+
}
110+
Export-ModuleMember -Function Get-AzDevOpsGroupDetails
111+
112+
<#
113+
.SYNOPSIS
114+
Export all groups from Azure DevOps for a project to a JSON file
115+
116+
.DESCRIPTION
117+
Export all groups from Azure DevOps for a project to a JSON file
118+
119+
.PARAMETER Project
120+
The name of the project to get groups for
121+
122+
.PARAMETER OutputPath
123+
The folder path to the JSON file to export to
124+
125+
.EXAMPLE
126+
Export-AzDevOpsGroups -Project 'MyProject' -OutputPath 'C:\Temp\'
127+
128+
.NOTES
129+
This function requires a connection to Azure DevOps. See Connect-AzDevOps for more information.
130+
#>
131+
Function Export-AzDevOpsGroups {
132+
[CmdletBinding()]
133+
param (
134+
[Parameter(Mandatory=$true)]
135+
[string]
136+
$Project,
137+
[Parameter(Mandatory=$true)]
138+
[string]
139+
$OutputPath
140+
)
141+
if($null -eq $script:connection) {
142+
throw 'Not connected to Azure DevOps. Run Connect-AzDevOps first.'
143+
}
144+
try {
145+
$groups = Get-AzDevOpsGroups -Project $Project
146+
}
147+
catch {
148+
throw "Failed to get groups from Azure DevOps"
149+
}
150+
$groupDetails = @()
151+
foreach($group in $groups) {
152+
$thisGroup = Get-AzDevOpsGroupDetails -Group $group
153+
# Add an ObjectType property to the group object
154+
$thisGroup | Add-Member -MemberType NoteProperty -Name ObjectType -Value 'Azure.DevOps.Group' -Force
155+
# Add the group name to the group object as an ObjectName property with a convention of Organization.Project.GroupName
156+
$thisGroup | Add-Member -MemberType NoteProperty -Name ObjectName -Value "$($script:connection.Organization).$($Project).$($group.displayName)" -Force
157+
158+
$groupDetails += $thisGroup
159+
}
160+
$groupDetails | ConvertTo-Json -Depth 100 | Out-File -FilePath "$OutputPath\groups.ado.json"
161+
}
162+
Export-ModuleMember -Function Export-AzDevOpsGroups
163+
# End of Function Export-AzDevOpsGroups

src/PSRule.Rules.AzureDevOps/PSRule.Rules.AzureDevOps.psm1

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,23 @@ Function Export-AzDevOpsRuleData {
4040
[string]
4141
$OutputPath
4242
)
43+
Write-Verbose "Exporting rule data for project $Project to $OutputPath"
44+
Write-Verbose "Exporting repos and branch policies"
4345
Export-AzDevOpsReposAndBranchPolicies -Project $Project -OutputPath $OutputPath
46+
Write-Verbose "Exporting environment checks"
4447
Export-AzDevOpsEnvironmentChecks -Project $Project -OutputPath $OutputPath
48+
Write-Verbose "Exporting service connections"
4549
Export-AzDevOpsServiceConnections -Project $Project -OutputPath $OutputPath
50+
Write-Verbose "Exporting pipelines"
4651
Export-AzDevOpsPipelines -Project $Project -OutputPath $OutputPath
52+
Write-Verbose "Exporting pipelines settings"
4753
Export-AzDevOpsPipelinesSettings -Project $Project -OutputPath $OutputPath
54+
Write-Verbose "Exporting variable groups"
4855
Export-AzDevOpsVariableGroups -Project $Project -OutputPath $OutputPath
56+
Write-Verbose "Exporting release definitions"
4957
Export-AzDevOpsReleaseDefinitions -Project $Project -OutputPath $OutputPath
58+
Write-Verbose "Exporting groups"
59+
Export-AzDevOpsGroups -Project $Project -OutputPath $OutputPath
5060
}
5161
Export-ModuleMember -Function Export-AzDevOpsRuleData -Alias Export-AzDevOpsProjectRuleData
5262
# End of Function Export-AzDevOpsRuleData
@@ -71,7 +81,7 @@ Function Export-AzDevOpsOrganizationRuleData {
7181
[string]
7282
$OutputPath
7383
)
74-
$projects = Get-AzDevOpsProjects
84+
$projects = Get-AzDevOpsProject
7585
$projects | ForEach-Object {
7686
$project = $_
7787
# Create a subfolder for each project
@@ -85,7 +95,7 @@ Function Export-AzDevOpsOrganizationRuleData {
8595
Export-ModuleMember -Function Export-AzDevOpsOrganizationRuleData
8696
# End of Function Export-AzDevOpsOrganizationRuleData
8797

88-
Export-ModuleMember -Function Get-AzDevOpsProjects
98+
Export-ModuleMember -Function Get-AzDevOpsProject
8999
Export-ModuleMember -Function Connect-AzDevOps
90100
Export-ModuleMember -Function Disconnect-AzDevOps
91101

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
category: Microsoft Azure DevOps Pipelines
3+
severity: Severe
4+
online version: https://github.com/cloudyspells/PSRule.Rules.AzureDevOps/blob/main/src/PSRule.Rules.AzureDevOps/en/Azure.DevOps.Groups.ProjectAdmins.MaxMembers.md
5+
---
6+
7+
# Azure.DevOps.Groups.ProjectAdmins.MinMembers
8+
9+
## SYNOPSIS
10+
11+
The project administrators group should have at most 4 members.
12+
13+
## DESCRIPTION
14+
15+
The project administrators group should have at most 4 members. This ensures that there is not too many people who can manage the project. This rule applies to the default project administrators group.
16+
17+
Mininum TokenType: `ReadOnly`
18+
19+
This setting is configurable and can be changed to suit your organization's needs with `ProjectAdminsMaxMembers` in the `configuration` section of your ps-rule.yaml file.
20+
21+
## RECOMMENDATION
22+
23+
Consider removing members from the project administrators group.
24+
25+
## LINKS
26+
27+
- [Azure DevOps Security best practices - Tasks](https://learn.microsoft.com/en-us/azure/devops/organizations/security/security-best-practices?view=azure-devops#tasks)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
category: Microsoft Azure DevOps Pipelines
3+
severity: Severe
4+
online version: https://github.com/cloudyspells/PSRule.Rules.AzureDevOps/blob/main/src/PSRule.Rules.AzureDevOps/en/Azure.DevOps.Groups.ProjectAdmins.MinMembers.md
5+
---
6+
7+
# Azure.DevOps.Groups.ProjectAdmins.MinMembers
8+
9+
## SYNOPSIS
10+
11+
The project administrators group should have at least 2 members.
12+
13+
## DESCRIPTION
14+
15+
The project administrators group should have at least 2 members. This ensures that there is more than one person who can manage the project. This rule applies to the default project administrators group.
16+
17+
18+
Mininum TokenType: `ReadOnly`
19+
20+
This setting is configurable and can be changed to suit your organization's needs with `ProjectAdminsMinMembers` in the `configuration` section of your ps-rule.yaml file.
21+
22+
## RECOMMENDATION
23+
24+
Consider adding more than one member to the project administrators group.
25+
26+
## LINKS
27+
28+
- [Azure DevOps Security best practices - Tasks](https://learn.microsoft.com/en-us/azure/devops/organizations/security/security-best-practices?view=azure-devops#tasks)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
category: Microsoft Azure DevOps Pipelines
3+
severity: Severe
4+
online version: https://github.com/cloudyspells/PSRule.Rules.AzureDevOps/blob/main/src/PSRule.Rules.AzureDevOps/en/Azure.DevOps.Groups.ProjectValidUsers.DoNotAssignMemberOfOtherGroups.md
5+
---
6+
7+
# Azure.DevOps.Groups.ProjectValidUsers.DoNotAssignMemberOfOtherGroups
8+
9+
## SYNOPSIS
10+
11+
The project valid users group should not be a member of any other group.
12+
13+
## DESCRIPTION
14+
15+
The project valid users group is the minimum permissions level group for a
16+
project. This group should not be a member of any other group. This rule applies
17+
to the default project valid users group.
18+
19+
Mininum TokenType: `ReadOnly`
20+
21+
## RECOMMENDATION
22+
23+
Consider removing the project valid users group from any other groups.
24+
25+
## LINKS
26+
27+
- [Azure DevOps Security best practices - Tasks](https://learn.microsoft.com/en-us/azure/devops/organizations/security/security-best-practices?view=azure-devops#tasks)

0 commit comments

Comments
 (0)