Skip to content

Commit bd752b0

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 bd752b0

File tree

3 files changed

+80
-18
lines changed

3 files changed

+80
-18
lines changed

data/repository_metadata.go

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

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

2326
func (r *GitHubRepositoryMetadata) IsActive() bool {
@@ -28,6 +31,30 @@ func (r *GitHubRepositoryMetadata) IsPublic() bool {
2831
return !r.ghRepo.GetPrivate()
2932
}
3033

34+
func (r *GitHubRepositoryMetadata) IsDefaultBranchProtected() *bool {
35+
if r.defaultBranchRules == nil {
36+
return nil
37+
}
38+
updateBlockedByRule := r.defaultBranchRules != nil && len(r.defaultBranchRules.Update) > 0
39+
return &updateBlockedByRule
40+
}
41+
42+
func (r *GitHubRepositoryMetadata) IsDefaultBranchProtectedFromDeletion() *bool {
43+
if r.defaultBranchRules == nil {
44+
return nil
45+
}
46+
deletionBlockedByRule := r.defaultBranchRules != nil && len(r.defaultBranchRules.Deletion) > 0
47+
return &deletionBlockedByRule
48+
}
49+
50+
func (r *GitHubRepositoryMetadata) DefaultBranchRequiresPRReviews() *bool {
51+
if r.defaultBranchRules == nil {
52+
return nil
53+
}
54+
requiresReviews := r.defaultBranchRules != nil && r.defaultBranchRules.PullRequest != nil && len(r.defaultBranchRules.PullRequest) > 0 && r.defaultBranchRules.PullRequest[0].Parameters.RequiredApprovingReviewCount > 0
55+
return &requiresReviews
56+
}
57+
3158
func (r *GitHubRepositoryMetadata) OrganizationBlogURL() *string {
3259
if r.ghOrg != nil {
3360
return r.ghOrg.Blog
@@ -53,8 +80,30 @@ func loadRepositoryMetadata(ghClient *github.Client, owner, repo string) (ghRepo
5380
ghRepo: repository,
5481
}, nil
5582
}
83+
branchRules, err := getRuleset(ghClient, owner, repo, repository.GetDefaultBranch())
84+
if err != nil {
85+
return repository, &GitHubRepositoryMetadata{
86+
ghRepo: repository,
87+
ghOrg: organization,
88+
}, nil
89+
}
5690
return repository, &GitHubRepositoryMetadata{
57-
ghRepo: repository,
58-
ghOrg: organization,
91+
ghRepo: repository,
92+
ghOrg: organization,
93+
defaultBranchRules: branchRules,
5994
}, nil
6095
}
96+
97+
func getRuleset(ghClient *github.Client, owner, repo string, branchName string) (*github.BranchRules, error) {
98+
branchRules, _, err := ghClient.Repositories.GetRulesForBranch(
99+
context.Background(),
100+
owner,
101+
repo,
102+
branchName,
103+
nil,
104+
)
105+
if err != nil {
106+
return nil, err
107+
}
108+
return branchRules, nil
109+
}

evaluation_plans/osps/access_control/evaluations.go

Lines changed: 2 additions & 2 deletions
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

@@ -78,7 +78,7 @@ func OSPS_AC_03() (evaluation *layer4.ControlEvaluation) {
7878
"Maturity Level 3",
7979
},
8080
[]layer4.AssessmentStep{
81-
branchProtectionPreventsDeletion, // This checks branch protection, but not rulesets yet
81+
defaultBranchPreventsDeletion,
8282
},
8383
)
8484

evaluation_plans/osps/access_control/steps.go

Lines changed: 23 additions & 10 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,28 +39,41 @@ 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

48-
func branchProtectionPreventsDeletion(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
56+
func defaultBranchPreventsDeletion(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
4957
payload, message := reusable_steps.VerifyPayload(payloadData)
5058
if message != "" {
5159
return layer4.Unknown, message
5260
}
5361

54-
allowsDeletion := payload.Repository.DefaultBranchRef.RefUpdateRule.AllowsDeletions
62+
branchProtectionAllowsDeletion := payload.Repository.DefaultBranchRef.RefUpdateRule.AllowsDeletions
63+
deletionRule := payload.RepositoryMetadata.IsDefaultBranchProtectedFromDeletion()
64+
branchRulesAllowDeletion := deletionRule == nil || !*deletionRule
5565

56-
if allowsDeletion {
66+
if branchProtectionAllowsDeletion && branchRulesAllowDeletion {
5767
result = layer4.Failed
58-
message = "Branch protection rule allows deletions"
68+
message = "Default branch is not protected from deletions"
5969
} else {
6070
result = layer4.Passed
61-
message = "Branch protection rule prevents deletions"
71+
if !branchProtectionAllowsDeletion {
72+
message = "Default branch is protected from deletions by branch protection rules"
73+
}
74+
message = "Default branch is protected from deletions by rulesets"
6275
}
63-
return
76+
return result, message
6477
}
6578

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

0 commit comments

Comments
 (0)