Skip to content

Commit 371db9c

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 c66be99 commit 371db9c

File tree

3 files changed

+81
-18
lines changed

3 files changed

+81
-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
@@ -64,7 +64,7 @@ func OSPS_AC_03() (evaluation *layer4.ControlEvaluation) {
6464
"Maturity Level 3",
6565
},
6666
[]layer4.AssessmentStep{
67-
branchProtectionRestrictsPushes, // This checks branch protection, but not rulesets yet
67+
defaultBranchRestrictsPushes,
6868
},
6969
)
7070

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

evaluation_plans/osps/access_control/steps.go

Lines changed: 24 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
@@ -36,28 +36,42 @@ func branchProtectionRestrictsPushes(payloadData any, _ map[string]*layer4.Chang
3636
result = layer4.Passed
3737
message = "Branch protection rule requires approving reviews"
3838
} else {
39-
result = layer4.NeedsReview
40-
message = "Branch protection rule does not restrict pushes or require approving reviews; Rulesets not yet evaluated."
39+
if payload.RepositoryMetadata.IsDefaultBranchProtected() != nil && *payload.RepositoryMetadata.IsDefaultBranchProtected() {
40+
result = layer4.Passed
41+
message = "Branch rule restricts pushes"
42+
} else if payload.RepositoryMetadata.DefaultBranchRequiresPRReviews() != nil && *payload.RepositoryMetadata.DefaultBranchRequiresPRReviews() {
43+
result = layer4.Passed
44+
message = "Branch rule requires approving reviews"
45+
} else {
46+
result = layer4.Failed
47+
message = "Default branch is not protected"
48+
}
4149
}
42-
return
50+
return result, message
4351
}
4452

45-
func branchProtectionPreventsDeletion(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
53+
func defaultBranchPreventsDeletion(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
4654
payload, message := reusable_steps.VerifyPayload(payloadData)
4755
if message != "" {
4856
return layer4.Unknown, message
4957
}
5058

51-
allowsDeletion := payload.Repository.DefaultBranchRef.RefUpdateRule.AllowsDeletions
59+
branchProtectionAllowsDeletion := payload.Repository.DefaultBranchRef.RefUpdateRule.AllowsDeletions
60+
deletionRule := payload.RepositoryMetadata.IsDefaultBranchProtectedFromDeletion()
61+
branchRulesAllowDeletion := deletionRule == nil || !*deletionRule
5262

53-
if allowsDeletion {
63+
if branchProtectionAllowsDeletion && branchRulesAllowDeletion {
5464
result = layer4.Failed
55-
message = "Branch protection rule allows deletions"
65+
message = "Default branch is not protected from deletions"
5666
} else {
5767
result = layer4.Passed
58-
message = "Branch protection rule prevents deletions"
68+
if *deletionRule {
69+
message = "Default branch is protected from deletions by rulesets"
70+
} else {
71+
message = "Default branch is protected from deletions by branch protection rules"
72+
}
5973
}
60-
return
74+
return result, message
6175
}
6276

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

0 commit comments

Comments
 (0)