Skip to content

Commit 6181f79

Browse files
committed
feat: add support for branch protections via rules
This commit adds support for reading and interpreting the rules applied to the default branch of the repo. Evaluations that previously only considered the state of branch protection rules will now also consider the state of branch rules. Signed-off-by: Travis Truman <trumant@gmail.com>
1 parent e91fedc commit 6181f79

File tree

3 files changed

+59
-11
lines changed

3 files changed

+59
-11
lines changed

data/repository_metadata.go

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ type RepositoryMetadata interface {
1111
IsPublic() bool
1212
OrganizationBlogURL() *string
1313
IsMFARequiredForAdministrativeActions() *bool
14+
IsDefaultBranchProtected() *bool
15+
DefaultBranchRequiresPRReviews() *bool
1416
}
1517

1618
type GitHubRepositoryMetadata struct {
17-
Releases []ReleaseData
18-
Rulesets []Ruleset
19-
ghRepo *github.Repository
20-
ghOrg *github.Organization
19+
Releases []ReleaseData
20+
defaultBranchRules *github.BranchRules
21+
ghRepo *github.Repository
22+
ghOrg *github.Organization
2123
}
2224

2325
func (r *GitHubRepositoryMetadata) IsActive() bool {
@@ -28,6 +30,22 @@ func (r *GitHubRepositoryMetadata) IsPublic() bool {
2830
return !r.ghRepo.GetPrivate()
2931
}
3032

33+
func (r *GitHubRepositoryMetadata) IsDefaultBranchProtected() *bool {
34+
if r.defaultBranchRules == nil {
35+
return nil
36+
}
37+
updateBlockedByRule := r.defaultBranchRules != nil && len(r.defaultBranchRules.Update) > 0
38+
return &updateBlockedByRule
39+
}
40+
41+
func (r *GitHubRepositoryMetadata) DefaultBranchRequiresPRReviews() *bool {
42+
if r.defaultBranchRules == nil {
43+
return nil
44+
}
45+
requiresReviews := r.defaultBranchRules != nil && r.defaultBranchRules.PullRequest != nil && len(r.defaultBranchRules.PullRequest) > 0 && r.defaultBranchRules.PullRequest[0].Parameters.RequiredApprovingReviewCount > 0
46+
return &requiresReviews
47+
}
48+
3149
func (r *GitHubRepositoryMetadata) OrganizationBlogURL() *string {
3250
if r.ghOrg != nil {
3351
return r.ghOrg.Blog
@@ -53,8 +71,30 @@ func loadRepositoryMetadata(ghClient *github.Client, owner, repo string) (ghRepo
5371
ghRepo: repository,
5472
}, nil
5573
}
74+
branchRules, err := getRuleset(ghClient, owner, repo, repository.GetDefaultBranch())
75+
if err != nil {
76+
return repository, &GitHubRepositoryMetadata{
77+
ghRepo: repository,
78+
ghOrg: organization,
79+
}, nil
80+
}
5681
return repository, &GitHubRepositoryMetadata{
57-
ghRepo: repository,
58-
ghOrg: organization,
82+
ghRepo: repository,
83+
ghOrg: organization,
84+
defaultBranchRules: branchRules,
5985
}, nil
6086
}
87+
88+
func getRuleset(ghClient *github.Client, owner, repo string, branchName string) (*github.BranchRules, error) {
89+
branchRules, _, err := ghClient.Repositories.GetRulesForBranch(
90+
context.Background(),
91+
owner,
92+
repo,
93+
branchName,
94+
nil,
95+
)
96+
if err != nil {
97+
return nil, err
98+
}
99+
return branchRules, nil
100+
}

evaluation_plans/osps/access_control/evaluations.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func OSPS_AC_03() (evaluation *layer4.ControlEvaluation) {
6565
},
6666
[]layer4.AssessmentStep{
6767
reusable_steps.IsCodeRepo,
68-
branchProtectionRestrictsPushes, // This checks branch protection, but not rulesets yet
68+
defaultBranchRestrictsPushes,
6969
},
7070
)
7171

evaluation_plans/osps/access_control/steps.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func orgRequiresMFA(payloadData any, _ map[string]*layer4.Change) (result layer4
2222
return layer4.Failed, "Two-factor authentication is NOT configured as required by the parent organization"
2323
}
2424

25-
func branchProtectionRestrictsPushes(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
25+
func defaultBranchRestrictsPushes(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
2626
payload, message := reusable_steps.VerifyPayload(payloadData)
2727
if message != "" {
2828
return layer4.Unknown, message
@@ -39,10 +39,18 @@ func branchProtectionRestrictsPushes(payloadData any, _ map[string]*layer4.Chang
3939
result = layer4.Passed
4040
message = "Branch protection rule requires approving reviews"
4141
} else {
42-
result = layer4.NeedsReview
43-
message = "Branch protection rule does not restrict pushes or require approving reviews; Rulesets not yet evaluated."
42+
if payload.RepositoryMetadata.IsDefaultBranchProtected() != nil && *payload.RepositoryMetadata.IsDefaultBranchProtected() {
43+
result = layer4.Passed
44+
message = "Branch rule restricts pushes"
45+
} else if payload.RepositoryMetadata.DefaultBranchRequiresPRReviews() != nil && *payload.RepositoryMetadata.DefaultBranchRequiresPRReviews() {
46+
result = layer4.Passed
47+
message = "Branch rule requires approving reviews"
48+
} else {
49+
result = layer4.Failed
50+
message = "Default branch is not protected"
51+
}
4452
}
45-
return
53+
return result, message
4654
}
4755

4856
func branchProtectionPreventsDeletion(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {

0 commit comments

Comments
 (0)