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

✨ show non-compliant code changes for CI-Tests, Code-Review and SAST checks in --show-details mode #2835

Merged
merged 9 commits into from
Apr 21, 2023
2 changes: 1 addition & 1 deletion checks/evaluation/ci_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func CITests(name string, c *checker.CITestData, dl checker.DetailLogger) checke
// Log message says commit, but really we only care about PRs, and
// use only one commit (branch HEAD) to refer to all commits in a PR
dl.Debug(&checker.LogMessage{
Text: fmt.Sprintf("merged PR without CI test at HEAD: %s", r.HeadSHA),
Text: fmt.Sprintf("merged PR %d without CI test at HEAD: %s", r.PullRequestNumber, r.HeadSHA),
})
}
}
Expand Down
18 changes: 18 additions & 0 deletions checks/evaluation/code_review.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package evaluation

import (
"fmt"
"strings"

"github.com/ossf/scorecard/v4/checker"
sce "github.com/ossf/scorecard/v4/errors"
Expand Down Expand Up @@ -46,6 +47,8 @@ func CodeReview(name string, dl checker.DetailLogger, r *checker.CodeReviewData)
foundBotReviewActivity := false
nUnreviewedHumanChanges := 0
nUnreviewedBotChanges := 0

var unreviewedHumanRevisionIDs, unreviewedBotRevisionIDs []string
for i := range r.DefaultBranchChangesets {
cs := &r.DefaultBranchChangesets[i]
isReviewed := reviewScoreForChangeset(cs) >= changesReviewed
Expand All @@ -58,11 +61,26 @@ func CodeReview(name string, dl checker.DetailLogger, r *checker.CodeReviewData)
foundHumanReviewActivity = true
case !isReviewed && isBotCommit:
nUnreviewedBotChanges += 1
unreviewedBotRevisionIDs = append(unreviewedBotRevisionIDs, cs.RevisionID)
case !isReviewed && !isBotCommit:
nUnreviewedHumanChanges += 1
unreviewedHumanRevisionIDs = append(unreviewedHumanRevisionIDs, cs.RevisionID)
}
}

// Let's include non-compliant revision IDs in details
if len(unreviewedHumanRevisionIDs) > 0 {
dl.Debug(&checker.LogMessage{
Text: fmt.Sprintf("List of revision IDs not reviewed by humans:%s", strings.Join(unreviewedHumanRevisionIDs, ",")),
})
}

if len(unreviewedBotRevisionIDs) > 0 {
dl.Debug(&checker.LogMessage{
Text: fmt.Sprintf("List of bot revision IDs not reviewed by humans:%s", strings.Join(unreviewedBotRevisionIDs, ",")),
})
}

if foundBotReviewActivity && !foundHumanReviewActivity {
reason := fmt.Sprintf("found no human review activity in the last %v changesets", len(r.DefaultBranchChangesets))
return checker.CreateInconclusiveResult(name, reason)
Expand Down
12 changes: 8 additions & 4 deletions checks/evaluation/code_review_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ func TestCodeReview(t *testing.T) {
{
name: "NoReviews",
expected: scut.TestReturn{
Score: checker.MinResultScore,
Score: checker.MinResultScore,
NumberOfDebug: 1,
},
rawData: &checker.CodeReviewData{
DefaultBranchChangesets: []checker.Changeset{
Expand All @@ -66,7 +67,8 @@ func TestCodeReview(t *testing.T) {
{
name: "Unreviewed human and bot changes",
expected: scut.TestReturn{
Score: checker.MinResultScore,
Score: checker.MinResultScore,
NumberOfDebug: 1,
},
rawData: &checker.CodeReviewData{
DefaultBranchChangesets: []checker.Changeset{
Expand All @@ -82,7 +84,8 @@ func TestCodeReview(t *testing.T) {
{
name: "all human changesets reviewed, missing review on bot changeset",
expected: scut.TestReturn{
Score: 7,
Score: 7,
NumberOfDebug: 1,
},
rawData: &checker.CodeReviewData{
DefaultBranchChangesets: []checker.Changeset{
Expand Down Expand Up @@ -147,7 +150,8 @@ func TestCodeReview(t *testing.T) {
{
name: "bot commits only",
expected: scut.TestReturn{
Score: checker.InconclusiveResultScore,
Score: checker.InconclusiveResultScore,
NumberOfDebug: 1,
},
rawData: &checker.CodeReviewData{
DefaultBranchChangesets: []checker.Changeset{
Expand Down
35 changes: 29 additions & 6 deletions checks/sast.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func init() {

// SAST runs SAST check.
func SAST(c *checker.CheckRequest) checker.CheckResult {
sastScore, sastErr := sastToolInCheckRuns(c)
sastScore, nonCompliantPRs, sastErr := sastToolInCheckRuns(c)
if sastErr != nil {
return checker.CreateRuntimeErrorResult(CheckSAST, sastErr)
}
Expand Down Expand Up @@ -96,6 +96,9 @@ func SAST(c *checker.CheckRequest) checker.CheckResult {
case codeQlScore == checker.MaxResultScore:
const sastWeight = 3
const codeQlWeight = 7
c.Dlogger.Debug(&checker.LogMessage{
Text: getNonCompliantPRMessage(nonCompliantPRs),
})
score := checker.AggregateScoresWithWeight(map[int]int{sastScore: sastWeight, codeQlScore: codeQlWeight})
return checker.CreateResultWithScore(CheckSAST, "SAST tool detected but not run on all commmits", score)
default:
Expand All @@ -117,6 +120,9 @@ func SAST(c *checker.CheckRequest) checker.CheckResult {
return checker.CreateMaxScoreResult(CheckSAST, "SAST tool is run on all commits")
}

c.Dlogger.Debug(&checker.LogMessage{
Text: getNonCompliantPRMessage(nonCompliantPRs),
})
return checker.CreateResultWithScore(CheckSAST,
checker.NormalizeReason("SAST tool is not run on all commits", sastScore), sastScore)
}
Expand All @@ -125,15 +131,16 @@ func SAST(c *checker.CheckRequest) checker.CheckResult {
return checker.CreateRuntimeErrorResult(CheckSAST, sce.WithMessage(sce.ErrScorecardInternal, "contact team"))
}

func sastToolInCheckRuns(c *checker.CheckRequest) (int, error) {
func sastToolInCheckRuns(c *checker.CheckRequest) (int, map[int]int, error) {
commits, err := c.RepoClient.ListCommits()
if err != nil {
return checker.InconclusiveResultScore,
return checker.InconclusiveResultScore, nil,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("RepoClient.ListCommits: %v", err))
}

totalMerged := 0
totalTested := 0
nonCompliantPRs := make(map[int]int)
for i := range commits {
pr := commits[i].AssociatedMergeRequest
// TODO(#575): We ignore associated PRs if Scorecard is being run on a fork
Expand All @@ -142,9 +149,10 @@ func sastToolInCheckRuns(c *checker.CheckRequest) (int, error) {
continue
}
totalMerged++
checked := false
crs, err := c.RepoClient.ListCheckRunsForRef(pr.HeadSHA)
if err != nil {
return checker.InconclusiveResultScore,
return checker.InconclusiveResultScore, nil,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Client.Checks.ListCheckRunsForRef: %v", err))
}
// Note: crs may be `nil`: in this case
Expand All @@ -163,15 +171,19 @@ func sastToolInCheckRuns(c *checker.CheckRequest) (int, error) {
Text: fmt.Sprintf("tool detected: %v", cr.App.Slug),
})
totalTested++
checked = true
break
}
}
if !checked {
nonCompliantPRs[pr.Number] = pr.Number
}
}
if totalMerged == 0 {
c.Dlogger.Warn(&checker.LogMessage{
Text: "no pull requests merged into dev branch",
})
return checker.InconclusiveResultScore, nil
return checker.InconclusiveResultScore, nil, nil
}

if totalTested == totalMerged {
Expand All @@ -184,7 +196,7 @@ func sastToolInCheckRuns(c *checker.CheckRequest) (int, error) {
})
}

return checker.CreateProportionalScore(totalTested, totalMerged), nil
return checker.CreateProportionalScore(totalTested, totalMerged), nonCompliantPRs, nil
}

func codeQLInCheckDefinitions(c *checker.CheckRequest) (int, error) {
Expand Down Expand Up @@ -366,3 +378,14 @@ func findLine(content, data []byte) (uint, error) {

return 0, nil
}

func getNonCompliantPRMessage(intMap map[int]int) string {
var sb strings.Builder
for _, value := range intMap {
if len(sb.String()) != 0 {
sb.WriteString(", ")
}
sb.WriteString(fmt.Sprintf("%d", value))
}
return fmt.Sprintf("List of pull requests without CI test: %s", sb.String())
}
10 changes: 6 additions & 4 deletions e2e/code_review_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var _ = Describe("E2E TEST:"+checks.CheckCodeReview, func() {
Score: checker.MinResultScore,
NumberOfWarn: 0,
NumberOfInfo: 0,
NumberOfDebug: 0,
NumberOfDebug: 1,
}
result := checks.CodeReview(&req)
Expect(scut.ValidateTestReturn(nil, "use code reviews", &expected, &result, &dl)).Should(BeTrue())
Expand All @@ -75,7 +75,7 @@ var _ = Describe("E2E TEST:"+checks.CheckCodeReview, func() {
Score: checker.MinResultScore,
NumberOfWarn: 0,
NumberOfInfo: 0,
NumberOfDebug: 0,
NumberOfDebug: 1,
}
result := checks.CodeReview(&req)
Expect(scut.ValidateTestReturn(nil, "use code reviews", &expected, &result, &dl)).Should(BeTrue())
Expand Down Expand Up @@ -117,7 +117,8 @@ var _ = Describe("E2E TEST:"+checks.CheckCodeReview, func() {
Dlogger: &dl,
}
expected := scut.TestReturn{
Score: checker.InconclusiveResultScore,
Score: checker.InconclusiveResultScore,
NumberOfDebug: 1,
}
result := checks.CodeReview(&req)
Expect(scut.ValidateTestReturn(nil, "use code reviews", &expected, &result, &dl)).Should(BeTrue())
Expand All @@ -138,7 +139,8 @@ var _ = Describe("E2E TEST:"+checks.CheckCodeReview, func() {
Dlogger: &dl,
}
expected := scut.TestReturn{
Score: checker.MinResultScore,
Score: checker.MinResultScore,
NumberOfDebug: 1,
}
result := checks.CodeReview(&req)
Expect(scut.ValidateTestReturn(nil, "use code reviews", &expected, &result, &dl)).Should(BeTrue())
Expand Down