Skip to content

Commit

Permalink
Add CIIClient interface (#1262)
Browse files Browse the repository at this point in the history
Co-authored-by: Azeem Shaikh <[email protected]>
  • Loading branch information
azeemshaikh38 and azeemsgoogle authored Nov 15, 2021
1 parent d490455 commit 6223b66
Show file tree
Hide file tree
Showing 16 changed files with 410 additions and 104 deletions.
16 changes: 9 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,16 @@ cron/data/metadata.pb.go: cron/data/metadata.proto | $(PROTOC)
protoc --go_out=../../../ cron/data/metadata.proto

generate-mocks: ## Compiles and generates all mocks using mockgen.
generate-mocks: clients/mockrepo/client.go clients/mockrepo/repo.go
clients/mockrepo/client.go: clients/repo_client.go
generate-mocks: clients/mockclients/repo_client.go clients/mockclients/repo.go clients/mockclients/cii_client.go
clients/mockclients/repo_client.go: clients/repo_client.go
# Generating MockRepoClient
$(MOCKGEN) -source=clients/repo_client.go -destination clients/mockrepo/client.go -package mockrepo -copyright_file clients/mockrepo/license.txt
clients/mockrepo/repo.go: clients/repo.go
# Generating MockRepoClient
$(MOCKGEN) -source=clients/repo.go -destination clients/mockrepo/repo.go -package mockrepo -copyright_file clients/mockrepo/license.txt

$(MOCKGEN) -source=clients/repo_client.go -destination=clients/mockclients/repo_client.go -package=mockrepo -copyright_file=clients/mockclients/license.txt
clients/mockclients/repo.go: clients/repo.go
# Generating MockRepo
$(MOCKGEN) -source=clients/repo.go -destination=clients/mockclients/repo.go -package=mockrepo -copyright_file=clients/mockclients/license.txt
clients/mockclients/cii_client.go: clients/cii_client.go
# Generating MockCIIClient
$(MOCKGEN) -source=clients/cii_client.go -destination=clients/mockclients/cii_client.go -package=mockrepo -copyright_file=clients/mockclients/license.txt

generate-docs: ## Generates docs
generate-docs: validate-docs docs/checks.md
Expand Down
1 change: 1 addition & 0 deletions checker/check_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
type CheckRequest struct {
Ctx context.Context
RepoClient clients.RepoClient
CIIClient clients.CIIBestPracticesClient
Dlogger DetailLogger
Repo clients.Repo
}
2 changes: 1 addition & 1 deletion checks/branch_protection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (

"github.com/ossf/scorecard/v3/checker"
"github.com/ossf/scorecard/v3/clients"
"github.com/ossf/scorecard/v3/clients/mockrepo"
mockrepo "github.com/ossf/scorecard/v3/clients/mockclients"
sce "github.com/ossf/scorecard/v3/errors"
scut "github.com/ossf/scorecard/v3/utests"
)
Expand Down
110 changes: 23 additions & 87 deletions checks/cii_best_practices.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,111 +15,47 @@
package checks

import (
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/http"
"strings"
"time"

"github.com/ossf/scorecard/v3/checker"
"github.com/ossf/scorecard/v3/clients"
sce "github.com/ossf/scorecard/v3/errors"
)

// CheckCIIBestPractices is the registered name for CIIBestPractices.
const CheckCIIBestPractices = "CII-Best-Practices"

var errTooManyRequests = errors.New("failed after exponential backoff")
const (
// CheckCIIBestPractices is the registered name for CIIBestPractices.
CheckCIIBestPractices = "CII-Best-Practices"
silverScore = 7
passingScore = 5
inProgressScore = 2
)

//nolint:gochecknoinits
func init() {
registerCheck(CheckCIIBestPractices, CIIBestPractices)
}

type expBackoffTransport struct {
numRetries uint8
}

func (transport *expBackoffTransport) RoundTrip(req *http.Request) (*http.Response, error) {
for i := 0; i < int(transport.numRetries); i++ {
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != http.StatusTooManyRequests {
// nolint: wrapcheck
return resp, err
}
time.Sleep(time.Duration(math.Pow(2, float64(i))) * time.Second)
}
return nil, errTooManyRequests
}

type response struct {
BadgeLevel string `json:"badge_level"`
}

// CIIBestPractices runs CII-Best-Practices check.
func CIIBestPractices(c *checker.CheckRequest) checker.CheckResult {
// TODO: not supported for local clients.
repoURI := fmt.Sprintf("https://%s", c.Repo.URI())
url := fmt.Sprintf("https://bestpractices.coreinfrastructure.org/projects.json?url=%s", repoURI)
req, err := http.NewRequestWithContext(c.Ctx, "GET", url, nil)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("http.NewRequestWithContext: %v", err))
return checker.CreateRuntimeErrorResult(CheckCIIBestPractices, e)
}

httpClient := http.Client{
Transport: &expBackoffTransport{
numRetries: 3,
},
}
resp, err := httpClient.Do(req)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("http.NewRequestWithContext: %v", err))
return checker.CreateRuntimeErrorResult(CheckCIIBestPractices, e)
}
defer resp.Body.Close()

b, err := io.ReadAll(resp.Body)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("io.ReadAll: %v", err))
return checker.CreateRuntimeErrorResult(CheckCIIBestPractices, e)
}

parsedResponse := []response{}
if err := json.Unmarshal(b, &parsedResponse); err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("json.Unmarshal on %s - %s: %v", resp.Status, parsedResponse, err))
return checker.CreateRuntimeErrorResult(CheckCIIBestPractices, e)
}

if len(parsedResponse) < 1 {
return checker.CreateMinScoreResult(CheckCIIBestPractices, "no badge found")
}

result := parsedResponse[0]

if result.BadgeLevel != "" {
// Three levels: passing, silver and gold,
// https://bestpractices.coreinfrastructure.org/en/criteria.
const silverScore = 7
const passingScore = 5
const inProgressScore = 2
switch {
default:
e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("unsupported badge: %v", result.BadgeLevel))
return checker.CreateRuntimeErrorResult(CheckCIIBestPractices, e)
case strings.Contains(result.BadgeLevel, "in_progress"):
badgeLevel, err := c.CIIClient.GetBadgeLevel(c.Ctx, c.Repo.URI())
if err == nil {
switch badgeLevel {
case clients.NotFound:
return checker.CreateMinScoreResult(CheckCIIBestPractices, "no badge detected")
case clients.InProgress:
return checker.CreateResultWithScore(CheckCIIBestPractices, "badge detected: in_progress", inProgressScore)
case strings.Contains(result.BadgeLevel, "silver"):
case clients.Passing:
return checker.CreateResultWithScore(CheckCIIBestPractices, "badge detected: passing", passingScore)
case clients.Silver:
return checker.CreateResultWithScore(CheckCIIBestPractices, "badge detected: silver", silverScore)
case strings.Contains(result.BadgeLevel, "gold"):
case clients.Gold:
return checker.CreateMaxScoreResult(CheckCIIBestPractices, "badge detected: gold")
case strings.Contains(result.BadgeLevel, "passing"):
return checker.CreateResultWithScore(CheckCIIBestPractices, "badge detected: passing", passingScore)
case clients.Unknown:
e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("unsupported badge: %v", badgeLevel))
return checker.CreateRuntimeErrorResult(CheckCIIBestPractices, e)
}
}

return checker.CreateMinScoreResult(CheckCIIBestPractices, "no badge detected")
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
return checker.CreateRuntimeErrorResult(CheckCIIBestPractices, e)
}
131 changes: 131 additions & 0 deletions checks/cii_best_practices_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2020 Security Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package checks

import (
"context"
"errors"
"testing"

"github.com/golang/mock/gomock"

"github.com/ossf/scorecard/v3/checker"
"github.com/ossf/scorecard/v3/clients"
mockrepo "github.com/ossf/scorecard/v3/clients/mockclients"
sce "github.com/ossf/scorecard/v3/errors"
scut "github.com/ossf/scorecard/v3/utests"
)

var errTest = errors.New("test error")

func TestCIIBestPractices(t *testing.T) {
t.Parallel()
tests := []struct {
err error
name string
uri string
expected scut.TestReturn
badgeLevel clients.BadgeLevel
}{
{
name: "CheckURIUsed",
uri: "github.com/owner/repo",
badgeLevel: clients.NotFound,
expected: scut.TestReturn{
Score: checker.MinResultScore,
},
},
{
name: "CheckErrorHandling",
err: errTest,
expected: scut.TestReturn{
Score: -1,
Error: sce.ErrScorecardInternal,
},
},
{
name: "NotFoundBadge",
badgeLevel: clients.NotFound,
expected: scut.TestReturn{
Score: checker.MinResultScore,
},
},
{
name: "InProgressBadge",
badgeLevel: clients.InProgress,
expected: scut.TestReturn{
Score: inProgressScore,
},
},
{
name: "PassingBadge",
badgeLevel: clients.Passing,
expected: scut.TestReturn{
Score: passingScore,
},
},
{
name: "SilverBadge",
badgeLevel: clients.Silver,
expected: scut.TestReturn{
Score: silverScore,
},
},
{
name: "GoldBadge",
badgeLevel: clients.Gold,
expected: scut.TestReturn{
Score: checker.MaxResultScore,
},
},
{
name: "UnknownBadge",
badgeLevel: clients.Unknown,
expected: scut.TestReturn{
Score: -1,
Error: sce.ErrScorecardInternal,
},
},
}

for _, tt := range tests {
tt := tt // Re-initializing variable so it is not changed while executing the closure below
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

ctrl := gomock.NewController(t)

mockRepo := mockrepo.NewMockRepo(ctrl)
mockRepo.EXPECT().URI().Return(tt.uri).AnyTimes()

mockCIIClient := mockrepo.NewMockCIIBestPracticesClient(ctrl)
mockCIIClient.EXPECT().GetBadgeLevel(gomock.Any(), tt.uri).DoAndReturn(
func(context.Context, string) (clients.BadgeLevel, error) {
return tt.badgeLevel, tt.err
}).MinTimes(1)

req := checker.CheckRequest{
Repo: mockRepo,
CIIClient: mockCIIClient,
}
res := CIIBestPractices(&req)
dl := scut.TestDetailLogger{}
if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &res, &dl) {
t.Fail()
}
ctrl.Finish()
})
}
}
48 changes: 48 additions & 0 deletions clients/cii_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2021 Security Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package clients

import (
"context"
)

// BadgeLevel corresponds to CII-Best-Practices badge levels.
// https://bestpractices.coreinfrastructure.org/en
type BadgeLevel uint

const (
// Unknown or non-parsable CII Best Practices badge.
Unknown BadgeLevel = iota
// NotFound represents when CII Best Practices returns an empty response for a project.
NotFound
// InProgress state of CII Best Practices badge.
InProgress
// Passing level for CII Best Practices badge.
Passing
// Silver level for CII Best Practices badge.
Silver
// Gold level for CII Best Practices badge.
Gold
)

// CIIBestPracticesClient interface returns the BadgeLevel for a repo URL.
type CIIBestPracticesClient interface {
GetBadgeLevel(ctx context.Context, uri string) (BadgeLevel, error)
}

// DefaultCIIBestPracticesClient returns HTTPClientCIIBestPractices implementation of the interface.
func DefaultCIIBestPracticesClient() CIIBestPracticesClient {
return &HTTPClientCIIBestPractices{}
}
Loading

0 comments on commit 6223b66

Please sign in to comment.