Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Ensure CODEOWNERS file exists for corresponding Branch-Protection check #2463

Merged
merged 5 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion checker/raw_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ type WebhooksData struct {
// BranchProtectionsData contains the raw results
// for the Branch-Protection check.
type BranchProtectionsData struct {
Branches []clients.BranchRef
Branches []clients.BranchRef
CodeownersFiles []string
}

// Tool represents a tool.
Expand Down
6 changes: 4 additions & 2 deletions checks/branch_protection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) {
defaultBranch string
releases []string
nonadmin bool
repoFiles []string
}{
{
name: "Nil release and main branch names",
Expand Down Expand Up @@ -172,7 +173,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) {
expected: scut.TestReturn{
Error: nil,
Score: 2,
NumberOfWarn: 7,
NumberOfWarn: 8,
NumberOfInfo: 9,
NumberOfDebug: 0,
},
Expand Down Expand Up @@ -226,7 +227,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) {
expected: scut.TestReturn{
Error: nil,
Score: 8,
NumberOfWarn: 2,
NumberOfWarn: 4,
NumberOfInfo: 14,
NumberOfDebug: 0,
},
Expand Down Expand Up @@ -412,6 +413,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) {
DoAndReturn(func(b string) (*clients.BranchRef, error) {
return getBranch(tt.branches, b, tt.nonadmin), nil
}).AnyTimes()
mockRepoClient.EXPECT().ListFiles(gomock.Any()).AnyTimes().Return(tt.repoFiles, nil)
dl := scut.TestDetailLogger{}
req := checker.CheckRequest{
Dlogger: &dl,
Expand Down
12 changes: 9 additions & 3 deletions checks/evaluation/branch_protection.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func BranchProtection(name string, dl checker.DetailLogger,
score.scores.thoroughReview, score.maxes.thoroughReview = nonAdminThoroughReviewProtection(&b, dl)
// Do we want this?
score.scores.adminThoroughReview, score.maxes.adminThoroughReview = adminThoroughReviewProtection(&b, dl)
score.scores.codeownerReview, score.maxes.codeownerReview = codeownersBranchProtection(&b, dl)
score.scores.codeownerReview, score.maxes.codeownerReview = codeownerBranchProtection(&b, r.CodeownersFiles, dl)

scores = append(scores, score)
}
Expand Down Expand Up @@ -436,7 +436,9 @@ func nonAdminThoroughReviewProtection(branch *clients.BranchRef, dl checker.Deta
return score, max
}

func codeownersBranchProtection(branch *clients.BranchRef, dl checker.DetailLogger) (int, int) {
func codeownerBranchProtection(
branch *clients.BranchRef, codeownersFiles []string, dl checker.DetailLogger,
) (int, int) {
score := 0
max := 1

Expand All @@ -446,7 +448,11 @@ func codeownersBranchProtection(branch *clients.BranchRef, dl checker.DetailLogg
switch *branch.BranchProtectionRule.RequiredPullRequestReviews.RequireCodeOwnerReviews {
case true:
info(dl, log, "codeowner review is required on branch '%s'", *branch.Name)
score++
if len(codeownersFiles) == 0 {
warn(dl, log, "codeowners branch protection is being ignored - but no codeowners file found in repo")
} else {
score++
}
default:
warn(dl, log, "codeowner review is not required on branch '%s'", *branch.Name)
}
Expand Down
50 changes: 41 additions & 9 deletions checks/evaluation/branch_protection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
scut "github.com/ossf/scorecard/v4/utests"
)

func testScore(branch *clients.BranchRef, dl checker.DetailLogger) (int, error) {
func testScore(branch *clients.BranchRef, codeownersFiles []string, dl checker.DetailLogger) (int, error) {
var score levelScore
score.scores.basic, score.maxes.basic = basicNonAdminProtection(branch, dl)
score.scores.adminBasic, score.maxes.adminBasic = basicAdminProtection(branch, dl)
Expand All @@ -31,7 +31,7 @@ func testScore(branch *clients.BranchRef, dl checker.DetailLogger) (int, error)
score.scores.context, score.maxes.context = nonAdminContextProtection(branch, dl)
score.scores.thoroughReview, score.maxes.thoroughReview = nonAdminThoroughReviewProtection(branch, dl)
score.scores.adminThoroughReview, score.maxes.adminThoroughReview = adminThoroughReviewProtection(branch, dl)
score.scores.codeownerReview, score.maxes.codeownerReview = codeownersBranchProtection(branch, dl)
score.scores.codeownerReview, score.maxes.codeownerReview = codeownerBranchProtection(branch, codeownersFiles, dl)

return computeScore([]levelScore{score})
}
Expand All @@ -44,9 +44,10 @@ func TestIsBranchProtected(t *testing.T) {
var oneVal int32 = 1
branchVal := "branch-name"
tests := []struct {
name string
branch *clients.BranchRef
expected scut.TestReturn
name string
branch *clients.BranchRef
codeownersFiles []string
expected scut.TestReturn
}{
{
name: "Nothing is enabled",
Expand Down Expand Up @@ -308,7 +309,7 @@ func TestIsBranchProtected(t *testing.T) {
expected: scut.TestReturn{
Error: nil,
Score: 8,
NumberOfWarn: 1,
NumberOfWarn: 2,
NumberOfInfo: 7,
NumberOfDebug: 0,
},
Expand All @@ -335,11 +336,42 @@ func TestIsBranchProtected(t *testing.T) {
},
{
name: "Branches are protected and require codeowner review",
expected: scut.TestReturn{
Error: nil,
Score: 8,
NumberOfWarn: 1,
NumberOfInfo: 7,
NumberOfDebug: 0,
},
branch: &clients.BranchRef{
Name: &branchVal,
Protected: &trueVal,
BranchProtectionRule: clients.BranchProtectionRule{
EnforceAdmins: &trueVal,
RequireLinearHistory: &trueVal,
AllowForcePushes: &falseVal,
AllowDeletions: &falseVal,
CheckRules: clients.StatusChecksRule{
RequiresStatusChecks: &trueVal,
UpToDateBeforeMerge: &trueVal,
Contexts: []string{"foo"},
},
RequiredPullRequestReviews: clients.PullRequestReviewRule{
DismissStaleReviews: &trueVal,
RequireCodeOwnerReviews: &trueVal,
RequiredApprovingReviewCount: &oneVal,
},
},
},
codeownersFiles: []string{".github/CODEOWNERS"},
},
{
name: "Branches are protected and require codeowner review, but file is not present",
expected: scut.TestReturn{
Error: nil,
Score: 8,
NumberOfWarn: 2,
NumberOfInfo: 6,
NumberOfInfo: 7,
NumberOfDebug: 0,
},
branch: &clients.BranchRef{
Expand All @@ -357,7 +389,7 @@ func TestIsBranchProtected(t *testing.T) {
},
RequiredPullRequestReviews: clients.PullRequestReviewRule{
DismissStaleReviews: &trueVal,
RequireCodeOwnerReviews: &falseVal,
RequireCodeOwnerReviews: &trueVal,
RequiredApprovingReviewCount: &oneVal,
},
},
Expand All @@ -369,7 +401,7 @@ func TestIsBranchProtected(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
dl := scut.TestDetailLogger{}
score, err := testScore(tt.branch, &dl)
score, err := testScore(tt.branch, tt.codeownersFiles, &dl)
actual := &checker.CheckResult{
Score: score,
Error: err,
Expand Down
46 changes: 45 additions & 1 deletion checks/raw/branch_protection.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ package raw

import (
"fmt"
"reflect"
"regexp"

"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/checks/fileparser"
"github.com/ossf/scorecard/v4/clients"
)

Expand Down Expand Up @@ -105,12 +107,54 @@ func BranchProtection(c clients.RepoClient) (checker.BranchProtectionsData, erro
// Branch doesn't exist or was deleted. Continue.
}

codeownersFiles := []string{}
if err := collectCodeownersFiles(c, &codeownersFiles); err != nil {
return checker.BranchProtectionsData{}, err
}

// No error, return the data.
return checker.BranchProtectionsData{
Branches: branches.set,
Branches: branches.set,
CodeownersFiles: codeownersFiles,
}, nil
}

func collectCodeownersFiles(c clients.RepoClient, codeownersFiles *[]string) error {
return fileparser.OnMatchingFileContentDo(c, fileparser.PathMatcher{
Pattern: "CODEOWNERS",
CaseSensitive: true,
}, addCodeownersFile, codeownersFiles)
}

var addCodeownersFile fileparser.DoWhileTrueOnFileContent = func(
path string,
content []byte,
args ...interface{},
) (bool, error) {
fmt.Printf("got codeowners file at %s\n", path)
raghavkaul marked this conversation as resolved.
Show resolved Hide resolved

if len(args) != 1 {
return false, fmt.Errorf(
"addCodeownersFile requires exactly 1 arguments: got %v: %w",
len(args), errInvalidArgLength)
}

codeownersFiles := dataAsStringSlicePtr(args[0])

*codeownersFiles = append(*codeownersFiles, path)

return true, nil
}

func dataAsStringSlicePtr(data interface{}) *[]string {
pdata, ok := data.(*[]string)
if !ok {
// panic if it is not correct type
panic(fmt.Sprintf("expected type *[]string, got %v", reflect.TypeOf(data)))
}
return pdata
}

func branchRedirect(name string) string {
// Ideally, we should check using repositories.GetBranch if there was a branch redirect.
// See https://github.com/google/go-github/issues/1895
Expand Down
19 changes: 18 additions & 1 deletion checks/raw/branch_protection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func TestBranchProtection(t *testing.T) {
tests := []struct {
name string
branches branchesArg
repoFiles []string
releases []clients.Release
releasesErr error
want checker.BranchProtectionsData
Expand All @@ -80,6 +81,9 @@ func TestBranchProtection(t *testing.T) {
err: errBPTest,
},
},
want: checker.BranchProtectionsData{
CodeownersFiles: []string{},
},
},
{
name: "null-default-branch-only",
Expand All @@ -90,6 +94,9 @@ func TestBranchProtection(t *testing.T) {
branchRef: nil,
},
},
want: checker.BranchProtectionsData{
CodeownersFiles: []string{},
},
},
{
name: "default-branch-only",
Expand All @@ -108,6 +115,7 @@ func TestBranchProtection(t *testing.T) {
Name: &defaultBranchName,
},
},
CodeownersFiles: []string{},
},
},
{
Expand All @@ -117,6 +125,9 @@ func TestBranchProtection(t *testing.T) {
},
{
name: "no-releases",
want: checker.BranchProtectionsData{
CodeownersFiles: []string{},
},
},
{
name: "empty-targetcommitish",
Expand Down Expand Up @@ -155,6 +166,9 @@ func TestBranchProtection(t *testing.T) {
branchRef: nil,
},
},
want: checker.BranchProtectionsData{
CodeownersFiles: []string{},
},
},
{
name: "add-release-branch",
Expand All @@ -177,6 +191,7 @@ func TestBranchProtection(t *testing.T) {
Name: &releaseBranchName,
},
},
CodeownersFiles: []string{},
},
},
{
Expand All @@ -200,6 +215,7 @@ func TestBranchProtection(t *testing.T) {
Name: &mainBranchName,
},
},
CodeownersFiles: []string{},
},
},
{
Expand Down Expand Up @@ -233,6 +249,7 @@ func TestBranchProtection(t *testing.T) {
Name: &releaseBranchName,
},
},
CodeownersFiles: []string{},
},
},
// TODO: Add tests for commitSHA regex matching.
Expand All @@ -255,7 +272,7 @@ func TestBranchProtection(t *testing.T) {
DoAndReturn(func() ([]clients.Release, error) {
return tt.releases, tt.releasesErr
})

mockRepoClient.EXPECT().ListFiles(gomock.Any()).AnyTimes().Return(tt.repoFiles, nil)
rawData, err := BranchProtection(mockRepoClient)
if !errors.Is(err, tt.wantErr) {
t.Errorf("failed. expected: %v, got: %v", tt.wantErr, err)
Expand Down
2 changes: 1 addition & 1 deletion e2e/branch_protection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ var _ = Describe("E2E TEST PAT:"+checks.CheckBranchProtection, func() {
expected := scut.TestReturn{
Error: nil,
Score: 6,
NumberOfWarn: 1,
NumberOfWarn: 2,
NumberOfInfo: 4,
NumberOfDebug: 3,
}
Expand Down
15 changes: 12 additions & 3 deletions pkg/json_raw_results.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ type jsonBranchProtection struct {
Name string `json:"name"`
}

type jsonBranchProtectionMetadata struct {
Branches []jsonBranchProtection `json:"branches"`
CodeownersFiles []string `json:"codeownersFiles"`
}

type jsonReview struct {
State string `json:"state"`
Reviewer jsonUser `json:"reviewer"`
Expand Down Expand Up @@ -265,7 +270,7 @@ type jsonRawResults struct {
// Note: we return one at most.
DependencyUpdateTools []jsonTool `json:"dependencyUpdateTools"`
// Branch protection settings for development and release branches.
BranchProtections []jsonBranchProtection `json:"branchProtections"`
BranchProtections jsonBranchProtectionMetadata `json:"branchProtections"`
// Contributors. Note: we could use the list of commits instead to store this data.
// However, it's harder to get statistics using commit list, so we have a dedicated
// structure for it.
Expand Down Expand Up @@ -711,7 +716,7 @@ func (r *jsonScorecardRawResult) addDependencyUpdateToolRawResults(dut *checker.

//nolint:unparam
func (r *jsonScorecardRawResult) addBranchProtectionRawResults(bp *checker.BranchProtectionsData) error {
r.Results.BranchProtections = []jsonBranchProtection{}
branches := []jsonBranchProtection{}
for _, v := range bp.Branches {
var bp *jsonBranchProtectionSettings
if v.Protected != nil && *v.Protected {
Expand All @@ -728,11 +733,15 @@ func (r *jsonScorecardRawResult) addBranchProtectionRawResults(bp *checker.Branc
StatusCheckContexts: v.BranchProtectionRule.CheckRules.Contexts,
}
}
r.Results.BranchProtections = append(r.Results.BranchProtections, jsonBranchProtection{
branches = append(branches, jsonBranchProtection{
Name: *v.Name,
Protection: bp,
})
}
r.Results.BranchProtections.Branches = branches

r.Results.BranchProtections.CodeownersFiles = bp.CodeownersFiles

return nil
}

Expand Down